mcpField
P
playwright-test

playwright-test

github.com/mizchiテスト・品質41691 回インストール2 週間前に更新
リポジトリ

playwright-test について

playwright-testは、Playwrightのテストスイート作成と実行を支援するスキルです。安定性、ネットワークのモック、認証状態の再利用、CI設定に加え、テストの分割とレポート統合も扱います。

主な機能

  1. 01Playwrightテストプロジェクトの設定
  2. 02再試行するアサーションによる不安定さの軽減
  3. 03APIモックとHARの記録・再生
  4. 04認証状態の再利用
  5. 05CI実行の分割とレポート統合

活用シーン

  1. 01E2Eブラウザーテストの作成
  2. 02CIで不安定なテストの調査
  3. 03テスト用APIレスポンスのモック
  4. 04複数ブラウザーと分割による並列テスト

インストール

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

$npx skills add https://github.com/mizchi/skills --skill playwright-test

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

同梱ファイルREADME.mdSKILL-ja.md

詳細

Playwright Test

設定テンプレート

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,       // Forbid .only on CI
  retries: process.env.CI ? 2 : 0,    // Retry only on CI
  workers: process.env.CI ? 1 : undefined,
  reporter: process.env.CI
    ? [['html'], ['github']]           // CI: HTML + GitHub annotations
    : [['html']],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',          // Record trace only on retry
    screenshot: 'only-on-failure',
    video: 'on-first-retry',
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'], storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

GitHub Actions

Linux フォント(CI で必須)

Ubuntu/Debian には、デフォルトで日本語/CJK フォントが含まれていません。そのため、スクリーンショットで文字化けやレイアウト崩れが発生します。

      - name: Install fonts
        run: |
          sudo apt-get update
          sudo apt-get install -y fonts-noto-cjk fonts-noto-color-emoji

--with-deps オプションを指定すると、Playwright が必要なシステム依存関係をインストールしますが、フォントは含まれません。

基本(シャードなし)
name: E2E Tests
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with: { node-version: 24 }
      - run: npm ci
      - run: npx playwright install chromium --with-deps
      - run: sudo apt-get install -y fonts-noto-cjk fonts-noto-color-emoji
      - run: npx playwright test
      - uses: actions/upload-artifact@v7
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14
シャード実行(並列分割)

テストを複数のジョブに分割して、実行を高速化します。

name: E2E Tests (Sharded)
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with: { node-version: 24 }
      - run: npm ci
      - run: npx playwright install chromium --with-deps
      - run: npx playwright test --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v7
        if: ${{ !cancelled() }}
        with:
          name: blob-report-${{ strategy.job-index }}
          path: blob-report/
          retention-days: 1

  merge-reports:
    if: ${{ !cancelled() }}
    needs: e2e
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with: { node-version: 24 }
      - run: npm ci
      - uses: actions/download-artifact@v8
        with:
          path: all-blob-reports
          pattern: blob-report-*
          merge-multiple: true
      - run: npx playwright merge-reports --reporter html ./all-blob-reports
      - uses: actions/upload-artifact@v7
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

シャーディング用に blob reporter を設定に追加します。

reporter: process.env.CI
  ? [['blob'], ['github']]    // For shards: emit blob
  : [['html']],
シャード × ブラウザーのマトリックス(複数のブラウザーを並列実行)

複数のブラウザー × 複数のシャードを同時に実行するには、2 つの matrix 軸を使用します。

jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with: { node-version: 24 }
      - run: npm ci
      - run: npx playwright install ${{ matrix.browser }} --with-deps
      - run: sudo apt-get install -y fonts-noto-cjk fonts-noto-color-emoji
      - run: npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v7
        if: ${{ !cancelled() }}
        with:
          name: blob-${{ matrix.browser }}-${{ strategy.job-index }}
          path: blob-report/
          retention-days: 1

マージジョブはすべての blob を 1 つの HTML にまとめます。ブラウザー × シャードごとにアーティファクト名が異なるため(blob-chromium-0 / blob-firefox-1 ...)、パターンには blob-* を使用します。

  merge-reports:
    if: ${{ !cancelled() }}
    needs: e2e
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with: { node-version: 24 }
      - run: npm ci
      - uses: actions/download-artifact@v8
        with:
          path: all-blob-reports
          pattern: blob-*             # Collect across browsers
          merge-multiple: true
      - run: npx playwright merge-reports --reporter html ./all-blob-reports
      - uses: actions/upload-artifact@v7
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

