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- Node.js 18+
- Playwright installed (npx playwright install)
- A running web application or dev server
How to use e2e-testing
- 1.Create a tests/e2e directory and organize test files by feature (auth, features, api)
- 2.Define Page Object classes in tests/pages/ to encapsulate page interactions and locators
- 3.Write test specs using test.describe() and test() with beforeEach setup hooks
- 4.Configure playwright.config.ts with baseURL, reporters, projects (browsers), and webServer settings
- 5.Run tests locally with npx playwright test and review HTML reports in playwright-report/
- 6.Identify flaky tests by running npx playwright test --repeat-each=10 on suspect specs
- 7.Fix flakiness by replacing arbitrary waits with proper locator waits and response waits
- 8.Set up CI/CD with .github/workflows/e2e.yml to run tests on push and upload artifacts
Use cases
- 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
- 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
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.
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.
Use context.addInitScript() to mock window.ethereum or other providers before navigation, then verify the UI responds correctly to the mocked responses.
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.
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()
})
Related skills
More from affaan-m/ecc and the wider catalog.
ecc-guide
Navigate ECC's agents, skills, commands, hooks, and install profiles by reading live repository state.
ecc-recipes
Agent skill from affaan-m/ecc.
ecc-tools-cost-audit
Audit ECC Tools GitHub App cost spikes, PR recursion, quota bypass, and premium-model leakage.
email-ops
Evidence-first email triage, drafting, and send verification for organized mailbox workflows.
energy-procurement
Codified energy procurement expertise for tariff optimization, demand charge management, and multi-facility cost strategy.
enterprise-agent-ops
Operate long-lived agent workloads with observability, security boundaries, and lifecycle management.