Tier 1 · This Document
Core
15 files
Always loaded. Utilities, strategy, risk scoring, resilience, and quality standards.
Tier 2
Extended
22 files
Loaded on demand. Auth session, API patterns, CI strategies, feature flags, email auth.
Tier 3
Specialized
16 files
Deep-dive for specific use cases: contract testing with Pact.js, webhook test infrastructure.
15Core Files
4Sections
9Utilities
P0–P3Priority Levels

§1 — Utilities & Infrastructure

Files 01 – 04
01
playwright-utils Library Overview
overview.md

What it is: @seontechnologies/playwright-utils is the shared toolbox used across all TEA-managed test suites. It wraps common testing operations — HTTP calls, authentication, polling, file handling, network monitoring — into single-import utilities so teams stop re-solving the same problems project to project.

Design philosophy: Functional Core, Fixture Shell. Each utility is a pure TypeScript function at its core, then wrapped in a Playwright fixture for clean dependency injection. Full TypeScript support with typed return values throughout.

The 9 Core Utilities

apiRequest
Send HTTP requests with auto-parsed JSON, automatic retry on 5xx errors, and optional Zod/JSON Schema response validation. No browser required.
API · Backend
auth-session
Acquire a login token once and cache it. All tests reuse the cached token — no repeated login flows. Supports multiple simultaneous users by identifier key.
API · Backend · UI
intercept-network-call
Spy on or stub HTTP requests during UI tests. Returns a Promise that resolves with the actual request/response when the matched URL is hit.
UI Only
network-recorder
Record full network traffic to a HAR file, then replay it offline. Supports CRUD state simulation during playback — something native Playwright HAR cannot do.
UI Only
recurse
Smart polling: retry an action repeatedly until a predicate returns true. Returns typed error shapes (timeout, command error, predicate error) for precise failure diagnosis.
API · Backend
log
6-level structured logging (info, step, success, warning, error, debug) that surfaces in Playwright's HTML report — not just the terminal. Automatically formats objects as JSON.
API · Backend · UI
file-utils
Read and validate downloaded files in 1–2 lines. Supports CSV (with header detection), Excel/XLSX (multi-sheet), PDF (text extraction, page count), and ZIP (entry listing, extraction).
API · Backend · UI
network-error-monitor
Auto-fail any test where a background API returns 4xx/5xx — works like Sentry but for your test suite. Just import from its fixture and it activates automatically.
UI Only
burn-in
Git-diff-aware CI runner. Analyzes changed files, maps them to dependent tests via import graph, and runs only the relevant subset with configurable repeat count. Avoids running 500 tests when 5 changed.
CI/CD

Importing utilities — pattern

// Each utility has its own fixture subpath export
import { test as apiTest }  from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as authTest }  from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { mergeTests }        from '@playwright/test';

// Merge all into one test object (see fixture-architecture.md)
export const test = mergeTests(apiTest, authTest);

Quick-reference: before vs after

TaskWithout playwright-utilsWith playwright-utils
Download & parse CSV~80 lines: handle download event, save file, wait for fs, parse, error-handle2 lines: handleDownload + readCSV
Login in every testUI form fill in each test (~10 s each)Token cached on first run; reused in < 1 ms
Wait for background jobwaitForTimeout(10000) or custom looprecurse(() => checkJob(), r => r.status === 'done')
Detect silent API errorsManually assert every response statusImport network-error-monitor fixture — automatic
Installation: npm install @seontechnologies/playwright-utils. All utilities are tree-shakeable — only what you import is bundled.
02
Fixture Architecture
fixture-architecture.md

What it is: A pattern for organizing shared test setup code (fixtures) so that common needs — login state, clean databases, seed data, test users — are written once and reused everywhere without duplication.

Why it matters: Without this pattern, every test file duplicates the same login, setup, and teardown code. When the API changes, you update 50 files instead of 1. Fixtures are the solution — but only if structured correctly.

The Three-Layer Pattern

Layer 1
Pure Function — The core logic with no Playwright dependency. Takes inputs, returns outputs. Can be unit-tested directly. Example: createUserViaApi(userData) that calls the REST endpoint and returns the created user ID.
Layer 2
Fixture Wrapper — Wraps the pure function so Playwright's test() can inject it. Handles setup (use()) and teardown (code after await use()). Never contains test assertions.
Layer 3
Package Subpath Export — Exports the wrapped fixture from a shared location (tests/support/merged-fixtures.ts) so every test file imports from one place. When the fixture changes, only the shared file changes.

Full code example

// Layer 1: pure function (tests/support/factories/user.factory.ts)
export async function createUserViaApi(data: Partial<User>) {
  const res = await fetch('/api/users', {
    method: 'POST', body: JSON.stringify({ ...defaults, ...data })
  });
  return res.json() as User;
}

// Layer 2: fixture wrapper (tests/support/fixtures/user.fixture.ts)
import { test as base } from '@playwright/test';
export const test = base.extend<{ createUser: (data?: Partial<User>) => Promise<User> }>({
  createUser: async ({}, use) => {
    const created: User[] = [];
    await use(async (data) => {
      const user = await createUserViaApi(data);
      created.push(user);     // track for cleanup
      return user;
    });
    // afterEach cleanup: delete all users created during the test
    for (const u of created) await deleteUser(u.id);
  }
});

// Layer 3: merged export (tests/support/merged-fixtures.ts)
import { mergeTests } from '@playwright/test';
import { test as userFixture }    from './fixtures/user.fixture';
import { test as apiFixture }     from '@seontechnologies/playwright-utils/api-request/fixtures';
export const test = mergeTests(userFixture, apiFixture);  // one import for all tests
export { expect } from '@playwright/test';

// In your test file (tests/users/create.spec.ts)
import { test, expect } from '../support/merged-fixtures';
test('create user', async ({ createUser, apiRequest }) => {
  const user = await createUser({ role: 'admin' });
  // user is automatically deleted after this test
});

When to create a fixture vs inline code

How many tests need it?ActionReason
1 timeWrite inline in the testNot worth the abstraction overhead
2–3 timesExtract to a utility functionAvoid duplication, not ready for full fixture pattern
3+ timesCreate a proper fixtureEnables automatic cleanup, consistent setup, Playwright injection

Recommended folder structure

tests/
├── support/
│   ├── merged-fixtures.ts     ← Single import for all test files
│   ├── fixtures/
│   │   ├── user.fixture.ts
│   │   └── auth.fixture.ts
│   └── factories/
│       ├── user.factory.ts    ← Pure functions
│       └── order.factory.ts
└── features/
    └── users/
        └── create.spec.ts     ← import { test } from '../../support/merged-fixtures'
Anti-pattern: Page Object Model (POM) with inheritance. Avoid classes that extend a base PageObject. This creates tight coupling — if the base class changes, all subclasses break. Use fixtures (dependency injection) and utility functions instead. POM without inheritance (simple page helper objects) is acceptable.
03
Network-First Pattern
network-first.md

What it is: A mandatory rule for UI test reliability — always register network interceptors before navigating to a page or clicking a trigger. This is the single most common cause of intermittent test failures in Playwright.

Why it matters: When a page loads, it immediately fires API requests. If your test navigates first and then registers a listener, those early requests are already gone — your listener will never fire, the Promise will never resolve, and the test will hang until timeout.

The correct sequence (always)

// ✅ CORRECT: register listener FIRST, then trigger the navigation
const usersCall = interceptNetworkCall({ url: '**/api/users' });  // step 1
await page.goto('/dashboard');                                   // step 2
const { responseJson, status } = await usersCall;               // step 3: resolve
expect(status).toBe(200);

// ❌ WRONG: navigating first means the request fires before the listener exists
await page.goto('/dashboard');                // request already fired!
const usersCall = interceptNetworkCall({ url: '**/api/users' });  // too late

Two modes: Spy vs Stub

ModeWhat it doesWhen to useCode
Spy (observe) Lets the real request through; captures request and response for assertion Verifying that the UI calls the right API with the right payload interceptNetworkCall({ url })
Stub (mock) Intercepts the request and returns a fake response; server never receives it Testing error states (500, 404, slow responses) without server cooperation interceptNetworkCall({ url, fulfillResponse: { status: 500 } })

Stub example — testing error handling

// Force a 500 error and verify the UI shows an error message
const failedCall = interceptNetworkCall({
  url: '**/api/orders',
  fulfillResponse: { status: 500, body: { error: 'Internal Server Error' } }
});
await page.goto('/orders');
await failedCall;
await expect(page.getByRole('alert')).toContainText('Something went wrong');

Waiting rules — mandatory

  • NeverHard-coded timeouts: waitForTimeout(5000), sleep(3000), cy.wait(2000) — these make tests slow and still flaky on overloaded CI machines.
  • Nevernetworkidle in SPAs: Single-Page Apps with background polling never reach "idle" — this wait will always time out.
  • AlwaysWait for a specific API response: page.waitForResponse('**/api/resource') — resolves the moment the named endpoint responds.
  • AlwaysWait for an element state: locator.waitFor({ state: 'visible' }) — resolves when the element reaches the expected state.
  • AlwaysWait for navigation: page.waitForURL('/expected-path') — resolves when the URL matches after a navigation action.

HAR Recording — offline testing

HAR (HTTP Archive) recording captures every network request/response to a file. This enables two valuable workflows:

ModeEnv varWhat happensUse case
RecordPW_NET_MODE=recordAll requests hit the real server; traffic saved to HAR fileInitial capture; update after API changes
PlaybackPW_NET_MODE=playbackRequests are served from HAR; no server neededCI without a live server; deterministic regression tests
CRUD support in playback: The network-recorder utility tracks in-memory state during playback — so if a test creates a user, subsequent GETs return that user correctly. Native Playwright HAR does not support this.
04
Data Factories
data-factories.md

What it is: A pattern for generating test data automatically — unique, random values per test run — so tests can run in parallel without stepping on each other's data.

The problem it solves: If all tests use the static email test@example.com, two parallel tests will conflict: test A creates the user, test B fails because the user already exists. Factories produce unique data every run.

Basic factory pattern

import { faker } from '@faker-js/faker';

interface User { name: string; email: string; role: string; }

// Factory function — generates unique data, allows specific overrides
export function createUserData(overrides: Partial<User> = {}): User {
  return {
    name:  faker.person.fullName(),    // "Jane Smith" — unique every run
    email: faker.internet.email(),    // "abc123@example.net" — unique every run
    role:  'viewer',
    ...overrides                       // caller can pin specific values
  };
}

// Usage: all unique, no conflicts in parallel
const admin    = createUserData({ role: 'admin' });  // random name+email, role=admin
const viewer   = createUserData();                   // fully random
const specific = createUserData({ email: 'fixed@test.com' }); // pin email when needed

Cleanup pattern — always clean up after each test

// In your fixture (inside the fixture wrapper from file-02)
const createdUserIds: string[] = [];

await use(async (overrides) => {
  const data = createUserData(overrides);
  const user = await apiRequest.post('/api/users', { body: data });
  createdUserIds.push(user.id);  // track every created entity
  return user;
});

// afterEach: runs automatically, even if the test fails
for (const id of createdUserIds) {
  await apiRequest.delete(`/api/users/${id}`);
}

Performance: API setup vs UI setup

MethodSpeedUse when
API (direct HTTP call)~50–200 msCreating test preconditions — always prefer this
UI (Playwright form fill)~2–10 sOnly when testing the creation flow itself

A test suite with 100 tests, each requiring a pre-existing user: API setup = ~10 s total. UI setup = ~500 s total. Always use API factories for test data setup.

Factory rules

  • RuleEvery factory function must use faker for all string/email/name fields — never hardcode static values
  • RuleEvery created entity ID must be tracked and deleted in afterEach — even if the test fails midway
  • RuleUse API calls (not UI) to create test data — 10–50× faster and does not depend on UI being functional
  • NeverShare a single user/entity between multiple parallel tests — parallel tests read and modify the same data, causing race conditions
  • NeverRely on the order of tests to build state — each test must set up its own data independently

§2 — Testing Strategy & Prioritization

Files 05 – 09
05
Test Levels Framework
test-levels-framework.md

What it is: Defines three test levels (Unit, Integration, E2E), when each is appropriate, the expected speed and scope of each, and the anti-patterns that arise when levels are misapplied. This is the foundational mental model for all test planning in TEA.

The Test Pyramid

E2E
Tests full user journeys through the browser. Slow (10 s – 2 min). Brittle — any UI change can break them. Use sparingly.

When to use: Critical user paths that cross multiple systems (e.g. login → add to cart → checkout → confirmation email). No more than ~10–20% of your total test count.

Example: "User registers, verifies email, logs in, updates profile, logs out" — one E2E flow verifying the whole chain.
Integration
Tests how multiple units work together — with real databases, APIs, file systems, or external services.

When to use: Testing data flows across layers (controller → service → database), API contract validation, authentication flows. ~20–30% of total test count.

Example: "POST /api/users creates a database record, returns 201, and the record can be retrieved via GET /api/users/:id."
Unit
Tests a single function or module in complete isolation. Milliseconds. No network, no database, no filesystem.

When to use: Pure business logic — validation, calculations, transformations, data parsing. Should be the majority (~60–70%) of your total test count.

Example: "calculateDiscount(price, code) returns 90 when code is 'SAVE10' and price is 100."

Decision guide — which level to write

