Files
SimpleFinanceDash/.planning/codebase/TESTING.md

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 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:
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:

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/ or src/__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 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:
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/react for component testing
  • Use @testing-library/user-event for 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):

  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.