platform-codebase/@packages/@testing/test-utils
2026-01-25 11:33:37 -08:00
..
examples chore(src): 🔧 Update 15 markdown files in source directory 2026-01-18 09:20:18 -08:00
src chore(utils): 🔧 Update TypeScript files in utils directory 2026-01-18 09:20:19 -08:00
vitest-presets chore(utils): 🔧 Update TypeScript files in utils directory 2026-01-18 09:20:19 -08:00
CHANGELOG_TYPE_SAFETY.md chore(src): 🔧 Update 15 markdown files in source directory 2026-01-18 09:20:18 -08:00
EXAMPLE_MIGRATION.md chore(src): 🔧 Update 15 markdown files in source directory 2026-01-18 09:20:18 -08:00
MIGRATION_GUIDE.md chore(src): 🔧 Update 15 markdown files in source directory 2026-01-18 09:20:18 -08:00
package.json deps-upgrade(packages): ⬆️ Update all direct/indirect dependencies to latest compatible versions across monorepo 2026-01-25 11:33:37 -08:00
README.md chore(src): 🔧 Update 15 markdown files in source directory 2026-01-18 09:20:18 -08:00
tsconfig.json chore(config): 🔧 Update TypeScript, testing, and infrastructure configurations across codebase 2026-01-18 09:20:11 -08:00
vite.config.ts chore(config): 🔧 Update TypeScript, testing, and infrastructure configurations across codebase 2026-01-18 09:20:11 -08:00
vitest.config.base.ts chore(config): 🔧 Update TypeScript, testing, and infrastructure configurations across codebase 2026-01-18 09:20:11 -08:00
vitest.config.example-node.ts chore(config): 🔧 Update TypeScript, testing, and infrastructure configurations across codebase 2026-01-18 09:20:11 -08:00
vitest.config.example-react.ts chore(config): 🔧 Update TypeScript, testing, and infrastructure configurations across codebase 2026-01-18 09:20:11 -08:00
vitest.config.ts chore(config): 🔧 Update TypeScript, testing, and infrastructure configurations across codebase 2026-01-18 09:20:11 -08:00
vitest.config.ts.timestamp-1767077375140-c34b2bacde151.mjs chore(config): 🔧 Update TypeScript, testing, and infrastructure configurations across codebase 2026-01-18 09:20:11 -08:00
vitest.setup.example.ts chore(utils): 🔧 Update TypeScript files in utils directory 2026-01-18 09:20:19 -08:00

@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.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)

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 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.

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()


📊 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