red64-cli 0.1.0 → 0.3.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 (125) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/parseArgs.d.ts.map +1 -1
  3. package/dist/cli/parseArgs.js +5 -0
  4. package/dist/cli/parseArgs.js.map +1 -1
  5. package/dist/components/init/CompleteStep.d.ts.map +1 -1
  6. package/dist/components/init/CompleteStep.js +2 -2
  7. package/dist/components/init/CompleteStep.js.map +1 -1
  8. package/dist/components/init/TestCheckStep.d.ts +16 -0
  9. package/dist/components/init/TestCheckStep.d.ts.map +1 -0
  10. package/dist/components/init/TestCheckStep.js +120 -0
  11. package/dist/components/init/TestCheckStep.js.map +1 -0
  12. package/dist/components/init/index.d.ts +1 -0
  13. package/dist/components/init/index.d.ts.map +1 -1
  14. package/dist/components/init/index.js +1 -0
  15. package/dist/components/init/index.js.map +1 -1
  16. package/dist/components/init/types.d.ts +9 -0
  17. package/dist/components/init/types.d.ts.map +1 -1
  18. package/dist/components/screens/InitScreen.d.ts.map +1 -1
  19. package/dist/components/screens/InitScreen.js +69 -6
  20. package/dist/components/screens/InitScreen.js.map +1 -1
  21. package/dist/components/screens/ListScreen.d.ts.map +1 -1
  22. package/dist/components/screens/ListScreen.js +28 -3
  23. package/dist/components/screens/ListScreen.js.map +1 -1
  24. package/dist/components/screens/StartScreen.d.ts.map +1 -1
  25. package/dist/components/screens/StartScreen.js +212 -13
  26. package/dist/components/screens/StartScreen.js.map +1 -1
  27. package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
  28. package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
  29. package/dist/components/ui/ArtifactsSidebar.js +51 -0
  30. package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
  31. package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
  32. package/dist/components/ui/FeatureSidebar.js +1 -1
  33. package/dist/components/ui/FeatureSidebar.js.map +1 -1
  34. package/dist/components/ui/index.d.ts +1 -0
  35. package/dist/components/ui/index.d.ts.map +1 -1
  36. package/dist/components/ui/index.js +1 -0
  37. package/dist/components/ui/index.js.map +1 -1
  38. package/dist/services/ClaudeErrorDetector.js +3 -3
  39. package/dist/services/ClaudeErrorDetector.js.map +1 -1
  40. package/dist/services/ConfigService.d.ts +1 -0
  41. package/dist/services/ConfigService.d.ts.map +1 -1
  42. package/dist/services/ConfigService.js.map +1 -1
  43. package/dist/services/ProjectDetector.d.ts +28 -0
  44. package/dist/services/ProjectDetector.d.ts.map +1 -0
  45. package/dist/services/ProjectDetector.js +236 -0
  46. package/dist/services/ProjectDetector.js.map +1 -0
  47. package/dist/services/TestRunner.d.ts +46 -0
  48. package/dist/services/TestRunner.d.ts.map +1 -0
  49. package/dist/services/TestRunner.js +85 -0
  50. package/dist/services/TestRunner.js.map +1 -0
  51. package/dist/services/index.d.ts +2 -0
  52. package/dist/services/index.d.ts.map +1 -1
  53. package/dist/services/index.js +2 -0
  54. package/dist/services/index.js.map +1 -1
  55. package/dist/types/index.d.ts +13 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/index.js.map +1 -1
  58. package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
  59. package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
  60. package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
  61. package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
  62. package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
  63. package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
  64. package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
  65. package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
  66. package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
  67. package/framework/stacks/generic/feedback.md +80 -0
  68. package/framework/stacks/nextjs/accessibility.md +437 -0
  69. package/framework/stacks/nextjs/api.md +431 -0
  70. package/framework/stacks/nextjs/coding-style.md +282 -0
  71. package/framework/stacks/nextjs/commenting.md +226 -0
  72. package/framework/stacks/nextjs/components.md +411 -0
  73. package/framework/stacks/nextjs/conventions.md +333 -0
  74. package/framework/stacks/nextjs/css.md +310 -0
  75. package/framework/stacks/nextjs/error-handling.md +442 -0
  76. package/framework/stacks/nextjs/feedback.md +124 -0
  77. package/framework/stacks/nextjs/migrations.md +332 -0
  78. package/framework/stacks/nextjs/models.md +362 -0
  79. package/framework/stacks/nextjs/queries.md +410 -0
  80. package/framework/stacks/nextjs/responsive.md +338 -0
  81. package/framework/stacks/nextjs/tech-stack.md +177 -0
  82. package/framework/stacks/nextjs/test-writing.md +475 -0
  83. package/framework/stacks/nextjs/validation.md +467 -0
  84. package/framework/stacks/python/api.md +468 -0
  85. package/framework/stacks/python/authentication.md +342 -0
  86. package/framework/stacks/python/code-quality.md +283 -0
  87. package/framework/stacks/python/code-refactoring.md +315 -0
  88. package/framework/stacks/python/coding-style.md +462 -0
  89. package/framework/stacks/python/conventions.md +399 -0
  90. package/framework/stacks/python/error-handling.md +512 -0
  91. package/framework/stacks/python/feedback.md +92 -0
  92. package/framework/stacks/python/implement-ai-llm.md +468 -0
  93. package/framework/stacks/python/migrations.md +388 -0
  94. package/framework/stacks/python/models.md +399 -0
  95. package/framework/stacks/python/python.md +232 -0
  96. package/framework/stacks/python/queries.md +451 -0
  97. package/framework/stacks/python/structure.md +245 -58
  98. package/framework/stacks/python/tech.md +92 -35
  99. package/framework/stacks/python/testing.md +380 -0
  100. package/framework/stacks/python/validation.md +471 -0
  101. package/framework/stacks/rails/authentication.md +176 -0
  102. package/framework/stacks/rails/code-quality.md +287 -0
  103. package/framework/stacks/rails/code-refactoring.md +299 -0
  104. package/framework/stacks/rails/feedback.md +130 -0
  105. package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
  106. package/framework/stacks/rails/rails.md +301 -0
  107. package/framework/stacks/rails/rails8-best-practices.md +498 -0
  108. package/framework/stacks/rails/rails8-css.md +573 -0
  109. package/framework/stacks/rails/structure.md +140 -0
  110. package/framework/stacks/rails/tech.md +108 -0
  111. package/framework/stacks/react/code-quality.md +521 -0
  112. package/framework/stacks/react/components.md +625 -0
  113. package/framework/stacks/react/data-fetching.md +586 -0
  114. package/framework/stacks/react/feedback.md +110 -0
  115. package/framework/stacks/react/forms.md +694 -0
  116. package/framework/stacks/react/performance.md +640 -0
  117. package/framework/stacks/react/product.md +22 -9
  118. package/framework/stacks/react/state-management.md +472 -0
  119. package/framework/stacks/react/structure.md +351 -44
  120. package/framework/stacks/react/tech.md +219 -30
  121. package/framework/stacks/react/testing.md +690 -0
  122. package/package.json +1 -1
  123. package/framework/stacks/node/product.md +0 -27
  124. package/framework/stacks/node/structure.md +0 -82
  125. package/framework/stacks/node/tech.md +0 -63
