@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,188 @@
1
+ # Test Generation Checklist
2
+
3
+ Use this checklist when generating or reviewing tests for frontend components.
4
+
5
+ ## Pre-Generation
6
+
7
+ - [ ] Read the component source code completely
8
+ - [ ] Identify component type (component, hook, utility, page)
9
+ - [ ] Analyze component complexity and features
10
+ - [ ] Note complexity score and features detected
11
+ - [ ] Check for existing tests in the same directory
12
+ - [ ] **Identify ALL files in the directory** that need testing (not just index)
13
+
14
+ ## Testing Strategy
15
+
16
+ ### ⚠️ Incremental Workflow (CRITICAL for Multi-File)
17
+
18
+ - [ ] **NEVER generate all tests at once** - process one file at a time
19
+ - [ ] Order files by complexity: utilities → hooks → simple → complex → integration
20
+ - [ ] Create a todo list to track progress before starting
21
+ - [ ] For EACH file: write → run test → verify pass → then next
22
+ - [ ] **DO NOT proceed** to next file until current one passes
23
+
24
+ ### Path-Level Coverage
25
+
26
+ - [ ] **Test ALL files** in the assigned directory/path
27
+ - [ ] List all components, hooks, utilities that need coverage
28
+ - [ ] Decide: single spec file (integration) or multiple spec files (unit)
29
+
30
+ ### Complexity Assessment
31
+
32
+ - [ ] **Medium Complexity and higher**: Consider refactoring before testing
33
+ - [ ] **500+ lines**: Consider splitting before testing
34
+ - [ ] **Low complexity**: Use multiple describe blocks, organized structure
35
+
36
+ ### Integration vs Mocking
37
+
38
+ - [ ] Only mock: API calls, complex context providers, third-party libs with side effects
39
+ - [ ] Prefer integration testing when using single spec file
40
+
41
+ ## Required Test Sections
42
+
43
+ ### All Components MUST Have
44
+
45
+ - [ ] **Rendering tests** - Component renders without crashing
46
+ - [ ] **Props tests** - Required props, optional props, default values
47
+ - [ ] **Edge cases** - null, undefined, empty values, boundaries
48
+
49
+ ### Conditional Sections (Add When Feature Present)
50
+
51
+ | Feature | Add Tests For |
52
+ | ----------------------- | ------------------------------------- |
53
+ | `useState` | Initial state, transitions, cleanup |
54
+ | `useEffect` | Execution, dependencies, cleanup |
55
+ | Event handlers | onClick, onChange, onSubmit, keyboard |
56
+ | API calls | Loading, success, error states |
57
+ | Routing | Navigation, params, query strings |
58
+ | `useCallback`/`useMemo` | Referential equality |
59
+ | Context | Provider values, consumer behavior |
60
+ | Forms | Validation, submission, error display |
61
+
62
+ ## Code Quality Checklist
63
+
64
+ ### Structure
65
+
66
+ - [ ] Uses `describe` blocks to group related tests
67
+ - [ ] Test names follow `should <behavior> when <condition>` pattern
68
+ - [ ] AAA pattern (Arrange-Act-Assert) is clear
69
+ - [ ] Comments explain complex test scenarios
70
+
71
+ ### Mocks
72
+
73
+ - [ ] `vi.clearAllMocks()` in `beforeEach` (not `afterEach`)
74
+ - [ ] Shared mock state reset in `beforeEach`
75
+ - [ ] Router mocks match actual Next.js API
76
+ - [ ] Mocks reflect actual component conditional behavior
77
+ - [ ] Only mock: API services, complex context providers, third-party libs
78
+
79
+ ### Queries
80
+
81
+ - [ ] Prefer semantic queries (`getByRole`, `getByLabelText`)
82
+ - [ ] Use `queryBy*` for absence assertions
83
+ - [ ] Use `findBy*` for async elements
84
+ - [ ] `getByTestId` only as last resort
85
+
86
+ ### Async
87
+
88
+ - [ ] All async tests use `async/await`
89
+ - [ ] `waitFor` wraps async assertions
90
+ - [ ] Fake timers properly setup/teardown
91
+ - [ ] No floating promises
92
+
93
+ ### TypeScript
94
+
95
+ - [ ] No `any` types without justification
96
+ - [ ] Mock data uses actual types from source
97
+ - [ ] Factory functions have proper return types
98
+
99
+ ## Coverage Goals (Per File)
100
+
101
+ For the current file being tested:
102
+
103
+ - [ ] 100% function coverage
104
+ - [ ] 100% statement coverage
105
+ - [ ] >95% branch coverage
106
+ - [ ] >95% line coverage
107
+
108
+ ## Post-Generation (Per File)
109
+
110
+ **Run these checks after EACH test file, not just at the end:**
111
+
112
+ - [ ] Run `npm run test-unit path/to/file.spec.tsx` - **MUST PASS before next file**
113
+ - [ ] Fix any failures immediately
114
+ - [ ] Mark file as complete in todo list
115
+ - [ ] Only then proceed to next file
116
+
117
+ ### After All Files Complete
118
+
119
+ - [ ] Run full directory test: `npm run test-unit path/to/directory/`
120
+ - [ ] Check coverage report: `npm run test-unit:coverage path/to/directory/`
121
+ - [ ] Run `npm run lint --fix` on all test files
122
+ - [ ] Run `npm run typecheck`
123
+
124
+ ## Common Issues to Watch
125
+
126
+ ### False Positives
127
+
128
+ ```typescript
129
+ // ❌ Mock doesn't match actual behavior
130
+ vi.mock('./Component', () => () => <div>Mocked</div>)
131
+
132
+ // ✅ Mock matches actual conditional logic
133
+ vi.mock('./Component', () => ({ isOpen }: any) =>
134
+ isOpen ? <div>Content</div> : null
135
+ )
136
+ ```
137
+
138
+ ### State Leakage
139
+
140
+ ```typescript
141
+ // ❌ Shared state not reset
142
+ let mockState = false
143
+ vi.mock('./useHook', () => () => mockState)
144
+
145
+ // ✅ Reset in beforeEach
146
+ beforeEach(() => {
147
+ mockState = false
148
+ })
149
+ ```
150
+
151
+ ### Async Race Conditions
152
+
153
+ ```typescript
154
+ // ❌ Not awaited
155
+ it('loads data', () => {
156
+ render(<Component />)
157
+ expect(screen.getByText('Data')).toBeInTheDocument()
158
+ })
159
+
160
+ // ✅ Properly awaited
161
+ it('loads data', async () => {
162
+ render(<Component />)
163
+ await waitFor(() => {
164
+ expect(screen.getByText('Data')).toBeInTheDocument()
165
+ })
166
+ })
167
+ ```
168
+
169
+ ### Missing Edge Cases
170
+
171
+ Always test these scenarios:
172
+
173
+ - `null` / `undefined` inputs
174
+ - Empty strings / arrays / objects
175
+ - Boundary values (0, -1, MAX_INT)
176
+ - Error states
177
+ - Loading states
178
+ - Disabled states
179
+
180
+ ## Quick Commands
181
+
182
+ ```bash
183
+ # Run specific test
184
+ npm run test-unit path/to/file.spec.tsx
185
+
186
+ # Run with coverage
187
+ npm run test-unit:coverage path/to/file.spec.tsx
188
+ ```
@@ -0,0 +1,449 @@
1
+ # Common Testing Patterns
2
+
3
+ ## Query Priority
4
+
5
+ Use queries in this order (most to least preferred):
6
+
7
+ ```typescript
8
+ // 1. getByRole - Most recommended (accessibility)
9
+ screen.getByRole('button', { name: /submit/i })
10
+ screen.getByRole('textbox', { name: /email/i })
11
+ screen.getByRole('heading', { level: 1 })
12
+
13
+ // 2. getByLabelText - Form fields
14
+ screen.getByLabelText('Email address')
15
+ screen.getByLabelText(/password/i)
16
+
17
+ // 3. getByPlaceholderText - When no label
18
+ screen.getByPlaceholderText('Search...')
19
+
20
+ // 4. getByText - Non-interactive elements
21
+ screen.getByText('Welcome')
22
+ screen.getByText(/loading/i)
23
+
24
+ // 5. getByDisplayValue - Current input value
25
+ screen.getByDisplayValue('current value')
26
+
27
+ // 6. getByAltText - Images
28
+ screen.getByAltText('Company logo')
29
+
30
+ // 7. getByTitle - Tooltip elements
31
+ screen.getByTitle('Close')
32
+
33
+ // 8. getByTestId - Last resort only!
34
+ screen.getByTestId('custom-element')
35
+ ```
36
+
37
+ ## Event Handling Patterns
38
+
39
+ ### Click Events
40
+
41
+ ```typescript
42
+ // Basic click
43
+ fireEvent.click(screen.getByRole('button'))
44
+
45
+ // With userEvent (preferred for realistic interaction)
46
+ const user = userEvent.setup()
47
+ await user.click(screen.getByRole('button'))
48
+
49
+ // Double click
50
+ await user.dblClick(screen.getByRole('button'))
51
+
52
+ // Right click
53
+ await user.pointer({ keys: '[MouseRight]', target: screen.getByRole('button') })
54
+ ```
55
+
56
+ ### Form Input
57
+
58
+ ```typescript
59
+ const user = userEvent.setup()
60
+
61
+ // Type in input
62
+ await user.type(screen.getByRole('textbox'), 'Hello World')
63
+
64
+ // Clear and type
65
+ await user.clear(screen.getByRole('textbox'))
66
+ await user.type(screen.getByRole('textbox'), 'New value')
67
+
68
+ // Select option
69
+ await user.selectOptions(screen.getByRole('combobox'), 'option-value')
70
+
71
+ // Check checkbox
72
+ await user.click(screen.getByRole('checkbox'))
73
+
74
+ // Upload file
75
+ const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
76
+ await user.upload(screen.getByLabelText(/upload/i), file)
77
+ ```
78
+
79
+ ### Keyboard Events
80
+
81
+ ```typescript
82
+ const user = userEvent.setup()
83
+
84
+ // Press Enter
85
+ await user.keyboard('{Enter}')
86
+
87
+ // Press Escape
88
+ await user.keyboard('{Escape}')
89
+
90
+ // Keyboard shortcut
91
+ await user.keyboard('{Control>}a{/Control}') // Ctrl+A
92
+
93
+ // Tab navigation
94
+ await user.tab()
95
+
96
+ // Arrow keys
97
+ await user.keyboard('{ArrowDown}')
98
+ await user.keyboard('{ArrowUp}')
99
+ ```
100
+
101
+ ## Component State Testing
102
+
103
+ ### Testing State Transitions
104
+
105
+ ```typescript
106
+ describe('Counter', () => {
107
+ it('should increment count', async () => {
108
+ const user = userEvent.setup()
109
+ render(<Counter initialCount={0} />)
110
+
111
+ // Initial state
112
+ expect(screen.getByText('Count: 0')).toBeInTheDocument()
113
+
114
+ // Trigger transition
115
+ await user.click(screen.getByRole('button', { name: /increment/i }))
116
+
117
+ // New state
118
+ expect(screen.getByText('Count: 1')).toBeInTheDocument()
119
+ })
120
+ })
121
+ ```
122
+
123
+ ### Testing Controlled Components
124
+
125
+ ```typescript
126
+ describe('ControlledInput', () => {
127
+ it('should call onChange with new value', async () => {
128
+ const user = userEvent.setup()
129
+ const handleChange = vi.fn()
130
+
131
+ render(<ControlledInput value="" onChange={handleChange} />)
132
+
133
+ await user.type(screen.getByRole('textbox'), 'a')
134
+
135
+ expect(handleChange).toHaveBeenCalledWith('a')
136
+ })
137
+
138
+ it('should display controlled value', () => {
139
+ render(<ControlledInput value="controlled" onChange={vi.fn()} />)
140
+
141
+ expect(screen.getByRole('textbox')).toHaveValue('controlled')
142
+ })
143
+ })
144
+ ```
145
+
146
+ ## Conditional Rendering Testing
147
+
148
+ ```typescript
149
+ describe('ConditionalComponent', () => {
150
+ it('should show loading state', () => {
151
+ render(<DataDisplay isLoading={true} data={null} />)
152
+
153
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
154
+ expect(screen.queryByTestId('data-content')).not.toBeInTheDocument()
155
+ })
156
+
157
+ it('should show error state', () => {
158
+ render(<DataDisplay isLoading={false} data={null} error="Failed to load" />)
159
+
160
+ expect(screen.getByText(/failed to load/i)).toBeInTheDocument()
161
+ })
162
+
163
+ it('should show data when loaded', () => {
164
+ render(<DataDisplay isLoading={false} data={{ name: 'Test' }} />)
165
+
166
+ expect(screen.getByText('Test')).toBeInTheDocument()
167
+ })
168
+
169
+ it('should show empty state when no data', () => {
170
+ render(<DataDisplay isLoading={false} data={[]} />)
171
+
172
+ expect(screen.getByText(/no data/i)).toBeInTheDocument()
173
+ })
174
+ })
175
+ ```
176
+
177
+ ## List Rendering Testing
178
+
179
+ ```typescript
180
+ describe('ItemList', () => {
181
+ const items = [
182
+ { id: '1', name: 'Item 1' },
183
+ { id: '2', name: 'Item 2' },
184
+ { id: '3', name: 'Item 3' },
185
+ ]
186
+
187
+ it('should render all items', () => {
188
+ render(<ItemList items={items} />)
189
+
190
+ expect(screen.getAllByRole('listitem')).toHaveLength(3)
191
+ items.forEach(item => {
192
+ expect(screen.getByText(item.name)).toBeInTheDocument()
193
+ })
194
+ })
195
+
196
+ it('should handle item selection', async () => {
197
+ const user = userEvent.setup()
198
+ const onSelect = vi.fn()
199
+
200
+ render(<ItemList items={items} onSelect={onSelect} />)
201
+
202
+ await user.click(screen.getByText('Item 2'))
203
+
204
+ expect(onSelect).toHaveBeenCalledWith(items[1])
205
+ })
206
+
207
+ it('should handle empty list', () => {
208
+ render(<ItemList items={[]} />)
209
+
210
+ expect(screen.getByText(/no items/i)).toBeInTheDocument()
211
+ })
212
+ })
213
+ ```
214
+
215
+ ## Modal/Dialog Testing
216
+
217
+ ```typescript
218
+ describe('Modal', () => {
219
+ it('should not render when closed', () => {
220
+ render(<Modal isOpen={false} onClose={vi.fn()} />)
221
+
222
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
223
+ })
224
+
225
+ it('should render when open', () => {
226
+ render(<Modal isOpen={true} onClose={vi.fn()} />)
227
+
228
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
229
+ })
230
+
231
+ it('should call onClose when clicking overlay', async () => {
232
+ const user = userEvent.setup()
233
+ const handleClose = vi.fn()
234
+
235
+ render(<Modal isOpen={true} onClose={handleClose} />)
236
+
237
+ await user.click(screen.getByTestId('modal-overlay'))
238
+
239
+ expect(handleClose).toHaveBeenCalled()
240
+ })
241
+
242
+ it('should call onClose when pressing Escape', async () => {
243
+ const user = userEvent.setup()
244
+ const handleClose = vi.fn()
245
+
246
+ render(<Modal isOpen={true} onClose={handleClose} />)
247
+
248
+ await user.keyboard('{Escape}')
249
+
250
+ expect(handleClose).toHaveBeenCalled()
251
+ })
252
+
253
+ it('should trap focus inside modal', async () => {
254
+ const user = userEvent.setup()
255
+
256
+ render(
257
+ <Modal isOpen={true} onClose={vi.fn()}>
258
+ <button>First</button>
259
+ <button>Second</button>
260
+ </Modal>
261
+ )
262
+
263
+ // Focus should cycle within modal
264
+ await user.tab()
265
+ expect(screen.getByText('First')).toHaveFocus()
266
+
267
+ await user.tab()
268
+ expect(screen.getByText('Second')).toHaveFocus()
269
+
270
+ await user.tab()
271
+ expect(screen.getByText('First')).toHaveFocus() // Cycles back
272
+ })
273
+ })
274
+ ```
275
+
276
+ ## Form Testing
277
+
278
+ ```typescript
279
+ describe('LoginForm', () => {
280
+ it('should submit valid form', async () => {
281
+ const user = userEvent.setup()
282
+ const onSubmit = vi.fn()
283
+
284
+ render(<LoginForm onSubmit={onSubmit} />)
285
+
286
+ await user.type(screen.getByLabelText(/email/i), 'test@example.com')
287
+ await user.type(screen.getByLabelText(/password/i), 'password123')
288
+ await user.click(screen.getByRole('button', { name: /sign in/i }))
289
+
290
+ expect(onSubmit).toHaveBeenCalledWith({
291
+ email: 'test@example.com',
292
+ password: 'password123',
293
+ })
294
+ })
295
+
296
+ it('should show validation errors', async () => {
297
+ const user = userEvent.setup()
298
+
299
+ render(<LoginForm onSubmit={vi.fn()} />)
300
+
301
+ // Submit empty form
302
+ await user.click(screen.getByRole('button', { name: /sign in/i }))
303
+
304
+ expect(screen.getByText(/email is required/i)).toBeInTheDocument()
305
+ expect(screen.getByText(/password is required/i)).toBeInTheDocument()
306
+ })
307
+
308
+ it('should validate email format', async () => {
309
+ const user = userEvent.setup()
310
+
311
+ render(<LoginForm onSubmit={vi.fn()} />)
312
+
313
+ await user.type(screen.getByLabelText(/email/i), 'invalid-email')
314
+ await user.click(screen.getByRole('button', { name: /sign in/i }))
315
+
316
+ expect(screen.getByText(/invalid email/i)).toBeInTheDocument()
317
+ })
318
+
319
+ it('should disable submit button while submitting', async () => {
320
+ const user = userEvent.setup()
321
+ const onSubmit = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)))
322
+
323
+ render(<LoginForm onSubmit={onSubmit} />)
324
+
325
+ await user.type(screen.getByLabelText(/email/i), 'test@example.com')
326
+ await user.type(screen.getByLabelText(/password/i), 'password123')
327
+ await user.click(screen.getByRole('button', { name: /sign in/i }))
328
+
329
+ expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled()
330
+
331
+ await waitFor(() => {
332
+ expect(screen.getByRole('button', { name: /sign in/i })).toBeEnabled()
333
+ })
334
+ })
335
+ })
336
+ ```
337
+
338
+ ## Data-Driven Tests with test.each
339
+
340
+ ```typescript
341
+ describe('StatusBadge', () => {
342
+ test.each([
343
+ ['success', 'bg-green-500'],
344
+ ['warning', 'bg-yellow-500'],
345
+ ['error', 'bg-red-500'],
346
+ ['info', 'bg-blue-500'],
347
+ ])('should apply correct class for %s status', (status, expectedClass) => {
348
+ render(<StatusBadge status={status} />)
349
+
350
+ expect(screen.getByTestId('status-badge')).toHaveClass(expectedClass)
351
+ })
352
+
353
+ test.each([
354
+ { input: null, expected: 'Unknown' },
355
+ { input: undefined, expected: 'Unknown' },
356
+ { input: '', expected: 'Unknown' },
357
+ { input: 'invalid', expected: 'Unknown' },
358
+ ])('should show "Unknown" for invalid input: $input', ({ input, expected }) => {
359
+ render(<StatusBadge status={input} />)
360
+
361
+ expect(screen.getByText(expected)).toBeInTheDocument()
362
+ })
363
+ })
364
+ ```
365
+
366
+ ## Debugging Tips
367
+
368
+ ```typescript
369
+ // Print entire DOM
370
+ screen.debug()
371
+
372
+ // Print specific element
373
+ screen.debug(screen.getByRole('button'))
374
+
375
+ // Log testing playground URL
376
+ screen.logTestingPlaygroundURL()
377
+
378
+ // Pretty print DOM
379
+ import { prettyDOM } from '@testing-library/react'
380
+ console.log(prettyDOM(screen.getByRole('dialog')))
381
+
382
+ // Check available roles
383
+ import { getRoles } from '@testing-library/react'
384
+ console.log(getRoles(container))
385
+ ```
386
+
387
+ ## Common Mistakes to Avoid
388
+
389
+ ### ❌ Don't Use Implementation Details
390
+
391
+ ```typescript
392
+ // Bad - testing implementation
393
+ expect(component.state.isOpen).toBe(true)
394
+ expect(wrapper.find('.internal-class').length).toBe(1)
395
+
396
+ // Good - testing behavior
397
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
398
+ ```
399
+
400
+ ### ❌ Don't Forget Cleanup
401
+
402
+ ```typescript
403
+ // Bad - may leak state between tests
404
+ it('test 1', () => {
405
+ render(<Component />)
406
+ })
407
+
408
+ // Good - cleanup is automatic with RTL, but reset mocks
409
+ beforeEach(() => {
410
+ vi.clearAllMocks()
411
+ })
412
+ ```
413
+
414
+ ### ❌ Don't Use Exact String Matching (Prefer Black-Box Assertions)
415
+
416
+ ```typescript
417
+ // ❌ Bad - hardcoded strings are brittle
418
+ expect(screen.getByText('Submit Form')).toBeInTheDocument()
419
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
420
+
421
+ // ✅ Good - role-based queries (most semantic)
422
+ expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument()
423
+ expect(screen.getByRole('status')).toBeInTheDocument()
424
+
425
+ // ✅ Good - pattern matching (flexible)
426
+ expect(screen.getByText(/submit/i)).toBeInTheDocument()
427
+ expect(screen.getByText(/loading/i)).toBeInTheDocument()
428
+
429
+ // ✅ Good - test behavior, not exact UI text
430
+ expect(screen.getByRole('button')).toBeDisabled()
431
+ expect(screen.getByRole('alert')).toBeInTheDocument()
432
+ ```
433
+
434
+ **Why prefer black-box assertions?**
435
+
436
+ - Text content may change (i18n, copy updates)
437
+ - Role-based queries test accessibility
438
+ - Pattern matching is resilient to minor changes
439
+ - Tests focus on behavior, not implementation details
440
+
441
+ ### ❌ Don't Assert on Absence Without Query
442
+
443
+ ```typescript
444
+ // Bad - throws if not found
445
+ expect(screen.getByText('Error')).not.toBeInTheDocument() // Error!
446
+
447
+ // Good - use queryBy for absence assertions
448
+ expect(screen.queryByText('Error')).not.toBeInTheDocument()
449
+ ```