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>
296 lines
6.6 KiB
Markdown
296 lines
6.6 KiB
Markdown
# 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
|
|
|
|
```json
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
}))
|
|
```
|
|
|
|
```typescript
|
|
// 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.):
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```bash
|
|
# 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 ✅
|