platform-codebase/@packages/@testing/test-utils/README.md

638 lines
14 KiB
Markdown
Executable file

# @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(<MyComponent />, {
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(<MyComponent />, { 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<T>(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<typeof vi.fn>`
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<void>`
Wait for a specified duration.
```typescript
import { waitForAsync } from '@lilith/test-utils'
await waitForAsync(100) // Wait 100ms
```
#### `waitForCondition(condition: () => boolean | Promise<boolean>, options?: { timeout?: number, interval?: number }): Promise<void>`
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<T>(defaults: T): (overrides?: Partial<T>) => 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<User>({
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(<MyComponent />, { 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 <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
}
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(<MyComponent />, {
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