概要
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 が表示される」と「入力キーワードがメッセージに含まれる」の両方を確認する。コンポーネントの表示とメッセージ内容の両方を検証できる