e2e-testing-standards

Implement robust E2E tests with Playwright or Cypress using Page Object Model, proper waits, and CI/CD integration. Covers selector strategies, flaky test prevention, and cross-browser testing patterns.

Best use case

e2e-testing-standards is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Implement robust E2E tests with Playwright or Cypress using Page Object Model, proper waits, and CI/CD integration. Covers selector strategies, flaky test prevention, and cross-browser testing patterns.

Teams using e2e-testing-standards should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/e2e-testing/SKILL.md --create-dirs "https://raw.githubusercontent.com/williamzujkowski/standards/main/skills/testing/e2e-testing/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/e2e-testing/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How e2e-testing-standards Compares

Feature / Agente2e-testing-standardsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Implement robust E2E tests with Playwright or Cypress using Page Object Model, proper waits, and CI/CD integration. Covers selector strategies, flaky test prevention, and cross-browser testing patterns.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# E2E Testing Standards

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

## Quick Reference

### Framework Comparison

| Feature | Playwright | Cypress |
|---------|-----------|---------|
| Browsers | Chromium, Firefox, WebKit | Chromium (Firefox experimental) |
| Auto-waiting | Built-in | Built-in |
| Debugging | Trace viewer | Time-travel |
| Best for | Cross-browser, complex scenarios | Rapid development, visual debugging |

### Page Object Model Pattern

```typescript
// 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)**

1. `data-testid` attributes (most reliable)
2. Accessibility roles/labels
3. Text content
4. CSS selectors (last resort)

**Waits**

- Rely on framework auto-waiting
- Use explicit waits for specific conditions
- Never use hard-coded `sleep()` or `wait(ms)`

**Test Data**

- Use test-specific data (not production)
- Clean up after tests
- Use API calls for setup when possible

---

## Framework Setup

### Playwright Setup

```bash
npm init playwright@latest
```

**Project Structure**

```
tests/
  e2e/
    auth/login.spec.ts
    fixtures/test-data.ts
  page-objects/
    LoginPage.ts
    BasePage.ts
playwright.config.ts
```

**Configuration**

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

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  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'] } },
  ],
});
```

### Cypress Setup

```bash
npm install --save-dev cypress
npx cypress open
```

**Configuration**

```typescript
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
  },
  retries: { runMode: 2, openMode: 0 },
});
```

---

## Page Object Model

### Base Page Pattern

```typescript
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');
  }
}
```

### Feature Page Object

```typescript
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  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 getErrorText(): Promise<string> {
    return await this.errorMessage.textContent() || '';
  }
}
```

---

## Locator Strategies

### Best Practices

```typescript
// 1. Test IDs (Recommended)
await page.getByTestId('submit-button').click();
cy.get('[data-testid="submit-button"]').click();

// 2. Accessibility Attributes
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email address').fill('user@example.com');

// 3. Text Content
await page.getByText('Welcome back').click();
cy.contains('Welcome back').click();

// 4. CSS Selectors (Last Resort)
await page.locator('.submit-btn.primary').click();
```

### Good vs Bad Examples

```typescript
// 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

Both frameworks automatically wait for elements to be:

- Present in DOM
- Visible
- Enabled (for interactions)
- Stable (not animating)

```typescript
// No explicit wait needed
await page.click('button'); // Waits for button to be clickable
cy.get('button').click();   // Waits for button to be clickable
```

### Explicit Waits

```typescript
// Wait for element state
await page.waitForSelector('[data-testid="results"]', { state: 'visible' });
cy.get('[data-testid="results"]').should('be.visible');

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

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

### Anti-Patterns

```typescript
// NEVER: Hard-coded delays
await page.waitForTimeout(5000); // Brittle and slow

// BETTER: Wait for specific condition
await page.waitForSelector('[data-testid="loaded"]');
```

---

## Flaky Test Prevention

### Common Causes and Solutions

**1. Race Conditions**

```typescript
// Flaky
await page.goto('/dashboard');
await page.click('[data-testid="menu-button"]');

// Stable
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.getByTestId('menu-button').click();
```

**2. Test Interdependence**

```typescript
// Flaky: Tests share state
test('create item', async ({ page }) => {
  // Creates item in shared database
});

test('list items', async ({ page }) => {
  // Depends on previous test!
});

// Stable: Each test is independent
test('create item', async ({ page }) => {
  await createItemViaAPI(); // Setup
  await page.goto('/items');
  await deleteItemViaAPI(); // Teardown
});
```

