Claude Code Plugins

Community-maintained marketplace

Feedback

e2e-testing-standards

@williamzujkowski/standards
5
0

Playwright

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-testing-standards
category testing
difficulty intermediate
estimated_time 45 minutes
prerequisites Basic JavaScript/TypeScript, Understanding of web applications, Familiarity with testing concepts
learning_outcomes Implement robust E2E tests with Playwright or Cypress, Apply Page Object Model pattern effectively, Create maintainable test suites, Prevent flaky tests, Integrate E2E testing into CI/CD pipelines
related_skills unit-testing, integration-testing, accessibility-testing
tags e2e-testing, playwright, cypress, automation, page-object-model
description Playwright

E2E Testing Standards

End-to-end testing validates complete user workflows across the entire application stack, simulating real user interactions to ensure system reliability.

Level 1: Quick Reference

Framework Comparison

Playwright

  • Multi-browser (Chromium, Firefox, WebKit)
  • Auto-waiting built-in
  • Network interception
  • Multiple contexts/pages
  • Best for: Complex scenarios, cross-browser testing

Cypress

  • Chromium-based (Firefox experimental)
  • Time-travel debugging
  • Real-time reloads
  • Simpler API
  • Best for: Rapid development, visual debugging

Page Object Model Pattern

// Page Object
class LoginPage {
  constructor(private page: Page) {}

  async login(username: string, password: string) {
    await this.page.fill('[data-testid="username"]', username);
    await this.page.fill('[data-testid="password"]', password);
    await this.page.click('[data-testid="login-button"]');
  }
}

// Test
test('user can login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login('user@example.com', 'password');
  await expect(page).toHaveURL('/dashboard');
});

Essential Checklist

Selectors (Priority Order)

  • Use data-testid attributes first
  • Use accessibility roles/labels second
  • Avoid CSS classes and IDs
  • Never use XPath unless absolutely necessary

Waits

  • Rely on framework auto-waiting (Playwright/Cypress)
  • Use explicit waits for specific conditions
  • Avoid hard-coded sleep() or wait(ms)
  • Wait for network idle when needed

Assertions

  • Assert on meaningful user-visible state
  • Check URL changes for navigation
  • Verify element visibility/presence
  • Validate text content, not structure

Test Data

  • Use test-specific data (not production)
  • Clean up after tests (database, files)
  • Avoid dependencies between tests
  • Use API calls for setup when possible

Flakiness Prevention

  • Never use fixed delays
  • Handle async operations properly
  • Mock external dependencies
  • Run tests in isolation

Level 2: Implementation Guide

Test Framework Selection

Playwright Setup

Installation

npm init playwright@latest
# Select: TypeScript, tests folder, GitHub Actions

Project Structure

tests/
├── e2e/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   └── registration.spec.ts
│   ├── checkout/
│   │   └── purchase-flow.spec.ts
│   └── fixtures/
│       └── test-data.ts
├── page-objects/
│   ├── LoginPage.ts
│   ├── DashboardPage.ts
│   └── BasePage.ts
└── playwright.config.ts

Configuration (see config/playwright.config.ts)

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['junit', { outputFile: 'test-results/junit.xml' }]
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
  webServer: {
    command: 'npm run start',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

Cypress Setup

Installation

npm install --save-dev cypress
npx cypress open  # First-time setup wizard

Project Structure

cypress/
├── e2e/
│   ├── auth/
│   │   ├── login.cy.ts
│   │   └── registration.cy.ts
│   └── checkout/
│       └── purchase-flow.cy.ts
├── support/
│   ├── commands.ts
│   ├── e2e.ts
│   └── page-objects/
│       ├── LoginPage.ts
│       └── DashboardPage.ts
├── fixtures/
│   └── users.json
└── cypress.config.ts

Configuration (see config/cypress.config.ts)

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
  retries: {
    runMode: 2,
    openMode: 0,
  },
});

Page Object Model Implementation

Base Page Pattern

// BasePage.ts
import { Page } from '@playwright/test';

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

  async navigate(path: string = '') {
    await this.page.goto(path);
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }

  async getPageTitle(): Promise<string> {
    return await this.page.title();
  }

  async takeScreenshot(name: string) {
    await this.page.screenshot({
      path: `screenshots/${name}.png`,
      fullPage: true
    });
  }
}

Feature-Specific Page Objects

// LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  // Locators
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    super(page);
    this.usernameInput = page.getByTestId('username-input');
    this.passwordInput = page.getByTestId('password-input');
    this.loginButton = page.getByRole('button', { name: 'Log in' });
    this.errorMessage = page.getByTestId('error-message');
  }

  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async loginWithEnter(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.passwordInput.press('Enter');
  }

  async getErrorText(): Promise<string> {
    return await this.errorMessage.textContent() || '';
  }

  async isErrorVisible(): Promise<boolean> {
    return await this.errorMessage.isVisible();
  }
}

