Playwright 完全ガイド【2026年最新】|E2E / コンポーネント / API テスト・Locator・Web-first Assertions・Fixtures・storageState・Trace Viewer・UI Mode・Codegen・Aria Snapshots・CI 統合まで実戦パターンで解説

Playwright は Microsoft 発の E2E テストフレームワークとして、Cypress / Selenium / Puppeteer のシェアを完全に塗り替えた存在です。3 ブラウザ(Chromium / Firefox / WebKit)を 1 API でauto-waiting で Flaky なし並列実行・シャーディング・ビデオ / スクリーンショット / トレース自動取得Codegen / UI Mode / Trace Viewer という最強のデバッグツール——E2E テストが「遅くて壊れやすい」時代の常識を過去のものにしました。

2025〜2026 年に入ってさらに進化しました。Aria Snapshots(アクセシビリティツリー比較)、locator.describe() によるトレース可読性向上、expect().toContainClass()IndexedDB 対応 storageStateBox FixturesCopy Prompt による LLM 連携testProject.workersTimeline in SpeedboardCodegen の toBeVisible 自動生成など、v1.49〜v1.58 で実戦用途の穴がほぼすべて埋まりました

この記事では 2026 年 4 月時点の Playwright を前提に、E2E / コンポーネント / API テストの書き方、ユーザー視点ロケーターの優先順位、Web-first Assertions、Fixtures、storageState でのログイン共有、UI Mode・Trace Viewer・Codegen の使い方、Aria Snapshots と Visual Regression、Parallel 実行・Sharding、Page Object Model、GitHub Actions CI 統合、落とし穴までを実戦コードで網羅します。

スポンサーリンク
  1. 2026 年 4 月時点のバージョン整理
  2. Playwright vs Cypress vs Selenium vs Puppeteer
  3. インストールと初期セットアップ
  4. Locator ── ユーザー視点での要素指定
    1. フィルタリングとチェーン
    2. locator.describe() でトレースを読みやすく(v1.57+)
  5. Web-first Assertions ── auto-waiting が効く expect
  6. テストの基本構造 ── test / describe / hooks
  7. storageState ── ログイン状態を全テストで共有
  8. Fixtures ── test.extend でカスタム依存性注入
    1. Box Fixtures(v1.51+)── 実装詳細をレポートから隠す
  9. Page Object Model ── 巨大アプリでの保守性
  10. Codegen / UI Mode / Trace Viewer ── 三大ツール
    1. Codegen ── 操作を記録してコード生成
    2. UI Mode ── 視覚的にテストを走らせる
    3. Trace Viewer ── 失敗を 100% 再現
    4. Copy Prompt ── LLM デバッグ(v1.51+)
  11. API テスト ── ブラウザを立ち上げずに REST を叩く
  12. コンポーネントテスト
  13. Aria Snapshots と Visual Regression
    1. Aria Snapshot(v1.49+)── アクセシビリティツリー比較
    2. toHaveScreenshot ── ピクセル比較
  14. 並列実行とシャーディング ── CI 時間を桁違いに短縮
  15. GitHub Actions での CI 統合
  16. 落とし穴と注意点
    1. CSS Locator を使い続けてテストが壊れやすい
    2. await 忘れ
    3. 非同期の手動 wait(waitForTimeout)
    4. テスト間で DB を共有
    5. .only / .skip を残してコミット
    6. Webkit の時差や挙動差
    7. storageState の期限切れ
  17. よくある質問
  18. まとめ

2026 年 4 月時点のバージョン整理

