概要
一覧ページのソートセレクトを操作したときに URL の sort クエリパラメータが更新されること、およびカード表示順が変化することを Playwright でE2Eテストする。select 要素の操作・waitForURL での遷移確認・先頭カードのテキスト検証を組み合わせて実装する。
インストール
npm install @playwright/test
npx playwright install
実装
ページオブジェクト
// e2e/pages/SamplesPage.ts
import type { Page, Locator } from "@playwright/test";
export class SamplesPage {
readonly page: Page;
readonly sortSelect: Locator;
readonly sampleCards: Locator;
constructor(page: Page) {
this.page = page;
this.sortSelect = page.getByRole("combobox", { name: "並び順" });
this.sampleCards = page.getByTestId("sample-card");
}
async goto(params?: Record<string, string>) {
const query = params
? "?" + new URLSearchParams(params).toString()
: "";
await this.page.goto(`/samples${query}`);
}
async selectSort(value: string) {
await this.sortSelect.selectOption(value);
}
async firstCardTitle(): Promise<string> {
return (await this.sampleCards.first().getByRole("heading").textContent()) ?? "";
}
}
ソート URL 同期テスト
// e2e/sort-flow.spec.ts
import { test, expect } from "@playwright/test";
import { SamplesPage } from "./pages/SamplesPage";
test.describe("ソート変更", () => {
test("sort=title を選択すると URL に sort=title が付く", async ({ page }) => {
const samplesPage = new SamplesPage(page);
await samplesPage.goto();
await samplesPage.selectSort("title");
await page.waitForURL((url) => url.searchParams.get("sort") === "title");
expect(page.url()).toContain("sort=title");
});
test("sort=createdAt を選択すると URL に sort=createdAt が付く", async ({ page }) => {
const samplesPage = new SamplesPage(page);
await samplesPage.goto();
await samplesPage.selectSort("createdAt");
await page.waitForURL((url) => url.searchParams.get("sort") === "createdAt");
expect(page.url()).toContain("sort=createdAt");
});
test("ソートを変更するとカードの並び順が変わる", async ({ page }) => {
const samplesPage = new SamplesPage(page);
await samplesPage.goto();
// デフォルト(updatedAt)の先頭タイトルを記録
const defaultFirst = await samplesPage.firstCardTitle();
// タイトル順に変更
await samplesPage.selectSort("title");
await page.waitForURL((url) => url.searchParams.get("sort") === "title");
const titleSortFirst = await samplesPage.firstCardTitle();
// ソートが異なれば先頭も変わる(データが2件以上ある前提)
expect(titleSortFirst).not.toBe(defaultFirst);
});
});
他のフィルタと組み合わせたソートテスト
// e2e/sort-with-filter.spec.ts
import { test, expect } from "@playwright/test";
import { SamplesPage } from "./pages/SamplesPage";
test("カテゴリ絞り込み中にソートを変更しても絞り込みが維持される", async ({ page }) => {
const samplesPage = new SamplesPage(page);
// カテゴリ絞り込み済みの状態から開始
await samplesPage.goto({ category: "routing" });
await samplesPage.selectSort("title");
await page.waitForURL(
(url) =>
url.searchParams.get("sort") === "title" &&
url.searchParams.get("category") === "routing"
);
const url = new URL(page.url());
expect(url.searchParams.get("sort")).toBe("title");
expect(url.searchParams.get("category")).toBe("routing");
});
playwright.config.ts での設定
// 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,
},
});
ポイント
waitForURLにコールバックを渡すことで、URL のsearchParamsを直接検査できる。文字列マッチより意図が明確で、クエリパラメータの順序に依存しない- ソート変更後に
selectSortを呼ぶと Next.js のrouter.pushが走り、URL が変化するまでの非同期待機が必要。waitForURLがその完了を保証する - ページオブジェクトパターン(
SamplesPageクラス)を使うことで、セレクタをテストコードから分離する。UI 構造が変わったときの修正が 1 箇所で済む - 表示順変化の検証は「先頭カードのタイトルが変わること」で代替する。全件順序の厳密な検証は DB 依存度が高く壊れやすいため、変化の有無を確認するアプローチが堅牢
reuseExistingServer: !process.env.CIで、ローカル開発時は既存のdevサーバーを再利用できる。CI では毎回起動して一貫性を保つgetByRole("combobox", { name: "並び順" })でaria-labelか<label>テキストを使ってセレクトを特定する。data-testidより意味のあるセレクタで可読性が高い