**3. Network Instability**

```typescript
// Flaky: Depends on real API
await page.goto('/profile');
await expect(page.getByTestId('username')).toContainText('John');

// Stable: Mock API responses
await page.route('/api/user', route => {
  route.fulfill({
    status: 200,
    body: JSON.stringify({ name: 'John' }),
  });
});
await page.goto('/profile');
```

---

## CI/CD Integration

### GitHub Actions

```yaml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }}
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
```

### Running Tests

```bash
# All browsers
npx playwright test

# Specific browser
npx playwright test --project=firefox

# Test sharding (parallel machines)
npx playwright test --shard=1/4
```

---

## Test Data Management

### Fixtures

```typescript
export const testUsers = {
  admin: { email: 'admin@test.com', password: 'Admin123!', role: 'admin' },
  user: { email: 'user@test.com', password: 'User123!', role: 'user' },
};
```

### API Setup/Teardown

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

export const test = base.extend({
  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();

    await use({ token, userId });

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

---

## Advanced Patterns

### Authentication State Reuse

```typescript
// auth.setup.ts
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.context().storageState({ path: 'auth-state.json' });
});
```

### Network Mocking

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

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

### Accessibility Testing

```typescript
import AxeBuilder from '@axe-core/playwright';

test('homepage has no accessibility violations', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});
```

---

## Deep Dive Resources

For complete examples and advanced configurations, see:

- **REFERENCE.md** - Complete test suites, advanced page objects, visual testing, Docker integration
- **Playwright Docs**: https://playwright.dev/docs/intro
- **Cypress Docs**: https://docs.cypress.io/guides/overview/why-cypress

### Related Skills

- unit-testing
- integration-testing
- accessibility-testing

Related Skills

unit-testing

13
from williamzujkowski/standards

Unit testing standards following TDD methodology, test pyramid principles, and comprehensive coverage practices. Covers pytest, Jest, mocking, fixtures, and CI integration for reliable test suites.

performance-testing-standards

13
from williamzujkowski/standards

Performance testing standards for load, stress, spike, and soak testing. Covers k6 and JMeter implementation, SLI/SLO definitions, CI/CD integration, and bottleneck analysis. Use when validating system behavior under load, establishing baselines, or capacity planning.

integration-testing

13
from williamzujkowski/standards

Integration testing standards for API testing, database testing, and service-to-service communication. Covers test containers, Docker Compose, API mocking, and contract testing for reliable integration suites.

testing

13
from williamzujkowski/standards

Comprehensive testing standards including unit, integration, security, and property-based testing with TDD methodology

typescript-coding-standards

13
from williamzujkowski/standards

TypeScript coding standards covering strict type system, advanced types, decorators, generics, and best practices for type-safe applications. Use for TypeScript projects requiring robust type safety and maintainable code.

swift-coding-standards

13
from williamzujkowski/standards

Master Swift coding standards with Apple's guidelines, protocol-oriented design, and modern concurrency patterns

shell-scripting-standards

13
from williamzujkowski/standards

Essential patterns for reliable shell scripts - portable shebangs, error handling, quoting rules, functions, testing with Bats, and ShellCheck integration.

rust-coding-standards

13
from williamzujkowski/standards

Master Rust's ownership system, type safety, and zero-cost abstractions for building safe, concurrent, and performant systems. Covers borrowing, lifetimes, traits, error handling, async/await, and testing patterns.

python-coding-standards

13
from williamzujkowski/standards

Python coding standards following PEP 8, type hints, testing best practices, and modern Python patterns. Use for Python projects requiring clean, maintainable, production-ready code with comprehensive testing.

kotlin-coding-standards

13
from williamzujkowski/standards

Master Kotlin coding standards with null safety, coroutines, and idiomatic patterns. Use when developing JVM/Android applications requiring type-safe async programming.

javascript-coding-standards

13
from williamzujkowski/standards

JavaScript/ES6+ coding standards following Airbnb guidelines, modern patterns, React best practices, and comprehensive Jest testing. Use for JavaScript projects requiring clean, maintainable, production-ready code with modern tooling.

go-coding-standards

13
from williamzujkowski/standards

Go coding standards following idiomatic Go patterns, error handling, concurrency best practices, and modern Go tooling. Use for Go projects requiring clean, efficient, production-ready code with comprehensive testing.