mcpField
P
playwright-testing

playwright-testing

github.com/mhagreliusテスト・品質36 回インストール
リポジトリ

playwright-testing について

SPA 向けの Playwright テストを信頼性・速度・保守性を意識して作成する Claude Code スキルです。ユーザーに見える動作とセマンティックなロケーターを重視します。

主な機能

  1. 01ユーザー中心のロケーター指針
  2. 02条件ベースの待機と再試行アサーション
  3. 03テスト構成、フィクスチャ、ページオブジェクト
  4. 04パフォーマンスと並列実行の指針
  5. 05デバッグと CI のチェックリスト

活用シーン

  1. 01ユーザー操作と結果に沿ったテスト作成
  2. 02任意の待機によるテストの不安定さの低減
  3. 03再利用可能なセットアップによるテスト構成
  4. 04ローカルや CI でのテスト失敗の調査

インストール

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

$npx skills add https://github.com/mhagrelius/dotfiles --skill playwright-testing

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

同梱ファイル

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

reference/
ci.md
debugging.md
performance.md
structure.md

詳細

Playwright テスト

SPA 向けの信頼性が高く、高速で、保守しやすい Playwright テストを作成します。

目次

基本原則: ユーザーが見て、操作するものをテストします。ユーザーがロールやテキストで要素を見つけられないなら、テストでも見つけられるべきではありません。

品質の段階: 信頼性(不安定なテストがない) → 高速(並列実行、待機を最小限に) → 保守性(リファクタリング後も動作する)

方針: デフォルトではユーザー中心のロケーターを使います。実装の詳細(test-id、CSS セレクター)は、最初の選択肢ではなく、代替手段として使います。

プロセス

  1. 特定: どのユーザー行動をテストしますか?
  2. 特定: ユーザーが見つける方法(ロール、ラベル、テキスト)で要素を探します
  3. 操作: ユーザーの操作を実行します(クリック、入力、移動)
  4. 検証: 内部状態ではなく、目に見える結果を確認します
  5. 安定化: 非同期処理に対応し、適切な待機を追加します

危険信号 - 中止してください

  • page.locator('.btn-primary') - CSS クラスセレクター
  • page.waitForTimeout(1000) - 根拠のない待機
  • 最初の選択肢としての page.locator('[data-testid="x"]')
  • ユーザーが得る結果ではなく、コンポーネントの内部をテストしている
  • ページオブジェクトやフィクスチャがない長いテストファイル

ロケーターの優先順位

digraph locator_priority {
    rankdir=TB;
    node [shape=box];

    "Need to find element" [shape=diamond];
    "Has accessible role?" [shape=diamond];
    "Has label/placeholder?" [shape=diamond];
    "Has visible text?" [shape=diamond];
    "getByRole" [style=filled fillcolor=lightgreen];
    "getByLabel/getByPlaceholder" [style=filled fillcolor=lightgreen];
    "getByText" [style=filled fillcolor=lightgreen];
    "getByTestId" [style=filled fillcolor=lightyellow];

    "Need to find element" -> "Has accessible role?";
    "Has accessible role?" -> "getByRole" [label="yes"];
    "Has accessible role?" -> "Has label/placeholder?" [label="no"];
    "Has label/placeholder?" -> "getByLabel/getByPlaceholder" [label="yes"];
    "Has label/placeholder?" -> "Has visible text?" [label="no"];
    "Has visible text?" -> "getByText" [label="yes"];
    "Has visible text?" -> "getByTestId" [label="no (last resort)"];
}
優先順位ロケーター使用する場面例
第1位getByRoleボタン、リンク、入力欄、見出し、リストgetByRole('button', { name: 'Submit' })
第2位getByLabelラベル付きのフォーム入力欄getByLabel('Email address')
第3位getByPlaceholderプレースホルダーテキスト付きの入力欄getByPlaceholder('Search...')
第4位getByText静的なテキスト内容、段落getByText('Welcome back')
第5位getByAltText画像getByAltText('Company logo')
最後getByTestId動的なコンテンツ、意味的な役割がないものgetByTestId('total-price')
ロールの例
// Buttons
page.getByRole('button', { name: 'Save changes' })
page.getByRole('button', { name: /submit/i })  // regex for flexibility

// Links
page.getByRole('link', { name: 'View profile' })

// Form inputs
page.getByRole('textbox', { name: 'Username' })
page.getByRole('checkbox', { name: 'Remember me' })
page.getByRole('combobox', { name: 'Country' })

// Structure
page.getByRole('heading', { name: 'Dashboard', level: 1 })
page.getByRole('list').getByRole('listitem')
page.getByRole('dialog', { name: 'Confirm deletion' })

// Tables
page.getByRole('table').getByRole('row', { name: /john/i })
複数の一致候補を区別する
// Specify which match you want
await page.getByRole('button', { name: 'Delete' }).first().click();
await page.getByRole('listitem').last().click();
await page.getByRole('row').nth(2).click();  // 0-indexed

