playwright-e2e

SKILL.md

Playwright E2E Testing

Overview

This project uses Playwright for E2E testing with a focus on user-centric testing that avoids implementation details.

Test Configuration

  • Unit tests: Vitest for client components and synchronous server components
  • E2E tests: Playwright with automatic dev server startup
  • Location: e2e/ directory
  • HTML reporter: Enabled with trace collection on retry
  • Duration: Full E2E suite takes ~15 minutes

Core Philosophy: Test Like a User

Users interact with what they see, not technical implementation details.

✅ Test User-Visible Behavior

  • Wait for text to appear/disappear
  • Check for visible elements
  • Interact with labeled buttons and links
  • Verify content changes

❌ Avoid Testing Implementation Details

  • Don't assert on URLs or pathnames
  • Don't check cookies or localStorage
  • Don't wait for networkidle or technical states
  • Don't verify internal state or data structures

Best Practices

1. Use Semantic Locators

Always prefer role-based locators that match how users perceive the page.

✅ Good:

page.getByRole("button", { name: "Submit" });
page.getByRole("heading", { name: "Welcome" });
page.getByRole("link", { name: "Learn More" });
page.getByLabel("Email address");
page.getByPlaceholder("Enter your name");
page.getByText("Success!");

❌ Bad:

page.locator('button[aria-label="Submit"]'); // CSS selector
page.locator(".submit-btn"); // Class name
page.locator("#submit"); // ID
page.locator('[data-testid="submit"]'); // Test ID

2. Wait for Visible Changes

Wait for actual UI changes users would see, not technical state.

✅ Good:

await page.getByRole("button", { name: "Load More" }).click();
await expect(page.getByRole("heading", { name: "Results" })).toBeVisible();

❌ Bad:

await page.waitForLoadState("networkidle");
await page.waitForTimeout(500);
await page.waitForFunction(() => window.location.pathname === "/results");

3. Simplify Test Setup

Minimize beforeEach steps and avoid redundant operations.

✅ Good:

test("user can browse products", async ({ page }) => {
  await page.goto("/");
  await expect(page.getByRole("heading", { name: "Products" })).toBeVisible();

  // Test continues...
});

❌ Bad:

test.beforeEach(async ({ page }) => {
  await page.goto("/");
  await page.waitForLoadState("networkidle");
  await page.context().clearCookies();
  await page.reload();
  await page.waitForLoadState("networkidle");
});

test("user can browse products", async ({ page }) => {
  // Test continues...
});

Complete Example: Before & After

❌ Bad: Testing Implementation Details

test("language switch", async ({ page }) => {
  await page.goto("/");
  await page.waitForLoadState("networkidle"); // Technical state
  await page.context().clearCookies();
  await page.reload();
  await page.waitForLoadState("networkidle");

  const button = page.locator('button[aria-label="Select a language"]'); // CSS selector
  await button.click();
  await page.waitForTimeout(200); // Arbitrary wait

  const option = page.locator('a[aria-label="切换至中文"]');
  await option.click();
  await page.waitForFunction(() => window.location.pathname.includes("/zh")); // URL check

  const cookies = await page.context().cookies(); // Implementation detail
  expect(cookies.find((c) => c.name === "NEXT_LOCALE")?.value).toBe("zh");
});

✅ Good: Testing User-Visible Behavior

test("language switch", async ({ page }) => {
  await page.goto("/");
  await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();

  await page.getByRole("button", { name: "Select a language" }).click();
  await page.getByRole("link", { name: "切换至中文" }).click();

  // Wait for actual content to change
  await expect(page.getByRole("heading", { name: "欢迎" })).toBeVisible();
});

Common Patterns

Navigation and Verification

await page.goto("/products");
await expect(page.getByRole("heading", { name: "Our Products" })).toBeVisible();

Form Interaction

await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Password").fill("secure123");
await page.getByRole("button", { name: "Sign In" }).click();
await expect(page.getByText("Welcome back!")).toBeVisible();

List Interaction

await page.getByRole("button", { name: "Action" }).first().click();
await expect(page.getByText("Action completed")).toBeVisible();

Conditional Elements

if (await page.getByRole("button", { name: "Accept" }).isVisible()) {
  await page.getByRole("button", { name: "Accept" }).click();
}

Running Tests

pnpm test:e2e                              # Run all E2E tests (auto-starts dev server)
pnpm test:e2e e2e/some-file.spec.ts        # Run specific test file
pnpm test:e2e --grep "test name"           # Run tests matching pattern
pnpm test                                  # Run unit tests (Vitest)

Verification Workflow

IMPORTANT: After writing or modifying E2E tests, YOU must run them to verify they pass.

  1. Run the specific test file or use --grep to run just the new test
  2. If the test fails, fix the issue and re-run
  3. Only report completion after the test passes

Do NOT tell the user to run the tests themselves - run them and report the results.

Key Reminders

  1. Think like a user - What would the user see and do?
  2. Use semantic locators - Roles, labels, text users see
  3. Wait for content - Visible elements, not technical state
  4. Avoid implementation - No URLs, cookies, localStorage assertions
  5. Keep it simple - Minimal setup, clear test flow
Weekly Installs
8
GitHub Stars
4
First Seen
Jan 25, 2026
Installed on
opencode7
gemini-cli7
claude-code7
github-copilot7
amp7
codex7