@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.
Files changed (120) hide show
  1. package/.agent/PROMPT.md +58 -0
  2. package/.agent/STEERING.md +3 -0
  3. package/.agent/logs/LOG.md +13 -0
  4. package/.agent/prd/.gitkeep +0 -0
  5. package/.agent/screenshots/.gitkeep +0 -0
  6. package/.agent/skills/component-refactoring/SKILL.md +247 -0
  7. package/.agent/skills/component-refactoring/references/complexity-patterns.md +485 -0
  8. package/.agent/skills/component-refactoring/references/component-splitting.md +419 -0
  9. package/.agent/skills/component-refactoring/references/hook-extraction.md +317 -0
  10. package/.agent/skills/e2e-tester/SKILL.md +595 -0
  11. package/.agent/skills/frontend-code-review/SKILL.md +73 -0
  12. package/.agent/skills/frontend-code-review/references/code-quality.md +28 -0
  13. package/.agent/skills/frontend-code-review/references/performance.md +36 -0
  14. package/.agent/skills/frontend-testing/SKILL.md +316 -0
  15. package/.agent/skills/frontend-testing/assets/component-test.template.tsx +293 -0
  16. package/.agent/skills/frontend-testing/assets/hook-test.template.ts +207 -0
  17. package/.agent/skills/frontend-testing/assets/utility-test.template.ts +154 -0
  18. package/.agent/skills/frontend-testing/references/async-testing.md +345 -0
  19. package/.agent/skills/frontend-testing/references/checklist.md +188 -0
  20. package/.agent/skills/frontend-testing/references/common-patterns.md +449 -0
  21. package/.agent/skills/frontend-testing/references/mocking.md +289 -0
  22. package/.agent/skills/frontend-testing/references/workflow.md +265 -0
  23. package/.agent/skills/prd-creator/JSON.md +613 -0
  24. package/.agent/skills/prd-creator/PRD.md +196 -0
  25. package/.agent/skills/prd-creator/SKILL.md +143 -0
  26. package/.agent/skills/skill-creator/SKILL.md +355 -0
  27. package/.agent/skills/skill-creator/references/output-patterns.md +86 -0
  28. package/.agent/skills/skill-creator/references/workflows.md +28 -0
  29. package/.agent/skills/skill-creator/scripts/init_skill.py +300 -0
  30. package/.agent/skills/skill-creator/scripts/package_skill.py +110 -0
  31. package/.agent/skills/vercel-react-best-practices/AGENTS.md +2249 -0
  32. package/.agent/skills/vercel-react-best-practices/SKILL.md +125 -0
  33. package/.agent/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  34. package/.agent/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
  35. package/.agent/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  36. package/.agent/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  37. package/.agent/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
  38. package/.agent/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  39. package/.agent/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  40. package/.agent/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  41. package/.agent/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  42. package/.agent/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  43. package/.agent/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  44. package/.agent/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  45. package/.agent/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  46. package/.agent/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  47. package/.agent/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +82 -0
  48. package/.agent/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  49. package/.agent/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  50. package/.agent/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  51. package/.agent/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  52. package/.agent/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  53. package/.agent/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  54. package/.agent/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  55. package/.agent/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  56. package/.agent/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  57. package/.agent/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  58. package/.agent/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  59. package/.agent/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  60. package/.agent/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  61. package/.agent/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  62. package/.agent/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  63. package/.agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  64. package/.agent/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  65. package/.agent/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  66. package/.agent/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  67. package/.agent/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  68. package/.agent/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  69. package/.agent/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  70. package/.agent/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  71. package/.agent/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  72. package/.agent/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  73. package/.agent/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  74. package/.agent/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  75. package/.agent/skills/vercel-react-best-practices/rules/server-cache-react.md +26 -0
  76. package/.agent/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +79 -0
  77. package/.agent/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  78. package/.agent/skills/vitest-best-practices/AGENTS.md +84 -0
  79. package/.agent/skills/vitest-best-practices/SKILL.md +130 -0
  80. package/.agent/skills/vitest-best-practices/references/aaa-pattern.md +260 -0
  81. package/.agent/skills/vitest-best-practices/references/assertions.md +393 -0
  82. package/.agent/skills/vitest-best-practices/references/async-testing.md +454 -0
  83. package/.agent/skills/vitest-best-practices/references/error-handling.md +382 -0
  84. package/.agent/skills/vitest-best-practices/references/organization.md +212 -0
  85. package/.agent/skills/vitest-best-practices/references/parameterized-tests.md +297 -0
  86. package/.agent/skills/vitest-best-practices/references/performance.md +528 -0
  87. package/.agent/skills/vitest-best-practices/references/snapshot-testing.md +483 -0
  88. package/.agent/skills/vitest-best-practices/references/test-doubles.md +499 -0
  89. package/.agent/skills/vitest-best-practices/references/vitest-features.md +529 -0
  90. package/.agent/skills/web-design-guidelines/SKILL.md +39 -0
  91. package/.agent/tasks/.gitkeep +0 -0
  92. package/.agent/tasks.json +1 -0
  93. package/.claude/agents/code-reviewer.md +172 -0
  94. package/.claude/commands/aw.md +50 -0
  95. package/.claude/hooks/play-sound.js +87 -0
  96. package/.claude/hooks/pre-tool-use.js +40 -0
  97. package/.claude/settings.json +54 -0
  98. package/.claude/settings.local.json +13 -0
  99. package/.mcp.json +31 -0
  100. package/AGENTS.md +44 -0
  101. package/CLAUDE.md +1 -0
  102. package/README.md +236 -0
  103. package/bin/cli.js +156 -0
  104. package/bin/lib/copy.js +149 -0
  105. package/bin/lib/display.js +137 -0
  106. package/package.json +65 -0
  107. package/ralph.sh +333 -0
  108. package/scripts/lib/args.sh +44 -0
  109. package/scripts/lib/cleanup.sh +53 -0
  110. package/scripts/lib/constants.sh +25 -0
  111. package/scripts/lib/display.sh +196 -0
  112. package/scripts/lib/logging.sh +30 -0
  113. package/scripts/lib/notify.sh +41 -0
  114. package/scripts/lib/output.sh +147 -0
  115. package/scripts/lib/preflight.sh +57 -0
  116. package/scripts/lib/preview.sh +77 -0
  117. package/scripts/lib/promise.sh +76 -0
  118. package/scripts/lib/spinner.sh +85 -0
  119. package/scripts/lib/terminal.sh +57 -0
  120. package/scripts/lib/timing.sh +223 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Test Template for Custom Hooks