リリース 時期 主なハイライト
Playwright 1.49 2024 年末 Aria SnapshotstoMatchAriaSnapshot)導入。アクセシビリティツリー比較
Playwright 1.51 2025 年 IndexedDB 対応 storageState、Box Fixtures、Copy Prompt(LLM プロンプト生成)
Playwright 1.52 2025 年 expect().toContainClass()testProject.workers(プロジェクト単位の並列数)
Playwright 1.55 2025 年 Codegen で toBeVisible アサーションを自動生成
Playwright 1.57 2025 年 locator.describe() でトレース / レポートに説明付与、UI Mode のシステムテーマ対応
Playwright 1.58 2026 年 HTML Report の Speedboard タブに Timeline、コードエディタ内 Ctrl+F 検索、JSON 自動整形
推奨 2026 年 4 月 Playwright 1.58+ 系。新規プロジェクトはこの系列から
なぜ Playwright が勝ったのか:3 ブラウザ対応(Safari/WebKit含む)を 1 API で、② auto-waiting でテスト安定性が段違い、③ Trace Viewer で失敗時の「何が起きたか」を 100% 再現、④ Codegen でブラウザ操作を記録してコード生成、⑤ 並列実行とシャーディングで CI 速度 10 倍。特に ② が競合との決定差で、Cypress 時代の「flaky test の呪い」を過去のものにしました。

Playwright vs Cypress vs Selenium vs Puppeteer

観点 Playwright Cypress Selenium Puppeteer
ブラウザ ◎ 3 種(Chromium/Firefox/WebKit) △ Chromium / Firefox のみ ◎ すべて × Chromium のみ
auto-waiting ◎ 標準 ○ 一部 × 手動 × 手動
並列実行 ◎ ワーカ並列 △ 有償版のみ ○ Hub 経由 ○ 独自実装
API テスト ◎ 組込み △ 一部 × 非対応 △ 一部
Trace Viewer ◎ 完全 △ 限定 × ×
コンポーネントテスト ○ 対応 ◎ 強力 × ×
言語対応 TS/JS/Python/.NET/Java TS/JS のみ 多言語 TS/JS のみ
選定の指針: E2E・クロスブラウザ・Safari 必須なら Playwright が第一選択、開発中のコンポーネント単位 UI テストなら Cypress Component Test(Playwright も対応済み)、レガシーな社内 Web システムなら Selenium、単純なスクレイピング / PDF 生成なら Puppeteer と棲み分けるのが 2026 年の定石です。

インストールと初期セットアップ

新規プロジェクト
# 対話形式で雛形生成
npm init playwright@latest
# or Bun: bun create playwright
# or Deno: deno run -A npm:create-playwright@latest
#
# ? Do you want to use TypeScript or JavaScript? TypeScript
# ? Where to put your end-to-end tests? tests
# ? Add a GitHub Actions workflow? y
# ? Install Playwright browsers? y

# 既存プロジェクトに追加
npm install --save-dev @playwright/test
npx playwright install --with-deps

# バージョン確認
npx playwright --version
playwright.config.ts ── 本番に耐える標準構成
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,        // CI で .only が残っていたら失敗
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [
    ["html", { open: "never" }],
    ["github"],                         // GitHub Actions 用 inline annotation
    ["json", { outputFile: "playwright-report/results.json" }],
  ],

  use: {
    baseURL: process.env.BASE_URL ?? "http://localhost:3000",
    trace: "on-first-retry",           // 失敗リトライ時に trace 保存
    screenshot: "only-on-failure",
    video: "retain-on-failure",
    locale: "ja-JP",
    timezoneId: "Asia/Tokyo",
  },

  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox",  use: { ...devices["Desktop Firefox"] } },
    { name: "webkit",   use: { ...devices["Desktop Safari"] } },
    { name: "mobile",   use: { ...devices["iPhone 15"] } },
  ],

  // 開発サーバーを自動起動(次の URL が返るまで待つ)
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

Locator ── ユーザー視点での要素指定

Playwright のロケーターは「実装詳細(CSS クラス / 構造)」ではなく「ユーザーから見えるもの(役割 / ラベル / テキスト)」を優先します。これにより、スタイル変更や DOM リファクタリングで落ちにくいテストが書けます。