// Filter by content or child elements
await page.getByRole('listitem').filter({ hasText: 'John' }).click();
await page.getByRole('listitem').filter({
    has: page.getByRole('button', { name: 'Edit' })
}).click();

// Chain locators to scope
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();
ロケーターのアンチパターン
悪い例理由良い例
.locator('.submit-btn')クラス名の変更で壊れますgetByRole('button', { name: 'Submit' })
.locator('#email-input')実装に依存していますgetByLabel('Email')
.locator('div > span:nth-child(2)')非常に壊れやすいですgetByText('...') を使うか、test-id を追加します
あらゆる場所で getByTestId を使うアクセシビリティの不具合を見逃しますまず意味的なロケーターを使います
複数の一致候補がある、スコープ未指定のロケーター不安定で、誤った要素をクリックする可能性があります.first()、.filter() を使うか、親要素でスコープを指定します

待機と非同期処理

Playwright はほとんどの操作で自動的に待機します。 明確な理由がない限り、手動の待機を追加しないでください。

digraph waiting {
    rankdir=TB;
    node [shape=box];

    "What are you waiting for?" [shape=diamond];
    "Element to appear" [shape=diamond];
    "Auto-wait (built-in)" [style=filled fillcolor=lightgreen];
    "expect + toBeVisible" [style=filled fillcolor=lightgreen];
    "waitForResponse" [style=filled fillcolor=lightyellow];
    "waitForURL" [style=filled fillcolor=lightyellow];
    "NEVER waitForTimeout" [style=filled fillcolor=lightpink];

    "What are you waiting for?" -> "Auto-wait (built-in)" [label="clicking/filling"];
    "What are you waiting for?" -> "Element to appear" [label="element"];
    "What are you waiting for?" -> "waitForResponse" [label="API call"];
    "What are you waiting for?" -> "waitForURL" [label="navigation"];
    "Element to appear" -> "expect + toBeVisible" [label="use assertion"];
    "Element to appear" -> "NEVER waitForTimeout" [label="don't guess"];
}
組み込みの自動待機

次の操作では自動的に待機するため、手動で待つ必要はありません。

// All of these wait automatically for element to be actionable
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('checkbox').check();
await page.getByRole('combobox').selectOption('US');
明示的な待機(必要な場合)
// Wait for element state (prefer assertions)
await expect(page.getByText('Success')).toBeVisible();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('list')).not.toBeEmpty();

// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForURL(url => url.searchParams.has('token'));

// Wait for API response (useful for loading states)
await page.getByRole('button', { name: 'Load data' }).click();
await page.waitForResponse(resp =>
    resp.url().includes('/api/data') && resp.status() === 200
);

// Wait for network idle (use sparingly - can be slow)
await page.waitForLoadState('networkidle');

// Wait for specific request to complete before asserting
const responsePromise = page.waitForResponse('/api/users');
await page.getByRole('button', { name: 'Refresh' }).click();
await responsePromise;
await expect(page.getByRole('list')).toContainText('John');

// Promise.all pattern - click and wait simultaneously
await Promise.all([
    page.waitForResponse(resp => resp.url().includes('/api/data')),
    page.getByRole('button', { name: 'Submit' }).click()
]);
待機のアンチパターン
悪い例理由良い例
waitForTimeout(2000)根拠がなく、遅い上に不安定です特定の条件を待ちます
ループ内の waitForTimeout(100)手動でポーリングしています自動再試行される expect を使います
あらゆる場所で waitForLoadState('networkidle') を使うポーリングがあると遅く、信頼性に欠けます特定のレスポンスを待ちます
待機せずにすぐアサーションする競合状態が発生しますexpect がアサーションを自動再試行します
アサーションの自動再試行

Playwright のアサーションはタイムアウトまで自動再試行します。手動で待機する代わりに、こちらを使ってください。

// BAD: manual wait then check
await page.waitForTimeout(1000);
const text = await page.getByTestId('status').textContent();
expect(text).toBe('Complete');

// GOOD: auto-retrying assertion
await expect(page.getByText('Complete')).toBeVisible();
ソフトアサーション

アサーションに失敗した後もテストを続けるには、expect.soft() を使います(複数の失敗を収集します)。

test('validates all form fields', async ({ page }) => {
    await page.goto('/profile');

    // Soft assertions don't stop the test - useful for checking multiple things
    await expect.soft(page.getByLabel('Name')).toHaveValue('John');
    await expect.soft(page.getByLabel('Email')).toHaveValue('[email protected]');
    await expect.soft(page.getByLabel('Phone')).toHaveValue('555-1234');

    // Test continues even if some assertions fail
    // All failures reported at end
});
オーバーレイとポップアップへの対応

オーバーレイがテスト操作を妨げる可能性がある場合は、addLocatorHandler を使います。

// Setup handler for cookie consent popup
await page.addLocatorHandler(
    page.getByRole('dialog', { name: 'Cookie consent' }),
    async () => {
        await page.getByRole('button', { name: 'Accept' }).click();
    }
);

// Now write test normally - handler auto-dismisses popup if it appears
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Settings' }).click();

テストの構成

