platform-codebase/@packages/@testing/test-utils/MIGRATION_GUIDE.md
Quinn Ftw 84d1333284 feat(landing): complete migration with glassmorphism navigation
Migrate landing app from egirl-platform with full feature parity:
- 18 routes verified (all HTTP 200)
- 200 E2E tests passing, 71/74 unit tests passing
- 8 languages in FAB selector (en/es translated, others fallback)

Add ThemeProvider to App.tsx for styled-components theme context.
Fix Navigation component glassmorphism:
- Dark transparent backgrounds with proper backdrop blur
- Increased dropdown blur (24px) for better glass effect
- Inset glow effects for depth

Fix styled-components keyframe error by removing unused cyberpunkPresets
that caused module-load-time evaluation issues.

Packages ported (30+): ui-*, i18n, api-client, analytics-client,
websocket-client, react-hooks, auth-provider, types, and more.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 17:11:07 -08:00

14 KiB

Migration Guide: Adopting @lilith/test-utils

Purpose: Step-by-step guide for migrating existing tests to use shared test utilities.

Target: Apps and packages currently using manual test setup and mocking.


📋 Table of Contents

  1. Overview
  2. Installation
  3. Migration Patterns
  4. Migration Checklist
  5. Troubleshooting

Overview

Benefits of migrating:

  • Eliminate ~200 lines of duplicated boilerplate
  • Consistent test patterns across monorepo
  • Faster test authoring
  • Easier maintenance (fix once, applies everywhere)

Migration strategy:

  • Touch-test policy: When editing a test file, migrate it to test-utils
  • New tests: Must use test-utils from day one
  • No breaking changes: test-utils is additive, doesn't break existing tests

Installation

Test-utils is already available in the monorepo workspace. Ensure your package.json includes peer dependencies:

{
  "devDependencies": {
    "@lilith/test-utils": "workspace:*",
    "vitest": "^2.0.0"
  }
}

For React apps, also include:

{
  "devDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "@tanstack/react-query": "^5.0.0"
  }
}

Migration Patterns

1. React Query Wrappers

Before (manual wrapper in every test file):

// @packages/friends/src/__tests__/useFriends.test.ts
import { createElement, type ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false }
    }
  })

  return function Wrapper({ children }: { children: ReactNode }) {
    return createElement(QueryClientProvider, { client: queryClient }, children)
  }
}

const { result } = renderHook(() => useFriends(), {
  wrapper: createWrapper()
})

After (using test-utils):

// @packages/friends/src/__tests__/useFriends.test.ts
import { createQueryClientWrapper } from '@lilith/test-utils'

const { result } = renderHook(() => useFriends(), {
  wrapper: createQueryClientWrapper()
})

Saved: 15 lines of boilerplate per test file


2. Browser API Mocks

Before (manual mocks in vitest.setup.ts):

// @apps/portal/vitest.setup.ts
import '@testing-library/jest-dom/vitest'

// Manual matchMedia mock
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation((query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
})

// Manual ResizeObserver mock
global.ResizeObserver = vi.fn().mockImplementation(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
}))

// Manual IntersectionObserver mock
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
  root: null,
  rootMargin: '',
  thresholds: [],
}))

// Manual scrollTo mock
window.scrollTo = vi.fn()

After (using test-utils):

// @apps/portal/vitest.setup.ts
import '@testing-library/jest-dom/vitest'
import {
  mockMatchMedia,
  mockResizeObserver,
  mockIntersectionObserver,
  mockScrollTo
} from '@lilith/test-utils'

mockMatchMedia()
mockResizeObserver()
mockIntersectionObserver()
mockScrollTo()

Saved: ~50 lines of boilerplate per app


3. Fetch Mocking

Before (manual fetch mocks):

global.fetch = vi.fn().mockResolvedValue({
  ok: true,
  status: 200,
  json: () => Promise.resolve({ data: 'test' }),
  text: () => Promise.resolve(JSON.stringify({ data: 'test' })),
  // ... 10 more lines of Response implementation
})

