| name | e2e |
| description | Playwright E2E testing patterns with chrome-devtools MCP integration. Reference for integration tests, A11y validation, and visual regression. |
E2E Testing Skill
Playwright Setup
pnpm add -D @playwright/test
pnpm exec playwright install
playwright.config.ts
import { defineConfig } from "@playwright/test"
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
use: {
baseURL: "http://localhost:5173",
trace: "on-first-retry",
},
webServer: {
command: "pnpm dev",
url: "http://localhost:5173",
reuseExistingServer: !process.env.CI,
},
})
Test Patterns
Basic Structure
// e2e/example.spec.ts
import { test, expect } from "@playwright/test"
test.describe("Feature", () => {
test("displays correct ARIA structure", async ({ page }) => {
await page.goto("/")
const tree = page.getByRole("tree")
await expect(tree).toBeVisible()
await expect(tree).toHaveAttribute("aria-label", "File explorer")
})
test("keyboard navigation works", async ({ page }) => {
await page.goto("/")
const firstItem = page.getByRole("treeitem").first()
await firstItem.focus()
// ArrowDown moves to next
await page.keyboard.press("ArrowDown")
await expect(page.getByRole("treeitem").nth(1)).toBeFocused()
// ArrowRight expands
await page.keyboard.press("ArrowRight")
await expect(firstItem).toHaveAttribute("aria-expanded", "true")
})
})
A11y Testing with axe-core
import AxeBuilder from "@axe-core/playwright"
test("meets accessibility standards", async ({ page }) => {
await page.goto("/")
const results = await new AxeBuilder({ page }).analyze()
expect(results.violations).toEqual([])
})
Form Interaction
test("submits form correctly", async ({ page }) => {
await page.goto("/form")
await page.getByLabel("Name").fill("John Doe")
await page.getByLabel("Email").fill("john@example.com")
await page.getByRole("button", { name: "Submit" }).click()
await expect(page.getByText("Success")).toBeVisible()
})
chrome-devtools MCP Integration
When running E2E verification via Claude Code:
1. Start the application:
pnpm dev
2. Navigate to page:
mcp__chrome-devtools__navigate_page
url: "http://localhost:5173"
3. Take accessibility snapshot:
mcp__chrome-devtools__take_snapshot
→ Get A11y tree
4. Verify:
- role="tree" exists
- aria-expanded attributes
- aria-selected attributes
5. Test keyboard navigation:
mcp__chrome-devtools__press_key
key: "ArrowDown"
6. Verify focus movement:
mcp__chrome-devtools__take_snapshot
→ Confirm focus changed
Visual Regression
test("visual regression", async ({ page }) => {
await page.goto("/")
await expect(page).toHaveScreenshot("homepage.png")
})
test("component visual regression", async ({ page }) => {
await page.goto("/components/button")
const button = page.getByRole("button", { name: "Primary" })
await expect(button).toHaveScreenshot("button-primary.png")
})
Updating Snapshots
pnpm e2e --update-snapshots
Page Object Pattern
// e2e/pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("/login")
}
async login(email: string, password: string) {
await this.page.getByLabel("Email").fill(email)
await this.page.getByLabel("Password").fill(password)
await this.page.getByRole("button", { name: "Sign in" }).click()
}
async expectError(message: string) {
await expect(this.page.getByRole("alert")).toContainText(message)
}
}
// e2e/auth.spec.ts
test("shows error for invalid credentials", async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login("invalid@example.com", "wrong")
await loginPage.expectError("Invalid credentials")
})
Fixtures
// e2e/fixtures.ts
import { test as base } from "@playwright/test"
import { LoginPage } from "./pages/LoginPage"
type Fixtures = {
loginPage: LoginPage
}
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page))
},
})
// Usage
test("login flow", async ({ loginPage }) => {
await loginPage.goto()
await loginPage.login("user@example.com", "password")
})
Commands
pnpm e2e # Run E2E tests
pnpm e2e:headed # Run with browser visible
pnpm e2e:debug # Debug mode
pnpm e2e:ui # Interactive UI mode
When to Use E2E vs Other Test Types
Test Pyramid Strategy
▲ E2E (Playwright)
╱ ╲ - Full user journeys
╱ ╲ - Cross-page flows
╱─────╲ - Critical paths only
╱ ╲
╱ Component╲ - Storybook play functions
╱ Tests ╲ - Vitest Browser Mode
╱────────────╲- Isolated component behavior
╱ ╲
╱ Unit Tests ╲ - Vitest
╱ (Base) ╲- Pure functions, logic
╱──────────────────╲
Decision Matrix
| Test Scope | Tool | When to Use |
|---|---|---|
| Unit | Vitest | Pure functions, utilities, state logic |
| Component | Storybook + Vitest Browser | Single component behavior, props, states |
| Integration | Vitest Browser | Multiple components together |
| E2E | Playwright | Full user flows, multi-page, auth |
E2E Test Selection Criteria
Include in E2E:
- Critical business flows (checkout, signup, login)
- Multi-page navigation flows
- Flows requiring server state
- External API integrations
- Authentication/authorization
Avoid in E2E (use component tests):
- Individual component variants
- Form validation rules
- UI state toggling
- Styling variations
Cost-Benefit Analysis
| Factor | Unit | Component | E2E |
|---|---|---|---|
| Speed | ~1ms | ~50ms | ~2-5s |
| Reliability | High | High | Medium |
| Maintenance | Low | Low | High |
| Coverage | Narrow | Medium | Wide |
| Debug ease | Easy | Easy | Hard |
Rule of thumb: Maximize unit/component tests, minimize E2E to critical paths only.
CI Integration
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- name: Unit & Component Tests
run: pnpm test
- name: E2E Tests
run: pnpm e2e
env:
CI: true
Parallel E2E Execution
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? "github" : "list",
})