Skip to content

Testing

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/expert-react-frontend-engineer/testing.md
DescriptionNot 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 renderHook from 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 msw for API mocking.
  • Query components: Wrap with QueryClientProvider using a test-specific client with retry: false and gcTime: 0.
  • Test behavior, not implementation. Never test internal state directly.
  • Test accessibility: Use toHaveAccessibleName, toBeEnabled, toBeDisabled, toHaveNoViolations.
  • Test keyboard interactions: Use userEvent.keyboard to 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-a11y panel 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 meta
type 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

PurposeTool
Unit / componentVitest + React Testing Library
Accessibility automatedaxe via jest-axe or @storybook/addon-a11y
API mockingmsw (Mock Service Worker)
Visual regressionLost Pixel (NOT Chromatic — closed SaaS)
E2ENot prescribed — pick per project

Validation Pipeline

Run these checks before marking any component change done:

Terminal window
pnpm lint
pnpm typecheck
pnpm test # includes axe CI checks for critical components
pnpm run readiness:check
# For Storybook / visual changes:
pnpm run catalog
pnpm build-storybook
pnpm test:visual
# Full build:
pnpm build

Never claim a check passed unless it was run in the current session. If a check is skipped, say so explicitly.