ジョブ数は browsers x shards(3 x 4 = 12 jobs)になるため、組み合わせの急増に注意してください。fail-fast: false を設定すると、1 つのジョブが失敗しても他のジョブは実行を継続します。workers: 1 はシャード内の並列処理数を制御します(シャードはすでに並列実行されるため、CI では 1 で問題ありません。ローカル開発で自動設定にする場合は undefined を使用してください)。

リトライ戦略
// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,  // Up to 2 retries on CI

  use: {
    trace: 'on-first-retry',         // Capture trace on retry
    screenshot: 'only-on-failure',
    video: 'on-first-retry',
  },
});

テストごとのリトライ:

// Change retry count for a specific test only
test('flaky external API test', async ({ page }) => {
  test.info().annotations.push({ type: 'retry', description: 'External dependency' });
  // ...
});

// Per describe
test.describe('payment flow', () => {
  test.describe.configure({ retries: 3 });
  // ...
});
トレース/スクリーンショット/動画の選択
設定キャプチャするタイミング目的/サイズ
'on'すべてのテストデバッグ専用です。CI では推奨しません(アーティファクトが膨大になります)
'on-first-retry'リトライ時のみCI で推奨されるデフォルト設定です。不安定なテストの調査には十分で、サイズを最小限に抑えられます
'retain-on-failure'失敗時に保持初回実行での失敗を検出したい場合に使用します。リトライを無効にしている場合に便利です
'off'キャプチャしません大規模なテストスイートでアーティファクトの容量を節約します

選択基準:リトライを 1 回以上設定する場合は、on-first-retry を使用します(初回失敗後、リトライ時にトレースを記録するため、サイズを半分に抑えられます)。リトライを行わない場合、または 1 回の失敗をすぐに調査したい場合は、retain-on-failure を使用します。「失敗時のみ」と指定されている場合は、通常 retain-on-failure を意味します。

ブラウザーごとの条件付きテスト

ブラウザーごとにテストをスキップする、または差異を処理する方法:

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

// Skip one test based on browser condition
test('webkit only feature', async ({ page, browserName }) => {
  test.skip(browserName !== 'webkit', 'Safari-specific behavior');
  // ...
});

// Skip a describe block
test.describe('chromium-only suite', () => {
  test.skip(({ browserName }) => browserName !== 'chromium', 'Uses CDP');
  test('uses cdp api', async ({ page }) => { /* ... */ });
});

// Change retry / mode per describe
test.describe('payment flow', () => {
  test.describe.configure({ retries: 3, mode: 'serial' });
  test('step 1', async ({ page }) => { /* ... */ });
  test('step 2', async ({ page }) => { /* inherits state from step 1 */ });
});

browserName は chromium / firefox / webkit のいずれか 1 つの値を取ります。タグベースの CLI フィルタリング(--grep @chromium-only)と組み合わせると、CI 設定の柔軟性が高まります。

不安定なテストの検出ワークフロー

CI で不安定なテストを検出し、PR にコメントを投稿したり統計を集計したりするには、JSON reporter を追加します。

reporter: process.env.CI
  ? [['blob'], ['github'], ['json', { outputFile: 'test-results/results.json' }]]
  : [['html']],

results.json には各テストの status / retries / duration が含まれています。後続の処理で抽出します。

# retry > 0 and final pass = flaky candidate
jq '.suites[].specs[] | select(.tests[].results | length > 1 and .[-1].status == "passed")' \
  test-results/results.json

一般的な方法は、GitHub Actions でアーティファクトとしてアップロードし、別のジョブで集計して、過去 N 回の実行における傾向を追跡することです。@playwright/test 1.40 以降では、expect.configure({ flaky: true }) などの組み込みサポートも利用できます。

ルール:固定時間の待機を使用しない

ルール:固定時間の待機を使用しない

Playwright は要素が操作可能になるまで自動的に待機します。waitForTimeout() の使用は禁止です。

// BAD
await page.waitForTimeout(3000);
await page.click('#submit');

