mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
Compare commits
16 Commits
v6.0.0-Bet
...
feat/remov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7205451346 | ||
|
|
6faa25843f | ||
|
|
e99a02f409 | ||
|
|
5d1b58b238 | ||
|
|
dd8c54cc9f | ||
|
|
4ded43d707 | ||
|
|
181aeac04a | ||
|
|
f7466c2530 | ||
|
|
7d3d51ff4f | ||
|
|
4c92e0cc88 | ||
|
|
0d2b8c3429 | ||
|
|
984bd9e558 | ||
|
|
6a282f86b4 | ||
|
|
6c5381b6dc | ||
|
|
27c18e0020 | ||
|
|
9ebc4ce9c0 |
@@ -1,6 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [6.0.0-Beta.0]
|
||||
## [6.0.0-Beta.2]
|
||||
|
||||
- Fix installer so commands match what is installed, centralize most ide into a central file instead of separate files for each ide.
|
||||
- Specific IDEs may still need udpates, but all is config driven now and should be easier to maintain
|
||||
- Kiro still needs updates, but its been in this state since contributed, will investigate soon
|
||||
- Any version older than Beta.0 will recommend removal and reinstall to project. From later alphas though its sufficient to quick update if still desired, but best is just start fresh with Beta.
|
||||
|
||||
## [6.0.0-Beta.1]
|
||||
|
||||
**Release: January 2026 - Alpha to Beta Transition**
|
||||
|
||||
|
||||
50
README.md
50
README.md
@@ -32,10 +32,10 @@ Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, Windsu
|
||||
|
||||
> **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like:
|
||||
|
||||
- `/bmad-help How should I build a web app for for my TShirt Business that can scale to millions?`
|
||||
- `/bmad-help How should I build a web app for my TShirt Business that can scale to millions?`
|
||||
- `/bmad-help I just finished the architecture, I am not sure what to do next`
|
||||
|
||||
And the amazing this is BMad Help evolves depending on what modules you install also!
|
||||
And the amazing thing is BMad Help evolves depending on what modules you install also!
|
||||
- `/bmad-help Im interested in really exploring creative ways to demo BMad at work, what do you recommend to help plan a great slide deck and compelling narrative?`, and if you have the Creative Intelligence Suite installed, it will offer you different or complimentary advice than if you just have BMad Method Module installed!
|
||||
|
||||
The workflows below show the fastest path to working code. You can also load agents directly for a more structured process, extensive planning, or to learn about agile development practices — the agents guide you with menus, explanations, and elicitation at each step.
|
||||
@@ -59,27 +59,59 @@ Products, platforms, complex features — structured planning then build:
|
||||
5. `/sprint-planning` — initialize sprint tracking
|
||||
6. **Repeat per story:** `/create-story` → `/dev-story` → `/code-review`
|
||||
|
||||
Every step tells you what's next. Optional phases (brainstorming, research, UX design) are available when you need them — ask `/bmad-help` anytime. For a detailed walkthrough, see the [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/getting-started-bmadv6/).
|
||||
Every step tells you what's next. Optional phases (brainstorming, research, UX design) are available when you need them — ask `/bmad-help` anytime. For a detailed walkthrough, see the [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/).
|
||||
|
||||
## Modules
|
||||
|
||||
BMad Method extends with official modules for specialized domains. Modules are available during installation and can be added to your project at any time. After the V6 beta period these will also be available as Plugins and Granular Skills.
|
||||
|
||||
| Module | GitHub | NPM | Purpose |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| Module | GitHub | NPM | Purpose |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
||||
| **BMad Method (BMM)** | [bmad-code-org/BMAD-METHOD](https://github.com/bmad-code-org/BMAD-METHOD) | [bmad-method](https://www.npmjs.com/package/bmad-method) | Core framework with 34+ workflows across 4 development phases |
|
||||
| **BMad Builder (BMB)** | [bmad-code-org/bmad-builder](https://github.com/bmad-code-org/bmad-builder) | [bmad-builder](https://www.npmjs.com/package/bmad-builder) | Create custom BMad agents, workflows, and domain-specific modules |
|
||||
| **Test Architect (TEA)** 🆕 | [bmad-code-org/tea](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise) | [tea](https://www.npmjs.com/package/bmad-method-test-architecture-enterprise) | Risk-based test strategy, automation, and release gates (8 workflows) |
|
||||
| **Game Dev Studio (BMGD)** | [bmad-code-org/bmad-module-game-dev-studio](https://github.com/bmad-code-org/bmad-module-game-dev-studio) | [bmad-game-dev-studio](https://www.npmjs.com/package/bmad-game-dev-studio) | Game development workflows for Unity, Unreal, and Godot |
|
||||
| **Creative Intelligence Suite (CIS)** | [bmad-code-org/bmad-module-creative-intelligence-suite](https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite) | [bmad-creative-intelligence-suite](https://www.npmjs.com/package/bmad-creative-intelligence-suite) | Innovation, brainstorming, design thinking, and problem-solving |
|
||||
|
||||
* More modules are coming in the next 2 weeks from BMad Official, and a community marketplace for the installer also will be coming with the final V6 release!
|
||||
|
||||
## Testing Agents
|
||||
|
||||
BMad provides two testing options to fit your needs:
|
||||
|
||||
### Quinn (QA) - Built-in
|
||||
|
||||
**Quick test automation for rapid coverage**
|
||||
|
||||
- ✅ **Always available** in BMM module (no separate install)
|
||||
- ✅ **Simple**: One workflow (`QA` - Automate)
|
||||
- ✅ **Beginner-friendly**: Standard test framework patterns
|
||||
- ✅ **Fast**: Generate tests and ship
|
||||
|
||||
**Use Quinn for:** Small projects, quick coverage, standard patterns
|
||||
|
||||
### Test Architect (TEA) - Optional Module
|
||||
|
||||
**Enterprise-grade test strategy and quality engineering**
|
||||
|
||||
- 🆕 **Standalone module** (install separately)
|
||||
- 🏗️ **Comprehensive**: 8 workflows covering full test lifecycle
|
||||
- 🎯 **Advanced**: Risk-based planning, quality gates, NFR assessment
|
||||
- 📚 **Knowledge-driven**: 34 testing patterns and best practices
|
||||
- 📖 [Test Architect Documentation](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/)
|
||||
|
||||
**Use TEA for:** Enterprise projects, test strategy, compliance, release gates
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
**[Full Documentation](http://docs.bmad-method.org)** — Tutorials, how-to guides, concepts, and reference
|
||||
**[BMad Documentation](http://docs.bmad-method.org)** — Tutorials, how-to guides, concepts, and reference
|
||||
**[Test Architect Documentation](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/)** — TEA standalone module documentation
|
||||
|
||||
- [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/getting-started-bmadv6/)
|
||||
- [Upgrading from Previous Versions](http://docs.bmad-method.org/how-to/installation/upgrade-to-v6/)
|
||||
- [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/)
|
||||
- [Upgrading from Previous Versions](http://docs.bmad-method.org/how-to/upgrade-to-v6/)
|
||||
- [Test Architect Migration Guide](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/migration/) — Upgrading from BMM-embedded TEA
|
||||
|
||||
### For v4 Users
|
||||
|
||||
@@ -96,7 +128,7 @@ BMad Method extends with official modules for specialized domains. Modules are a
|
||||
|
||||
BMad is free for everyone — and always will be. If you'd like to support development:
|
||||
|
||||
- ⭐ Please click the star project icon at near the top right of this page
|
||||
- ⭐ Please click the star project icon near the top right of this page
|
||||
- ☕ [Buy Me a Coffee](https://buymeacoffee.com/bmad) — Fuel the development
|
||||
- 🏢 Corporate sponsorship — DM on Discord
|
||||
- 🎤 Speaking & Media — Available for conferences, podcasts, interviews (BM on Discord)
|
||||
|
||||
@@ -151,7 +151,7 @@ prompts:
|
||||
|
||||
## Workflow Customization
|
||||
|
||||
Information about customizing existing BMad MEthod workflows and skills are coming soon.
|
||||
Information about customizing existing BMad Method workflows and skills are coming soon.
|
||||
|
||||
## Module Customization
|
||||
|
||||
|
||||
@@ -53,10 +53,13 @@ Build it, one story at a time.
|
||||
| `sprint-planning` | Initialize tracking (once per project) | `sprint-status.yaml` |
|
||||
| `create-story` | Prepare next story for implementation | `story-[slug].md` |
|
||||
| `dev-story` | Implement the story | Working code + tests |
|
||||
| `automate` (QA) | Generate tests for existing features | Test suite |
|
||||
| `code-review` | Validate implementation quality | Approved or changes requested |
|
||||
| `correct-course` | Handle significant mid-sprint changes | Updated plan or re-routing |
|
||||
| `retrospective` | Review after epic completion | Lessons learned |
|
||||
|
||||
**Quinn (QA Agent):** Built-in QA agent for test automation. Trigger with `QA` or `bmad-bmm-automate`. Generates standard API and E2E tests using your project's test framework. Beginner-friendly, no configuration needed. For advanced test strategy, install [Test Architect (TEA)](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/) module.
|
||||
|
||||
## Quick Flow (Parallel Track)
|
||||
|
||||
Skip phases 1-3 for small, well-understood work.
|
||||
|
||||
@@ -1,710 +0,0 @@
|
||||
---
|
||||
title: "TEA Engagement Models Explained"
|
||||
description: Understanding the five ways to use TEA - from standalone to full BMad Method integration
|
||||
---
|
||||
|
||||
# TEA Engagement Models Explained
|
||||
|
||||
TEA is optional and flexible. There are five valid ways to engage with TEA - choose intentionally based on your project needs and methodology.
|
||||
|
||||
## Overview
|
||||
|
||||
**TEA is not mandatory.** Pick the engagement model that fits your context:
|
||||
|
||||
1. **No TEA** - Skip all TEA workflows, use existing testing approach
|
||||
2. **TEA Solo** - Use TEA standalone without BMad Method
|
||||
3. **TEA Lite** - Beginner approach using just `automate`
|
||||
4. **TEA Integrated (Greenfield)** - Full BMad Method integration from scratch
|
||||
5. **TEA Integrated (Brownfield)** - Full BMad Method integration with existing code
|
||||
|
||||
## The Problem
|
||||
|
||||
### One-Size-Fits-All Doesn't Work
|
||||
|
||||
**Traditional testing tools force one approach:**
|
||||
- Must use entire framework
|
||||
- All-or-nothing adoption
|
||||
- No flexibility for different project types
|
||||
- Teams abandon tool if it doesn't fit
|
||||
|
||||
**TEA recognizes:**
|
||||
- Different projects have different needs
|
||||
- Different teams have different maturity levels
|
||||
- Different contexts require different approaches
|
||||
- Flexibility increases adoption
|
||||
|
||||
## The Five Engagement Models
|
||||
|
||||
### Model 1: No TEA
|
||||
|
||||
**What:** Skip all TEA workflows, use your existing testing approach.
|
||||
|
||||
**When to Use:**
|
||||
- Team has established testing practices
|
||||
- Quality is already high
|
||||
- Testing tools already in place
|
||||
- TEA doesn't add value
|
||||
|
||||
**What You Miss:**
|
||||
- Risk-based test planning
|
||||
- Systematic quality review
|
||||
- Gate decisions with evidence
|
||||
- Knowledge base patterns
|
||||
|
||||
**What You Keep:**
|
||||
- Full control
|
||||
- Existing tools
|
||||
- Team expertise
|
||||
- No learning curve
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Your team:
|
||||
- 10-year veteran QA team
|
||||
- Established testing practices
|
||||
- High-quality test suite
|
||||
- No problems to solve
|
||||
|
||||
Decision: Skip TEA, keep what works
|
||||
```
|
||||
|
||||
**Verdict:** Valid choice if existing approach works.
|
||||
|
||||
---
|
||||
|
||||
### Model 2: TEA Solo
|
||||
|
||||
**What:** Use TEA workflows standalone without full BMad Method integration.
|
||||
|
||||
**When to Use:**
|
||||
- Non-BMad projects
|
||||
- Want TEA's quality operating model only
|
||||
- Don't need full planning workflow
|
||||
- Bring your own requirements
|
||||
|
||||
**Typical Sequence:**
|
||||
```
|
||||
1. `test-design` (system or epic)
|
||||
2. `atdd` or `automate`
|
||||
3. `test-review` (optional)
|
||||
4. `trace` (coverage + gate decision)
|
||||
```
|
||||
|
||||
**You Bring:**
|
||||
- Requirements (user stories, acceptance criteria)
|
||||
- Development environment
|
||||
- Project context
|
||||
|
||||
**TEA Provides:**
|
||||
- Risk-based test planning (`test-design`)
|
||||
- Test generation (`atdd`, `automate`)
|
||||
- Quality review (`test-review`)
|
||||
- Coverage traceability (`trace`)
|
||||
|
||||
**Optional:**
|
||||
- Framework setup (`framework`) if needed
|
||||
- CI configuration (`ci`) if needed
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Your project:
|
||||
- Using Scrum (not BMad Method)
|
||||
- Jira for story management
|
||||
- Need better test strategy
|
||||
|
||||
Workflow:
|
||||
1. Export stories from Jira
|
||||
2. Run `test-design` on epic
|
||||
3. Run `atdd` for each story
|
||||
4. Implement features
|
||||
5. Run `trace` for coverage
|
||||
```
|
||||
|
||||
**Verdict:** Best for teams wanting TEA benefits without BMad Method commitment.
|
||||
|
||||
---
|
||||
|
||||
### Model 3: TEA Lite
|
||||
|
||||
**What:** Beginner approach using just `automate` to test existing features.
|
||||
|
||||
**When to Use:**
|
||||
- Learning TEA fundamentals
|
||||
- Want quick results
|
||||
- Testing existing application
|
||||
- No time for full methodology
|
||||
|
||||
**Workflow:**
|
||||
```
|
||||
1. `framework` (setup test infrastructure)
|
||||
2. `test-design` (optional, risk assessment)
|
||||
3. `automate` (generate tests for existing features)
|
||||
4. Run tests (they pass immediately)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Beginner developer:
|
||||
- Never used TEA before
|
||||
- Want to add tests to existing app
|
||||
- 30 minutes available
|
||||
|
||||
Steps:
|
||||
1. Run `framework`
|
||||
2. Run `automate` on TodoMVC demo
|
||||
3. Tests generated and passing
|
||||
4. Learn TEA basics
|
||||
```
|
||||
|
||||
**What You Get:**
|
||||
- Working test framework
|
||||
- Passing tests for existing features
|
||||
- Learning experience
|
||||
- Foundation to expand
|
||||
|
||||
**What You Miss:**
|
||||
- TDD workflow (ATDD)
|
||||
- Risk-based planning (test-design depth)
|
||||
- Quality gates (trace Phase 2)
|
||||
- Full TEA capabilities
|
||||
|
||||
**Verdict:** Perfect entry point for beginners.
|
||||
|
||||
---
|
||||
|
||||
### Model 4: TEA Integrated (Greenfield)
|
||||
|
||||
**What:** Full BMad Method integration with TEA workflows across all phases.
|
||||
|
||||
**When to Use:**
|
||||
- New projects starting from scratch
|
||||
- Using BMad Method or Enterprise track
|
||||
- Want complete quality operating model
|
||||
- Testing is critical to success
|
||||
|
||||
**Lifecycle:**
|
||||
|
||||
**Phase 2: Planning**
|
||||
- PM creates PRD with NFRs
|
||||
- (Optional) TEA runs `nfr-assess` (Enterprise only)
|
||||
|
||||
**Phase 3: Solutioning**
|
||||
- Architect creates architecture
|
||||
- TEA runs `test-design` (system-level) → testability review
|
||||
- TEA runs `framework` → test infrastructure
|
||||
- TEA runs `ci` → CI/CD pipeline
|
||||
- Architect runs `implementation-readiness` (fed by test design)
|
||||
|
||||
**Phase 4: Implementation (Per Epic)**
|
||||
- SM runs `sprint-planning`
|
||||
- TEA runs `test-design` (epic-level) → risk assessment for THIS epic
|
||||
- SM creates stories
|
||||
- (Optional) TEA runs `atdd` → failing tests before dev
|
||||
- DEV implements story
|
||||
- TEA runs `automate` → expand coverage
|
||||
- (Optional) TEA runs `test-review` → quality audit
|
||||
- TEA runs `trace` Phase 1 → refresh coverage
|
||||
|
||||
**Release Gate:**
|
||||
- (Optional) TEA runs `test-review` → final audit
|
||||
- (Optional) TEA runs `nfr-assess` → validate NFRs
|
||||
- TEA runs `trace` Phase 2 → gate decision (PASS/CONCERNS/FAIL/WAIVED)
|
||||
|
||||
**What You Get:**
|
||||
- Complete quality operating model
|
||||
- Systematic test planning
|
||||
- Risk-based prioritization
|
||||
- Evidence-based gate decisions
|
||||
- Consistent patterns across epics
|
||||
|
||||
**Example:**
|
||||
```
|
||||
New SaaS product:
|
||||
- 50 stories across 8 epics
|
||||
- Security critical
|
||||
- Need quality gates
|
||||
|
||||
Workflow:
|
||||
- Phase 2: Define NFRs in PRD
|
||||
- Phase 3: Architecture → test design → framework → CI
|
||||
- Phase 4: Per epic: test design → ATDD → dev → automate → review → trace
|
||||
- Gate: NFR assess → trace Phase 2 → decision
|
||||
```
|
||||
|
||||
**Verdict:** Most comprehensive TEA usage, best for structured teams.
|
||||
|
||||
---
|
||||
|
||||
### Model 5: TEA Integrated (Brownfield)
|
||||
|
||||
**What:** Full BMad Method integration with TEA for existing codebases.
|
||||
|
||||
**When to Use:**
|
||||
- Existing codebase with legacy tests
|
||||
- Want to improve test quality incrementally
|
||||
- Adding features to existing application
|
||||
- Need to establish coverage baseline
|
||||
|
||||
**Differences from Greenfield:**
|
||||
|
||||
**Phase 0: Documentation (if needed)**
|
||||
```
|
||||
- Run `document-project`
|
||||
- Create baseline documentation
|
||||
```
|
||||
|
||||
**Phase 2: Planning**
|
||||
```
|
||||
- TEA runs `trace` Phase 1 → establish coverage baseline
|
||||
- PM creates PRD (with existing system context)
|
||||
```
|
||||
|
||||
**Phase 3: Solutioning**
|
||||
```
|
||||
- Architect creates architecture (with brownfield constraints)
|
||||
- TEA runs `test-design` (system-level) → testability review
|
||||
- TEA runs `framework` (only if modernizing test infra)
|
||||
- TEA runs `ci` (update existing CI or create new)
|
||||
```
|
||||
|
||||
**Phase 4: Implementation**
|
||||
```
|
||||
- TEA runs `test-design` (epic-level) → focus on REGRESSION HOTSPOTS
|
||||
- Per story: ATDD → dev → automate
|
||||
- TEA runs `test-review` → improve legacy test quality
|
||||
- TEA runs `trace` Phase 1 → track coverage improvement
|
||||
```
|
||||
|
||||
**Brownfield-Specific:**
|
||||
- Baseline coverage BEFORE planning
|
||||
- Focus on regression hotspots (bug-prone areas)
|
||||
- Incremental quality improvement
|
||||
- Compare coverage to baseline (trending up?)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Legacy e-commerce platform:
|
||||
- 200 existing tests (30% passing, 70% flaky)
|
||||
- Adding new checkout flow
|
||||
- Want to improve quality
|
||||
|
||||
Workflow:
|
||||
1. Phase 2: `trace` baseline → 30% coverage
|
||||
2. Phase 3: `test-design` → identify regression risks
|
||||
3. Phase 4: Fix top 20 flaky tests + add tests for new checkout
|
||||
4. Gate: `trace` → 60% coverage (2x improvement)
|
||||
```
|
||||
|
||||
**Verdict:** Best for incrementally improving legacy systems.
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide: Which Model?
|
||||
|
||||
### Quick Decision Tree
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
||||
flowchart TD
|
||||
Start([Choose TEA Model]) --> BMad{Using<br/>BMad Method?}
|
||||
|
||||
BMad -->|No| NonBMad{Project Type?}
|
||||
NonBMad -->|Learning| Lite[TEA Lite<br/>Just automate<br/>30 min tutorial]
|
||||
NonBMad -->|Serious Project| Solo[TEA Solo<br/>Standalone workflows<br/>Full capabilities]
|
||||
|
||||
BMad -->|Yes| WantTEA{Want TEA?}
|
||||
WantTEA -->|No| None[No TEA<br/>Use existing approach<br/>Valid choice]
|
||||
WantTEA -->|Yes| ProjectType{New or<br/>Existing?}
|
||||
|
||||
ProjectType -->|New Project| Green[TEA Integrated<br/>Greenfield<br/>Full lifecycle]
|
||||
ProjectType -->|Existing Code| Brown[TEA Integrated<br/>Brownfield<br/>Baseline + improve]
|
||||
|
||||
Green --> Compliance{Compliance<br/>Needs?}
|
||||
Compliance -->|Yes| Enterprise[Enterprise Track<br/>NFR + audit trails]
|
||||
Compliance -->|No| Method[BMad Method Track<br/>Standard quality]
|
||||
|
||||
style Lite fill:#bbdefb,stroke:#1565c0,stroke-width:2px
|
||||
style Solo fill:#c5cae9,stroke:#283593,stroke-width:2px
|
||||
style None fill:#e0e0e0,stroke:#616161,stroke-width:1px
|
||||
style Green fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
|
||||
style Brown fill:#fff9c4,stroke:#f57f17,stroke-width:2px
|
||||
style Enterprise fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
|
||||
style Method fill:#e1f5fe,stroke:#01579b,stroke-width:2px
|
||||
```
|
||||
|
||||
**Decision Path Examples:**
|
||||
- Learning TEA → TEA Lite (blue)
|
||||
- Non-BMad project → TEA Solo (purple)
|
||||
- BMad + new project + compliance → Enterprise (purple)
|
||||
- BMad + existing code → Brownfield (yellow)
|
||||
- Don't want TEA → No TEA (gray)
|
||||
|
||||
### By Project Type
|
||||
|
||||
| Project Type | Recommended Model | Why |
|
||||
|--------------|------------------|-----|
|
||||
| **New SaaS product** | TEA Integrated (Greenfield) | Full quality operating model from day one |
|
||||
| **Existing app + new feature** | TEA Integrated (Brownfield) | Improve incrementally while adding features |
|
||||
| **Bug fix** | TEA Lite or No TEA | Quick flow, minimal overhead |
|
||||
| **Learning project** | TEA Lite | Learn basics with immediate results |
|
||||
| **Non-BMad enterprise** | TEA Solo | Quality model without full methodology |
|
||||
| **High-quality existing tests** | No TEA | Keep what works |
|
||||
|
||||
### By Team Maturity
|
||||
|
||||
| Team Maturity | Recommended Model | Why |
|
||||
|---------------|------------------|-----|
|
||||
| **Beginners** | TEA Lite → TEA Solo | Learn basics, then expand |
|
||||
| **Intermediate** | TEA Solo or Integrated | Depends on methodology |
|
||||
| **Advanced** | TEA Integrated or No TEA | Full model or existing expertise |
|
||||
|
||||
### By Compliance Needs
|
||||
|
||||
| Compliance | Recommended Model | Why |
|
||||
|------------|------------------|-----|
|
||||
| **None** | Any model | Choose based on project needs |
|
||||
| **Light** (internal audit) | TEA Solo or Integrated | Gate decisions helpful |
|
||||
| **Heavy** (SOC 2, HIPAA) | TEA Integrated (Enterprise) | NFR assessment mandatory |
|
||||
|
||||
## Switching Between Models
|
||||
|
||||
### Can Change Models Mid-Project
|
||||
|
||||
**Scenario:** Start with TEA Lite, expand to TEA Solo
|
||||
|
||||
```
|
||||
Week 1: TEA Lite
|
||||
- Run `framework`
|
||||
- Run `automate`
|
||||
- Learn basics
|
||||
|
||||
Week 2: Expand to TEA Solo
|
||||
- Add `test-design`
|
||||
- Use `atdd` for new features
|
||||
- Add `test-review`
|
||||
|
||||
Week 3: Continue expanding
|
||||
- Add `trace` for coverage
|
||||
- Setup `ci`
|
||||
- Full TEA Solo workflow
|
||||
```
|
||||
|
||||
**Benefit:** Start small, expand as comfortable.
|
||||
|
||||
### Can Mix Models
|
||||
|
||||
**Scenario:** TEA Integrated for main features, No TEA for bug fixes
|
||||
|
||||
```
|
||||
Main features (epics):
|
||||
- Use full TEA workflow
|
||||
- Risk assessment, ATDD, quality gates
|
||||
|
||||
Bug fixes:
|
||||
- Skip TEA
|
||||
- Quick Flow + manual testing
|
||||
- Move fast
|
||||
|
||||
Result: TEA where it adds value, skip where it doesn't
|
||||
```
|
||||
|
||||
**Benefit:** Flexible, pragmatic, not dogmatic.
|
||||
|
||||
## Comparison Table
|
||||
|
||||
| Aspect | No TEA | TEA Lite | TEA Solo | Integrated (Green) | Integrated (Brown) |
|
||||
|--------|--------|----------|----------|-------------------|-------------------|
|
||||
| **BMad Required** | No | No | No | Yes | Yes |
|
||||
| **Learning Curve** | None | Low | Medium | High | High |
|
||||
| **Setup Time** | 0 | 30 min | 2 hours | 1 day | 2 days |
|
||||
| **Workflows Used** | 0 | 2-3 | 4-6 | 8 | 8 |
|
||||
| **Test Planning** | Manual | Optional | Yes | Systematic | + Regression focus |
|
||||
| **Quality Gates** | No | No | Optional | Yes | Yes + baseline |
|
||||
| **NFR Assessment** | No | No | No | Optional | Recommended |
|
||||
| **Coverage Tracking** | Manual | No | Optional | Yes | Yes + trending |
|
||||
| **Best For** | Experts | Beginners | Standalone | New projects | Legacy code |
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: Startup (TEA Lite → TEA Integrated)
|
||||
|
||||
**Month 1:** TEA Lite
|
||||
```
|
||||
Team: 3 developers, no QA
|
||||
Testing: Manual only
|
||||
Decision: Start with TEA Lite
|
||||
|
||||
Result:
|
||||
- Run `framework` (Playwright setup)
|
||||
- Run `automate` (20 tests generated)
|
||||
- Learning TEA basics
|
||||
```
|
||||
|
||||
**Month 3:** TEA Solo
|
||||
```
|
||||
Team: Growing to 5 developers
|
||||
Testing: Automated tests exist
|
||||
Decision: Expand to TEA Solo
|
||||
|
||||
Result:
|
||||
- Add `test-design` (risk assessment)
|
||||
- Add `atdd` (TDD workflow)
|
||||
- Add `test-review` (quality audits)
|
||||
```
|
||||
|
||||
**Month 6:** TEA Integrated
|
||||
```
|
||||
Team: 8 developers, 1 QA
|
||||
Testing: Critical to business
|
||||
Decision: Full BMad Method + TEA Integrated
|
||||
|
||||
Result:
|
||||
- Full lifecycle integration
|
||||
- Quality gates before releases
|
||||
- NFR assessment for enterprise customers
|
||||
```
|
||||
|
||||
### Example 2: Enterprise (TEA Integrated - Brownfield)
|
||||
|
||||
**Project:** Legacy banking application
|
||||
|
||||
**Challenge:**
|
||||
- 500 existing tests (50% flaky)
|
||||
- Adding new features
|
||||
- SOC 2 compliance required
|
||||
|
||||
**Model:** TEA Integrated (Brownfield)
|
||||
|
||||
**Phase 2:**
|
||||
```
|
||||
- `trace` baseline → 45% coverage (lots of gaps)
|
||||
- Document current state
|
||||
```
|
||||
|
||||
**Phase 3:**
|
||||
```
|
||||
- `test-design` (system) → identify regression hotspots
|
||||
- `framework` → modernize test infrastructure
|
||||
- `ci` → add selective testing
|
||||
```
|
||||
|
||||
**Phase 4:**
|
||||
```
|
||||
Per epic:
|
||||
- `test-design` → focus on regression + new features
|
||||
- Fix top 10 flaky tests
|
||||
- `atdd` for new features
|
||||
- `automate` for coverage expansion
|
||||
- `test-review` → track quality improvement
|
||||
- `trace` → compare to baseline
|
||||
```
|
||||
|
||||
**Result after 6 months:**
|
||||
- Coverage: 45% → 85%
|
||||
- Quality score: 52 → 82
|
||||
- Flakiness: 50% → 2%
|
||||
- SOC 2 compliant (traceability + NFR evidence)
|
||||
|
||||
### Example 3: Consultancy (TEA Solo)
|
||||
|
||||
**Context:** Testing consultancy working with multiple clients
|
||||
|
||||
**Challenge:**
|
||||
- Different clients use different methodologies
|
||||
- Need consistent testing approach
|
||||
- Not always using BMad Method
|
||||
|
||||
**Model:** TEA Solo (bring to any client project)
|
||||
|
||||
**Workflow:**
|
||||
```
|
||||
Client project 1 (Scrum):
|
||||
- Import Jira stories
|
||||
- Run `test-design`
|
||||
- Generate tests with `atdd`/`automate`
|
||||
- Deliver quality report with `test-review`
|
||||
|
||||
Client project 2 (Kanban):
|
||||
- Import requirements from Notion
|
||||
- Same TEA workflow
|
||||
- Consistent quality across clients
|
||||
|
||||
Client project 3 (Ad-hoc):
|
||||
- Document requirements manually
|
||||
- Same TEA workflow
|
||||
- Same patterns, different context
|
||||
```
|
||||
|
||||
**Benefit:** Consistent testing approach regardless of client methodology.
|
||||
|
||||
## Choosing Your Model
|
||||
|
||||
### Start Here Questions
|
||||
|
||||
**Question 1:** Are you using BMad Method?
|
||||
- **No** → TEA Solo or TEA Lite or No TEA
|
||||
- **Yes** → TEA Integrated or No TEA
|
||||
|
||||
**Question 2:** Is this a new project?
|
||||
- **Yes** → TEA Integrated (Greenfield) or TEA Lite
|
||||
- **No** → TEA Integrated (Brownfield) or TEA Solo
|
||||
|
||||
**Question 3:** What's your testing maturity?
|
||||
- **Beginner** → TEA Lite
|
||||
- **Intermediate** → TEA Solo or Integrated
|
||||
- **Advanced** → TEA Integrated or No TEA (already expert)
|
||||
|
||||
**Question 4:** Do you need compliance/quality gates?
|
||||
- **Yes** → TEA Integrated (Enterprise)
|
||||
- **No** → Any model
|
||||
|
||||
**Question 5:** How much time can you invest?
|
||||
- **30 minutes** → TEA Lite
|
||||
- **Few hours** → TEA Solo
|
||||
- **Multiple days** → TEA Integrated
|
||||
|
||||
### Recommendation Matrix
|
||||
|
||||
| Your Context | Recommended Model | Alternative |
|
||||
|--------------|------------------|-------------|
|
||||
| BMad Method + new project | TEA Integrated (Greenfield) | TEA Lite (learning) |
|
||||
| BMad Method + existing code | TEA Integrated (Brownfield) | TEA Solo |
|
||||
| Non-BMad + need quality | TEA Solo | TEA Lite |
|
||||
| Just learning testing | TEA Lite | No TEA (learn basics first) |
|
||||
| Enterprise + compliance | TEA Integrated (Enterprise) | TEA Solo |
|
||||
| Established QA team | No TEA | TEA Solo (supplement) |
|
||||
|
||||
## Transitioning Between Models
|
||||
|
||||
### TEA Lite → TEA Solo
|
||||
|
||||
**When:** Outgrow beginner approach, need more workflows.
|
||||
|
||||
**Steps:**
|
||||
1. Continue using `framework` and `automate`
|
||||
2. Add `test-design` for planning
|
||||
3. Add `atdd` for TDD workflow
|
||||
4. Add `test-review` for quality audits
|
||||
5. Add `trace` for coverage tracking
|
||||
|
||||
**Timeline:** 2-4 weeks of gradual expansion
|
||||
|
||||
### TEA Solo → TEA Integrated
|
||||
|
||||
**When:** Adopt BMad Method, want full integration.
|
||||
|
||||
**Steps:**
|
||||
1. Install BMad Method (see installation guide)
|
||||
2. Run planning workflows (PRD, architecture)
|
||||
3. Integrate TEA into Phase 3 (system-level test design)
|
||||
4. Follow integrated lifecycle (per epic workflows)
|
||||
5. Add release gates (trace Phase 2)
|
||||
|
||||
**Timeline:** 1-2 sprints of transition
|
||||
|
||||
### TEA Integrated → TEA Solo
|
||||
|
||||
**When:** Moving away from BMad Method, keep TEA.
|
||||
|
||||
**Steps:**
|
||||
1. Export BMad artifacts (PRD, architecture, stories)
|
||||
2. Continue using TEA workflows standalone
|
||||
3. Skip BMad-specific integration
|
||||
4. Bring your own requirements to TEA
|
||||
|
||||
**Timeline:** Immediate (just skip BMad workflows)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: TEA Lite for Learning, Then Choose
|
||||
|
||||
```
|
||||
Phase 1 (Week 1-2): TEA Lite
|
||||
- Learn with `automate` on demo app
|
||||
- Understand TEA fundamentals
|
||||
- Low commitment
|
||||
|
||||
Phase 2 (Week 3-4): Evaluate
|
||||
- Try `test-design` (planning)
|
||||
- Try `atdd` (TDD)
|
||||
- See if value justifies investment
|
||||
|
||||
Phase 3 (Month 2+): Decide
|
||||
- Valuable → Expand to TEA Solo or Integrated
|
||||
- Not valuable → Stay with TEA Lite or No TEA
|
||||
```
|
||||
|
||||
### Pattern 2: TEA Solo for Quality, Skip Full Method
|
||||
|
||||
```
|
||||
Team decision:
|
||||
- Don't want full BMad Method (too heavyweight)
|
||||
- Want systematic testing (TEA benefits)
|
||||
|
||||
Approach: TEA Solo only
|
||||
- Use existing project management (Jira, Linear)
|
||||
- Use TEA for testing only
|
||||
- Get quality without methodology commitment
|
||||
```
|
||||
|
||||
### Pattern 3: Integrated for Critical, Lite for Non-Critical
|
||||
|
||||
```
|
||||
Critical features (payment, auth):
|
||||
- Full TEA Integrated workflow
|
||||
- Risk assessment, ATDD, quality gates
|
||||
- High confidence required
|
||||
|
||||
Non-critical features (UI tweaks):
|
||||
- TEA Lite or No TEA
|
||||
- Quick tests, minimal overhead
|
||||
- Move fast
|
||||
```
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
Each model uses different TEA workflows. See:
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Model details
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - Workflow reference
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Setup options
|
||||
|
||||
## Related Concepts
|
||||
|
||||
**Core TEA Concepts:**
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Risk assessment in different models
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Quality across all models
|
||||
- [Knowledge Base System](/docs/tea/explanation/knowledge-base-system.md) - Consistent patterns across models
|
||||
|
||||
**Technical Patterns:**
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Infrastructure in different models
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Reliability in all models
|
||||
|
||||
**Overview:**
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - 5 engagement models with cheat sheets
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - Design philosophy
|
||||
|
||||
## Practical Guides
|
||||
|
||||
**Getting Started:**
|
||||
- [TEA Lite Quickstart Tutorial](/docs/tea/tutorials/tea-lite-quickstart.md) - Model 3: TEA Lite
|
||||
|
||||
**Use-Case Guides:**
|
||||
- [Using TEA with Existing Tests](/docs/tea/how-to/brownfield/use-tea-with-existing-tests.md) - Model 5: Brownfield
|
||||
- [Running TEA for Enterprise](/docs/tea/how-to/brownfield/use-tea-for-enterprise.md) - Enterprise integration
|
||||
|
||||
**All Workflow Guides:**
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Used in TEA Solo and Integrated
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md)
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md)
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md)
|
||||
- [How to Run Trace](/docs/tea/how-to/workflows/run-trace.md)
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - All workflows explained
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Config per model
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - TEA Lite, TEA Solo, TEA Integrated terms
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,457 +0,0 @@
|
||||
---
|
||||
title: "Fixture Architecture Explained"
|
||||
description: Understanding TEA's pure function → fixture → composition pattern for reusable test utilities
|
||||
---
|
||||
|
||||
# Fixture Architecture Explained
|
||||
|
||||
Fixture architecture is TEA's pattern for building reusable, testable, and composable test utilities. The core principle: build pure functions first, wrap in framework fixtures second.
|
||||
|
||||
## Overview
|
||||
|
||||
**The Pattern:**
|
||||
1. Write utility as pure function (unit-testable)
|
||||
2. Wrap in framework fixture (Playwright, Cypress)
|
||||
3. Compose fixtures with mergeTests (combine capabilities)
|
||||
4. Package for reuse across projects
|
||||
|
||||
**Why this order?**
|
||||
- Pure functions are easier to test
|
||||
- Fixtures depend on framework (less portable)
|
||||
- Composition happens at fixture level
|
||||
- Reusability maximized
|
||||
|
||||
### Fixture Architecture Flow
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
||||
flowchart TD
|
||||
Start([Testing Need]) --> Pure[Step 1: Pure Function<br/>helpers/api-request.ts]
|
||||
Pure -->|Unit testable<br/>Framework agnostic| Fixture[Step 2: Fixture Wrapper<br/>fixtures/api-request.ts]
|
||||
Fixture -->|Injects framework<br/>dependencies| Compose[Step 3: Composition<br/>fixtures/index.ts]
|
||||
Compose -->|mergeTests| Use[Step 4: Use in Tests<br/>tests/**.spec.ts]
|
||||
|
||||
Pure -.->|Can test in isolation| UnitTest[Unit Tests<br/>No framework needed]
|
||||
Fixture -.->|Reusable pattern| Other[Other Projects<br/>Package export]
|
||||
Compose -.->|Combine utilities| Multi[Multiple Fixtures<br/>One test]
|
||||
|
||||
style Pure fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
|
||||
style Fixture fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
style Compose fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
|
||||
style Use fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
|
||||
style UnitTest fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px
|
||||
style Other fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px
|
||||
style Multi fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px
|
||||
```
|
||||
|
||||
**Benefits at Each Step:**
|
||||
1. **Pure Function:** Testable, portable, reusable
|
||||
2. **Fixture:** Framework integration, clean API
|
||||
3. **Composition:** Combine capabilities, flexible
|
||||
4. **Usage:** Simple imports, type-safe
|
||||
|
||||
## The Problem
|
||||
|
||||
### Framework-First Approach (Common Anti-Pattern)
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Built as fixture from the start
|
||||
export const test = base.extend({
|
||||
apiRequest: async ({ request }, use) => {
|
||||
await use(async (options) => {
|
||||
const response = await request.fetch(options.url, {
|
||||
method: options.method,
|
||||
data: options.data
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`API request failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Cannot unit test (requires Playwright context)
|
||||
- Tied to framework (not reusable in other tools)
|
||||
- Hard to compose with other fixtures
|
||||
- Difficult to mock for testing the utility itself
|
||||
|
||||
### Copy-Paste Utilities
|
||||
|
||||
```typescript
|
||||
// test-1.spec.ts
|
||||
test('test 1', async ({ request }) => {
|
||||
const response = await request.post('/api/users', { data: {...} });
|
||||
const body = await response.json();
|
||||
if (!response.ok()) throw new Error('Failed');
|
||||
// ... repeated in every test
|
||||
});
|
||||
|
||||
// test-2.spec.ts
|
||||
test('test 2', async ({ request }) => {
|
||||
const response = await request.post('/api/users', { data: {...} });
|
||||
const body = await response.json();
|
||||
if (!response.ok()) throw new Error('Failed');
|
||||
// ... same code repeated
|
||||
});
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Code duplication (violates DRY)
|
||||
- Inconsistent error handling
|
||||
- Hard to update (change 50 tests)
|
||||
- No shared behavior
|
||||
|
||||
## The Solution: Three-Step Pattern
|
||||
|
||||
### Step 1: Pure Function
|
||||
|
||||
```typescript
|
||||
// helpers/api-request.ts
|
||||
|
||||
/**
|
||||
* Make API request with automatic error handling
|
||||
* Pure function - no framework dependencies
|
||||
*/
|
||||
export async function apiRequest({
|
||||
request, // Passed in (dependency injection)
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
headers = {}
|
||||
}: ApiRequestParams): Promise<ApiResponse> {
|
||||
const response = await request.fetch(url, {
|
||||
method,
|
||||
data,
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`API request failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status(),
|
||||
body: await response.json()
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Can unit test this function!
|
||||
describe('apiRequest', () => {
|
||||
it('should throw on non-OK response', async () => {
|
||||
const mockRequest = {
|
||||
fetch: vi.fn().mockResolvedValue({ ok: () => false, status: () => 500 })
|
||||
};
|
||||
|
||||
await expect(apiRequest({
|
||||
request: mockRequest,
|
||||
method: 'GET',
|
||||
url: '/api/test'
|
||||
})).rejects.toThrow('API request failed: 500');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Unit testable (mock dependencies)
|
||||
- Framework-agnostic (works with any HTTP client)
|
||||
- Easy to reason about (pure function)
|
||||
- Portable (can use in Node scripts, CLI tools)
|
||||
|
||||
### Step 2: Fixture Wrapper
|
||||
|
||||
```typescript
|
||||
// fixtures/api-request.ts
|
||||
import { test as base } from '@playwright/test';
|
||||
import { apiRequest as apiRequestFn } from '../helpers/api-request';
|
||||
|
||||
/**
|
||||
* Playwright fixture wrapping the pure function
|
||||
*/
|
||||
export const test = base.extend<{ apiRequest: typeof apiRequestFn }>({
|
||||
apiRequest: async ({ request }, use) => {
|
||||
// Inject framework dependency (request)
|
||||
await use((params) => apiRequestFn({ request, ...params }));
|
||||
}
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Fixture provides framework context (request)
|
||||
- Pure function handles logic
|
||||
- Clean separation of concerns
|
||||
- Can swap frameworks (Cypress, etc.) by changing wrapper only
|
||||
|
||||
### Step 3: Composition with mergeTests
|
||||
|
||||
```typescript
|
||||
// fixtures/index.ts
|
||||
import { mergeTests } from '@playwright/test';
|
||||
import { test as apiRequestTest } from './api-request';
|
||||
import { test as authSessionTest } from './auth-session';
|
||||
import { test as logTest } from './log';
|
||||
|
||||
/**
|
||||
* Compose all fixtures into one test
|
||||
*/
|
||||
export const test = mergeTests(
|
||||
apiRequestTest,
|
||||
authSessionTest,
|
||||
logTest
|
||||
);
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// tests/profile.spec.ts
|
||||
import { test, expect } from '../support/fixtures';
|
||||
|
||||
test('should update profile', async ({ apiRequest, authToken, log }) => {
|
||||
log.info('Starting profile update test');
|
||||
|
||||
// Use API request fixture (matches pure function signature)
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
url: '/api/profile',
|
||||
data: { name: 'New Name' },
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.name).toBe('New Name');
|
||||
|
||||
log.info('Profile updated successfully');
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** This example uses the vanilla pure function signature (`url`, `data`). Playwright Utils uses different parameter names (`path`, `body`). See [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) for the utilities API.
|
||||
|
||||
**Note:** `authToken` requires auth-session fixture setup with provider configuration. See [auth-session documentation](https://seontechnologies.github.io/playwright-utils/auth-session.html).
|
||||
|
||||
**Benefits:**
|
||||
- Use multiple fixtures in one test
|
||||
- No manual composition needed
|
||||
- Type-safe (TypeScript knows all fixture types)
|
||||
- Clean imports
|
||||
|
||||
## How It Works in TEA
|
||||
|
||||
### TEA Generates This Pattern
|
||||
|
||||
When you run `framework` with `tea_use_playwright_utils: true`:
|
||||
|
||||
**TEA scaffolds:**
|
||||
```
|
||||
tests/
|
||||
├── support/
|
||||
│ ├── helpers/ # Pure functions
|
||||
│ │ ├── api-request.ts
|
||||
│ │ └── auth-session.ts
|
||||
│ └── fixtures/ # Framework wrappers
|
||||
│ ├── api-request.ts
|
||||
│ ├── auth-session.ts
|
||||
│ └── index.ts # Composition
|
||||
└── e2e/
|
||||
└── example.spec.ts # Uses composed fixtures
|
||||
```
|
||||
|
||||
### TEA Reviews Against This Pattern
|
||||
|
||||
When you run `test-review`:
|
||||
|
||||
**TEA checks:**
|
||||
- Are utilities pure functions? ✓
|
||||
- Are fixtures minimal wrappers? ✓
|
||||
- Is composition used? ✓
|
||||
- Can utilities be unit tested? ✓
|
||||
|
||||
## Package Export Pattern
|
||||
|
||||
### Make Fixtures Reusable Across Projects
|
||||
|
||||
**Option 1: Build Your Own (Vanilla)**
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"name": "@company/test-utils",
|
||||
"exports": {
|
||||
"./api-request": "./fixtures/api-request.ts",
|
||||
"./auth-session": "./fixtures/auth-session.ts",
|
||||
"./log": "./fixtures/log.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test as apiTest } from '@company/test-utils/api-request';
|
||||
import { test as authTest } from '@company/test-utils/auth-session';
|
||||
import { mergeTests } from '@playwright/test';
|
||||
|
||||
export const test = mergeTests(apiTest, authTest);
|
||||
```
|
||||
|
||||
**Option 2: Use Playwright Utils (Recommended)**
|
||||
```bash
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test as base } from '@playwright/test';
|
||||
import { mergeTests } from '@playwright/test';
|
||||
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { createAuthFixtures } from '@seontechnologies/playwright-utils/auth-session';
|
||||
|
||||
const authFixtureTest = base.extend(createAuthFixtures());
|
||||
export const test = mergeTests(apiRequestFixture, authFixtureTest);
|
||||
// Production-ready utilities, battle-tested!
|
||||
```
|
||||
|
||||
**Note:** Auth-session requires provider configuration. See [auth-session setup guide](https://seontechnologies.github.io/playwright-utils/auth-session.html).
|
||||
|
||||
**Why Playwright Utils:**
|
||||
- Already built, tested, and maintained
|
||||
- Consistent patterns across projects
|
||||
- 11 utilities available (API, auth, network, logging, files)
|
||||
- Community support and documentation
|
||||
- Regular updates and improvements
|
||||
|
||||
**When to Build Your Own:**
|
||||
- Company-specific patterns
|
||||
- Custom authentication systems
|
||||
- Unique requirements not covered by utilities
|
||||
|
||||
## Comparison: Good vs Bad Patterns
|
||||
|
||||
### Anti-Pattern: God Fixture
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Everything in one fixture
|
||||
export const test = base.extend({
|
||||
testUtils: async ({ page, request, context }, use) => {
|
||||
await use({
|
||||
// 50 different methods crammed into one fixture
|
||||
apiRequest: async (...) => { },
|
||||
login: async (...) => { },
|
||||
createUser: async (...) => { },
|
||||
deleteUser: async (...) => { },
|
||||
uploadFile: async (...) => { },
|
||||
// ... 45 more methods
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Cannot test individual utilities
|
||||
- Cannot compose (all-or-nothing)
|
||||
- Cannot reuse specific utilities
|
||||
- Hard to maintain (1000+ line file)
|
||||
|
||||
### Good Pattern: Single-Concern Fixtures
|
||||
|
||||
```typescript
|
||||
// ✅ Good: One concern per fixture
|
||||
|
||||
// api-request.ts
|
||||
export const test = base.extend({ apiRequest });
|
||||
|
||||
// auth-session.ts
|
||||
export const test = base.extend({ authSession });
|
||||
|
||||
// log.ts
|
||||
export const test = base.extend({ log });
|
||||
|
||||
// Compose as needed
|
||||
import { mergeTests } from '@playwright/test';
|
||||
export const test = mergeTests(apiRequestTest, authSessionTest, logTest);
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Each fixture is unit-testable
|
||||
- Compose only what you need
|
||||
- Reuse individual fixtures
|
||||
- Easy to maintain (small files)
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
For detailed fixture architecture patterns, see the knowledge base:
|
||||
- [Knowledge Base Index - Architecture & Fixtures](/docs/tea/reference/knowledge-base.md)
|
||||
- [Complete Knowledge Base Index](/docs/tea/reference/knowledge-base.md)
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
### Always Use For:
|
||||
|
||||
**Reusable utilities:**
|
||||
- API request helpers
|
||||
- Authentication handlers
|
||||
- File operations
|
||||
- Network mocking
|
||||
|
||||
**Test infrastructure:**
|
||||
- Shared fixtures across teams
|
||||
- Packaged utilities (playwright-utils)
|
||||
- Company-wide test standards
|
||||
|
||||
### Consider Skipping For:
|
||||
|
||||
**One-off test setup:**
|
||||
```typescript
|
||||
// Simple one-time setup - inline is fine
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.click('#accept-cookies');
|
||||
});
|
||||
```
|
||||
|
||||
**Test-specific helpers:**
|
||||
```typescript
|
||||
// Used in one test file only - keep local
|
||||
function createTestUser(name: string) {
|
||||
return { name, email: `${name}@test.com` };
|
||||
}
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
**Core TEA Concepts:**
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Quality standards fixtures enforce
|
||||
- [Knowledge Base System](/docs/tea/explanation/knowledge-base-system.md) - Fixture patterns in knowledge base
|
||||
|
||||
**Technical Patterns:**
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Network fixtures explained
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Fixture complexity matches risk
|
||||
|
||||
**Overview:**
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Fixture architecture in workflows
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - Why fixtures matter
|
||||
|
||||
## Practical Guides
|
||||
|
||||
**Setup Guides:**
|
||||
- [How to Set Up Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md) - TEA scaffolds fixtures
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - Production-ready fixtures
|
||||
|
||||
**Workflow Guides:**
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Using fixtures in tests
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Fixture composition examples
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - `framework` command
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Fixture architecture fragments
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - Fixture architecture term
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,554 +0,0 @@
|
||||
---
|
||||
title: "Knowledge Base System Explained"
|
||||
description: Understanding how TEA uses tea-index.csv for context engineering and consistent test quality
|
||||
---
|
||||
|
||||
# Knowledge Base System Explained
|
||||
|
||||
TEA's knowledge base system is how context engineering works - automatically loading domain-specific standards into AI context so tests are consistently high-quality regardless of prompt variation.
|
||||
|
||||
## Overview
|
||||
|
||||
**The Problem:** AI without context produces inconsistent results.
|
||||
|
||||
**Traditional approach:**
|
||||
```
|
||||
User: "Write tests for login"
|
||||
AI: [Generates tests with random quality]
|
||||
- Sometimes uses hard waits
|
||||
- Sometimes uses good patterns
|
||||
- Inconsistent across sessions
|
||||
- Quality depends on prompt
|
||||
```
|
||||
|
||||
**TEA with knowledge base:**
|
||||
```
|
||||
User: "Write tests for login"
|
||||
TEA: [Loads test-quality.md, network-first.md, auth-session.md]
|
||||
TEA: [Generates tests following established patterns]
|
||||
- Always uses network-first patterns
|
||||
- Always uses proper fixtures
|
||||
- Consistent across all sessions
|
||||
- Quality independent of prompt
|
||||
```
|
||||
|
||||
**Result:** Systematic quality, not random chance.
|
||||
|
||||
## The Problem
|
||||
|
||||
### Prompt-Driven Testing = Inconsistency
|
||||
|
||||
**Session 1:**
|
||||
```
|
||||
User: "Write tests for profile editing"
|
||||
|
||||
AI: [No context loaded]
|
||||
// Generates test with hard waits
|
||||
await page.waitForTimeout(3000);
|
||||
```
|
||||
|
||||
**Session 2:**
|
||||
```
|
||||
User: "Write comprehensive tests for profile editing with best practices"
|
||||
|
||||
AI: [Still no systematic context]
|
||||
// Generates test with some improvements, but still issues
|
||||
await page.waitForSelector('.success', { timeout: 10000 });
|
||||
```
|
||||
|
||||
**Session 3:**
|
||||
```
|
||||
User: "Write tests using network-first patterns and proper fixtures"
|
||||
|
||||
AI: [Better prompt, but still reinventing patterns]
|
||||
// Generates test with network-first, but inconsistent with other tests
|
||||
```
|
||||
|
||||
**Problem:** Quality depends on prompt engineering skill, no consistency.
|
||||
|
||||
### Knowledge Drift
|
||||
|
||||
Without a knowledge base:
|
||||
- Team A uses pattern X
|
||||
- Team B uses pattern Y
|
||||
- Both work, but inconsistent
|
||||
- No single source of truth
|
||||
- Patterns drift over time
|
||||
|
||||
## The Solution: tea-index.csv Manifest
|
||||
|
||||
### How It Works
|
||||
|
||||
**1. Manifest Defines Fragments**
|
||||
|
||||
`src/bmm/testarch/tea-index.csv`:
|
||||
```csv
|
||||
id,name,description,tags,fragment_file
|
||||
test-quality,Test Quality,Execution limits and isolation rules,quality;standards,knowledge/test-quality.md
|
||||
network-first,Network-First Safeguards,Intercept-before-navigate workflow,network;stability,knowledge/network-first.md
|
||||
fixture-architecture,Fixture Architecture,Composable fixture patterns,fixtures;architecture,knowledge/fixture-architecture.md
|
||||
```
|
||||
|
||||
**2. Workflow Loads Relevant Fragments**
|
||||
|
||||
When user runs `atdd`:
|
||||
```
|
||||
TEA reads tea-index.csv
|
||||
Identifies fragments needed for ATDD:
|
||||
- test-quality.md (quality standards)
|
||||
- network-first.md (avoid flakiness)
|
||||
- component-tdd.md (TDD patterns)
|
||||
- fixture-architecture.md (reusable fixtures)
|
||||
- data-factories.md (test data)
|
||||
|
||||
Loads only these 5 fragments (not all 33)
|
||||
Generates tests following these patterns
|
||||
```
|
||||
|
||||
**3. Consistent Output**
|
||||
|
||||
Every time `atdd` runs:
|
||||
- Same fragments loaded
|
||||
- Same patterns applied
|
||||
- Same quality standards
|
||||
- Consistent test structure
|
||||
|
||||
**Result:** Tests look like they were written by the same expert, every time.
|
||||
|
||||
### Knowledge Base Loading Diagram
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
||||
flowchart TD
|
||||
User([User: atdd]) --> Workflow[TEA Workflow<br/>Triggered]
|
||||
Workflow --> Read[Read Manifest<br/>tea-index.csv]
|
||||
|
||||
Read --> Identify{Identify Relevant<br/>Fragments for ATDD}
|
||||
|
||||
Identify -->|Needed| L1[✓ test-quality.md]
|
||||
Identify -->|Needed| L2[✓ network-first.md]
|
||||
Identify -->|Needed| L3[✓ component-tdd.md]
|
||||
Identify -->|Needed| L4[✓ data-factories.md]
|
||||
Identify -->|Needed| L5[✓ fixture-architecture.md]
|
||||
|
||||
Identify -.->|Skip| S1[✗ contract-testing.md]
|
||||
Identify -.->|Skip| S2[✗ burn-in.md]
|
||||
Identify -.->|Skip| S3[+ 26 other fragments]
|
||||
|
||||
L1 --> Context[AI Context<br/>5 fragments loaded]
|
||||
L2 --> Context
|
||||
L3 --> Context
|
||||
L4 --> Context
|
||||
L5 --> Context
|
||||
|
||||
Context --> Gen[Generate Tests<br/>Following patterns]
|
||||
Gen --> Out([Consistent Output<br/>Same quality every time])
|
||||
|
||||
style User fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
|
||||
style Read fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
style L1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
|
||||
style L2 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
|
||||
style L3 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
|
||||
style L4 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
|
||||
style L5 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
|
||||
style S1 fill:#e0e0e0,stroke:#616161,stroke-width:1px
|
||||
style S2 fill:#e0e0e0,stroke:#616161,stroke-width:1px
|
||||
style S3 fill:#e0e0e0,stroke:#616161,stroke-width:1px
|
||||
style Context fill:#f3e5f5,stroke:#6a1b9a,stroke-width:3px
|
||||
style Out fill:#4caf50,stroke:#1b5e20,stroke-width:3px,color:#fff
|
||||
```
|
||||
|
||||
## Fragment Structure
|
||||
|
||||
### Anatomy of a Fragment
|
||||
|
||||
Each fragment follows this structure:
|
||||
|
||||
```markdown
|
||||
# Fragment Name
|
||||
|
||||
## Principle
|
||||
[One sentence - what is this pattern?]
|
||||
|
||||
## Rationale
|
||||
[Why use this instead of alternatives?]
|
||||
Why this pattern exists
|
||||
Problems it solves
|
||||
Benefits it provides
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Example 1: Basic Usage
|
||||
```code
|
||||
[Runnable code example]
|
||||
```
|
||||
[Explanation of example]
|
||||
|
||||
### Example 2: Advanced Pattern
|
||||
```code
|
||||
[More complex example]
|
||||
```
|
||||
[Explanation]
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Don't Do This
|
||||
```code
|
||||
[Bad code example]
|
||||
```
|
||||
[Why it's bad]
|
||||
[What breaks]
|
||||
|
||||
## Related Patterns
|
||||
- [Link to related fragment]
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
### Example: test-quality.md Fragment
|
||||
|
||||
```markdown
|
||||
# Test Quality
|
||||
|
||||
## Principle
|
||||
Tests must be deterministic, isolated, explicit, focused, and fast.
|
||||
|
||||
## Rationale
|
||||
Tests that fail randomly, depend on each other, or take too long lose team trust.
|
||||
[... detailed explanation ...]
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Example 1: Deterministic Test
|
||||
```typescript
|
||||
// ✅ Wait for actual response, not timeout
|
||||
const promise = page.waitForResponse(matcher);
|
||||
await page.click('button');
|
||||
await promise;
|
||||
```
|
||||
|
||||
### Example 2: Isolated Test
|
||||
```typescript
|
||||
// ✅ Self-cleaning test
|
||||
test('test', async ({ page }) => {
|
||||
const userId = await createTestUser();
|
||||
// ... test logic ...
|
||||
await deleteTestUser(userId); // Cleanup
|
||||
});
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Hard Waits
|
||||
```typescript
|
||||
// ❌ Non-deterministic
|
||||
await page.waitForTimeout(3000);
|
||||
```
|
||||
[Why this causes flakiness]
|
||||
```
|
||||
|
||||
**Total:** 24.5 KB, 12 code examples
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
|
||||
## How TEA Uses the Knowledge Base
|
||||
|
||||
### Workflow-Specific Loading
|
||||
|
||||
**Different workflows load different fragments:**
|
||||
|
||||
| Workflow | Fragments Loaded | Purpose |
|
||||
|----------|-----------------|---------|
|
||||
| `framework` | fixture-architecture, playwright-config, fixtures-composition | Infrastructure patterns |
|
||||
| `test-design` | test-quality, test-priorities-matrix, risk-governance | Planning standards |
|
||||
| `atdd` | test-quality, component-tdd, network-first, data-factories | TDD patterns |
|
||||
| `automate` | test-quality, test-levels-framework, selector-resilience | Comprehensive generation |
|
||||
| `test-review` | All quality/resilience/debugging fragments | Full audit patterns |
|
||||
| `ci` | ci-burn-in, burn-in, selective-testing | CI/CD optimization |
|
||||
|
||||
**Benefit:** Only load what's needed (focused context, no bloat).
|
||||
|
||||
### Dynamic Fragment Selection
|
||||
|
||||
TEA doesn't load all 33 fragments at once:
|
||||
|
||||
```
|
||||
User runs: atdd for authentication feature
|
||||
|
||||
TEA analyzes context:
|
||||
- Feature type: Authentication
|
||||
- Relevant fragments:
|
||||
- test-quality.md (always loaded)
|
||||
- auth-session.md (auth patterns)
|
||||
- network-first.md (avoid flakiness)
|
||||
- email-auth.md (if email-based auth)
|
||||
- data-factories.md (test users)
|
||||
|
||||
Skips:
|
||||
- contract-testing.md (not relevant)
|
||||
- feature-flags.md (not relevant)
|
||||
- file-utils.md (not relevant)
|
||||
|
||||
Result: 5 relevant fragments loaded, 28 skipped
|
||||
```
|
||||
|
||||
**Benefit:** Focused context = better results, lower token usage.
|
||||
|
||||
## Context Engineering in Practice
|
||||
|
||||
### Example: Consistent Test Generation
|
||||
|
||||
**Without Knowledge Base (Vanilla Playwright, Random Quality):**
|
||||
```
|
||||
Session 1: User runs atdd
|
||||
AI: [Guesses patterns from general knowledge]
|
||||
|
||||
Generated:
|
||||
test('api test', async ({ request }) => {
|
||||
const response = await request.get('/api/users');
|
||||
await page.waitForTimeout(2000); // Hard wait
|
||||
const users = await response.json();
|
||||
// Random quality
|
||||
});
|
||||
|
||||
Session 2: User runs atdd (different day)
|
||||
AI: [Different random patterns]
|
||||
|
||||
Generated:
|
||||
test('api test', async ({ request }) => {
|
||||
const response = await request.get('/api/users');
|
||||
const users = await response.json();
|
||||
// Better but inconsistent
|
||||
});
|
||||
|
||||
Result: Inconsistent quality, random patterns
|
||||
```
|
||||
|
||||
**With Knowledge Base (TEA + Playwright Utils):**
|
||||
```
|
||||
Session 1: User runs atdd
|
||||
TEA: [Loads test-quality.md, network-first.md, api-request.md from tea-index.csv]
|
||||
|
||||
Generated:
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
|
||||
test('should fetch users', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users'
|
||||
}).validateSchema(UsersSchema); // Chained validation
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
Session 2: User runs atdd (different day)
|
||||
TEA: [Loads same fragments from tea-index.csv]
|
||||
|
||||
Generated: Identical pattern, same quality
|
||||
|
||||
Result: Systematic quality, established patterns (ALWAYS uses apiRequest utility when playwright-utils enabled)
|
||||
```
|
||||
|
||||
**Key Difference:**
|
||||
- **Without KB:** Random patterns, inconsistent APIs
|
||||
- **With KB:** Always uses `apiRequest` utility, always validates schemas, always returns `{ status, body }`
|
||||
|
||||
### Example: Test Review Consistency
|
||||
|
||||
**Without Knowledge Base:**
|
||||
```
|
||||
test-review session 1:
|
||||
"This test looks okay" [50 issues missed]
|
||||
|
||||
test-review session 2:
|
||||
"This test has some issues" [Different issues flagged]
|
||||
|
||||
Result: Inconsistent feedback
|
||||
```
|
||||
|
||||
**With Knowledge Base:**
|
||||
```
|
||||
test-review session 1:
|
||||
[Loads all quality fragments]
|
||||
Flags: 12 hard waits, 5 conditionals (based on test-quality.md)
|
||||
|
||||
test-review session 2:
|
||||
[Loads same fragments]
|
||||
Flags: Same issues with same explanations
|
||||
|
||||
Result: Consistent, reliable feedback
|
||||
```
|
||||
|
||||
## Maintaining the Knowledge Base
|
||||
|
||||
### When to Add a Fragment
|
||||
|
||||
**Good reasons:**
|
||||
- Pattern is used across multiple workflows
|
||||
- Standard is non-obvious (needs documentation)
|
||||
- Team asks "how should we handle X?" repeatedly
|
||||
- New tool integration (e.g., new testing library)
|
||||
|
||||
**Bad reasons:**
|
||||
- One-off pattern (document in test file instead)
|
||||
- Obvious pattern (everyone knows this)
|
||||
- Experimental (not proven yet)
|
||||
|
||||
### Fragment Quality Standards
|
||||
|
||||
**Good fragment:**
|
||||
- Principle stated in one sentence
|
||||
- Rationale explains why clearly
|
||||
- 3+ pattern examples with code
|
||||
- Anti-patterns shown (what not to do)
|
||||
- Self-contained (minimal dependencies)
|
||||
|
||||
**Example size:** 10-30 KB optimal
|
||||
|
||||
### Updating Existing Fragments
|
||||
|
||||
**When to update:**
|
||||
- Pattern evolved (better approach discovered)
|
||||
- Tool updated (new Playwright API)
|
||||
- Team feedback (pattern unclear)
|
||||
- Bug in example code
|
||||
|
||||
**How to update:**
|
||||
1. Edit fragment markdown file
|
||||
2. Update examples
|
||||
3. Test with affected workflows
|
||||
4. Ensure no breaking changes
|
||||
|
||||
**No need to update tea-index.csv** unless description/tags change.
|
||||
|
||||
## Benefits of Knowledge Base System
|
||||
|
||||
### 1. Consistency
|
||||
|
||||
**Before:** Test quality varies by who wrote it
|
||||
**After:** All tests follow same patterns (TEA-generated or reviewed)
|
||||
|
||||
### 2. Onboarding
|
||||
|
||||
**Before:** New team member reads 20 documents, asks 50 questions
|
||||
**After:** New team member runs `atdd`, sees patterns in generated code, learns by example
|
||||
|
||||
### 3. Quality Gates
|
||||
|
||||
**Before:** "Is this test good?" → subjective opinion
|
||||
**After:** `test-review` → objective score against knowledge base
|
||||
|
||||
### 4. Pattern Evolution
|
||||
|
||||
**Before:** Update tests manually across 100 files
|
||||
**After:** Update fragment once, all new tests use new pattern
|
||||
|
||||
### 5. Cross-Project Reuse
|
||||
|
||||
**Before:** Reinvent patterns for each project
|
||||
**After:** Same fragments across all BMad projects (consistency at scale)
|
||||
|
||||
## Comparison: With vs Without Knowledge Base
|
||||
|
||||
### Scenario: Testing Async Background Job
|
||||
|
||||
**Without Knowledge Base:**
|
||||
|
||||
Developer 1:
|
||||
```typescript
|
||||
// Uses hard wait
|
||||
await page.click('button');
|
||||
await page.waitForTimeout(10000); // Hope job finishes
|
||||
```
|
||||
|
||||
Developer 2:
|
||||
```typescript
|
||||
// Uses polling
|
||||
await page.click('button');
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const status = await page.locator('.status').textContent();
|
||||
if (status === 'complete') break;
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
```
|
||||
|
||||
Developer 3:
|
||||
```typescript
|
||||
// Uses waitForSelector
|
||||
await page.click('button');
|
||||
await page.waitForSelector('.success', { timeout: 30000 });
|
||||
```
|
||||
|
||||
**Result:** 3 different patterns, all suboptimal.
|
||||
|
||||
**With Knowledge Base (recurse.md fragment):**
|
||||
|
||||
All developers:
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('job completion', async ({ apiRequest, recurse }) => {
|
||||
// Start async job
|
||||
const { body: job } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/jobs'
|
||||
});
|
||||
|
||||
// Poll until complete (correct API: command, predicate, options)
|
||||
const result = await recurse(
|
||||
() => apiRequest({ method: 'GET', path: `/api/jobs/${job.id}` }),
|
||||
(response) => response.body.status === 'completed', // response.body from apiRequest
|
||||
{
|
||||
timeout: 30000,
|
||||
interval: 2000,
|
||||
log: 'Waiting for job to complete'
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.body.status).toBe('completed');
|
||||
});
|
||||
```
|
||||
|
||||
**Result:** Consistent pattern using correct playwright-utils API (command, predicate, options).
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
For details on the knowledge base index, see:
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md)
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md)
|
||||
|
||||
## Related Concepts
|
||||
|
||||
**Core TEA Concepts:**
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Standards in knowledge base
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Risk patterns in knowledge base
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - Knowledge base across all models
|
||||
|
||||
**Technical Patterns:**
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Fixture patterns in knowledge base
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Network patterns in knowledge base
|
||||
|
||||
**Overview:**
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Knowledge base in workflows
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - **Foundation: Context engineering philosophy** (why knowledge base solves AI test problems)
|
||||
|
||||
## Practical Guides
|
||||
|
||||
**All Workflow Guides Use Knowledge Base:**
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md)
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md)
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md)
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md)
|
||||
|
||||
**Integration:**
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - PW-Utils in knowledge base
|
||||
|
||||
## Reference
|
||||
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Complete fragment index
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - Which workflows load which fragments
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Config affects fragment loading
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - Context engineering, knowledge fragment terms
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,853 +0,0 @@
|
||||
---
|
||||
title: "Network-First Patterns Explained"
|
||||
description: Understanding how TEA eliminates test flakiness by waiting for actual network responses
|
||||
---
|
||||
|
||||
# Network-First Patterns Explained
|
||||
|
||||
Network-first patterns are TEA's solution to test flakiness. Instead of guessing how long to wait with fixed timeouts, wait for the actual network event that causes UI changes.
|
||||
|
||||
## Overview
|
||||
|
||||
**The Core Principle:**
|
||||
UI changes because APIs respond. Wait for the API response, not an arbitrary timeout.
|
||||
|
||||
**Traditional approach:**
|
||||
```typescript
|
||||
await page.click('button');
|
||||
await page.waitForTimeout(3000); // Hope 3 seconds is enough
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
```
|
||||
|
||||
**Network-first approach:**
|
||||
```typescript
|
||||
const responsePromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/submit') && resp.ok()
|
||||
);
|
||||
await page.click('button');
|
||||
await responsePromise; // Wait for actual response
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
```
|
||||
|
||||
**Result:** Deterministic tests that wait exactly as long as needed.
|
||||
|
||||
## The Problem
|
||||
|
||||
### Hard Waits Create Flakiness
|
||||
|
||||
```typescript
|
||||
// ❌ The flaky test pattern
|
||||
test('should submit form', async ({ page }) => {
|
||||
await page.fill('#name', 'Test User');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForTimeout(2000); // Wait 2 seconds
|
||||
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Why this fails:**
|
||||
- **Fast network:** Wastes 1.5 seconds waiting
|
||||
- **Slow network:** Not enough time, test fails
|
||||
- **CI environment:** Slower than local, fails randomly
|
||||
- **Under load:** API takes 3 seconds, test fails
|
||||
|
||||
**Result:** "Works on my machine" syndrome, flaky CI.
|
||||
|
||||
### The Timeout Escalation Trap
|
||||
|
||||
```typescript
|
||||
// Developer sees flaky test
|
||||
await page.waitForTimeout(2000); // Failed in CI
|
||||
|
||||
// Increases timeout
|
||||
await page.waitForTimeout(5000); // Still fails sometimes
|
||||
|
||||
// Increases again
|
||||
await page.waitForTimeout(10000); // Now it passes... slowly
|
||||
|
||||
// Problem: Now EVERY test waits 10 seconds
|
||||
// Suite that took 5 minutes now takes 30 minutes
|
||||
```
|
||||
|
||||
**Result:** Slow, still-flaky tests.
|
||||
|
||||
### Race Conditions
|
||||
|
||||
```typescript
|
||||
// ❌ Navigate-then-wait race condition
|
||||
test('should load dashboard data', async ({ page }) => {
|
||||
await page.goto('/dashboard'); // Navigation starts
|
||||
|
||||
// Race condition! API might not have responded yet
|
||||
await expect(page.locator('.data-table')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. `goto()` starts navigation
|
||||
2. Page loads HTML
|
||||
3. JavaScript requests `/api/dashboard`
|
||||
4. Test checks for `.data-table` BEFORE API responds
|
||||
5. Test fails intermittently
|
||||
|
||||
**Result:** "Sometimes it works, sometimes it doesn't."
|
||||
|
||||
## The Solution: Intercept-Before-Navigate
|
||||
|
||||
### Wait for Response Before Asserting
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Network-first pattern
|
||||
test('should load dashboard data', async ({ page }) => {
|
||||
// Set up promise BEFORE navigation
|
||||
const dashboardPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/dashboard') && resp.ok()
|
||||
);
|
||||
|
||||
// Navigate
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Wait for API response
|
||||
const response = await dashboardPromise;
|
||||
const data = await response.json();
|
||||
|
||||
// Now assert UI
|
||||
await expect(page.locator('.data-table')).toBeVisible();
|
||||
await expect(page.locator('.data-table tr')).toHaveCount(data.items.length);
|
||||
});
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Wait set up BEFORE navigation (no race)
|
||||
- Wait for actual API response (deterministic)
|
||||
- No fixed timeout (fast when API is fast)
|
||||
- Validates API response (catch backend errors)
|
||||
|
||||
**With Playwright Utils (Even Cleaner):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('should load dashboard data', async ({ page, interceptNetworkCall }) => {
|
||||
// Set up interception BEFORE navigation
|
||||
const dashboardCall = interceptNetworkCall({
|
||||
method: 'GET',
|
||||
url: '**/api/dashboard'
|
||||
});
|
||||
|
||||
// Navigate
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Wait for API response (automatic JSON parsing)
|
||||
const { status, responseJson: data } = await dashboardCall;
|
||||
|
||||
// Validate API response
|
||||
expect(status).toBe(200);
|
||||
expect(data.items).toBeDefined();
|
||||
|
||||
// Assert UI matches API data
|
||||
await expect(page.locator('.data-table')).toBeVisible();
|
||||
await expect(page.locator('.data-table tr')).toHaveCount(data.items.length);
|
||||
});
|
||||
```
|
||||
|
||||
**Playwright Utils Benefits:**
|
||||
- Automatic JSON parsing (no `await response.json()`)
|
||||
- Returns `{ status, responseJson, requestJson }` structure
|
||||
- Cleaner API (no need to check `resp.ok()`)
|
||||
- Same intercept-before-navigate pattern
|
||||
|
||||
### Intercept-Before-Navigate Pattern
|
||||
|
||||
**Key insight:** Set up wait BEFORE triggering the action.
|
||||
|
||||
```typescript
|
||||
// ✅ Pattern: Intercept → Action → Await
|
||||
|
||||
// 1. Intercept (set up wait)
|
||||
const promise = page.waitForResponse(matcher);
|
||||
|
||||
// 2. Action (trigger request)
|
||||
await page.click('button');
|
||||
|
||||
// 3. Await (wait for actual response)
|
||||
await promise;
|
||||
```
|
||||
|
||||
**Why this order:**
|
||||
- `waitForResponse()` starts listening immediately
|
||||
- Then trigger the action that makes the request
|
||||
- Then wait for the promise to resolve
|
||||
- No race condition possible
|
||||
|
||||
#### Intercept-Before-Navigate Flow
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
||||
sequenceDiagram
|
||||
participant Test
|
||||
participant Playwright
|
||||
participant Browser
|
||||
participant API
|
||||
|
||||
rect rgb(200, 230, 201)
|
||||
Note over Test,Playwright: ✅ CORRECT: Intercept First
|
||||
Test->>Playwright: 1. waitForResponse(matcher)
|
||||
Note over Playwright: Starts listening for response
|
||||
Test->>Browser: 2. click('button')
|
||||
Browser->>API: 3. POST /api/submit
|
||||
API-->>Browser: 4. 200 OK {success: true}
|
||||
Browser-->>Playwright: 5. Response captured
|
||||
Test->>Playwright: 6. await promise
|
||||
Playwright-->>Test: 7. Returns response
|
||||
Note over Test: No race condition!
|
||||
end
|
||||
|
||||
rect rgb(255, 205, 210)
|
||||
Note over Test,API: ❌ WRONG: Action First
|
||||
Test->>Browser: 1. click('button')
|
||||
Browser->>API: 2. POST /api/submit
|
||||
API-->>Browser: 3. 200 OK (already happened!)
|
||||
Test->>Playwright: 4. waitForResponse(matcher)
|
||||
Note over Test,Playwright: Too late - response already occurred
|
||||
Note over Test: Race condition! Test hangs or fails
|
||||
end
|
||||
```
|
||||
|
||||
**Correct Order (Green):**
|
||||
1. Set up listener (`waitForResponse`)
|
||||
2. Trigger action (`click`)
|
||||
3. Wait for response (`await promise`)
|
||||
|
||||
**Wrong Order (Red):**
|
||||
1. Trigger action first
|
||||
2. Set up listener too late
|
||||
3. Response already happened - missed!
|
||||
|
||||
## How It Works in TEA
|
||||
|
||||
### TEA Generates Network-First Tests
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// When you run `atdd` or `automate`, TEA generates:
|
||||
|
||||
test('should create user', async ({ page }) => {
|
||||
// TEA automatically includes network wait
|
||||
const createUserPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/users') &&
|
||||
resp.request().method() === 'POST' &&
|
||||
resp.ok()
|
||||
);
|
||||
|
||||
await page.fill('#name', 'Test User');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
const response = await createUserPromise;
|
||||
const user = await response.json();
|
||||
|
||||
// Validate both API and UI
|
||||
expect(user.id).toBeDefined();
|
||||
await expect(page.locator('.success')).toContainText(user.name);
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils (if `tea_use_playwright_utils: true`):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('should create user', async ({ page, interceptNetworkCall }) => {
|
||||
// TEA uses interceptNetworkCall for cleaner interception
|
||||
const createUserCall = interceptNetworkCall({
|
||||
method: 'POST',
|
||||
url: '**/api/users'
|
||||
});
|
||||
|
||||
await page.getByLabel('Name').fill('Test User');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Wait for response (automatic JSON parsing)
|
||||
const { status, responseJson: user } = await createUserCall;
|
||||
|
||||
// Validate both API and UI
|
||||
expect(status).toBe(201);
|
||||
expect(user.id).toBeDefined();
|
||||
await expect(page.locator('.success')).toContainText(user.name);
|
||||
});
|
||||
```
|
||||
|
||||
**Playwright Utils Benefits:**
|
||||
- Automatic JSON parsing (`responseJson` ready to use)
|
||||
- No manual `await response.json()`
|
||||
- Returns `{ status, responseJson }` structure
|
||||
- Cleaner, more readable code
|
||||
|
||||
### TEA Reviews for Hard Waits
|
||||
|
||||
When you run `test-review`:
|
||||
|
||||
```markdown
|
||||
## Critical Issue: Hard Wait Detected
|
||||
|
||||
**File:** tests/e2e/submit.spec.ts:45
|
||||
**Issue:** Using `page.waitForTimeout(3000)`
|
||||
**Severity:** Critical (causes flakiness)
|
||||
|
||||
**Current Code:**
|
||||
```typescript
|
||||
await page.click('button');
|
||||
await page.waitForTimeout(3000); // ❌
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
const responsePromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/submit') && resp.ok()
|
||||
);
|
||||
await page.click('button');
|
||||
await responsePromise; // ✅
|
||||
```
|
||||
|
||||
**Why:** Hard waits are non-deterministic. Use network-first patterns.
|
||||
```
|
||||
|
||||
## Pattern Variations
|
||||
|
||||
### Basic Response Wait
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// Wait for any successful response
|
||||
const promise = page.waitForResponse(resp => resp.ok());
|
||||
await page.click('button');
|
||||
await promise;
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('basic wait', async ({ page, interceptNetworkCall }) => {
|
||||
const responseCall = interceptNetworkCall({ url: '**' }); // Match any
|
||||
await page.click('button');
|
||||
const { status } = await responseCall;
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Specific URL Match
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// Wait for specific endpoint
|
||||
const promise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/users/123')
|
||||
);
|
||||
await page.goto('/user/123');
|
||||
await promise;
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
test('specific URL', async ({ page, interceptNetworkCall }) => {
|
||||
const userCall = interceptNetworkCall({ url: '**/api/users/123' });
|
||||
await page.goto('/user/123');
|
||||
const { status, responseJson } = await userCall;
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Method + Status Match
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// Wait for POST that returns 201
|
||||
const promise = page.waitForResponse(
|
||||
resp =>
|
||||
resp.url().includes('/api/users') &&
|
||||
resp.request().method() === 'POST' &&
|
||||
resp.status() === 201
|
||||
);
|
||||
await page.click('button[type="submit"]');
|
||||
await promise;
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
test('method and status', async ({ page, interceptNetworkCall }) => {
|
||||
const createCall = interceptNetworkCall({
|
||||
method: 'POST',
|
||||
url: '**/api/users'
|
||||
});
|
||||
await page.click('button[type="submit"]');
|
||||
const { status, responseJson } = await createCall;
|
||||
expect(status).toBe(201); // Explicit status check
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Multiple Responses
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// Wait for multiple API calls
|
||||
const [usersResp, postsResp] = await Promise.all([
|
||||
page.waitForResponse(resp => resp.url().includes('/api/users')),
|
||||
page.waitForResponse(resp => resp.url().includes('/api/posts')),
|
||||
page.goto('/dashboard') // Triggers both requests
|
||||
]);
|
||||
|
||||
const users = await usersResp.json();
|
||||
const posts = await postsResp.json();
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
test('multiple responses', async ({ page, interceptNetworkCall }) => {
|
||||
const usersCall = interceptNetworkCall({ url: '**/api/users' });
|
||||
const postsCall = interceptNetworkCall({ url: '**/api/posts' });
|
||||
|
||||
await page.goto('/dashboard'); // Triggers both
|
||||
|
||||
const [{ responseJson: users }, { responseJson: posts }] = await Promise.all([
|
||||
usersCall,
|
||||
postsCall
|
||||
]);
|
||||
|
||||
expect(users).toBeInstanceOf(Array);
|
||||
expect(posts).toBeInstanceOf(Array);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Validate Response Data
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// Verify API response before asserting UI
|
||||
const promise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/checkout') && resp.ok()
|
||||
);
|
||||
|
||||
await page.click('button:has-text("Complete Order")');
|
||||
|
||||
const response = await promise;
|
||||
const order = await response.json();
|
||||
|
||||
// Response validation
|
||||
expect(order.status).toBe('confirmed');
|
||||
expect(order.total).toBeGreaterThan(0);
|
||||
|
||||
// UI validation
|
||||
await expect(page.locator('.order-confirmation')).toContainText(order.id);
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
test('validate response data', async ({ page, interceptNetworkCall }) => {
|
||||
const checkoutCall = interceptNetworkCall({
|
||||
method: 'POST',
|
||||
url: '**/api/checkout'
|
||||
});
|
||||
|
||||
await page.click('button:has-text("Complete Order")');
|
||||
|
||||
const { status, responseJson: order } = await checkoutCall;
|
||||
|
||||
// Response validation (automatic JSON parsing)
|
||||
expect(status).toBe(200);
|
||||
expect(order.status).toBe('confirmed');
|
||||
expect(order.total).toBeGreaterThan(0);
|
||||
|
||||
// UI validation
|
||||
await expect(page.locator('.order-confirmation')).toContainText(order.id);
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### HAR Recording for Offline Testing
|
||||
|
||||
**Vanilla Playwright (Manual HAR Handling):**
|
||||
|
||||
```typescript
|
||||
// First run: Record mode (saves HAR file)
|
||||
test('offline testing - RECORD', async ({ page, context }) => {
|
||||
// Record mode: Save network traffic to HAR
|
||||
await context.routeFromHAR('./hars/dashboard.har', {
|
||||
url: '**/api/**',
|
||||
update: true // Update HAR file
|
||||
});
|
||||
|
||||
await page.goto('/dashboard');
|
||||
// All network traffic saved to dashboard.har
|
||||
});
|
||||
|
||||
// Subsequent runs: Playback mode (uses saved HAR)
|
||||
test('offline testing - PLAYBACK', async ({ page, context }) => {
|
||||
// Playback mode: Use saved network traffic
|
||||
await context.routeFromHAR('./hars/dashboard.har', {
|
||||
url: '**/api/**',
|
||||
update: false // Use existing HAR, no network calls
|
||||
});
|
||||
|
||||
await page.goto('/dashboard');
|
||||
// Uses recorded responses, no backend needed
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils (Automatic HAR Management):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures';
|
||||
|
||||
// Record mode: Set environment variable
|
||||
process.env.PW_NET_MODE = 'record';
|
||||
|
||||
test('should work offline', async ({ page, context, networkRecorder }) => {
|
||||
await networkRecorder.setup(context); // Handles HAR automatically
|
||||
|
||||
await page.goto('/dashboard');
|
||||
await page.click('#add-item');
|
||||
// All network traffic recorded, CRUD operations detected
|
||||
});
|
||||
```
|
||||
|
||||
**Switch to playback:**
|
||||
```bash
|
||||
# Playback mode (offline)
|
||||
PW_NET_MODE=playback npx playwright test
|
||||
# Uses HAR file, no backend needed!
|
||||
```
|
||||
|
||||
**Playwright Utils Benefits:**
|
||||
- Automatic HAR file management (naming, paths)
|
||||
- CRUD operation detection (stateful mocking)
|
||||
- Environment variable control (easy switching)
|
||||
- Works for complex interactions (create, update, delete)
|
||||
- No manual route configuration
|
||||
|
||||
### Network Request Interception
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
test('should handle API error', async ({ page }) => {
|
||||
// Manual route setup
|
||||
await page.route('**/api/users', (route) => {
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
body: JSON.stringify({ error: 'Internal server error' })
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/users');
|
||||
|
||||
const response = await page.waitForResponse('**/api/users');
|
||||
const error = await response.json();
|
||||
|
||||
expect(error.error).toContain('Internal server');
|
||||
await expect(page.locator('.error-message')).toContainText('Server error');
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('should handle API error', async ({ page, interceptNetworkCall }) => {
|
||||
// Stub API to return error (set up BEFORE navigation)
|
||||
const usersCall = interceptNetworkCall({
|
||||
method: 'GET',
|
||||
url: '**/api/users',
|
||||
fulfillResponse: {
|
||||
status: 500,
|
||||
body: { error: 'Internal server error' }
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/users');
|
||||
|
||||
// Wait for mocked response and access parsed data
|
||||
const { status, responseJson } = await usersCall;
|
||||
|
||||
expect(status).toBe(500);
|
||||
expect(responseJson.error).toContain('Internal server');
|
||||
await expect(page.locator('.error-message')).toContainText('Server error');
|
||||
});
|
||||
```
|
||||
|
||||
**Playwright Utils Benefits:**
|
||||
- Automatic JSON parsing (`responseJson` ready to use)
|
||||
- Returns promise with `{ status, responseJson, requestJson }`
|
||||
- No need to pass `page` (auto-injected by fixture)
|
||||
- Glob pattern matching (simpler than regex)
|
||||
- Single declarative call (setup + wait in one)
|
||||
|
||||
## Comparison: Traditional vs Network-First
|
||||
|
||||
### Loading Dashboard Data
|
||||
|
||||
**Traditional (Flaky):**
|
||||
```typescript
|
||||
test('dashboard loads data', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await page.waitForTimeout(2000); // ❌ Magic number
|
||||
await expect(page.locator('table tr')).toHaveCount(5);
|
||||
});
|
||||
```
|
||||
|
||||
**Failure modes:**
|
||||
- API takes 2.5s → test fails
|
||||
- API returns 3 items not 5 → hard to debug (which issue?)
|
||||
- CI slower than local → fails in CI only
|
||||
|
||||
**Network-First (Deterministic):**
|
||||
```typescript
|
||||
test('dashboard loads data', async ({ page }) => {
|
||||
const apiPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/dashboard') && resp.ok()
|
||||
);
|
||||
|
||||
await page.goto('/dashboard');
|
||||
|
||||
const response = await apiPromise;
|
||||
const { items } = await response.json();
|
||||
|
||||
// Validate API response
|
||||
expect(items).toHaveLength(5);
|
||||
|
||||
// Validate UI matches API
|
||||
await expect(page.locator('table tr')).toHaveCount(items.length);
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Waits exactly as long as needed (100ms or 5s, doesn't matter)
|
||||
- Validates API response (catch backend errors)
|
||||
- Validates UI matches API (catch frontend bugs)
|
||||
- Works in any environment (local, CI, staging)
|
||||
|
||||
**With Playwright Utils (Even Better):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('dashboard loads data', async ({ page, interceptNetworkCall }) => {
|
||||
const dashboardCall = interceptNetworkCall({
|
||||
method: 'GET',
|
||||
url: '**/api/dashboard'
|
||||
});
|
||||
|
||||
await page.goto('/dashboard');
|
||||
|
||||
const { status, responseJson: { items } } = await dashboardCall;
|
||||
|
||||
// Validate API response (automatic JSON parsing)
|
||||
expect(status).toBe(200);
|
||||
expect(items).toHaveLength(5);
|
||||
|
||||
// Validate UI matches API
|
||||
await expect(page.locator('table tr')).toHaveCount(items.length);
|
||||
});
|
||||
```
|
||||
|
||||
**Additional Benefits:**
|
||||
- No manual `await response.json()` (automatic parsing)
|
||||
- Cleaner destructuring of nested data
|
||||
- Consistent API across all network calls
|
||||
|
||||
---
|
||||
|
||||
### Form Submission
|
||||
|
||||
**Traditional (Flaky):**
|
||||
```typescript
|
||||
test('form submission', async ({ page }) => {
|
||||
await page.fill('#email', 'test@example.com');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000); // ❌ Hope it's enough
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Network-First (Deterministic):**
|
||||
```typescript
|
||||
test('form submission', async ({ page }) => {
|
||||
const submitPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/submit') &&
|
||||
resp.request().method() === 'POST' &&
|
||||
resp.ok()
|
||||
);
|
||||
|
||||
await page.fill('#email', 'test@example.com');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
const response = await submitPromise;
|
||||
const result = await response.json();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('form submission', async ({ page, interceptNetworkCall }) => {
|
||||
const submitCall = interceptNetworkCall({
|
||||
method: 'POST',
|
||||
url: '**/api/submit'
|
||||
});
|
||||
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
const { status, responseJson: result } = await submitCall;
|
||||
|
||||
// Automatic JSON parsing, no manual await
|
||||
expect(status).toBe(200);
|
||||
expect(result.success).toBe(true);
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Progression:**
|
||||
- Traditional: Hard waits (flaky)
|
||||
- Network-First (Vanilla): waitForResponse (deterministic)
|
||||
- Network-First (PW-Utils): interceptNetworkCall (deterministic + cleaner API)
|
||||
|
||||
---
|
||||
|
||||
## Common Misconceptions
|
||||
|
||||
### "I Already Use waitForSelector"
|
||||
|
||||
```typescript
|
||||
// This is still a hard wait in disguise
|
||||
await page.click('button');
|
||||
await page.waitForSelector('.success', { timeout: 5000 });
|
||||
```
|
||||
|
||||
**Problem:** Waiting for DOM, not for the API that caused DOM change.
|
||||
|
||||
**Better:**
|
||||
```typescript
|
||||
await page.waitForResponse(matcher); // Wait for root cause
|
||||
await page.waitForSelector('.success'); // Then validate UI
|
||||
```
|
||||
|
||||
### "My Tests Are Fast, Why Add Complexity?"
|
||||
|
||||
**Short-term:** Tests are fast locally
|
||||
|
||||
**Long-term problems:**
|
||||
- Different environments (CI slower)
|
||||
- Under load (API slower)
|
||||
- Network variability (random)
|
||||
- Scaling test suite (100 → 1000 tests)
|
||||
|
||||
**Network-first prevents these issues before they appear.**
|
||||
|
||||
### "Too Much Boilerplate"
|
||||
|
||||
**Problem:** `waitForResponse` is verbose, repeated in every test.
|
||||
|
||||
**Solution:** Use Playwright Utils `interceptNetworkCall` - built-in fixture that reduces boilerplate.
|
||||
|
||||
**Vanilla Playwright (Repetitive):**
|
||||
```typescript
|
||||
test('test 1', async ({ page }) => {
|
||||
const promise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/submit') && resp.ok()
|
||||
);
|
||||
await page.click('button');
|
||||
await promise;
|
||||
});
|
||||
|
||||
test('test 2', async ({ page }) => {
|
||||
const promise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/load') && resp.ok()
|
||||
);
|
||||
await page.click('button');
|
||||
await promise;
|
||||
});
|
||||
// Repeated pattern in every test
|
||||
```
|
||||
|
||||
**With Playwright Utils (Cleaner):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('test 1', async ({ page, interceptNetworkCall }) => {
|
||||
const submitCall = interceptNetworkCall({ url: '**/api/submit' });
|
||||
await page.click('button');
|
||||
const { status, responseJson } = await submitCall;
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
test('test 2', async ({ page, interceptNetworkCall }) => {
|
||||
const loadCall = interceptNetworkCall({ url: '**/api/load' });
|
||||
await page.click('button');
|
||||
const { responseJson } = await loadCall;
|
||||
// Automatic JSON parsing, cleaner API
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Less boilerplate (fixture handles complexity)
|
||||
- Automatic JSON parsing
|
||||
- Glob pattern matching (`**/api/**`)
|
||||
- Consistent API across all tests
|
||||
|
||||
See [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md#intercept-network-call) for setup.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
For detailed network-first patterns, see the knowledge base:
|
||||
- [Knowledge Base Index - Network & Reliability](/docs/tea/reference/knowledge-base.md)
|
||||
- [Complete Knowledge Base Index](/docs/tea/reference/knowledge-base.md)
|
||||
|
||||
## Related Concepts
|
||||
|
||||
**Core TEA Concepts:**
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Determinism requires network-first
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - High-risk features need reliable tests
|
||||
|
||||
**Technical Patterns:**
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Network utilities as fixtures
|
||||
- [Knowledge Base System](/docs/tea/explanation/knowledge-base-system.md) - Network patterns in knowledge base
|
||||
|
||||
**Overview:**
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Network-first in workflows
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - Why flakiness matters
|
||||
|
||||
## Practical Guides
|
||||
|
||||
**Workflow Guides:**
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Review for hard waits
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Generate network-first tests
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Expand with network patterns
|
||||
|
||||
**Use-Case Guides:**
|
||||
- [Using TEA with Existing Tests](/docs/tea/how-to/brownfield/use-tea-with-existing-tests.md) - Fix flaky legacy tests
|
||||
|
||||
**Customization:**
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - Network utilities (recorder, interceptor, error monitor)
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - All workflows use network-first
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Network-first fragment
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - Network-first pattern term
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,586 +0,0 @@
|
||||
---
|
||||
title: "Risk-Based Testing Explained"
|
||||
description: Understanding how TEA uses probability × impact scoring to prioritize testing effort
|
||||
---
|
||||
|
||||
# Risk-Based Testing Explained
|
||||
|
||||
Risk-based testing is TEA's core principle: testing depth scales with business impact. Instead of testing everything equally, focus effort where failures hurt most.
|
||||
|
||||
## Overview
|
||||
|
||||
Traditional testing approaches treat all features equally:
|
||||
- Every feature gets same test coverage
|
||||
- Same level of scrutiny regardless of impact
|
||||
- No systematic prioritization
|
||||
- Testing becomes checkbox exercise
|
||||
|
||||
**Risk-based testing asks:**
|
||||
- What's the probability this will fail?
|
||||
- What's the impact if it does fail?
|
||||
- How much testing is appropriate for this risk level?
|
||||
|
||||
**Result:** Testing effort matches business criticality.
|
||||
|
||||
## The Problem
|
||||
|
||||
### Equal Testing for Unequal Risk
|
||||
|
||||
```markdown
|
||||
Feature A: User login (critical path, millions of users)
|
||||
Feature B: Export to PDF (nice-to-have, rarely used)
|
||||
|
||||
Traditional approach:
|
||||
- Both get 10 tests
|
||||
- Both get same review scrutiny
|
||||
- Both take same development time
|
||||
|
||||
Problem: Wasting effort on low-impact features while under-testing critical paths.
|
||||
```
|
||||
|
||||
### No Objective Prioritization
|
||||
|
||||
```markdown
|
||||
PM: "We need more tests for checkout"
|
||||
QA: "How many tests?"
|
||||
PM: "I don't know... a lot?"
|
||||
QA: "How do we know when we have enough?"
|
||||
PM: "When it feels safe?"
|
||||
|
||||
Problem: Subjective decisions, no data, political debates.
|
||||
```
|
||||
|
||||
## The Solution: Probability × Impact Scoring
|
||||
|
||||
### Risk Score = Probability × Impact
|
||||
|
||||
**Probability** (How likely to fail?)
|
||||
- **1 (Low):** Stable, well-tested, simple logic
|
||||
- **2 (Medium):** Moderate complexity, some unknowns
|
||||
- **3 (High):** Complex, untested, many edge cases
|
||||
|
||||
**Impact** (How bad if it fails?)
|
||||
- **1 (Low):** Minor inconvenience, few users affected
|
||||
- **2 (Medium):** Degraded experience, workarounds exist
|
||||
- **3 (High):** Critical path broken, business impact
|
||||
|
||||
**Score Range:** 1-9
|
||||
|
||||
#### Risk Scoring Matrix
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
||||
graph TD
|
||||
subgraph Matrix[" "]
|
||||
direction TB
|
||||
subgraph Impact3["Impact: HIGH (3)"]
|
||||
P1I3["Score: 3<br/>Low Risk"]
|
||||
P2I3["Score: 6<br/>HIGH RISK<br/>Mitigation Required"]
|
||||
P3I3["Score: 9<br/>CRITICAL<br/>Blocks Release"]
|
||||
end
|
||||
subgraph Impact2["Impact: MEDIUM (2)"]
|
||||
P1I2["Score: 2<br/>Low Risk"]
|
||||
P2I2["Score: 4<br/>Medium Risk"]
|
||||
P3I2["Score: 6<br/>HIGH RISK<br/>Mitigation Required"]
|
||||
end
|
||||
subgraph Impact1["Impact: LOW (1)"]
|
||||
P1I1["Score: 1<br/>Low Risk"]
|
||||
P2I1["Score: 2<br/>Low Risk"]
|
||||
P3I1["Score: 3<br/>Low Risk"]
|
||||
end
|
||||
end
|
||||
|
||||
Prob1["Probability: LOW (1)"] -.-> P1I1
|
||||
Prob1 -.-> P1I2
|
||||
Prob1 -.-> P1I3
|
||||
|
||||
Prob2["Probability: MEDIUM (2)"] -.-> P2I1
|
||||
Prob2 -.-> P2I2
|
||||
Prob2 -.-> P2I3
|
||||
|
||||
Prob3["Probability: HIGH (3)"] -.-> P3I1
|
||||
Prob3 -.-> P3I2
|
||||
Prob3 -.-> P3I3
|
||||
|
||||
style P3I3 fill:#f44336,stroke:#b71c1c,stroke-width:3px,color:#fff
|
||||
style P2I3 fill:#ff9800,stroke:#e65100,stroke-width:2px,color:#000
|
||||
style P3I2 fill:#ff9800,stroke:#e65100,stroke-width:2px,color:#000
|
||||
style P2I2 fill:#fff9c4,stroke:#f57f17,stroke-width:1px,color:#000
|
||||
style P1I1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
style P2I1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
style P3I1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
style P1I2 fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
style P1I3 fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
```
|
||||
|
||||
**Legend:**
|
||||
- 🔴 Red (Score 9): CRITICAL - Blocks release
|
||||
- 🟠 Orange (Score 6-8): HIGH RISK - Mitigation required
|
||||
- 🟡 Yellow (Score 4-5): MEDIUM - Mitigation recommended
|
||||
- 🟢 Green (Score 1-3): LOW - Optional mitigation
|
||||
|
||||
### Scoring Examples
|
||||
|
||||
**Score 9 (Critical):**
|
||||
```
|
||||
Feature: Payment processing
|
||||
Probability: 3 (complex third-party integration)
|
||||
Impact: 3 (broken payments = lost revenue)
|
||||
Score: 3 × 3 = 9
|
||||
|
||||
Action: Extensive testing required
|
||||
- E2E tests for all payment flows
|
||||
- API tests for all payment scenarios
|
||||
- Error handling for all failure modes
|
||||
- Security testing for payment data
|
||||
- Load testing for high traffic
|
||||
- Monitoring and alerts
|
||||
```
|
||||
|
||||
**Score 1 (Low):**
|
||||
```
|
||||
Feature: Change profile theme color
|
||||
Probability: 1 (simple UI toggle)
|
||||
Impact: 1 (cosmetic only)
|
||||
Score: 1 × 1 = 1
|
||||
|
||||
Action: Minimal testing
|
||||
- One E2E smoke test
|
||||
- Skip edge cases
|
||||
- No API tests needed
|
||||
```
|
||||
|
||||
**Score 6 (Medium-High):**
|
||||
```
|
||||
Feature: User profile editing
|
||||
Probability: 2 (moderate complexity)
|
||||
Impact: 3 (users can't update info)
|
||||
Score: 2 × 3 = 6
|
||||
|
||||
Action: Focused testing
|
||||
- E2E test for happy path
|
||||
- API tests for CRUD operations
|
||||
- Validation testing
|
||||
- Skip low-value edge cases
|
||||
```
|
||||
|
||||
## How It Works in TEA
|
||||
|
||||
### 1. Risk Categories
|
||||
|
||||
TEA assesses risk across 6 categories:
|
||||
|
||||
**TECH** - Technical debt, architecture fragility
|
||||
```
|
||||
Example: Migrating from REST to GraphQL
|
||||
Probability: 3 (major architectural change)
|
||||
Impact: 3 (affects all API consumers)
|
||||
Score: 9 - Extensive integration testing required
|
||||
```
|
||||
|
||||
**SEC** - Security vulnerabilities
|
||||
```
|
||||
Example: Adding OAuth integration
|
||||
Probability: 2 (third-party dependency)
|
||||
Impact: 3 (auth breach = data exposure)
|
||||
Score: 6 - Security testing mandatory
|
||||
```
|
||||
|
||||
**PERF** - Performance degradation
|
||||
```
|
||||
Example: Adding real-time notifications
|
||||
Probability: 2 (WebSocket complexity)
|
||||
Impact: 2 (slower experience)
|
||||
Score: 4 - Load testing recommended
|
||||
```
|
||||
|
||||
**DATA** - Data integrity, corruption
|
||||
```
|
||||
Example: Database migration
|
||||
Probability: 2 (schema changes)
|
||||
Impact: 3 (data loss unacceptable)
|
||||
Score: 6 - Data validation tests required
|
||||
```
|
||||
|
||||
**BUS** - Business logic errors
|
||||
```
|
||||
Example: Discount calculation
|
||||
Probability: 2 (business rules complex)
|
||||
Impact: 3 (wrong prices = revenue loss)
|
||||
Score: 6 - Business logic tests mandatory
|
||||
```
|
||||
|
||||
**OPS** - Operational issues
|
||||
```
|
||||
Example: Logging system update
|
||||
Probability: 1 (straightforward)
|
||||
Impact: 2 (debugging harder without logs)
|
||||
Score: 2 - Basic smoke test sufficient
|
||||
```
|
||||
|
||||
### 2. Test Priorities (P0-P3)
|
||||
|
||||
Risk scores inform test priorities (but aren't the only factor):
|
||||
|
||||
**P0 - Critical Path**
|
||||
- **Risk Scores:** Typically 6-9 (high risk)
|
||||
- **Other Factors:** Revenue impact, security-critical, regulatory compliance, frequent usage
|
||||
- **Coverage Target:** 100%
|
||||
- **Test Levels:** E2E + API
|
||||
- **Example:** Login, checkout, payment processing
|
||||
|
||||
**P1 - High Value**
|
||||
- **Risk Scores:** Typically 4-6 (medium-high risk)
|
||||
- **Other Factors:** Core user journeys, complex logic, integration points
|
||||
- **Coverage Target:** 90%
|
||||
- **Test Levels:** API + selective E2E
|
||||
- **Example:** Profile editing, search, filters
|
||||
|
||||
**P2 - Medium Value**
|
||||
- **Risk Scores:** Typically 2-4 (medium risk)
|
||||
- **Other Factors:** Secondary features, admin functionality, reporting
|
||||
- **Coverage Target:** 50%
|
||||
- **Test Levels:** API happy path only
|
||||
- **Example:** Export features, advanced settings
|
||||
|
||||
**P3 - Low Value**
|
||||
- **Risk Scores:** Typically 1-2 (low risk)
|
||||
- **Other Factors:** Rarely used, nice-to-have, cosmetic
|
||||
- **Coverage Target:** 20% (smoke test)
|
||||
- **Test Levels:** E2E smoke test only
|
||||
- **Example:** Theme customization, experimental features
|
||||
|
||||
**Note:** Priorities consider risk scores plus business context (usage frequency, user impact, etc.). See [Test Priorities Matrix](/docs/tea/reference/knowledge-base.md#quality-standards) for complete criteria.
|
||||
|
||||
### 3. Mitigation Plans
|
||||
|
||||
**Scores ≥6 require documented mitigation:**
|
||||
|
||||
```markdown
|
||||
## Risk Mitigation
|
||||
|
||||
**Risk:** Payment integration failure (Score: 9)
|
||||
|
||||
**Mitigation Plan:**
|
||||
- Create comprehensive test suite (20+ tests)
|
||||
- Add payment sandbox environment
|
||||
- Implement retry logic with idempotency
|
||||
- Add monitoring and alerts
|
||||
- Document rollback procedure
|
||||
|
||||
**Owner:** Backend team lead
|
||||
**Deadline:** Before production deployment
|
||||
**Status:** In progress
|
||||
```
|
||||
|
||||
**Gate Rules:**
|
||||
- **Score = 9** (Critical): Mandatory FAIL - blocks release without mitigation
|
||||
- **Score 6-8** (High): Requires mitigation plan, becomes CONCERNS if incomplete
|
||||
- **Score 4-5** (Medium): Mitigation recommended but not required
|
||||
- **Score 1-3** (Low): No mitigation needed
|
||||
|
||||
## Comparison: Traditional vs Risk-Based
|
||||
|
||||
### Traditional Approach
|
||||
|
||||
```typescript
|
||||
// Test everything equally
|
||||
describe('User profile', () => {
|
||||
test('should display name');
|
||||
test('should display email');
|
||||
test('should display phone');
|
||||
test('should display address');
|
||||
test('should display bio');
|
||||
test('should display avatar');
|
||||
test('should display join date');
|
||||
test('should display last login');
|
||||
test('should display theme preference');
|
||||
test('should display language preference');
|
||||
// 10 tests for profile display (all equal priority)
|
||||
});
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Same effort for critical (name) vs trivial (theme)
|
||||
- No guidance on what matters
|
||||
- Wastes time on low-value tests
|
||||
|
||||
### Risk-Based Approach
|
||||
|
||||
```typescript
|
||||
// Test based on risk
|
||||
|
||||
describe('User profile - Critical (P0)', () => {
|
||||
test('should display name and email'); // Score: 9 (identity critical)
|
||||
test('should allow editing name and email');
|
||||
test('should validate email format');
|
||||
test('should prevent unauthorized edits');
|
||||
// 4 focused tests on high-risk areas
|
||||
});
|
||||
|
||||
describe('User profile - High Value (P1)', () => {
|
||||
test('should upload avatar'); // Score: 6 (users care about this)
|
||||
test('should update bio');
|
||||
// 2 tests for high-value features
|
||||
});
|
||||
|
||||
// P2: Theme preference - single smoke test
|
||||
// P3: Last login display - skip (read-only, low value)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- 6 focused tests vs 10 unfocused tests
|
||||
- Effort matches business impact
|
||||
- Clear priorities guide development
|
||||
- No wasted effort on trivial features
|
||||
|
||||
## When to Use Risk-Based Testing
|
||||
|
||||
### Always Use For:
|
||||
|
||||
**Enterprise projects:**
|
||||
- High stakes (revenue, compliance, security)
|
||||
- Many features competing for test effort
|
||||
- Need objective prioritization
|
||||
|
||||
**Large codebases:**
|
||||
- Can't test everything exhaustively
|
||||
- Need to focus limited QA resources
|
||||
- Want data-driven decisions
|
||||
|
||||
**Regulated industries:**
|
||||
- Must justify testing decisions
|
||||
- Auditors want risk assessments
|
||||
- Compliance requires evidence
|
||||
|
||||
### Consider Skipping For:
|
||||
|
||||
**Tiny projects:**
|
||||
- 5 features total
|
||||
- Can test everything thoroughly
|
||||
- Risk scoring is overhead
|
||||
|
||||
**Prototypes:**
|
||||
- Throw-away code
|
||||
- Speed over quality
|
||||
- Learning experiments
|
||||
|
||||
## Real-World Example
|
||||
|
||||
### Scenario: E-Commerce Checkout Redesign
|
||||
|
||||
**Feature:** Redesigning checkout flow from 5 steps to 3 steps
|
||||
|
||||
**Risk Assessment:**
|
||||
|
||||
| Component | Probability | Impact | Score | Priority | Testing |
|
||||
|-----------|-------------|--------|-------|----------|---------|
|
||||
| **Payment processing** | 3 | 3 | 9 | P0 | 15 E2E + 20 API tests |
|
||||
| **Order validation** | 2 | 3 | 6 | P1 | 5 E2E + 10 API tests |
|
||||
| **Shipping calculation** | 2 | 2 | 4 | P1 | 3 E2E + 8 API tests |
|
||||
| **Promo code validation** | 2 | 2 | 4 | P1 | 2 E2E + 5 API tests |
|
||||
| **Gift message** | 1 | 1 | 1 | P3 | 1 E2E smoke test |
|
||||
|
||||
**Test Budget:** 40 hours
|
||||
|
||||
**Allocation:**
|
||||
- Payment (Score 9): 20 hours (50%)
|
||||
- Order validation (Score 6): 8 hours (20%)
|
||||
- Shipping (Score 4): 6 hours (15%)
|
||||
- Promo codes (Score 4): 4 hours (10%)
|
||||
- Gift message (Score 1): 2 hours (5%)
|
||||
|
||||
**Result:** 50% of effort on highest-risk feature (payment), proportional allocation for others.
|
||||
|
||||
### Without Risk-Based Testing:
|
||||
|
||||
**Equal allocation:** 8 hours per component = wasted effort on gift message, under-testing payment.
|
||||
|
||||
**Result:** Payment bugs slip through (critical), perfect testing of gift message (trivial).
|
||||
|
||||
## Mitigation Strategies by Risk Level
|
||||
|
||||
### Score 9: Mandatory Mitigation (Blocks Release)
|
||||
|
||||
```markdown
|
||||
**Gate Impact:** FAIL - Cannot deploy without mitigation
|
||||
|
||||
**Actions:**
|
||||
- Comprehensive test suite (E2E, API, security)
|
||||
- Multiple test environments (dev, staging, prod-mirror)
|
||||
- Load testing and performance validation
|
||||
- Security audit and penetration testing
|
||||
- Monitoring and alerting
|
||||
- Rollback plan documented
|
||||
- On-call rotation assigned
|
||||
|
||||
**Cannot deploy until score is mitigated below 9.**
|
||||
```
|
||||
|
||||
### Score 6-8: Required Mitigation (Gate: CONCERNS)
|
||||
|
||||
```markdown
|
||||
**Gate Impact:** CONCERNS - Can deploy with documented mitigation plan
|
||||
|
||||
**Actions:**
|
||||
- Targeted test suite (happy path + critical errors)
|
||||
- Test environment setup
|
||||
- Monitoring plan
|
||||
- Document mitigation and owners
|
||||
|
||||
**Can deploy with approved mitigation plan.**
|
||||
```
|
||||
|
||||
### Score 4-5: Recommended Mitigation
|
||||
|
||||
```markdown
|
||||
**Gate Impact:** Advisory - Does not affect gate decision
|
||||
|
||||
**Actions:**
|
||||
- Basic test coverage
|
||||
- Standard monitoring
|
||||
- Document known limitations
|
||||
|
||||
**Can deploy, mitigation recommended but not required.**
|
||||
```
|
||||
|
||||
### Score 1-3: Optional Mitigation
|
||||
|
||||
```markdown
|
||||
**Gate Impact:** None
|
||||
|
||||
**Actions:**
|
||||
- Smoke test if desired
|
||||
- Feature flag for easy disable (optional)
|
||||
|
||||
**Can deploy without mitigation.**
|
||||
```
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
For detailed risk governance patterns, see the knowledge base:
|
||||
- [Knowledge Base Index - Risk & Gates](/docs/tea/reference/knowledge-base.md)
|
||||
- [TEA Command Reference - `test-design`](/docs/tea/reference/commands.md#test-design)
|
||||
|
||||
### Risk Scoring Matrix
|
||||
|
||||
TEA uses this framework in `test-design`:
|
||||
|
||||
```
|
||||
Impact
|
||||
1 2 3
|
||||
┌────┬────┬────┐
|
||||
1 │ 1 │ 2 │ 3 │ Low risk
|
||||
P 2 │ 2 │ 4 │ 6 │ Medium risk
|
||||
r 3 │ 3 │ 6 │ 9 │ High risk
|
||||
o └────┴────┴────┘
|
||||
b Low Med High
|
||||
```
|
||||
|
||||
### Gate Decision Rules
|
||||
|
||||
| Score | Mitigation Required | Gate Impact |
|
||||
|-------|-------------------|-------------|
|
||||
| **9** | Mandatory, blocks release | FAIL if no mitigation |
|
||||
| **6-8** | Required, documented plan | CONCERNS if incomplete |
|
||||
| **4-5** | Recommended | Advisory only |
|
||||
| **1-3** | Optional | No impact |
|
||||
|
||||
#### Gate Decision Flow
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
||||
flowchart TD
|
||||
Start([Risk Assessment]) --> Score{Risk Score?}
|
||||
|
||||
Score -->|Score = 9| Critical[CRITICAL RISK<br/>Score: 9]
|
||||
Score -->|Score 6-8| High[HIGH RISK<br/>Score: 6-8]
|
||||
Score -->|Score 4-5| Medium[MEDIUM RISK<br/>Score: 4-5]
|
||||
Score -->|Score 1-3| Low[LOW RISK<br/>Score: 1-3]
|
||||
|
||||
Critical --> HasMit9{Mitigation<br/>Plan?}
|
||||
HasMit9 -->|Yes| Concerns9[CONCERNS ⚠️<br/>Can deploy with plan]
|
||||
HasMit9 -->|No| Fail[FAIL ❌<br/>Blocks release]
|
||||
|
||||
High --> HasMit6{Mitigation<br/>Plan?}
|
||||
HasMit6 -->|Yes| Pass6[PASS ✅<br/>or CONCERNS ⚠️]
|
||||
HasMit6 -->|No| Concerns6[CONCERNS ⚠️<br/>Document plan needed]
|
||||
|
||||
Medium --> Advisory[Advisory Only<br/>No gate impact]
|
||||
Low --> NoAction[No Action<br/>Proceed]
|
||||
|
||||
style Critical fill:#f44336,stroke:#b71c1c,stroke-width:3px,color:#fff
|
||||
style Fail fill:#d32f2f,stroke:#b71c1c,stroke-width:3px,color:#fff
|
||||
style High fill:#ff9800,stroke:#e65100,stroke-width:2px,color:#000
|
||||
style Concerns9 fill:#ffc107,stroke:#f57f17,stroke-width:2px,color:#000
|
||||
style Concerns6 fill:#ffc107,stroke:#f57f17,stroke-width:2px,color:#000
|
||||
style Pass6 fill:#4caf50,stroke:#1b5e20,stroke-width:2px,color:#fff
|
||||
style Medium fill:#fff9c4,stroke:#f57f17,stroke-width:1px,color:#000
|
||||
style Low fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
style Advisory fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
style NoAction fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px,color:#000
|
||||
```
|
||||
|
||||
## Common Misconceptions
|
||||
|
||||
### "Risk-based = Less Testing"
|
||||
|
||||
**Wrong:** Risk-based testing often means MORE testing where it matters.
|
||||
|
||||
**Example:**
|
||||
- Traditional: 50 tests spread equally
|
||||
- Risk-based: 70 tests focused on P0/P1 (more total, better allocated)
|
||||
|
||||
### "Low Priority = Skip Testing"
|
||||
|
||||
**Wrong:** P3 still gets smoke tests.
|
||||
|
||||
**Correct:**
|
||||
- P3: Smoke test (feature works at all)
|
||||
- P2: Happy path (feature works correctly)
|
||||
- P1: Happy path + errors
|
||||
- P0: Comprehensive (all scenarios)
|
||||
|
||||
### "Risk Scores Are Permanent"
|
||||
|
||||
**Wrong:** Risk changes over time.
|
||||
|
||||
**Correct:**
|
||||
- Initial launch: Payment is Score 9 (untested integration)
|
||||
- After 6 months: Payment is Score 6 (proven in production)
|
||||
- Re-assess risk quarterly
|
||||
|
||||
## Related Concepts
|
||||
|
||||
**Core TEA Concepts:**
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Quality complements risk assessment
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - When risk-based testing matters most
|
||||
- [Knowledge Base System](/docs/tea/explanation/knowledge-base-system.md) - How risk patterns are loaded
|
||||
|
||||
**Technical Patterns:**
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Building risk-appropriate test infrastructure
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Quality patterns for high-risk features
|
||||
|
||||
**Overview:**
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Risk assessment in TEA lifecycle
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - Design philosophy
|
||||
|
||||
## Practical Guides
|
||||
|
||||
**Workflow Guides:**
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Apply risk scoring
|
||||
- [How to Run Trace](/docs/tea/how-to/workflows/run-trace.md) - Gate decisions based on risk
|
||||
- [How to Run NFR Assessment](/docs/tea/how-to/workflows/run-nfr-assess.md) - NFR risk assessment
|
||||
|
||||
**Use-Case Guides:**
|
||||
- [Running TEA for Enterprise](/docs/tea/how-to/brownfield/use-tea-for-enterprise.md) - Enterprise risk management
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - `test-design`, `nfr-assess`, `trace`
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Risk governance fragments
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - Risk-based testing term
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,410 +0,0 @@
|
||||
---
|
||||
title: "Test Architect (TEA) Overview"
|
||||
description: Understanding the Test Architect (TEA) agent and its role in BMad Method
|
||||
---
|
||||
|
||||
|
||||
The Test Architect (TEA) is a specialized agent focused on quality strategy, test automation, and release gates in BMad Method projects.
|
||||
|
||||
:::tip[Design Philosophy]
|
||||
TEA was built to solve AI-generated tests that rot in review. For the problem statement and design principles, see [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md). For setup, see [Setup Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md).
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
- **Persona:** Murat, Master Test Architect and Quality Advisor focused on risk-based testing, fixture architecture, ATDD, and CI/CD governance.
|
||||
- **Mission:** Deliver actionable quality strategies, automation coverage, and gate decisions that scale with project complexity and compliance demands.
|
||||
- **Use When:** BMad Method or Enterprise track projects, integration risk is non-trivial, brownfield regression risk exists, or compliance/NFR evidence is required. (Quick Flow projects typically don't require TEA)
|
||||
|
||||
## Choose Your TEA Engagement Model
|
||||
|
||||
BMad does not mandate TEA. There are five valid ways to use it (or skip it). Pick one intentionally.
|
||||
|
||||
1. **No TEA**
|
||||
- Skip all TEA workflows. Use your existing team testing approach.
|
||||
|
||||
2. **TEA Solo (Standalone)**
|
||||
- Use TEA on a non-BMad project. Bring your own requirements, acceptance criteria, and environments.
|
||||
- Typical sequence: `test-design` (system or epic) -> `atdd` and/or `automate` -> optional `test-review` -> `trace` for coverage and gate decisions.
|
||||
- Run `framework` or `ci` only if you want TEA to scaffold the harness or pipeline; they work best after you decide the stack/architecture.
|
||||
|
||||
**TEA Lite (Beginner Approach):**
|
||||
- Simplest way to use TEA - just use `automate` to test existing features.
|
||||
- Perfect for learning TEA fundamentals in 30 minutes.
|
||||
- See [TEA Lite Quickstart Tutorial](/docs/tea/tutorials/tea-lite-quickstart.md).
|
||||
|
||||
3. **Integrated: Greenfield - BMad Method (Simple/Standard Work)**
|
||||
- Phase 3: system-level `test-design`, then `framework` and `ci`.
|
||||
- Phase 4: per-epic `test-design`, optional `atdd`, then `automate` and optional `test-review`.
|
||||
- Gate (Phase 2): `trace`.
|
||||
|
||||
4. **Integrated: Brownfield - BMad Method or Enterprise (Simple or Complex)**
|
||||
- Phase 2: baseline `trace`.
|
||||
- Phase 3: system-level `test-design`, then `framework` and `ci`.
|
||||
- Phase 4: per-epic `test-design` focused on regression and integration risks.
|
||||
- Gate (Phase 2): `trace`; `nfr-assess` (if not done earlier).
|
||||
- For brownfield BMad Method, follow the same flow with `nfr-assess` optional.
|
||||
|
||||
5. **Integrated: Greenfield - Enterprise Method (Enterprise/Compliance Work)**
|
||||
- Phase 2: `nfr-assess`.
|
||||
- Phase 3: system-level `test-design`, then `framework` and `ci`.
|
||||
- Phase 4: per-epic `test-design`, plus `atdd`/`automate`/`test-review`.
|
||||
- Gate (Phase 2): `trace`; archive artifacts as needed.
|
||||
|
||||
If you are unsure, default to the integrated path for your track and adjust later.
|
||||
|
||||
## TEA Command Catalog
|
||||
|
||||
| Command | Primary Outputs | Notes | With Playwright MCP Enhancements |
|
||||
| -------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `framework` | Playwright/Cypress scaffold, `.env.example`, `.nvmrc`, sample specs | Use when no production-ready harness exists | - |
|
||||
| `ci` | CI workflow, selective test scripts, secrets checklist | Platform-aware (GitHub Actions default) | - |
|
||||
| `test-design` | Combined risk assessment, mitigation plan, and coverage strategy | Risk scoring + optional exploratory mode | **+ Exploratory**: Interactive UI discovery with browser automation (uncover actual functionality) |
|
||||
| `atdd` | Failing acceptance tests + implementation checklist | TDD red phase + optional recording mode | **+ Recording**: UI selectors verified with live browser; API tests benefit from trace analysis |
|
||||
| `automate` | Prioritized specs, fixtures, README/script updates, DoD summary | Optional healing/recording, avoid duplicate coverage | **+ Healing**: Visual debugging + trace analysis for test fixes; **+ Recording**: Verified selectors (UI) + network inspection (API) |
|
||||
| `test-review` | Test quality review report with 0-100 score, violations, fixes | Reviews tests against knowledge base patterns | - |
|
||||
| `nfr-assess` | NFR assessment report with actions | Focus on security/performance/reliability | - |
|
||||
| `trace` | Phase 1: Coverage matrix, recommendations. Phase 2: Gate decision (PASS/CONCERNS/FAIL/WAIVED) | Two-phase workflow: traceability + gate decision | - |
|
||||
|
||||
## TEA Workflow Lifecycle
|
||||
|
||||
**Phase Numbering Note:** BMad uses a 4-phase methodology with optional Phase 1 and a documentation prerequisite:
|
||||
|
||||
- **Documentation** (Optional for brownfield): Prerequisite using `document-project`
|
||||
- **Phase 1** (Optional): Discovery/Analysis (`brainstorm`, `research`, `product-brief`)
|
||||
- **Phase 2** (Required): Planning (`prd` creates PRD with FRs/NFRs)
|
||||
- **Phase 3** (Track-dependent): Solutioning (`architecture` → `test-design` (system-level) → `create-epics-and-stories` → TEA: `framework`, `ci` → `implementation-readiness`)
|
||||
- **Phase 4** (Required): Implementation (`sprint-planning` → per-epic: `test-design` → per-story: dev workflows)
|
||||
|
||||
TEA integrates into the BMad development lifecycle during Solutioning (Phase 3) and Implementation (Phase 4):
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#fff','primaryTextColor':'#000','primaryBorderColor':'#000','lineColor':'#000','secondaryColor':'#fff','tertiaryColor':'#fff','fontSize':'16px','fontFamily':'arial'}}}%%
|
||||
graph TB
|
||||
subgraph Phase2["<b>Phase 2: PLANNING</b>"]
|
||||
PM["<b>PM: prd (creates PRD with FRs/NFRs)</b>"]
|
||||
PlanNote["<b>Business requirements phase</b>"]
|
||||
NFR2["<b>TEA: nfr-assess (optional, enterprise)</b>"]
|
||||
PM -.-> NFR2
|
||||
NFR2 -.-> PlanNote
|
||||
PM -.-> PlanNote
|
||||
end
|
||||
|
||||
subgraph Phase3["<b>Phase 3: SOLUTIONING</b>"]
|
||||
Architecture["<b>Architect: architecture</b>"]
|
||||
EpicsStories["<b>PM/Architect: create-epics-and-stories</b>"]
|
||||
TestDesignSys["<b>TEA: test-design (system-level)</b>"]
|
||||
Framework["<b>TEA: framework (optional if needed)</b>"]
|
||||
CI["<b>TEA: ci (optional if needed)</b>"]
|
||||
GateCheck["<b>Architect: implementation-readiness</b>"]
|
||||
Architecture --> EpicsStories
|
||||
Architecture --> TestDesignSys
|
||||
TestDesignSys --> Framework
|
||||
EpicsStories --> Framework
|
||||
Framework --> CI
|
||||
CI --> GateCheck
|
||||
Phase3Note["<b>Epics created AFTER architecture,</b><br/><b>then system-level test design and test infrastructure setup</b>"]
|
||||
EpicsStories -.-> Phase3Note
|
||||
end
|
||||
|
||||
subgraph Phase4["<b>Phase 4: IMPLEMENTATION - Per Epic Cycle</b>"]
|
||||
SprintPlan["<b>SM: sprint-planning</b>"]
|
||||
TestDesign["<b>TEA: test-design (per epic)</b>"]
|
||||
CreateStory["<b>SM: create-story</b>"]
|
||||
ATDD["<b>TEA: atdd (optional, before dev)</b>"]
|
||||
DevImpl["<b>DEV: implements story</b>"]
|
||||
Automate["<b>TEA: automate</b>"]
|
||||
TestReview1["<b>TEA: test-review (optional)</b>"]
|
||||
Trace1["<b>TEA: trace (refresh coverage)</b>"]
|
||||
|
||||
SprintPlan --> TestDesign
|
||||
TestDesign --> CreateStory
|
||||
CreateStory --> ATDD
|
||||
ATDD --> DevImpl
|
||||
DevImpl --> Automate
|
||||
Automate --> TestReview1
|
||||
TestReview1 --> Trace1
|
||||
Trace1 -.->|next story| CreateStory
|
||||
TestDesignNote["<b>Test design: 'How do I test THIS epic?'</b><br/>Creates test-design-epic-N.md per epic"]
|
||||
TestDesign -.-> TestDesignNote
|
||||
end
|
||||
|
||||
subgraph Gate["<b>EPIC/RELEASE GATE</b>"]
|
||||
NFR["<b>TEA: nfr-assess (if not done earlier)</b>"]
|
||||
TestReview2["<b>TEA: test-review (final audit, optional)</b>"]
|
||||
TraceGate["<b>TEA: trace - Phase 2: Gate</b>"]
|
||||
GateDecision{"<b>Gate Decision</b>"}
|
||||
|
||||
NFR --> TestReview2
|
||||
TestReview2 --> TraceGate
|
||||
TraceGate --> GateDecision
|
||||
GateDecision -->|PASS| Pass["<b>PASS ✅</b>"]
|
||||
GateDecision -->|CONCERNS| Concerns["<b>CONCERNS ⚠️</b>"]
|
||||
GateDecision -->|FAIL| Fail["<b>FAIL ❌</b>"]
|
||||
GateDecision -->|WAIVED| Waived["<b>WAIVED ⏭️</b>"]
|
||||
end
|
||||
|
||||
Phase2 --> Phase3
|
||||
Phase3 --> Phase4
|
||||
Phase4 --> Gate
|
||||
|
||||
style Phase2 fill:#bbdefb,stroke:#0d47a1,stroke-width:3px,color:#000
|
||||
style Phase3 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#000
|
||||
style Phase4 fill:#e1bee7,stroke:#4a148c,stroke-width:3px,color:#000
|
||||
style Gate fill:#ffe082,stroke:#f57c00,stroke-width:3px,color:#000
|
||||
style Pass fill:#4caf50,stroke:#1b5e20,stroke-width:3px,color:#000
|
||||
style Concerns fill:#ffc107,stroke:#f57f17,stroke-width:3px,color:#000
|
||||
style Fail fill:#f44336,stroke:#b71c1c,stroke-width:3px,color:#000
|
||||
style Waived fill:#9c27b0,stroke:#4a148c,stroke-width:3px,color:#000
|
||||
```
|
||||
|
||||
**TEA workflows:** `framework` and `ci` run once in Phase 3 after architecture. `test-design` is **dual-mode**:
|
||||
|
||||
- **System-level (Phase 3):** Run immediately after architecture/ADR drafting to produce TWO documents: `test-design-architecture.md` (for Architecture/Dev teams: testability gaps, ASRs, NFR requirements) + `test-design-qa.md` (for QA team: test execution recipe, coverage plan, Sprint 0 setup). Feeds the implementation-readiness gate.
|
||||
- **Epic-level (Phase 4):** Run per-epic to produce `test-design-epic-N.md` (risk, priorities, coverage plan).
|
||||
|
||||
The Quick Flow track skips Phases 1 and 3.
|
||||
BMad Method and Enterprise use all phases based on project needs.
|
||||
When an ADR or architecture draft is produced, run `test-design` in **system-level** mode before the implementation-readiness gate. This ensures the ADR has an attached testability review and ADR → test mapping. Keep the test-design updated if ADRs change.
|
||||
|
||||
## Why TEA Is Different from Other BMM Agents
|
||||
|
||||
TEA spans multiple phases (Phase 3, Phase 4, and the release gate). Most BMM agents operate in a single phase. That multi-phase role is paired with a dedicated testing knowledge base so standards stay consistent across projects.
|
||||
|
||||
### TEA's 8 Workflows Across Phases
|
||||
|
||||
| Phase | TEA Workflows | Frequency | Purpose |
|
||||
| ----------- | --------------------------------------------------------- | ---------------- | ------------------------------------------------------- |
|
||||
| **Phase 2** | (none) | - | Planning phase - PM defines requirements |
|
||||
| **Phase 3** | `test-design` (system-level), `framework`, `ci` | Once per project | System testability review and test infrastructure setup |
|
||||
| **Phase 4** | `test-design`, `atdd`, `automate`, `test-review`, `trace` | Per epic/story | Test planning per epic, then per-story testing |
|
||||
| **Release** | `nfr-assess`, `trace` (Phase 2: gate) | Per epic/release | Go/no-go decision |
|
||||
|
||||
**Note**: `trace` is a two-phase workflow: Phase 1 (traceability) + Phase 2 (gate decision). This reduces cognitive load while maintaining natural workflow.
|
||||
|
||||
### Why TEA Requires Its Own Knowledge Base
|
||||
|
||||
TEA uniquely requires:
|
||||
|
||||
- **Extensive domain knowledge**: Test patterns, CI/CD, fixtures, and quality practices
|
||||
- **Cross-cutting concerns**: Standards that apply across all BMad projects (not just PRDs or stories)
|
||||
- **Optional integrations**: Playwright-utils and MCP enhancements
|
||||
|
||||
This architecture lets TEA maintain consistent, production-ready testing patterns while operating across multiple phases.
|
||||
|
||||
## Track Cheat Sheets (Condensed)
|
||||
|
||||
These cheat sheets map TEA workflows to the **BMad Method and Enterprise tracks** across the **4-Phase Methodology** (Phase 1: Analysis, Phase 2: Planning, Phase 3: Solutioning, Phase 4: Implementation).
|
||||
|
||||
**Note:** The Quick Flow track typically doesn't require TEA (covered in Overview). These cheat sheets focus on BMad Method and Enterprise tracks where TEA adds value.
|
||||
|
||||
**Legend for Track Deltas:**
|
||||
|
||||
- ➕ = New workflow or phase added (doesn't exist in baseline)
|
||||
- 🔄 = Modified focus (same workflow, different emphasis or purpose)
|
||||
- 📦 = Additional output or archival requirement
|
||||
|
||||
### Greenfield - BMad Method (Simple/Standard Work)
|
||||
|
||||
**Planning Track:** BMad Method (PRD + Architecture)
|
||||
**Use Case:** New projects with standard complexity
|
||||
|
||||
| Workflow Stage | Test Architect | Dev / Team | Outputs |
|
||||
| -------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| **Phase 1**: Discovery | - | Analyst `product-brief` (optional) | `product-brief.md` |
|
||||
| **Phase 2**: Planning | - | PM `prd` (creates PRD with FRs/NFRs) | PRD with functional/non-functional requirements |
|
||||
| **Phase 3**: Solutioning | Run `framework`, `ci` AFTER architecture and epic creation | Architect `architecture`, `create-epics-and-stories`, `implementation-readiness` | Architecture, epics/stories, test scaffold, CI pipeline |
|
||||
| **Phase 4**: Sprint Start | - | SM `sprint-planning` | Sprint status file with all epics and stories |
|
||||
| **Phase 4**: Epic Planning | Run `test-design` for THIS epic (per-epic test plan) | Review epic scope | `test-design-epic-N.md` with risk assessment and test plan |
|
||||
| **Phase 4**: Story Dev | (Optional) `atdd` before dev, then `automate` after | SM `create-story`, DEV implements | Tests, story implementation |
|
||||
| **Phase 4**: Story Review | Execute `test-review` (optional), re-run `trace` | Address recommendations, update code/tests | Quality report, refreshed coverage matrix |
|
||||
| **Phase 4**: Release Gate | (Optional) `test-review` for final audit, Run `trace` (Phase 2) | Confirm Definition of Done, share release notes | Quality audit, Gate YAML + release summary |
|
||||
|
||||
**Key notes:**
|
||||
- Run `framework` and `ci` once in Phase 3 after architecture.
|
||||
- Run `test-design` per epic in Phase 4; use `atdd` before dev when helpful.
|
||||
- Use `trace` for gate decisions; `test-review` is an optional audit.
|
||||
|
||||
### Brownfield - BMad Method or Enterprise (Simple or Complex)
|
||||
|
||||
**Planning Tracks:** BMad Method or Enterprise Method
|
||||
**Use Case:** Existing codebases: simple additions (BMad Method) or complex enterprise requirements (Enterprise Method)
|
||||
|
||||
**🔄 Brownfield Deltas from Greenfield:**
|
||||
|
||||
- ➕ Documentation (Prerequisite) - Document existing codebase if undocumented
|
||||
- ➕ Phase 2: `trace` - Baseline existing test coverage before planning
|
||||
- 🔄 Phase 4: `test-design` - Focus on regression hotspots and brownfield risks
|
||||
- 🔄 Phase 4: Story Review - May include `nfr-assess` if not done earlier
|
||||
|
||||
| Workflow Stage | Test Architect | Dev / Team | Outputs |
|
||||
| --------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **Documentation**: Prerequisite ➕ | - | Analyst `document-project` (if undocumented) | Comprehensive project documentation |
|
||||
| **Phase 1**: Discovery | - | Analyst/PM/Architect rerun planning workflows | Updated planning artifacts in `{output_folder}` |
|
||||
| **Phase 2**: Planning | Run ➕ `trace` (baseline coverage) | PM `prd` (creates PRD with FRs/NFRs) | PRD with FRs/NFRs, ➕ coverage baseline |
|
||||
| **Phase 3**: Solutioning | Run `framework`, `ci` AFTER architecture and epic creation | Architect `architecture`, `create-epics-and-stories`, `implementation-readiness` | Architecture, epics/stories, test framework, CI pipeline |
|
||||
| **Phase 4**: Sprint Start | - | SM `sprint-planning` | Sprint status file with all epics and stories |
|
||||
| **Phase 4**: Epic Planning | Run `test-design` for THIS epic 🔄 (regression hotspots) | Review epic scope and brownfield risks | `test-design-epic-N.md` with brownfield risk assessment and mitigation |
|
||||
| **Phase 4**: Story Dev | (Optional) `atdd` before dev, then `automate` after | SM `create-story`, DEV implements | Tests, story implementation |
|
||||
| **Phase 4**: Story Review | Apply `test-review` (optional), re-run `trace`, ➕ `nfr-assess` if needed | Resolve gaps, update docs/tests | Quality report, refreshed coverage matrix, NFR report |
|
||||
| **Phase 4**: Release Gate | (Optional) `test-review` for final audit, Run `trace` (Phase 2) | Capture sign-offs, share release notes | Quality audit, Gate YAML + release summary |
|
||||
|
||||
**Key notes:**
|
||||
- Start with `trace` in Phase 2 to baseline coverage.
|
||||
- Focus `test-design` on regression hotspots and integration risk.
|
||||
- Run `nfr-assess` before the gate if it wasn't done earlier.
|
||||
|
||||
### Greenfield - Enterprise Method (Enterprise/Compliance Work)
|
||||
|
||||
**Planning Track:** Enterprise Method (BMad Method + extended security/devops/test strategies)
|
||||
**Use Case:** New enterprise projects with compliance, security, or complex regulatory requirements
|
||||
|
||||
**🏢 Enterprise Deltas from BMad Method:**
|
||||
|
||||
- ➕ Phase 1: `research` - Domain and compliance research (recommended)
|
||||
- ➕ Phase 2: `nfr-assess` - Capture NFR requirements early (security/performance/reliability)
|
||||
- 🔄 Phase 4: `test-design` - Enterprise focus (compliance, security architecture alignment)
|
||||
- 📦 Release Gate - Archive artifacts and compliance evidence for audits
|
||||
|
||||
| Workflow Stage | Test Architect | Dev / Team | Outputs |
|
||||
| -------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| **Phase 1**: Discovery | - | Analyst ➕ `research`, `product-brief` | Domain research, compliance analysis, product brief |
|
||||
| **Phase 2**: Planning | Run ➕ `nfr-assess` | PM `prd` (creates PRD with FRs/NFRs), UX `create-ux-design` | Enterprise PRD with FRs/NFRs, UX design, ➕ NFR documentation |
|
||||
| **Phase 3**: Solutioning | Run `framework`, `ci` AFTER architecture and epic creation | Architect `architecture`, `create-epics-and-stories`, `implementation-readiness` | Architecture, epics/stories, test framework, CI pipeline |
|
||||
| **Phase 4**: Sprint Start | - | SM `sprint-planning` | Sprint plan with all epics |
|
||||
| **Phase 4**: Epic Planning | Run `test-design` for THIS epic 🔄 (compliance focus) | Review epic scope and compliance requirements | `test-design-epic-N.md` with security/performance/compliance focus |
|
||||
| **Phase 4**: Story Dev | (Optional) `atdd`, `automate`, `test-review`, `trace` per story | SM `create-story`, DEV implements | Tests, fixtures, quality reports, coverage matrices |
|
||||
| **Phase 4**: Release Gate | Final `test-review` audit, Run `trace` (Phase 2), 📦 archive artifacts | Capture sign-offs, 📦 compliance evidence | Quality audit, updated assessments, gate YAML, 📦 audit trail |
|
||||
|
||||
**Key notes:**
|
||||
- Run `nfr-assess` early in Phase 2.
|
||||
- `test-design` emphasizes compliance, security, and performance alignment.
|
||||
- Archive artifacts at the release gate for audits.
|
||||
|
||||
**Related how-to guides:**
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md)
|
||||
- [How to Set Up a Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md)
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md)
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md)
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md)
|
||||
- [How to Set Up CI Pipeline](/docs/tea/how-to/workflows/setup-ci.md)
|
||||
- [How to Run NFR Assessment](/docs/tea/how-to/workflows/run-nfr-assess.md)
|
||||
- [How to Run Trace](/docs/tea/how-to/workflows/run-trace.md)
|
||||
|
||||
## Deep Dive Concepts
|
||||
|
||||
Want to understand TEA principles and patterns in depth?
|
||||
|
||||
**Core Principles:**
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Probability × impact scoring, P0-P3 priorities
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Definition of Done, determinism, isolation
|
||||
- [Knowledge Base System](/docs/tea/explanation/knowledge-base-system.md) - Context engineering with tea-index.csv
|
||||
|
||||
**Technical Patterns:**
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Pure function → fixture → composition
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Eliminating flakiness with intercept-before-navigate
|
||||
|
||||
**Engagement & Strategy:**
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - TEA Lite, TEA Solo, TEA Integrated (5 models explained)
|
||||
|
||||
**Philosophy:**
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - **Start here to understand WHY TEA exists** - The problem with AI-generated tests and TEA's three-part solution
|
||||
|
||||
## Optional Integrations
|
||||
|
||||
### Playwright Utils (`@seontechnologies/playwright-utils`)
|
||||
|
||||
Production-ready fixtures and utilities that enhance TEA workflows.
|
||||
|
||||
- Install: `npm install -D @seontechnologies/playwright-utils`
|
||||
> Note: Playwright Utils is enabled via the installer. Only set `tea_use_playwright_utils` in `_bmad/bmm/config.yaml` if you need to override the installer choice.
|
||||
- Impacts: `framework`, `atdd`, `automate`, `test-review`, `ci`
|
||||
- Utilities include: api-request, auth-session, network-recorder, intercept-network-call, recurse, log, file-utils, burn-in, network-error-monitor, fixtures-composition
|
||||
|
||||
### Playwright MCP Enhancements
|
||||
|
||||
Live browser verification for test design and automation.
|
||||
|
||||
**Two Playwright MCP servers** (actively maintained, continuously updated):
|
||||
|
||||
- `playwright` - Browser automation (`npx @playwright/mcp@latest`)
|
||||
- `playwright-test` - Test runner with failure analysis (`npx playwright run-test-mcp-server`)
|
||||
|
||||
**Configuration example**:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
},
|
||||
"playwright-test": {
|
||||
"command": "npx",
|
||||
"args": ["playwright", "run-test-mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Helps `test-design` validate actual UI behavior.
|
||||
- Helps `atdd` and `automate` verify selectors against the live DOM.
|
||||
- Enhances healing with `browser_snapshot`, console, network, and locator tools.
|
||||
|
||||
**To disable**: set `tea_use_mcp_enhancements: false` in `_bmad/bmm/config.yaml` or remove MCPs from IDE config.
|
||||
|
||||
---
|
||||
|
||||
## Complete TEA Documentation Navigation
|
||||
|
||||
### Start Here
|
||||
|
||||
**New to TEA? Start with the tutorial:**
|
||||
- [TEA Lite Quickstart Tutorial](/docs/tea/tutorials/tea-lite-quickstart.md) - 30-minute beginner guide using TodoMVC
|
||||
|
||||
### Workflow Guides (Task-Oriented)
|
||||
|
||||
**All 8 TEA workflows with step-by-step instructions:**
|
||||
1. [How to Set Up a Test Framework with TEA](/docs/tea/how-to/workflows/setup-test-framework.md) - Scaffold Playwright or Cypress
|
||||
2. [How to Set Up CI Pipeline with TEA](/docs/tea/how-to/workflows/setup-ci.md) - Configure CI/CD with selective testing
|
||||
3. [How to Run Test Design with TEA](/docs/tea/how-to/workflows/run-test-design.md) - Risk-based test planning (system or epic)
|
||||
4. [How to Run ATDD with TEA](/docs/tea/how-to/workflows/run-atdd.md) - Generate failing tests before implementation
|
||||
5. [How to Run Automate with TEA](/docs/tea/how-to/workflows/run-automate.md) - Expand test coverage after implementation
|
||||
6. [How to Run Test Review with TEA](/docs/tea/how-to/workflows/run-test-review.md) - Audit test quality (0-100 scoring)
|
||||
7. [How to Run NFR Assessment with TEA](/docs/tea/how-to/workflows/run-nfr-assess.md) - Validate non-functional requirements
|
||||
8. [How to Run Trace with TEA](/docs/tea/how-to/workflows/run-trace.md) - Coverage traceability + gate decisions
|
||||
|
||||
### Customization & Integration
|
||||
|
||||
**Optional enhancements to TEA workflows:**
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - Production-ready fixtures and 9 utilities
|
||||
- [Enable TEA MCP Enhancements](/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md) - Live browser verification, visual debugging
|
||||
|
||||
### Use-Case Guides
|
||||
|
||||
**Specialized guidance for specific contexts:**
|
||||
- [Using TEA with Existing Tests (Brownfield)](/docs/tea/how-to/brownfield/use-tea-with-existing-tests.md) - Incremental improvement, regression hotspots, baseline coverage
|
||||
- [Running TEA for Enterprise](/docs/tea/how-to/brownfield/use-tea-for-enterprise.md) - Compliance, NFR assessment, audit trails, SOC 2/HIPAA
|
||||
|
||||
### Concept Deep Dives (Understanding-Oriented)
|
||||
|
||||
**Understand the principles and patterns:**
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Probability × impact scoring, P0-P3 priorities, mitigation strategies
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Definition of Done, determinism, isolation, explicit assertions
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Pure function → fixture → composition pattern
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Intercept-before-navigate, eliminating flakiness
|
||||
- [Knowledge Base System](/docs/tea/explanation/knowledge-base-system.md) - Context engineering with tea-index.csv, 33 fragments
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - TEA Lite, TEA Solo, TEA Integrated (5 models explained)
|
||||
|
||||
### Philosophy & Design
|
||||
|
||||
**Why TEA exists and how it works:**
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - **Start here to understand WHY** - The problem with AI-generated tests and TEA's three-part solution
|
||||
|
||||
### Reference (Quick Lookup)
|
||||
|
||||
**Factual information for quick reference:**
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - All 8 workflows: inputs, outputs, phases, frequency
|
||||
- [TEA Configuration Reference](/docs/tea/reference/configuration.md) - Config options, file locations, setup examples
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - 33 fragments categorized and explained
|
||||
- [Glossary - TEA Section](/docs/tea/glossary/index.md#test-architect-tea-concepts) - 20 TEA-specific terms defined
|
||||
@@ -1,907 +0,0 @@
|
||||
---
|
||||
title: "Test Quality Standards Explained"
|
||||
description: Understanding TEA's Definition of Done for deterministic, isolated, and maintainable tests
|
||||
---
|
||||
|
||||
# Test Quality Standards Explained
|
||||
|
||||
Test quality standards define what makes a test "good" in TEA. These aren't suggestions - they're the Definition of Done that prevents tests from rotting in review.
|
||||
|
||||
## Overview
|
||||
|
||||
**TEA's Quality Principles:**
|
||||
- **Deterministic** - Same result every run
|
||||
- **Isolated** - No dependencies on other tests
|
||||
- **Explicit** - Assertions visible in test body
|
||||
- **Focused** - Single responsibility, appropriate size
|
||||
- **Fast** - Execute in reasonable time
|
||||
|
||||
**Why these matter:** Tests that violate these principles create maintenance burden, slow down development, and lose team trust.
|
||||
|
||||
## The Problem
|
||||
|
||||
### Tests That Rot in Review
|
||||
|
||||
```typescript
|
||||
// ❌ The anti-pattern: This test will rot
|
||||
test('user can do stuff', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForTimeout(5000); // Non-deterministic
|
||||
|
||||
if (await page.locator('.banner').isVisible()) { // Conditional
|
||||
await page.click('.dismiss');
|
||||
}
|
||||
|
||||
try { // Try-catch for flow control
|
||||
await page.click('#load-more');
|
||||
} catch (e) {
|
||||
// Silently continue
|
||||
}
|
||||
|
||||
// ... 300 more lines of test logic
|
||||
// ... no clear assertions
|
||||
});
|
||||
```
|
||||
|
||||
**What's wrong:**
|
||||
- **Hard wait** - Flaky, wastes time
|
||||
- **Conditional** - Non-deterministic behavior
|
||||
- **Try-catch** - Hides failures
|
||||
- **Too large** - Hard to maintain
|
||||
- **Vague name** - Unclear purpose
|
||||
- **No explicit assertions** - What's being tested?
|
||||
|
||||
**Result:** PR review comments: "This test is flaky, please fix" → never merged → test deleted → coverage lost
|
||||
|
||||
### AI-Generated Tests Without Standards
|
||||
|
||||
AI-generated tests without quality guardrails:
|
||||
|
||||
```typescript
|
||||
// AI generates 50 tests like this:
|
||||
test('test1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForTimeout(3000);
|
||||
// ... flaky, vague, redundant
|
||||
});
|
||||
|
||||
test('test2', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForTimeout(3000);
|
||||
// ... duplicates test1
|
||||
});
|
||||
|
||||
// ... 48 more similar tests
|
||||
```
|
||||
|
||||
**Result:** 50 tests, 80% redundant, 90% flaky, 0% trusted by team - low-quality outputs that create maintenance burden.
|
||||
|
||||
## The Solution: TEA's Quality Standards
|
||||
|
||||
### 1. Determinism (No Flakiness)
|
||||
|
||||
**Rule:** Test produces same result every run.
|
||||
|
||||
**Requirements:**
|
||||
- ❌ No hard waits (`waitForTimeout`)
|
||||
- ❌ No conditionals for flow control (`if/else`)
|
||||
- ❌ No try-catch for flow control
|
||||
- ✅ Use network-first patterns (wait for responses)
|
||||
- ✅ Use explicit waits (waitForSelector, waitForResponse)
|
||||
|
||||
**Bad Example:**
|
||||
```typescript
|
||||
test('flaky test', async ({ page }) => {
|
||||
await page.click('button');
|
||||
await page.waitForTimeout(2000); // ❌ Might be too short
|
||||
|
||||
if (await page.locator('.modal').isVisible()) { // ❌ Non-deterministic
|
||||
await page.click('.dismiss');
|
||||
}
|
||||
|
||||
try { // ❌ Silently handles errors
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
} catch (e) {
|
||||
// Test passes even if assertion fails!
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Good Example (Vanilla Playwright):**
|
||||
```typescript
|
||||
test('deterministic test', async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/submit') && resp.ok()
|
||||
);
|
||||
|
||||
await page.click('button');
|
||||
await responsePromise; // ✅ Wait for actual response
|
||||
|
||||
// Modal should ALWAYS show (make it deterministic)
|
||||
await expect(page.locator('.modal')).toBeVisible();
|
||||
await page.click('.dismiss');
|
||||
|
||||
// Explicit assertion (fails if not visible)
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils (Even Cleaner):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('deterministic test', async ({ page, interceptNetworkCall }) => {
|
||||
const submitCall = interceptNetworkCall({
|
||||
method: 'POST',
|
||||
url: '**/api/submit'
|
||||
});
|
||||
|
||||
await page.click('button');
|
||||
|
||||
// Wait for actual response (automatic JSON parsing)
|
||||
const { status, responseJson } = await submitCall;
|
||||
expect(status).toBe(200);
|
||||
|
||||
// Modal should ALWAYS show (make it deterministic)
|
||||
await expect(page.locator('.modal')).toBeVisible();
|
||||
await page.click('.dismiss');
|
||||
|
||||
// Explicit assertion (fails if not visible)
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Why both work:**
|
||||
- Waits for actual event (network response)
|
||||
- No conditionals (behavior is deterministic)
|
||||
- Assertions fail loudly (no silent failures)
|
||||
- Same result every run (deterministic)
|
||||
|
||||
**Playwright Utils additional benefits:**
|
||||
- Automatic JSON parsing
|
||||
- `{ status, responseJson }` structure (can validate response data)
|
||||
- No manual `await response.json()`
|
||||
|
||||
### 2. Isolation (No Dependencies)
|
||||
|
||||
**Rule:** Test runs independently, no shared state.
|
||||
|
||||
**Requirements:**
|
||||
- ✅ Self-cleaning (cleanup after test)
|
||||
- ✅ No global state dependencies
|
||||
- ✅ Can run in parallel
|
||||
- ✅ Can run in any order
|
||||
- ✅ Use unique test data
|
||||
|
||||
**Bad Example:**
|
||||
```typescript
|
||||
// ❌ Tests depend on execution order
|
||||
let userId: string; // Shared global state
|
||||
|
||||
test('create user', async ({ apiRequest }) => {
|
||||
const { body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users',
|
||||
body: { email: 'test@example.com' } (hard-coded)
|
||||
});
|
||||
userId = body.id; // Store in global
|
||||
});
|
||||
|
||||
test('update user', async ({ apiRequest }) => {
|
||||
// Depends on previous test setting userId
|
||||
await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: `/api/users/${userId}`,
|
||||
body: { name: 'Updated' }
|
||||
});
|
||||
// No cleanup - leaves user in database
|
||||
});
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Tests must run in order (can't parallelize)
|
||||
- Second test fails if first skipped (`.only`)
|
||||
- Hard-coded data causes conflicts
|
||||
- No cleanup (database fills with test data)
|
||||
|
||||
**Good Example (Vanilla Playwright):**
|
||||
```typescript
|
||||
test('should update user profile', async ({ request }) => {
|
||||
// Create unique test data
|
||||
const testEmail = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Setup: Create user
|
||||
const createResp = await request.post('/api/users', {
|
||||
data: { email: testEmail, name: 'Original' }
|
||||
});
|
||||
const user = await createResp.json();
|
||||
|
||||
// Test: Update user
|
||||
const updateResp = await request.patch(`/api/users/${user.id}`, {
|
||||
data: { name: 'Updated' }
|
||||
});
|
||||
const updated = await updateResp.json();
|
||||
|
||||
expect(updated.name).toBe('Updated');
|
||||
|
||||
// Cleanup: Delete user
|
||||
await request.delete(`/api/users/${user.id}`);
|
||||
});
|
||||
```
|
||||
|
||||
**Even Better (With Playwright Utils):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
test('should update user profile', async ({ apiRequest }) => {
|
||||
// Dynamic unique test data
|
||||
const testEmail = faker.internet.email();
|
||||
|
||||
// Setup: Create user
|
||||
const { status: createStatus, body: user } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users',
|
||||
body: { email: testEmail, name: faker.person.fullName() }
|
||||
});
|
||||
|
||||
expect(createStatus).toBe(201);
|
||||
|
||||
// Test: Update user
|
||||
const { status, body: updated } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: `/api/users/${user.id}`,
|
||||
body: { name: 'Updated Name' }
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
|
||||
// Cleanup: Delete user
|
||||
await apiRequest({
|
||||
method: 'DELETE',
|
||||
path: `/api/users/${user.id}`
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Playwright Utils Benefits:**
|
||||
- `{ status, body }` destructuring (cleaner than `response.status()` + `await response.json()`)
|
||||
- No manual `await response.json()`
|
||||
- Automatic retry for 5xx errors
|
||||
- Optional schema validation with `.validateSchema()`
|
||||
|
||||
**Why it works:**
|
||||
- No global state
|
||||
- Unique test data (no conflicts)
|
||||
- Self-cleaning (deletes user)
|
||||
- Can run in parallel
|
||||
- Can run in any order
|
||||
|
||||
### 3. Explicit Assertions (No Hidden Validation)
|
||||
|
||||
**Rule:** Assertions visible in test body, not abstracted.
|
||||
|
||||
**Requirements:**
|
||||
- ✅ Assertions in test code (not helper functions)
|
||||
- ✅ Specific assertions (not generic `toBeTruthy`)
|
||||
- ✅ Meaningful expectations (test actual behavior)
|
||||
|
||||
**Bad Example:**
|
||||
```typescript
|
||||
// ❌ Assertions hidden in helper
|
||||
async function verifyProfilePage(page: Page) {
|
||||
// Assertions buried in helper (not visible in test)
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(page.locator('.email')).toContainText('@');
|
||||
await expect(page.locator('.name')).not.toBeEmpty();
|
||||
}
|
||||
|
||||
test('profile page', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await verifyProfilePage(page); // What's being verified?
|
||||
});
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Can't see what's tested (need to read helper)
|
||||
- Hard to debug failures (which assertion failed?)
|
||||
- Reduces test readability
|
||||
- Hides important validation
|
||||
|
||||
**Good Example:**
|
||||
```typescript
|
||||
// ✅ Assertions explicit in test
|
||||
test('should display profile with correct data', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
|
||||
// Explicit assertions - clear what's tested
|
||||
await expect(page.locator('h1')).toContainText('Test User');
|
||||
await expect(page.locator('.email')).toContainText('test@example.com');
|
||||
await expect(page.locator('.bio')).toContainText('Software Engineer');
|
||||
await expect(page.locator('img[alt="Avatar"]')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Why it works:**
|
||||
- See what's tested at a glance
|
||||
- Debug failures easily (know which assertion failed)
|
||||
- Test is self-documenting
|
||||
- No hidden behavior
|
||||
|
||||
**Exception:** Use helper for setup/cleanup, not assertions.
|
||||
|
||||
### 4. Focused Tests (Appropriate Size)
|
||||
|
||||
**Rule:** Test has single responsibility, reasonable size.
|
||||
|
||||
**Requirements:**
|
||||
- ✅ Test size < 300 lines
|
||||
- ✅ Single responsibility (test one thing well)
|
||||
- ✅ Clear describe/test names
|
||||
- ✅ Appropriate scope (not too granular, not too broad)
|
||||
|
||||
**Bad Example:**
|
||||
```typescript
|
||||
// ❌ 500-line test testing everything
|
||||
test('complete user flow', async ({ page }) => {
|
||||
// Registration (50 lines)
|
||||
await page.goto('/register');
|
||||
await page.fill('#email', 'test@example.com');
|
||||
// ... 48 more lines
|
||||
|
||||
// Profile setup (100 lines)
|
||||
await page.goto('/profile');
|
||||
// ... 98 more lines
|
||||
|
||||
// Settings configuration (150 lines)
|
||||
await page.goto('/settings');
|
||||
// ... 148 more lines
|
||||
|
||||
// Data export (200 lines)
|
||||
await page.goto('/export');
|
||||
// ... 198 more lines
|
||||
|
||||
// Total: 500 lines, testing 4 different features
|
||||
});
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Failure in line 50 prevents testing lines 51-500
|
||||
- Hard to understand (what's being tested?)
|
||||
- Slow to execute (testing too much)
|
||||
- Hard to debug (which feature failed?)
|
||||
|
||||
**Good Example:**
|
||||
```typescript
|
||||
// ✅ Focused tests - one responsibility each
|
||||
|
||||
test('should register new user', async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
await page.fill('#email', 'test@example.com');
|
||||
await page.fill('#password', 'password123');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page).toHaveURL('/welcome');
|
||||
await expect(page.locator('h1')).toContainText('Welcome');
|
||||
});
|
||||
|
||||
test('should configure user profile', async ({ page, authSession }) => {
|
||||
await authSession.login({ email: 'test@example.com', password: 'pass' });
|
||||
await page.goto('/profile');
|
||||
|
||||
await page.fill('#name', 'Test User');
|
||||
await page.fill('#bio', 'Software Engineer');
|
||||
await page.click('button:has-text("Save")');
|
||||
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
});
|
||||
|
||||
// ... separate tests for settings, export (each < 50 lines)
|
||||
```
|
||||
|
||||
**Why it works:**
|
||||
- Each test has one responsibility
|
||||
- Failure is easy to diagnose
|
||||
- Can run tests independently
|
||||
- Test names describe exactly what's tested
|
||||
|
||||
### 5. Fast Execution (Performance Budget)
|
||||
|
||||
**Rule:** Individual test executes in < 1.5 minutes.
|
||||
|
||||
**Requirements:**
|
||||
- ✅ Test execution < 90 seconds
|
||||
- ✅ Efficient selectors (getByRole > XPath)
|
||||
- ✅ Minimal redundant actions
|
||||
- ✅ Parallel execution enabled
|
||||
|
||||
**Bad Example:**
|
||||
```typescript
|
||||
// ❌ Slow test (3+ minutes)
|
||||
test('slow test', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForTimeout(10000); // 10s wasted
|
||||
|
||||
// Navigate through 10 pages (2 minutes)
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
await page.click(`a[href="/page-${i}"]`);
|
||||
await page.waitForTimeout(5000); // 5s per page = 50s wasted
|
||||
}
|
||||
|
||||
// Complex XPath selector (slow)
|
||||
await page.locator('//div[@class="container"]/section[3]/div[2]/p').click();
|
||||
|
||||
// More waiting
|
||||
await page.waitForTimeout(30000); // 30s wasted
|
||||
|
||||
await expect(page.locator('.result')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Total time:** 3+ minutes (95 seconds wasted on hard waits)
|
||||
|
||||
**Good Example (Vanilla Playwright):**
|
||||
```typescript
|
||||
// ✅ Fast test (< 10 seconds)
|
||||
test('fast test', async ({ page }) => {
|
||||
// Set up response wait
|
||||
const apiPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/result') && resp.ok()
|
||||
);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Direct navigation (skip intermediate pages)
|
||||
await page.goto('/page-10');
|
||||
|
||||
// Efficient selector
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Wait for actual response (fast when API is fast)
|
||||
await apiPromise;
|
||||
|
||||
await expect(page.locator('.result')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('fast test', async ({ page, interceptNetworkCall }) => {
|
||||
// Set up interception
|
||||
const resultCall = interceptNetworkCall({
|
||||
method: 'GET',
|
||||
url: '**/api/result'
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Direct navigation (skip intermediate pages)
|
||||
await page.goto('/page-10');
|
||||
|
||||
// Efficient selector
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Wait for actual response (automatic JSON parsing)
|
||||
const { status, responseJson } = await resultCall;
|
||||
|
||||
expect(status).toBe(200);
|
||||
await expect(page.locator('.result')).toBeVisible();
|
||||
|
||||
// Can also validate response data if needed
|
||||
// expect(responseJson.data).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
**Total time:** < 10 seconds (no wasted waits)
|
||||
|
||||
**Both examples achieve:**
|
||||
- No hard waits (wait for actual events)
|
||||
- Direct navigation (skip unnecessary steps)
|
||||
- Efficient selectors (getByRole)
|
||||
- Fast execution
|
||||
|
||||
**Playwright Utils bonus:**
|
||||
- Can validate API response data easily
|
||||
- Automatic JSON parsing
|
||||
- Cleaner API
|
||||
|
||||
## TEA's Quality Scoring
|
||||
|
||||
TEA reviews tests against these standards in `test-review`:
|
||||
|
||||
### Scoring Categories (100 points total)
|
||||
|
||||
**Determinism (35 points):**
|
||||
- No hard waits: 10 points
|
||||
- No conditionals: 10 points
|
||||
- No try-catch flow: 10 points
|
||||
- Network-first patterns: 5 points
|
||||
|
||||
**Isolation (25 points):**
|
||||
- Self-cleaning: 15 points
|
||||
- No global state: 5 points
|
||||
- Parallel-safe: 5 points
|
||||
|
||||
**Assertions (20 points):**
|
||||
- Explicit in test body: 10 points
|
||||
- Specific and meaningful: 10 points
|
||||
|
||||
**Structure (10 points):**
|
||||
- Test size < 300 lines: 5 points
|
||||
- Clear naming: 5 points
|
||||
|
||||
**Performance (10 points):**
|
||||
- Execution time < 1.5 min: 10 points
|
||||
|
||||
#### Quality Scoring Breakdown
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
||||
pie title Test Quality Score (100 points)
|
||||
"Determinism" : 35
|
||||
"Isolation" : 25
|
||||
"Assertions" : 20
|
||||
"Structure" : 10
|
||||
"Performance" : 10
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'13px'}}}%%
|
||||
flowchart LR
|
||||
subgraph Det[Determinism - 35 pts]
|
||||
D1[No hard waits<br/>10 pts]
|
||||
D2[No conditionals<br/>10 pts]
|
||||
D3[No try-catch flow<br/>10 pts]
|
||||
D4[Network-first<br/>5 pts]
|
||||
end
|
||||
|
||||
subgraph Iso[Isolation - 25 pts]
|
||||
I1[Self-cleaning<br/>15 pts]
|
||||
I2[No global state<br/>5 pts]
|
||||
I3[Parallel-safe<br/>5 pts]
|
||||
end
|
||||
|
||||
subgraph Assrt[Assertions - 20 pts]
|
||||
A1[Explicit in body<br/>10 pts]
|
||||
A2[Specific/meaningful<br/>10 pts]
|
||||
end
|
||||
|
||||
subgraph Struct[Structure - 10 pts]
|
||||
S1[Size < 300 lines<br/>5 pts]
|
||||
S2[Clear naming<br/>5 pts]
|
||||
end
|
||||
|
||||
subgraph Perf[Performance - 10 pts]
|
||||
P1[Time < 1.5 min<br/>10 pts]
|
||||
end
|
||||
|
||||
Det --> Total([Total: 100 points])
|
||||
Iso --> Total
|
||||
Assrt --> Total
|
||||
Struct --> Total
|
||||
Perf --> Total
|
||||
|
||||
style Det fill:#ffebee,stroke:#c62828,stroke-width:2px
|
||||
style Iso fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
|
||||
style Assrt fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
|
||||
style Struct fill:#fff9c4,stroke:#f57f17,stroke-width:2px
|
||||
style Perf fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
|
||||
style Total fill:#fff,stroke:#000,stroke-width:3px
|
||||
```
|
||||
|
||||
### Score Interpretation
|
||||
|
||||
| Score | Interpretation | Action |
|
||||
| ---------- | -------------- | -------------------------------------- |
|
||||
| **90-100** | Excellent | Production-ready, minimal changes |
|
||||
| **80-89** | Good | Minor improvements recommended |
|
||||
| **70-79** | Acceptable | Address recommendations before release |
|
||||
| **60-69** | Needs Work | Fix critical issues |
|
||||
| **< 60** | Critical | Significant refactoring needed |
|
||||
|
||||
## Comparison: Good vs Bad Tests
|
||||
|
||||
### Example: User Login
|
||||
|
||||
**Bad Test (Score: 45/100):**
|
||||
```typescript
|
||||
test('login test', async ({ page }) => { // Vague name
|
||||
await page.goto('/login');
|
||||
await page.waitForTimeout(3000); // -10 (hard wait)
|
||||
|
||||
await page.fill('[name="email"]', 'test@example.com');
|
||||
await page.fill('[name="password"]', 'password');
|
||||
|
||||
if (await page.locator('.remember-me').isVisible()) { // -10 (conditional)
|
||||
await page.click('.remember-me');
|
||||
}
|
||||
|
||||
await page.click('button');
|
||||
|
||||
try { // -10 (try-catch flow)
|
||||
await page.waitForURL('/dashboard', { timeout: 5000 });
|
||||
} catch (e) {
|
||||
// Ignore navigation failure
|
||||
}
|
||||
|
||||
// No assertions! -10
|
||||
// No cleanup! -10
|
||||
});
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- Determinism: 5/35 (hard wait, conditional, try-catch)
|
||||
- Isolation: 10/25 (no cleanup)
|
||||
- Assertions: 0/20 (no assertions!)
|
||||
- Structure: 15/10 (okay)
|
||||
- Performance: 5/10 (slow)
|
||||
- **Total: 45/100**
|
||||
|
||||
**Good Test (Score: 95/100):**
|
||||
```typescript
|
||||
test('should login with valid credentials and redirect to dashboard', async ({ page, authSession }) => {
|
||||
// Use fixture for deterministic auth
|
||||
const loginPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/auth/login') && resp.ok()
|
||||
);
|
||||
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
// Wait for actual API response
|
||||
const response = await loginPromise;
|
||||
const { token } = await response.json();
|
||||
|
||||
// Explicit assertions
|
||||
expect(token).toBeDefined();
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
await expect(page.getByText('Welcome back')).toBeVisible();
|
||||
|
||||
// Cleanup handled by authSession fixture
|
||||
});
|
||||
```
|
||||
|
||||
**Quality:**
|
||||
- Determinism: 35/35 (network-first, no conditionals)
|
||||
- Isolation: 25/25 (fixture handles cleanup)
|
||||
- Assertions: 20/20 (explicit and specific)
|
||||
- Structure: 10/10 (clear name, focused)
|
||||
- Performance: 5/10 (< 1 min)
|
||||
- **Total: 95/100**
|
||||
|
||||
### Example: API Testing
|
||||
|
||||
**Bad Test (Score: 50/100):**
|
||||
```typescript
|
||||
test('api test', async ({ request }) => {
|
||||
const response = await request.post('/api/users', {
|
||||
data: { email: 'test@example.com' } // Hard-coded (conflicts)
|
||||
});
|
||||
|
||||
if (response.ok()) { // Conditional
|
||||
const user = await response.json();
|
||||
// Weak assertion
|
||||
expect(user).toBeTruthy();
|
||||
}
|
||||
|
||||
// No cleanup - user left in database
|
||||
});
|
||||
```
|
||||
|
||||
**Good Test (Score: 92/100):**
|
||||
```typescript
|
||||
test('should create user with valid data', async ({ apiRequest }) => {
|
||||
// Unique test data
|
||||
const testEmail = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Create user
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users',
|
||||
body: { email: testEmail, name: 'Test User' }
|
||||
});
|
||||
|
||||
// Explicit assertions
|
||||
expect(status).toBe(201);
|
||||
expect(body.id).toBeDefined();
|
||||
expect(body.email).toBe(testEmail);
|
||||
expect(body.name).toBe('Test User');
|
||||
|
||||
// Cleanup
|
||||
await apiRequest({
|
||||
method: 'DELETE',
|
||||
path: `/api/users/${body.id}`
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## How TEA Enforces Standards
|
||||
|
||||
### During Test Generation (`atdd`, `automate`)
|
||||
|
||||
TEA generates tests following standards by default:
|
||||
|
||||
```typescript
|
||||
// TEA-generated test (automatically follows standards)
|
||||
test('should submit contact form', async ({ page }) => {
|
||||
// Network-first pattern (no hard waits)
|
||||
const submitPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/contact') && resp.ok()
|
||||
);
|
||||
|
||||
// Accessible selectors (resilient)
|
||||
await page.getByLabel('Name').fill('Test User');
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await page.getByLabel('Message').fill('Test message');
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
const response = await submitPromise;
|
||||
const result = await response.json();
|
||||
|
||||
// Explicit assertions
|
||||
expect(result.success).toBe(true);
|
||||
await expect(page.getByText('Message sent')).toBeVisible();
|
||||
|
||||
// Size: 15 lines (< 300 ✓)
|
||||
// Execution: ~2 seconds (< 90s ✓)
|
||||
});
|
||||
```
|
||||
|
||||
### During Test Review (`test-review`)
|
||||
|
||||
TEA audits tests and flags violations:
|
||||
|
||||
```markdown
|
||||
## Critical Issues
|
||||
|
||||
### Hard Wait Detected (tests/login.spec.ts:23)
|
||||
**Issue:** `await page.waitForTimeout(3000)`
|
||||
**Score Impact:** -10 (Determinism)
|
||||
**Fix:** Use network-first pattern
|
||||
|
||||
### Conditional Flow Control (tests/profile.spec.ts:45)
|
||||
**Issue:** `if (await page.locator('.banner').isVisible())`
|
||||
**Score Impact:** -10 (Determinism)
|
||||
**Fix:** Make banner presence deterministic
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Extract Fixture (tests/auth.spec.ts)
|
||||
**Issue:** Login code repeated 5 times
|
||||
**Score Impact:** -3 (Structure)
|
||||
**Fix:** Extract to authSession fixture
|
||||
```
|
||||
|
||||
## Definition of Done Checklist
|
||||
|
||||
When is a test "done"?
|
||||
|
||||
**Test Quality DoD:**
|
||||
- [ ] No hard waits (`waitForTimeout`)
|
||||
- [ ] No conditionals for flow control
|
||||
- [ ] No try-catch for flow control
|
||||
- [ ] Network-first patterns used
|
||||
- [ ] Assertions explicit in test body
|
||||
- [ ] Test size < 300 lines
|
||||
- [ ] Clear, descriptive test name
|
||||
- [ ] Self-cleaning (cleanup in afterEach or test)
|
||||
- [ ] Unique test data (no hard-coded values)
|
||||
- [ ] Execution time < 1.5 minutes
|
||||
- [ ] Can run in parallel
|
||||
- [ ] Can run in any order
|
||||
|
||||
**Code Review DoD:**
|
||||
- [ ] Test quality score > 80
|
||||
- [ ] No critical issues from `test-review`
|
||||
- [ ] Follows project patterns (fixtures, selectors)
|
||||
- [ ] Test reviewed by team member
|
||||
|
||||
## Common Quality Issues
|
||||
|
||||
### Issue: "My test needs conditionals for optional elements"
|
||||
|
||||
**Wrong approach:**
|
||||
```typescript
|
||||
if (await page.locator('.banner').isVisible()) {
|
||||
await page.click('.dismiss');
|
||||
}
|
||||
```
|
||||
|
||||
**Right approach - Make it deterministic:**
|
||||
```typescript
|
||||
// Option 1: Always expect banner
|
||||
await expect(page.locator('.banner')).toBeVisible();
|
||||
await page.click('.dismiss');
|
||||
|
||||
// Option 2: Test both scenarios separately
|
||||
test('should show banner for new users', ...);
|
||||
test('should not show banner for returning users', ...);
|
||||
```
|
||||
|
||||
### Issue: "My test needs try-catch for error handling"
|
||||
|
||||
**Wrong approach:**
|
||||
```typescript
|
||||
try {
|
||||
await page.click('#optional-button');
|
||||
} catch (e) {
|
||||
// Silently continue
|
||||
}
|
||||
```
|
||||
|
||||
**Right approach - Make failures explicit:**
|
||||
```typescript
|
||||
// Option 1: Button should exist
|
||||
await page.click('#optional-button'); // Fails loudly if missing
|
||||
|
||||
// Option 2: Button might not exist (test both)
|
||||
test('should work with optional button', async ({ page }) => {
|
||||
const hasButton = await page.locator('#optional-button').count() > 0;
|
||||
if (hasButton) {
|
||||
await page.click('#optional-button');
|
||||
}
|
||||
// But now you're testing optional behavior explicitly
|
||||
});
|
||||
```
|
||||
|
||||
### Issue: "Hard waits are easier than network patterns"
|
||||
|
||||
**Short-term:** Hard waits seem simpler
|
||||
**Long-term:** Flaky tests waste more time than learning network patterns
|
||||
|
||||
**Investment:**
|
||||
- 30 minutes to learn network-first patterns
|
||||
- Prevents hundreds of hours debugging flaky tests
|
||||
- Tests run faster (no wasted waits)
|
||||
- Team trusts test suite
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
For detailed test quality patterns, see:
|
||||
- [Test Quality Fragment](/docs/tea/reference/knowledge-base.md#quality-standards)
|
||||
- [Test Levels Framework Fragment](/docs/tea/reference/knowledge-base.md#quality-standards)
|
||||
- [Complete Knowledge Base Index](/docs/tea/reference/knowledge-base.md)
|
||||
|
||||
## Related Concepts
|
||||
|
||||
**Core TEA Concepts:**
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Quality scales with risk
|
||||
- [Knowledge Base System](/docs/tea/explanation/knowledge-base-system.md) - How standards are enforced
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - Quality in different models
|
||||
|
||||
**Technical Patterns:**
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Determinism explained
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Isolation through fixtures
|
||||
|
||||
**Overview:**
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Quality standards in lifecycle
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - Why quality matters
|
||||
|
||||
## Practical Guides
|
||||
|
||||
**Workflow Guides:**
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Audit against these standards
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Generate quality tests
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Expand with quality
|
||||
|
||||
**Use-Case Guides:**
|
||||
- [Using TEA with Existing Tests](/docs/tea/how-to/brownfield/use-tea-with-existing-tests.md) - Improve legacy quality
|
||||
- [Running TEA for Enterprise](/docs/tea/how-to/brownfield/use-tea-for-enterprise.md) - Enterprise quality thresholds
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - `test-review` command
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Test quality fragment
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - TEA terminology
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,112 +0,0 @@
|
||||
---
|
||||
title: "AI-Generated Testing: Why Most Approaches Fail"
|
||||
description: How Playwright-Utils, TEA workflows, and Playwright MCPs solve AI test quality problems
|
||||
---
|
||||
|
||||
|
||||
AI-generated tests frequently fail in production because they lack systematic quality standards. This document explains the problem and presents a solution combining three components: Playwright-Utils, TEA (Test Architect), and Playwright MCPs.
|
||||
|
||||
:::note[Source]
|
||||
This article is adapted from [The Testing Meta Most Teams Have Not Caught Up To Yet](https://dev.to/muratkeremozcan/the-testing-meta-most-teams-have-not-caught-up-to-yet-5765) by Murat K Ozcan.
|
||||
:::
|
||||
|
||||
## The Problem with AI-Generated Tests
|
||||
|
||||
When teams use AI to generate tests without structure, they often produce what can be called "slop factory" outputs:
|
||||
|
||||
| Issue | Description |
|
||||
|-------|-------------|
|
||||
| Redundant coverage | Multiple tests covering the same functionality |
|
||||
| Incorrect assertions | Tests that pass but don't actually verify behavior |
|
||||
| Flaky tests | Non-deterministic tests that randomly pass or fail |
|
||||
| Unreviewable diffs | Generated code too verbose or inconsistent to review |
|
||||
|
||||
The core problem is that prompt-driven testing paths lean into nondeterminism, which is the exact opposite of what testing exists to protect.
|
||||
|
||||
:::caution[The Paradox]
|
||||
AI excels at generating code quickly, but testing requires precision and consistency. Without guardrails, AI-generated tests amplify the chaos they're meant to prevent.
|
||||
:::
|
||||
|
||||
## The Solution: A Three-Part Stack
|
||||
|
||||
The solution combines three components that work together to enforce quality:
|
||||
|
||||
### Playwright-Utils
|
||||
|
||||
Bridges the gap between Cypress ergonomics and Playwright's capabilities by standardizing commonly reinvented primitives through utility functions.
|
||||
|
||||
| Utility | Purpose |
|
||||
|---------|---------|
|
||||
| api-request | API calls with schema validation |
|
||||
| auth-session | Authentication handling |
|
||||
| intercept-network-call | Network mocking and interception |
|
||||
| recurse | Retry logic and polling |
|
||||
| log | Structured logging |
|
||||
| network-recorder | Record and replay network traffic |
|
||||
| burn-in | Smart test selection for CI |
|
||||
| network-error-monitor | HTTP error detection |
|
||||
| file-utils | CSV/PDF handling |
|
||||
|
||||
These utilities eliminate the need to reinvent authentication, API calls, retries, and logging for every project.
|
||||
|
||||
### TEA (Test Architect Agent)
|
||||
|
||||
A quality operating model packaged as eight executable workflows spanning test design, CI/CD gates, and release readiness. TEA encodes test architecture expertise into repeatable processes.
|
||||
|
||||
| Workflow | Purpose |
|
||||
|----------|---------|
|
||||
| `test-design` | Risk-based test planning per epic |
|
||||
| `framework` | Scaffold production-ready test infrastructure |
|
||||
| `ci` | CI pipeline with selective testing |
|
||||
| `atdd` | Acceptance test-driven development |
|
||||
| `automate` | Prioritized test automation |
|
||||
| `test-review` | Test quality audits (0-100 score) |
|
||||
| `nfr-assess` | Non-functional requirements assessment |
|
||||
| `trace` | Coverage traceability and gate decisions |
|
||||
|
||||
:::tip[Key Insight]
|
||||
TEA doesn't just generate tests—it provides a complete quality operating model with workflows for planning, execution, and release gates.
|
||||
:::
|
||||
|
||||
### Playwright MCPs
|
||||
|
||||
Model Context Protocols enable real-time verification during test generation. Instead of inferring selectors and behavior from documentation, MCPs allow agents to:
|
||||
|
||||
- Run flows and confirm the DOM against the accessibility tree
|
||||
- Validate network responses in real-time
|
||||
- Discover actual functionality through interactive exploration
|
||||
- Verify generated tests against live applications
|
||||
|
||||
## How They Work Together
|
||||
|
||||
The three components form a quality pipeline:
|
||||
|
||||
| Stage | Component | Action |
|
||||
|-------|-----------|--------|
|
||||
| Standards | Playwright-Utils | Provides production-ready patterns and utilities |
|
||||
| Process | TEA Workflows | Enforces systematic test planning and review |
|
||||
| Verification | Playwright MCPs | Validates generated tests against live applications |
|
||||
|
||||
**Before (AI-only):** 20 tests with redundant coverage, incorrect assertions, and flaky behavior.
|
||||
|
||||
**After (Full Stack):** Risk-based selection, verified selectors, validated behavior, reviewable code.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Traditional AI testing approaches fail because they:
|
||||
|
||||
- **Lack quality standards** — No consistent patterns or utilities
|
||||
- **Skip planning** — Jump straight to test generation without risk assessment
|
||||
- **Can't verify** — Generate tests without validating against actual behavior
|
||||
- **Don't review** — No systematic audit of generated test quality
|
||||
|
||||
The three-part stack addresses each gap:
|
||||
|
||||
| Gap | Solution |
|
||||
|-----|----------|
|
||||
| No standards | Playwright-Utils provides production-ready patterns |
|
||||
| No planning | TEA `test-design` creates risk-based test plans |
|
||||
| No verification | Playwright MCPs validate against live applications |
|
||||
| No review | TEA `test-review` audits quality with scoring |
|
||||
|
||||
This approach is sometimes called *context engineering*—loading domain-specific standards into AI context automatically rather than relying on prompts alone. TEA's `tea-index.csv` manifest loads relevant knowledge fragments so the AI doesn't relearn testing patterns each session.
|
||||
@@ -1,159 +0,0 @@
|
||||
---
|
||||
title: "BMad Glossary"
|
||||
---
|
||||
|
||||
Terminology reference for the BMad Method.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Term | Definition |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Agent** | Specialized AI persona with specific expertise (PM, Architect, SM, DEV, TEA) that guides users through workflows and creates deliverables. |
|
||||
| **BMad** | Breakthrough Method of Agile AI-Driven Development — AI-driven agile framework with specialized agents, guided workflows, and scale-adaptive intelligence. |
|
||||
| **BMad Method** | Complete methodology for AI-assisted software development, encompassing planning, architecture, implementation, and quality assurance workflows that adapt to project complexity. |
|
||||
| **BMM** | BMad Method Module — core orchestration system providing comprehensive lifecycle management through specialized agents and workflows. |
|
||||
| **Scale-Adaptive System** | Intelligent workflow orchestration that adjusts planning depth and documentation requirements based on project needs through three planning tracks. |
|
||||
| **Workflow** | Multi-step guided process that orchestrates AI agent activities to produce specific deliverables. Workflows are interactive and adapt to user context. |
|
||||
|
||||
## Scale and Complexity
|
||||
|
||||
| Term | Definition |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **BMad Method Track** | Full product planning track using PRD + Architecture + UX. Best for products, platforms, and complex features. Typical range: 10-50+ stories. |
|
||||
| **Enterprise Method Track** | Extended planning track adding Security Architecture, DevOps Strategy, and Test Strategy. Best for compliance needs and multi-tenant systems. Typical range: 30+ stories. |
|
||||
| **Planning Track** | Methodology path (Quick Flow, BMad Method, or Enterprise) chosen based on planning needs and complexity, not story count alone. |
|
||||
| **Quick Flow Track** | Fast implementation track using tech-spec only. Best for bug fixes, small features, and clear-scope changes. Typical range: 1-15 stories. |
|
||||
|
||||
## Planning Documents
|
||||
|
||||
| Term | Definition |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Architecture Document** | *BMad Method/Enterprise.* System-wide design document defining structure, components, data models, integration patterns, security, and deployment. |
|
||||
| **Epics** | High-level feature groupings containing multiple related stories. Typically 5-15 stories each representing cohesive functionality. |
|
||||
| **Game Brief** | *BMGD.* Document capturing game's core vision, pillars, target audience, and scope. Foundation for the GDD. |
|
||||
| **GDD** | *BMGD.* Game Design Document — comprehensive document detailing all aspects of game design: mechanics, systems, content, and more. |
|
||||
| **PRD** | *BMad Method/Enterprise.* Product Requirements Document containing vision, goals, FRs, NFRs, and success criteria. Focuses on WHAT to build. |
|
||||
| **Product Brief** | *Phase 1.* Optional strategic document capturing product vision, market context, and high-level requirements before detailed planning. |
|
||||
| **Tech-Spec** | *Quick Flow only.* Comprehensive technical plan with problem statement, solution approach, file-level changes, and testing strategy. |
|
||||
|
||||
## Workflow and Phases
|
||||
|
||||
| Term | Definition |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Phase 0: Documentation** | *Brownfield.* Conditional prerequisite phase creating codebase documentation before planning. Only required if existing docs are insufficient. |
|
||||
| **Phase 1: Analysis** | Discovery phase including brainstorming, research, and product brief creation. Optional for Quick Flow, recommended for BMad Method. |
|
||||
| **Phase 2: Planning** | Required phase creating formal requirements. Routes to tech-spec (Quick Flow) or PRD (BMad Method/Enterprise). |
|
||||
| **Phase 3: Solutioning** | *BMad Method/Enterprise.* Architecture design phase including creation, validation, and gate checks. |
|
||||
| **Phase 4: Implementation** | Required sprint-based development through story-by-story iteration using sprint-planning, create-story, dev-story, and code-review workflows. |
|
||||
| **Quick Spec Flow** | Fast-track workflow for Quick Flow projects going straight from idea to tech-spec to implementation. |
|
||||
| **Workflow Init** | Initialization workflow creating bmm-workflow-status.yaml, detecting project type, and determining planning track. |
|
||||
| **Workflow Status** | Universal entry point checking for existing status file, displaying progress, and recommending next action. |
|
||||
|
||||
## Agents and Roles
|
||||
|
||||
| Term | Definition |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Analyst** | Agent that initializes workflows, conducts research, creates product briefs, and tracks progress. Often the entry point for new projects. |
|
||||
| **Architect** | Agent designing system architecture, creating architecture documents, and validating designs. Primary agent for Phase 3. |
|
||||
| **BMad Master** | Meta-level orchestrator from BMad Core facilitating party mode and providing high-level guidance across all modules. |
|
||||
| **DEV** | Developer agent implementing stories, writing code, running tests, and performing code reviews. Primary implementer in Phase 4. |
|
||||
| **Game Architect** | *BMGD.* Agent designing game system architecture and validating game-specific technical designs. |
|
||||
| **Game Designer** | *BMGD.* Agent creating game design documents (GDD) and running game-specific workflows. |
|
||||
| **Party Mode** | Multi-agent collaboration feature where agents discuss challenges together. BMad Master orchestrates, selecting 2-3 relevant agents per message. |
|
||||
| **PM** | Product Manager agent creating PRDs and tech-specs. Primary agent for Phase 2 planning. |
|
||||
| **SM** | Scrum Master agent managing sprints, creating stories, and coordinating implementation. Primary orchestrator for Phase 4. |
|
||||
| **TEA** | Test Architect agent responsible for test strategy, quality gates, and NFR assessment. Integrates throughout all phases. |
|
||||
| **Technical Writer** | Agent specialized in creating technical documentation, diagrams, and maintaining documentation standards. |
|
||||
| **UX Designer** | Agent creating UX design documents, interaction patterns, and visual specifications for UI-heavy projects. |
|
||||
|
||||
## Status and Tracking
|
||||
|
||||
| Term | Definition |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **bmm-workflow-status.yaml** | *Phases 1-3.* Tracking file showing current phase, completed workflows, and next recommended actions. |
|
||||
| **DoD** | Definition of Done — criteria for marking a story complete: implementation done, tests passing, code reviewed, docs updated. |
|
||||
| **Epic Status Progression** | `backlog → in-progress → done` — lifecycle states for epics during implementation. |
|
||||
| **Gate Check** | Validation workflow (implementation-readiness) ensuring PRD, Architecture, and Epics are aligned before Phase 4. |
|
||||
| **Retrospective** | Workflow after each epic capturing learnings and improvements for continuous improvement. |
|
||||
| **sprint-status.yaml** | *Phase 4.* Single source of truth for implementation tracking containing all epics, stories, and their statuses. |
|
||||
| **Story Status Progression** | `backlog → ready-for-dev → in-progress → review → done` — lifecycle states for stories. |
|
||||
|
||||
## Project Types
|
||||
|
||||
| Term | Definition |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Brownfield** | Existing project with established codebase and patterns. Requires understanding existing architecture and planning integration. |
|
||||
| **Convention Detection** | *Quick Flow.* Feature auto-detecting existing code style, naming conventions, and frameworks from brownfield codebases. |
|
||||
| **document-project** | *Brownfield.* Workflow analyzing and documenting existing codebase with three scan levels: quick, deep, exhaustive. |
|
||||
| **Feature Flags** | *Brownfield.* Implementation technique for gradual rollout, easy rollback, and A/B testing of new functionality. |
|
||||
| **Greenfield** | New project starting from scratch with freedom to establish patterns, choose stack, and design from clean slate. |
|
||||
| **Integration Points** | *Brownfield.* Specific locations where new code connects with existing systems. Must be documented in tech-specs. |
|
||||
|
||||
## Implementation Terms
|
||||
|
||||
| Term | Definition |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Context Engineering** | Loading domain-specific standards into AI context automatically via manifests, ensuring consistent outputs regardless of prompt variation. |
|
||||
| **Correct Course** | Workflow for navigating significant changes when implementation is off-track. Analyzes impact and recommends adjustments. |
|
||||
| **Shard / Sharding** | Splitting large planning documents into section-based files for LLM optimization. Phase 4 workflows load only needed sections. |
|
||||
| **Sprint** | Time-boxed period of development work, typically 1-2 weeks. |
|
||||
| **Sprint Planning** | Workflow initializing Phase 4 by creating sprint-status.yaml and extracting epics/stories from planning docs. |
|
||||
| **Story** | Single unit of implementable work with clear acceptance criteria, typically 2-8 hours of effort. Grouped into epics. |
|
||||
| **Story Context** | Implementation guidance embedded in story files during create-story, referencing existing patterns and approaches. |
|
||||
| **Story File** | Markdown file containing story description, acceptance criteria, technical notes, and testing requirements. |
|
||||
| **Track Selection** | Automatic analysis by `bmad-help` suggesting appropriate track based on complexity indicators. User can override. |
|
||||
|
||||
## Game Development Terms
|
||||
|
||||
| Term | Definition |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| **Core Fantasy** | *BMGD.* The emotional experience players seek from your game — what they want to FEEL. |
|
||||
| **Core Loop** | *BMGD.* Fundamental cycle of actions players repeat throughout gameplay. The heart of your game. |
|
||||
| **Design Pillar** | *BMGD.* Core principle guiding all design decisions. Typically 3-5 pillars define a game's identity. |
|
||||
| **Environmental Storytelling** | *BMGD.* Narrative communicated through the game world itself rather than explicit dialogue. |
|
||||
| **Game Type** | *BMGD.* Genre classification determining which specialized GDD sections are included. |
|
||||
| **MDA Framework** | *BMGD.* Mechanics → Dynamics → Aesthetics — framework for analyzing and designing games. |
|
||||
| **Meta-Progression** | *BMGD.* Persistent progression carrying between individual runs or sessions. |
|
||||
| **Metroidvania** | *BMGD.* Genre featuring interconnected world exploration with ability-gated progression. |
|
||||
| **Narrative Complexity** | *BMGD.* How central story is to the game: Critical, Heavy, Moderate, or Light. |
|
||||
| **Permadeath** | *BMGD.* Game mechanic where character death is permanent, typically requiring a new run. |
|
||||
| **Player Agency** | *BMGD.* Degree to which players can make meaningful choices affecting outcomes. |
|
||||
| **Procedural Generation** | *BMGD.* Algorithmic creation of game content (levels, items, characters) rather than hand-crafted. |
|
||||
| **Roguelike** | *BMGD.* Genre featuring procedural generation, permadeath, and run-based progression. |
|
||||
|
||||
## Test Architect (TEA) Concepts
|
||||
|
||||
| Term | Definition |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **ATDD** | Acceptance Test-Driven Development — Generating failing acceptance tests BEFORE implementation (TDD red phase). |
|
||||
| **Burn-in Testing** | Running tests multiple times (typically 5-10 iterations) to detect flakiness and intermittent failures. |
|
||||
| **Component Testing** | Testing UI components in isolation using framework-specific tools (Cypress Component Testing or Vitest + React Testing Library). |
|
||||
| **Coverage Traceability** | Mapping acceptance criteria to implemented tests with classification (FULL/PARTIAL/NONE) to identify gaps and measure completeness. |
|
||||
| **Epic-Level Test Design** | Test planning per epic (Phase 4) focusing on risk assessment, priorities, and coverage strategy for that specific epic. |
|
||||
| **Fixture Architecture** | Pattern of building pure functions first, then wrapping in framework-specific fixtures for testability, reusability, and composition. |
|
||||
| **Gate Decision** | Go/no-go decision for release with four outcomes: PASS ✅ (ready), CONCERNS ⚠️ (proceed with mitigation), FAIL ❌ (blocked), WAIVED ⏭️ (approved despite issues). |
|
||||
| **Knowledge Fragment** | Individual markdown file in TEA's knowledge base covering a specific testing pattern or practice (33 fragments total). |
|
||||
| **MCP Enhancements** | Model Context Protocol servers enabling live browser verification during test generation (exploratory, recording, and healing modes). |
|
||||
| **Network-First Pattern** | Testing pattern that waits for actual network responses instead of fixed timeouts to avoid race conditions and flakiness. |
|
||||
| **NFR Assessment** | Validation of non-functional requirements (security, performance, reliability, maintainability) with evidence-based decisions. |
|
||||
| **Playwright Utils** | Optional package (`@seontechnologies/playwright-utils`) providing production-ready fixtures and utilities for Playwright tests. |
|
||||
| **Risk-Based Testing** | Testing approach where depth scales with business impact using probability × impact scoring (1-9 scale). |
|
||||
| **System-Level Test Design** | Test planning at architecture level (Phase 3) focusing on testability review, ADR mapping, and test infrastructure needs. |
|
||||
| **tea-index.csv** | Manifest file tracking all knowledge fragments, their descriptions, tags, and which workflows load them. |
|
||||
| **TEA Integrated** | Full BMad Method integration with TEA workflows across all phases (Phase 2, 3, 4, and Release Gate). |
|
||||
| **TEA Lite** | Beginner approach using just `automate` to test existing features (simplest way to use TEA). |
|
||||
| **TEA Solo** | Standalone engagement model using TEA without full BMad Method integration (bring your own requirements). |
|
||||
| **Test Priorities** | Classification system for test importance: P0 (critical path), P1 (high value), P2 (medium value), P3 (low value). |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Complete TEA capabilities
|
||||
- [TEA Knowledge Base](/docs/tea/reference/knowledge-base.md) - Fragment index
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - Workflow reference
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Config options
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org)
|
||||
@@ -1,525 +0,0 @@
|
||||
---
|
||||
title: "Running TEA for Enterprise Projects"
|
||||
description: Use TEA with compliance, security, and regulatory requirements in enterprise environments
|
||||
---
|
||||
|
||||
# Running TEA for Enterprise Projects
|
||||
|
||||
Use TEA on enterprise projects with compliance, security, audit, and regulatory requirements. This guide covers NFR assessment, audit trails, and evidence collection.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Enterprise track projects (not Quick Flow or simple BMad Method)
|
||||
- Compliance requirements (SOC 2, HIPAA, GDPR, etc.)
|
||||
- Security-critical applications (finance, healthcare, government)
|
||||
- Audit trail requirements
|
||||
- Strict NFR thresholds (performance, security, reliability)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed (Enterprise track selected)
|
||||
- TEA agent available
|
||||
- Compliance requirements documented
|
||||
- Stakeholders identified (who approves gates)
|
||||
|
||||
## Enterprise-Specific TEA Workflows
|
||||
|
||||
### NFR Assessment (`nfr-assess`)
|
||||
|
||||
**Purpose:** Validate non-functional requirements with evidence.
|
||||
|
||||
**When:** Phase 2 (early) and Release Gate
|
||||
|
||||
**Why Enterprise Needs This:**
|
||||
- Compliance mandates specific thresholds
|
||||
- Audit trails required for certification
|
||||
- Security requirements are non-negotiable
|
||||
- Performance SLAs are contractual
|
||||
|
||||
**Example:**
|
||||
```
|
||||
nfr-assess
|
||||
|
||||
Categories: Security, Performance, Reliability, Maintainability
|
||||
|
||||
Security thresholds:
|
||||
- Zero critical vulnerabilities (required by SOC 2)
|
||||
- All endpoints require authentication
|
||||
- Data encrypted at rest (FIPS 140-2)
|
||||
- Audit logging on all data access
|
||||
|
||||
Evidence:
|
||||
- Security scan: reports/nessus-scan.pdf
|
||||
- Penetration test: reports/pentest-2026-01.pdf
|
||||
- Compliance audit: reports/soc2-evidence.zip
|
||||
```
|
||||
|
||||
**Output:** NFR assessment with PASS/CONCERNS/FAIL for each category.
|
||||
|
||||
### Trace with Audit Evidence (`trace`)
|
||||
|
||||
**Purpose:** Requirements traceability with audit trail.
|
||||
|
||||
**When:** Phase 2 (baseline), Phase 4 (refresh), Release Gate
|
||||
|
||||
**Why Enterprise Needs This:**
|
||||
- Auditors require requirements-to-test mapping
|
||||
- Compliance certifications need traceability
|
||||
- Regulatory bodies want evidence
|
||||
|
||||
**Example:**
|
||||
```
|
||||
trace Phase 1
|
||||
|
||||
Requirements: PRD.md (with compliance requirements)
|
||||
Test location: tests/
|
||||
|
||||
Output: traceability-matrix.md with:
|
||||
- Requirement-to-test mapping
|
||||
- Compliance requirement coverage
|
||||
- Gap prioritization
|
||||
- Recommendations
|
||||
```
|
||||
|
||||
**For Release Gate:**
|
||||
```
|
||||
trace Phase 2
|
||||
|
||||
Generate gate-decision-{gate_type}-{story_id}.md with:
|
||||
- Evidence references
|
||||
- Approver signatures
|
||||
- Compliance checklist
|
||||
- Decision rationale
|
||||
```
|
||||
|
||||
### Test Design with Compliance Focus (`test-design`)
|
||||
|
||||
**Purpose:** Risk assessment with compliance and security focus.
|
||||
|
||||
**When:** Phase 3 (system-level), Phase 4 (epic-level)
|
||||
|
||||
**Why Enterprise Needs This:**
|
||||
- Security architecture alignment required
|
||||
- Compliance requirements must be testable
|
||||
- Performance requirements are contractual
|
||||
|
||||
**Example:**
|
||||
```
|
||||
test-design
|
||||
|
||||
Mode: System-level
|
||||
|
||||
Focus areas:
|
||||
- Security architecture (authentication, authorization, encryption)
|
||||
- Performance requirements (SLA: P99 <200ms)
|
||||
- Compliance (HIPAA PHI handling, audit logging)
|
||||
|
||||
Output: TWO documents (system-level):
|
||||
- `test-design-architecture.md`: Security gaps, compliance requirements, performance SLOs for Architecture team
|
||||
- `test-design-qa.md`: Security testing strategy, compliance test mapping, performance testing plan for QA team
|
||||
- Audit logging validation
|
||||
```
|
||||
|
||||
## Enterprise TEA Lifecycle
|
||||
|
||||
### Phase 1: Discovery (Optional but Recommended)
|
||||
|
||||
**Research compliance requirements:**
|
||||
```
|
||||
Analyst: research
|
||||
|
||||
Topics:
|
||||
- Industry compliance (SOC 2, HIPAA, GDPR)
|
||||
- Security standards (OWASP Top 10)
|
||||
- Performance benchmarks (industry P99)
|
||||
```
|
||||
|
||||
### Phase 2: Planning (Required)
|
||||
|
||||
**1. Define NFRs early:**
|
||||
```
|
||||
PM: prd
|
||||
|
||||
Include in PRD:
|
||||
- Security requirements (authentication, encryption)
|
||||
- Performance SLAs (response time, throughput)
|
||||
- Reliability targets (uptime, RTO, RPO)
|
||||
- Compliance mandates (data retention, audit logs)
|
||||
```
|
||||
|
||||
**2. Assess NFRs:**
|
||||
```
|
||||
TEA: nfr-assess
|
||||
|
||||
Categories: All (Security, Performance, Reliability, Maintainability)
|
||||
|
||||
Output: nfr-assessment.md
|
||||
- NFR requirements documented
|
||||
- Acceptance criteria defined
|
||||
- Test strategy planned
|
||||
```
|
||||
|
||||
**3. Baseline (brownfield only):**
|
||||
```
|
||||
TEA: trace Phase 1
|
||||
|
||||
Establish baseline coverage before new work
|
||||
```
|
||||
|
||||
### Phase 3: Solutioning (Required)
|
||||
|
||||
**1. Architecture with testability review:**
|
||||
```
|
||||
Architect: architecture
|
||||
|
||||
TEA: test-design (system-level)
|
||||
|
||||
Focus:
|
||||
- Security architecture testability
|
||||
- Performance testing strategy
|
||||
- Compliance requirement mapping
|
||||
```
|
||||
|
||||
**2. Test infrastructure:**
|
||||
```
|
||||
TEA: framework
|
||||
|
||||
Requirements:
|
||||
- Separate test environments (dev, staging, prod-mirror)
|
||||
- Secure test data handling (PHI, PII)
|
||||
- Audit logging in tests
|
||||
```
|
||||
|
||||
**3. CI/CD with compliance:**
|
||||
```
|
||||
TEA: ci
|
||||
|
||||
Requirements:
|
||||
- Secrets management (Vault, AWS Secrets Manager)
|
||||
- Test isolation (no cross-contamination)
|
||||
- Artifact retention (compliance audit trail)
|
||||
- Access controls (who can run production tests)
|
||||
```
|
||||
|
||||
### Phase 4: Implementation (Required)
|
||||
|
||||
**Per epic:**
|
||||
```
|
||||
1. TEA: test-design (epic-level)
|
||||
Focus: Compliance, security, performance for THIS epic
|
||||
|
||||
2. TEA: atdd (optional)
|
||||
Generate tests including security/compliance scenarios
|
||||
|
||||
3. DEV: Implement story
|
||||
|
||||
4. TEA: automate
|
||||
Expand coverage including compliance edge cases
|
||||
|
||||
5. TEA: test-review
|
||||
Audit quality (score >80 per epic, rises to >85 at release)
|
||||
|
||||
6. TEA: trace Phase 1
|
||||
Refresh coverage, verify compliance requirements tested
|
||||
```
|
||||
|
||||
### Release Gate (Required)
|
||||
|
||||
**1. Final NFR assessment:**
|
||||
```
|
||||
TEA: nfr-assess
|
||||
|
||||
All categories (if not done earlier)
|
||||
Latest evidence (performance tests, security scans)
|
||||
```
|
||||
|
||||
**2. Final quality audit:**
|
||||
```
|
||||
TEA: test-review tests/
|
||||
|
||||
Full suite review
|
||||
Quality target: >85 for enterprise
|
||||
```
|
||||
|
||||
**3. Gate decision:**
|
||||
```
|
||||
TEA: trace Phase 2
|
||||
|
||||
Evidence required:
|
||||
- traceability-matrix.md (from Phase 1)
|
||||
- test-review.md (from quality audit)
|
||||
- nfr-assessment.md (from NFR assessment)
|
||||
- Test execution results (must have test results available)
|
||||
|
||||
Decision: PASS/CONCERNS/FAIL/WAIVED
|
||||
|
||||
Archive all artifacts for compliance audit
|
||||
```
|
||||
|
||||
**Note:** Phase 2 requires test execution results. If results aren't available, Phase 2 will be skipped.
|
||||
|
||||
**4. Archive for audit:**
|
||||
```
|
||||
Archive:
|
||||
- All test results
|
||||
- Coverage reports
|
||||
- NFR assessments
|
||||
- Gate decisions
|
||||
- Approver signatures
|
||||
|
||||
Retention: Per compliance requirements (7 years for HIPAA)
|
||||
```
|
||||
|
||||
## Enterprise-Specific Requirements
|
||||
|
||||
### Evidence Collection
|
||||
|
||||
**Required artifacts:**
|
||||
- Requirements traceability matrix
|
||||
- Test execution results (with timestamps)
|
||||
- NFR assessment reports
|
||||
- Security scan results
|
||||
- Performance test results
|
||||
- Gate decision records
|
||||
- Approver signatures
|
||||
|
||||
**Storage:**
|
||||
```
|
||||
compliance/
|
||||
├── 2026-Q1/
|
||||
│ ├── release-1.2.0/
|
||||
│ │ ├── traceability-matrix.md
|
||||
│ │ ├── test-review.md
|
||||
│ │ ├── nfr-assessment.md
|
||||
│ │ ├── gate-decision-release-v1.2.0.md
|
||||
│ │ ├── test-results/
|
||||
│ │ ├── security-scans/
|
||||
│ │ └── approvals.pdf
|
||||
```
|
||||
|
||||
**Retention:** 7 years (HIPAA), 3 years (SOC 2), per your compliance needs
|
||||
|
||||
### Approver Workflows
|
||||
|
||||
**Multi-level approval required:**
|
||||
|
||||
```markdown
|
||||
## Gate Approvals Required
|
||||
|
||||
### Technical Approval
|
||||
- [ ] QA Lead - Test coverage adequate
|
||||
- [ ] Tech Lead - Technical quality acceptable
|
||||
- [ ] Security Lead - Security requirements met
|
||||
|
||||
### Business Approval
|
||||
- [ ] Product Manager - Business requirements met
|
||||
- [ ] Compliance Officer - Regulatory requirements met
|
||||
|
||||
### Executive Approval (for major releases)
|
||||
- [ ] VP Engineering - Overall quality acceptable
|
||||
- [ ] CTO - Architecture approved for production
|
||||
```
|
||||
|
||||
### Compliance Checklists
|
||||
|
||||
**SOC 2 Example:**
|
||||
```markdown
|
||||
## SOC 2 Compliance Checklist
|
||||
|
||||
### Access Controls
|
||||
- [ ] All API endpoints require authentication
|
||||
- [ ] Authorization tested for all protected resources
|
||||
- [ ] Session management secure (token expiration tested)
|
||||
|
||||
### Audit Logging
|
||||
- [ ] All data access logged
|
||||
- [ ] Logs immutable (append-only)
|
||||
- [ ] Log retention policy enforced
|
||||
|
||||
### Data Protection
|
||||
- [ ] Data encrypted at rest (tested)
|
||||
- [ ] Data encrypted in transit (HTTPS enforced)
|
||||
- [ ] PII handling compliant (masking tested)
|
||||
|
||||
### Testing Evidence
|
||||
- [ ] Test coverage >80% (verified)
|
||||
- [ ] Security tests passing (100%)
|
||||
- [ ] Traceability matrix complete
|
||||
```
|
||||
|
||||
**HIPAA Example:**
|
||||
```markdown
|
||||
## HIPAA Compliance Checklist
|
||||
|
||||
### PHI Protection
|
||||
- [ ] PHI encrypted at rest (AES-256)
|
||||
- [ ] PHI encrypted in transit (TLS 1.3)
|
||||
- [ ] PHI access logged (audit trail)
|
||||
|
||||
### Access Controls
|
||||
- [ ] Role-based access control (RBAC tested)
|
||||
- [ ] Minimum necessary access (tested)
|
||||
- [ ] Authentication strong (MFA tested)
|
||||
|
||||
### Breach Notification
|
||||
- [ ] Breach detection tested
|
||||
- [ ] Notification workflow tested
|
||||
- [ ] Incident response plan tested
|
||||
```
|
||||
|
||||
## Enterprise Tips
|
||||
|
||||
### Start with Security
|
||||
|
||||
**Priority 1:** Security requirements
|
||||
```
|
||||
1. Document all security requirements
|
||||
2. Generate security tests with `atdd`
|
||||
3. Run security test suite
|
||||
4. Pass security audit BEFORE moving forward
|
||||
```
|
||||
|
||||
**Why:** Security failures block everything in enterprise.
|
||||
|
||||
**Example: RBAC Testing**
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
test('should enforce role-based access', async ({ request }) => {
|
||||
// Login as regular user
|
||||
const userResp = await request.post('/api/auth/login', {
|
||||
data: { email: 'user@example.com', password: 'pass' }
|
||||
});
|
||||
const { token: userToken } = await userResp.json();
|
||||
|
||||
// Try to access admin endpoint
|
||||
const adminResp = await request.get('/api/admin/users', {
|
||||
headers: { Authorization: `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
expect(adminResp.status()).toBe(403); // Forbidden
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils (Cleaner, Reusable):**
|
||||
```typescript
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { createAuthFixtures } from '@seontechnologies/playwright-utils/auth-session';
|
||||
import { mergeTests } from '@playwright/test';
|
||||
|
||||
const authFixtureTest = base.extend(createAuthFixtures());
|
||||
export const testWithAuth = mergeTests(apiRequestFixture, authFixtureTest);
|
||||
|
||||
testWithAuth('should enforce role-based access', async ({ apiRequest, authToken }) => {
|
||||
// Auth token from fixture (configured for 'user' role)
|
||||
const { status } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/admin/users', // Admin endpoint
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
expect(status).toBe(403); // Regular user denied
|
||||
});
|
||||
|
||||
testWithAuth('admin can access admin endpoint', async ({ apiRequest, authToken, authOptions }) => {
|
||||
// Override to admin role
|
||||
authOptions.userIdentifier = 'admin';
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/admin/users',
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
expect(status).toBe(200); // Admin allowed
|
||||
expect(body).toBeInstanceOf(Array);
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** Auth-session requires provider setup in global-setup.ts. See [auth-session configuration](https://seontechnologies.github.io/playwright-utils/auth-session.html).
|
||||
|
||||
**Playwright Utils Benefits for Compliance:**
|
||||
- Multi-user auth testing (regular, admin, etc.)
|
||||
- Token persistence (faster test execution)
|
||||
- Consistent auth patterns (audit trail)
|
||||
- Automatic cleanup
|
||||
|
||||
### Set Higher Quality Thresholds
|
||||
|
||||
**Enterprise quality targets:**
|
||||
- Test coverage: >85% (vs 80% for non-enterprise)
|
||||
- Quality score: >85 (vs 75 for non-enterprise)
|
||||
- P0 coverage: 100% (non-negotiable)
|
||||
- P1 coverage: >95% (vs 90% for non-enterprise)
|
||||
|
||||
**Rationale:** Enterprise systems affect more users, higher stakes.
|
||||
|
||||
### Document Everything
|
||||
|
||||
**Auditors need:**
|
||||
- Why decisions were made (rationale)
|
||||
- Who approved (signatures)
|
||||
- When (timestamps)
|
||||
- What evidence (test results, scan reports)
|
||||
|
||||
**Use TEA's structured outputs:**
|
||||
- Reports have timestamps
|
||||
- Decisions have rationale
|
||||
- Evidence is referenced
|
||||
- Audit trail is automatic
|
||||
|
||||
### Budget for Compliance Testing
|
||||
|
||||
**Enterprise testing costs more:**
|
||||
- Penetration testing: $10k-50k
|
||||
- Security audits: $5k-20k
|
||||
- Performance testing tools: $500-5k/month
|
||||
- Compliance consulting: $200-500/hour
|
||||
|
||||
**Plan accordingly:**
|
||||
- Budget in project cost
|
||||
- Schedule early (3+ months for SOC 2)
|
||||
- Don't skip (non-negotiable for compliance)
|
||||
|
||||
### Use External Validators
|
||||
|
||||
**Don't self-certify:**
|
||||
- Penetration testing: Hire external firm
|
||||
- Security audits: Independent auditor
|
||||
- Compliance: Certification body
|
||||
- Performance: Load testing service
|
||||
|
||||
**TEA's role:** Prepare for external validation, don't replace it.
|
||||
|
||||
## Related Guides
|
||||
|
||||
**Workflow Guides:**
|
||||
- [How to Run NFR Assessment](/docs/tea/how-to/workflows/run-nfr-assess.md) - Deep dive on NFRs
|
||||
- [How to Run Trace](/docs/tea/how-to/workflows/run-trace.md) - Gate decisions with evidence
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Quality audits
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Compliance-focused planning
|
||||
|
||||
**Use-Case Guides:**
|
||||
- [Using TEA with Existing Tests](/docs/tea/how-to/brownfield/use-tea-with-existing-tests.md) - Brownfield patterns
|
||||
|
||||
**Customization:**
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - Production-ready utilities
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - Enterprise model explained
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Probability × impact scoring
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Enterprise quality thresholds
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Complete TEA lifecycle
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - All 8 workflows
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Enterprise config options
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Testing patterns
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - TEA terminology
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,577 +0,0 @@
|
||||
---
|
||||
title: "Using TEA with Existing Tests (Brownfield)"
|
||||
description: Apply TEA workflows to legacy codebases with existing test suites
|
||||
---
|
||||
|
||||
# Using TEA with Existing Tests (Brownfield)
|
||||
|
||||
Use TEA on brownfield projects (existing codebases with legacy tests) to establish coverage baselines, identify gaps, and improve test quality without starting from scratch.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Existing codebase with some tests already written
|
||||
- Legacy test suite needs quality improvement
|
||||
- Adding features to existing application
|
||||
- Need to understand current test coverage
|
||||
- Want to prevent regression as you add features
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- Existing codebase with tests (even if incomplete or low quality)
|
||||
- Tests run successfully (or at least can be executed)
|
||||
|
||||
**Note:** If your codebase is completely undocumented, run `document-project` first to create baseline documentation.
|
||||
|
||||
## Brownfield Strategy
|
||||
|
||||
### Phase 1: Establish Baseline
|
||||
|
||||
Understand what you have before changing anything.
|
||||
|
||||
#### Step 1: Baseline Coverage with `trace`
|
||||
|
||||
Run `trace` Phase 1 to map existing tests to requirements:
|
||||
|
||||
```
|
||||
trace
|
||||
```
|
||||
|
||||
**Select:** Phase 1 (Requirements Traceability)
|
||||
|
||||
**Provide:**
|
||||
- Existing requirements docs (PRD, user stories, feature specs)
|
||||
- Test location (`tests/` or wherever tests live)
|
||||
- Focus areas (specific features if large codebase)
|
||||
|
||||
**Output:** `traceability-matrix.md` showing:
|
||||
- Which requirements have tests
|
||||
- Which requirements lack coverage
|
||||
- Coverage classification (FULL/PARTIAL/NONE)
|
||||
- Gap prioritization
|
||||
|
||||
**Example Baseline:**
|
||||
```markdown
|
||||
# Baseline Coverage (Before Improvements)
|
||||
|
||||
**Total Requirements:** 50
|
||||
**Full Coverage:** 15 (30%)
|
||||
**Partial Coverage:** 20 (40%)
|
||||
**No Coverage:** 15 (30%)
|
||||
|
||||
**By Priority:**
|
||||
- P0: 50% coverage (5/10) ❌ Critical gap
|
||||
- P1: 40% coverage (8/20) ⚠️ Needs improvement
|
||||
- P2: 20% coverage (2/10) ✅ Acceptable
|
||||
```
|
||||
|
||||
This baseline becomes your improvement target.
|
||||
|
||||
#### Step 2: Quality Audit with `test-review`
|
||||
|
||||
Run `test-review` on existing tests:
|
||||
|
||||
```
|
||||
test-review tests/
|
||||
```
|
||||
|
||||
**Output:** `test-review.md` with quality score and issues.
|
||||
|
||||
**Common Brownfield Issues:**
|
||||
- Hard waits everywhere (`page.waitForTimeout(5000)`)
|
||||
- Fragile CSS selectors (`.class > div:nth-child(3)`)
|
||||
- No test isolation (tests depend on execution order)
|
||||
- Try-catch for flow control
|
||||
- Tests don't clean up (leave test data in DB)
|
||||
|
||||
**Example Baseline Quality:**
|
||||
```markdown
|
||||
# Quality Score: 55/100
|
||||
|
||||
**Critical Issues:** 12
|
||||
- 8 hard waits
|
||||
- 4 conditional flow control
|
||||
|
||||
**Recommendations:** 25
|
||||
- Extract fixtures
|
||||
- Improve selectors
|
||||
- Add network assertions
|
||||
```
|
||||
|
||||
This shows where to focus improvement efforts.
|
||||
|
||||
### Phase 2: Prioritize Improvements
|
||||
|
||||
Don't try to fix everything at once.
|
||||
|
||||
#### Focus on Critical Path First
|
||||
|
||||
**Priority 1: P0 Requirements**
|
||||
```
|
||||
Goal: Get P0 coverage to 100%
|
||||
|
||||
Actions:
|
||||
1. Identify P0 requirements with no tests (from trace)
|
||||
2. Run `automate` to generate tests for missing P0 scenarios
|
||||
3. Fix critical quality issues in P0 tests (from test-review)
|
||||
```
|
||||
|
||||
**Priority 2: Fix Flaky Tests**
|
||||
```
|
||||
Goal: Eliminate flakiness
|
||||
|
||||
Actions:
|
||||
1. Identify tests with hard waits (from test-review)
|
||||
2. Replace with network-first patterns
|
||||
3. Run burn-in loops to verify stability
|
||||
```
|
||||
|
||||
**Example Modernization:**
|
||||
|
||||
**Before (Flaky - Hard Waits):**
|
||||
```typescript
|
||||
test('checkout completes', async ({ page }) => {
|
||||
await page.click('button[name="checkout"]');
|
||||
await page.waitForTimeout(5000); // ❌ Flaky
|
||||
await expect(page.locator('.confirmation')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**After (Network-First - Vanilla):**
|
||||
```typescript
|
||||
test('checkout completes', async ({ page }) => {
|
||||
const checkoutPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/checkout') && resp.ok()
|
||||
);
|
||||
await page.click('button[name="checkout"]');
|
||||
await checkoutPromise; // ✅ Deterministic
|
||||
await expect(page.locator('.confirmation')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**After (With Playwright Utils - Cleaner API):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('checkout completes', async ({ page, interceptNetworkCall }) => {
|
||||
// Use interceptNetworkCall for cleaner network interception
|
||||
const checkoutCall = interceptNetworkCall({
|
||||
method: 'POST',
|
||||
url: '**/api/checkout'
|
||||
});
|
||||
|
||||
await page.click('button[name="checkout"]');
|
||||
|
||||
// Wait for response (automatic JSON parsing)
|
||||
const { status, responseJson: order } = await checkoutCall;
|
||||
|
||||
// Validate API response
|
||||
expect(status).toBe(200);
|
||||
expect(order.status).toBe('confirmed');
|
||||
|
||||
// Validate UI
|
||||
await expect(page.locator('.confirmation')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Playwright Utils Benefits:**
|
||||
- `interceptNetworkCall` for cleaner network interception
|
||||
- Automatic JSON parsing (`responseJson` ready to use)
|
||||
- No manual `await response.json()`
|
||||
- Glob pattern matching (`**/api/checkout`)
|
||||
- Cleaner, more maintainable code
|
||||
|
||||
**For automatic error detection,** use `network-error-monitor` fixture separately. See [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md#network-error-monitor).
|
||||
|
||||
**Priority 3: P1 Requirements**
|
||||
```
|
||||
Goal: Get P1 coverage to 80%+
|
||||
|
||||
Actions:
|
||||
1. Generate tests for highest-risk P1 gaps
|
||||
2. Improve test quality incrementally
|
||||
```
|
||||
|
||||
#### Create Improvement Roadmap
|
||||
|
||||
```markdown
|
||||
# Test Improvement Roadmap
|
||||
|
||||
## Week 1: Critical Path (P0)
|
||||
- [ ] Add 5 missing P0 tests (Epic 1: Auth)
|
||||
- [ ] Fix 8 hard waits in auth tests
|
||||
- [ ] Verify P0 coverage = 100%
|
||||
|
||||
## Week 2: Flakiness
|
||||
- [ ] Replace all hard waits with network-first
|
||||
- [ ] Fix conditional flow control
|
||||
- [ ] Run burn-in loops (target: 0 failures in 10 runs)
|
||||
|
||||
## Week 3: High-Value Coverage (P1)
|
||||
- [ ] Add 10 missing P1 tests
|
||||
- [ ] Improve selector resilience
|
||||
- [ ] P1 coverage target: 80%
|
||||
|
||||
## Week 4: Quality Polish
|
||||
- [ ] Extract fixtures for common patterns
|
||||
- [ ] Add network assertions
|
||||
- [ ] Quality score target: 75+
|
||||
```
|
||||
|
||||
### Phase 3: Incremental Improvement
|
||||
|
||||
Apply TEA workflows to new work while improving legacy tests.
|
||||
|
||||
#### For New Features (Greenfield Within Brownfield)
|
||||
|
||||
**Use full TEA workflow:**
|
||||
```
|
||||
1. `test-design` (epic-level) - Plan tests for new feature
|
||||
2. `atdd` - Generate failing tests first (TDD)
|
||||
3. Implement feature
|
||||
4. `automate` - Expand coverage
|
||||
5. `test-review` - Ensure quality
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- New code has high-quality tests from day one
|
||||
- Gradually raises overall quality
|
||||
- Team learns good patterns
|
||||
|
||||
#### For Bug Fixes (Regression Prevention)
|
||||
|
||||
**Add regression tests:**
|
||||
```
|
||||
1. Reproduce bug with failing test
|
||||
2. Fix bug
|
||||
3. Verify test passes
|
||||
4. Run `test-review` on regression test
|
||||
5. Add to regression test suite
|
||||
```
|
||||
|
||||
#### For Refactoring (Regression Safety)
|
||||
|
||||
**Before refactoring:**
|
||||
```
|
||||
1. Run `trace` - Baseline coverage
|
||||
2. Note current coverage %
|
||||
3. Refactor code
|
||||
4. Run `trace` - Verify coverage maintained
|
||||
5. No coverage should decrease
|
||||
```
|
||||
|
||||
### Phase 4: Continuous Improvement
|
||||
|
||||
Track improvement over time.
|
||||
|
||||
#### Quarterly Quality Audits
|
||||
|
||||
**Q1 Baseline:**
|
||||
```
|
||||
Coverage: 30%
|
||||
Quality Score: 55/100
|
||||
Flakiness: 15% fail rate
|
||||
```
|
||||
|
||||
**Q2 Target:**
|
||||
```
|
||||
Coverage: 50% (focus on P0)
|
||||
Quality Score: 65/100
|
||||
Flakiness: 5%
|
||||
```
|
||||
|
||||
**Q3 Target:**
|
||||
```
|
||||
Coverage: 70%
|
||||
Quality Score: 75/100
|
||||
Flakiness: 1%
|
||||
```
|
||||
|
||||
**Q4 Target:**
|
||||
```
|
||||
Coverage: 85%
|
||||
Quality Score: 85/100
|
||||
Flakiness: <0.5%
|
||||
```
|
||||
|
||||
## Brownfield-Specific Tips
|
||||
|
||||
### Don't Rewrite Everything
|
||||
|
||||
**Common mistake:**
|
||||
```
|
||||
"Our tests are bad, let's delete them all and start over!"
|
||||
```
|
||||
|
||||
**Better approach:**
|
||||
```
|
||||
"Our tests are bad, let's:
|
||||
1. Keep tests that work (even if not perfect)
|
||||
2. Fix critical quality issues incrementally
|
||||
3. Add tests for gaps
|
||||
4. Gradually improve over time"
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- Rewriting is risky (might lose coverage)
|
||||
- Incremental improvement is safer
|
||||
- Team learns gradually
|
||||
- Business value delivered continuously
|
||||
|
||||
### Use Regression Hotspots
|
||||
|
||||
**Identify regression-prone areas:**
|
||||
```markdown
|
||||
## Regression Hotspots
|
||||
|
||||
**Based on:**
|
||||
- Bug reports (last 6 months)
|
||||
- Customer complaints
|
||||
- Code complexity (cyclomatic complexity >10)
|
||||
- Frequent changes (git log analysis)
|
||||
|
||||
**High-Risk Areas:**
|
||||
1. Authentication flow (12 bugs in 6 months)
|
||||
2. Checkout process (8 bugs)
|
||||
3. Payment integration (6 bugs)
|
||||
|
||||
**Test Priority:**
|
||||
- Add regression tests for these areas FIRST
|
||||
- Ensure P0 coverage before touching code
|
||||
```
|
||||
|
||||
### Quarantine Flaky Tests
|
||||
|
||||
Don't let flaky tests block improvement:
|
||||
|
||||
```typescript
|
||||
// Mark flaky tests with .skip temporarily
|
||||
test.skip('flaky test - needs fixing', async ({ page }) => {
|
||||
// TODO: Fix hard wait on line 45
|
||||
// TODO: Add network-first pattern
|
||||
});
|
||||
```
|
||||
|
||||
**Track quarantined tests:**
|
||||
```markdown
|
||||
# Quarantined Tests
|
||||
|
||||
| Test | Reason | Owner | Target Fix Date |
|
||||
| ------------------- | -------------------------- | -------- | --------------- |
|
||||
| checkout.spec.ts:45 | Hard wait causes flakiness | QA Team | 2026-01-20 |
|
||||
| profile.spec.ts:28 | Conditional flow control | Dev Team | 2026-01-25 |
|
||||
```
|
||||
|
||||
**Fix systematically:**
|
||||
- Don't accumulate quarantined tests
|
||||
- Set deadlines for fixes
|
||||
- Review quarantine list weekly
|
||||
|
||||
### Migrate One Directory at a Time
|
||||
|
||||
**Large test suite?** Improve incrementally:
|
||||
|
||||
**Week 1:** `tests/auth/`
|
||||
```
|
||||
1. Run `test-review` on auth tests
|
||||
2. Fix critical issues
|
||||
3. Re-review
|
||||
4. Mark directory as "modernized"
|
||||
```
|
||||
|
||||
**Week 2:** `tests/api/`
|
||||
```
|
||||
Same process
|
||||
```
|
||||
|
||||
**Week 3:** `tests/e2e/`
|
||||
```
|
||||
Same process
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Focused improvement
|
||||
- Visible progress
|
||||
- Team learns patterns
|
||||
- Lower risk
|
||||
|
||||
### Document Migration Status
|
||||
|
||||
**Track which tests are modernized:**
|
||||
|
||||
```markdown
|
||||
# Test Suite Status
|
||||
|
||||
| Directory | Tests | Quality Score | Status | Notes |
|
||||
| ------------------ | ----- | ------------- | ------------- | -------------- |
|
||||
| tests/auth/ | 15 | 85/100 | ✅ Modernized | Week 1 cleanup |
|
||||
| tests/api/ | 32 | 78/100 | ⚠️ In Progress | Week 2 |
|
||||
| tests/e2e/ | 28 | 62/100 | ❌ Legacy | Week 3 planned |
|
||||
| tests/integration/ | 12 | 45/100 | ❌ Legacy | Week 4 planned |
|
||||
|
||||
**Legend:**
|
||||
- ✅ Modernized: Quality >80, no critical issues
|
||||
- ⚠️ In Progress: Active improvement
|
||||
- ❌ Legacy: Not yet touched
|
||||
```
|
||||
|
||||
## Common Brownfield Challenges
|
||||
|
||||
### "We Don't Know What Tests Cover"
|
||||
|
||||
**Problem:** No documentation, unclear what tests do.
|
||||
|
||||
**Solution:**
|
||||
```
|
||||
1. Run `trace` - TEA analyzes tests and maps to requirements
|
||||
2. Review traceability matrix
|
||||
3. Document findings
|
||||
4. Use as baseline for improvement
|
||||
```
|
||||
|
||||
TEA reverse-engineers test coverage even without documentation.
|
||||
|
||||
### "Tests Are Too Brittle to Touch"
|
||||
|
||||
**Problem:** Afraid to modify tests (might break them).
|
||||
|
||||
**Solution:**
|
||||
```
|
||||
1. Run tests, capture current behavior (baseline)
|
||||
2. Make small improvement (fix one hard wait)
|
||||
3. Run tests again
|
||||
4. If still pass, continue
|
||||
5. If fail, investigate why
|
||||
|
||||
Incremental changes = lower risk
|
||||
```
|
||||
|
||||
### "No One Knows How to Run Tests"
|
||||
|
||||
**Problem:** Test documentation is outdated or missing.
|
||||
|
||||
**Solution:**
|
||||
```
|
||||
1. Document manually or ask TEA to help analyze test structure
|
||||
2. Create tests/README.md with:
|
||||
- How to install dependencies
|
||||
- How to run tests (npx playwright test, npm test, etc.)
|
||||
- What each test directory contains
|
||||
- Common issues and troubleshooting
|
||||
3. Commit documentation for team
|
||||
```
|
||||
|
||||
**Note:** `framework` is for new test setup, not existing tests. For brownfield, document what you have.
|
||||
|
||||
### "Tests Take Hours to Run"
|
||||
|
||||
**Problem:** Full test suite takes 4+ hours.
|
||||
|
||||
**Solution:**
|
||||
```
|
||||
1. Configure parallel execution (shard tests across workers)
|
||||
2. Add selective testing (run only affected tests on PR)
|
||||
3. Run full suite nightly only
|
||||
4. Optimize slow tests (remove hard waits, improve selectors)
|
||||
|
||||
Before: 4 hours sequential
|
||||
After: 15 minutes with sharding + selective testing
|
||||
```
|
||||
|
||||
**How `ci` helps:**
|
||||
- Scaffolds CI configuration with parallel sharding examples
|
||||
- Provides selective testing script templates
|
||||
- Documents burn-in and optimization strategies
|
||||
- But YOU configure workers, test selection, and optimization
|
||||
|
||||
**With Playwright Utils burn-in:**
|
||||
- Smart selective testing based on git diff
|
||||
- Volume control (run percentage of affected tests)
|
||||
- See [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md#burn-in)
|
||||
|
||||
### "We Have Tests But They Always Fail"
|
||||
|
||||
**Problem:** Tests are so flaky they're ignored.
|
||||
|
||||
**Solution:**
|
||||
```
|
||||
1. Run `test-review` to identify flakiness patterns
|
||||
2. Fix top 5 flaky tests (biggest impact)
|
||||
3. Quarantine remaining flaky tests
|
||||
4. Re-enable as you fix them
|
||||
|
||||
Don't let perfect be the enemy of good
|
||||
```
|
||||
|
||||
## Brownfield TEA Workflow
|
||||
|
||||
### Recommended Sequence
|
||||
|
||||
**1. Documentation (if needed):**
|
||||
```
|
||||
document-project
|
||||
```
|
||||
|
||||
**2. Baseline (Phase 2):**
|
||||
```
|
||||
trace Phase 1 - Establish coverage baseline
|
||||
test-review - Establish quality baseline
|
||||
```
|
||||
|
||||
**3. Planning (Phase 2-3):**
|
||||
```
|
||||
prd - Document requirements (if missing)
|
||||
architecture - Document architecture (if missing)
|
||||
test-design (system-level) - Testability review
|
||||
```
|
||||
|
||||
**4. Infrastructure (Phase 3):**
|
||||
```
|
||||
framework - Modernize test framework (if needed)
|
||||
ci - Setup or improve CI/CD
|
||||
```
|
||||
|
||||
**5. Per Epic (Phase 4):**
|
||||
```
|
||||
test-design (epic-level) - Focus on regression hotspots
|
||||
automate - Add missing tests
|
||||
test-review - Ensure quality
|
||||
trace Phase 1 - Refresh coverage
|
||||
```
|
||||
|
||||
**6. Release Gate:**
|
||||
```
|
||||
nfr-assess - Validate NFRs (if enterprise)
|
||||
trace Phase 2 - Gate decision
|
||||
```
|
||||
|
||||
## Related Guides
|
||||
|
||||
**Workflow Guides:**
|
||||
- [How to Run Trace](/docs/tea/how-to/workflows/run-trace.md) - Baseline coverage analysis
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Quality audit
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Fill coverage gaps
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Risk assessment
|
||||
|
||||
**Customization:**
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - Modernize tests with utilities
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - Brownfield model explained
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - What makes tests good
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Fix flakiness
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Prioritize improvements
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - All 8 workflows
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Config options
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Testing patterns
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - TEA terminology
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,424 +0,0 @@
|
||||
---
|
||||
title: "Enable TEA MCP Enhancements"
|
||||
description: Configure Playwright MCP servers for live browser verification during TEA workflows
|
||||
---
|
||||
|
||||
# Enable TEA MCP Enhancements
|
||||
|
||||
Configure Model Context Protocol (MCP) servers to enable live browser verification, exploratory mode, and recording mode in TEA workflows.
|
||||
|
||||
## What are MCP Enhancements?
|
||||
|
||||
MCP (Model Context Protocol) servers enable AI agents to interact with live browsers during test generation. This allows TEA to:
|
||||
|
||||
- **Explore UIs interactively** - Discover actual functionality through browser automation
|
||||
- **Verify selectors** - Generate accurate locators from real DOM
|
||||
- **Validate behavior** - Confirm test scenarios against live applications
|
||||
- **Debug visually** - Use trace viewer and screenshots during generation
|
||||
|
||||
## When to Use This
|
||||
|
||||
**For UI Testing:**
|
||||
- Want exploratory mode in `test-design` (browser-based UI discovery)
|
||||
- Want recording mode in `atdd` or `automate` (verify selectors with live browser)
|
||||
- Want healing mode in `automate` (fix tests with visual debugging)
|
||||
- Need accurate selectors from actual DOM
|
||||
- Debugging complex UI interactions
|
||||
|
||||
**For API Testing:**
|
||||
- Want healing mode in `automate` (analyze failures with trace data)
|
||||
- Need to debug test failures (network responses, request/response data, timing)
|
||||
- Want to inspect trace files (network traffic, errors, race conditions)
|
||||
|
||||
**For Both:**
|
||||
- Visual debugging (trace viewer shows network + UI)
|
||||
- Test failure analysis (MCP can run tests and extract errors)
|
||||
- Understanding complex test failures (network + DOM together)
|
||||
|
||||
**Don't use if:**
|
||||
- You don't have MCP servers configured
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- IDE with MCP support (Cursor, VS Code with Claude extension)
|
||||
- Node.js v18 or later
|
||||
- Playwright installed
|
||||
|
||||
## Available MCP Servers
|
||||
|
||||
**Two Playwright MCP servers** (actively maintained, continuously updated):
|
||||
|
||||
### 1. Playwright MCP - Browser Automation
|
||||
|
||||
**Command:** `npx @playwright/mcp@latest`
|
||||
|
||||
**Capabilities:**
|
||||
- Navigate to URLs
|
||||
- Click elements
|
||||
- Fill forms
|
||||
- Take screenshots
|
||||
- Extract DOM information
|
||||
|
||||
**Best for:** Exploratory mode, recording mode
|
||||
|
||||
### 2. Playwright Test MCP - Test Runner
|
||||
|
||||
**Command:** `npx playwright run-test-mcp-server`
|
||||
|
||||
**Capabilities:**
|
||||
- Run test files
|
||||
- Analyze failures
|
||||
- Extract error messages
|
||||
- Show trace files
|
||||
|
||||
**Best for:** Healing mode, debugging
|
||||
|
||||
### Recommended: Configure Both
|
||||
|
||||
Both servers work together to provide full TEA MCP capabilities.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Configure MCP Servers
|
||||
|
||||
Add to your IDE's MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
},
|
||||
"playwright-test": {
|
||||
"command": "npx",
|
||||
"args": ["playwright", "run-test-mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [TEA Overview](/docs/tea/explanation/tea-overview.md#playwright-mcp-enhancements) for IDE-specific config locations.
|
||||
|
||||
### 2. Enable in BMAD
|
||||
|
||||
Answer "Yes" when prompted during installation, or set in config:
|
||||
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
tea_use_mcp_enhancements: true
|
||||
```
|
||||
|
||||
### 3. Verify MCPs Running
|
||||
|
||||
Ensure your MCP servers are running in your IDE.
|
||||
|
||||
## How MCP Enhances TEA Workflows
|
||||
|
||||
### test-design: Exploratory Mode
|
||||
|
||||
**Without MCP:**
|
||||
- TEA infers UI functionality from documentation
|
||||
- Relies on your description of features
|
||||
- May miss actual UI behavior
|
||||
|
||||
**With MCP:**
|
||||
TEA can open live browser to:
|
||||
```
|
||||
"Let me explore the profile page to understand the UI"
|
||||
|
||||
[TEA navigates to /profile]
|
||||
[Takes screenshot]
|
||||
[Extracts accessible elements]
|
||||
|
||||
"I see the profile has:
|
||||
- Name field (editable)
|
||||
- Email field (editable)
|
||||
- Avatar upload button
|
||||
- Save button
|
||||
- Cancel button
|
||||
|
||||
I'll design tests for these interactions."
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Accurate test design based on actual UI
|
||||
- Discovers functionality you might not describe
|
||||
- Validates test scenarios are possible
|
||||
|
||||
### atdd: Recording Mode
|
||||
|
||||
**Without MCP:**
|
||||
- TEA generates selectors from best practices
|
||||
- TEA infers API patterns from documentation
|
||||
|
||||
**With MCP (Recording Mode):**
|
||||
|
||||
**For UI Tests:**
|
||||
```
|
||||
[TEA navigates to /login with live browser]
|
||||
[Inspects actual form fields]
|
||||
|
||||
"I see:
|
||||
- Email input has label 'Email Address' (not 'Email')
|
||||
- Password input has label 'Your Password'
|
||||
- Submit button has text 'Sign In' (not 'Login')
|
||||
|
||||
I'll use these exact selectors."
|
||||
```
|
||||
|
||||
**For API Tests:**
|
||||
```
|
||||
[TEA analyzes trace files from test runs]
|
||||
[Inspects network requests/responses]
|
||||
|
||||
"I see the API returns:
|
||||
- POST /api/login → 200 with { token, userId }
|
||||
- Response time: 150ms
|
||||
- Required headers: Content-Type, Authorization
|
||||
|
||||
I'll validate these in tests."
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- UI: Accurate selectors from real DOM
|
||||
- API: Validated request/response patterns from trace
|
||||
- Both: Tests work on first run
|
||||
|
||||
### automate: Healing + Recording Modes
|
||||
|
||||
**Without MCP:**
|
||||
- TEA analyzes test code only
|
||||
- Suggests fixes based on static analysis
|
||||
- Generates tests from documentation/code
|
||||
|
||||
**With MCP:**
|
||||
|
||||
**Healing Mode (UI + API):**
|
||||
```
|
||||
[TEA opens trace file]
|
||||
[Analyzes screenshots + network tab]
|
||||
|
||||
UI failures: "Button selector changed from 'Save' to 'Save Changes'"
|
||||
API failures: "Response structure changed, expected {id} got {userId}"
|
||||
|
||||
[TEA makes fixes]
|
||||
[Verifies with trace analysis]
|
||||
```
|
||||
|
||||
**Recording Mode (UI + API):**
|
||||
```
|
||||
UI: [Inspects actual DOM, generates verified selectors]
|
||||
API: [Analyzes network traffic, validates request/response patterns]
|
||||
|
||||
[Generates tests with verified patterns]
|
||||
[Tests work on first run]
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Visual debugging + trace analysis (not just UI)
|
||||
- Verified selectors (UI) + network patterns (API)
|
||||
- Tests verified against actual application behavior
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Exploratory Mode Example
|
||||
|
||||
```
|
||||
User: Run test-design
|
||||
|
||||
TEA: What feature are you designing tests for?
|
||||
|
||||
User: The checkout flow at https://shop.example.com
|
||||
|
||||
TEA: [With MCP] Let me explore the checkout flow
|
||||
[Opens browser]
|
||||
[Navigates to checkout]
|
||||
[Takes screenshots]
|
||||
[Identifies all interactive elements]
|
||||
|
||||
TEA: I've explored the checkout flow. I found:
|
||||
- 4-step wizard (Cart → Shipping → Payment → Confirm)
|
||||
- Shipping options (Standard, Express, Overnight)
|
||||
- Payment methods (Credit Card, PayPal)
|
||||
- Promo code field
|
||||
- Order summary with real-time totals
|
||||
|
||||
Test design will cover all these scenarios.
|
||||
```
|
||||
|
||||
### Recording Mode Example
|
||||
|
||||
```
|
||||
User: Run atdd for user registration feature
|
||||
|
||||
TEA: [With MCP] Let me verify the registration form selectors
|
||||
[Opens browser at /register]
|
||||
[Inspects form]
|
||||
|
||||
TEA: I see the actual form has:
|
||||
- "Full Name" label (not "Name")
|
||||
- "Email Address" label (not "Email")
|
||||
- "Create Password" label (not "Password")
|
||||
- "I agree to Terms" checkbox
|
||||
- "Create Account" button (not "Register")
|
||||
|
||||
Generating tests with verified selectors...
|
||||
```
|
||||
|
||||
### Healing Mode Example
|
||||
|
||||
```
|
||||
User: Run automate with healing mode
|
||||
|
||||
TEA: [With MCP] Running tests and checking for failures
|
||||
[Runs test suite]
|
||||
[Test fails: selector not found]
|
||||
|
||||
TEA: Test failed on selector '.submit-btn'
|
||||
[Opens trace viewer]
|
||||
[Sees button class changed to '.submit-button']
|
||||
|
||||
Fixing selector and verifying...
|
||||
[Updates test]
|
||||
[Re-runs with MCP]
|
||||
[Test passes]
|
||||
|
||||
Updated test with corrected selector.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP Servers Not Running
|
||||
|
||||
**Problem:** TEA says MCP enhancements aren't available.
|
||||
|
||||
**Causes:**
|
||||
1. MCP servers not configured in IDE
|
||||
2. Config syntax error in JSON
|
||||
3. IDE not restarted after config
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify MCP config file exists
|
||||
ls ~/.cursor/config.json
|
||||
|
||||
# Validate JSON syntax
|
||||
cat ~/.cursor/config.json | python -m json.tool
|
||||
|
||||
# Restart IDE
|
||||
# Cmd+Q (quit) then reopen
|
||||
```
|
||||
|
||||
### Browser Doesn't Open
|
||||
|
||||
**Problem:** MCP enabled but browser never opens.
|
||||
|
||||
**Causes:**
|
||||
1. Playwright browsers not installed
|
||||
2. Headless mode enabled
|
||||
3. MCP server crashed
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Install browsers
|
||||
npx playwright install
|
||||
|
||||
# Check MCP server logs (in IDE)
|
||||
# Look for error messages
|
||||
|
||||
# Try manual MCP server
|
||||
npx @playwright/mcp@latest
|
||||
# Should start without errors
|
||||
```
|
||||
|
||||
### TEA Doesn't Use MCP
|
||||
|
||||
**Problem:** `tea_use_mcp_enhancements: true` but TEA doesn't use browser.
|
||||
|
||||
**Causes:**
|
||||
1. Config not saved
|
||||
2. Workflow run before config update
|
||||
3. MCP servers not running
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify config
|
||||
grep tea_use_mcp_enhancements _bmad/bmm/config.yaml
|
||||
# Should show: tea_use_mcp_enhancements: true
|
||||
|
||||
# Restart IDE (reload MCP servers)
|
||||
|
||||
# Start fresh chat (TEA loads config at start)
|
||||
```
|
||||
|
||||
### Selector Verification Fails
|
||||
|
||||
**Problem:** MCP can't find elements TEA is looking for.
|
||||
|
||||
**Causes:**
|
||||
1. Page not fully loaded
|
||||
2. Element behind modal/overlay
|
||||
3. Element requires authentication
|
||||
|
||||
**Solution:**
|
||||
TEA will handle this automatically:
|
||||
- Wait for page load
|
||||
- Dismiss modals if present
|
||||
- Handle auth if needed
|
||||
|
||||
If persistent, provide TEA more context:
|
||||
```
|
||||
"The element is behind a modal - dismiss the modal first"
|
||||
"The page requires login - use credentials X"
|
||||
```
|
||||
|
||||
### MCP Slows Down Workflows
|
||||
|
||||
**Problem:** Workflows take much longer with MCP enabled.
|
||||
|
||||
**Cause:** Browser automation adds overhead.
|
||||
|
||||
**Solution:**
|
||||
Use MCP selectively:
|
||||
- **Enable for:** Complex UIs, new projects, debugging
|
||||
- **Disable for:** Simple features, well-known patterns, API-only testing
|
||||
|
||||
Toggle quickly:
|
||||
```yaml
|
||||
# For this feature (complex UI)
|
||||
tea_use_mcp_enhancements: true
|
||||
|
||||
# For next feature (simple API)
|
||||
tea_use_mcp_enhancements: false
|
||||
```
|
||||
|
||||
## Related Guides
|
||||
|
||||
**Getting Started:**
|
||||
- [TEA Lite Quickstart Tutorial](/docs/tea/tutorials/tea-lite-quickstart.md) - Learn TEA basics first
|
||||
|
||||
**Workflow Guides (MCP-Enhanced):**
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Exploratory mode with browser
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Recording mode for accurate selectors
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Healing mode for debugging
|
||||
|
||||
**Other Customization:**
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - Production-ready utilities
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - MCP enhancements in lifecycle
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - When to use MCP enhancements
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - tea_use_mcp_enhancements option
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - MCP-enhanced workflows
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - MCP Enhancements term
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,813 +0,0 @@
|
||||
---
|
||||
title: "Integrate Playwright Utils with TEA"
|
||||
description: Add production-ready fixtures and utilities to your TEA-generated tests
|
||||
---
|
||||
|
||||
# Integrate Playwright Utils with TEA
|
||||
|
||||
Integrate `@seontechnologies/playwright-utils` with TEA to get production-ready fixtures, utilities, and patterns in your test suite.
|
||||
|
||||
## What is Playwright Utils?
|
||||
|
||||
A production-ready utility library that provides:
|
||||
- Typed API request helper
|
||||
- Authentication session management
|
||||
- Network recording and replay (HAR)
|
||||
- Network request interception
|
||||
- Async polling (recurse)
|
||||
- Structured logging
|
||||
- File validation (CSV, PDF, XLSX, ZIP)
|
||||
- Burn-in testing utilities
|
||||
- Network error monitoring
|
||||
|
||||
**Repository:** [https://github.com/seontechnologies/playwright-utils](https://github.com/seontechnologies/playwright-utils)
|
||||
|
||||
**npm Package:** `@seontechnologies/playwright-utils`
|
||||
|
||||
## When to Use This
|
||||
|
||||
- You want production-ready fixtures (not DIY)
|
||||
- Your team benefits from standardized patterns
|
||||
- You need utilities like API testing, auth handling, network mocking
|
||||
- You want TEA to generate tests using these utilities
|
||||
- You're building reusable test infrastructure
|
||||
|
||||
**Don't use if:**
|
||||
- You're just learning testing (keep it simple first)
|
||||
- You have your own fixture library
|
||||
- You don't need the utilities
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- Test framework setup complete (Playwright)
|
||||
- Node.js v18 or later
|
||||
|
||||
**Note:** Playwright Utils is for Playwright only (not Cypress).
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Install Package
|
||||
|
||||
```bash
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
```
|
||||
|
||||
### Step 2: Enable in TEA Config
|
||||
|
||||
Edit `_bmad/bmm/config.yaml`:
|
||||
|
||||
```yaml
|
||||
tea_use_playwright_utils: true
|
||||
```
|
||||
|
||||
**Note:** If you enabled this during BMad installation, it's already set.
|
||||
|
||||
### Step 3: Verify Installation
|
||||
|
||||
```bash
|
||||
# Check package installed
|
||||
npm list @seontechnologies/playwright-utils
|
||||
|
||||
# Check TEA config
|
||||
grep tea_use_playwright_utils _bmad/bmm/config.yaml
|
||||
```
|
||||
|
||||
Should show:
|
||||
```
|
||||
@seontechnologies/playwright-utils@2.x.x
|
||||
tea_use_playwright_utils: true
|
||||
```
|
||||
|
||||
## What Changes When Enabled
|
||||
|
||||
### `framework` Workflow
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// Basic Playwright fixtures only
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('api test', async ({ request }) => {
|
||||
const response = await request.get('/api/users');
|
||||
const users = await response.json();
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils (Combined Fixtures):**
|
||||
```typescript
|
||||
// All utilities available via single import
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('api test', async ({ apiRequest, authToken, log }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users',
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
log.info('Fetched users', body);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils (Selective Merge):**
|
||||
```typescript
|
||||
import { mergeTests } from '@playwright/test';
|
||||
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { test as logFixture } from '@seontechnologies/playwright-utils/log/fixtures';
|
||||
|
||||
export const test = mergeTests(apiRequestFixture, logFixture);
|
||||
export { expect } from '@playwright/test';
|
||||
|
||||
test('api test', async ({ apiRequest, log }) => {
|
||||
log.info('Fetching users');
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users'
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
### `atdd` and `automate` Workflows
|
||||
|
||||
**Without Playwright Utils:**
|
||||
```typescript
|
||||
// Manual API calls
|
||||
test('should fetch profile', async ({ page, request }) => {
|
||||
const response = await request.get('/api/profile');
|
||||
const profile = await response.json();
|
||||
// Manual parsing and validation
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
|
||||
test('should fetch profile', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/profile' // 'path' not 'url'
|
||||
}).validateSchema(ProfileSchema); // Chained validation
|
||||
|
||||
expect(status).toBe(200);
|
||||
// body is type-safe: { id: string, name: string, email: string }
|
||||
});
|
||||
```
|
||||
|
||||
### `test-review` Workflow
|
||||
|
||||
**Without Playwright Utils:**
|
||||
Reviews against generic Playwright patterns
|
||||
|
||||
**With Playwright Utils:**
|
||||
Reviews against playwright-utils best practices:
|
||||
- Fixture composition patterns
|
||||
- Utility usage (apiRequest, authSession, etc.)
|
||||
- Network-first patterns
|
||||
- Structured logging
|
||||
|
||||
### `ci` Workflow
|
||||
|
||||
**Without Playwright Utils:**
|
||||
- Parallel sharding
|
||||
- Burn-in loops (basic shell scripts)
|
||||
- CI triggers (PR, push, schedule)
|
||||
- Artifact collection
|
||||
|
||||
**With Playwright Utils:**
|
||||
Enhanced with smart testing:
|
||||
- Burn-in utility (git diff-based, volume control)
|
||||
- Selective testing (skip config/docs/types changes)
|
||||
- Test prioritization by file changes
|
||||
|
||||
## Available Utilities
|
||||
|
||||
### api-request
|
||||
|
||||
Typed HTTP client with schema validation.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/api-request.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Vanilla Playwright | api-request Utility |
|
||||
|-------------------|---------------------|
|
||||
| Manual `await response.json()` | Automatic JSON parsing |
|
||||
| `response.status()` + separate body parsing | Returns `{ status, body }` structure |
|
||||
| No built-in retry | Automatic retry for 5xx errors |
|
||||
| No schema validation | Single-line `.validateSchema()` |
|
||||
| Verbose status checking | Clean destructuring |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
test('should create user', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users', // Note: 'path' not 'url'
|
||||
body: { name: 'Test User', email: 'test@example.com' } // Note: 'body' not 'data'
|
||||
}).validateSchema(UserSchema); // Chained method (can await separately if needed)
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body.id).toBeDefined();
|
||||
expect(body.email).toBe('test@example.com');
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Returns `{ status, body }` structure
|
||||
- Schema validation with `.validateSchema()` chained method
|
||||
- Automatic retry for 5xx errors
|
||||
- Type-safe response body
|
||||
|
||||
### auth-session
|
||||
|
||||
Authentication session management with token persistence.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/auth-session.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Vanilla Playwright Auth | auth-session |
|
||||
|------------------------|--------------|
|
||||
| Re-authenticate every test run (slow) | Authenticate once, persist to disk |
|
||||
| Single user per setup | Multi-user support (roles, accounts) |
|
||||
| No token expiration handling | Automatic token renewal |
|
||||
| Manual session management | Provider pattern (flexible auth) |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('should access protected route', async ({ page, authToken }) => {
|
||||
// authToken automatically fetched and persisted
|
||||
// No manual login needed - handled by fixture
|
||||
|
||||
await page.goto('/dashboard');
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
|
||||
// Token is reused across tests (persisted to disk)
|
||||
});
|
||||
```
|
||||
|
||||
**Configuration required** (see auth-session docs for provider setup):
|
||||
```typescript
|
||||
// global-setup.ts
|
||||
import { authStorageInit, setAuthProvider, authGlobalInit } from '@seontechnologies/playwright-utils/auth-session';
|
||||
|
||||
async function globalSetup() {
|
||||
authStorageInit();
|
||||
setAuthProvider(myCustomProvider); // Define your auth mechanism
|
||||
await authGlobalInit(); // Fetch token once
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Token fetched once, reused across all tests
|
||||
- Persisted to disk (faster subsequent runs)
|
||||
- Multi-user support via `authOptions.userIdentifier`
|
||||
- Automatic token renewal if expired
|
||||
|
||||
### network-recorder
|
||||
|
||||
Record and replay network traffic (HAR) for offline testing.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/network-recorder.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Vanilla Playwright HAR | network-recorder |
|
||||
|------------------------|------------------|
|
||||
| Manual `routeFromHAR()` configuration | Automatic HAR management with `PW_NET_MODE` |
|
||||
| Separate record/playback test files | Same test, switch env var |
|
||||
| No CRUD detection | Stateful mocking (POST/PUT/DELETE work) |
|
||||
| Manual HAR file paths | Auto-organized by test name |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures';
|
||||
|
||||
// Record mode: Set environment variable
|
||||
process.env.PW_NET_MODE = 'record';
|
||||
|
||||
test('should work with recorded traffic', async ({ page, context, networkRecorder }) => {
|
||||
// Setup recorder (records or replays based on PW_NET_MODE)
|
||||
await networkRecorder.setup(context);
|
||||
|
||||
// Your normal test code
|
||||
await page.goto('/dashboard');
|
||||
await page.click('#add-item');
|
||||
|
||||
// First run (record): Saves traffic to HAR file
|
||||
// Subsequent runs (playback): Uses HAR file, no backend needed
|
||||
});
|
||||
```
|
||||
|
||||
**Switch modes:**
|
||||
```bash
|
||||
# Record traffic
|
||||
PW_NET_MODE=record npx playwright test
|
||||
|
||||
# Playback traffic (offline)
|
||||
PW_NET_MODE=playback npx playwright test
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Offline testing (no backend needed)
|
||||
- Deterministic responses (same every time)
|
||||
- Faster execution (no network latency)
|
||||
- Stateful mocking (CRUD operations work)
|
||||
|
||||
### intercept-network-call
|
||||
|
||||
Spy or stub network requests with automatic JSON parsing.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/intercept-network-call.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Vanilla Playwright | interceptNetworkCall |
|
||||
|-------------------|----------------------|
|
||||
| Route setup + response waiting (separate steps) | Single declarative call |
|
||||
| Manual `await response.json()` | Automatic JSON parsing (`responseJson`) |
|
||||
| Complex filter predicates | Simple glob patterns (`**/api/**`) |
|
||||
| Verbose syntax | Concise, readable API |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('should handle API errors', async ({ page, interceptNetworkCall }) => {
|
||||
// Stub API to return error (set up BEFORE navigation)
|
||||
const profileCall = interceptNetworkCall({
|
||||
method: 'GET',
|
||||
url: '**/api/profile',
|
||||
fulfillResponse: {
|
||||
status: 500,
|
||||
body: { error: 'Server error' }
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/profile');
|
||||
|
||||
// Wait for the intercepted response
|
||||
const { status, responseJson } = await profileCall;
|
||||
|
||||
expect(status).toBe(500);
|
||||
expect(responseJson.error).toBe('Server error');
|
||||
await expect(page.getByText('Server error occurred')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Automatic JSON parsing (`responseJson` ready to use)
|
||||
- Spy mode (observe real traffic) or stub mode (mock responses)
|
||||
- Glob pattern URL matching
|
||||
- Returns promise with `{ status, responseJson, requestJson }`
|
||||
|
||||
### recurse
|
||||
|
||||
Async polling for eventual consistency (Cypress-style).
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/recurse.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Manual Polling | recurse Utility |
|
||||
|----------------|-----------------|
|
||||
| `while` loops with `waitForTimeout` | Smart polling with exponential backoff |
|
||||
| Hard-coded retry logic | Configurable timeout/interval |
|
||||
| No logging visibility | Optional logging with custom messages |
|
||||
| Verbose, error-prone | Clean, readable API |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('should wait for async job completion', async ({ apiRequest, recurse }) => {
|
||||
// Start async job
|
||||
const { body: job } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/jobs'
|
||||
});
|
||||
|
||||
// Poll until complete (smart waiting)
|
||||
const completed = await recurse(
|
||||
() => apiRequest({ method: 'GET', path: `/api/jobs/${job.id}` }),
|
||||
(result) => result.body.status === 'completed',
|
||||
{
|
||||
timeout: 30000,
|
||||
interval: 2000,
|
||||
log: 'Waiting for job to complete'
|
||||
}
|
||||
});
|
||||
|
||||
expect(completed.body.status).toBe('completed');
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Smart polling with configurable interval
|
||||
- Handles async jobs, background tasks
|
||||
- Optional logging for debugging
|
||||
- Better than hard waits or manual polling loops
|
||||
|
||||
### log
|
||||
|
||||
Structured logging that integrates with Playwright reports.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/log.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Console.log / print | log Utility |
|
||||
|--------------------|-------------|
|
||||
| Not in test reports | Integrated with Playwright reports |
|
||||
| No step visualization | `.step()` shows in Playwright UI |
|
||||
| Manual object formatting | Logs objects seamlessly |
|
||||
| No structured output | JSON artifacts for debugging |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { log } from '@seontechnologies/playwright-utils';
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('should login', async ({ page }) => {
|
||||
await log.info('Starting login test');
|
||||
|
||||
await page.goto('/login');
|
||||
await log.step('Navigated to login page'); // Shows in Playwright UI
|
||||
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await log.debug('Filled email field');
|
||||
|
||||
await log.success('Login completed');
|
||||
// Logs appear in test output and Playwright reports
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Direct import (no fixture needed for basic usage)
|
||||
- Structured logs in test reports
|
||||
- `.step()` shows in Playwright UI
|
||||
- Logs objects seamlessly (no special handling needed)
|
||||
- Trace test execution
|
||||
|
||||
### file-utils
|
||||
|
||||
Read and validate CSV, PDF, XLSX, ZIP files.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/file-utils.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Vanilla Playwright | file-utils |
|
||||
|-------------------|------------|
|
||||
| ~80 lines per CSV flow | ~10 lines end-to-end |
|
||||
| Manual download event handling | `handleDownload()` encapsulates all |
|
||||
| External parsing libraries | Auto-parsing (CSV, XLSX, PDF, ZIP) |
|
||||
| No validation helpers | Built-in validation (headers, row count) |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { handleDownload, readCSV } from '@seontechnologies/playwright-utils/file-utils';
|
||||
import { expect } from '@playwright/test';
|
||||
import path from 'node:path';
|
||||
|
||||
const DOWNLOAD_DIR = path.join(__dirname, '../downloads');
|
||||
|
||||
test('should export valid CSV', async ({ page }) => {
|
||||
// Handle download and get file path
|
||||
const downloadPath = await handleDownload({
|
||||
page,
|
||||
downloadDir: DOWNLOAD_DIR,
|
||||
trigger: () => page.click('button:has-text("Export")')
|
||||
});
|
||||
|
||||
// Read and parse CSV
|
||||
const csvResult = await readCSV({ filePath: downloadPath });
|
||||
const { data, headers } = csvResult.content;
|
||||
|
||||
// Validate structure
|
||||
expect(headers).toEqual(['Name', 'Email', 'Status']);
|
||||
expect(data.length).toBeGreaterThan(0);
|
||||
expect(data[0]).toMatchObject({
|
||||
Name: expect.any(String),
|
||||
Email: expect.any(String),
|
||||
Status: expect.any(String)
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Handles downloads automatically
|
||||
- Auto-parses CSV, XLSX, PDF, ZIP
|
||||
- Type-safe access to parsed data
|
||||
- Returns structured `{ headers, data }`
|
||||
|
||||
### burn-in
|
||||
|
||||
Smart test selection with git diff analysis for CI optimization.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/burn-in.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Playwright `--only-changed` | burn-in Utility |
|
||||
|-----------------------------|-----------------|
|
||||
| Config changes trigger all tests | Smart filtering (skip configs, types, docs) |
|
||||
| All or nothing | Volume control (run percentage) |
|
||||
| No customization | Custom dependency analysis |
|
||||
| Slow CI on minor changes | Fast CI with intelligent selection |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// scripts/burn-in-changed.ts
|
||||
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in';
|
||||
|
||||
async function main() {
|
||||
await runBurnIn({
|
||||
configPath: 'playwright.burn-in.config.ts',
|
||||
baseBranch: 'main'
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
**Config:**
|
||||
```typescript
|
||||
// playwright.burn-in.config.ts
|
||||
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in';
|
||||
|
||||
const config: BurnInConfig = {
|
||||
skipBurnInPatterns: [
|
||||
'**/config/**',
|
||||
'**/*.md',
|
||||
'**/*types*'
|
||||
],
|
||||
burnInTestPercentage: 0.3,
|
||||
burnIn: {
|
||||
repeatEach: 3,
|
||||
retries: 1
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
**Package script:**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test:burn-in": "tsx scripts/burn-in-changed.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Ensure flake-free tests upfront** - Never deal with test flake again
|
||||
- Smart filtering (skip config, types, docs changes)
|
||||
- Volume control (run percentage of affected tests)
|
||||
- Git diff-based test selection
|
||||
- Faster CI feedback
|
||||
|
||||
### network-error-monitor
|
||||
|
||||
Automatically detect HTTP 4xx/5xx errors during tests.
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/network-error-monitor.html>
|
||||
|
||||
**Why Use This?**
|
||||
|
||||
| Vanilla Playwright | network-error-monitor |
|
||||
|-------------------|----------------------|
|
||||
| UI passes, backend 500 ignored | Auto-fails on any 4xx/5xx |
|
||||
| Manual error checking | Zero boilerplate (auto-enabled) |
|
||||
| Silent failures slip through | Acts like Sentry for tests |
|
||||
| No domino effect prevention | Limits cascading failures |
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
|
||||
|
||||
// That's it! Network monitoring is automatically enabled
|
||||
test('should not have API errors', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await page.click('button');
|
||||
|
||||
// Test fails automatically if any HTTP 4xx/5xx errors occur
|
||||
// Error message shows: "Network errors detected: 2 request(s) failed"
|
||||
// GET 500 https://api.example.com/users
|
||||
// POST 503 https://api.example.com/metrics
|
||||
});
|
||||
```
|
||||
|
||||
**Opt-out for validation tests:**
|
||||
```typescript
|
||||
// When testing error scenarios, opt-out with annotation
|
||||
test('should show error message on 404',
|
||||
{ annotation: [{ type: 'skipNetworkMonitoring' }] }, // Array format
|
||||
async ({ page }) => {
|
||||
await page.goto('/invalid-page'); // Will 404
|
||||
await expect(page.getByText('Page not found')).toBeVisible();
|
||||
// Test won't fail on 404 because of annotation
|
||||
}
|
||||
);
|
||||
|
||||
// Or opt-out entire describe block
|
||||
test.describe('error handling',
|
||||
{ annotation: [{ type: 'skipNetworkMonitoring' }] },
|
||||
() => {
|
||||
test('handles 404', async ({ page }) => {
|
||||
// Monitoring disabled for all tests in block
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Auto-enabled (zero setup)
|
||||
- Catches silent backend failures (500, 503, 504)
|
||||
- **Prevents domino effect** (limits cascading failures from one bad endpoint)
|
||||
- Opt-out with annotations for validation tests
|
||||
- Structured error reporting (JSON artifacts)
|
||||
|
||||
## Fixture Composition
|
||||
|
||||
**Option 1: Use Package's Combined Fixtures (Simplest)**
|
||||
|
||||
```typescript
|
||||
// Import all utilities at once
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { log } from '@seontechnologies/playwright-utils';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('api test', async ({ apiRequest, interceptNetworkCall }) => {
|
||||
await log.info('Fetching users');
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users'
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
**Option 2: Create Custom Merged Fixtures (Selective)**
|
||||
|
||||
**File 1: support/merged-fixtures.ts**
|
||||
```typescript
|
||||
import { test as base, mergeTests } from '@playwright/test';
|
||||
import { test as apiRequest } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { test as interceptNetworkCall } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
|
||||
import { test as networkErrorMonitor } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
|
||||
import { log } from '@seontechnologies/playwright-utils';
|
||||
|
||||
// Merge only what you need
|
||||
export const test = mergeTests(
|
||||
base,
|
||||
apiRequest,
|
||||
interceptNetworkCall,
|
||||
networkErrorMonitor
|
||||
);
|
||||
|
||||
export const expect = base.expect;
|
||||
export { log };
|
||||
```
|
||||
|
||||
**File 2: tests/api/users.spec.ts**
|
||||
```typescript
|
||||
import { test, expect, log } from '../support/merged-fixtures';
|
||||
|
||||
test('api test', async ({ apiRequest, interceptNetworkCall }) => {
|
||||
await log.info('Fetching users');
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users'
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
**Contrast:**
|
||||
- Option 1: All utilities available, zero setup
|
||||
- Option 2: Pick utilities you need, one central file
|
||||
|
||||
**See working examples:** <https://github.com/seontechnologies/playwright-utils/tree/main/playwright/support>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Import Errors
|
||||
|
||||
**Problem:** Cannot find module '@seontechnologies/playwright-utils/api-request'
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify package installed
|
||||
npm list @seontechnologies/playwright-utils
|
||||
|
||||
# Check package.json has correct version
|
||||
"@seontechnologies/playwright-utils": "^2.0.0"
|
||||
|
||||
# Reinstall if needed
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
```
|
||||
|
||||
### TEA Not Using Utilities
|
||||
|
||||
**Problem:** TEA generates tests without playwright-utils.
|
||||
|
||||
**Causes:**
|
||||
1. Config not set: `tea_use_playwright_utils: false`
|
||||
2. Workflow run before config change
|
||||
3. Package not installed
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check config
|
||||
grep tea_use_playwright_utils _bmad/bmm/config.yaml
|
||||
|
||||
# Should show: tea_use_playwright_utils: true
|
||||
|
||||
# Start fresh chat (TEA loads config at start)
|
||||
```
|
||||
|
||||
### Type Errors with apiRequest
|
||||
|
||||
**Problem:** TypeScript errors on apiRequest response.
|
||||
|
||||
**Cause:** No schema validation.
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Add Zod schema for type safety
|
||||
import { z } from 'zod';
|
||||
|
||||
const ProfileSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/profile' // 'path' not 'url'
|
||||
}).validateSchema(ProfileSchema); // Chained method
|
||||
|
||||
expect(status).toBe(200);
|
||||
// body is typed as { id: string, name: string, email: string }
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
## Related Guides
|
||||
|
||||
**Getting Started:**
|
||||
- [TEA Lite Quickstart Tutorial](/docs/tea/tutorials/tea-lite-quickstart.md) - Learn TEA basics
|
||||
- [How to Set Up Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md) - Initial framework setup
|
||||
|
||||
**Workflow Guides:**
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Generate tests with utilities
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Expand coverage with utilities
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Review against PW-Utils patterns
|
||||
|
||||
**Other Customization:**
|
||||
- [Enable MCP Enhancements](/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md) - Live browser verification
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - **Why Playwright Utils matters** (part of TEA's three-part solution)
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Pure function → fixture pattern
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Network utilities explained
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Patterns PW-Utils enforces
|
||||
|
||||
## Reference
|
||||
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - tea_use_playwright_utils option
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Playwright Utils fragments
|
||||
- [Glossary](/docs/tea/glossary/index.md#test-architect-tea-concepts) - Playwright Utils term
|
||||
- [Official PW-Utils Docs](https://seontechnologies.github.io/playwright-utils/) - Complete API reference
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,436 +0,0 @@
|
||||
---
|
||||
title: "How to Run ATDD with TEA"
|
||||
description: Generate failing acceptance tests before implementation using TEA's ATDD workflow
|
||||
---
|
||||
|
||||
# How to Run ATDD with TEA
|
||||
|
||||
Use TEA's `atdd` workflow to generate failing acceptance tests BEFORE implementation. This is the TDD (Test-Driven Development) red phase - tests fail first, guide development, then pass.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- You're about to implement a NEW feature (feature doesn't exist yet)
|
||||
- You want to follow TDD workflow (red → green → refactor)
|
||||
- You want tests to guide your implementation
|
||||
- You're practicing acceptance test-driven development
|
||||
|
||||
**Don't use this if:**
|
||||
- Feature already exists (use `automate` instead)
|
||||
- You want tests that pass immediately
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- Test framework setup complete (run `framework` if needed)
|
||||
- Story or feature defined with acceptance criteria
|
||||
|
||||
**Note:** This guide uses Playwright examples. If using Cypress, commands and syntax will differ (e.g., `cy.get()` instead of `page.locator()`).
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load TEA Agent
|
||||
|
||||
Start a fresh chat and load TEA:
|
||||
|
||||
```
|
||||
tea
|
||||
```
|
||||
|
||||
### 2. Run the ATDD Workflow
|
||||
|
||||
```
|
||||
atdd
|
||||
```
|
||||
|
||||
### 3. Provide Context
|
||||
|
||||
TEA will ask for:
|
||||
|
||||
**Story/Feature Details:**
|
||||
```
|
||||
We're adding a user profile page where users can:
|
||||
- View their profile information
|
||||
- Edit their name and email
|
||||
- Upload a profile picture
|
||||
- Save changes with validation
|
||||
```
|
||||
|
||||
**Acceptance Criteria:**
|
||||
```
|
||||
Given I'm logged in
|
||||
When I navigate to /profile
|
||||
Then I see my current name and email
|
||||
|
||||
Given I'm on the profile page
|
||||
When I click "Edit Profile"
|
||||
Then I can modify my name and email
|
||||
|
||||
Given I've edited my profile
|
||||
When I click "Save"
|
||||
Then my changes are persisted
|
||||
And I see a success message
|
||||
|
||||
Given I upload an invalid file type
|
||||
When I try to save
|
||||
Then I see an error message
|
||||
And changes are not saved
|
||||
```
|
||||
|
||||
**Reference Documents** (optional):
|
||||
- Point to your story file
|
||||
- Reference PRD or tech spec
|
||||
- Link to test design (if you ran `test-design` first)
|
||||
|
||||
### 4. Specify Test Levels
|
||||
|
||||
TEA will ask what test levels to generate:
|
||||
|
||||
**Options:**
|
||||
- E2E tests (browser-based, full user journey)
|
||||
- API tests (backend only, faster)
|
||||
- Component tests (UI components in isolation)
|
||||
- Mix of levels (see [API Tests First, E2E Later](#api-tests-first-e2e-later) tip)
|
||||
|
||||
### Component Testing by Framework
|
||||
|
||||
TEA generates component tests using framework-appropriate tools:
|
||||
|
||||
| Your Framework | Component Testing Tool |
|
||||
| -------------- | ------------------------------------------- |
|
||||
| **Cypress** | Cypress Component Testing (*.cy.tsx) |
|
||||
| **Playwright** | Vitest + React Testing Library (*.test.tsx) |
|
||||
|
||||
**Example response:**
|
||||
```
|
||||
Generate:
|
||||
- API tests for profile CRUD operations
|
||||
- E2E tests for the complete profile editing flow
|
||||
- Component tests for ProfileForm validation (if using Cypress or Vitest)
|
||||
- Focus on P0 and P1 scenarios
|
||||
```
|
||||
|
||||
### 5. Review Generated Tests
|
||||
|
||||
TEA generates **failing tests** in appropriate directories:
|
||||
|
||||
#### API Tests (`tests/api/profile.spec.ts`):
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Profile API', () => {
|
||||
test('should fetch user profile', async ({ request }) => {
|
||||
const response = await request.get('/api/profile');
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
const profile = await response.json();
|
||||
expect(profile).toHaveProperty('name');
|
||||
expect(profile).toHaveProperty('email');
|
||||
expect(profile).toHaveProperty('avatarUrl');
|
||||
});
|
||||
|
||||
test('should update user profile', async ({ request }) => {
|
||||
const response = await request.patch('/api/profile', {
|
||||
data: {
|
||||
name: 'Updated Name',
|
||||
email: 'updated@example.com'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
const updated = await response.json();
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
expect(updated.email).toBe('updated@example.com');
|
||||
});
|
||||
|
||||
test('should validate email format', async ({ request }) => {
|
||||
const response = await request.patch('/api/profile', {
|
||||
data: {
|
||||
email: 'invalid-email'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(400);
|
||||
const error = await response.json();
|
||||
expect(error.message).toContain('Invalid email format');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ProfileSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
avatarUrl: z.string().url()
|
||||
});
|
||||
|
||||
test.describe('Profile API', () => {
|
||||
test('should fetch user profile', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/profile'
|
||||
}).validateSchema(ProfileSchema); // Chained validation
|
||||
|
||||
expect(status).toBe(200);
|
||||
// Schema already validated, type-safe access
|
||||
expect(body.name).toBeDefined();
|
||||
expect(body.email).toContain('@');
|
||||
});
|
||||
|
||||
test('should update user profile', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: {
|
||||
name: 'Updated Name',
|
||||
email: 'updated@example.com'
|
||||
}
|
||||
}).validateSchema(ProfileSchema); // Chained validation
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.name).toBe('Updated Name');
|
||||
expect(body.email).toBe('updated@example.com');
|
||||
});
|
||||
|
||||
test('should validate email format', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { email: 'invalid-email' }
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body.message).toContain('Invalid email format');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Benefits:**
|
||||
- Returns `{ status, body }` (cleaner than `response.status()` + `await response.json()`)
|
||||
- Automatic schema validation with Zod
|
||||
- Type-safe response bodies
|
||||
- Automatic retry for 5xx errors
|
||||
- Less boilerplate
|
||||
|
||||
#### E2E Tests (`tests/e2e/profile.spec.ts`):
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('should edit and save profile', async ({ page }) => {
|
||||
// Login first
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
// Navigate to profile
|
||||
await page.goto('/profile');
|
||||
|
||||
// Edit profile
|
||||
await page.getByRole('button', { name: 'Edit Profile' }).click();
|
||||
await page.getByLabel('Name').fill('Updated Name');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Verify success
|
||||
await expect(page.getByText('Profile updated')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
TEA generates additional E2E tests for display, validation errors, etc. based on acceptance criteria.
|
||||
|
||||
#### Implementation Checklist
|
||||
|
||||
TEA also provides an implementation checklist:
|
||||
|
||||
```markdown
|
||||
## Implementation Checklist
|
||||
|
||||
### Backend
|
||||
- [ ] Create `GET /api/profile` endpoint
|
||||
- [ ] Create `PATCH /api/profile` endpoint
|
||||
- [ ] Add email validation middleware
|
||||
- [ ] Add profile picture upload handling
|
||||
- [ ] Write API unit tests
|
||||
|
||||
### Frontend
|
||||
- [ ] Create ProfilePage component
|
||||
- [ ] Implement profile form with validation
|
||||
- [ ] Add file upload for avatar
|
||||
- [ ] Handle API errors gracefully
|
||||
- [ ] Add loading states
|
||||
|
||||
### Tests
|
||||
- [x] API tests generated (failing)
|
||||
- [x] E2E tests generated (failing)
|
||||
- [ ] Run tests after implementation (should pass)
|
||||
```
|
||||
|
||||
### 6. Verify Tests Fail
|
||||
|
||||
This is the TDD red phase - tests MUST fail before implementation.
|
||||
|
||||
**For Playwright:**
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
**For Cypress:**
|
||||
```bash
|
||||
npx cypress run
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Running 6 tests using 1 worker
|
||||
|
||||
✗ tests/api/profile.spec.ts:3:3 › should fetch user profile
|
||||
Error: expect(received).toBe(expected)
|
||||
Expected: 200
|
||||
Received: 404
|
||||
|
||||
✗ tests/e2e/profile.spec.ts:10:3 › should display current profile information
|
||||
Error: page.goto: net::ERR_ABORTED
|
||||
```
|
||||
|
||||
**All tests should fail!** This confirms:
|
||||
- Feature doesn't exist yet
|
||||
- Tests will guide implementation
|
||||
- You have clear success criteria
|
||||
|
||||
### 7. Implement the Feature
|
||||
|
||||
Now implement the feature following the test guidance:
|
||||
|
||||
1. Start with API tests (backend first)
|
||||
2. Make API tests pass
|
||||
3. Move to E2E tests (frontend)
|
||||
4. Make E2E tests pass
|
||||
5. Refactor with confidence (tests protect you)
|
||||
|
||||
### 8. Verify Tests Pass
|
||||
|
||||
After implementation, run your test suite.
|
||||
|
||||
**For Playwright:**
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
**For Cypress:**
|
||||
```bash
|
||||
npx cypress run
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Running 6 tests using 1 worker
|
||||
|
||||
✓ tests/api/profile.spec.ts:3:3 › should fetch user profile (850ms)
|
||||
✓ tests/api/profile.spec.ts:15:3 › should update user profile (1.2s)
|
||||
✓ tests/api/profile.spec.ts:30:3 › should validate email format (650ms)
|
||||
✓ tests/e2e/profile.spec.ts:10:3 › should display current profile (2.1s)
|
||||
✓ tests/e2e/profile.spec.ts:18:3 › should edit and save profile (3.2s)
|
||||
✓ tests/e2e/profile.spec.ts:35:3 › should show validation error (1.8s)
|
||||
|
||||
6 passed (9.8s)
|
||||
```
|
||||
|
||||
**Green!** You've completed the TDD cycle: red → green → refactor.
|
||||
|
||||
## What You Get
|
||||
|
||||
### Failing Tests
|
||||
- API tests for backend endpoints
|
||||
- E2E tests for user workflows
|
||||
- Component tests (if requested)
|
||||
- All tests fail initially (red phase)
|
||||
|
||||
### Implementation Guidance
|
||||
- Clear checklist of what to build
|
||||
- Acceptance criteria translated to assertions
|
||||
- Edge cases and error scenarios identified
|
||||
|
||||
### TDD Workflow Support
|
||||
- Tests guide implementation
|
||||
- Confidence to refactor
|
||||
- Living documentation of features
|
||||
|
||||
## Tips
|
||||
|
||||
### Start with Test Design
|
||||
|
||||
Run `test-design` before `atdd` for better results:
|
||||
|
||||
```
|
||||
test-design # Risk assessment and priorities
|
||||
atdd # Generate tests based on design
|
||||
```
|
||||
|
||||
### MCP Enhancements (Optional)
|
||||
|
||||
If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `atdd`.
|
||||
|
||||
**Note:** ATDD is for features that don't exist yet, so recording mode (verify selectors with live UI) only applies if you have skeleton/mockup UI already implemented. For typical ATDD (no UI yet), TEA infers selectors from best practices.
|
||||
|
||||
See [Enable MCP Enhancements](/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
|
||||
|
||||
### Focus on P0/P1 Scenarios
|
||||
|
||||
Don't generate tests for everything at once:
|
||||
|
||||
```
|
||||
Generate tests for:
|
||||
- P0: Critical path (happy path)
|
||||
- P1: High value (validation, errors)
|
||||
|
||||
Skip P2/P3 for now - add later with automate
|
||||
```
|
||||
|
||||
### API Tests First, E2E Later
|
||||
|
||||
Recommended order:
|
||||
1. Generate API tests with `atdd`
|
||||
2. Implement backend (make API tests pass)
|
||||
3. Generate E2E tests with `atdd` (or `automate`)
|
||||
4. Implement frontend (make E2E tests pass)
|
||||
|
||||
This "outside-in" approach is faster and more reliable.
|
||||
|
||||
### Keep Tests Deterministic
|
||||
|
||||
TEA generates deterministic tests by default:
|
||||
- No hard waits (`waitForTimeout`)
|
||||
- Network-first patterns (wait for responses)
|
||||
- Explicit assertions (no conditionals)
|
||||
|
||||
Don't modify these patterns - they prevent flakiness!
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Plan before generating
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Tests for existing features
|
||||
- [How to Set Up Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md) - Initial setup
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - **Why TEA generates quality tests** (foundational)
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Why P0 vs P3 matters
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - What makes tests good
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Avoiding flakiness
|
||||
|
||||
## Reference
|
||||
|
||||
- [Command: *atdd](/docs/tea/reference/commands.md#atdd) - Full command reference
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - MCP and Playwright Utils options
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,653 +0,0 @@
|
||||
---
|
||||
title: "How to Run Automate with TEA"
|
||||
description: Expand test automation coverage after implementation using TEA's automate workflow
|
||||
---
|
||||
|
||||
# How to Run Automate with TEA
|
||||
|
||||
Use TEA's `automate` workflow to generate comprehensive tests for existing features. Unlike `*atdd`, these tests pass immediately because the feature already exists.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Feature already exists and works
|
||||
- Want to add test coverage to existing code
|
||||
- Need tests that pass immediately
|
||||
- Expanding existing test suite
|
||||
- Adding tests to legacy code
|
||||
|
||||
**Don't use this if:**
|
||||
- Feature doesn't exist yet (use `atdd` instead)
|
||||
- Want failing tests to guide development (use `atdd` for TDD)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- Test framework setup complete (run `framework` if needed)
|
||||
- Feature implemented and working
|
||||
|
||||
**Note:** This guide uses Playwright examples. If using Cypress, commands and syntax will differ.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load TEA Agent
|
||||
|
||||
Start a fresh chat and load TEA:
|
||||
|
||||
```
|
||||
tea
|
||||
```
|
||||
|
||||
### 2. Run the Automate Workflow
|
||||
|
||||
```
|
||||
automate
|
||||
```
|
||||
|
||||
### 3. Provide Context
|
||||
|
||||
TEA will ask for context about what you're testing.
|
||||
|
||||
#### Option A: BMad-Integrated Mode (Recommended)
|
||||
|
||||
If you have BMad artifacts (stories, test designs, PRDs):
|
||||
|
||||
**What are you testing?**
|
||||
```
|
||||
I'm testing the user profile feature we just implemented.
|
||||
Story: story-profile-management.md
|
||||
Test Design: test-design-epic-1.md
|
||||
```
|
||||
|
||||
**Reference documents:**
|
||||
- Story file with acceptance criteria
|
||||
- Test design document (if available)
|
||||
- PRD sections relevant to this feature
|
||||
- Tech spec (if available)
|
||||
|
||||
**Existing tests:**
|
||||
```
|
||||
We have basic tests in tests/e2e/profile-view.spec.ts
|
||||
Avoid duplicating that coverage
|
||||
```
|
||||
|
||||
TEA will analyze your artifacts and generate comprehensive tests that:
|
||||
- Cover acceptance criteria from the story
|
||||
- Follow priorities from test design (P0 → P1 → P2)
|
||||
- Avoid duplicating existing tests
|
||||
- Include edge cases and error scenarios
|
||||
|
||||
#### Option B: Standalone Mode
|
||||
|
||||
If you're using TEA Solo or don't have BMad artifacts:
|
||||
|
||||
**What are you testing?**
|
||||
```
|
||||
TodoMVC React application at https://todomvc.com/examples/react/dist/
|
||||
Features: Create todos, mark as complete, filter by status, delete todos
|
||||
```
|
||||
|
||||
**Specific scenarios to cover:**
|
||||
```
|
||||
- Creating todos (happy path)
|
||||
- Marking todos as complete/incomplete
|
||||
- Filtering (All, Active, Completed)
|
||||
- Deleting todos
|
||||
- Edge cases (empty input, long text)
|
||||
```
|
||||
|
||||
TEA will analyze the application and generate tests based on your description.
|
||||
|
||||
### 4. Specify Test Levels
|
||||
|
||||
TEA will ask which test levels to generate:
|
||||
|
||||
**Options:**
|
||||
- **E2E tests** - Full browser-based user workflows
|
||||
- **API tests** - Backend endpoint testing (faster, more reliable)
|
||||
- **Component tests** - UI component testing in isolation (framework-dependent)
|
||||
- **Mix** - Combination of levels (recommended)
|
||||
|
||||
**Example response:**
|
||||
```
|
||||
Generate:
|
||||
- API tests for all CRUD operations
|
||||
- E2E tests for critical user workflows (P0)
|
||||
- Focus on P0 and P1 scenarios
|
||||
- Skip P3 (low priority edge cases)
|
||||
```
|
||||
|
||||
### 5. Review Generated Tests
|
||||
|
||||
TEA generates a comprehensive test suite with multiple test levels.
|
||||
|
||||
#### API Tests (`tests/api/profile.spec.ts`):
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Profile API', () => {
|
||||
let authToken: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
// Manual auth token fetch
|
||||
const response = await request.post('/api/auth/login', {
|
||||
data: { email: 'test@example.com', password: 'password123' }
|
||||
});
|
||||
const { token } = await response.json();
|
||||
authToken = token;
|
||||
});
|
||||
|
||||
test('should fetch user profile', async ({ request }) => {
|
||||
const response = await request.get('/api/profile', {
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const profile = await response.json();
|
||||
expect(profile).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: expect.any(String),
|
||||
email: expect.any(String)
|
||||
});
|
||||
});
|
||||
|
||||
test('should update profile successfully', async ({ request }) => {
|
||||
const response = await request.patch('/api/profile', {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
data: {
|
||||
name: 'Updated Name',
|
||||
bio: 'Test bio'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const updated = await response.json();
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
expect(updated.bio).toBe('Test bio');
|
||||
});
|
||||
|
||||
test('should validate email format', async ({ request }) => {
|
||||
const response = await request.patch('/api/profile', {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
data: { email: 'invalid-email' }
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(400);
|
||||
const error = await response.json();
|
||||
expect(error.message).toContain('Invalid email');
|
||||
});
|
||||
|
||||
test('should require authentication', async ({ request }) => {
|
||||
const response = await request.get('/api/profile');
|
||||
expect(response.status()).toBe(401);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { createAuthFixtures } from '@seontechnologies/playwright-utils/auth-session';
|
||||
import { mergeTests } from '@playwright/test';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ProfileSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
// Merge API and auth fixtures
|
||||
const authFixtureTest = base.extend(createAuthFixtures());
|
||||
export const testWithAuth = mergeTests(apiRequestFixture, authFixtureTest);
|
||||
|
||||
testWithAuth.describe('Profile API', () => {
|
||||
testWithAuth('should fetch user profile', async ({ apiRequest, authToken }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/profile',
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
}).validateSchema(ProfileSchema); // Chained validation
|
||||
|
||||
expect(status).toBe(200);
|
||||
// Schema already validated, type-safe access
|
||||
expect(body.name).toBeDefined();
|
||||
});
|
||||
|
||||
testWithAuth('should update profile successfully', async ({ apiRequest, authToken }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { name: 'Updated Name', bio: 'Test bio' },
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
}).validateSchema(ProfileSchema); // Chained validation
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.name).toBe('Updated Name');
|
||||
});
|
||||
|
||||
testWithAuth('should validate email format', async ({ apiRequest, authToken }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { email: 'invalid-email' },
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body.message).toContain('Invalid email');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Differences:**
|
||||
- `authToken` fixture (persisted, reused across tests)
|
||||
- `apiRequest` returns `{ status, body }` (cleaner)
|
||||
- Schema validation with Zod (type-safe)
|
||||
- Automatic retry for 5xx errors
|
||||
- Less boilerplate (no manual `await response.json()` everywhere)
|
||||
|
||||
#### E2E Tests (`tests/e2e/profile.spec.ts`):
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('should edit profile', async ({ page }) => {
|
||||
// Login
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
// Edit profile
|
||||
await page.goto('/profile');
|
||||
await page.getByRole('button', { name: 'Edit Profile' }).click();
|
||||
await page.getByLabel('Name').fill('New Name');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Verify success
|
||||
await expect(page.getByText('Profile updated')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
TEA generates additional tests for validation, edge cases, etc. based on priorities.
|
||||
|
||||
#### Fixtures (`tests/support/fixtures/profile.ts`):
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
import { test as base, Page } from '@playwright/test';
|
||||
|
||||
type ProfileFixtures = {
|
||||
authenticatedPage: Page;
|
||||
testProfile: {
|
||||
name: string;
|
||||
email: string;
|
||||
bio: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const test = base.extend<ProfileFixtures>({
|
||||
authenticatedPage: async ({ page }, use) => {
|
||||
// Manual login flow
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
|
||||
await use(page);
|
||||
},
|
||||
|
||||
testProfile: async ({ request }, use) => {
|
||||
// Static test data
|
||||
const profile = {
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
bio: 'Test bio'
|
||||
};
|
||||
|
||||
await use(profile);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test as base } from '@playwright/test';
|
||||
import { createAuthFixtures } from '@seontechnologies/playwright-utils/auth-session';
|
||||
import { mergeTests } from '@playwright/test';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
type ProfileFixtures = {
|
||||
testProfile: {
|
||||
name: string;
|
||||
email: string;
|
||||
bio: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Merge auth fixtures with custom fixtures
|
||||
const authTest = base.extend(createAuthFixtures());
|
||||
const profileTest = base.extend<ProfileFixtures>({
|
||||
testProfile: async ({}, use) => {
|
||||
// Dynamic test data with faker
|
||||
const profile = {
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
bio: faker.person.bio()
|
||||
};
|
||||
|
||||
await use(profile);
|
||||
}
|
||||
});
|
||||
|
||||
export const test = mergeTests(authTest, profileTest);
|
||||
export { expect } from '@playwright/test';
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { test, expect } from '../support/fixtures/profile';
|
||||
|
||||
test('should update profile', async ({ page, authToken, testProfile }) => {
|
||||
// authToken from auth-session (automatic, persisted)
|
||||
// testProfile from custom fixture (dynamic data)
|
||||
|
||||
await page.goto('/profile');
|
||||
// Test with dynamic, unique data
|
||||
});
|
||||
```
|
||||
|
||||
**Key Benefits:**
|
||||
- `authToken` fixture (persisted token, no manual login)
|
||||
- Dynamic test data with faker (no conflicts)
|
||||
- Fixture composition with mergeTests
|
||||
- Reusable across test files
|
||||
|
||||
### 6. Review Additional Artifacts
|
||||
|
||||
TEA also generates:
|
||||
|
||||
#### Updated README (`tests/README.md`):
|
||||
|
||||
```markdown
|
||||
# Test Suite
|
||||
|
||||
## Running Tests
|
||||
|
||||
### All Tests
|
||||
npm test
|
||||
|
||||
### Specific Levels
|
||||
npm run test:api # API tests only
|
||||
npm run test:e2e # E2E tests only
|
||||
npm run test:smoke # Smoke tests (@smoke tag)
|
||||
|
||||
### Single File
|
||||
npx playwright test tests/api/profile.spec.ts
|
||||
|
||||
## Test Structure
|
||||
|
||||
tests/
|
||||
├── api/ # API tests (fast, reliable)
|
||||
├── e2e/ # E2E tests (full workflows)
|
||||
├── fixtures/ # Shared test utilities
|
||||
└── README.md
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Follow the patterns in existing tests:
|
||||
- Use fixtures for authentication
|
||||
- Network-first patterns (no hard waits)
|
||||
- Explicit assertions
|
||||
- Self-cleaning tests
|
||||
```
|
||||
|
||||
#### Definition of Done Summary:
|
||||
|
||||
```markdown
|
||||
## Test Quality Checklist
|
||||
|
||||
✅ All tests pass on first run
|
||||
✅ No hard waits (waitForTimeout)
|
||||
✅ No conditionals for flow control
|
||||
✅ Assertions are explicit
|
||||
✅ Tests clean up after themselves
|
||||
✅ Tests can run in parallel
|
||||
✅ Execution time < 1.5 minutes per test
|
||||
✅ Test files < 300 lines
|
||||
```
|
||||
|
||||
### 7. Run the Tests
|
||||
|
||||
All tests should pass immediately since the feature exists:
|
||||
|
||||
**For Playwright:**
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
**For Cypress:**
|
||||
```bash
|
||||
npx cypress run
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Running 15 tests using 4 workers
|
||||
|
||||
✓ tests/api/profile.spec.ts (4 tests) - 2.1s
|
||||
✓ tests/e2e/profile-workflow.spec.ts (2 tests) - 5.3s
|
||||
|
||||
15 passed (7.4s)
|
||||
```
|
||||
|
||||
**All green!** Tests pass because feature already exists.
|
||||
|
||||
### 8. Review Test Coverage
|
||||
|
||||
Check which scenarios are covered:
|
||||
|
||||
```bash
|
||||
# View test report
|
||||
npx playwright show-report
|
||||
|
||||
# Check coverage (if configured)
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
Compare against:
|
||||
- Acceptance criteria from story
|
||||
- Test priorities from test design
|
||||
- Edge cases and error scenarios
|
||||
|
||||
## What You Get
|
||||
|
||||
### Comprehensive Test Suite
|
||||
- **API tests** - Fast, reliable backend testing
|
||||
- **E2E tests** - Critical user workflows
|
||||
- **Component tests** - UI component testing (if requested)
|
||||
- **Fixtures** - Shared utilities and setup
|
||||
|
||||
### Component Testing by Framework
|
||||
|
||||
TEA supports component testing using framework-appropriate tools:
|
||||
|
||||
| Your Framework | Component Testing Tool | Tests Location |
|
||||
| -------------- | ------------------------------ | ----------------------------------------- |
|
||||
| **Cypress** | Cypress Component Testing | `tests/component/` |
|
||||
| **Playwright** | Vitest + React Testing Library | `tests/component/` or `src/**/*.test.tsx` |
|
||||
|
||||
**Note:** Component tests use separate tooling from E2E tests:
|
||||
- Cypress users: TEA generates Cypress Component Tests
|
||||
- Playwright users: TEA generates Vitest + React Testing Library tests
|
||||
|
||||
### Quality Features
|
||||
- **Network-first patterns** - Wait for actual responses, not timeouts
|
||||
- **Deterministic tests** - No flakiness, no conditionals
|
||||
- **Self-cleaning** - Tests don't leave test data behind
|
||||
- **Parallel-safe** - Can run all tests concurrently
|
||||
|
||||
### Documentation
|
||||
- **Updated README** - How to run tests
|
||||
- **Test structure explanation** - Where tests live
|
||||
- **Definition of Done** - Quality standards
|
||||
|
||||
## Tips
|
||||
|
||||
### Start with Test Design
|
||||
|
||||
Run `test-design` before `automate` for better results:
|
||||
|
||||
```
|
||||
test-design # Risk assessment, priorities
|
||||
automate # Generate tests based on priorities
|
||||
```
|
||||
|
||||
TEA will focus on P0/P1 scenarios and skip low-value tests.
|
||||
|
||||
### Prioritize Test Levels
|
||||
|
||||
Not everything needs E2E tests:
|
||||
|
||||
**Good strategy:**
|
||||
```
|
||||
- P0 scenarios: API + E2E tests
|
||||
- P1 scenarios: API tests only
|
||||
- P2 scenarios: API tests (happy path)
|
||||
- P3 scenarios: Skip or add later
|
||||
```
|
||||
|
||||
**Why?**
|
||||
- API tests are 10x faster than E2E
|
||||
- API tests are more reliable (no browser flakiness)
|
||||
- E2E tests reserved for critical user journeys
|
||||
|
||||
### Avoid Duplicate Coverage
|
||||
|
||||
Tell TEA about existing tests:
|
||||
|
||||
```
|
||||
We already have tests in:
|
||||
- tests/e2e/profile-view.spec.ts (viewing profile)
|
||||
- tests/api/auth.spec.ts (authentication)
|
||||
|
||||
Don't duplicate that coverage
|
||||
```
|
||||
|
||||
TEA will analyze existing tests and only generate new scenarios.
|
||||
|
||||
### MCP Enhancements (Optional)
|
||||
|
||||
If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `automate` for:
|
||||
|
||||
- **Healing mode:** Fix broken selectors, update assertions, enhance with trace analysis
|
||||
- **Recording mode:** Verify selectors with live browser, capture network requests
|
||||
|
||||
No prompts - TEA uses MCPs automatically when available. See [Enable MCP Enhancements](/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
|
||||
|
||||
### Generate Tests Incrementally
|
||||
|
||||
Don't generate all tests at once:
|
||||
|
||||
**Iteration 1:**
|
||||
```
|
||||
Generate P0 tests only (critical path)
|
||||
Run: automate
|
||||
```
|
||||
|
||||
**Iteration 2:**
|
||||
```
|
||||
Generate P1 tests (high value scenarios)
|
||||
Run: automate
|
||||
Tell TEA to avoid P0 coverage
|
||||
```
|
||||
|
||||
**Iteration 3:**
|
||||
```
|
||||
Generate P2 tests (if time permits)
|
||||
Run: automate
|
||||
```
|
||||
|
||||
This iterative approach:
|
||||
- Provides fast feedback
|
||||
- Allows validation before proceeding
|
||||
- Keeps test generation focused
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Tests Pass But Coverage Is Incomplete
|
||||
|
||||
**Problem:** Tests pass but don't cover all scenarios.
|
||||
|
||||
**Cause:** TEA wasn't given complete context.
|
||||
|
||||
**Solution:** Provide more details:
|
||||
```
|
||||
Generate tests for:
|
||||
- All acceptance criteria in story-profile.md
|
||||
- Error scenarios (validation, authorization)
|
||||
- Edge cases (empty fields, long inputs)
|
||||
```
|
||||
|
||||
### Too Many Tests Generated
|
||||
|
||||
**Problem:** TEA generated 50 tests for a simple feature.
|
||||
|
||||
**Cause:** Didn't specify priorities or scope.
|
||||
|
||||
**Solution:** Be specific:
|
||||
```
|
||||
Generate ONLY:
|
||||
- P0 and P1 scenarios
|
||||
- API tests for all scenarios
|
||||
- E2E tests only for critical workflows
|
||||
- Skip P2/P3 for now
|
||||
```
|
||||
|
||||
### Tests Duplicate Existing Coverage
|
||||
|
||||
**Problem:** New tests cover the same scenarios as existing tests.
|
||||
|
||||
**Cause:** Didn't tell TEA about existing tests.
|
||||
|
||||
**Solution:** Specify existing coverage:
|
||||
```
|
||||
We already have these tests:
|
||||
- tests/api/profile.spec.ts (GET /api/profile)
|
||||
- tests/e2e/profile-view.spec.ts (viewing profile)
|
||||
|
||||
Generate tests for scenarios NOT covered by those files
|
||||
```
|
||||
|
||||
### MCP Enhancements for Better Selectors
|
||||
|
||||
If you have MCP servers configured, TEA verifies selectors against live browser. Otherwise, TEA generates accessible selectors (`getByRole`, `getByLabel`) by default.
|
||||
|
||||
Setup: Answer "Yes" to MCPs in BMad installer + configure MCP servers in your IDE. See [Enable MCP Enhancements](/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md).
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Plan before generating
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Failing tests before implementation
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Audit generated quality
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - **Why TEA generates quality tests** (foundational)
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Why prioritize P0 over P3
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - What makes tests good
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Reusable test patterns
|
||||
|
||||
## Reference
|
||||
|
||||
- [Command: *automate](/docs/tea/reference/commands.md#automate) - Full command reference
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - MCP and Playwright Utils options
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,679 +0,0 @@
|
||||
---
|
||||
title: "How to Run NFR Assessment with TEA"
|
||||
description: Validate non-functional requirements for security, performance, reliability, and maintainability using TEA
|
||||
---
|
||||
|
||||
# How to Run NFR Assessment with TEA
|
||||
|
||||
Use TEA's `nfr-assess` workflow to validate non-functional requirements (NFRs) with evidence-based assessment across security, performance, reliability, and maintainability.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Enterprise projects with compliance requirements
|
||||
- Projects with strict NFR thresholds
|
||||
- Before production release
|
||||
- When NFRs are critical to project success
|
||||
- Security or performance is mission-critical
|
||||
|
||||
**Best for:**
|
||||
- Enterprise track projects
|
||||
- Compliance-heavy industries (finance, healthcare, government)
|
||||
- High-traffic applications
|
||||
- Security-critical systems
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- NFRs defined in PRD or requirements doc
|
||||
- Evidence preferred but not required (test results, security scans, performance metrics)
|
||||
|
||||
**Note:** You can run NFR assessment without complete evidence. TEA will mark categories as CONCERNS where evidence is missing and document what's needed.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Run the NFR Assessment Workflow
|
||||
|
||||
Start a fresh chat and run:
|
||||
|
||||
```
|
||||
nfr-assess
|
||||
```
|
||||
|
||||
This loads TEA and starts the NFR assessment workflow.
|
||||
|
||||
### 2. Specify NFR Categories
|
||||
|
||||
TEA will ask which NFR categories to assess.
|
||||
|
||||
**Available Categories:**
|
||||
|
||||
| Category | Focus Areas |
|
||||
|----------|-------------|
|
||||
| **Security** | Authentication, authorization, encryption, vulnerabilities, security headers, input validation |
|
||||
| **Performance** | Response time, throughput, resource usage, database queries, frontend load time |
|
||||
| **Reliability** | Error handling, recovery mechanisms, availability, failover, data backup |
|
||||
| **Maintainability** | Code quality, test coverage, technical debt, documentation, dependency health |
|
||||
|
||||
**Example Response:**
|
||||
```
|
||||
Assess:
|
||||
- Security (critical for user data)
|
||||
- Performance (API must be fast)
|
||||
- Reliability (99.9% uptime requirement)
|
||||
|
||||
Skip maintainability for now
|
||||
```
|
||||
|
||||
### 3. Provide NFR Thresholds
|
||||
|
||||
TEA will ask for specific thresholds for each category.
|
||||
|
||||
**Critical Principle: Never guess thresholds.**
|
||||
|
||||
If you don't know the exact requirement, tell TEA to mark as CONCERNS and request clarification from stakeholders.
|
||||
|
||||
#### Security Thresholds
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Requirements:
|
||||
- All endpoints require authentication: YES
|
||||
- Data encrypted at rest: YES (PostgreSQL TDE)
|
||||
- Zero critical vulnerabilities: YES (npm audit)
|
||||
- Input validation on all endpoints: YES (Zod schemas)
|
||||
- Security headers configured: YES (helmet.js)
|
||||
```
|
||||
|
||||
#### Performance Thresholds
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Requirements:
|
||||
- API response time P99: < 200ms
|
||||
- API response time P95: < 150ms
|
||||
- Throughput: > 1000 requests/second
|
||||
- Frontend initial load: < 2 seconds
|
||||
- Database query time P99: < 50ms
|
||||
```
|
||||
|
||||
#### Reliability Thresholds
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Requirements:
|
||||
- Error handling: All endpoints return structured errors
|
||||
- Availability: 99.9% uptime
|
||||
- Recovery time: < 5 minutes (RTO)
|
||||
- Data backup: Daily automated backups
|
||||
- Failover: Automatic with < 30s downtime
|
||||
```
|
||||
|
||||
#### Maintainability Thresholds
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Requirements:
|
||||
- Test coverage: > 80%
|
||||
- Code quality: SonarQube grade A
|
||||
- Documentation: All APIs documented
|
||||
- Dependency age: < 6 months outdated
|
||||
- Technical debt: < 10% of codebase
|
||||
```
|
||||
|
||||
### 4. Provide Evidence
|
||||
|
||||
TEA will ask where to find evidence for each requirement.
|
||||
|
||||
**Evidence Sources:**
|
||||
|
||||
| Category | Evidence Type | Location |
|
||||
|----------|---------------|----------|
|
||||
| Security | Security scan reports | `/reports/security-scan.pdf` |
|
||||
| Security | Vulnerability scan | `npm audit`, `snyk test` results |
|
||||
| Security | Auth test results | Test reports showing auth coverage |
|
||||
| Performance | Load test results | `/reports/k6-load-test.json` |
|
||||
| Performance | APM data | Datadog, New Relic dashboards |
|
||||
| Performance | Lighthouse scores | `/reports/lighthouse.json` |
|
||||
| Reliability | Error rate metrics | Production monitoring dashboards |
|
||||
| Reliability | Uptime data | StatusPage, PagerDuty logs |
|
||||
| Maintainability | Coverage reports | `/reports/coverage/index.html` |
|
||||
| Maintainability | Code quality | SonarQube dashboard |
|
||||
|
||||
**Example Response:**
|
||||
```
|
||||
Evidence:
|
||||
- Security: npm audit results (clean), auth tests 15/15 passing
|
||||
- Performance: k6 load test at /reports/k6-results.json
|
||||
- Reliability: Error rate 0.01% in staging (logs in Datadog)
|
||||
|
||||
Don't have:
|
||||
- Uptime data (new system, no baseline)
|
||||
- Mark as CONCERNS and request monitoring setup
|
||||
```
|
||||
|
||||
### 5. Review NFR Assessment Report
|
||||
|
||||
TEA generates a comprehensive assessment report.
|
||||
|
||||
#### Assessment Report (`nfr-assessment.md`):
|
||||
|
||||
```markdown
|
||||
# Non-Functional Requirements Assessment
|
||||
|
||||
**Date:** 2026-01-13
|
||||
**Epic:** User Profile Management
|
||||
**Release:** v1.2.0
|
||||
**Overall Decision:** CONCERNS ⚠️
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Category | Status | Critical Issues |
|
||||
|----------|--------|-----------------|
|
||||
| Security | PASS ✅ | 0 |
|
||||
| Performance | CONCERNS ⚠️ | 2 |
|
||||
| Reliability | PASS ✅ | 0 |
|
||||
| Maintainability | PASS ✅ | 0 |
|
||||
|
||||
**Decision Rationale:**
|
||||
Performance metrics below target (P99 latency, throughput). Mitigation plan in place. Security and reliability meet all requirements.
|
||||
|
||||
---
|
||||
|
||||
## Security Assessment
|
||||
|
||||
**Status:** PASS ✅
|
||||
|
||||
### Requirements Met
|
||||
|
||||
| Requirement | Target | Actual | Status |
|
||||
|-------------|--------|--------|--------|
|
||||
| Authentication required | All endpoints | 100% enforced | ✅ |
|
||||
| Data encryption at rest | PostgreSQL TDE | Enabled | ✅ |
|
||||
| Critical vulnerabilities | 0 | 0 | ✅ |
|
||||
| Input validation | All endpoints | Zod schemas on 100% | ✅ |
|
||||
| Security headers | Configured | helmet.js enabled | ✅ |
|
||||
|
||||
### Evidence
|
||||
|
||||
**Security Scan:**
|
||||
```bash
|
||||
$ npm audit
|
||||
found 0 vulnerabilities
|
||||
```
|
||||
|
||||
**Authentication Tests:**
|
||||
- 15/15 auth tests passing
|
||||
- Tested unauthorized access (401 responses)
|
||||
- Token validation working
|
||||
|
||||
**Penetration Testing:**
|
||||
- Report: `/reports/pentest-2026-01.pdf`
|
||||
- Findings: 0 critical, 2 low (addressed)
|
||||
|
||||
**Conclusion:** All security requirements met. No blockers.
|
||||
|
||||
---
|
||||
|
||||
## Performance Assessment
|
||||
|
||||
**Status:** CONCERNS ⚠️
|
||||
|
||||
### Requirements Status
|
||||
|
||||
| Metric | Target | Actual | Status |
|
||||
|--------|--------|--------|--------|
|
||||
| API response P99 | < 200ms | 350ms | ❌ Exceeds |
|
||||
| API response P95 | < 150ms | 180ms | ⚠️ Exceeds |
|
||||
| Throughput | > 1000 rps | 850 rps | ⚠️ Below |
|
||||
| Frontend load | < 2s | 1.8s | ✅ Met |
|
||||
| DB query P99 | < 50ms | 85ms | ❌ Exceeds |
|
||||
|
||||
### Issues Identified
|
||||
|
||||
#### Issue 1: P99 Latency Exceeds Target
|
||||
|
||||
**Measured:** 350ms P99 (target: <200ms)
|
||||
**Root Cause:** Database queries not optimized
|
||||
- Missing indexes on profile queries
|
||||
- N+1 query problem in profile endpoint
|
||||
|
||||
**Impact:** User experience degraded for 1% of requests
|
||||
|
||||
**Mitigation Plan:**
|
||||
- Add composite index on `(user_id, profile_id)` - backend team, 2 days
|
||||
- Refactor profile endpoint to use joins instead of multiple queries - backend team, 3 days
|
||||
- Re-run load tests after optimization - QA team, 1 day
|
||||
|
||||
**Owner:** Backend team lead
|
||||
**Deadline:** Before release (January 20, 2026)
|
||||
|
||||
#### Issue 2: Throughput Below Target
|
||||
|
||||
**Measured:** 850 rps (target: >1000 rps)
|
||||
**Root Cause:** Connection pool size too small
|
||||
- PostgreSQL max_connections = 100 (too low)
|
||||
- No connection pooling in application
|
||||
|
||||
**Impact:** System cannot handle expected traffic
|
||||
|
||||
**Mitigation Plan:**
|
||||
- Increase PostgreSQL max_connections to 500 - DevOps, 1 day
|
||||
- Implement connection pooling with pg-pool - backend team, 2 days
|
||||
- Re-run load tests - QA team, 1 day
|
||||
|
||||
**Owner:** DevOps + Backend team
|
||||
**Deadline:** Before release (January 20, 2026)
|
||||
|
||||
### Evidence
|
||||
|
||||
**Load Testing:**
|
||||
```
|
||||
Tool: k6
|
||||
Duration: 10 minutes
|
||||
Virtual Users: 500 concurrent
|
||||
Report: /reports/k6-load-test.json
|
||||
```
|
||||
|
||||
**Results:**
|
||||
```
|
||||
scenarios: (100.00%) 1 scenario, 500 max VUs, 10m30s max duration
|
||||
✓ http_req_duration..............: avg=250ms min=45ms med=180ms max=2.1s p(90)=280ms p(95)=350ms
|
||||
http_reqs......................: 85000 (850/s)
|
||||
http_req_failed................: 0.1%
|
||||
```
|
||||
|
||||
**APM Data:**
|
||||
- Tool: Datadog
|
||||
- Dashboard: <https://app.datadoghq.com/dashboard/abc123>
|
||||
|
||||
**Conclusion:** Performance issues identified with mitigation plan. Re-assess after optimization.
|
||||
|
||||
---
|
||||
|
||||
## Reliability Assessment
|
||||
|
||||
**Status:** PASS ✅
|
||||
|
||||
### Requirements Met
|
||||
|
||||
| Requirement | Target | Actual | Status |
|
||||
|-------------|--------|--------|--------|
|
||||
| Error handling | Structured errors | 100% endpoints | ✅ |
|
||||
| Availability | 99.9% uptime | 99.95% (staging) | ✅ |
|
||||
| Recovery time | < 5 min (RTO) | 3 min (tested) | ✅ |
|
||||
| Data backup | Daily | Automated daily | ✅ |
|
||||
| Failover | < 30s downtime | 15s (tested) | ✅ |
|
||||
|
||||
### Evidence
|
||||
|
||||
**Error Handling Tests:**
|
||||
- All endpoints return structured JSON errors
|
||||
- Error codes standardized (400, 401, 403, 404, 500)
|
||||
- Error messages user-friendly (no stack traces)
|
||||
|
||||
**Chaos Engineering:**
|
||||
- Tested database failover: 15s downtime ✅
|
||||
- Tested service crash recovery: 3 min ✅
|
||||
- Tested network partition: Graceful degradation ✅
|
||||
|
||||
**Monitoring:**
|
||||
- Staging uptime (30 days): 99.95%
|
||||
- Error rate: 0.01% (target: <0.1%)
|
||||
- P50 availability: 100%
|
||||
|
||||
**Conclusion:** All reliability requirements exceeded. No issues.
|
||||
|
||||
---
|
||||
|
||||
## Maintainability Assessment
|
||||
|
||||
**Status:** PASS ✅
|
||||
|
||||
### Requirements Met
|
||||
|
||||
| Requirement | Target | Actual | Status |
|
||||
|-------------|--------|--------|--------|
|
||||
| Test coverage | > 80% | 85% | ✅ |
|
||||
| Code quality | Grade A | Grade A | ✅ |
|
||||
| Documentation | All APIs | 100% documented | ✅ |
|
||||
| Outdated dependencies | < 6 months | 3 months avg | ✅ |
|
||||
| Technical debt | < 10% | 7% | ✅ |
|
||||
|
||||
### Evidence
|
||||
|
||||
**Test Coverage:**
|
||||
```
|
||||
Statements : 85.2% ( 1205/1414 )
|
||||
Branches : 82.1% ( 412/502 )
|
||||
Functions : 88.5% ( 201/227 )
|
||||
Lines : 85.2% ( 1205/1414 )
|
||||
```
|
||||
|
||||
**Code Quality:**
|
||||
- SonarQube: Grade A
|
||||
- Maintainability rating: A
|
||||
- Technical debt ratio: 7%
|
||||
- Code smells: 12 (all minor)
|
||||
|
||||
**Documentation:**
|
||||
- API docs: 100% coverage (OpenAPI spec)
|
||||
- README: Complete and up-to-date
|
||||
- Architecture docs: ADRs for all major decisions
|
||||
|
||||
**Conclusion:** All maintainability requirements met. Codebase is healthy.
|
||||
|
||||
---
|
||||
|
||||
## Overall Gate Decision
|
||||
|
||||
### Decision: CONCERNS ⚠️
|
||||
|
||||
**Rationale:**
|
||||
- **Blockers:** None
|
||||
- **Concerns:** Performance metrics below target (P99 latency, throughput)
|
||||
- **Mitigation:** Plan in place with clear owners and deadlines (5 days total)
|
||||
- **Passing:** Security, reliability, maintainability all green
|
||||
|
||||
### Actions Required Before Release
|
||||
|
||||
1. **Optimize database queries** (backend team, 3 days)
|
||||
- Add indexes
|
||||
- Fix N+1 queries
|
||||
- Implement connection pooling
|
||||
|
||||
2. **Re-run performance tests** (QA team, 1 day)
|
||||
- Validate P99 < 200ms
|
||||
- Validate throughput > 1000 rps
|
||||
|
||||
3. **Update this assessment** (TEA, 1 hour)
|
||||
- Re-run `*nfr-assess` with new results
|
||||
- Confirm PASS status
|
||||
|
||||
### Waiver Option (If Business Approves)
|
||||
|
||||
If business decides to deploy with current performance:
|
||||
|
||||
**Waiver Justification:**
|
||||
```markdown
|
||||
## Performance Waiver
|
||||
|
||||
**Waived By:** VP Engineering, Product Manager
|
||||
**Date:** 2026-01-15
|
||||
**Reason:** Business priority to launch by Q1
|
||||
**Conditions:**
|
||||
- Set monitoring alerts for P99 > 300ms
|
||||
- Plan optimization for v1.3 (February release)
|
||||
- Document known performance limitations in release notes
|
||||
|
||||
**Accepted Risk:**
|
||||
- 1% of users experience slower response (350ms vs 200ms)
|
||||
- System can handle current traffic (850 rps sufficient for launch)
|
||||
- Optimization planned for next release
|
||||
```
|
||||
|
||||
### Approvals
|
||||
|
||||
- [ ] Product Manager - Review business impact
|
||||
- [ ] Tech Lead - Review mitigation plan
|
||||
- [ ] QA Lead - Validate test evidence
|
||||
- [ ] DevOps - Confirm infrastructure ready
|
||||
|
||||
---
|
||||
|
||||
## Monitoring Plan Post-Release
|
||||
|
||||
**Performance Alerts:**
|
||||
- P99 latency > 400ms (critical)
|
||||
- Throughput < 700 rps (warning)
|
||||
- Error rate > 1% (critical)
|
||||
|
||||
**Review Cadence:**
|
||||
- Daily: Check performance dashboards
|
||||
- Weekly: Review alert trends
|
||||
- Monthly: Re-assess NFRs
|
||||
```
|
||||
|
||||
## What You Get
|
||||
|
||||
### NFR Assessment Report
|
||||
- Category-by-category analysis (Security, Performance, Reliability, Maintainability)
|
||||
- Requirements status (target vs actual)
|
||||
- Evidence for each requirement
|
||||
- Issues identified with root cause analysis
|
||||
|
||||
### Gate Decision
|
||||
- **PASS** ✅ - All NFRs met, ready to release
|
||||
- **CONCERNS** ⚠️ - Some NFRs not met, mitigation plan exists
|
||||
- **FAIL** ❌ - Critical NFRs not met, blocks release
|
||||
- **WAIVED** ⏭️ - Business-approved waiver with documented risk
|
||||
|
||||
### Mitigation Plans
|
||||
- Specific actions to address concerns
|
||||
- Owners and deadlines
|
||||
- Re-assessment criteria
|
||||
|
||||
### Monitoring Plan
|
||||
- Post-release monitoring strategy
|
||||
- Alert thresholds
|
||||
- Review cadence
|
||||
|
||||
## Tips
|
||||
|
||||
### Run NFR Assessment Early
|
||||
|
||||
**Phase 2 (Enterprise):**
|
||||
Run `nfr-assess` during planning to:
|
||||
- Identify NFR requirements early
|
||||
- Plan for performance testing
|
||||
- Budget for security audits
|
||||
- Set up monitoring infrastructure
|
||||
|
||||
**Phase 4 or Gate:**
|
||||
Re-run before release to validate all requirements met.
|
||||
|
||||
### Never Guess Thresholds
|
||||
|
||||
If you don't know the NFR target:
|
||||
|
||||
**Don't:**
|
||||
```
|
||||
API response time should probably be under 500ms
|
||||
```
|
||||
|
||||
**Do:**
|
||||
```
|
||||
Mark as CONCERNS - Request threshold from stakeholders
|
||||
"What is the acceptable API response time?"
|
||||
```
|
||||
|
||||
### Collect Evidence Beforehand
|
||||
|
||||
Before running `*nfr-assess`, gather:
|
||||
|
||||
**Security:**
|
||||
```bash
|
||||
npm audit # Vulnerability scan
|
||||
snyk test # Alternative security scan
|
||||
npm run test:security # Security test suite
|
||||
```
|
||||
|
||||
**Performance:**
|
||||
```bash
|
||||
npm run test:load # k6 or artillery load tests
|
||||
npm run test:lighthouse # Frontend performance
|
||||
npm run test:db-performance # Database query analysis
|
||||
```
|
||||
|
||||
**Reliability:**
|
||||
- Production error rate (last 30 days)
|
||||
- Uptime data (StatusPage, PagerDuty)
|
||||
- Incident response times
|
||||
|
||||
**Maintainability:**
|
||||
```bash
|
||||
npm run test:coverage # Test coverage report
|
||||
npm run lint # Code quality check
|
||||
npm outdated # Dependency freshness
|
||||
```
|
||||
|
||||
### Use Real Data, Not Assumptions
|
||||
|
||||
**Don't:**
|
||||
```
|
||||
System is probably fast enough
|
||||
Security seems fine
|
||||
```
|
||||
|
||||
**Do:**
|
||||
```
|
||||
Load test results show P99 = 350ms
|
||||
npm audit shows 0 vulnerabilities
|
||||
Test coverage report shows 85%
|
||||
```
|
||||
|
||||
Evidence-based decisions prevent surprises in production.
|
||||
|
||||
### Document Waivers Thoroughly
|
||||
|
||||
If business approves waiver:
|
||||
|
||||
**Required:**
|
||||
- Who approved (name, role, date)
|
||||
- Why (business justification)
|
||||
- Conditions (monitoring, future plans)
|
||||
- Accepted risk (quantified impact)
|
||||
|
||||
**Example:**
|
||||
```markdown
|
||||
Waived by: CTO, VP Product (2026-01-15)
|
||||
Reason: Q1 launch critical for investor demo
|
||||
Conditions: Optimize in v1.3, monitor closely
|
||||
Risk: 1% of users experience 350ms latency (acceptable for launch)
|
||||
```
|
||||
|
||||
### Re-Assess After Fixes
|
||||
|
||||
After implementing mitigations:
|
||||
|
||||
```
|
||||
1. Fix performance issues
|
||||
2. Run load tests again
|
||||
3. Run nfr-assess with new evidence
|
||||
4. Verify PASS status
|
||||
```
|
||||
|
||||
Don't deploy with CONCERNS without mitigation or waiver.
|
||||
|
||||
### Integrate with Release Checklist
|
||||
|
||||
```markdown
|
||||
## Release Checklist
|
||||
|
||||
### Pre-Release
|
||||
- [ ] All tests passing
|
||||
- [ ] Test coverage > 80%
|
||||
- [ ] Run nfr-assess
|
||||
- [ ] NFR status: PASS or WAIVED
|
||||
|
||||
### Performance
|
||||
- [ ] Load tests completed
|
||||
- [ ] P99 latency meets threshold
|
||||
- [ ] Throughput meets threshold
|
||||
|
||||
### Security
|
||||
- [ ] Security scan clean
|
||||
- [ ] Auth tests passing
|
||||
- [ ] Penetration test complete
|
||||
|
||||
### Post-Release
|
||||
- [ ] Monitoring alerts configured
|
||||
- [ ] Dashboards updated
|
||||
- [ ] Incident response plan ready
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### No Evidence Available
|
||||
|
||||
**Problem:** Don't have performance data, security scans, etc.
|
||||
|
||||
**Solution:**
|
||||
```
|
||||
Mark as CONCERNS for categories without evidence
|
||||
Document what evidence is needed
|
||||
Set up tests/scans before re-assessment
|
||||
```
|
||||
|
||||
**Don't block on missing evidence** - document what's needed and proceed.
|
||||
|
||||
### Thresholds Too Strict
|
||||
|
||||
**Problem:** Can't meet unrealistic thresholds.
|
||||
|
||||
**Symptoms:**
|
||||
- P99 < 50ms (impossible for complex queries)
|
||||
- 100% test coverage (impractical)
|
||||
- Zero technical debt (unrealistic)
|
||||
|
||||
**Solution:**
|
||||
```
|
||||
Negotiate thresholds with stakeholders:
|
||||
- "P99 < 50ms is unrealistic for our DB queries"
|
||||
- "Propose P99 < 200ms based on industry standards"
|
||||
- "Show evidence from load tests"
|
||||
```
|
||||
|
||||
Use data to negotiate realistic requirements.
|
||||
|
||||
### Assessment Takes Too Long
|
||||
|
||||
**Problem:** Gathering evidence for all categories is time-consuming.
|
||||
|
||||
**Solution:** Focus on critical categories first:
|
||||
|
||||
**For most projects:**
|
||||
```
|
||||
Priority 1: Security (always critical)
|
||||
Priority 2: Performance (if high-traffic)
|
||||
Priority 3: Reliability (if uptime critical)
|
||||
Priority 4: Maintainability (nice to have)
|
||||
```
|
||||
|
||||
Assess categories incrementally, not all at once.
|
||||
|
||||
### CONCERNS vs FAIL - When to Block?
|
||||
|
||||
**CONCERNS** ⚠️:
|
||||
- Issues exist but not critical
|
||||
- Mitigation plan in place
|
||||
- Business accepts risk (with waiver)
|
||||
- Can deploy with monitoring
|
||||
|
||||
**FAIL** ❌:
|
||||
- Critical security vulnerability (CVE critical)
|
||||
- System unusable (error rate >10%)
|
||||
- Data loss risk (no backups)
|
||||
- Zero mitigation possible
|
||||
|
||||
**Rule of thumb:** If you can mitigate or monitor, use CONCERNS. Reserve FAIL for absolute blockers.
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to Run Trace](/docs/tea/how-to/workflows/run-trace.md) - Gate decision complements NFR
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Quality complements NFR
|
||||
- [Run TEA for Enterprise](/docs/tea/how-to/brownfield/use-tea-for-enterprise.md) - Enterprise workflow
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Risk assessment principles
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - NFR in release gates
|
||||
|
||||
## Reference
|
||||
|
||||
- [Command: *nfr-assess](/docs/tea/reference/commands.md#nfr-assess) - Full command reference
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Enterprise config options
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
title: "How to Run Test Design with TEA"
|
||||
description: How to create comprehensive test plans using TEA's test-design workflow
|
||||
---
|
||||
|
||||
Use TEA's `test-design` workflow to create comprehensive test plans with risk assessment and coverage strategies.
|
||||
|
||||
## When to Use This
|
||||
|
||||
**System-level (Phase 3):**
|
||||
- After architecture is complete
|
||||
- Before implementation-readiness gate
|
||||
- To validate architecture testability
|
||||
|
||||
**Epic-level (Phase 4):**
|
||||
- At the start of each epic
|
||||
- Before implementing stories in the epic
|
||||
- To identify epic-specific testing needs
|
||||
|
||||
:::note[Prerequisites]
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- For system-level: Architecture document complete
|
||||
- For epic-level: Epic defined with stories
|
||||
:::
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load the TEA Agent
|
||||
|
||||
Start a fresh chat and load the TEA (Test Architect) agent.
|
||||
|
||||
### 2. Run the Test Design Workflow
|
||||
|
||||
```
|
||||
test-design
|
||||
```
|
||||
|
||||
### 3. Specify the Mode
|
||||
|
||||
TEA will ask if you want:
|
||||
|
||||
- **System-level** — For architecture testability review (Phase 3)
|
||||
- **Epic-level** — For epic-specific test planning (Phase 4)
|
||||
|
||||
### 4. Provide Context
|
||||
|
||||
For system-level:
|
||||
- Point to your architecture document
|
||||
- Reference any ADRs (Architecture Decision Records)
|
||||
|
||||
For epic-level:
|
||||
- Specify which epic you're planning
|
||||
- Reference the epic file with stories
|
||||
|
||||
### 5. Review the Output
|
||||
|
||||
TEA generates test design document(s) based on mode.
|
||||
|
||||
## What You Get
|
||||
|
||||
**System-Level Output (TWO Documents):**
|
||||
|
||||
TEA produces two focused documents for system-level mode:
|
||||
|
||||
1. **`test-design-architecture.md`** (for Architecture/Dev teams)
|
||||
- Purpose: Architectural concerns, testability gaps, NFR requirements
|
||||
- Quick Guide with 🚨 BLOCKERS / ⚠️ HIGH PRIORITY / 📋 INFO ONLY
|
||||
- Risk assessment (high/medium/low-priority with scoring)
|
||||
- Testability concerns and architectural gaps
|
||||
- Risk mitigation plans for high-priority risks (≥6)
|
||||
- Assumptions and dependencies
|
||||
|
||||
2. **`test-design-qa.md`** (for QA team)
|
||||
- Purpose: Test execution recipe, coverage plan, Sprint 0 setup
|
||||
- Quick Reference for QA (Before You Start, Execution Order, Need Help)
|
||||
- System architecture summary
|
||||
- Test environment requirements (moved up - early in doc)
|
||||
- Testability assessment (prerequisites checklist)
|
||||
- Test levels strategy (unit/integration/E2E split)
|
||||
- Test coverage plan (P0/P1/P2/P3 with detailed scenarios + checkboxes)
|
||||
- Sprint 0 setup requirements (blockers, infrastructure, environments)
|
||||
- NFR readiness summary
|
||||
|
||||
**Why Two Documents?**
|
||||
- **Architecture teams** can scan blockers in <5 min (Quick Guide format)
|
||||
- **QA teams** have actionable test recipes (step-by-step with checklists)
|
||||
- **No redundancy** between documents (cross-references instead of duplication)
|
||||
- **Clear separation** of concerns (what to deliver vs how to test)
|
||||
|
||||
**Epic-Level Output (ONE Document):**
|
||||
|
||||
**`test-design-epic-N.md`** (combined risk assessment + test plan)
|
||||
- Risk assessment for the epic
|
||||
- Test priorities (P0-P3)
|
||||
- Coverage plan
|
||||
- Regression hotspots (for brownfield)
|
||||
- Integration risks
|
||||
- Mitigation strategies
|
||||
|
||||
## Test Design for Different Tracks
|
||||
|
||||
| Track | Phase 3 Focus | Phase 4 Focus |
|
||||
|-------|---------------|---------------|
|
||||
| **Greenfield** | System-level testability review | Per-epic risk assessment and test plan |
|
||||
| **Brownfield** | System-level + existing test baseline | Regression hotspots, integration risks |
|
||||
| **Enterprise** | Compliance-aware testability | Security/performance/compliance focus |
|
||||
|
||||
## Examples
|
||||
|
||||
**System-Level (Two Documents):**
|
||||
- `cluster-search/cluster-search-test-design-architecture.md` - Architecture doc with Quick Guide
|
||||
- `cluster-search/cluster-search-test-design-qa.md` - QA doc with test scenarios
|
||||
|
||||
**Key Pattern:**
|
||||
- Architecture doc: "ASR-1: OAuth 2.1 required (see QA doc for 12 test scenarios)"
|
||||
- QA doc: "OAuth tests: 12 P0 scenarios (see Architecture doc R-001 for risk details)"
|
||||
- No duplication, just cross-references
|
||||
|
||||
## Tips
|
||||
|
||||
- **Run system-level right after architecture** — Early testability review
|
||||
- **Run epic-level at the start of each epic** — Targeted test planning
|
||||
- **Update if ADRs change** — Keep test design aligned
|
||||
- **Use output to guide other workflows** — Feeds into `atdd` and `automate`
|
||||
- **Architecture teams review Architecture doc** — Focus on blockers and mitigation plans
|
||||
- **QA teams use QA doc as implementation guide** — Follow test scenarios and Sprint 0 checklist
|
||||
|
||||
## Next Steps
|
||||
|
||||
After test design:
|
||||
|
||||
1. **Setup Test Framework** — If not already configured
|
||||
2. **Implementation Readiness** — System-level feeds into gate check
|
||||
3. **Story Implementation** — Epic-level guides testing during dev
|
||||
@@ -1,605 +0,0 @@
|
||||
---
|
||||
title: "How to Run Test Review with TEA"
|
||||
description: Audit test quality using TEA's comprehensive knowledge base and get 0-100 scoring
|
||||
---
|
||||
|
||||
# How to Run Test Review with TEA
|
||||
|
||||
Use TEA's `test-review` workflow to audit test quality with objective scoring and actionable feedback. TEA reviews tests against its knowledge base of best practices.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Want to validate test quality objectively
|
||||
- Need quality metrics for release gates
|
||||
- Preparing for production deployment
|
||||
- Reviewing team-written tests
|
||||
- Auditing AI-generated tests
|
||||
- Onboarding new team members (show good patterns)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- Tests written (to review)
|
||||
- Test framework configured
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load TEA Agent
|
||||
|
||||
Start a fresh chat and load TEA:
|
||||
|
||||
```
|
||||
tea
|
||||
```
|
||||
|
||||
### 2. Run the Test Review Workflow
|
||||
|
||||
```
|
||||
test-review
|
||||
```
|
||||
|
||||
### 3. Specify Review Scope
|
||||
|
||||
TEA will ask what to review.
|
||||
|
||||
#### Option A: Single File
|
||||
|
||||
Review one test file:
|
||||
|
||||
```
|
||||
tests/e2e/checkout.spec.ts
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
- Reviewing specific failing tests
|
||||
- Quick feedback on new tests
|
||||
- Learning from specific examples
|
||||
|
||||
#### Option B: Directory
|
||||
|
||||
Review all tests in a directory:
|
||||
|
||||
```
|
||||
tests/e2e/
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
- Reviewing E2E test suite
|
||||
- Comparing test quality across files
|
||||
- Finding patterns of issues
|
||||
|
||||
#### Option C: Entire Suite
|
||||
|
||||
Review all tests:
|
||||
|
||||
```
|
||||
tests/
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
- Release gate quality check
|
||||
- Comprehensive audit
|
||||
- Establishing baseline metrics
|
||||
|
||||
### 4. Review the Quality Report
|
||||
|
||||
TEA generates a comprehensive quality report with scoring.
|
||||
|
||||
#### Report Structure (`test-review.md`):
|
||||
|
||||
```markdown
|
||||
# Test Quality Review Report
|
||||
|
||||
**Date:** 2026-01-13
|
||||
**Scope:** tests/e2e/
|
||||
**Overall Score:** 76/100
|
||||
|
||||
## Summary
|
||||
|
||||
- **Tests Reviewed:** 12
|
||||
- **Passing Quality:** 9 tests (75%)
|
||||
- **Needs Improvement:** 3 tests (25%)
|
||||
- **Critical Issues:** 2
|
||||
- **Recommendations:** 6
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. Hard Waits Detected
|
||||
|
||||
**File:** `tests/e2e/checkout.spec.ts:45`
|
||||
**Issue:** Using `page.waitForTimeout(3000)`
|
||||
**Impact:** Test is flaky and unnecessarily slow
|
||||
**Severity:** Critical
|
||||
|
||||
**Current Code:**
|
||||
```typescript
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000); // ❌ Hard wait
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
await page.click('button[type="submit"]');
|
||||
// Wait for the API response that triggers success message
|
||||
await page.waitForResponse(resp =>
|
||||
resp.url().includes('/api/checkout') && resp.ok()
|
||||
);
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
```
|
||||
|
||||
**Why This Matters:**
|
||||
- Hard waits are fixed timeouts that don't wait for actual conditions
|
||||
- Tests fail intermittently on slower machines
|
||||
- Wastes time waiting even when response is fast
|
||||
- Network-first patterns are more reliable
|
||||
|
||||
---
|
||||
|
||||
### 2. Conditional Flow Control
|
||||
|
||||
**File:** `tests/e2e/profile.spec.ts:28`
|
||||
**Issue:** Using if/else to handle optional elements
|
||||
**Impact:** Non-deterministic test behavior
|
||||
**Severity:** Critical
|
||||
|
||||
**Current Code:**
|
||||
```typescript
|
||||
if (await page.locator('.banner').isVisible()) {
|
||||
await page.click('.dismiss');
|
||||
}
|
||||
// ❌ Test behavior changes based on banner presence
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// Option 1: Make banner presence deterministic
|
||||
await expect(page.locator('.banner')).toBeVisible();
|
||||
await page.click('.dismiss');
|
||||
|
||||
// Option 2: Test both scenarios separately
|
||||
test('should show banner for new users', async ({ page }) => {
|
||||
// Test with banner
|
||||
});
|
||||
|
||||
test('should not show banner for returning users', async ({ page }) => {
|
||||
// Test without banner
|
||||
});
|
||||
```
|
||||
|
||||
**Why This Matters:**
|
||||
- Tests should be deterministic (same result every run)
|
||||
- Conditionals hide bugs (what if banner should always show?)
|
||||
- Makes debugging harder
|
||||
- Violates test isolation principle
|
||||
|
||||
## Recommendations
|
||||
|
||||
### 1. Extract Repeated Setup
|
||||
|
||||
**File:** `tests/e2e/profile.spec.ts`
|
||||
**Issue:** Login code duplicated in every test
|
||||
**Severity:** Medium
|
||||
**Impact:** Maintenance burden, test verbosity
|
||||
|
||||
**Current:**
|
||||
```typescript
|
||||
test('test 1', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('[name="email"]', 'test@example.com');
|
||||
await page.fill('[name="password"]', 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
// Test logic...
|
||||
});
|
||||
|
||||
test('test 2', async ({ page }) => {
|
||||
// Same login code repeated
|
||||
});
|
||||
```
|
||||
|
||||
**Fix (Vanilla Playwright):**
|
||||
```typescript
|
||||
// Create fixture in tests/support/fixtures/auth.ts
|
||||
import { test as base, Page } from '@playwright/test';
|
||||
|
||||
export const test = base.extend<{ authenticatedPage: Page }>({
|
||||
authenticatedPage: async ({ page }, use) => {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill('test@example.com');
|
||||
await page.getByLabel('Password').fill('password');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
await use(page);
|
||||
}
|
||||
});
|
||||
|
||||
// Use in tests
|
||||
test('test 1', async ({ authenticatedPage }) => {
|
||||
// Already logged in
|
||||
});
|
||||
```
|
||||
|
||||
**Better (With Playwright Utils):**
|
||||
```typescript
|
||||
// Use built-in auth-session fixture
|
||||
import { test as base } from '@playwright/test';
|
||||
import { createAuthFixtures } from '@seontechnologies/playwright-utils/auth-session';
|
||||
|
||||
export const test = base.extend(createAuthFixtures());
|
||||
|
||||
// Use in tests - even simpler
|
||||
test('test 1', async ({ page, authToken }) => {
|
||||
// authToken already available (persisted, reused)
|
||||
await page.goto('/dashboard');
|
||||
// Already authenticated via authToken
|
||||
});
|
||||
```
|
||||
|
||||
**Playwright Utils Benefits:**
|
||||
- Token persisted to disk (faster subsequent runs)
|
||||
- Multi-user support out of the box
|
||||
- Automatic token renewal if expired
|
||||
- No manual login flow needed
|
||||
|
||||
---
|
||||
|
||||
### 2. Add Network Assertions
|
||||
|
||||
**File:** `tests/e2e/api-calls.spec.ts`
|
||||
**Issue:** No verification of API responses
|
||||
**Severity:** Low
|
||||
**Impact:** Tests don't catch API errors
|
||||
|
||||
**Current:**
|
||||
```typescript
|
||||
await page.click('button[name="save"]');
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
// ❌ What if API returned 500 but UI shows cached success?
|
||||
```
|
||||
|
||||
**Enhancement:**
|
||||
```typescript
|
||||
const responsePromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/profile') && resp.status() === 200
|
||||
);
|
||||
await page.click('button[name="save"]');
|
||||
const response = await responsePromise;
|
||||
|
||||
// Verify API response
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(true);
|
||||
|
||||
// Verify UI
|
||||
await expect(page.locator('.success')).toBeVisible();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Improve Test Names
|
||||
|
||||
**File:** `tests/e2e/checkout.spec.ts`
|
||||
**Issue:** Vague test names
|
||||
**Severity:** Low
|
||||
**Impact:** Hard to understand test purpose
|
||||
|
||||
**Current:**
|
||||
```typescript
|
||||
test('should work', async ({ page }) => { });
|
||||
test('test checkout', async ({ page }) => { });
|
||||
```
|
||||
|
||||
**Better:**
|
||||
```typescript
|
||||
test('should complete checkout with valid credit card', async ({ page }) => { });
|
||||
test('should show validation error for expired card', async ({ page }) => { });
|
||||
```
|
||||
|
||||
## Quality Scores by Category
|
||||
|
||||
| Category | Score | Target | Status |
|
||||
|----------|-------|--------|--------|
|
||||
| **Determinism** | 26/35 | 30/35 | ⚠️ Needs Improvement |
|
||||
| **Isolation** | 22/25 | 20/25 | ✅ Good |
|
||||
| **Assertions** | 18/20 | 16/20 | ✅ Good |
|
||||
| **Structure** | 7/10 | 8/10 | ⚠️ Minor Issues |
|
||||
| **Performance** | 3/10 | 8/10 | ❌ Critical |
|
||||
|
||||
### Scoring Breakdown
|
||||
|
||||
**Determinism (35 points max):**
|
||||
- No hard waits: 0/10 ❌ (found 3 instances)
|
||||
- No conditionals: 8/10 ⚠️ (found 2 instances)
|
||||
- No try-catch flow control: 10/10 ✅
|
||||
- Network-first patterns: 8/15 ⚠️ (some tests missing)
|
||||
|
||||
**Isolation (25 points max):**
|
||||
- Self-cleaning: 20/20 ✅
|
||||
- No global state: 5/5 ✅
|
||||
- Parallel-safe: 0/0 ✅ (not tested)
|
||||
|
||||
**Assertions (20 points max):**
|
||||
- Explicit in test body: 15/15 ✅
|
||||
- Specific and meaningful: 3/5 ⚠️ (some weak assertions)
|
||||
|
||||
**Structure (10 points max):**
|
||||
- Test size < 300 lines: 5/5 ✅
|
||||
- Clear names: 2/5 ⚠️ (some vague names)
|
||||
|
||||
**Performance (10 points max):**
|
||||
- Execution time < 1.5 min: 3/10 ❌ (3 tests exceed limit)
|
||||
|
||||
## Files Reviewed
|
||||
|
||||
| File | Score | Issues | Status |
|
||||
|------|-------|--------|--------|
|
||||
| `tests/e2e/checkout.spec.ts` | 65/100 | 4 | ❌ Needs Work |
|
||||
| `tests/e2e/profile.spec.ts` | 72/100 | 3 | ⚠️ Needs Improvement |
|
||||
| `tests/e2e/search.spec.ts` | 88/100 | 1 | ✅ Good |
|
||||
| `tests/api/profile.spec.ts` | 92/100 | 0 | ✅ Excellent |
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate (Fix Critical Issues)
|
||||
1. Remove hard waits in `checkout.spec.ts` (line 45, 67, 89)
|
||||
2. Fix conditional in `profile.spec.ts` (line 28)
|
||||
3. Optimize slow tests in `checkout.spec.ts`
|
||||
|
||||
### Short-term (Apply Recommendations)
|
||||
4. Extract login fixture from `profile.spec.ts`
|
||||
5. Add network assertions to `api-calls.spec.ts`
|
||||
6. Improve test names in `checkout.spec.ts`
|
||||
|
||||
### Long-term (Continuous Improvement)
|
||||
7. Re-run `test-review` after fixes (target: 85/100)
|
||||
8. Add performance budgets to CI
|
||||
9. Document test patterns for team
|
||||
|
||||
## Knowledge Base References
|
||||
|
||||
TEA reviewed against these patterns:
|
||||
- [test-quality.md](/docs/tea/reference/knowledge-base.md#test-quality) - Execution limits, isolation
|
||||
- [network-first.md](/docs/tea/reference/knowledge-base.md#network-first) - Deterministic waits
|
||||
- [timing-debugging.md](/docs/tea/reference/knowledge-base.md#timing-debugging) - Race conditions
|
||||
- [selector-resilience.md](/docs/tea/reference/knowledge-base.md#selector-resilience) - Robust selectors
|
||||
```
|
||||
|
||||
## Understanding the Scores
|
||||
|
||||
### What Do Scores Mean?
|
||||
|
||||
| Score Range | Interpretation | Action |
|
||||
|-------------|----------------|--------|
|
||||
| **90-100** | Excellent | Minimal changes needed, production-ready |
|
||||
| **80-89** | Good | Minor improvements recommended |
|
||||
| **70-79** | Acceptable | Address recommendations before release |
|
||||
| **60-69** | Needs Improvement | Fix critical issues, apply recommendations |
|
||||
| **< 60** | Critical | Significant refactoring needed |
|
||||
|
||||
### Scoring Criteria
|
||||
|
||||
**Determinism (35 points):**
|
||||
- Tests produce same result every run
|
||||
- No random failures (flakiness)
|
||||
- No environment-dependent behavior
|
||||
|
||||
**Isolation (25 points):**
|
||||
- Tests don't depend on each other
|
||||
- Can run in any order
|
||||
- Clean up after themselves
|
||||
|
||||
**Assertions (20 points):**
|
||||
- Verify actual behavior
|
||||
- Specific and meaningful
|
||||
- Not abstracted away in helpers
|
||||
|
||||
**Structure (10 points):**
|
||||
- Readable and maintainable
|
||||
- Appropriate size
|
||||
- Clear naming
|
||||
|
||||
**Performance (10 points):**
|
||||
- Fast execution
|
||||
- Efficient selectors
|
||||
- No unnecessary waits
|
||||
|
||||
## What You Get
|
||||
|
||||
### Quality Report
|
||||
- Overall score (0-100)
|
||||
- Category scores (Determinism, Isolation, etc.)
|
||||
- File-by-file breakdown
|
||||
|
||||
### Critical Issues
|
||||
- Specific line numbers
|
||||
- Code examples (current vs fixed)
|
||||
- Why it matters explanation
|
||||
- Impact assessment
|
||||
|
||||
### Recommendations
|
||||
- Actionable improvements
|
||||
- Code examples
|
||||
- Priority/severity levels
|
||||
|
||||
### Next Steps
|
||||
- Immediate actions (fix critical)
|
||||
- Short-term improvements
|
||||
- Long-term quality goals
|
||||
|
||||
## Tips
|
||||
|
||||
### Review Before Release
|
||||
|
||||
Make test review part of release checklist:
|
||||
|
||||
```markdown
|
||||
## Release Checklist
|
||||
- [ ] All tests passing
|
||||
- [ ] Test review score > 80
|
||||
- [ ] Critical issues resolved
|
||||
- [ ] Performance within budget
|
||||
```
|
||||
|
||||
### Review After AI Generation
|
||||
|
||||
Always review AI-generated tests:
|
||||
|
||||
```
|
||||
1. Run atdd or automate
|
||||
2. Run test-review on generated tests
|
||||
3. Fix critical issues
|
||||
4. Commit tests
|
||||
```
|
||||
|
||||
### Set Quality Gates
|
||||
|
||||
Use scores as quality gates:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
- name: Review test quality
|
||||
run: |
|
||||
# Run test review
|
||||
# Parse score from report
|
||||
if [ $SCORE -lt 80 ]; then
|
||||
echo "Test quality below threshold"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Review Regularly
|
||||
|
||||
Schedule periodic reviews:
|
||||
|
||||
- **Per story:** Optional (spot check new tests)
|
||||
- **Per epic:** Recommended (ensure consistency)
|
||||
- **Per release:** Recommended for quality gates (required if using formal gate process)
|
||||
- **Quarterly:** Audit entire suite
|
||||
|
||||
### Focus Reviews
|
||||
|
||||
For large suites, review incrementally:
|
||||
|
||||
**Week 1:** Review E2E tests
|
||||
**Week 2:** Review API tests
|
||||
**Week 3:** Review component tests (Cypress CT or Vitest)
|
||||
**Week 4:** Apply fixes across all suites
|
||||
|
||||
**Component Testing Note:** TEA reviews component tests using framework-specific knowledge:
|
||||
- **Cypress:** Reviews Cypress Component Testing specs (*.cy.tsx)
|
||||
- **Playwright:** Reviews Vitest component tests (*.test.tsx)
|
||||
|
||||
### Use Reviews for Learning
|
||||
|
||||
Share reports with team:
|
||||
|
||||
```
|
||||
Team Meeting:
|
||||
- Review test-review.md
|
||||
- Discuss critical issues
|
||||
- Agree on patterns
|
||||
- Update team guidelines
|
||||
```
|
||||
|
||||
### Compare Over Time
|
||||
|
||||
Track improvement:
|
||||
|
||||
```markdown
|
||||
## Quality Trend
|
||||
|
||||
| Date | Score | Critical Issues | Notes |
|
||||
|------|-------|-----------------|-------|
|
||||
| 2026-01-01 | 65 | 5 | Baseline |
|
||||
| 2026-01-15 | 72 | 2 | Fixed hard waits |
|
||||
| 2026-02-01 | 84 | 0 | All critical resolved |
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Low Determinism Score
|
||||
|
||||
**Symptoms:**
|
||||
- Tests fail randomly
|
||||
- "Works on my machine"
|
||||
- CI failures that don't reproduce locally
|
||||
|
||||
**Common Causes:**
|
||||
- Hard waits (`waitForTimeout`)
|
||||
- Conditional flow control (`if/else`)
|
||||
- Try-catch for flow control
|
||||
- Missing network-first patterns
|
||||
|
||||
**Fix:** Review determinism section, apply network-first patterns
|
||||
|
||||
### Low Performance Score
|
||||
|
||||
**Symptoms:**
|
||||
- Tests take > 1.5 minutes each
|
||||
- Test suite takes hours
|
||||
- CI times out
|
||||
|
||||
**Common Causes:**
|
||||
- Unnecessary waits (hard timeouts)
|
||||
- Inefficient selectors (XPath, complex CSS)
|
||||
- Not using parallelization
|
||||
- Heavy setup in every test
|
||||
|
||||
**Fix:** Optimize waits, improve selectors, use fixtures
|
||||
|
||||
### Low Isolation Score
|
||||
|
||||
**Symptoms:**
|
||||
- Tests fail when run in different order
|
||||
- Tests fail in parallel
|
||||
- Test data conflicts
|
||||
|
||||
**Common Causes:**
|
||||
- Shared global state
|
||||
- Tests don't clean up
|
||||
- Hard-coded test data
|
||||
- Database not reset between tests
|
||||
|
||||
**Fix:** Use fixtures, clean up in afterEach, use unique test data
|
||||
|
||||
### "Too Many Issues to Fix"
|
||||
|
||||
**Problem:** Report shows 50+ issues, overwhelming.
|
||||
|
||||
**Solution:** Prioritize:
|
||||
1. Fix all critical issues first
|
||||
2. Apply top 3 recommendations
|
||||
3. Re-run review
|
||||
4. Iterate
|
||||
|
||||
Don't try to fix everything at once.
|
||||
|
||||
### Reviews Take Too Long
|
||||
|
||||
**Problem:** Reviewing entire suite takes hours.
|
||||
|
||||
**Solution:** Review incrementally:
|
||||
- Review new tests in PR review
|
||||
- Schedule directory reviews weekly
|
||||
- Full suite review quarterly
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Generate tests to review
|
||||
- [How to Run Automate](/docs/tea/how-to/workflows/run-automate.md) - Expand coverage to review
|
||||
- [How to Run Trace](/docs/tea/how-to/workflows/run-trace.md) - Coverage complements quality
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - What makes tests good
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Avoiding flakiness
|
||||
- [Fixture Architecture](/docs/tea/explanation/fixture-architecture.md) - Reusable patterns
|
||||
|
||||
## Reference
|
||||
|
||||
- [Command: *test-review](/docs/tea/reference/commands.md#test-review) - Full command reference
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Patterns TEA reviews against
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,883 +0,0 @@
|
||||
---
|
||||
title: "How to Run Trace with TEA"
|
||||
description: Map requirements to tests and make quality gate decisions using TEA's trace workflow
|
||||
---
|
||||
|
||||
# How to Run Trace with TEA
|
||||
|
||||
Use TEA's `trace` workflow for requirements traceability and quality gate decisions. This is a two-phase workflow: Phase 1 analyzes coverage, Phase 2 makes the go/no-go decision.
|
||||
|
||||
## When to Use This
|
||||
|
||||
### Phase 1: Requirements Traceability
|
||||
- Map acceptance criteria to implemented tests
|
||||
- Identify coverage gaps
|
||||
- Prioritize missing tests
|
||||
- Refresh coverage after each story/epic
|
||||
|
||||
### Phase 2: Quality Gate Decision
|
||||
- Make go/no-go decision for release
|
||||
- Validate coverage meets thresholds
|
||||
- Document gate decision with evidence
|
||||
- Support business-approved waivers
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- Requirements defined (stories, acceptance criteria, test design)
|
||||
- Tests implemented
|
||||
- For brownfield: Existing codebase with tests
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Run the Trace Workflow
|
||||
|
||||
```
|
||||
trace
|
||||
```
|
||||
|
||||
### 2. Specify Phase
|
||||
|
||||
TEA will ask which phase you're running.
|
||||
|
||||
**Phase 1: Requirements Traceability**
|
||||
- Analyze coverage
|
||||
- Identify gaps
|
||||
- Generate recommendations
|
||||
|
||||
**Phase 2: Quality Gate Decision**
|
||||
- Make PASS/CONCERNS/FAIL/WAIVED decision
|
||||
- Requires Phase 1 complete
|
||||
|
||||
**Typical flow:** Run Phase 1 first, review gaps, then run Phase 2 for gate decision.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Requirements Traceability
|
||||
|
||||
### 3. Provide Requirements Source
|
||||
|
||||
TEA will ask where requirements are defined.
|
||||
|
||||
**Options:**
|
||||
|
||||
| Source | Example | Best For |
|
||||
| --------------- | ----------------------------- | ---------------------- |
|
||||
| **Story file** | `story-profile-management.md` | Single story coverage |
|
||||
| **Test design** | `test-design-epic-1.md` | Epic coverage |
|
||||
| **PRD** | `PRD.md` | System-level coverage |
|
||||
| **Multiple** | All of the above | Comprehensive analysis |
|
||||
|
||||
**Example Response:**
|
||||
```
|
||||
Requirements:
|
||||
- story-profile-management.md (acceptance criteria)
|
||||
- test-design-epic-1.md (test priorities)
|
||||
```
|
||||
|
||||
### 4. Specify Test Location
|
||||
|
||||
TEA will ask where tests are located.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Test location: tests/
|
||||
Include:
|
||||
- tests/api/
|
||||
- tests/e2e/
|
||||
```
|
||||
|
||||
### 5. Specify Focus Areas (Optional)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Focus on:
|
||||
- Profile CRUD operations
|
||||
- Validation scenarios
|
||||
- Authorization checks
|
||||
```
|
||||
|
||||
### 6. Review Coverage Matrix
|
||||
|
||||
TEA generates a comprehensive traceability matrix.
|
||||
|
||||
#### Traceability Matrix (`traceability-matrix.md`):
|
||||
|
||||
```markdown
|
||||
# Requirements Traceability Matrix
|
||||
|
||||
**Date:** 2026-01-13
|
||||
**Scope:** Epic 1 - User Profile Management
|
||||
**Phase:** Phase 1 (Traceability Analysis)
|
||||
|
||||
## Coverage Summary
|
||||
|
||||
| Metric | Count | Percentage |
|
||||
| ---------------------- | ----- | ---------- |
|
||||
| **Total Requirements** | 15 | 100% |
|
||||
| **Full Coverage** | 11 | 73% |
|
||||
| **Partial Coverage** | 3 | 20% |
|
||||
| **No Coverage** | 1 | 7% |
|
||||
|
||||
### By Priority
|
||||
|
||||
| Priority | Total | Covered | Percentage |
|
||||
| -------- | ----- | ------- | ----------------- |
|
||||
| **P0** | 5 | 5 | 100% ✅ |
|
||||
| **P1** | 6 | 5 | 83% ⚠️ |
|
||||
| **P2** | 3 | 1 | 33% ⚠️ |
|
||||
| **P3** | 1 | 0 | 0% ✅ (acceptable) |
|
||||
|
||||
---
|
||||
|
||||
## Detailed Traceability
|
||||
|
||||
### ✅ Requirement 1: User can view their profile (P0)
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- User navigates to /profile
|
||||
- Profile displays name, email, avatar
|
||||
- Data is current (not cached)
|
||||
|
||||
**Test Coverage:** FULL ✅
|
||||
|
||||
**Tests:**
|
||||
- `tests/e2e/profile-view.spec.ts:15` - "should display profile page with current data"
|
||||
- ✅ Navigates to /profile
|
||||
- ✅ Verifies name, email visible
|
||||
- ✅ Verifies avatar displayed
|
||||
- ✅ Validates data freshness via API assertion
|
||||
|
||||
- `tests/api/profile.spec.ts:8` - "should fetch user profile via API"
|
||||
- ✅ Calls GET /api/profile
|
||||
- ✅ Validates response schema
|
||||
- ✅ Confirms all fields present
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Requirement 2: User can edit profile (P0)
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- User clicks "Edit Profile"
|
||||
- Can modify name, email, bio
|
||||
- Can upload avatar
|
||||
- Changes are persisted
|
||||
- Success message shown
|
||||
|
||||
**Test Coverage:** PARTIAL ⚠️
|
||||
|
||||
**Tests:**
|
||||
- `tests/e2e/profile-edit.spec.ts:22` - "should edit and save profile"
|
||||
- ✅ Clicks edit button
|
||||
- ✅ Modifies name and email
|
||||
- ⚠️ **Does NOT test bio field**
|
||||
- ❌ **Does NOT test avatar upload**
|
||||
- ✅ Verifies persistence
|
||||
- ✅ Verifies success message
|
||||
|
||||
- `tests/api/profile.spec.ts:25` - "should update profile via PATCH"
|
||||
- ✅ Calls PATCH /api/profile
|
||||
- ✅ Validates update response
|
||||
- ⚠️ **Only tests name/email, not bio/avatar**
|
||||
|
||||
**Missing Coverage:**
|
||||
- Bio field not tested in E2E or API
|
||||
- Avatar upload not tested
|
||||
|
||||
**Gap Severity:** HIGH (P0 requirement, critical path)
|
||||
|
||||
---
|
||||
|
||||
### ✅ Requirement 3: Invalid email shows validation error (P1)
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Enter invalid email format
|
||||
- See error message
|
||||
- Cannot save changes
|
||||
|
||||
**Test Coverage:** FULL ✅
|
||||
|
||||
**Tests:**
|
||||
- `tests/e2e/profile-edit.spec.ts:45` - "should show validation error for invalid email"
|
||||
- `tests/api/profile.spec.ts:50` - "should return 400 for invalid email"
|
||||
|
||||
---
|
||||
|
||||
### ❌ Requirement 15: Profile export as PDF (P2)
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- User clicks "Export Profile"
|
||||
- PDF downloads with profile data
|
||||
|
||||
**Test Coverage:** NONE ❌
|
||||
|
||||
**Gap Analysis:**
|
||||
- **Priority:** P2 (medium)
|
||||
- **Risk:** Low (non-critical feature)
|
||||
- **Recommendation:** Add in next iteration (not blocking for release)
|
||||
|
||||
---
|
||||
|
||||
## Gap Prioritization
|
||||
|
||||
### Critical Gaps (Must Fix Before Release)
|
||||
|
||||
| Gap | Requirement | Priority | Risk | Recommendation |
|
||||
| --- | ------------------------ | -------- | ---- | ------------------- |
|
||||
| 1 | Bio field not tested | P0 | High | Add E2E + API tests |
|
||||
| 2 | Avatar upload not tested | P0 | High | Add E2E + API tests |
|
||||
|
||||
**Estimated Effort:** 3 hours
|
||||
**Owner:** QA team
|
||||
**Deadline:** Before release
|
||||
|
||||
### Non-Critical Gaps (Can Defer)
|
||||
|
||||
| Gap | Requirement | Priority | Risk | Recommendation |
|
||||
| --- | ------------------------- | -------- | ---- | ------------------- |
|
||||
| 3 | Profile export not tested | P2 | Low | Add in v1.3 release |
|
||||
|
||||
**Estimated Effort:** 2 hours
|
||||
**Owner:** QA team
|
||||
**Deadline:** Next release (February)
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### 1. Add Bio Field Tests
|
||||
|
||||
**Tests Needed (Vanilla Playwright):**
|
||||
```typescript
|
||||
// tests/e2e/profile-edit.spec.ts
|
||||
test('should edit bio field', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
await page.getByLabel('Bio').fill('New bio text');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByText('New bio text')).toBeVisible();
|
||||
});
|
||||
|
||||
// tests/api/profile.spec.ts
|
||||
test('should update bio via API', async ({ request }) => {
|
||||
const response = await request.patch('/api/profile', {
|
||||
data: { bio: 'Updated bio' }
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const { bio } = await response.json();
|
||||
expect(bio).toBe('Updated bio');
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
// tests/e2e/profile-edit.spec.ts
|
||||
import { test } from '../support/fixtures'; // Composed with authToken
|
||||
|
||||
test('should edit bio field', async ({ page, authToken }) => {
|
||||
await page.goto('/profile');
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
await page.getByLabel('Bio').fill('New bio text');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByText('New bio text')).toBeVisible();
|
||||
});
|
||||
|
||||
// tests/api/profile.spec.ts
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { createAuthFixtures } from '@seontechnologies/playwright-utils/auth-session';
|
||||
import { mergeTests } from '@playwright/test';
|
||||
|
||||
// Merge API request + auth fixtures
|
||||
const authFixtureTest = base.extend(createAuthFixtures());
|
||||
const test = mergeTests(apiRequestFixture, authFixtureTest);
|
||||
|
||||
test('should update bio via API', async ({ apiRequest, authToken }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { bio: 'Updated bio' },
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.bio).toBe('Updated bio');
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** `authToken` requires auth-session fixture setup. See [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md#auth-session).
|
||||
|
||||
### 2. Add Avatar Upload Tests
|
||||
|
||||
**Tests Needed:**
|
||||
```typescript
|
||||
// tests/e2e/profile-edit.spec.ts
|
||||
test('should upload avatar image', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
|
||||
// Upload file
|
||||
await page.setInputFiles('[type="file"]', 'fixtures/avatar.png');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Verify uploaded image displays
|
||||
await expect(page.locator('img[alt="Profile avatar"]')).toBeVisible();
|
||||
});
|
||||
|
||||
// tests/api/profile.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
test('should accept valid image upload', async ({ request }) => {
|
||||
const response = await request.post('/api/profile/avatar', {
|
||||
multipart: {
|
||||
file: {
|
||||
name: 'avatar.png',
|
||||
mimeType: 'image/png',
|
||||
buffer: await fs.readFile('fixtures/avatar.png')
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After reviewing traceability:
|
||||
|
||||
1. **Fix critical gaps** - Add tests for P0/P1 requirements
|
||||
2. **Run `test-review`** - Ensure new tests meet quality standards
|
||||
3. **Run Phase 2** - Make gate decision after gaps addressed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Quality Gate Decision
|
||||
|
||||
After Phase 1 coverage analysis is complete, run Phase 2 for the gate decision.
|
||||
|
||||
**Prerequisites:**
|
||||
- Phase 1 traceability matrix complete
|
||||
- Test execution results available (must have test results)
|
||||
|
||||
**Note:** Phase 2 will skip if test execution results aren't provided. The workflow requires actual test run results to make gate decisions.
|
||||
|
||||
### 7. Run Phase 2
|
||||
|
||||
```
|
||||
trace
|
||||
```
|
||||
|
||||
Select "Phase 2: Quality Gate Decision"
|
||||
|
||||
### 8. Provide Additional Context
|
||||
|
||||
TEA will ask for:
|
||||
|
||||
**Gate Type:**
|
||||
- Story gate (small release)
|
||||
- Epic gate (larger release)
|
||||
- Release gate (production deployment)
|
||||
- Hotfix gate (emergency fix)
|
||||
|
||||
**Decision Mode:**
|
||||
- **Deterministic** - Rule-based (coverage %, quality scores)
|
||||
- **Manual** - Team decision with TEA guidance
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Gate type: Epic gate
|
||||
Decision mode: Deterministic
|
||||
```
|
||||
|
||||
### 9. Provide Supporting Evidence
|
||||
|
||||
TEA will request:
|
||||
|
||||
**Phase 1 Results:**
|
||||
```
|
||||
traceability-matrix.md (from Phase 1)
|
||||
```
|
||||
|
||||
**Test Quality (Optional):**
|
||||
```
|
||||
test-review.md (from test-review)
|
||||
```
|
||||
|
||||
**NFR Assessment (Optional):**
|
||||
```
|
||||
nfr-assessment.md (from nfr-assess)
|
||||
```
|
||||
|
||||
### 10. Review Gate Decision
|
||||
|
||||
TEA makes evidence-based gate decision and writes to separate file.
|
||||
|
||||
#### Gate Decision (`gate-decision-{gate_type}-{story_id}.md`):
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
# Phase 2: Quality Gate Decision
|
||||
|
||||
**Gate Type:** Epic Gate
|
||||
**Decision:** PASS ✅
|
||||
**Date:** 2026-01-13
|
||||
**Approvers:** Product Manager, Tech Lead, QA Lead
|
||||
|
||||
## Decision Summary
|
||||
|
||||
**Verdict:** Ready to release
|
||||
|
||||
**Evidence:**
|
||||
- P0 coverage: 100% (5/5 requirements)
|
||||
- P1 coverage: 100% (6/6 requirements)
|
||||
- P2 coverage: 33% (1/3 requirements) - acceptable
|
||||
- Test quality score: 84/100
|
||||
- NFR assessment: PASS
|
||||
|
||||
## Coverage Analysis
|
||||
|
||||
| Priority | Required Coverage | Actual Coverage | Status |
|
||||
| -------- | ----------------- | --------------- | --------------------- |
|
||||
| **P0** | 100% | 100% | ✅ PASS |
|
||||
| **P1** | 90% | 100% | ✅ PASS |
|
||||
| **P2** | 50% | 33% | ⚠️ Below (acceptable) |
|
||||
| **P3** | 20% | 0% | ✅ PASS (low priority) |
|
||||
|
||||
**Rationale:**
|
||||
- All critical path (P0) requirements fully tested
|
||||
- All high-value (P1) requirements fully tested
|
||||
- P2 gap (profile export) is low risk and deferred to next release
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
| Metric | Threshold | Actual | Status |
|
||||
| ------------------ | --------- | ------ | ------ |
|
||||
| P0/P1 Coverage | >95% | 100% | ✅ |
|
||||
| Test Quality Score | >80 | 84 | ✅ |
|
||||
| NFR Status | PASS | PASS | ✅ |
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
### Accepted Risks
|
||||
|
||||
**Risk 1: Profile export not tested (P2)**
|
||||
- **Impact:** Medium (users can't export profile)
|
||||
- **Mitigation:** Feature flag disabled by default
|
||||
- **Plan:** Add tests in v1.3 release (February)
|
||||
- **Monitoring:** Track feature flag usage
|
||||
|
||||
## Approvals
|
||||
|
||||
- [x] **Product Manager** - Business requirements met (Approved: 2026-01-13)
|
||||
- [x] **Tech Lead** - Technical quality acceptable (Approved: 2026-01-13)
|
||||
- [x] **QA Lead** - Test coverage sufficient (Approved: 2026-01-13)
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Deployment
|
||||
1. Merge to main branch
|
||||
2. Deploy to staging
|
||||
3. Run smoke tests in staging
|
||||
4. Deploy to production
|
||||
5. Monitor for 24 hours
|
||||
|
||||
### Monitoring
|
||||
- Set alerts for profile endpoint (P99 > 200ms)
|
||||
- Track error rates (target: <0.1%)
|
||||
- Monitor profile export feature flag usage
|
||||
|
||||
### Future Work
|
||||
- Add profile export tests (v1.3)
|
||||
- Expand P2 coverage to 50%
|
||||
```
|
||||
|
||||
### Gate Decision Rules
|
||||
|
||||
TEA uses deterministic rules when decision_mode = "deterministic":
|
||||
|
||||
| P0 Coverage | P1 Coverage | Overall Coverage | Decision |
|
||||
| ----------- | ----------- | ---------------- | ---------------------------- |
|
||||
| 100% | ≥90% | ≥80% | **PASS** ✅ |
|
||||
| 100% | 80-89% | ≥80% | **CONCERNS** ⚠️ |
|
||||
| <100% | Any | Any | **FAIL** ❌ |
|
||||
| Any | <80% | Any | **FAIL** ❌ |
|
||||
| Any | Any | <80% | **FAIL** ❌ |
|
||||
| Any | Any | Any | **WAIVED** ⏭️ (with approval) |
|
||||
|
||||
**Detailed Rules:**
|
||||
- **PASS:** P0=100%, P1≥90%, Overall≥80%
|
||||
- **CONCERNS:** P0=100%, P1 80-89%, Overall≥80% (below threshold but not critical)
|
||||
- **FAIL:** P0<100% OR P1<80% OR Overall<80% (critical gaps)
|
||||
|
||||
**PASS** ✅: All criteria met, ready to release
|
||||
|
||||
**CONCERNS** ⚠️: Some criteria not met, but:
|
||||
- Mitigation plan exists
|
||||
- Risk is acceptable
|
||||
- Team approves proceeding
|
||||
- Monitoring in place
|
||||
|
||||
**FAIL** ❌: Critical criteria not met:
|
||||
- P0 requirements not tested
|
||||
- Critical security vulnerabilities
|
||||
- System is broken
|
||||
- Cannot deploy
|
||||
|
||||
**WAIVED** ⏭️: Business approves proceeding despite concerns:
|
||||
- Documented business justification
|
||||
- Accepted risks quantified
|
||||
- Approver signatures
|
||||
- Future plans documented
|
||||
|
||||
### Example CONCERNS Decision
|
||||
|
||||
```markdown
|
||||
## Decision Summary
|
||||
|
||||
**Verdict:** CONCERNS ⚠️ - Proceed with monitoring
|
||||
|
||||
**Evidence:**
|
||||
- P0 coverage: 100%
|
||||
- P1 coverage: 85% (below 90% target)
|
||||
- Test quality: 78/100 (below 80 target)
|
||||
|
||||
**Gaps:**
|
||||
- 1 P1 requirement not tested (avatar upload)
|
||||
- Test quality score slightly below threshold
|
||||
|
||||
**Mitigation:**
|
||||
- Avatar upload not critical for v1.2 launch
|
||||
- Test quality issues are minor (no flakiness)
|
||||
- Monitoring alerts configured
|
||||
|
||||
**Approvals:**
|
||||
- Product Manager: APPROVED (business priority to launch)
|
||||
- Tech Lead: APPROVED (technical risk acceptable)
|
||||
```
|
||||
|
||||
### Example FAIL Decision
|
||||
|
||||
```markdown
|
||||
## Decision Summary
|
||||
|
||||
**Verdict:** FAIL ❌ - Cannot release
|
||||
|
||||
**Evidence:**
|
||||
- P0 coverage: 60% (below 95% threshold)
|
||||
- Critical security vulnerability (CVE-2024-12345)
|
||||
- Test quality: 55/100
|
||||
|
||||
**Blockers:**
|
||||
1. **Login flow not tested** (P0 requirement)
|
||||
- Critical path completely untested
|
||||
- Must add E2E and API tests
|
||||
|
||||
2. **SQL injection vulnerability**
|
||||
- Critical security issue
|
||||
- Must fix before deployment
|
||||
|
||||
**Actions Required:**
|
||||
1. Add login tests (QA team, 2 days)
|
||||
2. Fix SQL injection (backend team, 1 day)
|
||||
3. Re-run security scan (DevOps, 1 hour)
|
||||
4. Re-run trace after fixes
|
||||
|
||||
**Cannot proceed until all blockers resolved.**
|
||||
```
|
||||
|
||||
## What You Get
|
||||
|
||||
### Phase 1: Traceability Matrix
|
||||
- Requirement-to-test mapping
|
||||
- Coverage classification (FULL/PARTIAL/NONE)
|
||||
- Gap identification with priorities
|
||||
- Actionable recommendations
|
||||
|
||||
### Phase 2: Gate Decision
|
||||
- Go/no-go verdict (PASS/CONCERNS/FAIL/WAIVED)
|
||||
- Evidence summary
|
||||
- Approval signatures
|
||||
- Next steps and monitoring plan
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Greenfield Projects
|
||||
|
||||
**Phase 3:**
|
||||
```
|
||||
After architecture complete:
|
||||
1. Run test-design (system-level)
|
||||
2. Run trace Phase 1 (baseline)
|
||||
3. Use for implementation-readiness gate
|
||||
```
|
||||
|
||||
**Phase 4:**
|
||||
```
|
||||
After each epic/story:
|
||||
1. Run trace Phase 1 (refresh coverage)
|
||||
2. Identify gaps
|
||||
3. Add missing tests
|
||||
```
|
||||
|
||||
**Release Gate:**
|
||||
```
|
||||
Before deployment:
|
||||
1. Run trace Phase 1 (final coverage check)
|
||||
2. Run trace Phase 2 (make gate decision)
|
||||
3. Get approvals
|
||||
4. Deploy (if PASS or WAIVED)
|
||||
```
|
||||
|
||||
### Brownfield Projects
|
||||
|
||||
**Phase 2:**
|
||||
```
|
||||
Before planning new work:
|
||||
1. Run trace Phase 1 (establish baseline)
|
||||
2. Understand existing coverage
|
||||
3. Plan testing strategy
|
||||
```
|
||||
|
||||
**Phase 4:**
|
||||
```
|
||||
After each epic/story:
|
||||
1. Run trace Phase 1 (refresh)
|
||||
2. Compare to baseline
|
||||
3. Track coverage improvement
|
||||
```
|
||||
|
||||
**Release Gate:**
|
||||
```
|
||||
Before deployment:
|
||||
1. Run trace Phase 1 (final check)
|
||||
2. Run trace Phase 2 (gate decision)
|
||||
3. Compare to baseline
|
||||
4. Deploy if coverage maintained or improved
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
### Run Phase 1 Frequently
|
||||
|
||||
Don't wait until release gate:
|
||||
|
||||
```
|
||||
After Story 1: trace Phase 1 (identify gaps early)
|
||||
After Story 2: trace Phase 1 (refresh)
|
||||
After Story 3: trace Phase 1 (refresh)
|
||||
Before Release: trace Phase 1 + Phase 2 (final gate)
|
||||
```
|
||||
|
||||
**Benefit:** Catch gaps early when they're cheap to fix.
|
||||
|
||||
### Use Coverage Trends
|
||||
|
||||
Track improvement over time:
|
||||
|
||||
```markdown
|
||||
## Coverage Trend
|
||||
|
||||
| Date | Epic | P0/P1 Coverage | Quality Score | Status |
|
||||
| ---------- | -------- | -------------- | ------------- | -------------- |
|
||||
| 2026-01-01 | Baseline | 45% | - | Starting point |
|
||||
| 2026-01-08 | Epic 1 | 78% | 72 | Improving |
|
||||
| 2026-01-15 | Epic 2 | 92% | 84 | Near target |
|
||||
| 2026-01-20 | Epic 3 | 100% | 88 | Ready! |
|
||||
```
|
||||
|
||||
### Set Coverage Targets by Priority
|
||||
|
||||
Don't aim for 100% across all priorities:
|
||||
|
||||
**Recommended Targets:**
|
||||
- **P0:** 100% (critical path must be tested)
|
||||
- **P1:** 90% (high-value scenarios)
|
||||
- **P2:** 50% (nice-to-have features)
|
||||
- **P3:** 20% (low-value edge cases)
|
||||
|
||||
### Use Classification Strategically
|
||||
|
||||
**FULL** ✅: Requirement completely tested
|
||||
- E2E test covers full user workflow
|
||||
- API test validates backend behavior
|
||||
- All acceptance criteria covered
|
||||
|
||||
**PARTIAL** ⚠️: Some aspects tested
|
||||
- E2E test exists but missing scenarios
|
||||
- API test exists but incomplete
|
||||
- Some acceptance criteria not covered
|
||||
|
||||
**NONE** ❌: No tests exist
|
||||
- Requirement identified but not tested
|
||||
- May be intentional (low priority) or oversight
|
||||
|
||||
**Classification helps prioritize:**
|
||||
- Fix NONE coverage for P0/P1 requirements first
|
||||
- Enhance PARTIAL coverage for P0 requirements
|
||||
- Accept PARTIAL or NONE for P2/P3 if time-constrained
|
||||
|
||||
### Automate Gate Decisions
|
||||
|
||||
Use traceability in CI:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/gate-check.yml
|
||||
- name: Check coverage
|
||||
run: |
|
||||
# Run trace Phase 1
|
||||
# Parse coverage percentages
|
||||
if [ $P0_COVERAGE -lt 95 ]; then
|
||||
echo "P0 coverage below 95%"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Document Waivers Clearly
|
||||
|
||||
If proceeding with WAIVED:
|
||||
|
||||
**Required:**
|
||||
```markdown
|
||||
## Waiver Documentation
|
||||
|
||||
**Waived By:** VP Engineering, Product Lead
|
||||
**Date:** 2026-01-15
|
||||
**Gate Type:** Release Gate v1.2
|
||||
|
||||
**Justification:**
|
||||
Business critical to launch by Q1 for investor demo.
|
||||
Performance concerns acceptable for initial user base.
|
||||
|
||||
**Conditions:**
|
||||
- Set monitoring alerts for P99 > 300ms
|
||||
- Plan optimization for v1.3 (due February 28)
|
||||
- Monitor user feedback closely
|
||||
|
||||
**Accepted Risks:**
|
||||
- 1% of users may experience 350ms latency
|
||||
- Avatar upload feature incomplete
|
||||
- Profile export deferred to next release
|
||||
|
||||
**Quantified Impact:**
|
||||
- Affects <100 users at current scale
|
||||
- Workaround exists (manual export)
|
||||
- Monitoring will catch issues early
|
||||
|
||||
**Approvals:**
|
||||
- VP Engineering: [Signature] Date: 2026-01-15
|
||||
- Product Lead: [Signature] Date: 2026-01-15
|
||||
- QA Lead: [Signature] Date: 2026-01-15
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Too Many Gaps to Fix
|
||||
|
||||
**Problem:** Phase 1 shows 50 uncovered requirements.
|
||||
|
||||
**Solution:** Prioritize ruthlessly:
|
||||
1. Fix all P0 gaps (critical path)
|
||||
2. Fix high-risk P1 gaps
|
||||
3. Accept low-risk P1 gaps with mitigation
|
||||
4. Defer all P2/P3 gaps
|
||||
|
||||
**Don't try to fix everything** - focus on what matters for release.
|
||||
|
||||
### Can't Find Test Coverage
|
||||
|
||||
**Problem:** Tests exist but TEA can't map them to requirements.
|
||||
|
||||
**Cause:** Tests don't reference requirements.
|
||||
|
||||
**Solution:** Add traceability comments:
|
||||
```typescript
|
||||
test('should display profile', async ({ page }) => {
|
||||
// Covers: Requirement 1 - User can view profile
|
||||
// Acceptance criteria: Navigate to /profile, see name/email
|
||||
await page.goto('/profile');
|
||||
await expect(page.getByText('Test User')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
Or use test IDs:
|
||||
```typescript
|
||||
test('[REQ-1] should display profile', async ({ page }) => {
|
||||
// Test code...
|
||||
});
|
||||
```
|
||||
|
||||
### Unclear What "FULL" vs "PARTIAL" Means
|
||||
|
||||
**FULL** ✅: All acceptance criteria tested
|
||||
```
|
||||
Requirement: User can edit profile
|
||||
Acceptance criteria:
|
||||
- Can modify name ✅ Tested
|
||||
- Can modify email ✅ Tested
|
||||
- Can upload avatar ✅ Tested
|
||||
- Changes persist ✅ Tested
|
||||
Result: FULL coverage
|
||||
```
|
||||
|
||||
**PARTIAL** ⚠️: Some criteria tested, some not
|
||||
```
|
||||
Requirement: User can edit profile
|
||||
Acceptance criteria:
|
||||
- Can modify name ✅ Tested
|
||||
- Can modify email ✅ Tested
|
||||
- Can upload avatar ❌ Not tested
|
||||
- Changes persist ✅ Tested
|
||||
Result: PARTIAL coverage (3/4 criteria)
|
||||
```
|
||||
|
||||
### Gate Decision Unclear
|
||||
|
||||
**Problem:** Not sure if PASS or CONCERNS is appropriate.
|
||||
|
||||
**Guideline:**
|
||||
|
||||
**Use PASS** ✅ if:
|
||||
- All P0 requirements 100% covered
|
||||
- P1 requirements >90% covered
|
||||
- No critical issues
|
||||
- NFRs met
|
||||
|
||||
**Use CONCERNS** ⚠️ if:
|
||||
- P1 coverage 85-90% (close to threshold)
|
||||
- Minor quality issues (score 70-79)
|
||||
- NFRs have mitigation plans
|
||||
- Team agrees risk is acceptable
|
||||
|
||||
**Use FAIL** ❌ if:
|
||||
- P0 coverage <100% (critical path gaps)
|
||||
- P1 coverage <85%
|
||||
- Critical security/performance issues
|
||||
- No mitigation possible
|
||||
|
||||
**When in doubt, use CONCERNS** and document the risk.
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Provides requirements for traceability
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Quality scores feed gate
|
||||
- [How to Run NFR Assessment](/docs/tea/how-to/workflows/run-nfr-assess.md) - NFR status feeds gate
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - Why P0 vs P3 matters
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Gate decisions in context
|
||||
|
||||
## Reference
|
||||
|
||||
- [Command: *trace](/docs/tea/reference/commands.md#trace) - Full command reference
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Config options
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,712 +0,0 @@
|
||||
---
|
||||
title: "How to Set Up CI Pipeline with TEA"
|
||||
description: Configure automated test execution with selective testing and burn-in loops using TEA
|
||||
---
|
||||
|
||||
# How to Set Up CI Pipeline with TEA
|
||||
|
||||
Use TEA's `ci` workflow to scaffold production-ready CI/CD configuration for automated test execution with selective testing, parallel sharding, and flakiness detection.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Need to automate test execution in CI/CD
|
||||
- Want selective testing (only run affected tests)
|
||||
- Need parallel execution for faster feedback
|
||||
- Want burn-in loops for flakiness detection
|
||||
- Setting up new CI/CD pipeline
|
||||
- Optimizing existing CI/CD workflow
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- BMad Method installed
|
||||
- TEA agent available
|
||||
- Test framework configured (run `framework` first)
|
||||
- Tests written (have something to run in CI)
|
||||
- CI/CD platform access (GitHub Actions, GitLab CI, etc.)
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load TEA Agent
|
||||
|
||||
Start a fresh chat and load TEA:
|
||||
|
||||
```
|
||||
tea
|
||||
```
|
||||
|
||||
### 2. Run the CI Workflow
|
||||
|
||||
```
|
||||
ci
|
||||
```
|
||||
|
||||
### 3. Select CI/CD Platform
|
||||
|
||||
TEA will ask which platform you're using.
|
||||
|
||||
**Supported Platforms:**
|
||||
- **GitHub Actions** (most common)
|
||||
- **GitLab CI**
|
||||
- **Circle CI**
|
||||
- **Jenkins**
|
||||
- **Other** (TEA provides generic template)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GitHub Actions
|
||||
```
|
||||
|
||||
### 4. Configure Test Strategy
|
||||
|
||||
TEA will ask about your test execution strategy.
|
||||
|
||||
#### Repository Structure
|
||||
|
||||
**Question:** "What's your repository structure?"
|
||||
|
||||
**Options:**
|
||||
- **Single app** - One application in root
|
||||
- **Monorepo** - Multiple apps/packages
|
||||
- **Monorepo with affected detection** - Only test changed packages
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Monorepo with multiple apps
|
||||
Need selective testing for changed packages only
|
||||
```
|
||||
|
||||
#### Parallel Execution
|
||||
|
||||
**Question:** "Want to shard tests for parallel execution?"
|
||||
|
||||
**Options:**
|
||||
- **No sharding** - Run tests sequentially
|
||||
- **Shard by workers** - Split across N workers
|
||||
- **Shard by file** - Each file runs in parallel
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Yes, shard across 4 workers for faster execution
|
||||
```
|
||||
|
||||
**Why Shard?**
|
||||
- **4 workers:** 20-minute suite → 5 minutes
|
||||
- **Better resource usage:** Utilize CI runners efficiently
|
||||
- **Faster feedback:** Developers wait less
|
||||
|
||||
#### Burn-In Loops
|
||||
|
||||
**Question:** "Want burn-in loops for flakiness detection?"
|
||||
|
||||
**Options:**
|
||||
- **No burn-in** - Run tests once
|
||||
- **PR burn-in** - Run tests multiple times on PRs
|
||||
- **Nightly burn-in** - Dedicated flakiness detection job
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Yes, run tests 5 times on PRs to catch flaky tests early
|
||||
```
|
||||
|
||||
**Why Burn-In?**
|
||||
- Catches flaky tests before they merge
|
||||
- Prevents intermittent CI failures
|
||||
- Builds confidence in test suite
|
||||
|
||||
### 5. Review Generated CI Configuration
|
||||
|
||||
TEA generates platform-specific workflow files.
|
||||
|
||||
#### GitHub Actions (`.github/workflows/test.yml`):
|
||||
|
||||
```yaml
|
||||
name: Test Suite
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # Nightly at 2 AM
|
||||
|
||||
jobs:
|
||||
# Main test job with sharding
|
||||
test:
|
||||
name: Test (Shard ${{ matrix.shard }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run tests
|
||||
run: npx playwright test --shard=${{ matrix.shard }}/4
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload test report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-${{ matrix.shard }}
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
# Burn-in job for flakiness detection (PRs only)
|
||||
burn-in:
|
||||
name: Burn-In (Flakiness Detection)
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run burn-in loop
|
||||
run: |
|
||||
for i in {1..5}; do
|
||||
echo "=== Burn-in iteration $i/5 ==="
|
||||
npx playwright test --grep-invert "@skip" || exit 1
|
||||
done
|
||||
|
||||
- name: Upload burn-in results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: burn-in-failures
|
||||
path: test-results/
|
||||
|
||||
# Selective testing (changed files only)
|
||||
selective:
|
||||
name: Selective Tests
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Full history for git diff
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run selective tests
|
||||
run: npm run test:changed
|
||||
```
|
||||
|
||||
#### GitLab CI (`.gitlab-ci.yml`):
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
NODE_VERSION: "18"
|
||||
|
||||
stages:
|
||||
- test
|
||||
- burn-in
|
||||
|
||||
# Test job with parallel execution
|
||||
test:
|
||||
stage: test
|
||||
image: node:$NODE_VERSION
|
||||
parallel: 4
|
||||
script:
|
||||
- npm ci
|
||||
- npx playwright install --with-deps
|
||||
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- test-results/
|
||||
- playwright-report/
|
||||
expire_in: 7 days
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
# Burn-in job for flakiness detection
|
||||
burn-in:
|
||||
stage: burn-in
|
||||
image: node:$NODE_VERSION
|
||||
script:
|
||||
- npm ci
|
||||
- npx playwright install --with-deps
|
||||
- |
|
||||
for i in {1..5}; do
|
||||
echo "=== Burn-in iteration $i/5 ==="
|
||||
npx playwright test || exit 1
|
||||
done
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- test-results/
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
```
|
||||
|
||||
#### Burn-In Testing
|
||||
|
||||
**Option 1: Classic Burn-In (Playwright Built-In)**
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:burn-in": "playwright test --repeat-each=5 --retries=0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- Runs every test 5 times
|
||||
- Fails if any iteration fails
|
||||
- Detects flakiness before merge
|
||||
|
||||
**Use when:** Small test suite, want to run everything multiple times
|
||||
|
||||
---
|
||||
|
||||
**Option 2: Smart Burn-In (Playwright Utils)**
|
||||
|
||||
If `tea_use_playwright_utils: true`:
|
||||
|
||||
**scripts/burn-in-changed.ts:**
|
||||
```typescript
|
||||
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in';
|
||||
|
||||
await runBurnIn({
|
||||
configPath: 'playwright.burn-in.config.ts',
|
||||
baseBranch: 'main'
|
||||
});
|
||||
```
|
||||
|
||||
**playwright.burn-in.config.ts:**
|
||||
```typescript
|
||||
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in';
|
||||
|
||||
const config: BurnInConfig = {
|
||||
skipBurnInPatterns: ['**/config/**', '**/*.md', '**/*types*'],
|
||||
burnInTestPercentage: 0.3,
|
||||
burnIn: { repeatEach: 5, retries: 0 }
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
**package.json:**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test:burn-in": "tsx scripts/burn-in-changed.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- Git diff analysis (only affected tests)
|
||||
- Smart filtering (skip configs, docs, types)
|
||||
- Volume control (run 30% of affected tests)
|
||||
- Each test runs 5 times
|
||||
|
||||
**Use when:** Large test suite, want intelligent selection
|
||||
|
||||
---
|
||||
|
||||
**Comparison:**
|
||||
|
||||
| Feature | Classic Burn-In | Smart Burn-In (PW-Utils) |
|
||||
|---------|----------------|--------------------------|
|
||||
| Changed 1 file | Runs all 500 tests × 5 = 2500 runs | Runs 3 affected tests × 5 = 15 runs |
|
||||
| Config change | Runs all tests | Skips (no tests affected) |
|
||||
| Type change | Runs all tests | Skips (no runtime impact) |
|
||||
| Setup | Zero config | Requires config file |
|
||||
|
||||
**Recommendation:** Start with classic (simple), upgrade to smart (faster) when suite grows.
|
||||
|
||||
### 6. Configure Secrets
|
||||
|
||||
TEA provides a secrets checklist.
|
||||
|
||||
**Required Secrets** (add to CI/CD platform):
|
||||
|
||||
```markdown
|
||||
## GitHub Actions Secrets
|
||||
|
||||
Repository Settings → Secrets and variables → Actions
|
||||
|
||||
### Required
|
||||
- None (tests run without external auth)
|
||||
|
||||
### Optional
|
||||
- `TEST_USER_EMAIL` - Test user credentials
|
||||
- `TEST_USER_PASSWORD` - Test user password
|
||||
- `API_BASE_URL` - API endpoint for tests
|
||||
- `DATABASE_URL` - Test database (if needed)
|
||||
```
|
||||
|
||||
**How to Add Secrets:**
|
||||
|
||||
**GitHub Actions:**
|
||||
1. Go to repo Settings → Secrets → Actions
|
||||
2. Click "New repository secret"
|
||||
3. Add name and value
|
||||
4. Use in workflow: `${{ secrets.TEST_USER_EMAIL }}`
|
||||
|
||||
**GitLab CI:**
|
||||
1. Go to Project Settings → CI/CD → Variables
|
||||
2. Add variable name and value
|
||||
3. Use in workflow: `$TEST_USER_EMAIL`
|
||||
|
||||
### 7. Test the CI Pipeline
|
||||
|
||||
#### Push and Verify
|
||||
|
||||
**Commit the workflow file:**
|
||||
```bash
|
||||
git add .github/workflows/test.yml
|
||||
git commit -m "ci: add automated test pipeline"
|
||||
git push
|
||||
```
|
||||
|
||||
**Watch the CI run:**
|
||||
- GitHub Actions: Go to Actions tab
|
||||
- GitLab CI: Go to CI/CD → Pipelines
|
||||
- Circle CI: Go to Pipelines
|
||||
|
||||
**Expected Result:**
|
||||
```
|
||||
✓ test (shard 1/4) - 3m 24s
|
||||
✓ test (shard 2/4) - 3m 18s
|
||||
✓ test (shard 3/4) - 3m 31s
|
||||
✓ test (shard 4/4) - 3m 15s
|
||||
✓ burn-in - 15m 42s
|
||||
```
|
||||
|
||||
#### Test on Pull Request
|
||||
|
||||
**Create test PR:**
|
||||
```bash
|
||||
git checkout -b test-ci-setup
|
||||
echo "# Test" > test.md
|
||||
git add test.md
|
||||
git commit -m "test: verify CI setup"
|
||||
git push -u origin test-ci-setup
|
||||
```
|
||||
|
||||
**Open PR and verify:**
|
||||
- Tests run automatically
|
||||
- Burn-in runs (if configured for PRs)
|
||||
- Selective tests run (if applicable)
|
||||
- All checks pass ✓
|
||||
|
||||
## What You Get
|
||||
|
||||
### Automated Test Execution
|
||||
- **On every PR** - Catch issues before merge
|
||||
- **On every push to main** - Protect production
|
||||
- **Nightly** - Comprehensive regression testing
|
||||
|
||||
### Parallel Execution
|
||||
- **4x faster feedback** - Shard across multiple workers
|
||||
- **Efficient resource usage** - Maximize CI runner utilization
|
||||
|
||||
### Selective Testing
|
||||
- **Run only affected tests** - Git diff-based selection
|
||||
- **Faster PR feedback** - Don't run entire suite every time
|
||||
|
||||
### Flakiness Detection
|
||||
- **Burn-in loops** - Run tests multiple times
|
||||
- **Early detection** - Catch flaky tests in PRs
|
||||
- **Confidence building** - Know tests are reliable
|
||||
|
||||
### Artifact Collection
|
||||
- **Test results** - Saved for 7 days
|
||||
- **Screenshots** - On test failures
|
||||
- **Videos** - Full test recordings
|
||||
- **Traces** - Playwright trace files for debugging
|
||||
|
||||
## Tips
|
||||
|
||||
### Start Simple, Add Complexity
|
||||
|
||||
**Week 1:** Basic pipeline
|
||||
```yaml
|
||||
- Run tests on PR
|
||||
- Single worker (no sharding)
|
||||
```
|
||||
|
||||
**Week 2:** Add parallelization
|
||||
```yaml
|
||||
- Shard across 4 workers
|
||||
- Faster feedback
|
||||
```
|
||||
|
||||
**Week 3:** Add selective testing
|
||||
```yaml
|
||||
- Git diff-based selection
|
||||
- Skip unaffected tests
|
||||
```
|
||||
|
||||
**Week 4:** Add burn-in
|
||||
```yaml
|
||||
- Detect flaky tests
|
||||
- Run on PR and nightly
|
||||
```
|
||||
|
||||
### Optimize for Feedback Speed
|
||||
|
||||
**Goal:** PR feedback in < 5 minutes
|
||||
|
||||
**Strategies:**
|
||||
- Shard tests across workers (4 workers = 4x faster)
|
||||
- Use selective testing (run 20% of tests, not 100%)
|
||||
- Cache dependencies (`actions/cache`, `cache: 'npm'`)
|
||||
- Run smoke tests first, full suite after
|
||||
|
||||
**Example fast workflow:**
|
||||
```yaml
|
||||
jobs:
|
||||
smoke:
|
||||
# Run critical path tests (2 min)
|
||||
run: npm run test:smoke
|
||||
|
||||
full:
|
||||
needs: smoke
|
||||
# Run full suite only if smoke passes (10 min)
|
||||
run: npm test
|
||||
```
|
||||
|
||||
### Use Test Tags
|
||||
|
||||
Tag tests for selective execution:
|
||||
|
||||
```typescript
|
||||
// Critical path tests (always run)
|
||||
test('@critical should login', async ({ page }) => { });
|
||||
|
||||
// Smoke tests (run first)
|
||||
test('@smoke should load homepage', async ({ page }) => { });
|
||||
|
||||
// Slow tests (run nightly only)
|
||||
test('@slow should process large file', async ({ page }) => { });
|
||||
|
||||
// Skip in CI
|
||||
test('@local-only should use local service', async ({ page }) => { });
|
||||
```
|
||||
|
||||
**In CI:**
|
||||
```bash
|
||||
# PR: Run critical and smoke only
|
||||
npx playwright test --grep "@critical|@smoke"
|
||||
|
||||
# Nightly: Run everything except local-only
|
||||
npx playwright test --grep-invert "@local-only"
|
||||
```
|
||||
|
||||
### Monitor CI Performance
|
||||
|
||||
Track metrics:
|
||||
|
||||
```markdown
|
||||
## CI Metrics
|
||||
|
||||
| Metric | Target | Current | Status |
|
||||
|--------|--------|---------|--------|
|
||||
| PR feedback time | < 5 min | 3m 24s | ✅ |
|
||||
| Full suite time | < 15 min | 12m 18s | ✅ |
|
||||
| Flakiness rate | < 1% | 0.3% | ✅ |
|
||||
| CI cost/month | < $100 | $75 | ✅ |
|
||||
```
|
||||
|
||||
### Handle Flaky Tests
|
||||
|
||||
When burn-in detects flakiness:
|
||||
|
||||
1. **Quarantine flaky test:**
|
||||
```typescript
|
||||
test.skip('flaky test - investigating', async ({ page }) => {
|
||||
// TODO: Fix flakiness
|
||||
});
|
||||
```
|
||||
|
||||
2. **Investigate with trace viewer:**
|
||||
```bash
|
||||
npx playwright show-trace test-results/trace.zip
|
||||
```
|
||||
|
||||
3. **Fix root cause:**
|
||||
- Add network-first patterns
|
||||
- Remove hard waits
|
||||
- Fix race conditions
|
||||
|
||||
4. **Verify fix:**
|
||||
```bash
|
||||
npm run test:burn-in -- tests/flaky.spec.ts --repeat 20
|
||||
```
|
||||
|
||||
### Secure Secrets
|
||||
|
||||
**Don't commit secrets to code:**
|
||||
```yaml
|
||||
# ❌ Bad
|
||||
- run: API_KEY=sk-1234... npm test
|
||||
|
||||
# ✅ Good
|
||||
- run: npm test
|
||||
env:
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
```
|
||||
|
||||
**Use environment-specific secrets:**
|
||||
- `STAGING_API_URL`
|
||||
- `PROD_API_URL`
|
||||
- `TEST_API_URL`
|
||||
|
||||
### Cache Aggressively
|
||||
|
||||
Speed up CI with caching:
|
||||
|
||||
```yaml
|
||||
# Cache node_modules
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'npm'
|
||||
|
||||
# Cache Playwright browsers
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ hashFiles('package-lock.json') }}
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Tests Pass Locally, Fail in CI
|
||||
|
||||
**Symptoms:**
|
||||
- Green locally, red in CI
|
||||
- "Works on my machine"
|
||||
|
||||
**Common Causes:**
|
||||
- Different Node version
|
||||
- Different browser version
|
||||
- Missing environment variables
|
||||
- Timezone differences
|
||||
- Race conditions (CI slower)
|
||||
|
||||
**Solutions:**
|
||||
```yaml
|
||||
# Pin Node version
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
# Pin browser versions
|
||||
- run: npx playwright install --with-deps chromium@1.40.0
|
||||
|
||||
# Set timezone
|
||||
env:
|
||||
TZ: 'America/New_York'
|
||||
```
|
||||
|
||||
### CI Takes Too Long
|
||||
|
||||
**Problem:** CI takes 30+ minutes, developers wait too long.
|
||||
|
||||
**Solutions:**
|
||||
1. **Shard tests:** 4 workers = 4x faster
|
||||
2. **Selective testing:** Only run affected tests on PR
|
||||
3. **Smoke tests first:** Run critical path (2 min), full suite after
|
||||
4. **Cache dependencies:** `npm ci` with cache
|
||||
5. **Optimize tests:** Remove slow tests, hard waits
|
||||
|
||||
### Burn-In Always Fails
|
||||
|
||||
**Problem:** Burn-in job fails every time.
|
||||
|
||||
**Cause:** Test suite is flaky.
|
||||
|
||||
**Solution:**
|
||||
1. Identify flaky tests (check which iteration fails)
|
||||
2. Fix flaky tests using `test-review`
|
||||
3. Re-run burn-in on specific files:
|
||||
```bash
|
||||
npm run test:burn-in tests/flaky.spec.ts
|
||||
```
|
||||
|
||||
### Out of CI Minutes
|
||||
|
||||
**Problem:** Using too many CI minutes, hitting plan limit.
|
||||
|
||||
**Solutions:**
|
||||
1. Run full suite only on main branch
|
||||
2. Use selective testing on PRs
|
||||
3. Run expensive tests nightly only
|
||||
4. Self-host runners (for GitHub Actions)
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to Set Up Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md) - Run first
|
||||
- [How to Run Test Review](/docs/tea/how-to/workflows/run-test-review.md) - Audit CI tests
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) - Burn-in utility
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Test Quality Standards](/docs/tea/explanation/test-quality-standards.md) - Why determinism matters
|
||||
- [Network-First Patterns](/docs/tea/explanation/network-first-patterns.md) - Avoid CI flakiness
|
||||
|
||||
## Reference
|
||||
|
||||
- [Command: *ci](/docs/tea/reference/commands.md#ci) - Full command reference
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - CI-related config options
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
title: "How to Set Up a Test Framework with TEA"
|
||||
description: How to set up a production-ready test framework using TEA
|
||||
---
|
||||
|
||||
Use TEA's `framework` workflow to scaffold a production-ready test framework for your project.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- No existing test framework in your project
|
||||
- Current test setup isn't production-ready
|
||||
- Starting a new project that needs testing infrastructure
|
||||
- Phase 3 (Solutioning) after architecture is complete
|
||||
|
||||
:::note[Prerequisites]
|
||||
- BMad Method installed
|
||||
- Architecture completed (or at least tech stack decided)
|
||||
- TEA agent available
|
||||
:::
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load the TEA Agent
|
||||
|
||||
Start a fresh chat and load the TEA (Test Architect) agent.
|
||||
|
||||
### 2. Run the Framework Workflow
|
||||
|
||||
```
|
||||
framework
|
||||
```
|
||||
|
||||
### 3. Answer TEA's Questions
|
||||
|
||||
TEA will ask about:
|
||||
|
||||
- Your tech stack (React, Node, etc.)
|
||||
- Preferred test framework (Playwright, Cypress, Jest)
|
||||
- Testing scope (E2E, integration, unit)
|
||||
- CI/CD platform (GitHub Actions, etc.)
|
||||
|
||||
### 4. Review Generated Output
|
||||
|
||||
TEA generates:
|
||||
|
||||
- **Test scaffold** — Directory structure and config files
|
||||
- **Sample specs** — Example tests following best practices
|
||||
- **`.env.example`** — Environment variable template
|
||||
- **`.nvmrc`** — Node version specification
|
||||
- **README updates** — Testing documentation
|
||||
|
||||
## What You Get
|
||||
|
||||
```
|
||||
tests/
|
||||
├── e2e/
|
||||
│ ├── example.spec.ts
|
||||
│ └── fixtures/
|
||||
├── integration/
|
||||
├── unit/
|
||||
├── playwright.config.ts # or cypress.config.ts
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Optional: Playwright Utils Integration
|
||||
|
||||
TEA can integrate with `@seontechnologies/playwright-utils` for advanced fixtures:
|
||||
|
||||
```bash
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
```
|
||||
|
||||
Enable during BMad installation or set `tea_use_playwright_utils: true` in config.
|
||||
|
||||
**Utilities available:** api-request, network-recorder, auth-session, intercept-network-call, recurse, log, file-utils, burn-in, network-error-monitor
|
||||
|
||||
## Optional: MCP Enhancements
|
||||
|
||||
TEA can use Playwright MCP servers for enhanced capabilities:
|
||||
|
||||
- `playwright` — Browser automation
|
||||
- `playwright-test` — Test runner with failure analysis
|
||||
|
||||
Configure in your IDE's MCP settings.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Run only once per repository** — Framework setup is a one-time operation
|
||||
- **Run after architecture is complete** — Framework aligns with tech stack
|
||||
- **Follow up with CI setup** — Run `ci` to configure CI/CD pipeline
|
||||
|
||||
## Next Steps
|
||||
|
||||
After test framework setup:
|
||||
|
||||
1. **Test Design** — Create test plans for system or epics
|
||||
2. **CI Configuration** — Set up automated test runs
|
||||
3. **Story Implementation** — Tests are ready for development
|
||||
@@ -1,276 +0,0 @@
|
||||
---
|
||||
title: "TEA Command Reference"
|
||||
description: Quick reference for all 8 TEA workflows - inputs, outputs, and links to detailed guides
|
||||
---
|
||||
|
||||
# TEA Command Reference
|
||||
|
||||
Quick reference for all 8 TEA (Test Architect) workflows. For detailed step-by-step guides, see the how-to documentation.
|
||||
|
||||
## Quick Index
|
||||
|
||||
- [`framework`](#framework) - Scaffold test framework
|
||||
- [`ci`](#ci) - Setup CI/CD pipeline
|
||||
- [`test-design`](#test-design) - Risk-based test planning
|
||||
- [`atdd`](#atdd) - Acceptance TDD
|
||||
- [`automate`](#automate) - Test automation
|
||||
- [`test-review`](#test-review) - Quality audit
|
||||
- [`nfr-assess`](#nfr-assess) - NFR assessment
|
||||
- [`trace`](#trace) - Coverage traceability
|
||||
|
||||
---
|
||||
|
||||
## framework
|
||||
|
||||
**Purpose:** Scaffold production-ready test framework (Playwright or Cypress)
|
||||
|
||||
**Phase:** Phase 3 (Solutioning)
|
||||
|
||||
**Frequency:** Once per project
|
||||
|
||||
**Key Inputs:**
|
||||
- Tech stack, test framework choice, testing scope
|
||||
|
||||
**Key Outputs:**
|
||||
- `tests/` directory with `support/fixtures/` and `support/helpers/`
|
||||
- `playwright.config.ts` or `cypress.config.ts`
|
||||
- `.env.example`, `.nvmrc`
|
||||
- Sample tests with best practices
|
||||
|
||||
**How-To Guide:** [Setup Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md)
|
||||
|
||||
---
|
||||
|
||||
## ci
|
||||
|
||||
**Purpose:** Setup CI/CD pipeline with selective testing and burn-in
|
||||
|
||||
**Phase:** Phase 3 (Solutioning)
|
||||
|
||||
**Frequency:** Once per project
|
||||
|
||||
**Key Inputs:**
|
||||
- CI platform (GitHub Actions, GitLab CI, etc.)
|
||||
- Sharding strategy, burn-in preferences
|
||||
|
||||
**Key Outputs:**
|
||||
- Platform-specific CI workflow (`.github/workflows/test.yml`, etc.)
|
||||
- Parallel execution configuration
|
||||
- Burn-in loops for flakiness detection
|
||||
- Secrets checklist
|
||||
|
||||
**How-To Guide:** [Setup CI Pipeline](/docs/tea/how-to/workflows/setup-ci.md)
|
||||
|
||||
---
|
||||
|
||||
## test-design
|
||||
|
||||
**Purpose:** Risk-based test planning with coverage strategy
|
||||
|
||||
**Phase:** Phase 3 (system-level), Phase 4 (epic-level)
|
||||
|
||||
**Frequency:** Once (system), per epic (epic-level)
|
||||
|
||||
**Modes:**
|
||||
- **System-level:** Architecture testability review (TWO documents)
|
||||
- **Epic-level:** Per-epic risk assessment (ONE document)
|
||||
|
||||
**Key Inputs:**
|
||||
- System-level: Architecture, PRD, ADRs
|
||||
- Epic-level: Epic, stories, acceptance criteria
|
||||
|
||||
**Key Outputs:**
|
||||
|
||||
**System-Level (TWO Documents):**
|
||||
- `test-design-architecture.md` - For Architecture/Dev teams
|
||||
- Quick Guide (🚨 BLOCKERS / ⚠️ HIGH PRIORITY / 📋 INFO ONLY)
|
||||
- Risk assessment with scoring
|
||||
- Testability concerns and gaps
|
||||
- Mitigation plans
|
||||
- `test-design-qa.md` - For QA team
|
||||
- Test execution recipe
|
||||
- Coverage plan (P0/P1/P2/P3 with checkboxes)
|
||||
- Sprint 0 setup requirements
|
||||
- NFR readiness summary
|
||||
|
||||
**Epic-Level (ONE Document):**
|
||||
- `test-design-epic-N.md`
|
||||
- Risk assessment (probability × impact scores)
|
||||
- Test priorities (P0-P3)
|
||||
- Coverage strategy
|
||||
- Mitigation plans
|
||||
|
||||
**Why Two Documents for System-Level?**
|
||||
- Architecture teams scan blockers in <5 min
|
||||
- QA teams have actionable test recipes
|
||||
- No redundancy (cross-references instead)
|
||||
- Clear separation (what to deliver vs how to test)
|
||||
|
||||
**MCP Enhancement:** Exploratory mode (live browser UI discovery)
|
||||
|
||||
**How-To Guide:** [Run Test Design](/docs/tea/how-to/workflows/run-test-design.md)
|
||||
|
||||
---
|
||||
|
||||
## atdd
|
||||
|
||||
**Purpose:** Generate failing acceptance tests BEFORE implementation (TDD red phase)
|
||||
|
||||
**Phase:** Phase 4 (Implementation)
|
||||
|
||||
**Frequency:** Per story (optional)
|
||||
|
||||
**Key Inputs:**
|
||||
- Story with acceptance criteria, test design, test levels
|
||||
|
||||
**Key Outputs:**
|
||||
- Failing tests (`tests/api/`, `tests/e2e/`)
|
||||
- Implementation checklist
|
||||
- All tests fail initially (red phase)
|
||||
|
||||
**MCP Enhancement:** Recording mode (for skeleton UI only - rare)
|
||||
|
||||
**How-To Guide:** [Run ATDD](/docs/tea/how-to/workflows/run-atdd.md)
|
||||
|
||||
---
|
||||
|
||||
## automate
|
||||
|
||||
**Purpose:** Expand test coverage after implementation
|
||||
|
||||
**Phase:** Phase 4 (Implementation)
|
||||
|
||||
**Frequency:** Per story/feature
|
||||
|
||||
**Key Inputs:**
|
||||
- Feature description, test design, existing tests to avoid duplication
|
||||
|
||||
**Key Outputs:**
|
||||
- Comprehensive test suite (`tests/e2e/`, `tests/api/`)
|
||||
- Updated fixtures, README
|
||||
- Definition of Done summary
|
||||
|
||||
**MCP Enhancement:** Healing + Recording modes (fix tests, verify selectors)
|
||||
|
||||
**How-To Guide:** [Run Automate](/docs/tea/how-to/workflows/run-automate.md)
|
||||
|
||||
---
|
||||
|
||||
## test-review
|
||||
|
||||
**Purpose:** Audit test quality with 0-100 scoring
|
||||
|
||||
**Phase:** Phase 4 (optional per story), Release Gate
|
||||
|
||||
**Frequency:** Per epic or before release
|
||||
|
||||
**Key Inputs:**
|
||||
- Test scope (file, directory, or entire suite)
|
||||
|
||||
**Key Outputs:**
|
||||
- `test-review.md` with quality score (0-100)
|
||||
- Critical issues with fixes
|
||||
- Recommendations
|
||||
- Category scores (Determinism, Isolation, Assertions, Structure, Performance)
|
||||
|
||||
**Scoring Categories:**
|
||||
- Determinism: 35 points
|
||||
- Isolation: 25 points
|
||||
- Assertions: 20 points
|
||||
- Structure: 10 points
|
||||
- Performance: 10 points
|
||||
|
||||
**How-To Guide:** [Run Test Review](/docs/tea/how-to/workflows/run-test-review.md)
|
||||
|
||||
---
|
||||
|
||||
## nfr-assess
|
||||
|
||||
**Purpose:** Validate non-functional requirements with evidence
|
||||
|
||||
**Phase:** Phase 2 (enterprise), Release Gate
|
||||
|
||||
**Frequency:** Per release (enterprise projects)
|
||||
|
||||
**Key Inputs:**
|
||||
- NFR categories (Security, Performance, Reliability, Maintainability)
|
||||
- Thresholds, evidence location
|
||||
|
||||
**Key Outputs:**
|
||||
- `nfr-assessment.md`
|
||||
- Category assessments (PASS/CONCERNS/FAIL)
|
||||
- Mitigation plans
|
||||
- Gate decision inputs
|
||||
|
||||
**How-To Guide:** [Run NFR Assessment](/docs/tea/how-to/workflows/run-nfr-assess.md)
|
||||
|
||||
---
|
||||
|
||||
## trace
|
||||
|
||||
**Purpose:** Requirements traceability + quality gate decision
|
||||
|
||||
**Phase:** Phase 2/4 (traceability), Release Gate (decision)
|
||||
|
||||
**Frequency:** Baseline, per epic refresh, release gate
|
||||
|
||||
**Two-Phase Workflow:**
|
||||
|
||||
**Phase 1: Traceability**
|
||||
- Requirements → test mapping
|
||||
- Coverage classification (FULL/PARTIAL/NONE)
|
||||
- Gap prioritization
|
||||
- Output: `traceability-matrix.md`
|
||||
|
||||
**Phase 2: Gate Decision**
|
||||
- PASS/CONCERNS/FAIL/WAIVED decision
|
||||
- Evidence-based (coverage %, quality scores, NFRs)
|
||||
- Output: `gate-decision-{gate_type}-{story_id}.md`
|
||||
|
||||
**Gate Rules:**
|
||||
- P0 coverage: 100% required
|
||||
- P1 coverage: ≥90% for PASS, 80-89% for CONCERNS, <80% FAIL
|
||||
- Overall coverage: ≥80% required
|
||||
|
||||
**How-To Guide:** [Run Trace](/docs/tea/how-to/workflows/run-trace.md)
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Command | Phase | Frequency | Primary Output |
|
||||
|---------|-------|-----------|----------------|
|
||||
| `framework` | 3 | Once | Test infrastructure |
|
||||
| `ci` | 3 | Once | CI/CD pipeline |
|
||||
| `test-design` | 3, 4 | System + per epic | Test design doc |
|
||||
| `atdd` | 4 | Per story (optional) | Failing tests |
|
||||
| `automate` | 4 | Per story | Passing tests |
|
||||
| `test-review` | 4, Gate | Per epic/release | Quality report |
|
||||
| `nfr-assess` | 2, Gate | Per release | NFR assessment |
|
||||
| `trace` | 2, 4, Gate | Baseline + refresh + gate | Coverage matrix + decision |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
**How-To Guides (Detailed Instructions):**
|
||||
- [Setup Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md)
|
||||
- [Setup CI Pipeline](/docs/tea/how-to/workflows/setup-ci.md)
|
||||
- [Run Test Design](/docs/tea/how-to/workflows/run-test-design.md)
|
||||
- [Run ATDD](/docs/tea/how-to/workflows/run-atdd.md)
|
||||
- [Run Automate](/docs/tea/how-to/workflows/run-automate.md)
|
||||
- [Run Test Review](/docs/tea/how-to/workflows/run-test-review.md)
|
||||
- [Run NFR Assessment](/docs/tea/how-to/workflows/run-nfr-assess.md)
|
||||
- [Run Trace](/docs/tea/how-to/workflows/run-trace.md)
|
||||
|
||||
**Explanation:**
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Complete TEA lifecycle
|
||||
- [Engagement Models](/docs/tea/explanation/engagement-models.md) - When to use which workflows
|
||||
|
||||
**Reference:**
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Config options
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md) - Pattern fragments
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,678 +0,0 @@
|
||||
---
|
||||
title: "TEA Configuration Reference"
|
||||
description: Complete reference for TEA configuration options and file locations
|
||||
---
|
||||
|
||||
# TEA Configuration Reference
|
||||
|
||||
Complete reference for all TEA (Test Architect) configuration options.
|
||||
|
||||
## Configuration File Locations
|
||||
|
||||
### User Configuration (Installer-Generated)
|
||||
|
||||
**Location:** `_bmad/bmm/config.yaml`
|
||||
|
||||
**Purpose:** Project-specific configuration values for your repository
|
||||
|
||||
**Created By:** BMad installer
|
||||
|
||||
**Status:** Typically gitignored (user-specific values)
|
||||
|
||||
**Usage:** Edit this file to change TEA behavior in your project
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
project_name: my-awesome-app
|
||||
user_skill_level: intermediate
|
||||
output_folder: _bmad-output
|
||||
tea_use_playwright_utils: true
|
||||
tea_use_mcp_enhancements: false
|
||||
```
|
||||
|
||||
### Canonical Schema (Source of Truth)
|
||||
|
||||
**Location:** `src/bmm/module.yaml`
|
||||
|
||||
**Purpose:** Defines available configuration keys, defaults, and installer prompts
|
||||
|
||||
**Created By:** BMAD maintainers (part of BMAD repo)
|
||||
|
||||
**Status:** Versioned in BMAD repository
|
||||
|
||||
**Usage:** Reference only (do not edit unless contributing to BMAD)
|
||||
|
||||
**Note:** The installer reads `module.yaml` to prompt for config values, then writes user choices to `_bmad/bmm/config.yaml` in your project.
|
||||
|
||||
---
|
||||
|
||||
## TEA Configuration Options
|
||||
|
||||
### tea_use_playwright_utils
|
||||
|
||||
Enable Playwright Utils integration for production-ready fixtures and utilities.
|
||||
|
||||
**Schema Location:** `src/bmm/module.yaml:52-56`
|
||||
|
||||
**User Config:** `_bmad/bmm/config.yaml`
|
||||
|
||||
**Type:** `boolean`
|
||||
|
||||
**Default:** `false` (set via installer prompt during installation)
|
||||
|
||||
**Installer Prompt:**
|
||||
```
|
||||
Are you using playwright-utils (@seontechnologies/playwright-utils) in your project?
|
||||
You must install packages yourself, or use test architect's `framework` command.
|
||||
```
|
||||
|
||||
**Purpose:** Enables TEA to:
|
||||
- Include playwright-utils in `framework` scaffold
|
||||
- Generate tests using playwright-utils fixtures
|
||||
- Review tests against playwright-utils patterns
|
||||
- Configure CI with burn-in and selective testing utilities
|
||||
|
||||
**Affects Workflows:**
|
||||
- `framework` - Includes playwright-utils imports and fixture examples
|
||||
- `atdd` - Uses fixtures like `apiRequest`, `authSession` in generated tests
|
||||
- `automate` - Leverages utilities for test patterns
|
||||
- `test-review` - Reviews against playwright-utils best practices
|
||||
- `ci` - Includes burn-in utility and selective testing
|
||||
|
||||
**Example (Enable):**
|
||||
```yaml
|
||||
tea_use_playwright_utils: true
|
||||
```
|
||||
|
||||
**Example (Disable):**
|
||||
```yaml
|
||||
tea_use_playwright_utils: false
|
||||
```
|
||||
|
||||
**Prerequisites:**
|
||||
```bash
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
```
|
||||
|
||||
**Related:**
|
||||
- [Integrate Playwright Utils Guide](/docs/tea/how-to/customization/integrate-playwright-utils.md)
|
||||
- [Playwright Utils on npm](https://www.npmjs.com/package/@seontechnologies/playwright-utils)
|
||||
|
||||
---
|
||||
|
||||
### tea_use_mcp_enhancements
|
||||
|
||||
Enable Playwright MCP servers for live browser verification during test generation.
|
||||
|
||||
**Schema Location:** `src/bmm/module.yaml:47-50`
|
||||
|
||||
**User Config:** `_bmad/bmm/config.yaml`
|
||||
|
||||
**Type:** `boolean`
|
||||
|
||||
**Default:** `false`
|
||||
|
||||
**Installer Prompt:**
|
||||
```
|
||||
Test Architect Playwright MCP capabilities (healing, exploratory, verification) are optionally available.
|
||||
You will have to setup your MCPs yourself; refer to https://docs.bmad-method.org/explanation/features/tea-overview for configuration examples.
|
||||
Would you like to enable MCP enhancements in Test Architect?
|
||||
```
|
||||
|
||||
**Purpose:** Enables TEA to use Model Context Protocol servers for:
|
||||
- Live browser automation during test design
|
||||
- Selector verification with actual DOM
|
||||
- Interactive UI discovery
|
||||
- Visual debugging and healing
|
||||
|
||||
**Affects Workflows:**
|
||||
- `test-design` - Enables exploratory mode (browser-based UI discovery)
|
||||
- `atdd` - Enables recording mode (verify selectors with live browser)
|
||||
- `automate` - Enables healing mode (fix tests with visual debugging)
|
||||
|
||||
**MCP Servers Required:**
|
||||
|
||||
**Two Playwright MCP servers** (actively maintained, continuously updated):
|
||||
|
||||
- `playwright` - Browser automation (`npx @playwright/mcp@latest`)
|
||||
- `playwright-test` - Test runner with failure analysis (`npx playwright run-test-mcp-server`)
|
||||
|
||||
**Configuration example**:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
},
|
||||
"playwright-test": {
|
||||
"command": "npx",
|
||||
"args": ["playwright", "run-test-mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration:** Refer to your AI agent's documentation for MCP server setup instructions.
|
||||
|
||||
**Example (Enable):**
|
||||
```yaml
|
||||
tea_use_mcp_enhancements: true
|
||||
```
|
||||
|
||||
**Example (Disable):**
|
||||
```yaml
|
||||
tea_use_mcp_enhancements: false
|
||||
```
|
||||
|
||||
**Prerequisites:**
|
||||
1. MCP servers installed in IDE configuration
|
||||
2. `@playwright/mcp` package available globally or locally
|
||||
3. Browser binaries installed (`npx playwright install`)
|
||||
|
||||
**Related:**
|
||||
- [Enable MCP Enhancements Guide](/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md)
|
||||
- [TEA Overview - MCP Section](/docs/tea/explanation/tea-overview.md#playwright-mcp-enhancements)
|
||||
- [Playwright MCP on npm](https://www.npmjs.com/package/@playwright/mcp)
|
||||
|
||||
---
|
||||
|
||||
## Core BMM Configuration (Inherited by TEA)
|
||||
|
||||
TEA also uses core BMM configuration options from `_bmad/bmm/config.yaml`:
|
||||
|
||||
### output_folder
|
||||
|
||||
**Type:** `string`
|
||||
|
||||
**Default:** `_bmad-output`
|
||||
|
||||
**Purpose:** Where TEA writes output files (test designs, reports, traceability matrices)
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
output_folder: _bmad-output
|
||||
```
|
||||
|
||||
**TEA Output Files:**
|
||||
- `test-design-architecture.md` + `test-design-qa.md` (from `test-design` system-level - TWO documents)
|
||||
- `test-design-epic-N.md` (from `test-design` epic-level)
|
||||
- `test-review.md` (from `test-review`)
|
||||
- `traceability-matrix.md` (from `trace` Phase 1)
|
||||
- `gate-decision-{gate_type}-{story_id}.md` (from `trace` Phase 2)
|
||||
- `nfr-assessment.md` (from `nfr-assess`)
|
||||
- `automation-summary.md` (from `automate`)
|
||||
- `atdd-checklist-{story_id}.md` (from `atdd`)
|
||||
|
||||
---
|
||||
|
||||
### user_skill_level
|
||||
|
||||
**Type:** `enum`
|
||||
|
||||
**Options:** `beginner` | `intermediate` | `expert`
|
||||
|
||||
**Default:** `intermediate`
|
||||
|
||||
**Purpose:** Affects how TEA explains concepts in chat responses
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
user_skill_level: beginner
|
||||
```
|
||||
|
||||
**Impact on TEA:**
|
||||
- **Beginner:** More detailed explanations, links to concepts, verbose guidance
|
||||
- **Intermediate:** Balanced explanations, assumes basic knowledge
|
||||
- **Expert:** Concise, technical, minimal hand-holding
|
||||
|
||||
---
|
||||
|
||||
### project_name
|
||||
|
||||
**Type:** `string`
|
||||
|
||||
**Default:** Directory name
|
||||
|
||||
**Purpose:** Used in TEA-generated documentation and reports
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
project_name: my-awesome-app
|
||||
```
|
||||
|
||||
**Used in:**
|
||||
- Report headers
|
||||
- Documentation titles
|
||||
- CI configuration comments
|
||||
|
||||
---
|
||||
|
||||
### communication_language
|
||||
|
||||
**Type:** `string`
|
||||
|
||||
**Default:** `english`
|
||||
|
||||
**Purpose:** Language for TEA chat responses
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
communication_language: english
|
||||
```
|
||||
|
||||
**Supported:** Any language (TEA responds in specified language)
|
||||
|
||||
---
|
||||
|
||||
### document_output_language
|
||||
|
||||
**Type:** `string`
|
||||
|
||||
**Default:** `english`
|
||||
|
||||
**Purpose:** Language for TEA-generated documents (test designs, reports)
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
document_output_language: english
|
||||
```
|
||||
|
||||
**Note:** Can differ from `communication_language` - chat in Spanish, generate docs in English.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
TEA workflows may use environment variables for test configuration.
|
||||
|
||||
### Test Framework Variables
|
||||
|
||||
**Playwright:**
|
||||
```bash
|
||||
# .env
|
||||
BASE_URL=https://todomvc.com/examples/react/dist/
|
||||
API_BASE_URL=https://api.example.com
|
||||
TEST_USER_EMAIL=test@example.com
|
||||
TEST_USER_PASSWORD=password123
|
||||
```
|
||||
|
||||
**Cypress:**
|
||||
```bash
|
||||
# cypress.env.json or .env
|
||||
CYPRESS_BASE_URL=https://example.com
|
||||
CYPRESS_API_URL=https://api.example.com
|
||||
```
|
||||
|
||||
### CI/CD Variables
|
||||
|
||||
Set in CI platform (GitHub Actions secrets, GitLab CI variables):
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
env:
|
||||
BASE_URL: ${{ secrets.STAGING_URL }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
TEST_USER_EMAIL: ${{ secrets.TEST_USER }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Patterns
|
||||
|
||||
### Development vs Production
|
||||
|
||||
**Separate configs for environments:**
|
||||
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
output_folder: _bmad-output
|
||||
|
||||
# .env.development
|
||||
BASE_URL=http://localhost:3000
|
||||
API_BASE_URL=http://localhost:4000
|
||||
|
||||
# .env.staging
|
||||
BASE_URL=https://staging.example.com
|
||||
API_BASE_URL=https://api-staging.example.com
|
||||
|
||||
# .env.production (read-only tests only!)
|
||||
BASE_URL=https://example.com
|
||||
API_BASE_URL=https://api.example.com
|
||||
```
|
||||
|
||||
### Team vs Individual
|
||||
|
||||
**Team config (committed):**
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml.example (committed to repo)
|
||||
project_name: team-project
|
||||
output_folder: _bmad-output
|
||||
tea_use_playwright_utils: true
|
||||
tea_use_mcp_enhancements: false
|
||||
```
|
||||
|
||||
**Individual config (typically gitignored):**
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml (user adds to .gitignore)
|
||||
user_name: John Doe
|
||||
user_skill_level: expert
|
||||
tea_use_mcp_enhancements: true # Individual preference
|
||||
```
|
||||
|
||||
### Monorepo Configuration
|
||||
|
||||
**Root config:**
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml (root)
|
||||
project_name: monorepo-parent
|
||||
output_folder: _bmad-output
|
||||
```
|
||||
|
||||
**Package-specific:**
|
||||
```yaml
|
||||
# packages/web-app/_bmad/bmm/config.yaml
|
||||
project_name: web-app
|
||||
output_folder: ../../_bmad-output/web-app
|
||||
tea_use_playwright_utils: true
|
||||
|
||||
# packages/mobile-app/_bmad/bmm/config.yaml
|
||||
project_name: mobile-app
|
||||
output_folder: ../../_bmad-output/mobile-app
|
||||
tea_use_playwright_utils: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
### 1. Use Version Control Wisely
|
||||
|
||||
**Commit:**
|
||||
```
|
||||
_bmad/bmm/config.yaml.example # Template for team
|
||||
.nvmrc # Node version
|
||||
package.json # Dependencies
|
||||
```
|
||||
|
||||
**Recommended for .gitignore:**
|
||||
```
|
||||
_bmad/bmm/config.yaml # User-specific values
|
||||
.env # Secrets
|
||||
.env.local # Local overrides
|
||||
```
|
||||
|
||||
### 2. Document Required Setup
|
||||
|
||||
**In your README:**
|
||||
```markdown
|
||||
## Setup
|
||||
|
||||
1. Install BMad
|
||||
|
||||
2. Copy config template:
|
||||
cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml
|
||||
|
||||
3. Edit config with your values:
|
||||
- Set user_name
|
||||
- Enable tea_use_playwright_utils if using playwright-utils
|
||||
- Enable tea_use_mcp_enhancements if MCPs configured
|
||||
```
|
||||
|
||||
### 3. Validate Configuration
|
||||
|
||||
**Check config is valid:**
|
||||
```bash
|
||||
# Check TEA config is set
|
||||
cat _bmad/bmm/config.yaml | grep tea_use
|
||||
|
||||
# Verify playwright-utils installed (if enabled)
|
||||
npm list @seontechnologies/playwright-utils
|
||||
|
||||
# Verify MCP servers configured (if enabled)
|
||||
# Check your IDE's MCP settings
|
||||
```
|
||||
|
||||
### 4. Keep Config Minimal
|
||||
|
||||
**Don't over-configure:**
|
||||
```yaml
|
||||
# ❌ Bad - overriding everything unnecessarily
|
||||
project_name: my-project
|
||||
user_name: John Doe
|
||||
user_skill_level: expert
|
||||
output_folder: custom/path
|
||||
planning_artifacts: custom/planning
|
||||
implementation_artifacts: custom/implementation
|
||||
project_knowledge: custom/docs
|
||||
tea_use_playwright_utils: true
|
||||
tea_use_mcp_enhancements: true
|
||||
communication_language: english
|
||||
document_output_language: english
|
||||
# Overriding 11 config options when most can use defaults
|
||||
|
||||
# ✅ Good - only essential overrides
|
||||
tea_use_playwright_utils: true
|
||||
output_folder: docs/testing
|
||||
# Only override what differs from defaults
|
||||
```
|
||||
|
||||
**Use defaults when possible** - only override what you actually need to change.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Configuration Not Loaded
|
||||
|
||||
**Problem:** TEA doesn't use my config values.
|
||||
|
||||
**Causes:**
|
||||
1. Config file in wrong location
|
||||
2. YAML syntax error
|
||||
3. Typo in config key
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check file exists
|
||||
ls -la _bmad/bmm/config.yaml
|
||||
|
||||
# Validate YAML syntax
|
||||
npm install -g js-yaml
|
||||
js-yaml _bmad/bmm/config.yaml
|
||||
|
||||
# Check for typos (compare to module.yaml)
|
||||
diff _bmad/bmm/config.yaml src/bmm/module.yaml
|
||||
```
|
||||
|
||||
### Playwright Utils Not Working
|
||||
|
||||
**Problem:** `tea_use_playwright_utils: true` but TEA doesn't use utilities.
|
||||
|
||||
**Causes:**
|
||||
1. Package not installed
|
||||
2. Config file not saved
|
||||
3. Workflow run before config update
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify package installed
|
||||
npm list @seontechnologies/playwright-utils
|
||||
|
||||
# Check config value
|
||||
grep tea_use_playwright_utils _bmad/bmm/config.yaml
|
||||
|
||||
# Re-run workflow in fresh chat
|
||||
# (TEA loads config at workflow start)
|
||||
```
|
||||
|
||||
### MCP Enhancements Not Working
|
||||
|
||||
**Problem:** `tea_use_mcp_enhancements: true` but no browser opens.
|
||||
|
||||
**Causes:**
|
||||
1. MCP servers not configured in IDE
|
||||
2. MCP package not installed
|
||||
3. Browser binaries missing
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check MCP package available
|
||||
npx @playwright/mcp@latest --version
|
||||
|
||||
# Install browsers
|
||||
npx playwright install
|
||||
|
||||
# Verify IDE MCP config
|
||||
# Check ~/.cursor/config.json or VS Code settings
|
||||
```
|
||||
|
||||
### Config Changes Not Applied
|
||||
|
||||
**Problem:** Updated config but TEA still uses old values.
|
||||
|
||||
**Cause:** TEA loads config at workflow start.
|
||||
|
||||
**Solution:**
|
||||
1. Save `_bmad/bmm/config.yaml`
|
||||
2. Start fresh chat
|
||||
3. Run TEA workflow
|
||||
4. Config will be reloaded
|
||||
|
||||
**TEA doesn't reload config mid-chat** - always start fresh chat after config changes.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Recommended Setup (Full Stack)
|
||||
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
project_name: my-project
|
||||
user_skill_level: beginner # or intermediate/expert
|
||||
output_folder: _bmad-output
|
||||
tea_use_playwright_utils: true # Recommended
|
||||
tea_use_mcp_enhancements: true # Recommended
|
||||
```
|
||||
|
||||
**Why recommended:**
|
||||
- Playwright Utils: Production-ready fixtures and utilities
|
||||
- MCP enhancements: Live browser verification, visual debugging
|
||||
- Together: The three-part stack (see [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md))
|
||||
|
||||
**Prerequisites:**
|
||||
```bash
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
# Configure MCP servers in IDE (see Enable MCP Enhancements guide)
|
||||
```
|
||||
|
||||
**Best for:** Everyone (beginners learn good patterns from day one)
|
||||
|
||||
---
|
||||
|
||||
### Minimal Setup (Learning Only)
|
||||
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
project_name: my-project
|
||||
output_folder: _bmad-output
|
||||
tea_use_playwright_utils: false
|
||||
tea_use_mcp_enhancements: false
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
- First-time TEA users (keep it simple initially)
|
||||
- Quick experiments
|
||||
- Learning basics before adding integrations
|
||||
|
||||
**Note:** Can enable integrations later as you learn
|
||||
|
||||
---
|
||||
|
||||
### Monorepo Setup
|
||||
|
||||
**Root config:**
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml (root)
|
||||
project_name: monorepo
|
||||
output_folder: _bmad-output
|
||||
tea_use_playwright_utils: true
|
||||
```
|
||||
|
||||
**Package configs:**
|
||||
```yaml
|
||||
# apps/web/_bmad/bmm/config.yaml
|
||||
project_name: web-app
|
||||
output_folder: ../../_bmad-output/web
|
||||
|
||||
# apps/api/_bmad/bmm/config.yaml
|
||||
project_name: api-service
|
||||
output_folder: ../../_bmad-output/api
|
||||
tea_use_playwright_utils: false # Using vanilla Playwright only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Team Template
|
||||
|
||||
**Commit this template:**
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml.example
|
||||
# Copy to config.yaml and fill in your values
|
||||
|
||||
project_name: your-project-name
|
||||
user_name: Your Name
|
||||
user_skill_level: intermediate # beginner | intermediate | expert
|
||||
output_folder: _bmad-output
|
||||
planning_artifacts: _bmad-output/planning-artifacts
|
||||
implementation_artifacts: _bmad-output/implementation-artifacts
|
||||
project_knowledge: docs
|
||||
|
||||
# TEA Configuration (Recommended: Enable both for full stack)
|
||||
tea_use_playwright_utils: true # Recommended - production-ready utilities
|
||||
tea_use_mcp_enhancements: true # Recommended - live browser verification
|
||||
|
||||
# Languages
|
||||
communication_language: english
|
||||
document_output_language: english
|
||||
```
|
||||
|
||||
**Team instructions:**
|
||||
```markdown
|
||||
## Setup for New Team Members
|
||||
|
||||
1. Clone repo
|
||||
2. Copy config template:
|
||||
cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml
|
||||
3. Edit with your name and preferences
|
||||
4. Install dependencies:
|
||||
npm install
|
||||
5. (Optional) Enable playwright-utils:
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
Set tea_use_playwright_utils: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
### How-To Guides
|
||||
- [Set Up Test Framework](/docs/tea/how-to/workflows/setup-test-framework.md)
|
||||
- [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md)
|
||||
- [Enable MCP Enhancements](/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md)
|
||||
|
||||
### Reference
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md)
|
||||
- [Knowledge Base Index](/docs/tea/reference/knowledge-base.md)
|
||||
- [Glossary](/docs/tea/glossary/index.md)
|
||||
|
||||
### Explanation
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md)
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md)
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,340 +0,0 @@
|
||||
---
|
||||
title: "TEA Knowledge Base Index"
|
||||
description: Complete index of TEA's 33 knowledge fragments for context engineering
|
||||
---
|
||||
|
||||
# TEA Knowledge Base Index
|
||||
|
||||
TEA uses 33 specialized knowledge fragments for context engineering. These fragments are loaded dynamically based on workflow needs via the `tea-index.csv` manifest.
|
||||
|
||||
## What is Context Engineering?
|
||||
|
||||
**Context engineering** is the practice of loading domain-specific standards into AI context automatically rather than relying on prompts alone.
|
||||
|
||||
Instead of asking AI to "write good tests" every time, TEA:
|
||||
1. Reads `tea-index.csv` to identify relevant fragments for the workflow
|
||||
2. Loads only the fragments needed (keeps context focused)
|
||||
3. Operates with domain-specific standards, not generic knowledge
|
||||
4. Produces consistent, production-ready tests across projects
|
||||
|
||||
**Example:**
|
||||
```
|
||||
User runs: `test-design`
|
||||
|
||||
TEA reads tea-index.csv:
|
||||
- Loads: test-quality.md, test-priorities-matrix.md, risk-governance.md
|
||||
- Skips: network-recorder.md, burn-in.md (not needed for test design)
|
||||
|
||||
Result: Focused context, consistent quality standards
|
||||
```
|
||||
|
||||
## How Knowledge Loading Works
|
||||
|
||||
### 1. Workflow Trigger
|
||||
User runs a TEA workflow (e.g., `test-design`)
|
||||
|
||||
### 2. Manifest Lookup
|
||||
TEA reads `src/bmm/testarch/tea-index.csv`:
|
||||
```csv
|
||||
id,name,description,tags,fragment_file
|
||||
test-quality,Test Quality,Execution limits and isolation rules,quality;standards,knowledge/test-quality.md
|
||||
risk-governance,Risk Governance,Risk scoring and gate decisions,risk;governance,knowledge/risk-governance.md
|
||||
```
|
||||
|
||||
### 3. Dynamic Loading
|
||||
Only fragments needed for the workflow are loaded into context
|
||||
|
||||
### 4. Consistent Output
|
||||
AI operates with established patterns, producing consistent results
|
||||
|
||||
## Fragment Categories
|
||||
|
||||
### Architecture & Fixtures
|
||||
|
||||
Core patterns for test infrastructure and fixture composition.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [fixture-architecture](../../../src/bmm/testarch/knowledge/fixture-architecture.md) | Pure function → Fixture → mergeTests composition with auto-cleanup | Testability, composition, reusability |
|
||||
| [network-first](../../../src/bmm/testarch/knowledge/network-first.md) | Intercept-before-navigate workflow, HAR capture, deterministic waits | Flakiness prevention, network patterns |
|
||||
| [playwright-config](../../../src/bmm/testarch/knowledge/playwright-config.md) | Environment switching, timeout standards, artifact outputs | Configuration, environments, CI |
|
||||
| [fixtures-composition](../../../src/bmm/testarch/knowledge/fixtures-composition.md) | mergeTests composition patterns for combining utilities | Fixture merging, utility composition |
|
||||
|
||||
**Used in:** `framework`, `test-design`, `atdd`, `automate`, `test-review`
|
||||
|
||||
---
|
||||
|
||||
### Data & Setup
|
||||
|
||||
Patterns for test data generation, authentication, and setup.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [data-factories](../../../src/bmm/testarch/knowledge/data-factories.md) | Factory patterns with faker, overrides, API seeding, cleanup | Test data, factories, cleanup |
|
||||
| [email-auth](../../../src/bmm/testarch/knowledge/email-auth.md) | Magic link extraction, state preservation, negative flows | Authentication, email testing |
|
||||
| [auth-session](../../../src/bmm/testarch/knowledge/auth-session.md) | Token persistence, multi-user, API/browser authentication | Auth patterns, session management |
|
||||
|
||||
**Used in:** `framework`, `atdd`, `automate`, `test-review`
|
||||
|
||||
---
|
||||
|
||||
### Network & Reliability
|
||||
|
||||
Network interception, error handling, and reliability patterns.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [network-recorder](../../../src/bmm/testarch/knowledge/network-recorder.md) | HAR record/playback, CRUD detection for offline testing | Offline testing, network replay |
|
||||
| [intercept-network-call](../../../src/bmm/testarch/knowledge/intercept-network-call.md) | Network spy/stub, JSON parsing for UI tests | Mocking, interception, stubbing |
|
||||
| [error-handling](../../../src/bmm/testarch/knowledge/error-handling.md) | Scoped exception handling, retry validation, telemetry logging | Error patterns, resilience |
|
||||
| [network-error-monitor](../../../src/bmm/testarch/knowledge/network-error-monitor.md) | HTTP 4xx/5xx detection for UI tests | Error detection, monitoring |
|
||||
|
||||
**Used in:** `atdd`, `automate`, `test-review`
|
||||
|
||||
---
|
||||
|
||||
### Test Execution & CI
|
||||
|
||||
CI/CD patterns, burn-in testing, and selective test execution.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [ci-burn-in](../../../src/bmm/testarch/knowledge/ci-burn-in.md) | Staged jobs, shard orchestration, burn-in loops | CI/CD, flakiness detection |
|
||||
| [burn-in](../../../src/bmm/testarch/knowledge/burn-in.md) | Smart test selection, git diff for CI optimization | Test selection, performance |
|
||||
| [selective-testing](../../../src/bmm/testarch/knowledge/selective-testing.md) | Tag/grep usage, spec filters, diff-based runs | Test filtering, optimization |
|
||||
|
||||
**Used in:** `ci`, `test-review`
|
||||
|
||||
---
|
||||
|
||||
### Quality & Standards
|
||||
|
||||
Test quality standards, test level selection, and TDD patterns.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [test-quality](../../../src/bmm/testarch/knowledge/test-quality.md) | Execution limits, isolation rules, green criteria | DoD, best practices, anti-patterns |
|
||||
| [test-levels-framework](../../../src/bmm/testarch/knowledge/test-levels-framework.md) | Guidelines for unit, integration, E2E selection | Test pyramid, level selection |
|
||||
| [test-priorities-matrix](../../../src/bmm/testarch/knowledge/test-priorities-matrix.md) | P0-P3 criteria, coverage targets, execution ordering | Prioritization, risk-based testing |
|
||||
| [test-healing-patterns](../../../src/bmm/testarch/knowledge/test-healing-patterns.md) | Common failure patterns and automated fixes | Debugging, healing, fixes |
|
||||
| [component-tdd](../../../src/bmm/testarch/knowledge/component-tdd.md) | Red→green→refactor workflow, provider isolation | TDD, component testing |
|
||||
|
||||
**Used in:** `test-design`, `atdd`, `automate`, `test-review`, `trace`
|
||||
|
||||
---
|
||||
|
||||
### Risk & Gates
|
||||
|
||||
Risk assessment, governance, and gate decision frameworks.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [risk-governance](../../../src/bmm/testarch/knowledge/risk-governance.md) | Scoring matrix, category ownership, gate decision rules | Risk assessment, governance |
|
||||
| [probability-impact](../../../src/bmm/testarch/knowledge/probability-impact.md) | Probability × impact scale for scoring matrix | Risk scoring, impact analysis |
|
||||
| [nfr-criteria](../../../src/bmm/testarch/knowledge/nfr-criteria.md) | Security, performance, reliability, maintainability status | NFRs, compliance, enterprise |
|
||||
|
||||
**Used in:** `test-design`, `nfr-assess`, `trace`
|
||||
|
||||
---
|
||||
|
||||
### Selectors & Timing
|
||||
|
||||
Selector resilience, race condition debugging, and visual debugging.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [selector-resilience](../../../src/bmm/testarch/knowledge/selector-resilience.md) | Robust selector strategies and debugging | Selectors, locators, resilience |
|
||||
| [timing-debugging](../../../src/bmm/testarch/knowledge/timing-debugging.md) | Race condition identification and deterministic fixes | Race conditions, timing issues |
|
||||
| [visual-debugging](../../../src/bmm/testarch/knowledge/visual-debugging.md) | Trace viewer usage, artifact expectations | Debugging, trace viewer, artifacts |
|
||||
|
||||
**Used in:** `atdd`, `automate`, `test-review`
|
||||
|
||||
---
|
||||
|
||||
### Feature Flags & Testing Patterns
|
||||
|
||||
Feature flag testing, contract testing, and API testing patterns.
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [feature-flags](../../../src/bmm/testarch/knowledge/feature-flags.md) | Enum management, targeting helpers, cleanup, checklists | Feature flags, toggles |
|
||||
| [contract-testing](../../../src/bmm/testarch/knowledge/contract-testing.md) | Pact publishing, provider verification, resilience | Contract testing, Pact |
|
||||
| [api-testing-patterns](../../../src/bmm/testarch/knowledge/api-testing-patterns.md) | Pure API patterns without browser | API testing, backend testing |
|
||||
|
||||
**Used in:** `test-design`, `atdd`, `automate`
|
||||
|
||||
---
|
||||
|
||||
### Playwright-Utils Integration
|
||||
|
||||
Patterns for using `@seontechnologies/playwright-utils` package (9 utilities).
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| [api-request](../../../src/bmm/testarch/knowledge/api-request.md) | Typed HTTP client, schema validation, retry logic | API calls, HTTP, validation |
|
||||
| [auth-session](../../../src/bmm/testarch/knowledge/auth-session.md) | Token persistence, multi-user, API/browser authentication | Auth patterns, session management |
|
||||
| [network-recorder](../../../src/bmm/testarch/knowledge/network-recorder.md) | HAR record/playback, CRUD detection for offline testing | Offline testing, network replay |
|
||||
| [intercept-network-call](../../../src/bmm/testarch/knowledge/intercept-network-call.md) | Network spy/stub, JSON parsing for UI tests | Mocking, interception, stubbing |
|
||||
| [recurse](../../../src/bmm/testarch/knowledge/recurse.md) | Async polling for API responses, background jobs | Polling, eventual consistency |
|
||||
| [log](../../../src/bmm/testarch/knowledge/log.md) | Structured logging for API and UI tests | Logging, debugging, reporting |
|
||||
| [file-utils](../../../src/bmm/testarch/knowledge/file-utils.md) | CSV/XLSX/PDF/ZIP handling with download support | File validation, exports |
|
||||
| [burn-in](../../../src/bmm/testarch/knowledge/burn-in.md) | Smart test selection with git diff analysis | CI optimization, selective testing |
|
||||
| [network-error-monitor](../../../src/bmm/testarch/knowledge/network-error-monitor.md) | Auto-detect HTTP 4xx/5xx errors during tests | Error monitoring, silent failures |
|
||||
|
||||
**Note:** `fixtures-composition` is listed under Architecture & Fixtures (general Playwright `mergeTests` pattern, applies to all fixtures).
|
||||
|
||||
**Used in:** `framework` (if `tea_use_playwright_utils: true`), `atdd`, `automate`, `test-review`, `ci`
|
||||
|
||||
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/>
|
||||
|
||||
---
|
||||
|
||||
## Fragment Manifest (tea-index.csv)
|
||||
|
||||
**Location:** `src/bmm/testarch/tea-index.csv`
|
||||
|
||||
**Purpose:** Tracks all knowledge fragments and their usage in workflows
|
||||
|
||||
**Structure:**
|
||||
```csv
|
||||
id,name,description,tags,fragment_file
|
||||
test-quality,Test Quality,Execution limits and isolation rules,quality;standards,knowledge/test-quality.md
|
||||
risk-governance,Risk Governance,Risk scoring and gate decisions,risk;governance,knowledge/risk-governance.md
|
||||
```
|
||||
|
||||
**Columns:**
|
||||
- `id` - Unique fragment identifier (kebab-case)
|
||||
- `name` - Human-readable fragment name
|
||||
- `description` - What the fragment covers
|
||||
- `tags` - Searchable tags (semicolon-separated)
|
||||
- `fragment_file` - Relative path to fragment markdown file
|
||||
|
||||
**Fragment Location:** `src/bmm/testarch/knowledge/` (all 33 fragments in single directory)
|
||||
|
||||
**Manifest:** `src/bmm/testarch/tea-index.csv`
|
||||
|
||||
---
|
||||
|
||||
## Workflow Fragment Loading
|
||||
|
||||
Each TEA workflow loads specific fragments:
|
||||
|
||||
### `framework`
|
||||
**Key Fragments:**
|
||||
- fixture-architecture.md
|
||||
- playwright-config.md
|
||||
- fixtures-composition.md
|
||||
|
||||
**Purpose:** Test infrastructure patterns and fixture composition
|
||||
|
||||
**Note:** Loads additional fragments based on framework choice (Playwright/Cypress) and config (`tea_use_playwright_utils`).
|
||||
|
||||
---
|
||||
|
||||
### `test-design`
|
||||
**Key Fragments:**
|
||||
- test-quality.md
|
||||
- test-priorities-matrix.md
|
||||
- test-levels-framework.md
|
||||
- risk-governance.md
|
||||
- probability-impact.md
|
||||
|
||||
**Purpose:** Risk assessment and test planning standards
|
||||
|
||||
**Note:** Loads additional fragments based on mode (system-level vs epic-level) and focus areas.
|
||||
|
||||
---
|
||||
|
||||
### `atdd`
|
||||
**Key Fragments:**
|
||||
- test-quality.md
|
||||
- component-tdd.md
|
||||
- fixture-architecture.md
|
||||
- network-first.md
|
||||
- data-factories.md
|
||||
- selector-resilience.md
|
||||
- timing-debugging.md
|
||||
- test-healing-patterns.md
|
||||
|
||||
**Purpose:** TDD patterns and test generation standards
|
||||
|
||||
**Note:** Loads auth, network, and utility fragments based on feature requirements.
|
||||
|
||||
---
|
||||
|
||||
### `automate`
|
||||
**Key Fragments:**
|
||||
- test-quality.md
|
||||
- test-levels-framework.md
|
||||
- test-priorities-matrix.md
|
||||
- fixture-architecture.md
|
||||
- network-first.md
|
||||
- selector-resilience.md
|
||||
- test-healing-patterns.md
|
||||
- timing-debugging.md
|
||||
|
||||
**Purpose:** Comprehensive test generation with quality standards
|
||||
|
||||
**Note:** Loads additional fragments for data factories, auth, network utilities based on test needs.
|
||||
|
||||
---
|
||||
|
||||
### `test-review`
|
||||
**Key Fragments:**
|
||||
- test-quality.md
|
||||
- test-healing-patterns.md
|
||||
- selector-resilience.md
|
||||
- timing-debugging.md
|
||||
- visual-debugging.md
|
||||
- network-first.md
|
||||
- test-levels-framework.md
|
||||
- fixture-architecture.md
|
||||
|
||||
**Purpose:** Comprehensive quality review against all standards
|
||||
|
||||
**Note:** Loads all applicable playwright-utils fragments when `tea_use_playwright_utils: true`.
|
||||
|
||||
---
|
||||
|
||||
### `ci`
|
||||
**Key Fragments:**
|
||||
- ci-burn-in.md
|
||||
- burn-in.md
|
||||
- selective-testing.md
|
||||
- playwright-config.md
|
||||
|
||||
**Purpose:** CI/CD best practices and optimization
|
||||
|
||||
---
|
||||
|
||||
### `nfr-assess`
|
||||
**Key Fragments:**
|
||||
- nfr-criteria.md
|
||||
- risk-governance.md
|
||||
- probability-impact.md
|
||||
|
||||
**Purpose:** NFR assessment frameworks and decision rules
|
||||
|
||||
---
|
||||
|
||||
### `trace`
|
||||
**Key Fragments:**
|
||||
- test-priorities-matrix.md
|
||||
- risk-governance.md
|
||||
- test-quality.md
|
||||
|
||||
**Purpose:** Traceability and gate decision standards
|
||||
|
||||
**Note:** Loads nfr-criteria.md if NFR assessment is part of gate decision.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - How knowledge base fits in TEA
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - Context engineering philosophy
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - Workflows that use fragments
|
||||
|
||||
---
|
||||
|
||||
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|
||||
@@ -1,444 +0,0 @@
|
||||
---
|
||||
title: "Getting Started with Test Architect"
|
||||
description: Learn Test Architect fundamentals by generating and running tests for an existing demo app in 30 minutes
|
||||
---
|
||||
|
||||
Welcome! **Test Architect (TEA) Lite** is the simplest way to get started with TEA - just use the `automate` workflow (e.g., `/automate` in Claude Code) to generate tests for existing features. Perfect for beginners who want to learn TEA fundamentals quickly.
|
||||
|
||||
## What You'll Build
|
||||
|
||||
By the end of this 30-minute tutorial, you'll have:
|
||||
- A working Playwright test framework
|
||||
- Your first risk-based test plan
|
||||
- Passing tests for an existing demo app feature
|
||||
|
||||
:::note[Prerequisites]
|
||||
- Node.js installed (v20 or later)
|
||||
- 30 minutes of focused time
|
||||
- We'll use TodoMVC (<https://todomvc.com/examples/react/dist/>) as our demo app
|
||||
:::
|
||||
|
||||
:::tip[Quick Path]
|
||||
Load TEA (`tea`) → scaffold framework (`framework`) → create test plan (`test-design`) → generate tests (`automate`) → run with `npx playwright test`.
|
||||
:::
|
||||
|
||||
## TEA Approaches Explained
|
||||
|
||||
Before we start, understand the three ways to use TEA:
|
||||
|
||||
- **TEA Lite** (this tutorial): Beginner using just the `automate` workflow to test existing features
|
||||
- **TEA Solo**: Using TEA standalone without full BMad Method integration
|
||||
- **TEA Integrated**: Full BMad Method with all TEA workflows across phases
|
||||
|
||||
This tutorial focuses on **TEA Lite** - the fastest way to see TEA in action.
|
||||
|
||||
## Step 0: Setup (2 minutes)
|
||||
|
||||
We'll test TodoMVC, a standard demo app used across testing documentation.
|
||||
|
||||
**Demo App:** <https://todomvc.com/examples/react/dist/>
|
||||
|
||||
No installation needed - TodoMVC runs in your browser. Open the link above and:
|
||||
1. Add a few todos (type and press Enter)
|
||||
2. Mark some as complete (click checkbox)
|
||||
3. Try the "All", "Active", "Completed" filters
|
||||
|
||||
You've just explored the features we'll test!
|
||||
|
||||
## Step 1: Install BMad and Scaffold Framework (10 minutes)
|
||||
|
||||
### Install BMad Method
|
||||
|
||||
Install BMad (see installation guide for latest command).
|
||||
|
||||
When prompted:
|
||||
- **Select modules:** Choose "BMM: BMad Method" (press Space, then Enter)
|
||||
- **Project name:** Keep default or enter your project name
|
||||
- **Experience level:** Choose "beginner" for this tutorial
|
||||
- **Planning artifacts folder:** Keep default
|
||||
- **Implementation artifacts folder:** Keep default
|
||||
- **Project knowledge folder:** Keep default
|
||||
- **Enable TEA Playwright Model Context Protocol (MCP) enhancements?** Choose "No" for now (we'll explore this later)
|
||||
- **Using playwright-utils?** Choose "No" for now (we'll explore this later)
|
||||
|
||||
BMad is now installed! You'll see a `_bmad/` folder in your project.
|
||||
|
||||
### Load TEA Agent
|
||||
|
||||
Start a new chat with your AI assistant (Claude, etc.) and type:
|
||||
|
||||
```
|
||||
tea
|
||||
```
|
||||
|
||||
This loads the Test Architect agent. You'll see TEA's menu with available workflows.
|
||||
|
||||
### Scaffold Test Framework
|
||||
|
||||
In your chat, run:
|
||||
|
||||
```
|
||||
framework
|
||||
```
|
||||
|
||||
TEA will ask you questions:
|
||||
|
||||
**Q: What's your tech stack?**
|
||||
A: "We're testing a React web application (TodoMVC)"
|
||||
|
||||
**Q: Which test framework?**
|
||||
A: "Playwright"
|
||||
|
||||
**Q: Testing scope?**
|
||||
A: "End-to-end (E2E) testing for a web application"
|
||||
|
||||
**Q: Continuous integration/continuous deployment (CI/CD) platform?**
|
||||
A: "GitHub Actions" (or your preference)
|
||||
|
||||
TEA will generate:
|
||||
- `tests/` directory with Playwright config
|
||||
- `playwright.config.ts` with base configuration
|
||||
- Sample test structure
|
||||
- `.env.example` for environment variables
|
||||
- `.nvmrc` for Node version
|
||||
|
||||
**Verify the setup:**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
You now have a production-ready test framework!
|
||||
|
||||
## Step 2: Your First Test Design (5 minutes)
|
||||
|
||||
Test design is where TEA shines - risk-based planning before writing tests.
|
||||
|
||||
### Run Test Design
|
||||
|
||||
In your chat with TEA, run:
|
||||
|
||||
```
|
||||
test-design
|
||||
```
|
||||
|
||||
**Q: System-level or epic-level?**
|
||||
A: "Epic-level - I want to test TodoMVC's basic functionality"
|
||||
|
||||
**Q: What feature are you testing?**
|
||||
A: "TodoMVC's core operations - creating, completing, and deleting todos"
|
||||
|
||||
**Q: Any specific risks or concerns?**
|
||||
A: "We want to ensure the filter buttons (All, Active, Completed) work correctly"
|
||||
|
||||
TEA will analyze and create `test-design-epic-1.md` with:
|
||||
|
||||
1. **Risk Assessment**
|
||||
- Probability × Impact scoring
|
||||
- Risk categories (TECH, SEC, PERF, DATA, BUS, OPS)
|
||||
- High-risk areas identified
|
||||
|
||||
2. **Test Priorities**
|
||||
- P0: Critical path (creating and displaying todos)
|
||||
- P1: High value (completing todos, filters)
|
||||
- P2: Medium value (deleting todos)
|
||||
- P3: Low value (edge cases)
|
||||
|
||||
3. **Coverage Strategy**
|
||||
- E2E tests for user workflows
|
||||
- Which scenarios need testing
|
||||
- Suggested test structure
|
||||
|
||||
**Review the test design file** - notice how TEA provides a systematic approach to what needs testing and why.
|
||||
|
||||
## Step 3: Generate Tests for Existing Features (5 minutes)
|
||||
|
||||
Now the magic happens - TEA generates tests based on your test design.
|
||||
|
||||
### Run Automate
|
||||
|
||||
In your chat with TEA, run:
|
||||
|
||||
```
|
||||
automate
|
||||
```
|
||||
|
||||
**Q: What are you testing?**
|
||||
A: "TodoMVC React app at <https://todomvc.com/examples/react/dist/> - focus on the test design we just created"
|
||||
|
||||
**Q: Reference existing docs?**
|
||||
A: "Yes, use test-design-epic-1.md"
|
||||
|
||||
**Q: Any specific test scenarios?**
|
||||
A: "Cover the P0 and P1 scenarios from the test design"
|
||||
|
||||
TEA will generate:
|
||||
|
||||
**`tests/e2e/todomvc.spec.ts`** with tests like:
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('TodoMVC - Core Functionality', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('https://todomvc.com/examples/react/dist/');
|
||||
});
|
||||
|
||||
test('should create a new todo', async ({ page }) => {
|
||||
// TodoMVC uses a simple input without placeholder or test IDs
|
||||
const todoInput = page.locator('.new-todo');
|
||||
await todoInput.fill('Buy groceries');
|
||||
await todoInput.press('Enter');
|
||||
|
||||
// Verify todo appears in list
|
||||
await expect(page.locator('.todo-list li')).toContainText('Buy groceries');
|
||||
});
|
||||
|
||||
test('should mark todo as complete', async ({ page }) => {
|
||||
// Create a todo
|
||||
const todoInput = page.locator('.new-todo');
|
||||
await todoInput.fill('Complete tutorial');
|
||||
await todoInput.press('Enter');
|
||||
|
||||
// Mark as complete using the toggle checkbox
|
||||
await page.locator('.todo-list li .toggle').click();
|
||||
|
||||
// Verify completed state
|
||||
await expect(page.locator('.todo-list li')).toHaveClass(/completed/);
|
||||
});
|
||||
|
||||
test('should filter todos by status', async ({ page }) => {
|
||||
// Create multiple todos
|
||||
const todoInput = page.locator('.new-todo');
|
||||
await todoInput.fill('Buy groceries');
|
||||
await todoInput.press('Enter');
|
||||
await todoInput.fill('Write tests');
|
||||
await todoInput.press('Enter');
|
||||
|
||||
// Complete the first todo ("Buy groceries")
|
||||
await page.locator('.todo-list li .toggle').first().click();
|
||||
|
||||
// Test Active filter (shows only incomplete todos)
|
||||
await page.locator('.filters a[href="#/active"]').click();
|
||||
await expect(page.locator('.todo-list li')).toHaveCount(1);
|
||||
await expect(page.locator('.todo-list li')).toContainText('Write tests');
|
||||
|
||||
// Test Completed filter (shows only completed todos)
|
||||
await page.locator('.filters a[href="#/completed"]').click();
|
||||
await expect(page.locator('.todo-list li')).toHaveCount(1);
|
||||
await expect(page.locator('.todo-list li')).toContainText('Buy groceries');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
TEA also creates:
|
||||
- **`tests/README.md`** - How to run tests, project conventions
|
||||
- **Definition of Done summary** - What makes a test "good"
|
||||
|
||||
### With Playwright Utils (Optional Enhancement)
|
||||
|
||||
If you have `tea_use_playwright_utils: true` in your config, TEA generates tests using production-ready utilities:
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
test('should mark todo as complete', async ({ page, request }) => {
|
||||
// Manual API call
|
||||
const response = await request.post('/api/todos', {
|
||||
data: { title: 'Complete tutorial' }
|
||||
});
|
||||
const todo = await response.json();
|
||||
|
||||
await page.goto('/');
|
||||
await page.locator(`.todo-list li:has-text("${todo.title}") .toggle`).click();
|
||||
await expect(page.locator('.todo-list li')).toHaveClass(/completed/);
|
||||
});
|
||||
```
|
||||
|
||||
**With Playwright Utils:**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('should mark todo as complete', async ({ page, apiRequest }) => {
|
||||
// Typed API call with cleaner syntax
|
||||
const { status, body: todo } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/todos',
|
||||
body: { title: 'Complete tutorial' }
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
await page.goto('/');
|
||||
await page.locator(`.todo-list li:has-text("${todo.title}") .toggle`).click();
|
||||
await expect(page.locator('.todo-list li')).toHaveClass(/completed/);
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Type-safe API responses (`{ status, body }`)
|
||||
- Automatic retry for 5xx errors
|
||||
- Built-in schema validation
|
||||
- Cleaner, more maintainable code
|
||||
|
||||
See [Integrate Playwright Utils](/docs/tea/how-to/customization/integrate-playwright-utils.md) to enable this.
|
||||
|
||||
## Step 4: Run and Validate (5 minutes)
|
||||
|
||||
Time to see your tests in action!
|
||||
|
||||
### Run the Tests
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
Running 3 tests using 1 worker
|
||||
|
||||
✓ tests/e2e/todomvc.spec.ts:7:3 › should create a new todo (2s)
|
||||
✓ tests/e2e/todomvc.spec.ts:15:3 › should mark todo as complete (2s)
|
||||
✓ tests/e2e/todomvc.spec.ts:30:3 › should filter todos by status (3s)
|
||||
|
||||
3 passed (7s)
|
||||
```
|
||||
|
||||
All green! Your tests are passing against the existing TodoMVC app.
|
||||
|
||||
### View Test Report
|
||||
|
||||
```bash
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
Opens a beautiful HTML report showing:
|
||||
- Test execution timeline
|
||||
- Screenshots (if any failures)
|
||||
- Trace viewer for debugging
|
||||
|
||||
### What Just Happened?
|
||||
|
||||
You used **TEA Lite** to:
|
||||
1. Scaffold a production-ready test framework (`framework`)
|
||||
2. Create a risk-based test plan (`test-design`)
|
||||
3. Generate comprehensive tests (`automate`)
|
||||
4. Run tests against an existing application
|
||||
|
||||
All in 30 minutes!
|
||||
|
||||
## What You Learned
|
||||
|
||||
Congratulations! You've completed the TEA Lite tutorial. You learned:
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
| -------------- | ------------------------------------ |
|
||||
| `tea` | Load the TEA agent |
|
||||
| `framework` | Scaffold test infrastructure |
|
||||
| `test-design` | Risk-based test planning |
|
||||
| `automate` | Generate tests for existing features |
|
||||
|
||||
### TEA Principles
|
||||
- **Risk-based testing** - Depth scales with impact (P0 vs P3)
|
||||
- **Test design first** - Plan before generating
|
||||
- **Network-first patterns** - Tests wait for actual responses (no hard waits)
|
||||
- **Production-ready from day one** - Not toy examples
|
||||
|
||||
:::tip[Key Takeaway]
|
||||
TEA Lite (just `automate`) is perfect for beginners learning TEA fundamentals, testing existing applications, quick test coverage expansion, and teams wanting fast results.
|
||||
:::
|
||||
|
||||
## Understanding ATDD vs Automate
|
||||
|
||||
This tutorial used the `automate` workflow to generate tests for **existing features** (tests pass immediately).
|
||||
|
||||
**When to use `automate`:**
|
||||
- Feature already exists
|
||||
- Want to add test coverage
|
||||
- Tests should pass on first run
|
||||
|
||||
**When to use `atdd` (Acceptance Test-Driven Development):**
|
||||
- Feature doesn't exist yet (Test-Driven Development workflow)
|
||||
- Want failing tests BEFORE implementation
|
||||
- Following red → green → refactor cycle
|
||||
|
||||
See [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) for the test-drive development (TDD) approach.
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Level Up Your TEA Skills
|
||||
|
||||
**How-To Guides** (task-oriented):
|
||||
- [How to Run Test Design](/docs/tea/how-to/workflows/run-test-design.md) - Deep dive into risk assessment
|
||||
- [How to Run ATDD](/docs/tea/how-to/workflows/run-atdd.md) - Generate failing tests first (TDD)
|
||||
- [How to Set Up CI Pipeline](/docs/tea/how-to/workflows/setup-ci.md) - Automate test execution
|
||||
- [How to Review Test Quality](/docs/tea/how-to/workflows/run-test-review.md) - Audit test quality
|
||||
|
||||
**Explanation** (understanding-oriented):
|
||||
- [TEA Overview](/docs/tea/explanation/tea-overview.md) - Complete TEA capabilities
|
||||
- [Testing as Engineering](/docs/tea/explanation/testing-as-engineering.md) - **Why TEA exists** (problem + solution)
|
||||
- [Risk-Based Testing](/docs/tea/explanation/risk-based-testing.md) - How risk scoring works
|
||||
|
||||
**Reference** (quick lookup):
|
||||
- [TEA Command Reference](/docs/tea/reference/commands.md) - All 8 TEA workflows
|
||||
- [TEA Configuration](/docs/tea/reference/configuration.md) - Config options
|
||||
- [Glossary](/docs/tea/glossary/index.md) - TEA terminology
|
||||
|
||||
### Try TEA Solo
|
||||
|
||||
Ready for standalone usage without full BMad Method? Use TEA Solo:
|
||||
- Run any TEA workflow independently
|
||||
- Bring your own requirements
|
||||
- Use on non-BMad projects
|
||||
|
||||
See [TEA Overview](/docs/tea/explanation/tea-overview.md) for engagement models.
|
||||
|
||||
### Go Full TEA Integrated
|
||||
|
||||
Want the complete quality operating model? Try TEA Integrated with BMad Method:
|
||||
- Phase 2: Planning with non-functional requirements (NFR) assessment
|
||||
- Phase 3: Architecture testability review
|
||||
- Phase 4: Per-epic test design → `atdd` → `automate`
|
||||
- Release Gate: Coverage traceability and gate decisions
|
||||
|
||||
See [BMad Method Documentation](/) for the full workflow.
|
||||
|
||||
## Common Questions
|
||||
|
||||
- [Why can't my tests find elements?](#why-cant-my-tests-find-elements)
|
||||
- [How do I fix network timeouts?](#how-do-i-fix-network-timeouts)
|
||||
|
||||
### Why can't my tests find elements?
|
||||
|
||||
TodoMVC doesn't use test IDs or accessible roles consistently. The selectors in this tutorial use CSS classes that match TodoMVC's actual structure:
|
||||
|
||||
```typescript
|
||||
// TodoMVC uses these CSS classes:
|
||||
page.locator('.new-todo') // Input field
|
||||
page.locator('.todo-list li') // Todo items
|
||||
page.locator('.toggle') // Checkbox
|
||||
|
||||
// If testing your own app, prefer accessible selectors:
|
||||
page.getByRole('textbox')
|
||||
page.getByRole('listitem')
|
||||
page.getByRole('checkbox')
|
||||
```
|
||||
|
||||
In production code, use accessible selectors (`getByRole`, `getByLabel`, `getByText`) for better resilience. TodoMVC is used here for learning, not as a selector best practice example.
|
||||
|
||||
### How do I fix network timeouts?
|
||||
|
||||
Increase timeout in `playwright.config.ts`:
|
||||
|
||||
```typescript
|
||||
use: {
|
||||
timeout: 30000, // 30 seconds
|
||||
}
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Documentation:** <https://docs.bmad-method.org>
|
||||
- **GitHub Issues:** <https://github.com/bmad-code-org/bmad-method/issues>
|
||||
- **Discord:** Join the BMAD community
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-beta.0",
|
||||
"version": "6.0.0-Beta.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-beta.0",
|
||||
"version": "6.0.0-Beta.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.11.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "bmad-method",
|
||||
"version": "6.0.0-beta.0",
|
||||
"version": "6.0.0-Beta.2",
|
||||
"description": "Breakthrough Method of Agile AI-driven Development",
|
||||
"keywords": [
|
||||
"agile",
|
||||
|
||||
57
src/bmm/agents/quinn.agent.yaml
Normal file
57
src/bmm/agents/quinn.agent.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
agent:
|
||||
metadata:
|
||||
id: "_bmad/bmm/agents/quinn"
|
||||
name: Quinn
|
||||
title: QA Engineer
|
||||
icon: 🧪
|
||||
module: bmm
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: QA Engineer
|
||||
identity: |
|
||||
Pragmatic test automation engineer focused on rapid test coverage.
|
||||
Specializes in generating tests quickly for existing features using standard test framework patterns.
|
||||
Simpler, more direct approach than the advanced Test Architect module.
|
||||
communication_style: |
|
||||
Practical and straightforward. Gets tests written fast without overthinking.
|
||||
'Ship it and iterate' mentality. Focuses on coverage first, optimization later.
|
||||
principles:
|
||||
- Generate API and E2E tests for implemented code
|
||||
- Tests should pass on first run
|
||||
|
||||
critical_actions:
|
||||
- Never skip running the generated tests to verify they pass
|
||||
- Always use standard test framework APIs (no external utilities)
|
||||
- Keep tests simple and maintainable
|
||||
- Focus on realistic user scenarios
|
||||
|
||||
menu:
|
||||
- trigger: QA
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/qa/automate/workflow.yaml"
|
||||
description: "[QA] Automate - Generate tests for existing features (simplified)"
|
||||
|
||||
prompts:
|
||||
- id: welcome
|
||||
content: |
|
||||
👋 Hi, I'm Quinn - your QA Engineer.
|
||||
|
||||
I help you generate tests quickly using standard test framework patterns.
|
||||
|
||||
**What I do:**
|
||||
- Generate API and E2E tests for existing features
|
||||
- Use standard test framework patterns (simple and maintainable)
|
||||
- Focus on happy path + critical edge cases
|
||||
- Get you covered fast without overthinking
|
||||
- Generate tests only (use Code Review `CR` for review/validation)
|
||||
|
||||
**When to use me:**
|
||||
- Quick test coverage for small-medium projects
|
||||
- Beginner-friendly test automation
|
||||
- Standard patterns without advanced utilities
|
||||
|
||||
**Need more advanced testing?**
|
||||
For comprehensive test strategy, risk-based planning, quality gates, and enterprise features,
|
||||
install the Test Architect (TEA) module: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/
|
||||
|
||||
Ready to generate some tests? Just say `QA` or `bmad-bmm-automate`!
|
||||
@@ -1,63 +0,0 @@
|
||||
# Test Architect + Quality Advisor Agent Definition
|
||||
|
||||
agent:
|
||||
webskip: true
|
||||
metadata:
|
||||
id: "_bmad/bmm/agents/tea.md"
|
||||
name: Murat
|
||||
title: Master Test Architect
|
||||
icon: 🧪
|
||||
module: bmm
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Master Test Architect
|
||||
identity: Test architect specializing in API testing, backend services, UI automation, CI/CD pipelines, and scalable quality gates. Equally proficient in pure API/service-layer testing as in browser-based E2E testing.
|
||||
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/bmm/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/bmm/testarch/knowledge/ before giving recommendations"
|
||||
- "Cross-check recommendations with the current official Playwright, Cypress, Pact, and CI platform documentation"
|
||||
|
||||
menu:
|
||||
- trigger: TF or fuzzy match on test-framework
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/framework/workflow.yaml"
|
||||
description: "[TF] Test Framework: Initialize production-ready test framework architecture"
|
||||
|
||||
- trigger: AT or fuzzy match on atdd
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/atdd/workflow.yaml"
|
||||
description: "[AT] Automated Test: Generate API and/or E2E tests first, before starting implementation on a story"
|
||||
|
||||
- trigger: TA or fuzzy match on test-automate
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/automate/workflow.yaml"
|
||||
description: "[TA] Test Automation: Generate comprehensive test automation framework for your whole project"
|
||||
|
||||
- trigger: TD or fuzzy match on test-design
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/test-design/workflow.yaml"
|
||||
description: "[TD] Test Design: Create comprehensive test scenarios ahead of development."
|
||||
|
||||
- trigger: TR or fuzzy match on test-trace
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/trace/workflow.yaml"
|
||||
description: "[TR] Trace Requirements: Map requirements to tests (Phase 1) and make quality gate decision (Phase 2)"
|
||||
|
||||
- trigger: NR or fuzzy match on nfr-assess
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/nfr-assess/workflow.yaml"
|
||||
description: "[NR] Non-Functional Requirements: Validate against the project implementation"
|
||||
|
||||
- trigger: CI or fuzzy match on continuous-integration
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/ci/workflow.yaml"
|
||||
description: "[CI] Continuous Integration: Recommend and Scaffold CI/CD quality pipeline"
|
||||
|
||||
- trigger: RV or fuzzy match on test-review
|
||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/test-review/workflow.yaml"
|
||||
description: "[RV] Review Tests: Perform a quality check against written tests using comprehensive knowledge base and best practices"
|
||||
@@ -1,32 +1,32 @@
|
||||
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,
|
||||
bmm,anytime,Document Project,DP,10,_bmad/bmm/workflows/document-project/workflow.yaml,bmad_bmm_document-project,false,analyst,Create Mode,"Analyze an existing project to produce useful documentation",project-knowledge,*,
|
||||
bmm,anytime,Quick Spec,TS,20,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad_bmm_quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps utilities without extensive planning",planning_artifacts,"tech spec",
|
||||
bmm,anytime,Quick Dev,QD,30,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad_bmm_quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,,
|
||||
bmm,anytime,Correct Course,CC,40,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad_bmm_correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal",
|
||||
bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad_brainstorming,false,analyst,data=_bmad/bmm/data/project-context-template.md,"Expert Guided Facilitation through a single or multiple techniques",planning_artifacts,"brainstorming session",
|
||||
bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad_bmm_research,false,analyst,Create Mode research_type=market,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad_bmm_research,false,analyst,Create Mode research_type=domain,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad_bmm_research,false,analyst,Create Mode research_type=technical,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad_bmm_create-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief",
|
||||
bmm,1-analysis,Validate Brief,VB,40,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad_bmm_validate-brief,false,analyst,Validate Mode,"Validates product brief completeness",planning_artifacts,"brief validation report",
|
||||
bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad_bmm_prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd,
|
||||
bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad_bmm_prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report",
|
||||
bmm,2-planning,Create UX,CU,30,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad_bmm_create-ux-design,false,ux-designer,Create Mode,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project",planning_artifacts,"ux design",
|
||||
bmm,2-planning,Validate UX,VU,40,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad_bmm_create-ux-design,false,ux-designer,Validate Mode,"Validates UX design deliverables",planning_artifacts,"ux validation report",
|
||||
,anytime,Create Dataflow,CDF,50,_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml,bmad_bmm_create-excalidraw-dataflow,false,ux-designer,Create Mode,"Create data flow diagrams (DFD) in Excalidraw format - can be called standalone or during any workflow to add visual documentation",planning_artifacts,"dataflow diagram",
|
||||
,anytime,Create Diagram,CED,51,_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml,bmad_bmm_create-excalidraw-diagram,false,ux-designer,Create Mode,"Create system architecture diagrams ERDs UML diagrams or general technical diagrams in Excalidraw format - use anytime or call from architecture workflow to add visual documentation",planning_artifacts,"diagram",
|
||||
,anytime,Create Flowchart,CFC,52,_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml,bmad_bmm_create-excalidraw-flowchart,false,ux-designer,Create Mode,"Create a flowchart visualization in Excalidraw format for processes pipelines or logic flows - use anytime or during architecture to add process documentation",planning_artifacts,"flowchart",
|
||||
,anytime,Create Wireframe,CEW,53,_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml,bmad_bmm_create-excalidraw-wireframe,false,ux-designer,Create Mode,"Create website or app wireframes in Excalidraw format - use anytime standalone or call from UX workflow to add UI mockups",planning_artifacts,"wireframe",
|
||||
bmm,3-solutioning,Create Architecture,CA,10,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad_bmm_create-architecture,true,architect,Create Mode,"Guided Workflow to document technical decisions",planning_artifacts,architecture,
|
||||
bmm,3-solutioning,Validate Architecture,VA,20,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad_bmm_create-architecture,false,architect,Validate Mode,"Validates architecture completeness",planning_artifacts,"architecture validation report",
|
||||
bmm,3-solutioning,Create Epics and Stories,CE,30,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad_bmm_create-epics-and-stories,true,pm,Create Mode,"Create the Epics and Stories Listing",planning_artifacts,"epics and stories",
|
||||
bmm,3-solutioning,Validate Epics and Stories,VE,40,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad_bmm_create-epics-and-stories,false,pm,Validate Mode,"Validates epics and stories completeness",planning_artifacts,"epics validation report",
|
||||
bmm,3-solutioning,Test Design,TD,50,_bmad/bmm/workflows/testarch/test-design/workflow.yaml,bmad_bmm_testarch-test-design,false,tea,Create Mode,"Create comprehensive test scenarios ahead of development, recommended if string test compliance or assurance is needed. Very critical for distributed applications with separate front ends and backends outside of a monorepo.",planning_artifacts,"test design",
|
||||
bmm,3-solutioning,Check Implementation Readiness,IR,70,_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md,bmad_bmm_check-implementation-readiness,true,architect,Validate Mode,"Ensure PRD UX Architecture and Epics Stories are aligned",planning_artifacts,"readiness report",
|
||||
bmm,4-implementation,Sprint Planning,SP,10,_bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml,bmad_bmm_sprint-planning,true,sm,Create Mode,"Generate sprint plan for development tasks - this kicks off the implementation phase by producing a plan the implementation agents will follow in sequence for every story in the plan.",implementation_artifacts,"sprint status",
|
||||
bmm,4-implementation,Sprint Status,SS,20,_bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml,bmad_bmm_sprint-status,false,sm,Create Mode,"Anytime: Summarize sprint status and route to next workflow",,,
|
||||
bmm,4-implementation,Create Story,CS,30,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad_bmm_create-story,true,sm,Create Mode,"Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER",implementation_artifacts,story,
|
||||
bmm,4-implementation,Validate Story,VS,35,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad_bmm_create-story,false,sm,Validate Mode,"Validates story readiness and completeness before development work begins",implementation_artifacts,"story validation report",
|
||||
bmm,4-implementation,Dev Story,DS,40,_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml,bmad_bmm_dev-story,true,dev,Create Mode,"Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed",,,
|
||||
bmm,4-implementation,Code Review,CR,50,_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml,bmad_bmm_code-review,false,dev,Create Mode,"Story cycle: If issues back to DS if approved then next CS or ER if epic complete",,,
|
||||
bmm,4-implementation,Retrospective,ER,60,_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml,bmad_bmm_retrospective,false,sm,Create Mode,"Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC",implementation_artifacts,retrospective,
|
||||
bmm,anytime,Document Project,DP,10,_bmad/bmm/workflows/document-project/workflow.yaml,bmad-bmm-document-project,false,analyst,Create Mode,"Analyze an existing project to produce useful documentation",project-knowledge,*,
|
||||
bmm,anytime,Quick Spec,TS,20,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad-bmm-quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps utilities without extensive planning",planning_artifacts,"tech spec",
|
||||
bmm,anytime,Quick Dev,QD,30,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad-bmm-quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,,
|
||||
bmm,anytime,Correct Course,CC,40,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad-bmm-correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal",
|
||||
bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=_bmad/bmm/data/project-context-template.md,"Expert Guided Facilitation through a single or multiple techniques",planning_artifacts,"brainstorming session",
|
||||
bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=market,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=domain,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=technical,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project-knowledge","research documents"
|
||||
bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-create-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief",
|
||||
bmm,1-analysis,Validate Brief,VB,40,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-validate-brief,false,analyst,Validate Mode,"Validates product brief completeness",planning_artifacts,"brief validation report",
|
||||
bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd,
|
||||
bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report",
|
||||
bmm,2-planning,Create UX,CU,30,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Create Mode,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project",planning_artifacts,"ux design",
|
||||
bmm,2-planning,Validate UX,VU,40,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Validate Mode,"Validates UX design deliverables",planning_artifacts,"ux validation report",
|
||||
,anytime,Create Dataflow,CDF,50,_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml,bmad-bmm-create-excalidraw-dataflow,false,ux-designer,Create Mode,"Create data flow diagrams (DFD) in Excalidraw format - can be called standalone or during any workflow to add visual documentation",planning_artifacts,"dataflow diagram",
|
||||
,anytime,Create Diagram,CED,51,_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml,bmad-bmm-create-excalidraw-diagram,false,ux-designer,Create Mode,"Create system architecture diagrams ERDs UML diagrams or general technical diagrams in Excalidraw format - use anytime or call from architecture workflow to add visual documentation",planning_artifacts,"diagram",
|
||||
,anytime,Create Flowchart,CFC,52,_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml,bmad-bmm-create-excalidraw-flowchart,false,ux-designer,Create Mode,"Create a flowchart visualization in Excalidraw format for processes pipelines or logic flows - use anytime or during architecture to add process documentation",planning_artifacts,"flowchart",
|
||||
,anytime,Create Wireframe,CEW,53,_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml,bmad-bmm-create-excalidraw-wireframe,false,ux-designer,Create Mode,"Create website or app wireframes in Excalidraw format - use anytime standalone or call from UX workflow to add UI mockups",planning_artifacts,"wireframe",
|
||||
bmm,3-solutioning,Create Architecture,CA,10,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,true,architect,Create Mode,"Guided Workflow to document technical decisions",planning_artifacts,architecture,
|
||||
bmm,3-solutioning,Validate Architecture,VA,20,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,false,architect,Validate Mode,"Validates architecture completeness",planning_artifacts,"architecture validation report",
|
||||
bmm,3-solutioning,Create Epics and Stories,CE,30,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,true,pm,Create Mode,"Create the Epics and Stories Listing",planning_artifacts,"epics and stories",
|
||||
bmm,3-solutioning,Validate Epics and Stories,VE,40,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,false,pm,Validate Mode,"Validates epics and stories completeness",planning_artifacts,"epics validation report",
|
||||
bmm,3-solutioning,Check Implementation Readiness,IR,70,_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md,bmad-bmm-check-implementation-readiness,true,architect,Validate Mode,"Ensure PRD UX Architecture and Epics Stories are aligned",planning_artifacts,"readiness report",
|
||||
bmm,4-implementation,Sprint Planning,SP,10,_bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml,bmad-bmm-sprint-planning,true,sm,Create Mode,"Generate sprint plan for development tasks - this kicks off the implementation phase by producing a plan the implementation agents will follow in sequence for every story in the plan.",implementation_artifacts,"sprint status",
|
||||
bmm,4-implementation,Sprint Status,SS,20,_bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml,bmad-bmm-sprint-status,false,sm,Create Mode,"Anytime: Summarize sprint status and route to next workflow",,,
|
||||
bmm,4-implementation,Create Story,CS,30,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad-bmm-create-story,true,sm,Create Mode,"Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER",implementation_artifacts,story,
|
||||
bmm,4-implementation,Validate Story,VS,35,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad-bmm-create-story,false,sm,Validate Mode,"Validates story readiness and completeness before development work begins",implementation_artifacts,"story validation report",
|
||||
bmm,4-implementation,Dev Story,DS,40,_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml,bmad-bmm-dev-story,true,dev,Create Mode,"Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed",,,
|
||||
bmm,4-implementation,Code Review,CR,50,_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml,bmad-bmm-code-review,false,dev,Create Mode,"Story cycle: If issues back to DS if approved then next CS or ER if epic complete",,,
|
||||
bmm,4-implementation,Retrospective,ER,60,_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml,bmad-bmm-retrospective,false,sm,Create Mode,"Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC",implementation_artifacts,retrospective,
|
||||
bmm,4-implementation,Automate,QA,45,_bmad/bmm/workflows/qa/automate/workflow.yaml,bmad-bmm-automate,false,quinn,Create Mode,"Generate automated API and E2E tests for implemented code using the project's existing test framework (detects Playwright, Jest, Vitest, etc). Use after implementation to add test coverage. NOT for code review or story validation - use CR for that.",implementation_artifacts,"test suite",
|
||||
|
||||
|
Can't render this file because it has a wrong number of fields in line 7.
|
@@ -5,7 +5,6 @@ name,displayName,title,icon,role,identity,communicationStyle,principles,module,p
|
||||
"pm","John","Product Manager","📋","Investigative Product Strategist + Market-Savvy PM","Product management veteran with 8+ years launching B2B and consumer products. Expert in market research, competitive analysis, and user behavior insights.","Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters.","Uncover the deeper WHY behind every requirement. Ruthless prioritization to achieve MVP goals. Proactively identify risks. Align efforts with measurable business impact.","bmm","bmad/bmm/agents/pm.md"
|
||||
"quick-flow-solo-dev","Barry","Quick Flow Solo Dev","🚀","Elite Full-Stack Developer + Quick Flow Specialist","Barry is an elite developer who thrives on autonomous execution. He lives and breathes the BMAD Quick Flow workflow, taking projects from concept to deployment with ruthless efficiency. No handoffs, no delays - just pure, focused development. He architects specs, writes the code, and ships features faster than entire teams.","Direct, confident, and implementation-focused. Uses tech slang and gets straight to the point. No fluff, just results. Every response moves the project forward.","Planning and execution are two sides of the same coin. Quick Flow is my religion. Specs are for building, not bureaucracy. Code that ships is better than perfect code that doesn't. Documentation happens alongside development, not after. Ship early, ship often.","bmm","bmad/bmm/agents/quick-flow-solo-dev.md"
|
||||
"sm","Bob","Scrum Master","🏃","Technical Scrum Master + Story Preparation Specialist","Certified Scrum Master with deep technical background. Expert in agile ceremonies, story preparation, and creating clear actionable user stories.","Crisp and checklist-driven. Every word has a purpose, every requirement crystal clear. Zero tolerance for ambiguity.","Strict boundaries between story prep and implementation. Stories are single source of truth. Perfect alignment between PRD and dev execution. Enable efficient sprints.","bmm","bmad/bmm/agents/sm.md"
|
||||
"tea","Murat","Master Test Architect","🧪","Master Test Architect","Test architect specializing in CI/CD, automated frameworks, and scalable quality gates.","Blends data with gut instinct. 'Strong opinions, weakly held' is their mantra. Speaks in risk calculations and impact assessments.","Risk-based testing. Depth scales with impact. Quality gates backed by data. Tests mirror usage. Flakiness is critical debt. Tests first AI implements suite validates.","bmm","bmad/bmm/agents/tea.md"
|
||||
"tech-writer","Paige","Technical Writer","📚","Technical Documentation Specialist + Knowledge Curator","Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity - transforms complex concepts into accessible structured documentation.","Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines.","Documentation is teaching. Every doc helps someone accomplish a task. Clarity above all. Docs are living artifacts that evolve with code.","bmm","bmad/bmm/agents/tech-writer.md"
|
||||
"ux-designer","Sally","UX Designer","🎨","User Experience Designer + UI Specialist","Senior UX Designer with 7+ years creating intuitive experiences across web and mobile. Expert in user research, interaction design, AI-assisted tools.","Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair.","Every decision serves genuine user needs. Start simple evolve through feedback. Balance empathy with edge case attention. AI tools accelerate human-centered design.","bmm","bmad/bmm/agents/ux-designer.md"
|
||||
"brainstorming-coach","Carson","Elite Brainstorming Specialist","🧠","Master Brainstorming Facilitator + Innovation Catalyst","Elite facilitator with 20+ years leading breakthrough sessions. Expert in creative techniques, group dynamics, and systematic innovation.","Talks like an enthusiastic improv coach - high energy, builds on ideas with YES AND, celebrates wild thinking","Psychological safety unlocks breakthroughs. Wild ideas today become innovations tomorrow. Humor and play are serious innovation tools.","cis","bmad/cis/agents/brainstorming-coach.md"
|
||||
|
||||
|
@@ -1,350 +0,0 @@
|
||||
# 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 | Sprint 0 blocker |
|
||||
| ⬜ Sample Requests: Examples | ⬜ | Pending schemas | Finalize ADR Tools |
|
||||
|
||||
**Overall Status:** ⚠️ CONCERNS (2/4 criteria met)
|
||||
|
||||
**Next Actions:**
|
||||
- [ ] Backend: Implement Athena mock endpoints (Sprint 0)
|
||||
- [ ] Backend: Implement `/api/test-data` (Sprint 0)
|
||||
- [ ] PM: Finalize sample requests (Sprint 1)
|
||||
|
||||
{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)
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
# 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
|
||||
|
||||
## 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
|
||||
|
||||
**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[]
|
||||
```
|
||||
@@ -1,843 +0,0 @@
|
||||
# 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
|
||||
|
||||
### 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
|
||||
|
||||
## 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 }) => { /* ... */ });
|
||||
});
|
||||
```
|
||||
@@ -1,552 +0,0 @@
|
||||
# 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',
|
||||
});
|
||||
```
|
||||
@@ -1,273 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,675 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## 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, SEON production pipelines_
|
||||
@@ -1,486 +0,0 @@
|
||||
# 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._
|
||||
@@ -1,957 +0,0 @@
|
||||
# Contract Testing Essentials (Pact)
|
||||
|
||||
## Principle
|
||||
|
||||
Contract testing validates API contracts between consumer and provider services without requiring integrated end-to-end tests. Store consumer contracts alongside integration specs, version contracts semantically, and publish on every CI run. Provider verification before merge surfaces breaking changes immediately, while explicit fallback behavior (timeouts, retries, error payloads) captures resilience guarantees in contracts.
|
||||
|
||||
## Rationale
|
||||
|
||||
Traditional integration testing requires running both consumer and provider simultaneously, creating slow, flaky tests with complex setup. Contract testing decouples services: consumers define expectations (pact files), providers verify against those expectations independently. This enables parallel development, catches breaking changes early, and documents API behavior as executable specifications. Pair contract tests with API smoke tests to validate data mapping and UI rendering in tandem.
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Example 1: Pact Consumer Test (Frontend → Backend API)
|
||||
|
||||
**Context**: React application consuming a user management API, defining expected interactions.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
// tests/contract/user-api.pact.spec.ts
|
||||
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
|
||||
import { getUserById, createUser, User } from '@/api/user-service';
|
||||
|
||||
const { like, eachLike, string, integer } = MatchersV3;
|
||||
|
||||
/**
|
||||
* Consumer-Driven Contract Test
|
||||
* - Consumer (React app) defines expected API behavior
|
||||
* - Generates pact file for provider to verify
|
||||
* - Runs in isolation (no real backend required)
|
||||
*/
|
||||
|
||||
const provider = new PactV3({
|
||||
consumer: 'user-management-web',
|
||||
provider: 'user-api-service',
|
||||
dir: './pacts', // Output directory for pact files
|
||||
logLevel: 'warn',
|
||||
});
|
||||
|
||||
describe('User API Contract', () => {
|
||||
describe('GET /users/:id', () => {
|
||||
it('should return user when user exists', async () => {
|
||||
// Arrange: Define expected interaction
|
||||
await provider
|
||||
.given('user with id 1 exists') // Provider state
|
||||
.uponReceiving('a request for user 1')
|
||||
.withRequest({
|
||||
method: 'GET',
|
||||
path: '/users/1',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: like('Bearer token123'), // Matcher: any string
|
||||
},
|
||||
})
|
||||
.willRespondWith({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: like({
|
||||
id: integer(1),
|
||||
name: string('John Doe'),
|
||||
email: string('john@example.com'),
|
||||
role: string('user'),
|
||||
createdAt: string('2025-01-15T10:00:00Z'),
|
||||
}),
|
||||
})
|
||||
.executeTest(async (mockServer) => {
|
||||
// Act: Call consumer code against mock server
|
||||
const user = await getUserById(1, {
|
||||
baseURL: mockServer.url,
|
||||
headers: { Authorization: 'Bearer token123' },
|
||||
});
|
||||
|
||||
// Assert: Validate consumer behavior
|
||||
expect(user).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
role: 'user',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle 404 when user does not exist', async () => {
|
||||
await provider
|
||||
.given('user with id 999 does not exist')
|
||||
.uponReceiving('a request for non-existent user')
|
||||
.withRequest({
|
||||
method: 'GET',
|
||||
path: '/users/999',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.willRespondWith({
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
error: 'User not found',
|
||||
code: 'USER_NOT_FOUND',
|
||||
},
|
||||
})
|
||||
.executeTest(async (mockServer) => {
|
||||
// Act & Assert: Consumer handles 404 gracefully
|
||||
await expect(getUserById(999, { baseURL: mockServer.url })).rejects.toThrow('User not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /users', () => {
|
||||
it('should create user and return 201', async () => {
|
||||
const newUser: Omit<User, 'id' | 'createdAt'> = {
|
||||
name: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
await provider
|
||||
.given('no users exist')
|
||||
.uponReceiving('a request to create a user')
|
||||
.withRequest({
|
||||
method: 'POST',
|
||||
path: '/users',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: like(newUser),
|
||||
})
|
||||
.willRespondWith({
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: like({
|
||||
id: integer(2),
|
||||
name: string('Jane Smith'),
|
||||
email: string('jane@example.com'),
|
||||
role: string('admin'),
|
||||
createdAt: string('2025-01-15T11:00:00Z'),
|
||||
}),
|
||||
})
|
||||
.executeTest(async (mockServer) => {
|
||||
const createdUser = await createUser(newUser, {
|
||||
baseURL: mockServer.url,
|
||||
});
|
||||
|
||||
expect(createdUser).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
name: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
role: 'admin',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**package.json scripts**:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test:contract": "jest tests/contract --testTimeout=30000",
|
||||
"pact:publish": "pact-broker publish ./pacts --consumer-app-version=$GIT_SHA --broker-base-url=$PACT_BROKER_URL --broker-token=$PACT_BROKER_TOKEN"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- **Consumer-driven**: Frontend defines expectations, not backend
|
||||
- **Matchers**: `like`, `string`, `integer` for flexible matching
|
||||
- **Provider states**: given() sets up test preconditions
|
||||
- **Isolation**: No real backend needed, runs fast
|
||||
- **Pact generation**: Automatically creates JSON pact files
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Pact Provider Verification (Backend validates contracts)
|
||||
|
||||
**Context**: Node.js/Express API verifying pacts published by consumers.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
// tests/contract/user-api.provider.spec.ts
|
||||
import { Verifier, VerifierOptions } from '@pact-foundation/pact';
|
||||
import { server } from '../../src/server'; // Your Express/Fastify app
|
||||
import { seedDatabase, resetDatabase } from '../support/db-helpers';
|
||||
|
||||
/**
|
||||
* Provider Verification Test
|
||||
* - Provider (backend API) verifies against published pacts
|
||||
* - State handlers setup test data for each interaction
|
||||
* - Runs before merge to catch breaking changes
|
||||
*/
|
||||
|
||||
describe('Pact Provider Verification', () => {
|
||||
let serverInstance;
|
||||
const PORT = 3001;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Start provider server
|
||||
serverInstance = server.listen(PORT);
|
||||
console.log(`Provider server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await serverInstance.close();
|
||||
});
|
||||
|
||||
it('should verify pacts from all consumers', async () => {
|
||||
const opts: VerifierOptions = {
|
||||
// Provider details
|
||||
provider: 'user-api-service',
|
||||
providerBaseUrl: `http://localhost:${PORT}`,
|
||||
|
||||
// Pact Broker configuration
|
||||
pactBrokerUrl: process.env.PACT_BROKER_URL,
|
||||
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
|
||||
publishVerificationResult: process.env.CI === 'true',
|
||||
providerVersion: process.env.GIT_SHA || 'dev',
|
||||
|
||||
// State handlers: Setup provider state for each interaction
|
||||
stateHandlers: {
|
||||
'user with id 1 exists': async () => {
|
||||
await seedDatabase({
|
||||
users: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
role: 'user',
|
||||
createdAt: '2025-01-15T10:00:00Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
return 'User seeded successfully';
|
||||
},
|
||||
|
||||
'user with id 999 does not exist': async () => {
|
||||
// Ensure user doesn't exist
|
||||
await resetDatabase();
|
||||
return 'Database reset';
|
||||
},
|
||||
|
||||
'no users exist': async () => {
|
||||
await resetDatabase();
|
||||
return 'Database empty';
|
||||
},
|
||||
},
|
||||
|
||||
// Request filters: Add auth headers to all requests
|
||||
requestFilter: (req, res, next) => {
|
||||
// Mock authentication for verification
|
||||
req.headers['x-user-id'] = 'test-user';
|
||||
req.headers['authorization'] = 'Bearer valid-test-token';
|
||||
next();
|
||||
},
|
||||
|
||||
// Timeout for verification
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
// Run verification
|
||||
await new Verifier(opts).verifyProvider();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**CI integration**:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/pact-provider.yml
|
||||
name: Pact Provider Verification
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
verify-contracts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Start database
|
||||
run: docker-compose up -d postgres
|
||||
|
||||
- name: Run migrations
|
||||
run: npm run db:migrate
|
||||
|
||||
- name: Verify pacts
|
||||
run: npm run test:contract:provider
|
||||
env:
|
||||
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
|
||||
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
CI: true
|
||||
|
||||
- name: Can I Deploy?
|
||||
run: |
|
||||
npx pact-broker can-i-deploy \
|
||||
--pacticipant user-api-service \
|
||||
--version ${{ github.sha }} \
|
||||
--to-environment production
|
||||
env:
|
||||
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_URL }}
|
||||
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- **State handlers**: Setup provider data for each given() state
|
||||
- **Request filters**: Add auth/headers for verification requests
|
||||
- **CI publishing**: Verification results sent to broker
|
||||
- **can-i-deploy**: Safety check before production deployment
|
||||
- **Database isolation**: Reset between state handlers
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Contract CI Integration (Consumer & Provider Workflow)
|
||||
|
||||
**Context**: Complete CI/CD workflow coordinating consumer pact publishing and provider verification.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/pact-consumer.yml (Consumer side)
|
||||
name: Pact Consumer Tests
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
consumer-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run consumer contract tests
|
||||
run: npm run test:contract
|
||||
|
||||
- name: Publish pacts to broker
|
||||
if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request'
|
||||
run: |
|
||||
npx pact-broker publish ./pacts \
|
||||
--consumer-app-version ${{ github.sha }} \
|
||||
--branch ${{ github.head_ref || github.ref_name }} \
|
||||
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
|
||||
--broker-token ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
|
||||
- name: Tag pact with environment (main branch only)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
npx pact-broker create-version-tag \
|
||||
--pacticipant user-management-web \
|
||||
--version ${{ github.sha }} \
|
||||
--tag production \
|
||||
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
|
||||
--broker-token ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .github/workflows/pact-provider.yml (Provider side)
|
||||
name: Pact Provider Verification
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
repository_dispatch:
|
||||
types: [pact_changed] # Webhook from Pact Broker
|
||||
|
||||
jobs:
|
||||
verify-contracts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Start dependencies
|
||||
run: docker-compose up -d
|
||||
|
||||
- name: Run provider verification
|
||||
run: npm run test:contract:provider
|
||||
env:
|
||||
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
|
||||
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
CI: true
|
||||
|
||||
- name: Publish verification results
|
||||
if: always()
|
||||
run: echo "Verification results published to broker"
|
||||
|
||||
- name: Can I Deploy to Production?
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
npx pact-broker can-i-deploy \
|
||||
--pacticipant user-api-service \
|
||||
--version ${{ github.sha }} \
|
||||
--to-environment production \
|
||||
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
|
||||
--broker-token ${{ secrets.PACT_BROKER_TOKEN }} \
|
||||
--retry-while-unknown 6 \
|
||||
--retry-interval 10
|
||||
|
||||
- name: Record deployment (if can-i-deploy passed)
|
||||
if: success() && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
npx pact-broker record-deployment \
|
||||
--pacticipant user-api-service \
|
||||
--version ${{ github.sha }} \
|
||||
--environment production \
|
||||
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
|
||||
--broker-token ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
```
|
||||
|
||||
**Pact Broker Webhook Configuration**:
|
||||
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"name": "contract_content_changed"
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.github.com/repos/your-org/user-api/dispatches",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${user.githubToken}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
},
|
||||
"body": {
|
||||
"event_type": "pact_changed",
|
||||
"client_payload": {
|
||||
"pact_url": "${pactbroker.pactUrl}",
|
||||
"consumer": "${pactbroker.consumerName}",
|
||||
"provider": "${pactbroker.providerName}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- **Automatic trigger**: Consumer pact changes trigger provider verification via webhook
|
||||
- **Branch tracking**: Pacts published per branch for feature testing
|
||||
- **can-i-deploy**: Safety gate before production deployment
|
||||
- **Record deployment**: Track which version is in each environment
|
||||
- **Parallel dev**: Consumer and provider teams work independently
|
||||
|
||||
---
|
||||
|
||||
### Example 4: Resilience Coverage (Testing Fallback Behavior)
|
||||
|
||||
**Context**: Capture timeout, retry, and error handling behavior explicitly in contracts.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
// tests/contract/user-api-resilience.pact.spec.ts
|
||||
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
|
||||
import { getUserById, ApiError } from '@/api/user-service';
|
||||
|
||||
const { like, string } = MatchersV3;
|
||||
|
||||
const provider = new PactV3({
|
||||
consumer: 'user-management-web',
|
||||
provider: 'user-api-service',
|
||||
dir: './pacts',
|
||||
});
|
||||
|
||||
describe('User API Resilience Contract', () => {
|
||||
/**
|
||||
* Test 500 error handling
|
||||
* Verifies consumer handles server errors gracefully
|
||||
*/
|
||||
it('should handle 500 errors with retry logic', async () => {
|
||||
await provider
|
||||
.given('server is experiencing errors')
|
||||
.uponReceiving('a request that returns 500')
|
||||
.withRequest({
|
||||
method: 'GET',
|
||||
path: '/users/1',
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
.willRespondWith({
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
error: 'Internal server error',
|
||||
code: 'INTERNAL_ERROR',
|
||||
retryable: true,
|
||||
},
|
||||
})
|
||||
.executeTest(async (mockServer) => {
|
||||
// Consumer should retry on 500
|
||||
try {
|
||||
await getUserById(1, {
|
||||
baseURL: mockServer.url,
|
||||
retries: 3,
|
||||
retryDelay: 100,
|
||||
});
|
||||
fail('Should have thrown error after retries');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect((error as ApiError).code).toBe('INTERNAL_ERROR');
|
||||
expect((error as ApiError).retryable).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test 429 rate limiting
|
||||
* Verifies consumer respects rate limits
|
||||
*/
|
||||
it('should handle 429 rate limit with backoff', async () => {
|
||||
await provider
|
||||
.given('rate limit exceeded for user')
|
||||
.uponReceiving('a request that is rate limited')
|
||||
.withRequest({
|
||||
method: 'GET',
|
||||
path: '/users/1',
|
||||
})
|
||||
.willRespondWith({
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Retry-After': '60', // Retry after 60 seconds
|
||||
},
|
||||
body: {
|
||||
error: 'Too many requests',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
},
|
||||
})
|
||||
.executeTest(async (mockServer) => {
|
||||
try {
|
||||
await getUserById(1, {
|
||||
baseURL: mockServer.url,
|
||||
respectRateLimit: true,
|
||||
});
|
||||
fail('Should have thrown rate limit error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect((error as ApiError).code).toBe('RATE_LIMIT_EXCEEDED');
|
||||
expect((error as ApiError).retryAfter).toBe(60);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test timeout handling
|
||||
* Verifies consumer has appropriate timeout configuration
|
||||
*/
|
||||
it('should timeout after 10 seconds', async () => {
|
||||
await provider
|
||||
.given('server is slow to respond')
|
||||
.uponReceiving('a request that times out')
|
||||
.withRequest({
|
||||
method: 'GET',
|
||||
path: '/users/1',
|
||||
})
|
||||
.willRespondWith({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: like({ id: 1, name: 'John' }),
|
||||
})
|
||||
.withDelay(15000) // Simulate 15 second delay
|
||||
.executeTest(async (mockServer) => {
|
||||
try {
|
||||
await getUserById(1, {
|
||||
baseURL: mockServer.url,
|
||||
timeout: 10000, // 10 second timeout
|
||||
});
|
||||
fail('Should have timed out');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect((error as ApiError).code).toBe('TIMEOUT');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test partial response (optional fields)
|
||||
* Verifies consumer handles missing optional data
|
||||
*/
|
||||
it('should handle response with missing optional fields', async () => {
|
||||
await provider
|
||||
.given('user exists with minimal data')
|
||||
.uponReceiving('a request for user with partial data')
|
||||
.withRequest({
|
||||
method: 'GET',
|
||||
path: '/users/1',
|
||||
})
|
||||
.willRespondWith({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
id: integer(1),
|
||||
name: string('John Doe'),
|
||||
email: string('john@example.com'),
|
||||
// role, createdAt, etc. omitted (optional fields)
|
||||
},
|
||||
})
|
||||
.executeTest(async (mockServer) => {
|
||||
const user = await getUserById(1, { baseURL: mockServer.url });
|
||||
|
||||
// Consumer handles missing optional fields gracefully
|
||||
expect(user.id).toBe(1);
|
||||
expect(user.name).toBe('John Doe');
|
||||
expect(user.role).toBeUndefined(); // Optional field
|
||||
expect(user.createdAt).toBeUndefined(); // Optional field
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**API client with retry logic**:
|
||||
|
||||
```typescript
|
||||
// src/api/user-service.ts
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public retryable: boolean = false,
|
||||
public retryAfter?: number,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User API client with retry and error handling
|
||||
*/
|
||||
export async function getUserById(
|
||||
id: number,
|
||||
config?: AxiosRequestConfig & { retries?: number; retryDelay?: number; respectRateLimit?: boolean },
|
||||
): Promise<User> {
|
||||
const { retries = 3, retryDelay = 1000, respectRateLimit = true, ...axiosConfig } = config || {};
|
||||
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await axios.get(`/users/${id}`, axiosConfig);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
|
||||
// Handle rate limiting
|
||||
if (error.response?.status === 429) {
|
||||
const retryAfter = parseInt(error.response.headers['retry-after'] || '60');
|
||||
throw new ApiError('Too many requests', 'RATE_LIMIT_EXCEEDED', false, retryAfter);
|
||||
}
|
||||
|
||||
// Retry on 500 errors
|
||||
if (error.response?.status === 500 && attempt < retries) {
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle 404
|
||||
if (error.response?.status === 404) {
|
||||
throw new ApiError('User not found', 'USER_NOT_FOUND', false);
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
throw new ApiError('Request timeout', 'TIMEOUT', true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApiError('Request failed after retries', 'INTERNAL_ERROR', true);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- **Resilience contracts**: Timeouts, retries, errors explicitly tested
|
||||
- **State handlers**: Provider sets up each test scenario
|
||||
- **Error handling**: Consumer validates graceful degradation
|
||||
- **Retry logic**: Exponential backoff tested
|
||||
- **Optional fields**: Consumer handles partial responses
|
||||
|
||||
---
|
||||
|
||||
### Example 4: Pact Broker Housekeeping & Lifecycle Management
|
||||
|
||||
**Context**: Automated broker maintenance to prevent contract sprawl and noise.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
// scripts/pact-broker-housekeeping.ts
|
||||
/**
|
||||
* Pact Broker Housekeeping Script
|
||||
* - Archive superseded contracts
|
||||
* - Expire unused pacts
|
||||
* - Tag releases for environment tracking
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const PACT_BROKER_URL = process.env.PACT_BROKER_URL!;
|
||||
const PACT_BROKER_TOKEN = process.env.PACT_BROKER_TOKEN!;
|
||||
const PACTICIPANT = 'user-api-service';
|
||||
|
||||
/**
|
||||
* Tag release with environment
|
||||
*/
|
||||
function tagRelease(version: string, environment: 'staging' | 'production') {
|
||||
console.log(`🏷️ Tagging ${PACTICIPANT} v${version} as ${environment}`);
|
||||
|
||||
execSync(
|
||||
`npx pact-broker create-version-tag \
|
||||
--pacticipant ${PACTICIPANT} \
|
||||
--version ${version} \
|
||||
--tag ${environment} \
|
||||
--broker-base-url ${PACT_BROKER_URL} \
|
||||
--broker-token ${PACT_BROKER_TOKEN}`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record deployment to environment
|
||||
*/
|
||||
function recordDeployment(version: string, environment: 'staging' | 'production') {
|
||||
console.log(`📝 Recording deployment of ${PACTICIPANT} v${version} to ${environment}`);
|
||||
|
||||
execSync(
|
||||
`npx pact-broker record-deployment \
|
||||
--pacticipant ${PACTICIPANT} \
|
||||
--version ${version} \
|
||||
--environment ${environment} \
|
||||
--broker-base-url ${PACT_BROKER_URL} \
|
||||
--broker-token ${PACT_BROKER_TOKEN}`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old pact versions (retention policy)
|
||||
* Keep: last 30 days, all production tags, latest from each branch
|
||||
*/
|
||||
function cleanupOldPacts() {
|
||||
console.log(`🧹 Cleaning up old pacts for ${PACTICIPANT}`);
|
||||
|
||||
execSync(
|
||||
`npx pact-broker clean \
|
||||
--pacticipant ${PACTICIPANT} \
|
||||
--broker-base-url ${PACT_BROKER_URL} \
|
||||
--broker-token ${PACT_BROKER_TOKEN} \
|
||||
--keep-latest-for-branch 1 \
|
||||
--keep-min-age 30`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check deployment compatibility
|
||||
*/
|
||||
function canIDeploy(version: string, toEnvironment: string): boolean {
|
||||
console.log(`🔍 Checking if ${PACTICIPANT} v${version} can deploy to ${toEnvironment}`);
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`npx pact-broker can-i-deploy \
|
||||
--pacticipant ${PACTICIPANT} \
|
||||
--version ${version} \
|
||||
--to-environment ${toEnvironment} \
|
||||
--broker-base-url ${PACT_BROKER_URL} \
|
||||
--broker-token ${PACT_BROKER_TOKEN} \
|
||||
--retry-while-unknown 6 \
|
||||
--retry-interval 10`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`❌ Cannot deploy to ${toEnvironment}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main housekeeping workflow
|
||||
*/
|
||||
async function main() {
|
||||
const command = process.argv[2];
|
||||
const version = process.argv[3];
|
||||
const environment = process.argv[4] as 'staging' | 'production';
|
||||
|
||||
switch (command) {
|
||||
case 'tag-release':
|
||||
tagRelease(version, environment);
|
||||
break;
|
||||
|
||||
case 'record-deployment':
|
||||
recordDeployment(version, environment);
|
||||
break;
|
||||
|
||||
case 'can-i-deploy':
|
||||
const canDeploy = canIDeploy(version, environment);
|
||||
process.exit(canDeploy ? 0 : 1);
|
||||
|
||||
case 'cleanup':
|
||||
cleanupOldPacts();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error('Unknown command. Use: tag-release | record-deployment | can-i-deploy | cleanup');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
**package.json scripts**:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"pact:tag": "ts-node scripts/pact-broker-housekeeping.ts tag-release",
|
||||
"pact:record": "ts-node scripts/pact-broker-housekeeping.ts record-deployment",
|
||||
"pact:can-deploy": "ts-node scripts/pact-broker-housekeeping.ts can-i-deploy",
|
||||
"pact:cleanup": "ts-node scripts/pact-broker-housekeeping.ts cleanup"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Deployment workflow integration**:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy-production.yml
|
||||
name: Deploy to Production
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
verify-contracts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check pact compatibility
|
||||
run: npm run pact:can-deploy ${{ github.ref_name }} production
|
||||
env:
|
||||
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
|
||||
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
|
||||
deploy:
|
||||
needs: verify-contracts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to production
|
||||
run: ./scripts/deploy.sh production
|
||||
|
||||
- name: Record deployment in Pact Broker
|
||||
run: npm run pact:record ${{ github.ref_name }} production
|
||||
env:
|
||||
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
|
||||
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
```
|
||||
|
||||
**Scheduled cleanup**:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/pact-housekeeping.yml
|
||||
name: Pact Broker Housekeeping
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 0' # Weekly on Sunday at 2 AM
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup old pacts
|
||||
run: npm run pact:cleanup
|
||||
env:
|
||||
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
|
||||
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- **Automated tagging**: Releases tagged with environment
|
||||
- **Deployment tracking**: Broker knows which version is where
|
||||
- **Safety gate**: can-i-deploy blocks incompatible deployments
|
||||
- **Retention policy**: Keep recent, production, and branch-latest pacts
|
||||
- **Webhook triggers**: Provider verification runs on consumer changes
|
||||
|
||||
---
|
||||
|
||||
## Contract Testing Checklist
|
||||
|
||||
Before implementing contract testing, verify:
|
||||
|
||||
- [ ] **Pact Broker setup**: Hosted (Pactflow) or self-hosted broker configured
|
||||
- [ ] **Consumer tests**: Generate pacts in CI, publish to broker on merge
|
||||
- [ ] **Provider verification**: Runs on PR, verifies all consumer pacts
|
||||
- [ ] **State handlers**: Provider implements all given() states
|
||||
- [ ] **can-i-deploy**: Blocks deployment if contracts incompatible
|
||||
- [ ] **Webhooks configured**: Consumer changes trigger provider verification
|
||||
- [ ] **Retention policy**: Old pacts archived (keep 30 days, all production tags)
|
||||
- [ ] **Resilience tested**: Timeouts, retries, error codes in contracts
|
||||
|
||||
## Integration Points
|
||||
|
||||
- Used in workflows: `*automate` (integration test generation), `*ci` (contract CI setup)
|
||||
- Related fragments: `test-levels-framework.md`, `ci-burn-in.md`
|
||||
- Tools: Pact.js, Pact Broker (Pactflow or self-hosted), Pact CLI
|
||||
|
||||
_Source: Pact consumer/provider sample repos, Murat contract testing blog, Pact official documentation_
|
||||
@@ -1,500 +0,0 @@
|
||||
# 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._
|
||||
@@ -1,721 +0,0 @@
|
||||
# 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_
|
||||
@@ -1,725 +0,0 @@
|
||||
# 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, SEON production error handling_
|
||||
@@ -1,750 +0,0 @@
|
||||
# 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, SEON feature flag governance_
|
||||
@@ -1,463 +0,0 @@
|
||||
# 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);
|
||||
});
|
||||
```
|
||||
@@ -1,401 +0,0 @@
|
||||
# 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), SEON production patterns, Playwright fixture docs._
|
||||
@@ -1,382 +0,0 @@
|
||||
# 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);
|
||||
```
|
||||
@@ -1,430 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,429 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,405 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,486 +0,0 @@
|
||||
# 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._
|
||||
@@ -1,527 +0,0 @@
|
||||
# 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.seondev.space',
|
||||
'admin-staging.seon.io': 'admin.seondev.space',
|
||||
'admin.seon.io': 'admin.seondev.space',
|
||||
},
|
||||
patterns: [
|
||||
{ match: /admin-\d+\.seondev\.space/, replace: 'admin.seondev.space' },
|
||||
{ match: /admin-staging-pr-\w+-\d\.seon\.io/, replace: 'admin.seondev.space' }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**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
|
||||
});
|
||||
```
|
||||
@@ -1,670 +0,0 @@
|
||||
# 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('<script>');
|
||||
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_
|
||||
@@ -1,286 +0,0 @@
|
||||
# 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 utilities**: Used at SEON Technologies in production
|
||||
- **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 and retry | **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.
|
||||
@@ -1,730 +0,0 @@
|
||||
# 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, SEON configuration example, Murat testing philosophy (lines 216-271)._
|
||||
@@ -1,601 +0,0 @@
|
||||
# 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_
|
||||
@@ -1,421 +0,0 @@
|
||||
# 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)
|
||||
);
|
||||
```
|
||||
@@ -1,615 +0,0 @@
|
||||
# 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, SEON production gate workflows, ISO 31000 risk management standards_
|
||||
@@ -1,732 +0,0 @@
|
||||
# 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, SEON CI optimization_
|
||||
```
|
||||
@@ -1,527 +0,0 @@
|
||||
# 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_
|
||||
@@ -1,644 +0,0 @@
|
||||
# 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_
|
||||
@@ -1,473 +0,0 @@
|
||||
<!-- 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._
|
||||
@@ -1,373 +0,0 @@
|
||||
<!-- 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_
|
||||
@@ -1,664 +0,0 @@
|
||||
# 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)._
|
||||
@@ -1,372 +0,0 @@
|
||||
# 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_
|
||||
@@ -1,524 +0,0 @@
|
||||
# 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), SEON production debugging patterns_
|
||||
@@ -1,35 +0,0 @@
|
||||
id,name,description,tags,fragment_file
|
||||
fixture-architecture,Fixture Architecture,"Composable fixture patterns (pure function → fixture → merge) and reuse rules","fixtures,architecture,playwright,cypress",knowledge/fixture-architecture.md
|
||||
network-first,Network-First Safeguards,"Intercept-before-navigate workflow, HAR capture, deterministic waits, edge mocking","network,stability,playwright,cypress,ui",knowledge/network-first.md
|
||||
data-factories,Data Factories and API Setup,"Factories with overrides, API seeding, cleanup discipline","data,factories,setup,api,backend,seeding",knowledge/data-factories.md
|
||||
component-tdd,Component TDD Loop,"Red→green→refactor workflow, provider isolation, accessibility assertions","component-testing,tdd,ui",knowledge/component-tdd.md
|
||||
playwright-config,Playwright Config Guardrails,"Environment switching, timeout standards, artifact outputs","playwright,config,env",knowledge/playwright-config.md
|
||||
ci-burn-in,CI and Burn-In Strategy,"Staged jobs, shard orchestration, burn-in loops, artifact policy","ci,automation,flakiness",knowledge/ci-burn-in.md
|
||||
selective-testing,Selective Test Execution,"Tag/grep usage, spec filters, diff-based runs, promotion rules","risk-based,selection,strategy",knowledge/selective-testing.md
|
||||
feature-flags,Feature Flag Governance,"Enum management, targeting helpers, cleanup, release checklists","feature-flags,governance,launchdarkly",knowledge/feature-flags.md
|
||||
contract-testing,Contract Testing Essentials,"Pact publishing, provider verification, resilience coverage","contract-testing,pact,api,backend,microservices,service-contract",knowledge/contract-testing.md
|
||||
email-auth,Email Authentication Testing,"Magic link extraction, state preservation, caching, negative flows","email-authentication,security,workflow",knowledge/email-auth.md
|
||||
error-handling,Error Handling Checks,"Scoped exception handling, retry validation, telemetry logging","resilience,error-handling,stability,api,backend",knowledge/error-handling.md
|
||||
visual-debugging,Visual Debugging Toolkit,"Trace viewer usage, artifact expectations, accessibility integration","debugging,dx,tooling,ui",knowledge/visual-debugging.md
|
||||
risk-governance,Risk Governance,"Scoring matrix, category ownership, gate decision rules","risk,governance,gates",knowledge/risk-governance.md
|
||||
probability-impact,Probability and Impact Scale,"Shared definitions for scoring matrix and gate thresholds","risk,scoring,scale",knowledge/probability-impact.md
|
||||
test-quality,Test Quality Definition of Done,"Execution limits, isolation rules, green criteria","quality,definition-of-done,tests",knowledge/test-quality.md
|
||||
nfr-criteria,NFR Review Criteria,"Security, performance, reliability, maintainability status definitions","nfr,assessment,quality",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",knowledge/test-levels-framework.md
|
||||
test-priorities,Test Priorities Matrix,"P0–P3 criteria, coverage targets, execution ordering","testing,prioritization,risk",knowledge/test-priorities-matrix.md
|
||||
test-healing-patterns,Test Healing Patterns,"Common failure patterns and automated fixes","healing,debugging,patterns",knowledge/test-healing-patterns.md
|
||||
selector-resilience,Selector Resilience,"Robust selector strategies and debugging techniques","selectors,locators,debugging,ui",knowledge/selector-resilience.md
|
||||
timing-debugging,Timing Debugging,"Race condition identification and deterministic wait fixes","timing,async,debugging",knowledge/timing-debugging.md
|
||||
overview,Playwright Utils Overview,"Installation, design principles, fixture patterns for API and UI testing","playwright-utils,fixtures,api,backend,ui",knowledge/overview.md
|
||||
api-request,API Request,"Typed HTTP client, schema validation, retry logic for API and service testing","api,backend,service-testing,api-testing,playwright-utils",knowledge/api-request.md
|
||||
network-recorder,Network Recorder,"HAR record/playback, CRUD detection for offline UI testing","network,playwright-utils,ui,har",knowledge/network-recorder.md
|
||||
auth-session,Auth Session,"Token persistence, multi-user, API and browser authentication","auth,playwright-utils,api,backend,jwt,token",knowledge/auth-session.md
|
||||
intercept-network-call,Intercept Network Call,"Network spy/stub, JSON parsing for UI tests","network,playwright-utils,ui",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",knowledge/recurse.md
|
||||
log,Log Utility,"Report logging, structured output for API and UI tests","logging,playwright-utils,api,ui",knowledge/log.md
|
||||
file-utils,File Utilities,"CSV/XLSX/PDF/ZIP validation for API exports and UI downloads","files,playwright-utils,api,backend,ui",knowledge/file-utils.md
|
||||
burn-in,Burn-in Runner,"Smart test selection, git diff for CI optimization","ci,playwright-utils",knowledge/burn-in.md
|
||||
network-error-monitor,Network Error Monitor,"HTTP 4xx/5xx detection for UI tests","monitoring,playwright-utils,ui",knowledge/network-error-monitor.md
|
||||
fixtures-composition,Fixtures Composition,"mergeTests composition patterns for combining utilities","fixtures,playwright-utils",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",knowledge/api-testing-patterns.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",knowledge/adr-quality-readiness-checklist.md
|
||||
|
@@ -4,7 +4,7 @@ description: 'Discovery & Understanding - Understand what user wants to edit and
|
||||
|
||||
# File references (ONLY variables used in this step)
|
||||
altStepFile: './step-e-01b-legacy-conversion.md'
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
|
||||
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
|
||||
---
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Legacy PRD Conversion Assessment - Analyze legacy PRD and propose
|
||||
# File references (ONLY variables used in this step)
|
||||
nextStepFile: './step-e-02-review.md'
|
||||
prdFile: '{prd_file_path}'
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
---
|
||||
|
||||
# Step E-1B: Legacy PRD Conversion Assessment
|
||||
|
||||
@@ -6,7 +6,7 @@ description: 'Deep Review & Analysis - Thoroughly review existing PRD and prepar
|
||||
nextStepFile: './step-e-03-edit.md'
|
||||
prdFile: '{prd_file_path}'
|
||||
validationReport: '{validation_report_path}' # If provided
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
|
||||
---
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Edit & Update - Apply changes to PRD following approved change pla
|
||||
# File references (ONLY variables used in this step)
|
||||
nextStepFile: './step-e-04-complete.md'
|
||||
prdFile: '{prd_file_path}'
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
---
|
||||
|
||||
# Step E-3: Edit & Update
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
1. Review the comprehensive story in {{story_file}}
|
||||
2. Run dev agents `dev-story` for optimized implementation
|
||||
3. Run `code-review` when complete (auto-marks done)
|
||||
4. Optional: Run TEA `*automate` after `dev-story` to generate guardrail tests
|
||||
4. Optional: If Test Architect module installed, run `/bmad:tea:automate` after `dev-story` to generate guardrail tests
|
||||
|
||||
**The developer now has everything needed for flawless implementation!**
|
||||
</output>
|
||||
|
||||
@@ -397,7 +397,7 @@
|
||||
- Verify all acceptance criteria are met
|
||||
- Ensure deployment readiness if applicable
|
||||
- Run `code-review` workflow for peer review
|
||||
- Optional: Run TEA `*automate` to expand guardrail tests
|
||||
- Optional: If Test Architect module installed, run `/bmad:tea:automate` to expand guardrail tests
|
||||
</action>
|
||||
|
||||
<output>💡 **Tip:** For best results, run `code-review` using a **different** LLM than the one that implemented this story.</output>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: 'step-01-understand'
|
||||
description: 'Analyze the requirement delta between current state and what user wants to build'
|
||||
|
||||
workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/create-tech-spec'
|
||||
workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-spec'
|
||||
nextStepFile: './step-02-investigate.md'
|
||||
skipToStepFile: './step-03-generate.md'
|
||||
templateFile: '{workflow_path}/tech-spec-template.md'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: 'step-02-investigate'
|
||||
description: 'Map technical constraints and anchor points within the codebase'
|
||||
|
||||
workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/create-tech-spec'
|
||||
workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-spec'
|
||||
nextStepFile: './step-03-generate.md'
|
||||
wipFile: '{implementation_artifacts}/tech-spec-wip.md'
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: 'step-03-generate'
|
||||
description: 'Build the implementation plan based on the technical mapping of constraints'
|
||||
|
||||
workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/create-tech-spec'
|
||||
workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-spec'
|
||||
nextStepFile: './step-04-review.md'
|
||||
wipFile: '{implementation_artifacts}/tech-spec-wip.md'
|
||||
---
|
||||
|
||||
@@ -139,7 +139,7 @@ b) **HALT and wait for user selection.**
|
||||
#### Menu Handling Logic:
|
||||
|
||||
- IF A: Read fully and follow: `{advanced_elicitation}` with current spec content, process enhanced insights, ask user "Accept improvements? (y/n)", if yes update spec then redisplay menu, if no keep original then redisplay menu
|
||||
- IF B: Load and execute `{quick_dev_workflow}` with the final spec file (warn: fresh context is better)
|
||||
- IF B: Read the entire workflow file at `{quick_dev_workflow}` and follow the instructions with the final spec file (warn: fresh context is better)
|
||||
- IF D: Exit workflow - display final confirmation and path to spec
|
||||
- IF P: Read fully and follow: `{party_mode_exec}` with current spec content, process collaborative insights, ask user "Accept changes? (y/n)", if yes update spec then redisplay menu, if no keep original then redisplay menu
|
||||
- IF R: Execute Adversarial Review (see below)
|
||||
|
||||
33
src/bmm/workflows/qa/automate/checklist.md
Normal file
33
src/bmm/workflows/qa/automate/checklist.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Quinn Automate - Validation Checklist
|
||||
|
||||
## Test Generation
|
||||
|
||||
- [ ] API tests generated (if applicable)
|
||||
- [ ] E2E tests generated (if UI exists)
|
||||
- [ ] Tests use standard test framework APIs
|
||||
- [ ] Tests cover happy path
|
||||
- [ ] Tests cover 1-2 critical error cases
|
||||
|
||||
## Test Quality
|
||||
|
||||
- [ ] All generated tests run successfully
|
||||
- [ ] Tests use proper locators (semantic, accessible)
|
||||
- [ ] Tests have clear descriptions
|
||||
- [ ] No hardcoded waits or sleeps
|
||||
- [ ] Tests are independent (no order dependency)
|
||||
|
||||
## Output
|
||||
|
||||
- [ ] Test summary created
|
||||
- [ ] Tests saved to appropriate directories
|
||||
- [ ] Summary includes coverage metrics
|
||||
|
||||
## Validation
|
||||
|
||||
Run the tests using your project's test command.
|
||||
|
||||
**Expected**: All tests pass ✅
|
||||
|
||||
---
|
||||
|
||||
**Need more comprehensive testing?** Install [Test Architect (TEA)](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/) for advanced workflows.
|
||||
110
src/bmm/workflows/qa/automate/instructions.md
Normal file
110
src/bmm/workflows/qa/automate/instructions.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Quinn QA - Automate
|
||||
|
||||
**Goal**: Generate automated API and E2E tests for implemented code.
|
||||
|
||||
**Scope**: This workflow generates tests ONLY. It does **not** perform code review or story validation (use Code Review `CR` for that).
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 0: Detect Test Framework
|
||||
|
||||
Check project for existing test framework:
|
||||
|
||||
- Look for `package.json` dependencies (playwright, jest, vitest, cypress, etc.)
|
||||
- Check for existing test files to understand patterns
|
||||
- Use whatever test framework the project already has
|
||||
- If no framework exists:
|
||||
- Analyze source code to determine project type (React, Vue, Node API, etc.)
|
||||
- Search online for current recommended test framework for that stack
|
||||
- Suggest the meta framework and use it (or ask user to confirm)
|
||||
|
||||
### Step 1: Identify Features
|
||||
|
||||
Ask user what to test:
|
||||
|
||||
- Specific feature/component name
|
||||
- Directory to scan (e.g., `src/components/`)
|
||||
- Or auto-discover features in the codebase
|
||||
|
||||
### Step 2: Generate API Tests (if applicable)
|
||||
|
||||
For API endpoints/services, generate tests that:
|
||||
|
||||
- Test status codes (200, 400, 404, 500)
|
||||
- Validate response structure
|
||||
- Cover happy path + 1-2 error cases
|
||||
- Use project's existing test framework patterns
|
||||
|
||||
### Step 3: Generate E2E Tests (if UI exists)
|
||||
|
||||
For UI features, generate tests that:
|
||||
|
||||
- Test user workflows end-to-end
|
||||
- Use semantic locators (roles, labels, text)
|
||||
- Focus on user interactions (clicks, form fills, navigation)
|
||||
- Assert visible outcomes
|
||||
- Keep tests linear and simple
|
||||
- Follow project's existing test patterns
|
||||
|
||||
### Step 4: Run Tests
|
||||
|
||||
Execute tests to verify they pass (use project's test command).
|
||||
|
||||
If failures occur, fix them immediately.
|
||||
|
||||
### Step 5: Create Summary
|
||||
|
||||
Output markdown summary:
|
||||
|
||||
```markdown
|
||||
# Test Automation Summary
|
||||
|
||||
## Generated Tests
|
||||
|
||||
### API Tests
|
||||
- [x] tests/api/endpoint.spec.ts - Endpoint validation
|
||||
|
||||
### E2E Tests
|
||||
- [x] tests/e2e/feature.spec.ts - User workflow
|
||||
|
||||
## Coverage
|
||||
- API endpoints: 5/10 covered
|
||||
- UI features: 3/8 covered
|
||||
|
||||
## Next Steps
|
||||
- Run tests in CI
|
||||
- Add more edge cases as needed
|
||||
```
|
||||
|
||||
## Keep It Simple
|
||||
|
||||
**Do:**
|
||||
|
||||
- Use standard test framework APIs
|
||||
- Focus on happy path + critical errors
|
||||
- Write readable, maintainable tests
|
||||
- Run tests to verify they pass
|
||||
|
||||
**Avoid:**
|
||||
|
||||
- Complex fixture composition
|
||||
- Over-engineering
|
||||
- Unnecessary abstractions
|
||||
|
||||
**For Advanced Features:**
|
||||
|
||||
If the project needs:
|
||||
|
||||
- Risk-based test strategy
|
||||
- Test design planning
|
||||
- Quality gates and NFR assessment
|
||||
- Comprehensive coverage analysis
|
||||
- Advanced testing patterns and utilities
|
||||
|
||||
→ **Install Test Architect (TEA) module**: <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/>
|
||||
|
||||
## Output
|
||||
|
||||
Save summary to: `{implementation_artifacts}/tests/test-summary.md`
|
||||
|
||||
**Done!** Tests generated and verified.
|
||||
49
src/bmm/workflows/qa/automate/workflow.yaml
Normal file
49
src/bmm/workflows/qa/automate/workflow.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
# Quinn QA workflow: Automate
|
||||
name: qa-automate
|
||||
description: "Generate tests quickly for existing features using standard test patterns"
|
||||
author: "BMad"
|
||||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
implementation_artifacts: "{config_source}:implementation_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: "{project-root}/_bmad/bmm/workflows/qa/automate"
|
||||
instructions: "{installed_path}/instructions.md"
|
||||
validation: "{installed_path}/checklist.md"
|
||||
template: false
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
# Directory paths
|
||||
test_dir: "{project-root}/tests" # Root test directory
|
||||
source_dir: "{project-root}" # Source code directory
|
||||
|
||||
# Output configuration
|
||||
default_output_file: "{implementation_artifacts}/tests/test-summary.md"
|
||||
|
||||
# Required tools
|
||||
required_tools:
|
||||
- read_file # Read source code and existing tests
|
||||
- write_file # Create test files
|
||||
- create_directory # Create test directories
|
||||
- list_files # Discover features
|
||||
- search_repo # Find patterns
|
||||
- glob # Find files
|
||||
|
||||
tags:
|
||||
- qa
|
||||
- automation
|
||||
- testing
|
||||
|
||||
execution_hints:
|
||||
interactive: false
|
||||
autonomous: true
|
||||
iterative: false
|
||||
|
||||
web_bundle: false
|
||||
@@ -1,363 +0,0 @@
|
||||
# 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}
|
||||
@@ -1,374 +0,0 @@
|
||||
# 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 `{output_folder}/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
|
||||
@@ -1,806 +0,0 @@
|
||||
<!-- Powered by BMAD-CORE™ -->
|
||||
|
||||
# Acceptance Test-Driven Development (ATDD)
|
||||
|
||||
**Workflow ID**: `_bmad/bmm/testarch/atdd`
|
||||
**Version**: 4.0 (BMad v6)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Generates failing acceptance tests BEFORE implementation following TDD's red-green-refactor cycle. This workflow creates comprehensive test coverage at appropriate levels (E2E, API, Component) with supporting infrastructure (fixtures, factories, mocks) and provides an implementation checklist to guide development.
|
||||
|
||||
**Core Principle**: Tests fail first (red phase), then guide development to green, then enable confident refactoring.
|
||||
|
||||
---
|
||||
|
||||
## Preflight Requirements
|
||||
|
||||
**Critical:** Verify these requirements before proceeding. If any fail, HALT and notify the user.
|
||||
|
||||
- ✅ Story approved with clear acceptance criteria
|
||||
- ✅ Development sandbox/environment ready
|
||||
- ✅ Framework scaffolding exists (run `framework` workflow if missing)
|
||||
- ✅ Test framework configuration available (playwright.config.ts or cypress.config.ts)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Load Story Context and Requirements
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Read Story Markdown**
|
||||
- Load story file from `{story_file}` variable
|
||||
- Extract acceptance criteria (all testable requirements)
|
||||
- Identify affected systems and components
|
||||
- Note any technical constraints or dependencies
|
||||
|
||||
2. **Load Framework Configuration**
|
||||
- Read framework config (playwright.config.ts or cypress.config.ts)
|
||||
- Identify test directory structure
|
||||
- Check existing fixture patterns
|
||||
- Note test runner capabilities
|
||||
|
||||
3. **Load Existing Test Patterns**
|
||||
- Search `{test_dir}` for similar tests
|
||||
- Identify reusable fixtures and helpers
|
||||
- Check data factory patterns
|
||||
- Note naming conventions
|
||||
|
||||
4. **Check Playwright Utils Flag**
|
||||
|
||||
Read `{config_source}` and check `config.tea_use_playwright_utils`.
|
||||
|
||||
5. **Load Knowledge Base Fragments**
|
||||
|
||||
**Critical:** Consult `{project-root}/_bmad/bmm/testarch/tea-index.csv` to load:
|
||||
|
||||
**Core Patterns (Always load):**
|
||||
- `data-factories.md` - Factory patterns using faker (override patterns, nested factories, API seeding, 498 lines, 5 examples)
|
||||
- `component-tdd.md` - Component test strategies (red-green-refactor, provider isolation, accessibility, visual regression, 480 lines, 4 examples)
|
||||
- `test-quality.md` - Test design principles (deterministic tests, isolated with cleanup, explicit assertions, length limits, execution time optimization, 658 lines, 5 examples)
|
||||
- `test-healing-patterns.md` - Common failure patterns and healing strategies (stale selectors, race conditions, dynamic data, network errors, hard waits, 648 lines, 5 examples)
|
||||
- `selector-resilience.md` - Selector best practices (data-testid > ARIA > text > CSS hierarchy, dynamic patterns, anti-patterns, 541 lines, 4 examples)
|
||||
- `timing-debugging.md` - Race condition prevention and async debugging (network-first, deterministic waiting, anti-patterns, 370 lines, 3 examples)
|
||||
|
||||
**If `config.tea_use_playwright_utils: true` (All Utilities):**
|
||||
- `overview.md` - Playwright utils for ATDD patterns
|
||||
- `api-request.md` - API test examples with schema validation
|
||||
- `network-recorder.md` - HAR record/playback for UI acceptance tests
|
||||
- `auth-session.md` - Auth setup for acceptance tests
|
||||
- `intercept-network-call.md` - Network interception in ATDD scenarios
|
||||
- `recurse.md` - Polling for async acceptance criteria
|
||||
- `log.md` - Logging in ATDD tests
|
||||
- `file-utils.md` - File download validation in acceptance tests
|
||||
- `network-error-monitor.md` - Catch silent failures in ATDD
|
||||
- `fixtures-composition.md` - Composing utilities for ATDD
|
||||
|
||||
**If `config.tea_use_playwright_utils: false`:**
|
||||
- `fixture-architecture.md` - Test fixture patterns with auto-cleanup (pure function → fixture → mergeTests composition, 406 lines, 5 examples)
|
||||
- `network-first.md` - Route interception patterns (intercept before navigate, HAR capture, deterministic waiting, 489 lines, 5 examples)
|
||||
|
||||
**Halt Condition:** If story has no acceptance criteria or framework is missing, HALT with message: "ATDD requires clear acceptance criteria and test framework setup"
|
||||
|
||||
---
|
||||
|
||||
## Step 1.5: Generation Mode Selection (NEW - Phase 2.5)
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Detect Generation Mode**
|
||||
|
||||
Determine mode based on scenario complexity:
|
||||
|
||||
**AI Generation Mode (DEFAULT)**:
|
||||
- Clear acceptance criteria with standard patterns
|
||||
- Uses: AI-generated tests from requirements
|
||||
- Appropriate for: CRUD, auth, navigation, API tests
|
||||
- Fastest approach
|
||||
|
||||
**Recording Mode (OPTIONAL - Complex UI)**:
|
||||
- Complex UI interactions (drag-drop, wizards, multi-page flows)
|
||||
- Uses: Interactive test recording with Playwright MCP
|
||||
- Appropriate for: Visual workflows, unclear requirements
|
||||
- Only if config.tea_use_mcp_enhancements is true AND MCP available
|
||||
|
||||
2. **AI Generation Mode (DEFAULT - Continue to Step 2)**
|
||||
|
||||
For standard scenarios:
|
||||
- Continue with existing workflow (Step 2: Select Test Levels and Strategy)
|
||||
- AI generates tests based on acceptance criteria from Step 1
|
||||
- Use knowledge base patterns for test structure
|
||||
|
||||
3. **Recording Mode (OPTIONAL - Complex UI Only)**
|
||||
|
||||
For complex UI scenarios AND config.tea_use_mcp_enhancements is true:
|
||||
|
||||
**A. Check MCP Availability**
|
||||
|
||||
If Playwright MCP tools are available in your IDE:
|
||||
- Use MCP recording mode (Step 3.B)
|
||||
|
||||
If MCP unavailable:
|
||||
- Fallback to AI generation mode (silent, automatic)
|
||||
- Continue to Step 2
|
||||
|
||||
**B. Interactive Test Recording (MCP-Based)**
|
||||
|
||||
Use Playwright MCP test-generator tools:
|
||||
|
||||
**Setup:**
|
||||
|
||||
```
|
||||
1. Use generator_setup_page to initialize recording session
|
||||
2. Navigate to application starting URL (from story context)
|
||||
3. Ready to record user interactions
|
||||
```
|
||||
|
||||
**Recording Process (Per Acceptance Criterion):**
|
||||
|
||||
```
|
||||
4. Read acceptance criterion from story
|
||||
5. Manually execute test scenario using browser_* tools:
|
||||
- browser_navigate: Navigate to pages
|
||||
- browser_click: Click buttons, links, elements
|
||||
- browser_type: Fill form fields
|
||||
- browser_select: Select dropdown options
|
||||
- browser_check: Check/uncheck checkboxes
|
||||
6. Add verification steps using browser_verify_* tools:
|
||||
- browser_verify_text: Verify text content
|
||||
- browser_verify_visible: Verify element visibility
|
||||
- browser_verify_url: Verify URL navigation
|
||||
7. Capture interaction log with generator_read_log
|
||||
8. Generate test file with generator_write_test
|
||||
9. Repeat for next acceptance criterion
|
||||
```
|
||||
|
||||
**Post-Recording Enhancement:**
|
||||
|
||||
```
|
||||
10. Review generated test code
|
||||
11. Enhance with knowledge base patterns:
|
||||
- Add Given-When-Then comments
|
||||
- Replace recorded selectors with data-testid (if needed)
|
||||
- Add network-first interception (from network-first.md)
|
||||
- Add fixtures for auth/data setup (from fixture-architecture.md)
|
||||
- Use factories for test data (from data-factories.md)
|
||||
12. Verify tests fail (missing implementation)
|
||||
13. Continue to Step 4 (Build Data Infrastructure)
|
||||
```
|
||||
|
||||
**When to Use Recording Mode:**
|
||||
- ✅ Complex UI interactions (drag-drop, multi-step forms, wizards)
|
||||
- ✅ Visual workflows (modals, dialogs, animations)
|
||||
- ✅ Unclear requirements (exploratory, discovering expected behavior)
|
||||
- ✅ Multi-page flows (checkout, registration, onboarding)
|
||||
- ❌ NOT for simple CRUD (AI generation faster)
|
||||
- ❌ NOT for API-only tests (no UI to record)
|
||||
|
||||
**When to Use AI Generation (Default):**
|
||||
- ✅ Clear acceptance criteria available
|
||||
- ✅ Standard patterns (login, CRUD, navigation)
|
||||
- ✅ Need many tests quickly
|
||||
- ✅ API/backend tests (no UI interaction)
|
||||
|
||||
4. **Proceed to Test Level Selection**
|
||||
|
||||
After mode selection:
|
||||
- AI Generation: Continue to Step 2 (Select Test Levels and Strategy)
|
||||
- Recording: Skip to Step 4 (Build Data Infrastructure) - tests already generated
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Select Test Levels and Strategy
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Analyze Acceptance Criteria**
|
||||
|
||||
For each acceptance criterion, determine:
|
||||
- Does it require full user journey? → E2E test
|
||||
- Does it test business logic/API contract? → API test
|
||||
- Does it validate UI component behavior? → Component test
|
||||
- Can it be unit tested? → Unit test
|
||||
|
||||
2. **Apply Test Level Selection Framework**
|
||||
|
||||
**Knowledge Base Reference**: `test-levels-framework.md`
|
||||
|
||||
**E2E (End-to-End)**:
|
||||
- Critical user journeys (login, checkout, core workflow)
|
||||
- Multi-system integration
|
||||
- User-facing acceptance criteria
|
||||
- **Characteristics**: High confidence, slow execution, brittle
|
||||
|
||||
**API (Integration)**:
|
||||
- Business logic validation
|
||||
- Service contracts
|
||||
- Data transformations
|
||||
- **Characteristics**: Fast feedback, good balance, stable
|
||||
|
||||
**Component**:
|
||||
- UI component behavior (buttons, forms, modals)
|
||||
- Interaction testing
|
||||
- Visual regression
|
||||
- **Characteristics**: Fast, isolated, granular
|
||||
|
||||
**Unit**:
|
||||
- Pure business logic
|
||||
- Edge cases
|
||||
- Error handling
|
||||
- **Characteristics**: Fastest, most granular
|
||||
|
||||
3. **Avoid Duplicate Coverage**
|
||||
|
||||
Don't test same behavior at multiple levels unless necessary:
|
||||
- Use E2E for critical happy path only
|
||||
- Use API tests for complex business logic variations
|
||||
- Use component tests for UI interaction edge cases
|
||||
- Use unit tests for pure logic edge cases
|
||||
|
||||
4. **Prioritize Tests**
|
||||
|
||||
If test-design document exists, align with priority levels:
|
||||
- P0 scenarios → Must cover in failing tests
|
||||
- P1 scenarios → Should cover if time permits
|
||||
- P2/P3 scenarios → Optional for this iteration
|
||||
|
||||
**Decision Point:** Set `primary_level` variable to main test level for this story (typically E2E or API)
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Generate Failing Tests
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Create Test File Structure**
|
||||
|
||||
```
|
||||
tests/
|
||||
├── e2e/
|
||||
│ └── {feature-name}.spec.ts # E2E acceptance tests
|
||||
├── api/
|
||||
│ └── {feature-name}.api.spec.ts # API contract tests
|
||||
├── component/
|
||||
│ └── {ComponentName}.test.tsx # Component tests
|
||||
└── support/
|
||||
├── fixtures/ # Test fixtures
|
||||
├── factories/ # Data factories
|
||||
└── helpers/ # Utility functions
|
||||
```
|
||||
|
||||
2. **Write Failing E2E Tests (If Applicable)**
|
||||
|
||||
**Use Given-When-Then format:**
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('User Login', () => {
|
||||
test('should display error for invalid credentials', async ({ page }) => {
|
||||
// GIVEN: User is on login page
|
||||
await page.goto('/login');
|
||||
|
||||
// WHEN: User submits invalid credentials
|
||||
await page.fill('[data-testid="email-input"]', 'invalid@example.com');
|
||||
await page.fill('[data-testid="password-input"]', 'wrongpassword');
|
||||
await page.click('[data-testid="login-button"]');
|
||||
|
||||
// THEN: Error message is displayed
|
||||
await expect(page.locator('[data-testid="error-message"]')).toHaveText('Invalid email or password');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Critical patterns:**
|
||||
- One assertion per test (atomic tests)
|
||||
- Explicit waits (no hard waits/sleeps)
|
||||
- Network-first approach (route interception before navigation)
|
||||
- data-testid selectors for stability
|
||||
- Clear Given-When-Then structure
|
||||
|
||||
3. **Apply Network-First Pattern**
|
||||
|
||||
**Knowledge Base Reference**: `network-first.md`
|
||||
|
||||
```typescript
|
||||
test('should load user dashboard after login', async ({ page }) => {
|
||||
// CRITICAL: Intercept routes BEFORE navigation
|
||||
await page.route('**/api/user', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({ id: 1, name: 'Test User' }),
|
||||
}),
|
||||
);
|
||||
|
||||
// NOW navigate
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await expect(page.locator('[data-testid="user-name"]')).toHaveText('Test User');
|
||||
});
|
||||
```
|
||||
|
||||
4. **Write Failing API Tests (If Applicable)**
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('User API', () => {
|
||||
test('POST /api/users - should create new user', async ({ request }) => {
|
||||
// GIVEN: Valid user data
|
||||
const userData = {
|
||||
email: 'newuser@example.com',
|
||||
name: 'New User',
|
||||
};
|
||||
|
||||
// WHEN: Creating user via API
|
||||
const response = await request.post('/api/users', {
|
||||
data: userData,
|
||||
});
|
||||
|
||||
// THEN: User is created successfully
|
||||
expect(response.status()).toBe(201);
|
||||
const body = await response.json();
|
||||
expect(body).toMatchObject({
|
||||
email: userData.email,
|
||||
name: userData.name,
|
||||
id: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
5. **Write Failing Component Tests (If Applicable)**
|
||||
|
||||
**Knowledge Base Reference**: `component-tdd.md`
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { LoginForm } from './LoginForm';
|
||||
|
||||
test.describe('LoginForm Component', () => {
|
||||
test('should disable submit button when fields are empty', async ({ mount }) => {
|
||||
// GIVEN: LoginForm is mounted
|
||||
const component = await mount(<LoginForm />);
|
||||
|
||||
// WHEN: Form is initially rendered
|
||||
const submitButton = component.locator('button[type="submit"]');
|
||||
|
||||
// THEN: Submit button is disabled
|
||||
await expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
6. **Verify Tests Fail Initially**
|
||||
|
||||
**Critical verification:**
|
||||
- Run tests locally to confirm they fail
|
||||
- Failure should be due to missing implementation, not test errors
|
||||
- Failure messages should be clear and actionable
|
||||
- All tests must be in RED phase before sharing with DEV
|
||||
|
||||
**Important:** Tests MUST fail initially. If a test passes before implementation, it's not a valid acceptance test.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Build Data Infrastructure
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Create Data Factories**
|
||||
|
||||
**Knowledge Base Reference**: `data-factories.md`
|
||||
|
||||
```typescript
|
||||
// tests/support/factories/user.factory.ts
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export const createUser = (overrides = {}) => ({
|
||||
id: faker.number.int(),
|
||||
email: faker.internet.email(),
|
||||
name: faker.person.fullName(),
|
||||
createdAt: faker.date.recent().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const createUsers = (count: number) => Array.from({ length: count }, () => createUser());
|
||||
```
|
||||
|
||||
**Factory principles:**
|
||||
- Use faker for random data (no hardcoded values)
|
||||
- Support overrides for specific scenarios
|
||||
- Generate complete valid objects
|
||||
- Include helper functions for bulk creation
|
||||
|
||||
2. **Create Test Fixtures**
|
||||
|
||||
**Knowledge Base Reference**: `fixture-architecture.md`
|
||||
|
||||
```typescript
|
||||
// tests/support/fixtures/auth.fixture.ts
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export const test = base.extend({
|
||||
authenticatedUser: async ({ page }, use) => {
|
||||
// Setup: Create and authenticate user
|
||||
const user = await createUser();
|
||||
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 page.waitForURL('/dashboard');
|
||||
|
||||
// Provide to test
|
||||
await use(user);
|
||||
|
||||
// Cleanup: Delete user
|
||||
await deleteUser(user.id);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Fixture principles:**
|
||||
- Auto-cleanup (always delete created data)
|
||||
- Composable (fixtures can use other fixtures)
|
||||
- Isolated (each test gets fresh data)
|
||||
- Type-safe
|
||||
|
||||
3. **Document Mock Requirements**
|
||||
|
||||
If external services need mocking, document requirements:
|
||||
|
||||
```markdown
|
||||
### Mock Requirements for DEV Team
|
||||
|
||||
**Payment Gateway Mock**:
|
||||
|
||||
- Endpoint: `POST /api/payments`
|
||||
- Success response: `{ status: 'success', transactionId: '123' }`
|
||||
- Failure response: `{ status: 'failed', error: 'Insufficient funds' }`
|
||||
|
||||
**Email Service Mock**:
|
||||
|
||||
- Should not send real emails in test environment
|
||||
- Log email contents for verification
|
||||
```
|
||||
|
||||
4. **List Required data-testid Attributes**
|
||||
|
||||
```markdown
|
||||
### Required data-testid Attributes
|
||||
|
||||
**Login Page**:
|
||||
|
||||
- `email-input` - Email input field
|
||||
- `password-input` - Password input field
|
||||
- `login-button` - Submit button
|
||||
- `error-message` - Error message container
|
||||
|
||||
**Dashboard Page**:
|
||||
|
||||
- `user-name` - User name display
|
||||
- `logout-button` - Logout button
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Create Implementation Checklist
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Map Tests to Implementation Tasks**
|
||||
|
||||
For each failing test, create corresponding implementation task:
|
||||
|
||||
```markdown
|
||||
## Implementation Checklist
|
||||
|
||||
### Epic X - User Authentication
|
||||
|
||||
#### Test: User Login with Valid Credentials
|
||||
|
||||
- [ ] Create `/login` route
|
||||
- [ ] Implement login form component
|
||||
- [ ] Add email/password validation
|
||||
- [ ] Integrate authentication API
|
||||
- [ ] Add `data-testid` attributes: `email-input`, `password-input`, `login-button`
|
||||
- [ ] Implement error handling
|
||||
- [ ] Run test: `npm run test:e2e -- login.spec.ts`
|
||||
- [ ] ✅ Test passes (green phase)
|
||||
|
||||
#### Test: Display Error for Invalid Credentials
|
||||
|
||||
- [ ] Add error state management
|
||||
- [ ] Display error message UI
|
||||
- [ ] Add `data-testid="error-message"`
|
||||
- [ ] Run test: `npm run test:e2e -- login.spec.ts`
|
||||
- [ ] ✅ Test passes (green phase)
|
||||
```
|
||||
|
||||
2. **Include Red-Green-Refactor Guidance**
|
||||
|
||||
```markdown
|
||||
## Red-Green-Refactor Workflow
|
||||
|
||||
**RED Phase** (Complete):
|
||||
|
||||
- ✅ All tests written and failing
|
||||
- ✅ Fixtures and factories created
|
||||
- ✅ Mock requirements documented
|
||||
|
||||
**GREEN Phase** (DEV Team):
|
||||
|
||||
1. Pick one failing test
|
||||
2. Implement minimal code to make it pass
|
||||
3. Run test to verify green
|
||||
4. Move to next test
|
||||
5. Repeat until all tests pass
|
||||
|
||||
**REFACTOR Phase** (DEV Team):
|
||||
|
||||
1. All tests passing (green)
|
||||
2. Improve code quality
|
||||
3. Extract duplications
|
||||
4. Optimize performance
|
||||
5. Ensure tests still pass
|
||||
```
|
||||
|
||||
3. **Add Execution Commands**
|
||||
|
||||
````markdown
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all failing tests
|
||||
npm run test:e2e
|
||||
|
||||
# Run specific test file
|
||||
npm run test:e2e -- login.spec.ts
|
||||
|
||||
# Run tests in headed mode (see browser)
|
||||
npm run test:e2e -- --headed
|
||||
|
||||
# Debug specific test
|
||||
npm run test:e2e -- login.spec.ts --debug
|
||||
```
|
||||
````
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Generate Deliverables
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Create ATDD Checklist Document**
|
||||
|
||||
Use template structure at `{installed_path}/atdd-checklist-template.md`:
|
||||
- Story summary
|
||||
- Acceptance criteria breakdown
|
||||
- Test files created (with paths)
|
||||
- Data factories created
|
||||
- Fixtures created
|
||||
- Mock requirements
|
||||
- Required data-testid attributes
|
||||
- Implementation checklist
|
||||
- Red-green-refactor workflow
|
||||
- Execution commands
|
||||
|
||||
2. **Verify All Tests Fail**
|
||||
|
||||
Before finalizing:
|
||||
- Run full test suite locally
|
||||
- Confirm all tests in RED phase
|
||||
- Document expected failure messages
|
||||
- Ensure failures are due to missing implementation, not test bugs
|
||||
|
||||
3. **Write to Output File**
|
||||
|
||||
Save to `{output_folder}/atdd-checklist-{story_id}.md`
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Red-Green-Refactor Cycle
|
||||
|
||||
**RED Phase** (TEA responsibility):
|
||||
|
||||
- Write failing tests first
|
||||
- Tests define expected behavior
|
||||
- Tests must fail for right reason (missing implementation)
|
||||
|
||||
**GREEN Phase** (DEV responsibility):
|
||||
|
||||
- Implement minimal code to pass tests
|
||||
- One test at a time
|
||||
- Don't over-engineer
|
||||
|
||||
**REFACTOR Phase** (DEV responsibility):
|
||||
|
||||
- Improve code quality with confidence
|
||||
- Tests provide safety net
|
||||
- Extract duplications, optimize
|
||||
|
||||
### Given-When-Then Structure
|
||||
|
||||
**GIVEN** (Setup):
|
||||
|
||||
- Arrange test preconditions
|
||||
- Create necessary data
|
||||
- Navigate to starting point
|
||||
|
||||
**WHEN** (Action):
|
||||
|
||||
- Execute the behavior being tested
|
||||
- Single action per test
|
||||
|
||||
**THEN** (Assertion):
|
||||
|
||||
- Verify expected outcome
|
||||
- One assertion per test (atomic)
|
||||
|
||||
### Network-First Testing
|
||||
|
||||
**Critical pattern:**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: Intercept BEFORE navigation
|
||||
await page.route('**/api/data', handler);
|
||||
await page.goto('/page');
|
||||
|
||||
// ❌ WRONG: Navigate then intercept (race condition)
|
||||
await page.goto('/page');
|
||||
await page.route('**/api/data', handler); // Too late!
|
||||
```
|
||||
|
||||
### Data Factory Best Practices
|
||||
|
||||
**Use faker for all test data:**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: Random data
|
||||
email: faker.internet.email();
|
||||
|
||||
// ❌ WRONG: Hardcoded data (collisions, maintenance burden)
|
||||
email: 'test@example.com';
|
||||
```
|
||||
|
||||
**Auto-cleanup principle:**
|
||||
|
||||
- Every factory that creates data must provide cleanup
|
||||
- Fixtures automatically cleanup in teardown
|
||||
- No manual cleanup in test code
|
||||
|
||||
### One Assertion Per Test
|
||||
|
||||
**Atomic test design:**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: One assertion
|
||||
test('should display user name', async ({ page }) => {
|
||||
await expect(page.locator('[data-testid="user-name"]')).toHaveText('John');
|
||||
});
|
||||
|
||||
// ❌ WRONG: Multiple assertions (not atomic)
|
||||
test('should display user info', async ({ page }) => {
|
||||
await expect(page.locator('[data-testid="user-name"]')).toHaveText('John');
|
||||
await expect(page.locator('[data-testid="user-email"]')).toHaveText('john@example.com');
|
||||
});
|
||||
```
|
||||
|
||||
**Why?** If second assertion fails, you don't know if first is still valid.
|
||||
|
||||
### Component Test Strategy
|
||||
|
||||
**When to use component tests:**
|
||||
|
||||
- Complex UI interactions (drag-drop, keyboard nav)
|
||||
- Form validation logic
|
||||
- State management within component
|
||||
- Visual edge cases
|
||||
|
||||
**When NOT to use:**
|
||||
|
||||
- Simple rendering (snapshot tests are sufficient)
|
||||
- Integration with backend (use E2E or API tests)
|
||||
- Full user journeys (use E2E tests)
|
||||
|
||||
### Knowledge Base Integration
|
||||
|
||||
**Core Fragments (Auto-loaded in Step 1):**
|
||||
|
||||
- `fixture-architecture.md` - Pure function → fixture → mergeTests patterns (406 lines, 5 examples)
|
||||
- `data-factories.md` - Factory patterns with faker, overrides, API seeding (498 lines, 5 examples)
|
||||
- `component-tdd.md` - Red-green-refactor, provider isolation, accessibility, visual regression (480 lines, 4 examples)
|
||||
- `network-first.md` - Intercept before navigate, HAR capture, deterministic waiting (489 lines, 5 examples)
|
||||
- `test-quality.md` - Deterministic tests, cleanup, explicit assertions, length/time limits (658 lines, 5 examples)
|
||||
- `test-healing-patterns.md` - Common failure patterns: stale selectors, race conditions, dynamic data, network errors, hard waits (648 lines, 5 examples)
|
||||
- `selector-resilience.md` - Selector hierarchy (data-testid > ARIA > text > CSS), dynamic patterns, anti-patterns (541 lines, 4 examples)
|
||||
- `timing-debugging.md` - Race condition prevention, deterministic waiting, async debugging (370 lines, 3 examples)
|
||||
|
||||
**Reference for Test Level Selection:**
|
||||
|
||||
- `test-levels-framework.md` - E2E vs API vs Component vs Unit decision framework (467 lines, 4 examples)
|
||||
|
||||
**Manual Reference (Optional):**
|
||||
|
||||
- Use `tea-index.csv` to find additional specialized fragments as needed
|
||||
|
||||
---
|
||||
|
||||
## Output Summary
|
||||
|
||||
After completing this workflow, provide a summary:
|
||||
|
||||
```markdown
|
||||
## ATDD Complete - Tests in RED Phase
|
||||
|
||||
**Story**: {story_id}
|
||||
**Primary Test Level**: {primary_level}
|
||||
|
||||
**Failing Tests Created**:
|
||||
|
||||
- E2E tests: {e2e_count} tests in {e2e_files}
|
||||
- API tests: {api_count} tests in {api_files}
|
||||
- Component tests: {component_count} tests in {component_files}
|
||||
|
||||
**Supporting Infrastructure**:
|
||||
|
||||
- Data factories: {factory_count} factories created
|
||||
- Fixtures: {fixture_count} fixtures with auto-cleanup
|
||||
- Mock requirements: {mock_count} services documented
|
||||
|
||||
**Implementation Checklist**:
|
||||
|
||||
- Total tasks: {task_count}
|
||||
- Estimated effort: {effort_estimate} hours
|
||||
|
||||
**Required data-testid Attributes**: {data_testid_count} attributes documented
|
||||
|
||||
**Next Steps for DEV Team**:
|
||||
|
||||
1. Run failing tests: `npm run test:e2e`
|
||||
2. Review implementation checklist
|
||||
3. Implement one test at a time (RED → GREEN)
|
||||
4. Refactor with confidence (tests provide safety net)
|
||||
5. Share progress in daily standup
|
||||
|
||||
**Output File**: {output_file}
|
||||
**Manual Handoff**: Share `{output_file}` and failing tests with the dev workflow (not auto-consumed).
|
||||
|
||||
**Knowledge Base References Applied**:
|
||||
|
||||
- Fixture architecture patterns
|
||||
- Data factory patterns with faker
|
||||
- Network-first route interception
|
||||
- Component TDD strategies
|
||||
- Test quality principles
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
After completing all steps, verify:
|
||||
|
||||
- [ ] Story acceptance criteria analyzed and mapped to tests
|
||||
- [ ] Appropriate test levels selected (E2E, API, Component)
|
||||
- [ ] All tests written in Given-When-Then format
|
||||
- [ ] All tests fail initially (RED phase verified)
|
||||
- [ ] Network-first pattern applied (route interception before navigation)
|
||||
- [ ] Data factories created with faker
|
||||
- [ ] Fixtures created with auto-cleanup
|
||||
- [ ] Mock requirements documented for DEV team
|
||||
- [ ] Required data-testid attributes listed
|
||||
- [ ] Implementation checklist created with clear tasks
|
||||
- [ ] Red-green-refactor workflow documented
|
||||
- [ ] Execution commands provided
|
||||
- [ ] Output file created and formatted correctly
|
||||
|
||||
Refer to `checklist.md` for comprehensive validation criteria.
|
||||
@@ -1,47 +0,0 @@
|
||||
# Test Architect workflow: atdd
|
||||
name: testarch-atdd
|
||||
description: "Generate failing acceptance tests before implementation using TDD red-green-refactor cycle"
|
||||
author: "BMad"
|
||||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
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: "{project-root}/_bmad/bmm/workflows/testarch/atdd"
|
||||
instructions: "{installed_path}/instructions.md"
|
||||
validation: "{installed_path}/checklist.md"
|
||||
template: "{installed_path}/atdd-checklist-template.md"
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
test_dir: "{project-root}/tests" # Root test directory
|
||||
|
||||
# Output configuration
|
||||
default_output_file: "{output_folder}/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
|
||||
|
||||
web_bundle: false
|
||||
@@ -1,582 +0,0 @@
|
||||
# Automate Workflow Validation Checklist
|
||||
|
||||
Use this checklist to validate that the automate workflow has been executed correctly and all deliverables meet quality standards.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting this workflow, verify:
|
||||
|
||||
- [ ] Framework scaffolding configured (playwright.config.ts or cypress.config.ts exists)
|
||||
- [ ] Test directory structure exists (tests/ folder with subdirectories)
|
||||
- [ ] Package.json has test framework dependencies installed
|
||||
|
||||
**Halt only if:** Framework scaffolding is completely missing (run `framework` workflow first)
|
||||
|
||||
**Note:** BMad artifacts (story, tech-spec, PRD) are OPTIONAL - workflow can run without them
|
||||
**Note:** `automate` generates tests; it does not run `*atdd` or `*test-review`. If ATDD outputs exist, use them as input and avoid duplicate coverage.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Execution Mode Determination and Context Loading
|
||||
|
||||
### Mode Detection
|
||||
|
||||
- [ ] Execution mode correctly determined:
|
||||
- [ ] BMad-Integrated Mode (story_file variable set) OR
|
||||
- [ ] Standalone Mode (target_feature or target_files set) OR
|
||||
- [ ] Auto-discover Mode (no targets specified)
|
||||
|
||||
### BMad Artifacts (If Available - OPTIONAL)
|
||||
|
||||
- [ ] Story markdown loaded (if `{story_file}` provided)
|
||||
- [ ] Acceptance criteria extracted from story (if available)
|
||||
- [ ] Tech-spec.md loaded (if `{use_tech_spec}` true and file exists)
|
||||
- [ ] Test-design.md loaded (if `{use_test_design}` true and file exists)
|
||||
- [ ] PRD.md loaded (if `{use_prd}` true and file exists)
|
||||
- [ ] **Note**: Absence of BMad artifacts does NOT halt workflow
|
||||
|
||||
### Framework Configuration
|
||||
|
||||
- [ ] Test framework config loaded (playwright.config.ts or cypress.config.ts)
|
||||
- [ ] Test directory structure identified from `{test_dir}`
|
||||
- [ ] Existing test patterns reviewed
|
||||
- [ ] Test runner capabilities noted (parallel execution, fixtures, etc.)
|
||||
|
||||
### Coverage Analysis
|
||||
|
||||
- [ ] Existing test files searched in `{test_dir}` (if `{analyze_coverage}` true)
|
||||
- [ ] Tested features vs untested features identified
|
||||
- [ ] Coverage gaps mapped (tests to source files)
|
||||
- [ ] Existing fixture and factory patterns checked
|
||||
|
||||
### Knowledge Base Fragments Loaded
|
||||
|
||||
- [ ] `test-levels-framework.md` - Test level selection
|
||||
- [ ] `test-priorities.md` - Priority classification (P0-P3)
|
||||
- [ ] `fixture-architecture.md` - Fixture patterns with auto-cleanup
|
||||
- [ ] `data-factories.md` - Factory patterns using faker
|
||||
- [ ] `selective-testing.md` - Targeted test execution strategies
|
||||
- [ ] `ci-burn-in.md` - Flaky test detection patterns
|
||||
- [ ] `test-quality.md` - Test design principles
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Automation Targets Identification
|
||||
|
||||
### Target Determination
|
||||
|
||||
**BMad-Integrated Mode (if story available):**
|
||||
|
||||
- [ ] Acceptance criteria mapped to test scenarios
|
||||
- [ ] Features implemented in story identified
|
||||
- [ ] Existing ATDD tests checked (if any)
|
||||
- [ ] Expansion beyond ATDD planned (edge cases, negative paths)
|
||||
|
||||
**Standalone Mode (if no story):**
|
||||
|
||||
- [ ] Specific feature analyzed (if `{target_feature}` specified)
|
||||
- [ ] Specific files analyzed (if `{target_files}` specified)
|
||||
- [ ] Features auto-discovered (if `{auto_discover_features}` true)
|
||||
- [ ] Features prioritized by:
|
||||
- [ ] No test coverage (highest priority)
|
||||
- [ ] Complex business logic
|
||||
- [ ] External integrations (API, database, auth)
|
||||
- [ ] Critical user paths (login, checkout, etc.)
|
||||
|
||||
### Test Level Selection
|
||||
|
||||
- [ ] Test level selection framework applied (from `test-levels-framework.md`)
|
||||
- [ ] E2E tests identified: Critical user journeys, multi-system integration
|
||||
- [ ] API tests identified: Business logic, service contracts, data transformations
|
||||
- [ ] Component tests identified: UI behavior, interactions, state management
|
||||
- [ ] Unit tests identified: Pure logic, edge cases, error handling
|
||||
|
||||
### Duplicate Coverage Avoidance
|
||||
|
||||
- [ ] Same behavior NOT tested at multiple levels unnecessarily
|
||||
- [ ] E2E used for critical happy path only
|
||||
- [ ] API tests used for business logic variations
|
||||
- [ ] Component tests used for UI interaction edge cases
|
||||
- [ ] Unit tests used for pure logic edge cases
|
||||
|
||||
### Priority Assignment
|
||||
|
||||
- [ ] Test priorities assigned using `test-priorities.md` framework
|
||||
- [ ] P0 tests: Critical paths, security-critical, data integrity
|
||||
- [ ] P1 tests: Important features, integration points, error handling
|
||||
- [ ] P2 tests: Edge cases, less-critical variations, performance
|
||||
- [ ] P3 tests: Nice-to-have, rarely-used features, exploratory
|
||||
- [ ] Priority variables respected:
|
||||
- [ ] `{include_p0}` = true (always include)
|
||||
- [ ] `{include_p1}` = true (high priority)
|
||||
- [ ] `{include_p2}` = true (medium priority)
|
||||
- [ ] `{include_p3}` = false (low priority, skip by default)
|
||||
|
||||
### Coverage Plan Created
|
||||
|
||||
- [ ] Test coverage plan documented
|
||||
- [ ] What will be tested at each level listed
|
||||
- [ ] Priorities assigned to each test
|
||||
- [ ] Coverage strategy clear (critical-paths, comprehensive, or selective)
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Test Infrastructure Generated
|
||||
|
||||
### Fixture Architecture
|
||||
|
||||
- [ ] Existing fixtures checked in `tests/support/fixtures/`
|
||||
- [ ] Fixture architecture created/enhanced (if `{generate_fixtures}` true)
|
||||
- [ ] All fixtures use Playwright's `test.extend()` pattern
|
||||
- [ ] All fixtures have auto-cleanup in teardown
|
||||
- [ ] Common fixtures created/enhanced:
|
||||
- [ ] authenticatedUser (with auto-delete)
|
||||
- [ ] apiRequest (authenticated client)
|
||||
- [ ] mockNetwork (external service mocking)
|
||||
- [ ] testDatabase (with auto-cleanup)
|
||||
|
||||
### Data Factories
|
||||
|
||||
- [ ] Existing factories checked in `tests/support/factories/`
|
||||
- [ ] Factory architecture created/enhanced (if `{generate_factories}` true)
|
||||
- [ ] All factories use `@faker-js/faker` for random data (no hardcoded values)
|
||||
- [ ] All factories support overrides for specific scenarios
|
||||
- [ ] Common factories created/enhanced:
|
||||
- [ ] User factory (email, password, name, role)
|
||||
- [ ] Product factory (name, price, SKU)
|
||||
- [ ] Order factory (items, total, status)
|
||||
- [ ] Cleanup helpers provided (e.g., deleteUser(), deleteProduct())
|
||||
|
||||
### Helper Utilities
|
||||
|
||||
- [ ] Existing helpers checked in `tests/support/helpers/` (if `{update_helpers}` true)
|
||||
- [ ] Common utilities created/enhanced:
|
||||
- [ ] waitFor (polling for complex conditions)
|
||||
- [ ] retry (retry helper for flaky operations)
|
||||
- [ ] testData (test data generation)
|
||||
- [ ] assertions (custom assertion helpers)
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Test Files Generated
|
||||
|
||||
### Test File Structure
|
||||
|
||||
- [ ] Test files organized correctly:
|
||||
- [ ] `tests/e2e/` for E2E tests
|
||||
- [ ] `tests/api/` for API tests
|
||||
- [ ] `tests/component/` for component tests
|
||||
- [ ] `tests/unit/` for unit tests
|
||||
- [ ] `tests/support/` for fixtures/factories/helpers
|
||||
|
||||
### E2E Tests (If Applicable)
|
||||
|
||||
- [ ] E2E test files created in `tests/e2e/`
|
||||
- [ ] All tests follow Given-When-Then format
|
||||
- [ ] All tests have priority tags ([P0], [P1], [P2], [P3]) in test name
|
||||
- [ ] All tests use data-testid selectors (not CSS classes)
|
||||
- [ ] One assertion per test (atomic design)
|
||||
- [ ] No hard waits or sleeps (explicit waits only)
|
||||
- [ ] Network-first pattern applied (route interception BEFORE navigation)
|
||||
- [ ] Clear Given-When-Then comments in test code
|
||||
|
||||
### API Tests (If Applicable)
|
||||
|
||||
- [ ] API test files created in `tests/api/`
|
||||
- [ ] All tests follow Given-When-Then format
|
||||
- [ ] All tests have priority tags in test name
|
||||
- [ ] API contracts validated (request/response structure)
|
||||
- [ ] HTTP status codes verified
|
||||
- [ ] Response body validation includes required fields
|
||||
- [ ] Error cases tested (400, 401, 403, 404, 500)
|
||||
- [ ] JWT token format validated (if auth tests)
|
||||
|
||||
### Component Tests (If Applicable)
|
||||
|
||||
- [ ] Component test files created in `tests/component/`
|
||||
- [ ] All tests follow Given-When-Then format
|
||||
- [ ] All tests have priority tags in test name
|
||||
- [ ] Component mounting works correctly
|
||||
- [ ] Interaction testing covers user actions (click, hover, keyboard)
|
||||
- [ ] State management validated
|
||||
- [ ] Props and events tested
|
||||
|
||||
### Unit Tests (If Applicable)
|
||||
|
||||
- [ ] Unit test files created in `tests/unit/`
|
||||
- [ ] All tests follow Given-When-Then format
|
||||
- [ ] All tests have priority tags in test name
|
||||
- [ ] Pure logic tested (no dependencies)
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Error handling tested
|
||||
|
||||
### Quality Standards Enforced
|
||||
|
||||
- [ ] All tests use Given-When-Then format with clear comments
|
||||
- [ ] All tests have descriptive names with priority tags
|
||||
- [ ] 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)
|
||||
- [ ] All tests use data-testid selectors (E2E tests)
|
||||
- [ ] No hard waits: `await page.waitForTimeout()` (forbidden)
|
||||
- [ ] No conditional flow: `if (await element.isVisible())` (forbidden)
|
||||
- [ ] No try-catch for test logic (only for cleanup)
|
||||
- [ ] No hardcoded test data (use factories with faker)
|
||||
- [ ] No page object classes (tests are direct and simple)
|
||||
- [ ] No shared state between tests
|
||||
|
||||
### Network-First Pattern Applied
|
||||
|
||||
- [ ] Route interception set up BEFORE navigation (E2E tests with network requests)
|
||||
- [ ] `page.route()` called before `page.goto()` to prevent race conditions
|
||||
- [ ] Network-first pattern verified in all E2E tests that make API calls
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Test Validation and Healing (NEW - Phase 2.5)
|
||||
|
||||
### Healing Configuration
|
||||
|
||||
- [ ] Healing configuration checked:
|
||||
- [ ] `{auto_validate}` setting noted (default: true)
|
||||
- [ ] `{auto_heal_failures}` setting noted (default: false)
|
||||
- [ ] `{max_healing_iterations}` setting noted (default: 3)
|
||||
- [ ] `{use_mcp_healing}` setting noted (default: true)
|
||||
|
||||
### Healing Knowledge Fragments Loaded (If Healing Enabled)
|
||||
|
||||
- [ ] `test-healing-patterns.md` loaded (common failure patterns and fixes)
|
||||
- [ ] `selector-resilience.md` loaded (selector refactoring guide)
|
||||
- [ ] `timing-debugging.md` loaded (race condition fixes)
|
||||
|
||||
### Test Execution and Validation
|
||||
|
||||
- [ ] Generated tests executed (if `{auto_validate}` true)
|
||||
- [ ] Test results captured:
|
||||
- [ ] Total tests run
|
||||
- [ ] Passing tests count
|
||||
- [ ] Failing tests count
|
||||
- [ ] Error messages and stack traces captured
|
||||
|
||||
### Healing Loop (If Enabled and Tests Failed)
|
||||
|
||||
- [ ] Healing loop entered (if `{auto_heal_failures}` true AND tests failed)
|
||||
- [ ] For each failing test:
|
||||
- [ ] Failure pattern identified (selector, timing, data, network, hard wait)
|
||||
- [ ] Appropriate healing strategy applied:
|
||||
- [ ] Stale selector → Replaced with data-testid or ARIA role
|
||||
- [ ] Race condition → Added network-first interception or state waits
|
||||
- [ ] Dynamic data → Replaced hardcoded values with regex/dynamic generation
|
||||
- [ ] Network error → Added route mocking
|
||||
- [ ] Hard wait → Replaced with event-based wait
|
||||
- [ ] Healed test re-run to validate fix
|
||||
- [ ] Iteration count tracked (max 3 attempts)
|
||||
|
||||
### Unfixable Tests Handling
|
||||
|
||||
- [ ] Tests that couldn't be healed after 3 iterations marked with `test.fixme()` (if `{mark_unhealable_as_fixme}` true)
|
||||
- [ ] Detailed comment added to test.fixme() tests:
|
||||
- [ ] What failure occurred
|
||||
- [ ] What healing was attempted (3 iterations)
|
||||
- [ ] Why healing failed
|
||||
- [ ] Manual investigation steps needed
|
||||
- [ ] Original test logic preserved in comments
|
||||
|
||||
### Healing Report Generated
|
||||
|
||||
- [ ] Healing report generated (if healing attempted)
|
||||
- [ ] Report includes:
|
||||
- [ ] Auto-heal enabled status
|
||||
- [ ] Healing mode (MCP-assisted or Pattern-based)
|
||||
- [ ] Iterations allowed (max_healing_iterations)
|
||||
- [ ] Validation results (total, passing, failing)
|
||||
- [ ] Successfully healed tests (count, file:line, fix applied)
|
||||
- [ ] Unable to heal tests (count, file:line, reason)
|
||||
- [ ] Healing patterns applied (selector fixes, timing fixes, data fixes)
|
||||
- [ ] Knowledge base references used
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Documentation and Scripts Updated
|
||||
|
||||
### Test README Updated
|
||||
|
||||
- [ ] `tests/README.md` created or updated (if `{update_readme}` true)
|
||||
- [ ] Test suite structure overview included
|
||||
- [ ] Test execution instructions provided (all, specific files, by priority)
|
||||
- [ ] Fixture usage examples provided
|
||||
- [ ] Factory usage examples provided
|
||||
- [ ] Priority tagging convention explained ([P0], [P1], [P2], [P3])
|
||||
- [ ] How to write new tests documented
|
||||
- [ ] Common patterns documented
|
||||
- [ ] Anti-patterns documented (what to avoid)
|
||||
|
||||
### package.json Scripts Updated
|
||||
|
||||
- [ ] package.json scripts added/updated (if `{update_package_scripts}` true)
|
||||
- [ ] `test:e2e` script for all E2E tests
|
||||
- [ ] `test:e2e:p0` script for P0 tests only
|
||||
- [ ] `test:e2e:p1` script for P0 + P1 tests
|
||||
- [ ] `test:api` script for API tests
|
||||
- [ ] `test:component` script for component tests
|
||||
- [ ] `test:unit` script for unit tests (if applicable)
|
||||
|
||||
### Test Suite Executed
|
||||
|
||||
- [ ] Test suite run locally (if `{run_tests_after_generation}` true)
|
||||
- [ ] Test results captured (passing/failing counts)
|
||||
- [ ] No flaky patterns detected (tests are deterministic)
|
||||
- [ ] Setup requirements documented (if any)
|
||||
- [ ] Known issues documented (if any)
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Automation Summary Generated
|
||||
|
||||
### Automation Summary Document
|
||||
|
||||
- [ ] Output file created at `{output_summary}`
|
||||
- [ ] Document includes execution mode (BMad-Integrated, Standalone, Auto-discover)
|
||||
- [ ] Feature analysis included (source files, coverage gaps) - Standalone mode
|
||||
- [ ] Tests created listed (E2E, API, Component, Unit) with counts and paths
|
||||
- [ ] Infrastructure created listed (fixtures, factories, helpers)
|
||||
- [ ] Test execution instructions provided
|
||||
- [ ] Coverage analysis included:
|
||||
- [ ] Total test count
|
||||
- [ ] Priority breakdown (P0, P1, P2, P3 counts)
|
||||
- [ ] Test level breakdown (E2E, API, Component, Unit counts)
|
||||
- [ ] Coverage percentage (if calculated)
|
||||
- [ ] Coverage status (acceptance criteria covered, gaps identified)
|
||||
- [ ] Definition of Done checklist included
|
||||
- [ ] Next steps provided
|
||||
- [ ] Recommendations included (if Standalone mode)
|
||||
|
||||
### Summary Provided to User
|
||||
|
||||
- [ ] Concise summary output provided
|
||||
- [ ] Total tests created across test levels
|
||||
- [ ] Priority breakdown (P0, P1, P2, P3 counts)
|
||||
- [ ] Infrastructure counts (fixtures, factories, helpers)
|
||||
- [ ] Test execution command provided
|
||||
- [ ] Output file path provided
|
||||
- [ ] Next steps listed
|
||||
|
||||
---
|
||||
|
||||
## Quality Checks
|
||||
|
||||
### Test Design Quality
|
||||
|
||||
- [ ] Tests are readable (clear Given-When-Then structure)
|
||||
- [ ] Tests are maintainable (use factories/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)
|
||||
- [ ] Tests are lean (files under {max_file_lines} lines)
|
||||
|
||||
### Knowledge Base Integration
|
||||
|
||||
- [ ] Test level selection framework applied (from `test-levels-framework.md`)
|
||||
- [ ] Priority classification applied (from `test-priorities.md`)
|
||||
- [ ] Fixture architecture patterns applied (from `fixture-architecture.md`)
|
||||
- [ ] Data factory patterns applied (from `data-factories.md`)
|
||||
- [ ] Selective testing strategies considered (from `selective-testing.md`)
|
||||
- [ ] Flaky test detection patterns considered (from `ci-burn-in.md`)
|
||||
- [ ] Test quality principles applied (from `test-quality.md`)
|
||||
|
||||
### 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
|
||||
- [ ] No console.log or debug statements in test code
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### With Framework Workflow
|
||||
|
||||
- [ ] Test framework configuration detected and used
|
||||
- [ ] Directory structure matches framework setup
|
||||
- [ ] Fixtures and helpers follow established patterns
|
||||
- [ ] Naming conventions consistent with framework standards
|
||||
|
||||
### With BMad Workflows (If Available - OPTIONAL)
|
||||
|
||||
**With Story Workflow:**
|
||||
|
||||
- [ ] Story ID correctly referenced in output (if story available)
|
||||
- [ ] Acceptance criteria from story reflected in tests (if story available)
|
||||
- [ ] Technical constraints from story considered (if story available)
|
||||
|
||||
**With test-design Workflow:**
|
||||
|
||||
- [ ] P0 scenarios from test-design prioritized (if test-design available)
|
||||
- [ ] Risk assessment from test-design considered (if test-design available)
|
||||
- [ ] Coverage strategy aligned with test-design (if test-design available)
|
||||
|
||||
**With atdd Workflow:**
|
||||
|
||||
- [ ] ATDD artifacts provided or located (manual handoff; `atdd` not auto-run)
|
||||
- [ ] Existing ATDD tests checked (if story had ATDD workflow run)
|
||||
- [ ] Expansion beyond ATDD planned (edge cases, negative paths)
|
||||
- [ ] No duplicate coverage with ATDD tests
|
||||
|
||||
### With CI Pipeline
|
||||
|
||||
- [ ] Tests can run in CI environment
|
||||
- [ ] Tests are parallelizable (no shared state)
|
||||
- [ ] Tests have appropriate timeouts
|
||||
- [ ] Tests clean up their data (no CI environment pollution)
|
||||
|
||||
---
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
All of the following must be true before marking this workflow as complete:
|
||||
|
||||
- [ ] **Execution mode determined** (BMad-Integrated, Standalone, or Auto-discover)
|
||||
- [ ] **Framework configuration loaded** and validated
|
||||
- [ ] **Coverage analysis completed** (gaps identified if analyze_coverage true)
|
||||
- [ ] **Automation targets identified** (what needs testing)
|
||||
- [ ] **Test levels selected** appropriately (E2E, API, Component, Unit)
|
||||
- [ ] **Duplicate coverage avoided** (same behavior not tested at multiple levels)
|
||||
- [ ] **Test priorities assigned** (P0, P1, P2, P3)
|
||||
- [ ] **Fixture architecture created/enhanced** with auto-cleanup
|
||||
- [ ] **Data factories created/enhanced** using faker (no hardcoded data)
|
||||
- [ ] **Helper utilities created/enhanced** (if needed)
|
||||
- [ ] **Test files generated** at appropriate levels (E2E, API, Component, Unit)
|
||||
- [ ] **Given-When-Then format used** consistently across all tests
|
||||
- [ ] **Priority tags added** to all test names ([P0], [P1], [P2], [P3])
|
||||
- [ ] **data-testid selectors used** in E2E tests (not CSS classes)
|
||||
- [ ] **Network-first pattern applied** (route interception before navigation)
|
||||
- [ ] **Quality standards enforced** (no hard waits, no flaky patterns, self-cleaning, deterministic)
|
||||
- [ ] **Test README updated** with execution instructions and patterns
|
||||
- [ ] **package.json scripts updated** with test execution commands
|
||||
- [ ] **Test suite run locally** (if run_tests_after_generation true)
|
||||
- [ ] **Tests validated** (if auto_validate enabled)
|
||||
- [ ] **Failures healed** (if auto_heal_failures enabled and tests failed)
|
||||
- [ ] **Healing report generated** (if healing attempted)
|
||||
- [ ] **Unfixable tests marked** with test.fixme() and detailed comments (if any)
|
||||
- [ ] **Automation summary created** and saved to correct location
|
||||
- [ ] **Output file formatted correctly**
|
||||
- [ ] **Knowledge base references applied** and documented (including healing fragments if used)
|
||||
- [ ] **No test quality issues** (flaky patterns, race conditions, hardcoded data, page objects)
|
||||
|
||||
---
|
||||
|
||||
## Common Issues and Resolutions
|
||||
|
||||
### Issue: BMad artifacts not found
|
||||
|
||||
**Problem:** Story, tech-spec, or PRD files not found when variables are set.
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- **automate does NOT require BMad artifacts** - they are OPTIONAL enhancements
|
||||
- If files not found, switch to Standalone Mode automatically
|
||||
- Analyze source code directly without BMad context
|
||||
- Continue workflow without halting
|
||||
|
||||
### Issue: Framework configuration not found
|
||||
|
||||
**Problem:** No playwright.config.ts or cypress.config.ts found.
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- **HALT workflow** - framework is required
|
||||
- Message: "Framework scaffolding required. Run `bmad tea *framework` first."
|
||||
- User must run framework workflow before automate
|
||||
|
||||
### Issue: No automation targets identified
|
||||
|
||||
**Problem:** Neither story, target_feature, nor target_files specified, and auto-discover finds nothing.
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- Check if source_dir variable is correct
|
||||
- Verify source code exists in project
|
||||
- Ask user to specify target_feature or target_files explicitly
|
||||
- Provide examples: `target_feature: "src/auth/"` or `target_files: "src/auth/login.ts,src/auth/session.ts"`
|
||||
|
||||
### Issue: Duplicate coverage detected
|
||||
|
||||
**Problem:** Same behavior tested at multiple levels (E2E + API + Component).
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- Review test level selection framework (test-levels-framework.md)
|
||||
- Use E2E for critical happy path ONLY
|
||||
- Use API for business logic variations
|
||||
- Use Component for UI edge cases
|
||||
- Remove redundant tests that duplicate coverage
|
||||
|
||||
### Issue: Tests have hardcoded data
|
||||
|
||||
**Problem:** Tests use hardcoded email addresses, passwords, or other data.
|
||||
|
||||
**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
|
||||
- Example: `createUser({ email: faker.internet.email() })`
|
||||
|
||||
### Issue: Tests are flaky
|
||||
|
||||
**Problem:** Tests fail intermittently, pass on retry.
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- Remove all hard waits (`page.waitForTimeout()`)
|
||||
- Use explicit waits (`page.waitForSelector()`)
|
||||
- Apply network-first pattern (route interception before navigation)
|
||||
- Remove conditional flow (`if (await element.isVisible())`)
|
||||
- Ensure tests are deterministic (no race conditions)
|
||||
- Run burn-in loop (10 iterations) to detect flakiness
|
||||
|
||||
### Issue: Fixtures don't clean up data
|
||||
|
||||
**Problem:** Test data persists after test run, causing test pollution.
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- Ensure all fixtures have cleanup in teardown phase
|
||||
- Cleanup happens AFTER `await use(data)`
|
||||
- Call deletion/cleanup functions (deleteUser, deleteProduct, etc.)
|
||||
- Verify cleanup works by checking database/storage after test run
|
||||
|
||||
### Issue: Tests too slow
|
||||
|
||||
**Problem:** Tests take longer than 90 seconds (max_test_duration).
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- Remove unnecessary waits and delays
|
||||
- Use parallel execution where possible
|
||||
- Mock external services (don't make real API calls)
|
||||
- Use API tests instead of E2E for business logic
|
||||
- Optimize test data creation (use in-memory database, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Notes for TEA Agent
|
||||
|
||||
- **automate is flexible:** Can work with or without BMad artifacts (story, tech-spec, PRD are OPTIONAL)
|
||||
- **Standalone mode is powerful:** Analyze any codebase and generate tests independently
|
||||
- **Auto-discover mode:** Scan codebase for features needing tests when no targets specified
|
||||
- **Framework is the ONLY hard requirement:** HALT if framework config missing, otherwise proceed
|
||||
- **Avoid duplicate coverage:** E2E for critical paths only, API/Component for variations
|
||||
- **Priority tagging enables selective execution:** P0 tests run on every commit, P1 on PR, P2 nightly
|
||||
- **Network-first pattern prevents race conditions:** Route interception BEFORE navigation
|
||||
- **No page objects:** Keep tests simple, direct, and maintainable
|
||||
- **Use knowledge base:** Load relevant fragments (test-levels, test-priorities, fixture-architecture, data-factories, healing patterns) for guidance
|
||||
- **Deterministic tests only:** No hard waits, no conditional flow, no flaky patterns allowed
|
||||
- **Optional healing:** auto_heal_failures disabled by default (opt-in for automatic test healing)
|
||||
- **Graceful degradation:** Healing works without Playwright MCP (pattern-based fallback)
|
||||
- **Unfixable tests handled:** Mark with test.fixme() and detailed comments (not silently broken)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
||||
# Test Architect workflow: automate
|
||||
name: testarch-automate
|
||||
description: "Expand test automation coverage after implementation or analyze existing codebase to generate comprehensive test suite"
|
||||
author: "BMad"
|
||||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
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: "{project-root}/_bmad/bmm/workflows/testarch/automate"
|
||||
instructions: "{installed_path}/instructions.md"
|
||||
validation: "{installed_path}/checklist.md"
|
||||
template: false
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
# Execution mode and targeting
|
||||
standalone_mode: true # Can work without BMad artifacts (true) or integrate with BMad (false)
|
||||
coverage_target: "critical-paths" # critical-paths, comprehensive, selective
|
||||
|
||||
# Directory paths
|
||||
test_dir: "{project-root}/tests" # Root test directory
|
||||
source_dir: "{project-root}/src" # Source code directory
|
||||
|
||||
# Output configuration
|
||||
default_output_file: "{output_folder}/automation-summary.md"
|
||||
|
||||
# Required tools
|
||||
required_tools:
|
||||
- read_file # Read source code, existing tests, BMad artifacts
|
||||
- write_file # Create test files, fixtures, factories, summaries
|
||||
- create_directory # Create test directories
|
||||
- list_files # Discover features and existing tests
|
||||
- search_repo # Find coverage gaps and patterns
|
||||
- glob # Find test files and source files
|
||||
|
||||
tags:
|
||||
- qa
|
||||
- automation
|
||||
- test-architect
|
||||
- regression
|
||||
- coverage
|
||||
|
||||
execution_hints:
|
||||
interactive: false # Minimize prompts
|
||||
autonomous: true # Proceed without user input unless blocked
|
||||
iterative: true
|
||||
|
||||
web_bundle: false
|
||||
@@ -1,247 +0,0 @@
|
||||
# CI/CD Pipeline Setup - Validation Checklist
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [ ] Git repository initialized (`.git/` exists)
|
||||
- [ ] Git remote configured (`git remote -v` shows origin)
|
||||
- [ ] Test framework configured (`playwright.config._` or `cypress.config._`)
|
||||
- [ ] Local tests pass (`npm run test:e2e` succeeds)
|
||||
- [ ] Team agrees on CI platform
|
||||
- [ ] Access to CI platform settings (if updating)
|
||||
|
||||
Note: CI setup is typically a one-time task per repo and can be run any time after the test framework is configured.
|
||||
|
||||
## Process Steps
|
||||
|
||||
### Step 1: Preflight Checks
|
||||
|
||||
- [ ] Git repository validated
|
||||
- [ ] Framework configuration detected
|
||||
- [ ] Local test execution successful
|
||||
- [ ] CI platform detected or selected
|
||||
- [ ] Node version identified (.nvmrc or default)
|
||||
- [ ] No blocking issues found
|
||||
|
||||
### Step 2: CI Pipeline Configuration
|
||||
|
||||
- [ ] CI configuration file created (`.github/workflows/test.yml` or `.gitlab-ci.yml`)
|
||||
- [ ] File is syntactically valid (no YAML errors)
|
||||
- [ ] Correct framework commands configured
|
||||
- [ ] Node version matches project
|
||||
- [ ] Test directory paths correct
|
||||
|
||||
### Step 3: Parallel Sharding
|
||||
|
||||
- [ ] Matrix strategy configured (4 shards default)
|
||||
- [ ] Shard syntax correct for framework
|
||||
- [ ] fail-fast set to false
|
||||
- [ ] Shard count appropriate for test suite size
|
||||
|
||||
### Step 4: Burn-In Loop
|
||||
|
||||
- [ ] Burn-in job created
|
||||
- [ ] 10 iterations configured
|
||||
- [ ] Proper exit on failure (`|| exit 1`)
|
||||
- [ ] Runs on appropriate triggers (PR, cron)
|
||||
- [ ] Failure artifacts uploaded
|
||||
|
||||
### Step 5: Caching Configuration
|
||||
|
||||
- [ ] Dependency cache configured (npm/yarn)
|
||||
- [ ] Cache key uses lockfile hash
|
||||
- [ ] Browser cache configured (Playwright/Cypress)
|
||||
- [ ] Restore-keys defined for fallback
|
||||
- [ ] Cache paths correct for platform
|
||||
|
||||
### Step 6: Artifact Collection
|
||||
|
||||
- [ ] Artifacts upload on failure only
|
||||
- [ ] Correct artifact paths (test-results/, traces/, etc.)
|
||||
- [ ] Retention days set (30 default)
|
||||
- [ ] Artifact names unique per shard
|
||||
- [ ] No sensitive data in artifacts
|
||||
|
||||
### Step 7: Retry Logic
|
||||
|
||||
- [ ] Retry action/strategy configured
|
||||
- [ ] Max attempts: 2-3
|
||||
- [ ] Timeout appropriate (30 min)
|
||||
- [ ] Retry only on transient errors
|
||||
|
||||
### Step 8: Helper Scripts
|
||||
|
||||
- [ ] `scripts/test-changed.sh` created
|
||||
- [ ] `scripts/ci-local.sh` created
|
||||
- [ ] `scripts/burn-in.sh` created (optional)
|
||||
- [ ] Scripts are executable (`chmod +x`)
|
||||
- [ ] Scripts use correct test commands
|
||||
- [ ] Shebang present (`#!/bin/bash`)
|
||||
|
||||
### Step 9: Documentation
|
||||
|
||||
- [ ] `docs/ci.md` created with pipeline guide
|
||||
- [ ] `docs/ci-secrets-checklist.md` created
|
||||
- [ ] Required secrets documented
|
||||
- [ ] Setup instructions clear
|
||||
- [ ] Troubleshooting section included
|
||||
- [ ] Badge URLs provided (optional)
|
||||
|
||||
## Output Validation
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
- [ ] CI file loads without errors
|
||||
- [ ] All paths resolve correctly
|
||||
- [ ] No hardcoded values (use env vars)
|
||||
- [ ] Triggers configured (push, pull_request, schedule)
|
||||
- [ ] Platform-specific syntax correct
|
||||
|
||||
### Execution Validation
|
||||
|
||||
- [ ] First CI run triggered (push to remote)
|
||||
- [ ] Pipeline starts without errors
|
||||
- [ ] All jobs appear in CI dashboard
|
||||
- [ ] Caching works (check logs for cache hit)
|
||||
- [ ] Tests execute in parallel
|
||||
- [ ] Artifacts collected on failure
|
||||
|
||||
### Performance Validation
|
||||
|
||||
- [ ] Lint stage: <2 minutes
|
||||
- [ ] Test stage (per shard): <10 minutes
|
||||
- [ ] Burn-in stage: <30 minutes
|
||||
- [ ] Total pipeline: <45 minutes
|
||||
- [ ] Cache reduces install time by 2-5 minutes
|
||||
|
||||
## Quality Checks
|
||||
|
||||
### Best Practices Compliance
|
||||
|
||||
- [ ] Burn-in loop follows production patterns
|
||||
- [ ] Parallel sharding configured optimally
|
||||
- [ ] Failure-only artifact collection
|
||||
- [ ] Selective testing enabled (optional)
|
||||
- [ ] Retry logic handles transient failures only
|
||||
- [ ] No secrets in configuration files
|
||||
|
||||
### Knowledge Base Alignment
|
||||
|
||||
- [ ] Burn-in pattern matches `ci-burn-in.md`
|
||||
- [ ] Selective testing matches `selective-testing.md`
|
||||
- [ ] Artifact collection matches `visual-debugging.md`
|
||||
- [ ] Test quality matches `test-quality.md`
|
||||
|
||||
### Security Checks
|
||||
|
||||
- [ ] No credentials in CI configuration
|
||||
- [ ] Secrets use platform secret management
|
||||
- [ ] Environment variables for sensitive data
|
||||
- [ ] Artifact retention appropriate (not too long)
|
||||
- [ ] No debug output exposing secrets
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Status File Integration
|
||||
|
||||
- [ ] CI setup logged in Quality & Testing Progress section
|
||||
- [ ] Status updated with completion timestamp
|
||||
- [ ] Platform and configuration noted
|
||||
|
||||
### Knowledge Base Integration
|
||||
|
||||
- [ ] Relevant knowledge fragments loaded
|
||||
- [ ] Patterns applied from knowledge base
|
||||
- [ ] Documentation references knowledge base
|
||||
- [ ] Knowledge base references in README
|
||||
|
||||
### Workflow Dependencies
|
||||
|
||||
- [ ] `framework` workflow completed first
|
||||
- [ ] Can proceed to `atdd` workflow after CI setup
|
||||
- [ ] Can proceed to `automate` workflow
|
||||
- [ ] CI integrates with `gate` workflow
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
**All must be true:**
|
||||
|
||||
- [ ] All prerequisites met
|
||||
- [ ] All process steps completed
|
||||
- [ ] All output validations passed
|
||||
- [ ] All quality checks passed
|
||||
- [ ] All integration points verified
|
||||
- [ ] First CI run successful
|
||||
- [ ] Performance targets met
|
||||
- [ ] Documentation complete
|
||||
|
||||
## Post-Workflow Actions
|
||||
|
||||
**User must complete:**
|
||||
|
||||
1. [ ] Commit CI configuration
|
||||
2. [ ] Push to remote repository
|
||||
3. [ ] Configure required secrets in CI platform
|
||||
4. [ ] Open PR to trigger first CI run
|
||||
5. [ ] Monitor and verify pipeline execution
|
||||
6. [ ] Adjust parallelism if needed (based on actual run times)
|
||||
7. [ ] Set up notifications (optional)
|
||||
|
||||
**Recommended next workflows:**
|
||||
|
||||
1. [ ] Run `atdd` workflow for test generation
|
||||
2. [ ] Run `automate` workflow for coverage expansion
|
||||
3. [ ] Run `gate` workflow for quality gates
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If workflow fails:
|
||||
|
||||
1. [ ] Delete CI configuration file
|
||||
2. [ ] Remove helper scripts directory
|
||||
3. [ ] Remove documentation (docs/ci.md, etc.)
|
||||
4. [ ] Clear CI platform secrets (if added)
|
||||
5. [ ] Review error logs
|
||||
6. [ ] Fix issues and retry workflow
|
||||
|
||||
## Notes
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue**: CI file syntax errors
|
||||
|
||||
- **Solution**: Validate YAML syntax online or with linter
|
||||
|
||||
**Issue**: Tests fail in CI but pass locally
|
||||
|
||||
- **Solution**: Use `scripts/ci-local.sh` to mirror CI environment
|
||||
|
||||
**Issue**: Caching not working
|
||||
|
||||
- **Solution**: Check cache key formula, verify paths
|
||||
|
||||
**Issue**: Burn-in too slow
|
||||
|
||||
- **Solution**: Reduce iterations or run on cron only
|
||||
|
||||
### Platform-Specific
|
||||
|
||||
**GitHub Actions:**
|
||||
|
||||
- Secrets: Repository Settings → Secrets and variables → Actions
|
||||
- Runners: Ubuntu latest recommended
|
||||
- Concurrency limits: 20 jobs for free tier
|
||||
|
||||
**GitLab CI:**
|
||||
|
||||
- Variables: Project Settings → CI/CD → Variables
|
||||
- Runners: Shared or project-specific
|
||||
- Pipeline quota: 400 minutes/month free tier
|
||||
|
||||
---
|
||||
|
||||
**Checklist Complete**: Sign off when all items validated.
|
||||
|
||||
**Completed by:** {name}
|
||||
**Date:** {date}
|
||||
**Platform:** {GitHub Actions, GitLab CI, Other}
|
||||
**Notes:** {notes}
|
||||
@@ -1,198 +0,0 @@
|
||||
# GitHub Actions CI/CD Pipeline for Test Execution
|
||||
# Generated by BMad TEA Agent - Test Architect Module
|
||||
# Optimized for: Playwright/Cypress, Parallel Sharding, Burn-In Loop
|
||||
|
||||
name: Test Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# Weekly burn-in on Sundays at 2 AM UTC
|
||||
- cron: "0 2 * * 0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Lint stage - Code quality checks
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Determine Node version
|
||||
id: node-version
|
||||
run: |
|
||||
if [ -f .nvmrc ]; then
|
||||
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
|
||||
echo "Using Node from .nvmrc"
|
||||
else
|
||||
echo "value=24" >> "$GITHUB_OUTPUT"
|
||||
echo "Using default Node 24 (current LTS)"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ steps.node-version.outputs.value }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
# Test stage - Parallel execution with sharding
|
||||
test:
|
||||
name: Test (Shard ${{ matrix.shard }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: lint
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Determine Node version
|
||||
id: node-version
|
||||
run: |
|
||||
if [ -f .nvmrc ]; then
|
||||
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
|
||||
echo "Using Node from .nvmrc"
|
||||
else
|
||||
echo "value=22" >> "$GITHUB_OUTPUT"
|
||||
echo "Using default Node 22 (current LTS)"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ steps.node-version.outputs.value }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run tests (shard ${{ matrix.shard }}/4)
|
||||
run: npm run test:e2e -- --shard=${{ matrix.shard }}/4
|
||||
|
||||
- name: Upload test results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}
|
||||
path: |
|
||||
test-results/
|
||||
playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# Burn-in stage - Flaky test detection
|
||||
burn-in:
|
||||
name: Burn-In (Flaky Detection)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
needs: test
|
||||
# Only run burn-in on PRs to main/develop or on schedule
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'schedule'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Determine Node version
|
||||
id: node-version
|
||||
run: |
|
||||
if [ -f .nvmrc ]; then
|
||||
echo "value=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
|
||||
echo "Using Node from .nvmrc"
|
||||
else
|
||||
echo "value=22" >> "$GITHUB_OUTPUT"
|
||||
echo "Using default Node 22 (current LTS)"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ steps.node-version.outputs.value }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run burn-in loop (10 iterations)
|
||||
run: |
|
||||
echo "🔥 Starting burn-in loop - detecting flaky tests"
|
||||
for i in {1..10}; do
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "🔥 Burn-in iteration $i/10"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
npm run test:e2e || exit 1
|
||||
done
|
||||
echo "✅ Burn-in complete - no flaky tests detected"
|
||||
|
||||
- name: Upload burn-in failure artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: burn-in-failures
|
||||
path: |
|
||||
test-results/
|
||||
playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# Report stage - Aggregate and publish results
|
||||
report:
|
||||
name: Test Report
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, burn-in]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Generate summary
|
||||
run: |
|
||||
echo "## Test Execution Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status**: ${{ needs.test.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Burn-in**: ${{ needs.burn-in.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Shards**: 4" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ needs.burn-in.result }}" == "failure" ]; then
|
||||
echo "⚠️ **Flaky tests detected** - Review burn-in artifacts" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -1,149 +0,0 @@
|
||||
# GitLab CI/CD Pipeline for Test Execution
|
||||
# Generated by BMad TEA Agent - Test Architect Module
|
||||
# Optimized for: Playwright/Cypress, Parallel Sharding, Burn-In Loop
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
- burn-in
|
||||
- report
|
||||
|
||||
variables:
|
||||
# Disable git depth for accurate change detection
|
||||
GIT_DEPTH: 0
|
||||
# Use npm ci for faster, deterministic installs
|
||||
npm_config_cache: "$CI_PROJECT_DIR/.npm"
|
||||
# Playwright browser cache
|
||||
PLAYWRIGHT_BROWSERS_PATH: "$CI_PROJECT_DIR/.cache/ms-playwright"
|
||||
# Default Node version when .nvmrc is missing
|
||||
DEFAULT_NODE_VERSION: "24"
|
||||
|
||||
# Caching configuration
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- package-lock.json
|
||||
paths:
|
||||
- .npm/
|
||||
- .cache/ms-playwright/
|
||||
- node_modules/
|
||||
|
||||
# Lint stage - Code quality checks
|
||||
lint:
|
||||
stage: lint
|
||||
image: node:$DEFAULT_NODE_VERSION
|
||||
before_script:
|
||||
- |
|
||||
NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
|
||||
echo "Using Node $NODE_VERSION"
|
||||
npm install -g n
|
||||
n "$NODE_VERSION"
|
||||
node -v
|
||||
- npm ci
|
||||
script:
|
||||
- npm run lint
|
||||
timeout: 5 minutes
|
||||
|
||||
# Test stage - Parallel execution with sharding
|
||||
.test-template: &test-template
|
||||
stage: test
|
||||
image: node:$DEFAULT_NODE_VERSION
|
||||
needs:
|
||||
- lint
|
||||
before_script:
|
||||
- |
|
||||
NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
|
||||
echo "Using Node $NODE_VERSION"
|
||||
npm install -g n
|
||||
n "$NODE_VERSION"
|
||||
node -v
|
||||
- npm ci
|
||||
- npx playwright install --with-deps chromium
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- test-results/
|
||||
- playwright-report/
|
||||
expire_in: 30 days
|
||||
timeout: 30 minutes
|
||||
|
||||
test:shard-1:
|
||||
<<: *test-template
|
||||
script:
|
||||
- npm run test:e2e -- --shard=1/4
|
||||
|
||||
test:shard-2:
|
||||
<<: *test-template
|
||||
script:
|
||||
- npm run test:e2e -- --shard=2/4
|
||||
|
||||
test:shard-3:
|
||||
<<: *test-template
|
||||
script:
|
||||
- npm run test:e2e -- --shard=3/4
|
||||
|
||||
test:shard-4:
|
||||
<<: *test-template
|
||||
script:
|
||||
- npm run test:e2e -- --shard=4/4
|
||||
|
||||
# Burn-in stage - Flaky test detection
|
||||
burn-in:
|
||||
stage: burn-in
|
||||
image: node:$DEFAULT_NODE_VERSION
|
||||
needs:
|
||||
- test:shard-1
|
||||
- test:shard-2
|
||||
- test:shard-3
|
||||
- test:shard-4
|
||||
# Only run burn-in on merge requests to main/develop or on schedule
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||
before_script:
|
||||
- |
|
||||
NODE_VERSION=$(cat .nvmrc 2>/dev/null || echo "$DEFAULT_NODE_VERSION")
|
||||
echo "Using Node $NODE_VERSION"
|
||||
npm install -g n
|
||||
n "$NODE_VERSION"
|
||||
node -v
|
||||
- npm ci
|
||||
- npx playwright install --with-deps chromium
|
||||
script:
|
||||
- |
|
||||
echo "🔥 Starting burn-in loop - detecting flaky tests"
|
||||
for i in {1..10}; do
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "🔥 Burn-in iteration $i/10"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
npm run test:e2e || exit 1
|
||||
done
|
||||
echo "✅ Burn-in complete - no flaky tests detected"
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- test-results/
|
||||
- playwright-report/
|
||||
expire_in: 30 days
|
||||
timeout: 60 minutes
|
||||
|
||||
# Report stage - Aggregate results
|
||||
report:
|
||||
stage: report
|
||||
image: alpine:latest
|
||||
needs:
|
||||
- test:shard-1
|
||||
- test:shard-2
|
||||
- test:shard-3
|
||||
- test:shard-4
|
||||
- burn-in
|
||||
when: always
|
||||
script:
|
||||
- |
|
||||
echo "## Test Execution Summary"
|
||||
echo ""
|
||||
echo "- Pipeline: $CI_PIPELINE_ID"
|
||||
echo "- Shards: 4"
|
||||
echo "- Branch: $CI_COMMIT_REF_NAME"
|
||||
echo ""
|
||||
echo "View detailed results in job artifacts"
|
||||
@@ -1,536 +0,0 @@
|
||||
<!-- Powered by BMAD-CORE™ -->
|
||||
|
||||
# CI/CD Pipeline Setup
|
||||
|
||||
**Workflow ID**: `_bmad/bmm/testarch/ci`
|
||||
**Version**: 4.0 (BMad v6)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Scaffolds a production-ready CI/CD quality pipeline with test execution, burn-in loops for flaky test detection, parallel sharding, artifact collection, and notification configuration. This workflow creates platform-specific CI configuration optimized for fast feedback and reliable test execution.
|
||||
|
||||
Note: This is typically a one-time setup per repo; run it any time after the test framework exists, ideally before feature work starts.
|
||||
|
||||
---
|
||||
|
||||
## Preflight Requirements
|
||||
|
||||
**Critical:** Verify these requirements before proceeding. If any fail, HALT and notify the user.
|
||||
|
||||
- ✅ Git repository is initialized (`.git/` directory exists)
|
||||
- ✅ Local test suite passes (`npm run test:e2e` succeeds)
|
||||
- ✅ Test framework is configured (from `framework` workflow)
|
||||
- ✅ Team agrees on target CI platform (GitHub Actions, GitLab CI, Circle CI, etc.)
|
||||
- ✅ Access to CI platform settings/secrets available (if updating existing pipeline)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Run Preflight Checks
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Verify Git Repository**
|
||||
- Check for `.git/` directory
|
||||
- Confirm remote repository configured (`git remote -v`)
|
||||
- If not initialized, HALT with message: "Git repository required for CI/CD setup"
|
||||
|
||||
2. **Validate Test Framework**
|
||||
- Look for `playwright.config.*` or `cypress.config.*`
|
||||
- Read framework configuration to extract:
|
||||
- Test directory location
|
||||
- Test command
|
||||
- Reporter configuration
|
||||
- Timeout settings
|
||||
- If not found, HALT with message: "Run `framework` workflow first to set up test infrastructure"
|
||||
|
||||
3. **Run Local Tests**
|
||||
- Execute `npm run test:e2e` (or equivalent from package.json)
|
||||
- Ensure tests pass before CI setup
|
||||
- If tests fail, HALT with message: "Fix failing tests before setting up CI/CD"
|
||||
|
||||
4. **Detect CI Platform**
|
||||
- Check for existing CI configuration:
|
||||
- `.github/workflows/*.yml` (GitHub Actions)
|
||||
- `.gitlab-ci.yml` (GitLab CI)
|
||||
- `.circleci/config.yml` (Circle CI)
|
||||
- `Jenkinsfile` (Jenkins)
|
||||
- If found, ask user: "Update existing CI configuration or create new?"
|
||||
- If not found, detect platform from git remote:
|
||||
- `github.com` → GitHub Actions (default)
|
||||
- `gitlab.com` → GitLab CI
|
||||
- Ask user if unable to auto-detect
|
||||
|
||||
5. **Read Environment Configuration**
|
||||
- Use `.nvmrc` for Node version if present
|
||||
- If missing, default to a current LTS (Node 24) or newer instead of a fixed old version
|
||||
- Read `package.json` to identify dependencies (affects caching strategy)
|
||||
|
||||
**Halt Condition:** If preflight checks fail, stop immediately and report which requirement failed.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Scaffold CI Pipeline
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Select CI Platform Template**
|
||||
|
||||
Based on detection or user preference, use the appropriate template:
|
||||
|
||||
**GitHub Actions** (`.github/workflows/test.yml`):
|
||||
- Most common platform
|
||||
- Excellent caching and matrix support
|
||||
- Free for public repos, generous free tier for private
|
||||
|
||||
**GitLab CI** (`.gitlab-ci.yml`):
|
||||
- Integrated with GitLab
|
||||
- Built-in registry and runners
|
||||
- Powerful pipeline features
|
||||
|
||||
**Circle CI** (`.circleci/config.yml`):
|
||||
- Fast execution with parallelism
|
||||
- Docker-first approach
|
||||
- Enterprise features
|
||||
|
||||
**Jenkins** (`Jenkinsfile`):
|
||||
- Self-hosted option
|
||||
- Maximum customization
|
||||
- Requires infrastructure management
|
||||
|
||||
2. **Generate Pipeline Configuration**
|
||||
|
||||
Use templates from `{installed_path}/` directory:
|
||||
- `github-actions-template.yml`
|
||||
- `gitlab-ci-template.yml`
|
||||
|
||||
**Key pipeline stages:**
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- lint # Code quality checks
|
||||
- test # Test execution (parallel shards)
|
||||
- burn-in # Flaky test detection
|
||||
- report # Aggregate results and publish
|
||||
```
|
||||
|
||||
3. **Configure Test Execution**
|
||||
|
||||
**Parallel Sharding:**
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
|
||||
steps:
|
||||
- name: Run tests
|
||||
run: npm run test:e2e -- --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
```
|
||||
|
||||
**Purpose:** Splits tests into N parallel jobs for faster execution (target: <10 min per shard)
|
||||
|
||||
4. **Add Burn-In Loop**
|
||||
|
||||
**Critical pattern from production systems:**
|
||||
|
||||
```yaml
|
||||
burn-in:
|
||||
name: Flaky Test Detection
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run burn-in loop (10 iterations)
|
||||
run: |
|
||||
for i in {1..10}; do
|
||||
echo "🔥 Burn-in iteration $i/10"
|
||||
npm run test:e2e || exit 1
|
||||
done
|
||||
|
||||
- name: Upload failure artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: burn-in-failures
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
```
|
||||
|
||||
**Purpose:** Runs tests multiple times to catch non-deterministic failures before they reach main branch.
|
||||
|
||||
**When to run:**
|
||||
- On pull requests to main/develop
|
||||
- Weekly on cron schedule
|
||||
- After significant test infrastructure changes
|
||||
|
||||
5. **Configure Caching**
|
||||
|
||||
**Node modules cache:**
|
||||
|
||||
```yaml
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
```
|
||||
|
||||
**Browser binaries cache (Playwright):**
|
||||
|
||||
```yaml
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
|
||||
```
|
||||
|
||||
**Purpose:** Reduces CI execution time by 2-5 minutes per run.
|
||||
|
||||
6. **Configure Artifact Collection**
|
||||
|
||||
**Failure artifacts only:**
|
||||
|
||||
```yaml
|
||||
- name: Upload test results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}
|
||||
path: |
|
||||
test-results/
|
||||
playwright-report/
|
||||
retention-days: 30
|
||||
```
|
||||
|
||||
**Artifacts to collect:**
|
||||
- Traces (Playwright) - full debugging context
|
||||
- Screenshots - visual evidence of failures
|
||||
- Videos - interaction playback
|
||||
- HTML reports - detailed test results
|
||||
- Console logs - error messages and warnings
|
||||
|
||||
7. **Add Retry Logic**
|
||||
|
||||
```yaml
|
||||
- name: Run tests with retries
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
retry_on: error
|
||||
command: npm run test:e2e
|
||||
```
|
||||
|
||||
**Purpose:** Handles transient failures (network issues, race conditions)
|
||||
|
||||
8. **Configure Notifications** (Optional)
|
||||
|
||||
If `notify_on_failure` is enabled:
|
||||
|
||||
```yaml
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
text: 'Test failures detected in PR #${{ github.event.pull_request.number }}'
|
||||
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
```
|
||||
|
||||
9. **Generate Helper Scripts**
|
||||
|
||||
**Selective testing script** (`scripts/test-changed.sh`):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Run only tests for changed files
|
||||
|
||||
CHANGED_FILES=$(git diff --name-only HEAD~1)
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "src/.*\.ts$"; then
|
||||
echo "Running affected tests..."
|
||||
npm run test:e2e -- --grep="$(echo $CHANGED_FILES | sed 's/src\///g' | sed 's/\.ts//g')"
|
||||
else
|
||||
echo "No test-affecting changes detected"
|
||||
fi
|
||||
```
|
||||
|
||||
**Local mirror script** (`scripts/ci-local.sh`):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Mirror CI execution locally for debugging
|
||||
|
||||
echo "🔍 Running CI pipeline locally..."
|
||||
|
||||
# Lint
|
||||
npm run lint || exit 1
|
||||
|
||||
# Tests
|
||||
npm run test:e2e || exit 1
|
||||
|
||||
# Burn-in (reduced iterations)
|
||||
for i in {1..3}; do
|
||||
echo "🔥 Burn-in $i/3"
|
||||
npm run test:e2e || exit 1
|
||||
done
|
||||
|
||||
echo "✅ Local CI pipeline passed"
|
||||
```
|
||||
|
||||
10. **Generate Documentation**
|
||||
|
||||
**CI README** (`docs/ci.md`):
|
||||
- Pipeline stages and purpose
|
||||
- How to run locally
|
||||
- Debugging failed CI runs
|
||||
- Secrets and environment variables needed
|
||||
- Notification setup
|
||||
- Badge URLs for README
|
||||
|
||||
**Secrets checklist** (`docs/ci-secrets-checklist.md`):
|
||||
- Required secrets list (SLACK_WEBHOOK, etc.)
|
||||
- Where to configure in CI platform
|
||||
- Security best practices
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Deliverables
|
||||
|
||||
### Primary Artifacts Created
|
||||
|
||||
1. **CI Configuration File**
|
||||
- `.github/workflows/test.yml` (GitHub Actions)
|
||||
- `.gitlab-ci.yml` (GitLab CI)
|
||||
- `.circleci/config.yml` (Circle CI)
|
||||
|
||||
2. **Pipeline Stages**
|
||||
- **Lint**: Code quality checks (ESLint, Prettier)
|
||||
- **Test**: Parallel test execution (4 shards)
|
||||
- **Burn-in**: Flaky test detection (10 iterations)
|
||||
- **Report**: Result aggregation and publishing
|
||||
|
||||
3. **Helper Scripts**
|
||||
- `scripts/test-changed.sh` - Selective testing
|
||||
- `scripts/ci-local.sh` - Local CI mirror
|
||||
- `scripts/burn-in.sh` - Standalone burn-in execution
|
||||
|
||||
4. **Documentation**
|
||||
- `docs/ci.md` - CI pipeline guide
|
||||
- `docs/ci-secrets-checklist.md` - Required secrets
|
||||
- Inline comments in CI configuration
|
||||
|
||||
5. **Optimization Features**
|
||||
- Dependency caching (npm, browser binaries)
|
||||
- Parallel sharding (4 jobs default)
|
||||
- Retry logic (2 retries on failure)
|
||||
- Failure-only artifact upload
|
||||
|
||||
### Performance Targets
|
||||
|
||||
- **Lint stage**: <2 minutes
|
||||
- **Test stage** (per shard): <10 minutes
|
||||
- **Burn-in stage**: <30 minutes (10 iterations)
|
||||
- **Total pipeline**: <45 minutes
|
||||
|
||||
**Speedup:** 20× faster than sequential execution through parallelism and caching.
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Knowledge Base Integration
|
||||
|
||||
**Critical:** Check configuration and load appropriate fragments.
|
||||
|
||||
Read `{config_source}` and check `config.tea_use_playwright_utils`.
|
||||
|
||||
**Core CI Patterns (Always load):**
|
||||
|
||||
- `ci-burn-in.md` - Burn-in loop patterns: 10-iteration detection, GitHub Actions workflow, shard orchestration, selective execution (678 lines, 4 examples)
|
||||
- `selective-testing.md` - Changed test detection strategies: tag-based, spec filters, diff-based selection, promotion rules (727 lines, 4 examples)
|
||||
- `visual-debugging.md` - Artifact collection best practices: trace viewer, HAR recording, custom artifacts, accessibility integration (522 lines, 5 examples)
|
||||
- `test-quality.md` - CI-specific test quality criteria: deterministic tests, isolated with cleanup, explicit assertions, length/time optimization (658 lines, 5 examples)
|
||||
- `playwright-config.md` - CI-optimized configuration: parallelization, artifact output, project dependencies, sharding (722 lines, 5 examples)
|
||||
|
||||
**If `config.tea_use_playwright_utils: true`:**
|
||||
|
||||
Load playwright-utils CI-relevant fragments:
|
||||
|
||||
- `burn-in.md` - Smart test selection with git diff analysis (very important for CI optimization)
|
||||
- `network-error-monitor.md` - Automatic HTTP 4xx/5xx detection (recommend in CI pipelines)
|
||||
|
||||
Recommend:
|
||||
|
||||
- Add burn-in script for pull request validation
|
||||
- Enable network-error-monitor in merged fixtures for catching silent failures
|
||||
- Reference full docs in `*framework` and `*automate` workflows
|
||||
|
||||
### CI Platform-Specific Guidance
|
||||
|
||||
**GitHub Actions:**
|
||||
|
||||
- Use `actions/cache` for caching
|
||||
- Matrix strategy for parallelism
|
||||
- Secrets in repository settings
|
||||
- Free 2000 minutes/month for private repos
|
||||
|
||||
**GitLab CI:**
|
||||
|
||||
- Use `.gitlab-ci.yml` in root
|
||||
- `cache:` directive for caching
|
||||
- Parallel execution with `parallel: 4`
|
||||
- Variables in project CI/CD settings
|
||||
|
||||
**Circle CI:**
|
||||
|
||||
- Use `.circleci/config.yml`
|
||||
- Docker executors recommended
|
||||
- Parallelism with `parallelism: 4`
|
||||
- Context for shared secrets
|
||||
|
||||
### Burn-In Loop Strategy
|
||||
|
||||
**When to run:**
|
||||
|
||||
- ✅ On PRs to main/develop branches
|
||||
- ✅ Weekly on schedule (cron)
|
||||
- ✅ After test infrastructure changes
|
||||
- ❌ Not on every commit (too slow)
|
||||
|
||||
**Iterations:**
|
||||
|
||||
- **10 iterations** for thorough detection
|
||||
- **3 iterations** for quick feedback
|
||||
- **100 iterations** for high-confidence stability
|
||||
|
||||
**Failure threshold:**
|
||||
|
||||
- Even ONE failure in burn-in → tests are flaky
|
||||
- Must fix before merging
|
||||
|
||||
### Artifact Retention
|
||||
|
||||
**Failure artifacts only:**
|
||||
|
||||
- Saves storage costs
|
||||
- Maintains debugging capability
|
||||
- 30-day retention default
|
||||
|
||||
**Artifact types:**
|
||||
|
||||
- Traces (Playwright) - 5-10 MB per test
|
||||
- Screenshots - 100-500 KB per screenshot
|
||||
- Videos - 2-5 MB per test
|
||||
- HTML reports - 1-2 MB per run
|
||||
|
||||
### Selective Testing
|
||||
|
||||
**Detect changed files:**
|
||||
|
||||
```bash
|
||||
git diff --name-only HEAD~1
|
||||
```
|
||||
|
||||
**Run affected tests only:**
|
||||
|
||||
- Faster feedback for small changes
|
||||
- Full suite still runs on main branch
|
||||
- Reduces CI time by 50-80% for focused PRs
|
||||
|
||||
**Trade-off:**
|
||||
|
||||
- May miss integration issues
|
||||
- Run full suite at least on merge
|
||||
|
||||
### Local CI Mirror
|
||||
|
||||
**Purpose:** Debug CI failures locally
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
./scripts/ci-local.sh
|
||||
```
|
||||
|
||||
**Mirrors CI environment:**
|
||||
|
||||
- Same Node version
|
||||
- Same test command
|
||||
- Same stages (lint → test → burn-in)
|
||||
- Reduced burn-in iterations (3 vs 10)
|
||||
|
||||
---
|
||||
|
||||
## Output Summary
|
||||
|
||||
After completing this workflow, provide a summary:
|
||||
|
||||
```markdown
|
||||
## CI/CD Pipeline Complete
|
||||
|
||||
**Platform**: GitHub Actions (or GitLab CI, etc.)
|
||||
|
||||
**Artifacts Created**:
|
||||
|
||||
- ✅ Pipeline configuration: .github/workflows/test.yml
|
||||
- ✅ Burn-in loop: 10 iterations for flaky detection
|
||||
- ✅ Parallel sharding: 4 jobs for fast execution
|
||||
- ✅ Caching: Dependencies + browser binaries
|
||||
- ✅ Artifact collection: Failure-only traces/screenshots/videos
|
||||
- ✅ Helper scripts: test-changed.sh, ci-local.sh, burn-in.sh
|
||||
- ✅ Documentation: docs/ci.md, docs/ci-secrets-checklist.md
|
||||
|
||||
**Performance:**
|
||||
|
||||
- Lint: <2 min
|
||||
- Test (per shard): <10 min
|
||||
- Burn-in: <30 min
|
||||
- Total: <45 min (20× speedup vs sequential)
|
||||
|
||||
**Next Steps**:
|
||||
|
||||
1. Commit CI configuration: `git add .github/workflows/test.yml && git commit -m "ci: add test pipeline"`
|
||||
2. Push to remote: `git push`
|
||||
3. Configure required secrets in CI platform settings (see docs/ci-secrets-checklist.md)
|
||||
4. Open a PR to trigger first CI run
|
||||
5. Monitor pipeline execution and adjust parallelism if needed
|
||||
|
||||
**Knowledge Base References Applied**:
|
||||
|
||||
- Burn-in loop pattern (ci-burn-in.md)
|
||||
- Selective testing strategy (selective-testing.md)
|
||||
- Artifact collection (visual-debugging.md)
|
||||
- Test quality criteria (test-quality.md)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
After completing all steps, verify:
|
||||
|
||||
- [ ] CI configuration file created and syntactically valid
|
||||
- [ ] Burn-in loop configured (10 iterations)
|
||||
- [ ] Parallel sharding enabled (4 jobs)
|
||||
- [ ] Caching configured (dependencies + browsers)
|
||||
- [ ] Artifact collection on failure only
|
||||
- [ ] Helper scripts created and executable (`chmod +x`)
|
||||
- [ ] Documentation complete (ci.md, secrets checklist)
|
||||
- [ ] No errors or warnings during scaffold
|
||||
|
||||
Refer to `checklist.md` for comprehensive validation criteria.
|
||||
@@ -1,47 +0,0 @@
|
||||
# Test Architect workflow: ci
|
||||
name: testarch-ci
|
||||
description: "Scaffold CI/CD quality pipeline with test execution, burn-in loops, and artifact collection"
|
||||
author: "BMad"
|
||||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
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: "{project-root}/_bmad/bmm/workflows/testarch/ci"
|
||||
instructions: "{installed_path}/instructions.md"
|
||||
validation: "{installed_path}/checklist.md"
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
ci_platform: "auto" # auto, github-actions, gitlab-ci, circle-ci, jenkins - user can override
|
||||
test_dir: "{project-root}/tests" # Root test directory
|
||||
|
||||
# Output configuration
|
||||
default_output_file: "{project-root}/.github/workflows/test.yml" # GitHub Actions default
|
||||
|
||||
# Required tools
|
||||
required_tools:
|
||||
- read_file # Read .nvmrc, package.json, framework config
|
||||
- write_file # Create CI config, scripts, documentation
|
||||
- create_directory # Create .github/workflows/ or .gitlab-ci/ directories
|
||||
- list_files # Detect existing CI configuration
|
||||
- search_repo # Find test files for selective testing
|
||||
|
||||
tags:
|
||||
- qa
|
||||
- ci-cd
|
||||
- test-architect
|
||||
- pipeline
|
||||
- automation
|
||||
|
||||
execution_hints:
|
||||
interactive: false # Minimize prompts, auto-detect when possible
|
||||
autonomous: true # Proceed without user input unless blocked
|
||||
iterative: true
|
||||
|
||||
web_bundle: false
|
||||
@@ -1,320 +0,0 @@
|
||||
# Test Framework Setup - Validation Checklist
|
||||
|
||||
This checklist ensures the framework workflow completes successfully and all deliverables meet quality standards.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting the workflow:
|
||||
|
||||
- [ ] Project root contains valid `package.json`
|
||||
- [ ] No existing modern E2E framework detected (`playwright.config.*`, `cypress.config.*`)
|
||||
- [ ] Project type identifiable (React, Vue, Angular, Next.js, Node, etc.)
|
||||
- [ ] Bundler identifiable (Vite, Webpack, Rollup, esbuild) or not applicable
|
||||
- [ ] User has write permissions to create directories and files
|
||||
|
||||
---
|
||||
|
||||
## Process Steps
|
||||
|
||||
### Step 1: Preflight Checks
|
||||
|
||||
- [ ] package.json successfully read and parsed
|
||||
- [ ] Project type extracted correctly
|
||||
- [ ] Bundler identified (or marked as N/A for backend projects)
|
||||
- [ ] No framework conflicts detected
|
||||
- [ ] Architecture documents located (if available)
|
||||
|
||||
### Step 2: Framework Selection
|
||||
|
||||
- [ ] Framework auto-detection logic executed
|
||||
- [ ] Framework choice justified (Playwright vs Cypress)
|
||||
- [ ] Framework preference respected (if explicitly set)
|
||||
- [ ] User notified of framework selection and rationale
|
||||
|
||||
### Step 3: Directory Structure
|
||||
|
||||
- [ ] `tests/` root directory created
|
||||
- [ ] `tests/e2e/` directory created (or user's preferred structure)
|
||||
- [ ] `tests/support/` directory created (critical pattern)
|
||||
- [ ] `tests/support/fixtures/` directory created
|
||||
- [ ] `tests/support/fixtures/factories/` directory created
|
||||
- [ ] `tests/support/helpers/` directory created
|
||||
- [ ] `tests/support/page-objects/` directory created (if applicable)
|
||||
- [ ] All directories have correct permissions
|
||||
|
||||
**Note**: Test organization is flexible (e2e/, api/, integration/). The **support/** folder is the key pattern.
|
||||
|
||||
### Step 4: Configuration Files
|
||||
|
||||
- [ ] Framework config file created (`playwright.config.ts` or `cypress.config.ts`)
|
||||
- [ ] Config file uses TypeScript (if `use_typescript: true`)
|
||||
- [ ] Timeouts configured correctly (action: 15s, navigation: 30s, test: 60s)
|
||||
- [ ] Base URL configured with environment variable fallback
|
||||
- [ ] Trace/screenshot/video set to retain-on-failure
|
||||
- [ ] Multiple reporters configured (HTML + JUnit + console)
|
||||
- [ ] Parallel execution enabled
|
||||
- [ ] CI-specific settings configured (retries, workers)
|
||||
- [ ] Config file is syntactically valid (no compilation errors)
|
||||
|
||||
### Step 5: Environment Configuration
|
||||
|
||||
- [ ] `.env.example` created in project root
|
||||
- [ ] `TEST_ENV` variable defined
|
||||
- [ ] `BASE_URL` variable defined with default
|
||||
- [ ] `API_URL` variable defined (if applicable)
|
||||
- [ ] Authentication variables defined (if applicable)
|
||||
- [ ] Feature flag variables defined (if applicable)
|
||||
- [ ] `.nvmrc` created with appropriate Node version
|
||||
|
||||
### Step 6: Fixture Architecture
|
||||
|
||||
- [ ] `tests/support/fixtures/index.ts` created
|
||||
- [ ] Base fixture extended from Playwright/Cypress
|
||||
- [ ] Type definitions for fixtures created
|
||||
- [ ] mergeTests pattern implemented (if multiple fixtures)
|
||||
- [ ] Auto-cleanup logic included in fixtures
|
||||
- [ ] Fixture architecture follows knowledge base patterns
|
||||
|
||||
### Step 7: Data Factories
|
||||
|
||||
- [ ] At least one factory created (e.g., UserFactory)
|
||||
- [ ] Factories use @faker-js/faker for realistic data
|
||||
- [ ] Factories track created entities (for cleanup)
|
||||
- [ ] Factories implement `cleanup()` method
|
||||
- [ ] Factories integrate with fixtures
|
||||
- [ ] Factories follow knowledge base patterns
|
||||
|
||||
### Step 8: Sample Tests
|
||||
|
||||
- [ ] Example test file created (`tests/e2e/example.spec.ts`)
|
||||
- [ ] Test uses fixture architecture
|
||||
- [ ] Test demonstrates data factory usage
|
||||
- [ ] Test uses proper selector strategy (data-testid)
|
||||
- [ ] Test follows Given-When-Then structure
|
||||
- [ ] Test includes proper assertions
|
||||
- [ ] Network interception demonstrated (if applicable)
|
||||
|
||||
### Step 9: Helper Utilities
|
||||
|
||||
- [ ] API helper created (if API testing needed)
|
||||
- [ ] Network helper created (if network mocking needed)
|
||||
- [ ] Auth helper created (if authentication needed)
|
||||
- [ ] Helpers follow functional patterns
|
||||
- [ ] Helpers have proper error handling
|
||||
|
||||
### Step 10: Documentation
|
||||
|
||||
- [ ] `tests/README.md` created
|
||||
- [ ] Setup instructions included
|
||||
- [ ] Running tests section included
|
||||
- [ ] Architecture overview section included
|
||||
- [ ] Best practices section included
|
||||
- [ ] CI integration section included
|
||||
- [ ] Knowledge base references included
|
||||
- [ ] Troubleshooting section included
|
||||
|
||||
### Step 11: Package.json Updates
|
||||
|
||||
- [ ] Minimal test script added to package.json: `test:e2e`
|
||||
- [ ] Test framework dependency added (if not already present)
|
||||
- [ ] Type definitions added (if TypeScript)
|
||||
- [ ] Users can extend with additional scripts as needed
|
||||
|
||||
---
|
||||
|
||||
## Output Validation
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
- [ ] Config file loads without errors
|
||||
- [ ] Config file passes linting (if linter configured)
|
||||
- [ ] Config file uses correct syntax for chosen framework
|
||||
- [ ] All paths in config resolve correctly
|
||||
- [ ] Reporter output directories exist or are created on test run
|
||||
|
||||
### Test Execution Validation
|
||||
|
||||
- [ ] Sample test runs successfully
|
||||
- [ ] Test execution produces expected output (pass/fail)
|
||||
- [ ] Test artifacts generated correctly (traces, screenshots, videos)
|
||||
- [ ] Test report generated successfully
|
||||
- [ ] No console errors or warnings during test run
|
||||
|
||||
### Directory Structure Validation
|
||||
|
||||
- [ ] All required directories exist
|
||||
- [ ] Directory structure matches framework conventions
|
||||
- [ ] No duplicate or conflicting directories
|
||||
- [ ] Directories accessible with correct permissions
|
||||
|
||||
### File Integrity Validation
|
||||
|
||||
- [ ] All generated files are syntactically correct
|
||||
- [ ] No placeholder text left in files (e.g., "TODO", "FIXME")
|
||||
- [ ] All imports resolve correctly
|
||||
- [ ] No hardcoded credentials or secrets in files
|
||||
- [ ] All file paths use correct separators for OS
|
||||
|
||||
---
|
||||
|
||||
## Quality Checks
|
||||
|
||||
### Code Quality
|
||||
|
||||
- [ ] Generated code follows project coding standards
|
||||
- [ ] TypeScript types are complete and accurate (no `any` unless necessary)
|
||||
- [ ] No unused imports or variables
|
||||
- [ ] Consistent code formatting (matches project style)
|
||||
- [ ] No linting errors in generated files
|
||||
|
||||
### Best Practices Compliance
|
||||
|
||||
- [ ] Fixture architecture follows pure function → fixture → mergeTests pattern
|
||||
- [ ] Data factories implement auto-cleanup
|
||||
- [ ] Network interception occurs before navigation
|
||||
- [ ] Selectors use data-testid strategy
|
||||
- [ ] Artifacts only captured on failure
|
||||
- [ ] Tests follow Given-When-Then structure
|
||||
- [ ] No hard-coded waits or sleeps
|
||||
|
||||
### Knowledge Base Alignment
|
||||
|
||||
- [ ] Fixture pattern matches `fixture-architecture.md`
|
||||
- [ ] Data factories match `data-factories.md`
|
||||
- [ ] Network handling matches `network-first.md`
|
||||
- [ ] Config follows `playwright-config.md` or `test-config.md`
|
||||
- [ ] Test quality matches `test-quality.md`
|
||||
|
||||
### Security Checks
|
||||
|
||||
- [ ] No credentials in configuration files
|
||||
- [ ] .env.example contains placeholders, not real values
|
||||
- [ ] Sensitive test data handled securely
|
||||
- [ ] API keys and tokens use environment variables
|
||||
- [ ] No secrets committed to version control
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Status File Integration
|
||||
|
||||
- [ ] Framework initialization logged in Quality & Testing Progress section
|
||||
- [ ] Status file updated with completion timestamp
|
||||
- [ ] Status file shows framework: Playwright or Cypress
|
||||
|
||||
### Knowledge Base Integration
|
||||
|
||||
- [ ] Relevant knowledge fragments identified from tea-index.csv
|
||||
- [ ] Knowledge fragments successfully loaded
|
||||
- [ ] Patterns from knowledge base applied correctly
|
||||
- [ ] Knowledge base references included in documentation
|
||||
|
||||
### Workflow Dependencies
|
||||
|
||||
- [ ] Can proceed to `ci` workflow after completion
|
||||
- [ ] Can proceed to `test-design` workflow after completion
|
||||
- [ ] Can proceed to `atdd` workflow after completion
|
||||
- [ ] Framework setup compatible with downstream workflows
|
||||
|
||||
---
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
**All of the following must be true:**
|
||||
|
||||
- [ ] All prerequisite checks passed
|
||||
- [ ] All process steps completed without errors
|
||||
- [ ] All output validations passed
|
||||
- [ ] All quality checks passed
|
||||
- [ ] All integration points verified
|
||||
- [ ] Sample test executes successfully
|
||||
- [ ] User can run `npm run test:e2e` without errors
|
||||
- [ ] Documentation is complete and accurate
|
||||
- [ ] No critical issues or blockers identified
|
||||
|
||||
---
|
||||
|
||||
## Post-Workflow Actions
|
||||
|
||||
**User must complete:**
|
||||
|
||||
1. [ ] Copy `.env.example` to `.env`
|
||||
2. [ ] Fill in environment-specific values in `.env`
|
||||
3. [ ] Run `npm install` to install test dependencies
|
||||
4. [ ] Run `npm run test:e2e` to verify setup
|
||||
5. [ ] Review `tests/README.md` for project-specific guidance
|
||||
|
||||
**Recommended next workflows:**
|
||||
|
||||
1. [ ] Run `ci` workflow to set up CI/CD pipeline
|
||||
2. [ ] Run `test-design` workflow to plan test coverage
|
||||
3. [ ] Run `atdd` workflow when ready to develop stories
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If workflow fails and needs to be rolled back:
|
||||
|
||||
1. [ ] Delete `tests/` directory
|
||||
2. [ ] Remove test scripts from package.json
|
||||
3. [ ] Delete `.env.example` (if created)
|
||||
4. [ ] Delete `.nvmrc` (if created)
|
||||
5. [ ] Delete framework config file
|
||||
6. [ ] Remove test dependencies from package.json (if added)
|
||||
7. [ ] Run `npm install` to clean up node_modules
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue**: Config file has TypeScript errors
|
||||
|
||||
- **Solution**: Ensure `@playwright/test` or `cypress` types are installed
|
||||
|
||||
**Issue**: Sample test fails to run
|
||||
|
||||
- **Solution**: Check BASE_URL in .env, ensure app is running
|
||||
|
||||
**Issue**: Fixture cleanup not working
|
||||
|
||||
- **Solution**: Verify cleanup() is called in fixture teardown
|
||||
|
||||
**Issue**: Network interception not working
|
||||
|
||||
- **Solution**: Ensure route setup occurs before page.goto()
|
||||
|
||||
### Framework-Specific Considerations
|
||||
|
||||
**Playwright:**
|
||||
|
||||
- Requires Node.js 18+
|
||||
- Browser binaries auto-installed on first run
|
||||
- Trace viewer requires running `npx playwright show-trace`
|
||||
|
||||
**Cypress:**
|
||||
|
||||
- Requires Node.js 18+
|
||||
- Cypress app opens on first run
|
||||
- Component testing requires additional setup
|
||||
|
||||
### Version Compatibility
|
||||
|
||||
- [ ] Node.js version matches .nvmrc
|
||||
- [ ] Framework version compatible with Node.js version
|
||||
- [ ] TypeScript version compatible with framework
|
||||
- [ ] All peer dependencies satisfied
|
||||
|
||||
---
|
||||
|
||||
**Checklist Complete**: Sign off when all items checked and validated.
|
||||
|
||||
**Completed by:** {name}
|
||||
**Date:** {date}
|
||||
**Framework:** { Playwright / Cypress or something else}
|
||||
**Notes:** {notes}
|
||||
@@ -1,481 +0,0 @@
|
||||
<!-- Powered by BMAD-CORE™ -->
|
||||
|
||||
# Test Framework Setup
|
||||
|
||||
**Workflow ID**: `_bmad/bmm/testarch/framework`
|
||||
**Version**: 4.0 (BMad v6)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Initialize a production-ready test framework architecture (Playwright or Cypress) with fixtures, helpers, configuration, and best practices. This workflow scaffolds the complete testing infrastructure for modern web applications.
|
||||
|
||||
---
|
||||
|
||||
## Preflight Requirements
|
||||
|
||||
**Critical:** Verify these requirements before proceeding. If any fail, HALT and notify the user.
|
||||
|
||||
- ✅ `package.json` exists in project root
|
||||
- ✅ No modern E2E test harness is already configured (check for existing `playwright.config.*` or `cypress.config.*`)
|
||||
- ✅ Architectural/stack context available (project type, bundler, dependencies)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Run Preflight Checks
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Validate package.json**
|
||||
- Read `{project-root}/package.json`
|
||||
- Extract project type (React, Vue, Angular, Next.js, Node, etc.)
|
||||
- Identify bundler (Vite, Webpack, Rollup, esbuild)
|
||||
- Note existing test dependencies
|
||||
|
||||
2. **Check for Existing Framework**
|
||||
- Search for `playwright.config.*`, `cypress.config.*`, `cypress.json`
|
||||
- Check `package.json` for `@playwright/test` or `cypress` dependencies
|
||||
- If found, HALT with message: "Existing test framework detected. Use workflow `upgrade-framework` instead."
|
||||
|
||||
3. **Gather Context**
|
||||
- Look for architecture documents (`architecture.md`, `tech-spec*.md`)
|
||||
- Check for API documentation or endpoint lists
|
||||
- Identify authentication requirements
|
||||
|
||||
**Halt Condition:** If preflight checks fail, stop immediately and report which requirement failed.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Scaffold Framework
|
||||
|
||||
### Actions
|
||||
|
||||
1. **Framework Selection**
|
||||
|
||||
**Default Logic:**
|
||||
- **Playwright** (recommended for):
|
||||
- Large repositories (100+ files)
|
||||
- Performance-critical applications
|
||||
- Multi-browser support needed
|
||||
- Complex user flows requiring video/trace debugging
|
||||
- Projects requiring worker parallelism
|
||||
|
||||
- **Cypress** (recommended for):
|
||||
- Small teams prioritizing developer experience
|
||||
- Component testing focus
|
||||
- Real-time reloading during test development
|
||||
- Simpler setup requirements
|
||||
|
||||
**Detection Strategy:**
|
||||
- Check `package.json` for existing preference
|
||||
- Consider `project_size` variable from workflow config
|
||||
- Use `framework_preference` variable if set
|
||||
- Default to **Playwright** if uncertain
|
||||
|
||||
2. **Create Directory Structure**
|
||||
|
||||
```
|
||||
{project-root}/
|
||||
├── tests/ # Root test directory
|
||||
│ ├── e2e/ # Test files (users organize as needed)
|
||||
│ ├── support/ # Framework infrastructure (key pattern)
|
||||
│ │ ├── fixtures/ # Test fixtures (data, mocks)
|
||||
│ │ ├── helpers/ # Utility functions
|
||||
│ │ └── page-objects/ # Page object models (optional)
|
||||
│ └── README.md # Test suite documentation
|
||||
```
|
||||
|
||||
**Note**: Users organize test files (e2e/, api/, integration/, component/) as needed. The **support/** folder is the critical pattern for fixtures and helpers used across tests.
|
||||
|
||||
3. **Generate Configuration File**
|
||||
|
||||
**For Playwright** (`playwright.config.ts` or `playwright.config.js`):
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
||||
timeout: 60 * 1000, // Test timeout: 60s
|
||||
expect: {
|
||||
timeout: 15 * 1000, // Assertion timeout: 15s
|
||||
},
|
||||
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
actionTimeout: 15 * 1000, // Action timeout: 15s
|
||||
navigationTimeout: 30 * 1000, // Navigation timeout: 30s
|
||||
},
|
||||
|
||||
reporter: [['html', { outputFolder: 'test-results/html' }], ['junit', { outputFile: 'test-results/junit.xml' }], ['list']],
|
||||
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
**For Cypress** (`cypress.config.ts` or `cypress.config.js`):
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
|
||||
specPattern: 'tests/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
||||
supportFile: 'tests/support/e2e.ts',
|
||||
video: false,
|
||||
screenshotOnRunFailure: true,
|
||||
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
|
||||
defaultCommandTimeout: 15000,
|
||||
requestTimeout: 30000,
|
||||
responseTimeout: 30000,
|
||||
pageLoadTimeout: 60000,
|
||||
});
|
||||
```
|
||||
|
||||
4. **Generate Environment Configuration**
|
||||
|
||||
Create `.env.example`:
|
||||
|
||||
```bash
|
||||
# Test Environment Configuration
|
||||
TEST_ENV=local
|
||||
BASE_URL=http://localhost:3000
|
||||
API_URL=http://localhost:3001/api
|
||||
|
||||
# Authentication (if applicable)
|
||||
TEST_USER_EMAIL=test@example.com
|
||||
TEST_USER_PASSWORD=
|
||||
|
||||
# Feature Flags (if applicable)
|
||||
FEATURE_FLAG_NEW_UI=true
|
||||
|
||||
# API Keys (if applicable)
|
||||
TEST_API_KEY=
|
||||
```
|
||||
|
||||
5. **Generate Node Version File**
|
||||
|
||||
Create `.nvmrc`:
|
||||
|
||||
```
|
||||
20.11.0
|
||||
```
|
||||
|
||||
(Use Node version from existing `.nvmrc` or default to current LTS)
|
||||
|
||||
6. **Implement Fixture Architecture**
|
||||
|
||||
**Knowledge Base Reference**: `testarch/knowledge/fixture-architecture.md`
|
||||
|
||||
Create `tests/support/fixtures/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { test as base } from '@playwright/test';
|
||||
import { UserFactory } from './factories/user-factory';
|
||||
|
||||
type TestFixtures = {
|
||||
userFactory: UserFactory;
|
||||
};
|
||||
|
||||
export const test = base.extend<TestFixtures>({
|
||||
userFactory: async ({}, use) => {
|
||||
const factory = new UserFactory();
|
||||
await use(factory);
|
||||
await factory.cleanup(); // Auto-cleanup
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
```
|
||||
|
||||
7. **Implement Data Factories**
|
||||
|
||||
**Knowledge Base Reference**: `testarch/knowledge/data-factories.md`
|
||||
|
||||
Create `tests/support/fixtures/factories/user-factory.ts`:
|
||||
|
||||
```typescript
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export class UserFactory {
|
||||
private createdUsers: string[] = [];
|
||||
|
||||
async createUser(overrides = {}) {
|
||||
const user = {
|
||||
email: faker.internet.email(),
|
||||
name: faker.person.fullName(),
|
||||
password: faker.internet.password({ length: 12 }),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
// API call to create user
|
||||
const response = await fetch(`${process.env.API_URL}/users`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
|
||||
const created = await response.json();
|
||||
this.createdUsers.push(created.id);
|
||||
return created;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
// Delete all created users
|
||||
for (const userId of this.createdUsers) {
|
||||
await fetch(`${process.env.API_URL}/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
this.createdUsers = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
8. **Generate Sample Tests**
|
||||
|
||||
Create `tests/e2e/example.spec.ts`:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../support/fixtures';
|
||||
|
||||
test.describe('Example Test Suite', () => {
|
||||
test('should load homepage', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/Home/i);
|
||||
});
|
||||
|
||||
test('should create user and login', async ({ page, userFactory }) => {
|
||||
// Create test user
|
||||
const user = await userFactory.createUser();
|
||||
|
||||
// Login
|
||||
await page.goto('/login');
|
||||
await page.fill('[data-testid="email-input"]', user.email);
|
||||
await page.fill('[data-testid="password-input"]', user.password);
|
||||
await page.click('[data-testid="login-button"]');
|
||||
|
||||
// Assert login success
|
||||
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
9. **Update package.json Scripts**
|
||||
|
||||
Add minimal test script to `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test:e2e": "playwright test"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Users can add additional scripts as needed (e.g., `--ui`, `--headed`, `--debug`, `show-report`).
|
||||
|
||||
10. **Generate Documentation**
|
||||
|
||||
Create `tests/README.md` with setup instructions (see Step 3 deliverables).
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Deliverables
|
||||
|
||||
### Primary Artifacts Created
|
||||
|
||||
1. **Configuration File**
|
||||
- `playwright.config.ts` or `cypress.config.ts`
|
||||
- Timeouts: action 15s, navigation 30s, test 60s
|
||||
- Reporters: HTML + JUnit XML
|
||||
|
||||
2. **Directory Structure**
|
||||
- `tests/` with `e2e/`, `api/`, `support/` subdirectories
|
||||
- `support/fixtures/` for test fixtures
|
||||
- `support/helpers/` for utility functions
|
||||
|
||||
3. **Environment Configuration**
|
||||
- `.env.example` with `TEST_ENV`, `BASE_URL`, `API_URL`
|
||||
- `.nvmrc` with Node version
|
||||
|
||||
4. **Test Infrastructure**
|
||||
- Fixture architecture (`mergeTests` pattern)
|
||||
- Data factories (faker-based, with auto-cleanup)
|
||||
- Sample tests demonstrating patterns
|
||||
|
||||
5. **Documentation**
|
||||
- `tests/README.md` with setup instructions
|
||||
- Comments in config files explaining options
|
||||
|
||||
### README Contents
|
||||
|
||||
The generated `tests/README.md` should include:
|
||||
|
||||
- **Setup Instructions**: How to install dependencies, configure environment
|
||||
- **Running Tests**: Commands for local execution, headed mode, debug mode
|
||||
- **Architecture Overview**: Fixture pattern, data factories, page objects
|
||||
- **Best Practices**: Selector strategy (data-testid), test isolation, cleanup
|
||||
- **CI Integration**: How tests run in CI/CD pipeline
|
||||
- **Knowledge Base References**: Links to relevant TEA knowledge fragments
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Knowledge Base Integration
|
||||
|
||||
**Critical:** Check configuration and load appropriate fragments.
|
||||
|
||||
Read `{config_source}` and check `config.tea_use_playwright_utils`.
|
||||
|
||||
**If `config.tea_use_playwright_utils: true` (Playwright Utils Integration):**
|
||||
|
||||
Consult `{project-root}/_bmad/bmm/testarch/tea-index.csv` and load:
|
||||
|
||||
- `overview.md` - Playwright utils installation and design principles
|
||||
- `fixtures-composition.md` - mergeTests composition with playwright-utils
|
||||
- `auth-session.md` - Token persistence setup (if auth needed)
|
||||
- `api-request.md` - API testing utilities (if API tests planned)
|
||||
- `burn-in.md` - Smart test selection for CI (recommend during framework setup)
|
||||
- `network-error-monitor.md` - Automatic HTTP error detection (recommend in merged fixtures)
|
||||
- `data-factories.md` - Factory patterns with faker (498 lines, 5 examples)
|
||||
|
||||
Recommend installing playwright-utils:
|
||||
|
||||
```bash
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
```
|
||||
|
||||
Recommend adding burn-in and network-error-monitor to merged fixtures for enhanced reliability.
|
||||
|
||||
**If `config.tea_use_playwright_utils: false` (Traditional Patterns):**
|
||||
|
||||
Consult `{project-root}/_bmad/bmm/testarch/tea-index.csv` and load:
|
||||
|
||||
- `fixture-architecture.md` - Pure function → fixture → `mergeTests` composition with auto-cleanup (406 lines, 5 examples)
|
||||
- `data-factories.md` - Faker-based factories with overrides, nested factories, API seeding, auto-cleanup (498 lines, 5 examples)
|
||||
- `network-first.md` - Network-first testing safeguards: intercept before navigate, HAR capture, deterministic waiting (489 lines, 5 examples)
|
||||
- `playwright-config.md` - Playwright-specific configuration: environment-based, timeout standards, artifact output, parallelization, project config (722 lines, 5 examples)
|
||||
- `test-quality.md` - Test design principles: deterministic, isolated with cleanup, explicit assertions, length/time limits (658 lines, 5 examples)
|
||||
|
||||
### Framework-Specific Guidance
|
||||
|
||||
**Playwright Advantages:**
|
||||
|
||||
- Worker parallelism (significantly faster for large suites)
|
||||
- Trace viewer (powerful debugging with screenshots, network, console)
|
||||
- Multi-language support (TypeScript, JavaScript, Python, C#, Java)
|
||||
- Built-in API testing capabilities
|
||||
- Better handling of multiple browser contexts
|
||||
|
||||
**Cypress Advantages:**
|
||||
|
||||
- Superior developer experience (real-time reloading)
|
||||
- Excellent for component testing (Cypress CT or use Vitest)
|
||||
- Simpler setup for small teams
|
||||
- Better suited for watch mode during development
|
||||
|
||||
**Avoid Cypress when:**
|
||||
|
||||
- API chains are heavy and complex
|
||||
- Multi-tab/window scenarios are common
|
||||
- Worker parallelism is critical for CI performance
|
||||
|
||||
### Selector Strategy
|
||||
|
||||
**Always recommend**:
|
||||
|
||||
- `data-testid` attributes for UI elements
|
||||
- `data-cy` attributes if Cypress is chosen
|
||||
- Avoid brittle CSS selectors or XPath
|
||||
|
||||
### Contract Testing
|
||||
|
||||
For microservices architectures, **recommend Pact** for consumer-driven contract testing alongside E2E tests.
|
||||
|
||||
### Failure Artifacts
|
||||
|
||||
Configure **failure-only** capture:
|
||||
|
||||
- Screenshots: only on failure
|
||||
- Videos: retain on failure (delete on success)
|
||||
- Traces: retain on failure (Playwright)
|
||||
|
||||
This reduces storage overhead while maintaining debugging capability.
|
||||
|
||||
---
|
||||
|
||||
## Output Summary
|
||||
|
||||
After completing this workflow, provide a summary:
|
||||
|
||||
```markdown
|
||||
## Framework Scaffold Complete
|
||||
|
||||
**Framework Selected**: Playwright (or Cypress)
|
||||
|
||||
**Artifacts Created**:
|
||||
|
||||
- ✅ Configuration file: `playwright.config.ts`
|
||||
- ✅ Directory structure: `tests/e2e/`, `tests/support/`
|
||||
- ✅ Environment config: `.env.example`
|
||||
- ✅ Node version: `.nvmrc`
|
||||
- ✅ Fixture architecture: `tests/support/fixtures/`
|
||||
- ✅ Data factories: `tests/support/fixtures/factories/`
|
||||
- ✅ Sample tests: `tests/e2e/example.spec.ts`
|
||||
- ✅ Documentation: `tests/README.md`
|
||||
|
||||
**Next Steps**:
|
||||
|
||||
1. Copy `.env.example` to `.env` and fill in environment variables
|
||||
2. Run `npm install` to install test dependencies
|
||||
3. Run `npm run test:e2e` to execute sample tests
|
||||
4. Review `tests/README.md` for detailed setup instructions
|
||||
|
||||
**Knowledge Base References Applied**:
|
||||
|
||||
- Fixture architecture pattern (pure functions + mergeTests)
|
||||
- Data factories with auto-cleanup (faker-based)
|
||||
- Network-first testing safeguards
|
||||
- Failure-only artifact capture
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
After completing all steps, verify:
|
||||
|
||||
- [ ] Configuration file created and valid
|
||||
- [ ] Directory structure exists
|
||||
- [ ] Environment configuration generated
|
||||
- [ ] Sample tests run successfully
|
||||
- [ ] Documentation complete and accurate
|
||||
- [ ] No errors or warnings during scaffold
|
||||
|
||||
Refer to `checklist.md` for comprehensive validation criteria.
|
||||
@@ -1,49 +0,0 @@
|
||||
# Test Architect workflow: framework
|
||||
name: testarch-framework
|
||||
description: "Initialize production-ready test framework architecture (Playwright or Cypress) with fixtures, helpers, and configuration"
|
||||
author: "BMad"
|
||||
|
||||
# Critical variables from config
|
||||
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||
output_folder: "{config_source}:output_folder"
|
||||
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: "{project-root}/_bmad/bmm/workflows/testarch/framework"
|
||||
instructions: "{installed_path}/instructions.md"
|
||||
validation: "{installed_path}/checklist.md"
|
||||
|
||||
# Variables and inputs
|
||||
variables:
|
||||
test_dir: "{project-root}/tests" # Root test directory
|
||||
use_typescript: true # Prefer TypeScript configuration
|
||||
framework_preference: "auto" # auto, playwright, cypress - user can override auto-detection
|
||||
project_size: "auto" # auto, small, large - influences framework recommendation
|
||||
|
||||
# Output configuration
|
||||
default_output_file: "{test_dir}/README.md" # Main deliverable is test setup README
|
||||
|
||||
# Required tools
|
||||
required_tools:
|
||||
- read_file # Read package.json, existing configs
|
||||
- write_file # Create config files, helpers, fixtures, tests
|
||||
- create_directory # Create test directory structure
|
||||
- list_files # Check for existing framework
|
||||
- search_repo # Find architecture docs
|
||||
|
||||
tags:
|
||||
- qa
|
||||
- setup
|
||||
- test-architect
|
||||
- framework
|
||||
- initialization
|
||||
|
||||
execution_hints:
|
||||
interactive: false # Minimize prompts; auto-detect when possible
|
||||
autonomous: true # Proceed without user input unless blocked
|
||||
iterative: true
|
||||
|
||||
web_bundle: false
|
||||
@@ -1,407 +0,0 @@
|
||||
# Non-Functional Requirements Assessment - Validation Checklist
|
||||
|
||||
**Workflow:** `testarch-nfr`
|
||||
**Purpose:** Ensure comprehensive and evidence-based NFR assessment with actionable recommendations
|
||||
|
||||
---
|
||||
|
||||
Note: `nfr-assess` evaluates existing evidence; it does not run tests or CI workflows.
|
||||
|
||||
## Prerequisites Validation
|
||||
|
||||
- [ ] Implementation is deployed and accessible for evaluation
|
||||
- [ ] Evidence sources are available (test results, metrics, logs, CI results)
|
||||
- [ ] NFR categories are determined (performance, security, reliability, maintainability, custom)
|
||||
- [ ] Evidence directories exist and are accessible (`test_results_dir`, `metrics_dir`, `logs_dir`)
|
||||
- [ ] Knowledge base is loaded (nfr-criteria, ci-burn-in, test-quality)
|
||||
|
||||
---
|
||||
|
||||
## Context Loading
|
||||
|
||||
- [ ] Tech-spec.md loaded successfully (if available)
|
||||
- [ ] PRD.md loaded (if available)
|
||||
- [ ] Story file loaded (if applicable)
|
||||
- [ ] Relevant knowledge fragments loaded from `tea-index.csv`:
|
||||
- [ ] `nfr-criteria.md`
|
||||
- [ ] `ci-burn-in.md`
|
||||
- [ ] `test-quality.md`
|
||||
- [ ] `playwright-config.md` (if using Playwright)
|
||||
|
||||
---
|
||||
|
||||
## NFR Categories and Thresholds
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] Response time threshold defined or marked as UNKNOWN
|
||||
- [ ] Throughput threshold defined or marked as UNKNOWN
|
||||
- [ ] Resource usage thresholds defined or marked as UNKNOWN
|
||||
- [ ] Scalability requirements defined or marked as UNKNOWN
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Authentication requirements defined or marked as UNKNOWN
|
||||
- [ ] Authorization requirements defined or marked as UNKNOWN
|
||||
- [ ] Data protection requirements defined or marked as UNKNOWN
|
||||
- [ ] Vulnerability management thresholds defined or marked as UNKNOWN
|
||||
- [ ] Compliance requirements identified (GDPR, HIPAA, PCI-DSS, etc.)
|
||||
|
||||
### Reliability
|
||||
|
||||
- [ ] Availability (uptime) threshold defined or marked as UNKNOWN
|
||||
- [ ] Error rate threshold defined or marked as UNKNOWN
|
||||
- [ ] MTTR (Mean Time To Recovery) threshold defined or marked as UNKNOWN
|
||||
- [ ] Fault tolerance requirements defined or marked as UNKNOWN
|
||||
- [ ] Disaster recovery requirements defined (RTO, RPO) or marked as UNKNOWN
|
||||
|
||||
### Maintainability
|
||||
|
||||
- [ ] Test coverage threshold defined or marked as UNKNOWN
|
||||
- [ ] Code quality threshold defined or marked as UNKNOWN
|
||||
- [ ] Technical debt threshold defined or marked as UNKNOWN
|
||||
- [ ] Documentation completeness threshold defined or marked as UNKNOWN
|
||||
|
||||
### Custom NFR Categories (if applicable)
|
||||
|
||||
- [ ] Custom NFR category 1: Thresholds defined or marked as UNKNOWN
|
||||
- [ ] Custom NFR category 2: Thresholds defined or marked as UNKNOWN
|
||||
- [ ] Custom NFR category 3: Thresholds defined or marked as UNKNOWN
|
||||
|
||||
---
|
||||
|
||||
## Evidence Gathering
|
||||
|
||||
### Performance Evidence
|
||||
|
||||
- [ ] Load test results collected (JMeter, k6, Gatling, etc.)
|
||||
- [ ] Application metrics collected (response times, throughput, resource usage)
|
||||
- [ ] APM data collected (New Relic, Datadog, Dynatrace, etc.)
|
||||
- [ ] Lighthouse reports collected (if web app)
|
||||
- [ ] Playwright performance traces collected (if applicable)
|
||||
|
||||
### Security Evidence
|
||||
|
||||
- [ ] SAST results collected (SonarQube, Checkmarx, Veracode, etc.)
|
||||
- [ ] DAST results collected (OWASP ZAP, Burp Suite, etc.)
|
||||
- [ ] Dependency scanning results collected (Snyk, Dependabot, npm audit)
|
||||
- [ ] Penetration test reports collected (if available)
|
||||
- [ ] Security audit logs collected
|
||||
- [ ] Compliance audit results collected (if applicable)
|
||||
|
||||
### Reliability Evidence
|
||||
|
||||
- [ ] Uptime monitoring data collected (Pingdom, UptimeRobot, StatusCake)
|
||||
- [ ] Error logs collected
|
||||
- [ ] Error rate metrics collected
|
||||
- [ ] CI burn-in results collected (stability over time)
|
||||
- [ ] Chaos engineering test results collected (if available)
|
||||
- [ ] Failover/recovery test results collected (if available)
|
||||
- [ ] Incident reports and postmortems collected (if applicable)
|
||||
|
||||
### Maintainability Evidence
|
||||
|
||||
- [ ] Code coverage reports collected (Istanbul, NYC, c8, JaCoCo)
|
||||
- [ ] Static analysis results collected (ESLint, SonarQube, CodeClimate)
|
||||
- [ ] Technical debt metrics collected
|
||||
- [ ] Documentation audit results collected
|
||||
- [ ] Test review report collected (from test-review workflow, if available)
|
||||
- [ ] Git metrics collected (code churn, commit frequency, etc.)
|
||||
|
||||
---
|
||||
|
||||
## NFR Assessment with Deterministic Rules
|
||||
|
||||
### Performance Assessment
|
||||
|
||||
- [ ] Response time assessed against threshold
|
||||
- [ ] Throughput assessed against threshold
|
||||
- [ ] Resource usage assessed against threshold
|
||||
- [ ] Scalability assessed against requirements
|
||||
- [ ] Status classified (PASS/CONCERNS/FAIL) with justification
|
||||
- [ ] Evidence source documented (file path, metric name)
|
||||
|
||||
### Security Assessment
|
||||
|
||||
- [ ] Authentication strength assessed against requirements
|
||||
- [ ] Authorization controls assessed against requirements
|
||||
- [ ] Data protection assessed against requirements
|
||||
- [ ] Vulnerability management assessed against thresholds
|
||||
- [ ] Compliance assessed against requirements
|
||||
- [ ] Status classified (PASS/CONCERNS/FAIL) with justification
|
||||
- [ ] Evidence source documented (file path, scan result)
|
||||
|
||||
### Reliability Assessment
|
||||
|
||||
- [ ] Availability (uptime) assessed against threshold
|
||||
- [ ] Error rate assessed against threshold
|
||||
- [ ] MTTR assessed against threshold
|
||||
- [ ] Fault tolerance assessed against requirements
|
||||
- [ ] Disaster recovery assessed against requirements (RTO, RPO)
|
||||
- [ ] CI burn-in assessed (stability over time)
|
||||
- [ ] Status classified (PASS/CONCERNS/FAIL) with justification
|
||||
- [ ] Evidence source documented (file path, monitoring data)
|
||||
|
||||
### Maintainability Assessment
|
||||
|
||||
- [ ] Test coverage assessed against threshold
|
||||
- [ ] Code quality assessed against threshold
|
||||
- [ ] Technical debt assessed against threshold
|
||||
- [ ] Documentation completeness assessed against threshold
|
||||
- [ ] Test quality assessed (from test-review, if available)
|
||||
- [ ] Status classified (PASS/CONCERNS/FAIL) with justification
|
||||
- [ ] Evidence source documented (file path, coverage report)
|
||||
|
||||
### Custom NFR Assessment (if applicable)
|
||||
|
||||
- [ ] Custom NFR 1 assessed against threshold with justification
|
||||
- [ ] Custom NFR 2 assessed against threshold with justification
|
||||
- [ ] Custom NFR 3 assessed against threshold with justification
|
||||
|
||||
---
|
||||
|
||||
## Status Classification Validation
|
||||
|
||||
### PASS Criteria Verified
|
||||
|
||||
- [ ] Evidence exists for PASS status
|
||||
- [ ] Evidence meets or exceeds threshold
|
||||
- [ ] No concerns flagged in evidence
|
||||
- [ ] Quality is acceptable
|
||||
|
||||
### CONCERNS Criteria Verified
|
||||
|
||||
- [ ] Threshold is UNKNOWN (documented) OR
|
||||
- [ ] Evidence is MISSING or INCOMPLETE (documented) OR
|
||||
- [ ] Evidence is close to threshold (within 10%, documented) OR
|
||||
- [ ] Evidence shows intermittent issues (documented)
|
||||
|
||||
### FAIL Criteria Verified
|
||||
|
||||
- [ ] Evidence exists BUT does not meet threshold (documented) OR
|
||||
- [ ] Critical evidence is MISSING (documented) OR
|
||||
- [ ] Evidence shows consistent failures (documented) OR
|
||||
- [ ] Quality is unacceptable (documented)
|
||||
|
||||
### No Threshold Guessing
|
||||
|
||||
- [ ] All thresholds are either defined or marked as UNKNOWN
|
||||
- [ ] No thresholds were guessed or inferred
|
||||
- [ ] All UNKNOWN thresholds result in CONCERNS status
|
||||
|
||||
---
|
||||
|
||||
## Quick Wins and Recommended Actions
|
||||
|
||||
### Quick Wins Identified
|
||||
|
||||
- [ ] Low-effort, high-impact improvements identified for CONCERNS/FAIL
|
||||
- [ ] Configuration changes (no code changes) identified
|
||||
- [ ] Optimization opportunities identified (caching, indexing, compression)
|
||||
- [ ] Monitoring additions identified (detect issues before failures)
|
||||
|
||||
### Recommended Actions
|
||||
|
||||
- [ ] Specific remediation steps provided (not generic advice)
|
||||
- [ ] Priority assigned (CRITICAL, HIGH, MEDIUM, LOW)
|
||||
- [ ] Estimated effort provided (hours, days)
|
||||
- [ ] Owner suggestions provided (dev, ops, security)
|
||||
|
||||
### Monitoring Hooks
|
||||
|
||||
- [ ] Performance monitoring suggested (APM, synthetic monitoring)
|
||||
- [ ] Error tracking suggested (Sentry, Rollbar, error logs)
|
||||
- [ ] Security monitoring suggested (intrusion detection, audit logs)
|
||||
- [ ] Alerting thresholds suggested (notify before breach)
|
||||
|
||||
### Fail-Fast Mechanisms
|
||||
|
||||
- [ ] Circuit breakers suggested for reliability
|
||||
- [ ] Rate limiting suggested for performance
|
||||
- [ ] Validation gates suggested for security
|
||||
- [ ] Smoke tests suggested for maintainability
|
||||
|
||||
---
|
||||
|
||||
## Deliverables Generated
|
||||
|
||||
### NFR Assessment Report
|
||||
|
||||
- [ ] File created at `{output_folder}/nfr-assessment.md`
|
||||
- [ ] Template from `nfr-report-template.md` used
|
||||
- [ ] Executive summary included (overall status, critical issues)
|
||||
- [ ] Assessment by category included (performance, security, reliability, maintainability)
|
||||
- [ ] Evidence for each NFR documented
|
||||
- [ ] Status classifications documented (PASS/CONCERNS/FAIL)
|
||||
- [ ] Findings summary included (PASS count, CONCERNS count, FAIL count)
|
||||
- [ ] Quick wins section included
|
||||
- [ ] Recommended actions section included
|
||||
- [ ] Evidence gaps checklist included
|
||||
|
||||
### Gate YAML Snippet (if enabled)
|
||||
|
||||
- [ ] YAML snippet generated
|
||||
- [ ] Date included
|
||||
- [ ] Categories status included (performance, security, reliability, maintainability)
|
||||
- [ ] Overall status included (PASS/CONCERNS/FAIL)
|
||||
- [ ] Issue counts included (critical, high, medium, concerns)
|
||||
- [ ] Blockers flag included (true/false)
|
||||
- [ ] Recommendations included
|
||||
|
||||
### Evidence Checklist (if enabled)
|
||||
|
||||
- [ ] All NFRs with MISSING or INCOMPLETE evidence listed
|
||||
- [ ] Owners assigned for evidence collection
|
||||
- [ ] Suggested evidence sources provided
|
||||
- [ ] Deadlines set for evidence collection
|
||||
|
||||
### Updated Story File (if enabled and requested)
|
||||
|
||||
- [ ] "NFR Assessment" section added to story markdown
|
||||
- [ ] Link to NFR assessment report included
|
||||
- [ ] Overall status and critical issues included
|
||||
- [ ] Gate status included
|
||||
|
||||
---
|
||||
|
||||
## Quality Assurance
|
||||
|
||||
### Accuracy Checks
|
||||
|
||||
- [ ] All NFR categories assessed (none skipped)
|
||||
- [ ] All thresholds documented (defined or UNKNOWN)
|
||||
- [ ] All evidence sources documented (file paths, metric names)
|
||||
- [ ] Status classifications are deterministic and consistent
|
||||
- [ ] No false positives (status correctly assigned)
|
||||
- [ ] No false negatives (all issues identified)
|
||||
|
||||
### Completeness Checks
|
||||
|
||||
- [ ] All NFR categories covered (performance, security, reliability, maintainability, custom)
|
||||
- [ ] All evidence sources checked (test results, metrics, logs, CI results)
|
||||
- [ ] All status types used appropriately (PASS, CONCERNS, FAIL)
|
||||
- [ ] All NFRs with CONCERNS/FAIL have recommendations
|
||||
- [ ] All evidence gaps have owners and deadlines
|
||||
|
||||
### Actionability Checks
|
||||
|
||||
- [ ] Recommendations are specific (not generic)
|
||||
- [ ] Remediation steps are clear and actionable
|
||||
- [ ] Priorities are assigned (CRITICAL, HIGH, MEDIUM, LOW)
|
||||
- [ ] Effort estimates are provided (hours, days)
|
||||
- [ ] Owners are suggested (dev, ops, security)
|
||||
|
||||
---
|
||||
|
||||
## Integration with BMad Artifacts
|
||||
|
||||
### With tech-spec.md
|
||||
|
||||
- [ ] Tech spec loaded for NFR requirements and thresholds
|
||||
- [ ] Performance targets extracted
|
||||
- [ ] Security requirements extracted
|
||||
- [ ] Reliability SLAs extracted
|
||||
- [ ] Architectural decisions considered
|
||||
|
||||
### With test-design.md
|
||||
|
||||
- [ ] Test design loaded for NFR test plan
|
||||
- [ ] Test priorities referenced (P0/P1/P2/P3)
|
||||
- [ ] Assessment aligned with planned NFR validation
|
||||
|
||||
### With PRD.md
|
||||
|
||||
- [ ] PRD loaded for product-level NFR context
|
||||
- [ ] User experience goals considered
|
||||
- [ ] Unstated requirements checked
|
||||
- [ ] Product-level SLAs referenced
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates Validation
|
||||
|
||||
### Release Blocker (FAIL)
|
||||
|
||||
- [ ] Critical NFR status checked (security, reliability)
|
||||
- [ ] Performance failures assessed for user impact
|
||||
- [ ] Release blocker flagged if critical NFR has FAIL status
|
||||
|
||||
### PR Blocker (HIGH CONCERNS)
|
||||
|
||||
- [ ] High-priority NFR status checked
|
||||
- [ ] Multiple CONCERNS assessed
|
||||
- [ ] PR blocker flagged if HIGH priority issues exist
|
||||
|
||||
### Warning (CONCERNS)
|
||||
|
||||
- [ ] Any NFR with CONCERNS status flagged
|
||||
- [ ] Missing or incomplete evidence documented
|
||||
- [ ] Warning issued to address before next release
|
||||
|
||||
### Pass (PASS)
|
||||
|
||||
- [ ] All NFRs have PASS status
|
||||
- [ ] No blockers or concerns exist
|
||||
- [ ] Ready for release confirmed
|
||||
|
||||
---
|
||||
|
||||
## Non-Prescriptive Validation
|
||||
|
||||
- [ ] NFR categories adapted to team needs
|
||||
- [ ] Thresholds appropriate for project context
|
||||
- [ ] Assessment criteria customized as needed
|
||||
- [ ] Teams can extend with custom NFR categories
|
||||
- [ ] Integration with external tools supported (New Relic, Datadog, SonarQube, JIRA)
|
||||
|
||||
---
|
||||
|
||||
## Documentation and Communication
|
||||
|
||||
- [ ] NFR assessment report is readable and well-formatted
|
||||
- [ ] Tables render correctly in markdown
|
||||
- [ ] Code blocks have proper syntax highlighting
|
||||
- [ ] Links are valid and accessible
|
||||
- [ ] Recommendations are clear and prioritized
|
||||
- [ ] Overall status is prominent and unambiguous
|
||||
- [ ] Executive summary provides quick understanding
|
||||
|
||||
---
|
||||
|
||||
## Final Validation
|
||||
|
||||
- [ ] All prerequisites met
|
||||
- [ ] All NFR categories assessed with evidence (or gaps documented)
|
||||
- [ ] No thresholds were guessed (all defined or UNKNOWN)
|
||||
- [ ] Status classifications are deterministic and justified
|
||||
- [ ] Quick wins identified for all CONCERNS/FAIL
|
||||
- [ ] Recommended actions are specific and actionable
|
||||
- [ ] Evidence gaps documented with owners and deadlines
|
||||
- [ ] NFR assessment report generated and saved
|
||||
- [ ] Gate YAML snippet generated (if enabled)
|
||||
- [ ] Evidence checklist generated (if enabled)
|
||||
- [ ] Workflow completed successfully
|
||||
|
||||
---
|
||||
|
||||
## Sign-Off
|
||||
|
||||
**NFR Assessment Status:**
|
||||
|
||||
- [ ] ✅ PASS - All NFRs meet requirements, ready for release
|
||||
- [ ] ⚠️ CONCERNS - Some NFRs have concerns, address before next release
|
||||
- [ ] ❌ FAIL - Critical NFRs not met, BLOCKER for release
|
||||
|
||||
**Next Actions:**
|
||||
|
||||
- If PASS ✅: Proceed to `*gate` workflow or release
|
||||
- If CONCERNS ⚠️: Address HIGH/CRITICAL issues, re-run `*nfr-assess`
|
||||
- If FAIL ❌: Resolve FAIL status NFRs, re-run `*nfr-assess`
|
||||
|
||||
**Critical Issues:** {COUNT}
|
||||
**High Priority Issues:** {COUNT}
|
||||
**Concerns:** {COUNT}
|
||||
|
||||
---
|
||||
|
||||
<!-- Powered by BMAD-CORE™ -->
|
||||
@@ -1,726 +0,0 @@
|
||||
# Non-Functional Requirements Assessment - Instructions v4.0
|
||||
|
||||
**Workflow:** `testarch-nfr`
|
||||
**Purpose:** Assess non-functional requirements (performance, security, reliability, maintainability) before release with evidence-based validation
|
||||
**Agent:** Test Architect (TEA)
|
||||
**Format:** Pure Markdown v4.0 (no XML blocks)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow performs a comprehensive assessment of non-functional requirements (NFRs) to validate that the implementation meets performance, security, reliability, and maintainability standards before release. It uses evidence-based validation with deterministic PASS/CONCERNS/FAIL rules and provides actionable recommendations for remediation.
|
||||
|
||||
**Key Capabilities:**
|
||||
|
||||
- Assess multiple NFR categories (performance, security, reliability, maintainability, custom)
|
||||
- Validate NFRs against defined thresholds from tech specs, PRD, or defaults
|
||||
- Classify status deterministically (PASS/CONCERNS/FAIL) based on evidence
|
||||
- Never guess thresholds - mark as CONCERNS if unknown
|
||||
- Generate gate-ready YAML snippets for CI/CD integration
|
||||
- Provide quick wins and recommended actions for remediation
|
||||
- Create evidence checklists for gaps
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Required:**
|
||||
|
||||
- Implementation deployed locally or accessible for evaluation
|
||||
- Evidence sources available (test results, metrics, logs, CI results)
|
||||
|
||||
**Recommended:**
|
||||
|
||||
- NFR requirements defined in tech-spec.md, PRD.md, or story
|
||||
- Test results from performance, security, reliability tests
|
||||
- Application metrics (response times, error rates, throughput)
|
||||
- CI/CD pipeline results for burn-in validation
|
||||
|
||||
**Halt Conditions:**
|
||||
|
||||
- If NFR targets are undefined and cannot be obtained, halt and request definition
|
||||
- If implementation is not accessible for evaluation, halt and request deployment
|
||||
|
||||
---
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### Step 1: Load Context and Knowledge Base
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Load relevant knowledge fragments from `{project-root}/_bmad/bmm/testarch/tea-index.csv`:
|
||||
- `adr-quality-readiness-checklist.md` - 8-category 29-criteria NFR framework (testability, test data, scalability, DR, security, monitorability, QoS/QoE, deployability, ~450 lines)
|
||||
- `ci-burn-in.md` - CI/CD burn-in patterns for reliability validation (10-iteration detection, sharding, selective execution, 678 lines, 4 examples)
|
||||
- `test-quality.md` - Test quality expectations for maintainability (deterministic, isolated, explicit assertions, length/time limits, 658 lines, 5 examples)
|
||||
- `playwright-config.md` - Performance configuration patterns: parallelization, timeout standards, artifact output (722 lines, 5 examples)
|
||||
- `error-handling.md` - Reliability validation patterns: scoped exceptions, retry validation, telemetry logging, graceful degradation (736 lines, 4 examples)
|
||||
|
||||
2. Read story file (if provided):
|
||||
- Extract NFR requirements
|
||||
- Identify specific thresholds or SLAs
|
||||
- Note any custom NFR categories
|
||||
|
||||
3. Read related BMad artifacts (if available):
|
||||
- `tech-spec.md` - Technical NFR requirements and targets
|
||||
- `PRD.md` - Product-level NFR context (user expectations)
|
||||
- `test-design.md` - NFR test plan and priorities
|
||||
|
||||
**Output:** Complete understanding of NFR targets, evidence sources, and validation criteria
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Identify NFR Categories and Thresholds
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Determine which NFR categories to assess using ADR Quality Readiness Checklist (8 standard categories):
|
||||
- **1. Testability & Automation**: Isolation, headless interaction, state control, sample requests (4 criteria)
|
||||
- **2. Test Data Strategy**: Segregation, generation, teardown (3 criteria)
|
||||
- **3. Scalability & Availability**: Statelessness, bottlenecks, SLA definitions, circuit breakers (4 criteria)
|
||||
- **4. Disaster Recovery**: RTO/RPO, failover, backups (3 criteria)
|
||||
- **5. Security**: AuthN/AuthZ, encryption, secrets, input validation (4 criteria)
|
||||
- **6. Monitorability, Debuggability & Manageability**: Tracing, logs, metrics, config (4 criteria)
|
||||
- **7. QoS & QoE**: Latency, throttling, perceived performance, degradation (4 criteria)
|
||||
- **8. Deployability**: Zero downtime, backward compatibility, rollback (3 criteria)
|
||||
|
||||
2. Add custom NFR categories if specified (e.g., accessibility, internationalization, compliance) beyond the 8 standard categories
|
||||
|
||||
3. Gather thresholds for each NFR:
|
||||
- From tech-spec.md (primary source)
|
||||
- From PRD.md (product-level SLAs)
|
||||
- From story file (feature-specific requirements)
|
||||
- From workflow variables (default thresholds)
|
||||
- Mark thresholds as UNKNOWN if not defined
|
||||
|
||||
4. Never guess thresholds - if a threshold is unknown, mark the NFR as CONCERNS
|
||||
|
||||
**Output:** Complete list of NFRs to assess with defined (or UNKNOWN) thresholds
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Gather Evidence
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. For each NFR category, discover evidence sources:
|
||||
|
||||
**Performance Evidence:**
|
||||
- Load test results (JMeter, k6, Lighthouse)
|
||||
- Application metrics (response times, throughput, resource usage)
|
||||
- Performance monitoring data (New Relic, Datadog, APM)
|
||||
- Playwright performance traces (if applicable)
|
||||
|
||||
**Security Evidence:**
|
||||
- Security scan results (SAST, DAST, dependency scanning)
|
||||
- Authentication/authorization test results
|
||||
- Penetration test reports
|
||||
- Vulnerability assessment reports
|
||||
- Compliance audit results
|
||||
|
||||
**Reliability Evidence:**
|
||||
- Error logs and error rates
|
||||
- Uptime monitoring data
|
||||
- Chaos engineering test results
|
||||
- Failover/recovery test results
|
||||
- CI burn-in results (stability over time)
|
||||
|
||||
**Maintainability Evidence:**
|
||||
- Code coverage reports (Istanbul, NYC, c8)
|
||||
- Static analysis results (ESLint, SonarQube)
|
||||
- Technical debt metrics
|
||||
- Documentation completeness
|
||||
- Test quality assessment (from test-review workflow)
|
||||
|
||||
2. Read relevant files from evidence directories:
|
||||
- `{test_results_dir}` for test execution results
|
||||
- `{metrics_dir}` for application metrics
|
||||
- `{logs_dir}` for application logs
|
||||
- CI/CD pipeline results (if `include_ci_results` is true)
|
||||
|
||||
3. Mark NFRs without evidence as "NO EVIDENCE" - never infer or assume
|
||||
|
||||
**Output:** Comprehensive evidence inventory for each NFR
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Assess NFRs with Deterministic Rules
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. For each NFR, apply deterministic PASS/CONCERNS/FAIL rules:
|
||||
|
||||
**PASS Criteria:**
|
||||
- Evidence exists AND meets defined threshold
|
||||
- No concerns flagged in evidence
|
||||
- Example: Response time is 350ms (threshold: 500ms) → PASS
|
||||
|
||||
**CONCERNS Criteria:**
|
||||
- Threshold is UNKNOWN (not defined)
|
||||
- Evidence is MISSING or INCOMPLETE
|
||||
- Evidence is close to threshold (within 10%)
|
||||
- Evidence shows intermittent issues
|
||||
- Example: Response time is 480ms (threshold: 500ms, 96% of threshold) → CONCERNS
|
||||
|
||||
**FAIL Criteria:**
|
||||
- Evidence exists BUT does not meet threshold
|
||||
- Critical evidence is MISSING
|
||||
- Evidence shows consistent failures
|
||||
- Example: Response time is 750ms (threshold: 500ms) → FAIL
|
||||
|
||||
2. Document findings for each NFR:
|
||||
- Status (PASS/CONCERNS/FAIL)
|
||||
- Evidence source (file path, test name, metric name)
|
||||
- Actual value vs threshold
|
||||
- Justification for status classification
|
||||
|
||||
3. Classify severity based on category:
|
||||
- **CRITICAL**: Security failures, reliability failures (affect users immediately)
|
||||
- **HIGH**: Performance failures, maintainability failures (affect users soon)
|
||||
- **MEDIUM**: Concerns without failures (may affect users eventually)
|
||||
- **LOW**: Missing evidence for non-critical NFRs
|
||||
|
||||
**Output:** Complete NFR assessment with deterministic status classifications
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Identify Quick Wins and Recommended Actions
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. For each NFR with CONCERNS or FAIL status, identify quick wins:
|
||||
- Low-effort, high-impact improvements
|
||||
- Configuration changes (no code changes needed)
|
||||
- Optimization opportunities (caching, indexing, compression)
|
||||
- Monitoring additions (detect issues before they become failures)
|
||||
|
||||
2. Provide recommended actions for each issue:
|
||||
- Specific steps to remediate (not generic advice)
|
||||
- Priority (CRITICAL, HIGH, MEDIUM, LOW)
|
||||
- Estimated effort (hours, days)
|
||||
- Owner suggestion (dev, ops, security)
|
||||
|
||||
3. Suggest monitoring hooks for gaps:
|
||||
- Add performance monitoring (APM, synthetic monitoring)
|
||||
- Add error tracking (Sentry, Rollbar, error logs)
|
||||
- Add security monitoring (intrusion detection, audit logs)
|
||||
- Add alerting thresholds (notify before thresholds are breached)
|
||||
|
||||
4. Suggest fail-fast mechanisms:
|
||||
- Add circuit breakers for reliability
|
||||
- Add rate limiting for performance
|
||||
- Add validation gates for security
|
||||
- Add smoke tests for maintainability
|
||||
|
||||
**Output:** Actionable remediation plan with prioritized recommendations
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Generate Deliverables
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Create NFR assessment markdown file:
|
||||
- Use template from `nfr-report-template.md`
|
||||
- Include executive summary (overall status, critical issues)
|
||||
- Add NFR-by-NFR assessment (status, evidence, thresholds)
|
||||
- Add findings summary (PASS count, CONCERNS count, FAIL count)
|
||||
- Add quick wins section
|
||||
- Add recommended actions section
|
||||
- Add evidence gaps checklist
|
||||
- Save to `{output_folder}/nfr-assessment.md`
|
||||
|
||||
2. Generate gate YAML snippet (if enabled):
|
||||
|
||||
```yaml
|
||||
nfr_assessment:
|
||||
date: '2025-10-14'
|
||||
categories:
|
||||
performance: 'PASS'
|
||||
security: 'CONCERNS'
|
||||
reliability: 'PASS'
|
||||
maintainability: 'PASS'
|
||||
overall_status: 'CONCERNS'
|
||||
critical_issues: 0
|
||||
high_priority_issues: 1
|
||||
concerns: 2
|
||||
blockers: false
|
||||
```
|
||||
|
||||
3. Generate evidence checklist (if enabled):
|
||||
- List all NFRs with MISSING or INCOMPLETE evidence
|
||||
- Assign owners for evidence collection
|
||||
- Suggest evidence sources (tests, metrics, logs)
|
||||
- Set deadlines for evidence collection
|
||||
|
||||
4. Update story file (if enabled and requested):
|
||||
- Add "NFR Assessment" section to story markdown
|
||||
- Link to NFR assessment report
|
||||
- Include overall status and critical issues
|
||||
- Add gate status
|
||||
|
||||
**Output:** Complete NFR assessment documentation ready for review and CI/CD integration
|
||||
|
||||
---
|
||||
|
||||
## Non-Prescriptive Approach
|
||||
|
||||
**Minimal Examples:** This workflow provides principles and patterns, not rigid templates. Teams should adapt NFR categories, thresholds, and assessment criteria to their needs.
|
||||
|
||||
**Key Patterns to Follow:**
|
||||
|
||||
- Use evidence-based validation (no guessing or inference)
|
||||
- Apply deterministic rules (consistent PASS/CONCERNS/FAIL classification)
|
||||
- Never guess thresholds (mark as CONCERNS if unknown)
|
||||
- Provide actionable recommendations (specific steps, not generic advice)
|
||||
- Generate gate-ready artifacts (YAML snippets for CI/CD)
|
||||
|
||||
**Extend as Needed:**
|
||||
|
||||
- Add custom NFR categories (accessibility, internationalization, compliance)
|
||||
- Integrate with external tools (New Relic, Datadog, SonarQube, JIRA)
|
||||
- Add custom thresholds and rules
|
||||
- Link to external assessment systems
|
||||
|
||||
---
|
||||
|
||||
## NFR Categories and Criteria
|
||||
|
||||
### Performance
|
||||
|
||||
**Criteria:**
|
||||
|
||||
- Response time (p50, p95, p99 percentiles)
|
||||
- Throughput (requests per second, transactions per second)
|
||||
- Resource usage (CPU, memory, disk, network)
|
||||
- Scalability (horizontal, vertical)
|
||||
|
||||
**Thresholds (Default):**
|
||||
|
||||
- Response time p95: 500ms
|
||||
- Throughput: 100 RPS
|
||||
- CPU usage: < 70% average
|
||||
- Memory usage: < 80% max
|
||||
|
||||
**Evidence Sources:**
|
||||
|
||||
- Load test results (JMeter, k6, Gatling)
|
||||
- APM data (New Relic, Datadog, Dynatrace)
|
||||
- Lighthouse reports (for web apps)
|
||||
- Playwright performance traces
|
||||
|
||||
---
|
||||
|
||||
### Security
|
||||
|
||||
**Criteria:**
|
||||
|
||||
- Authentication (login security, session management)
|
||||
- Authorization (access control, permissions)
|
||||
- Data protection (encryption, PII handling)
|
||||
- Vulnerability management (SAST, DAST, dependency scanning)
|
||||
- Compliance (GDPR, HIPAA, PCI-DSS)
|
||||
|
||||
**Thresholds (Default):**
|
||||
|
||||
- Security score: >= 85/100
|
||||
- Critical vulnerabilities: 0
|
||||
- High vulnerabilities: < 3
|
||||
- Authentication strength: MFA enabled
|
||||
|
||||
**Evidence Sources:**
|
||||
|
||||
- SAST results (SonarQube, Checkmarx, Veracode)
|
||||
- DAST results (OWASP ZAP, Burp Suite)
|
||||
- Dependency scanning (Snyk, Dependabot, npm audit)
|
||||
- Penetration test reports
|
||||
- Security audit logs
|
||||
|
||||
---
|
||||
|
||||
### Reliability
|
||||
|
||||
**Criteria:**
|
||||
|
||||
- Availability (uptime percentage)
|
||||
- Error handling (graceful degradation, error recovery)
|
||||
- Fault tolerance (redundancy, failover)
|
||||
- Disaster recovery (backup, restore, RTO/RPO)
|
||||
- Stability (CI burn-in, chaos engineering)
|
||||
|
||||
**Thresholds (Default):**
|
||||
|
||||
- Uptime: >= 99.9% (three nines)
|
||||
- Error rate: < 0.1% (1 in 1000 requests)
|
||||
- MTTR (Mean Time To Recovery): < 15 minutes
|
||||
- CI burn-in: 100 consecutive successful runs
|
||||
|
||||
**Evidence Sources:**
|
||||
|
||||
- Uptime monitoring (Pingdom, UptimeRobot, StatusCake)
|
||||
- Error logs and error rates
|
||||
- CI burn-in results (see `ci-burn-in.md`)
|
||||
- Chaos engineering test results (Chaos Monkey, Gremlin)
|
||||
- Incident reports and postmortems
|
||||
|
||||
---
|
||||
|
||||
### Maintainability
|
||||
|
||||
**Criteria:**
|
||||
|
||||
- Code quality (complexity, duplication, code smells)
|
||||
- Test coverage (unit, integration, E2E)
|
||||
- Documentation (code comments, README, architecture docs)
|
||||
- Technical debt (debt ratio, code churn)
|
||||
- Test quality (from test-review workflow)
|
||||
|
||||
**Thresholds (Default):**
|
||||
|
||||
- Test coverage: >= 80%
|
||||
- Code quality score: >= 85/100
|
||||
- Technical debt ratio: < 5%
|
||||
- Documentation completeness: >= 90%
|
||||
|
||||
**Evidence Sources:**
|
||||
|
||||
- Coverage reports (Istanbul, NYC, c8, JaCoCo)
|
||||
- Static analysis (ESLint, SonarQube, CodeClimate)
|
||||
- Documentation audit (manual or automated)
|
||||
- Test review report (from test-review workflow)
|
||||
- Git metrics (code churn, commit frequency)
|
||||
|
||||
---
|
||||
|
||||
## Deterministic Assessment Rules
|
||||
|
||||
### PASS Rules
|
||||
|
||||
- Evidence exists
|
||||
- Evidence meets or exceeds threshold
|
||||
- No concerns flagged
|
||||
- Quality is acceptable
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||
NFR: Response Time p95
|
||||
Threshold: 500ms
|
||||
Evidence: Load test result shows 350ms p95
|
||||
Status: PASS ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CONCERNS Rules
|
||||
|
||||
- Threshold is UNKNOWN
|
||||
- Evidence is MISSING or INCOMPLETE
|
||||
- Evidence is close to threshold (within 10%)
|
||||
- Evidence shows intermittent issues
|
||||
- Quality is marginal
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||
NFR: Response Time p95
|
||||
Threshold: 500ms
|
||||
Evidence: Load test result shows 480ms p95 (96% of threshold)
|
||||
Status: CONCERNS ⚠️
|
||||
Recommendation: Optimize before production - very close to threshold
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### FAIL Rules
|
||||
|
||||
- Evidence exists BUT does not meet threshold
|
||||
- Critical evidence is MISSING
|
||||
- Evidence shows consistent failures
|
||||
- Quality is unacceptable
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||
NFR: Response Time p95
|
||||
Threshold: 500ms
|
||||
Evidence: Load test result shows 750ms p95 (150% of threshold)
|
||||
Status: FAIL ❌
|
||||
Recommendation: BLOCKER - optimize performance before release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with BMad Artifacts
|
||||
|
||||
### With tech-spec.md
|
||||
|
||||
- Primary source for NFR requirements and thresholds
|
||||
- Load performance targets, security requirements, reliability SLAs
|
||||
- Use architectural decisions to understand NFR trade-offs
|
||||
|
||||
### With test-design.md
|
||||
|
||||
- Understand NFR test plan and priorities
|
||||
- Reference test priorities (P0/P1/P2/P3) for severity classification
|
||||
- Align assessment with planned NFR validation
|
||||
|
||||
### With PRD.md
|
||||
|
||||
- Understand product-level NFR expectations
|
||||
- Verify NFRs align with user experience goals
|
||||
- Check for unstated NFR requirements (implied by product goals)
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates
|
||||
|
||||
### Release Blocker (FAIL)
|
||||
|
||||
- Critical NFR has FAIL status (security, reliability)
|
||||
- Performance failure affects user experience severely
|
||||
- Do not release until FAIL is resolved
|
||||
|
||||
### PR Blocker (HIGH CONCERNS)
|
||||
|
||||
- High-priority NFR has FAIL status
|
||||
- Multiple CONCERNS exist
|
||||
- Block PR merge until addressed
|
||||
|
||||
### Warning (CONCERNS)
|
||||
|
||||
- Any NFR has CONCERNS status
|
||||
- Evidence is missing or incomplete
|
||||
- Address before next release
|
||||
|
||||
### Pass (PASS)
|
||||
|
||||
- All NFRs have PASS status
|
||||
- No blockers or concerns
|
||||
- Ready for release
|
||||
|
||||
---
|
||||
|
||||
## Example NFR Assessment
|
||||
|
||||
````markdown
|
||||
# NFR Assessment - Story 1.3
|
||||
|
||||
**Feature:** User Authentication
|
||||
**Date:** 2025-10-14
|
||||
**Overall Status:** CONCERNS ⚠️ (1 HIGH issue)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Assessment:** 3 PASS, 1 CONCERNS, 0 FAIL
|
||||
**Blockers:** None
|
||||
**High Priority Issues:** 1 (Security - MFA not enforced)
|
||||
**Recommendation:** Address security concern before release
|
||||
|
||||
## Performance Assessment
|
||||
|
||||
### Response Time (p95)
|
||||
|
||||
- **Status:** PASS ✅
|
||||
- **Threshold:** 500ms
|
||||
- **Actual:** 320ms (64% of threshold)
|
||||
- **Evidence:** Load test results (test-results/load-2025-10-14.json)
|
||||
- **Findings:** Response time well below threshold across all percentiles
|
||||
|
||||
### Throughput
|
||||
|
||||
- **Status:** PASS ✅
|
||||
- **Threshold:** 100 RPS
|
||||
- **Actual:** 250 RPS (250% of threshold)
|
||||
- **Evidence:** Load test results (test-results/load-2025-10-14.json)
|
||||
- **Findings:** System handles 2.5x target load without degradation
|
||||
|
||||
## Security Assessment
|
||||
|
||||
### Authentication Strength
|
||||
|
||||
- **Status:** CONCERNS ⚠️
|
||||
- **Threshold:** MFA enabled for all users
|
||||
- **Actual:** MFA optional (not enforced)
|
||||
- **Evidence:** Security audit (security-audit-2025-10-14.md)
|
||||
- **Findings:** MFA is implemented but not enforced by default
|
||||
- **Recommendation:** HIGH - Enforce MFA for all new accounts, provide migration path for existing users
|
||||
|
||||
### Data Protection
|
||||
|
||||
- **Status:** PASS ✅
|
||||
- **Threshold:** PII encrypted at rest and in transit
|
||||
- **Actual:** AES-256 at rest, TLS 1.3 in transit
|
||||
- **Evidence:** Security scan (security-scan-2025-10-14.json)
|
||||
- **Findings:** All PII properly encrypted
|
||||
|
||||
## Reliability Assessment
|
||||
|
||||
### Uptime
|
||||
|
||||
- **Status:** PASS ✅
|
||||
- **Threshold:** 99.9% (three nines)
|
||||
- **Actual:** 99.95% over 30 days
|
||||
- **Evidence:** Uptime monitoring (uptime-report-2025-10-14.csv)
|
||||
- **Findings:** Exceeds target with margin
|
||||
|
||||
### Error Rate
|
||||
|
||||
- **Status:** PASS ✅
|
||||
- **Threshold:** < 0.1% (1 in 1000)
|
||||
- **Actual:** 0.05% (1 in 2000)
|
||||
- **Evidence:** Error logs (logs/errors-2025-10.log)
|
||||
- **Findings:** Error rate well below threshold
|
||||
|
||||
## Maintainability Assessment
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- **Status:** PASS ✅
|
||||
- **Threshold:** >= 80%
|
||||
- **Actual:** 87%
|
||||
- **Evidence:** Coverage report (coverage/lcov-report/index.html)
|
||||
- **Findings:** Coverage exceeds threshold with good distribution
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Status:** PASS ✅
|
||||
- **Threshold:** >= 85/100
|
||||
- **Actual:** 92/100
|
||||
- **Evidence:** SonarQube analysis (sonarqube-report-2025-10-14.pdf)
|
||||
- **Findings:** High code quality score with low technical debt
|
||||
|
||||
## Quick Wins
|
||||
|
||||
1. **Enforce MFA (Security)** - HIGH - 4 hours
|
||||
- Add configuration flag to enforce MFA for new accounts
|
||||
- No code changes needed, only config adjustment
|
||||
|
||||
## Recommended Actions
|
||||
|
||||
### Immediate (Before Release)
|
||||
|
||||
1. **Enforce MFA for all new accounts** - HIGH - 4 hours - Security Team
|
||||
- Add `ENFORCE_MFA=true` to production config
|
||||
- Update user onboarding flow to require MFA setup
|
||||
- Test MFA enforcement in staging environment
|
||||
|
||||
### Short-term (Next Sprint)
|
||||
|
||||
1. **Migrate existing users to MFA** - MEDIUM - 3 days - Product + Engineering
|
||||
- Design migration UX (prompt, incentives, deadline)
|
||||
- Implement migration flow with grace period
|
||||
- Communicate migration to existing users
|
||||
|
||||
## Evidence Gaps
|
||||
|
||||
- [ ] Chaos engineering test results (reliability)
|
||||
- Owner: DevOps Team
|
||||
- Deadline: 2025-10-21
|
||||
- Suggested evidence: Run chaos monkey tests in staging
|
||||
|
||||
- [ ] Penetration test report (security)
|
||||
- Owner: Security Team
|
||||
- Deadline: 2025-10-28
|
||||
- Suggested evidence: Schedule third-party pentest
|
||||
|
||||
## Gate YAML Snippet
|
||||
|
||||
```yaml
|
||||
nfr_assessment:
|
||||
date: '2025-10-14'
|
||||
story_id: '1.3'
|
||||
categories:
|
||||
performance: 'PASS'
|
||||
security: 'CONCERNS'
|
||||
reliability: 'PASS'
|
||||
maintainability: 'PASS'
|
||||
overall_status: 'CONCERNS'
|
||||
critical_issues: 0
|
||||
high_priority_issues: 1
|
||||
medium_priority_issues: 0
|
||||
concerns: 1
|
||||
blockers: false
|
||||
recommendations:
|
||||
- 'Enforce MFA for all new accounts (HIGH - 4 hours)'
|
||||
evidence_gaps: 2
|
||||
```
|
||||
````
|
||||
|
||||
## Recommendations Summary
|
||||
|
||||
- **Release Blocker:** None ✅
|
||||
- **High Priority:** 1 (Enforce MFA before release)
|
||||
- **Medium Priority:** 1 (Migrate existing users to MFA)
|
||||
- **Next Steps:** Address HIGH priority item, then proceed to gate workflow
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before completing this workflow, verify:
|
||||
|
||||
- ✅ All NFR categories assessed (performance, security, reliability, maintainability, custom)
|
||||
- ✅ Thresholds defined or marked as UNKNOWN
|
||||
- ✅ Evidence gathered for each NFR (or marked as MISSING)
|
||||
- ✅ Status classified deterministically (PASS/CONCERNS/FAIL)
|
||||
- ✅ No thresholds were guessed (marked as CONCERNS if unknown)
|
||||
- ✅ Quick wins identified for CONCERNS/FAIL
|
||||
- ✅ Recommended actions are specific and actionable
|
||||
- ✅ Evidence gaps documented with owners and deadlines
|
||||
- ✅ NFR assessment report generated and saved
|
||||
- ✅ Gate YAML snippet generated (if enabled)
|
||||
- ✅ Evidence checklist generated (if enabled)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Never Guess Thresholds:** If a threshold is unknown, mark as CONCERNS and recommend defining it
|
||||
- **Evidence-Based:** Every assessment must be backed by evidence (tests, metrics, logs, CI results)
|
||||
- **Deterministic Rules:** Use consistent PASS/CONCERNS/FAIL classification based on evidence
|
||||
- **Actionable Recommendations:** Provide specific steps, not generic advice
|
||||
- **Gate Integration:** Generate YAML snippets that can be consumed by CI/CD pipelines
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "NFR thresholds not defined"
|
||||
- Check tech-spec.md for NFR requirements
|
||||
- Check PRD.md for product-level SLAs
|
||||
- Check story file for feature-specific requirements
|
||||
- If thresholds truly unknown, mark as CONCERNS and recommend defining them
|
||||
|
||||
### "No evidence found"
|
||||
- Check evidence directories (test-results, metrics, logs)
|
||||
- Check CI/CD pipeline for test results
|
||||
- If evidence truly missing, mark NFR as "NO EVIDENCE" and recommend generating it
|
||||
|
||||
### "CONCERNS status but no threshold exceeded"
|
||||
- CONCERNS is correct when threshold is UNKNOWN or evidence is MISSING/INCOMPLETE
|
||||
- CONCERNS is also correct when evidence is close to threshold (within 10%)
|
||||
- Document why CONCERNS was assigned
|
||||
|
||||
### "FAIL status blocks release"
|
||||
- This is intentional - FAIL means critical NFR not met
|
||||
- Recommend remediation actions with specific steps
|
||||
- Re-run assessment after remediation
|
||||
|
||||
---
|
||||
|
||||
## Related Workflows
|
||||
|
||||
- **testarch-test-design** - Define NFR requirements and test plan
|
||||
- **testarch-framework** - Set up performance/security testing frameworks
|
||||
- **testarch-ci** - Configure CI/CD for NFR validation
|
||||
- **testarch-gate** - Use NFR assessment as input for quality gate decisions
|
||||
- **testarch-test-review** - Review test quality (maintainability NFR)
|
||||
|
||||
---
|
||||
|
||||
<!-- Powered by BMAD-CORE™ -->
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user