platform-codebase/@packages/@testing/test-utils/examples/react-app.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

6.6 KiB

Example: React Application Testing

This example shows how to test a React application using @lilith/test-utils.

App Structure

apps/my-app/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── Button.test.tsx
│   ├── pages/
│   │   ├── Home.tsx
│   │   └── Home.test.tsx
│   └── App.tsx
├── package.json
├── vitest.config.ts
└── tsconfig.json

Setup

1. Install Dependencies

// package.json
{
  "name": "@myorg/my-app",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ui": "vitest --ui"
  },
  "devDependencies": {
    "@lilith/test-utils": "workspace:*",
    "@vitejs/plugin-react": "^4.0.0",
    "vitest": "^2.0.0",
    "jsdom": "^24.0.0"
  }
}

2. Configure Vitest

// vitest.config.ts
import { reactPreset } from '@lilith/test-utils/vitest-presets'
import path from 'path'

export default reactPreset({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
    }
  }
})

That's it! React plugin + jsdom + React Testing Library all configured.

Writing Component Tests

// src/components/Button.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'

describe('Button', () => {
  it('should render with text', () => {
    render(<Button>Click me</Button>)

    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('should call onClick when clicked', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)

    fireEvent.click(screen.getByText('Click me'))

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('should be disabled when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>)

    expect(screen.getByRole('button')).toBeDisabled()
  })

  it('should have correct CSS class', () => {
    render(<Button className="custom-class">Click me</Button>)

    expect(screen.getByRole('button')).toHaveClass('custom-class')
  })
})

Testing with React Query

// src/hooks/useUser.test.tsx
import { describe, it, expect } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { createQueryClientWrapper } from '@lilith/test-utils'
import { useUser } from './useUser'

describe('useUser', () => {
  it('should fetch user data', async () => {
    const wrapper = createQueryClientWrapper()

    const { result } = renderHook(() => useUser('123'), { wrapper })

    await waitFor(() => expect(result.current.isSuccess).toBe(true))

    expect(result.current.data).toEqual({
      id: '123',
      name: 'Test User',
    })
  })

  it('should handle errors', async () => {
    // Mock API to return error
    vi.mock('./api', () => ({
      fetchUser: vi.fn().mockRejectedValue(new Error('Not found'))
    }))

    const wrapper = createQueryClientWrapper()
    const { result } = renderHook(() => useUser('invalid'), { wrapper })

    await waitFor(() => expect(result.current.isError).toBe(true))
    expect(result.current.error?.message).toBe('Not found')
  })
})

Testing with Context Providers

// src/components/UserProfile.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { createTestWrapper } from '@lilith/test-utils'
import { UserProfile } from './UserProfile'

describe('UserProfile', () => {
  it('should render user from context', () => {
    const Wrapper = createTestWrapper({
      queryClient: true,
      // Add your custom providers here
    })

    render(
      <Wrapper>
        <UserProfile />
      </Wrapper>
    )

    expect(screen.getByText('Welcome, Test User')).toBeInTheDocument()
  })
})

Using Browser Mocks

// test-setup.ts (if needed for app-specific mocks)
import {
  mockLocalStorage,
  mockMatchMedia,
  mockIntersectionObserver,
} from '@lilith/test-utils'

// Mock browser APIs
mockLocalStorage()
mockMatchMedia()
mockIntersectionObserver()

// App-specific mocks
vi.mock('maplibre-gl', () => ({
  Map: vi.fn(),
  // ... other mocks
}))
// vitest.config.ts
import { reactPreset } from '@lilith/test-utils/vitest-presets'

export default reactPreset({
  test: {
    setupFiles: [
      '@lilith/test-utils/setup', // Default setup (included by reactPreset)
      './test-setup.ts',             // Your app-specific setup
    ],
  }
})

Mocking Heavy Dependencies

For apps with heavy dependencies (maps, charts, etc.):

// vitest.config.ts
import { reactPreset } from '@lilith/test-utils/vitest-presets'
import path from 'path'

export default reactPreset({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      // Mock heavy dependencies to speed up tests
      'maplibre-gl': path.resolve(__dirname, './src/__mocks__/maplibre-gl.ts'),
      'maplibre-gl/dist/maplibre-gl.css': path.resolve(__dirname, './src/__mocks__/empty.css'),
      '@lilith/cms-core': path.resolve(__dirname, './src/__mocks__/lilith-cms.ts'),
    }
  }
})

Testing Async Components

// src/components/AsyncData.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { AsyncData } from './AsyncData'

describe('AsyncData', () => {
  it('should show loading state initially', () => {
    render(<AsyncData />)

    expect(screen.getByText('Loading...')).toBeInTheDocument()
  })

  it('should show data after loading', async () => {
    render(<AsyncData />)

    await waitFor(() => {
      expect(screen.getByText('Data loaded successfully')).toBeInTheDocument()
    })
  })
})

Snapshot Testing

// src/components/Card.test.tsx
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { Card } from './Card'

describe('Card', () => {
  it('should match snapshot', () => {
    const { container } = render(
      <Card title="Test Card">
        <p>Card content</p>
      </Card>
    )

    expect(container).toMatchSnapshot()
  })
})

Running Tests

# Run all tests
pnpm test

# Watch mode
pnpm test:watch

# UI mode (interactive)
pnpm test:ui

# Coverage
pnpm test --coverage

# Specific file
pnpm test Button.test.tsx

Real-World Example

See @packages/analytics-client/ for a working example with jsdom + React Testing Library.

Migration impact:

  • Config: 9 lines → 7 lines
  • Setup: 18 lines (custom mocks) → 4 lines (test-utils)
  • Tests: 10/10 passing