name: frontend-typescript-testing description: Designs tests with React Testing Library, MSW, and Playwright E2E. Applies component testing and E2E testing patterns.
TypeScript Testing Rules (Frontend)
References
| Test Type | Reference | When to Use |
|---|---|---|
| Unit / Integration | This document | Implementing React component tests with RTL + Vitest + MSW |
| E2E | references/e2e.md | Implementing browser-level E2E tests with Playwright |
Test Framework
- Vitest: This project uses Vitest
- React Testing Library: For component testing
- MSW (Mock Service Worker): For API mocking
- Test imports:
import { describe, it, expect, beforeEach, vi } from 'vitest' - Component test imports:
import { render, screen } from '@testing-library/react' - User interaction:
import userEvent from '@testing-library/user-event' - Mock creation: Use
vi.mock()
Basic Testing Policy
Quality Requirements
- Coverage: prioritize meaningful assertions on critical paths and high-reuse components; treat coverage as a signal for gaps, not a target (a target gets gamed into trivial tests — Goodhart's Law). Any numeric threshold is the project's CI config
- Independence: Each test can run independently without depending on other tests
- Reproducibility: Tests are environment-independent and always return the same results
- Readability: Test code maintains the same quality as production code
Where to concentrate test rigor
Test foundational, high-reuse units the hardest — shared components, custom hooks, and utils reused across many features carry the widest blast radius. Higher-composition surfaces (organisms, pages) lean on integration/E2E coverage instead. Any numeric threshold is the project's CI config.
Metrics (what coverage reports break down): Statements, Branches, Functions, Lines
Test Types and Scope
Unit Tests (React Testing Library)
- Verify behavior of individual components or functions
- Mock all external dependencies
- Most numerous, implemented with fine granularity
- Focus on user-observable behavior
Integration Tests (React Testing Library + MSW)
- Verify coordination between multiple components
- Mock APIs with MSW (Mock Service Worker)
- No actual DB connections (backend manages DB)
- Verify major functional flows
Cross-functional Verification in E2E Tests
- Mandatory verification of impact on existing features when adding new features
- Cover integration points with "High" and "Medium" impact levels from Design Doc's "Integration Point Map"
- Verification pattern: Existing feature operation -> Enable new feature -> Verify continuity of existing features
- Success criteria: No change in displayed content, rendering time within 5 seconds
- Designed for automatic execution in CI/CD pipelines
Test Implementation Conventions
Directory Structure (Co-location Principle)
src/
└── components/
└── Button/
├── Button.tsx
├── Button.test.tsx # Co-located with component
└── index.ts
Rationale:
- React Testing Library best practice
- Co-location principle: tests live alongside the implementation they cover
- Easy to find and maintain tests alongside implementation
Naming Conventions
- Test files:
{ComponentName}.test.tsx - Integration test files:
{FeatureName}.integration.test.tsx - Test suites: Names describing target components or features
- Test cases: Names describing expected behavior from user perspective
Test Code Quality Rules
Recommended: Keep all tests always active
- Merit: Guarantees test suite completeness
- Practice: Fix problematic tests and activate them
Avoid: test.skip() or commenting out
- Reason: Creates test gaps and incomplete quality checks
- Solution: Completely delete unnecessary tests
Mock Type Safety Enforcement
MSW (Mock Service Worker) Setup
// Type-safe MSW handler (MSW v2)
import { http, HttpResponse } from 'msw'
const handlers = [
http.get('/api/users/:id', () => {
return HttpResponse.json({ id: '1', name: 'John' } satisfies User)
})
]
Component Mock Type Safety
// Only required parts
type TestProps = Pick<ButtonProps, 'label' | 'onClick'>
const mockProps: TestProps = { label: 'Click', onClick: vi.fn() }
// Only when absolutely necessary, with clear justification
const mockRouter = {
push: vi.fn()
} as unknown as Router // Complex router type structure
Basic React Testing Library Example
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'
describe('Button', () => {
it('should call onClick when clicked', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(<Button label="Click me" onClick={onClick} />)
await user.click(screen.getByRole('button', { name: 'Click me' }))
expect(onClick).toHaveBeenCalledOnce()
})
})
Test Design Patterns
Test user-visible results, not implementation details. Query by accessibility (getByRole/getByLabelText/getByText), not getByTestId or container.querySelector. Cover empty, error, and loading/async states, not only the happy path; await async UI with findBy*.
// Test the user-visible result
it('increments count when clicked', async () => {
const user = userEvent.setup()
render(<Counter />)
await user.click(screen.getByRole('button', { name: '+' }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
// Error state: override the handler for one test
it('shows an error message on API failure', async () => {
server.use(http.get('/api/users', () => new HttpResponse(null, { status: 500 })))
render(<UserList />)
expect(await screen.findByText('Something went wrong')).toBeInTheDocument()
})