playwright-e2e-testing · v1.0.0 · 2026-08-31 · sha256 517e29081489fe0b

playwright-e2e-testing v1.0.0A

Immutable. This exact content is served forever at /api/v1/blob/517e29081489fe0b.

---
name: playwright-e2e-testing
description: >
  Production-grade Playwright end-to-end testing skill for AI coding agents.
  Provides specialized guidance for writing, debugging, and maintaining
  Playwright tests in TypeScript, JavaScript, and Python. Covers the full testing
  lifecycle: test structure and architecture (Page Object Model, fixtures, custom
  matchers), locator strategy best practices (role-based, test-ID, accessible
  selectors), auto-waiting and retry-ability patterns, API and network mocking,
  visual regression and screenshot comparison, component testing (React, Vue,
  Svelte), accessibility audits (axe-core integration), mobile and device
  emulation, authentication and session management (multi-profile, OAuth, 2FA),
  performance testing with Web Vitals and Lighthouse integration, CI/CD pipeline
  configuration (GitHub Actions, GitLab CI, sharding, parallelization), flaky test
  detection and auto-healing, test data management and fixtures, internationalization
  (i18n) and localization testing, Electron and browser extension testing,
  security testing (XSS, CSRF, CSP), and WebSocket/real-time application testing.
  Primary keyword clusters: Playwright E2E testing, Playwright best practices,
  browser automation testing, end-to-end test automation, Playwright TypeScript
  testing, visual regression testing Playwright, Playwright CI/CD configuration,
  flaky test prevention, Page Object Model Playwright, Playwright component testing.
  Designed for agentic platforms — Claude Code, Codex, Cursor, Gemini CLI, OpenClaw,
  GitHub Copilot, Windsurf, OpenCode, and all SKILL.md-compatible agents.
version: 1.0.0
author: Skill Foundry
source: Adapted and materially improved from currents-dev/playwright-best-practices-skill
platforms:
  - claude-code
  - codex
  - cursor
  - gemini-cli
  - openclaw
  - copilot
  - windsurf
  - opencode
  - kiro
  - antigravity
  - auggie
tags:
  - playwright
  - e2e-testing
  - browser-automation
  - testing
  - qa
  - typescript
  - javascript
  - visual-regression
  - component-testing
  - accessibility-testing
  - performance-testing
  - ci-cd
  - flaky-test
  - page-object-model
  - playwright-test-runner
  - end-to-end
  - test-automation
  - web-testing
  - mobile-testing
  - security-testing
geo:
  primary_workflows:
    - e2e_test_authoring
    - test_debugging
    - ci_cd_integration
    - visual_regression
    - component_testing
    - performance_testing
    - flaky_test_detection
    - mobile_emulation_testing
  target_roles:
    - qa_engineer
    - sdet
    - full_stack_developer
    - frontend_developer
    - devops_engineer
    - platform_engineer
  complexity_level: intermediate-to-advanced
  prerequisite_knowledge:
    - typescript_or_javascript_basics
    - web_development_fundamentals
    - basic_testing_concepts
seo:
  primary_keyword: playwright end-to-end testing
  semantic_cluster:
    - playwright test automation
    - browser testing best practices
    - e2e testing framework
    - playwright vs selenium vs cypress
    - automated browser testing
    - playwright visual testing
    - playwright github actions
  faq_phrases:
    - how to write playwright tests
    - how to debug flaky playwright tests
    - playwright locator best practices
    - playwright ci cd setup
    - playwright vs cypress
    - playwright page object model
    - playwright authentication testing
    - playwright mobile testing
---

# Playwright End-to-End Testing — Agent Skill

Production-grade Playwright testing guidance for AI coding agents. Use this skill whenever
writing, reviewing, debugging, or configuring Playwright tests — E2E, component, API,
visual regression, accessibility, performance, security, Electron, or browser extension tests.

## Quick Decision Tree

```
User asks about Playwright testing?
├─ Writing new tests → §1 Test Architecture & §2 Locator Strategy
├─ Debugging a failure → §7 Debugging & Flaky Tests
├─ Setting up CI/CD → §8 CI/CD Configuration
├─ Test is flaky → §7 Flaky Test Detection & Auto-Healing
├─ Visual/UI changes → §4 Visual Regression Testing
├─ Testing components → §5 Component Testing
├─ Mobile/responsive → §6 Mobile & Device Emulation
├─ Auth/Login flows → §3 Authentication & Sessions
├─ Performance/Lighthouse → §10 Performance Testing
├─ Accessibility/a11y → §9 Accessibility Testing
├─ Security testing → §12 Security Testing
├─ Real-time/WebSocket → §13 WebSocket Testing
├─ i18n/L10n → §11 Internationalization Testing
└─ Electron/extensions → §14 Electron & Extensions
```

