@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,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
|
+
```
|