優先度 メソッド 用途
① 最優先 getByRole(role, { name }) ARIA role(button / link / textbox / heading)
getByLabel(text) フォーム要素のラベル
getByPlaceholder(text) プレースホルダ
getByText(text) 可視テキスト
getByAltText(text) alt 属性
getByTitle(text) title 属性
⑦ 最終手段 getByTestId("id") テスト専用 data-testid
✗ 非推奨 page.locator("button.btn-primary") CSS / XPath は詳細結合
Locator の使い分け実例
test("login flow", async ({ page }) => {
  await page.goto("/login");

  // ラベルで入力欄を特定(スタイル変更の影響を受けない)
  await page.getByLabel("メールアドレス").fill("alice@example.com");
  await page.getByLabel("パスワード").fill("secret-pw");

  // ボタンは role + name で
  await page.getByRole("button", { name: "ログイン" }).click();

  // 見出しで画面遷移を検証
  await expect(page.getByRole("heading", { name: "ダッシュボード" })).toBeVisible();
});

フィルタリングとチェーン

filter / nth / and で絞り込む
// 商品リストの中から「Product 2」の「Add to cart」だけを押す
const product = page.getByRole("listitem")
  .filter({ hasText: "Product 2" });
await product.getByRole("button", { name: "Add to cart" }).click();

// 1 つしかないはずだが複数マッチしたら失敗させる
await page.getByRole("button", { name: "送信" }).click();

// 複数マッチが合法なら first() / last() / nth(n)
await page.getByRole("listitem").first().click();
await page.getByRole("listitem").nth(2).click();

// visible: true で可視要素のみにフィルタ(v1.51+)
await page.getByRole("button", { name: "削除" }).filter({ visible: true }).click();

locator.describe() でトレースを読みやすく(v1.57+)

テストヘルパ内でラベル付け
const submitButton = page
  .getByRole("button", { name: "送信" })
  .describe("メインフォームの送信ボタン");

await submitButton.click();
// Trace Viewer / HTML Report 上で「メインフォームの送信ボタン」と表示される

Web-first Assertions ── auto-waiting が効く expect

Playwright の expect(locator).xxx()条件が満たされるまで自動的にリトライします。明示的な waitFor が不要になり、flaky test がほぼ消えます。

代表的な Web-first assertions
// 可視・存在・状態
await expect(banner).toBeVisible();
await expect(modal).toBeHidden();
await expect(submit).toBeEnabled();
await expect(check).toBeChecked();

// テキスト・属性
await expect(h1).toHaveText("ようこそ");
await expect(h1).toContainText("ようこそ");
await expect(link).toHaveAttribute("href", "/home");
await expect(btn).toHaveClass(/primary/);
await expect(btn).toContainClass("primary");          // v1.52+(個別クラス判定が簡潔に)

// 数・URL・タイトル
await expect(page.getByRole("listitem")).toHaveCount(5);
await expect(page).toHaveURL("/dashboard");
await expect(page).toHaveTitle(/ダッシュボード/);

// カスタムタイムアウト(グローバル 5s → 個別 10s)
await expect(slowItem).toBeVisible({ timeout: 10_000 });
expect().not を使った否定アサーションに注意: await expect(elem).not.toBeVisible() はタイムアウト全体分「見えないこと」を待ちます。「最初から存在しない要素」を確認する場合もデフォルト 5 秒待つため、テスト時間が伸びます。明らかに即確認したいケースは { timeout: 1000 } を渡すか、await elem.isHidden() で即時判定にしてください。

テストの基本構造 ── test / describe / hooks

tests/auth.spec.ts
import { test, expect } from "@playwright/test";

test.describe("認証フロー", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
  });

  test.afterEach(async ({ page }, testInfo) => {
    // 失敗時のみスクリーンショットを artifact に添付
    if (testInfo.status !== "passed") {
      await page.screenshot({ path: `fail-${testInfo.title}.png` });
    }
  });

  test("正しい認証情報でログインできる", async ({ page }) => {
    await page.getByRole("link", { name: "ログイン" }).click();
    await page.getByLabel("メール").fill("alice@example.com");
    await page.getByLabel("パスワード").fill("correct-pw");
    await page.getByRole("button", { name: "ログイン" }).click();
    await expect(page).toHaveURL("/dashboard");
    await expect(page.getByText("ようこそ, Alice")).toBeVisible();
  });

  test("誤ったパスワードはエラー表示", async ({ page }) => {
    await page.getByRole("link", { name: "ログイン" }).click();
    await page.getByLabel("メール").fill("alice@example.com");
    await page.getByLabel("パスワード").fill("wrong-pw");
    await page.getByRole("button", { name: "ログイン" }).click();
    await expect(page.getByRole("alert")).toContainText("パスワードが違います");
  });

  // 並列モード切替(ファイル単位で直列にしたい場合)
  test.describe.configure({ mode: "serial" });
});

