Skip to content
Back to skills

Playwright E2e Patterns

ASecurity

Playwright E2E testing patterns. Use when playwright, E2E test, toBeVisible, route mock, modal, flaky test, or selector issues.

  • 5 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 2, 2026
developmenttypescriptgobashtestingdebuggingapi

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned September 2, 2026

npx -y skills add kookr-ai/kookr --skill playwright-e2e-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Playwright E2e Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Playwright E2e Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kookr-ai-playwright-e2e-patterns/badge)](https://www.skillsdirectory.com/skills/kookr-ai-playwright-e2e-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: playwright-e2e-patterns
description: Playwright E2E testing patterns. Use when playwright, E2E test, toBeVisible, route mock, modal, flaky test, or selector issues.
keywords: playwright, e2e, toBeVisible, route-mock, modal, flaky-test, firefox, selector, data-testid, storageState, serial
related: testing-patterns
---

# Playwright E2E Testing Patterns

E2E testing patterns for the Kookr dashboard.

## Quick Commands

```bash
npx playwright test                          # All tests (config at repo root, testDir ./e2e)
npx playwright test e2e/canary.spec.ts       # Specific file
npx playwright test --headed                 # See browser
npx playwright test --debug                  # Inspector
```

## Quick Reference

| Problem | Cause | Fix |
|---------|-------|-----|
| Clicks wrong modal | Generic `.modal` selector | Use `[data-modal="name"]` or `data-testid` |
| Mock doesn't work | Set up after navigation | Mock BEFORE `page.goto()` |
| Broad mock catches specific routes | Pattern too wide | Register specific routes first |
| Assertion fails on async content | Content loads after render | Use `toBeVisible({ timeout })` or `waitForResponse()` |
| Welcome modal blocks clicks | First-visit modal | Set localStorage via `addInitScript()` |
| Firefox-only failures | Timing differences | Use specific selectors + explicit waits |
| Serial tests conflict | Browser projects run parallel | Use `workers: 1` |
| Count is 0 but elements exist | Feature changed DOM structure | Inspect actual DOM with `innerHTML()` |
| WS message silently dropped | WS not connected when sent | Wait for connection indicator before sending |
| State leaks between tests | Reset endpoint incomplete | Clear ALL stateful subsystems in reset |

## Selector Strategy

Prefer `data-testid`, role-based (`getByRole('button', { name: 'Submit' })`). Avoid CSS classes (`.modal`, `.button`). Be specific: `[data-modal="delete-confirmation"]` not `.modal`.

## Route Mocking

Mock BEFORE navigation:
```typescript
await page.route('**/api/v1/tasks', route => route.fulfill({ json: mockTasks }));
await page.goto('http://localhost:30080');
```

Specific routes first:
```typescript
await page.route('**/api/v1/tasks/123', route => route.fulfill({ json: specificTask }));
await page.route('**/api/v1/tasks', route => route.fulfill({ json: taskList }));
```

Fallthrough pattern:
```typescript
await page.route('**/api/v1/**', route => {
  const url = route.request().url();
  if (url.includes('/tasks')) route.fulfill({ json: mockTasks });
  else if (url.includes('/workflows')) route.fulfill({ json: mockWorkflows });
  else route.continue();
});
```

## Welcome Modal Handling

```typescript
// Set localStorage before navigation
await page.addInitScript(() => localStorage.setItem('aegis_welcome_dismissed', 'true'));
await page.goto('/');

// Or use storageState
test.use({
  storageState: { cookies: [], origins: [{ origin: 'http://localhost:30080', localStorage: [{ name: 'aegis_welcome_dismissed', value: 'true' }] }] }
});

// Or dismiss if present
const modal = page.locator('[data-modal="welcome"]');
if (await modal.isVisible({ timeout: 1000 })) await modal.getByRole('button', { name: 'Close' }).click();
```

## Waiting for Dynamic Content

```typescript
await page.goto('http://localhost:30080');
await page.waitForResponse('**/api/v1/tasks');
await expect(page.locator('.task-item')).toHaveCount(5);
await expect(page.locator('.task-item').first()).toBeVisible({ timeout: 10000 });
```

## Debugging Failing Assertions

When a DOM assertion (`toHaveCount`, `toBeVisible`) fails, **don't increase the timeout first**. Follow this sequence:

1. **Inspect the actual DOM** — `await page.locator('.parent').innerHTML()` or `page.evaluate(() => document.body.innerHTML)`. A count of 0 could mean "not arrived yet" OR "rendered as a different element."
2. **Check server state** — `await request.get('/api/state')` to confirm the data exists server-side. If the server has the data but the DOM doesn't, it's a client delivery or rendering issue.
3. **Check for feature interactions** — grouping, filtering, collapsing, project selection can all replace expected elements with different ones. Same data, different DOM structure.
4. **Add intermediate assertions in loops** — when creating N items in a loop and asserting count at the end, assert after each iteration to find the exact step where things diverge.
5. **Only then consider timeouts** — if server state is correct AND the DOM structure matches expectations, a timeout increase may be warranted.

## WebSocket-Dependent Tests

When tests trigger server actions via REST and then assert on DOM that updates via WebSocket:

- **Wait for WS connection** before any WS-dependent action — check for a connection indicator element (e.g., `.health-dot-connected`)
- **After page reload** (`page.goto('/')`), the WS reconnects asynchronously — wait for the connection indicator, not just the page content
- **REST returns ≠ client received** — `injectEvent` returning `{ ok: true }` means the server processed it and called `ws.send()`, but the browser may not have received/rendered it yet
- **Assert intermediate state** — after each inject, wait for the corresponding DOM change before injecting the next event

## Test Isolation

Reset endpoints must clear ALL stateful subsystems, not just the obvious ones:
- Monitor (agents)
- Queue (anomalies, snoozed entries)
- Task store
- Watchdog state
- Terminal sessions
- Hook watchers

Missing any subsystem causes state leakage between tests — symptoms appear as unexpected counts, stale data, or "impossible" states.

## Best Practices

- Mock API responses for predictable state
- Run `workers: 1` if tests modify shared state
- Test Chromium, Firefox, WebKit; Firefox needs more explicit waits
- Debug: `page.on('console', msg => console.log(msg.text()))`
- Screenshot on failure: `testInfo.status !== 'passed' && page.screenshot()`
- Breakpoint: `await page.pause()`

## See Also

vitest-bun-mocking, e2e-test-troubleshooting, api-route-patterns

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.