Compare commits
1 Commits
d2c8095d0b
...
cd13dff8cb
| Author | SHA1 | Date |
|---|---|---|
|
|
cd13dff8cb |
85
SECURITY.md
85
SECURITY.md
|
|
@ -1,85 +0,0 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We release security patches for the following versions:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| Latest | :white_check_mark: |
|
||||
| < Latest | :x: |
|
||||
|
||||
We recommend always using the latest version of BMad Method to ensure you have the most recent security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly.
|
||||
|
||||
### How to Report
|
||||
|
||||
**Do NOT report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them via one of these methods:
|
||||
|
||||
1. **GitHub Security Advisories** (Preferred): Use [GitHub's private vulnerability reporting](https://github.com/bmad-code-org/BMAD-METHOD/security/advisories/new) to submit a confidential report.
|
||||
|
||||
2. **Discord**: Contact a maintainer directly via DM on our [Discord server](https://discord.gg/gk8jAdXWmj).
|
||||
|
||||
### What to Include
|
||||
|
||||
Please include as much of the following information as possible:
|
||||
|
||||
- Type of vulnerability (e.g., prompt injection, path traversal, etc.)
|
||||
- Full paths of source file(s) related to the vulnerability
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Proof-of-concept or exploit code (if available)
|
||||
- Impact assessment of the vulnerability
|
||||
|
||||
### Response Timeline
|
||||
|
||||
- **Initial Response**: Within 48 hours of receiving your report
|
||||
- **Status Update**: Within 7 days with our assessment
|
||||
- **Resolution Target**: Critical issues within 30 days; other issues within 90 days
|
||||
|
||||
### What to Expect
|
||||
|
||||
1. We will acknowledge receipt of your report
|
||||
2. We will investigate and validate the vulnerability
|
||||
3. We will work on a fix and coordinate disclosure timing with you
|
||||
4. We will credit you in the security advisory (unless you prefer to remain anonymous)
|
||||
|
||||
## Security Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Vulnerabilities in BMad Method core framework code
|
||||
- Security issues in agent definitions or workflows that could lead to unintended behavior
|
||||
- Path traversal or file system access issues
|
||||
- Prompt injection vulnerabilities that bypass intended agent behavior
|
||||
- Supply chain vulnerabilities in dependencies
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Security issues in user-created custom agents or modules
|
||||
- Vulnerabilities in third-party AI providers (Claude, GPT, etc.)
|
||||
- Issues that require physical access to a user's machine
|
||||
- Social engineering attacks
|
||||
- Denial of service attacks that don't exploit a specific vulnerability
|
||||
|
||||
## Security Best Practices for Users
|
||||
|
||||
When using BMad Method:
|
||||
|
||||
1. **Review Agent Outputs**: Always review AI-generated code before executing it
|
||||
2. **Limit File Access**: Configure your AI IDE to limit file system access where possible
|
||||
3. **Keep Updated**: Regularly update to the latest version
|
||||
4. **Validate Dependencies**: Review any dependencies added by generated code
|
||||
5. **Environment Isolation**: Consider running AI-assisted development in isolated environments
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We appreciate the security research community's efforts in helping keep BMad Method secure. Contributors who report valid security issues will be acknowledged in our security advisories.
|
||||
|
||||
---
|
||||
|
||||
Thank you for helping keep BMad Method and our community safe.
|
||||
|
|
@ -60,8 +60,8 @@ If you are unsure, default to the integrated path for your track and adjust late
|
|||
| `*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) |
|
||||
| `*atdd` | Failing acceptance tests + implementation checklist | TDD red phase + optional recording mode | **+ Recording**: AI generation verified with live browser (accurate selectors from real DOM) |
|
||||
| `*automate` | Prioritized specs, fixtures, README/script updates, DoD summary | Optional healing/recording, avoid duplicate coverage | **+ Healing**: Pattern fixes enhanced with visual debugging + **+ Recording**: AI verified with live browser |
|
||||
| `*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 | - |
|
||||
|
|
@ -308,7 +308,7 @@ Want to understand TEA principles and patterns in depth?
|
|||
- [Engagement Models](/docs/explanation/tea/engagement-models.md) - TEA Lite, TEA Solo, TEA Integrated (5 models explained)
|
||||
|
||||
**Philosophy:**
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Start here to understand WHY TEA exists** - The problem with AI-generated tests and TEA's three-part solution
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - Why TEA exists, problem statement
|
||||
|
||||
## Optional Integrations
|
||||
|
||||
|
|
|
|||
|
|
@ -594,7 +594,7 @@ Client project 3 (Ad-hoc):
|
|||
**When:** Adopt BMad Method, want full integration.
|
||||
|
||||
**Steps:**
|
||||
1. Install BMad Method (see installation guide)
|
||||
1. Install BMad Method (`npx bmad-method@alpha install`)
|
||||
2. Run planning workflows (PRD, architecture)
|
||||
3. Integrate TEA into Phase 3 (system-level test design)
|
||||
4. Follow integrated lifecycle (per epic workflows)
|
||||
|
|
@ -690,7 +690,7 @@ Each model uses different TEA workflows. See:
|
|||
|
||||
**Use-Case Guides:**
|
||||
- [Using TEA with Existing Tests](/docs/how-to/brownfield/use-tea-with-existing-tests.md) - Model 5: Brownfield
|
||||
- [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise integration
|
||||
- [Running TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise integration
|
||||
|
||||
**All Workflow Guides:**
|
||||
- [How to Run Test Design](/docs/how-to/workflows/run-test-design.md) - Used in TEA Solo and Integrated
|
||||
|
|
|
|||
|
|
@ -220,8 +220,8 @@ test('should update profile', async ({ apiRequest, authToken, log }) => {
|
|||
// Use API request fixture (matches pure function signature)
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
url: '/api/profile',
|
||||
data: { name: 'New Name' },
|
||||
url: '/api/profile', // Pure function uses 'url' (not 'path')
|
||||
data: { name: 'New Name' }, // Pure function uses 'data' (not 'body')
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -484,31 +484,22 @@ await page.waitForSelector('.success', { timeout: 30000 });
|
|||
|
||||
All developers:
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { test } from '@seontechnologies/playwright-utils/recurse/fixtures';
|
||||
|
||||
test('job completion', async ({ apiRequest, recurse }) => {
|
||||
// Start async job
|
||||
const { body: job } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/jobs'
|
||||
test('job completion', async ({ page, recurse }) => {
|
||||
await page.click('button');
|
||||
|
||||
const result = await recurse({
|
||||
fn: () => apiRequest({ method: 'GET', path: '/api/job/123' }),
|
||||
predicate: (job) => job.status === 'complete',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// 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');
|
||||
expect(result.status).toBe('complete');
|
||||
});
|
||||
```
|
||||
|
||||
**Result:** Consistent pattern using correct playwright-utils API (command, predicate, options).
|
||||
**Result:** Consistent pattern, established best practice.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
|
|
@ -529,7 +520,7 @@ For details on the knowledge base index, see:
|
|||
|
||||
**Overview:**
|
||||
- [TEA Overview](/docs/explanation/features/tea-overview.md) - Knowledge base in workflows
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Foundation: Context engineering philosophy** (why knowledge base solves AI test problems)
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - Context engineering philosophy
|
||||
|
||||
## Practical Guides
|
||||
|
||||
|
|
|
|||
|
|
@ -125,40 +125,6 @@ test('should load dashboard data', async ({ page }) => {
|
|||
- 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.
|
||||
|
|
@ -230,7 +196,6 @@ sequenceDiagram
|
|||
|
||||
### TEA Generates Network-First Tests
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// When you run *atdd or *automate, TEA generates:
|
||||
|
||||
|
|
@ -254,37 +219,6 @@ test('should create user', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
**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`:
|
||||
|
|
@ -318,7 +252,6 @@ await responsePromise; // ✅
|
|||
|
||||
### Basic Response Wait
|
||||
|
||||
**Vanilla Playwright:**
|
||||
```typescript
|
||||
// Wait for any successful response
|
||||
const promise = page.waitForResponse(resp => resp.ok());
|
||||
|
|
@ -326,23 +259,8 @@ 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(
|
||||
|
|
@ -352,21 +270,8 @@ 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(
|
||||
|
|
@ -379,24 +284,8 @@ 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([
|
||||
|
|
@ -409,29 +298,8 @@ 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(
|
||||
|
|
@ -451,28 +319,6 @@ expect(order.total).toBeGreaterThan(0);
|
|||
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
|
||||
|
|
@ -635,36 +481,6 @@ test('dashboard loads data', async ({ page }) => {
|
|||
- 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):**
|
||||
|
|
@ -697,35 +513,6 @@ test('form submission', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
**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"
|
||||
|
|
@ -758,57 +545,29 @@ await page.waitForSelector('.success'); // Then validate UI
|
|||
|
||||
### "Too Much Boilerplate"
|
||||
|
||||
**Problem:** `waitForResponse` is verbose, repeated in every test.
|
||||
**Solution:** Extract to fixtures (see Fixture Architecture)
|
||||
|
||||
**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;
|
||||
// Create reusable fixture
|
||||
export const test = base.extend({
|
||||
waitForApi: async ({ page }, use) => {
|
||||
await use((urlPattern: string) => {
|
||||
// Returns promise immediately (doesn't await)
|
||||
return page.waitForResponse(
|
||||
resp => resp.url().includes(urlPattern) && resp.ok()
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
// Use in tests
|
||||
test('test', async ({ page, waitForApi }) => {
|
||||
const promise = waitForApi('/api/submit'); // Get promise
|
||||
await page.click('button'); // Trigger action
|
||||
await promise; // Wait for response
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Less boilerplate (fixture handles complexity)
|
||||
- Automatic JSON parsing
|
||||
- Glob pattern matching (`**/api/**`)
|
||||
- Consistent API across all tests
|
||||
|
||||
See [Integrate Playwright Utils](/docs/how-to/customization/integrate-playwright-utils.md#intercept-network-call) for setup.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
For detailed network-first patterns, see the knowledge base:
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ flowchart TD
|
|||
- [How to Run NFR Assessment](/docs/how-to/workflows/run-nfr-assess.md) - NFR risk assessment
|
||||
|
||||
**Use-Case Guides:**
|
||||
- [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise risk management
|
||||
- [Running TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise risk management
|
||||
|
||||
## Reference
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ test('flaky test', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
**Good Example (Vanilla Playwright):**
|
||||
**Good Example:**
|
||||
```typescript
|
||||
test('deterministic test', async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
|
|
@ -126,43 +126,12 @@ test('deterministic test', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
**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:**
|
||||
**Why it works:**
|
||||
- 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.
|
||||
|
|
@ -183,7 +152,7 @@ test('create user', async ({ apiRequest }) => {
|
|||
const { body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users',
|
||||
body: { email: 'test@example.com' } (hard-coded)
|
||||
body: { email: 'test@example.com' } // 'body' not 'data' (hard-coded)
|
||||
});
|
||||
userId = body.id; // Store in global
|
||||
});
|
||||
|
|
@ -193,7 +162,7 @@ test('update user', async ({ apiRequest }) => {
|
|||
await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: `/api/users/${userId}`,
|
||||
body: { name: 'Updated' }
|
||||
body: { name: 'Updated' } // 'body' not 'data'
|
||||
});
|
||||
// No cleanup - leaves user in database
|
||||
});
|
||||
|
|
@ -244,7 +213,7 @@ test('should update user profile', async ({ apiRequest }) => {
|
|||
const { status: createStatus, body: user } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users',
|
||||
body: { email: testEmail, name: faker.person.fullName() }
|
||||
body: { email: testEmail, name: faker.person.fullName() } // 'body' not 'data'
|
||||
});
|
||||
|
||||
expect(createStatus).toBe(201);
|
||||
|
|
@ -253,7 +222,7 @@ test('should update user profile', async ({ apiRequest }) => {
|
|||
const { status, body: updated } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: `/api/users/${user.id}`,
|
||||
body: { name: 'Updated Name' }
|
||||
body: { name: 'Updated Name' } // 'body' not 'data'
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
|
@ -443,7 +412,7 @@ test('slow test', async ({ page }) => {
|
|||
|
||||
**Total time:** 3+ minutes (95 seconds wasted on hard waits)
|
||||
|
||||
**Good Example (Vanilla Playwright):**
|
||||
**Good Example:**
|
||||
```typescript
|
||||
// ✅ Fast test (< 10 seconds)
|
||||
test('fast test', async ({ page }) => {
|
||||
|
|
@ -467,50 +436,8 @@ test('fast test', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
**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`:
|
||||
|
|
@ -894,7 +821,7 @@ For detailed test quality patterns, see:
|
|||
|
||||
**Use-Case Guides:**
|
||||
- [Using TEA with Existing Tests](/docs/how-to/brownfield/use-tea-with-existing-tests.md) - Improve legacy quality
|
||||
- [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise quality thresholds
|
||||
- [Running TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise quality thresholds
|
||||
|
||||
## Reference
|
||||
|
||||
|
|
|
|||
|
|
@ -150,40 +150,34 @@ test('checkout completes', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
**After (With Playwright Utils - Cleaner API):**
|
||||
**After (With Playwright Utils + Auto Error Detection):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
|
||||
|
||||
test('checkout completes', async ({ page, interceptNetworkCall }) => {
|
||||
// Use interceptNetworkCall for cleaner network interception
|
||||
const checkoutCall = interceptNetworkCall({
|
||||
method: 'POST',
|
||||
url: '**/api/checkout'
|
||||
});
|
||||
// That's it! Just import the fixture - monitoring is automatic
|
||||
test('checkout completes', async ({ page }) => {
|
||||
const checkoutPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/checkout') && resp.ok()
|
||||
);
|
||||
|
||||
await page.click('button[name="checkout"]');
|
||||
const response = await checkoutPromise;
|
||||
const order = await response.json();
|
||||
|
||||
// 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();
|
||||
|
||||
// Zero setup - automatically fails if ANY 4xx/5xx occurred
|
||||
// Error message: "Network errors detected: POST 500 /api/payment"
|
||||
});
|
||||
```
|
||||
|
||||
**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/how-to/customization/integrate-playwright-utils.md#network-error-monitor).
|
||||
- Auto-enabled by fixture import (zero code changes)
|
||||
- Catches silent backend errors (500, 503, 504)
|
||||
- Test fails even if UI shows cached/stale success message
|
||||
- Structured error report in test output
|
||||
- No manual error checking needed
|
||||
|
||||
**Priority 3: P1 Requirements**
|
||||
```
|
||||
|
|
@ -358,10 +352,10 @@ test.skip('flaky test - needs fixing', async ({ page }) => {
|
|||
```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 |
|
||||
| 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:**
|
||||
|
|
@ -404,12 +398,12 @@ Same process
|
|||
```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 |
|
||||
| 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
|
||||
|
|
@ -471,26 +465,15 @@ Incremental changes = lower risk
|
|||
|
||||
**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)
|
||||
1. Run *ci to add selective testing
|
||||
2. Run only affected tests on PR
|
||||
3. Run full suite nightly
|
||||
4. Parallelize with sharding
|
||||
|
||||
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/how-to/customization/integrate-playwright-utils.md#burn-in)
|
||||
|
||||
### "We Have Tests But They Always Fail"
|
||||
|
||||
**Problem:** Tests are so flaky they're ignored.
|
||||
|
|
@ -547,6 +530,43 @@ Don't let perfect be the enemy of good
|
|||
*trace Phase 2 - Gate decision
|
||||
```
|
||||
|
||||
## Success Stories
|
||||
|
||||
### Example: E-Commerce Platform
|
||||
|
||||
**Starting Point:**
|
||||
- 200 E2E tests, 30% passing, 15-minute flakiness
|
||||
- No API tests
|
||||
- No coverage visibility
|
||||
|
||||
**After 3 Months with TEA:**
|
||||
- 150 E2E tests (removed duplicates), 95% passing, <1% flakiness
|
||||
- 300 API tests added (faster, more reliable)
|
||||
- P0 coverage: 100%, P1 coverage: 85%
|
||||
- Quality score: 82/100
|
||||
|
||||
**How:**
|
||||
- Month 1: Baseline + fix top 20 flaky tests
|
||||
- Month 2: Add API tests for critical path
|
||||
- Month 3: Improve quality + expand P1 coverage
|
||||
|
||||
### Example: SaaS Application
|
||||
|
||||
**Starting Point:**
|
||||
- 50 tests, quality score 48/100
|
||||
- Hard waits everywhere
|
||||
- Tests take 45 minutes
|
||||
|
||||
**After 6 Weeks with TEA:**
|
||||
- 120 tests, quality score 78/100
|
||||
- No hard waits (network-first patterns)
|
||||
- Tests take 8 minutes (parallel execution)
|
||||
|
||||
**How:**
|
||||
- Week 1-2: Replace hard waits with network-first
|
||||
- Week 3-4: Add selective testing + CI parallelization
|
||||
- Week 5-6: Generate tests for gaps with *automate
|
||||
|
||||
## Related Guides
|
||||
|
||||
**Workflow Guides:**
|
||||
|
|
|
|||
|
|
@ -18,25 +18,17 @@ MCP (Model Context Protocol) servers enable AI agents to interact with live brow
|
|||
|
||||
## 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 recording mode in `*atdd` (verify selectors with live browser)
|
||||
- Want healing mode in `*automate` (fix tests with visual debugging)
|
||||
- Debugging complex UI issues
|
||||
- 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're new to TEA (adds complexity)
|
||||
- You don't have MCP servers configured
|
||||
- Your tests work fine without it
|
||||
- You're testing APIs only (no UI)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
@ -79,11 +71,13 @@ MCP (Model Context Protocol) servers enable AI agents to interact with live brow
|
|||
|
||||
Both servers work together to provide full TEA MCP capabilities.
|
||||
|
||||
## Setup
|
||||
## Installation
|
||||
|
||||
### 1. Configure MCP Servers
|
||||
### Step 1: Configure MCP Servers in IDE
|
||||
|
||||
Add to your IDE's MCP configuration:
|
||||
Add this configuration to your IDE's MCP settings. See [TEA Overview](/docs/explanation/features/tea-overview.md#playwright-mcp-enhancements) for IDE-specific configuration locations.
|
||||
|
||||
**MCP Configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -100,20 +94,36 @@ Add to your IDE's MCP configuration:
|
|||
}
|
||||
```
|
||||
|
||||
See [TEA Overview](/docs/explanation/features/tea-overview.md#playwright-mcp-enhancements) for IDE-specific config locations.
|
||||
### Step 2: Install Playwright Browsers
|
||||
|
||||
### 2. Enable in BMAD
|
||||
```bash
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
Answer "Yes" when prompted during installation, or set in config:
|
||||
### Step 3: Enable in TEA Config
|
||||
|
||||
Edit `_bmad/bmm/config.yaml`:
|
||||
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
tea_use_mcp_enhancements: true
|
||||
```
|
||||
|
||||
### 3. Verify MCPs Running
|
||||
### Step 4: Restart IDE
|
||||
|
||||
Ensure your MCP servers are running in your IDE.
|
||||
Restart your IDE to load MCP server configuration.
|
||||
|
||||
### Step 5: Verify MCP Servers
|
||||
|
||||
Check MCP servers are running:
|
||||
|
||||
**In Cursor:**
|
||||
- Open command palette (Cmd/Ctrl + Shift + P)
|
||||
- Search "MCP"
|
||||
- Should see "Playwright" and "Playwright Test" servers listed
|
||||
|
||||
**In VS Code:**
|
||||
- Check Claude extension settings
|
||||
- Verify MCP servers are enabled
|
||||
|
||||
## How MCP Enhances TEA Workflows
|
||||
|
||||
|
|
@ -152,14 +162,16 @@ I'll design tests for these interactions."
|
|||
|
||||
**Without MCP:**
|
||||
- TEA generates selectors from best practices
|
||||
- TEA infers API patterns from documentation
|
||||
- May use `getByRole()` that doesn't match actual app
|
||||
- Selectors might need adjustment
|
||||
|
||||
**With MCP (Recording Mode):**
|
||||
|
||||
**For UI Tests:**
|
||||
**With MCP:**
|
||||
TEA verifies selectors with live browser:
|
||||
```
|
||||
[TEA navigates to /login with live browser]
|
||||
[Inspects actual form fields]
|
||||
"Let me verify the login form selectors"
|
||||
|
||||
[TEA navigates to /login]
|
||||
[Inspects form fields]
|
||||
|
||||
"I see:
|
||||
- Email input has label 'Email Address' (not 'Email')
|
||||
|
|
@ -169,58 +181,47 @@ I'll design tests for these interactions."
|
|||
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."
|
||||
**Generated test:**
|
||||
```typescript
|
||||
await page.getByLabel('Email Address').fill('test@example.com');
|
||||
await page.getByLabel('Your Password').fill('password');
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
// Selectors verified against actual DOM
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- UI: Accurate selectors from real DOM
|
||||
- API: Validated request/response patterns from trace
|
||||
- Both: Tests work on first run
|
||||
- Accurate selectors from real DOM
|
||||
- Tests work on first run
|
||||
- No trial-and-error selector debugging
|
||||
|
||||
### *automate: Healing + Recording Modes
|
||||
### *automate: Healing Mode
|
||||
|
||||
**Without MCP:**
|
||||
- TEA analyzes test code only
|
||||
- Suggests fixes based on static analysis
|
||||
- Generates tests from documentation/code
|
||||
- Can't verify fixes work
|
||||
|
||||
**With MCP:**
|
||||
|
||||
**Healing Mode (UI + API):**
|
||||
TEA uses visual debugging:
|
||||
```
|
||||
"This test is failing. Let me debug with trace viewer"
|
||||
|
||||
[TEA opens trace file]
|
||||
[Analyzes screenshots + network tab]
|
||||
[Analyzes screenshots]
|
||||
[Identifies selector changed]
|
||||
|
||||
UI failures: "Button selector changed from 'Save' to 'Save Changes'"
|
||||
API failures: "Response structure changed, expected {id} got {userId}"
|
||||
"The button selector changed from 'Save' to 'Save Changes'
|
||||
I'll update the test and verify it works"
|
||||
|
||||
[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]
|
||||
[TEA makes fix]
|
||||
[Runs test with MCP]
|
||||
[Confirms test passes]
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Visual debugging + trace analysis (not just UI)
|
||||
- Verified selectors (UI) + network patterns (API)
|
||||
- Tests verified against actual application behavior
|
||||
- Visual debugging during healing
|
||||
- Verified fixes (not guesses)
|
||||
- Faster resolution
|
||||
|
||||
## Usage Examples
|
||||
|
||||
|
|
@ -289,6 +290,43 @@ Fixing selector and verifying...
|
|||
Updated test with corrected selector.
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### MCP Server Arguments
|
||||
|
||||
**Playwright MCP with custom port:**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--port", "3000"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Playwright Test with specific browser:**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright-test": {
|
||||
"command": "npx",
|
||||
"args": ["playwright", "run-test-mcp-server", "--browser", "chromium"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# .env
|
||||
PLAYWRIGHT_BROWSER=chromium # Browser for MCP
|
||||
PLAYWRIGHT_HEADLESS=false # Show browser during MCP
|
||||
PLAYWRIGHT_SLOW_MO=100 # Slow down for visibility
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP Servers Not Running
|
||||
|
|
@ -395,6 +433,107 @@ tea_use_mcp_enhancements: true
|
|||
tea_use_mcp_enhancements: false
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Use MCP for Complex UIs
|
||||
|
||||
**Simple UI (skip MCP):**
|
||||
```
|
||||
Standard login form with email/password
|
||||
TEA can infer selectors without MCP
|
||||
```
|
||||
|
||||
**Complex UI (use MCP):**
|
||||
```
|
||||
Multi-step wizard with dynamic fields
|
||||
Conditional UI elements
|
||||
Third-party components
|
||||
Custom form widgets
|
||||
```
|
||||
|
||||
### Start Without MCP, Enable When Needed
|
||||
|
||||
**Learning path:**
|
||||
1. Week 1-2: TEA without MCP (learn basics)
|
||||
2. Week 3: Enable MCP (explore advanced features)
|
||||
3. Week 4+: Use MCP selectively (when it adds value)
|
||||
|
||||
### Combine with Playwright Utils
|
||||
|
||||
**Powerful combination:**
|
||||
```yaml
|
||||
tea_use_playwright_utils: true
|
||||
tea_use_mcp_enhancements: true
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Playwright Utils provides production-ready utilities
|
||||
- MCP verifies utilities work with actual app
|
||||
- Best of both worlds
|
||||
|
||||
### Use for Test Healing
|
||||
|
||||
**Scenario:** Test suite has 50 failing tests after UI update.
|
||||
|
||||
**With MCP:**
|
||||
```
|
||||
*automate (healing mode)
|
||||
|
||||
TEA:
|
||||
1. Opens trace viewer for each failure
|
||||
2. Identifies changed selectors
|
||||
3. Updates tests with corrected selectors
|
||||
4. Verifies fixes with browser
|
||||
5. Provides updated tests
|
||||
|
||||
Result: 45/50 tests auto-healed
|
||||
```
|
||||
|
||||
### Use for New Team Members
|
||||
|
||||
**Onboarding:**
|
||||
```
|
||||
New developer: "I don't know this codebase's UI"
|
||||
|
||||
Senior: "Run *test-design with MCP exploratory mode"
|
||||
|
||||
TEA explores UI and generates documentation:
|
||||
- UI structure discovered
|
||||
- Interactive elements mapped
|
||||
- Test design created automatically
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### MCP Servers Have Browser Access
|
||||
|
||||
**What MCP can do:**
|
||||
- Navigate to any URL
|
||||
- Click any element
|
||||
- Fill any form
|
||||
- Access browser storage
|
||||
- Read page content
|
||||
|
||||
**Best practices:**
|
||||
- Only configure MCP in trusted environments
|
||||
- Don't use MCP on production sites (use staging/dev)
|
||||
- Review generated tests before running on production
|
||||
- Keep MCP config in local files (not committed)
|
||||
|
||||
### Protect Credentials
|
||||
|
||||
**Don't:**
|
||||
```
|
||||
"TEA, login with mypassword123"
|
||||
# Password visible in chat history
|
||||
```
|
||||
|
||||
**Do:**
|
||||
```
|
||||
"TEA, login using credentials from .env"
|
||||
# Password loaded from environment, not in chat
|
||||
```
|
||||
|
||||
## Related Guides
|
||||
|
||||
**Getting Started:**
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ Edit `_bmad/bmm/config.yaml`:
|
|||
tea_use_playwright_utils: true
|
||||
```
|
||||
|
||||
**Note:** If you enabled this during BMad installation, it's already set.
|
||||
**Note:** If you enabled this during installation (`npx bmad-method@alpha install`), it's already set.
|
||||
|
||||
### Step 3: Verify Installation
|
||||
|
||||
|
|
@ -175,16 +175,13 @@ Reviews against playwright-utils best practices:
|
|||
### *ci Workflow
|
||||
|
||||
**Without Playwright Utils:**
|
||||
- Parallel sharding
|
||||
- Burn-in loops (basic shell scripts)
|
||||
- CI triggers (PR, push, schedule)
|
||||
- Artifact collection
|
||||
Basic CI configuration
|
||||
|
||||
**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
|
||||
Enhanced CI with:
|
||||
- Burn-in utility for smart test selection
|
||||
- Selective testing based on git diff
|
||||
- Test prioritization
|
||||
|
||||
## Available Utilities
|
||||
|
||||
|
|
@ -192,18 +189,6 @@ Enhanced with smart testing:
|
|||
|
||||
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';
|
||||
|
|
@ -221,7 +206,7 @@ test('should create user', async ({ 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)
|
||||
}).validateSchema(UserSchema); // Note: chained method
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body.id).toBeDefined();
|
||||
|
|
@ -239,17 +224,6 @@ test('should create user', async ({ apiRequest }) => {
|
|||
|
||||
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';
|
||||
|
|
@ -288,17 +262,6 @@ async function globalSetup() {
|
|||
|
||||
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';
|
||||
|
|
@ -338,17 +301,6 @@ PW_NET_MODE=playback npx playwright test
|
|||
|
||||
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';
|
||||
|
|
@ -385,17 +337,6 @@ test('should handle API errors', async ({ page, interceptNetworkCall }) => {
|
|||
|
||||
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';
|
||||
|
|
@ -432,17 +373,6 @@ test('should wait for async job completion', async ({ apiRequest, recurse }) =>
|
|||
|
||||
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';
|
||||
|
|
@ -466,24 +396,13 @@ test('should login', async ({ page }) => {
|
|||
- 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)
|
||||
- Supports object logging with `.debug()`
|
||||
- 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';
|
||||
|
|
@ -525,17 +444,6 @@ test('should export valid CSV', async ({ page }) => {
|
|||
|
||||
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
|
||||
|
|
@ -582,7 +490,6 @@ export default config;
|
|||
```
|
||||
|
||||
**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
|
||||
|
|
@ -592,17 +499,6 @@ export default config;
|
|||
|
||||
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';
|
||||
|
|
@ -644,76 +540,98 @@ test.describe('error handling',
|
|||
|
||||
**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)
|
||||
- Catches silent backend failures
|
||||
- Opt-out with annotations
|
||||
- Structured error reporting
|
||||
|
||||
## Fixture Composition
|
||||
|
||||
**Option 1: Use Package's Combined Fixtures (Simplest)**
|
||||
Combine utilities using `mergeTests`:
|
||||
|
||||
**Option 1: Use 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');
|
||||
test('full test', async ({ apiRequest, authToken, interceptNetworkCall }) => {
|
||||
await log.info('Starting test'); // log is direct import
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users'
|
||||
path: '/api/data',
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
await log.info('Data fetched', body);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
**Option 2: Create Custom Merged Fixtures (Selective)**
|
||||
**Note:** `log` is imported directly (not a fixture). `authToken` requires auth-session provider setup.
|
||||
|
||||
**File 1: support/merged-fixtures.ts**
|
||||
**Option 2: Merge Individual Fixtures (Selective)**
|
||||
```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 { test as base } from '@playwright/test';
|
||||
import { mergeTests } from '@playwright/test';
|
||||
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
|
||||
import { log } from '@seontechnologies/playwright-utils';
|
||||
|
||||
// Merge only what you need
|
||||
// Merge only the fixtures you need
|
||||
export const test = mergeTests(
|
||||
base,
|
||||
apiRequest,
|
||||
interceptNetworkCall,
|
||||
networkErrorMonitor
|
||||
apiRequestFixture,
|
||||
recurseFixture
|
||||
);
|
||||
|
||||
export const expect = base.expect;
|
||||
export { log };
|
||||
```
|
||||
export { expect } from '@playwright/test';
|
||||
|
||||
**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');
|
||||
// Use merged utilities in tests
|
||||
test('selective test', async ({ apiRequest, recurse }) => {
|
||||
await log.info('Starting test'); // log is direct import, not fixture
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users'
|
||||
path: '/api/data'
|
||||
});
|
||||
|
||||
await log.info('Data fetched', body);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
```
|
||||
|
||||
**Contrast:**
|
||||
- Option 1: All utilities available, zero setup
|
||||
- Option 2: Pick utilities you need, one central file
|
||||
**Note:** `log` is a direct utility (not a fixture), so import it separately.
|
||||
|
||||
**See working examples:** <https://github.com/seontechnologies/playwright-utils/tree/main/playwright/support>
|
||||
**Recommended:** Use Option 1 (combined fixtures) unless you need fine control over which utilities are included.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# .env
|
||||
PLAYWRIGHT_UTILS_LOG_LEVEL=debug # debug | info | warn | error
|
||||
PLAYWRIGHT_UTILS_RETRY_ATTEMPTS=3
|
||||
PLAYWRIGHT_UTILS_TIMEOUT=30000
|
||||
```
|
||||
|
||||
### Playwright Config
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
use: {
|
||||
// Playwright Utils works with standard Playwright config
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||
extraHTTPHeaders: {
|
||||
// Add headers used by utilities
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
@ -780,6 +698,47 @@ expect(status).toBe(200);
|
|||
|
||||
## Migration Guide
|
||||
|
||||
### Migrating Existing Tests
|
||||
|
||||
**Before (Vanilla Playwright):**
|
||||
```typescript
|
||||
test('should access protected route', async ({ page, request }) => {
|
||||
// Manual auth token fetch
|
||||
const response = await request.post('/api/auth/login', {
|
||||
data: { email: 'test@example.com', password: 'pass' }
|
||||
});
|
||||
const { token } = await response.json();
|
||||
|
||||
// Manual token storage
|
||||
await page.goto('/dashboard');
|
||||
await page.evaluate((token) => {
|
||||
localStorage.setItem('authToken', token);
|
||||
}, token);
|
||||
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
```
|
||||
|
||||
**After (With Playwright Utils):**
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures';
|
||||
|
||||
test('should access protected route', async ({ page, authToken }) => {
|
||||
// authToken automatically fetched and persisted by fixture
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Token is already in place (no manual storage needed)
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Token fetched once, reused across all tests (persisted to disk)
|
||||
- No manual token storage or management
|
||||
- Automatic token renewal if expired
|
||||
- Multi-user support via `authOptions.userIdentifier`
|
||||
- 10 lines → 5 lines (less code)
|
||||
|
||||
## Related Guides
|
||||
|
||||
**Getting Started:**
|
||||
|
|
@ -796,7 +755,6 @@ expect(status).toBe(200);
|
|||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why Playwright Utils matters** (part of TEA's three-part solution)
|
||||
- [Fixture Architecture](/docs/explanation/tea/fixture-architecture.md) - Pure function → fixture pattern
|
||||
- [Network-First Patterns](/docs/explanation/tea/network-first-patterns.md) - Network utilities explained
|
||||
- [Test Quality Standards](/docs/explanation/tea/test-quality-standards.md) - Patterns PW-Utils enforces
|
||||
|
|
|
|||
|
|
@ -90,15 +90,17 @@ TEA will ask what test levels to generate:
|
|||
- 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)
|
||||
- Mix of levels
|
||||
|
||||
**Recommended approach:** Generate API tests first, then E2E tests (see [API Tests First, E2E Later](#api-tests-first-e2e-later) tip below).
|
||||
|
||||
### Component Testing by Framework
|
||||
|
||||
TEA generates component tests using framework-appropriate tools:
|
||||
|
||||
| Your Framework | Component Testing Tool |
|
||||
| -------------- | ------------------------------------------- |
|
||||
| **Cypress** | Cypress Component Testing (*.cy.tsx) |
|
||||
| Your Framework | Component Testing Tool |
|
||||
|----------------|----------------------|
|
||||
| **Cypress** | Cypress Component Testing (*.cy.tsx) |
|
||||
| **Playwright** | Vitest + React Testing Library (*.test.tsx) |
|
||||
|
||||
**Example response:**
|
||||
|
|
@ -188,7 +190,7 @@ test.describe('Profile API', () => {
|
|||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: {
|
||||
body: { // 'body' not 'data'
|
||||
name: 'Updated Name',
|
||||
email: 'updated@example.com'
|
||||
}
|
||||
|
|
@ -203,7 +205,7 @@ test.describe('Profile API', () => {
|
|||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { email: 'invalid-email' }
|
||||
body: { email: 'invalid-email' } // 'body' not 'data'
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
|
|
@ -224,28 +226,52 @@ test.describe('Profile API', () => {
|
|||
```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();
|
||||
test.describe('Profile Page', () => {
|
||||
test.beforeEach(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');
|
||||
test('should display current profile information', async ({ page }) => {
|
||||
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();
|
||||
await expect(page.getByText('test@example.com')).toBeVisible();
|
||||
await expect(page.getByText('Test User')).toBeVisible();
|
||||
});
|
||||
|
||||
// Verify success
|
||||
await expect(page.getByText('Profile updated')).toBeVisible();
|
||||
test('should edit and save profile', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
|
||||
// Click edit
|
||||
await page.getByRole('button', { name: 'Edit Profile' }).click();
|
||||
|
||||
// Modify fields
|
||||
await page.getByLabel('Name').fill('Updated Name');
|
||||
await page.getByLabel('Email').fill('updated@example.com');
|
||||
|
||||
// Save
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Verify success
|
||||
await expect(page.getByText('Profile updated successfully')).toBeVisible();
|
||||
await expect(page.getByText('Updated Name')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show validation error for invalid email', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await page.getByRole('button', { name: 'Edit Profile' }).click();
|
||||
|
||||
await page.getByLabel('Email').fill('invalid-email');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.getByText('Invalid email format')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
TEA generates additional E2E tests for display, validation errors, etc. based on acceptance criteria.
|
||||
|
||||
#### Implementation Checklist
|
||||
|
||||
TEA also provides an implementation checklist:
|
||||
|
|
@ -374,13 +400,18 @@ Run `*test-design` before `*atdd` for better results:
|
|||
*atdd # Generate tests based on design
|
||||
```
|
||||
|
||||
### MCP Enhancements (Optional)
|
||||
### Recording Mode Note
|
||||
|
||||
If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `*atdd`.
|
||||
**Recording mode is NOT typically used with ATDD** because ATDD generates tests for features that don't exist yet (no UI to record against).
|
||||
|
||||
**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.
|
||||
If you have a skeleton UI or are refining existing tests, use `*automate` with recording mode instead. See [How to Run Automate](/docs/how-to/workflows/run-automate.md).
|
||||
|
||||
See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
|
||||
**Recording mode is only applicable for ATDD in the rare case where:**
|
||||
- You have skeleton/mockup UI already implemented
|
||||
- You want to verify selector patterns before full implementation
|
||||
- You're doing "UI-first" development (unusual for TDD)
|
||||
|
||||
For most ATDD workflows, **skip recording mode** - TEA will infer selectors from best practices.
|
||||
|
||||
### Focus on P0/P1 Scenarios
|
||||
|
||||
|
|
@ -413,6 +444,43 @@ TEA generates deterministic tests by default:
|
|||
|
||||
Don't modify these patterns - they prevent flakiness!
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Tests Don't Fail Initially
|
||||
|
||||
**Problem:** Tests pass on first run but feature doesn't exist.
|
||||
|
||||
**Cause:** Tests are hitting wrong endpoints or checking wrong things.
|
||||
|
||||
**Solution:** Review generated tests - ensure they match your feature requirements.
|
||||
|
||||
### 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 scenarios (2-3 tests)
|
||||
- Happy path for API
|
||||
- One E2E test for full flow
|
||||
```
|
||||
|
||||
### Selectors Are Fragile
|
||||
|
||||
**Problem:** E2E tests use brittle selectors (CSS, XPath).
|
||||
|
||||
**Solution:** Use MCP recording mode or specify accessible selectors:
|
||||
```
|
||||
Use accessible locators:
|
||||
- getByRole()
|
||||
- getByLabel()
|
||||
- getByText()
|
||||
Avoid CSS selectors
|
||||
```
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [How to Run Test Design](/docs/how-to/workflows/run-test-design.md) - Plan before generating
|
||||
|
|
@ -421,7 +489,6 @@ Don't modify these patterns - they prevent flakiness!
|
|||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why TEA generates quality tests** (foundational)
|
||||
- [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - Why P0 vs P3 matters
|
||||
- [Test Quality Standards](/docs/explanation/tea/test-quality-standards.md) - What makes tests good
|
||||
- [Network-First Patterns](/docs/explanation/tea/network-first-patterns.md) - Avoiding flakiness
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ testWithAuth.describe('Profile API', () => {
|
|||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { name: 'Updated Name', bio: 'Test bio' },
|
||||
body: { name: 'Updated Name', bio: 'Test bio' }, // 'body' not 'data'
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
}).validateSchema(ProfileSchema); // Chained validation
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ testWithAuth.describe('Profile API', () => {
|
|||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { email: 'invalid-email' },
|
||||
body: { email: 'invalid-email' }, // 'body' not 'data'
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
|
|
@ -250,31 +250,58 @@ testWithAuth.describe('Profile API', () => {
|
|||
- Automatic retry for 5xx errors
|
||||
- Less boilerplate (no manual `await response.json()` everywhere)
|
||||
|
||||
#### E2E Tests (`tests/e2e/profile.spec.ts`):
|
||||
#### E2E Tests (`tests/e2e/profile-workflow.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();
|
||||
test.describe('Profile Management Workflow', () => {
|
||||
test.beforeEach(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();
|
||||
// Wait for login to complete
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
});
|
||||
|
||||
// Verify success
|
||||
await expect(page.getByText('Profile updated')).toBeVisible();
|
||||
test('should view and edit profile', async ({ page }) => {
|
||||
// Navigate to profile
|
||||
await page.goto('/profile');
|
||||
|
||||
// Verify profile displays
|
||||
await expect(page.getByText('test@example.com')).toBeVisible();
|
||||
|
||||
// Edit 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();
|
||||
await expect(page.getByText('New Name')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show validation errors', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await page.getByRole('button', { name: 'Edit Profile' }).click();
|
||||
|
||||
// Enter invalid email
|
||||
await page.getByLabel('Email').fill('invalid');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Verify error shown
|
||||
await expect(page.getByText('Invalid email format')).toBeVisible();
|
||||
|
||||
// Profile should not be updated
|
||||
await page.reload();
|
||||
await expect(page.getByText('test@example.com')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
TEA generates additional tests for validation, edge cases, etc. based on priorities.
|
||||
|
||||
#### Fixtures (`tests/support/fixtures/profile.ts`):
|
||||
|
||||
**Vanilla Playwright:**
|
||||
|
|
@ -477,9 +504,9 @@ Compare against:
|
|||
|
||||
TEA supports component testing using framework-appropriate tools:
|
||||
|
||||
| Your Framework | Component Testing Tool | Tests Location |
|
||||
| -------------- | ------------------------------ | ----------------------------------------- |
|
||||
| **Cypress** | Cypress Component Testing | `tests/component/` |
|
||||
| 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:
|
||||
|
|
@ -541,14 +568,25 @@ Don't duplicate that coverage
|
|||
|
||||
TEA will analyze existing tests and only generate new scenarios.
|
||||
|
||||
### MCP Enhancements (Optional)
|
||||
### Use Healing Mode (Optional)
|
||||
|
||||
If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `*automate` for:
|
||||
If MCP enhancements enabled (`tea_use_mcp_enhancements: true`):
|
||||
|
||||
- **Healing mode:** Fix broken selectors, update assertions, enhance with trace analysis
|
||||
- **Recording mode:** Verify selectors with live browser, capture network requests
|
||||
When prompted, select "healing mode" to:
|
||||
- Fix broken selectors in existing tests
|
||||
- Update outdated assertions
|
||||
- Enhance with trace viewer insights
|
||||
|
||||
No prompts - TEA uses MCPs automatically when available. See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
|
||||
See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md)
|
||||
|
||||
### Use Recording Mode (Optional)
|
||||
|
||||
If MCP enhancements enabled:
|
||||
|
||||
When prompted, select "recording mode" to:
|
||||
- Verify selectors against live browser
|
||||
- Generate accurate locators from actual DOM
|
||||
- Capture network requests
|
||||
|
||||
### Generate Tests Incrementally
|
||||
|
||||
|
|
@ -624,11 +662,21 @@ We already have these tests:
|
|||
Generate tests for scenarios NOT covered by those files
|
||||
```
|
||||
|
||||
### MCP Enhancements for Better Selectors
|
||||
### Selectors Are Fragile
|
||||
|
||||
If you have MCP servers configured, TEA verifies selectors against live browser. Otherwise, TEA generates accessible selectors (`getByRole`, `getByLabel`) by default.
|
||||
**Problem:** E2E tests use brittle CSS selectors.
|
||||
|
||||
Setup: Answer "Yes" to MCPs in BMad installer + configure MCP servers in your IDE. See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md).
|
||||
**Solution:** Request accessible selectors:
|
||||
```
|
||||
Use accessible locators:
|
||||
- getByRole()
|
||||
- getByLabel()
|
||||
- getByText()
|
||||
|
||||
Avoid CSS selectors like .class-name or #id
|
||||
```
|
||||
|
||||
Or use MCP recording mode for verified selectors.
|
||||
|
||||
## Related Guides
|
||||
|
||||
|
|
@ -638,7 +686,6 @@ Setup: Answer "Yes" to MCPs in BMad installer + configure MCP servers in your ID
|
|||
|
||||
## Understanding the Concepts
|
||||
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why TEA generates quality tests** (foundational)
|
||||
- [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - Why prioritize P0 over P3
|
||||
- [Test Quality Standards](/docs/explanation/tea/test-quality-standards.md) - What makes tests good
|
||||
- [Fixture Architecture](/docs/explanation/tea/fixture-architecture.md) - Reusable test patterns
|
||||
|
|
|
|||
|
|
@ -662,7 +662,7 @@ Assess categories incrementally, not all at once.
|
|||
|
||||
- [How to Run Trace](/docs/how-to/workflows/run-trace.md) - Gate decision complements NFR
|
||||
- [How to Run Test Review](/docs/how-to/workflows/run-test-review.md) - Quality complements NFR
|
||||
- [Run TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise workflow
|
||||
- [Run TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise workflow
|
||||
|
||||
## Understanding the Concepts
|
||||
|
||||
|
|
|
|||
|
|
@ -62,12 +62,12 @@ 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 |
|
||||
| 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:**
|
||||
```
|
||||
|
|
@ -113,21 +113,21 @@ TEA generates a comprehensive traceability matrix.
|
|||
|
||||
## Coverage Summary
|
||||
|
||||
| Metric | Count | Percentage |
|
||||
| ---------------------- | ----- | ---------- |
|
||||
| **Total Requirements** | 15 | 100% |
|
||||
| **Full Coverage** | 11 | 73% |
|
||||
| **Partial Coverage** | 3 | 20% |
|
||||
| **No Coverage** | 1 | 7% |
|
||||
| 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) |
|
||||
| Priority | Total | Covered | Percentage |
|
||||
|----------|-------|---------|------------|
|
||||
| **P0** | 5 | 5 | 100% ✅ |
|
||||
| **P1** | 6 | 5 | 83% ⚠️ |
|
||||
| **P2** | 3 | 1 | 33% ⚠️ |
|
||||
| **P3** | 1 | 0 | 0% ✅ (acceptable) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -223,10 +223,10 @@ TEA generates a comprehensive traceability matrix.
|
|||
|
||||
### 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 |
|
||||
| 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
|
||||
|
|
@ -234,9 +234,9 @@ TEA generates a comprehensive traceability matrix.
|
|||
|
||||
### Non-Critical Gaps (Can Defer)
|
||||
|
||||
| Gap | Requirement | Priority | Risk | Recommendation |
|
||||
| --- | ------------------------- | -------- | ---- | ------------------- |
|
||||
| 3 | Profile export not tested | P2 | Low | Add in v1.3 release |
|
||||
| Gap | Requirement | Priority | Risk | Recommendation |
|
||||
|-----|-------------|----------|------|----------------|
|
||||
| 3 | Profile export not tested | P2 | Low | Add in v1.3 release |
|
||||
|
||||
**Estimated Effort:** 2 hours
|
||||
**Owner:** QA team
|
||||
|
|
@ -297,7 +297,7 @@ test('should update bio via API', async ({ apiRequest, authToken }) => {
|
|||
const { status, body } = await apiRequest({
|
||||
method: 'PATCH',
|
||||
path: '/api/profile',
|
||||
body: { bio: 'Updated bio' },
|
||||
body: { bio: 'Updated bio' }, // 'body' not 'data'
|
||||
headers: { Authorization: `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
|
|
@ -442,12 +442,12 @@ TEA makes evidence-based gate decision and writes to separate file.
|
|||
|
||||
## 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) |
|
||||
| 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
|
||||
|
|
@ -456,11 +456,11 @@ TEA makes evidence-based gate decision and writes to separate file.
|
|||
|
||||
## Quality Metrics
|
||||
|
||||
| Metric | Threshold | Actual | Status |
|
||||
| ------------------ | --------- | ------ | ------ |
|
||||
| P0/P1 Coverage | >95% | 100% | ✅ |
|
||||
| Test Quality Score | >80 | 84 | ✅ |
|
||||
| NFR Status | PASS | PASS | ✅ |
|
||||
| Metric | Threshold | Actual | Status |
|
||||
|--------|-----------|--------|--------|
|
||||
| P0/P1 Coverage | >95% | 100% | ✅ |
|
||||
| Test Quality Score | >80 | 84 | ✅ |
|
||||
| NFR Status | PASS | PASS | ✅ |
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
|
|
@ -501,14 +501,14 @@ TEA makes evidence-based gate decision and writes to separate file.
|
|||
|
||||
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) |
|
||||
| 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%
|
||||
|
|
@ -683,12 +683,12 @@ 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! |
|
||||
| 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
|
||||
|
|
|
|||
|
|
@ -290,84 +290,137 @@ burn-in:
|
|||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
```
|
||||
|
||||
#### Burn-In Testing
|
||||
#### Helper Scripts
|
||||
|
||||
**Option 1: Classic Burn-In (Playwright Built-In)**
|
||||
TEA generates shell scripts for CI and local development.
|
||||
|
||||
**Test Scripts** (`package.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:burn-in": "playwright test --repeat-each=5 --retries=0"
|
||||
"test:headed": "playwright test --headed",
|
||||
"test:debug": "playwright test --debug",
|
||||
"test:smoke": "playwright test --grep @smoke",
|
||||
"test:critical": "playwright test --grep @critical",
|
||||
"test:changed": "./scripts/test-changed.sh",
|
||||
"test:burn-in": "./scripts/burn-in.sh",
|
||||
"test:report": "playwright show-report",
|
||||
"ci:local": "./scripts/ci-local.sh"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- Runs every test 5 times
|
||||
- Fails if any iteration fails
|
||||
- Detects flakiness before merge
|
||||
**Selective Testing Script** (`scripts/test-changed.sh`):
|
||||
|
||||
**Use when:** Small test suite, want to run everything multiple times
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Run only tests for changed files
|
||||
|
||||
---
|
||||
CHANGED_FILES=$(git diff --name-only origin/main...HEAD)
|
||||
|
||||
**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'
|
||||
});
|
||||
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
|
||||
```
|
||||
|
||||
**playwright.burn-in.config.ts:**
|
||||
**Burn-In Script** (`scripts/burn-in.sh`):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Run tests multiple times to detect flakiness
|
||||
|
||||
ITERATIONS=${BURN_IN_ITERATIONS:-5}
|
||||
FAILURES=0
|
||||
|
||||
for i in $(seq 1 $ITERATIONS); do
|
||||
echo "=== Burn-in iteration $i/$ITERATIONS ==="
|
||||
|
||||
if npm test; then
|
||||
echo "✓ Iteration $i passed"
|
||||
else
|
||||
echo "✗ Iteration $i failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILURES -gt 0 ]; then
|
||||
echo "❌ Tests failed in $FAILURES/$ITERATIONS iterations"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All $ITERATIONS iterations passed"
|
||||
```
|
||||
|
||||
**Local CI 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 || exit 1
|
||||
|
||||
# Burn-in (reduced iterations for local)
|
||||
for i in {1..3}; do
|
||||
echo "🔥 Burn-in $i/3"
|
||||
npm test || exit 1
|
||||
done
|
||||
|
||||
echo "✅ Local CI pipeline passed"
|
||||
```
|
||||
|
||||
**Make scripts executable:**
|
||||
```bash
|
||||
chmod +x scripts/*.sh
|
||||
```
|
||||
|
||||
**Alternative: Smart Burn-In with Playwright Utils**
|
||||
|
||||
If `tea_use_playwright_utils: true`, you can use git diff-based burn-in:
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
```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: 5, retries: 0 }
|
||||
burnInTestPercentage: 0.3, // Run 30% of affected tests
|
||||
burnIn: { repeatEach: 5, retries: 1 }
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
**package.json:**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test:burn-in": "tsx scripts/burn-in-changed.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
**Benefits over shell script:**
|
||||
- Only runs tests affected by git changes (faster)
|
||||
- Smart filtering (skips config, docs, types)
|
||||
- Volume control (run percentage, not all tests)
|
||||
|
||||
**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.
|
||||
**Example:** Changed 1 file → runs 3 affected tests 5 times = 15 runs (not 500 tests × 5 = 2500 runs)
|
||||
|
||||
### 6. Configure Secrets
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,9 +15,9 @@ Complete reference for all TEA (Test Architect) configuration options.
|
|||
|
||||
**Purpose:** Project-specific configuration values for your repository
|
||||
|
||||
**Created By:** BMad installer
|
||||
**Created By:** `npx bmad-method@alpha install` command
|
||||
|
||||
**Status:** Typically gitignored (user-specific values)
|
||||
**Status:** Gitignored (not committed to repository)
|
||||
|
||||
**Usage:** Edit this file to change TEA behavior in your project
|
||||
|
||||
|
|
@ -155,7 +155,17 @@ Would you like to enable MCP enhancements in Test Architect?
|
|||
}
|
||||
```
|
||||
|
||||
**Configuration:** Refer to your AI agent's documentation for MCP server setup instructions.
|
||||
**Configuration Location (IDE-Specific):**
|
||||
|
||||
**Cursor:**
|
||||
```
|
||||
~/.cursor/config.json or workspace .cursor/config.json
|
||||
```
|
||||
|
||||
**VS Code with Claude:**
|
||||
```
|
||||
~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
|
||||
```
|
||||
|
||||
**Example (Enable):**
|
||||
```yaml
|
||||
|
|
@ -354,9 +364,9 @@ tea_use_playwright_utils: true
|
|||
tea_use_mcp_enhancements: false
|
||||
```
|
||||
|
||||
**Individual config (typically gitignored):**
|
||||
**Individual config (gitignored):**
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml (user adds to .gitignore)
|
||||
# _bmad/bmm/config.yaml (gitignored)
|
||||
user_name: John Doe
|
||||
user_skill_level: expert
|
||||
tea_use_mcp_enhancements: true # Individual preference
|
||||
|
|
@ -397,7 +407,7 @@ _bmad/bmm/config.yaml.example # Template for team
|
|||
package.json # Dependencies
|
||||
```
|
||||
|
||||
**Recommended for .gitignore:**
|
||||
**Gitignore:**
|
||||
```
|
||||
_bmad/bmm/config.yaml # User-specific values
|
||||
.env # Secrets
|
||||
|
|
@ -410,7 +420,8 @@ _bmad/bmm/config.yaml # User-specific values
|
|||
```markdown
|
||||
## Setup
|
||||
|
||||
1. Install BMad
|
||||
1. Install BMad:
|
||||
npx bmad-method@alpha install
|
||||
|
||||
2. Copy config template:
|
||||
cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml
|
||||
|
|
@ -547,48 +558,48 @@ npx playwright install
|
|||
|
||||
## 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/explanation/philosophy/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)
|
||||
### Minimal Setup (Defaults)
|
||||
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
project_name: my-project
|
||||
user_skill_level: intermediate
|
||||
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
|
||||
- New projects
|
||||
- Learning TEA
|
||||
- Simple testing needs
|
||||
|
||||
**Note:** Can enable integrations later as you learn
|
||||
---
|
||||
|
||||
### Advanced Setup (All Features)
|
||||
|
||||
```yaml
|
||||
# _bmad/bmm/config.yaml
|
||||
project_name: enterprise-app
|
||||
user_skill_level: expert
|
||||
output_folder: docs/testing
|
||||
planning_artifacts: docs/planning
|
||||
implementation_artifacts: docs/implementation
|
||||
project_knowledge: docs
|
||||
tea_use_playwright_utils: true
|
||||
tea_use_mcp_enhancements: true
|
||||
```
|
||||
|
||||
**Prerequisites:**
|
||||
```bash
|
||||
npm install -D @seontechnologies/playwright-utils
|
||||
# Configure MCP servers in IDE
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
- Enterprise projects
|
||||
- Teams with established testing practices
|
||||
- Projects needing advanced TEA features
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -611,7 +622,7 @@ 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
|
||||
tea_use_playwright_utils: false # API tests don't need it
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -631,9 +642,9 @@ 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
|
||||
# TEA Configuration
|
||||
tea_use_playwright_utils: false # Set true if using @seontechnologies/playwright-utils
|
||||
tea_use_mcp_enhancements: false # Set true if MCP servers configured in IDE
|
||||
|
||||
# Languages
|
||||
communication_language: english
|
||||
|
|
@ -657,6 +668,74 @@ document_output_language: english
|
|||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### When should I enable playwright-utils?
|
||||
|
||||
**Enable if:**
|
||||
- You're using or planning to use `@seontechnologies/playwright-utils`
|
||||
- You want production-ready fixtures and utilities
|
||||
- Your team benefits from standardized patterns
|
||||
- You need utilities like `apiRequest`, `authSession`, `networkRecorder`
|
||||
|
||||
**Skip if:**
|
||||
- You're just learning TEA (keep it simple)
|
||||
- You have your own fixture library
|
||||
- You don't need the utilities
|
||||
|
||||
### When should I enable MCP enhancements?
|
||||
|
||||
**Enable if:**
|
||||
- You want live browser verification during test generation
|
||||
- You're debugging complex UI issues
|
||||
- You want exploratory mode in `*test-design`
|
||||
- You want recording mode in `*atdd` for accurate selectors
|
||||
|
||||
**Skip if:**
|
||||
- You're new to TEA (adds complexity)
|
||||
- You don't have MCP servers configured
|
||||
- Your tests work fine without it
|
||||
|
||||
### Can I change config after installation?
|
||||
|
||||
**Yes!** Edit `_bmad/bmm/config.yaml` anytime.
|
||||
|
||||
**Important:** Start fresh chat after config changes (TEA loads config at workflow start).
|
||||
|
||||
### Can I have different configs per branch?
|
||||
|
||||
**Yes:**
|
||||
```bash
|
||||
# feature branch
|
||||
git checkout feature/new-testing
|
||||
# Edit config for experimentation
|
||||
vim _bmad/bmm/config.yaml
|
||||
|
||||
# main branch
|
||||
git checkout main
|
||||
# Config reverts to main branch values
|
||||
```
|
||||
|
||||
Config is gitignored, so each branch can have different values.
|
||||
|
||||
### How do I share config with team?
|
||||
|
||||
**Use config.yaml.example:**
|
||||
```bash
|
||||
# Commit template
|
||||
cp _bmad/bmm/config.yaml _bmad/bmm/config.yaml.example
|
||||
git add _bmad/bmm/config.yaml.example
|
||||
git commit -m "docs: add BMad config template"
|
||||
```
|
||||
|
||||
**Team members copy template:**
|
||||
```bash
|
||||
cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml
|
||||
# Edit with their values
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
### How-To Guides
|
||||
|
|
|
|||
|
|
@ -167,10 +167,11 @@ Feature flag testing, contract testing, and API testing patterns.
|
|||
|
||||
### Playwright-Utils Integration
|
||||
|
||||
Patterns for using `@seontechnologies/playwright-utils` package (9 utilities).
|
||||
Patterns for using `@seontechnologies/playwright-utils` package (11 utilities).
|
||||
|
||||
| Fragment | Description | Key Topics |
|
||||
|----------|-------------|-----------|
|
||||
| overview | Playwright Utils installation, design principles, fixture patterns | Getting started, principles, setup |
|
||||
| [api-request](../../../src/modules/bmm/testarch/knowledge/api-request.md) | Typed HTTP client, schema validation, retry logic | API calls, HTTP, validation |
|
||||
| [auth-session](../../../src/modules/bmm/testarch/knowledge/auth-session.md) | Token persistence, multi-user, API/browser authentication | Auth patterns, session management |
|
||||
| [network-recorder](../../../src/modules/bmm/testarch/knowledge/network-recorder.md) | HAR record/playback, CRUD detection for offline testing | Offline testing, network replay |
|
||||
|
|
@ -180,8 +181,9 @@ Patterns for using `@seontechnologies/playwright-utils` package (9 utilities).
|
|||
| [file-utils](../../../src/modules/bmm/testarch/knowledge/file-utils.md) | CSV/XLSX/PDF/ZIP handling with download support | File validation, exports |
|
||||
| [burn-in](../../../src/modules/bmm/testarch/knowledge/burn-in.md) | Smart test selection with git diff analysis | CI optimization, selective testing |
|
||||
| [network-error-monitor](../../../src/modules/bmm/testarch/knowledge/network-error-monitor.md) | Auto-detect HTTP 4xx/5xx errors during tests | Error monitoring, silent failures |
|
||||
| [fixtures-composition](../../../src/modules/bmm/testarch/knowledge/fixtures-composition.md) | mergeTests composition patterns for combining utilities | Fixture merging, utility composition |
|
||||
|
||||
**Note:** `fixtures-composition` is listed under Architecture & Fixtures (general Playwright `mergeTests` pattern, applies to all fixtures).
|
||||
**Note:** All 11 playwright-utils fragments are in the same `knowledge/` directory as other fragments.
|
||||
|
||||
**Used in:** `*framework` (if `tea_use_playwright_utils: true`), `*atdd`, `*automate`, `*test-review`, `*ci`
|
||||
|
||||
|
|
@ -209,11 +211,51 @@ risk-governance,Risk Governance,Risk scoring and gate decisions,risk;governance,
|
|||
- `tags` - Searchable tags (semicolon-separated)
|
||||
- `fragment_file` - Relative path to fragment markdown file
|
||||
|
||||
**Fragment Location:** `src/modules/bmm/testarch/knowledge/` (all 33 fragments in single directory)
|
||||
## Fragment Locations
|
||||
|
||||
**Manifest:** `src/modules/bmm/testarch/tea-index.csv`
|
||||
**Knowledge Base Directory:**
|
||||
```
|
||||
src/modules/bmm/testarch/knowledge/
|
||||
├── api-request.md
|
||||
├── api-testing-patterns.md
|
||||
├── auth-session.md
|
||||
├── burn-in.md
|
||||
├── ci-burn-in.md
|
||||
├── component-tdd.md
|
||||
├── contract-testing.md
|
||||
├── data-factories.md
|
||||
├── email-auth.md
|
||||
├── error-handling.md
|
||||
├── feature-flags.md
|
||||
├── file-utils.md
|
||||
├── fixture-architecture.md
|
||||
├── fixtures-composition.md
|
||||
├── intercept-network-call.md
|
||||
├── log.md
|
||||
├── network-error-monitor.md
|
||||
├── network-first.md
|
||||
├── network-recorder.md
|
||||
├── nfr-criteria.md
|
||||
├── playwright-config.md
|
||||
├── probability-impact.md
|
||||
├── recurse.md
|
||||
├── risk-governance.md
|
||||
├── selector-resilience.md
|
||||
├── selective-testing.md
|
||||
├── test-healing-patterns.md
|
||||
├── test-levels-framework.md
|
||||
├── test-priorities-matrix.md
|
||||
├── test-quality.md
|
||||
├── timing-debugging.md
|
||||
└── visual-debugging.md
|
||||
```
|
||||
|
||||
---
|
||||
**All fragments in single directory** (no subfolders)
|
||||
|
||||
**Manifest:**
|
||||
```
|
||||
src/modules/bmm/testarch/tea-index.csv
|
||||
```
|
||||
|
||||
## Workflow Fragment Loading
|
||||
|
||||
|
|
@ -329,6 +371,207 @@ Each TEA workflow loads specific fragments:
|
|||
|
||||
---
|
||||
|
||||
## Key Fragments Explained
|
||||
|
||||
### test-quality.md
|
||||
|
||||
**What it covers:**
|
||||
- Execution time limits (< 1.5 minutes)
|
||||
- Test size limits (< 300 lines)
|
||||
- No hard waits (waitForTimeout banned)
|
||||
- No conditionals for flow control
|
||||
- No try-catch for flow control
|
||||
- Assertions must be explicit
|
||||
- Self-cleaning tests for parallel execution
|
||||
|
||||
**Why it matters:**
|
||||
This is the Definition of Done for test quality. All TEA workflows reference this for quality standards.
|
||||
|
||||
**Code examples:** 12+
|
||||
|
||||
---
|
||||
|
||||
### network-first.md
|
||||
|
||||
**What it covers:**
|
||||
- Intercept-before-navigate pattern
|
||||
- Wait for network responses, not timeouts
|
||||
- HAR capture for offline testing
|
||||
- Deterministic waiting strategies
|
||||
|
||||
**Why it matters:**
|
||||
Prevents 90% of test flakiness. Core pattern for reliable E2E tests.
|
||||
|
||||
**Code examples:** 15+
|
||||
|
||||
---
|
||||
|
||||
### fixture-architecture.md
|
||||
|
||||
**What it covers:**
|
||||
- Build pure functions first
|
||||
- Wrap in framework fixtures second
|
||||
- Compose with mergeTests
|
||||
- Enable reusability and testability
|
||||
|
||||
**Why it matters:**
|
||||
Foundation of scalable test architecture. Makes utilities reusable and unit-testable.
|
||||
|
||||
**Code examples:** 10+
|
||||
|
||||
---
|
||||
|
||||
### risk-governance.md
|
||||
|
||||
**What it covers:**
|
||||
- Risk scoring matrix (Probability × Impact)
|
||||
- Risk categories (TECH, SEC, PERF, DATA, BUS, OPS)
|
||||
- Gate decision rules (PASS/CONCERNS/FAIL/WAIVED)
|
||||
- Mitigation planning
|
||||
|
||||
**Why it matters:**
|
||||
Objective, data-driven release decisions. Removes politics from quality gates.
|
||||
|
||||
**Code examples:** 5
|
||||
|
||||
---
|
||||
|
||||
### test-priorities-matrix.md
|
||||
|
||||
**What it covers:**
|
||||
- P0: Critical path (100% coverage required)
|
||||
- P1: High value (90% coverage target)
|
||||
- P2: Medium value (50% coverage target)
|
||||
- P3: Low value (20% coverage target)
|
||||
- Execution ordering (P0 → P1 → P2 → P3)
|
||||
|
||||
**Why it matters:**
|
||||
Focus testing effort on what matters. Don't waste time on P3 edge cases.
|
||||
|
||||
**Code examples:** 8
|
||||
|
||||
---
|
||||
|
||||
## Using Fragments Directly
|
||||
|
||||
### As a Learning Resource
|
||||
|
||||
Read fragments to learn patterns:
|
||||
|
||||
```bash
|
||||
# Read fixture architecture pattern
|
||||
cat src/modules/bmm/testarch/knowledge/fixture-architecture.md
|
||||
|
||||
# Read network-first pattern
|
||||
cat src/modules/bmm/testarch/knowledge/network-first.md
|
||||
```
|
||||
|
||||
### As Team Guidelines
|
||||
|
||||
Use fragments as team documentation:
|
||||
|
||||
```markdown
|
||||
# Team Testing Guidelines
|
||||
|
||||
## Fixture Architecture
|
||||
See: src/modules/bmm/testarch/knowledge/fixture-architecture.md
|
||||
|
||||
All fixtures must follow the pure function → fixture wrapper pattern.
|
||||
|
||||
## Network Patterns
|
||||
See: src/modules/bmm/testarch/knowledge/network-first.md
|
||||
|
||||
All tests must use network-first patterns. No hard waits allowed.
|
||||
```
|
||||
|
||||
### As Code Review Checklist
|
||||
|
||||
Reference fragments in code review:
|
||||
|
||||
```markdown
|
||||
## PR Review Checklist
|
||||
|
||||
- [ ] Tests follow test-quality.md standards (no hard waits, < 300 lines)
|
||||
- [ ] Selectors follow selector-resilience.md (prefer getByRole)
|
||||
- [ ] Network patterns follow network-first.md (wait for responses)
|
||||
- [ ] Fixtures follow fixture-architecture.md (pure functions)
|
||||
```
|
||||
|
||||
## Fragment Statistics
|
||||
|
||||
**Total Fragments:** 33
|
||||
**Total Size:** ~600 KB (all fragments combined)
|
||||
**Average Fragment Size:** ~18 KB
|
||||
**Largest Fragment:** contract-testing.md (~28 KB)
|
||||
**Smallest Fragment:** burn-in.md (~7 KB)
|
||||
|
||||
**By Category:**
|
||||
- Architecture & Fixtures: 4 fragments
|
||||
- Data & Setup: 3 fragments
|
||||
- Network & Reliability: 4 fragments
|
||||
- Test Execution & CI: 3 fragments
|
||||
- Quality & Standards: 5 fragments
|
||||
- Risk & Gates: 3 fragments
|
||||
- Selectors & Timing: 3 fragments
|
||||
- Feature Flags & Patterns: 3 fragments
|
||||
- Playwright-Utils Integration: 8 fragments
|
||||
|
||||
**Note:** Statistics may drift with updates. All fragments are in the same `knowledge/` directory.
|
||||
|
||||
## Contributing to Knowledge Base
|
||||
|
||||
### Adding New Fragments
|
||||
|
||||
1. Create fragment in `src/modules/bmm/testarch/knowledge/`
|
||||
2. Follow existing format (Principle, Rationale, Pattern Examples)
|
||||
3. Add to `tea-index.csv` with metadata
|
||||
4. Update workflow instructions to load fragment
|
||||
5. Test with TEA workflow
|
||||
|
||||
### Updating Existing Fragments
|
||||
|
||||
1. Edit fragment markdown file
|
||||
2. Update `tea-index.csv` if metadata changes (line count, examples)
|
||||
3. Test with affected workflows
|
||||
4. Ensure no breaking changes to patterns
|
||||
|
||||
### Fragment Quality Standards
|
||||
|
||||
**Good fragment:**
|
||||
- Principle stated clearly
|
||||
- Rationale explains why
|
||||
- Multiple pattern examples with code
|
||||
- Good vs bad comparisons
|
||||
- Self-contained (links to other fragments minimal)
|
||||
|
||||
**Example structure:**
|
||||
```markdown
|
||||
# Fragment Name
|
||||
|
||||
## Principle
|
||||
[One sentence - what is this pattern?]
|
||||
|
||||
## Rationale
|
||||
[Why use this instead of alternatives?]
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Example 1: Basic Usage
|
||||
[Code example with explanation]
|
||||
|
||||
### Example 2: Advanced Pattern
|
||||
[Code example with explanation]
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Don't Do This
|
||||
[Bad code example]
|
||||
[Why it's bad]
|
||||
|
||||
## Related Patterns
|
||||
- [Other fragment](../other-fragment.md)
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [TEA Overview](/docs/explanation/features/tea-overview.md) - How knowledge base fits in TEA
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ You've just explored the features we'll test!
|
|||
|
||||
### Install BMad Method
|
||||
|
||||
Install BMad (see installation guide for latest command).
|
||||
```bash
|
||||
npx bmad-method@alpha install
|
||||
```
|
||||
|
||||
When prompted:
|
||||
- **Select modules:** Choose "BMM: BMad Method" (press Space, then Enter)
|
||||
|
|
@ -270,7 +272,7 @@ test('should mark todo as complete', async ({ page, apiRequest }) => {
|
|||
const { status, body: todo } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/todos',
|
||||
body: { title: 'Complete tutorial' }
|
||||
body: { title: 'Complete tutorial' } // 'body' not 'data'
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
|
|
@ -391,7 +393,7 @@ See [How to Run ATDD](/docs/how-to/workflows/run-atdd.md) for the TDD approach.
|
|||
|
||||
**Explanation** (understanding-oriented):
|
||||
- [TEA Overview](/docs/explanation/features/tea-overview.md) - Complete TEA capabilities
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why TEA exists** (problem + solution)
|
||||
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - Design philosophy
|
||||
- [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - How risk scoring works
|
||||
|
||||
**Reference** (quick lookup):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"fs-extra": "^11.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"ignore": "^7.0.5",
|
||||
"inquirer": "^9.3.8",
|
||||
"js-yaml": "^4.1.0",
|
||||
"ora": "^5.4.1",
|
||||
"semver": "^7.6.3",
|
||||
|
|
@ -33,7 +34,6 @@
|
|||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"@astrojs/starlight": "^0.37.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@eslint/js": "^9.33.0",
|
||||
"archiver": "^7.0.1",
|
||||
"astro": "^5.16.0",
|
||||
|
|
@ -755,29 +755,6 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
||||
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
|
||||
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/core": "0.5.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
|
|
@ -2020,6 +1997,36 @@
|
|||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/external-editor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
|
||||
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chardet": "^2.1.1",
|
||||
"iconv-lite": "^0.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/figures": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
|
||||
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
|
|
@ -3633,7 +3640,7 @@
|
|||
"version": "25.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
|
|
@ -4021,7 +4028,6 @@
|
|||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^0.21.3"
|
||||
|
|
@ -4037,7 +4043,6 @@
|
|||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
|
@ -5591,6 +5596,12 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chardet": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
|
||||
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
|
|
@ -5771,6 +5782,15 @@
|
|||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-width": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
||||
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
|
|
@ -8243,6 +8263,22 @@
|
|||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
|
||||
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
|
@ -8378,6 +8414,43 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "9.3.8",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz",
|
||||
"integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/external-editor": "^1.0.2",
|
||||
"@inquirer/figures": "^1.0.3",
|
||||
"ansi-escapes": "^4.3.2",
|
||||
"cli-width": "^4.1.0",
|
||||
"mute-stream": "1.0.0",
|
||||
"ora": "^5.4.1",
|
||||
"run-async": "^3.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^6.2.0",
|
||||
"yoctocolors-cjs": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/iron-webcrypto": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||||
|
|
@ -11496,6 +11569,15 @@
|
|||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nano-spawn": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
||||
|
|
@ -13218,6 +13300,15 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/run-async": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
|
||||
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
|
@ -13242,6 +13333,15 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -13262,6 +13362,12 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||
|
|
@ -14135,7 +14241,6 @@
|
|||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
|
|
@ -14220,7 +14325,7 @@
|
|||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
|
|
@ -15153,6 +15258,18 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors-cjs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||
"boxen": "^5.1.2",
|
||||
"chalk": "^4.1.2",
|
||||
|
|
@ -79,6 +78,7 @@
|
|||
"fs-extra": "^11.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"ignore": "^7.0.5",
|
||||
"inquirer": "^9.3.8",
|
||||
"js-yaml": "^4.1.0",
|
||||
"ora": "^5.4.1",
|
||||
"semver": "^7.6.3",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const path = require('node:path');
|
|||
const fs = require('node:fs');
|
||||
|
||||
// Fix for stdin issues when running through npm on Windows
|
||||
// Ensures keyboard interaction works properly with CLI prompts
|
||||
// Ensures keyboard interaction works properly with inquirer prompts
|
||||
if (process.stdin.isTTY) {
|
||||
try {
|
||||
process.stdin.resume();
|
||||
|
|
|
|||
|
|
@ -71,10 +71,14 @@ module.exports = {
|
|||
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
||||
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
||||
|
||||
const prompts = require('../lib/prompts');
|
||||
await prompts.text({
|
||||
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
||||
});
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'continue',
|
||||
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
||||
},
|
||||
]);
|
||||
|
||||
console.log('');
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ const yaml = require('yaml');
|
|||
const chalk = require('chalk');
|
||||
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
||||
let _inquirer = null;
|
||||
async function getInquirer() {
|
||||
if (!_inquirer) {
|
||||
_inquirer = (await import('inquirer')).default;
|
||||
}
|
||||
return _inquirer;
|
||||
}
|
||||
|
||||
class ConfigCollector {
|
||||
constructor() {
|
||||
|
|
@ -175,6 +183,7 @@ class ConfigCollector {
|
|||
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
||||
*/
|
||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||
const inquirer = await getInquirer();
|
||||
this.currentProjectDir = projectDir;
|
||||
|
||||
// Load existing config if not already loaded
|
||||
|
|
@ -350,7 +359,7 @@ class ConfigCollector {
|
|||
// Only show header if we actually have questions
|
||||
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
|
||||
console.log(); // Line break before questions
|
||||
const promptedAnswers = await prompts.prompt(questions);
|
||||
const promptedAnswers = await inquirer.prompt(questions);
|
||||
|
||||
// Merge prompted answers with static answers
|
||||
Object.assign(allAnswers, promptedAnswers);
|
||||
|
|
@ -493,6 +502,7 @@ class ConfigCollector {
|
|||
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
||||
*/
|
||||
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
||||
const inquirer = await getInquirer();
|
||||
this.currentProjectDir = projectDir;
|
||||
// Load existing config if needed and not already loaded
|
||||
if (!skipLoadExisting && !this.existingConfig) {
|
||||
|
|
@ -587,7 +597,7 @@ class ConfigCollector {
|
|||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||
let customize = true;
|
||||
if (moduleName !== 'core') {
|
||||
const customizeAnswer = await prompts.prompt([
|
||||
const customizeAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'customize',
|
||||
|
|
@ -604,7 +614,7 @@ class ConfigCollector {
|
|||
|
||||
if (questionsWithoutDefaults.length > 0) {
|
||||
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
|
||||
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
|
||||
const promptedAnswers = await inquirer.prompt(questionsWithoutDefaults);
|
||||
Object.assign(allAnswers, promptedAnswers);
|
||||
}
|
||||
|
||||
|
|
@ -618,7 +628,7 @@ class ConfigCollector {
|
|||
allAnswers[question.name] = question.default;
|
||||
}
|
||||
} else {
|
||||
const promptedAnswers = await prompts.prompt(questions);
|
||||
const promptedAnswers = await inquirer.prompt(questions);
|
||||
Object.assign(allAnswers, promptedAnswers);
|
||||
}
|
||||
}
|
||||
|
|
@ -740,7 +750,7 @@ class ConfigCollector {
|
|||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||
|
||||
// Ask user if they want to accept defaults or customize on the next line
|
||||
const { customize } = await prompts.prompt([
|
||||
const { customize } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'customize',
|
||||
|
|
@ -835,7 +845,7 @@ class ConfigCollector {
|
|||
}
|
||||
|
||||
/**
|
||||
* Build a prompt question from a config item
|
||||
* Build an inquirer question from a config item
|
||||
* @param {string} moduleName - Module name
|
||||
* @param {string} key - Config key
|
||||
* @param {Object} item - Config item definition
|
||||
|
|
@ -997,7 +1007,7 @@ class ConfigCollector {
|
|||
message: message,
|
||||
};
|
||||
|
||||
// Set default - if it's dynamic, use a function that the prompt will evaluate with current answers
|
||||
// Set default - if it's dynamic, use a function that inquirer will evaluate with current answers
|
||||
// But if we have an existing value, always use that instead
|
||||
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
|
||||
question.default = existingValue;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ const { CLIUtils } = require('../../../lib/cli-utils');
|
|||
const { ManifestGenerator } = require('./manifest-generator');
|
||||
const { IdeConfigManager } = require('./ide-config-manager');
|
||||
const { CustomHandler } = require('../custom/handler');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
// BMAD installation folder name - this is constant and should never change
|
||||
const BMAD_FOLDER_NAME = '_bmad';
|
||||
|
|
@ -759,9 +758,6 @@ class Installer {
|
|||
config.skipIde = toolSelection.skipIde;
|
||||
const ideConfigurations = toolSelection.configurations;
|
||||
|
||||
// Add spacing after prompts before installation progress
|
||||
console.log('');
|
||||
|
||||
if (spinner.isSpinning) {
|
||||
spinner.text = 'Continuing installation...';
|
||||
} else {
|
||||
|
|
@ -2143,11 +2139,15 @@ class Installer {
|
|||
* Private: Prompt for update action
|
||||
*/
|
||||
async promptUpdateAction() {
|
||||
const action = await prompts.select({
|
||||
message: 'What would you like to do?',
|
||||
choices: [{ name: 'Update existing installation', value: 'update' }],
|
||||
});
|
||||
return { action };
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
return await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: 'What would you like to do?',
|
||||
choices: [{ name: 'Update existing installation', value: 'update' }],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2156,6 +2156,8 @@ class Installer {
|
|||
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
||||
*/
|
||||
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
|
||||
console.log('');
|
||||
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
||||
console.log(chalk.yellow('─'.repeat(80)));
|
||||
|
|
@ -2170,22 +2172,26 @@ class Installer {
|
|||
console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
|
||||
console.log('');
|
||||
|
||||
const proceed = await prompts.select({
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Exit and clean up manually (recommended)',
|
||||
value: 'exit',
|
||||
hint: 'Exit installation',
|
||||
},
|
||||
{
|
||||
name: 'Continue with installation anyway',
|
||||
value: 'continue',
|
||||
hint: 'Continue',
|
||||
},
|
||||
],
|
||||
default: 'exit',
|
||||
});
|
||||
const { proceed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'proceed',
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Exit and clean up manually (recommended)',
|
||||
value: 'exit',
|
||||
short: 'Exit installation',
|
||||
},
|
||||
{
|
||||
name: 'Continue with installation anyway',
|
||||
value: 'continue',
|
||||
short: 'Continue',
|
||||
},
|
||||
],
|
||||
default: 'exit',
|
||||
},
|
||||
]);
|
||||
|
||||
if (proceed === 'exit') {
|
||||
console.log('');
|
||||
|
|
@ -2431,6 +2437,7 @@ class Installer {
|
|||
|
||||
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
||||
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
let keptCount = 0;
|
||||
let updatedCount = 0;
|
||||
let removedCount = 0;
|
||||
|
|
@ -2444,12 +2451,12 @@ class Installer {
|
|||
{
|
||||
name: 'Keep installed (will not be processed)',
|
||||
value: 'keep',
|
||||
hint: 'Keep',
|
||||
short: 'Keep',
|
||||
},
|
||||
{
|
||||
name: 'Specify new source location',
|
||||
value: 'update',
|
||||
hint: 'Update',
|
||||
short: 'Update',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -2458,40 +2465,47 @@ class Installer {
|
|||
choices.push({
|
||||
name: '⚠️ REMOVE module completely (destructive!)',
|
||||
value: 'remove',
|
||||
hint: 'Remove',
|
||||
short: 'Remove',
|
||||
});
|
||||
}
|
||||
|
||||
const action = await prompts.select({
|
||||
message: `How would you like to handle "${missing.name}"?`,
|
||||
choices,
|
||||
});
|
||||
const { action } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: `How would you like to handle "${missing.name}"?`,
|
||||
choices,
|
||||
},
|
||||
]);
|
||||
|
||||
switch (action) {
|
||||
case 'update': {
|
||||
// Use sync validation because @clack/prompts doesn't support async validate
|
||||
const newSourcePath = await prompts.text({
|
||||
message: 'Enter the new path to the custom module:',
|
||||
default: missing.sourcePath,
|
||||
validate: (input) => {
|
||||
if (!input || input.trim() === '') {
|
||||
return 'Please enter a path';
|
||||
}
|
||||
const expandedPath = path.resolve(input.trim());
|
||||
if (!fs.pathExistsSync(expandedPath)) {
|
||||
return 'Path does not exist';
|
||||
}
|
||||
// Check if it looks like a valid module
|
||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||
const agentsPath = path.join(expandedPath, 'agents');
|
||||
const workflowsPath = path.join(expandedPath, 'workflows');
|
||||
const { newSourcePath } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'newSourcePath',
|
||||
message: 'Enter the new path to the custom module:',
|
||||
default: missing.sourcePath,
|
||||
validate: async (input) => {
|
||||
if (!input || input.trim() === '') {
|
||||
return 'Please enter a path';
|
||||
}
|
||||
const expandedPath = path.resolve(input.trim());
|
||||
if (!(await fs.pathExists(expandedPath))) {
|
||||
return 'Path does not exist';
|
||||
}
|
||||
// Check if it looks like a valid module
|
||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||
const agentsPath = path.join(expandedPath, 'agents');
|
||||
const workflowsPath = path.join(expandedPath, 'workflows');
|
||||
|
||||
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
|
||||
return 'Path does not appear to contain a valid custom module';
|
||||
}
|
||||
return; // clack expects undefined for valid input
|
||||
if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
|
||||
return 'Path does not appear to contain a valid custom module';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
]);
|
||||
|
||||
// Update the source in manifest
|
||||
const resolvedPath = path.resolve(newSourcePath.trim());
|
||||
|
|
@ -2517,38 +2531,46 @@ class Installer {
|
|||
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
||||
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
||||
|
||||
const confirmDelete = await prompts.confirm({
|
||||
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
||||
default: false,
|
||||
});
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirmDelete) {
|
||||
const typedConfirm = await prompts.text({
|
||||
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
||||
validate: (input) => {
|
||||
if (input !== 'DELETE') {
|
||||
return chalk.red('You must type "DELETE" exactly to proceed');
|
||||
}
|
||||
return; // clack expects undefined for valid input
|
||||
if (confirm) {
|
||||
const { typedConfirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'typedConfirm',
|
||||
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
||||
validate: (input) => {
|
||||
if (input !== 'DELETE') {
|
||||
return chalk.red('You must type "DELETE" exactly to proceed');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
]);
|
||||
|
||||
if (typedConfirm === 'DELETE') {
|
||||
// Remove the module from filesystem and manifest
|
||||
const modulePath = path.join(bmadDir, missing.id);
|
||||
const modulePath = path.join(bmadDir, moduleId);
|
||||
if (await fs.pathExists(modulePath)) {
|
||||
const fsExtra = require('fs-extra');
|
||||
await fsExtra.remove(modulePath);
|
||||
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
|
||||
}
|
||||
|
||||
await this.manifest.removeModule(bmadDir, missing.id);
|
||||
await this.manifest.removeCustomModule(bmadDir, missing.id);
|
||||
await this.manifest.removeModule(bmadDir, moduleId);
|
||||
await this.manifest.removeCustomModule(bmadDir, moduleId);
|
||||
console.log(chalk.yellow(` ✓ Removed from manifest`));
|
||||
|
||||
// Also remove from installedModules list
|
||||
if (installedModules && installedModules.includes(missing.id)) {
|
||||
const index = installedModules.indexOf(missing.id);
|
||||
if (installedModules && installedModules.includes(moduleId)) {
|
||||
const index = installedModules.indexOf(moduleId);
|
||||
if (index !== -1) {
|
||||
installedModules.splice(index, 1);
|
||||
}
|
||||
|
|
@ -2569,7 +2591,7 @@ class Installer {
|
|||
}
|
||||
case 'keep': {
|
||||
keptCount++;
|
||||
keptModulesWithoutSources.push(missing.id);
|
||||
keptModulesWithoutSources.push(moduleId);
|
||||
console.log(chalk.dim(` Module will be kept as-is`));
|
||||
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const {
|
|||
resolveSubagentFiles,
|
||||
} = require('./shared/module-injections');
|
||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Google Antigravity IDE setup handler
|
||||
|
|
@ -27,21 +26,6 @@ class AntigravitySetup extends BaseIdeSetup {
|
|||
this.workflowsDir = 'workflows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for subagent installation location
|
||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
||||
*/
|
||||
async _promptInstallLocation() {
|
||||
return prompts.select({
|
||||
message: 'Where would you like to install Antigravity subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configuration choices before installation
|
||||
* @param {Object} options - Configuration options
|
||||
|
|
@ -73,7 +57,21 @@ class AntigravitySetup extends BaseIdeSetup {
|
|||
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||
|
||||
if (config.subagentChoices.install !== 'none') {
|
||||
config.installLocation = await this._promptInstallLocation();
|
||||
// Ask for installation location
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
const locationAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'location',
|
||||
message: 'Where would you like to install Antigravity subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
},
|
||||
]);
|
||||
config.installLocation = locationAnswer.location;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -299,7 +297,20 @@ class AntigravitySetup extends BaseIdeSetup {
|
|||
choices = await this.promptSubagentInstallation(config.subagents);
|
||||
|
||||
if (choices.install !== 'none') {
|
||||
location = await this._promptInstallLocation();
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
const locationAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'location',
|
||||
message: 'Where would you like to install Antigravity subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
},
|
||||
]);
|
||||
location = locationAnswer.location;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,16 +334,22 @@ class AntigravitySetup extends BaseIdeSetup {
|
|||
* Prompt user for subagent installation preferences
|
||||
*/
|
||||
async promptSubagentInstallation(subagentConfig) {
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
|
||||
// First ask if they want to install subagents
|
||||
const install = await prompts.select({
|
||||
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
||||
choices: [
|
||||
{ name: 'Yes, install all subagents', value: 'all' },
|
||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||
{ name: 'No, skip subagent installation', value: 'none' },
|
||||
],
|
||||
default: 'all',
|
||||
});
|
||||
const { install } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'install',
|
||||
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
||||
choices: [
|
||||
{ name: 'Yes, install all subagents', value: 'all' },
|
||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||
{ name: 'No, skip subagent installation', value: 'none' },
|
||||
],
|
||||
default: 'all',
|
||||
},
|
||||
]);
|
||||
|
||||
if (install === 'selective') {
|
||||
// Show list of available subagents with descriptions
|
||||
|
|
@ -344,14 +361,18 @@ class AntigravitySetup extends BaseIdeSetup {
|
|||
'document-reviewer.md': 'Document quality review',
|
||||
};
|
||||
|
||||
const selected = await prompts.multiselect({
|
||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||
choices: subagentConfig.files.map((file) => ({
|
||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||
value: file,
|
||||
checked: true,
|
||||
})),
|
||||
});
|
||||
const { selected } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'selected',
|
||||
message: 'Select subagents to install:',
|
||||
choices: subagentConfig.files.map((file) => ({
|
||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||
value: file,
|
||||
checked: true,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
return { install: 'selective', selected };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const {
|
|||
resolveSubagentFiles,
|
||||
} = require('./shared/module-injections');
|
||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Claude Code IDE setup handler
|
||||
|
|
@ -26,21 +25,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||
this.agentsDir = 'agents';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for subagent installation location
|
||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
||||
*/
|
||||
async promptInstallLocation() {
|
||||
return prompts.select({
|
||||
message: 'Where would you like to install Claude Code subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configuration choices before installation
|
||||
* @param {Object} options - Configuration options
|
||||
|
|
@ -72,7 +56,21 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||
|
||||
if (config.subagentChoices.install !== 'none') {
|
||||
config.installLocation = await this.promptInstallLocation();
|
||||
// Ask for installation location
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
const locationAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'location',
|
||||
message: 'Where would you like to install Claude Code subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
},
|
||||
]);
|
||||
config.installLocation = locationAnswer.location;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -307,7 +305,20 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||
choices = await this.promptSubagentInstallation(config.subagents);
|
||||
|
||||
if (choices.install !== 'none') {
|
||||
location = await this.promptInstallLocation();
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
const locationAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'location',
|
||||
message: 'Where would you like to install Claude Code subagents?',
|
||||
choices: [
|
||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||
],
|
||||
default: 'project',
|
||||
},
|
||||
]);
|
||||
location = locationAnswer.location;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,16 +342,22 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||
* Prompt user for subagent installation preferences
|
||||
*/
|
||||
async promptSubagentInstallation(subagentConfig) {
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
|
||||
// First ask if they want to install subagents
|
||||
const install = await prompts.select({
|
||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
||||
choices: [
|
||||
{ name: 'Yes, install all subagents', value: 'all' },
|
||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||
{ name: 'No, skip subagent installation', value: 'none' },
|
||||
],
|
||||
default: 'all',
|
||||
});
|
||||
const { install } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'install',
|
||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
||||
choices: [
|
||||
{ name: 'Yes, install all subagents', value: 'all' },
|
||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||
{ name: 'No, skip subagent installation', value: 'none' },
|
||||
],
|
||||
default: 'all',
|
||||
},
|
||||
]);
|
||||
|
||||
if (install === 'selective') {
|
||||
// Show list of available subagents with descriptions
|
||||
|
|
@ -352,14 +369,18 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||
'document-reviewer.md': 'Document quality review',
|
||||
};
|
||||
|
||||
const selected = await prompts.multiselect({
|
||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||
options: subagentConfig.files.map((file) => ({
|
||||
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||
value: file,
|
||||
})),
|
||||
initialValues: subagentConfig.files,
|
||||
});
|
||||
const { selected } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'selected',
|
||||
message: 'Select subagents to install:',
|
||||
choices: subagentConfig.files.map((file) => ({
|
||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||
value: file,
|
||||
checked: true,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
return { install: 'selective', selected };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ const { BaseIdeSetup } = require('./_base-ide');
|
|||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Codex setup handler (CLI mode)
|
||||
|
|
@ -22,24 +21,32 @@ class CodexSetup extends BaseIdeSetup {
|
|||
* @returns {Object} Collected configuration
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
|
||||
let confirmed = false;
|
||||
let installLocation = 'global';
|
||||
|
||||
while (!confirmed) {
|
||||
installLocation = await prompts.select({
|
||||
message: 'Where would you like to install Codex CLI prompts?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
||||
value: 'global',
|
||||
},
|
||||
{
|
||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
||||
value: 'project',
|
||||
},
|
||||
],
|
||||
default: 'global',
|
||||
});
|
||||
const { location } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'location',
|
||||
message: 'Where would you like to install Codex CLI prompts?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
||||
value: 'global',
|
||||
},
|
||||
{
|
||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
||||
value: 'project',
|
||||
},
|
||||
],
|
||||
default: 'global',
|
||||
},
|
||||
]);
|
||||
|
||||
installLocation = location;
|
||||
|
||||
// Display detailed instructions for the chosen option
|
||||
console.log('');
|
||||
|
|
@ -50,10 +57,16 @@ class CodexSetup extends BaseIdeSetup {
|
|||
}
|
||||
|
||||
// Confirm the choice
|
||||
confirmed = await prompts.confirm({
|
||||
message: 'Proceed with this installation option?',
|
||||
default: true,
|
||||
});
|
||||
const { proceed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Proceed with this installation option?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
confirmed = proceed;
|
||||
|
||||
if (!confirmed) {
|
||||
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ const path = require('node:path');
|
|||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* GitHub Copilot setup handler
|
||||
|
|
@ -22,23 +21,29 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
|||
* @returns {Object} Collected configuration
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
const { default: inquirer } = await import('inquirer');
|
||||
const config = {};
|
||||
|
||||
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
||||
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
||||
|
||||
config.vsCodeConfig = await prompts.select({
|
||||
message: 'How would you like to configure VS Code settings?',
|
||||
choices: [
|
||||
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
||||
{ name: 'Configure each setting manually', value: 'manual' },
|
||||
{ name: 'Skip settings configuration', value: 'skip' },
|
||||
],
|
||||
default: 'defaults',
|
||||
});
|
||||
const response = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'configChoice',
|
||||
message: 'How would you like to configure VS Code settings?',
|
||||
choices: [
|
||||
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
||||
{ name: 'Configure each setting manually', value: 'manual' },
|
||||
{ name: 'Skip settings configuration', value: 'skip' },
|
||||
],
|
||||
default: 'defaults',
|
||||
},
|
||||
]);
|
||||
config.vsCodeConfig = response.configChoice;
|
||||
|
||||
if (config.vsCodeConfig === 'manual') {
|
||||
config.manualSettings = await prompts.prompt([
|
||||
if (response.configChoice === 'manual') {
|
||||
config.manualSettings = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'maxRequests',
|
||||
|
|
@ -47,8 +52,7 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
|||
validate: (input) => {
|
||||
const num = parseInt(input, 10);
|
||||
if (isNaN(num)) return 'Enter a valid number 1-50';
|
||||
if (num < 1 || num > 50) return 'Enter a number between 1-50';
|
||||
return true;
|
||||
return (num >= 1 && num <= 50) || 'Enter 1-50';
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,432 +0,0 @@
|
|||
/**
|
||||
* @clack/prompts wrapper for BMAD CLI
|
||||
*
|
||||
* This module provides a unified interface for CLI prompts using @clack/prompts.
|
||||
* It replaces Inquirer.js to fix Windows arrow key navigation issues (libuv #852).
|
||||
*
|
||||
* @module prompts
|
||||
*/
|
||||
|
||||
let _clack = null;
|
||||
|
||||
/**
|
||||
* Lazy-load @clack/prompts (ESM module)
|
||||
* @returns {Promise<Object>} The clack prompts module
|
||||
*/
|
||||
async function getClack() {
|
||||
if (!_clack) {
|
||||
_clack = await import('@clack/prompts');
|
||||
}
|
||||
return _clack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user cancellation gracefully
|
||||
* @param {any} value - The value to check
|
||||
* @param {string} [message='Operation cancelled'] - Message to display
|
||||
* @returns {boolean} True if cancelled
|
||||
*/
|
||||
async function handleCancel(value, message = 'Operation cancelled') {
|
||||
const clack = await getClack();
|
||||
if (clack.isCancel(value)) {
|
||||
clack.cancel(message);
|
||||
process.exit(0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display intro message
|
||||
* @param {string} message - The intro message
|
||||
*/
|
||||
async function intro(message) {
|
||||
const clack = await getClack();
|
||||
clack.intro(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display outro message
|
||||
* @param {string} message - The outro message
|
||||
*/
|
||||
async function outro(message) {
|
||||
const clack = await getClack();
|
||||
clack.outro(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a note/info box
|
||||
* @param {string} message - The note content
|
||||
* @param {string} [title] - Optional title
|
||||
*/
|
||||
async function note(message, title) {
|
||||
const clack = await getClack();
|
||||
clack.note(message, title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a spinner for async operations
|
||||
* @returns {Object} Spinner controller with start, stop, message methods
|
||||
*/
|
||||
async function spinner() {
|
||||
const clack = await getClack();
|
||||
return clack.spinner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-select prompt (replaces Inquirer 'list' type)
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {Array} options.choices - Array of choices [{name, value, hint?}]
|
||||
* @param {any} [options.default] - Default selected value
|
||||
* @returns {Promise<any>} Selected value
|
||||
*/
|
||||
async function select(options) {
|
||||
const clack = await getClack();
|
||||
|
||||
// Convert Inquirer-style choices to clack format
|
||||
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
||||
const clackOptions = options.choices
|
||||
.filter((c) => c.type !== 'separator') // Skip separators for now
|
||||
.map((choice) => {
|
||||
if (typeof choice === 'string' || typeof choice === 'number') {
|
||||
return { value: choice, label: String(choice) };
|
||||
}
|
||||
return {
|
||||
value: choice.value === undefined ? choice.name : choice.value,
|
||||
label: choice.name || choice.label || String(choice.value),
|
||||
hint: choice.hint || choice.description,
|
||||
};
|
||||
});
|
||||
|
||||
// Find initial value
|
||||
let initialValue;
|
||||
if (options.default !== undefined) {
|
||||
initialValue = options.default;
|
||||
}
|
||||
|
||||
const result = await clack.select({
|
||||
message: options.message,
|
||||
options: clackOptions,
|
||||
initialValue,
|
||||
});
|
||||
|
||||
await handleCancel(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-select prompt (replaces Inquirer 'checkbox' type)
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {Array} options.choices - Array of choices [{name, value, checked?, hint?}]
|
||||
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
||||
* @returns {Promise<Array>} Array of selected values
|
||||
*/
|
||||
async function multiselect(options) {
|
||||
const clack = await getClack();
|
||||
|
||||
// Support both clack-native (options) and Inquirer-style (choices) APIs
|
||||
let clackOptions;
|
||||
let initialValues;
|
||||
|
||||
if (options.options) {
|
||||
// Native clack format: options with label/value
|
||||
clackOptions = options.options;
|
||||
initialValues = options.initialValues || [];
|
||||
} else {
|
||||
// Convert Inquirer-style choices to clack format
|
||||
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
||||
clackOptions = options.choices
|
||||
.filter((c) => c.type !== 'separator') // Skip separators
|
||||
.map((choice) => {
|
||||
if (typeof choice === 'string' || typeof choice === 'number') {
|
||||
return { value: choice, label: String(choice) };
|
||||
}
|
||||
return {
|
||||
value: choice.value === undefined ? choice.name : choice.value,
|
||||
label: choice.name || choice.label || String(choice.value),
|
||||
hint: choice.hint || choice.description,
|
||||
};
|
||||
});
|
||||
|
||||
// Find initial values (pre-checked items)
|
||||
initialValues = options.choices
|
||||
.filter((c) => c.checked && c.type !== 'separator')
|
||||
.map((c) => (c.value === undefined ? c.name : c.value));
|
||||
}
|
||||
|
||||
const result = await clack.multiselect({
|
||||
message: options.message,
|
||||
options: clackOptions,
|
||||
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||
required: options.required || false,
|
||||
});
|
||||
|
||||
await handleCancel(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped multi-select prompt for categorized options
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {Object} options.options - Object mapping group names to arrays of choices
|
||||
* @param {Array} [options.initialValues] - Array of initially selected values
|
||||
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
||||
* @param {boolean} [options.selectableGroups=false] - Whether groups can be selected as a whole
|
||||
* @returns {Promise<Array>} Array of selected values
|
||||
*/
|
||||
async function groupMultiselect(options) {
|
||||
const clack = await getClack();
|
||||
|
||||
const result = await clack.groupMultiselect({
|
||||
message: options.message,
|
||||
options: options.options,
|
||||
initialValues: options.initialValues,
|
||||
required: options.required || false,
|
||||
});
|
||||
|
||||
await handleCancel(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm prompt (replaces Inquirer 'confirm' type)
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {boolean} [options.default=true] - Default value
|
||||
* @returns {Promise<boolean>} User's answer
|
||||
*/
|
||||
async function confirm(options) {
|
||||
const clack = await getClack();
|
||||
|
||||
const result = await clack.confirm({
|
||||
message: options.message,
|
||||
initialValue: options.default === undefined ? true : options.default,
|
||||
});
|
||||
|
||||
await handleCancel(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text input prompt (replaces Inquirer 'input' type)
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {string} [options.default] - Default value
|
||||
* @param {string} [options.placeholder] - Placeholder text (defaults to options.default if not provided)
|
||||
* @param {Function} [options.validate] - Validation function
|
||||
* @returns {Promise<string>} User's input
|
||||
*/
|
||||
async function text(options) {
|
||||
const clack = await getClack();
|
||||
|
||||
// Use default as placeholder if placeholder not explicitly provided
|
||||
// This shows the default value as grayed-out hint text
|
||||
const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
|
||||
|
||||
const result = await clack.text({
|
||||
message: options.message,
|
||||
defaultValue: options.default,
|
||||
placeholder: typeof placeholder === 'string' ? placeholder : undefined,
|
||||
validate: options.validate,
|
||||
});
|
||||
|
||||
await handleCancel(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Password input prompt (replaces Inquirer 'password' type)
|
||||
* @param {Object} options - Prompt options
|
||||
* @param {string} options.message - The question to ask
|
||||
* @param {Function} [options.validate] - Validation function
|
||||
* @returns {Promise<string>} User's input
|
||||
*/
|
||||
async function password(options) {
|
||||
const clack = await getClack();
|
||||
|
||||
const result = await clack.password({
|
||||
message: options.message,
|
||||
validate: options.validate,
|
||||
});
|
||||
|
||||
await handleCancel(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group multiple prompts together
|
||||
* @param {Object} prompts - Object of prompt functions
|
||||
* @param {Object} [options] - Group options
|
||||
* @returns {Promise<Object>} Object with all answers
|
||||
*/
|
||||
async function group(prompts, options = {}) {
|
||||
const clack = await getClack();
|
||||
|
||||
const result = await clack.group(prompts, {
|
||||
onCancel: () => {
|
||||
clack.cancel('Operation cancelled');
|
||||
process.exit(0);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run tasks with spinner feedback
|
||||
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function tasks(taskList) {
|
||||
const clack = await getClack();
|
||||
await clack.tasks(taskList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log messages with styling
|
||||
*/
|
||||
const log = {
|
||||
async info(message) {
|
||||
const clack = await getClack();
|
||||
clack.log.info(message);
|
||||
},
|
||||
async success(message) {
|
||||
const clack = await getClack();
|
||||
clack.log.success(message);
|
||||
},
|
||||
async warn(message) {
|
||||
const clack = await getClack();
|
||||
clack.log.warn(message);
|
||||
},
|
||||
async error(message) {
|
||||
const clack = await getClack();
|
||||
clack.log.error(message);
|
||||
},
|
||||
async message(message) {
|
||||
const clack = await getClack();
|
||||
clack.log.message(message);
|
||||
},
|
||||
async step(message) {
|
||||
const clack = await getClack();
|
||||
clack.log.step(message);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute an array of Inquirer-style questions using @clack/prompts
|
||||
* This provides compatibility with dynamic question arrays
|
||||
* @param {Array} questions - Array of Inquirer-style question objects
|
||||
* @returns {Promise<Object>} Object with answers keyed by question name
|
||||
*/
|
||||
async function prompt(questions) {
|
||||
const answers = {};
|
||||
|
||||
for (const question of questions) {
|
||||
const { type, name, message, choices, default: defaultValue, validate, when } = question;
|
||||
|
||||
// Handle conditional questions via 'when' property
|
||||
if (when !== undefined) {
|
||||
const shouldAsk = typeof when === 'function' ? await when(answers) : when;
|
||||
if (!shouldAsk) continue;
|
||||
}
|
||||
|
||||
let answer;
|
||||
|
||||
switch (type) {
|
||||
case 'input': {
|
||||
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
||||
answer = await text({
|
||||
message,
|
||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||
validate: validate
|
||||
? (val) => {
|
||||
const result = validate(val, answers);
|
||||
if (result instanceof Promise) {
|
||||
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
||||
}
|
||||
return result === true ? undefined : result;
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'confirm': {
|
||||
answer = await confirm({
|
||||
message,
|
||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
answer = await select({
|
||||
message,
|
||||
choices: choices || [],
|
||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'checkbox': {
|
||||
answer = await multiselect({
|
||||
message,
|
||||
choices: choices || [],
|
||||
required: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'password': {
|
||||
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
||||
answer = await password({
|
||||
message,
|
||||
validate: validate
|
||||
? (val) => {
|
||||
const result = validate(val, answers);
|
||||
if (result instanceof Promise) {
|
||||
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
||||
}
|
||||
return result === true ? undefined : result;
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Default to text input for unknown types
|
||||
answer = await text({
|
||||
message,
|
||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
answers[name] = answer;
|
||||
}
|
||||
|
||||
return answers;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getClack,
|
||||
handleCancel,
|
||||
intro,
|
||||
outro,
|
||||
note,
|
||||
spinner,
|
||||
select,
|
||||
multiselect,
|
||||
groupMultiselect,
|
||||
confirm,
|
||||
text,
|
||||
password,
|
||||
group,
|
||||
tasks,
|
||||
log,
|
||||
prompt,
|
||||
};
|
||||
|
|
@ -4,21 +4,16 @@ const os = require('node:os');
|
|||
const fs = require('fs-extra');
|
||||
const { CLIUtils } = require('./cli-utils');
|
||||
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||
const prompts = require('./prompts');
|
||||
|
||||
// Separator class for visual grouping in select/multiselect prompts
|
||||
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
||||
class Separator {
|
||||
constructor(text = '────────') {
|
||||
this.line = text;
|
||||
this.name = text;
|
||||
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
||||
let _inquirer = null;
|
||||
async function getInquirer() {
|
||||
if (!_inquirer) {
|
||||
_inquirer = (await import('inquirer')).default;
|
||||
}
|
||||
type = 'separator';
|
||||
return _inquirer;
|
||||
}
|
||||
|
||||
// Separator for choice lists (compatible interface)
|
||||
const choiceUtils = { Separator };
|
||||
|
||||
/**
|
||||
* UI utilities for the installer
|
||||
*/
|
||||
|
|
@ -28,6 +23,7 @@ class UI {
|
|||
* @returns {Object} Installation configuration
|
||||
*/
|
||||
async promptInstall() {
|
||||
const inquirer = await getInquirer();
|
||||
CLIUtils.displayLogo();
|
||||
|
||||
// Display version-specific start message from install-messages.yaml
|
||||
|
|
@ -117,20 +113,26 @@ class UI {
|
|||
console.log(chalk.yellow('─'.repeat(80)));
|
||||
console.log('');
|
||||
|
||||
const proceed = await prompts.select({
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Cancel and do a fresh install (recommended)',
|
||||
value: 'cancel',
|
||||
},
|
||||
{
|
||||
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
|
||||
value: 'proceed',
|
||||
},
|
||||
],
|
||||
default: 'cancel',
|
||||
});
|
||||
const { proceed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'proceed',
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Cancel and do a fresh install (recommended)',
|
||||
value: 'cancel',
|
||||
short: 'Cancel installation',
|
||||
},
|
||||
{
|
||||
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
|
||||
value: 'proceed',
|
||||
short: 'Proceed with update',
|
||||
},
|
||||
],
|
||||
default: 'cancel',
|
||||
},
|
||||
]);
|
||||
|
||||
if (proceed === 'cancel') {
|
||||
console.log('');
|
||||
|
|
@ -186,10 +188,14 @@ class UI {
|
|||
|
||||
// If Claude Code was selected, ask about TTS
|
||||
if (claudeCodeSelected) {
|
||||
const enableTts = await prompts.confirm({
|
||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||
default: false,
|
||||
});
|
||||
const { enableTts } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'enableTts',
|
||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (enableTts) {
|
||||
agentVibesConfig = { enabled: true, alreadyInstalled: false };
|
||||
|
|
@ -244,11 +250,18 @@ class UI {
|
|||
// Common actions
|
||||
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
|
||||
|
||||
actionType = await prompts.select({
|
||||
message: 'What would you like to do?',
|
||||
choices: choices,
|
||||
default: choices[0].value,
|
||||
});
|
||||
const promptResult = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'actionType',
|
||||
message: 'What would you like to do?',
|
||||
choices: choices,
|
||||
default: choices[0].value, // Use the first option as default
|
||||
},
|
||||
]);
|
||||
|
||||
// Extract actionType from prompt result
|
||||
actionType = promptResult.actionType;
|
||||
|
||||
// Handle quick update separately
|
||||
if (actionType === 'quick-update') {
|
||||
|
|
@ -277,10 +290,14 @@ class UI {
|
|||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||
|
||||
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
|
||||
const changeModuleSelection = await prompts.confirm({
|
||||
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||
default: false,
|
||||
});
|
||||
const { changeModuleSelection } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'changeModuleSelection',
|
||||
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
let selectedModules = [];
|
||||
if (changeModuleSelection) {
|
||||
|
|
@ -293,10 +310,14 @@ class UI {
|
|||
|
||||
// After module selection, ask about custom modules
|
||||
console.log('');
|
||||
const changeCustomModules = await prompts.confirm({
|
||||
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
|
||||
default: false,
|
||||
});
|
||||
const { changeCustomModules } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'changeCustomModules',
|
||||
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
|
||||
if (changeCustomModules) {
|
||||
|
|
@ -331,10 +352,15 @@ class UI {
|
|||
let enableTts = false;
|
||||
|
||||
if (hasClaudeCode) {
|
||||
enableTts = await prompts.confirm({
|
||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||
default: false,
|
||||
});
|
||||
const { enableTts: enable } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'enableTts',
|
||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
enableTts = enable;
|
||||
}
|
||||
|
||||
// Core config with existing defaults (ask after TTS)
|
||||
|
|
@ -359,10 +385,14 @@ class UI {
|
|||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||
|
||||
// Ask about official modules for new installations
|
||||
const wantsOfficialModules = await prompts.confirm({
|
||||
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||
default: true,
|
||||
});
|
||||
const { wantsOfficialModules } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'wantsOfficialModules',
|
||||
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
let selectedOfficialModules = [];
|
||||
if (wantsOfficialModules) {
|
||||
|
|
@ -371,10 +401,14 @@ class UI {
|
|||
}
|
||||
|
||||
// Ask about custom content
|
||||
const wantsCustomContent = await prompts.confirm({
|
||||
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
||||
default: false,
|
||||
});
|
||||
const { wantsCustomContent } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'wantsCustomContent',
|
||||
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (wantsCustomContent) {
|
||||
customContentConfig = await this.promptCustomContentSource();
|
||||
|
|
@ -425,6 +459,7 @@ class UI {
|
|||
* @returns {Object} Tool configuration
|
||||
*/
|
||||
async promptToolSelection(projectDir, selectedModules) {
|
||||
const inquirer = await getInquirer();
|
||||
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
||||
const { Detector } = require('../installers/lib/core/detector');
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
|
|
@ -442,14 +477,13 @@ class UI {
|
|||
const preferredIdes = ideManager.getPreferredIdes();
|
||||
const otherIdes = ideManager.getOtherIdes();
|
||||
|
||||
// Build grouped options object for groupMultiselect
|
||||
const groupedOptions = {};
|
||||
// Build IDE choices array with separators
|
||||
const ideChoices = [];
|
||||
const processedIdes = new Set();
|
||||
const initialValues = [];
|
||||
|
||||
// First, add previously configured IDEs at the top, marked with ✅
|
||||
if (configuredIdes.length > 0) {
|
||||
const configuredGroup = [];
|
||||
ideChoices.push(new inquirer.Separator('── Previously Configured ──'));
|
||||
for (const ideValue of configuredIdes) {
|
||||
// Skip empty or invalid IDE values
|
||||
if (!ideValue || typeof ideValue !== 'string') {
|
||||
|
|
@ -462,71 +496,81 @@ class UI {
|
|||
const ide = preferredIde || otherIde;
|
||||
|
||||
if (ide) {
|
||||
configuredGroup.push({
|
||||
label: `${ide.name} ✅`,
|
||||
ideChoices.push({
|
||||
name: `${ide.name} ✅`,
|
||||
value: ide.value,
|
||||
checked: true, // Previously configured IDEs are checked by default
|
||||
});
|
||||
processedIdes.add(ide.value);
|
||||
initialValues.push(ide.value); // Pre-select configured IDEs
|
||||
} else {
|
||||
// Warn about unrecognized IDE (but don't fail)
|
||||
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
|
||||
}
|
||||
}
|
||||
if (configuredGroup.length > 0) {
|
||||
groupedOptions['Previously Configured'] = configuredGroup;
|
||||
}
|
||||
}
|
||||
|
||||
// Add preferred tools (excluding already processed)
|
||||
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
|
||||
if (remainingPreferred.length > 0) {
|
||||
groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
|
||||
processedIdes.add(ide.value);
|
||||
return {
|
||||
label: `${ide.name} ⭐`,
|
||||
ideChoices.push(new inquirer.Separator('── Recommended Tools ──'));
|
||||
for (const ide of remainingPreferred) {
|
||||
ideChoices.push({
|
||||
name: `${ide.name} ⭐`,
|
||||
value: ide.value,
|
||||
};
|
||||
});
|
||||
checked: false,
|
||||
});
|
||||
processedIdes.add(ide.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Add other tools (excluding already processed)
|
||||
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value));
|
||||
if (remainingOther.length > 0) {
|
||||
groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({
|
||||
label: ide.name,
|
||||
value: ide.value,
|
||||
}));
|
||||
ideChoices.push(new inquirer.Separator('── Additional Tools ──'));
|
||||
for (const ide of remainingOther) {
|
||||
ideChoices.push({
|
||||
name: ide.name,
|
||||
value: ide.value,
|
||||
checked: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let selectedIdes = [];
|
||||
let answers;
|
||||
let userConfirmedNoTools = false;
|
||||
|
||||
// Loop until user selects at least one tool OR explicitly confirms no tools
|
||||
while (!userConfirmedNoTools) {
|
||||
selectedIdes = await prompts.groupMultiselect({
|
||||
message: `Select tools to configure ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||
options: groupedOptions,
|
||||
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||
required: false,
|
||||
});
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'ides',
|
||||
message: 'Select tools to configure:',
|
||||
choices: ideChoices,
|
||||
pageSize: 30,
|
||||
},
|
||||
]);
|
||||
|
||||
// If tools were selected, we're done
|
||||
if (selectedIdes && selectedIdes.length > 0) {
|
||||
if (answers.ides && answers.ides.length > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Warn that no tools were selected - users often miss the spacebar requirement
|
||||
console.log();
|
||||
console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!'));
|
||||
console.log(chalk.red(' You must press SPACE to select items, then ENTER to confirm.'));
|
||||
console.log(chalk.red(' You must press SPACEBAR to select items, then ENTER to confirm.'));
|
||||
console.log(chalk.red(' Simply highlighting an item does NOT select it.'));
|
||||
console.log();
|
||||
|
||||
const goBack = await prompts.confirm({
|
||||
message: chalk.yellow('Would you like to go back and select at least one tool?'),
|
||||
default: true,
|
||||
});
|
||||
const { goBack } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'goBack',
|
||||
message: chalk.yellow('Would you like to go back and select at least one tool?'),
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (goBack) {
|
||||
// Re-display a message before looping back
|
||||
|
|
@ -538,8 +582,8 @@ class UI {
|
|||
}
|
||||
|
||||
return {
|
||||
ides: selectedIdes || [],
|
||||
skipIde: !selectedIdes || selectedIdes.length === 0,
|
||||
ides: answers.ides || [],
|
||||
skipIde: !answers.ides || answers.ides.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -548,17 +592,23 @@ class UI {
|
|||
* @returns {Object} Update configuration
|
||||
*/
|
||||
async promptUpdate() {
|
||||
const backupFirst = await prompts.confirm({
|
||||
message: 'Create backup before updating?',
|
||||
default: true,
|
||||
});
|
||||
const inquirer = await getInquirer();
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backupFirst',
|
||||
message: 'Create backup before updating?',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'preserveCustomizations',
|
||||
message: 'Preserve local customizations?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const preserveCustomizations = await prompts.confirm({
|
||||
message: 'Preserve local customizations?',
|
||||
default: true,
|
||||
});
|
||||
|
||||
return { backupFirst, preserveCustomizations };
|
||||
return answers;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -567,17 +617,27 @@ class UI {
|
|||
* @returns {Array} Selected modules
|
||||
*/
|
||||
async promptModules(modules) {
|
||||
const inquirer = await getInquirer();
|
||||
const choices = modules.map((mod) => ({
|
||||
name: `${mod.name} - ${mod.description}`,
|
||||
value: mod.id,
|
||||
checked: false,
|
||||
}));
|
||||
|
||||
const selectedModules = await prompts.multiselect({
|
||||
message: `Select modules to add ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||
choices,
|
||||
required: true,
|
||||
});
|
||||
const { selectedModules } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'selectedModules',
|
||||
message: 'Select modules to add:',
|
||||
choices,
|
||||
validate: (answer) => {
|
||||
if (answer.length === 0) {
|
||||
return 'You must choose at least one module.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return selectedModules;
|
||||
}
|
||||
|
|
@ -589,10 +649,17 @@ class UI {
|
|||
* @returns {boolean} User confirmation
|
||||
*/
|
||||
async confirm(message, defaultValue = false) {
|
||||
return await prompts.confirm({
|
||||
message,
|
||||
default: defaultValue,
|
||||
});
|
||||
const inquirer = await getInquirer();
|
||||
const { confirmed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmed',
|
||||
message,
|
||||
default: defaultValue,
|
||||
},
|
||||
]);
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -686,9 +753,10 @@ class UI {
|
|||
* Get module choices for selection
|
||||
* @param {Set} installedModuleIds - Currently installed module IDs
|
||||
* @param {Object} customContentConfig - Custom content configuration
|
||||
* @returns {Array} Module choices for prompt
|
||||
* @returns {Array} Module choices for inquirer
|
||||
*/
|
||||
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
||||
const inquirer = await getInquirer();
|
||||
const moduleChoices = [];
|
||||
const isNewInstallation = installedModuleIds.size === 0;
|
||||
|
||||
|
|
@ -743,9 +811,9 @@ class UI {
|
|||
if (allCustomModules.length > 0) {
|
||||
// Add separator for custom content, all custom modules, and official content separator
|
||||
moduleChoices.push(
|
||||
new choiceUtils.Separator('── Custom Content ──'),
|
||||
new inquirer.Separator('── Custom Content ──'),
|
||||
...allCustomModules,
|
||||
new choiceUtils.Separator('── Official Content ──'),
|
||||
new inquirer.Separator('── Official Content ──'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -769,43 +837,44 @@ class UI {
|
|||
* @returns {Array} Selected module IDs
|
||||
*/
|
||||
async selectModules(moduleChoices, defaultSelections = []) {
|
||||
// Mark choices as checked based on defaultSelections
|
||||
const choicesWithDefaults = moduleChoices.map((choice) => ({
|
||||
...choice,
|
||||
checked: defaultSelections.includes(choice.value),
|
||||
}));
|
||||
const inquirer = await getInquirer();
|
||||
const moduleAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'modules',
|
||||
message: 'Select modules to install:',
|
||||
choices: moduleChoices,
|
||||
default: defaultSelections,
|
||||
},
|
||||
]);
|
||||
|
||||
const selected = await prompts.multiselect({
|
||||
message: `Select modules to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||
choices: choicesWithDefaults,
|
||||
required: false,
|
||||
});
|
||||
const selected = moduleAnswer.modules || [];
|
||||
|
||||
return selected || [];
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for directory selection
|
||||
* @returns {Object} Directory answer from prompt
|
||||
* @returns {Object} Directory answer from inquirer
|
||||
*/
|
||||
async promptForDirectory() {
|
||||
// Use sync validation because @clack/prompts doesn't support async validate
|
||||
const directory = await prompts.text({
|
||||
message: 'Installation directory:',
|
||||
default: process.cwd(),
|
||||
placeholder: process.cwd(),
|
||||
validate: (input) => this.validateDirectorySync(input),
|
||||
});
|
||||
|
||||
// Apply filter logic
|
||||
let filteredDir = directory;
|
||||
if (!filteredDir || filteredDir.trim() === '') {
|
||||
filteredDir = process.cwd();
|
||||
} else {
|
||||
filteredDir = this.expandUserPath(filteredDir);
|
||||
}
|
||||
|
||||
return { directory: filteredDir };
|
||||
const inquirer = await getInquirer();
|
||||
return await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'directory',
|
||||
message: `Installation directory:`,
|
||||
default: process.cwd(),
|
||||
validate: async (input) => this.validateDirectory(input),
|
||||
filter: (input) => {
|
||||
// If empty, use the default
|
||||
if (!input || input.trim() === '') {
|
||||
return process.cwd();
|
||||
}
|
||||
return this.expandUserPath(input);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -846,92 +915,45 @@ class UI {
|
|||
* @returns {boolean} Whether user confirmed
|
||||
*/
|
||||
async confirmDirectory(directory) {
|
||||
const inquirer = await getInquirer();
|
||||
const dirExists = await fs.pathExists(directory);
|
||||
|
||||
if (dirExists) {
|
||||
const proceed = await prompts.confirm({
|
||||
message: 'Install to this directory?',
|
||||
default: true,
|
||||
});
|
||||
const confirmAnswer = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: `Install to this directory?`,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!proceed) {
|
||||
if (!confirmAnswer.proceed) {
|
||||
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
||||
}
|
||||
|
||||
return proceed;
|
||||
return confirmAnswer.proceed;
|
||||
} else {
|
||||
// Ask for confirmation to create the directory
|
||||
const create = await prompts.confirm({
|
||||
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
|
||||
default: false,
|
||||
});
|
||||
const createConfirm = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'create',
|
||||
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!create) {
|
||||
if (!createConfirm.create) {
|
||||
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
||||
}
|
||||
|
||||
return create;
|
||||
return createConfirm.create;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate directory path for installation (sync version for clack prompts)
|
||||
* @param {string} input - User input path
|
||||
* @returns {string|undefined} Error message or undefined if valid
|
||||
*/
|
||||
validateDirectorySync(input) {
|
||||
// Allow empty input to use the default
|
||||
if (!input || input.trim() === '') {
|
||||
return; // Empty means use default, undefined = valid for clack
|
||||
}
|
||||
|
||||
let expandedPath;
|
||||
try {
|
||||
expandedPath = this.expandUserPath(input.trim());
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
// Check if the path exists
|
||||
const pathExists = fs.pathExistsSync(expandedPath);
|
||||
|
||||
if (!pathExists) {
|
||||
// Find the first existing parent directory
|
||||
const existingParent = this.findExistingParentSync(expandedPath);
|
||||
|
||||
if (!existingParent) {
|
||||
return 'Cannot create directory: no existing parent directory found';
|
||||
}
|
||||
|
||||
// Check if the existing parent is writable
|
||||
try {
|
||||
fs.accessSync(existingParent, fs.constants.W_OK);
|
||||
// Path doesn't exist but can be created - will prompt for confirmation later
|
||||
return;
|
||||
} catch {
|
||||
// Provide a detailed error message explaining both issues
|
||||
return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
|
||||
}
|
||||
}
|
||||
|
||||
// If it exists, validate it's a directory and writable
|
||||
const stat = fs.statSync(expandedPath);
|
||||
if (!stat.isDirectory()) {
|
||||
return `Path exists but is not a directory: ${expandedPath}`;
|
||||
}
|
||||
|
||||
// Check write permissions
|
||||
try {
|
||||
fs.accessSync(expandedPath, fs.constants.W_OK);
|
||||
} catch {
|
||||
return `Directory is not writable: ${expandedPath}`;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate directory path for installation (async version)
|
||||
* Validate directory path for installation
|
||||
* @param {string} input - User input path
|
||||
* @returns {string|true} Error message or true if valid
|
||||
*/
|
||||
|
|
@ -987,28 +1009,7 @@ class UI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find the first existing parent directory (sync version)
|
||||
* @param {string} targetPath - The path to check
|
||||
* @returns {string|null} The first existing parent directory, or null if none found
|
||||
*/
|
||||
findExistingParentSync(targetPath) {
|
||||
let currentPath = path.resolve(targetPath);
|
||||
|
||||
// Walk up the directory tree until we find an existing directory
|
||||
while (currentPath !== path.dirname(currentPath)) {
|
||||
// Stop at root
|
||||
const parent = path.dirname(currentPath);
|
||||
if (fs.pathExistsSync(parent)) {
|
||||
return parent;
|
||||
}
|
||||
currentPath = parent;
|
||||
}
|
||||
|
||||
return null; // No existing parent found (shouldn't happen in practice)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first existing parent directory (async version)
|
||||
* Find the first existing parent directory
|
||||
* @param {string} targetPath - The path to check
|
||||
* @returns {string|null} The first existing parent directory, or null if none found
|
||||
*/
|
||||
|
|
@ -1070,7 +1071,7 @@ class UI {
|
|||
* @sideeffects None - pure user input collection, no files written
|
||||
* @edgecases Shows warning if user enables TTS but AgentVibes not detected
|
||||
* @calledby promptInstall() during installation flow, after core config, before IDE selection
|
||||
* @calls checkAgentVibesInstalled(), prompts.select(), chalk.green/yellow/dim()
|
||||
* @calls checkAgentVibesInstalled(), inquirer.prompt(), chalk.green/yellow/dim()
|
||||
*
|
||||
* AI NOTE: This prompt is strategically positioned in installation flow:
|
||||
* - AFTER core config (user_name, etc)
|
||||
|
|
@ -1101,6 +1102,7 @@ class UI {
|
|||
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
||||
*/
|
||||
async promptAgentVibes(projectDir) {
|
||||
const inquirer = await getInquirer();
|
||||
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
||||
|
||||
// Check if AgentVibes is already installed
|
||||
|
|
@ -1112,19 +1114,23 @@ class UI {
|
|||
console.log(chalk.dim(' AgentVibes not detected'));
|
||||
}
|
||||
|
||||
const enableTts = await prompts.confirm({
|
||||
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
|
||||
default: false,
|
||||
});
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'enableTts',
|
||||
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
|
||||
default: false, // Default to yes - recommended for best experience
|
||||
},
|
||||
]);
|
||||
|
||||
if (enableTts && !agentVibesInstalled) {
|
||||
if (answers.enableTts && !agentVibesInstalled) {
|
||||
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
|
||||
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
|
||||
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: enableTts,
|
||||
enabled: answers.enableTts,
|
||||
alreadyInstalled: agentVibesInstalled,
|
||||
};
|
||||
}
|
||||
|
|
@ -1242,75 +1248,30 @@ class UI {
|
|||
return existingInstall.ides || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate custom content path synchronously
|
||||
* @param {string} input - User input path
|
||||
* @returns {string|undefined} Error message or undefined if valid
|
||||
*/
|
||||
validateCustomContentPathSync(input) {
|
||||
// Allow empty input to cancel
|
||||
if (!input || input.trim() === '') {
|
||||
return; // Allow empty to exit
|
||||
}
|
||||
|
||||
try {
|
||||
// Expand the path
|
||||
const expandedPath = this.expandUserPath(input.trim());
|
||||
|
||||
// Check if path exists
|
||||
if (!fs.pathExistsSync(expandedPath)) {
|
||||
return 'Path does not exist';
|
||||
}
|
||||
|
||||
// Check if it's a directory
|
||||
const stat = fs.statSync(expandedPath);
|
||||
if (!stat.isDirectory()) {
|
||||
return 'Path must be a directory';
|
||||
}
|
||||
|
||||
// Check for module.yaml in the root
|
||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||
if (!fs.pathExistsSync(moduleYamlPath)) {
|
||||
return 'Directory must contain a module.yaml file in the root';
|
||||
}
|
||||
|
||||
// Try to parse the module.yaml to get the module ID
|
||||
try {
|
||||
const yaml = require('yaml');
|
||||
const content = fs.readFileSync(moduleYamlPath, 'utf8');
|
||||
const moduleData = yaml.parse(content);
|
||||
if (!moduleData.code) {
|
||||
return 'module.yaml must contain a "code" field for the module ID';
|
||||
}
|
||||
} catch (error) {
|
||||
return 'Invalid module.yaml file: ' + error.message;
|
||||
}
|
||||
|
||||
return; // Valid
|
||||
} catch (error) {
|
||||
return 'Error validating path: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for custom content source location
|
||||
* @returns {Object} Custom content configuration
|
||||
*/
|
||||
async promptCustomContentSource() {
|
||||
const inquirer = await getInquirer();
|
||||
const customContentConfig = { hasCustomContent: true, sources: [] };
|
||||
|
||||
// Keep asking for more sources until user is done
|
||||
while (true) {
|
||||
// First ask if user wants to add another module or continue
|
||||
if (customContentConfig.sources.length > 0) {
|
||||
const action = await prompts.select({
|
||||
message: 'Would you like to:',
|
||||
choices: [
|
||||
{ name: 'Add another custom module', value: 'add' },
|
||||
{ name: 'Continue with installation', value: 'continue' },
|
||||
],
|
||||
default: 'continue',
|
||||
});
|
||||
const { action } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: 'Would you like to:',
|
||||
choices: [
|
||||
{ name: 'Add another custom module', value: 'add' },
|
||||
{ name: 'Continue with installation', value: 'continue' },
|
||||
],
|
||||
default: 'continue',
|
||||
},
|
||||
]);
|
||||
|
||||
if (action === 'continue') {
|
||||
break;
|
||||
|
|
@ -1321,11 +1282,57 @@ class UI {
|
|||
let isValid = false;
|
||||
|
||||
while (!isValid) {
|
||||
// Use sync validation because @clack/prompts doesn't support async validate
|
||||
const inputPath = await prompts.text({
|
||||
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
|
||||
validate: (input) => this.validateCustomContentPathSync(input),
|
||||
});
|
||||
const { path: inputPath } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'path',
|
||||
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
|
||||
validate: async (input) => {
|
||||
// Allow empty input to cancel
|
||||
if (!input || input.trim() === '') {
|
||||
return true; // Allow empty to exit
|
||||
}
|
||||
|
||||
try {
|
||||
// Expand the path
|
||||
const expandedPath = this.expandUserPath(input.trim());
|
||||
|
||||
// Check if path exists
|
||||
if (!(await fs.pathExists(expandedPath))) {
|
||||
return 'Path does not exist';
|
||||
}
|
||||
|
||||
// Check if it's a directory
|
||||
const stat = await fs.stat(expandedPath);
|
||||
if (!stat.isDirectory()) {
|
||||
return 'Path must be a directory';
|
||||
}
|
||||
|
||||
// Check for module.yaml in the root
|
||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||
if (!(await fs.pathExists(moduleYamlPath))) {
|
||||
return 'Directory must contain a module.yaml file in the root';
|
||||
}
|
||||
|
||||
// Try to parse the module.yaml to get the module ID
|
||||
try {
|
||||
const yaml = require('yaml');
|
||||
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
||||
const moduleData = yaml.parse(content);
|
||||
if (!moduleData.code) {
|
||||
return 'module.yaml must contain a "code" field for the module ID';
|
||||
}
|
||||
} catch (error) {
|
||||
return 'Invalid module.yaml file: ' + error.message;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return 'Error validating path: ' + error.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// If user pressed Enter without typing anything, exit the loop
|
||||
if (!inputPath || inputPath.trim() === '') {
|
||||
|
|
@ -1357,10 +1364,14 @@ class UI {
|
|||
}
|
||||
|
||||
// Ask if user wants to add these to the installation
|
||||
const shouldInstall = await prompts.confirm({
|
||||
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
|
||||
default: true,
|
||||
});
|
||||
const { shouldInstall } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'shouldInstall',
|
||||
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (shouldInstall) {
|
||||
customContentConfig.selected = true;
|
||||
|
|
@ -1380,6 +1391,7 @@ class UI {
|
|||
* @returns {Object} Result with selected custom modules and custom content config
|
||||
*/
|
||||
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
||||
const inquirer = await getInquirer();
|
||||
// Get existing installation to find custom modules
|
||||
const { existingInstall } = await this.getExistingInstallation(directory);
|
||||
|
||||
|
|
@ -1439,11 +1451,16 @@ class UI {
|
|||
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
|
||||
}
|
||||
|
||||
const customAction = await prompts.select({
|
||||
message: cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
|
||||
choices: choices,
|
||||
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
||||
});
|
||||
const { customAction } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'customAction',
|
||||
message:
|
||||
cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
|
||||
choices: choices,
|
||||
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
||||
},
|
||||
]);
|
||||
|
||||
switch (customAction) {
|
||||
case 'keep': {
|
||||
|
|
@ -1455,18 +1472,21 @@ class UI {
|
|||
|
||||
case 'select': {
|
||||
// Let user choose which to keep
|
||||
const selectChoices = cachedCustomModules.map((m) => ({
|
||||
const choices = cachedCustomModules.map((m) => ({
|
||||
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
|
||||
value: m.id,
|
||||
checked: m.checked,
|
||||
}));
|
||||
|
||||
const keepModules = await prompts.multiselect({
|
||||
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||
choices: selectChoices,
|
||||
required: false,
|
||||
});
|
||||
result.selectedCustomModules = keepModules || [];
|
||||
const { keepModules } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'keepModules',
|
||||
message: 'Select custom modules to keep:',
|
||||
choices: choices,
|
||||
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
|
||||
},
|
||||
]);
|
||||
result.selectedCustomModules = keepModules;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1566,6 +1586,7 @@ class UI {
|
|||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||
*/
|
||||
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
||||
const inquirer = await getInquirer();
|
||||
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
||||
|
||||
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
||||
|
|
@ -1606,20 +1627,26 @@ class UI {
|
|||
console.log(chalk.yellow('─'.repeat(80)));
|
||||
console.log('');
|
||||
|
||||
const proceed = await prompts.select({
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Proceed with update anyway (may have issues)',
|
||||
value: 'proceed',
|
||||
},
|
||||
{
|
||||
name: 'Cancel (recommended - do a fresh install instead)',
|
||||
value: 'cancel',
|
||||
},
|
||||
],
|
||||
default: 'cancel',
|
||||
});
|
||||
const { proceed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'proceed',
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Proceed with update anyway (may have issues)',
|
||||
value: 'proceed',
|
||||
short: 'Proceed with update',
|
||||
},
|
||||
{
|
||||
name: 'Cancel (recommended - do a fresh install instead)',
|
||||
value: 'cancel',
|
||||
short: 'Cancel installation',
|
||||
},
|
||||
],
|
||||
default: 'cancel',
|
||||
},
|
||||
]);
|
||||
|
||||
if (proceed === 'cancel') {
|
||||
console.log('');
|
||||
|
|
|
|||
Loading…
Reference in New Issue