PluginBench
Skill
Review
Audit score 70

e2e-testing

affaan-m/ecc

Playwright E2E testing patterns with Page Object Model, CI/CD integration, and flaky test strategies.

What is e2e-testing?

Comprehensive guide to building stable, maintainable end-to-end test suites using Playwright. Covers test organization, Page Object Model architecture, configuration for multiple browsers, artifact management, and proven techniques for identifying and fixing flaky tests.

  • Organize tests by feature with clear directory structure and fixtures
  • Implement Page Object Model for reusable, maintainable test code
  • Configure Playwright for multiple browsers (Chrome, Firefox, Safari, mobile) and CI/CD environments
  • Capture screenshots, videos, and traces for debugging failed tests
  • Identify and quarantine flaky tests with repeat runs and conditional skips
  • Handle race conditions, network timing, and animation delays with proper waits

How to install e2e-testing

npx skills add null --skill e2e-testing
Prerequisites
  • Node.js 18+
  • Playwright installed (npx playwright install)
  • A running web application or dev server
Claude Code
Cursor
Windsurf
Cline

How to use e2e-testing

  1. 1.Create a tests/e2e directory and organize test files by feature (auth, features, api)
  2. 2.Define Page Object classes in tests/pages/ to encapsulate page interactions and locators
  3. 3.Write test specs using test.describe() and test() with beforeEach setup hooks
  4. 4.Configure playwright.config.ts with baseURL, reporters, projects (browsers), and webServer settings
  5. 5.Run tests locally with npx playwright test and review HTML reports in playwright-report/
  6. 6.Identify flaky tests by running npx playwright test --repeat-each=10 on suspect specs
  7. 7.Fix flakiness by replacing arbitrary waits with proper locator waits and response waits
  8. 8.Set up CI/CD with .github/workflows/e2e.yml to run tests on push and upload artifacts

Use cases

Good for
  • Set up E2E test suites for web applications with multi-browser coverage
  • Debug test failures using screenshots, videos, and execution traces
  • Integrate Playwright tests into GitHub Actions CI/CD pipelines
  • Test complex user flows like authentication, search, and transactions
  • Identify root causes of intermittent test failures and apply targeted fixes
Who it's for
  • QA engineers building automated test suites
  • Frontend developers implementing E2E tests in their projects
  • DevOps engineers integrating tests into CI/CD pipelines
  • Teams working on financial or Web3 applications requiring critical flow testing

e2e-testing FAQ

How do I avoid race conditions in tests?

Use Playwright's auto-waiting locators (page.locator().click()) instead of manual clicks, and wait for specific network responses or load states rather than arbitrary timeouts.

What should I do when a test is flaky?

Use test.fixme() or test.skip() to quarantine it, run npx playwright test --repeat-each=10 to reproduce the issue, then identify the root cause (animation timing, network race, etc.) and apply the appropriate wait strategy.

How do I test wallet connections or Web3 interactions?

Use context.addInitScript() to mock window.ethereum or other providers before navigation, then verify the UI responds correctly to the mocked responses.

Can I run tests on multiple browsers in CI/CD?

Yes, define multiple projects in playwright.config.ts (chromium, firefox, webkit, mobile) and Playwright will run all of them. In CI, set workers to 1 to avoid resource contention.

How do I capture artifacts for failed tests?

Configure screenshot: 'only-on-failure', video: 'retain-on-failure', and trace: 'on-first-retry' in playwright.config.ts, then upload the playwright-report/ and artifacts/ directories in your CI workflow.

Full instructions (SKILL.md)

Source of truth, from affaan-m/ecc.


name: e2e-testing description: Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies. metadata: origin: ECC

E2E Testing Patterns

Comprehensive Playwright patterns for building stable, fast, and maintainable E2E test suites.

Test File Organization

tests/
├── e2e/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   ├── logout.spec.ts
│   │   └── register.spec.ts
│   ├── features/
│   │   ├── browse.spec.ts
│   │   ├── search.spec.ts
│   │   └── create.spec.ts
│   └── api/
│       └── endpoints.spec.ts
├── fixtures/
│   ├── auth.ts
│   └── data.ts
└── playwright.config.ts

Page Object Model (POM)

import { Page, Locator } from '@playwright/test'

export class ItemsPage {
  readonly page: Page
  readonly searchInput: Locator
  readonly itemCards: Locator
  readonly createButton: Locator

  constructor(page: Page) {
    this.page = page
    this.searchInput = page.locator('[data-testid="search-input"]')
    this.itemCards = page.locator('[data-testid="item-card"]')
    this.createButton = page.locator('[data-testid="create-btn"]')
  }

  async goto() {
    await this.page.goto('/items')
    await this.page.waitForLoadState('networkidle')
  }

  async search(query: string) {
    await this.searchInput.fill(query)
    await this.page.waitForResponse(resp => resp.url().includes('/api/search'))
    await this.page.waitForLoadState('networkidle')
  }

