@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,36 @@
|
|
|
1
|
+
# Rule Catalog — Performance
|
|
2
|
+
|
|
3
|
+
## Complex prop memoization
|
|
4
|
+
|
|
5
|
+
IsUrgent: True
|
|
6
|
+
Category: Performance
|
|
7
|
+
|
|
8
|
+
### Description
|
|
9
|
+
|
|
10
|
+
Wrap complex prop values (objects, arrays, maps) in `useMemo` prior to passing them into child components to guarantee stable references and prevent unnecessary renders.
|
|
11
|
+
|
|
12
|
+
Update this file when adding, editing, or removing Performance rules so the catalog remains accurate.
|
|
13
|
+
|
|
14
|
+
Wrong:
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<HeavyComp
|
|
18
|
+
config={{
|
|
19
|
+
provider: ...,
|
|
20
|
+
detail: ...
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Right:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
const config = useMemo(() => ({
|
|
29
|
+
provider: ...,
|
|
30
|
+
detail: ...
|
|
31
|
+
}), [provider, detail]);
|
|
32
|
+
|
|
33
|
+
<HeavyComp
|
|
34
|
+
config={config}
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-testing
|
|
3
|
+
description: Generate Vitest + React Testing Library tests for frontend components, hooks, and utilities. Triggers on testing, spec files, coverage, Vitest, RTL, unit tests, integration tests, or write/review test requests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This skill enables Claude to generate high-quality, comprehensive frontend tests following established conventions and best practices.
|
|
7
|
+
|
|
8
|
+
## When to Apply This Skill
|
|
9
|
+
|
|
10
|
+
Apply this skill when the user:
|
|
11
|
+
|
|
12
|
+
- Asks to **write tests** for a component, hook, or utility
|
|
13
|
+
- Asks to **review existing tests** for completeness
|
|
14
|
+
- Mentions **Vitest**, **React Testing Library**, **RTL**, or **spec files**
|
|
15
|
+
- Requests **test coverage** improvement
|
|
16
|
+
- Mentions **testing**, **unit tests**, or **integration tests** for frontend code
|
|
17
|
+
- Wants to understand **testing patterns** in the frontend codebase
|
|
18
|
+
|
|
19
|
+
**Do NOT apply** when:
|
|
20
|
+
|
|
21
|
+
- User is asking about E2E tests (Playwright)
|
|
22
|
+
- User is only asking conceptual questions without code context
|
|
23
|
+
|
|
24
|
+
## Quick Reference
|
|
25
|
+
|
|
26
|
+
### Tech Stack
|
|
27
|
+
|
|
28
|
+
| Tool | Version | Purpose |
|
|
29
|
+
| --------------------- | ------- | ----------------- |
|
|
30
|
+
| Vitest | 4+ | Test runner |
|
|
31
|
+
| React Testing Library | 16+ | Component testing |
|
|
32
|
+
| jsdom | - | Test environment |
|
|
33
|
+
| TypeScript | 5+ | Type safety |
|
|
34
|
+
|
|
35
|
+
Note: keep this list up to date with the project's dependencies.
|
|
36
|
+
|
|
37
|
+
### Key Commands
|
|
38
|
+
|
|
39
|
+
**Always prefer running specific tests over the entire suite** for faster feedback:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Run ALL tests (avoid during development)
|
|
43
|
+
npx vitest run
|
|
44
|
+
|
|
45
|
+
# ✅ PREFERRED: Run specific file
|
|
46
|
+
npx vitest run src/components/Button.spec.tsx
|
|
47
|
+
|
|
48
|
+
# ✅ PREFERRED: Run tests matching a pattern
|
|
49
|
+
npx vitest run --grep "Button"
|
|
50
|
+
npx vitest run -t "should render"
|
|
51
|
+
|
|
52
|
+
# ✅ PREFERRED: Run specific describe block
|
|
53
|
+
npx vitest run --grep "Button > Rendering"
|
|
54
|
+
|
|
55
|
+
# ✅ Run tests in a directory
|
|
56
|
+
npx vitest run src/components/
|
|
57
|
+
|
|
58
|
+
# ✅ Run single test by name
|
|
59
|
+
npx vitest run -t "should disable button when loading"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Watch Mode (Background Testing)
|
|
63
|
+
|
|
64
|
+
Use watch mode for efficient iterative development:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# ✅ Watch mode - reruns on file changes
|
|
68
|
+
npx vitest
|
|
69
|
+
|
|
70
|
+
# ✅ Watch specific file
|
|
71
|
+
npx vitest src/components/Button.spec.tsx
|
|
72
|
+
|
|
73
|
+
# ✅ Watch tests matching pattern
|
|
74
|
+
npx vitest --grep "Button"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### File Naming
|
|
78
|
+
|
|
79
|
+
- Test files: `ComponentName.spec.tsx` (same directory as component)
|
|
80
|
+
|
|
81
|
+
## Test Structure Template
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
85
|
+
import Component from './index'
|
|
86
|
+
|
|
87
|
+
// ✅ Import real project components (DO NOT mock these)
|
|
88
|
+
// import Loading from '@/app/components/base/loading'
|
|
89
|
+
// import { ChildComponent } from './child-component'
|
|
90
|
+
|
|
91
|
+
// ✅ Mock external dependencies only
|
|
92
|
+
vi.mock('@/service/api')
|
|
93
|
+
vi.mock('next/navigation', () => ({
|
|
94
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
95
|
+
usePathname: () => '/test',
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
// Shared state for mocks (if needed)
|
|
99
|
+
let mockSharedState = false
|
|
100
|
+
|
|
101
|
+
describe('ComponentName', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
vi.clearAllMocks() // ✅ Reset mocks BEFORE each test
|
|
104
|
+
mockSharedState = false // ✅ Reset shared state
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Rendering tests (REQUIRED)
|
|
108
|
+
describe('Rendering', () => {
|
|
109
|
+
it('should render without crashing', () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
const props = { title: 'Test' }
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
render(<Component {...props} />)
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Props tests (REQUIRED)
|
|
122
|
+
describe('Props', () => {
|
|
123
|
+
it('should apply custom className', () => {
|
|
124
|
+
render(<Component className="custom" />)
|
|
125
|
+
expect(screen.getByRole('button')).toHaveClass('custom')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// User Interactions
|
|
130
|
+
describe('User Interactions', () => {
|
|
131
|
+
it('should handle click events', () => {
|
|
132
|
+
const handleClick = vi.fn()
|
|
133
|
+
render(<Component onClick={handleClick} />)
|
|
134
|
+
|
|
135
|
+
fireEvent.click(screen.getByRole('button'))
|
|
136
|
+
|
|
137
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Edge Cases (REQUIRED)
|
|
142
|
+
describe('Edge Cases', () => {
|
|
143
|
+
it('should handle null data', () => {
|
|
144
|
+
render(<Component data={null} />)
|
|
145
|
+
expect(screen.getByText(/no data/i)).toBeInTheDocument()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should handle empty array', () => {
|
|
149
|
+
render(<Component items={[]} />)
|
|
150
|
+
expect(screen.getByText(/empty/i)).toBeInTheDocument()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Testing Workflow (CRITICAL)
|
|
157
|
+
|
|
158
|
+
### ⚠️ Incremental Approach Required
|
|
159
|
+
|
|
160
|
+
**NEVER generate all test files at once.** For complex components or multi-file directories:
|
|
161
|
+
|
|
162
|
+
1. **Analyze & Plan**: List all files, order by complexity (simple → complex)
|
|
163
|
+
1. **Process ONE at a time**: Write test → Run test → Fix if needed → Next
|
|
164
|
+
1. **Verify before proceeding**: Do NOT continue to next file until current passes
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
For each file:
|
|
168
|
+
┌────────────────────────────────────────────────┐
|
|
169
|
+
│ 1. Write test │
|
|
170
|
+
│ 2. Run: npm run test-unit <file>.spec.tsx │
|
|
171
|
+
│ 3. PASS? → Mark complete, next file │
|
|
172
|
+
│ FAIL? → Fix first, then continue │
|
|
173
|
+
└────────────────────────────────────────────────┘
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Complexity-Based Order
|
|
177
|
+
|
|
178
|
+
Process in this order for multi-file testing:
|
|
179
|
+
|
|
180
|
+
1. 🟢 Utility functions (simplest)
|
|
181
|
+
2. 🟢 Custom hooks
|
|
182
|
+
3. 🟡 Simple components (presentational)
|
|
183
|
+
4. 🟡 Medium components (state, effects)
|
|
184
|
+
5. 🔴 Complex components (API, routing)
|
|
185
|
+
6. 🔴 Integration tests (index files - last)
|
|
186
|
+
|
|
187
|
+
### When to Refactor First
|
|
188
|
+
|
|
189
|
+
- **Medium Complexity**: Break into smaller pieces before testing
|
|
190
|
+
- **500+ lines**: Consider splitting before testing
|
|
191
|
+
- **Many dependencies**: Extract logic into hooks first
|
|
192
|
+
|
|
193
|
+
## Testing Strategy
|
|
194
|
+
|
|
195
|
+
### Path-Level Testing (Directory Testing)
|
|
196
|
+
|
|
197
|
+
When assigned to test a directory/path, test **ALL content** within that path:
|
|
198
|
+
|
|
199
|
+
- Test all components, hooks, utilities in the directory (not just `index` file)
|
|
200
|
+
- Use incremental approach: one file at a time, verify each before proceeding
|
|
201
|
+
- Goal: 100% coverage of ALL files in the directory
|
|
202
|
+
|
|
203
|
+
### Integration Testing First
|
|
204
|
+
|
|
205
|
+
**Prefer integration testing** when writing tests for a directory:
|
|
206
|
+
|
|
207
|
+
- ✅ **Import real project components** directly (including base components and siblings)
|
|
208
|
+
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
|
|
209
|
+
- ❌ **DO NOT mock** base components (`@/app/components/base/*`)
|
|
210
|
+
- ❌ **DO NOT mock** sibling/child components in the same directory
|
|
211
|
+
|
|
212
|
+
## Core Principles
|
|
213
|
+
|
|
214
|
+
### 1. AAA Pattern (Arrange-Act-Assert)
|
|
215
|
+
|
|
216
|
+
Every test should clearly separate:
|
|
217
|
+
|
|
218
|
+
- **Arrange**: Setup test data and render component
|
|
219
|
+
- **Act**: Perform user actions
|
|
220
|
+
- **Assert**: Verify expected outcomes
|
|
221
|
+
|
|
222
|
+
### 2. Black-Box Testing
|
|
223
|
+
|
|
224
|
+
- Test observable behavior, not implementation details
|
|
225
|
+
- Use semantic queries (getByRole, getByLabelText)
|
|
226
|
+
- Avoid testing internal state directly
|
|
227
|
+
- **Prefer pattern matching over hardcoded strings** in assertions:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// ❌ Avoid: hardcoded text assertions
|
|
231
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
232
|
+
|
|
233
|
+
// ✅ Better: role-based queries
|
|
234
|
+
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
235
|
+
|
|
236
|
+
// ✅ Better: pattern matching
|
|
237
|
+
expect(screen.getByText(/loading/i)).toBeInTheDocument()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 3. Single Behavior Per Test
|
|
241
|
+
|
|
242
|
+
Each test verifies ONE user-observable behavior:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// ✅ Good: One behavior
|
|
246
|
+
it('should disable button when loading', () => {
|
|
247
|
+
render(<Button loading />)
|
|
248
|
+
expect(screen.getByRole('button')).toBeDisabled()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// ❌ Bad: Multiple behaviors
|
|
252
|
+
it('should handle loading state', () => {
|
|
253
|
+
render(<Button loading />)
|
|
254
|
+
expect(screen.getByRole('button')).toBeDisabled()
|
|
255
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
256
|
+
expect(screen.getByRole('button')).toHaveClass('loading')
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 4. Semantic Naming
|
|
261
|
+
|
|
262
|
+
Use `should <behavior> when <condition>`:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
it('should show error message when validation fails')
|
|
266
|
+
it('should call onSubmit when form is valid')
|
|
267
|
+
it('should disable input when isReadOnly is true')
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Required Test Scenarios
|
|
271
|
+
|
|
272
|
+
### Always Required (All Components)
|
|
273
|
+
|
|
274
|
+
1. **Rendering**: Component renders without crashing
|
|
275
|
+
1. **Props**: Required props, optional props, default values
|
|
276
|
+
1. **Edge Cases**: null, undefined, empty values, boundary conditions
|
|
277
|
+
|
|
278
|
+
### Conditional (When Present)
|
|
279
|
+
|
|
280
|
+
| Feature | Test Focus |
|
|
281
|
+
| ----------------------- | ----------------------------------------- |
|
|
282
|
+
| `useState` | Initial state, transitions, cleanup |
|
|
283
|
+
| `useEffect` | Execution, dependencies, cleanup |
|
|
284
|
+
| Event handlers | All onClick, onChange, onSubmit, keyboard |
|
|
285
|
+
| API calls | Loading, success, error states |
|
|
286
|
+
| Routing | Navigation, params, query strings |
|
|
287
|
+
| `useCallback`/`useMemo` | Referential equality |
|
|
288
|
+
| Context | Provider values, consumer behavior |
|
|
289
|
+
| Forms | Validation, submission, error display |
|
|
290
|
+
|
|
291
|
+
## Coverage Goals (Per File)
|
|
292
|
+
|
|
293
|
+
For each test file generated, aim for:
|
|
294
|
+
|
|
295
|
+
- ✅ **100%** function coverage
|
|
296
|
+
- ✅ **100%** statement coverage
|
|
297
|
+
- ✅ **>95%** branch coverage
|
|
298
|
+
- ✅ **>95%** line coverage
|
|
299
|
+
|
|
300
|
+
> **Note**: For multi-file directories, process one file at a time with full coverage each. See `references/workflow.md`.
|
|
301
|
+
|
|
302
|
+
## Detailed Guides
|
|
303
|
+
|
|
304
|
+
For more detailed information, refer to:
|
|
305
|
+
|
|
306
|
+
- `references/workflow.md` - **Incremental testing workflow** (MUST READ for multi-file testing)
|
|
307
|
+
- `references/mocking.md` - Mock patterns and best practices
|
|
308
|
+
- `references/async-testing.md` - Async operations and API calls
|
|
309
|
+
- `references/common-patterns.md` - Frequently used testing patterns
|
|
310
|
+
- `references/checklist.md` - Test generation checklist and validation steps
|
|
311
|
+
|
|
312
|
+
### Project Configuration
|
|
313
|
+
|
|
314
|
+
- `vitest.config.ts` - Vitest configuration
|
|
315
|
+
- `vitest.setup.ts` - Test environment setup
|
|
316
|
+
- Modules are not mocked automatically. Global mocks live in `vitest.setup.ts` (for example `react-i18next`, `next/image`); mock other modules like `ky` or `mime` locally in test files.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Template for React Components
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS STRUCTURE?
|
|
5
|
+
* - Organized sections make tests easy to navigate and maintain
|
|
6
|
+
* - Mocks at top ensure consistent test isolation
|
|
7
|
+
* - Factory functions reduce duplication and improve readability
|
|
8
|
+
* - describe blocks group related scenarios for better debugging
|
|
9
|
+
*
|
|
10
|
+
* INSTRUCTIONS:
|
|
11
|
+
* 1. Replace `ComponentName` with your component name
|
|
12
|
+
* 2. Update import path
|
|
13
|
+
* 3. Add/remove test sections based on component features
|
|
14
|
+
* 4. Follow AAA pattern: Arrange → Act → Assert
|
|
15
|
+
*
|
|
16
|
+
* Analyze complexity first to identify required test scenarios
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
20
|
+
import userEvent from '@testing-library/user-event'
|
|
21
|
+
// import ComponentName from './index'
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Mocks
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// WHY: Mocks must be hoisted to top of file (Vitest requirement).
|
|
27
|
+
// They run BEFORE imports, so keep them before component imports.
|
|
28
|
+
|
|
29
|
+
// i18n (automatically mocked)
|
|
30
|
+
// WHY: Global mock in web/vitest.setup.ts is auto-loaded by Vitest setup
|
|
31
|
+
// The global mock provides: useTranslation, Trans, useMixedTranslation, useGetLanguage
|
|
32
|
+
// No explicit mock needed for most tests
|
|
33
|
+
//
|
|
34
|
+
// Override only if custom translations are required:
|
|
35
|
+
// import { createReactI18nextMock } from '@/test/i18n-mock'
|
|
36
|
+
// vi.mock('react-i18next', () => createReactI18nextMock({
|
|
37
|
+
// 'my.custom.key': 'Custom Translation',
|
|
38
|
+
// 'button.save': 'Save',
|
|
39
|
+
// }))
|
|
40
|
+
|
|
41
|
+
// Router (if component uses useRouter, usePathname, useSearchParams)
|
|
42
|
+
// WHY: Isolates tests from Next.js routing, enables testing navigation behavior
|
|
43
|
+
// const mockPush = vi.fn()
|
|
44
|
+
// vi.mock('next/navigation', () => ({
|
|
45
|
+
// useRouter: () => ({ push: mockPush }),
|
|
46
|
+
// usePathname: () => '/test-path',
|
|
47
|
+
// }))
|
|
48
|
+
|
|
49
|
+
// API services (if component fetches data)
|
|
50
|
+
// WHY: Prevents real network calls, enables testing all states (loading/success/error)
|
|
51
|
+
// vi.mock('@/service/api')
|
|
52
|
+
// import * as api from '@/service/api'
|
|
53
|
+
// const mockedApi = vi.mocked(api)
|
|
54
|
+
|
|
55
|
+
// Shared mock state (for portal/dropdown components)
|
|
56
|
+
// WHY: Portal components like PortalToFollowElem need shared state between
|
|
57
|
+
// parent and child mocks to correctly simulate open/close behavior
|
|
58
|
+
// let mockOpenState = false
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Test Data Factories
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// WHY FACTORIES?
|
|
64
|
+
// - Avoid hard-coded test data scattered across tests
|
|
65
|
+
// - Easy to create variations with overrides
|
|
66
|
+
// - Type-safe when using actual types from source
|
|
67
|
+
// - Single source of truth for default test values
|
|
68
|
+
|
|
69
|
+
// const createMockProps = (overrides = {}) => ({
|
|
70
|
+
// // Default props that make component render successfully
|
|
71
|
+
// ...overrides,
|
|
72
|
+
// })
|
|
73
|
+
|
|
74
|
+
// const createMockItem = (overrides = {}) => ({
|
|
75
|
+
// id: 'item-1',
|
|
76
|
+
// name: 'Test Item',
|
|
77
|
+
// ...overrides,
|
|
78
|
+
// })
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Test Helpers
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
// const renderComponent = (props = {}) => {
|
|
85
|
+
// return render(<ComponentName {...createMockProps(props)} />)
|
|
86
|
+
// }
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Tests
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
describe('ComponentName', () => {
|
|
93
|
+
// WHY beforeEach with clearAllMocks?
|
|
94
|
+
// - Ensures each test starts with clean slate
|
|
95
|
+
// - Prevents mock call history from leaking between tests
|
|
96
|
+
// - MUST be beforeEach (not afterEach) to reset BEFORE assertions like toHaveBeenCalledTimes
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
vi.clearAllMocks()
|
|
99
|
+
// Reset shared mock state if used (CRITICAL for portal/dropdown tests)
|
|
100
|
+
// mockOpenState = false
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// --------------------------------------------------------------------------
|
|
104
|
+
// Rendering Tests (REQUIRED - Every component MUST have these)
|
|
105
|
+
// --------------------------------------------------------------------------
|
|
106
|
+
// WHY: Catches import errors, missing providers, and basic render issues
|
|
107
|
+
describe('Rendering', () => {
|
|
108
|
+
it('should render without crashing', () => {
|
|
109
|
+
// Arrange - Setup data and mocks
|
|
110
|
+
// const props = createMockProps()
|
|
111
|
+
|
|
112
|
+
// Act - Render the component
|
|
113
|
+
// render(<ComponentName {...props} />)
|
|
114
|
+
|
|
115
|
+
// Assert - Verify expected output
|
|
116
|
+
// Prefer getByRole for accessibility; it's what users "see"
|
|
117
|
+
// expect(screen.getByRole('...')).toBeInTheDocument()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should render with default props', () => {
|
|
121
|
+
// WHY: Verifies component works without optional props
|
|
122
|
+
// render(<ComponentName />)
|
|
123
|
+
// expect(screen.getByText('...')).toBeInTheDocument()
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// --------------------------------------------------------------------------
|
|
128
|
+
// Props Tests (REQUIRED - Every component MUST test prop behavior)
|
|
129
|
+
// --------------------------------------------------------------------------
|
|
130
|
+
// WHY: Props are the component's API contract. Test them thoroughly.
|
|
131
|
+
describe('Props', () => {
|
|
132
|
+
it('should apply custom className', () => {
|
|
133
|
+
// WHY: Common pattern - components should merge custom classes
|
|
134
|
+
// render(<ComponentName className="custom-class" />)
|
|
135
|
+
// expect(screen.getByTestId('component')).toHaveClass('custom-class')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should use default values for optional props', () => {
|
|
139
|
+
// WHY: Verifies TypeScript defaults work at runtime
|
|
140
|
+
// render(<ComponentName />)
|
|
141
|
+
// expect(screen.getByRole('...')).toHaveAttribute('...', 'default-value')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// --------------------------------------------------------------------------
|
|
146
|
+
// User Interactions (if component has event handlers - on*, handle*)
|
|
147
|
+
// --------------------------------------------------------------------------
|
|
148
|
+
// WHY: Event handlers are core functionality. Test from user's perspective.
|
|
149
|
+
describe('User Interactions', () => {
|
|
150
|
+
it('should call onClick when clicked', async () => {
|
|
151
|
+
// WHY userEvent over fireEvent?
|
|
152
|
+
// - userEvent simulates real user behavior (focus, hover, then click)
|
|
153
|
+
// - fireEvent is lower-level, doesn't trigger all browser events
|
|
154
|
+
// const user = userEvent.setup()
|
|
155
|
+
// const handleClick = vi.fn()
|
|
156
|
+
// render(<ComponentName onClick={handleClick} />)
|
|
157
|
+
//
|
|
158
|
+
// await user.click(screen.getByRole('button'))
|
|
159
|
+
//
|
|
160
|
+
// expect(handleClick).toHaveBeenCalledTimes(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should call onChange when value changes', async () => {
|
|
164
|
+
// const user = userEvent.setup()
|
|
165
|
+
// const handleChange = vi.fn()
|
|
166
|
+
// render(<ComponentName onChange={handleChange} />)
|
|
167
|
+
//
|
|
168
|
+
// await user.type(screen.getByRole('textbox'), 'new value')
|
|
169
|
+
//
|
|
170
|
+
// expect(handleChange).toHaveBeenCalled()
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// --------------------------------------------------------------------------
|
|
175
|
+
// State Management (if component uses useState/useReducer)
|
|
176
|
+
// --------------------------------------------------------------------------
|
|
177
|
+
// WHY: Test state through observable UI changes, not internal state values
|
|
178
|
+
describe('State Management', () => {
|
|
179
|
+
it('should update state on interaction', async () => {
|
|
180
|
+
// WHY test via UI, not state?
|
|
181
|
+
// - State is implementation detail; UI is what users see
|
|
182
|
+
// - If UI works correctly, state must be correct
|
|
183
|
+
// const user = userEvent.setup()
|
|
184
|
+
// render(<ComponentName />)
|
|
185
|
+
//
|
|
186
|
+
// // Initial state - verify what user sees
|
|
187
|
+
// expect(screen.getByText('Initial')).toBeInTheDocument()
|
|
188
|
+
//
|
|
189
|
+
// // Trigger state change via user action
|
|
190
|
+
// await user.click(screen.getByRole('button'))
|
|
191
|
+
//
|
|
192
|
+
// // New state - verify UI updated
|
|
193
|
+
// expect(screen.getByText('Updated')).toBeInTheDocument()
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// --------------------------------------------------------------------------
|
|
198
|
+
// Async Operations (if component fetches data - useQuery, fetch)
|
|
199
|
+
// --------------------------------------------------------------------------
|
|
200
|
+
// WHY: Async operations have 3 states users experience: loading, success, error
|
|
201
|
+
describe('Async Operations', () => {
|
|
202
|
+
it('should show loading state', () => {
|
|
203
|
+
// WHY never-resolving promise?
|
|
204
|
+
// - Keeps component in loading state for assertion
|
|
205
|
+
// - Alternative: use fake timers
|
|
206
|
+
// mockedApi.fetchData.mockImplementation(() => new Promise(() => {}))
|
|
207
|
+
// render(<ComponentName />)
|
|
208
|
+
//
|
|
209
|
+
// expect(screen.getByText(/loading/i)).toBeInTheDocument()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should show data on success', async () => {
|
|
213
|
+
// WHY waitFor?
|
|
214
|
+
// - Component updates asynchronously after fetch resolves
|
|
215
|
+
// - waitFor retries assertion until it passes or times out
|
|
216
|
+
// mockedApi.fetchData.mockResolvedValue({ items: ['Item 1'] })
|
|
217
|
+
// render(<ComponentName />)
|
|
218
|
+
//
|
|
219
|
+
// await waitFor(() => {
|
|
220
|
+
// expect(screen.getByText('Item 1')).toBeInTheDocument()
|
|
221
|
+
// })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('should show error on failure', async () => {
|
|
225
|
+
// mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
|
|
226
|
+
// render(<ComponentName />)
|
|
227
|
+
//
|
|
228
|
+
// await waitFor(() => {
|
|
229
|
+
// expect(screen.getByText(/error/i)).toBeInTheDocument()
|
|
230
|
+
// })
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// --------------------------------------------------------------------------
|
|
235
|
+
// Edge Cases (REQUIRED - Every component MUST handle edge cases)
|
|
236
|
+
// --------------------------------------------------------------------------
|
|
237
|
+
// WHY: Real-world data is messy. Components must handle:
|
|
238
|
+
// - Null/undefined from API failures or optional fields
|
|
239
|
+
// - Empty arrays/strings from user clearing data
|
|
240
|
+
// - Boundary values (0, MAX_INT, special characters)
|
|
241
|
+
describe('Edge Cases', () => {
|
|
242
|
+
it('should handle null value', () => {
|
|
243
|
+
// WHY test null specifically?
|
|
244
|
+
// - API might return null for missing data
|
|
245
|
+
// - Prevents "Cannot read property of null" in production
|
|
246
|
+
// render(<ComponentName value={null} />)
|
|
247
|
+
// expect(screen.getByText(/no data/i)).toBeInTheDocument()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should handle undefined value', () => {
|
|
251
|
+
// WHY test undefined separately from null?
|
|
252
|
+
// - TypeScript treats them differently
|
|
253
|
+
// - Optional props are undefined, not null
|
|
254
|
+
// render(<ComponentName value={undefined} />)
|
|
255
|
+
// expect(screen.getByText(/no data/i)).toBeInTheDocument()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should handle empty array', () => {
|
|
259
|
+
// WHY: Empty state often needs special UI (e.g., "No items yet")
|
|
260
|
+
// render(<ComponentName items={[]} />)
|
|
261
|
+
// expect(screen.getByText(/empty/i)).toBeInTheDocument()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should handle empty string', () => {
|
|
265
|
+
// WHY: Empty strings are truthy in JS but visually empty
|
|
266
|
+
// render(<ComponentName text="" />)
|
|
267
|
+
// expect(screen.getByText(/placeholder/i)).toBeInTheDocument()
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// --------------------------------------------------------------------------
|
|
272
|
+
// Accessibility (optional but recommended)
|
|
273
|
+
// --------------------------------------------------------------------------
|
|
274
|
+
// WHY: Accessibility is important for users
|
|
275
|
+
describe('Accessibility', () => {
|
|
276
|
+
it('should have accessible name', () => {
|
|
277
|
+
// WHY getByRole with name?
|
|
278
|
+
// - Tests that screen readers can identify the element
|
|
279
|
+
// - Enforces proper labeling practices
|
|
280
|
+
// render(<ComponentName label="Test Label" />)
|
|
281
|
+
// expect(screen.getByRole('button', { name: /test label/i })).toBeInTheDocument()
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should support keyboard navigation', async () => {
|
|
285
|
+
// WHY: Some users can't use a mouse
|
|
286
|
+
// const user = userEvent.setup()
|
|
287
|
+
// render(<ComponentName />)
|
|
288
|
+
//
|
|
289
|
+
// await user.tab()
|
|
290
|
+
// expect(screen.getByRole('button')).toHaveFocus()
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|