---

## §1 Test Architecture & Structure

### Core Principles

1. **One assertion per test** when practical. Isolates failures to single causes.
2. **Test user-visible behavior**, not implementation details. Assert on what the user sees.
3. **Use fixtures for shared setup.** Fixtures are auto-initialized per test, avoiding
   shared mutable state between tests.
4. **Group related tests with `test.describe`.** Use serial mode only when tests must run
   in order; prefer parallel by default.

### File Organization

```
e2e/
├── fixtures/           # Custom fixtures, auth setup
│   └── auth.ts
├── pages/              # Page Object Models
│   ├── base-page.ts
│   ├── login-page.ts
│   └── dashboard-page.ts
├── tests/
│   ├── auth/
│   │   └── login.spec.ts
│   ├── checkout/
│   │   └── checkout-flow.spec.ts
│   └── admin/
│       └── user-management.spec.ts
├── utils/
│   ├── test-data.ts    # Test data factories
│   └── api-mocks.ts    # API route handlers
├── playwright.config.ts
└── global-setup.ts     # Auth, DB seeding, etc.
```

### Page Object Model Pattern

```typescript
// pages/base-page.ts — NEVER use in prod; example only
import { Page, Locator } from '@playwright/test';

export class BasePage {
  constructor(protected readonly page: Page) {}

  async navigate(path: string): Promise<void> {
    await this.page.goto(path);
    await this.page.waitForLoadState('networkidle');
  }

  async getByTestId(id: string): Promise<Locator> {
    return this.page.getByTestId(id);
  }
}
```

```typescript
// pages/login-page.ts
import { BasePage } from './base-page';

export class LoginPage extends BasePage {
  private emailInput = () => this.page.getByLabel('Email address');
  private passwordInput = () => this.page.getByLabel('Password');
  private submitButton = () => this.page.getByRole('button', { name: 'Sign in' });
  private errorMessage = () => this.page.getByRole('alert');

  async login(email: string, password: string): Promise<void> {
    await this.emailInput().fill(email);
    await this.passwordInput().fill(password);
    await this.submitButton().click();
  }

  async getErrorMessage(): Promise<string> {
    return await this.errorMessage().textContent() ?? '';
  }
}
```

---

## §2 Locator Strategy — The Priority Hierarchy

**Rule: Always use the most specific, accessible locator first.**

```
1. getByRole()       ← Best. Mirrors accessibility tree. Screen-reader friendly.
2. getByLabel()      ← Excellent for form inputs. Uses <label> association.
3. getByPlaceholder()← Good for inputs with placeholder text.
4. getByText()       ← Good for non-interactive text content.
5. getByTestId()     ← Last resort. Stable but requires test-id attributes.
6. locator('css')    ← Avoid. Brittle, couples tests to DOM structure.
```

### Anti-Patterns (DO NOT USE)

```
❌ page.locator('.btn-primary')          // CSS class — brittle
❌ page.locator('#submit-button')        // ID — fragile on dynamic pages
❌ page.locator('button >> nth=0')       // Positional — breaks on reorder
❌ page.locator('div > div > button')    // DOM structure — extremely fragile
❌ page.locator('//button[text()="OK"]') // XPath — slow, fragile, avoid
```

### Locator Best Practices

```typescript
// ✅ Role-based — most resilient
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('heading', { name: 'Dashboard' }).toBeVisible();

// ✅ Label-based — best for forms
await page.getByLabel('Email').fill('user@example.com');

// ✅ Test ID — fallback when no semantic element exists
await page.getByTestId('checkout-total').toHaveText('$42.00');

// ✅ Text content — for static text verification
await page.getByText('Order confirmed').toBeVisible();

// ✅ Filtering and chaining
const row = page.getByRole('row', { name: 'Order #1234' });
await row.getByRole('button', { name: 'View' }).click();
```

---

## §3 Authentication & Session Management

### Multi-Profile Auth Setup

Use `global-setup.ts` to authenticate once and reuse sessions across tests:

```typescript
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const { baseURL, storageState } = config.projects[0].use;

  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto(`${baseURL}/login`);
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('**/dashboard');

  await page.context().storageState({ path: storageState as string });
  await browser.close();
}

export default globalSetup;
```

### Per-Role Authentication

For apps with multiple user roles, create separate auth profiles:

