mcpField
P
playwright-e2e-testing

playwright-e2e-testing

github.com/perdoliqueテスト・品質047 回インストール2 日前に更新
リポジトリ

playwright-e2e-testing について

playwright-e2e-testing は、Playwright のブラウザー、エンドツーエンド、API テストを作成・デバッグする開発者向けのスキルです。SPA の遷移、リクエストのモック、非同期待機、ユーザーに見える回帰の検証を扱います。

主な機能

  1. 01適切なテスト境界の選択
  2. 02テスト構成とフィクスチャの整理
  3. 03非同期 UI 状態の待機
  4. 04ユーザー視点のセレクター
  5. 05API リクエストと応答のモック
  6. 06ユーザーに見える回帰の検証

活用シーン

  1. 01ブラウザーと E2E フローのテスト作成
  2. 02SPA のルーティングとリダイレクトのテスト
  3. 03API エンドポイントの直接テスト
  4. 04失敗した Playwright テストのデバッグ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/perdolique/workflow --skill playwright-e2e-testing

インストール後、Claude Code で/playwright-e2e-testingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
evals/files/
performance.md
verification.md
evals/files/harness/
context.md
global.fixtures.ts
evals/files/regressions/
icon.html
icon.spec.ts
polling.html
polling.spec.ts
responsive.html
responsive.spec.ts
search.html
search.spec.ts
widget.html
widget.spec.ts
references/
configuration.md
fixtures.md
performance.md

詳細

Playwright によるブラウザー、E2E、API テスト

このスキルでは、Playwright によるブラウザー、E2E、直接的な API テストのパターンと慣例を紹介します。SPA 固有のルーティング、API のモック、非同期ナビゲーションの手法も含まれます。

このスキルを使用する場面

  • ユーザーフローのブラウザーまたは E2E テストを作成する場合
  • ページのルーティングやリダイレクトをテストする場合
  • ダイアログやモーダルの操作をテストする場合
  • Playwright の APIRequestContext を使ってエンドポイントを直接テストする場合
  • Playwright テスト用のモック、フィクスチャ、アサーションを作成する場合
  • 失敗する Playwright のブラウザー、E2E、API テストをデバッグする場合
  • タスクとプロジェクトで確立されているテストツールに基づいて、適切なテストスキルを選択します。モックやテストデータに関する一般的なリクエストは、そのコンテキストに応じて振り分けます。

プロジェクトの設定

playwright.config.ts の設定または変更時は、Web サーバーの設定、レポーター、トレース、スクリーンショット、CI の動作に関する慣例について、references/configuration.md を参照してください。

テストの境界を選ぶ

  • テストで検出したいブラウザーの動作と、起こり得る不具合を明確にします。まず既存のカバレッジを確認します。純粋な計算はプロジェクトのユニットテストに、HTTP の契約を直接検証するものは API テストに任せます。レンダリング、操作、リクエストの接続、ナビゲーション、ブラウザーの状態にはブラウザーを使用します。
  • 別途すべてをセットアップしなくても同じシナリオを保護できる場合は、既存のフローにアサーションを追加します。異なる不具合を防ぐ場合や、異なる状態が必要な場合は、別のテストケースにします。テストで検証するとしている処理の判断部分はモックしないようにします。

テストのインポート

API のインターセプトや共通のセットアップなどのために、Playwright の test オブジェクトを拡張したカスタムフィクスチャファイルがテストディレクトリにあるか確認します。ある場合は、そのファイルから test と expect をインポートし、プロジェクトのセットアップがテストに適用されるようにします。

import { test, expect } from '../fixtures/global.fixtures.ts';

プロジェクトにカスタムフィクスチャがない場合は、Playwright からインポートします。

import { test, expect } from '@playwright/test';

型のみのインポートは、常に直接行って構いません。

import type { Page, Locator } from '@playwright/test';

共有ブラウザーフィクスチャでは、アプリケーションの pageerror、console.error、ハイドレーションの警告、未解決のコンポーネントが発生した場合に失敗するよう設定します。プロジェクトのフィクスチャを再利用する場合も、これらのチェックを維持します。テストで明示的にアサーションするブラウザー生成のリソースエラーのみを除外します。

