@pageai/ralph-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/.agent/PROMPT.md +58 -0
- package/.agent/STEERING.md +3 -0
- package/.agent/logs/LOG.md +13 -0
- package/.agent/prd/.gitkeep +0 -0
- package/.agent/screenshots/.gitkeep +0 -0
- package/.agent/skills/component-refactoring/SKILL.md +247 -0
- package/.agent/skills/component-refactoring/references/complexity-patterns.md +485 -0
- package/.agent/skills/component-refactoring/references/component-splitting.md +419 -0
- package/.agent/skills/component-refactoring/references/hook-extraction.md +317 -0
- package/.agent/skills/e2e-tester/SKILL.md +595 -0
- package/.agent/skills/frontend-code-review/SKILL.md +73 -0
- package/.agent/skills/frontend-code-review/references/code-quality.md +28 -0
- package/.agent/skills/frontend-code-review/references/performance.md +36 -0
- package/.agent/skills/frontend-testing/SKILL.md +316 -0
- package/.agent/skills/frontend-testing/assets/component-test.template.tsx +293 -0
- package/.agent/skills/frontend-testing/assets/hook-test.template.ts +207 -0
- package/.agent/skills/frontend-testing/assets/utility-test.template.ts +154 -0
- package/.agent/skills/frontend-testing/references/async-testing.md +345 -0
- package/.agent/skills/frontend-testing/references/checklist.md +188 -0
- package/.agent/skills/frontend-testing/references/common-patterns.md +449 -0
- package/.agent/skills/frontend-testing/references/mocking.md +289 -0
- package/.agent/skills/frontend-testing/references/workflow.md +265 -0
- package/.agent/skills/prd-creator/JSON.md +613 -0
- package/.agent/skills/prd-creator/PRD.md +196 -0
- package/.agent/skills/prd-creator/SKILL.md +143 -0
- package/.agent/skills/skill-creator/SKILL.md +355 -0
- package/.agent/skills/skill-creator/references/output-patterns.md +86 -0
- package/.agent/skills/skill-creator/references/workflows.md +28 -0
- package/.agent/skills/skill-creator/scripts/init_skill.py +300 -0
- package/.agent/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.agent/skills/vercel-react-best-practices/AGENTS.md +2249 -0
- package/.agent/skills/vercel-react-best-practices/SKILL.md +125 -0
- package/.agent/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agent/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agent/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agent/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-cache-react.md +26 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +79 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.agent/skills/vitest-best-practices/AGENTS.md +84 -0
- package/.agent/skills/vitest-best-practices/SKILL.md +130 -0
- package/.agent/skills/vitest-best-practices/references/aaa-pattern.md +260 -0
- package/.agent/skills/vitest-best-practices/references/assertions.md +393 -0
- package/.agent/skills/vitest-best-practices/references/async-testing.md +454 -0
- package/.agent/skills/vitest-best-practices/references/error-handling.md +382 -0
- package/.agent/skills/vitest-best-practices/references/organization.md +212 -0
- package/.agent/skills/vitest-best-practices/references/parameterized-tests.md +297 -0
- package/.agent/skills/vitest-best-practices/references/performance.md +528 -0
- package/.agent/skills/vitest-best-practices/references/snapshot-testing.md +483 -0
- package/.agent/skills/vitest-best-practices/references/test-doubles.md +499 -0
- package/.agent/skills/vitest-best-practices/references/vitest-features.md +529 -0
- package/.agent/skills/web-design-guidelines/SKILL.md +39 -0
- package/.agent/tasks/.gitkeep +0 -0
- package/.agent/tasks.json +1 -0
- package/.claude/agents/code-reviewer.md +172 -0
- package/.claude/commands/aw.md +50 -0
- package/.claude/hooks/play-sound.js +87 -0
- package/.claude/hooks/pre-tool-use.js +40 -0
- package/.claude/settings.json +54 -0
- package/.claude/settings.local.json +13 -0
- package/.mcp.json +31 -0
- package/AGENTS.md +44 -0
- package/CLAUDE.md +1 -0
- package/README.md +236 -0
- package/bin/cli.js +156 -0
- package/bin/lib/copy.js +149 -0
- package/bin/lib/display.js +137 -0
- package/package.json +65 -0
- package/ralph.sh +333 -0
- package/scripts/lib/args.sh +44 -0
- package/scripts/lib/cleanup.sh +53 -0
- package/scripts/lib/constants.sh +25 -0
- package/scripts/lib/display.sh +196 -0
- package/scripts/lib/logging.sh +30 -0
- package/scripts/lib/notify.sh +41 -0
- package/scripts/lib/output.sh +147 -0
- package/scripts/lib/preflight.sh +57 -0
- package/scripts/lib/preview.sh +77 -0
- package/scripts/lib/promise.sh +76 -0
- package/scripts/lib/spinner.sh +85 -0
- package/scripts/lib/terminal.sh +57 -0
- package/scripts/lib/timing.sh +223 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# 1.7 Async Testing
|
|
2
|
+
|
|
3
|
+
Testing asynchronous code requires special handling to ensure tests wait for async operations to complete.
|
|
4
|
+
|
|
5
|
+
## Basic Async/Await
|
|
6
|
+
|
|
7
|
+
Always use `async`/`await` for testing async functions.
|
|
8
|
+
|
|
9
|
+
**✅ Correct: async/await**
|
|
10
|
+
```ts
|
|
11
|
+
describe('fetchUser', () => {
|
|
12
|
+
it('should return user data', async () => {
|
|
13
|
+
const user = await fetchUser('user-123');
|
|
14
|
+
|
|
15
|
+
expect(user).toEqual({
|
|
16
|
+
id: 'user-123',
|
|
17
|
+
name: 'John Doe',
|
|
18
|
+
email: 'john@example.com',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**❌ Incorrect: not awaiting async function**
|
|
25
|
+
```ts
|
|
26
|
+
it('should return user data', () => {
|
|
27
|
+
const user = fetchUser('user-123'); // Returns Promise, not user!
|
|
28
|
+
expect(user).toEqual({ id: 'user-123' }); // Test passes but wrong!
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
*Why?* Without `await`, you're comparing a Promise object, not the actual result.
|
|
32
|
+
|
|
33
|
+
## Testing Promises
|
|
34
|
+
|
|
35
|
+
Use `resolves` and `rejects` matchers for clean promise testing.
|
|
36
|
+
|
|
37
|
+
**✅ Correct: using resolves matcher**
|
|
38
|
+
```ts
|
|
39
|
+
it('should resolve with user data', async () => {
|
|
40
|
+
await expect(fetchUser('user-123')).resolves.toEqual({
|
|
41
|
+
id: 'user-123',
|
|
42
|
+
name: 'John Doe',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**✅ Correct: using rejects matcher**
|
|
48
|
+
```ts
|
|
49
|
+
it('should reject for invalid user', async () => {
|
|
50
|
+
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**❌ Incorrect: manual promise handling**
|
|
55
|
+
```ts
|
|
56
|
+
it('should return user', () => {
|
|
57
|
+
return fetchUser('user-123').then(user => {
|
|
58
|
+
expect(user.id).toBe('user-123');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
*Why?* While this works, `async`/`await` is clearer and more maintainable.
|
|
63
|
+
|
|
64
|
+
## Testing Multiple Async Operations
|
|
65
|
+
|
|
66
|
+
**✅ Correct: sequential async operations**
|
|
67
|
+
```ts
|
|
68
|
+
it('should create and update user', async () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
const userData = { name: 'John', email: 'john@example.com' };
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
const created = await userService.create(userData);
|
|
74
|
+
const updated = await userService.update(created.id, { name: 'Jane' });
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(updated.name).toBe('Jane');
|
|
78
|
+
expect(updated.email).toBe('john@example.com');
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**✅ Correct: parallel async operations**
|
|
83
|
+
```ts
|
|
84
|
+
it('should fetch multiple users in parallel', async () => {
|
|
85
|
+
const [user1, user2, user3] = await Promise.all([
|
|
86
|
+
fetchUser('user-1'),
|
|
87
|
+
fetchUser('user-2'),
|
|
88
|
+
fetchUser('user-3'),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
expect(user1.id).toBe('user-1');
|
|
92
|
+
expect(user2.id).toBe('user-2');
|
|
93
|
+
expect(user3.id).toBe('user-3');
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Testing Async Callbacks
|
|
98
|
+
|
|
99
|
+
For functions that use callbacks, promisify them or use done callback.
|
|
100
|
+
|
|
101
|
+
**✅ Correct: promisify callback-based code**
|
|
102
|
+
```ts
|
|
103
|
+
function fetchDataCallback(callback: (err: Error | null, data?: Data) => void) {
|
|
104
|
+
// ... async operation
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fetchDataPromise(): Promise<Data> {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
fetchDataCallback((err, data) => {
|
|
110
|
+
if (err) reject(err);
|
|
111
|
+
else resolve(data!);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
it('should fetch data', async () => {
|
|
117
|
+
const data = await fetchDataPromise();
|
|
118
|
+
expect(data).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**✅ Correct: using done callback (when promisify isn't possible)**
|
|
123
|
+
```ts
|
|
124
|
+
it('should call callback with data', (done) => {
|
|
125
|
+
fetchDataCallback((err, data) => {
|
|
126
|
+
expect(err).toBeNull();
|
|
127
|
+
expect(data).toBeDefined();
|
|
128
|
+
done();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Testing Timeouts and Delays
|
|
134
|
+
|
|
135
|
+
Use fake timers to speed up tests that involve delays.
|
|
136
|
+
|
|
137
|
+
**✅ Correct: fake timers for delays**
|
|
138
|
+
```ts
|
|
139
|
+
describe('retry logic', () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
vi.useFakeTimers();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
vi.useRealTimers();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should retry after delay', async () => {
|
|
149
|
+
const mockFn = vi.fn()
|
|
150
|
+
.mockRejectedValueOnce(new Error('Fail'))
|
|
151
|
+
.mockResolvedValueOnce('Success');
|
|
152
|
+
|
|
153
|
+
const promise = retryWithDelay(mockFn, { delay: 1000, maxRetries: 2 });
|
|
154
|
+
|
|
155
|
+
// Fast-forward time
|
|
156
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
157
|
+
|
|
158
|
+
const result = await promise;
|
|
159
|
+
expect(result).toBe('Success');
|
|
160
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**❌ Incorrect: actually waiting for delays**
|
|
166
|
+
```ts
|
|
167
|
+
it('should retry after delay', async () => {
|
|
168
|
+
// Test takes 1+ second to run!
|
|
169
|
+
const result = await retryWithDelay(mockFn, { delay: 1000 });
|
|
170
|
+
expect(result).toBe('Success');
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
*Why?* Real delays slow down your test suite. Use fake timers instead.
|
|
174
|
+
|
|
175
|
+
## Testing Concurrent Operations
|
|
176
|
+
|
|
177
|
+
Test that async operations work correctly when running concurrently.
|
|
178
|
+
|
|
179
|
+
**✅ Correct: testing race conditions**
|
|
180
|
+
```ts
|
|
181
|
+
describe('RateLimiter', () => {
|
|
182
|
+
it('should limit concurrent requests', async () => {
|
|
183
|
+
const limiter = new RateLimiter({ maxConcurrent: 2 });
|
|
184
|
+
const calls: number[] = [];
|
|
185
|
+
|
|
186
|
+
const task = async (id: number) => {
|
|
187
|
+
await limiter.acquire();
|
|
188
|
+
calls.push(id);
|
|
189
|
+
await delay(10);
|
|
190
|
+
limiter.release();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
await Promise.all([
|
|
194
|
+
task(1),
|
|
195
|
+
task(2),
|
|
196
|
+
task(3),
|
|
197
|
+
task(4),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
expect(calls).toHaveLength(4);
|
|
201
|
+
// Verify no more than 2 concurrent
|
|
202
|
+
expect(limiter.getMaxConcurrent()).toBe(2);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Testing Async Iteration
|
|
208
|
+
|
|
209
|
+
Test async generators and iterables properly.
|
|
210
|
+
|
|
211
|
+
**✅ Correct: testing async generators**
|
|
212
|
+
```ts
|
|
213
|
+
async function* generateNumbers() {
|
|
214
|
+
yield 1;
|
|
215
|
+
yield 2;
|
|
216
|
+
yield 3;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
it('should yield all numbers', async () => {
|
|
220
|
+
const numbers: number[] = [];
|
|
221
|
+
|
|
222
|
+
for await (const num of generateNumbers()) {
|
|
223
|
+
numbers.push(num);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
expect(numbers).toEqual([1, 2, 3]);
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Testing Event Emitters
|
|
231
|
+
|
|
232
|
+
Wait for async events using promises.
|
|
233
|
+
|
|
234
|
+
**✅ Correct: testing event emitters**
|
|
235
|
+
```ts
|
|
236
|
+
it('should emit "complete" event after processing', async () => {
|
|
237
|
+
const processor = new DataProcessor();
|
|
238
|
+
|
|
239
|
+
const completePromise = new Promise((resolve) => {
|
|
240
|
+
processor.once('complete', resolve);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
processor.process(data);
|
|
244
|
+
|
|
245
|
+
await completePromise;
|
|
246
|
+
expect(processor.isComplete()).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**✅ Correct: testing multiple events**
|
|
251
|
+
```ts
|
|
252
|
+
it('should emit progress events', async () => {
|
|
253
|
+
const processor = new DataProcessor();
|
|
254
|
+
const events: string[] = [];
|
|
255
|
+
|
|
256
|
+
processor.on('progress', (event) => {
|
|
257
|
+
events.push(event);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const completePromise = new Promise((resolve) => {
|
|
261
|
+
processor.once('complete', resolve);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
processor.process(data);
|
|
265
|
+
await completePromise;
|
|
266
|
+
|
|
267
|
+
expect(events).toEqual(['started', 'processing', 'done']);
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Testing Async Setup/Teardown
|
|
272
|
+
|
|
273
|
+
Use async `beforeEach` and `afterEach` for async setup.
|
|
274
|
+
|
|
275
|
+
**✅ Correct: async setup and teardown**
|
|
276
|
+
```ts
|
|
277
|
+
describe('DatabaseTests', () => {
|
|
278
|
+
let db: Database;
|
|
279
|
+
|
|
280
|
+
beforeEach(async () => {
|
|
281
|
+
db = await createDatabase();
|
|
282
|
+
await db.migrate();
|
|
283
|
+
await db.seed();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
afterEach(async () => {
|
|
287
|
+
await db.clear();
|
|
288
|
+
await db.close();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should query users', async () => {
|
|
292
|
+
const users = await db.query('SELECT * FROM users');
|
|
293
|
+
expect(users).toHaveLength(3);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Testing Promise Rejection
|
|
299
|
+
|
|
300
|
+
Always test both success and failure paths for async operations.
|
|
301
|
+
|
|
302
|
+
**✅ Correct: testing rejection cases**
|
|
303
|
+
```ts
|
|
304
|
+
describe('authenticateUser', () => {
|
|
305
|
+
it('should resolve with token for valid credentials', async () => {
|
|
306
|
+
const token = await authenticateUser('user@example.com', 'password123');
|
|
307
|
+
expect(token).toMatch(/^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should reject for invalid credentials', async () => {
|
|
311
|
+
await expect(
|
|
312
|
+
authenticateUser('user@example.com', 'wrongpassword')
|
|
313
|
+
).rejects.toThrow('Invalid credentials');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should reject for non-existent user', async () => {
|
|
317
|
+
await expect(
|
|
318
|
+
authenticateUser('nonexistent@example.com', 'password')
|
|
319
|
+
).rejects.toThrow('User not found');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Avoiding Unhandled Promise Rejections
|
|
325
|
+
|
|
326
|
+
Always handle or assert on promises.
|
|
327
|
+
|
|
328
|
+
**❌ Incorrect: unhandled promise**
|
|
329
|
+
```ts
|
|
330
|
+
it('should handle error', () => {
|
|
331
|
+
fetchUser('invalid'); // Promise rejection not handled!
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**✅ Correct: handling promise**
|
|
336
|
+
```ts
|
|
337
|
+
it('should handle error', async () => {
|
|
338
|
+
await expect(fetchUser('invalid')).rejects.toThrow();
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Testing Async Error Handling
|
|
343
|
+
|
|
344
|
+
Verify that errors are properly caught and handled.
|
|
345
|
+
|
|
346
|
+
**✅ Correct: testing try/catch behavior**
|
|
347
|
+
```ts
|
|
348
|
+
async function processWithRetry(fn: () => Promise<any>) {
|
|
349
|
+
try {
|
|
350
|
+
return await fn();
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// Retry once
|
|
353
|
+
return await fn();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
it('should retry on failure', async () => {
|
|
358
|
+
const mockFn = vi.fn()
|
|
359
|
+
.mockRejectedValueOnce(new Error('Fail'))
|
|
360
|
+
.mockResolvedValueOnce('Success');
|
|
361
|
+
|
|
362
|
+
const result = await processWithRetry(mockFn);
|
|
363
|
+
|
|
364
|
+
expect(result).toBe('Success');
|
|
365
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Testing Async Timeout Behavior
|
|
370
|
+
|
|
371
|
+
Test that operations timeout correctly.
|
|
372
|
+
|
|
373
|
+
**✅ Correct: testing timeouts**
|
|
374
|
+
```ts
|
|
375
|
+
describe('fetchWithTimeout', () => {
|
|
376
|
+
beforeEach(() => {
|
|
377
|
+
vi.useFakeTimers();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
afterEach(() => {
|
|
381
|
+
vi.useRealTimers();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should timeout after specified duration', async () => {
|
|
385
|
+
const slowFn = () => new Promise(resolve => {
|
|
386
|
+
setTimeout(() => resolve('done'), 5000);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const promise = fetchWithTimeout(slowFn, 1000);
|
|
390
|
+
|
|
391
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
392
|
+
|
|
393
|
+
await expect(promise).rejects.toThrow('Timeout');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should resolve before timeout', async () => {
|
|
397
|
+
const fastFn = () => new Promise(resolve => {
|
|
398
|
+
setTimeout(() => resolve('done'), 500);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const promise = fetchWithTimeout(fastFn, 1000);
|
|
402
|
+
|
|
403
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
404
|
+
|
|
405
|
+
await expect(promise).resolves.toBe('done');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Common Async Testing Anti-Patterns
|
|
411
|
+
|
|
412
|
+
**❌ Incorrect: forgetting async keyword**
|
|
413
|
+
```ts
|
|
414
|
+
it('should fetch user', () => { // Missing async!
|
|
415
|
+
await fetchUser('user-123'); // Syntax error!
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**❌ Incorrect: mixing async/await with done**
|
|
420
|
+
```ts
|
|
421
|
+
it('should fetch user', async (done) => { // Don't mix these!
|
|
422
|
+
const user = await fetchUser('user-123');
|
|
423
|
+
expect(user).toBeDefined();
|
|
424
|
+
done();
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**❌ Incorrect: not returning or awaiting promise**
|
|
429
|
+
```ts
|
|
430
|
+
it('should fetch user', () => {
|
|
431
|
+
// Promise not returned or awaited - test finishes before fetch!
|
|
432
|
+
fetchUser('user-123').then(user => {
|
|
433
|
+
expect(user).toBeDefined();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
*Fix:* Either `await` the promise or `return` it.
|
|
438
|
+
|
|
439
|
+
**✅ Correct: returning promise**
|
|
440
|
+
```ts
|
|
441
|
+
it('should fetch user', () => {
|
|
442
|
+
return fetchUser('user-123').then(user => {
|
|
443
|
+
expect(user).toBeDefined();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**✅ Better: using async/await**
|
|
449
|
+
```ts
|
|
450
|
+
it('should fetch user', async () => {
|
|
451
|
+
const user = await fetchUser('user-123');
|
|
452
|
+
expect(user).toBeDefined();
|
|
453
|
+
});
|
|
454
|
+
```
|