smithery/williamzujkowski

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.

Installation

$ npx skills add smithery/williamzujkowski --skill e2e-testing-standards

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/williamzujkowski · top by installs.

npx skills add smithery/williamzujkowski

Browse all from smithery/williamzujkowski

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 10,621 B
  • docs SUMMARY.md 231 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

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

// 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('[email protected]', '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

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

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

npm install --save-dev cypress
npx cypress open

Configuration

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

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

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

// 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('[email protected]');

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

// 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)
// 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

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

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

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

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

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

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

# 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

export const testUsers = {
  admin: { email: '[email protected]', password: 'Admin123!', role: 'admin' },
  user: { email: '[email protected]', password: 'User123!', role: 'user' },
};

API Setup/Teardown

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

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

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

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:

Related Skills

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