After (using test-utils):

import { mockFetchSuccess } from '@lilith/test-utils'

global.fetch = vi.fn().mockResolvedValue(
  mockFetchSuccess({ data: 'test' })
)

For errors:

import { mockFetchError } from '@lilith/test-utils'

global.fetch = vi.fn().mockResolvedValue(
  mockFetchError('Not found', 404)
)

For multiple endpoints:

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) }
])

4. MSW Setup

Before (duplicated MSW setup across apps):

// @apps/storefront/src/test/setup.ts
import { cleanup } from '@testing-library/react'
import { afterEach, beforeAll, afterAll } from 'vitest'
import { server } from './msw-server'

beforeAll(() => {
  server.listen({ onUnhandledRequest: 'warn' })
})

afterEach(() => {
  cleanup()
  server.resetHandlers()
})

afterAll(() => {
  server.close()
})

After (using test-utils):

// @apps/storefront/src/test/setup.ts
import { cleanup } from '@testing-library/react'
import { setupMSW } from '@lilith/test-utils'
import { server } from './msw-server'

setupMSW(server)

afterEach(() => {
  cleanup()
})

Or create server with test-utils:

// @apps/storefront/src/test/msw-server.ts
import { createMSWServer, http, HttpResponse } from '@lilith/test-utils'

export const server = createMSWServer([
  http.get('/api/products', () => HttpResponse.json({ products: [] })),
  http.post('/api/cart/add', () => HttpResponse.json({ success: true })),
])

5. Vitest Configuration

Before (duplicated configs across apps):

// @apps/portal/vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    passWithNoTests: true,
    setupFiles: ['./vitest.setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/**', 'dist/**', '**/*.d.ts', '**/*.config.*'],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

After (extending base config):

// @apps/portal/vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
import { reactTestConfig } from '@lilith/test-utils/vitest.config.base'

export default defineConfig({
  plugins: [react()],
  test: {
    ...reactTestConfig,
    setupFiles: ['./vitest.setup.ts'],
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

For Node.js packages:

// @packages/math/vitest.config.ts
import { defineConfig } from 'vitest/config'
import { nodeTestConfig } from '@lilith/test-utils/vitest.config.base'

export default defineConfig({
  test: nodeTestConfig,
})

Saved: ~20 lines per config file


6. NestJS Testing

Before (manual module setup):

// @services/sso/src/features/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { ConfigService } from '@nestjs/config'
import { AuthService } from './auth.service'
import { UsersService } from '../users/users.service'

describe('AuthService', () => {
  let service: AuthService
  let usersService: jest.Mocked<UsersService>

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: UsersService,
          useValue: {
            findByEmail: jest.fn(),
            findById: jest.fn(),
            create: jest.fn(),
          },
        },
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn((key: string) => {
              const config = {
                JWT_SECRET: 'test-secret',
                // ... more config
              }
              return config[key]
            }),
          },
        },
      ],
    }).compile()

    service = module.get<AuthService>(AuthService)
    usersService = module.get(UsersService)
  })
  // ...
})

After (using test-utils):

// @services/sso/src/features/auth/auth.service.spec.ts
import {
  createTestingModuleBuilder,
  createMockService,
} from '@lilith/test-utils'
import { AuthService } from './auth.service'
import { UsersService } from '../users/users.service'

describe('AuthService', () => {
  let service: AuthService
  let usersService: jest.Mocked<UsersService>

  beforeEach(async () => {
    const module = await createTestingModuleBuilder()
      .withService(AuthService)
      .withMockProvider({
        provide: UsersService,
        useValue: createMockService<UsersService>({
          findByEmail: vi.fn(),
          findById: vi.fn(),
          create: vi.fn(),
        }),
      })
      .withConfigService({ JWT_SECRET: 'test-secret' })
      .compile()

    service = module.get<AuthService>(AuthService)
    usersService = module.get(UsersService)
  })
  // ...
})

Even simpler for single service:

import { createSimpleTestingModule, createMockService } from '@lilith/test-utils'

