Awesome-claude-code playwright-e2e
Write Playwright e2e tests with semantic selectors and accessibility-first assertions. Use when creating or modifying e2e tests (*.spec.ts files using Playwright).
install
source · Clone the upstream repo
git clone https://github.com/houshuang/awesome-claude-code
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/houshuang/awesome-claude-code "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/playwright-e2e" ~/.claude/skills/houshuang-awesome-claude-code-playwright-e2e && rm -rf "$T"
manifest:
skills/playwright-e2e/SKILL.mdsource content
E2E Test (Playwright)
Guide for writing reliable, accessible Playwright e2e tests.
When to Use
- Creating new Playwright e2e test files (
)*.spec.ts - Modifying existing e2e tests
- Reviewing e2e test code for best practices
Locator Priority
Always prefer locators higher in this list. Drop to the next level only when the one above cannot express what you need.
| Priority | Locator | When to use |
|---|---|---|
| 1 | | Interactive elements with accessible roles |
| 2 | | Form fields with associated labels |
| 3 | | Inputs that only have placeholder text |
| 4 | | Non-interactive text content |
| 5 | | Elements without semantic roles (last resort) |
Role + Name Rule
Always pass the accessible name when using
getByRole:
// Good — targets a specific button page.getByRole("checkbox", { name: "Terms" }); // Bad — ambiguous if multiple buttons exist page.getByRole("checkbox");
Bare
getByRole (without { name }) is acceptable only when the test asserts count or the role is unique in scope:
// Acceptable — asserting total count, not targeting one element await expect(page.getByRole("checkbox")).toHaveCount(2);
Semantic Assertions
Use Playwright's built-in semantic assertions instead of checking attributes or booleans manually.
| Use this | Instead of |
|---|---|
| |
| |
| |
| |
| |
| |
| manual visibility checks |
Semantic assertions auto-retry until timeout — manual checks do not.
Anti-Patterns
Never use these
| Pattern | Why | Fix |
|---|---|---|
CSS class selectors (, ) | Fragile — classes change with styling | Use or |
| XPath | Hard to read and maintain | Use Playwright locators |
| Flaky — arbitrary delays | Use or |
| No auto-retry, races | Use |
/ | ElementHandle API, no auto-wait | Use |
Acceptable exceptions
These selectors are fine when semantic alternatives don't exist:
| Selector | Reason |
|---|---|
| ProseMirror framework convention for editor root |
| cmdk library renders this attribute; no ARIA role available |
Tag selectors (, ) | Asserting specific HTML structure when roles don't distinguish |
Test Structure
Basic test setup
import { test, expect } from "@playwright/test"; test.describe("MyFeature", () => { test.beforeEach(async ({ page }) => { await page.goto("http://localhost:YOUR_PORT/"); }); test("does something", async ({ page }) => { // Use semantic locators await page.getByRole("button", { name: "Submit" }).click(); await expect(page.getByText("Success")).toBeVisible(); }); });
Serial mode
Use
test.describe.configure({ mode: "serial" }) when tests share state within a describe block:
test.describe("Stateful flow", () => { test.describe.configure({ mode: "serial" }); test("step 1 — create item", async ({ page }) => { /* ... */ }); test("step 2 — edit item", async ({ page }) => { /* ... */ }); });
Helper patterns
Extract reusable selectors into helper functions at the top of the describe block:
const menuIsOpen = () => page.locator("[cmdk-root]"); const selectCommand = async (filter: string) => { await page.keyboard.type(filter); await page.keyboard.press("Enter"); };
Running E2E Tests
# Run all e2e tests npx playwright test # Run a specific spec file npx playwright test src/e2e/my-feature.spec.ts # Run in headed mode (visible browser) npx playwright test --headed # Debug a specific test npx playwright test --debug src/e2e/my-feature.spec.ts
Checklist
Before considering e2e tests complete:
- Locators prefer
>getByRole
>getByLabel
>getByPlaceholder
>getByTextgetByTestId
calls includegetByRole
unless asserting count or role is unique in scope{ name }- Assertions use semantic matchers (
,toBeChecked
,toBeVisible
) — no manual boolean checkstoHaveCount - No
— use auto-retrying assertions insteadwaitForTimeout() - No CSS class or ID selectors unless they fall under acceptable exceptions
- Tests pass:
npx playwright test