workflow-agent-cli 1.1.2
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/dist/chunk-VFN3BY56.js +120 -0
- package/dist/chunk-VFN3BY56.js.map +1 -0
- package/dist/chunk-X2NQJ2ZY.js +170 -0
- package/dist/chunk-X2NQJ2ZY.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1206 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +11 -0
- package/dist/config/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-CiJ4W7in.d.ts +97 -0
- package/dist/scripts/postinstall.d.ts +1 -0
- package/dist/scripts/postinstall.js +73 -0
- package/dist/scripts/postinstall.js.map +1 -0
- package/dist/validators/index.d.ts +16 -0
- package/dist/validators/index.js +17 -0
- package/dist/validators/index.js.map +1 -0
- package/package.json +80 -0
- package/templates/AGENT_EDITING_INSTRUCTIONS.md +887 -0
- package/templates/BRANCHING_STRATEGY.md +442 -0
- package/templates/COMPONENT_LIBRARY.md +611 -0
- package/templates/CUSTOM_SCOPE_TEMPLATE.md +228 -0
- package/templates/DEPLOYMENT_STRATEGY.md +509 -0
- package/templates/Guidelines.md +62 -0
- package/templates/LIBRARY_INVENTORY.md +615 -0
- package/templates/PROJECT_TEMPLATE_README.md +347 -0
- package/templates/SCOPE_CREATION_WORKFLOW.md +286 -0
- package/templates/SELF_IMPROVEMENT_MANDATE.md +298 -0
- package/templates/SINGLE_SOURCE_OF_TRUTH.md +492 -0
- package/templates/TESTING_STRATEGY.md +801 -0
- package/templates/_TEMPLATE_EXAMPLE.md +28 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
# Testing Strategy
|
|
2
|
+
|
|
3
|
+
> **Purpose**: This document defines the testing strategy, patterns, and requirements for the project. Following these guidelines ensures comprehensive test coverage and prevents regressions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Testing Pyramid](#testing-pyramid)
|
|
10
|
+
2. [Unit Testing with Vitest](#unit-testing-with-vitest)
|
|
11
|
+
3. [Component Library Testing](#component-library-testing)
|
|
12
|
+
4. [E2E Testing with Playwright](#e2e-testing-with-playwright)
|
|
13
|
+
5. [Test Infrastructure](#test-infrastructure)
|
|
14
|
+
6. [When Tests Are Required](#when-tests-are-required)
|
|
15
|
+
7. [Pre-Merge Checklist](#pre-merge-checklist)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Testing Pyramid
|
|
20
|
+
|
|
21
|
+
Our testing strategy follows the testing pyramid model:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
/\
|
|
25
|
+
/ \
|
|
26
|
+
/ E2E \ ← Few, slow, comprehensive user journey tests
|
|
27
|
+
/--------\
|
|
28
|
+
/ \
|
|
29
|
+
/ Integration \ ← Component + hook integration tests
|
|
30
|
+
/--------------\
|
|
31
|
+
/ \
|
|
32
|
+
/ Unit Tests \ ← Many, fast, focused tests
|
|
33
|
+
/--------------------\
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Layer | Tool | Quantity | Speed | Purpose |
|
|
37
|
+
| ----------- | ------------------------ | -------- | ------ | ------------------------------------------------------------- |
|
|
38
|
+
| Unit | Vitest | Many | Fast | Test individual functions, utilities, components in isolation |
|
|
39
|
+
| Integration | Vitest + Testing Library | Some | Medium | Test component interactions, hooks with mocked services |
|
|
40
|
+
| E2E | Playwright | Few | Slow | Test critical user journeys end-to-end |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Unit Testing with Vitest
|
|
45
|
+
|
|
46
|
+
### Configuration
|
|
47
|
+
|
|
48
|
+
Located in `vitest.config.ts`:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
export default defineConfig({
|
|
52
|
+
test: {
|
|
53
|
+
globals: true,
|
|
54
|
+
environment: 'jsdom',
|
|
55
|
+
setupFiles: ['./test/setup.ts'],
|
|
56
|
+
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
57
|
+
coverage: {
|
|
58
|
+
provider: 'v8',
|
|
59
|
+
reporter: ['text', 'json', 'html'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Running Tests
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Run all tests once
|
|
69
|
+
pnpm test
|
|
70
|
+
|
|
71
|
+
# Watch mode (re-runs on file changes)
|
|
72
|
+
pnpm test:watch
|
|
73
|
+
|
|
74
|
+
# Visual UI for test results
|
|
75
|
+
pnpm test:ui
|
|
76
|
+
|
|
77
|
+
# With coverage report
|
|
78
|
+
pnpm test:coverage
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Test File Naming & Location
|
|
82
|
+
|
|
83
|
+
| Source File | Test File Location |
|
|
84
|
+
| -------------------------------- | ------------------------------------- |
|
|
85
|
+
| `components/TaskCard.tsx` | `components/TaskCard.test.tsx` |
|
|
86
|
+
| `hooks/useDebounce.tsx` | `hooks/useDebounce.test.tsx` |
|
|
87
|
+
| `utils/authorization.ts` | `utils/authorization.test.ts` |
|
|
88
|
+
| `lib/validations/task.schema.ts` | `lib/validations/task.schema.test.ts` |
|
|
89
|
+
|
|
90
|
+
### Test Structure Pattern
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
94
|
+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
95
|
+
import userEvent from "@testing-library/user-event";
|
|
96
|
+
import {
|
|
97
|
+
mockTasks,
|
|
98
|
+
createMockTask,
|
|
99
|
+
seedTasks,
|
|
100
|
+
clearLocalStorage,
|
|
101
|
+
} from "@test/fixtures";
|
|
102
|
+
import { TaskCard } from "./TaskCard";
|
|
103
|
+
|
|
104
|
+
describe("TaskCard", () => {
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
clearLocalStorage();
|
|
107
|
+
seedTasks();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("rendering", () => {
|
|
115
|
+
it("should display task title and ticket ID", () => {
|
|
116
|
+
const task = createMockTask({ title: "Test Task", ticketId: "TEST-0001" });
|
|
117
|
+
render(<TaskCard task={task} />);
|
|
118
|
+
|
|
119
|
+
expect(screen.getByText("Test Task")).toBeInTheDocument();
|
|
120
|
+
expect(screen.getByText("TEST-0001")).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should show priority badge", () => {
|
|
124
|
+
const task = createMockTask({ priority: "high" });
|
|
125
|
+
render(<TaskCard task={task} />);
|
|
126
|
+
|
|
127
|
+
expect(screen.getByTestId("priority-badge")).toHaveTextContent("High");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("interactions", () => {
|
|
132
|
+
it("should call onEdit when edit button is clicked", async () => {
|
|
133
|
+
const user = userEvent.setup();
|
|
134
|
+
const onEdit = vi.fn();
|
|
135
|
+
const task = createMockTask();
|
|
136
|
+
|
|
137
|
+
render(<TaskCard task={task} onEdit={onEdit} />);
|
|
138
|
+
|
|
139
|
+
await user.click(screen.getByTestId("edit-task-btn"));
|
|
140
|
+
|
|
141
|
+
expect(onEdit).toHaveBeenCalledWith(task);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Mocking Patterns
|
|
148
|
+
|
|
149
|
+
#### Mocking Hooks
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { vi } from 'vitest';
|
|
153
|
+
|
|
154
|
+
// Mock a custom hook
|
|
155
|
+
vi.mock('@/hooks/useAuthorization', () => ({
|
|
156
|
+
useAuthorization: () => ({
|
|
157
|
+
can: {
|
|
158
|
+
editTask: () => true,
|
|
159
|
+
deleteTask: () => false,
|
|
160
|
+
},
|
|
161
|
+
currentUserRole: 'developer',
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Mocking Supabase Client
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Already set up in test/setup.ts - uses MSW for API mocking
|
|
170
|
+
// For unit tests, you can also mock directly:
|
|
171
|
+
|
|
172
|
+
vi.mock('@/lib/supabase/client', () => ({
|
|
173
|
+
getSupabaseClient: vi.fn(() => ({
|
|
174
|
+
auth: {
|
|
175
|
+
getUser: vi.fn().mockResolvedValue({
|
|
176
|
+
data: { user: { id: 'test-user-id', email: 'test@example.com' } },
|
|
177
|
+
error: null,
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
from: vi.fn(() => ({
|
|
181
|
+
select: vi.fn(() => ({
|
|
182
|
+
eq: vi.fn(() => ({
|
|
183
|
+
single: vi.fn().mockResolvedValue({ data: mockTask, error: null }),
|
|
184
|
+
})),
|
|
185
|
+
})),
|
|
186
|
+
})),
|
|
187
|
+
})),
|
|
188
|
+
}));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Mocking Server Actions
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
vi.mock('@/app/actions/tasks', () => ({
|
|
195
|
+
getTasks: vi.fn().mockResolvedValue({ data: mockTasks, error: null }),
|
|
196
|
+
createTask: vi.fn().mockResolvedValue({ data: mockTask, error: null }),
|
|
197
|
+
deleteTask: vi.fn().mockResolvedValue({ success: true, error: null }),
|
|
198
|
+
}));
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Component Library Testing
|
|
204
|
+
|
|
205
|
+
> **Reference**: See `guidelines/COMPONENT_LIBRARY.md` for component inventory and requirements.
|
|
206
|
+
|
|
207
|
+
Component library components require comprehensive testing with a **fail-fast approach**.
|
|
208
|
+
|
|
209
|
+
### Configuration
|
|
210
|
+
|
|
211
|
+
Vitest is configured with `bail: 1` to stop on first failure:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// vitest.config.ts
|
|
215
|
+
export default defineConfig({
|
|
216
|
+
test: {
|
|
217
|
+
bail: 1, // Stop on first failure
|
|
218
|
+
snapshotFormat: {
|
|
219
|
+
escapeString: false,
|
|
220
|
+
printBasicPrototype: false,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Running Component Tests
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Run component library tests only
|
|
230
|
+
pnpm test:components
|
|
231
|
+
|
|
232
|
+
# Watch mode for development
|
|
233
|
+
pnpm test:components:watch
|
|
234
|
+
|
|
235
|
+
# Update snapshots after intentional changes
|
|
236
|
+
pnpm test:update-snapshots
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Test File Structure
|
|
240
|
+
|
|
241
|
+
Every component in the library requires three files:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
components/
|
|
245
|
+
├── StatusBadge.tsx # Component implementation
|
|
246
|
+
├── StatusBadge.stories.tsx # Storybook story
|
|
247
|
+
├── StatusBadge.test.tsx # Unit + snapshot tests
|
|
248
|
+
└── __snapshots__/
|
|
249
|
+
└── StatusBadge.test.tsx.snap
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Required Test Coverage
|
|
253
|
+
|
|
254
|
+
#### 1. Behavioral Tests
|
|
255
|
+
|
|
256
|
+
Test all logic and interactions:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
260
|
+
import { render, screen } from "@testing-library/react";
|
|
261
|
+
import { StatusBadge } from "./StatusBadge";
|
|
262
|
+
import { mockFeatureFlag, clearFeatureFlagMocks, FeatureFlag } from "@/lib/feature-flags";
|
|
263
|
+
|
|
264
|
+
describe("StatusBadge", () => {
|
|
265
|
+
afterEach(() => {
|
|
266
|
+
clearFeatureFlagMocks();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should render without crashing", () => {
|
|
270
|
+
mockFeatureFlag(FeatureFlag.STATUS_BADGE, true);
|
|
271
|
+
render(<StatusBadge status="todo" />);
|
|
272
|
+
expect(screen.getByText("To Do")).toBeInTheDocument();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should apply correct color for each status", () => {
|
|
276
|
+
mockFeatureFlag(FeatureFlag.STATUS_BADGE, true);
|
|
277
|
+
const { container } = render(<StatusBadge status="in-progress" />);
|
|
278
|
+
expect(container.firstChild).toHaveClass("bg-sky-50");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should respect size prop", () => {
|
|
282
|
+
mockFeatureFlag(FeatureFlag.STATUS_BADGE, true);
|
|
283
|
+
const { container } = render(<StatusBadge status="todo" size="lg" />);
|
|
284
|
+
expect(container.firstChild).toHaveClass("text-base");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should apply custom className", () => {
|
|
288
|
+
mockFeatureFlag(FeatureFlag.STATUS_BADGE, true);
|
|
289
|
+
const { container } = render(<StatusBadge status="todo" className="custom" />);
|
|
290
|
+
expect(container.firstChild).toHaveClass("custom");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### 2. Feature Flag Tests
|
|
296
|
+
|
|
297
|
+
Test both enabled and disabled states:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
describe("Feature Flag Behavior", () => {
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
clearFeatureFlagMocks();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should return null when feature flag is disabled", () => {
|
|
306
|
+
mockFeatureFlag(FeatureFlag.STATUS_BADGE, false);
|
|
307
|
+
const { container } = render(<StatusBadge status="todo" />);
|
|
308
|
+
expect(container.firstChild).toBeNull();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should render component when feature flag is enabled", () => {
|
|
312
|
+
mockFeatureFlag(FeatureFlag.STATUS_BADGE, true);
|
|
313
|
+
const { container } = render(<StatusBadge status="todo" />);
|
|
314
|
+
expect(container.firstChild).not.toBeNull();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### 3. Snapshot Tests
|
|
320
|
+
|
|
321
|
+
Capture visual output for all variants:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
describe("Snapshots", () => {
|
|
325
|
+
beforeEach(() => {
|
|
326
|
+
mockFeatureFlag(FeatureFlag.STATUS_BADGE, true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
afterEach(() => {
|
|
330
|
+
clearFeatureFlagMocks();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Test each status
|
|
334
|
+
it.each(["todo", "in-progress", "review", "done"] as const)(
|
|
335
|
+
"should match snapshot for status: %s",
|
|
336
|
+
(status) => {
|
|
337
|
+
const { container } = render(<StatusBadge status={status} />);
|
|
338
|
+
expect(container).toMatchSnapshot();
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Test each size
|
|
343
|
+
it.each(["sm", "md", "lg"] as const)(
|
|
344
|
+
"should match snapshot for size: %s",
|
|
345
|
+
(size) => {
|
|
346
|
+
const { container } = render(<StatusBadge status="todo" size={size} />);
|
|
347
|
+
expect(container).toMatchSnapshot();
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Test with icon hidden
|
|
352
|
+
it("should match snapshot without icon", () => {
|
|
353
|
+
const { container } = render(<StatusBadge status="todo" showIcon={false} />);
|
|
354
|
+
expect(container).toMatchSnapshot();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Snapshot Update Workflow
|
|
360
|
+
|
|
361
|
+
When snapshots fail:
|
|
362
|
+
|
|
363
|
+
1. **Unintentional change?** → Fix the code, snapshots should pass
|
|
364
|
+
2. **Intentional change?** → Review the diff carefully:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# View snapshot diff
|
|
368
|
+
pnpm test:components
|
|
369
|
+
|
|
370
|
+
# If changes are correct, update snapshots
|
|
371
|
+
pnpm test:update-snapshots
|
|
372
|
+
|
|
373
|
+
# Commit updated snapshots with explanation
|
|
374
|
+
git add -A
|
|
375
|
+
git commit -m "test: update StatusBadge snapshots for new size variant"
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Component Test Checklist
|
|
379
|
+
|
|
380
|
+
Before submitting PR:
|
|
381
|
+
|
|
382
|
+
- [ ] All behavioral tests pass
|
|
383
|
+
- [ ] All variants have snapshot tests
|
|
384
|
+
- [ ] Feature flag enabled/disabled tested
|
|
385
|
+
- [ ] Custom className prop tested
|
|
386
|
+
- [ ] Edge cases handled (empty, null, undefined)
|
|
387
|
+
- [ ] Accessibility: ARIA attributes present (where applicable)
|
|
388
|
+
- [ ] Storybook story renders correctly
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## E2E Testing with Playwright
|
|
393
|
+
|
|
394
|
+
### Configuration
|
|
395
|
+
|
|
396
|
+
Located in `playwright.config.ts`:
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
export default defineConfig({
|
|
400
|
+
testDir: './e2e',
|
|
401
|
+
fullyParallel: true,
|
|
402
|
+
retries: process.env.CI ? 2 : 0,
|
|
403
|
+
workers: process.env.CI ? 1 : undefined,
|
|
404
|
+
reporter: 'html',
|
|
405
|
+
use: {
|
|
406
|
+
baseURL: 'http://localhost:5173',
|
|
407
|
+
trace: 'on-first-retry',
|
|
408
|
+
},
|
|
409
|
+
projects: [
|
|
410
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
411
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
412
|
+
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
413
|
+
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
|
|
414
|
+
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
|
|
415
|
+
],
|
|
416
|
+
webServer: {
|
|
417
|
+
command: 'pnpm dev',
|
|
418
|
+
url: 'http://localhost:5173',
|
|
419
|
+
reuseExistingServer: !process.env.CI,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Running E2E Tests
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
# Run all E2E tests
|
|
428
|
+
pnpm test:e2e
|
|
429
|
+
|
|
430
|
+
# Run with UI
|
|
431
|
+
npx playwright test --ui
|
|
432
|
+
|
|
433
|
+
# Run specific test file
|
|
434
|
+
npx playwright test e2e/critical-tests.spec.ts
|
|
435
|
+
|
|
436
|
+
# Run with headed browser
|
|
437
|
+
npx playwright test --headed
|
|
438
|
+
|
|
439
|
+
# Generate test report
|
|
440
|
+
npx playwright show-report
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### E2E Test Structure
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { test, expect } from '@playwright/test';
|
|
447
|
+
|
|
448
|
+
test.describe('Task Management', () => {
|
|
449
|
+
test.beforeEach(async ({ page }) => {
|
|
450
|
+
// Login or set up authenticated state
|
|
451
|
+
await page.goto('/login');
|
|
452
|
+
await page.fill('[data-testid="email-input"]', 'test@example.com');
|
|
453
|
+
await page.fill('[data-testid="password-input"]', 'password');
|
|
454
|
+
await page.click('[data-testid="login-button"]');
|
|
455
|
+
await page.waitForURL('/dashboard');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('should create a new task', async ({ page }) => {
|
|
459
|
+
// Navigate to board
|
|
460
|
+
await page.click('[data-testid="nav-boards"]');
|
|
461
|
+
|
|
462
|
+
// Open create dialog
|
|
463
|
+
await page.click('[data-testid="create-task-btn"]');
|
|
464
|
+
|
|
465
|
+
// Fill form
|
|
466
|
+
await page.fill('[data-testid="task-title-input"]', 'New E2E Task');
|
|
467
|
+
await page.selectOption('[data-testid="priority-select"]', 'high');
|
|
468
|
+
|
|
469
|
+
// Submit
|
|
470
|
+
await page.click('[data-testid="submit-task-btn"]');
|
|
471
|
+
|
|
472
|
+
// Verify creation
|
|
473
|
+
await expect(
|
|
474
|
+
page.locator('[data-testid="task-card"]').filter({ hasText: 'New E2E Task' })
|
|
475
|
+
).toBeVisible();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('should move task between columns', async ({ page }) => {
|
|
479
|
+
// Navigate to kanban board
|
|
480
|
+
await page.goto('/dashboard?view=kanban');
|
|
481
|
+
|
|
482
|
+
// Get task card
|
|
483
|
+
const taskCard = page.locator('[data-testid="task-card-TASK-0001"]');
|
|
484
|
+
const targetColumn = page.locator('[data-testid="column-in-progress"]');
|
|
485
|
+
|
|
486
|
+
// Drag and drop
|
|
487
|
+
await taskCard.dragTo(targetColumn);
|
|
488
|
+
|
|
489
|
+
// Verify move
|
|
490
|
+
await expect(targetColumn.locator('[data-testid="task-card-TASK-0001"]')).toBeVisible();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Critical Test Paths
|
|
496
|
+
|
|
497
|
+
The following user journeys MUST have E2E tests (`e2e/critical-tests.spec.ts`):
|
|
498
|
+
|
|
499
|
+
1. **Authentication Flow**
|
|
500
|
+
- Login with valid credentials
|
|
501
|
+
- Login with invalid credentials (error handling)
|
|
502
|
+
- Logout
|
|
503
|
+
|
|
504
|
+
2. **Task CRUD**
|
|
505
|
+
- Create task with required fields
|
|
506
|
+
- View task details
|
|
507
|
+
- Edit task
|
|
508
|
+
- Delete task
|
|
509
|
+
|
|
510
|
+
3. **Board Management**
|
|
511
|
+
- View kanban board
|
|
512
|
+
- Move task between columns
|
|
513
|
+
- Create new column
|
|
514
|
+
|
|
515
|
+
4. **Sprint Management**
|
|
516
|
+
- Create sprint
|
|
517
|
+
- Add tasks to sprint
|
|
518
|
+
- Complete sprint
|
|
519
|
+
|
|
520
|
+
5. **User Management (Admin)**
|
|
521
|
+
- View user list
|
|
522
|
+
- Change user role
|
|
523
|
+
- Suspend user
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Test Infrastructure
|
|
528
|
+
|
|
529
|
+
### Test Setup (`test/setup.ts`)
|
|
530
|
+
|
|
531
|
+
The setup file initializes:
|
|
532
|
+
|
|
533
|
+
1. **MSW Server** - Mocks API responses
|
|
534
|
+
2. **Next.js Mocks** - Router, cache, navigation
|
|
535
|
+
3. **Supabase Mocks** - Client initialization
|
|
536
|
+
4. **jsdom Environment** - DOM simulation
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { beforeAll, afterAll, afterEach, vi } from 'vitest';
|
|
540
|
+
import { server } from '@/lib/test-utils/server';
|
|
541
|
+
import '@testing-library/jest-dom';
|
|
542
|
+
|
|
543
|
+
// Start MSW server
|
|
544
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
|
545
|
+
afterEach(() => server.resetHandlers());
|
|
546
|
+
afterAll(() => server.close());
|
|
547
|
+
|
|
548
|
+
// Mock Next.js
|
|
549
|
+
vi.mock('next/navigation', () => ({
|
|
550
|
+
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
|
551
|
+
usePathname: () => '/dashboard',
|
|
552
|
+
useSearchParams: () => new URLSearchParams(),
|
|
553
|
+
}));
|
|
554
|
+
|
|
555
|
+
vi.mock('next/cache', () => ({
|
|
556
|
+
revalidatePath: vi.fn(),
|
|
557
|
+
revalidateTag: vi.fn(),
|
|
558
|
+
}));
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Test Fixtures (`test/fixtures.tsx`)
|
|
562
|
+
|
|
563
|
+
Provides mock data factories and seeding utilities:
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import type { Task, Board, Sprint, User } from '@/types';
|
|
567
|
+
|
|
568
|
+
// ============== Mock Data ==============
|
|
569
|
+
export const mockUser: User = {
|
|
570
|
+
id: 'user-001',
|
|
571
|
+
email: 'test@example.com',
|
|
572
|
+
name: 'Test User',
|
|
573
|
+
role: 'developer',
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
export const mockTasks: Task[] = [
|
|
577
|
+
{ id: 'task-001', ticketId: 'TEST-0001', title: 'First Task', priority: 'high', status: 'todo' },
|
|
578
|
+
{
|
|
579
|
+
id: 'task-002',
|
|
580
|
+
ticketId: 'TEST-0002',
|
|
581
|
+
title: 'Second Task',
|
|
582
|
+
priority: 'medium',
|
|
583
|
+
status: 'in_progress',
|
|
584
|
+
},
|
|
585
|
+
];
|
|
586
|
+
|
|
587
|
+
// ============== Factory Functions ==============
|
|
588
|
+
export function createMockTask(overrides: Partial<Task> = {}): Task {
|
|
589
|
+
return {
|
|
590
|
+
id: `task-${Date.now()}`,
|
|
591
|
+
ticketId: `TEST-${Math.floor(Math.random() * 9999)
|
|
592
|
+
.toString()
|
|
593
|
+
.padStart(4, '0')}`,
|
|
594
|
+
title: 'Mock Task',
|
|
595
|
+
description: '',
|
|
596
|
+
priority: 'medium',
|
|
597
|
+
status: 'todo',
|
|
598
|
+
type: 'task',
|
|
599
|
+
boardId: 'board-001',
|
|
600
|
+
createdAt: new Date().toISOString(),
|
|
601
|
+
updatedAt: new Date().toISOString(),
|
|
602
|
+
...overrides,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function createMockBoard(overrides: Partial<Board> = {}): Board {
|
|
607
|
+
/* ... */
|
|
608
|
+
}
|
|
609
|
+
export function createMockSprint(overrides: Partial<Sprint> = {}): Sprint {
|
|
610
|
+
/* ... */
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ============== Seeding Functions ==============
|
|
614
|
+
export function seedTasks(tasks: Task[] = mockTasks): void {
|
|
615
|
+
localStorage.setItem('pm_tasks', JSON.stringify(tasks));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function seedBoards(): void {
|
|
619
|
+
/* ... */
|
|
620
|
+
}
|
|
621
|
+
export function seedSprints(): void {
|
|
622
|
+
/* ... */
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function clearLocalStorage(): void {
|
|
626
|
+
localStorage.clear();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ============== Known Gaps ==============
|
|
630
|
+
// Document areas that don't have full test coverage yet
|
|
631
|
+
export const KNOWN_GAPS = {
|
|
632
|
+
dragAndDrop: 'Drag-and-drop testing requires special handling with react-dnd',
|
|
633
|
+
realTime: 'Real-time subscription testing not fully implemented',
|
|
634
|
+
fileUpload: 'File upload testing requires MSW file handling',
|
|
635
|
+
};
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### MSW Handlers (`lib/test-utils/handlers/`)
|
|
639
|
+
|
|
640
|
+
API mocking handlers organized by domain:
|
|
641
|
+
|
|
642
|
+
```
|
|
643
|
+
lib/test-utils/
|
|
644
|
+
├── handlers/
|
|
645
|
+
│ ├── index.ts # Combines all handlers
|
|
646
|
+
│ ├── tasks.ts # Task API handlers
|
|
647
|
+
│ ├── boards.ts # Board API handlers
|
|
648
|
+
│ ├── sprints.ts # Sprint API handlers
|
|
649
|
+
│ └── auth.ts # Auth API handlers
|
|
650
|
+
├── server.ts # MSW server setup
|
|
651
|
+
└── db.ts # Mock database state
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Example handler (`lib/test-utils/handlers/tasks.ts`):
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
import { http, HttpResponse } from 'msw';
|
|
658
|
+
import { mockTasks } from '@test/fixtures';
|
|
659
|
+
|
|
660
|
+
export const taskHandlers = [
|
|
661
|
+
http.get('*/rest/v1/tasks*', () => {
|
|
662
|
+
return HttpResponse.json(mockTasks);
|
|
663
|
+
}),
|
|
664
|
+
|
|
665
|
+
http.post('*/rest/v1/tasks', async ({ request }) => {
|
|
666
|
+
const body = await request.json();
|
|
667
|
+
const newTask = { id: `task-${Date.now()}`, ...body };
|
|
668
|
+
return HttpResponse.json(newTask, { status: 201 });
|
|
669
|
+
}),
|
|
670
|
+
|
|
671
|
+
http.delete('*/rest/v1/tasks*', () => {
|
|
672
|
+
return new HttpResponse(null, { status: 204 });
|
|
673
|
+
}),
|
|
674
|
+
];
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## When Tests Are Required
|
|
680
|
+
|
|
681
|
+
### Required Tests
|
|
682
|
+
|
|
683
|
+
| Change Type | Unit Test | E2E Test |
|
|
684
|
+
| ------------------------------------- | ---------------------- | ---------------------- |
|
|
685
|
+
| New component (complex) | ✅ Required | ⚠️ If critical path |
|
|
686
|
+
| New component (simple/presentational) | ⚠️ Recommended | ❌ Not required |
|
|
687
|
+
| New hook | ✅ Required | ❌ Not required |
|
|
688
|
+
| New server action | ⚠️ Recommended | ❌ Not required |
|
|
689
|
+
| New utility function | ✅ Required | ❌ Not required |
|
|
690
|
+
| New feature | ✅ Required | ✅ Required |
|
|
691
|
+
| Bug fix | ✅ Regression test | ⚠️ If E2E exists |
|
|
692
|
+
| Refactor | ✅ Existing tests pass | ✅ Existing tests pass |
|
|
693
|
+
|
|
694
|
+
### Definition of "Complex Component"
|
|
695
|
+
|
|
696
|
+
A component is considered complex if it has:
|
|
697
|
+
|
|
698
|
+
- State management (`useState`, `useReducer`)
|
|
699
|
+
- Side effects (`useEffect`)
|
|
700
|
+
- Event handlers that modify data
|
|
701
|
+
- Conditional rendering based on props/state
|
|
702
|
+
- Integration with context providers
|
|
703
|
+
- Form handling
|
|
704
|
+
|
|
705
|
+
### Test Coverage Targets
|
|
706
|
+
|
|
707
|
+
| Metric | Target |
|
|
708
|
+
| --------------- | ------ |
|
|
709
|
+
| Line Coverage | 70%+ |
|
|
710
|
+
| Branch Coverage | 60%+ |
|
|
711
|
+
| Critical Paths | 100% |
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Pre-Merge Checklist
|
|
716
|
+
|
|
717
|
+
Before creating a PR, ensure:
|
|
718
|
+
|
|
719
|
+
### Unit Tests
|
|
720
|
+
|
|
721
|
+
- [ ] All new functions/components have tests
|
|
722
|
+
- [ ] Tests cover happy path and error cases
|
|
723
|
+
- [ ] `pnpm test` passes locally
|
|
724
|
+
- [ ] No skipped tests (`.skip`) without justification
|
|
725
|
+
|
|
726
|
+
### E2E Tests
|
|
727
|
+
|
|
728
|
+
- [ ] Critical user journeys covered
|
|
729
|
+
- [ ] `data-testid` attributes added to new interactive elements
|
|
730
|
+
- [ ] `pnpm test:e2e` passes locally (or on CI)
|
|
731
|
+
|
|
732
|
+
### Test Quality
|
|
733
|
+
|
|
734
|
+
- [ ] Tests are isolated (don't depend on other tests)
|
|
735
|
+
- [ ] Tests clean up after themselves
|
|
736
|
+
- [ ] No flaky tests (tests that sometimes pass/fail)
|
|
737
|
+
- [ ] Descriptive test names (`should do X when Y`)
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## `data-testid` Conventions
|
|
742
|
+
|
|
743
|
+
Add `data-testid` attributes to elements that E2E tests need to interact with.
|
|
744
|
+
|
|
745
|
+
### Naming Pattern
|
|
746
|
+
|
|
747
|
+
```
|
|
748
|
+
<element>-<purpose>[-<identifier>]
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### Examples
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
// Buttons
|
|
755
|
+
<button data-testid="create-task-btn">Create</button>
|
|
756
|
+
<button data-testid="delete-task-btn">Delete</button>
|
|
757
|
+
<button data-testid="submit-form-btn">Submit</button>
|
|
758
|
+
|
|
759
|
+
// Inputs
|
|
760
|
+
<input data-testid="task-title-input" />
|
|
761
|
+
<input data-testid="email-input" />
|
|
762
|
+
<select data-testid="priority-select" />
|
|
763
|
+
|
|
764
|
+
// Cards/Items with IDs
|
|
765
|
+
<div data-testid={`task-card-${task.id}`}>...</div>
|
|
766
|
+
<div data-testid={`task-card-${task.ticketId}`}>...</div>
|
|
767
|
+
|
|
768
|
+
// Containers
|
|
769
|
+
<div data-testid="column-todo">...</div>
|
|
770
|
+
<div data-testid="column-in-progress">...</div>
|
|
771
|
+
<nav data-testid="main-sidebar">...</nav>
|
|
772
|
+
|
|
773
|
+
// Modal/Dialog
|
|
774
|
+
<div data-testid="task-dialog">...</div>
|
|
775
|
+
<div data-testid="confirm-dialog">...</div>
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### Anti-patterns
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
// ❌ Don't use generic IDs
|
|
782
|
+
<button data-testid="button">...</button>
|
|
783
|
+
<input data-testid="input" />
|
|
784
|
+
|
|
785
|
+
// ❌ Don't use CSS class-like naming
|
|
786
|
+
<div data-testid="task-card-container-wrapper">...</div>
|
|
787
|
+
|
|
788
|
+
// ❌ Don't use dynamic content in IDs (use stable identifiers)
|
|
789
|
+
<div data-testid={`task-${task.title.toLowerCase()}`}>...</div>
|
|
790
|
+
|
|
791
|
+
// ✅ Use stable identifiers
|
|
792
|
+
<div data-testid={`task-card-${task.id}`}>...</div>
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
## Related Documents
|
|
798
|
+
|
|
799
|
+
- [AGENT_EDITING_INSTRUCTIONS.md](AGENT_EDITING_INSTRUCTIONS.md) - Required files for each change type
|
|
800
|
+
- [TESTING_GUIDE.md](../TESTING_GUIDE.md) - Manual testing checklist
|
|
801
|
+
- [QUICK_START_TESTING.md](../QUICK_START_TESTING.md) - Getting started with testing
|