// GOOD: automatic waiting
await page.getByRole('button', { name: 'Submit' }).click();

// GOOD: web-first assertion (auto-retries)
await expect(page.getByText('Success')).toBeVisible();

// BAD: no retry
expect(await page.getByText('Success').isVisible()).toBe(true);

一度だけ読み取る API は自動リトライしません:

形式動作
expect(locator).toBeVisible() / toHaveText(...) など自動リトライ(デフォルトは 5s)します。これらを使用してください
await locator.isVisible() / innerText() / count() / textContent()一度だけ読み取り、リトライしません。テストが不安定になる大きな原因です

テストが不安定な場合は、一度だけ読み取る API を Web ファーストアサーションに置き換えられることがよくあります。

// BAD
const n = await page.locator('.row').count();
expect(n).toBeGreaterThan(0);

// GOOD
await expect(page.locator('.row')).not.toHaveCount(0);

明示的な待機が必要なケース:

await page.waitForURL('**/dashboard');          // After navigation
await page.waitForLoadState('networkidle');      // Heavy initial load
await page.waitForResponse('**/api/data');       // Wait for API response

ネットワークトリガー

アクションを実行する前に Promise を設定します。

const responsePromise = page.waitForResponse('**/api/users');
await page.getByRole('button', { name: 'Save' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);

// Conditional matching
const responsePromise = page.waitForResponse(
  resp => resp.url().includes('/api/users') && resp.request().method() === 'POST'
);

注意点:waitForResponse が永久に待機することがあります:対象の API が呼び出されない場合(初期バンドルにすべてのデータが含まれる SPA、キャッシュヒット時にスキップされる場合など)、タイムアウトまでブロックされます。代替手段の優先順位:

  1. まず、waitForResponse が必要かどうかを判断します(API 呼び出しが副作用の発生タイミングを確定する場合に必要です)
  2. API が呼び出されない場合は、Web ファーストアサーションだけで十分です(await expect(page.getByTestId('result')).toBeVisible())
  3. タイムアウトを制限するには、{ timeout: 5_000 } を渡します
  4. 任意のレスポンスをカウントするには、page.on('response', ...) をリスナーとして使用します(waitFor ではなくイベントを集約します)
API モック

page.goto() より前に page.route() を登録します。

await page.route('**/api/items', route => route.fulfill({
  status: 200,
  contentType: 'application/json',
  body: JSON.stringify({ items: [{ id: 1, name: 'Test' }] }),
}));

// Modify the response
await page.route('**/api/data', async route => {
  const response = await route.fetch();
  const json = await response.json();
  json.debug = true;
  await route.fulfill({ response, json });
});

// Block resources (speed up)
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
HAR によるネットワークの記録/再生

実際の API レスポンスを記録し、テスト中にそのまま再生します。

// Record: generate a HAR file during test execution
test('record HAR', async ({ page }) => {
  await page.routeFromHAR('tests/fixtures/api.har', {
    url: '**/api/**',
    update: true,  // true = record mode, false = replay mode
  });
  await page.goto('/');
  // ...interactions save API responses to the HAR
});

// Replay: return responses from the recorded HAR (no network needed)
test('replay from HAR', async ({ page }) => {
  await page.routeFromHAR('tests/fixtures/api.har', {
    url: '**/api/**',
    update: false,  // replay mode
  });
  await page.goto('/');
  await expect(page.getByText('data from API')).toBeVisible();
});

CLI から HAR を記録します。

npx playwright open --save-har=tests/fixtures/api.har https://example.com
リクエスト/レスポンスのアサーション
// Validate the request body
const requestPromise = page.waitForRequest('**/api/submit');
await page.getByRole('button', { name: 'Submit' }).click();
const request = await requestPromise;
expect(request.method()).toBe('POST');
expect(JSON.parse(request.postData()!)).toEqual({ name: 'test' });

// Validate the response body
const responsePromise = page.waitForResponse('**/api/submit');
await page.getByRole('button', { name: 'Submit' }).click();
const response = await responsePromise;
const body = await response.json();
expect(body.id).toBeDefined();
コンテキストレベルのルーティング

すべてのページに共通のモックを適用するには、context.route() を使用します。

test('context-level mock', async ({ context, page }) => {
  // Applies to every page in the context
  await context.route('**/api/config', route => route.fulfill({
    status: 200,
    json: { featureFlag: true },
  }));
  await page.goto('/');
  const popup = await page.waitForEvent('popup');  // Also applies to new tabs
  await expect(popup.getByText('Feature enabled')).toBeVisible();
});

ドラッグ&ドロップ

シンプルなケース
await page.locator('#source').dragTo(page.locator('#target'));
DnD ライブラリ(react-dnd、dnd-kit、SortableJS)

ポインターイベントベースのライブラリでは、dragTo が動作しないことがよくあります。

async function dragAndDrop(page: Page, source: Locator, target: Locator) {
  const srcBox = (await source.boundingBox())!;
  const tgtBox = (await target.boundingBox())!;

  await page.mouse.move(srcBox.x + srcBox.width / 2, srcBox.y + srcBox.height / 2);
  await page.mouse.down();
  await page.mouse.move(tgtBox.x + tgtBox.width / 2, tgtBox.y + tgtBox.height / 2, { steps: 10 });
  await page.mouse.up();
}
  • { steps: 10 } は、中間の pointermove/dragover イベントを生成します
  • DataTransfer を使用するライブラリでは、page.evaluate() によるイベントの合成が必要になることがあります
  • アニメーションではなく、最終状態(要素の順序/位置)をアサートします

ロケーター

優先順位(上位ほど推奨):

page.getByRole('button', { name: 'Submit' });  // 1. Role-based
page.getByLabel('Email');                        // 2. Label
page.getByText('Welcome');                       // 2. Text
page.getByTestId('nav-menu');                    // 3. Test ID
page.locator('button.btn-primary');              // 4. CSS (avoid)

チェーンとフィルター:

const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: 'Add to cart' }).click();
モーダル/ダイアログの処理