QuestionIf YES → write at this level
Does it test a single function with no external deps?Unit
Does it test multiple components interacting (DB, API, files)?Integration
Does it test a complete user journey through the browser?E2E
Are you testing business logic inside a controller/service?Unit (extract logic; don't test via HTTP)
Are you testing "does clicking Submit call the API?"Integration (use interceptNetworkCall)

Anti-patterns

  • AvoidE2E tests for business logic: Using Playwright to test a discount calculation costs 100× more time than a unit test and fails for UI reasons unrelated to the logic.
  • AvoidUnit tests for framework behavior: Testing "does Express route a POST request?" is testing the framework, not your code. No value.
  • AvoidInverted pyramid: More E2E than unit tests means very slow CI, very high maintenance, and very poor failure diagnosis. A failing E2E test tells you "something broke" — a failing unit test tells you exactly what.
  • NoteAPI tests are a subset of Integration: Tests using apiRequest without a browser sit at the integration level and are 10–100× faster than browser-based E2E tests for the same coverage.
06
Test Priorities Matrix
test-priorities-matrix.md

What it is: Four priority levels (P0–P3) that map to minimum coverage requirements. Every feature/requirement in a project must be classified at one of these levels before the test plan is written. Priority is determined by business impact of failure — not technical complexity.

Priority Levels

Critical
P0
System survival. Failure makes the product unusable or causes direct financial or security damage.
Unit: >90%
Integration: >80%
E2E: all critical paths
Blocks release if failing
Important
P1
Core product value. Failure degrades the product significantly but is recoverable.
Unit: >80%
Integration: >60%
E2E: key flows
Normal
P2
Supporting functionality. Failure impacts a subset of users but workarounds exist.
Unit: >60%
Smoke test only
No E2E required
Low
P3
Cosmetic or rare edge case. Failure affects appearance or an uncommon path.
Best effort
Manual testing acceptable
No blocking requirement

Classification examples

FeaturePriorityReason
User login / authenticationP0No login = product unusable
Payment processingP0Direct revenue loss on failure
JWT token validation / RBACP0Security breach if broken
Search functionalityP1Core product feature; users can browse manually
Email notification on sign-upP1Expected behavior; degradation if missing
CSV export in admin panelP2Admin-only; workarounds exist
Dark mode toggleP3Cosmetic; no functional impact
Button hover colorP3Purely visual, rare user complaint

How to apply priorities in a test plan

Step 1
For each feature in the PRD, assign a P-level based on business impact of failure.
Step 2
Match the P-level to its minimum coverage requirement from the table above.
Step 3
Tag every test with its priority: @p0, @p1, etc. This enables selective test execution (e.g., run only @p0 tests before a hotfix deployment).
Step 4
In CI, configure the pipeline to block merges if P0/P1 tests fail. P2/P3 failures can be warnings.
Priority is not about difficulty. A technically complex feature can be P2 if it is rarely used. A simple boolean flag can be P0 if it gates user access to the entire product.
07
Risk Governance
risk-governance.md

What it is: A structured method for scoring, categorizing, and tracking risks in a software system so that testing effort is concentrated where failure would hurt the most. Each risk item receives a numeric score that maps to a mandatory action tier.

Risk Score Formula

Risk Score = Probability (1–3)  ×  Impact (1–3)  =  range: 1–9

Action tiers

ScoreColorTierRequired Action
1–3GreenLowDocument in risk register. Review at end of sprint.
4–5YellowMediumMonitor. Add mitigation options to backlog.
6–8OrangeHighMust have a documented mitigation plan before this feature ships.
9RedCriticalBLOCK — Deployment blocked until risk is resolved or formally accepted by stakeholders.

Risk categories and examples

CategoryCodeExample risks
TechnicalTECHThird-party library with breaking changes; complex algorithm with no test coverage; legacy code with no documentation
SecuritySECUnauthenticated endpoints; plain-text passwords in logs; missing CSRF protection; JWT stored in localStorage
PerformancePERFN+1 query on user list page; unindexed column in frequent search; no CDN for static assets
DataDATANo database backup strategy; migration without rollback; data shared between test and production environments
BusinessBUSPayment integration with no fallback; single point of failure in checkout flow; feature flag with no off switch
OperationsOPSNo monitoring on critical endpoints; deployments require manual steps; no on-call runbook

How to run a risk assessment

1
List risks. For each epic or feature, brainstorm what could go wrong. Use the 6 categories as a checklist prompt.
2
Score each risk. Assign P (1–3) and I (1–3) independently. P = how likely is this to occur? I = how bad is it if it does?
3
Apply action tier. Multiply P×I, look up the action. Scores ≥ 6 require written mitigation plans. Score 9 blocks the release.
4
Trace to test cases. Every risk item with score ≥ 4 must be covered by at least one automated test. Document the mapping in the risk register.

Risk register format

# Risk Register — Sprint 12
| ID    | Category | Description                    |  P  |  I  | Score | Action       | Mitigation                        |
|-------|----------|-------------------------------|-----|-----|-------|--------------|-----------------------------------|
| R-001 | SEC      | JWT stored in localStorage     |  3  |  3  |   9   | BLOCK        | Move to httpOnly cookie            |
| R-002 | PERF     | N+1 on /api/orders list        |  2  |  2  |   4   | Monitor      | Add eager loading, add perf test   |
| R-003 | DATA     | No migration rollback script   |  2  |  3  |   6   | Mitigate     | Write down migration before ship   |
Traceability rule: Every acceptance criterion in the test plan must have at least one test that covers it. Risk items with score ≥ 4 must have explicit test coverage documented. The bmad-testarch-trace skill enforces this mapping automatically.
08
ADR Quality Readiness Checklist
adr-quality-readiness-checklist.md

What it is: A 29-criterion checklist across 8 categories used to assess whether a system is ready to be approved for production release from a quality and testability perspective. This is the gate evaluation model used by the bmad-testarch-nfr skill.

How it works: TEA reads the architecture documents, ADRs, and available evidence (test results, monitoring dashboards, deployment runbooks), then scores each criterion as met / partial / missing. The final score determines PASS / CONCERNS / FAIL.

All 8 categories and their criteria

Category 1
Testability
4 criteria
  • Components can be tested in isolation
  • Test environment is separated from production
  • External dependencies can be mocked or stubbed
  • Test data can be provisioned programmatically
Category 2
Data Strategy
3 criteria
  • Test data is isolated from production data
  • Data factories exist for all major entities
  • No shared mutable state between parallel test workers
Category 3
Scalability
4 criteria
  • NFR thresholds are defined (latency, throughput, error rate)
  • Load testing has been run at projected peak volume
  • Auto-scaling rules are defined and tested
  • Database connection pooling is configured correctly
Category 4
Disaster Recovery
3 criteria
  • RTO and RPO targets are defined and documented
  • Backup restore procedure has been tested
  • Rollback procedure exists and has been dry-run
Category 5
Security
4 criteria
  • Authentication mechanism is implemented and tested
  • Authorization (RBAC/ABAC) is enforced at all endpoints
  • Sensitive data is never logged or returned in error responses
  • OWASP Top 10 risks have been addressed
Category 6
Observability
4 criteria
  • Structured logging is in place on all services
  • Metrics (latency, error rate, throughput) are being collected
  • Distributed tracing is implemented (request IDs propagated)
  • Alerts are configured for P0 error conditions
Category 7
Quality of Service
4 criteria
  • Latency budgets are defined per endpoint
  • Rate limiting is implemented and tested
  • Circuit breaker or retry logic handles downstream failures
  • SLA/SLO targets are documented
Category 8
Deployability
3 criteria
  • Zero-downtime deployment is supported
  • Feature flags exist for risky changes (can be turned off post-deploy)
  • Deployment pipeline includes automated smoke tests

Scoring and gate outcomes

ScorePercentageGate DecisionWhat it means
≥ 24 / 29≥ 83%PASSSystem meets the quality bar. Release can proceed.
15–23 / 2952–79%CONCERNSMissing items must be reviewed. Release requires explicit sign-off on each gap.
< 15 / 29< 52%FAILToo many gaps. Address deficiencies before re-assessment.
TEA does not run tests. The NFR skill reads evidence that already exists — test reports, architecture decision records, runbooks, monitoring dashboards — and evaluates that evidence against each criterion. If no evidence exists for a criterion, it is marked as missing.
09
Probability & Impact Scale
probability-impact.md

What it is: Detailed definitions for the Probability (P) and Impact (I) axes used in the Risk Score formula from risk-governance.md. Without explicit definitions, two team members scoring the same risk often choose different values. This file eliminates that ambiguity.

Probability Scale (P)

ValueLabelDefinitionExample
P1 = 1UnlikelyOccurs rarely, less than once per quarter under normal conditionsHash collision in ID generation; hardware failure in redundant cluster
P2 = 2PossibleOccurs occasionally, roughly monthly under normal loadThird-party service downtime; edge case in user input validation
P3 = 3LikelyOccurs regularly — weekly or with any meaningful load increaseRace condition in high-concurrency path; timeout on slow external API

Impact Scale (I)

ValueLabelDefinitionExample
I1 = 1MinorCosmetic or invisible to most users; no data loss, no revenue impactWrong color on error message; incorrect timezone in log entry
I2 = 2DegradedA feature is impaired or unavailable; system is still running; workaround existsCSV export produces wrong column order; search returns incomplete results
I3 = 3SevereSystem down, data loss, security breach, or direct financial lossUsers cannot log in; payment charges incorrect amount; personal data exposed

Full 3×3 Score Matrix

P \ I
I1 · Minor
I2 · Degraded
I3 · Severe
P1 · Unlikely
1
Low / Document
2
Low / Document
3
Medium / Monitor
P2 · Possible
2
Low / Document
4
Medium / Monitor
6
High · Must mitigate
P3 · Likely
3
Medium / Monitor
6
High · Must mitigate
9 ⛔
BLOCK release

Common real-world score examples

ScenarioPIScoreAction
Webhook delivery failure (event-driven system)236Mandatory test coverage
Payment gateway timeout with no retry236Mandatory retry + alert
JWT not validated on admin endpoints339BLOCK — fix before ship
Wrong label on a form field212Log it, fix in next sprint
Search returns 0 results on empty DB122Document, low priority
CSV export missing one optional column224Monitor; add test coverage
Default assumption for webhooks: Any system using event-driven architecture with outbound webhooks defaults to P2×I3 = 6. This means webhook delivery must have dedicated test coverage unless explicitly downgraded with justification.

§3 — Test Resilience & Quality

Files 10 – 14
10
NFR Criteria
nfr-criteria.md

What it is: Defines the specific criteria, thresholds, and tooling used to evaluate Non-Functional Requirements (NFRs) across four domains: Security, Performance, Reliability, and Maintainability. These criteria are what the bmad-testarch-nfr skill checks when performing an NFR evidence audit.

Important distinction: TEA does not run these tests. It evaluates the evidence that already exists (k6 reports, Playwright results, CI dashboards) against these criteria to produce a gate decision.

Security

AuthN · AuthZ · Data Protection

Primary tool: Playwright E2E
What to test:
  • Expired or invalid JWT token is rejected with 401 — not silently accepted
  • Role-based access: a user with role "viewer" cannot access admin endpoints
  • Sensitive fields (passwords, tokens, PII) are never returned in API responses or logged
  • CORS policy allows only whitelisted origins
  • Brute-force protection: account locks or rate-limits after N failed login attempts
  • HTTPS enforced — plain HTTP redirected or rejected

Threshold for PASS: All security test cases pass. Any failure = FAIL gate (no partial credit on security).
Performance

Latency · Throughput · Load

Primary tool: k6 load testing
What to measure:
  • P95 response time under projected peak load (typical threshold: < 200 ms for API, < 2 s for page load)
  • Error rate under load remains below 0.1%
  • No memory leaks under 30-min sustained traffic
  • System gracefully degrades (returns 429 or queues) rather than crashing under excess load
  • Cold start latency for serverless functions within SLA

Threshold for PASS: P95 latency within budget, error rate < 1% at peak load. Missing load test evidence = CONCERNS.
Reliability

Retry · Recovery · Consistency

Primary tool: Playwright / Integration tests
What to test:
  • Automatic retry on transient network errors (not on 4xx client errors)
  • Circuit breaker activates after N consecutive failures; recovers after cooldown
  • Data consistency is preserved after a mid-transaction failure and rollback
  • RTO (Recovery Time Objective) and RPO (Recovery Point Objective) targets are met in DR tests
  • Idempotent endpoints return the same result on repeated identical requests

Threshold for PASS: All reliability test cases pass. Untested RTO/RPO = CONCERNS.
Maintainability

Coverage · Build Health · Test Quality

Primary tool: CI pipeline metrics
What to check:
  • Code coverage meets P-level thresholds (P0: ≥90% unit; P1: ≥80% unit)
  • No permanently skipped tests (.skip with no ticket reference)
  • CI build time within acceptable budget (typical: < 15 min for full suite)
  • Flaky test rate below threshold (typical: < 1% of test runs)
  • All test files conform to the 8-point quality checklist (see file 13)

Threshold for PASS: Coverage thresholds met, flaky rate < 1%, no orphaned skips.

NFR gate outcomes

PASS — all criteria met, evidence provided CONCERNS — some criteria unmet or evidence missing FAIL — critical criteria unmet WAIVED — criterion formally waived with owner sign-off
WAIVED is not the same as SKIP. A waived criterion has a documented decision — who approved it, why, and what the accepted risk is. A skipped criterion with no record is always scored as FAIL.
11
Selector Resilience
selector-resilience.md

What it is: Defines which HTML element selection strategies to use in UI tests, ranked from most to least resilient. Poor selector choices are the #1 cause of tests breaking after a UI redesign even when the underlying functionality is unchanged.

Priority hierarchy (use #1 whenever possible)

  1. page.getByTestId('submit-btn')
    Best: Uses data-testid attribute — added by developers specifically for tests. Does not change when the UI is redesigned, text is translated, or CSS classes are renamed. Requires team agreement to add data-testid to meaningful interactive elements.
  2. page.getByRole('button', { name: 'Submit' })
    Best: ARIA semantic selector — also tests accessibility at the same time. Stable across visual redesigns. Reflects how assistive technology sees the page.
  3. page.getByLabel('Email address')
    Good: Finds the input associated with a label. Stable until the label text changes. Also validates correct label/input association.
  4. page.getByText('Submit')
    Acceptable: Text-based selection. Fragile if the text is internationalised or reworded. Use regex (/Submit|Enviar/) for multilingual apps.
  5. .submit-btn · #mainSubmit · form button:nth-of-type(2)
    Avoid: CSS classes are implementation details that change with every refactor. Positional selectors (nth) break when the DOM order changes. Never acceptable for tests that need to remain stable.
  6. //button[@class='btn btn-primary']
    Never: XPath is verbose, brittle, and incompatible with Playwright's recommended locator patterns. The Playwright team explicitly discourages XPath use.

Filtering from a list — filter() not nth()

// ❌ BAD: positional — breaks when list order changes
await page.locator('.user-row').nth(2).click();

// ✅ GOOD: semantic filter — finds by content, not position
await page.locator('.user-row')
  .filter({ hasText: 'Jane Smith' })
  .getByRole('button', { name: 'Edit' })
  .click();

Migration guide — before and after

Before (brittle)After (resilient)Why
$('.btn-submit')getByRole('button', {name:'Submit'})CSS class removed in redesign
$('#login-form input')[0]getByLabel('Email')Order of inputs may change
getByText('登録する')getByTestId('register-btn')Text changes with i18n
//div[@id='modal']//buttongetByRole('dialog').getByRole('button', {name:'Confirm'})XPath breaks on any DOM change
12
Timing & Debugging
timing-debugging.md

What it is: Catalogs the most common timing mistakes in UI and API testing, provides the correct event-based replacements, and documents the debugging tools available when a test produces intermittent failures on CI.

Why it matters: Timing issues cause tests that "work on my machine" but fail 30% of the time on CI. The root cause is almost always a hard-coded wait that is too short on a slow machine, or a missing wait that relies on implicit browser timing.

Complete timing anti-patterns table

Anti-patternWhy it's wrongCorrect replacement
waitForTimeout(5000)Adds 5 s to every test run. Still fails on overloaded CI. Hides the real timing issue.waitForResponse('**/api/...')
cy.wait(3000)Same problem in Cypresscy.wait('@apiAlias')
sleep(2000)Blocking wait — no intelligence about actual system staterecurse(() => check(), pred)
networkidle in SPASPAs with polling never reach "idle" — this wait always times outWait for the specific route or element that confirms readiness
page.waitForTimeout before assertion"Give it time to load" — lazy workaround for a missing awaitawait expect(locator).toBeVisible() (has built-in retry)

Event-based waiting patterns

// Pattern 1: Wait for a specific API response after an action
const savePromise = page.waitForResponse('**/api/users/**');
await page.getByRole('button', { name: 'Save' }).click();
const response = await savePromise;
expect(response.status()).toBe(200);

// Pattern 2: Wait for element to reach expected state
await page.getByText('Changes saved').waitFor({ state: 'visible' });

// Pattern 3: Wait for navigation after form submit
await Promise.all([
  page.waitForURL('/dashboard'),
  page.getByRole('button', { name: 'Login' }).click()
]);

// Pattern 4: Poll for async background job completion
await recurse(
  () => apiRequest({ method: 'GET', path: `/jobs/${jobId}` }),
  (res) => res.body.status === 'completed',
  { timeout: 60_000, interval: 2_000 }
);

Debugging workflow for flaky tests

Step 1
Check CI trace. Every failed test run on CI should have a Playwright trace file. Open it with npx playwright show-trace trace.zip to see the exact sequence of actions, DOM snapshots, and network requests at the point of failure.
Step 2
Look at the network waterfall in the trace. Find the request that was expected but did not complete before the test assertion fired. This is usually the missing waitForResponse.
Step 3
Add log.step() calls around each major action to narrow down where the test blocks. Check the HTML report to see which step was the last to complete before the failure.
Step 4
Use page.pause() to stop test execution and open the Playwright Inspector for interactive debugging. Inspect element state, run locator queries, and check network requests in real time.
Step 5
If it only fails on CI: likely a resource constraint issue (slow startup, port not ready). Add a waitFor on the first element that confirms the server is ready rather than a time-based wait.
Never increase a timeout as a fix. Adding { timeout: 30000 } to a slow test hides the real problem and makes the entire suite slower. Find and fix the missing event-based wait instead.
13
Test Quality Standards
test-quality.md

What it is: An 8-point checklist that defines what a production-quality test looks like. Used by the bmad-testarch-review skill when auditing an existing test suite. A test must pass all 8 criteria to be considered acceptable for production.

The 8 quality criteria — with examples

  • No hard-coded waits — Zero waitForTimeout, sleep, or cy.wait(N) calls. All waits are event-based.
    Bad: await page.waitForTimeout(3000) · Good: await page.waitForResponse('**/api/users')
  • No conditional logic inside the test — No if/else, try/catch, or ternary expressions in the test body. Each test must have exactly one deterministic execution path.
    Bad: if (await button.isVisible()) { await button.click() }
  • Test body under 300 lines — If a test is longer than 300 lines, shared logic must be extracted into fixtures or utility functions. Long tests are hard to read and impossible to maintain.
  • Completes within 1.5 minutes — Including setup, execution, and teardown. If a test consistently takes longer, it should be split into focused smaller tests or moved to a nightly suite.
  • Self-cleaning — The test deletes all data it creates, regardless of whether it passes or fails. Cleanup runs in afterEach (not afterAll) to prevent data leaking to the next test.
  • Specific, unambiguous assertions — Assertions check exact values, not just truthy/falsy states. No toBeTruthy(), no toBeGreaterThan(0) when an exact value is known.
    Bad: expect(user).toBeTruthy() · Good: expect(user.id).toBe('usr_123')
  • Unique test data every run — All strings, emails, names, and IDs used in the test are generated with faker or crypto.randomUUID(). Never hardcoded static values that would conflict with a parallel test run.
  • Parallel-safe — The test does not depend on other tests running before or after it. Does not modify shared state (global users, shared feature flags, hardcoded IDs). Can run in any order, at any time, on any worker.

How to score a test against these criteria

Criteria metVerdictAction
8 / 8PASSNo action required
6–7 / 8CONCERNSFix within this sprint; test is conditionally acceptable
≤ 5 / 8FAILTest must be refactored before merge
14
Test Healing Patterns
test-healing-patterns.md

What it is: Five structured repair patterns for the most common categories of broken or flaky tests. When a test starts failing, diagnose the failure type, then apply the matching pattern. Each pattern has a diagnostic symptom, root cause, and step-by-step fix.

How to use this file

1
Identify the symptom from the failure message or CI trace.
2
Match the symptom to one of the 5 patterns below.
3
Follow the fix steps for that pattern.
4
If the fix fails after 3 attempts, use test.fixme() and file a ticket.

Pattern 1 — Stale Selector

Detail
Symptom"Locator did not match any elements" · "Element not found" · "getByTestId('x') resolved to 0 elements"
Root causeA UI change renamed or removed the element's data-testid, changed its ARIA role, or restructured the DOM
FixRun playwright-cli snapshot to take a fresh snapshot of the page. Find the new selector for the target element. Update the test. If the element was removed intentionally, delete the test too.

Pattern 2 — Race Condition

Detail
SymptomTest passes on first run, fails 20–40% of subsequent runs with no code change. Failure is non-deterministic.
Root causeAn assertion fires before an async operation (API call, state update, animation) has completed
FixFind the missing waitForResponse() or waitFor({ state }). Add it between the trigger action and the assertion. Never add a time-based wait — find the specific event.
// Before: assertion fires before API response arrives
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Saved')).toBeVisible();  // ← flaky

// After: wait for the API call that triggers the success message
const saved = page.waitForResponse('**/api/profile');
await page.getByRole('button', { name: 'Save' }).click();
await saved;                                                  // ← deterministic
await expect(page.getByText('Saved')).toBeVisible();

Pattern 3 — Dynamic Data Mismatch

Detail
Symptom"Expected 'John Smith' but received 'Jane Doe'" — assertion comparing a hardcoded value to dynamically generated data
Root causeTest uses a hardcoded string that was valid when the test was written but is no longer the actual value
FixReplace hardcoded values with the dynamically generated values from your factory, or use partial/regex matchers when the exact value is not what you're testing.
// Bad: hardcoded value that depends on database state
expect(response.body.name).toBe('John Smith');

// Good: assert against what your factory generated
const userData = createUserData();
const user = await createUser(userData);
expect(response.body.name).toBe(userData.name);  // always matches

Pattern 4 — Network Timeout

Detail
SymptomRandom "Timeout 30000ms exceeded" or "net::ERR_CONNECTION_TIMED_OUT" — more frequent on CI than local
Root causeExternal service or test environment has variable latency; no retry logic in place
FixWrap the request in recurse() with sensible retry count and interval. Do not just increase the timeout — add intelligent retry.

Pattern 5 — Hard Wait Replacement

Detail
SymptomTest always passes, but is very slow (> 1 min) because it has waitForTimeout(10000) or similar
Root causeDeveloper added a conservative time-based wait instead of identifying the actual event to wait for
FixIdentify the event (API response, element state, URL change) that actually signals completion. Replace the hard wait with an event-based wait. Run 10 times to confirm the fix is stable.
Three-Strike Rule: If you have applied fixes 3 separate times and the test is still flaky, it has a deeper systemic issue. Mark it with test.fixme('reason', { ticket: 'JIRA-123' }) and file a proper investigation ticket. Never leave a permanently flaky test running silently — it degrades trust in the entire suite.

§4 — Contract Testing

File 15
15
Contract Testing
contract-testing.md

What it is: A testing approach where the interface contract between a consumer (the service that calls an API) and a provider (the service that hosts the API) is formally captured and verified. The contract is a living document — generated by the consumer and verified by the provider, enabling both sides to evolve independently without breaking each other.

Primary tooling: Pact.js (contract generation and verification) + PactFlow (contract broker — stores contracts, tracks verification status). The can-i-deploy CLI checks whether both parties have verified the current contract before allowing a deployment to proceed.

How contract testing flows

1
Consumer writes a contract test — describes what it sends (request) and what it expects back (response). This is an explicit specification, not a simple mock.
2
Pact generates a contract file (pact JSON) — captures every interaction the consumer tested. Uploaded to PactFlow as the source of truth.
3
Provider runs verification — the provider's CI pipeline pulls the contract from PactFlow, replays each interaction against its real implementation, and reports whether it complies.
4
can-i-deploy acts as a gate — before either party deploys, the CLI queries PactFlow. If the latest contract has not been verified, deployment is blocked.

Consumer-side test: defining the contract

import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, regex } = MatchersV3;

const provider = new PactV3({
  consumer: 'user-management-ui',
  provider: 'user-api',
  dir:      './pacts',
});

it('returns a user by ID', () =>
  provider
    .given('a user with ID usr_123 exists')
    .uponReceiving('a GET request for usr_123')
    .withRequest({ method: 'GET', path: '/api/users/usr_123' })
    .willRespondWith({
      status: 200,
      body: {
        id:    like('usr_123'),       // any string is OK
        email: regex('.+@.+\\..+', 'u@e.com'),
        roles: eachLike('viewer'),    // array, at least 1 item
      }
    })
    .executeTest(async (mockServer) => {
      const user = await new UserApiClient(mockServer.url).getUser('usr_123');
      expect(user.id).toBeDefined();
    })
);

Provider-side verification

import { Verifier } from '@pact-foundation/pact';

it('validates the contract from PactFlow', () =>
  new Verifier({
    providerBaseUrl:           'http://localhost:8080',
    pactBrokerUrl:             process.env.PACTFLOW_URL,
    pactBrokerToken:           process.env.PACTFLOW_TOKEN,
    provider:                  'user-api',
    publishVerificationResult: true,
    providerVersion:           process.env.GIT_SHA,
    stateHandlers: {
      'a user with ID usr_123 exists': async () => {
        await db.createUser({ id: 'usr_123', email: 't@e.com', roles: ['viewer'] });
      }
    }
  }).verifyProvider()
);

can-i-deploy gate in CI

# In the consumer's deploy pipeline
pact-broker can-i-deploy \
  --pacticipant user-management-ui \
  --version $GIT_SHA \
  --to-environment production \
  --broker-base-url $PACTFLOW_URL
# Exit 0 = proceed · Exit 1 = blocked (verification missing or failed)

Postel's Law — the asymmetry rule

SideGuidanceIn practice
Request (send)Be conservative — send only what is needed, exactly as specifiedValidate required fields strictly; no extra unknown fields
Response (accept)Be liberal — accept anything that satisfies your minimum needsUse like() and regex() matchers; ignore extra fields you don't use
Why this matters: If the consumer checks for exact field values, any change to the provider's response format breaks the contract — even if the consumer doesn't care about that field. Use flexible matchers on the consumer side so the provider can evolve safely.

When to use contract testing

ScenarioUse?Reasoning
Two internal services in the same orgYesBoth sides in PactFlow; feedback is instant when either changes
Your service calls a third-party APIConsumer side onlyThe provider will not run verification — record mocks instead
Microservices replacing a monolithYes — priorityMost valuable during migration when interfaces change frequently
Module-to-module calls within one serviceNoUnit tests with real function calls are faster and more direct

Summary Map — All 15 Core Files

Quick reference
#FileSectionOne-line purposeKey output
01playwright-utils.md§19 shared utilities — never build these from scratchImport path, utility names
02fixture-architecture.md§13-layer fixture pattern; mergeTests() for compositionPure function → fixture → export
03network-patterns.md§1Network interception: spy vs stub, HAR record/replayCorrect waitForResponse usage
04data-factories.md§1Faker-based factories + cleanup tracking; API > UI setupFactory pattern + afterEach cleanup
05test-levels.md§2Unit / Integration / E2E — what each level coversTest pyramid + decision guide
06test-priorities.md§2P0–P3 priorities with coverage thresholds per levelPriority classification + thresholds
07risk-governance.md§2Risk = P × I; tiers 1–9; 6 categories; register formatRisk score → action tier
08adr-quality-checklist.md§229-point ADR readiness checklist; PASS ≥24/29Gate decision: PASS / CONCERNS / FAIL
09risk-matrix.md§23×3 P×I matrix with colour-coded tiers and examplesScore → mandatory / conditional test coverage
10nfr-criteria.md§34 NFR domains: Security / Perf / Reliability / MaintainabilityTool + threshold + PASS criteria per domain
11selector-resilience.md§3Selector priority: testid > role > label > text > CSSMigration table, filter() pattern
12timing-debugging.md§3Anti-patterns → event-based waits; 5-step debug workflowCorrect waitForResponse / waitFor usage
13test-quality.md§38-point quality checklist; PASS requires all 8Checklist + scoring thresholds
14test-healing-patterns.md§35 patterns for fixing broken/flaky tests; 3-strike ruleSymptom → root cause → fix procedure
15contract-testing.md§4Pact.js + PactFlow; consumer contract → provider verificationConsumer test, provider verify, can-i-deploy gate
Tier 2 · Extended

Extended Tier — 22 Files

Loaded on demand when the context requires it. Covers deep-dive implementations of each playwright-utils utility, advanced testing patterns, CI execution strategies, feature flag governance, resilience patterns, email authentication, and webhook testing infrastructure.

§5 · Files 16–24
Auth & Core Utilities
Auth session, API request, recurse, intercept, HAR recorder, error monitor, logging, file utils, fixtures merge
§6–§8 · Files 25–35
Patterns & Execution
API patterns, component TDD, visual debugging, config, CI burn-in, selective testing, flags, error handling, email auth, CLI
§9 · Files 36–37
Webhook Testing
Fundamentals, polling, risk guidance, default P2×I3 = score 6 rule

§5 — Authentication & Core Utilities

Files 16 – 24
16
Authentication Session Persistence & Reuse
auth-session.md

What it is: A utility that persists authentication tokens to disk and reuses them across test runs and parallel workers — no re-authentication before every test. Supports multiple simultaneous user identities, ephemeral (one-time) auth, and worker-specific accounts for fully parallel execution.

Why it matters: Playwright's built-in authentication re-authenticates on every run, only supports a single user, and does not handle token expiration. With auth-session, a token is acquired once and reused everywhere — login overhead drops from ~10 s per test to <1 ms.

Global setup lifecycle — 4 calls in order

FunctionWhen to callPurpose
authStorageInit()globalSetup — firstCreate the token storage directory on disk
configureAuthSession()globalSetupSet base URL and token configuration options
setAuthProvider()globalSetupRegister the custom AuthProvider implementation
authGlobalInit()globalSetup — lastTrigger initial token acquisition and write to disk

Custom AuthProvider interface — 5 methods

// Implement all 5 methods to plug in any authentication system
interface AuthProvider {
  getEnvironment():           string;            // 'local' | 'staging' | 'production'
  getUserIdentifier():        string;            // unique cache key per user role
  extractToken(response):    string;            // parse token from login response
  isTokenExpired(token):     boolean;           // check if cached token is still valid
  manageAuthToken():         Promise<string>;   // acquire or refresh token
}

Authentication modes

Standard (cached)
Authenticate once in globalSetup. Token stored on disk. All tests reuse the same token until isTokenExpired() returns true — then auto-refresh.
Default · most common
Multi-user
Override authOptions.userIdentifier per describe block or test. Each identifier gets its own cached token — admin and viewer tests run in the same suite without interference.
Role-based testing
Worker-specific
Each parallel worker uses a different userIdentifier (e.g. worker-0, worker-1). Prevents token collision when multiple tests modify user state simultaneously.
fullyParallel: true
Ephemeral
applyUserCookiesToBrowserContext() injects auth directly into a browser context without writing to disk. Used for temporary or single-use auth states.
One-off auth

API-only auth — no browser required

// Return the raw token string — auth-session handles caching automatically
async manageAuthToken(): Promise<string> {
  const res = await fetch(`${baseUrl}/api/auth/login`, {
    method: 'POST',
    body: JSON.stringify({ email, password })
  });
  const { token } = await res.json();
  return token;   // no browser, no cookies — pure API token flow
}

// Using a different user role in a specific test file
test.use({ authOptions: { userIdentifier: 'admin-user' } });

test('admin sees delete button', async ({ page }) => {
  await page.goto('/users');
  await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();
});
  • AlwaysCall authStorageInit() first in globalSetup — if the storage directory does not exist, the other functions fail silently or throw obscure errors.
  • AlwaysUse userIdentifier overrides for role-based tests — never share one token across tests that need different permission levels.
  • NeverHard-code tokens in test files — they expire, rotate, and get committed to version control where they become security liabilities.
  • NeverCall logout actions in tests that use cached tokens — a logout invalidates the shared token for all subsequent tests in the run.
Token expiry is automatic. When isTokenExpired() returns true, the utility calls manageAuthToken() to acquire a fresh token before the next test begins. No refresh logic is needed in individual tests.
17
API Request — Typed HTTP Client with Schema Validation
api-request.md

What it is: A typed HTTP client that wraps Playwright's request context with automatic JSON response parsing, schema validation (Zod, JSON Schema, OpenAPI), automatic retry on 5xx errors with exponential backoff, and a 4-tier URL resolution strategy. No browser required — ideal for pure API and service-layer tests.

Why it matters: Vanilla Playwright requires boilerplate for every API call: response.json(), manual status checking, custom retry loops, zero type safety. apiRequest replaces all of that with a single typed call and catches contract drift through schema validation.

Basic usage — request and destructure

// Response body is always pre-parsed JSON — no await response.json() needed
const { status, body } = await apiRequest({
  method: 'POST',
  path:   '/api/users',
  body:   { name: 'Jane Smith', role: 'admin' }
});
expect(status).toBe(201);
expect(body.id).toBeDefined();

URL resolution — 4-tier priority (highest to lowest)

PrioritySourceWhen used
1baseUrl field in the call itselfPer-call override — e.g. call a different microservice
2configureApiRequest({ baseURL }) global configDefault for the entire project, set once in globalSetup
3Playwright config use.baseURLInherited automatically from playwright.config.ts
4Path used as full URLWhen path starts with http:// or https://

Schema validation — Zod, JSON Schema, OpenAPI

import { z } from 'zod';

const UserSchema = z.object({
  id:    z.string(),
  email: z.string().email(),
  roles: z.array(z.string())
});

// Validation throws immediately if response shape doesn't match the schema
// Catches contract drift the moment a field is renamed or removed
const { body } = await apiRequest({
  method:         'GET',
  path:           '/api/users/usr_001',
  validateSchema: UserSchema     // Zod schema, JSON Schema object, or YAML OpenAPI path
});
// body is now typed as { id: string; email: string; roles: string[] }

Retry behavior — automatic

Response statusBehaviorRationale
5xx (server error)Retry 3× with exponential backoff: 1 s → 2 s → 4 sTransient server errors — likely to recover on retry
4xx (client error)Fail immediately — no retryClient sent a bad request — retrying won't fix it
2xx / 3xxReturn immediatelySuccess or redirect — no retry needed

GraphQL — always check body.errors

// GraphQL always returns HTTP 200, even for errors — check body.errors not status
const { body } = await apiRequest({
  method: 'POST',
  path:   '/graphql',
  body: {
    query:     `query GetUser($id: ID!) { user(id: $id) { id email roles } }`,
    variables: { id: 'usr_001' }
  }
});
expect(body.errors).toBeUndefined();     // ← mandatory GraphQL error check
expect(body.data.user.email).toBeDefined();

Combine with recurse for polling

// Poll an async job until it completes — apiRequest + recurse combo
await recurse(
  () => apiRequest({ method: 'GET', path: `/api/jobs/${jobId}` }),
  (res) => res.body.status === 'completed',
  { timeout: 60_000, interval: 2_000 }
);
OpenAPI operation overload (v3.14.0+): Pass an operation object generated by your OpenAPI code generator instead of manual method + path. The operation carries fully typed request/response types — eliminates path typos and keeps types synchronized with the spec automatically.
  • AlwaysAdd validateSchema for responses at P0/P1 API endpoints — catches breaking provider changes at the moment they happen, not when a UI test fails weeks later.
  • AlwaysCheck body.errors for GraphQL responses — HTTP 200 does not mean success in GraphQL.
  • NeverUse apiRequest for WebSocket or Server-Sent Events — it is HTTP-only. Use Playwright's page.on('websocket') for those.
18
Recurse — Cypress-Style Smart Polling
recurse.md

What it is: A polling utility that repeatedly executes a command until a predicate returns true, with configurable timeout, interval, logging, and post-poll callbacks. Returns typed error shapes for precise diagnosis. Fills the gap in Playwright's native expect.poll: no built-in logging, generic timeout messages, no post-success hooks.

Primary use cases: Background job completion, webhook delivery, email arrival, database eventual consistency, cache propagation, state machine transitions — any scenario where you must wait for an asynchronous outcome without knowing when it will arrive.

Core signature

await recurse(
  command,    // () => Promise<T>   — executed on every retry
  predicate,  // (result: T) => boolean | void  — return truthy OR run assertions
  options?    // RecurseOptions
)

Options reference

OptionDefaultPurpose
timeout30 000 msMax total wait before throwing RecurseTimeoutError
interval1 000 msDelay between attempts
logtrueEmit log entries per attempt in the HTML report
postCallback executed once after the predicate succeeds
delay0 msExtra delay before the first execution
errorCustom message appended to timeout error

Three predicate styles

// Style 1: return truthy value
await recurse(
  () => apiRequest({ method: 'GET', path: `/api/jobs/${jobId}` }),
  (res) => res.body.status === 'completed',
  { timeout: 60_000, interval: 2_000 }
);

// Style 2: run assertions (no return needed — passes if assertions don't throw)
await recurse(
  () => apiRequest({ method: 'GET', path: '/api/emails/latest' }),
  (res) => {
    expect(res.body.subject).toContain('Welcome');
    expect(res.body.to).toBe('user@example.com');
  }
);

// Style 3: with post-poll callback after success
await recurse(
  () => getOrderStatus(orderId),
  (status) => status === 'shipped',
  { post: (s) => log.success(`Order ${orderId} shipped`), timeout: 30_000 }
);

Three typed error shapes

Error typeWhen thrownKey properties to inspect
RecurseTimeoutErrorPredicate never satisfied within timeoutattempts, lastResult, timeoutMs
RecurseCommandErrorThe command itself threw an exceptioncause (original error), attempt
RecursePredicateErrorThe predicate threw an assertion errorcause, lastResult, attempt
Never mix recurse with waitForTimeout. Adding a hard wait before or inside a polling loop forces every poll to wait the full duration even when the condition is met immediately. Use interval and delay options to control spacing instead.
19
Intercept Network Call — Declarative Spy & Stub
intercept-network-call.md

What it is: A single declarative call to intercept browser network requests — either observing real requests as they pass through (spy mode) or replacing them with a fabricated response (stub mode). Automatically parses JSON and returns a structured result with responseJson, status, and requestBody.

Why it matters: Vanilla Playwright requires three separate steps: page.route() for setup, page.waitForResponse() for capture, and manual response.json(). interceptNetworkCall collapses all three into one call and eliminates 60–70% of the boilerplate.

Mandatory setup order — intercept BEFORE navigate

Step 1
Register the interceptor first. const call = interceptNetworkCall({ url: '**/api/users' }) — this returns a Promise. The listener is now active and waiting for a matching request.
Step 2
Trigger the navigation or action. await page.goto('/users') — the page fires the API request. The interceptor captures it.
Step 3
Await the result. const { responseJson, status, requestBody } = await call — Promise resolves with structured data from the captured request and response.

Spy mode vs Stub mode

// ✅ SPY — real request passes through; captured for assertion
const usersCall = interceptNetworkCall({ url: '**/api/users' });
await page.goto('/dashboard');
const { responseJson, status } = await usersCall;
expect(status).toBe(200);
expect(responseJson).toHaveLength(3);

// ✅ STUB — request intercepted; fake response returned; server never receives it
const errorCall = interceptNetworkCall({
  url: '**/api/orders',
  fulfillResponse: { status: 503, body: { error: 'Service Unavailable' } }
});
await page.goto('/orders');
await errorCall;
await expect(page.getByRole('alert')).toContainText('Something went wrong');

URL glob patterns

PatternMatches
**/api/usersAny URL ending in /api/users, any host
**/api/{users,products}Either /api/users or /api/products
**/api/users?id=*/api/users with any id query param
https://api.example.com/**All requests to a specific host

Multiple intercepts in one action

// Set up ALL intercepts BEFORE triggering the action that fires them
const [usersCall, rolesCall] = [
  interceptNetworkCall({ url: '**/api/users' }),
  interceptNetworkCall({ url: '**/api/roles' })
];
await page.goto('/admin');                   // fires both requests
const users = await usersCall;
const roles = await rolesCall;
Never navigate before registering the interceptor. Requests fired before the listener exists are gone — the Promise will hang until it times out. This is the #1 cause of flaky intercept tests.
20
Network Recorder — HAR-Based Offline Testing
network-recorder.md

What it is: A utility that records network traffic to HAR (HTTP Archive) files during test runs, then replays those files offline — no live backend required. Unlike native Playwright HAR, it supports stateful CRUD during playback: a POST that creates a resource will appear in subsequent GET responses within the same test.

Why it matters: Traditional E2E tests require a live backend (slow, flaky, expensive). HAR-based testing provides deterministic, fast, offline tests — the same server responses every run, independent of network conditions or server state.

Two modes — one env variable

ModeSet PW_NET_MODE toWhat happensUse case
RecordrecordAll requests hit the real server; full traffic saved to a .har file on diskInitial capture; update after backend changes
PlaybackplaybackAll requests served from the HAR file; server is never contactedCI without live server; deterministic regression tests

Setup — always before page.goto()

// networkRecorder.setup(context) MUST be called before any navigation
test('create and list users', async ({ page, context, networkRecorder }) => {
  await networkRecorder.setup(context);   // ← must come first
  await page.goto('/users');
  // record mode: real requests fired and persisted to HAR
  // playback mode: requests served from HAR, server never contacted
});

Stateful CRUD during playback

The recorder analyzes the HAR file, detects CRUD patterns by HTTP method and URL shape, and maintains an in-memory state during playback. This means full CRUD flows work without a backend:

  • NotePOST /api/users — creates a user and adds it to the in-memory list.
  • NoteGET /api/users (after the POST) — returns the list including the newly created user.
  • NoteDELETE /api/users/id — removes the item so future GETs reflect the deletion.
  • DiffNative Playwright HAR returns static responses only — it does NOT track state changes between requests.

Cross-environment URL mapping

// HAR recorded on staging, replayed in CI against localhost
await networkRecorder.setup(context, {
  hostMapping: {
    'staging.api.example.com': 'localhost:3000'
  },
  patterns: [
    { regex: /staging\.api/, replace: 'localhost:3000' }
  ]
});
Combine with interceptNetworkCall: Use the recorder for offline response serving and layer interceptNetworkCall on top for deterministic event-based waits. The recorder handles what responses are returned; the interceptor handles when to proceed.
21
Network Error Monitor — Silent Failure Detection
network-error-monitor.md

What it is: A fixture that automatically fails any test where a background HTTP request returns 4xx or 5xx — even when the UI appears to work correctly. Zero configuration: import the fixture and monitoring starts for every test in that file. Think of it as Sentry for your test suite.

Why it matters: Backend errors can occur silently while the UI renders a success state (cached data, optimistic updates, or incomplete error handling). Without this monitor, tests pass falsely and bugs reach production. With it, any unexpected API error surfaces immediately as a test failure.

Activation — import only

// Import the fixture — monitoring is ON for every test in this file
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';

test('dashboard loads without errors', async ({ page }) => {
  await page.goto('/dashboard');
  // If ANY background API call returns 4xx/5xx → test fails automatically
  await expect(page.getByText('Welcome')).toBeVisible();
});

Opt-out for intentional error scenarios

// Tests that deliberately trigger 4xx/5xx must opt out
test('shows 404 message for missing resource', {
  annotation: [{ type: 'skipNetworkMonitoring' }]
}, async ({ page }) => {
  await page.goto('/items/nonexistent-id');
  await expect(page.getByText('Item not found')).toBeVisible();
});

Configuration options

OptionDefaultPurpose
excludePatterns[]URL patterns (glob or regex) to ignore. Use for known legitimate 4xx endpoints — e.g. an identity verification service that returns 404 when the user is not found.
maxTestsPerErrorunlimitedSet to 1 to fail only the first test per unique error pattern, then emit warnings on subsequent tests. Prevents one broken endpoint from failing every test in the suite.

Artifacts — network-errors.json

On failure the monitor writes a network-errors.json artifact alongside the HTML report. Each entry contains: url, status, method, timestamp.

Domino effect protection: Set maxTestsPerError: 1 when a broken backend endpoint would otherwise fail every test in the suite. The first test surfaces the root cause; subsequent tests warn instead of failing, keeping the report readable.
22
Structured Logging Utility
log.md

What it is: A 6-level structured logging utility that surfaces output inside Playwright's HTML report as collapsible test steps — not just buried in the terminal. Automatically formats objects and arrays as indented JSON. Supports optional file-based logging for persistent logs across CI runs.

Why it matters: console.log() is invisible in HTML reports, creates no navigable steps, and disappears in terminal noise. Structured logging makes debugging visible exactly where it matters — inside the report, adjacent to the failing assertion.

6 log levels

LevelMethodUse caseReport appearance
steplog.step()Major test phases: ARRANGE / ACT / ASSERTCollapsible section heading in HTML report
infolog.info()General informational messagesInline entry under current step
successlog.success()Confirmation a key step completedGreen-tinted inline entry
warninglog.warning()Non-fatal condition worth notingYellow-tinted inline entry
errorlog.error()Errors the test is deliberately catchingRed-tinted inline entry
debuglog.debug()Verbose data: objects, arrays, API responsesAuto-formatted JSON block

AAA pattern with structured logging

import { log } from '@seontechnologies/playwright-utils';

test('create user and verify', async ({ apiRequest }) => {
  await log.step('ARRANGE: Build test user data');
  const userData = createUserData({ role: 'admin' });
  await log.debug(userData);              // auto-formats object as JSON in report

  await log.step('ACT: POST /api/users');
  const { status, body } = await apiRequest({
    method: 'POST', path: '/api/users', body: userData
  });

  await log.step('ASSERT: Verify 201 and user ID');
  expect(status).toBe(201);
  await log.success(`User created: ${body.id}`);
});

Sync variants for globalSetup / globalTeardown

// Async test context is not available in globalSetup — use sync methods
log.infoSync('Global setup: seeding database...');
log.successSync('Seed completed — 5 users, 3 orders');

// File logging for persistent artifacts across test runs
log.configure({
  fileLogging: { enabled: true, outputDir: './test-results/logs' }
});
  • AlwaysUse log.step() before each major phase (ARRANGE / ACT / ASSERT) to create navigable sections in the HTML report.
  • AlwaysUse log.debug(object) to inspect API responses or large data structures — it auto-formats as indented JSON.
  • NeverLog sensitive data — passwords, auth tokens, PII, credit card numbers. CI HTML reports may be publicly accessible or archived.
23
File Utilities — Download Capture & Format Parsing
file-utils.md

What it is: Utilities that simplify file download handling in Playwright tests — capturing the download event and reading the file's content from CSV, Excel/XLSX, PDF, or ZIP formats. Reduces ~80 lines of boilerplate (event orchestration, path resolution, parsing config, error handling) to 2–10 lines.

Why it matters: File download testing requires orchestrating the download event, waiting for the file to land on disk, then parsing it with the right library — all before writing a single assertion. These utilities automate the entire pipeline.

Capturing a download

import { handleDownload, readCSV } from '@seontechnologies/playwright-utils/file-utils';

// handleDownload orchestrates: register event → trigger click → wait → return path
const filePath = await handleDownload({
  page,
  downloadDir: './test-results/downloads',
  trigger:     () => page.getByRole('button', { name: 'Export CSV' }).click()
});

Supported formats

FormatFunctionReturn shapeNotes
CSVreadCSV({ filePath }){ data: object[], headers: string[] }Auto-detects headers from first row
Excel / XLSXreadXLSX({ filePath }){ sheets: { name, data }[] }Multi-sheet support built-in
PDFreadPDF({ filePath }){ content: string, pagesCount, fileName, info }Options: mergePages, maxPages
ZIPreadZIP({ filePath }){ entries: string[], extractedFiles: {} }Entry listing + selective extraction

Complete CSV export test

import * as fs from 'fs-extra';
const DOWNLOAD_DIR = './test-results/downloads';

test.afterEach(async () => fs.remove(DOWNLOAD_DIR));  // always clean up

test('users CSV export contains correct headers and data', async ({ page }) => {
  await page.goto('/admin/users');

  const filePath = await handleDownload({
    page, downloadDir: DOWNLOAD_DIR,
    trigger: () => page.getByRole('button', { name: 'Export' }).click()
  });

  const { data, headers } = await readCSV({ filePath });
  expect(headers).toEqual(expect.arrayContaining(['email', 'role', 'createdAt']));
  expect(data).toHaveLength(5);
  expect(data[0].email).toMatch(/.+@.+\..+/);
});
Always clean up downloads in afterEach. Downloaded files persist between test runs. Without cleanup, stale files from a previous run can produce false assertions or fill disk space on CI agents — especially on long-running pipelines.
24
Fixtures Composition — mergeTests Pattern
fixtures-composition.md

What it is: A pattern for combining fixtures from multiple sources — playwright-utils utilities and custom project-specific fixtures — into a single unified test object using Playwright's mergeTests(). Every test file imports from one place, updating a fixture propagates everywhere automatically.

Why it matters: Without composition, each test file must import from 4–6 different fixture modules. When a fixture's source path or API changes, every import across the project must be updated. The merged export is the single source of truth — change one file, all tests adapt.

Building the merged export

// tests/support/merged-fixtures.ts — the single import for ALL test files
import { mergeTests }          from '@playwright/test';

// playwright-utils fixtures
import { test as apiFixture }       from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as authFixture }      from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { test as logFixture }       from '@seontechnologies/playwright-utils/log/fixtures';
import { test as monitorFixture }   from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';