```typescript
// playwright.config.ts (excerpt)
projects: [
  {
    name: 'admin',
    use: {
      storageState: 'playwright/.auth/admin.json',
      ...devices['Desktop Chrome'],
    },
  },
  {
    name: 'customer',
    use: {
      storageState: 'playwright/.auth/customer.json',
      ...devices['Desktop Chrome'],
    },
  },
],
```

### OAuth / 2FA Handling

For flows requiring manual interaction:
1. Create auth profiles manually once via `npx playwright codegen --save-storage=auth.json`
2. Store the profile in CI as a secured artifact or encrypted file
3. Rotate auth profiles weekly or when passwords change
4. Never commit auth storage files to version control

---

## §4 Visual Regression Testing

### Screenshot Comparison

```typescript
import { test, expect } from '@playwright/test';

test('checkout page matches baseline', async ({ page }) => {
  await page.goto('/checkout');
  await expect(page).toHaveScreenshot('checkout-page.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.02,  // Allow 2% pixel difference
  });
});
```

### Visual Comparison Strategies

| Strategy | When to Use | Trade-off |
|----------|-------------|-----------|
| Full-page | Layout regressions | Sensitive to any change |
| Element-level | Specific component changes | More targeted, less noise |
| Masked regions | Dynamic content (dates, ads) | Requires maintaining mask list |
| Threshold-based | Slightly dynamic UIs | Risk of missing real regressions |

### Element-Level Snapshots (Preferred)

```typescript
// Prefer element-level over full-page for stability
await expect(page.getByTestId('pricing-card')).toHaveScreenshot();
await expect(page.getByRole('navigation')).toHaveScreenshot('nav.png');

// Mask dynamic content
await expect(page.getByTestId('dashboard')).toHaveScreenshot({
  mask: [page.getByTestId('timestamp'), page.getByTestId('avatar')],
});
```

---

## §5 Component Testing

Playwright supports component testing for React, Vue, Svelte, and Solid:

```typescript
// Button.spec.tsx — React component test
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('fires click handler', async ({ mount }) => {
  let clicked = false;
  const component = await mount(
    <Button onClick={() => { clicked = true; }}>Click me</Button>
  );
  await component.click();
  expect(clicked).toBe(true);
});

test('renders disabled state', async ({ mount }) => {
  const component = await mount(<Button disabled>Disabled</Button>);
  await expect(component).toBeDisabled();
});
```

### When to Use Component Tests vs E2E

- **Component tests:** Interaction logic, states, edge cases, accessibility of isolated components
- **E2E tests:** User flows spanning multiple pages, API integration, auth flows, critical paths
- **Rule of thumb:** Test at the lowest level that provides confidence. Component tests for
  component logic, E2E for integration points.

---

## §6 Mobile & Device Emulation

```typescript
// playwright.config.ts — device projects
projects: [
  {
    name: 'mobile-chrome',
    use: { ...devices['Pixel 7'] },
  },
  {
    name: 'mobile-safari',
    use: { ...devices['iPhone 14 Pro'] },
  },
  {
    name: 'tablet',
    use: { ...devices['iPad Pro 11'] },
  },
],

// Custom viewport for responsive breakpoints
test.use({ viewport: { width: 375, height: 812 } });

test('mobile navigation collapses to hamburger', async ({ page }) => {
  await page.goto('/');
  await expect(page.getByRole('navigation')).not.toBeVisible();
  await page.getByRole('button', { name: 'Menu' }).click();
  await expect(page.getByRole('navigation')).toBeVisible();
});
```

### Touch & Gesture Testing

```typescript
// Swipe gestures
await page.getByTestId('carousel').swipe('left');
await page.getByTestId('carousel').tap({ position: { x: 100, y: 50 } });

// Pinch zoom
await page.getByTestId('map').pinch({ scale: 2.0 });

// Geolocation mocking
await page.context().grantPermissions(['geolocation']);
await page.context().setGeolocation({ latitude: 48.8566, longitude: 2.3522 });
```

---

## §7 Debugging & Flaky Tests

### Debugging Checklist (Follow in Order)

1. **Run with `--debug`:** `npx playwright test --debug` — step through visually
2. **Add `page.pause()`:** Insert `await page.pause()` before failing line
3. **Check trace:** `npx playwright test --trace on` then `npx playwright show-trace`
4. **Review video:** Enable `video: 'on-first-retry'` in config
5. **Isolate the test:** Run with `.only` to check for test interaction
6. **Check for race conditions:** Missing `await`, missing waitFor, incorrect waitFor state
7. **Verify locators:** Use `--ui` mode to inspect selectors live

