@martinffx/atelier 2.0.0 → 3.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +94 -253
  3. package/package.json +1 -1
  4. package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
  5. package/skills/atelier-setup/SKILL.md +80 -0
  6. package/skills/code-review/references/reviewers.md +3 -3
  7. package/skills/code-review/references/rq.md +6 -6
  8. package/skills/spec-brainstorm/SKILL.md +5 -5
  9. package/skills/spec-implement/SKILL.md +2 -3
  10. package/skills/spec-plan/SKILL.md +21 -29
  11. package/skills/spec-plan/references/plan_template.md +77 -0
  12. package/skills/python-architecture/SKILL.md +0 -148
  13. package/skills/python-architecture/references/data-modeling.md +0 -558
  14. package/skills/python-architecture/references/ddd.md +0 -569
  15. package/skills/python-architecture/references/functional-core.md +0 -337
  16. package/skills/python-build-tools/SKILL.md +0 -153
  17. package/skills/python-build-tools/references/basedpyright.md +0 -321
  18. package/skills/python-build-tools/references/ruff.md +0 -225
  19. package/skills/python-build-tools/references/uv.md +0 -257
  20. package/skills/python-fastapi/SKILL.md +0 -184
  21. package/skills/python-fastapi/references/api-design.md +0 -563
  22. package/skills/python-fastapi/references/dependencies.md +0 -479
  23. package/skills/python-fastapi/references/middleware.md +0 -475
  24. package/skills/python-fastapi/references/validation.md +0 -497
  25. package/skills/python-modern-python/SKILL.md +0 -201
  26. package/skills/python-modern-python/references/async.md +0 -76
  27. package/skills/python-modern-python/references/pattern-matching.md +0 -89
  28. package/skills/python-modern-python/references/typing.md +0 -96
  29. package/skills/python-monorepo/SKILL.md +0 -146
  30. package/skills/python-monorepo/references/docker.md +0 -515
  31. package/skills/python-monorepo/references/namespace-packages.md +0 -426
  32. package/skills/python-monorepo/references/workspace-config.md +0 -341
  33. package/skills/python-sqlalchemy/SKILL.md +0 -119
  34. package/skills/python-sqlalchemy/references/async.md +0 -51
  35. package/skills/python-sqlalchemy/references/models.md +0 -67
  36. package/skills/python-sqlalchemy/references/queries.md +0 -57
  37. package/skills/python-temporal/SKILL.md +0 -128
  38. package/skills/python-temporal/references/patterns.md +0 -87
  39. package/skills/python-temporal/references/testing.md +0 -54
  40. package/skills/python-testing/SKILL.md +0 -161
  41. package/skills/python-testing/references/boundaries.md +0 -449
  42. package/skills/python-testing/references/mocking.md +0 -449
  43. package/skills/python-testing/references/pytest.md +0 -524
  44. package/skills/typescript-api-design/SKILL.md +0 -396
  45. package/skills/typescript-api-design/references/error-responses.md +0 -469
  46. package/skills/typescript-build-tools/SKILL.md +0 -447
  47. package/skills/typescript-build-tools/references/biome.md +0 -436
  48. package/skills/typescript-build-tools/references/bun.md +0 -432
  49. package/skills/typescript-build-tools/references/turborepo.md +0 -426
  50. package/skills/typescript-build-tools/references/vitest.md +0 -518
  51. package/skills/typescript-drizzle-orm/SKILL.md +0 -398
  52. package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
  53. package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
  54. package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
  55. package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
  56. package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
  57. package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
  58. package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
  59. package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
  60. package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
  61. package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
  62. package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
  63. package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
  64. package/skills/typescript-effect-ts/SKILL.md +0 -341
  65. package/skills/typescript-effect-ts/references/core.md +0 -412
  66. package/skills/typescript-effect-ts/references/error-handling.md +0 -468
  67. package/skills/typescript-effect-ts/references/resources.md +0 -476
  68. package/skills/typescript-effect-ts/references/schema.md +0 -536
  69. package/skills/typescript-effect-ts/references/services.md +0 -461
  70. package/skills/typescript-fastify/SKILL.md +0 -405
  71. package/skills/typescript-fastify/references/plugins.md +0 -329
  72. package/skills/typescript-fastify/references/typeid.md +0 -315
  73. package/skills/typescript-functional-patterns/SKILL.md +0 -402
  74. package/skills/typescript-functional-patterns/references/adts.md +0 -497
  75. package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
  76. package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
  77. package/skills/typescript-functional-patterns/references/option-result.md +0 -585
  78. package/skills/typescript-testing/SKILL.md +0 -353
  79. package/skills/typescript-testing/references/mocking.md +0 -675
  80. package/skills/typescript-testing/references/msw.md +0 -852
  81. package/skills/typescript-testing/references/snapshot-testing.md +0 -774
