---
name: playwright-skill
effort: medium
description: E2E browser testing with Playwright Test CLI. Write .spec.ts files in tests/e2e/, run via `npm run test:e2e`. Supports headed mode, debug, UI mode, filtering, retries, screenshots, and trace on failure. Use when user wants to test websites, automate browser interactions, validate web functionality, or perform any browser-based testing.
---

# Playwright E2E Testing (CLI)

All browser testing uses the **Playwright Test CLI** via `playwright.config.ts` at project root. Tests live in `${paths.e2e_tests_dir}` (default convention `tests/e2e/`).

**NEVER use ad-hoc scripts to `/tmp` as a substitute.** All tests MUST be `.spec.ts` files under `${paths.e2e_tests_dir}/`.

## Project Context

**Reads from `baldart.config.yml`:** `paths.e2e_tests_dir`, `paths.design_system`, `stack.testing.e2e`.
**Gated by features:** `features.has_design_system` (Design System Compliance section becomes BLOCKING when `true`), `features.multi_tenant_theming` (theming assertion rule activates when `true`).
**Refuses to run when** `stack.testing.e2e` is set to something other than `"playwright"` or empty (warn and ask). Empty = ask user once and persist.
**Overlay:** loads `.baldart/overlays/playwright-skill.md` if present — project-specific test URLs, auth helpers, theming pairing assertions.
**On missing/empty keys:** ask the user; do not assume defaults. See `framework/agents/project-context.md` § 3.

## Effort

**Baseline:** `effort: medium` (frontmatter). **Inline override:** pass
`effort=<low|medium|high|xhigh|max>` anywhere in the invocation to scale
reasoning depth for this run — detect it once at kickoff and strip the token
before consuming user input. Level→behavior mapping, parsing contract, and
precedence caveats: `framework/agents/effort-protocol.md`.


## MCP dependencies

See [framework/docs/MCP-INTEGRATION.md](../../../docs/MCP-INTEGRATION.md) for the BALDART MCP integration contract and the rationale behind this section.

**None.** This skill uses the **Playwright Test CLI** (`@playwright/test`) directly via `npm run test:e2e` — `.spec.ts` files under `${paths.e2e_tests_dir}`. It does NOT use Playwright MCP. Consumers don't need any MCP configured to use this skill. For ad-hoc interactive browser debugging during bug investigation (not E2E test authoring) see [bug/SKILL.md](../bug/SKILL.md), which DOES require `mcp__playwright__browser_*`.

## E2E Review Integration (since v3.18.0)

This skill is the tooling layer consumed by `/e2e-review` Phase 3 (functional
E2E). When the orchestrator invokes this skill, the contract is:

- The `coder` agent generates the `.spec.ts` under `${paths.e2e_tests_dir}/`
  from a Gherkin verification plan persisted at
  `.baldart/e2e-review/<CARD-ID>/plan.json` (do not invent scenarios outside
  the plan).
- The spec MUST screenshot every route in scope and save the file at
  `.baldart/e2e-review/<CARD-ID>/screenshots/<route-slug>.png` — that path is
  consumed verbatim by the `visual-fidelity-verifier` agent in Phase 4.
- Execution is **headless** in programmatic mode (invocation from `/new`);
  headed mode is reserved for OTP flows during manual `/e2e-review` runs.
- Failed scenarios become functional findings in the aggregate report; the
  severity is determined by the orchestrator (auth/payments/data-mutation =
  Critical; display-only = Major; flaky-passed-on-retry = Minor).

No logic of this skill changes for E2E-review consumption — the consumer
simply requires the conventions above. See
[`framework/.claude/skills/e2e-review/SKILL.md`](../e2e-review/SKILL.md) Phase 3
for the full contract.

## Quick Reference

| Action | Command |
|--------|---------|
| Run all e2e tests | `npm run test:e2e` |
| Run headed (visible browser) | `npm run test:e2e:headed` |
| Run with UI mode | `npm run test:e2e:ui` |
| Debug with Inspector | `npm run test:e2e:debug` |
| Run single file | `npx playwright test tests/e2e/my-test.spec.ts` |
| Run by title regex | `npx playwright test -g "login flow"` |
| Run specific project | `npx playwright test --project=chromium` |
| Show HTML report | `npm run test:e2e:report` |
| Update snapshots | `npx playwright test -u` |

