From 156915b7901e91b89942a265d38b294666148bf0 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 18 Oct 2026 09:54:27 +0000 Subject: [PATCH 1/2] test(e2e): cover default tab of user edit page by AtCoder verification status Co-Authored-By: Claude Sonnet 5.5 --- .../user-edit-e2e-default-tab/plan.md | 42 +++++++++++++ e2e/helpers/auth.ts | 7 ++- e2e/user_edit.spec.ts | 60 ++++++++++++++----- 3 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md diff --git a/docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md b/docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md new file mode 100644 index 000000000..6dec44b6f --- /dev/null +++ b/docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md @@ -0,0 +1,42 @@ +# `/users/edit` の E2E: 既定タブの検証を seed に追従させる(完了記録) + +## 結論 + +`e2e/user_edit.spec.ts` の「基本情報 tab is active by default」が毎回失敗していた件を、テスト側の修正で解消した。アプリと seed は変更していない。 +Playwright 1.64.1 の E2E 検証(#4131)で見つかった既存の失敗だった。 + +## 原因 + +- `/users/edit` は、AtCoder 認証済みのユーザーには「AtCoder IDを設定」タブを開く。これは `e9ea7a0a`(2026-03-27)で入った意図どおりの仕様。 +- spec は翌日の `053e0e96` で書かれ、当時の guest は未認証だった。`6696b604`(2026-09-19)で seed の guest を認証済み(`novisteps_guest`)にしたため、期待値が合わなくなった。 +- CI の E2E ジョブはコメントアウト中のため、検知されなかった。 + +## 弱いテストだった理由 + +「username is displayed」は、基本情報タブが開いていなくても成功していた。 + +- Flowbite の `Tabs` は選択中のタブの中身だけを描画する。 +- AtCoder タブのフォーム(`AtCoderVerificationForm.svelte`)にも「ユーザ名」の入力欄がある。テストはそちらを見つけて `guest` に一致していた。 +- 修正後は、基本情報タブをクリックして `aria-selected="true"` を確かめてから値を見る。描画されるパネルは常に1つなので、`tabpanel` で範囲を絞っても区別できない。 + +## 意思決定 + +- **既定タブの分岐を両方検証する(ユーザー決定):** 期待値を guest に合わせるだけでは、未認証側の分岐が E2E で検証されないまま残る。 +- **未認証ユーザーには seed の `Alice` を使う:** `prisma/users.ts` で、未認証の経路を検証するために未認証のまま残されている seed ユーザー。 +- **seed の guest を未認証に戻さない:** 認証済みが前提の機能を、実 API なしで使えるようにするための設定だから。 +- **`?tab=atcoder` の分岐は追加しない:** 既定タブの修正という範囲を超えるため(YAGNI)。 + +## 検証 + +- `user_edit.spec.ts` を `--repeat-each=3` で実行し、15 件すべて成功した。E2E 全体は 105 件成功・1 スキップ(既存の条件付き skip)。 +- 本体を一時的に改変し、それぞれのテストが失敗することを確かめた(確認後に元に戻した)。 + +| 改変(`src/routes/users/edit/+page.svelte`) | 失敗したテスト | +| --------------------------------------------------------------- | ---------------------------------------------------- | +| `shouldOpenAtCoderTab` から `atCoderAccount.isValidated` を外す | 認証済み(guest)の既定タブ | +| 基本情報タブのユーザ名を空にする | username is displayed after opening the 基本情報 tab | +| `shouldOpenAtCoderTab` を常に `true` にする | 未認証(Alice)の既定タブ | + +## 注意点 + +- 手元の DB で `Alice` を手動で AtCoder 認証すると、未認証側のテストが失敗する。seed は `atCoderHandle` を持つユーザー(admin・guest)だけを認証済みにする。 diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts index 8b3d0762b..ea267acce 100644 --- a/e2e/helpers/auth.ts +++ b/e2e/helpers/auth.ts @@ -15,11 +15,16 @@ export async function loginAsAdmin(page: Page): Promise { await loginAs(page, 'admin'); } -/** Logs in as a general (non-admin) user and waits for redirect to home. */ +/** Logs in as a general (non-admin) user verified with a fictional AtCoder handle. */ export async function loginAsUser(page: Page): Promise { await loginAs(page, 'guest'); } +/** Logs in as a general user without AtCoder verification and waits for redirect to home. */ +export async function loginAsUnverifiedUser(page: Page): Promise { + await loginAs(page, 'Alice'); +} + /** * Fills and submits the login form without navigating first. * Use when already on /login?redirectTo=... to preserve the redirect destination. diff --git a/e2e/user_edit.spec.ts b/e2e/user_edit.spec.ts index 39bca448b..f2f7510e3 100644 --- a/e2e/user_edit.spec.ts +++ b/e2e/user_edit.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from '@playwright/test'; -import { loginAsUser } from './helpers/auth'; +import { test, expect, type Page } from '@playwright/test'; +import { loginAsUnverifiedUser, loginAsUser } from './helpers/auth'; const TIMEOUT = 60 * 1000; const SHORT_TIMEOUT = 5000; @@ -7,31 +7,59 @@ const SHORT_TIMEOUT = 5000; // Mirrors /users/edit route const EDIT_PAGE_URL = '/users/edit'; +const BASIC_INFO_TAB_NAME = '基本情報'; +const ATCODER_TAB_NAME = 'AtCoder IDを設定'; + +function getTab(page: Page, name: string) { + return page.getByRole('tab', { name }); +} + +async function openEditPage(page: Page): Promise { + await page.goto(EDIT_PAGE_URL); + // Wait for page to stabilize before running assertions + await expect(getTab(page, BASIC_INFO_TAB_NAME)).toBeVisible({ timeout: TIMEOUT }); +} + test.describe('user edit page (/users/edit)', () => { test('unauthenticated user is redirected to /login', async ({ page }) => { await page.goto(EDIT_PAGE_URL); await expect(page).toHaveURL(/\/login/, { timeout: TIMEOUT }); }); - test.describe('logged-in user (guest)', () => { + test.describe('logged-in user without AtCoder verification (Alice)', () => { test.beforeEach(async ({ page }) => { - await loginAsUser(page); - await page.goto(EDIT_PAGE_URL); - // Wait for page to stabilize before running assertions - await expect(page.getByRole('tab', { name: '基本情報' })).toBeVisible({ timeout: TIMEOUT }); + await loginAsUnverifiedUser(page); + await openEditPage(page); }); test('基本情報 tab is active by default', async ({ page }) => { - // Tab visibility already asserted in beforeEach; check active state only - await expect(page.getByRole('tab', { name: '基本情報' })).toHaveAttribute( - 'aria-selected', - 'true', - { timeout: SHORT_TIMEOUT }, - ); + await expect(getTab(page, BASIC_INFO_TAB_NAME)).toHaveAttribute('aria-selected', 'true', { + timeout: SHORT_TIMEOUT, + }); + await expect(getTab(page, ATCODER_TAB_NAME)).toHaveAttribute('aria-selected', 'false'); + }); + }); + + test.describe('logged-in user with AtCoder verification (guest)', () => { + test.beforeEach(async ({ page }) => { + await loginAsUser(page); + await openEditPage(page); }); - test('username is displayed in the 基本情報 tab', async ({ page }) => { - // Assert the username label input shows the logged-in user's name + test('AtCoder IDを設定 tab is active by default', async ({ page }) => { + await expect(getTab(page, ATCODER_TAB_NAME)).toHaveAttribute('aria-selected', 'true', { + timeout: SHORT_TIMEOUT, + }); + await expect(getTab(page, BASIC_INFO_TAB_NAME)).toHaveAttribute('aria-selected', 'false'); + }); + + test('username is displayed after opening the 基本情報 tab', async ({ page }) => { + // The AtCoder tab also renders a ユーザ名 field, so confirm the 基本情報 tab is selected first + await getTab(page, BASIC_INFO_TAB_NAME).click(); + await expect(getTab(page, BASIC_INFO_TAB_NAME)).toHaveAttribute('aria-selected', 'true', { + timeout: SHORT_TIMEOUT, + }); + await expect(page.getByRole('textbox', { name: 'ユーザ名' })).toHaveValue('guest', { timeout: SHORT_TIMEOUT, }); @@ -39,7 +67,7 @@ test.describe('user edit page (/users/edit)', () => { test('アカウント削除 tab is not shown for the guest user', async ({ page }) => { // guest is excluded from account deletion (isGeneralUser returns false for username 'guest') - await expect(page.getByRole('tab', { name: 'アカウント削除' })).not.toBeVisible({ + await expect(getTab(page, 'アカウント削除')).not.toBeVisible({ timeout: SHORT_TIMEOUT, }); }); From e0ba1c6ff8834b71121924d273c1a35470c5f466 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 18 Oct 2026 09:55:03 +0000 Subject: [PATCH 2/2] docs: remove user-edit-e2e-default-tab plan Co-Authored-By: Claude Sonnet 5.5 --- .../user-edit-e2e-default-tab/plan.md | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md diff --git a/docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md b/docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md deleted file mode 100644 index 6dec44b6f..000000000 --- a/docs/dev-notes/2026-10-18/user-edit-e2e-default-tab/plan.md +++ /dev/null @@ -1,42 +0,0 @@ -# `/users/edit` の E2E: 既定タブの検証を seed に追従させる(完了記録) - -## 結論 - -`e2e/user_edit.spec.ts` の「基本情報 tab is active by default」が毎回失敗していた件を、テスト側の修正で解消した。アプリと seed は変更していない。 -Playwright 1.64.1 の E2E 検証(#4131)で見つかった既存の失敗だった。 - -## 原因 - -- `/users/edit` は、AtCoder 認証済みのユーザーには「AtCoder IDを設定」タブを開く。これは `e9ea7a0a`(2026-03-27)で入った意図どおりの仕様。 -- spec は翌日の `053e0e96` で書かれ、当時の guest は未認証だった。`6696b604`(2026-09-19)で seed の guest を認証済み(`novisteps_guest`)にしたため、期待値が合わなくなった。 -- CI の E2E ジョブはコメントアウト中のため、検知されなかった。 - -## 弱いテストだった理由 - -「username is displayed」は、基本情報タブが開いていなくても成功していた。 - -- Flowbite の `Tabs` は選択中のタブの中身だけを描画する。 -- AtCoder タブのフォーム(`AtCoderVerificationForm.svelte`)にも「ユーザ名」の入力欄がある。テストはそちらを見つけて `guest` に一致していた。 -- 修正後は、基本情報タブをクリックして `aria-selected="true"` を確かめてから値を見る。描画されるパネルは常に1つなので、`tabpanel` で範囲を絞っても区別できない。 - -## 意思決定 - -- **既定タブの分岐を両方検証する(ユーザー決定):** 期待値を guest に合わせるだけでは、未認証側の分岐が E2E で検証されないまま残る。 -- **未認証ユーザーには seed の `Alice` を使う:** `prisma/users.ts` で、未認証の経路を検証するために未認証のまま残されている seed ユーザー。 -- **seed の guest を未認証に戻さない:** 認証済みが前提の機能を、実 API なしで使えるようにするための設定だから。 -- **`?tab=atcoder` の分岐は追加しない:** 既定タブの修正という範囲を超えるため(YAGNI)。 - -## 検証 - -- `user_edit.spec.ts` を `--repeat-each=3` で実行し、15 件すべて成功した。E2E 全体は 105 件成功・1 スキップ(既存の条件付き skip)。 -- 本体を一時的に改変し、それぞれのテストが失敗することを確かめた(確認後に元に戻した)。 - -| 改変(`src/routes/users/edit/+page.svelte`) | 失敗したテスト | -| --------------------------------------------------------------- | ---------------------------------------------------- | -| `shouldOpenAtCoderTab` から `atCoderAccount.isValidated` を外す | 認証済み(guest)の既定タブ | -| 基本情報タブのユーザ名を空にする | username is displayed after opening the 基本情報 tab | -| `shouldOpenAtCoderTab` を常に `true` にする | 未認証(Alice)の既定タブ | - -## 注意点 - -- 手元の DB で `Alice` を手動で AtCoder 認証すると、未認証側のテストが失敗する。seed は `atCoderHandle` を持つユーザー(admin・guest)だけを認証済みにする。