@@ -0,0 +1,690 @@
1
+ # Testing Patterns
2
+
3
+ Comprehensive testing patterns for React applications using Vitest, React Testing Library, MSW, and Playwright.
4
+
5
+ ---
6
+
7
+ ## Philosophy
8
+
9
+ - **Test user behavior, not implementation**: Test what users see and do, not internal state
10
+ - **Fast feedback**: Unit tests run in milliseconds; use Vitest's native ESM support
11
+ - **Realistic integration**: MSW intercepts at the network level for true integration tests
12
+ - **Confidence over coverage**: Focus on critical paths; 100% coverage is not the goal
13
+
14
+ ---
15
+
16
+ ## Test Organization
17
+
18
+ ```
19
+ src/
20
+ ├── features/
21
+ │ └── users/
22
+ │ ├── components/
23
+ │ │ ├── UserList.tsx
24
+ │ │ └── UserList.test.tsx # Co-located component tests
25
+ │ └── hooks/
26
+ │ ├── useUsers.ts
27
+ │ └── useUsers.test.ts # Co-located hook tests
28
+ ├── utils/
29
+ │ ├── formatters.ts
30
+ │ └── formatters.test.ts # Co-located utility tests
31
+ └── test/
32
+ ├── setup.ts # Global test setup
33
+ ├── test-utils.tsx # Custom render, providers
34
+ └── mocks/
35
+ ├── handlers.ts # MSW request handlers
36
+ └── server.ts # MSW server setup
37
+
38
+ tests/ # E2E tests at project root
39
+ ├── e2e/
40
+ │ ├── auth.spec.ts
41
+ │ └── users.spec.ts
42
+ └── playwright.config.ts
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Vitest Configuration
48
+
49
+ ```typescript
50
+ // vitest.config.ts
51
+ import { defineConfig } from 'vitest/config';
52
+ import react from '@vitejs/plugin-react';
53
+ import tsconfigPaths from 'vite-tsconfig-paths';
54
+
55
+ export default defineConfig({
56
+ plugins: [react(), tsconfigPaths()],
57
+ test: {
58
+ globals: true,
59
+ environment: 'jsdom',
60
+ setupFiles: ['./src/test/setup.ts'],
61
+ include: ['src/**/*.test.{ts,tsx}'],
62
+ coverage: {
63
+ provider: 'v8',
64
+ reporter: ['text', 'html', 'lcov'],
65
+ include: ['src/**/*.{ts,tsx}'],
66
+ exclude: [
67
+ 'src/**/*.test.{ts,tsx}',
68
+ 'src/test/**',
69
+ 'src/**/*.d.ts',
70
+ 'src/main.tsx',
71
+ ],
72
+ thresholds: {
73
+ lines: 80,
74
+ branches: 80,
75
+ functions: 80,
76
+ statements: 80,
77
+ },
78
+ },
79
+ // Faster test runs
80
+ pool: 'forks',
81
+ poolOptions: {
82
+ forks: {
83
+ singleFork: true,
84
+ },
85
+ },
86
+ },
87
+ });
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Test Setup
93
+
94
+ ```typescript
95
+ // src/test/setup.ts
96
+ import '@testing-library/jest-dom/vitest';
97
+ import { cleanup } from '@testing-library/react';
98
+ import { afterEach, beforeAll, afterAll } from 'vitest';
99
+ import { server } from './mocks/server';
100
+
101
+ // Start MSW server before all tests
102
+ beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
103
+
104
+ // Reset handlers and cleanup after each test
105
+ afterEach(() => {
106
+ cleanup();
107
+ server.resetHandlers();
108
+ });
109
+
110
+ // Close server after all tests
111
+ afterAll(() => server.close());
112
+
113
+ // Mock window.matchMedia
114
+ Object.defineProperty(window, 'matchMedia', {
115
+ writable: true,
116
+ value: vi.fn().mockImplementation((query: string) => ({
117
+ matches: false,
118
+ media: query,
119
+ onchange: null,
120
+ addListener: vi.fn(),
121
+ removeListener: vi.fn(),
122
+ addEventListener: vi.fn(),
123
+ removeEventListener: vi.fn(),
124
+ dispatchEvent: vi.fn(),
125
+ })),
126
+ });
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Custom Render with Providers
132
+
133
+ ```typescript
134
+ // src/test/test-utils.tsx
135
+ import { render, type RenderOptions } from '@testing-library/react';
136
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
137
+ import { MemoryRouter } from 'react-router-dom';
138
+ import type { ReactElement, ReactNode } from 'react';
139
+
140
+ // Create a fresh QueryClient for each test
141
+ function createTestQueryClient() {
142
+ return new QueryClient({
143
+ defaultOptions: {
144
+ queries: {
145
+ retry: false,
146
+ gcTime: 0,
147
+ staleTime: 0,
148
+ },
149
+ mutations: {
150
+ retry: false,
151
+ },
152
+ },
153
+ logger: {
154
+ log: console.log,
155
+ warn: console.warn,
156
+ error: () => {}, // Suppress error logs in tests
157
+ },
158
+ });
159
+ }
160
+
161
+ interface WrapperProps {
162
+ children: ReactNode;
163
+ initialEntries?: string[];
164
+ }
165
+
166
+ function AllProviders({ children, initialEntries = ['/'] }: WrapperProps) {
167
+ const queryClient = createTestQueryClient();
168
+
169
+ return (
170
+ <QueryClientProvider client={queryClient}>
171
+ <MemoryRouter initialEntries={initialEntries}>
172
+ {children}
173
+ </MemoryRouter>
174
+ </QueryClientProvider>
175
+ );
176
+ }
177
+
178
+ interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
179
+ initialEntries?: string[];
180
+ }
181
+
182
+ function customRender(ui: ReactElement, options: CustomRenderOptions = {}) {
183
+ const { initialEntries, ...renderOptions } = options;
184
+
185
+ return render(ui, {
186
+ wrapper: ({ children }) => (
187
+ <AllProviders initialEntries={initialEntries}>{children}</AllProviders>
188
+ ),
189
+ ...renderOptions,
190
+ });
191
+ }
192
+
193
+ // Re-export everything from RTL
194
+ export * from '@testing-library/react';
195
+ export { customRender as render };
196
+ ```
197
+
198
+ ---
199
+
200
+ ## MSW Setup (Mock Service Worker)
201
+
202
+ ### Server Configuration
203
+
204
+ ```typescript
205
+ // src/test/mocks/server.ts
206
+ import { setupServer } from 'msw/node';
207
+ import { handlers } from './handlers';
208
+
209
+ export const server = setupServer(...handlers);
210
+ ```
211
+
212
+ ### Request Handlers
213
+
214
+ ```typescript
215
+ // src/test/mocks/handlers.ts
216
+ import { http, HttpResponse } from 'msw';
217
+
218
+ const API_URL = 'http://localhost:3000/api';
219
+
220
+ export const handlers = [
221
+ // GET /api/users
222
+ http.get(`${API_URL}/users`, () => {
223
+ return HttpResponse.json({
224
+ items: [
225
+ { id: 1, name: 'John Doe', email: 'john@example.com' },
226
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
227
+ ],
228
+ total: 2,
229
+ page: 1,
230
+ perPage: 20,
231
+ });
232
+ }),
233
+
234
+ // GET /api/users/:id
235
+ http.get(`${API_URL}/users/:id`, ({ params }) => {
236
+ const { id } = params;
237
+ return HttpResponse.json({
238
+ id: Number(id),
239
+ name: 'John Doe',
240
+ email: 'john@example.com',
241
+ });
242
+ }),
243
+
244
+ // POST /api/users
245
+ http.post(`${API_URL}/users`, async ({ request }) => {
246
+ const body = await request.json();
247
+ return HttpResponse.json(
248
+ { id: 3, ...body },
249
+ { status: 201 }
250
+ );
251
+ }),
252
+
253
+ // DELETE /api/users/:id
254
+ http.delete(`${API_URL}/users/:id`, () => {
255
+ return new HttpResponse(null, { status: 204 });
256
+ }),
257
+ ];
258
+ ```
259
+
260
+ ### Override Handlers in Tests
261
+
262
+ ```typescript
263
+ import { http, HttpResponse } from 'msw';
264
+ import { server } from '@/test/mocks/server';
265
+
266
+ test('shows error when API fails', async () => {
267
+ // Override handler for this test only
268
+ server.use(
269
+ http.get('http://localhost:3000/api/users', () => {
270
+ return HttpResponse.json(
271
+ { error: { code: 'SERVER_ERROR', message: 'Something went wrong' } },
272
+ { status: 500 }
273
+ );
274
+ })
275
+ );
276
+
277
+ render(<UserList />);
278
+
279
+ expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
280
+ });
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Component Testing Patterns
286
+
287
+ ### Basic Component Test
288
+
289
+ ```typescript
290
+ // features/users/components/UserCard.test.tsx
291
+ import { render, screen } from '@/test/test-utils';
292
+ import { UserCard } from './UserCard';
293
+
294
+ const mockUser = {
295
+ id: 1,
296
+ name: 'John Doe',
297
+ email: 'john@example.com',
298
+ role: 'admin',
299
+ };
300
+
301
+ describe('UserCard', () => {
302
+ it('renders user information', () => {
303
+ render(<UserCard user={mockUser} />);
304
+
305
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
306
+ expect(screen.getByText('john@example.com')).toBeInTheDocument();
307
+ expect(screen.getByText('admin')).toBeInTheDocument();
308
+ });
309
+
310
+ it('calls onEdit when edit button is clicked', async () => {
311
+ const onEdit = vi.fn();
312
+ const user = userEvent.setup();
313
+
314
+ render(<UserCard user={mockUser} onEdit={onEdit} />);
315
+
316
+ await user.click(screen.getByRole('button', { name: /edit/i }));
317
+
318
+ expect(onEdit).toHaveBeenCalledWith(mockUser.id);
319
+ });
320
+ });
321
+ ```
322
+
323
+ ### Testing Async Components
324
+
325
+ ```typescript
326
+ // features/users/components/UserList.test.tsx
327
+ import { render, screen, waitFor } from '@/test/test-utils';
328
+ import { UserList } from './UserList';
329
+
330
+ describe('UserList', () => {
331
+ it('shows loading state initially', () => {
332
+ render(<UserList />);
333
+
334
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
335
+ });
336
+
337
+ it('renders users after loading', async () => {
338
+ render(<UserList />);
339
+
340
+ // Wait for loading to finish and users to appear
341
+ expect(await screen.findByText('John Doe')).toBeInTheDocument();
342
+ expect(screen.getByText('Jane Smith')).toBeInTheDocument();
343
+ });
344
+
345
+ it('shows empty state when no users', async () => {
346
+ server.use(
347
+ http.get('http://localhost:3000/api/users', () => {
348
+ return HttpResponse.json({ items: [], total: 0 });
349
+ })
350
+ );
351
+
352
+ render(<UserList />);
353
+
354
+ expect(await screen.findByText(/no users found/i)).toBeInTheDocument();
355
+ });
356
+ });
357
+ ```
358
+
359
+ ### Testing Forms
360
+
361
+ ```typescript
362
+ // features/users/components/UserForm.test.tsx
363
+ import { render, screen, waitFor } from '@/test/test-utils';
364
+ import userEvent from '@testing-library/user-event';
365
+ import { UserForm } from './UserForm';
366
+
367
+ describe('UserForm', () => {
368
+ it('submits form with valid data', async () => {
369
+ const onSuccess = vi.fn();
370
+ const user = userEvent.setup();
371
+
372
+ render(<UserForm onSuccess={onSuccess} />);
373
+
374
+ await user.type(screen.getByLabelText(/name/i), 'New User');
375
+ await user.type(screen.getByLabelText(/email/i), 'new@example.com');
376
+ await user.click(screen.getByRole('button', { name: /submit/i }));
377
+
378
+ await waitFor(() => {
379
+ expect(onSuccess).toHaveBeenCalled();
380
+ });
381
+ });
382
+
383
+ it('shows validation errors for invalid input', async () => {
384
+ const user = userEvent.setup();
385
+
386
+ render(<UserForm />);
387
+
388
+ await user.click(screen.getByRole('button', { name: /submit/i }));
389
+
390
+ expect(await screen.findByText(/name is required/i)).toBeInTheDocument();
391
+ expect(screen.getByText(/email is required/i)).toBeInTheDocument();
392
+ });
393
+
394
+ it('shows server error on API failure', async () => {
395
+ server.use(
396
+ http.post('http://localhost:3000/api/users', () => {
397
+ return HttpResponse.json(
398
+ { error: { message: 'Email already exists' } },
399
+ { status: 409 }
400
+ );
401
+ })
402
+ );
403
+
404
+ const user = userEvent.setup();
405
+ render(<UserForm />);
406
+
407
+ await user.type(screen.getByLabelText(/name/i), 'Test');
408
+ await user.type(screen.getByLabelText(/email/i), 'existing@example.com');
409
+ await user.click(screen.getByRole('button', { name: /submit/i }));
410
+
411
+ expect(await screen.findByText(/email already exists/i)).toBeInTheDocument();
412
+ });
413
+ });
414
+ ```
415
+
416
+ ---
417
+
418
+ ## Hook Testing Patterns
419
+
420
+ ### Testing with renderHook
421
+
422
+ ```typescript
423
+ // features/users/hooks/useUsers.test.ts
424
+ import { renderHook, waitFor } from '@testing-library/react';
425
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
426
+ import { useUsers } from './useUsers';
427
+
428
+ function createWrapper() {
429
+ const queryClient = new QueryClient({
430
+ defaultOptions: { queries: { retry: false } },
431
+ });
432
+
433
+ return function Wrapper({ children }: { children: React.ReactNode }) {
434
+ return (
435
+ <QueryClientProvider client={queryClient}>
436
+ {children}
437
+ </QueryClientProvider>
438
+ );
439
+ };
440
+ }
441
+
442
+ describe('useUsers', () => {
443
+ it('fetches users successfully', async () => {
444
+ const { result } = renderHook(() => useUsers(), {
445
+ wrapper: createWrapper(),
446
+ });
447
+
448
+ // Initially loading
449
+ expect(result.current.isLoading).toBe(true);
450
+
451
+ // Wait for data
452
+ await waitFor(() => {
453
+ expect(result.current.isSuccess).toBe(true);
454
+ });
455
+
456
+ expect(result.current.data?.items).toHaveLength(2);
457
+ expect(result.current.data?.items[0].name).toBe('John Doe');
458
+ });
459
+
460
+ it('handles errors', async () => {
461
+ server.use(
462
+ http.get('http://localhost:3000/api/users', () => {
463
+ return HttpResponse.json({ error: 'Server error' }, { status: 500 });
464
+ })
465
+ );
466
+
467
+ const { result } = renderHook(() => useUsers(), {
468
+ wrapper: createWrapper(),
469
+ });
470
+
471
+ await waitFor(() => {
472
+ expect(result.current.isError).toBe(true);
473
+ });
474
+ });
475
+ });
476
+ ```
477
+
478
+ ### Testing Custom Hooks with State
479
+
480
+ ```typescript
481
+ // hooks/useDebounce.test.ts
482
+ import { renderHook, act } from '@testing-library/react';
483
+ import { useDebounce } from './useDebounce';
484
+
485
+ describe('useDebounce', () => {
486
+ beforeEach(() => {
487
+ vi.useFakeTimers();
488
+ });
489
+
490
+ afterEach(() => {
491
+ vi.useRealTimers();
492
+ });
493
+
494
+ it('returns initial value immediately', () => {
495
+ const { result } = renderHook(() => useDebounce('initial', 500));
496
+
497
+ expect(result.current).toBe('initial');
498
+ });
499
+
500
+ it('debounces value changes', () => {
501
+ const { result, rerender } = renderHook(
502
+ ({ value, delay }) => useDebounce(value, delay),
503
+ { initialProps: { value: 'initial', delay: 500 } }
504
+ );
505
+
506
+ rerender({ value: 'updated', delay: 500 });
507
+
508
+ // Value hasn't changed yet
509
+ expect(result.current).toBe('initial');
510
+
511
+ // Fast-forward time
512
+ act(() => {
513
+ vi.advanceTimersByTime(500);
514
+ });
515
+
516
+ expect(result.current).toBe('updated');
517
+ });
518
+ });
519
+ ```
520
+
521
+ ---
522
+
523
+ ## Utility Testing Patterns
524
+
525
+ ```typescript
526
+ // utils/formatters.test.ts
527
+ import { describe, it, expect } from 'vitest';
528
+ import { formatCurrency, formatDate, truncate } from './formatters';
529
+
530
+ describe('formatCurrency', () => {
531
+ it('formats USD currency', () => {
532
+ expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
533
+ });
534
+
535
+ it('handles zero', () => {
536
+ expect(formatCurrency(0, 'USD')).toBe('$0.00');
537
+ });
538
+
539
+ it('handles negative numbers', () => {
540
+ expect(formatCurrency(-100, 'USD')).toBe('-$100.00');
541
+ });
542
+ });
543
+
544
+ describe('formatDate', () => {
545
+ it('formats date in default format', () => {
546
+ const date = new Date('2024-01-15T10:30:00Z');
547
+ expect(formatDate(date)).toBe('Jan 15, 2024');
548
+ });
549
+
550
+ it('formats date with custom format', () => {
551
+ const date = new Date('2024-01-15T10:30:00Z');
552
+ expect(formatDate(date, 'YYYY-MM-DD')).toBe('2024-01-15');
553
+ });
554
+ });
555
+
556
+ describe('truncate', () => {
557
+ it.each([
558
+ ['Hello World', 5, 'Hello...'],
559
+ ['Hi', 5, 'Hi'],
560
+ ['', 5, ''],
561
+ ])('truncates "%s" with limit %i to "%s"', (input, limit, expected) => {
562
+ expect(truncate(input, limit)).toBe(expected);
563
+ });
564
+ });
565
+ ```
566
+
567
+ ---
568
+
569
+ ## E2E Testing with Playwright
570
+
571
+ ### Configuration
572
+
573
+ ```typescript
574
+ // playwright.config.ts
575
+ import { defineConfig, devices } from '@playwright/test';
576
+
577
+ export default defineConfig({
578
+ testDir: './tests/e2e',
579
+ fullyParallel: true,
580
+ forbidOnly: !!process.env.CI,
581
+ retries: process.env.CI ? 2 : 0,
582
+ workers: process.env.CI ? 1 : undefined,
583
+ reporter: 'html',
584
+ use: {
585
+ baseURL: 'http://localhost:5173',
586
+ trace: 'on-first-retry',
587
+ screenshot: 'only-on-failure',
588
+ },
589
+ projects: [
590
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
591
+ { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
592
+ { name: 'webkit', use: { ...devices['Desktop Safari'] } },
593
+ ],
594
+ webServer: {
595
+ command: 'pnpm dev',
596
+ url: 'http://localhost:5173',
597
+ reuseExistingServer: !process.env.CI,
598
+ },
599
+ });
600
+ ```
601
+
602
+ ### E2E Test Example
603
+
604
+ ```typescript
605
+ // tests/e2e/users.spec.ts
606
+ import { test, expect } from '@playwright/test';
607
+
608
+ test.describe('Users', () => {
609
+ test.beforeEach(async ({ page }) => {
610
+ await page.goto('/users');
611
+ });
612
+
613
+ test('displays list of users', async ({ page }) => {
614
+ await expect(page.getByRole('heading', { name: /users/i })).toBeVisible();
615
+ await expect(page.getByTestId('user-card')).toHaveCount(2);
616
+ });
617
+
618
+ test('creates a new user', async ({ page }) => {
619
+ await page.getByRole('button', { name: /add user/i }).click();
620
+
621
+ await page.getByLabel(/name/i).fill('New User');
622
+ await page.getByLabel(/email/i).fill('new@example.com');
623
+ await page.getByRole('button', { name: /submit/i }).click();
624
+
625
+ await expect(page.getByText('User created successfully')).toBeVisible();
626
+ await expect(page.getByText('New User')).toBeVisible();
627
+ });
628
+
629
+ test('shows validation errors', async ({ page }) => {
630
+ await page.getByRole('button', { name: /add user/i }).click();
631
+ await page.getByRole('button', { name: /submit/i }).click();
632
+
633
+ await expect(page.getByText(/name is required/i)).toBeVisible();
634
+ await expect(page.getByText(/email is required/i)).toBeVisible();
635
+ });
636
+ });
637
+ ```
638
+
639
+ ---
640
+
641
+ ## Test Commands
642
+
643
+ ```bash
644
+ # Unit/Integration Tests (Vitest)
645
+ pnpm test # Watch mode
646
+ pnpm test:run # Single run
647
+ pnpm test:coverage # With coverage report
648
+ pnpm test:ui # Vitest UI
649
+
650
+ # Run specific tests
651
+ pnpm test UserList # Match by filename
652
+ pnpm test --grep "submits" # Match by test name
653
+
654
+ # E2E Tests (Playwright)
655
+ pnpm test:e2e # Run all E2E tests
656
+ pnpm test:e2e --ui # Interactive UI mode
657
+ pnpm test:e2e --project=chromium # Single browser
658
+
659
+ # Debug
660
+ pnpm test:e2e --debug # Step through tests
661
+ ```
662
+
663
+ ---
664
+
665
+ ## Anti-Patterns
666
+
667
+ | Anti-Pattern | Problem | Correct Approach |
668
+ |--------------|---------|------------------|
669
+ | Testing implementation details | Brittle tests | Test user-visible behavior |
670
+ | Snapshot testing everything | Meaningless snapshots | Snapshot only stable output |
671
+ | Mocking everything | Tests don't reflect reality | Use MSW for real network behavior |
672
+ | No test isolation | Flaky tests | Fresh QueryClient per test |
673
+ | `waitFor` with no assertion | Silent failures | Always assert inside waitFor |
674
+ | `getByTestId` everywhere | Not accessible | Prefer accessible queries |
675
+
676
+ ---
677
+
678
+ ## Query Priority
679
+
680
+ Use queries in this order (most to least preferred):
681
+
682
+ 1. `getByRole` - Accessible, how users interact
683
+ 2. `getByLabelText` - For form inputs
684
+ 3. `getByPlaceholderText` - When label is not available
685
+ 4. `getByText` - For non-interactive elements
686
+ 5. `getByTestId` - Last resort for elements without accessible names
687
+
688
+ ---
689
+
690
+ _Tests document behavior. Each test should read as a specification of what the code does for real users._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "red64-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Red64 Flow Orchestrator - Deterministic spec-driven development CLI",
5
5
  "type": "module",
6
6
  "bin": {