docs: map existing codebase
This commit is contained in:
397
.planning/codebase/TESTING.md
Normal file
397
.planning/codebase/TESTING.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user