E2E Testing Patterns

August 6, 2026 · View on GitHub

Practical patterns for Playwright E2E tests (Web only). Complements e2e-testing.md.

iOS projects: Skip this file. For XCUITest patterns, simulator management, lifecycle testing, and the Apple platform test checklist, see ios-patterns.md § Apple Platform Testing.

Contents

Core Rules

Authentication: API First

// ✅ Fast: 3-5s saved per test
test.beforeEach(async ({ page, context }) => {
  await authenticateViaAPI(context)
  await page.goto(baseURL)
})

// ❌ Never use UI login in tests

Credentials: Environment Variables

const { username, password } = getTestCredentials()  // ✅
// ❌ Never hardcode: 'admin', 'password123'

State Restoration

let changedData: string | null = null

test.afterEach(async ({ page }) => {
  if (changedData) {
    await restoreState(page, changedData)
    changedData = null
  }
  // ❌ Never: catch (e) { console.log(e) }  // Silent failure pollutes tests
})

Timeout Strategy

OperationTimeoutRationale
Element visible2000msNormal UI
Dialog/animation2000msWith transition
Cleanup500msFast fail
API call5000msNetwork tolerance
Single test15000msMax limit

Anti-Patterns

PatternProblemFix
40-line defensive codeOver-engineeringSimple assertions
reload() then goto()Redundantreload() only
Promise.race with .catch(() => null)Silent failureVerify at least one succeeds
beforeAll authPage mismatchUse beforeEach
waitForTimeout()FlakyProper assertions

Debug Flow

Test slow/timeout?
├─ curl API → returns data?
├─ Create debug test → console errors?
└─ Root cause: test issue or app bug?

Key insight: Slow test often = app bug, not test issue

Selector Priority

  1. getByRole('button', { name: 'Submit' }) — Semantic
  2. getByText('Submit') — Text content
  3. locator('button:has-text("Submit")') — CSS + text
  4. locator('.submit-btn') — Class
  5. locator('#submit') — ID
  6. locator('div > button:nth-child(2)') — Structure (last resort)

Test Execution Strategy

Failure Interruption Principle

When a group of E2E tests has a significant failure rate (>30%), immediately stop testing, fix the issues, then resume.

Applicable scenarios:

  • Running a group of E2E tests (e.g., auth group, config group, custom action group)
  • Multiple consecutive failures within the same test group
  • Failure rate exceeds 30% threshold

Golden Ratio Thresholds:

See fail-fast.md for recommended thresholds.

Why interrupt:

  1. Save time - Avoid wasting precious E2E test time on tests destined to fail
  2. Fast feedback - Identify and fix root causes early, not accumulating errors
  3. Avoid test noise - Prevent连锁 failures from early failures
  4. Improve efficiency - Focus on core issues rather than blindly fixing symptoms

Example:

# ✅ Correct: Stop when failure rate > 30%
npm run test:e2e
# Fail 7/20 (35%) > 30%, stop immediately
# Fix issues → rerun
npm run test:e2e

# ❌ Wrong: Blindly run all tests, waste 2 hours
npm run test:e2e
# Find 15/20 tests failing, continue waiting...

理论基础: This principle aligns with the "Fail Fast" principle in software engineering, a mature testing strategy best practice.


Wait Strategy

// ✅ Playwright auto-waiting
await expect(element).toBeVisible()
await expect(element).toHaveText('Expected')

// ❌ Never
await page.waitForTimeout(1000)

The Web architecture-contract gate (dependency-cruiser + eslint concurrency baseline) has moved to web-patterns.md → Architecture-Contract Gate (Web / TypeScript). This file is now Playwright/E2E-specific.