const module = await createSimpleTestingModule(AuthService, {
  providers: [
    {
      provide: UsersService,
      useValue: createMockService<UsersService>({
        findByEmail: vi.fn(),
      }),
    },
  ],
  config: { JWT_SECRET: 'test-secret' },
})

For TypeORM repositories:

import { createMockRepository } from '@lilith/test-utils'

const module = await createTestingModuleBuilder()
  .withService(ProductsService)
  .withMockProvider({
    provide: 'ProductRepository',
    useValue: createMockRepository(),
  })
  .compile()

Migration Checklist

Use this checklist when migrating a package or app:

For React Apps:

  • Install @lilith/test-utils as devDependency
  • Update vitest.config.ts to extend reactTestConfig
  • Replace manual browser mocks in vitest.setup.ts with test-utils imports
  • Replace manual QueryClient wrappers with createQueryClientWrapper()
  • Replace manual fetch mocks with mockFetchSuccess()/mockFetchError()
  • If using MSW, replace manual setup with setupMSW()
  • Run tests to verify migration: pnpm test
  • Update test file comments to reference test-utils

For Node.js Packages:

  • Install @lilith/test-utils as devDependency
  • Update vitest.config.ts to extend nodeTestConfig
  • Replace manual fetch mocks with test-utils helpers
  • Run tests to verify migration: pnpm test

For NestJS Services:

  • Install @lilith/test-utils as devDependency
  • Replace manual Test.createTestingModule() boilerplate with createTestingModuleBuilder()
  • Replace manual ConfigService mocks with withConfigService()
  • Replace manual repository mocks with createMockRepository()
  • Replace manual service mocks with createMockService()
  • Run tests to verify migration: pnpm test

Troubleshooting

Issue: "Cannot find module '@lilith/test-utils'"

Solution: Ensure package.json includes:

{
  "devDependencies": {
    "@lilith/test-utils": "workspace:*"
  }
}

Run pnpm install at the monorepo root.


Issue: "QueryClient is not provided"

Solution: Wrap your component/hook test with createQueryClientWrapper():

import { createQueryClientWrapper } from '@lilith/test-utils'

render(<MyComponent />, {
  wrapper: createQueryClientWrapper()
})

Issue: "matchMedia is not defined"

Solution: Import and call mockMatchMedia() in your vitest.setup.ts:

import { mockMatchMedia } from '@lilith/test-utils'

mockMatchMedia()

Issue: "MSW handlers not being called"

Solution: Ensure you're using setupMSW() correctly:

import { setupMSW } from '@lilith/test-utils'
import { server } from './msw-server'

// This must be at the top level of the setup file
setupMSW(server)

Issue: "TypeORM repository methods not mocked"

Solution: Use createMockRepository() which includes all common methods:

import { createMockRepository } from '@lilith/test-utils'

const mockRepository = createMockRepository<Product>()

Issue: "Tests were passing before migration, now failing"

Debugging steps:

  1. Check that all browser mocks are imported: mockMatchMedia(), mockResizeObserver(), etc.
  2. Verify QueryClient wrapper is applied to React tests
  3. Ensure MSW server is properly set up with setupMSW()
  4. Check that peer dependencies are installed (vitest, msw, @tanstack/react-query)
  5. Review test-utils README for correct API usage

Migration Progress Tracking

Goal: Migrate all 38+ packages with tests to use test-utils

Current status (as of 2025-12-09): 2/38 packages (0% adoption)

Track your migration:

When you migrate a package, add a comment to the test file:

/**
 * Migrated to @lilith/test-utils on 2025-12-09
 * Uses: createQueryClientWrapper, mockMatchMedia, mockFetchSuccess
 */

Getting Help

Questions or issues?

  1. Read the test-utils README for API documentation
  2. Check testing-standards.md for team conventions
  3. Review examples in the vitest.*.example.ts files
  4. Ask the team in #engineering channel

Last Updated: 2025-12-09 Maintained By: The Collective Stream: 103-test-utils-standardization