web-testing-playwright-e2e について
実ブラウザーでのユーザーフローを検証する、Playwright のエンドツーエンドテスト向けガイドです。堅牢なロケーター、再試行アサーション、フィクスチャ、ネットワークモック、ビジュアル検証を扱います。
主な機能
- 01セマンティックロケーターと再試行アサーション
- 02並列テストを分離するフィクスチャ
- 03ネットワークのインターセプトとレスポンスモック
- 04スクリーンショットと ARIA スナップショットの検証
- 05クロック制御とページオブジェクト
活用シーン
- 01複数ページにまたがるユーザーフローのテスト
- 02安定した UI テストのための API モック
- 03ビジュアルまたはアクセシビリティ出力の確認
- 04並列テスト間でのセットアップ共有
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-testing-playwright-e2eインストール後、Claude Code で/web-testing-playwright-e2eを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/advanced-features.mdapi-mocking.mdcore.mdfixtures.mdpage-objects.mdvisual-testing.md詳細
Playwright E2E テストのパターン
クイックガイド: Playwright は、実際のブラウザーを操作して、ユーザーの一連の操作を最初から最後まで実行します。ロケーターは使用するたびに再解決される遅延クエリーであり、
expect(locator)のマッチャーは成功するまで再試行するため、適切なテストではスリープも古いハンドルへの対処も不要です。各テストには専用のブラウザーコンテキストが割り当てられます。テストは並列ワーカーで実行されるため、共通のセットアップはモジュールレベルの変数ではなく、フィクスチャを通じて提供します。
詳細なリソース:
- examples/core.md — ユーザーフロー、ページオブジェクト、ネットワークのモック、設定、認証フィクスチャ
- examples/page-objects.md — ベースページの継承と階層構造
- examples/api-mocking.md — 実際のレスポンスのインターセプトと変更
- examples/visual-testing.md — スクリーンショットの比較、マスキング、状態
- examples/fixtures.md — フィクスチャの合成、シードデータ、ストレージ状態
- examples/advanced-features.md — Clock API、ARIA スナップショット、ワーカースコープのフィクスチャ、ポーリングアサーション、アクセシビリティアサーション
- reference.md — ロケーターとアサーションの一覧、設定と CLI の参照、バージョン変更、トラブルシューティング
<critical_requirements>
Playwright コードを書く前に
まず getByRole() を使います。 ユーザーやスクリーンリーダーが要素を見つける方法で要素を選択するため、マークアップが変更されても機能し、同時にアクセシブルネームも検証できます。
expect(locator) のマッチャーでアサーションします。 条件が満たされるかタイムアウトするまで再試行するため、そもそも待機する理由がなくなります。固定時間の待機では、短すぎるか遅すぎるかのどちらかです。
リクエストを発生させるナビゲーションの前にルートを登録します。 ルートはコンテキストに紐づき、設定後に行われるリクエストにのみ適用されるため、page.goto() の後に page.route() を呼び出しても、インターセプトするはずだったリクエストは捕捉できません。
フィクスチャを使って、各テストに独自の状態を持たせます。 ワーカーは並列に実行され、モジュールレベルの変数は同じワーカー内で共有されますが、次のワーカーには引き継がれません。そのため、フィクスチャに記述したセットアップだけが、どちらでも同じように動作します。
</critical_requirements>
自動検出: Playwright、@playwright/test、playwright.config、page.goto、test.describe、 test.extend、expect(page)、getByRole、getByLabel、getByTestId、toBeVisible、toHaveURL、 toHaveScreenshot、toMatchAriaSnapshot、page.route、route.fulfill、page.clock、toPass、devices
対象範囲:
- ページやオリジンをまたぐ、実際のブラウザーでのユーザージャーニー
- ロケーター戦略、Web ファーストアサーション、ソフトアサーション
- テストのためのネットワークトラフィックのインターセプト、スタブ化、変更
- スクリーンショットの比較と ARIA ツリーのアサーション
- フィクスチャ、ワーカーのスコープ、並列実行、テスト設定
対象外:
- 純粋関数のユニットレベルのチェック — 入力と出力のアサーションにブラウザーを使っても意味はありません
- 単一コンポーネントのマウントと props の操作 — 粒度が異なるため、コンポーネントテストを担う仕組みに委ねます
- アプリケーション自体のアクセシビリティ改善 — このスキルはアクセシブルツリーをアサーションしますが、そのツリーがどうあるべきかは決めません
- スイート全体にわたるスクリーンショットのベースライン管理とレビューのワークフロー
ロケーターはクエリであり、ハンドルではありません。page.getByRole("button") は使用されるまで何も解決せず、使用するたびに再解決されます。そのため、古くなった要素を警戒する必要も、早めに取得しておく理由もありません。アサーションも同じ仕組みを受け継いでいます。expect(locator).toBeVisible() はタイムアウトまでポーリングします。
その他のことも同様です。スリープ、CSS セレクター、共有変数は、それぞれこの仕組みが最初から提供しているものを無駄にしてしまいます。
<decision_framework>
セットアップにはフックとフィクスチャのどちらを使うか:
- 1 つの
describeブロックだけで使う →beforeEach。セットアップを、利用する場所に置けます。 - 複数ファイルで使う、またはティアダウンが必要 → フィクスチャ。セットアップとティアダウンの両方をまとめ、組み合わせて利用できます。
- 実行全体で一度だけ必要 → 設定の
globalSetup。beforeAllは実行全体で一度ではなく、ワーカーごとに一度実行されます。 - コストが高く、ワーカー内で共有しても安全 → ワーカースコープのフィクスチャ。
ネットワークをスタブ化するか、実際にアクセスするか:
- サードパーティ API → スタブ化します。レート制限、コスト、サービス停止はテストの対象ではありません。
- 独自の API で、エラーパスをテストする → スタブ化します。一部のステータスは、必要に応じて発生させられないためです。
- 独自の API で、統合が正しいことをテストする → テスト用データベースに対して実際に通信させ、機能ごとにこの種のテストを 1 つ維持します。
- 独自の API で、UI の動作だけをテストする → 速度と決定性のためにスタブ化します。
</decision_framework>
基本的なパターン
パターン 1: テストの構成
test.describe でグループ化し、グループ内の各テストで繰り返されるナビゲーションを beforeEach で処理します。
test.describe("Login", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/login");
});
test("successful login lands on the dashboard", async ({ page }) => {
await page.getByLabel(/email/i).fill(VALID_EMAIL);
await page.getByRole("button", { name: /sign in/i }).click();
await expect(page).toHaveURL("/dashboard");
});
});
コード全文: examples/core.md
パターン 2: ページオブジェクトモデル
コンストラクターにロケーターを置き、クラスにドメインメソッドを定義します。同じページを複数のテストが扱うようになったら有効です。1 回限りのテストでは、ロケーターをその場で記述するほうが明快です。
export class LoginPage {
readonly emailInput: Locator;
readonly signInButton: Locator;
constructor(page: Page) {
this.emailInput = page.getByLabel(/email/i);
this.signInButton = page.getByRole("button", { name: /sign in/i });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
// ...
}
}
コード全文: examples/core.md — 継承については examples/page-objects.md
パターン 3:ロケーター戦略
要素の「種類」を指定するクエリを優先し、ロールもラベルもない場合にのみ、テスト ID にフォールバックします。
await page.getByRole("button", { name: /submit/i });
await page.getByLabel(/email address/i);
await page.getByTestId("user-avatar"); // no semantic role available
// Narrow within a list instead of writing a fragile selector
await page
.getByRole("listitem")
.filter({ hasText: "Product A" })
.getByRole("button", { name: /add to cart/i })
.click();
// Exclude and combine (v1.33+)
page.getByRole("listitem").filter({ hasNot: page.getByText("Out of stock") });
page.getByRole("button").and(page.getByTitle("Subscribe"));
優先順位表とロールの対応表:reference.md
パターン 4:Web ファーストのアサーション
expect(locator) のすべてのマッチャーは、否定形も含めて再試行されます。
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByRole("progressbar")).not.toBeVisible();
// Soft assertions collect every failure in one run instead of stopping at the first
await expect.soft(page.getByTestId("avatar")).toBeVisible();
await expect.soft(page.getByText("Premium")).toBeVisible();
アサーションの全表:reference.md
パターン 5:ネットワークのモック
page.route() は URL パターンでインターセプトし、route.fulfill() は応答を返し、route.abort() はリクエストを失敗させます。
await page.route("**/api/users", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ id: "user-123", name: "Jane Doe" }),
}),
);
await page.route("**/api/users", (route) => route.abort("failed")); // network failure
コード全体:examples/core.md — examples/api-mocking.md で実際のレスポンスを変更する方法
パターン 6:ビジュアルリグレッション
toHaveScreenshot() は初回実行時にベースラインを保存し、その後は比較します。自然に変化する部分はマスクします。
await expect(page).toHaveScreenshot("dashboard.png", {
mask: [page.getByTestId("current-time")],
animations: "disabled",
});
コード全体:examples/visual-testing.md
パターン 7:カスタムフィクスチャ
base.extend を使うと、ページオブジェクトやセットアップ用のフィクスチャを追加できます。auto フィクスチャは、テストで名前を指定しなくても実行されます。
export const test = base.extend<{ loginPage: LoginPage; authenticated: void }>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticated: [
async ({ context }, use) => {
await context.addCookies([SESSION_COOKIE]);
await use();
await context.clearCookies();
},
{ auto: true },
],
});
コード全体:examples/core.md — examples/fixtures.md の組み合わせとシード設定
パターン 8:時計の制御(v1.45+)
時間を待つのではなく、時間を進めます。clock.install() は、ほかのすべての時計呼び出しより前に実行します。
await page.clock.install({ time: new Date("2024-02-02T08:00:00") });
await page.goto("/dashboard");
await page.clock.fastForward("25:00");
await expect(page.getByText(/session expires/i)).toBeVisible();
コード全体:examples/advanced-features.md
パターン 9:ARIA スナップショット(v1.49+)
アクセシビリティツリー全体を、読みやすいひとつのブロックでアサートします。
await expect(page.getByRole("navigation")).toMatchAriaSnapshot(`
- navigation:
- link "Home"
- link "Products"
`);
コード全体:examples/advanced-features.md
<red_flags>
注意点
実行時に問題が発生します:
- アサーションの前に
page.waitForTimeout()を使う — 待機時間が短すぎると不安定になり、長すぎると遅くなります。再試行するマッチャーがすでに待機を処理します。 - リクエストを発生させるナビゲーションの後に
page.route()を登録する — ルートが一致しません。gotoの前、beforeEach、またはフィクスチャ内で設定します。 - CSS セレクターや ID セレクター(
.btn-primary、#submit-btn)を使う — スタイルやマークアップの変更で変わりますが、ロールやラベルのクエリは変わりません。 - テスト間で共有するモジュールレベルの変数 — 次のワーカーには存在せず、同一ワーカー内でも競合状態が発生します。フィクスチャを使います。
page.route()で?や[]を使うグロブパターン — v1.52 以降はサポートされていません。正規表現を使います。route.continue()でCookieヘッダーを上書きする — v1.52 以降は拒否されます。context.addCookies()を使います。_react/_vueセレクターエンジン — v1.58 で削除されました。ロールクエリまたはテスト ID を使います。
予期しない動作:
beforeAllは実行ごとに 1 回ではなく、ワーカーごとに 1 回実行されます — 1 回限りのセットアップはglobalSetupで行います。toBeVisible()は表示されるまで待機しますが、toBeAttached()は DOM のみを確認します。非表示の要素でも後者は満たされます。- v1.50 以降、編集できない要素に対する
toBeEditable()は false を返さず、例外をスローします。 - スクリーンショットは OS、ブラウザのビルド、フォントのレンダリングによって異なります — CI と同じ環境で生成されたベースラインとのみ比較します。
- 動的コンテンツ(タイムスタンプ、広告、アバター)をマスクせずに撮影すると、レイアウトではなくコンテンツの違いで失敗します。
- アサーションが継承するのはテストのタイムアウトではなく、設定の
expect.timeoutです — ページの読み込みが遅い場合、先にアサーションが失敗します。
</red_flags>