storageState ── ログイン状態を全テストで共有

毎テストごとにログインすると CI 時間が増えるので、セットアップでログインして storageState を保存 → 本テストは読み込んで再利用するのが定石です。Cookie・LocalStorage・IndexedDB(v1.51+)まで保存されます。

tests/global.setup.ts ── セットアッププロジェクト
import { test as setup, expect } from "@playwright/test";

const authFile = ".auth/user.json";

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("メール").fill(process.env.E2E_EMAIL!);
  await page.getByLabel("パスワード").fill(process.env.E2E_PASSWORD!);
  await page.getByRole("button", { name: "ログイン" }).click();
  await expect(page).toHaveURL("/dashboard");
  // Cookie / LocalStorage / IndexedDB を丸ごとダンプ
  await page.context().storageState({ path: authFile });
});
playwright.config.ts ── setup を dependencies に
export default defineConfig({
  projects: [
    // 1) セットアップ専用プロジェクト
    {
      name: "setup",
      testMatch: /global\.setup\.ts/,
    },
    // 2) 通常のテストは setup 後に走る + storageState を使う
    {
      name: "chromium",
      dependencies: ["setup"],
      use: {
        ...devices["Desktop Chrome"],
        storageState: ".auth/user.json",
      },
    },
  ],
});
IndexedDB を使うアプリでも OK(v1.51+): Firebase Auth・Dexie・Supabase など IndexedDB に認証情報を書き込むアプリでも storageState が正しく再現されます。古い Playwright ではこの部分が落ちていたため、v1.51 以降へ上げる動機として大きいです。

Fixtures ── test.extend でカスタム依存性注入

Fixtures は「テスト関数に何を渡すか」を宣言的に定義する仕組みで、ログイン済み User・API Client・DB クライアントなどを必要なテストにだけ自動注入できます。

fixtures/test-fixtures.ts
import { test as base, expect } from "@playwright/test";

type Fixtures = {
  apiClient: { get: (url: string) => Promise<any> };
  todoPage: import("./todo-page").TodoPage;
};

export const test = base.extend<Fixtures>({
  // 各テストで API 呼び出しできる軽量クライアント
  apiClient: async ({ request }, use) => {
    await use({
      async get(url) {
        const res = await request.get(url);
        return await res.json();
      },
    });
  },

  // Page Object Model を fixture として提供
  todoPage: async ({ page }, use) => {
    const { TodoPage } = await import("./todo-page");
    const todoPage = new TodoPage(page);
    await todoPage.goto();
    await use(todoPage);
  },
});

export { expect };
tests/todo.spec.ts ── fixture を受け取って使う
import { test, expect } from "../fixtures/test-fixtures";

test("TODO を追加できる", async ({ todoPage }) => {
  await todoPage.addTodo("レポートを書く");
  await expect(todoPage.listItem("レポートを書く")).toBeVisible();
});

test("API で残数を確認", async ({ apiClient }) => {
  const data = await apiClient.get("/api/todos/stats");
  expect(data.pending).toBeGreaterThan(0);
});

Box Fixtures(v1.51+)── 実装詳細をレポートから隠す

box: true で内部フィクスチャをレポートに出さない
export const test = base.extend<{ adminClient: unknown }>({
  adminClient: [async ({}, use) => {
    // 中身はテストレポートの step tree に出さない
    await use(createAdminClient());
  }, { box: true, title: "Admin API クライアント" }],
});

Page Object Model ── 巨大アプリでの保守性

pages/todo-page.ts
import type { Locator, Page } from "@playwright/test";

export class TodoPage {
  constructor(private readonly page: Page) {}

