@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,382 @@
|
|
|
1
|
+
# 1.4 Error Handling
|
|
2
|
+
|
|
3
|
+
Error handling is critical - test it as thoroughly as the happy path. Good error handling tests verify that your code fails gracefully and provides meaningful feedback.
|
|
4
|
+
|
|
5
|
+
## Basic Exception Testing
|
|
6
|
+
|
|
7
|
+
Use `toThrow` to verify exceptions are thrown for invalid inputs.
|
|
8
|
+
|
|
9
|
+
**✅ Correct: testing specific error messages**
|
|
10
|
+
```ts
|
|
11
|
+
describe('divide', () => {
|
|
12
|
+
it('should throw TypeError for division by zero', () => {
|
|
13
|
+
expect(() => divide(10, 0)).toThrow(TypeError);
|
|
14
|
+
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should throw TypeError for non-numeric inputs', () => {
|
|
18
|
+
expect(() => divide('10' as any, 5)).toThrow(TypeError);
|
|
19
|
+
expect(() => divide('10' as any, 5)).toThrow('Arguments must be numbers');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**❌ Incorrect: not testing error specifics**
|
|
25
|
+
```ts
|
|
26
|
+
it('should throw for division by zero', () => {
|
|
27
|
+
expect(() => divide(10, 0)).toThrow(); // Which error? What message?
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
*Why?* Testing only that an error is thrown doesn't verify you're throwing the right error with the right message.
|
|
31
|
+
|
|
32
|
+
## Testing Error Types
|
|
33
|
+
|
|
34
|
+
Always verify the error type and message, not just that an error was thrown.
|
|
35
|
+
|
|
36
|
+
**✅ Correct: specific error type and message**
|
|
37
|
+
```ts
|
|
38
|
+
class ValidationError extends Error {
|
|
39
|
+
constructor(message: string) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'ValidationError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('validateUser', () => {
|
|
46
|
+
it('should throw ValidationError for missing email', () => {
|
|
47
|
+
const invalidUser = { name: 'John' };
|
|
48
|
+
|
|
49
|
+
expect(() => validateUser(invalidUser)).toThrow(ValidationError);
|
|
50
|
+
expect(() => validateUser(invalidUser)).toThrow('Email is required');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should throw ValidationError for invalid email format', () => {
|
|
54
|
+
const invalidUser = { name: 'John', email: 'not-an-email' };
|
|
55
|
+
|
|
56
|
+
expect(() => validateUser(invalidUser)).toThrow(ValidationError);
|
|
57
|
+
expect(() => validateUser(invalidUser)).toThrow('Invalid email format');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**❌ Incorrect: generic error testing**
|
|
63
|
+
```ts
|
|
64
|
+
it('should throw error for invalid user', () => {
|
|
65
|
+
expect(() => validateUser({ name: 'John' })).toThrow(Error);
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
*Why?* Any error will pass this test, including unexpected errors from bugs in your code.
|
|
69
|
+
|
|
70
|
+
## Async Error Testing
|
|
71
|
+
|
|
72
|
+
Use `rejects` matchers for async functions that throw.
|
|
73
|
+
|
|
74
|
+
**✅ Correct: async error testing**
|
|
75
|
+
```ts
|
|
76
|
+
describe('fetchUser', () => {
|
|
77
|
+
it('should reject with error for non-existent user', async () => {
|
|
78
|
+
await expect(fetchUser('invalid-id')).rejects.toThrow('User not found');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reject with NotFoundError', async () => {
|
|
82
|
+
await expect(fetchUser('invalid-id')).rejects.toThrow(NotFoundError);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**❌ Incorrect: improper async error testing**
|
|
88
|
+
```ts
|
|
89
|
+
it('should throw for non-existent user', async () => {
|
|
90
|
+
try {
|
|
91
|
+
await fetchUser('invalid-id');
|
|
92
|
+
// Missing fail() here - test will pass if no error!
|
|
93
|
+
} catch (error) {
|
|
94
|
+
expect(error.message).toEqual('User not found');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
*Why?* If the function doesn't throw, the test will pass incorrectly. Use `rejects` matcher instead.
|
|
99
|
+
|
|
100
|
+
## Negative Testing
|
|
101
|
+
|
|
102
|
+
Test boundary conditions and invalid inputs comprehensively.
|
|
103
|
+
|
|
104
|
+
**✅ Correct: comprehensive negative testing**
|
|
105
|
+
```ts
|
|
106
|
+
describe('calculateAge', () => {
|
|
107
|
+
it.each([
|
|
108
|
+
{ birthdate: 'not-a-date', error: 'Invalid date format' },
|
|
109
|
+
{ birthdate: new Date('2099-01-01'), error: 'Birth date cannot be in the future' },
|
|
110
|
+
{ birthdate: null, error: 'Birth date is required' },
|
|
111
|
+
{ birthdate: undefined, error: 'Birth date is required' },
|
|
112
|
+
])('should throw for invalid birthdate: $birthdate',
|
|
113
|
+
({ birthdate, error }) => {
|
|
114
|
+
expect(() => calculateAge(birthdate as any)).toThrow(error);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
it('should calculate age for valid birthdate', () => {
|
|
119
|
+
const birthdate = new Date('1990-01-01');
|
|
120
|
+
const age = calculateAge(birthdate);
|
|
121
|
+
expect(age).toBeGreaterThan(0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Fault Injection
|
|
127
|
+
|
|
128
|
+
Simulate system failures to test error handling and resilience.
|
|
129
|
+
|
|
130
|
+
**✅ Correct: simulating network failures**
|
|
131
|
+
```ts
|
|
132
|
+
describe('DataService', () => {
|
|
133
|
+
it('should retry on network failure', async () => {
|
|
134
|
+
const apiClient = {
|
|
135
|
+
fetch: vi.fn()
|
|
136
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
137
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
138
|
+
.mockResolvedValueOnce({ data: 'success' }),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const service = new DataService(apiClient);
|
|
142
|
+
const result = await service.fetchWithRetry('/api/data');
|
|
143
|
+
|
|
144
|
+
expect(result).toEqual({ data: 'success' });
|
|
145
|
+
expect(apiClient.fetch).toHaveBeenCalledTimes(3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should fail after max retries', async () => {
|
|
149
|
+
const apiClient = {
|
|
150
|
+
fetch: vi.fn().mockRejectedValue(new Error('Network error')),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const service = new DataService(apiClient);
|
|
154
|
+
|
|
155
|
+
await expect(service.fetchWithRetry('/api/data'))
|
|
156
|
+
.rejects.toThrow('Max retries exceeded');
|
|
157
|
+
expect(apiClient.fetch).toHaveBeenCalledTimes(3);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**✅ Correct: simulating database errors**
|
|
163
|
+
```ts
|
|
164
|
+
describe('UserRepository', () => {
|
|
165
|
+
it('should handle database connection errors', async () => {
|
|
166
|
+
const db = {
|
|
167
|
+
query: vi.fn().mockRejectedValue(new Error('Connection lost')),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const repo = new UserRepository(db);
|
|
171
|
+
|
|
172
|
+
await expect(repo.findById('user-123'))
|
|
173
|
+
.rejects.toThrow('Database error: Connection lost');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should handle query timeouts', async () => {
|
|
177
|
+
const db = {
|
|
178
|
+
query: vi.fn().mockRejectedValue(new Error('Query timeout')),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const repo = new UserRepository(db);
|
|
182
|
+
|
|
183
|
+
await expect(repo.findById('user-123'))
|
|
184
|
+
.rejects.toThrow('Query timeout');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Recovery Testing
|
|
190
|
+
|
|
191
|
+
Verify that systems can recover from failures and return to normal operation.
|
|
192
|
+
|
|
193
|
+
**✅ Correct: testing circuit breaker recovery**
|
|
194
|
+
```ts
|
|
195
|
+
describe('CircuitBreaker', () => {
|
|
196
|
+
it('should open circuit after threshold failures', async () => {
|
|
197
|
+
const failingService = vi.fn().mockRejectedValue(new Error('Service down'));
|
|
198
|
+
const breaker = new CircuitBreaker(failingService, { threshold: 3 });
|
|
199
|
+
|
|
200
|
+
// Trigger failures to open circuit
|
|
201
|
+
for (let i = 0; i < 3; i++) {
|
|
202
|
+
await expect(breaker.call()).rejects.toThrow('Service down');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Circuit should now be open
|
|
206
|
+
await expect(breaker.call()).rejects.toThrow('Circuit breaker is open');
|
|
207
|
+
expect(failingService).toHaveBeenCalledTimes(3); // No more calls
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should close circuit after recovery period', async () => {
|
|
211
|
+
vi.useFakeTimers();
|
|
212
|
+
|
|
213
|
+
const service = vi.fn()
|
|
214
|
+
.mockRejectedValueOnce(new Error('Service down'))
|
|
215
|
+
.mockResolvedValue('success');
|
|
216
|
+
|
|
217
|
+
const breaker = new CircuitBreaker(service, {
|
|
218
|
+
threshold: 1,
|
|
219
|
+
resetTimeout: 5000,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Open circuit
|
|
223
|
+
await expect(breaker.call()).rejects.toThrow('Service down');
|
|
224
|
+
await expect(breaker.call()).rejects.toThrow('Circuit breaker is open');
|
|
225
|
+
|
|
226
|
+
// Wait for reset timeout
|
|
227
|
+
vi.advanceTimersByTime(5000);
|
|
228
|
+
|
|
229
|
+
// Circuit should allow retry
|
|
230
|
+
const result = await breaker.call();
|
|
231
|
+
expect(result).toEqual('success');
|
|
232
|
+
|
|
233
|
+
vi.useRealTimers();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Error Guessing
|
|
239
|
+
|
|
240
|
+
Anticipate edge cases based on domain knowledge.
|
|
241
|
+
|
|
242
|
+
**✅ Correct: testing common edge cases**
|
|
243
|
+
```ts
|
|
244
|
+
describe('parseJSON', () => {
|
|
245
|
+
it.each([
|
|
246
|
+
{ input: '', description: 'empty string' },
|
|
247
|
+
{ input: 'null', description: 'null value' },
|
|
248
|
+
{ input: 'undefined', description: 'undefined as string' },
|
|
249
|
+
{ input: '{broken json', description: 'malformed JSON' },
|
|
250
|
+
{ input: '{"key": undefined}', description: 'undefined in object' },
|
|
251
|
+
{ input: 'NaN', description: 'NaN value' },
|
|
252
|
+
{ input: '{\"key\": Infinity}', description: 'Infinity value' },
|
|
253
|
+
])('should handle $description gracefully', ({ input }) => {
|
|
254
|
+
expect(() => parseJSON(input)).toThrow(SyntaxError);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('processFile', () => {
|
|
259
|
+
it.each([
|
|
260
|
+
{ filename: '', error: 'Filename cannot be empty' },
|
|
261
|
+
{ filename: '../../../etc/passwd', error: 'Invalid filename' },
|
|
262
|
+
{ filename: 'file\x00name', error: 'Invalid characters' },
|
|
263
|
+
{ filename: '.'.repeat(300), error: 'Filename too long' },
|
|
264
|
+
])('should reject dangerous filename: "$filename"',
|
|
265
|
+
({ filename, error }) => {
|
|
266
|
+
expect(() => processFile(filename)).toThrow(error);
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Testing Error Boundaries (React)
|
|
273
|
+
|
|
274
|
+
For React components, test error boundaries handle errors gracefully.
|
|
275
|
+
|
|
276
|
+
**✅ Correct: testing error boundary**
|
|
277
|
+
```ts
|
|
278
|
+
describe('ErrorBoundary', () => {
|
|
279
|
+
it('should catch errors and display fallback UI', () => {
|
|
280
|
+
const ThrowError = () => {
|
|
281
|
+
throw new Error('Test error');
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const { getByText } = render(
|
|
285
|
+
<ErrorBoundary fallback={<div>Error occurred</div>}>
|
|
286
|
+
<ThrowError />
|
|
287
|
+
</ErrorBoundary>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(getByText('Error occurred')).toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should log error to error reporting service', () => {
|
|
294
|
+
const errorLogger = vi.fn();
|
|
295
|
+
const ThrowError = () => {
|
|
296
|
+
throw new Error('Test error');
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
render(
|
|
300
|
+
<ErrorBoundary onError={errorLogger}>
|
|
301
|
+
<ThrowError />
|
|
302
|
+
</ErrorBoundary>
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(errorLogger).toHaveBeenCalledWith(
|
|
306
|
+
expect.objectContaining({
|
|
307
|
+
message: 'Test error',
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Validation Errors
|
|
315
|
+
|
|
316
|
+
Test comprehensive input validation.
|
|
317
|
+
|
|
318
|
+
**✅ Correct: thorough validation testing**
|
|
319
|
+
```ts
|
|
320
|
+
describe('createOrder', () => {
|
|
321
|
+
it('should validate all required fields', () => {
|
|
322
|
+
const invalidOrders = [
|
|
323
|
+
{ error: 'Customer ID is required' },
|
|
324
|
+
{ customerId: 'c1', error: 'Items array cannot be empty' },
|
|
325
|
+
{ customerId: 'c1', items: [], error: 'Items array cannot be empty' },
|
|
326
|
+
{ customerId: '', items: [{ id: 1 }], error: 'Customer ID is required' },
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
invalidOrders.forEach(({ error, ...order }) => {
|
|
330
|
+
expect(() => createOrder(order as any)).toThrow(error);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should validate item quantities', () => {
|
|
335
|
+
const order = {
|
|
336
|
+
customerId: 'c1',
|
|
337
|
+
items: [{ id: 1, quantity: -1 }],
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
expect(() => createOrder(order)).toThrow('Quantity must be positive');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should validate item prices', () => {
|
|
344
|
+
const order = {
|
|
345
|
+
customerId: 'c1',
|
|
346
|
+
items: [{ id: 1, quantity: 1, price: -10 }],
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
expect(() => createOrder(order)).toThrow('Price cannot be negative');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Error Messages Quality
|
|
355
|
+
|
|
356
|
+
Test that error messages are helpful and actionable.
|
|
357
|
+
|
|
358
|
+
**✅ Correct: descriptive error messages**
|
|
359
|
+
```ts
|
|
360
|
+
describe('processPayment', () => {
|
|
361
|
+
it('should provide helpful error for insufficient funds', async () => {
|
|
362
|
+
const payment = { amount: 1000, accountBalance: 100 };
|
|
363
|
+
|
|
364
|
+
await expect(processPayment(payment))
|
|
365
|
+
.rejects.toThrow('Insufficient funds: balance $100, required $1000');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should include transaction ID in error messages', async () => {
|
|
369
|
+
const payment = { amount: 1000, transactionId: 'txn-123' };
|
|
370
|
+
|
|
371
|
+
await expect(processPayment(payment))
|
|
372
|
+
.rejects.toThrow(expect.stringContaining('txn-123'));
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**❌ Incorrect: vague error messages**
|
|
378
|
+
```ts
|
|
379
|
+
it('should throw error for invalid payment', async () => {
|
|
380
|
+
await expect(processPayment(payment)).rejects.toThrow('Error'); // Not helpful!
|
|
381
|
+
});
|
|
382
|
+
```
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# 1.1 Organization
|
|
2
|
+
|
|
3
|
+
## File Placement and Naming
|
|
4
|
+
|
|
5
|
+
Place test files next to their implementation for easy discovery and maintenance.
|
|
6
|
+
|
|
7
|
+
**❌ Incorrect: separate test directory**
|
|
8
|
+
```
|
|
9
|
+
src/
|
|
10
|
+
components/
|
|
11
|
+
button.tsx
|
|
12
|
+
utils/
|
|
13
|
+
formatters.ts
|
|
14
|
+
tests/
|
|
15
|
+
components/
|
|
16
|
+
button.test.tsx
|
|
17
|
+
utils/
|
|
18
|
+
formatters.test.ts
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**✅ Correct: co-located test files**
|
|
22
|
+
```
|
|
23
|
+
src/
|
|
24
|
+
components/
|
|
25
|
+
button.tsx
|
|
26
|
+
button.test.tsx
|
|
27
|
+
utils/
|
|
28
|
+
formatters.ts
|
|
29
|
+
formatters.test.ts
|
|
30
|
+
services/
|
|
31
|
+
api.ts
|
|
32
|
+
api.test.ts
|
|
33
|
+
```
|
|
34
|
+
*Why?* Separate test directories make it harder to find tests, keep them in sync with implementation, and increase cognitive overhead.
|
|
35
|
+
|
|
36
|
+
## Naming Conventions
|
|
37
|
+
|
|
38
|
+
Use consistent naming patterns for test files:
|
|
39
|
+
- `*.test.ts` or `*.spec.ts` for tests
|
|
40
|
+
|
|
41
|
+
**❌ Incorrect: vague or inconsistent names**
|
|
42
|
+
```
|
|
43
|
+
test1.ts
|
|
44
|
+
user_tests.ts // snake_case instead of kebab-case
|
|
45
|
+
payment.spec.js // mixing .js and .ts
|
|
46
|
+
authTest.ts // camelCase instead of kebab-case
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**✅ Correct: descriptive test file names**
|
|
50
|
+
```
|
|
51
|
+
user-service.test.ts
|
|
52
|
+
payment-processor.test.ts
|
|
53
|
+
auth.integration.test.ts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## One Test File Per Module
|
|
57
|
+
|
|
58
|
+
Each module, component, or class should have exactly one corresponding test file.
|
|
59
|
+
|
|
60
|
+
**❌ Incorrect: multiple test files for one module**
|
|
61
|
+
```
|
|
62
|
+
user-service.test.ts
|
|
63
|
+
user-service.get-user.test.ts
|
|
64
|
+
user-service.update-user.test.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**✅ Correct: one-to-one mapping**
|
|
68
|
+
```ts
|
|
69
|
+
// user-service.ts
|
|
70
|
+
export class UserService {
|
|
71
|
+
getUser(id: string) { /* ... */ }
|
|
72
|
+
updateUser(id: string, data: UserData) { /* ... */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// user-service.test.ts
|
|
76
|
+
describe('UserService', () => {
|
|
77
|
+
describe('getUser', () => { /* ... */ });
|
|
78
|
+
describe('updateUser', () => { /* ... */ });
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
*Why?* Multiple test files fragment related tests, making it harder to understand the full behavior of a module.
|
|
82
|
+
|
|
83
|
+
## Shared Test Utilities
|
|
84
|
+
|
|
85
|
+
Store reusable test setup, fixtures, and helpers in dedicated directories.
|
|
86
|
+
|
|
87
|
+
**✅ Correct: organized test utilities**
|
|
88
|
+
```
|
|
89
|
+
src/
|
|
90
|
+
test-utils/
|
|
91
|
+
setup.ts // Global test configuration
|
|
92
|
+
factories.ts // Test data factories
|
|
93
|
+
matchers.ts // Custom matchers
|
|
94
|
+
fixtures/
|
|
95
|
+
users.json // Test data
|
|
96
|
+
products.json
|
|
97
|
+
__tests__/
|
|
98
|
+
integration/ // Shared integration test setup
|
|
99
|
+
db-setup.ts
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Example test utility:**
|
|
103
|
+
```ts
|
|
104
|
+
// test-utils/factories.ts
|
|
105
|
+
export function createMockUser(overrides?: Partial<User>): User {
|
|
106
|
+
return {
|
|
107
|
+
id: 'test-user-id',
|
|
108
|
+
name: 'Test User',
|
|
109
|
+
email: 'test@example.com',
|
|
110
|
+
role: 'user',
|
|
111
|
+
...overrides,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// user-service.test.ts
|
|
116
|
+
import { createMockUser } from '../test-utils/factories';
|
|
117
|
+
|
|
118
|
+
it('should update user email', () => {
|
|
119
|
+
const user = createMockUser({ email: 'old@example.com' });
|
|
120
|
+
// ...
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Describe Block Organization
|
|
125
|
+
|
|
126
|
+
Use flat, focused describe blocks to group related tests.
|
|
127
|
+
|
|
128
|
+
**❌ Incorrect: deep nesting**
|
|
129
|
+
```ts
|
|
130
|
+
describe('ShoppingCart', () => {
|
|
131
|
+
describe('when cart is empty', () => {
|
|
132
|
+
describe('addItem', () => {
|
|
133
|
+
describe('with valid item', () => {
|
|
134
|
+
it('should add item', () => { /* ... */ });
|
|
135
|
+
});
|
|
136
|
+
describe('with invalid item', () => {
|
|
137
|
+
it('should throw', () => { /* ... */ });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('when cart has items', () => {
|
|
142
|
+
describe('addItem', () => {
|
|
143
|
+
// deeply nested...
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**✅ Correct: flat structure, grouped by method**
|
|
150
|
+
```ts
|
|
151
|
+
describe('ShoppingCart', () => {
|
|
152
|
+
describe('addItem', () => {
|
|
153
|
+
it('should add item to empty cart', () => { /* ... */ });
|
|
154
|
+
it('should increment quantity when adding existing item', () => { /* ... */ });
|
|
155
|
+
it('should throw when adding invalid item', () => { /* ... */ });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('removeItem', () => {
|
|
159
|
+
it('should remove item from cart', () => { /* ... */ });
|
|
160
|
+
it('should throw when removing non-existent item', () => { /* ... */ });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('calculateTotal', () => {
|
|
164
|
+
it('should return 0 for empty cart', () => { /* ... */ });
|
|
165
|
+
it('should sum all item prices', () => { /* ... */ });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
*Why?* Deep nesting makes tests harder to read and adds unnecessary indentation. Put context in the test name instead.
|
|
170
|
+
|
|
171
|
+
## Setup and Teardown
|
|
172
|
+
|
|
173
|
+
Use `beforeEach` and `afterEach` for common setup, but keep tests independent.
|
|
174
|
+
|
|
175
|
+
**❌ Incorrect: shared mutable state between tests**
|
|
176
|
+
```ts
|
|
177
|
+
describe('DatabaseService', () => {
|
|
178
|
+
const db = createTestDatabase(); // Shared across all tests!
|
|
179
|
+
|
|
180
|
+
it('should insert record', async () => {
|
|
181
|
+
await db.insert({ name: 'Test' });
|
|
182
|
+
// ...
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should update record', async () => {
|
|
186
|
+
// Depends on previous test's data!
|
|
187
|
+
await db.update(1, { name: 'Updated' });
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**✅ Correct: clean setup per test**
|
|
193
|
+
```ts
|
|
194
|
+
describe('DatabaseService', () => {
|
|
195
|
+
let db: Database;
|
|
196
|
+
|
|
197
|
+
beforeEach(async () => {
|
|
198
|
+
db = await createTestDatabase();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterEach(async () => {
|
|
202
|
+
await db.close();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should insert record', async () => {
|
|
206
|
+
await db.insert({ name: 'Test' });
|
|
207
|
+
const records = await db.query('SELECT * FROM users');
|
|
208
|
+
expect(records).toHaveLength(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
*Why?* Tests that share state are fragile and order-dependent. Each test should be fully independent.
|