# Testing Guide This document describes the testing approach for the Paper Trail Web application. ## Testing Stack | Tool | Purpose | |------|---------| | [Vitest](https://vitest.dev/) | Test runner and assertion library | | [React Testing Library](https://testing-library.com/react) | Component testing utilities | | [jsdom](https://github.com/jsdom/jsdom) | DOM simulation for Node.js | | [@testing-library/user-event](https://testing-library.com/user-event) | User interaction simulation | | [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) | Custom DOM matchers | ## Running Tests ```bash # Run tests in watch mode (development) pnpm test # Run tests once (CI) pnpm test:run # Run tests with UI pnpm test:ui ``` ## Test File Organization Tests are co-located with the code they test: ``` src/ ├── components/ │ ├── Header.tsx │ └── Header.test.tsx # Co-located test ├── utils/ │ ├── formatters.ts │ └── formatters.test.ts # Co-located test └── test/ └── setup.ts # Global test setup ``` ## Writing Tests ### Component Tests Use React Testing Library to test components: ```typescript import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect } from 'vitest'; import { MyComponent } from './MyComponent'; describe('MyComponent', () => { it('renders correctly', () => { render(); expect(screen.getByText('Hello')).toBeInTheDocument(); }); it('handles user interaction', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /submit/i })); expect(/* assertion */).toBe(/* expected */); }); }); ``` ### Testing with React Query Wrap components that use React Query: ```typescript import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render } from '@testing-library/react'; function renderWithQuery(ui: React.ReactElement) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); return render( {ui} ); } ``` ### Testing with React Router Wrap components that use routing: ```typescript import { MemoryRouter } from 'react-router-dom'; import { render } from '@testing-library/react'; function renderWithRouter(ui: React.ReactElement, { route = '/' } = {}) { return render( {ui} ); } ``` ### Mocking API Calls Mock the API service for isolated testing: ```typescript import { vi } from 'vitest'; import { api } from '../services/api'; vi.mock('../services/api'); describe('ComponentWithAPI', () => { it('displays data from API', async () => { vi.mocked(api.searchPoliticians).mockResolvedValue([ { id: '1', name: 'Test Politician' }, ]); render(); await waitFor(() => { expect(screen.getByText('Test Politician')).toBeInTheDocument(); }); }); }); ``` ## Testing Patterns ### Query by Role (Preferred) Prefer accessible queries that reflect how users interact with the UI: ```typescript // Good - queries by accessible role screen.getByRole('button', { name: /submit/i }); screen.getByRole('textbox', { name: /search/i }); screen.getByRole('heading', { level: 1 }); // Avoid - queries by implementation details screen.getByTestId('submit-button'); screen.getByClassName('btn-primary'); ``` ### Async Operations Use `waitFor` or `findBy` queries for async operations: ```typescript // Option 1: waitFor await waitFor(() => { expect(screen.getByText('Loaded')).toBeInTheDocument(); }); // Option 2: findBy (combines getBy + waitFor) const element = await screen.findByText('Loaded'); expect(element).toBeInTheDocument(); ``` ### User Events Use `userEvent` over `fireEvent` for realistic interactions: ```typescript const user = userEvent.setup(); // Typing await user.type(input, 'hello'); // Clicking await user.click(button); // Keyboard navigation await user.keyboard('{Enter}'); await user.tab(); ``` ## Test Configuration ### Vitest Config (`vitest.config.ts`) ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], css: true, }, }); ``` ### Test Setup (`src/test/setup.ts`) ```typescript import '@testing-library/jest-dom/vitest'; import { cleanup } from '@testing-library/react'; import { afterEach } from 'vitest'; // Cleanup after each test afterEach(() => { cleanup(); }); ``` ## What to Test ### Component Testing Priorities 1. **User interactions**: Clicks, typing, form submissions 2. **Conditional rendering**: Show/hide logic based on state 3. **Data display**: Correct rendering of fetched data 4. **Error states**: Error messages and fallbacks 5. **Loading states**: Loading indicators 6. **Accessibility**: Keyboard navigation, ARIA attributes ### Skip Testing - Implementation details (internal state, private methods) - Third-party library behavior - Styling/CSS (use visual regression tests if needed) ## Example Tests ### Testing a Search Component ```typescript import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi, describe, it, expect, beforeEach } from 'vitest'; import { SearchInput } from './SearchInput'; describe('SearchInput', () => { const mockOnSearch = vi.fn(); beforeEach(() => { mockOnSearch.mockClear(); }); it('calls onSearch when user types and submits', async () => { const user = userEvent.setup(); render(); const input = screen.getByRole('textbox', { name: /search/i }); await user.type(input, 'test query'); await user.keyboard('{Enter}'); expect(mockOnSearch).toHaveBeenCalledWith('test query'); }); it('debounces search calls', async () => { const user = userEvent.setup(); render(); const input = screen.getByRole('textbox'); await user.type(input, 'abc'); // Should not call immediately expect(mockOnSearch).not.toHaveBeenCalled(); // Wait for debounce await waitFor(() => { expect(mockOnSearch).toHaveBeenCalledWith('abc'); }); }); }); ``` ### Testing Error States ```typescript describe('PoliticianProfile', () => { it('displays error message when API fails', async () => { vi.mocked(api.getPolitician).mockRejectedValue(new Error('Not found')); renderWithQuery(); await waitFor(() => { expect(screen.getByText(/error/i)).toBeInTheDocument(); }); }); }); ``` ## CI Integration Tests run automatically on every PR and push to main: ```yaml # .github/workflows/ci.yml - name: Run tests run: pnpm run test:run ``` ## Debugging Tests ### Vitest UI Run `pnpm test:ui` for an interactive test dashboard. ### Debug Output Use `screen.debug()` to print the current DOM: ```typescript it('debugging example', () => { render(); screen.debug(); // Prints current DOM to console }); ``` ### Playground Use Testing Library's `screen.logTestingPlaygroundURL()` to generate a URL for the Testing Playground tool.