Skip to content

Commit 6889838

Browse files
authored
Testing methodology (#132)
* add api layer tests example * Add repo layer tests * Fix lint and typing issues * prevent updates from showing multiple times
1 parent fb3ff61 commit 6889838

21 files changed

Lines changed: 3544 additions & 84 deletions

package-lock.json

Lines changed: 1687 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"@types/node": "^22.17.1",
8585
"@types/react": "^18.2.15",
8686
"@types/react-dom": "^18.2.7",
87+
"@types/sqlite3": "^3.1.11",
8788
"@typescript-eslint/eslint-plugin": "^8.19.1",
8889
"@typescript-eslint/parser": "^8.19.1",
8990
"@vitejs/plugin-react": "^4.2.1",
@@ -94,6 +95,7 @@
9495
"eslint-plugin-import": "^2.31.0",
9596
"eslint-plugin-prettier": "^5.2.1",
9697
"postcss": "^8.4.49",
98+
"sqlite3": "^5.1.7",
9799
"tailwindcss": "^3.4.17",
98100
"typescript": "^5.4.5",
99101
"typescript-eslint": "^8.19.1",

src/CLAUDE.md

Lines changed: 236 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,14 +280,247 @@ export default function FlowSessionList() {
280280
- Use generic types for repository patterns
281281
- Leverage discriminated unions for state management
282282

283-
## Testing
283+
## Required Development Validation
284+
285+
**CRITICAL: Always run these commands before considering development complete:**
284286

285-
Run tests with:
286287
```bash
288+
# 1. REQUIRED: Run linting and fix any errors
289+
npm run lint
290+
291+
# 2. REQUIRED: Fix any auto-fixable issues
292+
npm run format
293+
294+
# 3. REQUIRED: Ensure TypeScript compilation and build succeed
295+
npm run build
296+
297+
# 4. RECOMMENDED: Run tests to validate functionality
287298
npm test
288299
```
289300

290-
Test files should be placed alongside the code they test with `.test.ts` or `.test.tsx` extensions.
301+
**All commands must pass without errors before:**
302+
- Committing code changes
303+
- Creating pull requests
304+
- Considering a task complete
305+
306+
## Testing
307+
308+
### Testing Strategy
309+
310+
Tests are written using Vitest and follow a layered approach matching our data architecture:
311+
312+
**Test Categories:**
313+
- **Repository Layer**: Test database operations and SQL queries
314+
- **API Layer**: Test business logic and data transformations
315+
- **Hooks Layer**: Generally not tested (see note below)
316+
317+
### API Layer Testing Patterns
318+
319+
When testing API functions, use this framework based on the function's complexity:
320+
321+
**1. Passthrough Functions (Simple Success Test)**
322+
Functions that only call repository methods without logic:
323+
324+
```typescript
325+
describe('getFocusSchedules', () => {
326+
it('should return schedules successfully', async () => {
327+
const mockSchedules = [{ id: '1', label: 'Test' }]
328+
vi.mocked(FocusScheduleRepo.getFocusSchedules).mockResolvedValue(mockSchedules as any)
329+
330+
const result = await FocusScheduleApi.getFocusSchedules()
331+
332+
expect(result).toBe(mockSchedules)
333+
})
334+
})
335+
```
336+
337+
**2. Business Logic Functions**
338+
Functions with conditional logic (if statements, loops, validations):
339+
340+
```typescript
341+
// Test each logical branch
342+
it('should throw error when workflow not found', async () => {
343+
vi.mocked(WorkflowApi.getWorkflowById).mockResolvedValue(null)
344+
345+
await expect(
346+
FocusScheduleApi.createFocusSchedule('invalid-workflow', mockTime, mockRecurrence)
347+
).rejects.toThrow('Workflow not found')
348+
})
349+
```
350+
351+
**3. Data Transformation Functions**
352+
Functions that reshape, aggregate, or transform data:
353+
354+
```typescript
355+
// Test data shape and transformation accuracy
356+
it('should transform parameters correctly with all fields', async () => {
357+
await FocusScheduleApi.createFocusSchedule('workflow-1', mockTime, mockRecurrence, 'Label')
358+
359+
expect(FocusScheduleRepo.createFocusSchedule).toHaveBeenCalledWith({
360+
id: 'test-uuid-123',
361+
label: 'Label',
362+
scheduled_time: '2024-01-15T09:00:00.000Z',
363+
workflow_id: 'workflow-1',
364+
recurrence_settings: '{"type":"daily"}',
365+
is_active: 1,
366+
})
367+
})
368+
```
369+
370+
**4. Complex Functions (Logic + Transformation)**
371+
Test both business logic flows AND data shape:
372+
373+
```typescript
374+
describe('formatScheduleDisplay', () => {
375+
// Test logic branches
376+
it('should format daily recurring schedule', () => {
377+
const schedule = { ...baseSchedule, recurrence: { type: 'daily' } }
378+
const result = FocusScheduleApi.formatScheduleDisplay(schedule)
379+
expect(result).toMatch(/Daily at \d{1,2}:\d{2} (AM|PM)/)
380+
})
381+
382+
// Test edge cases
383+
it('should handle edge cases gracefully', () => {
384+
const invalidSchedule = { ...baseSchedule, recurrence: { type: 'weekly', daysOfWeek: [] } }
385+
expect(FocusScheduleApi.formatScheduleDisplay(invalidSchedule)).toBe('Invalid schedule')
386+
})
387+
})
388+
```
389+
390+
### Repository Layer Testing Patterns
391+
392+
Repository tests focus on database operations and SQL query behavior. **Priority areas for testing:**
393+
394+
**1. WHERE Conditions**
395+
Test filtering logic to ensure correct data is returned:
396+
397+
```typescript
398+
it('should only return active records (is_active = 1)', async () => {
399+
// Insert mix of active/inactive records
400+
await testDb.execute(`
401+
INSERT INTO focus_schedule (id, workflow_id, is_active)
402+
VALUES ('active-1', 'workflow-1', 1), ('inactive-1', 'workflow-1', 0)
403+
`)
404+
405+
const results = await FocusScheduleRepo.getFocusSchedules()
406+
407+
expect(results.every(r => r.is_active === 1)).toBe(true)
408+
expect(results.find(r => r.id === 'inactive-1')).toBeUndefined()
409+
})
410+
```
411+
412+
**2. ORDER BY Clauses**
413+
Test sorting behavior by inserting data in non-sorted order:
414+
415+
```typescript
416+
it('should order by scheduled_time ASC', async () => {
417+
// Insert in non-chronological order
418+
await testDb.execute(`
419+
INSERT INTO focus_schedule (id, scheduled_time, workflow_id, is_active)
420+
VALUES
421+
('late', '2024-01-17T15:00:00Z', 'workflow-1', 1),
422+
('early', '2024-01-15T09:00:00Z', 'workflow-1', 1),
423+
('middle', '2024-01-16T12:00:00Z', 'workflow-1', 1)
424+
`)
425+
426+
const results = await FocusScheduleRepo.getFocusSchedules()
427+
428+
const times = results.map(r => r.scheduled_time)
429+
expect(times).toEqual([...times].sort()) // Should be sorted
430+
})
431+
```
432+
433+
**3. Data Transformations**
434+
Test field renaming, JSON parsing, and virtual field creation:
435+
436+
```typescript
437+
it('should parse JSON and create virtual fields', async () => {
438+
await testDb.execute(`
439+
INSERT INTO focus_schedule (id, recurrence_settings, workflow_id, is_active)
440+
VALUES ('test', '{"type":"daily","daysOfWeek":[1,2,3]}', 'workflow-1', 1)
441+
`)
442+
443+
const result = await FocusScheduleRepo.getFocusScheduleById('test')
444+
445+
// JSON parsed into virtual field
446+
expect(result.recurrence).toEqual({ type: 'daily', daysOfWeek: [1,2,3] })
447+
})
448+
449+
it('should add fields from JOINs', async () => {
450+
await testDb.execute(`
451+
INSERT INTO workflow (id, name, settings) VALUES ('wf-1', 'Deep Work', '{}');
452+
INSERT INTO focus_schedule (id, workflow_id, is_active) VALUES ('fs-1', 'wf-1', 1);
453+
`)
454+
455+
const results = await FocusScheduleRepo.getFocusSchedulesWithWorkflow()
456+
457+
expect(results[0]).toHaveProperty('workflow_name', 'Deep Work')
458+
})
459+
```
460+
461+
**Repository Test Setup Pattern:**
462+
463+
```typescript
464+
import { createEbbTestDatabase } from '@/lib/utils/testDb.util'
465+
import { getEbbDb } from '../ebbDb'
466+
467+
// Mock the database connection
468+
vi.mock('../ebbDb', () => ({
469+
getEbbDb: vi.fn()
470+
}))
471+
472+
// Mock UUID generation for consistent tests
473+
Object.defineProperty(globalThis, 'self', {
474+
value: { crypto: { randomUUID: vi.fn(() => 'test-uuid-123') } }
475+
})
476+
477+
describe('SomeRepo', () => {
478+
let testDb: Database
479+
480+
beforeEach(async () => {
481+
testDb = await createEbbTestDatabase() // Schema already applied
482+
vi.mocked(getEbbDb).mockResolvedValue(testDb)
483+
})
484+
485+
afterEach(async () => {
486+
if (testDb) await testDb.close()
487+
vi.clearAllMocks()
488+
})
489+
})
490+
```
491+
492+
### React Query Hooks Testing Policy
493+
494+
**Generally, do not test React Query hooks** unless otherwise directed
495+
### Test File Organization
496+
497+
```
498+
src/api/ebbApi/__tests__/
499+
├── focusScheduleApi.test.ts # API layer tests
500+
└── ...
501+
502+
src/db/ebb/__tests__/
503+
├── focusScheduleRepo.test.ts # Repository layer tests
504+
└── ...
505+
506+
# Note: src/api/hooks/ tests omitted per policy above
507+
```
508+
509+
### Running Tests
510+
511+
```bash
512+
# Run all tests
513+
npm test
514+
515+
# Run specific test file
516+
npm test -- src/api/ebbApi/__tests__/focusScheduleApi.test.ts
517+
518+
# Run tests in watch mode
519+
npm test -- --watch
520+
521+
# Run tests with coverage
522+
npm test -- --coverage
523+
```
291524

292525

293526
## Other preferences

0 commit comments

Comments
 (0)