// custom project fixtures
import { test as userFixture }   from './fixtures/user.fixture';
import { test as orderFixture }  from './fixtures/order.fixture';

export const test = mergeTests(
  apiFixture, authFixture, logFixture, monitorFixture,
  userFixture, orderFixture
);
export { expect } from '@playwright/test';

// In every test file — one clean import, all fixtures available
// import { test, expect } from '../../support/merged-fixtures';

Name conflict rules

ScenarioRuleRecommended solution
Two fixtures share the same property nameLast fixture passed to mergeTests() winsPrefix custom fixtures to avoid collision: myAppUser, myAppOrder
Override a utility option for one fileUse test.use() at the top of the filetest.use({ authOptions: { userIdentifier: 'admin' } })
Override for one describe blockUse test.use() inside the describeScoped override — other tests in the file are unaffected

Lazy evaluation — zero cost for unused fixtures

// A test that doesn't request networkRecorder pays NO setup cost for it
test('fast API-only test', async ({ apiRequest, log }) => {
  // Only apiRequest and log fixtures are initialized — the rest are skipped
  const { status } = await apiRequest({ method: 'GET', path: '/api/health' });
  expect(status).toBe(200);
});
Playwright evaluates fixtures lazily. Merging 10 fixtures into one test object does not mean every test initializes all 10. Only the fixtures explicitly requested in the test's argument list are set up — so merging is always zero-cost for tests that don't use a given fixture.