Complete example in templates/page-object.ts

Locator Strategies

Priority Order

1. Test IDs (Recommended)

// HTML
<button data-testid="submit-button">Submit</button>

// Playwright
await page.getByTestId('submit-button').click();

// Cypress
cy.get('[data-testid="submit-button"]').click();

2. Accessibility Attributes

// By role
await page.getByRole('button', { name: 'Submit' }).click();
cy.findByRole('button', { name: 'Submit' }).click();

// By label
await page.getByLabel('Email address').fill('user@example.com');
cy.findByLabelText('Email address').type('user@example.com');

// By placeholder
await page.getByPlaceholder('Enter your email').fill('user@example.com');

3. Text Content

// Exact text
await page.getByText('Welcome back').click();
cy.contains('Welcome back').click();

// Partial text
await page.getByText(/Welcome/i).click();
cy.contains(/Welcome/i).click();

4. CSS Selectors (Last Resort)

// Use only when above options aren't available
await page.locator('.submit-btn.primary').click();
cy.get('.submit-btn.primary').click();

Locator Best Practices

// ✅ Good: Resilient to UI changes
await page.getByRole('navigation').getByRole('link', { name: 'Products' });

// ❌ Bad: Brittle, breaks with styling changes
await page.locator('#nav > ul > li:nth-child(2) > a');

// ✅ Good: Semantic and accessible
await page.getByLabel('Search').fill('laptops');

// ❌ Bad: Depends on implementation details
await page.locator('input[name="q"][type="text"]').fill('laptops');

Waits and Timing

Auto-Waiting (Playwright/Cypress)

Both frameworks automatically wait for elements to be:

  • Present in DOM
  • Visible
  • Enabled (for interactions)
  • Stable (not animating)
// No explicit wait needed - framework handles it
await page.click('button'); // Waits for button to be clickable
cy.get('button').click();   // Waits for button to be clickable

Explicit Waits

Wait for Element State

// Playwright
await page.waitForSelector('[data-testid="results"]', { state: 'visible' });
await page.waitForSelector('[data-testid="loader"]', { state: 'hidden' });

// Cypress
cy.get('[data-testid="results"]').should('be.visible');
cy.get('[data-testid="loader"]').should('not.exist');

Wait for Network

// Playwright - Wait for API response
await page.waitForResponse(
  response => response.url().includes('/api/users') && response.status() === 200
);

// Cypress - Intercept and wait
cy.intercept('GET', '/api/users').as('getUsers');
cy.wait('@getUsers');

Wait for Navigation

// Playwright
await Promise.all([
  page.waitForNavigation(),
  page.click('a[href="/dashboard"]')
]);

// Cypress
cy.get('a[href="/dashboard"]').click();
cy.url().should('include', '/dashboard');

Custom Conditions

// Playwright - Wait for custom condition
await page.waitForFunction(() => {
  return document.querySelectorAll('.item').length > 5;
});

// Cypress - Wait for custom condition
cy.get('.item').should('have.length.greaterThan', 5);

Anti-Patterns

// ❌ NEVER: Hard-coded delays
await page.waitForTimeout(5000); // Brittle and slow
cy.wait(5000); // Brittle and slow

// ✅ BETTER: Wait for specific condition
await page.waitForSelector('[data-testid="loaded"]');
cy.get('[data-testid="loaded"]').should('be.visible');

// ❌ NEVER: Polling loops
while (!(await page.isVisible('.result'))) {
  await page.waitForTimeout(100);
}

// ✅ BETTER: Built-in waiting
await page.waitForSelector('.result', { state: 'visible', timeout: 10000 });

Visual Regression Testing

Playwright Visual Comparisons

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

test('homepage visual regression', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixels: 100, // Allow minor differences
    threshold: 0.2,     // 20% pixel difference threshold
  });
});

test('component visual regression', async ({ page }) => {
  await page.goto('/products');
  const productCard = page.locator('[data-testid="product-card"]').first();
  await expect(productCard).toHaveScreenshot('product-card.png');
});

Update Baselines

# Generate new baseline screenshots
npx playwright test --update-snapshots

# Update specific test
npx playwright test homepage.spec.ts --update-snapshots

Percy Integration (Cypress/Playwright)

// Install: npm install --save-dev @percy/playwright
import percySnapshot from '@percy/playwright';

test('visual test with Percy', async ({ page }) => {
  await page.goto('/');
  await percySnapshot(page, 'Homepage');
});
# Run with Percy
PERCY_TOKEN=your_token npx percy exec -- playwright test

Cross-Browser Testing

Playwright Multi-Browser

// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
});

Run Specific Browsers

# All browsers
npx playwright test

# Specific browser
npx playwright test --project=firefox

# Multiple browsers
npx playwright test --project=chromium --project=webkit