@@ -1,852 +0,0 @@
1
- # MSW (Mock Service Worker) Patterns
2
-
3
- Comprehensive guide to mocking HTTP requests at the network level using MSW with Vitest.
4
-
5
- ## Installation and Setup
6
-
7
- ### Dependencies
8
-
9
- ```bash
10
- bun add -d msw
11
- ```
12
-
13
- ### Basic Server Setup
14
-
15
- ```typescript
16
- // tests/setup.ts
17
- import { setupServer } from 'msw/node'
18
- import { http, HttpResponse } from 'msw'
19
- import { beforeAll, afterEach, afterAll } from 'vitest'
20
-
21
- // Define default handlers
22
- export const handlers = [
23
- http.get('/api/users', () => {
24
- return HttpResponse.json([
25
- { id: '1', name: 'Alice' },
26
- { id: '2', name: 'Bob' },
27
- ])
28
- }),
29
- ]
30
-
31
- // Create server instance
32
- export const server = setupServer(...handlers)
33
-
34
- // Setup lifecycle hooks
35
- beforeAll(() => {
36
- server.listen({ onUnhandledRequest: 'error' })
37
- })
38
-
39
- afterEach(() => {
40
- server.resetHandlers()
41
- })
42
-
43
- afterAll(() => {
44
- server.close()
45
- })
46
- ```
47
-
48
- ### Vitest Configuration
49
-
50
- ```typescript
51
- // vitest.config.ts
52
- import { defineConfig } from 'vitest/config'
53
-
54
- export default defineConfig({
55
- test: {
56
- setupFiles: ['./tests/setup.ts'],
57
- },
58
- })
59
- ```
60
-
61
- ## REST API Handlers
62
-
63
- ### HTTP Methods
64
-
65
- ```typescript
66
- import { http, HttpResponse } from 'msw'
67
-
68
- const handlers = [
69
- // GET
70
- http.get('/api/users', () => {
71
- return HttpResponse.json([{ id: '1', name: 'Alice' }])
72
- }),
73
-
74
- // POST
75
- http.post('/api/users', async ({ request }) => {
76
- const body = await request.json()
77
- return HttpResponse.json(
78
- { id: '123', ...body },
79
- { status: 201 }
80
- )
81
- }),
82
-
83
- // PUT
84
- http.put('/api/users/:id', async ({ params, request }) => {
85
- const body = await request.json()
86
- return HttpResponse.json({ id: params.id, ...body })
87
- }),
88
-
89
- // PATCH
90
- http.patch('/api/users/:id', async ({ params, request }) => {
91
- const updates = await request.json()
92
- return HttpResponse.json({ id: params.id, ...updates })
93
- }),
94
-
95
- // DELETE
96
- http.delete('/api/users/:id', ({ params }) => {
97
- return new HttpResponse(null, { status: 204 })
98
- }),
99
- ]
100
- ```
101
-
102
- ### Path Parameters
103
-
104
- ```typescript
105
- // Single parameter
106
- http.get('/api/users/:id', ({ params }) => {
107
- const { id } = params
108
- return HttpResponse.json({ id, name: 'Alice' })
109
- })
110
-
111
- // Multiple parameters
112
- http.get('/api/orgs/:orgId/repos/:repoId', ({ params }) => {
113
- const { orgId, repoId } = params
114
- return HttpResponse.json({
115
- org: orgId,
116
- repo: repoId,
117
- name: 'my-repo',
118
- })
119
- })
120
-
121
- // Wildcard
122
- http.get('/api/files/*', ({ params }) => {
123
- const path = params[0] // matches everything after /files/
124
- return HttpResponse.json({ path })
125
- })
126
- ```
127
-
128
- ### Query Parameters
129
-
130
- ```typescript
131
- http.get('/api/users', ({ request }) => {
132
- const url = new URL(request.url)
133
- const page = url.searchParams.get('page') || '1'
134
- const limit = url.searchParams.get('limit') || '10'
135
-
136
- return HttpResponse.json({
137
- data: users.slice(
138
- (parseInt(page) - 1) * parseInt(limit),
139
- parseInt(page) * parseInt(limit)
140
- ),
141
- page: parseInt(page),
142
- total: users.length,
143
- })
144
- })
145
- ```
146
-
147
- ### Request Headers
148
-
149
- ```typescript
150
- http.get('/api/protected', ({ request }) => {
151
- const auth = request.headers.get('Authorization')
152
-
153
- if (!auth || !auth.startsWith('Bearer ')) {
154
- return new HttpResponse(null, {
155
- status: 401,
156
- statusText: 'Unauthorized',
157
- })
158
- }
159
-
160
- const token = auth.substring(7)
161
- if (token !== 'valid-token') {
162
- return new HttpResponse(
163
- JSON.stringify({ error: 'Invalid token' }),
164
- { status: 403 }
165
- )
166
- }
167
-
168
- return HttpResponse.json({ message: 'Access granted' })
169
- })
170
- ```
171
-
172
- ### Request Body
173
-
174
- ```typescript
175
- // JSON body
176
- http.post('/api/users', async ({ request }) => {
177
- const body = await request.json()
178
-
179
- if (!body.email) {
180
- return HttpResponse.json(
181
- { error: 'Email required' },
182
- { status: 400 }
183
- )
184
- }
185
-
186
- return HttpResponse.json({ id: '123', ...body }, { status: 201 })
187
- })
188
-
189
- // Form data
190
- http.post('/api/upload', async ({ request }) => {
191
- const formData = await request.formData()
192
- const file = formData.get('file')
193
-
194
- return HttpResponse.json({
195
- filename: file?.name,
196
- size: file?.size,
197
- })
198
- })
199
-
200
- // Text body
201
- http.post('/api/webhook', async ({ request }) => {
202
- const body = await request.text()
203
- return new HttpResponse(null, { status: 200 })
204
- })
205
- ```
206
-
207
- ## Response Types
208
-
209
- ### JSON Response
210
-
211
- ```typescript
212
- http.get('/api/users', () => {
213
- return HttpResponse.json(
214
- { id: '1', name: 'Alice' },
215
- {
216
- status: 200,
217
- headers: {
218
- 'Content-Type': 'application/json',
219
- 'X-Custom-Header': 'value',
220
- },
221
- }
222
- )
223
- })
224
- ```
225
-
226
- ### Text Response
227
-
228
- ```typescript
229
- http.get('/api/health', () => {
230
- return new HttpResponse('OK', {
231
- status: 200,
232
- headers: { 'Content-Type': 'text/plain' },
233
- })
234
- })
235
- ```
236
-
237
- ### Binary Response
238
-
239
- ```typescript
240
- http.get('/api/download', () => {
241
- const buffer = new ArrayBuffer(8)
242
- return new HttpResponse(buffer, {
243
- headers: {
244
- 'Content-Type': 'application/octet-stream',
245
- 'Content-Disposition': 'attachment; filename="file.bin"',
246
- },
247
- })
248
- })
249
- ```
250
-
251
- ### Empty Response
252
-
253
- ```typescript
254
- http.delete('/api/users/:id', () => {
255
- return new HttpResponse(null, { status: 204 })
256
- })
257
- ```
258
-
259
- ## Error Responses
260
-
261
- ### HTTP Errors
262
-
263
- ```typescript
264
- // 404 Not Found
265
- http.get('/api/users/:id', ({ params }) => {
266
- return HttpResponse.json(
267
- { error: 'User not found' },
268
- { status: 404 }
269
- )
270
- })
271
-
272
- // 400 Bad Request
273
- http.post('/api/users', async ({ request }) => {
274
- const body = await request.json()
275
-
276
- if (!body.email) {
277
- return HttpResponse.json(
278
- { error: 'Email is required', field: 'email' },
279
- { status: 400 }
280
- )
281
- }
282
-
283
- return HttpResponse.json(body)
284
- })
285
-
286
- // 500 Internal Server Error
287
- http.get('/api/data', () => {
288
- return HttpResponse.json(
289
- { error: 'Internal server error' },
290
- { status: 500 }
291
- )
292
- })
293
- ```
294
-
295
- ### Network Errors
296
-
297
- ```typescript
298
- import { HttpResponse } from 'msw'
299
-
300
- // Network error (connection failed)
301
- http.get('/api/users', () => {
302
- return HttpResponse.error()
303
- })
304
-
305
- // Timeout (no response)
306
- http.get('/api/slow', async () => {
307
- await new Promise(() => {}) // Never resolves
308
- })
309
- ```
310
-
311
- ### Conditional Errors
312
-
313
- ```typescript
314
- let failureCount = 0
315
-
316
- http.get('/api/flaky', () => {
317
- failureCount++
318
-
319
- // Fail first 2 times, then succeed
320
- if (failureCount <= 2) {
321
- return HttpResponse.json(
322
- { error: 'Service unavailable' },
323
- { status: 503 }
324
- )
325
- }
326
-
327
- return HttpResponse.json({ data: 'success' })
328
- })
329
- ```
330
-
331
- ## Response Delays
332
-
333
- ### Fixed Delay
334
-
335
- ```typescript
336
- import { delay, http, HttpResponse } from 'msw'
337
-
338
- http.get('/api/slow', async () => {
339
- await delay(2000) // 2 second delay
340
- return HttpResponse.json({ data: 'delayed response' })
341
- })
342
- ```
343
-
344
- ### Random Delay
345
-
346
- ```typescript
347
- http.get('/api/variable', async () => {
348
- const delayMs = Math.random() * 1000 // 0-1 second
349
- await delay(delayMs)
350
- return HttpResponse.json({ data: 'response' })
351
- })
352
- ```
353
-
354
- ### Realistic Network Delays
355
-
356
- ```typescript
357
- const handlers = [
358
- http.get('/api/users', async () => {
359
- await delay('real') // Use realistic network latency
360
- return HttpResponse.json(users)
361
- }),
362
- ]
363
- ```
364
-
365
- ### Test Loading States
366
-
367
- ```typescript
368
- it('shows loading spinner', async () => {
369
- server.use(
370
- http.get('/api/users', async () => {
371
- await delay(1000)
372
- return HttpResponse.json(users)
373
- })
374
- )
375
-
376
- render(<UserList />)
377
-
378
- // Verify loading state
379
- expect(screen.getByText('Loading...')).toBeInTheDocument()
380
-
381
- // Wait for data
382
- await waitFor(() => {
383
- expect(screen.getByText('Alice')).toBeInTheDocument()
384
- })
385
- })
386
- ```
387
-
388
- ## Per-Test Overrides
389
-
390
- ### Runtime Handler Override
391
-
392
- ```typescript
393
- import { server } from './tests/setup'
394
- import { http, HttpResponse } from 'msw'
395
-
396
- it('handles user not found', async () => {
397
- // Override for this test only
398
- server.use(
399
- http.get('/api/users/:id', ({ params }) => {
400
- return HttpResponse.json(
401
- { error: 'User not found' },
402
- { status: 404 }
403
- )
404
- })
405
- )
406
-
407
- const result = await fetchUser('999')
408
- expect(result.error).toBe('User not found')
409
- })
410
-
411
- // Handler automatically resets after test (via afterEach hook)
412
- ```
413
-
414
- ### Multiple Overrides
415
-
416
- ```typescript
417
- it('handles multiple error scenarios', async () => {
418
- server.use(
419
- // Override user endpoint
420
- http.get('/api/users/:id', () => {
421
- return HttpResponse.json({ error: 'Not found' }, { status: 404 })
422
- }),
423
-
424
- // Override posts endpoint
425
- http.get('/api/posts', () => {
426
- return HttpResponse.json({ error: 'Forbidden' }, { status: 403 })
427
- })
428
- )
429
-
430
- // Test both scenarios
431
- await expect(fetchUser('1')).rejects.toThrow()
432
- await expect(fetchPosts()).rejects.toThrow()
433
- })
434
- ```
435
-
436
- ### One-Time Override
437
-
438
- ```typescript
439
- it('simulates temporary outage', async () => {
440
- server.use(
441
- http.get('/api/users', () => HttpResponse.error(), { once: true })
442
- )
443
-
444
- // First call fails
445
- await expect(fetchUsers()).rejects.toThrow()
446
-
447
- // Second call uses default handler (succeeds)
448
- const users = await fetchUsers()
449
- expect(users).toHaveLength(2)
450
- })
451
- ```
452
-
453
- ## TypeScript Integration
454
-
455
- ### Typed Request Bodies
456
-
457
- ```typescript
458
- interface CreateUserRequest {
459
- name: string
460
- email: string
461
- age?: number
462
- }
463
-
464
- interface User extends CreateUserRequest {
465
- id: string
466
- }
467
-
468
- http.post<never, CreateUserRequest, User>('/api/users', async ({ request }) => {
469
- const body = await request.json()
470
-
471
- // body is typed as CreateUserRequest
472
- const user: User = {
473
- id: crypto.randomUUID(),
474
- ...body,
475
- }
476
-
477
- return HttpResponse.json(user, { status: 201 })
478
- })
479
- ```
480
-
481
- ### Typed Path Parameters
482
-
483
- ```typescript
484
- interface UserParams {
485
- id: string
486
- }
487
-
488
- http.get<UserParams>('/api/users/:id', ({ params }) => {
489
- // params.id is typed as string
490
- return HttpResponse.json({
491
- id: params.id,
492
- name: 'Alice',
493
- })
494
- })
495
- ```
496
-
497
- ### Type-Safe Responses
498
-
499
- ```typescript
500
- interface ApiResponse<T> {
501
- data: T
502
- meta: {
503
- page: number
504
- total: number
505
- }
506
- }
507
-
508
- http.get('/api/users', (): HttpResponse<ApiResponse<User[]>> => {
509
- return HttpResponse.json({
510
- data: users,
511
- meta: { page: 1, total: users.length },
512
- })
513
- })
514
- ```
515
-
516
- ## GraphQL Handlers
517
-
518
- ### Basic GraphQL Setup
519
-
520
- ```typescript
521
- import { graphql, HttpResponse } from 'msw'
522
-
523
- const handlers = [
524
- graphql.query('GetUser', ({ variables }) => {
525
- return HttpResponse.json({
526
- data: {
527
- user: {
528
- id: variables.id,
529
- name: 'Alice',
530
- email: 'alice@example.com',
531
- },
532
- },
533
- })
534
- }),
535
-
536
- graphql.mutation('CreateUser', async ({ variables }) => {
537
- return HttpResponse.json({
538
- data: {
539
- createUser: {
540
- id: '123',
541
- name: variables.name,
542
- email: variables.email,
543
- },
544
- },
545
- })
546
- }),
547
- ]
548
- ```
549
-
550
- ### GraphQL Errors
551
-
552
- ```typescript
553
- graphql.query('GetUser', ({ variables }) => {
554
- return HttpResponse.json({
555
- errors: [
556
- {
557
- message: 'User not found',
558
- extensions: { code: 'NOT_FOUND' },
559
- },
560
- ],
561
- })
562
- })
563
- ```
564
-
565
- ### Multiple GraphQL Endpoints
566
-
567
- ```typescript
568
- const handlers = [
569
- // Main API
570
- graphql.query('GetUser', () => { /* ... */ }),
571
-
572
- // Admin API
573
- graphql.link('https://admin.example.com/graphql').query('GetStats', () => {
574
- return HttpResponse.json({ data: { stats: {} } })
575
- }),
576
- ]
577
- ```
578
-
579
- ## Advanced Patterns
580
-
581
- ### Stateful Handlers
582
-
583
- ```typescript
584
- let users: User[] = []
585
-
586
- const handlers = [
587
- http.get('/api/users', () => {
588
- return HttpResponse.json(users)
589
- }),
590
-
591
- http.post('/api/users', async ({ request }) => {
592
- const body = await request.json()
593
- const user = { id: crypto.randomUUID(), ...body }
594
- users.push(user)
595
- return HttpResponse.json(user, { status: 201 })
596
- }),
597
-
598
- http.delete('/api/users/:id', ({ params }) => {
599
- users = users.filter(u => u.id !== params.id)
600
- return new HttpResponse(null, { status: 204 })
601
- }),
602
- ]
603
-
604
- // Reset state between tests
605
- beforeEach(() => {
606
- users = []
607
- })
608
- ```
609
-
610
- ### Paginated Responses
611
-
612
- ```typescript
613
- http.get('/api/users', ({ request }) => {
614
- const url = new URL(request.url)
615
- const page = parseInt(url.searchParams.get('page') || '1')
616
- const limit = parseInt(url.searchParams.get('limit') || '10')
617
-
618
- const start = (page - 1) * limit
619
- const end = start + limit
620
-
621
- return HttpResponse.json({
622
- data: allUsers.slice(start, end),
623
- pagination: {
624
- page,
625
- limit,
626
- total: allUsers.length,
627
- pages: Math.ceil(allUsers.length / limit),
628
- },
629
- })
630
- })
631
- ```
632
-
633
- ### File Upload Handling
634
-
635
- ```typescript
636
- http.post('/api/upload', async ({ request }) => {
637
- const formData = await request.formData()
638
- const file = formData.get('file') as File
639
-
640
- if (!file) {
641
- return HttpResponse.json(
642
- { error: 'No file provided' },
643
- { status: 400 }
644
- )
645
- }
646
-
647
- if (file.size > 1024 * 1024 * 5) { // 5MB
648
- return HttpResponse.json(
649
- { error: 'File too large' },
650
- { status: 413 }
651
- )
652
- }
653
-
654
- return HttpResponse.json({
655
- id: crypto.randomUUID(),
656
- filename: file.name,
657
- size: file.size,
658
- type: file.type,
659
- })
660
- })
661
- ```
662
-
663
- ### Conditional Responses
664
-
665
- ```typescript
666
- http.get('/api/users/:id', ({ params, request }) => {
667
- const userId = params.id
668
- const auth = request.headers.get('Authorization')
669
-
670
- // Not authenticated
671
- if (!auth) {
672
- return new HttpResponse(null, { status: 401 })
673
- }
674
-
675
- // User not found
676
- const user = users.find(u => u.id === userId)
677
- if (!user) {
678
- return HttpResponse.json(
679
- { error: 'User not found' },
680
- { status: 404 }
681
- )
682
- }
683
-
684
- // Success
685
- return HttpResponse.json(user)
686
- })
687
- ```
688
-
689
- ## Testing Patterns
690
-
691
- ### Testing Error Handling
692
-
693
- ```typescript
694
- it('handles network errors', async () => {
695
- server.use(
696
- http.get('/api/users', () => HttpResponse.error())
697
- )
698
-
699
- await expect(fetchUsers()).rejects.toThrow('Network error')
700
- })
701
-
702
- it('handles 404 responses', async () => {
703
- server.use(
704
- http.get('/api/users/:id', () => {
705
- return HttpResponse.json(
706
- { error: 'Not found' },
707
- { status: 404 }
708
- )
709
- })
710
- )
711
-
712
- await expect(fetchUser('999')).rejects.toThrow('User not found')
713
- })
714
- ```
715
-
716
- ### Testing Retry Logic
717
-
718
- ```typescript
719
- it('retries failed requests', async () => {
720
- let attempts = 0
721
-
722
- server.use(
723
- http.get('/api/users', () => {
724
- attempts++
725
- if (attempts < 3) {
726
- return HttpResponse.json(
727
- { error: 'Service unavailable' },
728
- { status: 503 }
729
- )
730
- }
731
- return HttpResponse.json(users)
732
- })
733
- )
734
-
735
- const result = await fetchUsersWithRetry()
736
-
737
- expect(attempts).toBe(3)
738
- expect(result).toEqual(users)
739
- })
740
- ```
741
-
742
- ### Testing Optimistic Updates
743
-
744
- ```typescript
745
- it('handles optimistic update failure', async () => {
746
- server.use(
747
- http.put('/api/users/:id', () => {
748
- return HttpResponse.json(
749
- { error: 'Update failed' },
750
- { status: 500 }
751
- )
752
- })
753
- )
754
-
755
- render(<UserProfile userId="123" />)
756
-
757
- // Make optimistic update
758
- fireEvent.click(screen.getByText('Update Name'))
759
-
760
- // Should show optimistic state
761
- expect(screen.getByText('New Name')).toBeInTheDocument()
762
-
763
- // Wait for error and rollback
764
- await waitFor(() => {
765
- expect(screen.getByText('Old Name')).toBeInTheDocument()
766
- expect(screen.getByText('Update failed')).toBeInTheDocument()
767
- })
768
- })
769
- ```
770
-
771
- ## Guidelines
772
-
773
- 1. **Setup once, override per-test** - Define default handlers in setup file, override in individual tests
774
- 2. **Reset handlers between tests** - Use `server.resetHandlers()` in `afterEach` hook
775
- 3. **Use realistic delays** - Test loading states with `delay()` function
776
- 4. **Type your handlers** - Use TypeScript generics for request/response types
777
- 5. **Mock at network level** - Better than mocking fetch directly, works with any HTTP client
778
- 6. **Test error scenarios** - Use MSW to simulate network errors, timeouts, HTTP errors
779
- 7. **Keep handlers simple** - Complex logic should be tested separately, not in mock handlers
780
- 8. **Use stateful handlers sparingly** - Can make tests interdependent, prefer isolated state
781
- 9. **Fail on unhandled requests** - Use `onUnhandledRequest: 'error'` to catch missing mocks
782
- 10. **Group related handlers** - Organize handlers by feature or API domain
783
- 11. **Document conditional behavior** - Add comments when handlers have complex logic
784
- 12. **Test pagination separately** - Create dedicated handlers for pagination edge cases
785
-
786
- ## Common Pitfalls
787
-
788
- ### Missing Handler Resets
789
-
790
- ```typescript
791
- // BAD - handlers persist across tests
792
- afterEach(() => {
793
- // Forgot to reset handlers
794
- })
795
-
796
- // GOOD - reset after each test
797
- afterEach(() => {
798
- server.resetHandlers()
799
- })
800
- ```
801
-
802
- ### Over-Mocking
803
-
804
- ```typescript
805
- // BAD - mocking too much detail
806
- http.get('/api/users', () => {
807
- return HttpResponse.json({
808
- data: users,
809
- meta: { timestamp: Date.now(), version: '1.0', ... },
810
- debug: { queryTime: 123, ... },
811
- })
812
- })
813
-
814
- // GOOD - mock only what tests need
815
- http.get('/api/users', () => {
816
- return HttpResponse.json(users)
817
- })
818
- ```
819
-
820
- ### Forgetting async/await
821
-
822
- ```typescript
823
- // BAD - not awaiting request.json()
824
- http.post('/api/users', ({ request }) => {
825
- const body = request.json() // Missing await!
826
- return HttpResponse.json(body)
827
- })
828
-
829
- // GOOD - await async operations
830
- http.post('/api/users', async ({ request }) => {
831
- const body = await request.json()
832
- return HttpResponse.json(body)
833
- })
834
- ```
835
-
836
- ### Not Handling Headers
837
-
838
- ```typescript
839
- // BAD - ignoring auth headers in tests
840
- http.get('/api/protected', () => {
841
- return HttpResponse.json({ data: 'secret' })
842
- })
843
-
844
- // GOOD - verify auth in mock
845
- http.get('/api/protected', ({ request }) => {
846
- const auth = request.headers.get('Authorization')
847
- if (!auth) {
848
- return new HttpResponse(null, { status: 401 })
849
- }
850
- return HttpResponse.json({ data: 'secret' })
851
- })
852
- ```