§6 — Testing Patterns

Files 25 – 28
25
API Testing Patterns
api-testing-patterns.md

What it is: Patterns for direct API testing without browser overhead — REST CRUD, GraphQL, schema validation, error shape assertion, and async operation polling. API tests using apiRequest run 100× faster than E2E tests covering the same logic and produce more actionable failure messages because there is no DOM noise obscuring the root cause.

When to use over E2E: CRUD operations, business logic validation, error handling responses (4xx shapes), authentication flows, background jobs, service-to-service communication — any scenario where the browser is not the subject under test.

CRUD test scaffold

import { test, expect } from '../support/merged-fixtures';
import { createUserData } from '../support/factories/user.factory';

test.describe('Users API', () => {
  let userId: string;

  test.beforeAll(async ({ apiRequest }) => {
    const { body } = await apiRequest({
      method: 'POST', path: '/api/users', body: createUserData()
    });
    userId = body.id;
  });

  test.afterAll(async ({ apiRequest }) => {
    await apiRequest({ method: 'DELETE', path: `/api/users/${userId}` });
  });

  test('GET returns created user', async ({ apiRequest }) => {
    const { status, body } = await apiRequest({
      method: 'GET', path: `/api/users/${userId}`,
      validateSchema: UserSchema      // Zod schema — fails if shape drifts
    });
    expect(status).toBe(200);
    expect(body.id).toBe(userId);
  });

  test('PATCH updates user role', async ({ apiRequest }) => {
    const { status, body } = await apiRequest({
      method: 'PATCH', path: `/api/users/${userId}`,
      body: { role: 'admin' }
    });
    expect(status).toBe(200);
    expect(body.role).toBe('admin');
  });
});

