Testing
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/expert-react-frontend-engineer/testing.md |
| Description | Not specified |
Source Content
Testing
Do not write tests unless explicitly asked to. However, always write code as if tests will be written next. The architecture must support testing without significant rework.
Testability Rules
- Dumb components: render → assert → simulate → assert
- No mocking of internal hooks in unit tests
- Smart components (route-level): tested via integration tests
- Custom hooks: tested via
renderHookfrom Testing Library - If you need to mock more than 1 thing to test a component, refactor the architecture
- All side effects must be injectable (pass callbacks as props, not hardcoded internally)
- Avoid global state dependencies inside components — pass state in as props
If a component is hard to test, the architecture is wrong. Fix the architecture, don’t write complex mocks.
Dumb Components (Unit Tests)
import { render, screen } from '@testing-library/react'import userEvent from '@testing-library/user-event'import { UserCard } from './UserCard'
const mockUser = { id: '1', name: 'David', email: 'david@example.com', role: 'admin' as const,}
describe('UserCard', () => { it('renders user info', () => { render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} />) expect(screen.getByText('David')).toBeInTheDocument() expect(screen.getByText('david@example.com')).toBeInTheDocument() })
it('calls onDelete with user id', async () => { const onDelete = vi.fn() render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={onDelete} />) await userEvent.click(screen.getByRole('button', { name: /delete/i })) expect(onDelete).toHaveBeenCalledWith('1') })
it('shows loading state when deleting', () => { render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} isDeleting />) expect(screen.getByRole('button', { name: /deleting/i })).toBeDisabled() })
it('is keyboard accessible', async () => { const onDelete = vi.fn() render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={onDelete} />) const deleteBtn = screen.getByRole('button', { name: /delete/i }) deleteBtn.focus() await userEvent.keyboard('{Enter}') expect(onDelete).toHaveBeenCalledWith('1') })})Accessibility Testing
Test accessibility at the unit level using axe:
import { render } from '@testing-library/react'import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('has no axe violations', async () => { const { container } = render(<UserCard user={mockUser} onEdit={vi.fn()} onDelete={vi.fn()} />) const results = await axe(container) expect(results).toHaveNoViolations()})Always test a11y for:
- All interactive components (buttons, links, form controls)
- Any component that shows/hides content (dialogs, accordions, tooltips)
- Any component with dynamic content (loading states, error messages)
Testing Rules
- Dumb components: Unit test with props. No mocking of hooks or context.
- Custom hooks: Test with
renderHook. Mock only the API layer. - Smart components (pages): Integration tests with
mswfor API mocking. - Query components: Wrap with
QueryClientProviderusing a test-specific client withretry: falseandgcTime: 0. - Test behavior, not implementation. Never test internal state directly.
- Test accessibility: Use
toHaveAccessibleName,toBeEnabled,toBeDisabled,toHaveNoViolations. - Test keyboard interactions: Use
userEvent.keyboardto verify Enter/Space/Escape/Arrow behavior.
Storybook as a Test Surface
Every component should have a Storybook story that serves as a visual integration test. Stories must:
- Work with mock props alone (no live API, no mandatory router/provider context)
- Cover all meaningful states: default, loading, error, empty, disabled, hover
- Use the
@storybook/addon-a11ypanel during review - Register sub-components for compound components
import type { Meta, StoryObj } from '@storybook/react-vite'import { createComponentDocs } from '@/lib/storybook-docs'
const meta = { component: UserCard, tags: ['autodocs'], parameters: { docs: { description: { component: createComponentDocs({ summary: 'Displays a user with edit and delete actions.', when: 'In user management lists.', whenNot: 'For read-only user mentions — use UserBadge instead.', motion: 'No animation.', reducedMotion: 'No change.', }), }, }, },} satisfies Meta<typeof UserCard>
export default metatype Story = StoryObj<typeof meta>
export const Default: Story = { args: { user: { id: '1', name: 'David Holmes', email: 'david@example.com', role: 'admin' }, },}
export const Deleting: Story = { args: { ...Default.args, isDeleting: true, },}Tooling
| Purpose | Tool |
|---|---|
| Unit / component | Vitest + React Testing Library |
| Accessibility automated | axe via jest-axe or @storybook/addon-a11y |
| API mocking | msw (Mock Service Worker) |
| Visual regression | Lost Pixel (NOT Chromatic — closed SaaS) |
| E2E | Not prescribed — pick per project |
Validation Pipeline
Run these checks before marking any component change done:
pnpm lintpnpm typecheckpnpm test # includes axe CI checks for critical componentspnpm run readiness:check
# For Storybook / visual changes:pnpm run catalogpnpm build-storybookpnpm test:visual
# Full build:pnpm buildNever claim a check passed unless it was run in the current session. If a check is skipped, say so explicitly.