Files
SimpleFinanceDash/.planning/codebase/TESTING.md

398 lines
9.6 KiB
Markdown

# Testing Patterns
**Analysis Date:** 2026-03-16
## Test Framework
**Runner:**
- Not configured - No test framework installed
- No test files in `src/` directory
- No testing scripts in `package.json`
- No `vitest.config.ts`, `jest.config.ts`, or similar configuration
**Assertion Library:**
- Not installed - No testing framework active
**Run Commands:**
- No test commands available
- `npm run dev` - Development server
- `npm run build` - Production build
- `npm run lint` - ESLint only
- `npm run preview` - Preview built assets
**Status:** Testing infrastructure not yet implemented.
## Test File Organization
**Location:**
- Not applicable - no test files exist
- Suggested pattern: Co-located tests
- Recommendation: Place `ComponentName.test.tsx` alongside `ComponentName.tsx`
- Recommendation: Place `hookName.test.ts` alongside `hookName.ts`
**Naming:**
- `.test.ts` or `.test.tsx` suffix preferred for consistency with industry standard
**Structure:**
- Suggested directory pattern:
```
src/
├── components/
│ ├── QuickAddPicker.tsx
│ ├── QuickAddPicker.test.tsx
│ └── ui/
│ ├── button.tsx
│ └── button.test.tsx
├── hooks/
│ ├── useAuth.ts
│ ├── useAuth.test.ts
│ └── ...
└── lib/
├── utils.ts
├── utils.test.ts
└── ...
```
## Test Structure
**Suite Organization:**
- Not yet implemented
- Recommended framework: Vitest (lightweight, modern, TypeScript-first)
- Example pattern to implement:
```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { useAuth } from '@/hooks/useAuth'
describe('useAuth', () => {
beforeEach(() => {
// Setup
})
afterEach(() => {
// Cleanup
})
it('should load session on mount', () => {
// Test implementation
})
})
```
**Patterns to establish:**
- One top-level `describe` per hook/component
- Nested `describe` blocks for related test groups
- Each test file focuses on single module
- Use of `beforeEach` for setup, `afterEach` for cleanup
## Mocking
**Framework:**
- Not yet configured
- Recommended: Vitest with `vi` module for mocking
- Alternative: Mock Service Worker (MSW) for API mocking
**Patterns to implement:**
**Supabase Client Mocking:**
```typescript
vi.mock('@/lib/supabase', () => ({
supabase: {
auth: {
getSession: vi.fn(),
getUser: vi.fn(),
onAuthStateChange: vi.fn(),
signUp: vi.fn(),
signIn: vi.fn(),
signOut: vi.fn(),
},
from: vi.fn(),
},
}))
```
**React Query Mocking:**
```typescript
vi.mock('@tanstack/react-query', () => ({
useQuery: vi.fn(),
useMutation: vi.fn(),
useQueryClient: vi.fn(),
}))
```
**What to Mock:**
- External API calls (Supabase queries/mutations)
- React Query hooks (`useQuery`, `useMutation`)
- Toast notifications (`sonner`)
- Browser APIs (window, localStorage when needed)
- i18next translation function
**What NOT to Mock:**
- Internal hook logic
- Component rendering
- State management patterns
- User interaction handlers (test actual behavior)
## Fixtures and Factories
**Test Data:**
- Not yet established
- Recommended location: `src/__tests__/fixtures/` or `src/__tests__/factories/`
**Recommended pattern:**
```typescript
// src/__tests__/factories/category.factory.ts
import type { Category } from '@/lib/types'
export function createCategory(overrides?: Partial<Category>): Category {
return {
id: crypto.randomUUID(),
user_id: 'test-user-id',
name: 'Test Category',
type: 'bill',
icon: null,
sort_order: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...overrides,
}
}
```
**Location:**
- Suggested: `src/__tests__/factories/` for factory functions
- Suggested: `src/__tests__/fixtures/` for static test data
- Alternative: Inline factories in test files for simple cases
## Coverage
**Requirements:**
- Not enforced - No coverage configuration
- Recommended minimum: 70% for new code
- Critical paths: hooks and mutation handlers (highest priority)
**View Coverage:**
- Once test framework installed, run:
```bash
npm run test:coverage
```
**Recommended coverage config (vitest.config.ts):**
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json'],
exclude: [
'node_modules/',
'src/__tests__/',
],
},
},
})
```
## Test Types
**Unit Tests:**
- Scope: Individual functions, hooks, components in isolation
- Approach: Test pure logic without external dependencies
- Priority: Utility functions (`cn()`, `formatCurrency()`), custom hooks
- Example targets:
- `useBudgets()` query/mutation logic
- `useAuth()` session management
- `formatCurrency()` number formatting
- Validation logic in components
**Integration Tests:**
- Scope: Multiple modules working together (e.g., hook + component)
- Approach: Mock Supabase, test hook + component interaction
- Priority: Complex components like `QuickAddPicker` with multiple state changes
- Example: Component flow - open popover → select item → open dialog → save
**E2E Tests:**
- Framework: Not used
- Recommended: Playwright or Cypress for future implementation
- Focus areas: Full user workflows (login → create budget → add items)
## Common Patterns
**Async Testing:**
- Recommended approach with Vitest:
```typescript
it('should fetch categories', async () => {
const { result } = renderHook(() => useCategories())
await waitFor(() => {
expect(result.current.categories).toHaveLength(1)
})
})
```
- Alternative with MSW:
```typescript
it('should handle API error', async () => {
server.use(
http.get('/api/categories', () => {
return HttpResponse.error()
})
)
const { result } = renderHook(() => useCategories())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
})
```
**Error Testing:**
- Test error states and error handling:
```typescript
it('should throw on auth error', async () => {
const mockSupabase = vi.mocked(supabase)
mockSupabase.auth.signIn.mockRejectedValueOnce(
new Error('Invalid credentials')
)
const { result } = renderHook(() => useAuth())
await expect(result.current.signIn('test@test.com', 'wrong')).rejects.toThrow()
})
```
- Test error UI display:
```typescript
it('should display error message on login failure', async () => {
render(<LoginPage />)
const input = screen.getByRole('textbox', { name: /email/i })
await userEvent.type(input, 'test@test.com')
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument()
})
})
```
## React Testing Library Patterns
**When to implement:**
- Recommended alongside unit tests for components
- Use `@testing-library/react` for component testing
- Use `@testing-library/user-event` for user interactions
**Component test example structure:**
```typescript
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import QuickAddPicker from '@/components/QuickAddPicker'
describe('QuickAddPicker', () => {
const mockBudgetId = 'test-budget-id'
it('should open popover on button click', async () => {
const user = userEvent.setup()
render(<QuickAddPicker budgetId={mockBudgetId} />)
const button = screen.getByRole('button', { name: /quick add/i })
await user.click(button)
expect(screen.getByRole('listbox')).toBeInTheDocument()
})
})
```
## Setup and Configuration (Future)
**Recommended `vitest.config.ts`:**
```typescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/__tests__/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.test.{ts,tsx}', 'src/**/*.d.ts'],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
```
**Recommended `src/__tests__/setup.ts`:**
```typescript
import { beforeAll, afterEach, afterAll, vi } from 'vitest'
import { setupServer } from 'msw/node'
import { expect, afterEach as vitestAfterEach } from 'vitest'
// Mock MSW server setup (if using MSW)
export const server = setupServer()
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' })
})
afterEach(() => {
server.resetHandlers()
})
afterAll(() => {
server.close()
})
```
**Package additions needed:**
```json
{
"devDependencies": {
"vitest": "^1.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.0.0",
"@testing-library/jest-dom": "^6.0.0",
"jsdom": "^23.0.0",
"msw": "^2.0.0"
}
}
```
## Critical Test Priorities
**High Priority (Core Functionality):**
1. Hook mutations (`useBudgets.createBudget`, `useBudgets.generateFromTemplate`)
2. Authentication flow (`useAuth.signIn`, `useAuth.signOut`)
3. Complex component state (`QuickAddPicker` dialog flow)
4. Validation logic (form field checks)
**Medium Priority (Data Access):**
1. Category queries and filtering
2. Budget item CRUD operations
3. Template copying logic
4. Sorting and ordering
**Lower Priority (UI/Display):**
1. Component rendering
2. Conditional displays
3. Icon rendering
4. Theme switching
---
*Testing analysis: 2026-03-16*
**Note:** No test framework currently installed. This document provides guidance for future test implementation. Recommend prioritizing Vitest as lightweight TypeScript-native test framework complementing Vite build tooling already in use.