# React Testing

> When to activate: React Testing Library, vitest, jest, component tests, user-event, MSW, accessibility queries, async testing

- Skill: `mattakushi432/react-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/react-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/react-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/react-testing

---


# React Testing Patterns

## Setup
```ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    coverage: { thresholds: { lines: 80, functions: 80, branches: 80 } },
  },
})

// src/test/setup.ts
import '@testing-library/jest-dom'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'

afterEach(() => { cleanup() })
```

## Component Rendering
```tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

// Wrap with providers
function renderWithProviders(ui: React.ReactElement) {
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
  return render(
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={createMemoryRouter([{ path: '/', element: ui }])} />
    </QueryClientProvider>
  )
}
```

## Accessible Queries (priority order)
```tsx
// 1. By role — best (mirrors how users/AT perceive the element)
screen.getByRole('button', { name: /submit/i })
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('heading', { level: 1 })
screen.getByRole('link', { name: /home/i })

// 2. By label text
screen.getByLabelText(/email address/i)

// 3. By placeholder text
screen.getByPlaceholderText(/search/i)

// 4. By text
screen.getByText(/welcome back/i)

// 5. By test id (last resort)
screen.getByTestId('hero-section')
```

## User Interactions
```tsx
describe('LoginForm', () => {
  it('submits with valid credentials', async () => {
    const user = userEvent.setup()
    const onSubmit = vi.fn()
    render(<LoginForm onSubmit={onSubmit} />)

    await user.type(screen.getByLabelText(/email/i), 'alice@example.com')
    await user.type(screen.getByLabelText(/password/i), 'secret123')
    await user.click(screen.getByRole('button', { name: /log in/i }))

    expect(onSubmit).toHaveBeenCalledWith({
      email: 'alice@example.com',
      password: 'secret123',
    })
  })

  it('shows error for invalid email', async () => {
    const user = userEvent.setup()
    render(<LoginForm onSubmit={vi.fn()} />)

    await user.type(screen.getByLabelText(/email/i), 'not-an-email')
    await user.click(screen.getByRole('button', { name: /log in/i }))

    expect(screen.getByRole('alert')).toHaveTextContent(/invalid email/i)
  })
})
```

## Async & API Testing with MSW
```ts
// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: '1', name: 'Alice', email: 'alice@example.com' },
    ])
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json() as any
    return HttpResponse.json({ id: '2', ...body }, { status: 201 })
  }),
]

// src/test/setup.ts (add to existing)
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'

const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
```

```tsx
// Component test with data fetching
it('renders user list', async () => {
  renderWithProviders(<UserList />)

  // Wait for async content
  expect(await screen.findByText('Alice')).toBeInTheDocument()
})

// Override handler for specific test
it('shows error on API failure', async () => {
  server.use(
    http.get('/api/users', () => HttpResponse.json({ error: 'Server error' }, { status: 500 }))
  )
  renderWithProviders(<UserList />)
  expect(await screen.findByRole('alert')).toBeInTheDocument()
})
```

## Custom Hook Testing
```ts
import { renderHook, act } from '@testing-library/react'

describe('useCounter', () => {
  it('increments count', () => {
    const { result } = renderHook(() => useCounter(0))

    act(() => { result.current.increment() })

    expect(result.current.count).toBe(1)
  })

  it('resets to initial value', () => {
    const { result } = renderHook(() => useCounter(5))

    act(() => {
      result.current.increment()
      result.current.reset()
    })

    expect(result.current.count).toBe(5)
  })
})
```

## Snapshot Testing (selective use)
```tsx
// Use for stable, purely-presentational components
it('renders loading skeleton', () => {
  const { container } = render(<UserSkeleton />)
  expect(container.firstChild).toMatchSnapshot()
})
```

## Accessibility Testing
```tsx
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)

it('has no accessibility violations', async () => {
  const { container } = render(<LoginForm onSubmit={vi.fn()} />)
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})
```

## Testing Common Patterns

### Modal / Dialog
```tsx
it('opens and closes modal', async () => {
  const user = userEvent.setup()
  render(<ConfirmDialog />)

  await user.click(screen.getByRole('button', { name: /delete/i }))
  expect(screen.getByRole('dialog')).toBeInTheDocument()

  await user.click(screen.getByRole('button', { name: /cancel/i }))
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
```

### Form Validation
```tsx
it('disables submit button while loading', async () => {
  const user = userEvent.setup()
  render(<CreatePostForm />)

  await user.type(screen.getByLabelText(/title/i), 'My Post')
  await user.click(screen.getByRole('button', { name: /create/i }))

  expect(screen.getByRole('button', { name: /creating/i })).toBeDisabled()
})
```

