ai-flow-dev 1.0.1
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/LICENSE +21 -0
- package/README.md +408 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +791 -0
- package/dist/cli.js.map +1 -0
- package/dist/fs-utils.d.ts +2 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +46 -0
- package/dist/fs-utils.js.map +1 -0
- package/package.json +71 -0
- package/prompts/backend/flow-dev-feature.md +1318 -0
- package/prompts/backend/flow-dev-fix.md +903 -0
- package/prompts/backend/flow-dev-refactor.md +715 -0
- package/prompts/backend/flow-dev-review.md +401 -0
- package/prompts/backend/flow-dev-work.md +1129 -0
- package/prompts/backend/flow-docs-gen-phase-0.md +1840 -0
- package/prompts/backend/flow-docs-gen-phase-1.md +435 -0
- package/prompts/backend/flow-docs-gen-phase-2.md +460 -0
- package/prompts/backend/flow-docs-gen-phase-3.md +684 -0
- package/prompts/backend/flow-docs-gen-phase-4.md +516 -0
- package/prompts/backend/flow-docs-gen-phase-5.md +637 -0
- package/prompts/backend/flow-docs-gen-phase-6.md +465 -0
- package/prompts/backend/flow-docs-gen-phase-7.md +1207 -0
- package/prompts/backend/flow-docs-gen.md +820 -0
- package/prompts/backend/flow-docs-sync.md +526 -0
- package/prompts/backend/flow-project-init.md +248 -0
- package/prompts/backend/flow-project-roadmap.md +1159 -0
- package/prompts/frontend/flow-docs-gen-phase-0.md +494 -0
- package/prompts/frontend/flow-docs-gen-phase-1.md +449 -0
- package/prompts/frontend/flow-docs-gen-phase-2.md +983 -0
- package/prompts/frontend/flow-docs-gen-phase-3.md +685 -0
- package/prompts/frontend/flow-docs-gen-phase-4.md +480 -0
- package/prompts/frontend/flow-docs-gen-phase-5.md +483 -0
- package/prompts/frontend/flow-docs-gen-phase-6.md +570 -0
- package/prompts/frontend/flow-docs-gen-phase-7.md +582 -0
- package/prompts/frontend/flow-docs-gen.md +413 -0
- package/prompts/frontend/flow-docs-sync.md +561 -0
- package/prompts/mobile/flow-docs-gen-phase-0.md +387 -0
- package/prompts/mobile/flow-docs-gen-phase-1.md +530 -0
- package/prompts/mobile/flow-docs-gen-phase-2.md +584 -0
- package/prompts/mobile/flow-docs-gen-phase-3.md +659 -0
- package/prompts/mobile/flow-docs-gen-phase-4.md +363 -0
- package/prompts/mobile/flow-docs-gen-phase-5.md +369 -0
- package/prompts/mobile/flow-docs-gen-phase-6.md +490 -0
- package/prompts/mobile/flow-docs-gen-phase-7.md +407 -0
- package/prompts/mobile/flow-docs-gen.md +430 -0
- package/prompts/mobile/flow-docs-sync.md +634 -0
- package/templates/backend/.clauderules.template +111 -0
- package/templates/backend/.cursorrules.template +102 -0
- package/templates/backend/.env.example.template +122 -0
- package/templates/backend/README.template.md +200 -0
- package/templates/backend/ai-instructions.template.md +354 -0
- package/templates/backend/copilot-instructions.template.md +160 -0
- package/templates/backend/docs/api.template.md +251 -0
- package/templates/backend/docs/architecture.template.md +612 -0
- package/templates/backend/docs/business-flows.template.md +109 -0
- package/templates/backend/docs/code-standards.template.md +828 -0
- package/templates/backend/docs/contributing.template.md +163 -0
- package/templates/backend/docs/data-model.template.md +416 -0
- package/templates/backend/docs/operations.template.md +591 -0
- package/templates/backend/docs/testing.template.md +762 -0
- package/templates/backend/project-brief.template.md +176 -0
- package/templates/backend/specs/configuration.template.md +133 -0
- package/templates/backend/specs/security.template.md +422 -0
- package/templates/frontend/README.template.md +121 -0
- package/templates/frontend/ai-instructions.template.md +368 -0
- package/templates/frontend/docs/api-integration.template.md +390 -0
- package/templates/frontend/docs/components.template.md +567 -0
- package/templates/frontend/docs/error-handling.template.md +385 -0
- package/templates/frontend/docs/operations.template.md +123 -0
- package/templates/frontend/docs/performance.template.md +140 -0
- package/templates/frontend/docs/pwa.template.md +135 -0
- package/templates/frontend/docs/state-management.template.md +394 -0
- package/templates/frontend/docs/styling.template.md +779 -0
- package/templates/frontend/docs/testing.template.md +736 -0
- package/templates/frontend/project-brief.template.md +55 -0
- package/templates/frontend/specs/accessibility.template.md +111 -0
- package/templates/frontend/specs/configuration.template.md +520 -0
- package/templates/frontend/specs/security.template.md +197 -0
- package/templates/fullstack/README.template.md +282 -0
- package/templates/fullstack/ai-instructions.template.md +487 -0
- package/templates/fullstack/project-brief.template.md +197 -0
- package/templates/fullstack/specs/configuration.template.md +380 -0
- package/templates/mobile/AGENT.template.md +251 -0
- package/templates/mobile/README.template.md +195 -0
- package/templates/mobile/ai-instructions.template.md +221 -0
- package/templates/mobile/docs/app-store.template.md +163 -0
- package/templates/mobile/docs/architecture.template.md +100 -0
- package/templates/mobile/docs/native-features.template.md +137 -0
- package/templates/mobile/docs/navigation.template.md +81 -0
- package/templates/mobile/docs/offline-strategy.template.md +90 -0
- package/templates/mobile/docs/permissions.template.md +70 -0
- package/templates/mobile/docs/state-management.template.md +116 -0
- package/templates/mobile/docs/testing.template.md +146 -0
- package/templates/mobile/project-brief.template.md +97 -0
- package/templates/mobile/specs/build-configuration.template.md +116 -0
- package/templates/mobile/specs/deployment.template.md +114 -0
- package/templates/shared/AGENT.template.md +252 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
# Testing Strategy
|
|
2
|
+
|
|
3
|
+
> Testing approach and best practices for {{PROJECT_NAME}}
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎯 Testing Philosophy
|
|
8
|
+
|
|
9
|
+
**Goal:** Ship with confidence through comprehensive automated testing
|
|
10
|
+
|
|
11
|
+
**Principles:**
|
|
12
|
+
1. **Test user behavior, not implementation** - Tests should reflect how users interact with the app
|
|
13
|
+
2. **Write tests that give confidence** - Focus on tests that catch real bugs
|
|
14
|
+
3. **Avoid testing implementation details** - Refactors shouldn't break tests
|
|
15
|
+
4. **Fast feedback loops** - Unit tests run in milliseconds, E2E in seconds
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🏗️ Testing Pyramid
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/\
|
|
23
|
+
/E2E\ ← Few (Critical user journeys)
|
|
24
|
+
/------\
|
|
25
|
+
/Integration\ ← Some (Component interaction, API calls)
|
|
26
|
+
/------------\
|
|
27
|
+
/ Unit Tests \ ← Many (Business logic, utilities)
|
|
28
|
+
/----------------\
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Distribution:**
|
|
32
|
+
- **70%** Unit Tests (fast, isolated)
|
|
33
|
+
- **20%** Integration Tests (component + hooks + API)
|
|
34
|
+
- **10%** E2E Tests (full user flows)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🧪 Testing Stack
|
|
39
|
+
|
|
40
|
+
### Test Frameworks
|
|
41
|
+
|
|
42
|
+
**Unit & Integration:** {{UNIT_TEST_FRAMEWORK}}
|
|
43
|
+
**Component Testing:** {{COMPONENT_TEST_LIBRARY}}
|
|
44
|
+
**E2E Testing:** {{E2E_FRAMEWORK}}
|
|
45
|
+
|
|
46
|
+
### Supporting Libraries
|
|
47
|
+
|
|
48
|
+
- **Mocking:** {{MOCKING_LIBRARY}}
|
|
49
|
+
- **Code Coverage:** {{COVERAGE_TOOL}}
|
|
50
|
+
- **Visual Regression:** {{VISUAL_REGRESSION_TOOL}}
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 📦 Unit Testing
|
|
55
|
+
|
|
56
|
+
### What to Unit Test
|
|
57
|
+
|
|
58
|
+
✅ **DO test:**
|
|
59
|
+
- Pure functions (utilities, helpers)
|
|
60
|
+
- Business logic (validation, calculations)
|
|
61
|
+
- Custom hooks (isolated)
|
|
62
|
+
- Reducers/stores (state logic)
|
|
63
|
+
|
|
64
|
+
❌ **DON'T test:**
|
|
65
|
+
- Third-party libraries
|
|
66
|
+
- Framework internals
|
|
67
|
+
- Trivial code (getters/setters)
|
|
68
|
+
|
|
69
|
+
### Unit Test Patterns
|
|
70
|
+
|
|
71
|
+
#### Testing Pure Functions
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// utils/formatCurrency.ts
|
|
75
|
+
export const formatCurrency = (amount: number, currency = 'USD'): string => {
|
|
76
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// utils/formatCurrency.test.ts
|
|
80
|
+
import { formatCurrency } from './formatCurrency';
|
|
81
|
+
|
|
82
|
+
describe('formatCurrency', () => {
|
|
83
|
+
it('formats USD correctly', () => {
|
|
84
|
+
expect(formatCurrency(1234.56)).toBe('$1,234.56');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('formats EUR correctly', () => {
|
|
88
|
+
expect(formatCurrency(1234.56, 'EUR')).toBe('€1,234.56');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('handles zero', () => {
|
|
92
|
+
expect(formatCurrency(0)).toBe('$0.00');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles negative values', () => {
|
|
96
|
+
expect(formatCurrency(-100)).toBe('-$100.00');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Testing Custom Hooks
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// hooks/useCounter.ts
|
|
105
|
+
import { useState } from 'react';
|
|
106
|
+
|
|
107
|
+
export const useCounter = (initialValue = 0) => {
|
|
108
|
+
const [count, setCount] = useState(initialValue);
|
|
109
|
+
|
|
110
|
+
const increment = () => setCount(c => c + 1);
|
|
111
|
+
const decrement = () => setCount(c => c - 1);
|
|
112
|
+
const reset = () => setCount(initialValue);
|
|
113
|
+
|
|
114
|
+
return { count, increment, decrement, reset };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// hooks/useCounter.test.ts
|
|
118
|
+
import { renderHook, act } from '@testing-library/react';
|
|
119
|
+
import { useCounter } from './useCounter';
|
|
120
|
+
|
|
121
|
+
describe('useCounter', () => {
|
|
122
|
+
it('initializes with default value', () => {
|
|
123
|
+
const { result } = renderHook(() => useCounter());
|
|
124
|
+
expect(result.current.count).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('increments count', () => {
|
|
128
|
+
const { result } = renderHook(() => useCounter());
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
result.current.increment();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.current.count).toBe(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('resets to initial value', () => {
|
|
138
|
+
const { result } = renderHook(() => useCounter(10));
|
|
139
|
+
|
|
140
|
+
act(() => {
|
|
141
|
+
result.current.increment();
|
|
142
|
+
result.current.reset();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.current.count).toBe(10);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 🔗 Integration Testing
|
|
153
|
+
|
|
154
|
+
### What to Integration Test
|
|
155
|
+
|
|
156
|
+
✅ **DO test:**
|
|
157
|
+
- Component + hooks interaction
|
|
158
|
+
- API calls + data fetching
|
|
159
|
+
- Form submission flows
|
|
160
|
+
- Navigation between views
|
|
161
|
+
- State management integration
|
|
162
|
+
|
|
163
|
+
### Integration Test Patterns
|
|
164
|
+
|
|
165
|
+
#### Testing Components with API Calls
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// UserProfile.tsx
|
|
169
|
+
import { useQuery } from '@tanstack/react-query';
|
|
170
|
+
import { fetchUser } from './api';
|
|
171
|
+
|
|
172
|
+
export const UserProfile = ({ userId }: { userId: string }) => {
|
|
173
|
+
const { data, isLoading, error } = useQuery({
|
|
174
|
+
queryKey: ['user', userId],
|
|
175
|
+
queryFn: () => fetchUser(userId)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (isLoading) return <div>Loading...</div>;
|
|
179
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div>
|
|
183
|
+
<h1>{data.name}</h1>
|
|
184
|
+
<p>{data.email}</p>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// UserProfile.test.tsx
|
|
190
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
191
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
192
|
+
import { rest } from 'msw';
|
|
193
|
+
import { setupServer } from 'msw/node';
|
|
194
|
+
import { UserProfile } from './UserProfile';
|
|
195
|
+
|
|
196
|
+
const server = setupServer(
|
|
197
|
+
rest.get('/api/users/:id', (req, res, ctx) => {
|
|
198
|
+
return res(ctx.json({
|
|
199
|
+
id: '1',
|
|
200
|
+
name: 'Alice Johnson',
|
|
201
|
+
email: 'alice@example.com'
|
|
202
|
+
}));
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
beforeAll(() => server.listen());
|
|
207
|
+
afterEach(() => server.resetHandlers());
|
|
208
|
+
afterAll(() => server.close());
|
|
209
|
+
|
|
210
|
+
const createWrapper = () => {
|
|
211
|
+
const queryClient = new QueryClient({
|
|
212
|
+
defaultOptions: { queries: { retry: false } }
|
|
213
|
+
});
|
|
214
|
+
return ({ children }) => (
|
|
215
|
+
<QueryClientProvider client={queryClient}>
|
|
216
|
+
{children}
|
|
217
|
+
</QueryClientProvider>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
describe('UserProfile', () => {
|
|
222
|
+
it('displays user data', async () => {
|
|
223
|
+
render(<UserProfile userId="1" />, { wrapper: createWrapper() });
|
|
224
|
+
|
|
225
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
|
|
229
|
+
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('displays error message on failure', async () => {
|
|
234
|
+
server.use(
|
|
235
|
+
rest.get('/api/users/:id', (req, res, ctx) => {
|
|
236
|
+
return res(ctx.status(500));
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
render(<UserProfile userId="1" />, { wrapper: createWrapper() });
|
|
241
|
+
|
|
242
|
+
await waitFor(() => {
|
|
243
|
+
expect(screen.getByText(/error/i)).toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Testing Forms
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// LoginForm.test.tsx
|
|
253
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
254
|
+
import userEvent from '@testing-library/user-event';
|
|
255
|
+
import { LoginForm } from './LoginForm';
|
|
256
|
+
|
|
257
|
+
describe('LoginForm', () => {
|
|
258
|
+
it('submits form with valid data', async () => {
|
|
259
|
+
const onSubmit = vi.fn();
|
|
260
|
+
const user = userEvent.setup();
|
|
261
|
+
|
|
262
|
+
render(<LoginForm onSubmit={onSubmit} />);
|
|
263
|
+
|
|
264
|
+
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
|
|
265
|
+
await user.type(screen.getByLabelText(/password/i), 'password123');
|
|
266
|
+
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
267
|
+
|
|
268
|
+
await waitFor(() => {
|
|
269
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
270
|
+
email: 'test@example.com',
|
|
271
|
+
password: 'password123'
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('displays validation errors', async () => {
|
|
277
|
+
const user = userEvent.setup();
|
|
278
|
+
|
|
279
|
+
render(<LoginForm onSubmit={vi.fn()} />);
|
|
280
|
+
|
|
281
|
+
// Submit without filling fields
|
|
282
|
+
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
283
|
+
|
|
284
|
+
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
|
|
285
|
+
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('disables submit button while loading', async () => {
|
|
289
|
+
const onSubmit = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
290
|
+
const user = userEvent.setup();
|
|
291
|
+
|
|
292
|
+
render(<LoginForm onSubmit={onSubmit} />);
|
|
293
|
+
|
|
294
|
+
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
|
|
295
|
+
await user.type(screen.getByLabelText(/password/i), 'password123');
|
|
296
|
+
|
|
297
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
298
|
+
await user.click(submitButton);
|
|
299
|
+
|
|
300
|
+
expect(submitButton).toBeDisabled();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 🌐 E2E Testing
|
|
308
|
+
|
|
309
|
+
### What to E2E Test
|
|
310
|
+
|
|
311
|
+
✅ **DO test:**
|
|
312
|
+
- Critical user journeys (signup, checkout)
|
|
313
|
+
- Cross-page flows
|
|
314
|
+
- Authentication flows
|
|
315
|
+
- Payment processing
|
|
316
|
+
- Real browser interactions
|
|
317
|
+
|
|
318
|
+
❌ **DON'T test:**
|
|
319
|
+
- Every feature (too slow)
|
|
320
|
+
- Edge cases (use integration tests)
|
|
321
|
+
- Visual details (use visual regression)
|
|
322
|
+
|
|
323
|
+
### E2E Test Patterns ({{E2E_FRAMEWORK}})
|
|
324
|
+
|
|
325
|
+
#### User Journey Test
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// e2e/checkout.spec.ts
|
|
329
|
+
import { test, expect } from '@playwright/test';
|
|
330
|
+
|
|
331
|
+
test.describe('Checkout Flow', () => {
|
|
332
|
+
test.beforeEach(async ({ page }) => {
|
|
333
|
+
// Login before each test
|
|
334
|
+
await page.goto('/login');
|
|
335
|
+
await page.fill('[name="email"]', 'test@example.com');
|
|
336
|
+
await page.fill('[name="password"]', 'password123');
|
|
337
|
+
await page.click('button[type="submit"]');
|
|
338
|
+
await expect(page).toHaveURL('/dashboard');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('completes purchase successfully', async ({ page }) => {
|
|
342
|
+
// Add item to cart
|
|
343
|
+
await page.goto('/products');
|
|
344
|
+
await page.click('[data-testid="product-1"]');
|
|
345
|
+
await page.click('button:has-text("Add to Cart")');
|
|
346
|
+
|
|
347
|
+
// Verify cart badge
|
|
348
|
+
await expect(page.locator('[data-testid="cart-badge"]')).toHaveText('1');
|
|
349
|
+
|
|
350
|
+
// Go to checkout
|
|
351
|
+
await page.click('[data-testid="cart-icon"]');
|
|
352
|
+
await page.click('button:has-text("Checkout")');
|
|
353
|
+
|
|
354
|
+
// Fill shipping info
|
|
355
|
+
await page.fill('[name="address"]', '123 Main St');
|
|
356
|
+
await page.fill('[name="city"]', 'New York');
|
|
357
|
+
await page.fill('[name="zipCode"]', '10001');
|
|
358
|
+
await page.click('button:has-text("Continue to Payment")');
|
|
359
|
+
|
|
360
|
+
// Fill payment info (test mode)
|
|
361
|
+
await page.fill('[name="cardNumber"]', '4242424242424242');
|
|
362
|
+
await page.fill('[name="expiry"]', '12/25');
|
|
363
|
+
await page.fill('[name="cvc"]', '123');
|
|
364
|
+
await page.click('button:has-text("Place Order")');
|
|
365
|
+
|
|
366
|
+
// Verify success
|
|
367
|
+
await expect(page).toHaveURL(/\/order\/[a-z0-9]+/);
|
|
368
|
+
await expect(page.locator('h1')).toHaveText('Order Confirmed!');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('validates payment information', async ({ page }) => {
|
|
372
|
+
await page.goto('/checkout');
|
|
373
|
+
|
|
374
|
+
// Try to submit with invalid card
|
|
375
|
+
await page.fill('[name="cardNumber"]', '1234');
|
|
376
|
+
await page.click('button:has-text("Place Order")');
|
|
377
|
+
|
|
378
|
+
await expect(page.locator('.error')).toContainText('Invalid card number');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### API Mocking in E2E
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// e2e/dashboard.spec.ts
|
|
387
|
+
import { test, expect } from '@playwright/test';
|
|
388
|
+
|
|
389
|
+
test('displays dashboard with mocked data', async ({ page }) => {
|
|
390
|
+
// Mock API response
|
|
391
|
+
await page.route('/api/stats', route => {
|
|
392
|
+
route.fulfill({
|
|
393
|
+
status: 200,
|
|
394
|
+
contentType: 'application/json',
|
|
395
|
+
body: JSON.stringify({
|
|
396
|
+
totalSales: 15420,
|
|
397
|
+
orders: 234,
|
|
398
|
+
customers: 1250
|
|
399
|
+
})
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await page.goto('/dashboard');
|
|
404
|
+
|
|
405
|
+
await expect(page.locator('[data-testid="total-sales"]')).toHaveText('$15,420');
|
|
406
|
+
await expect(page.locator('[data-testid="total-orders"]')).toHaveText('234');
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 🎨 Visual Regression Testing
|
|
413
|
+
|
|
414
|
+
### Strategy: {{VISUAL_REGRESSION_TOOL}}
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// Using Playwright
|
|
418
|
+
import { test, expect } from '@playwright/test';
|
|
419
|
+
|
|
420
|
+
test('homepage looks correct', async ({ page }) => {
|
|
421
|
+
await page.goto('/');
|
|
422
|
+
await expect(page).toHaveScreenshot('homepage.png');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('button variants', async ({ page }) => {
|
|
426
|
+
await page.goto('/storybook/button');
|
|
427
|
+
await expect(page.locator('.button-primary')).toHaveScreenshot('button-primary.png');
|
|
428
|
+
await expect(page.locator('.button-secondary')).toHaveScreenshot('button-secondary.png');
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## 🧩 Testing Best Practices
|
|
435
|
+
|
|
436
|
+
### 1. Query Priorities (Testing Library)
|
|
437
|
+
|
|
438
|
+
**Priority order:**
|
|
439
|
+
|
|
440
|
+
1. **Accessible by everyone:** `getByRole`, `getByLabelText`, `getByPlaceholderText`, `getByText`
|
|
441
|
+
2. **Semantic queries:** `getByAltText`, `getByTitle`
|
|
442
|
+
3. **Test IDs (last resort):** `getByTestId`
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// ✅ Good - Accessible queries
|
|
446
|
+
screen.getByRole('button', { name: /submit/i });
|
|
447
|
+
screen.getByLabelText(/email address/i);
|
|
448
|
+
|
|
449
|
+
// ❌ Bad - Fragile test IDs
|
|
450
|
+
screen.getByTestId('submit-btn');
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### 2. Async Utilities
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// ✅ Good - waitFor for async changes
|
|
457
|
+
await waitFor(() => {
|
|
458
|
+
expect(screen.getByText('Success!')).toBeInTheDocument();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ✅ Good - findBy for async queries (combines getBy + waitFor)
|
|
462
|
+
const element = await screen.findByText('Success!');
|
|
463
|
+
|
|
464
|
+
// ❌ Bad - Manual timeout
|
|
465
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
466
|
+
expect(screen.getByText('Success!')).toBeInTheDocument();
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 3. User Interactions
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import userEvent from '@testing-library/user-event';
|
|
473
|
+
|
|
474
|
+
// ✅ Good - userEvent (realistic)
|
|
475
|
+
const user = userEvent.setup();
|
|
476
|
+
await user.type(input, 'hello');
|
|
477
|
+
await user.click(button);
|
|
478
|
+
|
|
479
|
+
// ❌ Bad - fireEvent (synthetic)
|
|
480
|
+
fireEvent.change(input, { target: { value: 'hello' } });
|
|
481
|
+
fireEvent.click(button);
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### 4. Test Organization
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
describe('UserDashboard', () => {
|
|
488
|
+
// Setup
|
|
489
|
+
let mockUser: User;
|
|
490
|
+
|
|
491
|
+
beforeEach(() => {
|
|
492
|
+
mockUser = { id: '1', name: 'Alice' };
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('when logged in', () => {
|
|
496
|
+
it('displays welcome message', () => {
|
|
497
|
+
// Test
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('shows user statistics', () => {
|
|
501
|
+
// Test
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe('when logged out', () => {
|
|
506
|
+
it('redirects to login', () => {
|
|
507
|
+
// Test
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## 📊 Code Coverage
|
|
516
|
+
|
|
517
|
+
### Coverage Targets
|
|
518
|
+
|
|
519
|
+
- **Statements:** {{COVERAGE_STATEMENTS}}%
|
|
520
|
+
- **Branches:** {{COVERAGE_BRANCHES}}%
|
|
521
|
+
- **Functions:** {{COVERAGE_FUNCTIONS}}%
|
|
522
|
+
- **Lines:** {{COVERAGE_LINES}}%
|
|
523
|
+
|
|
524
|
+
### Running Coverage
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
# Generate coverage report
|
|
528
|
+
{{PACKAGE_MANAGER}} run test:coverage
|
|
529
|
+
|
|
530
|
+
# View HTML report
|
|
531
|
+
open coverage/index.html
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Coverage Configuration
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// vitest.config.ts
|
|
538
|
+
export default defineConfig({
|
|
539
|
+
test: {
|
|
540
|
+
coverage: {
|
|
541
|
+
provider: 'v8',
|
|
542
|
+
reporter: ['text', 'html', 'lcov'],
|
|
543
|
+
include: ['src/**/*.{ts,tsx}'],
|
|
544
|
+
exclude: [
|
|
545
|
+
'src/**/*.test.{ts,tsx}',
|
|
546
|
+
'src/**/*.spec.{ts,tsx}',
|
|
547
|
+
'src/**/*.stories.{ts,tsx}',
|
|
548
|
+
'src/types/**'
|
|
549
|
+
],
|
|
550
|
+
thresholds: {
|
|
551
|
+
statements: 80,
|
|
552
|
+
branches: 75,
|
|
553
|
+
functions: 80,
|
|
554
|
+
lines: 80
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## 🔧 Mocking Strategies
|
|
564
|
+
|
|
565
|
+
### 1. Mock Service Worker (API Mocking)
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// mocks/handlers.ts
|
|
569
|
+
import { rest } from 'msw';
|
|
570
|
+
|
|
571
|
+
export const handlers = [
|
|
572
|
+
rest.get('/api/users/:id', (req, res, ctx) => {
|
|
573
|
+
return res(
|
|
574
|
+
ctx.status(200),
|
|
575
|
+
ctx.json({ id: req.params.id, name: 'Alice' })
|
|
576
|
+
);
|
|
577
|
+
}),
|
|
578
|
+
|
|
579
|
+
rest.post('/api/login', async (req, res, ctx) => {
|
|
580
|
+
const { email, password } = await req.json();
|
|
581
|
+
|
|
582
|
+
if (email === 'test@example.com' && password === 'password123') {
|
|
583
|
+
return res(
|
|
584
|
+
ctx.status(200),
|
|
585
|
+
ctx.json({ token: 'fake-jwt-token' })
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return res(
|
|
590
|
+
ctx.status(401),
|
|
591
|
+
ctx.json({ error: 'Invalid credentials' })
|
|
592
|
+
);
|
|
593
|
+
})
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
// mocks/server.ts
|
|
597
|
+
import { setupServer } from 'msw/node';
|
|
598
|
+
import { handlers } from './handlers';
|
|
599
|
+
|
|
600
|
+
export const server = setupServer(...handlers);
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 2. Module Mocking
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// vi.mock for Vitest
|
|
607
|
+
vi.mock('./api', () => ({
|
|
608
|
+
fetchUser: vi.fn(() => Promise.resolve({ id: '1', name: 'Alice' }))
|
|
609
|
+
}));
|
|
610
|
+
|
|
611
|
+
// jest.mock for Jest
|
|
612
|
+
jest.mock('./api', () => ({
|
|
613
|
+
fetchUser: jest.fn(() => Promise.resolve({ id: '1', name: 'Alice' }))
|
|
614
|
+
}));
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### 3. Component Mocking
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// Mock child component
|
|
621
|
+
vi.mock('./ComplexChart', () => ({
|
|
622
|
+
ComplexChart: () => <div>Chart Mock</div>
|
|
623
|
+
}));
|
|
624
|
+
|
|
625
|
+
test('renders dashboard', () => {
|
|
626
|
+
render(<Dashboard />);
|
|
627
|
+
expect(screen.getByText('Chart Mock')).toBeInTheDocument();
|
|
628
|
+
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## ⚠️ Common Testing Pitfalls
|
|
634
|
+
|
|
635
|
+
### 1. Testing Implementation Details
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
// ❌ Bad - Tests internal state
|
|
639
|
+
const { container } = render(<Counter />);
|
|
640
|
+
const button = container.querySelector('.increment-btn');
|
|
641
|
+
expect(button.className).toBe('increment-btn active');
|
|
642
|
+
|
|
643
|
+
// ✅ Good - Tests user-visible behavior
|
|
644
|
+
render(<Counter />);
|
|
645
|
+
const button = screen.getByRole('button', { name: /increment/i });
|
|
646
|
+
await userEvent.click(button);
|
|
647
|
+
expect(screen.getByText('Count: 1')).toBeInTheDocument();
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### 2. Not Cleaning Up
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
// ❌ Bad - State leaks between tests
|
|
654
|
+
let component;
|
|
655
|
+
|
|
656
|
+
test('test 1', () => {
|
|
657
|
+
component = render(<App />);
|
|
658
|
+
// No cleanup
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// ✅ Good - Automatic cleanup
|
|
662
|
+
import { render, cleanup } from '@testing-library/react';
|
|
663
|
+
|
|
664
|
+
afterEach(() => {
|
|
665
|
+
cleanup();
|
|
666
|
+
});
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### 3. Over-Mocking
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
// ❌ Bad - Mocking everything
|
|
673
|
+
vi.mock('./Button', () => ({ Button: () => <div>Button</div> }));
|
|
674
|
+
vi.mock('./Input', () => ({ Input: () => <div>Input</div> }));
|
|
675
|
+
vi.mock('./Form', () => ({ Form: () => <div>Form</div> }));
|
|
676
|
+
|
|
677
|
+
// ✅ Good - Only mock external dependencies
|
|
678
|
+
vi.mock('./api', () => ({ fetchData: vi.fn() }));
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## 🚀 CI/CD Integration
|
|
684
|
+
|
|
685
|
+
### GitHub Actions Example
|
|
686
|
+
|
|
687
|
+
```yaml
|
|
688
|
+
# .github/workflows/test.yml
|
|
689
|
+
name: Tests
|
|
690
|
+
|
|
691
|
+
on: [push, pull_request]
|
|
692
|
+
|
|
693
|
+
jobs:
|
|
694
|
+
test:
|
|
695
|
+
runs-on: ubuntu-latest
|
|
696
|
+
|
|
697
|
+
steps:
|
|
698
|
+
- uses: actions/checkout@v3
|
|
699
|
+
- uses: actions/setup-node@v3
|
|
700
|
+
with:
|
|
701
|
+
node-version: 18
|
|
702
|
+
|
|
703
|
+
- name: Install dependencies
|
|
704
|
+
run: npm ci
|
|
705
|
+
|
|
706
|
+
- name: Run unit & integration tests
|
|
707
|
+
run: npm run test:coverage
|
|
708
|
+
|
|
709
|
+
- name: Upload coverage
|
|
710
|
+
uses: codecov/codecov-action@v3
|
|
711
|
+
|
|
712
|
+
- name: Run E2E tests
|
|
713
|
+
run: npm run test:e2e
|
|
714
|
+
|
|
715
|
+
- name: Upload E2E artifacts
|
|
716
|
+
if: failure()
|
|
717
|
+
uses: actions/upload-artifact@v3
|
|
718
|
+
with:
|
|
719
|
+
name: playwright-screenshots
|
|
720
|
+
path: test-results/
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## 🔗 Related Documents
|
|
726
|
+
|
|
727
|
+
- [Component Architecture](components.md) - Component structure to test
|
|
728
|
+
- [State Management](state-management.md) - Testing stores and hooks
|
|
729
|
+
- [AI Instructions](../ai-instructions.md) - Testing requirements
|
|
730
|
+
- [Contributing](contributing.md) - How to write tests
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
**Last Updated:** {{GENERATION_DATE}}
|
|
735
|
+
|
|
736
|
+
**Testing Stack:** {{UNIT_TEST_FRAMEWORK}} + {{COMPONENT_TEST_LIBRARY}} + {{E2E_FRAMEWORK}}
|