  readonly input = this.page.getByPlaceholder("やることを入力");
  readonly list = this.page.getByRole("list", { name: "TODO 一覧" });

  async goto() { await this.page.goto("/todos"); }

  listItem(name: string): Locator {
    return this.list.getByRole("listitem").filter({ hasText: name });
  }

  async addTodo(text: string) {
    await this.input.fill(text);
    await this.input.press("Enter");
  }

  async toggleDone(name: string) {
    await this.listItem(name).getByRole("checkbox").check();
  }

  async deleteTodo(name: string) {
    await this.listItem(name).getByRole("button", { name: "削除" }).click();
  }
}
POM は fixture と組み合わせて: POM 単体だと「テストごとに const page = new TodoPage() を書く」手間が残ります。fixture として提供すれば test("...", async ({ todoPage }) => {}) だけで使えます。大規模プロジェクトでは POM と fixture をセットで設計するのが標準パターンです。

Codegen / UI Mode / Trace Viewer ── 三大ツール

Codegen ── 操作を記録してコード生成

ブラウザ操作を録画して TS コードに
npx playwright codegen http://localhost:3000

# ブラウザと Playwright Inspector が開く
# 画面を操作するだけで対応する TS コードが Inspector に生成される
# v1.55+ はクリック操作のあとに toBeVisible / toHaveText 等を自動提案
# 完了したらコピーしてテストファイルに貼り付け

UI Mode ── 視覚的にテストを走らせる

開発中に最も使うモード
npx playwright test --ui

# 機能:
# - テストツリーから選んで実行
# - タイムトラベル(過去の任意のステップに戻れる)
# - Locator Pick(画面からクリックでロケーター取得)
# - Watch モード(ファイル保存で自動再実行)
# - ネットワークパネル、コンソール、ソース表示
# - v1.58+ は JSON レスポンスの自動整形、コードエディタ内 Ctrl+F 検索

Trace Viewer ── 失敗を 100% 再現

trace を収集して開く
# trace: "on-first-retry" 設定済みなら CI で自動収集される
# ローカルで強制収集したい場合
npx playwright test --trace on

# trace ファイルを開く
npx playwright show-trace test-results/.../trace.zip

# HTML Report 経由でも開ける
npx playwright show-report
Trace Viewer の中身:タイムライン(各アクションの時間)、② Before/After スクリーンショット、③ DOM スナップショット(過去状態を完全再現して DOM を調査可能)、④ コンソール / ネットワーク / ソース、⑤ 呼び出しスタック。CI で失敗した時にこれがあれば「手元で再現できない」という悩みがなくなります。

Copy Prompt ── LLM デバッグ(v1.51+)

HTML Report / Trace Viewer / UI Mode のエラー画面に「Copy Prompt」ボタンがあり、エラー文脈とスタックを LLM 用プロンプトとして一発コピーできます。Claude や ChatGPT に貼り付ければ原因解析と修正案が即得られます。Claude Code との連携は Claude Code で Playwright と Browser Use を使うブラウザ自動操作ガイド を参照してください。

API テスト ── ブラウザを立ち上げずに REST を叩く

tests/api.spec.ts
import { test, expect } from "@playwright/test";

test.describe("REST API", () => {
  test("GET /api/posts", async ({ request }) => {
    const res = await request.get("/api/posts");
    expect(res.status()).toBe(200);
    const body = await res.json();
    expect(Array.isArray(body)).toBe(true);
  });

  test("POST /api/posts で新規作成", async ({ request }) => {
    const res = await request.post("/api/posts", {
      data: { title: "test", body: "content" },
      headers: { "Content-Type": "application/json" },
    });
    expect(res.status()).toBe(201);
    const body = await res.json();
    expect(body.id).toBeDefined();
  });
});
UI と API のハイブリッドテスト: ログインだけ API で済ませて認証済みの storageState を作り、残りの UI テストはそこから走らせる、という設計が最高速です。ブラウザ操作でのログインを毎回省略できるため CI 時間が劇的に減ります。

コンポーネントテスト

