@bradtaylorsf/alpha-loop 1.0.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 +294 -0
- package/agents/implementer.md +30 -0
- package/agents/reviewer.md +29 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +57 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +89 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/history.d.ts +8 -0
- package/dist/commands/history.js +185 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +241 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/run.d.ts +15 -0
- package/dist/commands/run.js +321 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +50 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/sync.d.ts +20 -0
- package/dist/commands/sync.js +149 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/vision.d.ts +1 -0
- package/dist/commands/vision.js +194 -0
- package/dist/commands/vision.js.map +1 -0
- package/dist/engine/agents.d.ts +41 -0
- package/dist/engine/agents.js +90 -0
- package/dist/engine/agents.js.map +1 -0
- package/dist/engine/config.d.ts +71 -0
- package/dist/engine/config.js +73 -0
- package/dist/engine/config.js.map +1 -0
- package/dist/engine/prerequisites.d.ts +34 -0
- package/dist/engine/prerequisites.js +90 -0
- package/dist/engine/prerequisites.js.map +1 -0
- package/dist/lib/agent.d.ts +25 -0
- package/dist/lib/agent.js +97 -0
- package/dist/lib/agent.js.map +1 -0
- package/dist/lib/config.d.ts +35 -0
- package/dist/lib/config.js +179 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/context.d.ts +17 -0
- package/dist/lib/context.js +96 -0
- package/dist/lib/context.js.map +1 -0
- package/dist/lib/github.d.ts +61 -0
- package/dist/lib/github.js +313 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/learning.d.ts +43 -0
- package/dist/lib/learning.js +207 -0
- package/dist/lib/learning.js.map +1 -0
- package/dist/lib/logger.d.ts +9 -0
- package/dist/lib/logger.js +28 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/pipeline.d.ts +18 -0
- package/dist/lib/pipeline.js +456 -0
- package/dist/lib/pipeline.js.map +1 -0
- package/dist/lib/preflight.d.ts +33 -0
- package/dist/lib/preflight.js +123 -0
- package/dist/lib/preflight.js.map +1 -0
- package/dist/lib/prerequisites.d.ts +12 -0
- package/dist/lib/prerequisites.js +54 -0
- package/dist/lib/prerequisites.js.map +1 -0
- package/dist/lib/prompts.d.ts +44 -0
- package/dist/lib/prompts.js +102 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/session.d.ts +28 -0
- package/dist/lib/session.js +173 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/shell.d.ts +32 -0
- package/dist/lib/shell.js +95 -0
- package/dist/lib/shell.js.map +1 -0
- package/dist/lib/testing.d.ts +10 -0
- package/dist/lib/testing.js +51 -0
- package/dist/lib/testing.js.map +1 -0
- package/dist/lib/verify.d.ts +18 -0
- package/dist/lib/verify.js +235 -0
- package/dist/lib/verify.js.map +1 -0
- package/dist/lib/vision.d.ts +9 -0
- package/dist/lib/vision.js +21 -0
- package/dist/lib/vision.js.map +1 -0
- package/dist/lib/worktree.d.ts +29 -0
- package/dist/lib/worktree.js +153 -0
- package/dist/lib/worktree.js.map +1 -0
- package/package.json +63 -0
- package/templates/agents/implementer.md +34 -0
- package/templates/agents/reviewer.md +48 -0
- package/templates/skills/code-review/SKILL.md +58 -0
- package/templates/skills/git-workflow/SKILL.md +53 -0
- package/templates/skills/implementation-planning/SKILL.md +64 -0
- package/templates/skills/security-analysis/SKILL.md +560 -0
- package/templates/skills/security-analysis/scripts/security-scanner.sh +227 -0
- package/templates/skills/test-robustness/SKILL.md +897 -0
- package/templates/skills/testing-patterns/SKILL.md +75 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-robustness
|
|
3
|
+
description: Comprehensive patterns for writing robust non-brittle tests including eliminating waitForTimeout, mocking time properly, and generating unique test data. Use when writing any tests to ensure fast reliable maintainable test suites.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test Robustness Skill
|
|
7
|
+
|
|
8
|
+
Comprehensive guide for writing robust, non-brittle tests that are fast, reliable, and maintainable.
|
|
9
|
+
|
|
10
|
+
## Critical Anti-Patterns to AVOID
|
|
11
|
+
|
|
12
|
+
### 1. ❌ Fixed Time Delays (`waitForTimeout`)
|
|
13
|
+
|
|
14
|
+
**NEVER use `page.waitForTimeout()` or arbitrary delays in tests.**
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// ❌ BRITTLE - Do NOT do this
|
|
18
|
+
await page.click('[data-testid="submit"]');
|
|
19
|
+
await page.waitForTimeout(1000); // Flaky! May be too short or unnecessarily long
|
|
20
|
+
expect(page.locator('.success')).toBeVisible();
|
|
21
|
+
|
|
22
|
+
// ❌ BRITTLE - Another bad example
|
|
23
|
+
await page.fill('#username', 'test');
|
|
24
|
+
await page.waitForTimeout(500); // Why 500ms? What are we waiting for?
|
|
25
|
+
await page.click('#submit');
|
|
26
|
+
|
|
27
|
+
// ✅ ROBUST - Do this instead
|
|
28
|
+
await page.click('[data-testid="submit"]');
|
|
29
|
+
await page.waitForSelector('.success', { state: 'visible' });
|
|
30
|
+
expect(page.locator('.success')).toBeVisible();
|
|
31
|
+
|
|
32
|
+
// ✅ ROBUST - Wait for specific condition
|
|
33
|
+
await page.fill('#username', 'test');
|
|
34
|
+
await expect(page.locator('#submit')).toBeEnabled();
|
|
35
|
+
await page.click('#submit');
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Why this is bad:**
|
|
39
|
+
- **Flaky**: May be too short on slow CI servers, causing random failures
|
|
40
|
+
- **Slow**: May be unnecessarily long on fast machines, wasting time
|
|
41
|
+
- **Unclear**: Doesn't express what you're actually waiting for
|
|
42
|
+
- **Brittle**: Breaks when timing changes (network, CPU, etc.)
|
|
43
|
+
|
|
44
|
+
**What to use instead:**
|
|
45
|
+
- `page.waitForSelector()` - Wait for element to appear
|
|
46
|
+
- `page.waitForResponse()` - Wait for specific network request
|
|
47
|
+
- `page.waitForNavigation()` - Wait for page navigation
|
|
48
|
+
- `expect().toBeVisible()` - Assert element is visible (auto-waits)
|
|
49
|
+
- `expect().toHaveText()` - Assert text content (auto-waits)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 2. ❌ Real Time Delays in Tests (`setTimeout`)
|
|
54
|
+
|
|
55
|
+
**NEVER use real timers in tests that check time-dependent behavior.**
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// ❌ BRITTLE - Do NOT do this (adds 1.1 seconds to test!)
|
|
59
|
+
it('timestamps should be recent', async () => {
|
|
60
|
+
const timestamp1 = Date.now();
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
const timestamp2 = Date.now();
|
|
63
|
+
expect(timestamp2).toBeGreaterThan(timestamp1);
|
|
64
|
+
}, 1100); // Slows test by 1.1 seconds!
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ✅ ROBUST - Mock time instead (instant!)
|
|
68
|
+
it('timestamps should be recent', async () => {
|
|
69
|
+
jest.useFakeTimers();
|
|
70
|
+
const timestamp1 = Date.now();
|
|
71
|
+
|
|
72
|
+
jest.advanceTimersByTime(1100);
|
|
73
|
+
|
|
74
|
+
const timestamp2 = Date.now();
|
|
75
|
+
expect(timestamp2).toBeGreaterThan(timestamp1);
|
|
76
|
+
|
|
77
|
+
jest.useRealTimers();
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Why this is bad:**
|
|
82
|
+
- **Slow**: Tests take actual real-world time to run (6.6 seconds wasted in our codebase!)
|
|
83
|
+
- **Flaky**: Can fail on slow machines or under load
|
|
84
|
+
- **Unnecessary**: Time can be mocked to run instantly
|
|
85
|
+
|
|
86
|
+
**What to use instead:**
|
|
87
|
+
- `jest.useFakeTimers()` - Mock all timer functions
|
|
88
|
+
- `jest.advanceTimersByTime(ms)` - Move time forward instantly
|
|
89
|
+
- `jest.runAllTimers()` - Run all pending timers
|
|
90
|
+
- `jest.useRealTimers()` - Restore real timers after test
|
|
91
|
+
|
|
92
|
+
See: `tests/helpers/time-helpers.ts` for reusable utilities.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### 3. ❌ Hard-Coded Test Data
|
|
97
|
+
|
|
98
|
+
**NEVER use hard-coded IDs, names, or data that could conflict between tests.**
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// ❌ BRITTLE - Hard-coded data causes conflicts
|
|
102
|
+
it('creates session', async () => {
|
|
103
|
+
await createSession('my-session'); // What if another test uses this name?
|
|
104
|
+
const session = await getSession('my-session');
|
|
105
|
+
expect(session.name).toBe('my-session');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ❌ BRITTLE - Hard-coded IDs
|
|
109
|
+
it('deletes session', async () => {
|
|
110
|
+
await deleteSession('session-123'); // Assumes this ID exists!
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ✅ ROBUST - Use templates with unique suffixes
|
|
114
|
+
import { createUniqueSession } from '../helpers/seed-data';
|
|
115
|
+
|
|
116
|
+
it('creates session', async () => {
|
|
117
|
+
const sessionName = createUniqueSession('my-session'); // 'my-session-1671234567890'
|
|
118
|
+
await createSession(sessionName);
|
|
119
|
+
const session = await getSession(sessionName);
|
|
120
|
+
expect(session.name).toBe(sessionName);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ✅ ROBUST - Create data in test setup
|
|
124
|
+
it('deletes session', async () => {
|
|
125
|
+
const session = await createSession(createUniqueSession('test'));
|
|
126
|
+
await deleteSession(session.id);
|
|
127
|
+
await expect(getSession(session.id)).rejects.toThrow();
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Why this is bad:**
|
|
132
|
+
- **Test Interference**: Tests can conflict when run in parallel
|
|
133
|
+
- **Fragile**: Breaks if database is cleared or data changes
|
|
134
|
+
- **Hard to Debug**: Failures don't indicate what data was expected
|
|
135
|
+
- **Not Repeatable**: Tests may pass/fail depending on order
|
|
136
|
+
|
|
137
|
+
**What to use instead:**
|
|
138
|
+
- Template-based data with unique suffixes (timestamps, UUIDs)
|
|
139
|
+
- Test setup/teardown to create/destroy data
|
|
140
|
+
- Database transactions (rollback after test)
|
|
141
|
+
- In-memory databases for unit tests
|
|
142
|
+
|
|
143
|
+
See: `tests/helpers/seed-data.ts` for data generation utilities.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### 4. ❌ Inconsistent Uniqueness Strategies
|
|
148
|
+
|
|
149
|
+
**Use a CONSISTENT strategy for generating unique test data.**
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// ❌ INCONSISTENT - Different strategies in different tests
|
|
153
|
+
it('test 1', async () => {
|
|
154
|
+
const name = `session-${Date.now()}`; // Uses timestamp
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('test 2', async () => {
|
|
158
|
+
const name = 'session-' + Math.random(); // Uses random number
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('test 3', async () => {
|
|
162
|
+
const name = 'session-test'; // No uniqueness at all!
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ✅ CONSISTENT - Use shared utility
|
|
166
|
+
import { uniqueSessionName } from '../helpers/seed-data';
|
|
167
|
+
|
|
168
|
+
it('test 1', async () => {
|
|
169
|
+
const name = uniqueSessionName('session'); // session-1671234567890
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('test 2', async () => {
|
|
173
|
+
const name = uniqueSessionName('session'); // session-1671234567891
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('test 3', async () => {
|
|
177
|
+
const name = uniqueSessionName('session'); // session-1671234567892
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Why this is bad:**
|
|
182
|
+
- **Hard to Maintain**: Every test does it differently
|
|
183
|
+
- **Potential Collisions**: Different strategies may conflict
|
|
184
|
+
- **Unclear Intent**: Hard to understand the pattern
|
|
185
|
+
- **Duplication**: Same logic repeated everywhere
|
|
186
|
+
|
|
187
|
+
**What to use instead:**
|
|
188
|
+
- Single utility function: `uniqueSessionName(prefix)`
|
|
189
|
+
- Consistent format: `${prefix}-${timestamp}` or `${prefix}-${uuid}`
|
|
190
|
+
- Centralized in test helpers
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Correct Patterns to USE
|
|
195
|
+
|
|
196
|
+
### ✅ Wait for Specific Elements
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Wait for element to appear
|
|
200
|
+
await page.waitForSelector('[data-testid="success-message"]', {
|
|
201
|
+
state: 'visible'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Wait for element to disappear
|
|
205
|
+
await page.waitForSelector('[data-testid="loading-spinner"]', {
|
|
206
|
+
state: 'hidden'
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Wait for element to be enabled
|
|
210
|
+
await page.waitForSelector('[data-testid="submit-button"]:not([disabled])');
|
|
211
|
+
|
|
212
|
+
// Playwright assertions (auto-wait built-in)
|
|
213
|
+
await expect(page.locator('[data-testid="result"]')).toBeVisible();
|
|
214
|
+
await expect(page.locator('[data-testid="result"]')).toHaveText('Success');
|
|
215
|
+
await expect(page.locator('[data-testid="input"]')).toBeEnabled();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### ✅ Wait for Network Activity
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Wait for specific API response
|
|
224
|
+
const responsePromise = page.waitForResponse(
|
|
225
|
+
response => response.url().includes('/api/session') && response.status() === 200
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await page.click('[data-testid="create-session"]');
|
|
229
|
+
const response = await responsePromise;
|
|
230
|
+
const data = await response.json();
|
|
231
|
+
|
|
232
|
+
expect(data.id).toBeDefined();
|
|
233
|
+
|
|
234
|
+
// Wait for multiple requests to complete
|
|
235
|
+
await Promise.all([
|
|
236
|
+
page.waitForResponse('/api/session'),
|
|
237
|
+
page.waitForResponse('/api/features'),
|
|
238
|
+
page.click('[data-testid="load-data"]')
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Wait for navigation
|
|
242
|
+
await Promise.all([
|
|
243
|
+
page.waitForNavigation(),
|
|
244
|
+
page.click('[data-testid="logout"]')
|
|
245
|
+
]);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### ✅ Mock Time Properly
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { useFakeTimers, advanceTime, useRealTimers } from '../helpers/time-helpers';
|
|
254
|
+
|
|
255
|
+
it('session expires after 1 hour', async () => {
|
|
256
|
+
useFakeTimers(); // Mock Date.now(), setTimeout, setInterval
|
|
257
|
+
|
|
258
|
+
const session = await createSession('test');
|
|
259
|
+
expect(session.expiresAt).toBe(Date.now() + 3600000); // 1 hour
|
|
260
|
+
|
|
261
|
+
advanceTime(3600001); // Advance 1 hour + 1ms
|
|
262
|
+
|
|
263
|
+
const expired = await isSessionExpired(session.id);
|
|
264
|
+
expect(expired).toBe(true);
|
|
265
|
+
|
|
266
|
+
useRealTimers(); // Restore real timers
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('retries after 5 seconds', async () => {
|
|
270
|
+
useFakeTimers();
|
|
271
|
+
|
|
272
|
+
const retryPromise = retryOperation(); // Returns after 5s timeout
|
|
273
|
+
|
|
274
|
+
advanceTime(5000); // Instantly "wait" 5 seconds
|
|
275
|
+
|
|
276
|
+
await expect(retryPromise).resolves.toBe('success');
|
|
277
|
+
|
|
278
|
+
useRealTimers();
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### ✅ Generate Unique Test Data
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// Use template-based seed data
|
|
288
|
+
import { seedSession, seedFeature, seedProject } from '../helpers/seed-data';
|
|
289
|
+
|
|
290
|
+
it('creates session with unique data', async () => {
|
|
291
|
+
// Use .alphacoder paths to avoid /tmp permission issues
|
|
292
|
+
const sessionData = seedSession({
|
|
293
|
+
projectPath: '.alphacoder/sessions/test-data/test-project'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// sessionData = {
|
|
297
|
+
// id: 'session-1671234567890',
|
|
298
|
+
// projectPath: '.alphacoder/sessions/test-data/test-project',
|
|
299
|
+
// features: [...],
|
|
300
|
+
// createdAt: 1671234567890
|
|
301
|
+
// }
|
|
302
|
+
|
|
303
|
+
const session = await createSession(sessionData);
|
|
304
|
+
expect(session.id).toBe(sessionData.id);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('creates multiple sessions without conflicts', async () => {
|
|
308
|
+
const session1 = seedSession();
|
|
309
|
+
const session2 = seedSession();
|
|
310
|
+
|
|
311
|
+
expect(session1.id).not.toBe(session2.id); // Different IDs
|
|
312
|
+
|
|
313
|
+
await createSession(session1);
|
|
314
|
+
await createSession(session2); // No conflicts!
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Helper Utilities
|
|
321
|
+
|
|
322
|
+
### Wait Helpers (`tests/helpers/wait-helpers.ts`)
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { Page } from '@playwright/test';
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Wait for element to be visible and enabled
|
|
329
|
+
*/
|
|
330
|
+
export async function waitForInteractive(page: Page, selector: string) {
|
|
331
|
+
await page.waitForSelector(selector, { state: 'visible' });
|
|
332
|
+
await page.waitForSelector(`${selector}:not([disabled])`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Wait for loading spinner to disappear
|
|
337
|
+
*/
|
|
338
|
+
export async function waitForLoadingComplete(page: Page) {
|
|
339
|
+
await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Wait for API call to complete
|
|
344
|
+
*/
|
|
345
|
+
export async function waitForApiResponse(page: Page, urlPattern: string) {
|
|
346
|
+
return page.waitForResponse(
|
|
347
|
+
response => response.url().includes(urlPattern) && response.ok()
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Wait for navigation and page load
|
|
353
|
+
*/
|
|
354
|
+
export async function waitForPageLoad(page: Page) {
|
|
355
|
+
await page.waitForLoadState('domcontentloaded');
|
|
356
|
+
await page.waitForLoadState('networkidle');
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Time Helpers (`tests/helpers/time-helpers.ts`)
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
/**
|
|
364
|
+
* Enable fake timers for testing time-dependent code
|
|
365
|
+
*/
|
|
366
|
+
export function useFakeTimers() {
|
|
367
|
+
jest.useFakeTimers();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Advance time by milliseconds (instant)
|
|
372
|
+
*/
|
|
373
|
+
export function advanceTime(ms: number) {
|
|
374
|
+
jest.advanceTimersByTime(ms);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Run all pending timers
|
|
379
|
+
*/
|
|
380
|
+
export function runAllTimers() {
|
|
381
|
+
jest.runAllTimers();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Restore real timers
|
|
386
|
+
*/
|
|
387
|
+
export function useRealTimers() {
|
|
388
|
+
jest.useRealTimers();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Mock Date.now() to return specific timestamp
|
|
393
|
+
*/
|
|
394
|
+
export function mockNow(timestamp: number) {
|
|
395
|
+
jest.spyOn(Date, 'now').mockReturnValue(timestamp);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Restore original Date.now()
|
|
400
|
+
*/
|
|
401
|
+
export function restoreNow() {
|
|
402
|
+
jest.spyOn(Date, 'now').mockRestore();
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Seed Data (`tests/helpers/seed-data.ts`)
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
let counter = 0;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Generate unique ID
|
|
413
|
+
*/
|
|
414
|
+
export function uniqueId(prefix = 'test'): string {
|
|
415
|
+
return `${prefix}-${Date.now()}-${counter++}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Generate unique session name
|
|
420
|
+
*/
|
|
421
|
+
export function uniqueSessionName(prefix = 'session'): string {
|
|
422
|
+
return uniqueId(prefix);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Create session seed data with defaults
|
|
427
|
+
* Uses .alphacoder paths to avoid /tmp permission issues
|
|
428
|
+
*/
|
|
429
|
+
export function seedSession(overrides: Partial<SessionData> = {}): SessionData {
|
|
430
|
+
return {
|
|
431
|
+
id: uniqueId('session'),
|
|
432
|
+
projectPath: `.alphacoder/sessions/test-data/test-${uniqueId()}`,
|
|
433
|
+
features: [],
|
|
434
|
+
createdAt: Date.now(),
|
|
435
|
+
status: 'initializing',
|
|
436
|
+
...overrides
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Create feature seed data with defaults
|
|
442
|
+
*/
|
|
443
|
+
export function seedFeature(overrides: Partial<FeatureData> = {}): FeatureData {
|
|
444
|
+
return {
|
|
445
|
+
id: uniqueId('feature'),
|
|
446
|
+
name: `Test Feature ${counter}`,
|
|
447
|
+
description: 'Test feature description',
|
|
448
|
+
status: 'pending',
|
|
449
|
+
...overrides
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Create project seed data with defaults
|
|
455
|
+
* Uses .alphacoder paths to avoid /tmp permission issues
|
|
456
|
+
*/
|
|
457
|
+
export function seedProject(overrides: Partial<ProjectData> = {}): ProjectData {
|
|
458
|
+
return {
|
|
459
|
+
id: uniqueId('project'),
|
|
460
|
+
name: `Test Project ${counter}`,
|
|
461
|
+
path: `.alphacoder/sessions/test-data/project-${uniqueId()}`,
|
|
462
|
+
...overrides
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Before/After Examples
|
|
470
|
+
|
|
471
|
+
### Example 1: Button Click and Response
|
|
472
|
+
|
|
473
|
+
**❌ BRITTLE**
|
|
474
|
+
```typescript
|
|
475
|
+
it('shows success message', async () => {
|
|
476
|
+
await page.click('[data-testid="submit"]');
|
|
477
|
+
await page.waitForTimeout(1000); // Arbitrary delay!
|
|
478
|
+
|
|
479
|
+
const message = await page.textContent('.message');
|
|
480
|
+
expect(message).toBe('Success');
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**✅ ROBUST**
|
|
485
|
+
```typescript
|
|
486
|
+
it('shows success message', async () => {
|
|
487
|
+
await page.click('[data-testid="submit"]');
|
|
488
|
+
|
|
489
|
+
// Wait for specific element
|
|
490
|
+
await page.waitForSelector('.message', { state: 'visible' });
|
|
491
|
+
|
|
492
|
+
// Or use auto-waiting assertion
|
|
493
|
+
await expect(page.locator('.message')).toHaveText('Success');
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### Example 2: Form Validation
|
|
500
|
+
|
|
501
|
+
**❌ BRITTLE**
|
|
502
|
+
```typescript
|
|
503
|
+
it('validates email', async () => {
|
|
504
|
+
await page.fill('#email', 'invalid');
|
|
505
|
+
await page.click('#submit');
|
|
506
|
+
await page.waitForTimeout(500); // What are we waiting for?
|
|
507
|
+
|
|
508
|
+
expect(await page.textContent('.error')).toContain('Invalid email');
|
|
509
|
+
});
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**✅ ROBUST**
|
|
513
|
+
```typescript
|
|
514
|
+
it('validates email', async () => {
|
|
515
|
+
await page.fill('#email', 'invalid');
|
|
516
|
+
await page.click('#submit');
|
|
517
|
+
|
|
518
|
+
// Wait for error message to appear
|
|
519
|
+
await expect(page.locator('.error')).toContainText('Invalid email');
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
### Example 3: Time-Based Logic
|
|
526
|
+
|
|
527
|
+
**❌ BRITTLE**
|
|
528
|
+
```typescript
|
|
529
|
+
it('session expires', async () => {
|
|
530
|
+
const session = await createSession('test');
|
|
531
|
+
|
|
532
|
+
// Wait actual 1.1 seconds!
|
|
533
|
+
await new Promise(resolve => setTimeout(resolve, 1100));
|
|
534
|
+
|
|
535
|
+
expect(session.expiresAt).toBeLessThan(Date.now());
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**✅ ROBUST**
|
|
540
|
+
```typescript
|
|
541
|
+
it('session expires', async () => {
|
|
542
|
+
jest.useFakeTimers();
|
|
543
|
+
|
|
544
|
+
const session = await createSession('test');
|
|
545
|
+
|
|
546
|
+
// Instantly advance time
|
|
547
|
+
jest.advanceTimersByTime(1100);
|
|
548
|
+
|
|
549
|
+
expect(session.expiresAt).toBeLessThan(Date.now());
|
|
550
|
+
|
|
551
|
+
jest.useRealTimers();
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
### Example 4: Test Data
|
|
558
|
+
|
|
559
|
+
**❌ BRITTLE**
|
|
560
|
+
```typescript
|
|
561
|
+
it('creates session', async () => {
|
|
562
|
+
// Hard-coded name - conflicts with parallel tests!
|
|
563
|
+
await createSession('my-session');
|
|
564
|
+
|
|
565
|
+
const session = await getSession('my-session');
|
|
566
|
+
expect(session).toBeDefined();
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**✅ ROBUST**
|
|
571
|
+
```typescript
|
|
572
|
+
it('creates session', async () => {
|
|
573
|
+
const sessionName = uniqueSessionName('my-session');
|
|
574
|
+
await createSession(sessionName);
|
|
575
|
+
|
|
576
|
+
const session = await getSession(sessionName);
|
|
577
|
+
expect(session).toBeDefined();
|
|
578
|
+
});
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
### Example 5: Loading States
|
|
584
|
+
|
|
585
|
+
**❌ BRITTLE**
|
|
586
|
+
```typescript
|
|
587
|
+
it('loads data', async () => {
|
|
588
|
+
await page.click('[data-testid="load"]');
|
|
589
|
+
await page.waitForTimeout(2000); // Hope data loads by then!
|
|
590
|
+
|
|
591
|
+
expect(await page.locator('.data').count()).toBeGreaterThan(0);
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**✅ ROBUST**
|
|
596
|
+
```typescript
|
|
597
|
+
it('loads data', async () => {
|
|
598
|
+
const responsePromise = page.waitForResponse('/api/data');
|
|
599
|
+
|
|
600
|
+
await page.click('[data-testid="load"]');
|
|
601
|
+
await responsePromise;
|
|
602
|
+
|
|
603
|
+
// Wait for UI to update
|
|
604
|
+
await expect(page.locator('.data').first()).toBeVisible();
|
|
605
|
+
expect(await page.locator('.data').count()).toBeGreaterThan(0);
|
|
606
|
+
});
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
### Example 6: Multi-Step Workflows
|
|
612
|
+
|
|
613
|
+
**❌ BRITTLE**
|
|
614
|
+
```typescript
|
|
615
|
+
it('completes workflow', async () => {
|
|
616
|
+
await page.click('[data-testid="step1"]');
|
|
617
|
+
await page.waitForTimeout(500);
|
|
618
|
+
|
|
619
|
+
await page.click('[data-testid="step2"]');
|
|
620
|
+
await page.waitForTimeout(500);
|
|
621
|
+
|
|
622
|
+
await page.click('[data-testid="step3"]');
|
|
623
|
+
await page.waitForTimeout(1000);
|
|
624
|
+
|
|
625
|
+
expect(await page.textContent('.result')).toBe('Complete');
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**✅ ROBUST**
|
|
630
|
+
```typescript
|
|
631
|
+
it('completes workflow', async () => {
|
|
632
|
+
// Step 1
|
|
633
|
+
await page.click('[data-testid="step1"]');
|
|
634
|
+
await expect(page.locator('[data-testid="step2"]')).toBeEnabled();
|
|
635
|
+
|
|
636
|
+
// Step 2
|
|
637
|
+
await page.click('[data-testid="step2"]');
|
|
638
|
+
await expect(page.locator('[data-testid="step3"]')).toBeEnabled();
|
|
639
|
+
|
|
640
|
+
// Step 3
|
|
641
|
+
await page.click('[data-testid="step3"]');
|
|
642
|
+
await expect(page.locator('.result')).toHaveText('Complete');
|
|
643
|
+
});
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## When to Use This Skill
|
|
649
|
+
|
|
650
|
+
- ✅ **Before writing any E2E tests** - Establish patterns upfront
|
|
651
|
+
- ✅ **Before writing integration tests** - Async behavior needs proper waits
|
|
652
|
+
- ✅ **When tests are flaky** - Random failures indicate timing issues
|
|
653
|
+
- ✅ **When tests are slow** - Look for `waitForTimeout` and `setTimeout`
|
|
654
|
+
- ✅ **When debugging test failures** - Check for brittle patterns
|
|
655
|
+
- ✅ **During code review** - Verify tests follow robust patterns
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Test Robustness Checklist
|
|
660
|
+
|
|
661
|
+
Use this checklist when writing or reviewing tests:
|
|
662
|
+
|
|
663
|
+
### Before Writing Tests
|
|
664
|
+
- [ ] Reviewed this skill document
|
|
665
|
+
- [ ] Imported helper utilities (`wait-helpers`, `time-helpers`, `seed-data`)
|
|
666
|
+
- [ ] Understand what conditions I'm waiting for (element, network, state)
|
|
667
|
+
|
|
668
|
+
### While Writing Tests
|
|
669
|
+
- [ ] **No `waitForTimeout` calls** - Use `waitForSelector`, `waitForResponse`, or assertions
|
|
670
|
+
- [ ] **No real time delays** - Use `jest.useFakeTimers()` and `advanceTimersByTime()`
|
|
671
|
+
- [ ] **All async operations have explicit waits** - Don't rely on arbitrary delays
|
|
672
|
+
- [ ] **Test data is unique** - Use `uniqueId()` or `seedSession()` helpers
|
|
673
|
+
- [ ] **Assertions use auto-wait** - Playwright's `expect()` automatically waits
|
|
674
|
+
|
|
675
|
+
### After Writing Tests
|
|
676
|
+
- [ ] **Run tests 10 times** - Verify no flakiness: `for i in {1..10}; do npm test; done`
|
|
677
|
+
- [ ] **Check test duration** - Should be fast (< 1s per test for unit, < 10s for E2E)
|
|
678
|
+
- [ ] **Verify in CI** - Tests pass consistently in CI environment
|
|
679
|
+
- [ ] **Review for clarity** - Explicit waits show what test expects
|
|
680
|
+
|
|
681
|
+
### Code Review Checklist
|
|
682
|
+
- [ ] **Zero `waitForTimeout` calls** - Flag for replacement
|
|
683
|
+
- [ ] **Zero `setTimeout` in tests** - Flag for fake timers
|
|
684
|
+
- [ ] **No hard-coded test data** - Should use helpers
|
|
685
|
+
- [ ] **Consistent uniqueness strategy** - All tests use same pattern
|
|
686
|
+
- [ ] **Clear wait conditions** - Can understand what test waits for
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Common Issues and Solutions
|
|
691
|
+
|
|
692
|
+
### Issue: "Element not found" Errors
|
|
693
|
+
|
|
694
|
+
**Problem**: Clicking element before it's ready
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
// ❌ Flaky
|
|
698
|
+
await page.click('[data-testid="button"]'); // May not exist yet!
|
|
699
|
+
|
|
700
|
+
// ✅ Robust
|
|
701
|
+
await page.waitForSelector('[data-testid="button"]', { state: 'visible' });
|
|
702
|
+
await page.click('[data-testid="button"]');
|
|
703
|
+
|
|
704
|
+
// ✅ Even better (auto-waits)
|
|
705
|
+
await expect(page.locator('[data-testid="button"]')).toBeVisible();
|
|
706
|
+
await page.click('[data-testid="button"]');
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Issue: "Timeout" Errors
|
|
710
|
+
|
|
711
|
+
**Problem**: Waiting for something that takes variable time
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
// ❌ Arbitrary timeout
|
|
715
|
+
await page.waitForTimeout(5000); // May be too short or too long
|
|
716
|
+
|
|
717
|
+
// ✅ Wait for specific condition
|
|
718
|
+
await page.waitForResponse(response =>
|
|
719
|
+
response.url().includes('/api/data') && response.ok()
|
|
720
|
+
);
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Issue: Tests Pass Locally, Fail in CI
|
|
724
|
+
|
|
725
|
+
**Problem**: CI is slower, arbitrary timeouts are too short
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
// ❌ Works locally (fast machine), fails CI (slow machine)
|
|
729
|
+
await page.waitForTimeout(1000);
|
|
730
|
+
|
|
731
|
+
// ✅ Works everywhere (waits for actual condition)
|
|
732
|
+
await expect(page.locator('.result')).toBeVisible();
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### Issue: Random Test Failures
|
|
736
|
+
|
|
737
|
+
**Problem**: Race conditions from parallel tests
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
// ❌ Tests conflict when run in parallel
|
|
741
|
+
it('test 1', async () => {
|
|
742
|
+
await createSession('my-session'); // Conflicts with test 2!
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('test 2', async () => {
|
|
746
|
+
await createSession('my-session'); // Same name!
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// ✅ Each test uses unique data
|
|
750
|
+
it('test 1', async () => {
|
|
751
|
+
await createSession(uniqueSessionName());
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('test 2', async () => {
|
|
755
|
+
await createSession(uniqueSessionName());
|
|
756
|
+
});
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Issue: Slow Test Suite
|
|
760
|
+
|
|
761
|
+
**Problem**: Using real timers for time-based tests
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
// ❌ Each test takes 1.1 seconds of real time
|
|
765
|
+
await new Promise(resolve => setTimeout(resolve, 1100));
|
|
766
|
+
|
|
767
|
+
// ✅ Each test takes ~0ms (instant)
|
|
768
|
+
jest.useFakeTimers();
|
|
769
|
+
jest.advanceTimersByTime(1100);
|
|
770
|
+
jest.useRealTimers();
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## Performance Impact
|
|
776
|
+
|
|
777
|
+
### Current State (Brittle Tests)
|
|
778
|
+
- **33 instances of `waitForTimeout`** - Each adds 100-2000ms
|
|
779
|
+
- **6 instances of `setTimeout(1100)`** - Adds 6.6 seconds
|
|
780
|
+
- **Total overhead**: ~15-30 seconds per test run
|
|
781
|
+
|
|
782
|
+
### After Refactoring (Robust Tests)
|
|
783
|
+
- **0 arbitrary timeouts** - All waits are condition-based
|
|
784
|
+
- **0 real time delays** - All timers are mocked
|
|
785
|
+
- **Total overhead**: ~0-2 seconds (only real network/UI time)
|
|
786
|
+
|
|
787
|
+
**Expected speedup**: 10-15x faster test suite
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## Migration Strategy
|
|
792
|
+
|
|
793
|
+
### Step 1: Audit Current Tests
|
|
794
|
+
```bash
|
|
795
|
+
# Find all waitForTimeout calls
|
|
796
|
+
grep -rn "waitForTimeout" tests/
|
|
797
|
+
|
|
798
|
+
# Find all setTimeout calls
|
|
799
|
+
grep -rn "setTimeout" tests/
|
|
800
|
+
|
|
801
|
+
# Find hard-coded test data
|
|
802
|
+
grep -rn "'session-" tests/
|
|
803
|
+
grep -rn "'feature-" tests/
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Step 2: Create Helper Files
|
|
807
|
+
1. Create `tests/helpers/wait-helpers.ts`
|
|
808
|
+
2. Create `tests/helpers/time-helpers.ts`
|
|
809
|
+
3. Create `tests/helpers/seed-data.ts`
|
|
810
|
+
|
|
811
|
+
### Step 3: Replace Patterns (One Test at a Time)
|
|
812
|
+
1. Replace `waitForTimeout` → `waitForSelector` or assertions
|
|
813
|
+
2. Replace `setTimeout` → `jest.useFakeTimers()` + `advanceTimersByTime()`
|
|
814
|
+
3. Replace hard-coded data → `uniqueSessionName()` or `seedSession()`
|
|
815
|
+
|
|
816
|
+
### Step 4: Verify Improvements
|
|
817
|
+
```bash
|
|
818
|
+
# Run tests 10 times to check for flakiness
|
|
819
|
+
for i in {1..10}; do npm test && echo "Run $i: PASS" || echo "Run $i: FAIL"; done
|
|
820
|
+
|
|
821
|
+
# Measure test duration
|
|
822
|
+
time npm test
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### Step 5: Add to CI Checks
|
|
826
|
+
- Lint rule: Flag `waitForTimeout` in PR reviews
|
|
827
|
+
- Lint rule: Flag `setTimeout` in test files
|
|
828
|
+
- Pre-commit hook: Validate test patterns
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## Reference
|
|
833
|
+
|
|
834
|
+
- **Playwright Waiting**: https://playwright.dev/docs/actionability
|
|
835
|
+
- **Playwright Assertions**: https://playwright.dev/docs/test-assertions
|
|
836
|
+
- **Jest Fake Timers**: https://jestjs.io/docs/timer-mocks
|
|
837
|
+
- **Testing Best Practices**: https://kentcdodds.com/blog/common-mistakes-with-react-testing-library
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## Quick Reference
|
|
842
|
+
|
|
843
|
+
### Replace These Patterns
|
|
844
|
+
|
|
845
|
+
| ❌ Brittle Pattern | ✅ Robust Pattern |
|
|
846
|
+
|-------------------|------------------|
|
|
847
|
+
| `await page.waitForTimeout(1000)` | `await page.waitForSelector('.element')` |
|
|
848
|
+
| `await page.waitForTimeout(500)` | `await expect(page.locator('.element')).toBeVisible()` |
|
|
849
|
+
| `setTimeout(() => {}, 1100)` | `jest.advanceTimersByTime(1100)` |
|
|
850
|
+
| `await createSession('my-session')` | `await createSession(uniqueSessionName())` |
|
|
851
|
+
| `const id = 'test-123'` | `const id = uniqueId('test')` |
|
|
852
|
+
| `await page.click(); await delay(500)` | `await page.click(); await waitForLoadingComplete(page)` |
|
|
853
|
+
|
|
854
|
+
### Import These Helpers
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
// Wait helpers
|
|
858
|
+
import {
|
|
859
|
+
waitForInteractive,
|
|
860
|
+
waitForLoadingComplete,
|
|
861
|
+
waitForApiResponse,
|
|
862
|
+
waitForPageLoad
|
|
863
|
+
} from '../helpers/wait-helpers';
|
|
864
|
+
|
|
865
|
+
// Time helpers
|
|
866
|
+
import {
|
|
867
|
+
useFakeTimers,
|
|
868
|
+
advanceTime,
|
|
869
|
+
runAllTimers,
|
|
870
|
+
useRealTimers,
|
|
871
|
+
mockNow,
|
|
872
|
+
restoreNow
|
|
873
|
+
} from '../helpers/time-helpers';
|
|
874
|
+
|
|
875
|
+
// Seed data helpers
|
|
876
|
+
import {
|
|
877
|
+
uniqueId,
|
|
878
|
+
uniqueSessionName,
|
|
879
|
+
seedSession,
|
|
880
|
+
seedFeature,
|
|
881
|
+
seedProject
|
|
882
|
+
} from '../helpers/seed-data';
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
---
|
|
886
|
+
|
|
887
|
+
## Summary
|
|
888
|
+
|
|
889
|
+
**Golden Rules for Robust Tests:**
|
|
890
|
+
|
|
891
|
+
1. **Never use `waitForTimeout`** - Wait for specific conditions
|
|
892
|
+
2. **Never use real timers** - Mock time for instant tests
|
|
893
|
+
3. **Never hard-code test data** - Use unique, generated data
|
|
894
|
+
4. **Always wait for conditions** - Element visible, network complete, state changed
|
|
895
|
+
5. **Always use helpers** - DRY, consistent, maintainable
|
|
896
|
+
|
|
897
|
+
**Result**: Fast, reliable, maintainable test suite that never flakes.
|