front-end-testing について
フロントエンドテストの計画と評価を支援するスキルです。ユーザーに見える挙動、ブラウザテスト、テスト証拠の限界を扱い、各種環境で UI テストを行うエージェント向けです。
主な機能
- 01ユーザーに見える挙動を優先
- 02jsdom とブラウザテストを比較
- 03Vitest Browser Mode の利用
- 04Playwright E2E テストの指針
- 05テスト証拠の限界を明記
活用シーン
- 01ブラウザ依存の挙動に合うテスト方式の選定
- 02ユーザー操作を中心とした UI テストの作成
- 03テスト成功で証明できない範囲の報告
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill front-end-testingインストール後、Claude Code で/front-end-testingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
resources/async-patterns.mddom-testing-library-legacy.mdmsw.mdplaywright-e2e.md詳細
フロントエンドテスト
React 固有のパターン(コンポーネント、フック、コンテキスト)では、react-testing スキルを読み込んでください。TDD ワークフローでは、tdd スキルを読み込んでください。一般的なテストパターン(ファクトリー、公開インターフェースのテスト)では、testing スキルを読み込んでください。
作業を引き渡す際は、必ずハーネスと、エビデンスがどこまでを裏付けるかを明記してください。 UI テストの作業が完了したと報告する場合は、返信、PR の本文、CI のステップラベルのいずれであっても、エビデンスを生成したランナーと環境(サーブされたアプリに対する Playwright、jsdom での Vitest、実ブラウザーでの Browser Mode)を記載し、同時に、それが証明しない最も近い点も明記してください。jsdom のスイートでは、実際のレンダリング、CSS、フォーカス、ブラウザー独自のイベントディスパッチは証明できません。コンポーネントレベルのスイートでは、サーブされたアプリ、そのルーティング、実サーバーは証明できません。1 つのユーザージャーニーでは、通らない経路は証明できません。境界を明記しない「すべてのテストに合格しました」は、テストが裏付ける以上の強い主張に見えてしまいます。
詳しいリソースは resources/ ディレクトリにあります。必要に応じて読み込んでください。
| リソース | 読み込むタイミング |
|---|---|
resources/playwright-e2e.md | 実行中のアプリに対する Playwright Test の E2E/ユーザージャーニースイートの作成や監査時 — リクエストを開始できる主体、リクエストを実行せずにネットワークを監視する方法、直接トランスポートの監査、認証/ライフサイクルのエビデンス |
resources/async-patterns.md | findBy/waitFor/waitForElementToBeRemoved の使用、ローディング状態やデバウンスのテスト、または waitFor の使用状況のレビュー時 |
resources/msw.md | API のモック時 — 完全な setupWorker(Browser Mode)と setupServer(Node/jsdom)のセットアップ、テストごとのオーバーライド |
resources/dom-testing-library-legacy.md | jsdom/@testing-library/dom のコードベースでの作業時 — screen オブジェクト、fireEvent と userEvent の比較、jest-dom マッチャー、ESLint プラグイン |
基本理念
実装の詳細ではなく、ユーザーが目にする動作をテストしてください。 これは、Browser Mode、jsdom など、あらゆる環境に当てはまります。
UI には 2 種類のユーザーがいます。
- エンドユーザー:DOM を介して操作します(クリック、入力、テキストの読み取り)
- 開発者:実装をリファクタリングするあなたです
Kent C. Dodds の原則:「テストがソフトウェアの使われ方に近いほど、より大きな信頼を得られます。」
偽陰性(リファクタリングでテストが壊れる):
// ❌ WRONG - Coupled to state implementation; breaks when state → signals → stores
it('should update internal state', () => {
const component = new CounterComponent();
component.setState({ count: 5 });
expect(component.state.count).toBe(5);
});
偽陽性(バグがテストを通過する):
// ❌ WRONG - Button exists but onClick is broken; test still passes
it('should render button', () => {
render('<button data-testid="submit-btn">Submit</button>');
expect(screen.getByTestId('submit-btn')).toBeInTheDocument();
});
✅ 正しい方法 — ユーザーと同じように UI を操作し、ユーザーに見えるものを検証する:ラベル付きフィールドに入力し、送信ボタンをクリックし、送信ハンドラーがフォームデータを受け取ったことを検証します。これならリファクタリングに耐え、契約をテストし、実際のバグ(壊れた onClick、バリデーションエラー)を検出できます。
ブラウザーで確認できる動作には Vitest Browser Mode を使用
主張が実際のレンダリング、CSS、イベント、フォーカス管理、アクセシビリティ、またはブラウザー API に依存する場合は、リポジトリがすでに Browser Mode をサポートしているか、追加のハーネスコストに見合うなら、Vitest Browser Mode を優先してください。ブラウザー固有の動作を伴わず、純粋なロジックやコンポーネントの契約を証明できる場合は、既存の安定した jsdom/happy-dom ハーネスを維持するか、より軽量な環境を使用してください。
jsdom ではなく Browser Mode を使用する理由
| 項目 | jsdom/happy-dom | Browser Mode |
|---|---|---|
| 環境 | Node.js 上でシミュレートされた DOM | 実ブラウザー(Chromium/Firefox/WebKit) |
| CSS | レンダリングされない | 実際の CSS レンダリング、レイアウト、計算済みスタイル |
| イベント | 合成 JavaScript イベント | CDP ベースの実ブラウザーイベント |
| API | Web API の一部 | ブラウザー API の全範囲 |
| フォーカス/a11y | 近似的 | 実際のフォーカス管理、アクセシビリティツリー |
| デバッグ | コンソールのみ | 完全なブラウザー DevTools |
セットアップ
依存関係や設定を変更する前に、リポジトリのパッケージマネージャー、ロックファイル、既存のテストハーネス、インストール済みのバージョンを確認してください。新しいハーネスの導入が妥当で、ユーザーからセットアップの許可を得ている場合は、公式の互換性情報やピア依存関係の情報に基づいて、相互に互換性のある正確なバージョンを選び、リポジトリのパッケージマネージャーでインストールしてください。また、リポジトリ内のバイナリのみを実行し、暗黙的なダウンロードは行わないでください。例:
<repo-pm> add --save-dev vitest@<reviewed-version> @vitest/browser-playwright@<reviewed-version>
<repo-pm> exec playwright install chromium # inspect this binary download before authorizing it
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: 'chromium' }],
},
},
})
確認済みのインストール済みバージョンで利用できる場合は、<repo-pm> exec vitest init browser を実行してローカルのセットアップウィザードを起動してください。まず、予定されている設定ファイルへの書き込み内容を確認してください。
ブラウザテストの集中的なフィードバック
ブラウザテストにも、tdd スキルの迅速なフィードバック方針を適用してください。
- Vitest Browser Mode では、リポジトリが所有するテストと差分で選択されたテストの使い分け、ライフサイクル、クリーンアップ、ネイティブなテスト選択、実行時の検証に関する規則として、
tddスキルおよび Vitest watch reference に記載されたものに従ってください。Node/SSR 4.1.10 のクリーンスタート検証は、Browser Mode の検証にはなりません。インストール済みのバージョンと設定を個別に確認してください。--changed --watchが起動後に VCS の影響範囲を再読み込みすると想定しないでください。 - ブラウザの依存関係は、インポートグラフの外部にあることがよくあります。ルーティング、グローバルスタイル、ブラウザのセットアップ、テンプレート、生成アセット、型のみの依存関係、ブラウザモードのオーケストレーション、Docker を使用するサービスまで、対象を広げてください。リポジトリの
watchTriggerPatterns、forceRerunTriggers、ルートのモノレポプロジェクト/タスクグラフは維持してください。 - Playwright Test には、影響を受ける範囲をネイティブに選択して一度だけ実行する機能があります。インストール済みのバージョンが対応している場合は、GREEN/REFACTOR ではリポジトリのスクリプト、または
playwright test --only-changed[=<real-base>]を優先してください。この機能は、変更されたテストファイルと、変更されたファイルをインポートするテストを選択しますが、Playwright はこれをヒューリスティックな方法と説明しています。ブラウザのジャーニーでは、アプリケーションコードをインポートせず、実行時に呼び出すことがよくあります。また、動的な依存関係やインポート以外の依存関係は検出されない場合があります。そうした変更では、共有フィクスチャ、認証状態、セットアップ、ルーティング、スタイリング、UI パッケージのすべての利用元を含む、リポジトリのマッピングに基づいて影響を受けるジャーニー/プロジェクト一式を使ってください。判断に迷う場合は対象を広げてください。 - RED の検証や既知の失敗のデバッグに限り、Playwright のファイル指定、
--grep、--last-failed、または手動で選んだプロジェクトフィルターを使用してください。GREEN/REFACTOR でプロジェクトフィルターを使えるのは、実行ランナー/ワークスペースのグラフにより、推移的な利用元も含む影響対象のプロジェクト一式が機械的に導出されている場合に限ります。--only-changedが期待するテストを実際に実行することを確認してください。選択対象が空の場合、GREEN の根拠にはなりません。 - 一度だけ実行するランナーの場合、テストを作成した後に、影響対象のコマンドを再実行してください。影響対象が空の場合、Vitest の
--passWithNoTests、Playwright の--pass-with-no-testsは、いずれも根拠として扱わないでください。 - PR の準備が整った段階で、ウォッチャーを停止し、対象リポジトリの変更ポリシーと、ウォッチモードを使わない完全なテストゲートを適用してください。このゲートには、設定済みのすべてのブラウザ/プロジェクトを対象とする、必須 UI マトリックス全体が含まれます。Chromium のみ、
--grep、または UI のみを対象とした結果では不十分です。
組み込みロケーター
Vitest Browser Mode には、Testing Library のクエリに対応する組み込みロケーターがあります。@testing-library/dom を別途インポートする必要はありません。
import { page } from 'vitest/browser'
// These work exactly like Testing Library queries
page.getByRole('button', { name: /submit/i })
page.getByText(/welcome/i)
page.getByLabelText(/email/i)
page.getByPlaceholder(/search/i)
page.getByAltText(/logo/i)
page.getByTestId('my-element') // Last resort only
再試行機能付きの組み込みアサーション
DOM のアサーションには expect.element() を使用してください。アサーションが成功するかタイムアウトするまで自動で再試行されるため、テストの不安定さを軽減できます。
// ✅ CORRECT - Auto-retrying assertion
await expect.element(page.getByText(/success/i)).toBeVisible()
await expect.element(page.getByRole('button')).toBeDisabled()
// Available matchers (no @testing-library/jest-dom needed):
await expect.element(el).toBeVisible()
await expect.element(el).toBeDisabled()
await expect.element(el).toHaveTextContent(/text/i)
await expect.element(el).toHaveValue('input value')
await expect.element(el).toHaveAttribute('aria-label', 'Close')
await expect.element(el).toBeChecked()
組み込みユーザーイベント(CDP ベース)
import { userEvent } from 'vitest/browser'
// Real browser events via Chrome DevTools Protocol
await userEvent.click(page.getByRole('button', { name: /submit/i }))
await userEvent.fill(page.getByLabelText(/email/i), '[email protected]')
await userEvent.keyboard('{Enter}')
await userEvent.selectOptions(page.getByLabelText(/country/i), 'USA')
await userEvent.clear(page.getByLabelText(/search/i))
または、ロケーターのメソッドを直接使用してください。
await page.getByRole('button', { name: /submit/i }).click()
await page.getByLabelText(/email/i).fill('[email protected]')
jsdom を使用するコードベースでは、代わりに @testing-library/user-event を使用してください。ユーザー操作には fireEvent よりもこちらを優先してください(resources/dom-testing-library-legacy.md を参照)。デフォルトでは、テストごとに新しい userEvent.setup() を作成してください。テストが並行実行されない場合に限り、各テストで新しいインスタンスを作成する beforeEach も有効です。スイート全体で共有するユーザーインスタンスは再利用しないでください。
マルチプロジェクトのセットアップ(Node + Browser)
ユニットテスト(Node)と UI テスト(ブラウザ)の両方が必要な場合:
export default defineConfig({
test: {
projects: [
{
test: {
include: ['tests/unit/**/*.test.ts'],
name: 'unit',
environment: 'node',
},
},
{
test: {
include: ['tests/browser/**/*.test.ts'],
name: 'browser',
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
},
],
},
})
ブラウザモードでの注意点
- インポートに対する
vi.spyOn:実際のブラウザーでは、ES モジュールの名前空間は封印されています。vi.mock('./module', { spy: true })は動作しますが、モジュールのモックは一時的な足場として扱ってください。依存関係を明示的な接点にできるよう、パラメーター注入を優先してください(finding-seamsスキルを読み込んでください)。ネットワークリクエストを行うモジュールに対しては、決してこれを解決策として使わないでください。どの環境でも、成功パスだけでなくエラーパスでも、MSW を使ってネットワーク層でモックしてください。 alert()/confirm():スレッドをブロックするダイアログは、ブラウザーの実行を停止させます。vi.spyOn(window, 'alert').mockImplementation(() => {})でモックしてください。act():ロケーターを介したコンポーネント操作では不要です。CDP イベントとexpect.element()の再試行でタイミングに対処できます。renderHookの状態更新には引き続きactが必要です(react-testingを参照)。
Playwright/ブラウザモードのテストの冪等性
Playwright 形式のテストはすべて、冪等でなければなりません。 各テストは、実行順序、実行回数、または先に実行された他のテストにかかわらず、同じ結果を返す必要があります。
ルール:
- 各テストは状態を一から作成します。他のテストの副作用に依存してはいけません。
- テスト中に作成した永続状態(データベースの行、localStorage、Cookie)をクリーンアップします。
- 並列実行に十分なエントロピーを持つ識別子を使用します(例:
crypto.randomUUID())。タイムスタンプだけでは衝突する可能性があります。 - テスト開始時の DOM が特定の状態だと想定してはいけません。新しくレンダリングしてください。
- テストでサーバーやデータベースを共有する場合は、分離戦略(トランザクション、テスト専用データ)を使用します。
// ❌ WRONG - Tests depend on shared state
it('creates a user', async () => {
await page.getByRole('button', { name: /create/i }).click()
// Creates user "Alice" in the database
})
it('lists users', async () => {
// Assumes "Alice" exists from previous test!
await expect.element(page.getByText('Alice')).toBeVisible()
})
// ✅ CORRECT - Each test is self-contained
it('creates and displays a user', async () => {
const uniqueName = `User-${crypto.randomUUID()}`
try {
await page.getByLabelText(/name/i).fill(uniqueName)
await page.getByRole('button', { name: /create/i }).click()
await expect.element(page.getByText(uniqueName)).toBeVisible()
} finally {
await testData.deleteUserByName(uniqueName) // repository-owned idempotent cleanup fixture
}
})
これが重要な理由: ブラウザモードでは、複数のブラウザーインスタンスでテストを並列実行できます。冪等でないテストは不安定な失敗を引き起こし、原因のデバッグがほぼ不可能になります。
Playwright E2E は別のテーマです
Vitest ブラウザモードはコンポーネントを分離してテストします。一方、実行中のアプリケーションに対する Playwright Test は、テストの主張が対象とするもの、つまりユーザージャーニー、フロントエンド自体のネットワーク動作、Cookie/CSRF の状態、リダイレクト、レンダリングなどをテストします。同じブラウザーエンジンでも、対象とハーネスは異なります。両者のガイダンスが共通して使えると決めつけてはいけません。
主張ひとつに、ハーネスひとつ。 主張が誤っている場合に失敗する最も軽量なハーネスでその主張を証明し、そこで止めてください。ひとつのマウント済みコンポーネント内の動作(バグ修正、エラーパス、復帰できなければならない無効化されたボタンなど)は、コンポーネントレベルのハーネスで証明できます。それをなぞり直す E2E 仕様を追加しても、得られる証拠は増えず、保守対象のスイートがもうひとつ増えて、ゲートが遅くなるだけです。主張そのものが提供中のアプリケーションに関するもの、つまり画面遷移、複数画面の順次表示、実際のサーバー、Cookie、リダイレクトなどである場合に Playwright を使用してください。すでにコンポーネントテストを書いているなら、ユーザージャーニーのテストを追加する理由を明確に説明できなければなりません。
E2E スイートを規定するルールはひとつです。ブラウザーやユーザージャーニーに関する主張は、ブラウザーを起点とする操作で証明しなければなりません。つまり、アクセシブルなロケーターによる操作か、ナビゲーションを使います。ユーザーやフロントエンドの代わりに直接 HTTP 呼び出しを使ってはいけません。テスト名が「ユーザーが作成する……」であるのに page.request.post('/api/...') を使っても、証明できるのは HTTP の契約であり、ユーザージャーニーではありません。ボタン、Cookie ポリシー、CSRF チェック、リダイレクト、レンダリングが壊れていても、テストは成功します。E2E/ユーザージャーニーのスイートを作成またはレビューする前に、resources/playwright-e2e.md を読み込んでください。そこには、判断ルール、証拠境界の表、安全なリクエスト観測、直接トランスポートの監査手順、認証/ライフサイクルの証拠に関する契約が記載されています。
クエリ選択の優先順位
最も重要なスキルは、適切なクエリを選ぶことです。 Browser Mode のロケーターと Testing Library のクエリはほぼ同じですが、名前が異なるものには以下で注記しています。
クエリは次の順に使用してください(アクセシビリティ優先)。
getByRole- 最優先です。ARIA ロールとアクセシブルネームでクエリし、スクリーンリーダーでの利用体験を反映します。セマンティックな HTML を促します。getByLabelText- 関連付けられた<label>を使ってフォームフィールドを取得します。getByPlaceholder- ラベルがない場合の入力欄の代替手段です(プレースホルダーでラベルを置き換えるべきではありません)。Testing Library ではgetByPlaceholderTextという名前です。getByText- ユーザーが読む、インタラクティブではないコンテンツを取得します。getByDisplayValue- 値があらかじめ入力された入力欄を取得します。Testing Library のみです。Browser Mode には該当するロケーターがないため、代わりにgetByRoleと値のアサーションを使用してください。getByAltText- 画像を取得します。getByTitle- 他のクエリが使えない場合に限り、まれに使用します。getByTestId- 最終手段としてのみ使用します。ユーザー向けではありません。
クエリのバリエーション(Testing Library)
getBy*- 要素が存在する必要があります(見つからない場合はエラーになります)。存在をアサートするときに使用します。queryBy*- 見つからない場合は null を返します。存在しないことをアサートするときに使用します。findBy*- 非同期で、要素が表示されるまで待機します。resources/async-patterns.mdを参照してください。
(Browser Mode のロケーターは遅延評価され、expect.element() によって再試行されるため、get/query/find の使い分けはほぼ不要です。要素がないことを確認するには、expect.element を使って .not.toBeInTheDocument() を指定してください。)
よくある間違い
// ❌ WRONG - querySelector (DOM implementation detail)
const button = container.querySelector('.submit-button');
// ❌ WRONG - testId when a role is available (not how users find the button)
screen.getByTestId('submit-button');
// ❌ WRONG - role without accessible name (which button? pages have many)
screen.getByRole('button');
// ✅ CORRECT - role + accessible name (how screen readers find it)
screen.getByRole('button', { name: /submit/i });
// ❌ WRONG - getBy to assert non-existence (awkward throw-based check)
expect(() => screen.getByText(/error/i)).toThrow();
// ✅ CORRECT - queryBy returns null
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
// Less specific: finds matching text without proving the element's role
screen.getByText(/welcome,\s+john doe/i);
// Preferred when this is a heading: query the accessible role and name
screen.getByRole('heading', { name: /welcome,\s+john doe/i });
アクセシビリティを優先したテスト
アクセシブルなクエリには、次の3つの利点があります。
- テストが実際の利用方法を反映する - スクリーンリーダーと同じ方法でクエリします。
- アプリのアクセシビリティが向上する - テストによってアクセシブルなマークアップが促されます。
- リファクタリングに強い - 実装ではなく、ユーザー体験に結び付きます。
アクセシブルなクエリが失敗した場合は、まずアクセシブルネームとロールを調べてください。失敗からアクセシビリティの問題が見つかることもありますが、クエリやテストのセットアップが間違っている可能性もあります。
クエリに一致する要素が複数ある場合も、同じ兆候です。クエリのスタイルを変えて解決してはいけません。より具体的なロールとアクセシブルネーム、結果そのものを示すユーザーに表示されるテキスト、filter({ hasText: /…/i })、またはアクセシブルなクエリで取得したコンテナー(getByRole('region', { name: /…/i })、within(screen.getByRole('form', { name: /…/i })))を使って、アクセシビリティを保ったまま曖昧さを解消してください。アクセシブルな方法で区別できない場合は、ページにアクセシブルネームが不足しています。追加してください。クラスや ID を使って範囲を絞り込み、曖昧な一致を回避してはいけません(page.locator('#panel').getByRole(...)、within(container.querySelector('.panel')))。この方法では、テストがユーザーには認識できないマークアップに再び結び付き、曖昧さの原因となったアクセシビリティの不足が見えなくなります。
ARIA よりもセマンティックな HTML を常に優先してください。
<div role="button" onclick="handleClick()" tabindex="0">Submit</div>
<button onclick="handleClick()">Submit</button>
セマンティックな HTML が使えない場合に限り(たとえば、カスタムモーダルでの role="dialog")、ARIA を追加してください。セマンティックな要素に重複して指定してはいけません。
非同期テスト
Browser Mode では、await expect.element(...) がほとんどの場合、自動的に待機を処理します。jsdom を使用するコードベースでは、要素の表示には findBy*、複雑な条件には waitFor、要素の消失には waitForElementToBeRemoved を使用してください。
主なルール: waitFor 内で副作用を起こさないでください。失敗の内容を分かりやすくするため、waitFor にはアサーションを1つだけ指定することを推奨します。findBy を waitFor で囲まないでください。
詳しいパターンとアンチパターンについては、resources/async-patterns.md を参照してください。
MSW による API モック
fetch/axios のモックではなく、MSW を使用してください。ネットワーク層でインターセプトするため、同じハンドラーをテスト、Storybook、開発環境で使用できます。
このルールの対象は、アプリ独自のリクエストモジュールであり、fetch 自体だけではありません。 ほとんどのアプリでは、トランスポートを 1 つのモジュール(api/client/service ファイル、生成された SDK など)でラップしています。そのモジュール自体を置き換える(vi.mock('./api') を使う、またはテスト対象に手書きのフェイククライアントを渡す)のは、1 層上で同じアンチパターンを行うことになります。テストから実際の URL 構築、シリアライズ、ステータス処理、エラーマッピングが取り除かれ、アプリが実際に行うリクエストについて何も検証できません。モックするのは レスポンス であり、リクエストを行うモジュールではありません。これは失敗するケースにも当てはまります。リクエストを失敗、タイムアウトさせる場合や、一度失敗してから成功させる場合は、テストごとのハンドラー(HttpResponse.error() または 2xx 以外のステータスを指定した server.use()/worker.use())を追加してください。自作モジュールに対して mockRejectedValueOnce を使うのは避けてください。
環境によって使用する API が異なります:
- ブラウザーモード:
msw/browserのsetupWorker(セットアップファイルでワーカーを起動し、テストごとのオーバーライドにはworker.use()を使用) - Node/jsdom:
msw/nodeのsetupServer(テストごとのオーバーライドにはserver.use()を使用)
ブラウザーモードで setupServer を使用しても、何も起こりません。テストは実際のブラウザーで実行されます。両方のセットアップの詳細については、resources/msw.md を参照してください。
主なアンチパターン
- 実装の詳細をテストする — 内部状態、プライベートメソッド、フックとして使う CSS クラスなど。ユーザーに見える動作をテストしてください。
- アクセシブルなクエリがあるのに
querySelector/testId を使う — 上記の「クエリ選択の優先順位」を参照してください。 - 共有変数を使った beforeEach でのレンダリング — 代わりに、テストごとにファクトリー関数を使用してください。
// ❌ WRONG - Shared state across tests
let button;
beforeEach(() => {
render('<button>Submit</button>');
button = screen.getByRole('button');
});
// ✅ CORRECT - Factory function per test
const renderButton = () => {
render('<button>Submit</button>');
return { button: screen.getByRole('button') };
};
ファクトリーのパターンについては、testing スキルを参照してください。
- MSW を使わず、ネットワークより上の層でネットワークを偽装する —
fetch/axiosのスタブ化や、アプリ独自のリクエスト/api/client モジュールのvi.mock(エラーパスをテストするために reject させる場合も含む)など。resources/msw.mdを参照してください。 - waitFor の誤用 —
resources/async-patterns.mdを参照してください。 - jsdom 固有のアンチパターン(
screenの使用を避ける、fireEventを使う、ハーネスがすでに提供している場合に不要なクリーンアップを行う、jest-dom マッチャーの代わりにプロパティをアサートする、ESLint プラグインが不足しているなど)—resources/dom-testing-library-legacy.mdを参照してください。 - ブラウザーの名前を装った HTTP レベルの近道 — 「journey」/「browser」/「E2E」テストを名乗りながら、ユーザーやフロントエンドが行ったとする処理を
page.request/page.evaluate(fetch)で実行することや、ブラウザー以外のクライアントであることを示す偽装ブラウザーヘッダー(Sec-Fetch-*、Origin)など。resources/playwright-e2e.mdを参照してください。
要約チェックリスト
UI テストをマージする前に、次の項目を確認してください。
- 主張、リポジトリのサポート状況、コストに合ったハーネスを選んでいること。ブラウザーで観測可能な動作には Browser Mode を使用し、契約を証明できる場合は、より軽量で安定したハーネスを使用します
- すべての Playwright/Browser Mode テストがべき等であること(テスト間で状態を共有しない)
- クエリには
getByRoleを最初に使用していること(組み込み、または Testing Library) - 自動再試行するアサーションには
expect.element()を使用していること(Browser Mode) - 操作には
userEventを使用していること(Browser Mode では CDP ベース、または@testing-library/user-event) - 実装の詳細ではなく、ユーザーが目にする動作をテストしていること
- 引き継ぎ時(返信、PR の本文、CI ステップのラベル)に、証拠の取得元となったハーネスを明記し、それでは証明できない最も近い主張を示していること
- クリーンアップが、このハーネスで自動的に行われることを確認済みであるか、テストセットアップに一度だけ登録されていること
- コンポーネントの操作に手動で
act()を呼び出していないこと(Browser Mode がタイミングを処理します) - API のモックには MSW を使用していること。Browser Mode では
setupWorker、Node/jsdom ではsetupServerを使用し、成功パスでも失敗パスでも、vi.mockやアプリ独自のリクエストモジュールの手書きフェイクを使用していないこと - TDD ワークフローに従っていること(
tddスキルを参照) - RED/デバッグ時のブラウザー実行では、結果を証明できる最小限のファイル/プロジェクト/grep の選択を使用していること
- GREEN/REFACTOR 時のブラウザーフィードバックでは、ランナー、ワークスペースオーケストレーター、またはリポジトリのマッピングから導き出された、影響を受ける範囲全体を使用していること。信頼できるグラフがない場合は、文書化された「担当スイートと既知のコンシューマー」のフォールバックを使用し、不確かな場合は範囲を広げ、テストファイルを手作業で選ぶことは決してしないでください
- 新しく作成したテストがアクティブなウォッチャーに追加されるか、影響を受ける範囲がワンショットで再実行されること。結果として報告するすべてのテストが、想定どおりに実行されていること
- モノレポでの実行では、ルートのプロジェクト/タスクグラフを使用し、影響を受けるすべてのワークスペースコンシューマーを含めていること
- 最終ツリーを対象に、必須のブラウザー/プロジェクトの全マトリックスを含む、リポジトリ全体の PR テストゲートが非ウォッチモードで成功していること
- 繰り返し使うデータやネストしたデータが明確になる場合はファクトリーを使用し、単純な一度きりの値はインラインのままにしていること(
testingスキルを参照) - Playwright の E2E スイートでは、ブラウザーまたはユーザージャーニーに関するすべての主張に、ブラウザーを開始元とする操作があること。また、直接 HTTP を使用するのは、
resources/playwright-e2e.mdで分類されている限定的な役割(契約、セットアップ、レディネス、事後条件、診断、外部アクター)に限り、ブラウザーの証拠として流用しないこと - React 固有のパターン(フック、コンテキスト、コンポーネント)については、
react-testingスキルを参照してください