Claude Code Plugins

Community-maintained marketplace

Feedback

End-to-end testing with Playwright. Use when writing E2E tests, implementing page objects, setting up test fixtures, handling authentication in tests, adding visual regression testing, or configuring E2E tests for CI. Covers when to use E2E vs integration tests, critical path identification, page object pattern, test isolation, and CI considerations.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name e2e-test
description End-to-end testing with Playwright. Use when writing E2E tests, implementing page objects, setting up test fixtures, handling authentication in tests, adding visual regression testing, or configuring E2E tests for CI. Covers when to use E2E vs integration tests, critical path identification, page object pattern, test isolation, and CI considerations.

E2E Testing with Playwright

E2E tests verify critical user journeys through the entire stack. Highest confidence, slowest to run, most expensive to maintain.

When to Use E2E

Use for: Critical revenue flows, auth flows, multi-step journeys, paths that cannot break in production.

Don't use for: Business logic (unit), API validation (integration), edge cases (unit/integration), component behavior (component tests).

Rule: If integration tests catch it, don't write E2E. E2E is for happy paths and critical flows only.

Critical Path Identification

Tier 1 (Must): Signup/signin, core product actions, revenue flows Tier 2 (Should): Team features, secondary features, admin ops Tier 3 (Nice): Settings, edge workflows, power user features

Start with Tier 1. One signup test > 20 preference tests.

Page Object Pattern

Encapsulate page selectors and interactions for maintainability.

export class LoginPage {
  constructor(private readonly page: Page) {}

  readonly emailInput = () => this.page.locator('input[type="email"]');
  readonly submitButton = () => this.page.locator('button[type="submit"]');

  async goto() { await this.page.goto('/sign-in'); }
  async login(email: string) {
    await this.emailInput().fill(email);
    await this.submitButton().click();
  }
  async assertLoggedIn() { await this.page.waitForURL('/workouts'); }
}

Principles:

  • Selectors as methods (lazy, prevents stale elements)
  • Actions return void
  • Assertions in page objects only if reused
  • One page object per logical page, not per route

Test Fixtures & Setup

// fixtures/auth.ts
export const TEST_USER = { email: 'e2e@test.com', password: 'Test123!' };

export async function loginAsTestUser(page: Page) {
  await page.goto('/sign-in');
  await page.getByLabel(/email/i).fill(TEST_USER.email);
  await page.getByRole('button', { name: /sign in/i }).click();
  await page.waitForURL(/\/(workouts|dashboard)/);
}
// global-setup.ts - runs once before all tests
async function globalSetup() { await seedTestUsers(); }

Key: Pre-seed test users in global setup. Faster and more reliable than creating per-test.

Test Isolation

Strategy 1: Reset per test (slowest) - beforeEach(() => resetDB()) Strategy 2: Unique data (recommended) - const name = \Test-${Date.now()}`` Strategy 3: Read-only (fastest) - global seed, no mutations

Use Strategy 2. Move to 1 only if tests interfere.

Authentication

Strategy 1: Login each test - Simple, slow (2-3s/test) Strategy 2: Session storage - Save once, reuse. Caveat: expires mid-run Strategy 3: API auth - Fastest, bypass UI

// Strategy 2: global-setup.ts
await page.context().storageState({ path: '.auth/user.json' });

// Strategy 3: beforeEach
const session = await createSessionViaAPI(TEST_USER);
await page.context().addCookies([{ name: 'session', value: session.token }]);

Use Strategy 3 for most tests. Use Strategy 1 only to test login flow itself.

Visual Regression

test('page looks correct', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('baseline.png');
});

Best practices:

  • Disable animations: page.emulateMedia({ reducedMotion: 'reduce' })
  • Stabilize dynamic content (mock timestamps/randomness)
  • Use sparingly (brittle, slow)
  • Focus on critical UI only (login, checkout)

Don't screenshot every page. Do screenshot where design regressions are risky.

CI Configuration

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? '50%' : undefined,
  reporter: process.env.CI ? [['blob'], ['github']] : [['html']],
});

Flakiness mitigation:

  • Explicit waits: await page.waitForSelector('.result') before asserting
  • Semantic selectors: getByRole('button', { name: /submit/i })
  • Retry in CI: 2 retries catches transient issues

Speed:

  • Parallelize (4+ workers in CI)
  • API for setup (bypass UI)
  • Shard across machines
  • Run on critical branches only (main, release)

Decision Tree

Single function → Unit | API endpoint → Integration | Multi-page flow → E2E | Business logic → Integration

WODsmith Structure

e2e/
├── fixtures/auth.ts      # Login helpers
├── pages/login.ts        # Page objects
├── auth.spec.ts          # Test specs
├── global-setup.ts       # DB seeding
└── playwright.config.ts

Global setup seeds DB. Fixtures provide auth. Page objects encapsulate UI. Specs test journeys.

Common Pitfalls

Testing too much: 10-step test fails at step 7 → unclear what broke. Split into focused tests.

No cleanup: Use unique data (Test-${Date.now()}) instead of cleanup. No conflicts, faster.

Implicit waits: Always waitForSelector() before assertions. Prevents race conditions.

References

See WODsmith e2e/ directory for real patterns: fixtures, page objects, global setup.