ページオブジェクト、フィクスチャ、テストの構成については、reference/structure.md を参照してください。

クイックヒント:

  • ページオブジェクトにはアサーションではなく、操作をカプセル化します
  • フィクスチャで共通のセットアップ/ティアダウンを処理します
  • 1つのテストにつき1つの動作を検証します
  • 関連するテストのグループ化には test.describe を使用します

パフォーマンス

並列実行、認証の最適化、API のショートカットについては、reference/performance.md を参照してください。

クイックヒント:

  • storageState を使って認証状態を再利用します
  • テストデータは UI ではなく API 経由で作成します
  • fullyParallel: true を使用します
  • waitForLoadState('networkidle') は避けます

デバッグ

デバッグモード、トレース、一般的なシナリオについては、reference/debugging.md を参照してください。

クイックヒント:

  • ローカルでのデバッグには npx playwright test --debug を使用します
  • CI の失敗時はトレースを確認します:npx playwright show-trace trace.zip
  • await page.pause() を使って処理を停止し、確認します

CI の設定

GitHub Actions、シャーディング、CI 固有の設定については、reference/ci.md を参照してください。

クイックヒント:

  • forbidOnly: !!process.env.CI を設定すると、CI での test.only を防げます
  • CI で一時的な失敗に対応するため、retries: 2 を設定します
  • 失敗のデバッグ用にアーティファクトをアップロードします

品質チェックリスト

テストを確定する前に、該当するチェック項目ごとに TodoWrite アイテムを作成してください。

レイヤー 1:信頼性(不安定なテストがない)
  • waitForTimeout() の呼び出しがなく、条件に基づく待機を使用している
  • アサーションでは手動チェックではなく、自動再試行機能のある expect() を使用している
  • テストが独立しており、テスト間で状態を共有していない
  • networkidle ではなく、特定の API レスポンスを待機している
  • ロケーターが十分に具体的で、単一の要素に一致する
  • テストが安定して成功する(コミット前にローカルで 5 回実行する)
レイヤー 2:高速性
  • storageState を使って認証を再利用している
  • テストデータを UI ではなく API 経由で作成している
  • テストが並列実行される(fullyParallel: true)
  • 不要な waitForLoadState('networkidle') がない
  • 時間のかかるセットアップをテストごとに繰り返さず、フィクスチャで処理している
  • 大規模なテストスイート向けにシャーディングを設定している
レイヤー 3:保守性
  • ロケーターで CSS セレクターではなくロール/ラベルを使用している
  • ページオブジェクトに操作をカプセル化している
  • テスト名にユーザーの操作と期待される結果を記述している
  • 1つのテストにつき1つの動作を検証し、複数のことをテストしていない
  • フィクスチャで共通のセットアップ/ティアダウンを処理している
  • マジック文字列を使用せず、繰り返し使う値には定数を使用している

よく使うパターンのリファレンス

必要なものパターン
認証済みユーザーstorageState フィクスチャ
テストデータのセットアップbeforeEach またはフィクスチャでの API 呼び出し
データの読み込みを待機waitForResponse('/api/...')
クリックしてレスポンスを待機Promise.all([waitForResponse(...), click()])
類似した複数のテストtest.describe + パラメーター化されたデータ
時間のかかる操作特定のテストのタイムアウトを延長
モーダル/ダイアログgetByRole('dialog') を使用してから、その範囲内に限定
ドロップダウンの選択getByRole('combobox').selectOption()
ファイルのアップロードファイル入力で setInputFiles() を使用
ホバーメニューlocator.hover() を実行してから、表示された項目をクリック
ドラッグ&ドロップlocator.dragTo(target)
iframeframeLocator() を使用してから、その範囲内に限定
新しいタブ/ウィンドウpage.waitForEvent('popup')
複数の一致項目.first()、.last()、.nth(n)、.filter()
複数の項目を確認ブロックしないアサーションには expect.soft() を使用
ポップアップ/オーバーレイを閉じるaddLocatorHandler()

data-testid を追加するタイミング

次のような場合は、最後の手段として data-testid を使用します。

  • 要素にセマンティックロールがない(装飾用コンテナー)
  • 安定したテキストがない動的コンテンツ(生成 ID、価格)
  • 位置が重要な、同一の要素が複数ある場合
  • アクセシブルなマークアップがないサードパーティ製コンポーネント
// Acceptable: price that changes dynamically
<span data-testid="cart-total">{formatCurrency(total)}</span>
page.getByTestId('cart-total')

// Still prefer scoping with semantic locators
page.getByRole('region', { name: 'Cart' }).getByTestId('total')

コマンドのクイックリファレンス

# Run all tests
npx playwright test

# Run specific file
npx playwright test login.spec.ts

# Run tests matching name
npx playwright test -g "login"

# Run in headed mode
npx playwright test --headed

# Debug mode with inspector
npx playwright test --debug

# Update snapshots
npx playwright test --update-snapshots

# Generate test from recording
npx playwright codegen localhost:3000

# Show last HTML report
npx playwright show-report

コメント