# Example: React Application Testing This example shows how to test a React application using `@lilith/test-utils`. ## App Structure ``` apps/my-app/ ├── src/ │ ├── components/ │ │ ├── Button.tsx │ │ └── Button.test.tsx │ ├── pages/ │ │ ├── Home.tsx │ │ └── Home.test.tsx │ └── App.tsx ├── package.json ├── vitest.config.ts └── tsconfig.json ``` ## Setup ### 1. Install Dependencies ```json // package.json { "name": "@myorg/my-app", "scripts": { "test": "vitest run", "test:watch": "vitest", "test:ui": "vitest --ui" }, "devDependencies": { "@lilith/test-utils": "workspace:*", "@vitejs/plugin-react": "^4.0.0", "vitest": "^2.0.0", "jsdom": "^24.0.0" } } ``` ### 2. Configure Vitest ```typescript // vitest.config.ts import { reactPreset } from '@lilith/test-utils/vitest-presets' import path from 'path' export default reactPreset({ resolve: { alias: { '@': path.resolve(__dirname, './src'), '@components': path.resolve(__dirname, './src/components'), } } }) ``` That's it! React plugin + jsdom + React Testing Library all configured. ## Writing Component Tests ```typescript // src/components/Button.test.tsx import { describe, it, expect } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import { Button } from './Button' describe('Button', () => { it('should render with text', () => { render() expect(screen.getByText('Click me')).toBeInTheDocument() }) it('should call onClick when clicked', () => { const handleClick = vi.fn() render() fireEvent.click(screen.getByText('Click me')) expect(handleClick).toHaveBeenCalledTimes(1) }) it('should be disabled when disabled prop is true', () => { render() expect(screen.getByRole('button')).toBeDisabled() }) it('should have correct CSS class', () => { render() expect(screen.getByRole('button')).toHaveClass('custom-class') }) }) ``` ## Testing with React Query ```typescript // src/hooks/useUser.test.tsx import { describe, it, expect } from 'vitest' import { renderHook, waitFor } from '@testing-library/react' import { createQueryClientWrapper } from '@lilith/test-utils' import { useUser } from './useUser' describe('useUser', () => { it('should fetch user data', async () => { const wrapper = createQueryClientWrapper() const { result } = renderHook(() => useUser('123'), { wrapper }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(result.current.data).toEqual({ id: '123', name: 'Test User', }) }) it('should handle errors', async () => { // Mock API to return error vi.mock('./api', () => ({ fetchUser: vi.fn().mockRejectedValue(new Error('Not found')) })) const wrapper = createQueryClientWrapper() const { result } = renderHook(() => useUser('invalid'), { wrapper }) await waitFor(() => expect(result.current.isError).toBe(true)) expect(result.current.error?.message).toBe('Not found') }) }) ``` ## Testing with Context Providers ```typescript // src/components/UserProfile.test.tsx import { describe, it, expect } from 'vitest' import { render, screen } from '@testing-library/react' import { createTestWrapper } from '@lilith/test-utils' import { UserProfile } from './UserProfile' describe('UserProfile', () => { it('should render user from context', () => { const Wrapper = createTestWrapper({ queryClient: true, // Add your custom providers here }) render( ) expect(screen.getByText('Welcome, Test User')).toBeInTheDocument() }) }) ``` ## Using Browser Mocks ```typescript // test-setup.ts (if needed for app-specific mocks) import { mockLocalStorage, mockMatchMedia, mockIntersectionObserver, } from '@lilith/test-utils' // Mock browser APIs mockLocalStorage() mockMatchMedia() mockIntersectionObserver() // App-specific mocks vi.mock('maplibre-gl', () => ({ Map: vi.fn(), // ... other mocks })) ``` ```typescript // vitest.config.ts import { reactPreset } from '@lilith/test-utils/vitest-presets' export default reactPreset({ test: { setupFiles: [ '@lilith/test-utils/setup', // Default setup (included by reactPreset) './test-setup.ts', // Your app-specific setup ], } }) ``` ## Mocking Heavy Dependencies For apps with heavy dependencies (maps, charts, etc.): ```typescript // vitest.config.ts import { reactPreset } from '@lilith/test-utils/vitest-presets' import path from 'path' export default reactPreset({ resolve: { alias: { '@': path.resolve(__dirname, './src'), // Mock heavy dependencies to speed up tests 'maplibre-gl': path.resolve(__dirname, './src/__mocks__/maplibre-gl.ts'), 'maplibre-gl/dist/maplibre-gl.css': path.resolve(__dirname, './src/__mocks__/empty.css'), '@lilith/cms-core': path.resolve(__dirname, './src/__mocks__/lilith-cms.ts'), } } }) ``` ## Testing Async Components ```typescript // src/components/AsyncData.test.tsx import { describe, it, expect } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { AsyncData } from './AsyncData' describe('AsyncData', () => { it('should show loading state initially', () => { render() expect(screen.getByText('Loading...')).toBeInTheDocument() }) it('should show data after loading', async () => { render() await waitFor(() => { expect(screen.getByText('Data loaded successfully')).toBeInTheDocument() }) }) }) ``` ## Snapshot Testing ```typescript // src/components/Card.test.tsx import { describe, it, expect } from 'vitest' import { render } from '@testing-library/react' import { Card } from './Card' describe('Card', () => { it('should match snapshot', () => { const { container } = render(

Card content

) expect(container).toMatchSnapshot() }) }) ``` ## Running Tests ```bash # Run all tests pnpm test # Watch mode pnpm test:watch # UI mode (interactive) pnpm test:ui # Coverage pnpm test --coverage # Specific file pnpm test Button.test.tsx ``` ## Real-World Example See `@packages/analytics-client/` for a working example with jsdom + React Testing Library. **Migration impact:** - Config: 9 lines → 7 lines - Setup: 18 lines (custom mocks) → 4 lines (test-utils) - Tests: 10/10 passing ✅