aigent-team 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/chunk-N3RYHWTR.js +267 -0
- package/dist/cli.js +576 -0
- package/dist/index.d.ts +234 -0
- package/dist/index.js +27 -0
- package/package.json +67 -0
- package/templates/shared/git-workflow.md +44 -0
- package/templates/shared/project-conventions.md +48 -0
- package/templates/teams/ba/agent.yaml +25 -0
- package/templates/teams/ba/references/acceptance-criteria.md +87 -0
- package/templates/teams/ba/references/api-contract-design.md +110 -0
- package/templates/teams/ba/references/requirements-analysis.md +83 -0
- package/templates/teams/ba/references/user-story-mapping.md +73 -0
- package/templates/teams/ba/skill.md +85 -0
- package/templates/teams/be/agent.yaml +34 -0
- package/templates/teams/be/conventions.md +102 -0
- package/templates/teams/be/references/api-design.md +91 -0
- package/templates/teams/be/references/async-processing.md +86 -0
- package/templates/teams/be/references/auth-security.md +58 -0
- package/templates/teams/be/references/caching.md +79 -0
- package/templates/teams/be/references/database.md +65 -0
- package/templates/teams/be/references/error-handling.md +106 -0
- package/templates/teams/be/references/observability.md +83 -0
- package/templates/teams/be/references/review-checklist.md +50 -0
- package/templates/teams/be/references/testing.md +100 -0
- package/templates/teams/be/review-checklist.md +54 -0
- package/templates/teams/be/skill.md +71 -0
- package/templates/teams/devops/agent.yaml +35 -0
- package/templates/teams/devops/conventions.md +133 -0
- package/templates/teams/devops/references/ci-cd.md +218 -0
- package/templates/teams/devops/references/cost-optimization.md +218 -0
- package/templates/teams/devops/references/disaster-recovery.md +199 -0
- package/templates/teams/devops/references/docker.md +237 -0
- package/templates/teams/devops/references/infrastructure-as-code.md +238 -0
- package/templates/teams/devops/references/kubernetes.md +397 -0
- package/templates/teams/devops/references/monitoring.md +224 -0
- package/templates/teams/devops/references/review-checklist.md +149 -0
- package/templates/teams/devops/references/security.md +225 -0
- package/templates/teams/devops/review-checklist.md +72 -0
- package/templates/teams/devops/skill.md +131 -0
- package/templates/teams/fe/agent.yaml +28 -0
- package/templates/teams/fe/conventions.md +80 -0
- package/templates/teams/fe/references/accessibility.md +92 -0
- package/templates/teams/fe/references/component-architecture.md +87 -0
- package/templates/teams/fe/references/css-styling.md +89 -0
- package/templates/teams/fe/references/forms.md +73 -0
- package/templates/teams/fe/references/performance.md +104 -0
- package/templates/teams/fe/references/review-checklist.md +51 -0
- package/templates/teams/fe/references/security.md +90 -0
- package/templates/teams/fe/references/state-management.md +117 -0
- package/templates/teams/fe/references/testing.md +112 -0
- package/templates/teams/fe/review-checklist.md +53 -0
- package/templates/teams/fe/skill.md +68 -0
- package/templates/teams/lead/agent.yaml +18 -0
- package/templates/teams/lead/references/cross-team-coordination.md +68 -0
- package/templates/teams/lead/references/quality-gates.md +64 -0
- package/templates/teams/lead/references/task-decomposition.md +69 -0
- package/templates/teams/lead/skill.md +83 -0
- package/templates/teams/qa/agent.yaml +32 -0
- package/templates/teams/qa/conventions.md +130 -0
- package/templates/teams/qa/references/ci-integration.md +337 -0
- package/templates/teams/qa/references/e2e-testing.md +292 -0
- package/templates/teams/qa/references/mocking.md +249 -0
- package/templates/teams/qa/references/performance-testing.md +288 -0
- package/templates/teams/qa/references/review-checklist.md +143 -0
- package/templates/teams/qa/references/security-testing.md +271 -0
- package/templates/teams/qa/references/test-data.md +275 -0
- package/templates/teams/qa/references/test-strategy.md +192 -0
- package/templates/teams/qa/review-checklist.md +53 -0
- package/templates/teams/qa/skill.md +131 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# E2E Testing Reference
|
|
2
|
+
|
|
3
|
+
## Page Object Model
|
|
4
|
+
|
|
5
|
+
Encapsulate page interactions behind a stable API. Tests read like user stories;
|
|
6
|
+
selectors and waits live in one place.
|
|
7
|
+
|
|
8
|
+
### Structure
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
e2e/
|
|
12
|
+
pages/
|
|
13
|
+
login.page.ts
|
|
14
|
+
dashboard.page.ts
|
|
15
|
+
checkout.page.ts
|
|
16
|
+
fixtures/
|
|
17
|
+
auth.fixture.ts
|
|
18
|
+
specs/
|
|
19
|
+
checkout.spec.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Page Object Example (Playwright)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// e2e/pages/checkout.page.ts
|
|
26
|
+
import { type Page, type Locator } from '@playwright/test';
|
|
27
|
+
|
|
28
|
+
export class CheckoutPage {
|
|
29
|
+
private readonly cartItems: Locator;
|
|
30
|
+
private readonly promoCodeInput: Locator;
|
|
31
|
+
private readonly applyPromoButton: Locator;
|
|
32
|
+
private readonly totalPrice: Locator;
|
|
33
|
+
private readonly placeOrderButton: Locator;
|
|
34
|
+
|
|
35
|
+
constructor(private readonly page: Page) {
|
|
36
|
+
this.cartItems = page.getByRole('list', { name: 'Cart items' }).getByRole('listitem');
|
|
37
|
+
this.promoCodeInput = page.getByLabel('Promo code');
|
|
38
|
+
this.applyPromoButton = page.getByRole('button', { name: 'Apply' });
|
|
39
|
+
this.totalPrice = page.getByTestId('total-price');
|
|
40
|
+
this.placeOrderButton = page.getByRole('button', { name: 'Place order' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async applyPromoCode(code: string) {
|
|
44
|
+
await this.promoCodeInput.fill(code);
|
|
45
|
+
await this.applyPromoButton.click();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async placeOrder() {
|
|
49
|
+
await this.placeOrderButton.click();
|
|
50
|
+
await this.page.waitForURL('**/confirmation');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getTotal(): Promise<string> {
|
|
54
|
+
return this.totalPrice.textContent() ?? '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getItemCount(): Promise<number> {
|
|
58
|
+
return this.cartItems.count();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Test Using Page Object
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// e2e/specs/checkout.spec.ts
|
|
67
|
+
import { test, expect } from '@playwright/test';
|
|
68
|
+
import { CheckoutPage } from '../pages/checkout.page';
|
|
69
|
+
|
|
70
|
+
test.describe('Checkout', () => {
|
|
71
|
+
let checkout: CheckoutPage;
|
|
72
|
+
|
|
73
|
+
test.beforeEach(async ({ page }) => {
|
|
74
|
+
// Seed cart via API — never through UI
|
|
75
|
+
await page.request.post('/api/test/seed-cart', {
|
|
76
|
+
data: { items: [{ sku: 'WIDGET-1', qty: 2 }] },
|
|
77
|
+
});
|
|
78
|
+
await page.goto('/checkout');
|
|
79
|
+
checkout = new CheckoutPage(page);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('applies promo code and updates total', async () => {
|
|
83
|
+
await checkout.applyPromoCode('SAVE20');
|
|
84
|
+
await expect(checkout.getTotal()).resolves.toContain('$16.00');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Element Selection Priority
|
|
92
|
+
|
|
93
|
+
Use the most accessible selector available. Order of preference:
|
|
94
|
+
|
|
95
|
+
1. **`getByRole`** — matches what assistive technology sees.
|
|
96
|
+
`page.getByRole('button', { name: 'Submit' })`
|
|
97
|
+
2. **`getByLabel`** — form fields with proper labels.
|
|
98
|
+
`page.getByLabel('Email address')`
|
|
99
|
+
3. **`getByPlaceholder`** — fallback for unlabelled inputs.
|
|
100
|
+
4. **`getByText`** — visible text content.
|
|
101
|
+
5. **`getByTestId`** — last resort; requires `data-testid` attribute.
|
|
102
|
+
|
|
103
|
+
Never use CSS selectors or XPath tied to DOM structure. They break on
|
|
104
|
+
refactors and provide no accessibility signal.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Wait Strategy
|
|
109
|
+
|
|
110
|
+
### Rule: Never Use Fixed Delays
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// BAD — arbitrary delay, still flaky
|
|
114
|
+
await page.waitForTimeout(3000);
|
|
115
|
+
|
|
116
|
+
// GOOD — wait for specific condition
|
|
117
|
+
await page.waitForSelector('[data-loaded="true"]');
|
|
118
|
+
await expect(page.getByText('Order confirmed')).toBeVisible();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Playwright Auto-Waiting
|
|
122
|
+
|
|
123
|
+
Playwright auto-waits for elements to be actionable before interacting.
|
|
124
|
+
Lean on this. Additional explicit waits for:
|
|
125
|
+
|
|
126
|
+
- **Navigation**: `await page.waitForURL('**/dashboard')`
|
|
127
|
+
- **Network idle**: `await page.waitForLoadState('networkidle')` (use
|
|
128
|
+
sparingly — prefer waiting for specific elements)
|
|
129
|
+
- **API response**: `await page.waitForResponse(resp => resp.url().includes('/api/orders') && resp.status() === 200)`
|
|
130
|
+
- **Custom condition**: `await expect(locator).toHaveCount(3)`
|
|
131
|
+
|
|
132
|
+
### Timeout Configuration
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// playwright.config.ts
|
|
136
|
+
export default defineConfig({
|
|
137
|
+
timeout: 30_000, // per-test timeout
|
|
138
|
+
expect: {
|
|
139
|
+
timeout: 5_000, // per-assertion timeout
|
|
140
|
+
},
|
|
141
|
+
use: {
|
|
142
|
+
actionTimeout: 10_000, // per-action timeout (click, fill, etc.)
|
|
143
|
+
navigationTimeout: 15_000,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Test Data Setup via API
|
|
151
|
+
|
|
152
|
+
Never create test data through the UI. It is slow, brittle, and couples
|
|
153
|
+
tests to unrelated pages.
|
|
154
|
+
|
|
155
|
+
### Pattern: API Seeding
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
test.beforeEach(async ({ page }) => {
|
|
159
|
+
// Create user and get auth token
|
|
160
|
+
const res = await page.request.post('/api/test/users', {
|
|
161
|
+
data: { email: 'qa@test.com', role: 'admin' },
|
|
162
|
+
});
|
|
163
|
+
const { token } = await res.json();
|
|
164
|
+
|
|
165
|
+
// Set auth cookie/header
|
|
166
|
+
await page.context().addCookies([{
|
|
167
|
+
name: 'auth_token',
|
|
168
|
+
value: token,
|
|
169
|
+
domain: 'localhost',
|
|
170
|
+
path: '/',
|
|
171
|
+
}]);
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Pattern: Storage State for Auth
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// e2e/fixtures/auth.fixture.ts — run once, reuse across tests
|
|
179
|
+
import { test as setup } from '@playwright/test';
|
|
180
|
+
|
|
181
|
+
setup('authenticate', async ({ page }) => {
|
|
182
|
+
await page.goto('/login');
|
|
183
|
+
await page.getByLabel('Email').fill('admin@test.com');
|
|
184
|
+
await page.getByLabel('Password').fill('test-password');
|
|
185
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
186
|
+
await page.waitForURL('**/dashboard');
|
|
187
|
+
await page.context().storageState({ path: '.auth/admin.json' });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// playwright.config.ts
|
|
191
|
+
export default defineConfig({
|
|
192
|
+
projects: [
|
|
193
|
+
{ name: 'setup', testMatch: /auth\.fixture\.ts/ },
|
|
194
|
+
{
|
|
195
|
+
name: 'tests',
|
|
196
|
+
dependencies: ['setup'],
|
|
197
|
+
use: { storageState: '.auth/admin.json' },
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Parallel Execution Safety
|
|
206
|
+
|
|
207
|
+
Playwright runs tests in parallel by default. Tests must be fully isolated:
|
|
208
|
+
|
|
209
|
+
- **No shared database rows.** Each test creates its own data with unique IDs.
|
|
210
|
+
- **No shared files.** Use temp directories or unique filenames.
|
|
211
|
+
- **No port conflicts.** Use dynamic ports or separate server instances.
|
|
212
|
+
- **No sequential assumptions.** Never rely on test execution order.
|
|
213
|
+
|
|
214
|
+
If a test cannot be parallelised (e.g., it modifies global settings), mark it:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
test.describe.serial('Admin settings', () => {
|
|
218
|
+
// These tests run sequentially within this describe block
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Flakiness Prevention
|
|
225
|
+
|
|
226
|
+
### Common Causes and Fixes
|
|
227
|
+
|
|
228
|
+
| Cause | Detection | Fix |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| Race condition (element not ready) | Passes locally, fails in CI | Replace `waitForTimeout` with explicit waits |
|
|
231
|
+
| Shared test state | Fails when run order changes | Isolate data per test |
|
|
232
|
+
| Animation interfering with click | Intermittent click misses | Disable animations in test config |
|
|
233
|
+
| Viewport-dependent layout | Fails on different screen sizes | Set fixed viewport in config |
|
|
234
|
+
| Third-party dependency | Flaky external API | Mock external services in E2E |
|
|
235
|
+
| Time-dependent logic | Fails at midnight/month-end | Mock clock or use stable test dates |
|
|
236
|
+
|
|
237
|
+
### Playwright Anti-Flake Config
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// playwright.config.ts
|
|
241
|
+
export default defineConfig({
|
|
242
|
+
retries: process.env.CI ? 2 : 0, // Retries hide flakiness locally
|
|
243
|
+
use: {
|
|
244
|
+
video: 'retain-on-failure',
|
|
245
|
+
screenshot: 'only-on-failure',
|
|
246
|
+
trace: 'retain-on-failure',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Flakiness Detection
|
|
252
|
+
|
|
253
|
+
Run suspect tests in a loop locally before merging:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npx playwright test checkout.spec.ts --repeat-each=20
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
If it fails once in 20, it is flaky. Fix before merging.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Screenshot and Trace on Failure
|
|
264
|
+
|
|
265
|
+
Always capture artifacts on failure for debugging:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// playwright.config.ts
|
|
269
|
+
export default defineConfig({
|
|
270
|
+
use: {
|
|
271
|
+
screenshot: 'only-on-failure',
|
|
272
|
+
video: 'retain-on-failure',
|
|
273
|
+
trace: 'retain-on-failure',
|
|
274
|
+
},
|
|
275
|
+
outputDir: 'test-results/',
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
In CI, upload the `test-results/` directory as a build artifact:
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
# GitHub Actions
|
|
283
|
+
- uses: actions/upload-artifact@v4
|
|
284
|
+
if: failure()
|
|
285
|
+
with:
|
|
286
|
+
name: playwright-results
|
|
287
|
+
path: test-results/
|
|
288
|
+
retention-days: 7
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Traces can be viewed at https://trace.playwright.dev — share the link in
|
|
292
|
+
bug reports for full reproduction context.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Mocking Reference
|
|
2
|
+
|
|
3
|
+
## Core Rule: Mock at System Boundaries Only
|
|
4
|
+
|
|
5
|
+
A mock replaces something your code *cannot control* during a test:
|
|
6
|
+
|
|
7
|
+
- **HTTP requests** to external services
|
|
8
|
+
- **System clock** (`Date.now`, timers)
|
|
9
|
+
- **Randomness** (`Math.random`, `crypto.randomUUID`)
|
|
10
|
+
- **File system** (only when testing I/O behaviour, not business logic)
|
|
11
|
+
|
|
12
|
+
Everything else — internal modules, utility functions, class methods — should
|
|
13
|
+
use the real implementation. If you feel the need to mock an internal module,
|
|
14
|
+
it is a design smell: extract an interface and inject the dependency.
|
|
15
|
+
|
|
16
|
+
### Why Not Mock Internals?
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// BAD — mocking internal module
|
|
20
|
+
vi.mock('../utils/calculateTax', () => ({
|
|
21
|
+
calculateTax: vi.fn(() => 5.00),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
test('order total includes tax', () => {
|
|
25
|
+
// This test proves nothing. You mocked the very thing
|
|
26
|
+
// that computes the value you're asserting on.
|
|
27
|
+
expect(getOrderTotal(items)).toBe(105.00);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// GOOD — test the real calculation
|
|
31
|
+
test('order total includes tax', () => {
|
|
32
|
+
const items = [buildOrderItem({ unitPrice: 100, quantity: 1 })];
|
|
33
|
+
// Real calculateTax runs; test verifies actual behaviour
|
|
34
|
+
expect(getOrderTotal(items)).toBe(105.00);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## MSW (Mock Service Worker) Setup
|
|
41
|
+
|
|
42
|
+
MSW intercepts HTTP requests at the network level. Your application code
|
|
43
|
+
uses real `fetch` / `axios` — no patching required.
|
|
44
|
+
|
|
45
|
+
### Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -D msw
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Handler Definition
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// tests/mocks/handlers.ts
|
|
55
|
+
import { http, HttpResponse } from 'msw';
|
|
56
|
+
|
|
57
|
+
export const handlers = [
|
|
58
|
+
http.get('/api/users/:id', ({ params }) => {
|
|
59
|
+
return HttpResponse.json({
|
|
60
|
+
id: params.id,
|
|
61
|
+
name: 'Test User',
|
|
62
|
+
email: 'test@example.com',
|
|
63
|
+
});
|
|
64
|
+
}),
|
|
65
|
+
|
|
66
|
+
http.post('/api/orders', async ({ request }) => {
|
|
67
|
+
const body = await request.json();
|
|
68
|
+
return HttpResponse.json(
|
|
69
|
+
{ id: 'order-123', ...body, status: 'created' },
|
|
70
|
+
{ status: 201 },
|
|
71
|
+
);
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
http.get('/api/inventory/:sku', () => {
|
|
75
|
+
return HttpResponse.json({ sku: 'WIDGET-1', stock: 42 });
|
|
76
|
+
}),
|
|
77
|
+
];
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Server Setup (Node — Vitest / Jest)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// tests/mocks/server.ts
|
|
84
|
+
import { setupServer } from 'msw/node';
|
|
85
|
+
import { handlers } from './handlers';
|
|
86
|
+
|
|
87
|
+
export const server = setupServer(...handlers);
|
|
88
|
+
|
|
89
|
+
// tests/setup.ts
|
|
90
|
+
import { server } from './mocks/server';
|
|
91
|
+
|
|
92
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
|
93
|
+
afterEach(() => server.resetHandlers());
|
|
94
|
+
afterAll(() => server.close());
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Critical**: `onUnhandledRequest: 'error'` ensures no real HTTP requests
|
|
98
|
+
leak through. If a test makes an unmocked request, it fails loudly.
|
|
99
|
+
|
|
100
|
+
### Per-Test Override
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { server } from '../mocks/server';
|
|
104
|
+
import { http, HttpResponse } from 'msw';
|
|
105
|
+
|
|
106
|
+
test('shows error when API returns 500', async () => {
|
|
107
|
+
server.use(
|
|
108
|
+
http.get('/api/users/:id', () => {
|
|
109
|
+
return HttpResponse.json(
|
|
110
|
+
{ error: 'Internal server error' },
|
|
111
|
+
{ status: 500 },
|
|
112
|
+
);
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
render(<UserProfile userId="1" />);
|
|
117
|
+
await expect(screen.findByText('Something went wrong')).resolves.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Browser Setup (Playwright / Cypress)
|
|
122
|
+
|
|
123
|
+
For E2E tests, prefer Playwright's built-in route interception over MSW
|
|
124
|
+
browser worker:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Playwright route mock
|
|
128
|
+
await page.route('**/api/external-service/**', (route) => {
|
|
129
|
+
route.fulfill({
|
|
130
|
+
status: 200,
|
|
131
|
+
contentType: 'application/json',
|
|
132
|
+
body: JSON.stringify({ status: 'ok' }),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Never Mock What You Don't Own (Without a Contract)
|
|
140
|
+
|
|
141
|
+
If you mock a third-party API, your mock can drift from reality. Protect
|
|
142
|
+
against this:
|
|
143
|
+
|
|
144
|
+
### Strategy 1: Record and Replay
|
|
145
|
+
|
|
146
|
+
Record real API responses once, replay in tests.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// Record phase (run manually):
|
|
150
|
+
const response = await fetch('https://api.stripe.com/v1/charges');
|
|
151
|
+
fs.writeFileSync('tests/fixtures/stripe-charges.json', await response.text());
|
|
152
|
+
|
|
153
|
+
// Test phase:
|
|
154
|
+
server.use(
|
|
155
|
+
http.get('https://api.stripe.com/v1/charges', () => {
|
|
156
|
+
const data = JSON.parse(fs.readFileSync('tests/fixtures/stripe-charges.json', 'utf-8'));
|
|
157
|
+
return HttpResponse.json(data);
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Refresh recordings periodically (monthly or on API version bump).
|
|
163
|
+
|
|
164
|
+
### Strategy 2: Schema Validation
|
|
165
|
+
|
|
166
|
+
Validate your mock responses against the provider's OpenAPI spec:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import Ajv from 'ajv';
|
|
170
|
+
import stripeSpec from './fixtures/stripe-openapi.json';
|
|
171
|
+
|
|
172
|
+
test('mock matches Stripe schema', () => {
|
|
173
|
+
const ajv = new Ajv();
|
|
174
|
+
const schema = stripeSpec.paths['/v1/charges'].get.responses['200'].content['application/json'].schema;
|
|
175
|
+
const valid = ajv.validate(schema, mockChargesResponse);
|
|
176
|
+
expect(valid).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Strategy 3: Contract Tests
|
|
181
|
+
|
|
182
|
+
For services you own, use Pact (see `test-strategy.md` > Contract Testing).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Verifying Mock Contracts Match Real APIs
|
|
187
|
+
|
|
188
|
+
Every mock handler should be traceable to a real API endpoint. Maintain a
|
|
189
|
+
mapping:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// tests/mocks/contract-map.ts
|
|
193
|
+
/**
|
|
194
|
+
* Maps mock handlers to real API documentation.
|
|
195
|
+
* Review this file when upgrading API versions.
|
|
196
|
+
*
|
|
197
|
+
* Handler: GET /api/users/:id
|
|
198
|
+
* Real endpoint: https://api.example.com/v2/users/{id}
|
|
199
|
+
* Schema: docs/openapi/users.yaml#/paths/~1users~1{id}/get
|
|
200
|
+
* Last verified: 2025-12-01
|
|
201
|
+
*/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Detecting Drift
|
|
205
|
+
|
|
206
|
+
Add a CI job (weekly) that:
|
|
207
|
+
|
|
208
|
+
1. Fetches the latest OpenAPI spec from the provider.
|
|
209
|
+
2. Validates all mock response fixtures against the spec.
|
|
210
|
+
3. Fails if any fixture is invalid — forces update.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Clock and Random Mocking
|
|
215
|
+
|
|
216
|
+
### Vitest Clock
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { vi } from 'vitest';
|
|
220
|
+
|
|
221
|
+
test('token expires after 1 hour', () => {
|
|
222
|
+
vi.useFakeTimers();
|
|
223
|
+
vi.setSystemTime(new Date('2025-06-15T10:00:00Z'));
|
|
224
|
+
|
|
225
|
+
const token = createToken();
|
|
226
|
+
expect(token.isExpired()).toBe(false);
|
|
227
|
+
|
|
228
|
+
vi.advanceTimersByTime(60 * 60 * 1000); // 1 hour
|
|
229
|
+
expect(token.isExpired()).toBe(true);
|
|
230
|
+
|
|
231
|
+
vi.useRealTimers();
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Deterministic Random
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
test('generates reproducible IDs', () => {
|
|
239
|
+
const mockRandom = vi.spyOn(Math, 'random').mockReturnValue(0.42);
|
|
240
|
+
|
|
241
|
+
const id = generateShortId();
|
|
242
|
+
expect(id).toBe('expected-id-based-on-0.42');
|
|
243
|
+
|
|
244
|
+
mockRandom.mockRestore();
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Always restore mocks after the test — use `afterEach(() => vi.restoreAllMocks())`
|
|
249
|
+
globally.
|