各テストでブラウザーの状態を新しくするために、提供されている page と context を使用します。テスト同士が状態を変更できない場合に限り、不変のデータやセットアップ用リソースを共有します。テストが失敗した場合も含め、page または context の破棄前に非同期のイベントハンドラーを完了させます。独自に登録したコンソールリスナーについては、フィクスチャのクリーンアップで await page.removeAllListeners('console', { behavior: 'wait' }) を実行すると、登録済みのハンドラーの完了を待機できます。ハンドラー内でペイロードの各読み取り失敗を処理し、1 つの拒否によって他の読み取りが保留中のままハンドラーが終了しないようにします。読み取りに成功した引数の値と、生の読み取りエラーを保持し、所有するすべての読み取りが完了した後に報告します。

ディレクトリ構成

テストは機能ドメインごとに整理します。通常、各ドメインには独自のフィクスチャファイルがあります。

tests/playwright/
├── constants.ts                     # Base URLs, shared constants
├── fixtures/
│   ├── global.fixtures.ts           # Extended test/expect (if project uses one)
│   ├── core-api.fixtures.ts         # Shared API mock helpers
│   └── {feature}.fixtures.ts        # Feature-specific test data
├── {feature-domain}/
│   └── {feature}.test.ts            # Test file
└── routing/
    └── redirects.test.ts            # Router/redirect tests

テストファイルの作成

基本構成
import type { Page } from '@playwright/test';
import { test, expect } from '../fixtures/global.fixtures.ts';
import { mockEndpoint } from '../fixtures/api.fixtures.ts';
import { baseFixture, variantFixture } from '../fixtures/{feature}.fixtures.ts';

// Helper functions (navigation, assertions)
async function openFeaturePage(page: Page): Promise<void> {
  await page.goto('/feature?id=test-id');
}

test.describe('Feature name', () => {
  test('description of expected behavior', async ({ page }) => {
    await mockEndpoint(page, baseFixture);
    await openFeaturePage(page);
    await expect(page.getByText('Expected text')).toBeVisible();
  });
});
ヘルパー関数

繰り返し行う操作には、テストファイルの先頭でヘルパー関数を定義します。ヘルパーを使うと読みやすさが向上し、重複を減らせます。

  • ナビゲーションヘルパー — openDialog(page)、gotoProductPage(page)
  • アサーションヘルパー — expectSummary(page, options)、expectAvailability(page, from, to)
  • セットアップヘルパー — テスト固有のモックのセットを用意する setupCommonMocks(page)
  • ロケーターヘルパー — Locator を返す actionButtons(page)
beforeEach による共通セットアップ

describe ブロック内のすべてのテストで同一のモックセットアップを共有する場合:

test.describe('Router redirects', () => {
  test.beforeEach(async ({ page }) => {
    await setupCommonMocks(page);
  });

  test('redirects when resource is not found', async ({ page }) => {
    await page.goto('/resource?id=unknown');
    await expect(page).toHaveURL(/\/error\?id=unknown$/u);
  });
});
タイミングとレイアウト
  • Playwright のアサーションを使って、関連する UI の状態を待ちます。固定時間のスリープや networkidle では、テスト対象の要素やデータの準備が整ったことを確認できません。
  • ポーリングやデバウンスの動作をテストする場合は、通常はナビゲーションの前など、アプリケーションのタイマーが開始する前に page.clock を設定します。シナリオで各タイマーコールバックの実行が必要な場合は runFor を使用します。テスト対象の動作に照らして、コールバックの実行回数を減らしてもよい場合に限り、時間を飛ばすために fastForward を使用します。
  • リクエストまたはレスポンスの待機は、トリガーとなる操作や時間の進行より前に登録します。時間を進め、関連する HTTP の結果を待ってから、表示された結果をアサートします。アプリケーションのポーリング、再読み込み、状態変更は実際に動作させ、プロダクト内部の処理を呼び出したり、最終的な DOM の状態を設定したりする代わりに、モックレスポンスを更新します。
  • レイアウトを測定する場合は、関連するフォントの読み込みと、完了までの時間が有限な要素のアニメーションを待ちます。モーションやトランジションのタイミングが仕様の一部である場合は、アニメーションを有効にしたまま、必要な中間状態と最終状態を確認します。ブラウザーの時計は、CSS アニメーションやサーバー時刻の証拠の代わりにはなりません。