  async getItemCount() {
    return await this.itemCards.count()
  }
}

Test Structure

import { test, expect } from '@playwright/test'
import { ItemsPage } from '../../pages/ItemsPage'

test.describe('Item Search', () => {
  let itemsPage: ItemsPage

  test.beforeEach(async ({ page }) => {
    itemsPage = new ItemsPage(page)
    await itemsPage.goto()
  })

  test('should search by keyword', async ({ page }) => {
    await itemsPage.search('test')

    const count = await itemsPage.getItemCount()
    expect(count).toBeGreaterThan(0)

    await expect(itemsPage.itemCards.first()).toContainText(/test/i)
    await page.screenshot({ path: 'artifacts/search-results.png' })
  })

  test('should handle no results', async ({ page }) => {
    await itemsPage.search('xyznonexistent123')

    await expect(page.locator('[data-testid="no-results"]')).toBeVisible()
    expect(await itemsPage.getItemCount()).toBe(0)
  })
})

Playwright Configuration

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', { outputFolder: 'playwright-report' }],
    ['junit', { outputFile: 'playwright-results.xml' }],
    ['json', { outputFile: 'playwright-results.json' }]
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    actionTimeout: 10000,
    navigationTimeout: 30000,
  },
  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'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
})

Flaky Test Patterns

Quarantine

test('flaky: complex search', async ({ page }) => {
  test.fixme(true, 'Flaky - Issue #123')
  // test code...
})

test('conditional skip', async ({ page }) => {
  test.skip(process.env.CI, 'Flaky in CI - Issue #123')
  // test code...
})

Identify Flakiness

npx playwright test tests/search.spec.ts --repeat-each=10
npx playwright test tests/search.spec.ts --retries=3

Common Causes & Fixes

Race conditions:

// Bad: assumes element is ready
await page.click('[data-testid="button"]')

// Good: auto-wait locator
await page.locator('[data-testid="button"]').click()

Network timing:

// Bad: arbitrary timeout
await page.waitForTimeout(5000)

// Good: wait for specific condition
await page.waitForResponse(resp => resp.url().includes('/api/data'))

Animation timing:

// Bad: click during animation
await page.click('[data-testid="menu-item"]')

// Good: wait for stability
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
await page.locator('[data-testid="menu-item"]').click()

Artifact Management

Screenshots

await page.screenshot({ path: 'artifacts/after-login.png' })
await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true })
await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' })

Traces

await browser.startTracing(page, {
  path: 'artifacts/trace.json',
  screenshots: true,
  snapshots: true,
})
// ... test actions ...
await browser.stopTracing()

Video

// In playwright.config.ts
use: {
  video: 'retain-on-failure',
  videosPath: 'artifacts/videos/'
}

CI/CD Integration

# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
        env:
          BASE_URL: ${{ vars.STAGING_URL }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Test Report Template

# E2E Test Report

**Date:** YYYY-MM-DD HH:MM
**Duration:** Xm Ys
**Status:** PASSING / FAILING

## Summary
- Total: X | Passed: Y (Z%) | Failed: A | Flaky: B | Skipped: C

## Failed Tests

### test-name
**File:** `tests/e2e/feature.spec.ts:45`
**Error:** Expected element to be visible
**Screenshot:** artifacts/failed.png
**Recommended Fix:** [description]

## Artifacts
- HTML Report: playwright-report/index.html
- Screenshots: artifacts/*.png
- Videos: artifacts/videos/*.webm
- Traces: artifacts/*.zip

Wallet / Web3 Testing

test('wallet connection', async ({ page, context }) => {
  // Mock wallet provider
  await context.addInitScript(() => {
    window.ethereum = {
      isMetaMask: true,
      request: async ({ method }) => {
        if (method === 'eth_requestAccounts')
          return ['0x1234567890123456789012345678901234567890']
        if (method === 'eth_chainId') return '0x1'
      }
    }
  })

  await page.goto('/')
  await page.locator('[data-testid="connect-wallet"]').click()
  await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234')
})

Financial / Critical Flow Testing

test('trade execution', async ({ page }) => {
  // Skip on production — real money
  test.skip(process.env.NODE_ENV === 'production', 'Skip on production')

  await page.goto('/markets/test-market')
  await page.locator('[data-testid="position-yes"]').click()
  await page.locator('[data-testid="trade-amount"]').fill('1.0')

  // Verify preview
  const preview = page.locator('[data-testid="trade-preview"]')
  await expect(preview).toContainText('1.0')

  // Confirm and wait for blockchain
  await page.locator('[data-testid="confirm-trade"]').click()
  await page.waitForResponse(
    resp => resp.url().includes('/api/trade') && resp.status() === 200,
    { timeout: 30000 }
  )

  await expect(page.locator('[data-testid="trade-success"]')).toBeVisible()
})