# @lilith/test-utils **Shared testing utilities for lilith-platform monorepo.** This package provides standardized mocks, helpers, and test configuration to eliminate duplication across apps, packages, and services. --- ## ๐Ÿ“ฆ Installation ```bash pnpm add -D @lilith/test-utils ``` **Peer dependencies:** - `vitest ^2.0.0` - `react ^18.0.0` (for React testing utilities) - `react-dom ^18.0.0` (for React testing utilities) - `@tanstack/react-query ^5.0.0` (for React Query utilities) --- ## ๐Ÿงช Quick Start ### Basic Unit Test (No React) ```typescript import { describe, it, expect } from 'vitest' describe('myFunction', () => { it('should work correctly', () => { expect(myFunction(1, 2)).toBe(3) }) }) ``` ### React Component Test ```typescript import { describe, it, expect } from 'vitest' import { render, screen } from '@testing-library/react' import { createQueryClientWrapper } from '@lilith/test-utils' import { MyComponent } from './MyComponent' describe('MyComponent', () => { it('should render correctly', () => { render(, { wrapper: createQueryClientWrapper() }) expect(screen.getByText('Hello')).toBeInTheDocument() }) }) ``` ### Test with Browser APIs ```typescript import { describe, it, expect, beforeEach } from 'vitest' import { mockLocalStorage, mockMatchMedia } from '@lilith/test-utils' describe('responsive feature', () => { beforeEach(() => { mockLocalStorage() mockMatchMedia(true) // true = matches media query }) it('should work with mocked APIs', () => { // Your test code }) }) ``` --- ## ๐Ÿ“š API Reference ### Browser API Mocks Mock common browser APIs that don't exist in jsdom test environment. #### `mockLocalStorage(): Storage` Creates an in-memory localStorage mock. ```typescript import { mockLocalStorage } from '@lilith/test-utils' beforeEach(() => { const storage = mockLocalStorage() storage.setItem('key', 'value') expect(storage.getItem('key')).toBe('value') }) ``` #### `mockSessionStorage(): Storage` Creates an in-memory sessionStorage mock. ```typescript import { mockSessionStorage } from '@lilith/test-utils' beforeEach(() => { const storage = mockSessionStorage() storage.setItem('session-key', 'session-value') }) ``` #### `mockMatchMedia(defaultMatches?: boolean): void` Mocks `window.matchMedia` for testing responsive components. ```typescript import { mockMatchMedia } from '@lilith/test-utils' // Desktop viewport mockMatchMedia(false) // Mobile viewport mockMatchMedia(true) ``` #### `mockIntersectionObserver(): void` Mocks IntersectionObserver API for lazy-loading/infinite scroll tests. ```typescript import { mockIntersectionObserver } from '@lilith/test-utils' beforeEach(() => { mockIntersectionObserver() }) ``` #### `mockResizeObserver(): void` Mocks ResizeObserver API for responsive component tests. ```typescript import { mockResizeObserver } from '@lilith/test-utils' beforeEach(() => { mockResizeObserver() }) ``` #### `mockScrollTo(): void` Mocks `window.scrollTo` to prevent errors in jsdom. ```typescript import { mockScrollTo } from '@lilith/test-utils' beforeEach(() => { mockScrollTo() }) ``` #### `mockBroadcastChannel(): void` Mocks BroadcastChannel API for cross-tab communication tests. ```typescript import { mockBroadcastChannel } from '@lilith/test-utils' beforeEach(() => { mockBroadcastChannel() }) ``` --- ### React Query Test Utilities Utilities for testing React Query hooks and components. #### `createTestQueryClient(): QueryClient` Creates a pre-configured QueryClient for testing (no retries, no caching). ```typescript import { createTestQueryClient } from '@lilith/test-utils' const queryClient = createTestQueryClient() ``` **Configuration:** - `retry: false` - No automatic retries - `gcTime: 0` - No garbage collection delay - `staleTime: 0` - Data immediately stale #### `createQueryClientWrapper(queryClient?: QueryClient): FC<{ children: ReactNode }>` Creates a wrapper component with QueryClientProvider for testing hooks. ```typescript import { renderHook } from '@testing-library/react' import { createQueryClientWrapper } from '@lilith/test-utils' import { useMyQuery } from './hooks' const { result } = renderHook(() => useMyQuery(), { wrapper: createQueryClientWrapper() }) ``` #### `createTestWrapper(options?: TestWrapperOptions): FC<{ children: ReactNode }>` Creates a comprehensive test wrapper with QueryClient + additional providers. ```typescript import { render } from '@testing-library/react' import { createTestWrapper } from '@lilith/test-utils' const wrapper = createTestWrapper({ queryClient: customQueryClient, // Optional additionalProviders: [ThemeProvider, AuthProvider] // Optional }) render(, { wrapper }) ``` **Options:** ```typescript interface TestWrapperOptions { queryClient?: QueryClient // Custom QueryClient instance additionalProviders?: FC<{ children: ReactNode }>[] // Additional context providers } ``` --- ### Fetch Mocking Utilities Utilities for mocking `fetch` API calls. #### `mockFetchSuccess(data: T, status?: number): Response` Creates a successful fetch Response mock. ```typescript import { mockFetchSuccess } from '@lilith/test-utils' global.fetch = vi.fn().mockResolvedValue( mockFetchSuccess({ id: 1, name: 'Test' }) ) ``` #### `mockFetchError(message: string, status?: number): Response` Creates an error fetch Response mock. ```typescript import { mockFetchError } from '@lilith/test-utils' global.fetch = vi.fn().mockResolvedValue( mockFetchError('Not found', 404) ) ``` #### `mockFetchSequence(responses: Array<{ url: string | RegExp, response: Response }>): typeof fetch` Creates a fetch mock that responds differently based on URL. ```typescript import { mockFetchSequence, mockFetchSuccess, mockFetchError } from '@lilith/test-utils' mockFetchSequence([ { url: '/api/users', response: mockFetchSuccess([{ id: 1 }]) }, { url: /\/api\/posts\/\d+/, response: mockFetchSuccess({ id: 1, title: 'Post' }) }, { url: '/api/error', response: mockFetchError('Server error', 500) } ]) ``` #### `createFetchMock(): ReturnType` Creates a basic fetch mock function. ```typescript import { createFetchMock } from '@lilith/test-utils' const fetchMock = createFetchMock() global.fetch = fetchMock fetchMock.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: 'test' }) }) ``` --- ### Wait Utilities Utilities for handling asynchronous test scenarios. #### `waitForAsync(ms: number): Promise` Wait for a specified duration. ```typescript import { waitForAsync } from '@lilith/test-utils' await waitForAsync(100) // Wait 100ms ``` #### `waitForCondition(condition: () => boolean | Promise, options?: { timeout?: number, interval?: number }): Promise` Wait until a condition is true. ```typescript import { waitForCondition } from '@lilith/test-utils' await waitForCondition( () => document.querySelector('.loaded') !== null, { timeout: 5000, interval: 50 } ) ``` **Options:** - `timeout` - Maximum wait time in ms (default: 5000) - `interval` - Check interval in ms (default: 50) --- ### Test Data Factories Utilities for generating consistent test data. #### `generateId(): string` Generates a random unique ID. ```typescript import { generateId } from '@lilith/test-utils' const userId = generateId() // "ab3f7e8d91762c3ef" ``` #### `generateDate(daysFromNow?: number): string` Generates an ISO date string. ```typescript import { generateDate } from '@lilith/test-utils' const today = generateDate() // "2024-12-09T12:34:56.789Z" const tomorrow = generateDate(1) // "2024-12-10T12:34:56.789Z" const yesterday = generateDate(-1) // "2024-12-08T12:34:56.789Z" ``` #### `generateTime(hours: number, minutes?: number): string` Generates a time string in HH:MM format. ```typescript import { generateTime } from '@lilith/test-utils' const time = generateTime(14, 30) // "14:30" ``` #### `createFactory(defaults: T): (overrides?: Partial) => T` Creates a factory function for building test objects. ```typescript import { createFactory } from '@lilith/test-utils' interface User { id: string name: string email: string age: number } const createUser = createFactory({ id: '1', name: 'Test User', email: 'test@example.com', age: 25 }) // Use defaults const user1 = createUser() // Override specific fields const user2 = createUser({ name: 'Jane Doe', age: 30 }) ``` --- ### Setup Utilities #### `setupTests(): void` Global test setup (imported in vitest.setup.ts). ```typescript import { setupTests } from '@lilith/test-utils' setupTests() ``` #### `cleanupTests(): void` Global test cleanup. ```typescript import { cleanupTests } from '@lilith/test-utils' afterEach(() => { cleanupTests() }) ``` --- ## ๐Ÿ—๏ธ Common Patterns ### Testing React Query Hooks ```typescript import { describe, it, expect, vi } from 'vitest' import { renderHook, waitFor } from '@testing-library/react' import { createQueryClientWrapper, mockFetchSuccess } from '@lilith/test-utils' describe('useFetchUser', () => { it('should fetch user data', async () => { global.fetch = vi.fn().mockResolvedValue( mockFetchSuccess({ id: 1, name: 'John' }) ) const { result } = renderHook(() => useFetchUser('1'), { wrapper: createQueryClientWrapper() }) expect(result.current.isLoading).toBe(true) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(result.current.data).toEqual({ id: 1, name: 'John' }) }) }) ``` ### Testing Components with Multiple Providers ```typescript import { render, screen } from '@testing-library/react' import { createTestWrapper } from '@lilith/test-utils' import { ThemeProvider } from './theme' import { AuthProvider } from './auth' const wrapper = createTestWrapper({ additionalProviders: [ThemeProvider, AuthProvider] }) render(, { wrapper }) expect(screen.getByText('Authenticated')).toBeInTheDocument() ``` ### Testing with Browser API Mocks ```typescript import { describe, it, expect, beforeEach } from 'vitest' import { mockLocalStorage, mockMatchMedia, mockIntersectionObserver } from '@lilith/test-utils' describe('responsive component with storage', () => { beforeEach(() => { mockLocalStorage() mockMatchMedia(true) // Mobile viewport mockIntersectionObserver() }) it('should handle mobile layout with storage', () => { // Your test code }) }) ``` --- ## ๐Ÿ“ Vitest Setup File Create `vitest.setup.ts` in your app/package: ```typescript import '@testing-library/jest-dom/vitest' import { mockMatchMedia, mockIntersectionObserver, mockResizeObserver, mockScrollTo } from '@lilith/test-utils' // Setup common browser mocks mockMatchMedia() mockIntersectionObserver() mockResizeObserver() mockScrollTo() ``` --- ## ๐ŸŽฏ Migration from Manual Mocks ### Before (Manual QueryClient wrapper) ```typescript // โŒ Duplicated in every test file function createWrapper() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) return function Wrapper({ children }) { return {children} } } const { result } = renderHook(() => useMyHook(), { wrapper: createWrapper() }) ``` ### After (Using test-utils) ```typescript // โœ… Import from shared package import { createQueryClientWrapper } from '@lilith/test-utils' const { result } = renderHook(() => useMyHook(), { wrapper: createQueryClientWrapper() }) ``` ### Before (Manual browser mocks) ```typescript // โŒ Duplicated in vitest.setup.ts across apps Object.defineProperty(window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, media: query, // ... 10 more lines })) }) global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })) ``` ### After (Using test-utils) ```typescript // โœ… Import from shared package import { mockMatchMedia, mockResizeObserver } from '@lilith/test-utils' mockMatchMedia() mockResizeObserver() ``` --- ## ๐Ÿ”— Related Documentation - [Testing Standards](../../.claude/instructions/testing-standards.md) - Team testing conventions - [Vitest Documentation](https://vitest.dev) - Vitest framework docs - [Testing Library](https://testing-library.com/react) - React Testing Library docs --- ## ๐Ÿ“Š Package Statistics **Current adoption**: 2 files (as of 2025-12-09) **Goal**: Standardize testing across 38+ packages with unit tests **Benefits**: - โœ… Eliminate ~200 lines of duplicated mock code across apps - โœ… Consistent test patterns across monorepo - โœ… Faster test authoring (import vs write boilerplate) - โœ… Single source of truth for test utilities --- ## ๐Ÿ› Troubleshooting ### QueryClient warnings in tests If you see warnings about QueryClient not being provided: ```typescript // Make sure you wrap with QueryClientProvider import { createQueryClientWrapper } from '@lilith/test-utils' render(, { wrapper: createQueryClientWrapper() }) ``` ### Browser API not mocked If you get errors about `matchMedia` or `IntersectionObserver` not being defined: ```typescript // Add to your vitest.setup.ts import { mockMatchMedia, mockIntersectionObserver } from '@lilith/test-utils' mockMatchMedia() mockIntersectionObserver() ``` ### Fetch is not mocked If your tests make real network requests: ```typescript import { createFetchMock, mockFetchSuccess } from '@lilith/test-utils' const fetchMock = createFetchMock() global.fetch = fetchMock fetchMock.mockResolvedValue(mockFetchSuccess({ data: 'test' })) ``` --- **Maintained by**: The Collective **Last Updated**: 2025-12-09 **Stream**: 103-test-utils-standardization