Browser-Specific Tests

test('feature only available in chromium', async ({ page, browserName }) => {
  test.skip(browserName !== 'chromium', 'Chromium-only feature');

  // Test chromium-specific feature
  await page.evaluate(() => {
    // Use chromium-specific API
  });
});

CI/CD Integration

GitHub Actions

# .github/workflows/e2e-tests.yml
name: E2E Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.browser }}

      - name: Run E2E tests
        run: npx playwright test --project=${{ matrix.browser }}
        env:
          BASE_URL: http://localhost:3000

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
          retention-days: 30

Docker Integration

# Dockerfile.e2e
FROM mcr.microsoft.com/playwright:v1.40.0-focal

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]
# docker-compose.e2e.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test

  e2e-tests:
    build:
      dockerfile: Dockerfile.e2e
    depends_on:
      - app
    environment:
      - BASE_URL=http://app:3000
    volumes:
      - ./test-results:/app/test-results

Complete script in scripts/run-e2e-tests.sh

Test Data Management

Fixtures and Factories

// fixtures/test-data.ts
export const testUsers = {
  admin: {
    email: 'admin@test.com',
    password: 'Admin123!',
    role: 'admin',
  },
  user: {
    email: 'user@test.com',
    password: 'User123!',
    role: 'user',
  },
};

export const testProducts = [
  { id: 1, name: 'Laptop', price: 999.99 },
  { id: 2, name: 'Mouse', price: 29.99 },
];

API Setup/Teardown

// Use API for data setup (faster than UI)
import { test as base } from '@playwright/test';

type TestFixtures = {
  authenticatedUser: { token: string; userId: string };
};

