AgentSkillsCN

testing-react

在使用Vitest/Jest与React Testing Library编写或修复React组件测试时使用此功能。涵盖渲染、用户事件、模拟、异步模式,以及无障碍测试。

SKILL.md
--- frontmatter
name: testing-react
description: Use when writing or fixing React component tests with Vitest/Jest and React Testing Library. Covers rendering, user events, mocking, async patterns, and accessibility testing.

React Testing with Vitest + React Testing Library

Core Principles

  • Test behavior, not implementation details. Never test internal state or component internals.
  • Query elements the way users find them: by role, label, text, placeholder.
  • Prefer userEvent over fireEvent for realistic interaction simulation.

Component Rendering

tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  it('renders user name and email', () => {
    render(<UserCard name="Alice" email="alice@example.com" />);
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });

  it('renders fallback avatar when no image provided', () => {
    render(<UserCard name="Alice" email="alice@example.com" />);
    expect(screen.getByRole('img', { name: /alice/i })).toHaveAttribute('src', '/default-avatar.png');
  });
});

User Events

tsx
import userEvent from '@testing-library/user-event';

it('calls onSubmit with form data', async () => {
  const user = userEvent.setup();
  const handleSubmit = vi.fn();

  render(<LoginForm onSubmit={handleSubmit} />);

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

  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'secret123',
  });
});

Async Testing

tsx
it('displays data after loading', async () => {
  render(<UserList />);

  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // waitFor retries until the assertion passes or times out
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
  });

  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

// Use findBy* as shorthand for waitFor + getBy
it('shows error on failure', async () => {
  server.use(http.get('/api/users', () => HttpResponse.error()));
  render(<UserList />);
  expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});

Mocking

tsx
// Mock a module
vi.mock('./api/users', () => ({
  fetchUsers: vi.fn().mockResolvedValue([{ id: 1, name: 'Alice' }]),
}));

// Mock a hook
vi.mock('./hooks/useAuth', () => ({
  useAuth: () => ({ user: { id: 1, name: 'Alice' }, isAuthenticated: true }),
}));

// Mock with MSW for network requests (preferred)
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([{ id: 1, name: 'Alice' }]);
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Snapshot Testing

Use sparingly and only for stable UI. Prefer explicit assertions.

tsx
it('matches snapshot for empty state', () => {
  const { container } = render(<EmptyState message="No results" />);
  expect(container.firstChild).toMatchSnapshot();
});

// Prefer inline snapshots for small outputs
it('renders correct class names', () => {
  render(<Badge variant="success">Done</Badge>);
  expect(screen.getByText('Done').className).toMatchInlineSnapshot('"badge badge-success"');
});

Accessibility Testing

tsx
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

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

Anti-Patterns

  • NEVER use container.querySelector when a Testing Library query works.
  • NEVER test implementation details like state values or component method calls.
  • NEVER use getByTestId as a first resort. Use semantic queries: getByRole, getByLabelText, getByText.
  • NEVER wrap act() manually around userEvent calls; userEvent.setup() handles it.
  • NEVER assert on snapshot length or raw HTML strings.

Wrapper for Providers

tsx
function renderWithProviders(ui: React.ReactElement, options = {}) {
  function Wrapper({ children }: { children: React.ReactNode }) {
    return (
      <QueryClientProvider client={new QueryClient()}>
        <ThemeProvider>{children}</ThemeProvider>
      </QueryClientProvider>
    );
  }
  return render(ui, { wrapper: Wrapper, ...options });
}