## Writing Tests

Tests go in `tests/e2e/*.spec.ts` using `@playwright/test` API:

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

test('homepage loads correctly', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle(/Fidelity/);
  await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});

test('login redirects to dashboard', async ({ page }) => {
  await page.goto('/login');
  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL(/dashboard/);
});
```

### Key differences from the old approach

| Old (run.js) | New (Playwright CLI) |
|-------------|---------------------|
| `const { chromium } = require('playwright')` | `import { test, expect } from '@playwright/test'` |
| `chromium.launch({ headless: false })` | Configured in `playwright.config.ts`, use `--headed` flag |
| `browser.newPage()` | `page` fixture injected automatically |
| `browser.close()` | Automatic cleanup via fixtures |
| `console.log('passed')` | `expect(locator).toBeVisible()` with auto-waiting |
| Write to `/tmp/*.js` | Write to `tests/e2e/*.spec.ts` |
| `cd $SKILL_DIR && node run.js` | `npx playwright test` |

### Responsive testing with projects

The config defines `chromium` (desktop) and `mobile` (iPhone 14) projects. Run both:

```bash
npx playwright test --project=chromium --project=mobile
```

Or just mobile:

```bash
npx playwright test --project=mobile
```

### Grouping with describe

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

test.describe('Booking flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/<project-specific-test-route>'); // see .baldart/overlays/playwright-skill.md for project routes
  });

  test('shows date picker on load', async ({ page }) => {
    await expect(page.getByTestId('date-picker')).toBeVisible();
  });

  test('advances to time slot step', async ({ page }) => {
    await page.getByTestId('date-picker').click();
    await page.getByText('18').click();
    await page.getByTestId('next-button').click();
    await expect(page.getByTestId('time-slots')).toBeVisible();
  });
});
```

## Configuration

The `playwright.config.ts` at project root manages all settings:

- **baseURL**: `http://localhost:3000` (from `BASE_URL` env var)
- **webServer**: auto-starts `npm run dev` if not already running
- **screenshot**: only on failure
- **trace**: on first retry
- **projects**: `chromium` (Desktop Chrome) + `mobile` (iPhone 14)
- **retries**: 0 locally, 2 on CI
- **headed**: visible locally, headless on CI

## Debugging

```bash
# Open Playwright Inspector (step-through, element picker)
npm run test:e2e:debug

# Interactive UI mode (run/rerun tests, time-travel, DOM inspector)
npm run test:e2e:ui

# Run headed with slowMo for visual debugging
npx playwright test --headed -- --slow-mo=500
```

## CI Integration

On CI, tests run headless with retries and produce HTML + JSON reports:

```bash
CI=true npm run test:e2e
```

Trace files and screenshots from failures are saved in `test-results/`.

## Tips

- Use `data-testid` attributes for reliable selectors
- Prefer `page.getByRole()`, `page.getByText()`, `page.getByTestId()` over CSS selectors
- Use `expect(locator)` assertions — they auto-wait (no manual `waitForSelector`)
- Use `test.describe()` to group related tests with shared `beforeEach`
- Use `test.skip()` or `test.fixme()` for tests that aren't ready
- Use `--only-changed` to run only tests affected by git changes

## Design System Compliance (UI validation)

When `features.has_design_system: true`, E2E tests that validate UI components MUST be grounded in the Design System SSOT so that assertions reflect canonical variants and states, not ad-hoc expectations:

1. Before writing a spec that asserts visual/component behavior, read `${paths.design_system}/INDEX.md` and the relevant `${paths.design_system}/components/<Name>.md` to confirm variants, states, and required props.
2. Assert against documented variants: e.g., `expect(button).toHaveAttribute('data-variant', 'primary')` rather than matching a specific color literal.
3. When `features.multi_tenant_theming: true`: validate token-pair presence per the project's theming pattern doc (bg class + matching text class). The exact assertion helper is listed in `.baldart/overlays/playwright-skill.md`.
4. Motion-sensitive flows MUST include a reduced-motion run (`test.use({ reducedMotion: 'reduce' })`) per the project's motion pattern doc — reduced-motion variants are mandatory, and tests must prove they exist.
5. Skipping this step when the spec validates UI is a protocol violation.
