Claude Code Plugins

Community-maintained marketplace

Feedback

Testing with Playwright

@QingqiShi/shiqingqi.com
2
0

Playwright end-to-end (e2e) testing best practices for user-centric testing using semantic locators. Use when writing E2E tests, integration tests, user flow tests, Playwright tests, test specs, or when the user mentions Playwright, e2e tests, getByRole, test flows, or user testing.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name Testing with Playwright
description Playwright end-to-end (e2e) testing best practices for user-centric testing using semantic locators. Use when writing E2E tests, integration tests, user flow tests, Playwright tests, test specs, or when the user mentions Playwright, e2e tests, getByRole, test flows, or user testing.

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        # Run unit tests (Vitest)

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