export const test = base.extend<TestFixtures>({
  authenticatedUser: async ({ request }, use) => {
    // Setup: Create user via API
    const response = await request.post('/api/auth/register', {
      data: {
        email: `test-${Date.now()}@example.com`,
        password: 'Test123!',
      },
    });
    const { token, userId } = await response.json();

    // Provide to test
    await use({ token, userId });

    // Teardown: Delete user via API
    await request.delete(`/api/users/${userId}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
  },
});

// Usage
test('user can update profile', async ({ page, authenticatedUser }) => {
  // Set authentication token
  await page.goto('/');
  await page.evaluate((token) => {
    localStorage.setItem('auth_token', token);
  }, authenticatedUser.token);

  await page.goto('/profile');
  // ... test continues
});

Database Seeding

// helpers/database.ts
import { PrismaClient } from '@prisma/client';

export class TestDatabase {
  private prisma = new PrismaClient();

  async seed() {
    await this.prisma.user.createMany({
      data: testUsers,
    });
    await this.prisma.product.createMany({
      data: testProducts,
    });
  }

  async reset() {
    await this.prisma.user.deleteMany();
    await this.prisma.product.deleteMany();
  }

  async cleanup() {
    await this.reset();
    await this.prisma.$disconnect();
  }
}

// In test
test.beforeEach(async () => {
  const db = new TestDatabase();
  await db.seed();
});

test.afterEach(async () => {
  const db = new TestDatabase();
  await db.cleanup();
});

Flaky Test Prevention

Common Causes and Solutions

1. Race Conditions

// ❌ Flaky: Might click before element is ready
await page.goto('/dashboard');
await page.click('[data-testid="menu-button"]');

// ✅ Stable: Wait for specific state
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.getByTestId('menu-button').click(); // Auto-waits

2. Timing Issues

// ❌ Flaky: Hardcoded wait
await page.fill('[data-testid="search"]', 'query');
await page.waitForTimeout(2000); // Arbitrary delay
await page.click('[data-testid="first-result"]');

// ✅ Stable: Wait for specific element
await page.fill('[data-testid="search"]', 'query');
await page.waitForSelector('[data-testid="first-result"]', { state: 'visible' });
await page.click('[data-testid="first-result"]');

3. Test Interdependence

// ❌ Flaky: Tests share state
test('create item', async ({ page }) => {
  await page.goto('/items');
  await page.click('[data-testid="add-item"]');
  // Creates item in shared database
});

test('list items', async ({ page }) => {
  await page.goto('/items');
  await expect(page.getByTestId('item-list')).toContainText('New item');
  // Depends on previous test!
});

// ✅ Stable: Each test is independent
test('create item', async ({ page }) => {
  await createItemViaAPI(); // Setup
  await page.goto('/items');
  await expect(page.getByTestId('item-list')).toContainText('New item');
  await deleteItemViaAPI(); // Teardown
});

4. Animation Interference

// ❌ Flaky: Click during animation
await page.click('[data-testid="modal-button"]');
await page.click('[data-testid="modal-close"]'); // Might miss if modal is animating

// ✅ Stable: Wait for stable state
await page.click('[data-testid="modal-button"]');
await page.waitForSelector('[data-testid="modal"]', { state: 'visible' });
await page.getByTestId('modal-close').click(); // Playwright waits for stability

5. Network Instability

// ❌ Flaky: Depends on real API
test('loads user data', async ({ page }) => {
  await page.goto('/profile');
  await expect(page.getByTestId('username')).toContainText('John');
});

// ✅ Stable: Mock API responses
test('loads user data', async ({ page }) => {
  await page.route('/api/user', route => {
    route.fulfill({
      status: 200,
      body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
    });
  });

  await page.goto('/profile');
  await expect(page.getByTestId('username')).toContainText('John');
});

Complete anti-flakiness guide in resources/e2e-best-practices.md

Advanced Patterns

Authentication State Reuse

// auth.setup.ts - Run once, save state
import { test as setup } from '@playwright/test';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="username"]', process.env.TEST_USER!);
  await page.fill('[data-testid="password"]', process.env.TEST_PASSWORD!);
  await page.click('[data-testid="login-button"]');

  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: 'auth-state.json' });
});

// playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /auth\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'auth-state.json', // Reuse auth state
      },
      dependencies: ['setup'], // Run setup first
    },
  ],
});

Parallel Execution with Test Sharding

# Split tests across 4 machines
npx playwright test --shard=1/4  # Machine 1
npx playwright test --shard=2/4  # Machine 2
npx playwright test --shard=3/4  # Machine 3
npx playwright test --shard=4/4  # Machine 4

Network Mocking

test('handles API error gracefully', async ({ page }) => {
  // Mock failed API response
  await page.route('/api/products', route => {
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Internal Server Error' }),
    });
  });

  await page.goto('/products');
  await expect(page.getByTestId('error-message'))
    .toContainText('Failed to load products');
});

Accessibility Testing Integration

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage should not have accessibility violations', async ({ page }) => {
  await page.goto('/');

  const accessibilityScanResults = await new AxeBuilder({ page }).analyze();

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

Level 3: Deep Dive Resources

Official Documentation

Books and Courses

Testing Patterns

Tools and Extensions

Community Resources

CI/CD Integration Guides

Examples

Basic Usage

// TODO: Add basic example for e2e-testing
// This example demonstrates core functionality

Advanced Usage

// TODO: Add advanced example for e2e-testing
// This example shows production-ready patterns

Integration Example

// TODO: Add integration example showing how e2e-testing
// works with other systems and services

See examples/e2e-testing/ for complete working examples.

Integration Points

This skill integrates with:

Upstream Dependencies

  • Tools: Common development tools and frameworks
  • Prerequisites: Basic understanding of general concepts

Downstream Consumers

  • Applications: Production systems requiring e2e-testing functionality
  • CI/CD Pipelines: Automated testing and deployment workflows
  • Monitoring Systems: Observability and logging platforms

Related Skills

  • See other skills in this category

Common Integration Patterns

  1. Development Workflow: How this skill fits into daily development
  2. Production Deployment: Integration with production systems
  3. Monitoring & Alerting: Observability integration points

Common Pitfalls

Pitfall 1: Insufficient Testing

Problem: Not testing edge cases and error conditions leads to production bugs

Solution: Implement comprehensive test coverage including:

  • Happy path scenarios
  • Error handling and edge cases
  • Integration points with external systems

Prevention: Enforce minimum code coverage (80%+) in CI/CD pipeline

Pitfall 2: Hardcoded Configuration

Problem: Hardcoding values makes applications inflexible and environment-dependent

Solution: Use environment variables and configuration management:

  • Separate config from code
  • Use environment-specific configuration files
  • Never commit secrets to version control

Prevention: Use tools like dotenv, config validators, and secret scanners

Pitfall 3: Ignoring Security Best Practices

Problem: Security vulnerabilities from not following established security patterns

Solution: Follow security guidelines:

  • Input validation and sanitization
  • Proper authentication and authorization
  • Encrypted data transmission (TLS/SSL)
  • Regular security audits and updates

Prevention: Use security linters, SAST tools, and regular dependency updates

Best Practices:

  • Follow established patterns and conventions for e2e-testing
  • Keep dependencies up to date and scan for vulnerabilities
  • Write comprehensive documentation and inline comments
  • Use linting and formatting tools consistently
  • Implement proper error handling and logging
  • Regular code reviews and pair programming
  • Monitor production metrics and set up alerts


Bundled Resources

See accompanying files:

  1. config/playwright.config.ts - Production-ready Playwright configuration
  2. config/cypress.config.ts - Production-ready Cypress configuration
  3. templates/page-object.ts - Complete Page Object Model template
  4. templates/test-template.spec.ts - Full test suite template
  5. scripts/run-e2e-tests.sh - CI/CD test execution script
  6. resources/e2e-best-practices.md - Comprehensive anti-flakiness guide