@playwright/experimental-ct-react などの導入
# React の場合
npm init playwright@latest --ct
# or Vue / Svelte / Solid 用の別パッケージ

npx playwright test --ui
Button.spec.tsx
import { test, expect } from "@playwright/experimental-ct-react";
import { Button } from "./Button";

test("variant prop が適用される", async ({ mount }) => {
  const component = await mount(
    <Button variant="primary">保存</Button>
  );
  await expect(component).toContainClass("bg-primary-500");
  await expect(component).toHaveText("保存");
});

test("onClick が呼ばれる", async ({ mount }) => {
  let clicked = 0;
  const component = await mount(
    <Button onClick={() => clicked++}>Click me</Button>
  );
  await component.click();
  expect(clicked).toBe(1);
});
いつコンポーネントテストを選ぶか: Storybook + Visual RegressionClaude Code × Storybook)で十分なケースも多いです。Playwright CT は「同じ Locator API で UI 部品単位も E2E も書きたい」プロジェクト向け。新規プロジェクトなら Storybook + Chromatic / Playwright のVisual Regression と E2E の 2 本立てが王道です。

Aria Snapshots と Visual Regression

Aria Snapshot(v1.49+)── アクセシビリティツリー比較

ページの構造を YAML で固定化
await expect(page.locator("main")).toMatchAriaSnapshot(`
  - heading "ようこそ" [level=1]
  - navigation:
    - link "ホーム"
    - link "設定"
  - button "新規作成"
`);
Aria Snapshot の強み:文字列・構造が YAML で人間に読みやすい、② スタイル変更では壊れない(ARIA セマンティクスが変わらない限り安定)、③ アクセシビリティ担保を自動で。従来のピクセル単位スクリーンショット比較と併用すると、機能テストとアクセシビリティ監視が同時に成立します。

toHaveScreenshot ── ピクセル比較

スクリーンショット比較
test("ランディングページのデザイン", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("landing.png", {
    maxDiffPixelRatio: 0.01,
    fullPage: true,
    mask: [page.locator(".dynamic-time")],  // 時刻表示など動的部分をマスク
  });
});
初回のベースライン生成
# 初回は --update-snapshots を付けて正解画像を保存
npx playwright test --update-snapshots

# 以降は変更検知したら失敗する
npx playwright test

並列実行とシャーディング ── CI 時間を桁違いに短縮

ワーカ並列 + CI でのシャーディング
# ローカルでもワーカ並列が既定(workers: 自動)
npx playwright test --workers 8

# CI で複数マシンに分散(4 マシン構成)
npx playwright test --shard=1/4    # マシン 1
npx playwright test --shard=2/4    # マシン 2
npx playwright test --shard=3/4    # マシン 3
npx playwright test --shard=4/4    # マシン 4

# レポートを後で合体させる
npx playwright merge-reports --reporter=html ./all-blob-reports
Sharding の現実的な効果: 300 テスト×5 分が 1 マシン 30 分だったのを、4 シャード並列で7〜8 分まで圧縮できます。Playwright は自動で testing を均等分配するので、shard 数を増やすだけで線形にスケールします。

GitHub Actions での CI 統合

.github/workflows/e2e.yml
name: E2E Tests
on:
  push: { branches: [main] }
  pull_request:

jobs:
  e2e:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with: { node-version: 22, cache: npm }
      - run: npm ci

      - name: Playwright ブラウザ
        run: npx playwright install --with-deps chromium firefox webkit

      - name: E2E 実行(シャード ${{ matrix.shard }}/4)
        run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=blob
        env:
          BASE_URL: http://localhost:3000
          E2E_EMAIL:    ${{ secrets.E2E_EMAIL }}
          E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: blob-${{ matrix.shard }}
          path: blob-report
          retention-days: 3

  merge-reports:
    if: always()
    needs: [e2e]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with: { node-version: 22 }
      - run: npm ci
      - uses: actions/download-artifact@v4
        with: { pattern: blob-*, merge-multiple: true, path: all-blob-reports }
      - run: npx playwright merge-reports --reporter=html all-blob-reports
      - uses: actions/upload-artifact@v4
        with: { name: html-report, path: playwright-report, retention-days: 7 }
