| .. | ||
| examples | ||
| src | ||
| vitest-presets | ||
| CHANGELOG_TYPE_SAFETY.md | ||
| EXAMPLE_MIGRATION.md | ||
| MIGRATION.md | ||
| MIGRATION_GUIDE.md | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.base.ts | ||
| vitest.config.example-node.ts | ||
| vitest.config.example-react.ts | ||
| vitest.config.ts | ||
| vitest.config.ts.timestamp-1767077375140-c34b2bacde151.mjs | ||
| vitest.setup.example.ts | ||
@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
pnpm add -D @lilith/test-utils
Peer dependencies:
vitest ^2.0.0react ^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)
import { describe, it, expect } from 'vitest'
describe('myFunction', () => {
it('should work correctly', () => {
expect(myFunction(1, 2)).toBe(3)
})
})
React Component Test
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
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.
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.
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.
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.
import { mockIntersectionObserver } from '@lilith/test-utils'
beforeEach(() => {
mockIntersectionObserver()
})
mockResizeObserver(): void
Mocks ResizeObserver API for responsive component tests.
import { mockResizeObserver } from '@lilith/test-utils'
beforeEach(() => {
mockResizeObserver()
})
mockScrollTo(): void
Mocks window.scrollTo to prevent errors in jsdom.
import { mockScrollTo } from '@lilith/test-utils'
beforeEach(() => {
mockScrollTo()
})
mockBroadcastChannel(): void
Mocks BroadcastChannel API for cross-tab communication tests.
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).
import { createTestQueryClient } from '@lilith/test-utils'
const queryClient = createTestQueryClient()
Configuration:
retry: false- No automatic retriesgcTime: 0- No garbage collection delaystaleTime: 0- Data immediately stale
createQueryClientWrapper(queryClient?: QueryClient): FC<{ children: ReactNode }>
Creates a wrapper component with QueryClientProvider for testing hooks.
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.
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:
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.
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.
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.
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.
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.
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.
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.
import { generateId } from '@lilith/test-utils'
const userId = generateId() // "ab3f7e8d91762c3ef"
generateDate(daysFromNow?: number): string
Generates an ISO date string.
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.
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.
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).
import { setupTests } from '@lilith/test-utils'
setupTests()
cleanupTests(): void
Global test cleanup.
import { cleanupTests } from '@lilith/test-utils'
afterEach(() => {
cleanupTests()
})
🏗️ Common Patterns
Testing React Query Hooks
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
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
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:
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)
// ❌ 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)
// ✅ Import from shared package
import { createQueryClientWrapper } from '@lilith/test-utils'
const { result } = renderHook(() => useMyHook(), {
wrapper: createQueryClientWrapper()
})
Before (Manual browser mocks)
// ❌ 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)
// ✅ Import from shared package
import { mockMatchMedia, mockResizeObserver } from '@lilith/test-utils'
mockMatchMedia()
mockResizeObserver()
🔗 Related Documentation
- Testing Standards - Team testing conventions
- Vitest Documentation - Vitest framework docs
- Testing Library - 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:
// 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:
// 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:
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