概要
Playwright を使い、ページネーション付き一覧ページで「次へ」「前へ」「ページ番号クリック」を操作し、URL の page クエリパラメータと表示カード件数が正しく変わることをE2Eテストする。page.url() と locator を組み合わせて URL 同期を検証するパターンを示す。
インストール
npm install -D @playwright/test
npx playwright install chromium
実装
基本的なページネーションフロー
// e2e/pagination.spec.ts
import { test, expect } from "@playwright/test";
const BASE_URL = "http://localhost:3000";
const PAGE_SIZE = 24;
test.describe("ページネーション", () => {
test("1 ページ目: URL に page クエリがなく、カードが最大 PAGE_SIZE 件表示される", async ({ page }) => {
await page.goto(`${BASE_URL}/samples`);
// page クエリなし or page=1 を確認
const url = new URL(page.url());
expect(url.searchParams.get("page")).toBeNull();
// カードが PAGE_SIZE 件以下
const cards = page.locator("[data-testid='sample-card']");
await expect(cards).toHaveCount(PAGE_SIZE);
});
test("「次のページ」をクリックすると page=2 になる", async ({ page }) => {
await page.goto(`${BASE_URL}/samples`);
await page.getByRole("link", { name: "次のページ" }).click();
await page.waitForURL(/page=2/);
const url = new URL(page.url());
expect(url.searchParams.get("page")).toBe("2");
});
test("ページ番号をクリックすると該当ページに遷移する", async ({ page }) => {
await page.goto(`${BASE_URL}/samples`);
await page.getByRole("link", { name: "3", exact: true }).first().click();
await page.waitForURL(/page=3/);
const url = new URL(page.url());
expect(url.searchParams.get("page")).toBe("3");
});
test("「前のページ」をクリックすると page が 1 つ戻る", async ({ page }) => {
await page.goto(`${BASE_URL}/samples?page=3`);
await page.getByRole("link", { name: "前のページ" }).click();
await page.waitForURL(/page=2/);
const url = new URL(page.url());
expect(url.searchParams.get("page")).toBe("2");
});
test("1 ページ目では「前のページ」リンクが表示されない", async ({ page }) => {
await page.goto(`${BASE_URL}/samples`);
await expect(
page.getByRole("link", { name: "前のページ" })
).not.toBeVisible();
});
});
フィルタとページネーションの組み合わせ
test.describe("フィルタ × ページネーション", () => {
test("フィルタ変更時に page がリセットされる", async ({ page }) => {
// 2 ページ目でフィルタを変更
await page.goto(`${BASE_URL}/samples?page=2`);
// フレームワークフィルタを変更
await page.getByRole("combobox", { name: "フレームワーク" }).selectOption("nextjs");
await page.waitForURL((url) => !url.includes("page=2"));
const url = new URL(page.url());
expect(url.searchParams.get("page")).toBeNull();
});
test("フィルタを維持したままページ遷移できる", async ({ page }) => {
await page.goto(`${BASE_URL}/samples?framework=nextjs`);
await page.getByRole("link", { name: "次のページ" }).click();
await page.waitForURL(/page=2/);
const url = new URL(page.url());
// フィルタが維持されていることを確認
expect(url.searchParams.get("framework")).toBe("nextjs");
expect(url.searchParams.get("page")).toBe("2");
});
});
aria-current="page" でアクティブページを検証
test("現在のページ番号が aria-current='page' になっている", async ({ page }) => {
await page.goto(`${BASE_URL}/samples?page=2`);
const current = page.locator("[aria-current='page']");
await expect(current).toHaveText("2");
});
Playwright の設定ファイル
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: {
baseURL: "http://localhost:3000",
},
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
ポイント
page.waitForURL(/page=2/)で URL 変化を待ってから検証する。click()直後にpage.url()を読むと遷移前の URL が返ることがあるpage.getByRole("link", { name: "次のページ" })はボタンテキストやaria-labelに対応するロールベースロケーターで、実装変更に強い。data-testidは最終手段aria-current="page"を検証することで、アクセシビリティ属性が正しく実装されているかをE2Eレベルで確認できるwebServer設定でreuseExistingServer: !process.env.CIを使うと、ローカルでは起動済みサーバーを再利用でき、CI では必ず新規起動できる- フィルタ × ページネーションの組み合わせテストは、フィルタ変更時の
pageリセットと、ページ遷移時のフィルタ維持の両方を検証することで、URL 状態管理のバグを早期に検出できる data-testid='sample-card'などのテスト用属性はprocess.env.NODE_ENV === 'test'のみ出力する実装にするか、E2E 用に常時出力してもよい(プロジェクトの方針による)