公式の Docker イメージ: mcr.microsoft.com/playwright:v1.58.0-noble を CI ジョブのコンテナにすればブラウザインストール不要になり、5〜10 秒の起動短縮になります。GitHub Actions の詳細は GitHub Actions 完全ガイド を参照してください。

落とし穴と注意点

CSS Locator を使い続けてテストが壊れやすい

page.locator("button.btn-primary.rounded") のように CSS クラスで要素を取ると、Tailwind のクラスが増減しただけで落ちます。getByRole("button", { name: "保存" }) のようにユーザー視点のロケーターに置き換えれば、実装のリファクタリングで壊れなくなります。Tailwind v4 で @theme を入れ替えた場合も E2E テストは無傷です(Tailwind CSS v4 完全ガイド)。

await 忘れ

Playwright の API はほぼすべて Promise を返します。page.click() のように await を付け忘れると、アクションが実行されないまま次に進んで「なぜかボタンが押されない」と悩みます。ESLint の no-floating-promises を必ず有効化してください(Biome 完全ガイド の Type Inference でも検出可能)。

非同期の手動 wait(waitForTimeout)

await page.waitForTimeout(2000) のような固定ウェイトはテスト時間の無駄遣いであり、ネットワーク遅延で失敗する原因にもなります。Playwright の expect(...) は auto-waiting するので、「要素が表示されるのを待つ」は expect(locator).toBeVisible() に置き換えます。

テスト間で DB を共有

テストがお互いに DB を介して依存すると、並列実行や順序入れ替えで壊れます。基本は各テストで独立した状態をセットアップ、難しければ test.describe.configure({ mode: "serial" }) で直列化して、できればテストごとに専用スキーマ / 一時 DBを使うのが理想です。

.only / .skip を残してコミット

test.only(...) を仕込んだままコミットすると他のテストがスキップされます。設定 forbidOnly: !!process.env.CI を入れると、CI で .only があった瞬間に失敗するため必ず気づけます。Git pre-commit hook に組み込むとさらに安全です(GitHub Actionsでも同じ指針)。

Webkit の時差や挙動差

Chromium では通るテストが WebKit だけ落ちる、というケースがあります。原因はタイムゾーン・ロケール・font rendering・CSS の微細な差playwright.config.tslocale: "ja-JP" / timezoneId: "Asia/Tokyo" を明示、toHaveScreenshotmaxDiffPixelRatio を許容値に設定する、が対処法です。

storageState の期限切れ

Cookie の有効期限が短いサイトでは、.auth/user.json に保存したトークンが CI 実行時には期限切れになっていることがあります。setup プロジェクトを毎回走らせて再ログインするか、storageState.expires を確認して期限前なら再利用、という制御を入れてください。

よくある質問