test.step() を使った複数ステップのフロー

購入やフォーム送信など、複雑なユーザーフローの意味のある段階には、名前付きの test.step() ブロックを使用します。これにより、レポートで失敗した段階を特定できます。プロジェクトに特定の規約がない限り、短くまとまったテストは、アクションとアサーションを直接並べて記述します。

test('completes purchase flow', async ({ page }) => {
  await test.step('prepare mocks', async () => {
    await mockProduct(page, productFixture);
    await mockCheckout(page);
  });

  const purchaseRequestPromise = page.waitForRequest(
    request => request.url().includes('/purchases') && request.method() === 'POST'
  );

  await test.step('open product page', async () => {
    await gotoProductPage(page);
  });

  await test.step('select option', async () => {
    await page.getByRole('button', { name: '14:00' }).click();
  });

  await test.step('verify confirmation page', async () => {
    await expect.poll(() => new URL(page.url()).pathname).toBe('/order-confirmation');
  });

  await test.step('submit order', async () => {
    await page.getByRole('button', { name: 'Confirm' }).click();
  });

  await test.step('verify purchase request', async () => {
    const request = await purchaseRequestPromise;

    expect(request.postDataJSON()).toEqual({
      items: [{ id: 'product-1', date: '2088-04-21T11:00:00.000Z' }]
    });
  });
});

複数ステップのフローでよく使うパターン:

  • page.waitForRequest() — リクエストをトリガーする操作より前に設定します
  • expect.poll() — SPA ナビゲーション後の非同期 URL 変更を待ちます
  • satisfies — リテラル型を保持したまま、リクエストペイロードの期待値を型チェックします
addInitScript を使った localStorage の設定

ページの読み込み前に localStorage の値を設定するには(ルーティングテスト用に状態を保存する場合など):

async function setSavedState(page: Page, key: string, value: unknown): Promise<void> {
  await page.addInitScript(
    ({ storageKey, data }: { storageKey: string; data: string }) => {
      localStorage.setItem(storageKey, data);
    },
    { storageKey: key, data: JSON.stringify(value) }
  );
}

addInitScript はページの読み込み前に実行されるため、初期化時にアプリが正しい localStorage の値を読み取ります。page.evaluate はページの読み込み後に実行されるため、ルートガードに対しては遅すぎます。

セレクターのパターン

ユーザーに見えるセレクターを、次の順に優先して使用します。

  1. page.getByRole('button', { name: 'Confirm' }) — アクセシブルなロールと名前
  2. page.getByText('Expected text') — 表示されるテキストコンテンツ
  3. page.getByTestId('action-button') — data-testid 属性
  4. page.getByText('text', { exact: true }) — 部分一致を避ける完全一致
  5. page.getByText(/regex pattern/u) — 動的コンテンツに対する正規表現

アクセシブルなロールと名前、関連するコンテナー、行またはカード、表示状態を使って、ロケーターを対象の要素に絞り込みます。位置がテスト対象の動作の一部である場合、または位置によって同等の一致要素を選ぶ妥当な理由がテストにある場合は、.first() または .nth() を使用します。

アサーションのパターン

// Visibility
await expect(page.getByText('Welcome')).toBeVisible();

// Element count
await expect(page.getByRole('button', { name: /\d{2}:\d{2}/u })).toHaveCount(3);
await expect(page.getByText('Not present')).toHaveCount(0); // Assert absence

// URL matching
await expect(page).toHaveURL('http://localhost:5050/error?id=unknown');
await expect(page).toHaveURL(/\/order-success\?id=test-id$/u);