### Flaky Test Detection Pattern

```typescript
// Detect flakes by running repeatedly
test.describe('Flaky detection', () => {
  test.describe.configure({ retries: 0 }); // Disable retries to expose flakes

  test('suspicious test', async ({ page }) => {
    // ... test body ...
  });
});

// CLI: npx playwright test --repeat-each=10 suspicious.spec.ts
```

### Common Flakiness Causes & Fixes

| Cause | Symptom | Fix |
|-------|---------|-----|
| No auto-wait | Timeout waiting for element | Use auto-waiting locators (getByRole, etc.) |
| Hard `page.waitForTimeout()` | Unreliable timing | Replace with `waitForSelector`, `waitForResponse` |
| Shared state between tests | Tests pass/fail based on order | Use fixtures, reset state in beforeEach |
| Network-dependent | Passes locally, fails in CI | Mock API responses with `page.route()` |
| Animation interference | Elements not interactable | `await element.scrollIntoViewIfNeeded()` |
| Timezone-sensitive | Date assertions fail at certain times | Mock `Date` or use fixed timezone |

### Auto-Healing Test Pattern

```typescript
test('resilient form submission with retry', async ({ page }) => {
  await page.goto('/contact');

  // Auto-heal: if submit is temporarily disabled, wait for it
  const submit = page.getByRole('button', { name: 'Send' });
  await expect(submit).toBeEnabled({ timeout: 10000 });

  await page.getByLabel('Name').fill('Test User');
  await page.getByLabel('Email').fill('test@example.com');
  await submit.click();

  // Wait for either success or retry-able error
  await expect(
    page.getByText(/message sent|try again/i)
  ).toBeVisible({ timeout: 15000 });
});
```

---

## §8 CI/CD Configuration

### GitHub Actions — Optimal Configuration

```yaml
# .github/workflows/playwright.yml
name: Playwright E2E Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]  # 4-way sharding
        shardTotal: [4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report-${{ matrix.shardIndex }}
          path: playwright-report/
          retention-days: 7
```

### Parallelization & Sharding

```typescript
// playwright.config.ts — CI-optimized
export default defineConfig({
  workers: process.env.CI ? 4 : undefined,
  retries: process.env.CI ? 2 : 0,
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'test-results/results.json' }],
    ['github'],  // GitHub Actions annotations
  ],
  use: {
    screenshot: 'only-on-failure',
    video: 'on-first-retry',
    trace: 'on-first-retry',
    actionTimeout: 15000,
  },
});
```

### CI Best Practices

- **Shard tests** across multiple runners for large suites (>100 tests)
- **Cache Playwright browsers** between runs for speed
- **Use `fail-fast: false`** to see all failures, not just the first
- **Set `timeout-minutes`** to catch hung test suites
- **Store reports as artifacts** for post-run debugging
- **Run full suite on main, smoke tests on PRs**
- **Use retries in CI but not locally** (to detect flakes early)

---

## §9 Accessibility Testing

```typescript
// integrate axe-core for automated a11y checks
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage has no critical a11y violations', async ({ page }) => {
  await page.goto('/');

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .exclude('[data-testid="third-party-widget"]')  // Skip third-party
    .analyze();

  expect(results.violations).toEqual([]);
});

// Test keyboard navigation
test('full keyboard navigation through checkout', async ({ page }) => {
  await page.goto('/checkout');
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Name')).toBeFocused();
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Email')).toBeFocused();
  // ... continue through entire flow
});
```

---

## §10 Performance Testing

```typescript
import { test } from '@playwright/test';
import { playAudit } from 'playwright-lighthouse';
import playwright from 'playwright';

test('homepage meets Lighthouse performance thresholds', async () => {
  const browser = await playwright.chromium.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/');

  await playAudit({
    page,
    thresholds: {
      performance: 90,
      accessibility: 95,
      'best-practices': 90,
      seo: 90,
    },
    port: 9222,
  });

  await browser.close();
});
```

### Web Vitals Assertions

```typescript
test('LCP is under 2.5s', async ({ page }) => {
  await page.goto('/heavy-page');
  const lcp = await page.evaluate(() => {
    return new Promise<number>((resolve) => {
      new PerformanceObserver((list) => {
        const entries = list.getEntries();
        resolve(entries[entries.length - 1].startTime);
      }).observe({ type: 'largest-contentful-paint', buffered: true });
    });
  });
  expect(lcp).toBeLessThan(2500);
});
```