3
+ *
4
+ * Instructions:
5
+ * 1. Replace `useHookName` with your hook name
6
+ * 2. Update import path
7
+ * 3. Add/remove test sections based on hook features
8
+ */
9
+
10
+ import { renderHook, act, waitFor } from '@testing-library/react'
11
+ // import { useHookName } from './use-hook-name'
12
+
13
+ // ============================================================================
14
+ // Mocks
15
+ // ============================================================================
16
+
17
+ // API services (if hook fetches data)
18
+ // vi.mock('@/service/api')
19
+ // import * as api from '@/service/api'
20
+ // const mockedApi = vi.mocked(api)
21
+
22
+ // ============================================================================
23
+ // Test Helpers
24
+ // ============================================================================
25
+
26
+ // Wrapper for hooks that need context
27
+ // const createWrapper = (contextValue = {}) => {
28
+ // return ({ children }: { children: React.ReactNode }) => (
29
+ // <SomeContext.Provider value={contextValue}>
30
+ // {children}
31
+ // </SomeContext.Provider>
32
+ // )
33
+ // }
34
+
35
+ // ============================================================================
36
+ // Tests
37
+ // ============================================================================
38
+
39
+ describe('useHookName', () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks()
42
+ })
43
+
44
+ // --------------------------------------------------------------------------
45
+ // Initial State
46
+ // --------------------------------------------------------------------------
47
+ describe('Initial State', () => {
48
+ it('should return initial state', () => {
49
+ // const { result } = renderHook(() => useHookName())
50
+ //
51
+ // expect(result.current.value).toBe(initialValue)
52
+ // expect(result.current.isLoading).toBe(false)
53
+ })
54
+
55
+ it('should accept initial value from props', () => {
56
+ // const { result } = renderHook(() => useHookName({ initialValue: 'custom' }))
57
+ //
58
+ // expect(result.current.value).toBe('custom')
59
+ })
60
+ })
61
+
62
+ // --------------------------------------------------------------------------
63
+ // State Updates
64
+ // --------------------------------------------------------------------------
65
+ describe('State Updates', () => {
66
+ it('should update value when setValue is called', () => {
67
+ // const { result } = renderHook(() => useHookName())
68
+ //
69
+ // act(() => {
70
+ // result.current.setValue('new value')
71
+ // })
72
+ //
73
+ // expect(result.current.value).toBe('new value')
74
+ })
75
+
76
+ it('should reset to initial value', () => {
77
+ // const { result } = renderHook(() => useHookName({ initialValue: 'initial' }))
78
+ //
79
+ // act(() => {
80
+ // result.current.setValue('changed')
81
+ // })
82
+ // expect(result.current.value).toBe('changed')
83
+ //
84
+ // act(() => {
85
+ // result.current.reset()
86
+ // })
87
+ // expect(result.current.value).toBe('initial')
88
+ })
89
+ })
90
+
91
+ // --------------------------------------------------------------------------
92
+ // Async Operations
93
+ // --------------------------------------------------------------------------
94
+ describe('Async Operations', () => {
95
+ it('should fetch data on mount', async () => {
96
+ // mockedApi.fetchData.mockResolvedValue({ data: 'test' })
97
+ //
98
+ // const { result } = renderHook(() => useHookName())
99
+ //
100
+ // // Initially loading
101
+ // expect(result.current.isLoading).toBe(true)
102
+ //
103
+ // // Wait for data
104
+ // await waitFor(() => {
105
+ // expect(result.current.isLoading).toBe(false)
106
+ // })
107
+ //
108
+ // expect(result.current.data).toEqual({ data: 'test' })
109
+ })
110
+
111
+ it('should handle fetch error', async () => {
112
+ // mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
113
+ //
114
+ // const { result } = renderHook(() => useHookName())
115
+ //
116
+ // await waitFor(() => {
117
+ // expect(result.current.error).toBeTruthy()
118
+ // })
119
+ //
120
+ // expect(result.current.error?.message).toBe('Network error')
121
+ })
122
+
123
+ it('should refetch when dependency changes', async () => {
124
+ // mockedApi.fetchData.mockResolvedValue({ data: 'test' })
125
+ //
126
+ // const { result, rerender } = renderHook(
127
+ // ({ id }) => useHookName(id),
128
+ // { initialProps: { id: '1' } }
129
+ // )
130
+ //
131
+ // await waitFor(() => {
132
+ // expect(mockedApi.fetchData).toHaveBeenCalledWith('1')
133
+ // })
134
+ //
135
+ // rerender({ id: '2' })
136
+ //
137
+ // await waitFor(() => {
138
+ // expect(mockedApi.fetchData).toHaveBeenCalledWith('2')
139
+ // })
140
+ })
141
+ })
142
+
143
+ // --------------------------------------------------------------------------
144
+ // Side Effects
145
+ // --------------------------------------------------------------------------
146
+ describe('Side Effects', () => {
147
+ it('should call callback when value changes', () => {
148
+ // const callback = vi.fn()
149
+ // const { result } = renderHook(() => useHookName({ onChange: callback }))
150
+ //
151
+ // act(() => {
152
+ // result.current.setValue('new value')
153
+ // })
154
+ //
155
+ // expect(callback).toHaveBeenCalledWith('new value')
156
+ })
157
+
158
+ it('should cleanup on unmount', () => {
159
+ // const cleanup = vi.fn()
160
+ // vi.spyOn(window, 'addEventListener')
161
+ // vi.spyOn(window, 'removeEventListener')
162
+ //
163
+ // const { unmount } = renderHook(() => useHookName())
164
+ //
165
+ // expect(window.addEventListener).toHaveBeenCalled()
166
+ //
167
+ // unmount()
168
+ //
169
+ // expect(window.removeEventListener).toHaveBeenCalled()
170
+ })
171
+ })
172
+
173
+ // --------------------------------------------------------------------------
174
+ // Edge Cases
175
+ // --------------------------------------------------------------------------
176
+ describe('Edge Cases', () => {
177
+ it('should handle null input', () => {
178
+ // const { result } = renderHook(() => useHookName(null))
179
+ //
180
+ // expect(result.current.value).toBeNull()
181
+ })
182
+
183
+ it('should handle rapid updates', () => {
184
+ // const { result } = renderHook(() => useHookName())
185
+ //
186
+ // act(() => {
187
+ // result.current.setValue('1')
188
+ // result.current.setValue('2')
189
+ // result.current.setValue('3')
190
+ // })
191
+ //
192
+ // expect(result.current.value).toBe('3')
193
+ })
194
+ })
195
+
196
+ // --------------------------------------------------------------------------
197
+ // With Context (if hook uses context)
198
+ // --------------------------------------------------------------------------
199
+ describe('With Context', () => {
200
+ it('should use context value', () => {
201
+ // const wrapper = createWrapper({ someValue: 'context-value' })
202
+ // const { result } = renderHook(() => useHookName(), { wrapper })
203
+ //
204
+ // expect(result.current.contextValue).toBe('context-value')
205
+ })
206
+ })
207
+ })
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Test Template for Utility Functions
3
+ *
4
+ * Instructions:
5
+ * 1. Replace `utilityFunction` with your function name
6
+ * 2. Update import path
7
+ * 3. Use test.each for data-driven tests
8
+ */
9
+
10
+ // import { utilityFunction } from './utility'
11
+
12
+ // ============================================================================
13
+ // Tests
14
+ // ============================================================================
15
+
16
+ describe('utilityFunction', () => {
17
+ // --------------------------------------------------------------------------
18
+ // Basic Functionality
19
+ // --------------------------------------------------------------------------
20
+ describe('Basic Functionality', () => {
21
+ it('should return expected result for valid input', () => {
22
+ // expect(utilityFunction('input')).toBe('expected-output')
23
+ })
24
+
25
+ it('should handle multiple arguments', () => {
26
+ // expect(utilityFunction('a', 'b', 'c')).toBe('abc')
27
+ })
28
+ })
29
+
30
+ // --------------------------------------------------------------------------
31
+ // Data-Driven Tests
32
+ // --------------------------------------------------------------------------
33
+ describe('Input/Output Mapping', () => {
34
+ test.each([
35
+ // [input, expected]
36
+ ['input1', 'output1'],
37
+ ['input2', 'output2'],
38
+ ['input3', 'output3'],
39
+ ])('should return %s for input %s', (input, expected) => {
40
+ // expect(utilityFunction(input)).toBe(expected)
41
+ })
42
+ })
43
+
44
+ // --------------------------------------------------------------------------
45
+ // Edge Cases
46
+ // --------------------------------------------------------------------------
47
+ describe('Edge Cases', () => {
48
+ it('should handle empty string', () => {
49
+ // expect(utilityFunction('')).toBe('')
50
+ })
51
+
52
+ it('should handle null', () => {
53
+ // expect(utilityFunction(null)).toBe(null)
54
+ // or
55
+ // expect(() => utilityFunction(null)).toThrow()
56
+ })
57
+
58
+ it('should handle undefined', () => {
59
+ // expect(utilityFunction(undefined)).toBe(undefined)
60
+ // or
61
+ // expect(() => utilityFunction(undefined)).toThrow()
62
+ })
63
+
64
+ it('should handle empty array', () => {
65
+ // expect(utilityFunction([])).toEqual([])
66
+ })
67
+
68
+ it('should handle empty object', () => {
69
+ // expect(utilityFunction({})).toEqual({})
70
+ })
71
+ })
72
+
73
+ // --------------------------------------------------------------------------
74
+ // Boundary Conditions
75
+ // --------------------------------------------------------------------------
76
+ describe('Boundary Conditions', () => {
77
+ it('should handle minimum value', () => {
78
+ // expect(utilityFunction(0)).toBe(0)
79
+ })
80
+
81
+ it('should handle maximum value', () => {
82
+ // expect(utilityFunction(Number.MAX_SAFE_INTEGER)).toBe(...)
83
+ })
84
+
85
+ it('should handle negative numbers', () => {
86
+ // expect(utilityFunction(-1)).toBe(...)
87
+ })
88
+ })
89
+
90
+ // --------------------------------------------------------------------------
91
+ // Type Coercion (if applicable)
92
+ // --------------------------------------------------------------------------
93
+ describe('Type Handling', () => {
94
+ it('should handle numeric string', () => {
95
+ // expect(utilityFunction('123')).toBe(123)
96
+ })
97
+
98
+ it('should handle boolean', () => {
99
+ // expect(utilityFunction(true)).toBe(...)
100
+ })
101
+ })
102
+
103
+ // --------------------------------------------------------------------------
104
+ // Error Cases
105
+ // --------------------------------------------------------------------------
106
+ describe('Error Handling', () => {
107
+ it('should throw for invalid input', () => {
108
+ // expect(() => utilityFunction('invalid')).toThrow('Error message')
109
+ })
110
+
111
+ it('should throw with specific error type', () => {
112
+ // expect(() => utilityFunction('invalid')).toThrow(ValidationError)
113
+ })
114
+ })
115
+
116
+ // --------------------------------------------------------------------------
117
+ // Complex Objects (if applicable)
118
+ // --------------------------------------------------------------------------
119
+ describe('Object Handling', () => {
120
+ it('should preserve object structure', () => {
121
+ // const input = { a: 1, b: 2 }
122
+ // expect(utilityFunction(input)).toEqual({ a: 1, b: 2 })
123
+ })
124
+
125
+ it('should handle nested objects', () => {
126
+ // const input = { nested: { deep: 'value' } }
127
+ // expect(utilityFunction(input)).toEqual({ nested: { deep: 'transformed' } })
128
+ })
129
+
130
+ it('should not mutate input', () => {
131
+ // const input = { a: 1 }
132
+ // const inputCopy = { ...input }
133
+ // utilityFunction(input)
134
+ // expect(input).toEqual(inputCopy)
135
+ })
136
+ })
137
+
138
+ // --------------------------------------------------------------------------
139
+ // Array Handling (if applicable)
140
+ // --------------------------------------------------------------------------
141
+ describe('Array Handling', () => {
142
+ it('should process all elements', () => {
143
+ // expect(utilityFunction([1, 2, 3])).toEqual([2, 4, 6])
144
+ })
145
+
146
+ it('should handle single element array', () => {
147
+ // expect(utilityFunction([1])).toEqual([2])
148
+ })
149
+
150
+ it('should preserve order', () => {
151
+ // expect(utilityFunction(['c', 'a', 'b'])).toEqual(['c', 'a', 'b'])
152
+ })
153
+ })
154
+ })
@@ -0,0 +1,345 @@
1
+ # Async Testing Guide
2
+
3
+ ## Core Async Patterns
4
+
5
+ ### 1. waitFor - Wait for Condition
6
+
7
+ ```typescript
8
+ import { render, screen, waitFor } from '@testing-library/react'
9
+
10
+ it('should load and display data', async () => {
11
+ render(<DataComponent />)
12
+
13
+ // Wait for element to appear
14
+ await waitFor(() => {
15
+ expect(screen.getByText('Loaded Data')).toBeInTheDocument()
16
+ })
17
+ })
18
+
19
+ it('should hide loading spinner after load', async () => {
20
+ render(<DataComponent />)
21
+
22
+ // Wait for element to disappear
23
+ await waitFor(() => {
24
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
25
+ })
26
+ })
27
+ ```
28
+
29
+ ### 2. findBy\* - Async Queries
30
+
31
+ ```typescript
32
+ it('should show user name after fetch', async () => {
33
+ render(<UserProfile />)
34
+
35
+ // findBy returns a promise, auto-waits up to 1000ms
36
+ const userName = await screen.findByText('John Doe')
37
+ expect(userName).toBeInTheDocument()
38
+
39
+ // findByRole with options
40
+ const button = await screen.findByRole('button', { name: /submit/i })
41
+ expect(button).toBeEnabled()
42
+ })
43
+ ```
44
+
45
+ ### 3. userEvent for Async Interactions
46
+
47
+ ```typescript
48
+ import userEvent from '@testing-library/user-event'
49
+
50
+ it('should submit form', async () => {
51
+ const user = userEvent.setup()
52
+ const onSubmit = vi.fn()
53
+
54
+ render(<Form onSubmit={onSubmit} />)
55
+
56
+ // userEvent methods are async
57
+ await user.type(screen.getByLabelText('Email'), 'test@example.com')
58
+ await user.click(screen.getByRole('button', { name: /submit/i }))
59
+
60
+ await waitFor(() => {
61
+ expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com' })
62
+ })
63
+ })
64
+ ```
65
+
66
+ ## Fake Timers
67
+
68
+ ### When to Use Fake Timers
69
+
70
+ - Testing components with `setTimeout`/`setInterval`
71
+ - Testing debounce/throttle behavior
72
+ - Testing animations or delayed transitions
73
+ - Testing polling or retry logic
74
+
75
+ ### Basic Fake Timer Setup
76
+
77
+ ```typescript
78
+ describe('Debounced Search', () => {
79
+ beforeEach(() => {
80
+ vi.useFakeTimers()
81
+ })
82
+
83
+ afterEach(() => {
84
+ vi.useRealTimers()
85
+ })
86
+
87
+ it('should debounce search input', async () => {
88
+ const onSearch = vi.fn()
89
+ render(<SearchInput onSearch={onSearch} debounceMs={300} />)
90
+
91
+ // Type in the input
92
+ fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } })
93
+
94
+ // Search not called immediately
95
+ expect(onSearch).not.toHaveBeenCalled()
96
+
97
+ // Advance timers
98
+ vi.advanceTimersByTime(300)
99
+
100
+ // Now search is called
101
+ expect(onSearch).toHaveBeenCalledWith('query')
102
+ })
103
+ })
104
+ ```
105
+
106
+ ### Fake Timers with Async Code
107
+
108
+ ```typescript
109
+ it('should retry on failure', async () => {
110
+ vi.useFakeTimers()
111
+ const fetchData = vi.fn()
112
+ .mockRejectedValueOnce(new Error('Network error'))
113
+ .mockResolvedValueOnce({ data: 'success' })
114
+
115
+ render(<RetryComponent fetchData={fetchData} retryDelayMs={1000} />)
116
+
117
+ // First call fails
118
+ await waitFor(() => {
119
+ expect(fetchData).toHaveBeenCalledTimes(1)
120
+ })
121
+
122
+ // Advance timer for retry
123
+ vi.advanceTimersByTime(1000)
124
+
125
+ // Second call succeeds
126
+ await waitFor(() => {
127
+ expect(fetchData).toHaveBeenCalledTimes(2)
128
+ expect(screen.getByText('success')).toBeInTheDocument()
129
+ })
130
+
131
+ vi.useRealTimers()
132
+ })
133
+ ```
134
+
135
+ ### Common Fake Timer Utilities
136
+
137
+ ```typescript
138
+ // Run all pending timers
139
+ vi.runAllTimers()
140
+
141
+ // Run only pending timers (not new ones created during execution)
142
+ vi.runOnlyPendingTimers()
143
+
144
+ // Advance by specific time
145
+ vi.advanceTimersByTime(1000)
146
+
147
+ // Get current fake time
148
+ Date.now()
149
+
150
+ // Clear all timers
151
+ vi.clearAllTimers()
152
+ ```
153
+
154
+ ## API Testing Patterns
155
+
156
+ ### Loading → Success → Error States
157
+
158
+ ```typescript
159
+ describe('DataFetcher', () => {
160
+ beforeEach(() => {
161
+ vi.clearAllMocks()
162
+ })
163
+
164
+ it('should show loading state', () => {
165
+ mockedApi.fetchData.mockImplementation(() => new Promise(() => {})) // Never resolves
166
+
167
+ render(<DataFetcher />)
168
+
169
+ expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
170
+ })
171
+
172
+ it('should show data on success', async () => {
173
+ mockedApi.fetchData.mockResolvedValue({ items: ['Item 1', 'Item 2'] })
174
+
175
+ render(<DataFetcher />)
176
+
177
+ // Use findBy* for multiple async elements (better error messages than waitFor with multiple assertions)
178
+ const item1 = await screen.findByText('Item 1')
179
+ const item2 = await screen.findByText('Item 2')
180
+ expect(item1).toBeInTheDocument()
181
+ expect(item2).toBeInTheDocument()
182
+
183
+ expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument()
184
+ })
185
+
186
+ it('should show error on failure', async () => {
187
+ mockedApi.fetchData.mockRejectedValue(new Error('Failed to fetch'))
188
+
189
+ render(<DataFetcher />)
190
+
191
+ await waitFor(() => {
192
+ expect(screen.getByText(/failed to fetch/i)).toBeInTheDocument()
193
+ })
194
+ })
195
+
196
+ it('should retry on error', async () => {
197
+ mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
198
+
199
+ render(<DataFetcher />)
200
+
201
+ await waitFor(() => {
202
+ expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument()
203
+ })
204
+
205
+ mockedApi.fetchData.mockResolvedValue({ items: ['Item 1'] })
206
+ fireEvent.click(screen.getByRole('button', { name: /retry/i }))
207
+
208
+ await waitFor(() => {
209
+ expect(screen.getByText('Item 1')).toBeInTheDocument()
210
+ })
211
+ })
212
+ })
213
+ ```
214
+
215
+ ### Testing Mutations
216
+
217
+ ```typescript
218
+ it('should submit form and show success', async () => {
219
+ const user = userEvent.setup()
220
+ mockedApi.createItem.mockResolvedValue({ id: '1', name: 'New Item' })
221
+
222
+ render(<CreateItemForm />)
223
+
224
+ await user.type(screen.getByLabelText('Name'), 'New Item')
225
+ await user.click(screen.getByRole('button', { name: /create/i }))
226
+
227
+ // Button should be disabled during submission
228
+ expect(screen.getByRole('button', { name: /creating/i })).toBeDisabled()
229
+
230
+ await waitFor(() => {
231
+ expect(screen.getByText(/created successfully/i)).toBeInTheDocument()
232
+ })
233
+
234
+ expect(mockedApi.createItem).toHaveBeenCalledWith({ name: 'New Item' })
235
+ })
236
+ ```
237
+
238
+ ## useEffect Testing
239
+
240
+ ### Testing Effect Execution
241
+
242
+ ```typescript
243
+ it('should fetch data on mount', async () => {
244
+ const fetchData = vi.fn().mockResolvedValue({ data: 'test' })
245
+
246
+ render(<ComponentWithEffect fetchData={fetchData} />)
247
+
248
+ await waitFor(() => {
249
+ expect(fetchData).toHaveBeenCalledTimes(1)
250
+ })
251
+ })
252
+ ```
253
+
254
+ ### Testing Effect Dependencies
255
+
256
+ ```typescript
257
+ it('should refetch when id changes', async () => {
258
+ const fetchData = vi.fn().mockResolvedValue({ data: 'test' })
259
+
260
+ const { rerender } = render(<ComponentWithEffect id="1" fetchData={fetchData} />)
261
+
262
+ await waitFor(() => {
263
+ expect(fetchData).toHaveBeenCalledWith('1')
264
+ })
265
+
266
+ rerender(<ComponentWithEffect id="2" fetchData={fetchData} />)
267
+
268
+ await waitFor(() => {
269
+ expect(fetchData).toHaveBeenCalledWith('2')
270
+ expect(fetchData).toHaveBeenCalledTimes(2)
271
+ })
272
+ })
273
+ ```
274
+
275
+ ### Testing Effect Cleanup
276
+
277
+ ```typescript
278
+ it('should cleanup subscription on unmount', () => {
279
+ const subscribe = vi.fn()
280
+ const unsubscribe = vi.fn()
281
+ subscribe.mockReturnValue(unsubscribe)
282
+
283
+ const { unmount } = render(<SubscriptionComponent subscribe={subscribe} />)
284
+
285
+ expect(subscribe).toHaveBeenCalledTimes(1)
286
+
287
+ unmount()
288
+
289
+ expect(unsubscribe).toHaveBeenCalledTimes(1)
290
+ })
291
+ ```
292
+
293
+ ## Common Async Pitfalls
294
+
295
+ ### ❌ Don't: Forget to await
296
+
297
+ ```typescript
298
+ // Bad - test may pass even if assertion fails
299
+ it('should load data', () => {
300
+ render(<Component />)
301
+ waitFor(() => {
302
+ expect(screen.getByText('Data')).toBeInTheDocument()
303
+ })
304
+ })
305
+
306
+ // Good - properly awaited
307
+ it('should load data', async () => {
308
+ render(<Component />)
309
+ await waitFor(() => {
310
+ expect(screen.getByText('Data')).toBeInTheDocument()
311
+ })
312
+ })
313
+ ```
314
+
315
+ ### ❌ Don't: Use multiple assertions in single waitFor
316
+
317
+ ```typescript
318
+ // Bad - if first assertion fails, won't know about second
319
+ await waitFor(() => {
320
+ expect(screen.getByText('Title')).toBeInTheDocument()
321
+ expect(screen.getByText('Description')).toBeInTheDocument()
322
+ })
323
+
324
+ // Good - separate waitFor or use findBy
325
+ const title = await screen.findByText('Title')
326
+ const description = await screen.findByText('Description')
327
+ expect(title).toBeInTheDocument()
328
+ expect(description).toBeInTheDocument()
329
+ ```
330
+
331
+ ### ❌ Don't: Mix fake timers with real async
332
+
333
+ ```typescript
334
+ // Bad - fake timers don't work well with real Promises
335
+ vi.useFakeTimers()
336
+ await waitFor(() => {
337
+ expect(screen.getByText('Data')).toBeInTheDocument()
338
+ }) // May timeout!
339
+
340
+ // Good - use runAllTimers or advanceTimersByTime
341
+ vi.useFakeTimers()
342
+ render(<Component />)
343
+ vi.runAllTimers()
344
+ expect(screen.getByText('Data')).toBeInTheDocument()
345
+ ```