レビュー済み·難易度: 中級·更新: 2026-04-18

Playwright で検索フォーム入力から結果表示・0 件状態までを E2E テストする

Playwright で検索フォームへの入力・URL パラメータ更新・一覧フィルタ結果確認・0 件 EmptyState 表示・フィルタリセットまでの検索フロー全体を E2E テストする例。

nextjstestingsearch-filterplaywright

対応バージョン

nextjs 15react 19playwright 1

前提環境

Playwright の基本テスト構成(test / expect / locator)と Next.js の検索フローを理解していること

概要

Playwright で検索フォームへの入力から始まる一連のフローを E2E テストする。入力による URL クエリパラメータの更新確認、ヒット結果の件数検証、0 件時の EmptyState 表示確認、フィルタリセット後の復元確認まで一通りカバーする。

インストール

npm install --save-dev @playwright/test
npx playwright install chromium

実装

Playwright 設定

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

export default defineConfig({
  testDir: "./e2e",
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});

検索フローの E2E テスト

// e2e/search.spec.ts
import { test, expect } from "@playwright/test";

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

  test("キーワードを入力すると URL クエリが更新される", async ({ page }) => {
    const searchInput = page.getByPlaceholder("キーワードで検索…");

    await searchInput.fill("Next.js");

    // URL に q パラメータが追加されることを確認
    await expect(page).toHaveURL(/q=Next\.js/);
  });

  test("ヒットした結果がリストに表示される", async ({ page }) => {
    await page.getByPlaceholder("キーワードで検索…").fill("prisma");

    // Prisma 関連のアイテムが表示されることを確認
    const items = page.locator("[data-testid='sample-item']");
    await expect(items).not.toHaveCount(0);

    // 表示されたアイテムのテキストにキーワードが含まれることを確認
    const firstItem = items.first();
    await expect(firstItem).toContainText(/prisma/i);
  });

  test("ヒットしないキーワードで 0 件 EmptyState が表示される", async ({ page }) => {
    await page.getByPlaceholder("キーワードで検索…").fill("xyzzy-no-match-keyword");

    // EmptyState が表示されることを確認
    await expect(page.getByText(/一致する結果が見つかりません/)).toBeVisible();

    // 入力したキーワードがメッセージに含まれることを確認
    await expect(page.getByText(/xyzzy-no-match-keyword/)).toBeVisible();
  });

  test("キーワードクリアボタンで検索が解除される", async ({ page }) => {
    await page.getByPlaceholder("キーワードで検索…").fill("xyzzy-no-match-keyword");
    await expect(page.getByText(/一致する結果が見つかりません/)).toBeVisible();

    // クリアボタンをクリック
    await page.getByRole("button", { name: "キーワードをクリア" }).click();

    // URL から q パラメータが消えることを確認
    await expect(page).not.toHaveURL(/q=/);

    // リストが復元されることを確認
    await expect(page.locator("[data-testid='sample-item']")).not.toHaveCount(0);
  });
});

フィルタ操作の E2E テスト

// e2e/filter.spec.ts
import { test, expect } from "@playwright/test";

test.describe("フィルタ操作", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/samples");
  });

  test("カテゴリフィルタを選択すると URL が更新される", async ({ page }) => {
    await page.getByRole("combobox", { name: "カテゴリ" }).selectOption("testing");

    await expect(page).toHaveURL(/category=testing/);
  });

  test("カテゴリフィルタで絞り込まれた結果が表示される", async ({ page }) => {
    await page.getByRole("combobox", { name: "カテゴリ" }).selectOption("testing");

    const items = page.locator("[data-testid='sample-item']");
    const count = await items.count();
    expect(count).toBeGreaterThan(0);
  });

  test("フィルタチップの削除ボタンで個別フィルタが解除される", async ({ page }) => {
    // カテゴリフィルタを適用
    await page.getByRole("combobox", { name: "カテゴリ" }).selectOption("testing");
    await expect(page.getByText("カテゴリ: testing")).toBeVisible();

    // フィルタチップの ✕ ボタンをクリック
    await page.getByRole("button", { name: "カテゴリ フィルタを削除" }).click();

    // フィルタチップが消えることを確認
    await expect(page.getByText("カテゴリ: testing")).not.toBeVisible();
    await expect(page).not.toHaveURL(/category=/);
  });

  test("すべてクリアで全フィルタが解除される", async ({ page }) => {
    // 複数フィルタを適用
    await page.getByPlaceholder("キーワードで検索…").fill("react");
    await page.getByRole("combobox", { name: "カテゴリ" }).selectOption("testing");

    // すべてクリアボタンをクリック
    await page.getByRole("button", { name: "すべてクリア" }).click();

    // URL からすべてのパラメータが消えることを確認
    await expect(page).not.toHaveURL(/[?&]/);
  });

  test("キーワード + カテゴリフィルタの複合条件で 0 件になる", async ({ page }) => {
    await page.getByPlaceholder("キーワードで検索…").fill("tailwind");
    await page.getByRole("combobox", { name: "カテゴリ" }).selectOption("testing");

    // tailwind + testing の組み合わせでは 0 件になることを確認
    await expect(page.getByText(/一致する結果が見つかりません/)).toBeVisible();

    // 補足テキストに両条件が原因であることが示されることを確認
    await expect(page.getByText(/キーワードを変えるか、フィルタ条件を解除/)).toBeVisible();
  });
});

ポイント

  • page.getByPlaceholder / page.getByRole などのセマンティックロケーターを使う。クラス名や ID ではなくアクセシブルな属性でセレクトすることで、UI 実装の変更に強いテストになる
  • URL クエリパラメータの確認は expect(page).toHaveURL(/q=xxx/) で正規表現マッチを使う。エンコードの差異を吸収しつつ存在確認できる
  • [data-testid='sample-item'] のようにテスト専用の data-testid 属性を付けることで、テキストや DOM 構造の変化に影響されないセレクタを確保できる
  • test.beforeEach で検索ページに移動しておき、各テストの前提条件を統一する。テスト間の依存をなくすために beforeEach で毎回リセットする
  • 0 件状態のテストは「EmptyState が表示される」と「入力キーワードがメッセージに含まれる」の両方を確認する。コンポーネントの表示とメッセージ内容の両方を検証できる

注意点

playwright-auth-flow-e2e はログイン→保護ページ→ログアウトの認証フロー E2E。playwright-file-upload-e2e は file input 操作 E2E。これは検索入力→URL 更新→結果 / 0件状態確認→フィルタリセットの検索フロー全体を E2E テストするパターンに特化。

関連サンプル