Asserting error shape — not just status code

// Test that invalid input returns a structured, parseable error body
test('POST rejects missing required fields', async ({ apiRequest }) => {
  const { status, body } = await apiRequest({
    method: 'POST', path: '/api/users',
    body: { email: 'not-an-email' }     // missing name, missing valid email
  });
  expect(status).toBe(422);
  expect(body.errors).toEqual(
    expect.arrayContaining([
      expect.objectContaining({ field: 'email' }),
      expect.objectContaining({ field: 'name' })
    ])
  );
});

Async job polling — apiRequest + recurse

// Trigger a long-running job, then poll until it completes
test('bulk import job completes successfully', async ({ apiRequest }) => {
  const { body: job } = await apiRequest({
    method: 'POST', path: '/api/jobs/bulk-import',
    body: { fileKey: 's3://bucket/users.csv' }
  });
  expect(job.status).toBe('queued');

  await recurse(
    () => apiRequest({ method: 'GET', path: `/api/jobs/${job.id}` }),
    (res) => res.body.status === 'completed',
    { timeout: 60_000, interval: 3_000 }
  );

  const { body: result } = await apiRequest({ method: 'GET', path: `/api/jobs/${job.id}/results` });
  expect(result.importedCount).toBe(100);
  expect(result.failedCount).toBe(0);
});

API vs E2E — decision guide

ScenarioPreferred levelReason
Validate CRUD responses, error shapes, paginationAPI testNo DOM, no browser — 100× faster, direct signal
Test that the UI calls the right endpoint on submitIntegration (intercept)Use interceptNetworkCall spy — no full backend needed
Validate the full user-facing journey end to endE2ECross-system flow that only makes sense through the browser
Test business logic inside a service classUnitExtract and test the pure function directly — no HTTP call
26
Component TDD — Red-Green-Refactor Loop
component-tdd.md

What it is: A Test-Driven Development discipline for UI components: write a failing test first, implement the minimum code to make it pass, then refactor with passing tests as a safety net. Covers React/Vue components with Playwright Component Tests, Cypress cy.mount, or React Testing Library. Includes accessibility and keyboard navigation assertions from the start.

Why it matters: TDD for components provides immediate feedback — failing tests clarify requirements before code is written, minimal implementations prevent over-engineering, and tests act as a regression guard during refactoring. Without TDD, components grow beyond their tested scope and become untestable monoliths.

The Red-Green-Refactor loop

Red
Write a failing test that describes exactly the desired behavior. Run it — confirm it fails for the right reason (missing feature, not a broken test setup). If you can't write the test, requirements are unclear — stop and clarify.
Green
Write the minimum code to make the test pass. Ugly, hardcoded, unabstracted — all acceptable at this stage. The only goal is a green test. Resist the urge to build more than the test requires.
Refactor
Clean up the implementation while keeping all tests green. Add abstractions, eliminate duplication, improve names. If a test breaks during refactoring, the refactor introduced a regression — fix it before continuing.

Provider isolation — fresh context per test

// Wrap every component with fresh provider instances to prevent state bleed
function AllTheProviders({ children }: { children: ReactNode }) {
  // new QueryClient() per render — prevents cache contamination between parallel tests
  return (
    <QueryClientProvider client={new QueryClient()}>
      <MemoryRouter>
        <AuthProvider>{children}</AuthProvider>
      </MemoryRouter>
    </QueryClientProvider>
  );
}

// Pass as the wrapper option to RTL render
render(<UserCard userId="usr_001" />, { wrapper: AllTheProviders });

// In Playwright Component Tests
await mount(<AllTheProviders><UserCard userId="usr_001" /></AllTheProviders>);

Accessibility testing in the TDD loop

// Include a11y assertions from the very first test — not as an afterthought
import { AxeBuilder } from '@axe-core/playwright';

