Testing Guide

This document explains how to run and write tests for the frontend application.

Quick Start

# Run all tests once
npm run test:run

# Run tests in watch mode (re-runs on file changes)
npm test

# Run tests with coverage report
npm run test:coverage

# Run tests with UI dashboard
npm run test:ui

# Run specific test file
npm test src/components/__tests__/LoadingSpinner.test.tsx

# Run tests matching a pattern
npm test -- --grep "should render"

Test Structure

Test Files Location

  • Components: src/components/__tests__/ComponentName.test.tsx
  • Utils: src/utils/__tests__/utilName.test.ts
  • Lib: src/lib/__tests__/libName.test.ts
  • Hooks: src/hooks/__tests__/hookName.test.ts

Test Setup

Tests use Vitest with the following tools:

  • @testing-library/react - React component testing
  • @testing-library/jest-dom - DOM matchers
  • @testing-library/user-event - User interaction simulation
  • jsdom - DOM environment for tests

Writing Tests

1. Basic Component Test

import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MyComponent } from '../MyComponent'

describe('MyComponent', () => {
  it('should render correctly', () => {
    render(<MyComponent text="Hello" />)
    
    expect(screen.getByText('Hello')).toBeInTheDocument()
  })

  it('should handle click events', async () => {
    const user = userEvent.setup()
    const handleClick = vi.fn()
    
    render(<MyComponent onClick={handleClick} />)
    
    await user.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledOnce()
  })
})

2. Testing Hooks

import { describe, it, expect } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useCounter } from '../useCounter'

describe('useCounter', () => {
  it('should increment counter', () => {
    const { result } = renderHook(() => useCounter(0))
    
    act(() => {
      result.current.increment()
    })
    
    expect(result.current.count).toBe(1)
  })
})

3. Testing Utilities

import { describe, it, expect } from 'vitest'
import { formatDate } from '../dateUtils'

describe('dateUtils', () => {
  it('should format dates correctly', () => {
    const date = new Date('2023-12-25')
    expect(formatDate(date)).toBe('December 25, 2023')
  })
})

4. Testing Async Functions

import { describe, it, expect, vi } from 'vitest'
import { fetchUserData } from '../api'

// Mock fetch
global.fetch = vi.fn()

describe('fetchUserData', () => {
  it('should fetch user data successfully', async () => {
    const mockData = { id: 1, name: 'John' }
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockData,
    })

    const result = await fetchUserData(1)
    expect(result).toEqual(mockData)
  })
})

Mocking

Global Mocks (Available Automatically)

  • Firebase - Mocked in src/test/setup.ts
  • Next.js Router - Mocked navigation functions
  • Langfuse - Mocked tracing functionality
  • fetch - Global fetch mock
  • IntersectionObserver - Mocked for component tests
  • ResizeObserver - Mocked for component tests

Custom Mocks in Tests

import { vi } from 'vitest'

// Mock a module
vi.mock('../api', () => ({
  fetchData: vi.fn().mockResolvedValue({ data: 'test' })
}))

// Mock a function
const mockFunction = vi.fn()
mockFunction.mockReturnValue('mocked return')

// Mock with different implementations
mockFunction
  .mockReturnValueOnce('first call')
  .mockReturnValueOnce('second call')
  .mockReturnValue('default')

Common Testing Patterns

1. Testing Form Components

it('should submit form with correct data', async () => {
  const user = userEvent.setup()
  const handleSubmit = vi.fn()
  
  render(<ContactForm onSubmit={handleSubmit} />)
  
  await user.type(screen.getByLabelText(/name/i), 'John Doe')
  await user.type(screen.getByLabelText(/email/i), 'john@example.com')
  await user.click(screen.getByRole('button', { name: /submit/i }))
  
  expect(handleSubmit).toHaveBeenCalledWith({
    name: 'John Doe',
    email: 'john@example.com'
  })
})

2. Testing Loading States

it('should show loading spinner while fetching', async () => {
  render(<UserProfile userId="123" />)
  
  expect(screen.getByText(/loading/i)).toBeInTheDocument()
  
  await waitFor(() => {
    expect(screen.queryByText(/loading/i)).not.toBeInTheDocument()
  })
})

3. Testing Error Handling

it('should display error message on fetch failure', async () => {
  fetch.mockRejectedValueOnce(new Error('Network error'))
  
  render(<UserProfile userId="123" />)
  
  await waitFor(() => {
    expect(screen.getByText(/error loading user/i)).toBeInTheDocument()
  })
})

4. Testing Component Props

it('should apply custom className', () => {
  const { container } = render(<Button className="custom-class" />)
  
  expect(container.firstElementChild).toHaveClass('custom-class')
})

Best Practices

1. Test Behavior, Not Implementation

// ❌ Bad - testing implementation
expect(component.state.loading).toBe(true)

// ✅ Good - testing behavior
expect(screen.getByText(/loading/i)).toBeInTheDocument()

2. Use Descriptive Test Names

// ❌ Bad
it('should work', () => {})

// ✅ Good
it('should display error message when API call fails', () => {})

3. Use Screen Queries Appropriately

// ✅ Preferred queries (in order)
screen.getByRole('button', { name: /submit/i })  // Most accessible
screen.getByLabelText(/email/i)                  // For form inputs
screen.getByText(/welcome/i)                     // For text content
screen.getByTestId('submit-button')              // Last resort

4. Clean Up After Tests

afterEach(() => {
  vi.clearAllMocks()       // Clear mock calls
  cleanup()               // Clean up DOM (automatic with setup)
})
describe('UserProfile', () => {
  describe('when user exists', () => {
    it('should display user name', () => {})
    it('should display user email', () => {})
  })

  describe('when user does not exist', () => {
    it('should display not found message', () => {})
  })
})

Configuration Files

  • vitest.config.ts - Main Vitest configuration
  • src/test/setup.ts - Test setup and global mocks
  • package.json - Test scripts

Coverage Reports

Generate coverage reports to see what code is tested:

npm run test:coverage

Coverage reports will be generated in the coverage/ directory and displayed in the terminal.

CI/CD Integration

Add this to your GitHub Actions workflow:

- name: Run Tests
  run: npm run test:run

- name: Generate Coverage
  run: npm run test:coverage

Example Test Files

See these files for examples:

  • src/utils/__tests__/vacChat.test.ts - Complex async testing with mocks
  • src/components/__tests__/LoadingSpinner.test.tsx - Component testing
  • src/lib/__tests__/utils.test.ts - Utility function testing

Debugging Tests

Run Single Test

npm test -- --grep "specific test name"

Enable Debugging

npm test -- --reporter=verbose

Use Test UI

npm run test:ui

This opens a web interface where you can see test results, coverage, and debug failing tests.

Common Errors and Solutions

1. “ReferenceError: HTMLElement is not defined”

  • Solution: Make sure environment: 'jsdom' is set in vitest.config.ts

2. “Cannot find module ‘@testing-library/jest-dom’”

  • Solution: Import is handled in src/test/setup.ts, make sure it’s configured correctly

3. “fetch is not defined”

  • Solution: fetch is mocked globally in src/test/setup.ts

4. Tests are flaky or timing out

  • Solution: Use waitFor for async operations and increase timeout if needed

Happy testing! 🧪