// Async URL change (SPA navigation)
await expect.poll(() => new URL(page.url()).pathname).toBe('/order-confirmation');

// Interactive state
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();

// Unicode-aware regex with explicit apostrophe variants
await expect(page.getByText(/^You(?:'|’|ʼ)re all set!$/u)).toBeVisible();

正規表現が Unicode 対応モードで解析されるよう、常に /u(Unicode)フラグを使用します。テキストにアポストロフィなどの複数の文字表記が含まれる可能性がある場合は、パターン自体に許容する文字のバリエーションを記述します。

ユーザーに見える回帰の証拠

  • アサーションを選ぶ前に、ユーザーを保護する観測可能な動作を定義します。
  • 実質的に変更されたすべての状態と遷移を、関連するポインター入力とキーボード入力で検証します。
  • 保護対象の動作が削除または迂回されたときに失敗する回帰テストを作成します。合格し続けるテストでは、その契約を保護できません。
  • 動作に必要な描画内容とジオメトリをアサートします。ラッパーの存在確認や toBeVisible() だけでは、空のアイコン、クリップされた子要素、ビューポート外のコンテンツでも合格することがあります。
  • 状態遷移では、関連する順序、タイミング、フォーカス、安定したコンテンツをアサートします。最終状態だけでは、ユーザーが一瞬のちらつき、古い結果、フォーカス喪失を回避できたことは証明できません。
  • レスポンシブ動作では、異なる問題を明らかにできるビューポートを選びます。オーバーフローにはサポート対象の最小幅、デスクトップレイアウトには一般的なデスクトップ幅、動作が異なる場合は変更されたブレークポイントの両側を使います。ビューポートを変更せずに異なるリスクが生じる場合は、コンテナレスポンシブなコンポーネントを狭いコンテナと広いコンテナの両方で検証します。
  • 保護対象のレイアウトに影響する可能性がある場合は、現実的な長文と、空白を含まない長いコンテンツを使います。ドキュメントと関連する子要素の矩形を確認します。ルートの scrollWidth だけを確認しても、子要素が表示されていることやビューポート内にあることは証明できません。
  • アサーションは契約に必要な範囲に絞ります。1つのコンポーネント状態だけが重要な場合に、焦点を絞った証拠をページ全体のスナップショットに置き換えないでください。

page.route() を使った API のモック

基本的なモック
async function mockProducts(page: Page, response: unknown): Promise<void> {
  await page.route('**/api/products**', async (route) => {
    await route.fulfill({ json: response });
  });
}
作成からポーリングまでのフローをモックする
async function mockPurchase(page: Page, purchaseId: string, response: unknown): Promise<void> {
  // Mock POST (create)
  await page.route('**/api/purchases', async (route) => {
    await route.fulfill({ json: { id: purchaseId } });
  });

  // Mock GET (status poll)
  await page.route(`**/api/purchases/${purchaseId}`, async (route) => {
    await route.fulfill({ json: response });
  });
}
関係のない外部リクエストを制御する

シナリオとプロジェクトのネットワークポリシーに応じて、ネットワーク境界を選びます。既存のモックを再利用し、シナリオに必要なレスポンスだけをオーバーライドします。グロブまたは正規表現を使うと、オリジンガードのインターセプトメッセージとハンドラーの両方からローカルアセットを除外できます。

const appOrigin = new URL(appBaseUrl).origin;
const escapedAppOrigin = appOrigin.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
const externalRequests = new RegExp(`^(?!(?:blob:)?${escapedAppOrigin}(?:/|$))`, 'u');

await page.route(externalRequests, async (route) => {
  await route.abort();
});

シナリオで外部連携そのものをテストする場合は、設定済みのテスト環境を使い、そのやり取りを実際に行います。テスト対象の動作以外の依存関係をモックします。

該当する場合は、完了、期限切れ、キャンセル、再試行、単回使用の値など、シナリオに関係するライフサイクルを各モックで再現します。実際の依存関係では発生しない状態をモックで生成しないでください。安全で現実的なモックを作れない場合は、公式のテストモードを使うか、その動作を未検証として明記します。

既存のモックをオーバーライドする

Playwright はルートの照合に 後から登録したものが優先される 方式を使います。同じパターンに対して新しいルートを登録すると、先に登録したモックをオーバーライドできます。

// Global fixture mocks /api/properties with defaults
// Your test overrides with custom data:
await page.route('**/api/properties/**', async (route) => {
  await route.fulfill({ json: customPropertyData });
});
リクエストのキャンセルを検証する

キャンセルが重要な場合は、古いレスポンスを保留し、置き換えをトリガーする前に requestfailed を登録します。中断(Chromium では ERR_ABORTED)をアサートした後、モックを解放し、現在のデータだけが描画されることを確認します。最終的な UI だけでは、通信がキャンセルされたことは証明できません。すでに中断されたルートを fulfill しないでください。

サードパーティの依存関係をモックする
async function setupExternalMocks(page: Page): Promise<void> {
  await page.route('https://analytics.example.com/**', async (route) => {
    await route.fulfill({ status: 200, body: '' });
  });
}

テストデータのフィクスチャを作成する

フィクスチャデータを作成または変更する場合は、フィクスチャガイドを参照してください。ベースオブジェクトとバリエーション、プライベートファクトリー、リテラル型、既存の契約、シナリオの日付、モックヘルパーについて説明しています。

API リクエストのテスト

サーバー側の API エンドポイントを直接テストする場合は、Playwright の APIRequestContext を使います。ブラウザーは不要です。

認証用のワーカー スコープのフィクスチャ

セッションとサーバーデータを安全に共有できる場合は、API 認証にワーカースコープのフィクスチャを使います。フィクスチャがセットアップとティアダウンを管理しますが、その APIRequestContext は引き続き変更可能な Cookie を保持します。共有認証またはサーバー状態を変更するテストには、別のコンテキストまたは ID を使います。

// tests/playwright/fixtures.ts
import { test as base, type APIRequestContext } from '@playwright/test'
import { appBaseUrl } from './constants'

interface WorkerFixtures {
  authedRequest: APIRequestContext;
}

export const test = base.extend<Record<never, never>, WorkerFixtures>({
  authedRequest: [
    async ({ playwright }, use) => {
      const request = await playwright.request.newContext({ baseURL: appBaseUrl })
      await request.post('/api/auth/create-session')
      await use(request)
      await request.dispose()
    },
    { scope: 'worker' },
  ],
})

API テストファイルでは、このファイルから test をインポートします。テストコールバックでは { authedRequest } としてフィクスチャを利用できます。

型安全な JSON パース

APIResponse.json() は Promise<any>(Playwright の Serializable = any)を返します。テストで JSON 本文を読み取る場合は、中間型として unknown を使用し、Valibot でパースします。

const response = await request.get('/api/items')
const raw: unknown = await response.json()
const body = v.parse(mySchema, raw)  // Valibot accepts unknown, returns typed result

Valibot の parse は実行時にデータの形状を検証し、型付けされたデータを返します。失敗箇所を明確に保つため、リクエスト、JSON の読み取り、パースはそれぞれ別の行に記述します。ステータス、ヘッダー、または本文がないことだけを確認するテストでは、JSON を読み取らず、対応するレスポンスアサーションを使用します。

API テストでアサートする内容

Valibot でレスポンスの形状を検証した後は、HTTP コントラクトとテスト対象の動作に注目してアサートします。スキーマですでに同じ要件を確認している場合に限り、形状に関するアサーションを削除します。

// Redundant when v.parse(schema, raw) already validates this shape
expect(body).toMatchObject({ id: expect.any(Number), name: expect.any(String) })

// Tests the HTTP contract, correct data, filter logic, and auth behavior
expect(response.status()).toBe(200)
expect(body.name).toBe('MSR')
expect(body.items.length).toBeGreaterThan(0)
expect(filteredBody.items.every(item => item.category.slug === 'sleeping-pads')).toBe(true)

価値の高い API アサーション:ステータスコード、特定の値(正しいレコードであることを確認する名前、スラッグ、ID)、フィルターの正確性、ページネーションの境界、認証の適用(セッションなしの場合は 401)。

401/未認証のレスポンスをテストする

認証が必要なテストの否定ケースでは、認証済みフィクスチャを再利用せず、その場で匿名コンテキストを作成します。

test('returns 401 without session cookie', async ({ playwright }) => {
  const anonRequest = await playwright.request.newContext({ baseURL: appBaseUrl })
  const response = await anonRequest.get('/api/equipment/groups')

  expect(response.status()).toBe(401)

  await anonRequest.dispose()
})
# Run all Playwright tests
vpx playwright test

# Run a specific test file
vpx playwright test tests/playwright/product/checkout.test.ts

# Run with UI mode for debugging
vpx playwright test --ui

# Run with headed browser
vpx playwright test --headed

# Run with Playwright inspector/debug mode
vpx playwright test --debug

# Open the last HTML report
vpx playwright show-report

利用可能なテストスクリプトは、プロジェクトの package.json で確認してください。多くのプロジェクトでは、test:playwright、test:e2e などのショートカットが定義されています。

変更したブラウザーシナリオは、プロジェクト内の該当するブラウザープロジェクトで、リトライなしで実行してください。サーバーがこのチェックアウトの現在のビルドを提供していることを確認し、サーバーの再利用と失敗時のトレースについては設定ガイドに従ってください。タスクの対象がスイートの実行時間、フィクスチャのオーバーヘッド、ワーカー、またはシャーディングの場合は、パフォーマンスガイドを読んでください。通常のテスト変更では、スイート全体のベンチマークを繰り返す必要はありません。

検証チェックリスト

E2E テストを完了とする前に、以下を確認してください。

  • プロジェクトにフィクスチャファイルがある場合はそこから test と expect をインポートし、ない場合は @playwright/test からインポートしている
  • Page や Locator などの Playwright の型を import type で直接インポートしている
  • 機能固有のフィクスチャで、エクスポートするすべてのオブジェクトに as const を使用している
  • 既知のプロジェクトコントラクトを持つフィクスチャには、互換性のある satisfies チェックを使用している。意図的に無効にしたフィクスチャは、該当するシナリオでも無効なままになっている
  • フィクスチャの日付が意図する状態を表している。動作が現在時刻や境界に依存する場合は、時間を制御している
  • test.describe() で関連するテストをグループ化している
  • ナビゲーションやアサーションのパターンが繰り返される場合は、ヘルパー関数を抽出している
  • 複雑なフローの重要な段階には test.step() ブロックを使用している。短く一貫性のあるテストは直接記述している
  • page.waitForRequest() を、トリガーとなる操作の前に設定している
  • 正規表現パターンで /u フラグを使用している
  • ロケーターが意図する要素を意味的に特定している。.first() または .nth() は意図的な位置指定として使用している
  • テスト対象の動作以外の依存関係は、プロジェクトのネットワークポリシーに従って制御している。テスト対象の統合は、設定済みのテスト環境で実際に動作させている
  • JSON を読み取る API テスト:本文を unknown として受け取り、Valibot で検証している。スキーマと重複する形状アサーションのみを削除している
  • JSON を読み取らない API テスト:ステータス、ヘッダー、本文がないことに対して、人工的なスキーマを使わず、対応するレスポンスアサーションを使用している
  • API テスト:認証テストでは認証済みフィクスチャではなく、その場で作成した匿名コンテキストを使用している
  • API テスト:認証には、独立した状態を保つスコープを持つ、フィクスチャが所有するコンテキストを使用している
  • テストが正常に実行される

参考ファイル

  • references/configuration.md — Playwright の設定:Web サーバー、レポーター、トレース、スクリーンショット、CI の動作
  • references/fixtures.md — フィクスチャの完全なパターン、ファクトリー関数、バリアントパターンの詳細
  • references/performance.md — フィクスチャのオーバーヘッド、ワーカーとシャードの選択、経過時間、リソース使用量の測定

コメント