---

## §11 Internationalization (i18n) Testing

```typescript
test('supports RTL layout for Arabic', async ({ page }) => {
  // Override browser locale
  await page.context().route('**/*', (route) => {
    const headers = { ...route.request().headers(), 'Accept-Language': 'ar' };
    route.continue({ headers });
  });

  await page.goto('/');

  // Verify RTL direction
  const html = page.locator('html');
  await expect(html).toHaveAttribute('dir', 'rtl');

  // Verify content is translated
  await expect(page.getByText('مرحباً')).toBeVisible();
});
```

---

## §12 Security Testing

```typescript
test('XSS payload is sanitized in search', async ({ page }) => {
  await page.goto('/search');
  const xssPayload = '<img src=x onerror=alert(1)>';

  await page.getByRole('searchbox').fill(xssPayload);
  await page.getByRole('button', { name: 'Search' }).click();

  // Verify the payload is rendered as text, not executed
  const results = page.getByTestId('search-results');
  await expect(results).toContainText(xssPayload);
  // Alert should NOT have appeared (would fail if XSS succeeds)
});

test('CSP headers are present', async ({ page }) => {
  const response = await page.goto('/');
  const csp = response?.headers()['content-security-policy'];
  expect(csp).toBeDefined();
  expect(csp).toContain("default-src 'self'");
});

test('CSRF token present on forms', async ({ page }) => {
  await page.goto('/profile/edit');
  const csrfInput = page.locator('input[name="csrf_token"], input[name="_csrf"]');
  await expect(csrfInput).toBeAttached();
  const token = await csrfInput.inputValue();
  expect(token.length).toBeGreaterThan(32);
});

test('Auth redirects work correctly', async ({ page }) => {
  await page.goto('/admin/dashboard');
  // Should redirect to login if unauthenticated
  await expect(page).toHaveURL(/\/login/);
});
```

---

## §13 WebSocket & Real-Time Testing

```typescript
test('receives real-time order update via WebSocket', async ({ page }) => {
  const wsPromise = page.waitForEvent('websocket');
  await page.goto('/orders/live');

  const ws = await wsPromise;
  const messagePromise = ws.waitForEvent('framereceived');

  // Trigger an update from another source
  await page.evaluate(() => {
    fetch('/api/orders/123/status', {
      method: 'PATCH',
      body: JSON.stringify({ status: 'shipped' }),
    });
  });

  const message = await messagePromise;
  const data = JSON.parse(message.payload as string);
  expect(data.status).toBe('shipped');
});
```

---

## §14 Electron & Browser Extension Testing

### Electron Testing

```typescript
import { _electron as electron } from '@playwright/test';

test('Electron app launches and shows main window', async () => {
  const app = await electron.launch({
    args: ['main.js'],
  });

  const window = await app.firstWindow();
  await expect(window.getByText('My App')).toBeVisible();

  await app.close();
});
```

### Extension Testing

```typescript
test('browser extension icon appears', async ({ page, context }) => {
  // Load extension via background page
  const background = await context.newPage();
  await background.goto(`chrome-extension://${EXTENSION_ID}/background.html`);
  // Verify extension functionality
  await page.goto('https://example.com');
  // ... verify extension modifies page as expected
});
```

---

## Reference Files

This skill includes detailed reference documents loaded on-demand:

- `references/locator-strategies.md` — Complete locator priority guide with anti-patterns
- `references/ci-cd-patterns.md` — CI/CD configurations for GitHub Actions, GitLab CI, CircleCI, Bitbucket
- `references/testing-types-matrix.md` — When to use E2E vs component vs API vs visual vs a11y testing

## Scripts

- `scripts/validate-playwright-setup.sh` — Validates Playwright installation and browser availability
- `scripts/generate-auth-profile.ts` — Generates and saves authentication profiles for test use
- `scripts/flake-detector.sh` — Runs a test suite repeatedly to detect intermittent failures

## Trigger Guidance

This skill activates when the user or agent is:
- Writing, reviewing, or refactoring Playwright test files
- Debugging test failures or investigating flaky tests
- Setting up Playwright in a new project or CI pipeline
- Asking about browser automation testing patterns
- Migrating from Cypress, Selenium, or Puppeteer to Playwright
- Implementing Page Object Model patterns
- Configuring test parallelization and sharding
- Setting up visual regression or component testing with Playwright
- Testing mobile-responsive layouts or device emulation
- Performing accessibility or performance audits via Playwright
- Testing Electron applications or browser extensions
- Writing security tests for web applications