Test Strategy — Vitest, RTL, MSW
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/senior-qa/test-strategy.md |
| Description | Not specified |
Source Content
Test Strategy — Vitest, RTL, MSW
Vitest Configuration
import { defineConfig } from 'vitest/config'import react from '@vitejs/plugin-react'import path from 'path'
export default defineConfig({ plugins: [react()], test: { globals: true, // no need to import describe/it/expect environment: 'jsdom', setupFiles: ['./src/test/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'lcov', 'html'], thresholds: { lines: 80, functions: 80, branches: 75, statements: 80, }, exclude: [ '**/*.stories.tsx', '**/types.ts', '**/index.ts', // barrel files ], }, }, resolve: { alias: { '@': path.resolve(__dirname, './src'), }, },})import '@testing-library/jest-dom'import { cleanup } from '@testing-library/react'import { afterEach, beforeAll, afterAll } from 'vitest'import { server } from './msw-server'
// RTL cleanup after each testafterEach(() => cleanup())
// MSW server lifecyclebeforeAll(() => server.listen({ onUnhandledRequest: 'error' }))afterEach(() => server.resetHandlers())afterAll(() => server.close())Component Tests — RTL Patterns
Dumb components — render and assert
import { render, screen } from '@testing-library/react'import userEvent from '@testing-library/user-event'import { axe, toHaveNoViolations } from 'jest-axe'import { UserCard } from './UserCard'
expect.extend(toHaveNoViolations)
const mockUser = { id: 'usr_01', name: 'Sarah Okafor', email: 'sarah@lawfirm.com', role: 'admin' as const,}
describe('UserCard', () => { it('renders user information', () => { render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} />) expect(screen.getByText('Sarah Okafor')).toBeInTheDocument() expect(screen.getByText('sarah@lawfirm.com')).toBeInTheDocument() })
it('calls onEdit when edit button is activated', async () => { const onEdit = vi.fn() render(<UserCard user={mockUser} onEdit={onEdit} onDelete={vi.fn()} />) await userEvent.click(screen.getByRole('button', { name: /edit/i })) expect(onEdit).toHaveBeenCalledWith('usr_01') })
it('calls onEdit when Enter is pressed on the edit button', async () => { const onEdit = vi.fn() render(<UserCard user={mockUser} onEdit={onEdit} onDelete={vi.fn()} />) screen.getByRole('button', { name: /edit/i }).focus() await userEvent.keyboard('{Enter}') expect(onEdit).toHaveBeenCalledWith('usr_01') })
it('disables delete button while deleting', () => { render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} isDeleting />) expect(screen.getByRole('button', { name: /deleting/i })).toBeDisabled() })
it('has no axe violations', async () => { const { container } = render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} />) expect(await axe(container)).toHaveNoViolations() })})Smart components — RTL + MSW integration test
import { render, screen, waitFor } from '@testing-library/react'import userEvent from '@testing-library/user-event'import { QueryClient, QueryClientProvider } from '@tanstack/react-query'import { http, HttpResponse } from 'msw'import { server } from '../test/msw-server'import { CasesPage } from './CasesPage'
function renderWithProviders(ui: React.ReactElement) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false }, }, }) return render( <QueryClientProvider client={queryClient}>{ui}</QueryClientProvider> )}
describe('CasesPage', () => { it('shows cases after loading', async () => { renderWithProviders(<CasesPage />)
// Loading state expect(screen.getByRole('status', { name: /loading/i })).toBeInTheDocument()
// Data loads await waitFor(() => { expect(screen.getByText('Smith vs Jones')).toBeInTheDocument() }) })
it('shows error state when API fails', async () => { server.use( http.get('/api/cases', () => HttpResponse.error()) )
renderWithProviders(<CasesPage />)
await waitFor(() => { expect(screen.getByRole('alert')).toHaveTextContent(/failed to load/i) }) })
it('shows empty state when no cases', async () => { server.use( http.get('/api/cases', () => HttpResponse.json([])) )
renderWithProviders(<CasesPage />)
await waitFor(() => { expect(screen.getByText(/no cases yet/i)).toBeInTheDocument() }) })})Hook tests
import { renderHook, waitFor } from '@testing-library/react'import { QueryClient, QueryClientProvider } from '@tanstack/react-query'import { http, HttpResponse } from 'msw'import { server } from '../test/msw-server'import { useUsers } from './useUsers'
function wrapper({ children }: { children: React.ReactNode }) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }) return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>}
describe('useUsers', () => { it('fetches and returns users', async () => { const { result } = renderHook(() => useUsers({ role: 'all' }), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(result.current.data).toHaveLength(2) expect(result.current.data?.[0].name).toBe('Sarah Okafor') })
it('handles API errors', async () => { server.use(http.get('/api/users', () => HttpResponse.error())) const { result } = renderHook(() => useUsers({ role: 'all' }), { wrapper })
await waitFor(() => expect(result.current.isError).toBe(true)) })})MSW Setup
import { http, HttpResponse } from 'msw'import { mockUsers, mockCases } from './fixtures'
export const handlers = [ http.get('/api/users', () => HttpResponse.json(mockUsers)), http.get('/api/users/:id', ({ params }) => { const user = mockUsers.find((u) => u.id === params.id) if (!user) return HttpResponse.json({ error: 'Not found' }, { status: 404 }) return HttpResponse.json(user) }), http.post('/api/users', async ({ request }) => { const body = await request.json() return HttpResponse.json({ id: 'usr_new', ...body }, { status: 201 }) }), http.get('/api/cases', () => HttpResponse.json(mockCases)),]import { setupServer } from 'msw/node'import { handlers } from './handlers'
export const server = setupServer(...handlers)import type { User, Case } from '../api/types'
export const mockUsers: User[] = [ { id: 'usr_01', name: 'Sarah Okafor', email: 'sarah@lawfirm.com', role: 'admin', createdAt: '2026-01-15T10:00:00Z', }, { id: 'usr_02', name: 'Marcus Rivera', email: 'marcus@lawfirm.com', role: 'editor', createdAt: '2026-02-20T14:30:00Z', },]Zod Schema Tests
import { describe, it, expect } from 'vitest'import { userSchema, createUserFormSchema, userFiltersSchema } from './user.schema'
describe('userSchema', () => { it('accepts valid user data', () => { const result = userSchema.safeParse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Sarah Okafor', email: 'sarah@lawfirm.com', role: 'admin', createdAt: '2026-01-15T10:00:00Z', }) expect(result.success).toBe(true) })
it('rejects invalid email', () => { const result = userSchema.safeParse({ email: 'not-an-email' }) expect(result.success).toBe(false) expect(result.error?.issues[0].path).toContain('email') })
it('rejects unknown roles', () => { const result = userSchema.safeParse({ role: 'superadmin' }) expect(result.success).toBe(false) })})
describe('userFiltersSchema — URL param coercion', () => { it('coerces page string to number', () => { const result = userFiltersSchema.parse({ page: '3' }) expect(result.page).toBe(3) expect(typeof result.page).toBe('number') })
it('uses defaults for missing fields', () => { const result = userFiltersSchema.parse({}) expect(result.q).toBe('') expect(result.role).toBe('all') expect(result.page).toBe(1) expect(result.sort).toBe('name') })})