getByRole('dialog') でモーダルにスコープを限定し、その中を検索します。toBeHidden() で閉じたことを確認します。

// Open
await page.getByRole('button', { name: 'New Project' }).click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();

// Interact within scope
await dialog.getByLabel('Name').fill('My Project');
await dialog.getByRole('button', { name: 'Save' }).click();

// Confirm closure (during fade-out animation, toBeHidden auto-retries)
await expect(dialog).toBeHidden();

// Verify the result outside
await expect(
  page.getByRole('list', { name: 'projects' }).getByRole('listitem').filter({ hasText: 'My Project' })
).toBeVisible();

role="alertdialog" は、警告ダイアログ(削除確認など)に対して getByRole('alertdialog') で使用します。

アサーション

Web ファーストアサーションは自動で再試行されます。

await expect(page.getByText('Success')).toBeVisible();
await expect(page.getByRole('listitem')).toHaveCount(3);
await expect(page.getByTestId('status')).toHaveText('Done');
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle(/Home/);

// Soft assertion (test continues even on failure)
await expect.soft(page.getByTestId('count')).toHaveText('5');

認証の再利用

// tests/auth.setup.ts
setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});

認証を必要としないテストでは、test.use({ storageState: { cookies: [], origins: [] } }) を使用します。

ファイル操作

// Upload
await page.getByLabel('Upload').setInputFiles('myfile.pdf');

// From a buffer (no file needed)
await page.getByLabel('Upload').setInputFiles({
  name: 'file.txt', mimeType: 'text/plain', buffer: Buffer.from('content'),
});

// Download
const downloadPromise = page.waitForEvent('download');
await page.getByText('Download').click();
const download = await downloadPromise;
await download.saveAs('/tmp/file.pdf');

ページオブジェクトモデル

シンプルに保ちます。アサーションはテスト側に記述します。

class LoginPage {
  constructor(private page: Page) {}
  readonly email = this.page.getByLabel('Email');
  readonly password = this.page.getByLabel('Password');
  readonly submit = this.page.getByRole('button', { name: 'Sign in' });

  async login(email: string, pass: string) {
    await this.email.fill(email);
    await this.password.fill(pass);
    await this.submit.click();
  }
}

デバッグ

npx playwright test --debug          # Launch Inspector
npx playwright test --ui             # UI mode (time-travel)
npx playwright test --trace on       # Generate trace
npx playwright show-report           # Show report

コード内で await page.pause() を呼び出すと、テストの途中で Inspector が開きます。

コメント