9.6 KiB
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 servernpm run build- Production buildnpm run lint- ESLint onlynpm 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.tsxalongsideComponentName.tsx - Recommendation: Place
hookName.test.tsalongsidehookName.ts
Naming:
.test.tsor.test.tsxsuffix 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:
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
describeper hook/component - Nested
describeblocks for related test groups - Each test file focuses on single module
- Use of
beforeEachfor setup,afterEachfor cleanup
Mocking
Framework:
- Not yet configured
- Recommended: Vitest with
vimodule for mocking - Alternative: Mock Service Worker (MSW) for API mocking
Patterns to implement:
Supabase Client Mocking:
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:
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/orsrc/__tests__/factories/
Recommended pattern:
// 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:
npm run test:coverage
Recommended coverage config (vitest.config.ts):
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 logicuseAuth()session managementformatCurrency()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
QuickAddPickerwith 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:
it('should fetch categories', async () => {
const { result } = renderHook(() => useCategories())
await waitFor(() => {
expect(result.current.categories).toHaveLength(1)
})
})
- Alternative with MSW:
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:
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:
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/reactfor component testing - Use
@testing-library/user-eventfor user interactions
Component test example structure:
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:
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:
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:
{
"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):
- Hook mutations (
useBudgets.createBudget,useBudgets.generateFromTemplate) - Authentication flow (
useAuth.signIn,useAuth.signOut) - Complex component state (
QuickAddPickerdialog flow) - Validation logic (form field checks)
Medium Priority (Data Access):
- Category queries and filtering
- Budget item CRUD operations
- Template copying logic
- Sorting and ordering
Lower Priority (UI/Display):
- Component rendering
- Conditional displays
- Icon rendering
- 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.