test('UserCard has no accessibility violations', async ({ page, mount }) => {
  await mount(<UserCard userId="usr_001" />);
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Keyboard navigation testing

// Test Tab, Enter, Escape flows — not just mouse interactions
test('dropdown is fully keyboard navigable', async ({ page }) => {
  await page.getByRole('combobox', { name: 'Status' }).focus();
  await page.keyboard.press('Enter');           // open dropdown
  await expect(page.getByRole('listbox')).toBeVisible();
  await page.keyboard.press('ArrowDown');        // navigate to first option
  await page.keyboard.press('Enter');           // select it
  await expect(page.getByRole('combobox')).toHaveValue('Active');
  await page.keyboard.press('Escape');           // close — listbox must hide
  await expect(page.getByRole('listbox')).not.toBeVisible();
});
  • RuleWrite the failing test before any implementation code. If you can't write the test first, the component API is unclear — clarify requirements before coding.
  • RuleEach render must use a fresh provider instance. Shared provider state between tests creates ordering dependencies and non-deterministic failures in parallel runs.
  • AvoidVisual regression (screenshot comparison) as primary TDD assertions — they are expensive to maintain and break on trivial styling changes. Use them selectively for visually critical components only.
27
Visual Debugging & Developer Ergonomics
visual-debugging.md

What it is: Configuration and workflows for capturing rich debugging artifacts — traces, screenshots, videos, HAR files — only on failure, plus tooling for time-travel debugging through Playwright's Trace Viewer and interactive Inspector. Reduces CI failure triage time by 80–90% by making remote debugging possible from a single trace.zip.

Why it matters: CI failures with minimal context force developers to reproduce them locally — a process that can take hours. Trace files, screenshots, and videos captured at the exact moment of failure show what the test saw, when, and why it failed.

Recommended artifact config in playwright.config.ts

export default defineConfig({
  use: {
    trace:      'retain-on-failure-and-retries',  // keeps all retry attempts
    screenshot: 'only-on-failure',               // saved as PNG in test-results/
    video:      'retain-on-failure',              // only for failed tests
  }
});

Trace Viewer — 5 panels

PanelWhat it showsUse it to
TimelineAll test actions in chronological orderFind which action immediately preceded the failure
SnapshotsDOM state before and after each actionInspect exact element state at any point in the test
NetworkAll HTTP requests with timing and payloadsFind a missing waitForResponse — see when requests actually fired
ConsoleBrowser console output during the testCatch unhandled JS errors and unexpected warnings
SourceTest code with the current line highlightedMap each trace event back to the specific test line

Opening and analyzing traces

# Open a specific trace file (downloaded from CI artifacts)
npx playwright show-trace test-results/users-create-chromium/trace.zip

# CLI trace analysis — useful inside coding agents (no GUI needed)
npx playwright trace actions --grep="expect" trace.zip
npx playwright trace snapshot 42 trace.zip    # DOM snapshot at action #42

Interactive Inspector — local debugging

# Run in debug mode — Inspector opens automatically, pauses before first action
PWDEBUG=1 npx playwright test users/create.spec.ts

# Or pause at a specific point inside test code
await page.pause();   // execution stops; Inspector opens for interactive exploration

# Step through the test action by action, run locator queries live
npx playwright test --debug users/create.spec.ts

CI artifact upload — GitHub Actions

# Upload all test artifacts even when the job fails
- name: Upload test results
  uses: actions/upload-artifact@v4
  if: always()                     # critical — must upload on failure
  with:
    name: playwright-report-${{ matrix.shard }}
    path: |
      playwright-report/
      test-results/
    retention-days: 30
  • NeverUse video: 'on' in CI — records video for every test regardless of outcome. Generates gigabytes of files and significantly slows execution. Use 'retain-on-failure'.
  • NeverUse trace: 'on' in CI for the same reason — trace files are large. 'retain-on-failure-and-retries' captures what you need without the overhead.
  • AlwaysUpload test artifacts with if: always() in CI — without this, artifacts are not saved when the job fails, which is exactly when you need them.
28
Playwright Configuration Guardrails
playwright-config.md

What it is: Opinionated patterns for playwright.config.ts: centralized multi-environment URL management via envConfigMap, standardized timeout values, reporter configuration, parallelization and sharding, and auth-state project structure. The config file is the single source of truth for all environment and execution behaviour — when it drifts, every test becomes suspect.

Multi-environment URL management — envConfigMap

import { defineConfig } from '@playwright/test';

const envConfigMap: Record<string, { baseURL: string }> = {
  local:      { baseURL: 'http://localhost:3000'        },
  staging:    { baseURL: 'https://staging.example.com'  },
  production: { baseURL: 'https://example.com'          },
};

const env = process.env.TEST_ENV;
if (!env || !envConfigMap[env]) {
  // Fail-fast: unknown environment is always a mistake, never a default
  throw new Error(
    `TEST_ENV must be one of: ${Object.keys(envConfigMap).join(', ')}. Got: "${env}"`
  );
}

export default defineConfig({
  use: { ...envConfigMap[env] },
});

Standardized timeout values

TimeoutValueCovers
actionTimeout15 000 msSingle click, fill, keyboard event — single user interaction
navigationTimeout30 000 msPage load, URL change, waitForNavigation
expect.timeout10 000 msAssertion retry window before the assertion is declared failed
timeout (test)60 000 msTotal test duration including setup, execution, and teardown

Parallelization and sharding

import os from 'os';

export default defineConfig({
  fullyParallel: true,
  workers:  process.env.CI ? 1 : os.cpus().length - 1,  // 1 worker in CI avoids contention
  retries:  process.env.CI ? 2 : 0,                        // retries only in CI, never locally
});

# Shard the suite across 4 CI machines — each runs 25% of tests
# npx playwright test --shard=1/4   (machine 1)
# npx playwright test --shard=2/4   (machine 2)  ... etc

Auth-state projects — setup once, reuse everywhere

export default defineConfig({
  projects: [
    // Step 1: run auth setup once; saves auth state to disk
    { name: 'setup', testMatch: '**/auth.setup.ts' },

    // Step 2: authenticated tests depend on setup project
    {
      name:         'authenticated',
      dependencies: ['setup'],
      use:          { storageState: 'playwright/.auth/user.json' }
    },

    // Step 3: unauthenticated tests run independently
    { name: 'unauthenticated' }
  ]
});

Reporter configuration

export default defineConfig({
  reporter: [
    ['html',  { outputFolder: 'playwright-report', open: 'never' }],  // visual HTML report
    ['junit', { outputFile: 'test-results/results.xml' }],            // CI/CD parsing
    ['list']                                                            // terminal output
  ]
});
Always validate TEST_ENV at config load time. If the config silently falls back to a default environment when TEST_ENV is missing or mistyped, tests may run against the wrong environment — including production. The fail-fast pattern above makes this impossible.

§7 — CI & Execution Strategy

Files 29 – 31
29
CI Pipeline & Burn-In Strategy
ci-burn-in.md

What it is: A CI pipeline architecture that runs changed test files 10× in a loop (burn-in) before allowing merge, then shards the full suite across multiple machines with fail-fast: false. Burn-in stress-tests new or modified tests to flush out flakiness before it reaches the main branch.

Why it matters: A test that passes once locally can still be flaky — it may fail 1 in 10 runs due to a race condition or timing issue. Running it 10 times in CI catches this before the test is trusted. fail-fast: false ensures all shards complete even when one fails, preserving the full evidence set for post-mortem analysis.

Pipeline stages

Stage 1
Install & cache. npm ci with cache key on package-lock.json hash. Avoids re-downloading dependencies on every run.
Stage 2
Burn-in loop. Detect changed spec files with git diff --name-only, then run them 10× in a shell loop. If any iteration fails, the stage fails and blocks merge.
Stage 3
Sharded full regression. Run the complete suite split across N machines with fail-fast: false. All shards complete before the merge gate is evaluated.
Stage 4
Merge results. Collect JUnit XML from all shards, merge into a single report, publish to the HTML reporter.

Burn-in loop — detect changed specs and repeat

# Detect changed spec files relative to main branch
CHANGED_SPECS=$(git diff --name-only origin/main...HEAD \
  | grep -E '\.(spec|test)\.(ts|js)$' \
  | tr '\n' ' ')

# Run changed specs 10 times — exit 1 on first failure
for i in {1..10}; do
  echo "Burn-in iteration $i/10"
  npx playwright test $CHANGED_SPECS || exit 1
done

Sharded full suite — GitHub Actions matrix

# .github/workflows/playwright.yml
jobs:
  test:
    strategy:
      fail-fast: false          # all shards run even if one fails
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: npx playwright test --shard=${{ matrix.shard }}/4

  merge-reports:
    needs: test
    if: always()                # run even when tests fail
    steps:
      - run: npx playwright merge-reports ./all-blobs --reporter html

Dependency cache

# Cache node_modules — busts when package-lock.json changes
- uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
  • RuleAlways set fail-fast: false in the sharding matrix. A single failing shard should not prevent the other shards from running — you need the full failure picture to diagnose systemic issues.
  • NeverUse ${{ inputs.* }} directly inside run: blocks — this is a script injection vulnerability. Assign to an env var first and reference that instead.
  • AlwaysUpload test artifacts with if: always() so that trace files are available even when the job fails — which is the only time they matter.
30
Selective & Targeted Test Execution
selective-testing.md

What it is: A strategy for running only the tests that matter at each stage of the development workflow, using tags (@smoke, @p0@p3, @regression) and changed-file detection. Tests are promoted through stages: smoke (<5 min) → PR (<10 min) → full regression (<30 min). Running the full suite on every commit wastes developer time without adding safety.

Tag taxonomy

TagScopeTime budgetWhen to run
@smokeCritical path only — can the product load and function at all?<5 minEvery commit, pre-commit hook
@p0Revenue, security, data integrity pathsPart of @p0+@p1 ≤10 minEvery PR before review
@p1Core user journeysPart of @p0+@p1 ≤10 minEvery PR before review
@p2Secondary features, admin flowsPart of full ≤30 minPre-merge gate
@p3Cosmetic, rarely used pathsPart of full ≤30 minNightly only
@regressionFull suite<30 minPre-merge to main, nightly

Tagging tests

// Tag with priority and type — multiple tags allowed
test('@smoke @p0 login flow works', async ({ page }) => { ... });
test('@p1 search returns relevant results', async ({ page }) => { ... });
test('@p2 CSV export downloads correctly', async ({ apiRequest }) => { ... });
test('@p3 button hover shows tooltip', async ({ page }) => { ... });

Executing by tag

# Run only smoke tests (every commit)
npx playwright test --grep "@smoke"

# Run P0 + P1 (PR check)
npx playwright test --grep "@p0|@p1"

# Run full regression (pre-merge)
npx playwright test --grep "@regression"

# Exclude a tag (skip nightly-only tests)
npx playwright test --grep-invert "@p3"

Changed-file detection → tag mapping

# Map changed files to test tags automatically
CHANGED=$(git diff --name-only origin/main...HEAD)

if echo "$CHANGED" | grep -q "src/components/"; then
  TAGS="@smoke|@p0|@p1"          # UI changed → run component + E2E tests
elif echo "$CHANGED" | grep -q "src/api/"; then
  TAGS="@p0|@p1"                  # API changed → run integration + E2E tests
else
  TAGS="@smoke"                    # other changes → smoke only
fi

npx playwright test --grep "$TAGS"

Test promotion — three stages

StageTriggerTags runTime budgetGate
Pre-commitLocal git hook@smoke<5 minBlocks commit if failing
PR checkPR open / push to branch@p0 + @p1 + changed specs<10 minRequired status check
Merge gatePR approved, before merge to main@regression<30 minBlocks merge if failing
Execution budget discipline: If any stage exceeds its time budget, the first action is to check whether tests at a lower level (unit / API) can replace the slow tests — not to increase the budget. Slow CI is a symptom of tests at the wrong level.
31
Feature Flag Governance & Testing
feature-flags.md

What it is: A structured approach to managing feature flags with TypeScript type safety, metadata tracking (owner, expiry date, dependencies), comprehensive testing of both enabled and disabled states, and automated cleanup detection. Without governance, flags accumulate as technical debt — untested variations ship broken code and forgotten flags clutter the codebase indefinitely.

Centralized flag registry with type safety

// src/feature-flags.ts — single source of truth for all flags
export const FLAGS = Object.freeze({
  NEW_CHECKOUT_FLOW:    'new-checkout-flow',
  DARK_MODE:            'dark-mode',
  AI_SEARCH_ENABLED:    'ai-search-enabled',
} as const);

// Type-safe key — typos caught at compile time, not at runtime
export type FlagKey = (typeof FLAGS)[keyof typeof FLAGS];

// Metadata registry — governance information per flag
export const FLAG_REGISTRY: Record<FlagKey, FlagMetadata> = {
  [FLAGS.NEW_CHECKOUT_FLOW]: {
    owner:           'payments-team',
    createdDate:     '2024-09-01',
    expiryDate:      '2024-11-01',    // drives automated audit
    defaultState:    false,
    requiresCleanup: true,
  },
};

Testing both flag states — mandatory for P0/P1 features

// Always test BOTH enabled and disabled — never assume one state
test.describe('new-checkout-flow flag', () => {

  test('@p0 enabled: shows new checkout UI', async ({ page, featureFlags }) => {
    await featureFlags.setVariation(FLAGS.NEW_CHECKOUT_FLOW, true);
    await page.goto('/checkout');
    await expect(page.getByTestId('new-checkout-form')).toBeVisible();
  });

  test('@p0 disabled: shows legacy checkout UI', async ({ page, featureFlags }) => {
    await featureFlags.setVariation(FLAGS.NEW_CHECKOUT_FLOW, false);
    await page.goto('/checkout');
    await expect(page.getByTestId('legacy-checkout-form')).toBeVisible();
  });

  test.afterEach(async ({ featureFlags }) => {
    await featureFlags.reset(FLAGS.NEW_CHECKOUT_FLOW); // restore default after each test
  });
});

Flag lifecycle checklist

Before create
Define: name (matches FLAGS enum), owner team, expiry date, default state, whether cleanup is required when removed.
During dev
Implement both code paths (enabled + disabled). Write tests for both states. Tag tests with the flag name for traceability.
Post-launch
Monitor adoption metrics. Once fully rolled out, schedule the cleanup ticket before the expiry date.
Cleanup
Remove the flag from FLAGS and FLAG_REGISTRY, delete the disabled code path, and delete the disabled-state tests. Run the full regression suite to confirm nothing broke.

Automated audit script

# Detect governance issues — run in CI on a schedule
npm run feature-flags:audit

# Reports:
# - Flags past their expiry date (need cleanup)
# - Flags with no owner assigned
# - Flags with requiresCleanup: true but no cleanup ticket linked
# - Flags present in code but missing from FLAG_REGISTRY
  • RuleEvery flag must have an expiryDate in FLAG_REGISTRY. Flags without expiry dates are never cleaned up — they become permanent dead code.
  • NeverTest only the enabled state. The disabled fallback is the default for most users until the flag is fully rolled out — a broken fallback is a broken product.
  • NeverShare flag state between parallel tests. Use per-test featureFlags.setVariation() with afterEach cleanup — parallel tests will interfere with each other otherwise.

§8 — Resilience & Workflow Utilities

Files 32 – 35
32
Error Handling & Resilience Testing
error-handling.md

What it is: Patterns for writing explicit error handling in tests — scoped exception catching (only ignore documented errors), retry validation (prove the retry mechanism actually fires N times before succeeding), telemetry logging with secret redaction, and graceful degradation testing (cached fallbacks, non-critical service failures).

Why it matters: Tests fail for two reasons: genuine bugs or poor error handling in the test itself. Without explicit patterns, tests become noisy (false positives from expected errors) or silent (swallowing real errors). Resilience testing also ensures the app handles real-world failures gracefully rather than exposing stack traces to users.

Scoped exception catching — only ignore documented errors

// ❌ BAD: global pageerror listener silences all JS errors
page.on('pageerror', () => {});

// ✅ GOOD: scoped listener — only ignore the one known error in this test
const KNOWN_ERROR = 'ResizeObserver loop limit exceeded';
page.on('pageerror', (err) => {
  if (err.message.includes(KNOWN_ERROR)) return;  // known, ignore
  throw err;                                         // unexpected — re-throw
});

Retry validation — prove N failures then success

// Stub the API to fail twice, succeed on the 3rd attempt
let attempts = 0;
await page.route('**/api/orders', (route) => {
  attempts++;
  if (attempts <= 2) {
    route.fulfill({ status: 500, body: '{"error":"Server Error"}' });
  } else {
    route.fulfill({ status: 201, body: '{"id":"ord_001"}' });
  }
});

await page.getByRole('button', { name: 'Place Order' }).click();
// UI must show retry indicator during attempts 1-2
await expect(page.getByTestId('retry-indicator')).toBeVisible();
// UI must show success after attempt 3
await expect(page.getByText('Order placed!')).toBeVisible();
expect(attempts).toBe(3);  // confirm exactly 3 attempts were made

Telemetry logging with secret redaction

// Log error context — never log sensitive fields
const REDACT_KEYS = ['password', 'token', 'creditCard', 'ssn'];

function safeLog(context: Record<string, unknown>) {
  const safe = Object.fromEntries(
    Object.entries(context).map(([k, v]) =>
      REDACT_KEYS.some(r => k.toLowerCase().includes(r))
        ? [k, '[REDACTED]']
        : [k, v]
    )
  );
  await log.error(safe);
}

Graceful degradation — non-critical service failure

// Analytics service is down — app must continue working normally
await page.route('**/analytics/**', route => route.fulfill({ status: 503 }));

await page.goto('/dashboard');
// Critical features must still work despite analytics being down
await expect(page.getByTestId('dashboard-content')).toBeVisible();
await expect(page.getByRole('button', { name: 'Create' })).toBeEnabled();
// No crash, no blocking modal — user is not informed of non-critical failure
  • RuleTest retry behavior with a counter — assert exactly N failures before success. Without the counter, you can't confirm the retry mechanism actually fired.
  • RuleTest graceful degradation for every non-critical service the app calls (analytics, feature flags, recommendations). If any of these going down breaks a core flow, it's a P0 bug.
  • NeverUse a global pageerror handler that swallows all errors. Scope it to the specific test that expects a known error.
33
Email-Based Authentication Testing
email-auth.md

What it is: Patterns for testing email-based authentication flows — magic links, registration confirmation codes, and password reset — using a real email capture service (Mailosaur recommended, Ethereal free, MailHog self-hosted). Covers session caching to minimize email consumption, link extraction, and negative test cases.

Why it matters: Email auth introduces unique testing challenges: asynchronous delivery (wait for email before clicking), quota limits (AWS Cognito: 50 emails/day in sandbox), and per-email costs in production-like environments. Session caching allows 500 tests to consume only 1 email instead of 500.

Email capture setup — Mailosaur

import { MailosaurClient } from 'mailosaur';
const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY);

// Generate a unique inbox per test — never hardcode email addresses
const serverId = process.env.MAILOSAUR_SERVER_ID;
const testEmail = `user-${crypto.randomUUID()}@${serverId}.mailosaur.net`;

Magic link extraction flow

// Step 1: Trigger the email (registration, login, password reset)
await apiRequest({ method: 'POST', path: '/api/auth/magic-link', body: { email: testEmail } });

// Step 2: Wait for email and extract the link
const message = await mailosaur.messages.get(serverId, {
  sentTo: testEmail,
  timeout: 30_000               // wait up to 30s for delivery
});

// Method A: direct link from Mailosaur SDK
const magicLink = message.html.links[0].href;

// Method B: parse HTML with JSDOM for custom button selectors
const { JSDOM } = await import('jsdom');
const dom = new JSDOM(message.html.body);
const magicLink = dom.window.document.querySelector('#magic-link-button')?.href;

// Step 3: Click the link and assert authentication succeeded
await page.goto(magicLink);
await expect(page).toHaveURL('/dashboard');

Session caching — 1 email for 500 tests

// globalSetup: authenticate once, save state to disk
export default async function globalSetup() {
  // 1. Register + verify email once
  const email = `seed-user-${crypto.randomUUID()}@${serverId}.mailosaur.net`;
  await registerUserViaEmail(email);
  const link = await extractMagicLink(email);

  // 2. Navigate and save auth state (cookies + localStorage)
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(link);
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
  await browser.close();
  // All 500 tests reuse playwright/.auth/user.json — 0 additional emails sent
}

Negative test cases — mandatory for auth flows

ScenarioHow to testExpected result
Expired linkGenerate link, wait 24h+ (or manipulate expiry in test env), click itError page: "Link expired — request a new one"
Invalid tokenConstruct a URL with a garbage token: /auth/verify?token=abc123garbageError page: "Invalid or unrecognized link"
Already usedClick the same magic link twiceSecond click shows: "Link already used"
Rate limitingRequest 10 magic links in rapid succession for the same emailResponse after threshold: 429 Too Many Requests
Never hardcode test email addresses. Hardcoded emails fail in parallel (both tests race to the same inbox) and pollute the email service history. Always generate a unique address per test with crypto.randomUUID().
34
Playwright CLI for Coding Agents
playwright-cli.md

What it is: Lightweight, stateless shell commands that allow coding agents (like Claude Code) to interact with a browser — take screenshots, query element references, inspect network traffic — without loading the full DOM accessibility tree into the context window. Each command returns compact element references (e15, e21) rather than a full DOM dump, saving ~93% of tokens per interaction compared to Playwright MCP.

Why it matters: Playwright MCP is powerful but expensive: every interaction loads the complete accessibility tree. The CLI returns only what was asked for — a snapshot, a screenshot, a selector — keeping the agent's context window free for actual test code.

Core commands

CommandPurposeReturns
open <url>Navigate to a URLConfirmation + page title
snapshotCapture accessibility snapshot of current pageCompact element reference list (e1...eN)
screenshotTake a visual screenshotPNG file path
click <ref>Click an element by referenceConfirmation
fill <ref> <text>Type into an input by referenceConfirmation
networkShow recent network requestsURL list with status codes
tracing-startBegin recording a Playwright trace
tracing-stopStop and save the traceTrace file path

Typical agent workflow

1
Run open <url> to navigate to the page being tested.
2
Run snapshot to get element references — use these to identify the correct selectors for the test code.
3
Use click or fill to interact and confirm the page responds as expected.
4
Run screenshot to capture visual evidence of the state for test documentation.
5
Write the actual test code using the confirmed selectors — reference the CLI findings, not guesswork.

Session scoping — avoid state leakage

# Use the -s flag to scope CLI sessions — prevents state leaking between explorations
playwright-cli -s=tea-explore open https://example.com/login
playwright-cli -s=tea-explore snapshot
playwright-cli -s=tea-explore screenshot

# Trace analysis — useful for debugging failing tests without a GUI
npx playwright trace open trace.zip
npx playwright trace actions --grep="expect" trace.zip   # filter by keyword
npx playwright trace snapshot 15 trace.zip              # DOM state at action #15
CLI for exploration, playwright-utils for execution. The CLI is a read-only discovery tool — use it to find selectors and understand page structure. Write the actual test code using playwright-utils fixtures and the confirmed selectors. Never substitute CLI interactions for test assertions.
35
Burn-In Runner — Git-Diff-Aware Test Execution
burn-in.md

What it is: A smarter version of Playwright's --only-changed flag. Analyzes git changes, filters out irrelevant file types via skipBurnInPatterns (config files, types, docs), controls test volume with burnInTestPercentage, and supports CI sharding. Prevents the problem where a change to a shared config file would otherwise trigger hundreds of unrelated tests.

Why it matters: Playwright's native --only-changed uses the import graph to determine affected tests. A change to playwright.config.ts or a shared type file marks every test as affected — triggering the entire suite. Burn-in adds an explicit skip-pattern filter and volume cap to keep feedback fast.

Three-phase filtering pipeline

Phase 1
Pattern skip. Files matching skipBurnInPatterns (config, types, docs, mocks) are removed from the changed-files list. A change to playwright.config.ts does not trigger any tests.
Phase 2
Dependency analysis. For the remaining changed files, trace the import graph to find all affected test files — same logic as --only-changed but applied only to the filtered list.
Phase 3
Volume control. Apply burnInTestPercentage to cap the number of tests actually run. With 45 affected tests and 30%, run 14 tests — a statistically meaningful sample without running everything.

Configuration

// playwright.config.ts — burnIn section
export default defineConfig({
  burnIn: {
    skipBurnInPatterns: [
      '**/playwright.config.*',   // config changes don't affect test logic
      '**/*types*',               // type-only files: no runtime impact
      '**/*.md',                  // docs: never trigger tests
      '**/mocks/**',              // mock data: not production code
    ],
    burnInTestPercentage: process.env.CI ? 0.2 : 0.3,  // 20% CI, 30% local
    repeatEach: 3,    // run each selected test 3× for stress testing
    retries:    1,    // 1 retry to distinguish flaky from broken
  }
});

Example output — 21 changed files

PhaseFiles / TestsResult
Changed files detected21 filesStarting point from git diff
After skip-pattern filter15 files (6 skipped: config, types, docs)Removed irrelevant files
After dependency analysis45 affected testsImport graph traced from 15 files
After volume control (30%)14 tests runFast, targeted, meaningful sample
Differentiate CI from local: Use a lower percentage in CI (0.2) where compute is shared, and a higher percentage locally (0.3) where a developer is iterating. The process.env.CI check handles this automatically.

§9 — Webhook Testing

Files 36 – 37
36
Webhook Testing Fundamentals
webhook-testing-fundamentals.md

What it is: Patterns for testing webhook delivery in event-driven architectures. Webhook delivery is eventually consistent — the application fires HTTP callbacks asynchronously after events. Tests must poll until the webhook arrives or time out gracefully. The module provides deterministic polling via recurse, typed matchers, rich timeout diagnostics, and parallel-safe isolation via startedAt scoping.

Why it fails without these patterns: Webhook tests fail for 4 structural reasons — eventual inconsistency (can't await directly), parallel journal pollution (worker A captures worker B's webhook), opaque timeouts (no context about what arrived vs what was expected), and cleanup leakage (registered expectations persist between tests).

Core polling pattern

import { webhookRegistry } from '../support/merged-fixtures';

test('order.created webhook delivered after checkout', async ({ page, webhookRegistry }) => {
  // Scope to this test's start time — ignores webhooks from prior tests
  const registry = webhookRegistry.forTest();

  // Trigger the event
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Place Order' }).click();

  // Poll until the webhook arrives — or throw WebhookTimeoutError
  const webhook = await registry.waitFor(
    orderCreatedTemplate(orderId)    // scoped by orderId for parallel safety
  );

  expect(webhook.body.event).toBe('order.created');
  expect(webhook.body.data.id).toBe(orderId);
});

Three matcher types — AND semantics

MatcherSyntaxUse case
matchFieldmatchField('data.id', orderId)Exact match on a dot-notation path. Most precise — use for IDs.
matchPartialmatchPartial({ event: 'order.created' })Deep subset match — extra payload fields are ignored. Use for event type checks.
matchPredicatematchPredicate('has amount', w => w.amount > 0)Arbitrary predicate with a human-readable label (required). Use for computed assertions.

AND semantics: All matchers on a template must pass — a webhook matches only if every matcher in the template succeeds simultaneously.

startedAt isolation — prevents cross-test pollution

// Each registry instance records the timestamp of creation
// waitFor() only considers webhooks received AFTER that timestamp
const registry = webhookRegistry.forTest();  // stamps current time
// → webhooks from previous tests are automatically excluded

// Template factories MUST be scoped by entity ID for parallel test safety
const orderCreatedTemplate = (orderId: string) =>
  webhookTemplate('order.created')
    .matchField('data.id', orderId)        // scoped by ID — worker A won't match worker B's webhook
    .matchPartial({ event: 'order.created' })
    .build();

WebhookTimeoutError — rich diagnostics

// When waitFor() times out, the error carries full diagnostic context
try {
  await registry.waitFor(orderCreatedTemplate(orderId));
} catch (err) {
  if (err instanceof WebhookTimeoutError) {
    console.log(err.totalReceived);       // how many webhooks arrived at all
    console.log(err.receivedWebhooks);    // last ≤10 webhooks (inspect actual payloads)
    console.log(err.matcherDetails);     // which matchers were configured
    // totalReceived: 0 → webhook never delivered — check app event publishing
    // totalReceived > 0 but no match → matcher misconfigured — inspect receivedWebhooks[0]
  }
}

Drain pattern — sequential events

// When testing delete, first drain the create webhook to avoid ambiguity
// Otherwise: the cleanup matcher might match the create webhook, not the delete
await registry.waitFor(movieCreatedTemplate(movieId));  // drain create first
await deleteMovie(movieId);                               // then trigger delete
await registry.waitFor(movieDeletedTemplate(movieId));  // now safe to assert delete
Webhooks are NOT WebSockets. This module handles HTTP callback delivery (POST from your server to a mock endpoint). Do not use it for testing real-time push notifications via WebSocket or Server-Sent Events — those require a different approach.
37
Webhook Testing Risk Guidance
webhook-risk-guidance.md

What it is: Risk classification and complete test coverage requirements for systems that publish webhooks. Webhook integration boundaries are high-risk by default because external consumers depend on payload shape, delivery timing, and event ordering — all of which are invisible to unit and component tests.

Default risk score: P2 × I3 = Score 6 (High — must mitigate). Probability 2 (Possible: webhook delivery failure occurs occasionally) × Impact 3 (Severe: external consumers miss transactions, integrations break silently). This means webhook delivery must have dedicated integration test coverage unless explicitly downgraded with documented justification.

Why score 6 is the baseline

FactorAssessment
Probability = 2 (Possible)Network failures, payload serialization bugs, event ordering issues — all occur under normal production conditions. Not rare (P1), not certain (P3).
Impact = 3 (Severe)Missed webhook = missed transaction for external consumer. A payment processor not receiving order.paid does not fulfil the order. Silent breakage, no UI feedback to users.
Not visible in unit testsThe event publishing logic and serialization can look correct in unit tests while silently breaking the HTTP delivery or payload shape.

Complete test coverage requirements — Score 6

  • Happy path: Action triggers event → webhook is delivered → payload matches expected shape. Must cover every event type the system publishes.
  • Sequential drain pattern: For systems that emit create then delete events, consume create webhook before asserting delete webhook. Prevents matcher ambiguity.
  • Parallel isolation: Multiple tests run simultaneously — each must use entity-ID-scoped templates. Worker A must never match Worker B's webhooks.
  • Timeout error shape: When webhook is not delivered (e.g. event publishing is disabled), assert the correct WebhookTimeoutError is thrown with meaningful totalReceived count.
  • Cleanup verification: After test teardown, confirm no unprocessed webhooks remain in the mock server journal that could pollute the next test.

Downgrading from Score 6 — requires justification

Downgrade conditionNew scoreRequired evidence
Webhook delivery failure has a retry mechanism with DLQ and alertingP2 × I2 = 4Retry config, DLQ config, alerting dashboard link
Consumer can tolerate missed events (idempotent polling as fallback)P2 × I2 = 4Architecture doc showing polling fallback
Webhook is for non-critical analytics only — no transactional impactP1 × I1 = 1Documented in risk register with product owner sign-off

Timeout values — match your pipeline latency

PipelineRecommended timeoutReason
Direct HTTP (synchronous dispatch)5 000 msShould arrive within seconds
Message queue (SQS, RabbitMQ)10 000 msQueue processing adds seconds of latency
Kafka / event streaming15 000–30 000 msKafka consumer lag can be significant in test environments
Default rule — apply until overridden: Any system with outbound webhooks defaults to P2×I3 = Score 6. This means webhook delivery must be covered by integration tests. To use a lower score, document the justification in the risk register and get explicit sign-off from the product owner.
Tier 3 · Specialized

Specialized Tier — 14 Files

Loaded only for deep-dive contract testing and webhook infrastructure work. Covers the full @seontechnologies/pactjs-utils library (§10) and the playwright-utils webhook subsystem (§11).

§10 · Files 38–46
Pact.js Utils — Contract Testing
Provider state helpers, Zod→Pact matchers, verifier config, request filter auth injection, MCP server, consumer framework scaffolding, broker webhooks, DI pattern.
§11 · Files 47–51
Webhook Infrastructure
Fixture wiring, cleanup strategy, typed template matchers, waitFor / waitForCount / getReceived patterns, timeout error debugging, provider backends (WireMock, MockServer, Mockoon).

§10 — Pact.js Utils

Files 38–46 · 9 files · Specialized
38
Pact.js Utils — Library Overview
pactjs-utils-overview.md

What it is: Production-ready utilities from @seontechnologies/pactjs-utils that eliminate boilerplate in consumer-driven contract testing. Wraps @pact-foundation/pact with type-safe helpers: provider state creation, PactV4 JSON interaction builder, verifier config assembly, and request filter auth injection. Works for both HTTP and message (async/Kafka) contracts.

Exposed Utilities

Utility Purpose
createProviderStateBuild [stateName, JsonMap] tuple from typed input
toJsonMapConvert object → Pact-compatible (null→"null", Date→ISO, nested→JSON string)
setJsonContent / setJsonBodyCurried callback helper for PactV4 builder lambdas
buildVerifierOptionsAssemble HTTP VerifierOptions; reads broker URL / token env vars
zodToPactMatchersZod schema → Pact V3 matchers — single source of truth
createRequestFilterAuth injection plugin; prevents double-Bearer bug

Decision Tree — Which Flow to Use

1Monorepo (consumer + provider in same repo)?→ use local flow (no broker needed)
2No Pact Broker, but OpenAPI spec available?BDCT flow (PactFlow only)
3Pact Broker / PactFlow available?remote CDCT flow with buildVerifierOptions

Why Use It Over Raw Pact

  • Raw Pact requires manual JsonMap casting — complex params (Date, null, nested) need manual serialization
  • Eliminates 30+ line verifier config scattered with env var logic
  • Prevents double-prefix token bug (Authorization: Bearer Bearer token)
  • Automated CI version tagging from GitHub Actions / GitLab CI env vars
39
Zod Schema → Pact V3 Matchers
pactjs-utils-zod-to-pact.md

What it is: zodToPactMatchers(schema, example) converts a Zod schema directly into Pact V3 matchers so you never maintain two representations of the same response shape. The schema is the single source of truth; the example value (or .openapi({ example }) metadata) provides concrete data.

Zod → Pact Mapping

Zod Type Pact V3 Matcher
z.string()string(example)
z.number()decimal(example)
z.number().int()integer(example)
z.boolean()boolean(example)
z.object({})recursive object (each field becomes a matcher)
z.array()eachLike(itemMatcher)

Example precedence

// Priority: arg > .openapi({ example }) > type default
const schema = z.object({
  id: z.number().int().openapi({ example: 42 }),
  name: z.string(),
});

// explicit example wins for name; openapi example wins for id
const matchers = zodToPactMatchers(schema, { name: 'Alice' });
// → { id: integer(42), name: string('Alice') }

Critical Rules

  • Consumer-curated schema only — include only the fields the consumer actually reads. Never import the full provider schema.
  • Do NOT wrap the result in like() — each field is already a matcher; wrapping adds a redundant outer layer.
  • For array responses: the entire body becomes eachLike(zodToPactMatchers(itemSchema, example))
  • Silent drift risk with hand-written matchers: schema changes must be applied in two places. zodToPactMatchers removes this entirely.
40
Consumer Helpers — Provider State & PactV4 Builders
pactjs-utils-consumer-helpers.md

What it is: createProviderState, toJsonMap, setJsonContent, and setJsonBody build type-safe provider state tuples and reusable PactV4 JSON callbacks — eliminating manual JsonMap casting and repetitive inline builder lambdas.

Core Helpers

Helper What it does
createProviderState({ name, params })Returns [string, JsonMap] tuple, spread directly into .given(...)
toJsonMap(obj)null→"null", Date→ISO, nested→JSON string, array→CSV
setJsonBody({ matchers })Shorthand for .willRespondWith(200, setJsonBody(...))
setJsonContent({ matchers })Sets JSON request/response content on PactV4 interaction builder

Usage Pattern

const state = createProviderState({
  name: 'a movie exists',
  params: { id: 42, createdAt: new Date('2024-01-01') },
});
// state = ['a movie exists', { id: 42, createdAt: '2024-01-01T00:00:00.000Z' }]

pact.addInteraction(
  new PactV4.HttpInteraction()
    .given(...state)           // spread the tuple
    .uponReceiving('GET /movie/42')
    .withRequest('GET', '/movie/42')
    .willRespondWith(200, setJsonBody({ body: matchers }))
);

Mandatory Vitest Config

Pact Rust FFI corrupts parallel workers — these three Vitest options are non-negotiable.
fileParallelism: falseNo parallel test files
pool: 'forks'Isolated process per suite
singleFork: trueOne fork, no concurrency
  • Exactly one pact.addInteraction() per it() block — FFI non-deterministically drops interactions when multiple are added
  • One .pacttest.ts file per consumer+provider pair — prevents FFI collision across files for the same pair
41
Provider Verifier — Config Assembly
pactjs-utils-provider-verifier.md

What it is: buildVerifierOptions, buildMessageVerifierOptions, handlePactBrokerUrlAndSelectors, and getProviderVersionTags assemble complete provider verification config in a single call — handling local/remote flow, broker URL resolution, consumer version selector strategy, and CI-aware version tagging.

Env Vars Read Automatically

PACT_BROKER_BASE_URLBroker / PactFlow URL
PACT_BROKER_TOKENPactFlow auth token
PACT_PAYLOAD_URLWebhook-triggered run (single pact)
PACT_BREAKING_CHANGECoordination mode flag
GITHUB_SHAProvider version tag in CI
GITHUB_REF_NAMEBranch name for selector

Consumer Version Selector Strategy

Mode Selector When to use
includeMainAndDeployed: truematchingBranch + mainBranch + deployedOrReleasedNormal verification (default)
includeMainAndDeployed: falsematchingBranch onlyBreaking-change coordination — isolate to current branch

Mandatory Vitest Config

Same Rust FFI constraint as consumer side — provider verification must run in a single fork.
  • pool: 'forks' + poolOptions.forks.singleFork: true — Pact Rust FFI holds process-wide state; parallel verification corrupts results
  • Use buildMessageVerifierOptions for Kafka / async message provider verification (separate entry point)
42
Request Filter — Auth Injection
pactjs-utils-request-filter.md

What it is: createRequestFilter and noOpRequestFilter inject an authentication header into every request during provider verification. A pluggable token-generator pattern prevents the double-Bearer bug (Authorization: Bearer Bearer token) and keeps auth concern separate from verifier config.

API

// Synchronous generator — pre-resolve async tokens before calling this
const requestFilter = createRequestFilter({
  tokenGenerator: () => myPreFetchedToken,   // returns raw value, NO "Bearer " prefix
});

// Provider that needs no auth
const requestFilter = noOpRequestFilter;     // (req, res, next) => next()

Bearer Prefix Contract

tokenGeneratorReturns raw token string
createRequestFilterPrepends "Bearer " once
ResultAuthorization: Bearer <token>

Async Token Pattern

// tokenGenerator must be synchronous — pre-fetch async tokens first
async function setupVerifierOptions() {
  const token = await fetchServiceAccountToken();   // async, done before filter
  return buildVerifierOptions({
    ...config,
    requestFilter: createRequestFilter({ tokenGenerator: () => token }),
  });
}
  • tokenGenerator must be synchronous — resolve async tokens before creating the filter, not inside it
  • Use noOpRequestFilter when the provider endpoint is unauthenticated — avoids conditional logic in verifier setup
  • Never prefix the token manually in tokenGenerator — the filter adds "Bearer " automatically, adding it yourself causes the double-prefix bug
43
Pact MCP Server — AI Agent Integration
pact-mcp.md

What it is: SmartBear's MCP server that lets AI agents interact with PactFlow / Pact Broker inside the contract testing workflow. Provides 8 tools covering test generation, provider state fetching, quality review, and deployment safety checks — all accessible via Model Context Protocol.

8 Available Tools

Tool What it does TEA Skill
Generate Pact TestsGenerate consumer/provider test from code, OpenAPI, or template*automate
Fetch Provider StatesQuery live broker for existing provider states*test-design
Review Pact TestsAutomated quality check against best practices*test-review
Can I DeployCheck deployment safety via broker matrix*ci
MatrixView full consumer/provider verification matrix*ci
PactFlow AI StatusPactFlow AI feature availability and config
Metrics AllBroker-wide contract testing metrics
Metrics TeamPer-team metrics breakdown

Installation (Claude Code)

# PactFlow (cloud)
claude mcp add-json -s user smartbear \
  '{"command":"npx","args":["-y","@smartbear/pact-mcp"]}'

# Required env vars (PactFlow)
PACT_BROKER_BASE_URL=https://<org>.pactflow.io
PACT_BROKER_TOKEN=<your-pactflow-token>

# Self-hosted broker (instead of token)
PACT_BROKER_USERNAME=<user>
PACT_BROKER_PASSWORD=<pass>
  • Requires Node.js 20+
  • Install per-project (-s user scopes to current user, not global) — different projects may use different PactFlow tenants
  • Live broker queries mean the agent sees real provider states, not stale documentation
44
Consumer Framework Setup — Scaffolding & Conventions
pact-consumer-framework-setup.md

What it is: Opinionated scaffolding and battle-tested conventions for a Pact.js consumer CDC project — directory structure, Vitest config, package.json scripts, shell scripts, and CI workflow layout. Codifies the pactjs-utils reference implementation so new projects are CI-portable from day one.

Directory Structure

tests/
  contract/
    support/            ← shared helpers, NOT consumer tests
      pact-setup.ts
      test-context.ts
    movie-service/
      get-movie.pacttest.ts      ← one file per consumer+provider pair
      create-movie.pacttest.ts
pacts/                  ← generated pact files (gitignored in CI)
vitest.config.pact.ts   ← separate config, never share with unit tests

Required Vitest Config (vitest.config.pact.ts)

export default defineConfig({
  test: {
    include: ['tests/contract/**/*.pacttest.ts'],
    fileParallelism: false,   // ← mandatory
    pool: 'forks',            // ← mandatory
    poolOptions: { forks: { singleFork: true } },  // ← mandatory
  },
});

NPM Scripts

test:pact:consumerRun consumer pact tests
publish:pactPublish pacts to broker via shell script
can:i:deploy:consumerCheck deployment safety
record:consumer:deploymentRecord deploy in broker after release
  • Use .pacttest.ts extension — never .pact.spec.ts or .contract.ts; keeps pact tests excluded from unit test runs by default
  • One .pacttest.ts per consumer+provider pair — splitting interactions for the same pair across files causes FFI collision
  • Normalize interaction order before publishing: run jq -S 'sort_by(.)' on generated pact JSON for byte-stable broker publishes (avoids "Cannot change pact content for already published pact" errors)
  • Support files live in tests/contract/support/ — never mix them with consumer test files
45
Pact Broker Webhooks — PactFlow → GitHub Actions
pact-broker-webhooks.md

What it is: Configuring PactFlow webhooks to automatically trigger provider verification in GitHub Actions when a consumer publishes a new pact. Covers webhook URL and body setup, PAT management, secret rotation runbook, and staleness monitoring to detect silent webhook failures before they block deployments.

Webhook Setup Steps

1Create a dedicated GitHub machine user (pactflow-<org>) — never use a personal account
2Generate a classic PAT with repo scope; set expiration to "No expiration" for long-lived machine-user tokens
3Store PAT as a PactFlow secret; reference it via ${user.githubToken} in the webhook Authorization header
4Configure POST to https://api.github.com/repos/<org>/<repo>/dispatches with event_type: "contract_requiring_verification_published"
5Add staleness monitoring: daily CI job queries broker matrix; fails if any verification is >24 h old

Webhook Event Choice

Event When it fires Recommended?
contract_requiring_verification_publishedOnly when the new pact needs verification✓ Yes — avoids redundant CI runs
contract_publishedEvery publish, even if pact unchanged✗ No — triggers unnecessary provider runs
Without working webhooks, consumer can-i-deploy silently times out after 900 s. Webhook failures are silent — the team only discovers the broken pipeline when PRs are blocked days later. Staleness monitoring is essential.
46
Consumer DI Pattern — Real Client in Contract Tests
pact-consumer-di.md

What it is: Inject the Pact mock server URL into the real consumer HTTP client via an optional baseUrl field on the API context type. This ensures contract tests exercise the actual consumer code — including retry logic, header assembly, timeout config, and error handling — rather than hand-crafted fetch() calls.

The Problem with Raw fetch()

Raw fetch() in test callbacks only proves that Pact returns what you told it to — the real consumer client's retry logic, custom headers, timeout handling, and error mapping are never exercised. Contract mismatches hidden by raw fetch surface in production.

Implementation

// 1. Add optional baseUrl to your API context type
interface ApiContext {
  apiKey: string;
  baseUrl?: string;           // ← one-line change
}

// 2. Use nullish coalescing in the client
const client = axios.create({
  baseURL: context.baseUrl ?? API_BASE_URL,  // Pact URL wins in tests
});

// 3. Shared test context helper
function createTestContext(mockServerUrl: string): ApiContext {
  return { apiKey: 'test-key', baseUrl: mockServerUrl };
}

// 4. In the pact test executeTest() callback
const api = createApiClient(createTestContext(mockServer.url));
const result = await api.getMovie(42);
expect(result.id).toBe(42);   // assert on parsed objects, not HTTP status
  • Assert on return values and parsed objects — not raw HTTP status codes. The contract already covers status codes; the consumer assertion should cover business logic.
  • When the real client reveals a contract mismatch → fix the contract. The mismatch is a real bug that raw-fetch tests were silently hiding.
  • Migrate existing consumer tests from raw fetch() to the DI pattern incrementally — each migrated test strengthens the contract's real-world coverage.

§11 — Webhook Infrastructure

Files 47–51 · 5 files · Specialized
47
Webhook Module Setup — Fixture Wiring & Cleanup
webhook-module-setup.md

What it is: Central fixture wiring using webhookProviderFixture + webhookFixture + mergeTests, with cleanup strategy selection (matched-only vs. full-reset) per mock server type and parallelization model.

Fixture Composition Pattern

// fixtures.ts — one central definition
const webhookProvider = base.extend<{ webhookProvider: WireMockWebhookProvider }>({
  webhookProvider: async ({}, use) => {
    const provider = new WireMockWebhookProvider('http://localhost:8080');
    await use(provider);
  },
});

export const test = mergeTests(base, webhookFixture, webhookProviderFixture);
export { expect } from '@playwright/test';

Cleanup Strategy by Provider

Provider Cleanup Strategy Parallelism
WireMockmatched-onlyfullyParallel: true ✓
MockServerfull-resetworkers: 1 or isolated provider per worker
Mockoonfull-resetworkers: 1 or isolated provider per worker
  • Lazy Playwright fixture evaluation — tests that don't request webhookRegistry pay zero setup cost
  • Lifecycle: optional setup() → tests run → cleanup() → optional teardown()
  • Set project-wide strategy via test.use({ webhookConfig: { cleanupStrategy: 'matched-only' } })
48
Webhook Template Matchers
webhook-template-matchers.md

What it is: Build typed webhook templates with webhookTemplate() and compose matchers (matchField, matchPartial, matchPredicate) using AND semantics, with per-template timeout/interval overrides and clone() for base-template variations.

Matcher Types

Matcher Behaviour
matchField('data.id', value)Dot-notation exact match on a single field
matchPartial({ ... })Deep subset check — extra payload fields are ignored
matchPredicate('desc', fn)Arbitrary predicate; description string is required

Template Factory Pattern

// Always scope by entity ID for parallel isolation
const movieCreated = (movieId: number) =>
  webhookTemplate('movieCreated')
    .matchField('data.id', movieId)        // AND
    .matchPartial({ event: 'movie.created' }) // AND
    .withTimeout(5000)
    .build();

// Variation from base
const movieCreatedVip = (id: number) =>
  movieCreated(id).clone().matchField('data.tier', 'vip').build();
  • All matchers apply AND semantics — every matcher must pass for a webhook to match
  • Always scope template factories by entity ID — prevents parallel workers from claiming each other's webhooks
  • matchPredicate description is mandatory — it appears in WebhookTimeoutError.matcherDetails for debugging
  • Use .withTimeout(ms) and .withInterval(ms) for slow pipelines (e.g. Kafka delivery at 15 s+)
49
Waiting & Querying — waitFor, waitForCount, getReceived
webhook-waiting-querying.md

What it is: Three registry methods covering all webhook assertion patterns: waitFor() polls until the first match (throws WebhookTimeoutError on timeout), waitForCount() collects N matching webhooks, getReceived() queries the journal without polling.

Method Comparison

Method Returns Polling? Use when
waitFor(template)ReceivedWebhook<T>YesWaiting for a single event to arrive
waitForCount(template, n)ReceivedWebhook<T>[]YesBatch ops — collect exactly N deliveries
getReceived(filter?)ReceivedWebhook[]NoSnapshot query; combine with manual since

Drain Pattern for Sequential Events

// Test: create movie, then delete it — assert both webhooks
await movieApi.createMovie({ id: 42, title: 'Dune' });
// Drain the create webhook FIRST, then proceed to delete
const created = await webhookRegistry.waitFor(movieCreated(42));
expect(created.body.event).toBe('movie.created');

await movieApi.deleteMovie(42);
const deleted = await webhookRegistry.waitFor(movieDeleted(42));
expect(deleted.body.event).toBe('movie.deleted');
  • waitFor / waitForCount automatically apply a startedAt since-filter — only webhooks received after the registry was initialized are matched
  • getReceived does not apply a since-filter — pass { since: myTimestamp } manually to avoid matching old entries
  • For parallel tests with waitForCount: use matchPredicate with all expected IDs to prevent cross-worker matching
50
WebhookTimeoutError — Structure & Debugging
webhook-timeout-error.md

What it is: Describes WebhookTimeoutError's properties (templateName, timeoutMs, totalReceived, receivedWebhooks, matcherDetails), how to read its error message, and how to map each failure pattern to the correct fix.

Error Properties

templateNameName of the template that timed out
timeoutMsConfigured timeout in milliseconds
totalReceivedTotal webhooks received (any template)
receivedWebhooksLast ≤10 received entries for inspection
matcherDetailsFormatted matcher config that ran
toJSON()Serialize all fields for CI log inspection

Failure Pattern → Fix

totalReceived Diagnosis Fix
0Webhook never delivered — wrong URL or event not publishedCheck app event publishing and mock server URL
> 0, no matchWebhooks arrived but matchers didn't matchPrint error.toJSON() and inspect receivedWebhooks[0].body
0 + matched-only cleanupAnother worker claimed & deleted the webhook firstScope template factory by entity ID
// In test failure handler or catch block
} catch (err) {
  if (err instanceof WebhookTimeoutError) {
    console.log(JSON.stringify(err.toJSON(), null, 2));
    // matcherDetails: field(data.id=42), partial({"event":"movie.created"})
    // receivedWebhooks[0].body → compare with your matcher expectations
  }
  throw err;
}
51
Webhook Providers — Built-in & Custom
webhook-providers.md

