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.
@@ -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