QPlaywright と Cypress はどう使い分けますか?
ASafari / WebKit を含む 3 ブラウザが必須、iframe や複数タブ・複数オリジンを扱う、API テストや並列実行をフル活用したい場合はPlaywright。チーム内で Cypress の経験が豊富で「開発中に UI 部品をインタラクティブにテストしたい」ならCypress Component Test。両者とも auto-waiting と UI Mode を持ちますが、Playwright のほうがマルチブラウザ・並列性能・Trace Viewerで上回ります。2026 年の新規プロジェクトは Playwright が無難です。
QgetByTestId はいつ使うべきですか?
Aまず getByRole / getByLabel / getByText で要素を特定できないか検討し、それでも無理(画像ボタンでテキストなし、複雑な SVG ベースの UI 等)な場合の最終手段として data-testid を埋め込みます。実装に専用属性を追加する時点でテストのカップリングが増えるため、乱用すると UI リファクタリング時の変更範囲が広がります。
QstorageState と API ログインはどちらが速いですか?
A両者を組み合わせるのが最速です。① setup プロジェクトで API を叩いてトークン取得、② その結果を Cookie や LocalStorage に詰めて storageState として保存、③ 本テストは storageState を使ってブラウザ UI を開くだけ。UI からログインするより10〜30 倍高速で、CI 時間が劇的に縮みます。
QCI で Playwright を速く動かすには?
A–shard で複数マシン並列、② 公式 Docker イメージ(mcr.microsoft.com/playwrightでブラウザインストール省略、③ storageState でログイン省略、④ retry を 1〜2 回に絞る(3 以上は flaky の隠蔽になる)、⑤ trace: “on-first-retry” で成功テストは trace を保存しない、⑥ 依存キャッシュactions/setup-node cache: npm)。GitHub Actions での具体構成は GitHub Actions 完全ガイドを参照してください。
QPage Object Model と Fixtures はどちらを使うべきですか?
A両方です。POM は「ページ単位の要素とアクションをまとめる」設計パターン、Fixtures は「テストへの依存性注入」の仕組み。POM をクラスで書き、それを fixture として test.extend で提供すると、テスト側は async ({ loginPage }) => {} のように 1 引数で使えるスッキリしたコードになります。大規模プロジェクトほどこの組合せが効いてきます。
QAria Snapshots と toHaveScreenshot はどう使い分けますか?
AAria Snapshotは「画面構造・アクセシビリティ・コンテンツ」が正しいかを YAML で確認するのに向く(スタイル変更で壊れない)。toHaveScreenshot は「見た目そのもの(CSS・配置・色)」を確認する(デザイン変更で壊れるが、それが狙い)。両方を組み合わせるのが堅牢で、構造を Aria で・見た目をスクリーンショットで守ると、テストが漏れなく壊れずに済みます。Storybook でのビジュアルリグレッションは Claude Code × Storybook UI カタログ駆動開発ガイドも参照。
QReact / Vue / Svelte / Astro のどれでも Playwright は同じように使えますか?
Aはい。Playwright はブラウザ側の DOM と挙動だけを対象にするので、裏のフレームワークが何であっても同じ書き方ができます。React 19 / Svelte 5 / Astro / Nuxt 4 いずれでも、同じ getByRole ベースの E2E テストが書けるのが魅力です。
QBun / Deno でも Playwright は動きますか?
APlaywright Test は Node.js 前提ですが、テスト対象のアプリは Bun / Deno で動かしても問題なしです。つまり「Bun で動かす Hono API を、Playwright(Node)でブラウザから E2E テストする」ような構成が自然にできます。Playwright 自体を Bun で実行したい場合は bunx playwright test で動くケースもありますが、公式サポート外なので CI では Node 22 を使うのが無難です。

まとめ

  • 2026 年の E2E デファクト: Playwright 1.58+。3 ブラウザ 1 API・auto-waiting・Trace Viewer で Flaky test が過去のものに
  • Locator はユーザー視点: getByRole(最優先)→ getByLabelgetByTextgetByTestId(最終手段)
  • Web-first Assertions: expect(locator).toBeVisible() 等が auto-waiting。toContainClass()(v1.52+)・toMatchAriaSnapshot()(v1.49+)も追加
  • Fixtures と Page Object Modelで保守性を担保。box: true で内部詳細を隠す
  • storageState でログイン共有: IndexedDB にも対応(v1.51+)。setup プロジェクト + dependencies の構成
  • Codegen / UI Mode / Trace Viewerが三大ツール。失敗の再現と原因特定が劇的に楽に
  • Copy Prompt(v1.51+)で LLM にエラー解析を投げられる。Claude Code との連携も強力
  • Sharding + Docker 公式イメージで CI 時間を桁違いに短縮
  • 落とし穴は CSS Locator・await 忘れ・waitForTimeout・test.only・WebKit 差分・storageState 期限

Claude Code との連携は Claude Code で Playwright と Browser Use を使うブラウザ自動操作ガイド、CI/CD は GitHub Actions 完全ガイド、対象アプリのフレームワーク別記事は React 19Svelte 5AstroNuxt 4Claude Code × Next.js、ツール周辺は Tailwind CSS v4BiomeBunDeno 2、UI コンポーネント検証は Claude Code × Storybook もあわせて、Playwright を核に据えた 2026 年型テスト基盤を構築してください。