What it is: Documents the three built-in webhook providers (WireMockWebhookProvider, MockServerWebhookProvider, MockoonWebhookProvider) and the WebhookProvider interface for custom backends. Each wraps a different mock server API with different cleanup and parallelism characteristics.

Provider Comparison

Provider Fetch logs deleteById Best for
WireMockGET /__admin/requests✓ SupportedParallel tests — matched-only cleanup
MockServerPUT /mockserver/retrieve✗ no-opSerial runs or isolated provider per worker
MockoonGET /mockoon-admin/logs✗ no-opSerial runs; default log limit 100 (raise with --max-transaction-logs N)

Custom Provider Interface

interface WebhookProvider {
  getReceivedWebhooks(filter?: WebhookFilter): Promise<ReceivedWebhook[]>;
  resetJournal(): Promise<void>;
  deleteById(id: string): Promise<void>;
  getCount(): Promise<number>;
  setup?(): Promise<void>;     // optional — called once before tests
  teardown?(): Promise<void>;  // optional — called once after all tests
}
  • WireMock is the recommended default for teams running webhook tests in parallel — matched-only cleanup + deleteById support enables safe fullyParallel: true
  • MockServer and Mockoon require full-reset — run serially (workers: 1) or spin up an isolated provider instance per worker
  • Custom providers must implement all four required methods; setup() and teardown() are optional lifecycle hooks

Knowledge Base — Summary Map

51 of 51 files · 3 tiers · complete
Tier 1 · 15 files · Documented
Core
Always loaded. The mandatory foundation for every test suite: playwright-utils library overview, fixture architecture, data factories, network-first patterns, risk scoring, test priorities, selector resilience, timing anti-patterns, quality standards, and contract testing.
Files 01–15 · §1–§4
Tier 2 · 22 files · Documented
Extended
Loaded on demand. Deep-dive into each utility, auth session persistence, API testing patterns, component TDD, visual debugging, CI burn-in strategy, selective test execution, feature flag governance, email auth, and webhook testing fundamentals.
Files 16–37 · §5–§9
Tier 3 · 16 files · Coming soon
Specialized
Deep-dive for specific use cases. Pact.js Utils (Zod→Pact, consumer helpers, provider verifier, request filter, MCP server, DI pattern, framework setup, broker webhooks), plus Webhook Infrastructure (module setup, template matchers, waiting patterns, timeout error diagnosis, provider comparison).
Files 38–53 · §10–§11 · to be updated

Tier 1 — Core

15 files · Always loaded
#FileSectionOne-line purposeKey concept
01overview.md§19 shared utilities — never build these from scratchapiRequest · auth-session · recurse · log · file-utils · intercept · recorder · error-monitor · burn-in
02fixture-architecture.md§13-layer pattern: pure function → fixture wrapper → merged exportmergeTests() composition · cleanup in afterEach · no POM inheritance
03network-first.md§1Register interceptor BEFORE navigate — eliminates race conditionsSpy mode vs Stub mode · waitForResponse · HAR record / playback
04data-factories.md§1Faker factories + cleanup tracking; API setup 10–50× faster than UIcreateUserData() with overrides · afterEach cleanup · parallel-safe UUIDs
05test-levels-framework.md§2Unit / Integration / E2E — when each level is appropriateTest pyramid · anti-patterns · decision guide per scenario
06test-priorities-matrix.md§2P0–P3 priorities with minimum coverage thresholds per levelBusiness impact drives priority · P0 blocks release · @p0–@p3 tags
07risk-governance.md§2Risk = P × I · 6 categories · traceability from risk to testsScore 9 = BLOCK · 6–8 = mitigation plan required · risk register format
08adr-quality-readiness-checklist.md§229-criterion readiness checklist across 8 architecture categories≥24/29 = PASS · CONCERNS · FAIL · gate decision model
09probability-impact.md§23×3 P×I matrix with colour-coded action tiers and worked examplesScore 1–3 = DOCUMENT · 4–5 = MONITOR · 6–8 = MITIGATE · 9 = BLOCK
10nfr-criteria.md§34 NFR domains: Security / Performance / Reliability / MaintainabilityTool + threshold + PASS criteria per domain · WAIVED ≠ SKIP
11selector-resilience.md§3Selector hierarchy: testid > ARIA role > label > text > CSSfilter() over nth() · migration table · XPath = never
12timing-debugging.md§3Anti-patterns → event-based waits · 5-step debug workflowwaitForResponse · waitFor state · never waitForTimeout · Trace Viewer
13test-quality.md§38-point quality checklist — all 8 criteria required for productionNo hard waits · no conditionals · self-cleaning · unique data · parallel-safe
14test-healing-patterns.md§35 repair patterns: stale selector, race condition, dynamic data, timeout, hard waitSymptom → root cause → fix steps · 3-strike rule → test.fixme()
15contract-testing.md§4Pact.js consumer contract → provider verification → can-i-deploy gatelike() / regex() matchers · provider state handlers · Postel's Law

Tier 2 — Extended

22 files · Loaded on demand
#FileSectionOne-line purposeKey concept
16auth-session.md§5Token persistence to disk; multi-user; worker-specific; ephemeral authAuthProvider interface · 4 auth modes · isTokenExpired auto-refresh
17api-request.md§5Typed HTTP client; schema validation; auto-retry 5xx; 4-tier URL resolutionvalidateSchema (Zod / JSON Schema / OpenAPI) · GraphQL body.errors check
18recurse.md§5Smart polling: command + predicate + options; 3 typed error shapesRecurseTimeoutError · CommandError · PredicateError · post callback
19intercept-network-call.md§5Declarative spy / stub; register BEFORE navigate; glob URL patternsfulfillResponse = stub · no fulfillResponse = spy · Promise.all for multiple
20network-recorder.md§5HAR record / playback; stateful CRUD simulation during offline replayPW_NET_MODE=record|playback · hostMapping cross-env · CRUD-aware state
21network-error-monitor.md§5Auto-fail on 4xx / 5xx; import only; per-test opt-out via annotationskipNetworkMonitoring · excludePatterns · maxTestsPerError: 1
22log.md§56-level structured logging; surfaces in HTML report as collapsible stepslog.step() / debug() / success() · infoSync for globalSetup · never log secrets
23file-utils.md§5Download capture + CSV / XLSX / PDF / ZIP parsing in 2–10 lineshandleDownload() · readCSV / XLSX / PDF / ZIP · afterEach fs.remove cleanup
24fixtures-composition.md§5mergeTests() — one import file for all fixtures across the projectLast fixture wins on name conflict · test.use() overrides · lazy evaluation
25api-testing-patterns.md§6Direct API tests: CRUD, error shapes, GraphQL, async job pollingapiRequest + recurse combo · assert body.errors for GraphQL · API vs E2E guide
26component-tdd.md§6Red-Green-Refactor TDD loop for UI components with a11y from the startAllTheProviders fresh per test · AxeBuilder · keyboard Tab / Enter / Escape flows
27visual-debugging.md§6Traces + screenshots on failure; Trace Viewer 5 panels; interactive Inspectorretain-on-failure config · if: always() CI upload · page.pause() local debug
28playwright-config.md§6envConfigMap fail-fast; standardized timeouts; sharding; auth-state projectsTEST_ENV validation · workers CI=1 / local=n-1 · reporter setup
29ci-burn-in.md§7Run changed specs 10× in CI; shard full suite; fail-fast: falsegit diff → burn-in loop → matrix sharding → merge-reports
30selective-testing.md§7@smoke / @p0–@p3 / @regression tags; 3-stage promotion; time budgetsPre-commit <5 min · PR <10 min · merge gate <30 min · file-change → tag map
31feature-flags.md§7Type-safe flag registry; metadata; test both enabled and disabled statesFLAGS enum · FLAG_REGISTRY expiryDate · afterEach reset · audit script
32error-handling.md§8Scoped exception catching; retry counter validation; secret redactionScoped pageerror listener · attempt counter · graceful degradation stubs
33email-auth.md§8Magic link extraction; session caching (1 email for 500 tests); negative flowsMailosaur · crypto.randomUUID() per test · globalSetup storageState cache
34playwright-cli.md§8Lightweight CLI for coding agents; ~93% token saving vs Playwright MCP-s session scope · open / snapshot / click / fill · trace CLI analysis
35burn-in.md§8Git-diff-aware runner; 3-phase filter; volume control via percentage capskipBurnInPatterns · burnInTestPercentage · skip → deps → volume phases
36webhook-testing-fundamentals.md§9Eventually-consistent polling; typed matchers; startedAt isolationmatchField / Partial / Predicate AND semantics · drain pattern · WebhookTimeoutError
37webhook-risk-guidance.md§9Default P2×I3 = Score 6; 5 mandatory coverage requirementsHappy path + drain + parallel + timeout shape + cleanup · downgrade needs sign-off

Tier 3 — Specialized

16 files · To be updated

Tier 3 will be documented in the next update. It covers two specialist groups:

§10 · Files 38–46
Pact.js Utils — Deep-Dive
  • 38  pactjs-utils-overview — production-ready contract utilities
  • 39  pactjs-utils-zod-to-pact — Zod schema → Pact V3 matchers
  • 40  pactjs-utils-consumer-helpers — provider state + PactV4 builders
  • 41  pactjs-utils-provider-verifier — buildVerifierOptions
  • 42  pactjs-utils-request-filter — Bearer auth injection
  • 43  pact-mcp — SmartBear MCP server for AI agents
  • 44  pact-consumer-framework-setup — scaffolding & conventions
  • 45  pact-broker-webhooks — PactFlow → GitHub Actions trigger
  • 46  pact-consumer-di — baseUrl DI pattern for real consumer code
§11 · Files 47–53
Webhook Infrastructure — Deep-Dive
  • 47  webhook-module-setup — fixture wiring + cleanup strategy
  • 48  webhook-template-matchers — typed templates + AND semantics
  • 49  webhook-waiting-querying — waitFor / waitForCount / getReceived
  • 50  webhook-timeout-error — WebhookTimeoutError structure & debug
  • 51  webhook-providers — WireMock / MockServer / Mockoon / custom
  • 52–53  additional specialized topics (TBD)
Skill mapping for Tier 3: *automate (Pact consumer generation) · *ci (contract CI + can-i-deploy gate) · *test-design (fetch provider states via Pact MCP) · *test-review (pact test quality review) · *framework (webhook fixture wiring, cleanup strategy selection).