mcpField
R
react-testing

react-testing

github.com/claude-dev-suiteTesting & quality4641 installsUpdated 5d ago
Repository

About react-testing

A reference skill for testing React components with Vitest and Testing Library. It helps React developers write behavior-focused tests, including user interaction, asynchronous updates, and mocks.

Key features

  1. 01Vitest and Testing Library setup
  2. 02Accessible element queries
  3. 03User interaction testing
  4. 04Async state and updates
  5. 05Function and module mocking

Use cases

  1. 01Test component behavior through rendered UI
  2. 02Check form interactions and submissions
  3. 03Cover loading and asynchronous data
  4. 04Mock API modules and function results

MCP support

The skill points to `mcp__documentation__fetch_docs` for React testing documentation.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill react-testing

After installing, type/react-testingin Claude Code to confirm it loaded.

Bundled filesadvanced.md

Details

From the project README

React Testing

Full Reference: See advanced.md for MSW setup, testing hooks, testing context, forms, accessibility testing, snapshots, and test patterns.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: react topic: testing for comprehensive documentation.

Setup with Vitest + Testing Library

// 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',
    css: true,
  },
});

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

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

Basic Component Testing

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';

describe('Button', () => {
  it('renders children', () => {
    render(<Button onClick={() => {}}>Click me</Button>);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    await userEvent.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button onClick={() => {}} disabled>Click me</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Query Methods

// Priority order (prefer accessible queries)
// 1. getByRole - accessible to everyone
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('heading', { level: 1 });

// 2. getByLabelText - for form fields
screen.getByLabelText(/email address/i);

// 3. getByPlaceholderText
screen.getByPlaceholderText(/enter your email/i);

// 4. getByText - for non-interactive elements
screen.getByText(/welcome to our app/i);

// 5. getByDisplayValue - for input values
screen.getByDisplayValue(/[email protected]/i);

// 6. getByAltText - for images
screen.getByAltText(/user avatar/i);

// 7. getByTitle
screen.getByTitle(/close/i);

// 8. getByTestId - last resort
screen.getByTestId('custom-element');

// Query variants
screen.getByRole('button');     // Throws if not found
screen.queryByRole('button');   // Returns null if not found
screen.findByRole('button');    // Returns Promise, waits for element
screen.getAllByRole('button');  // Returns array, throws if none
screen.queryAllByRole('button'); // Returns array (possibly empty)
screen.findAllByRole('button'); // Returns Promise of array

User Events

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

describe('Form', () => {
  it('submits with user input', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();

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

    // Type in inputs
    await user.type(screen.getByLabelText(/email/i), '[email protected]');
    await user.type(screen.getByLabelText(/password/i), 'secret123');

    // Click submit
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'secret123',
    });
  });

  it('handles keyboard navigation', async () => {
    const user = userEvent.setup();
    render(<Form />);

    // Tab through inputs
    await user.tab();
    expect(screen.getByLabelText(/email/i)).toHaveFocus();

    await user.tab();
    expect(screen.getByLabelText(/password/i)).toHaveFocus();

    // Type and submit with Enter
    await user.type(screen.getByLabelText(/password/i), 'secret{Enter}');
  });

  it('handles select and checkbox', async () => {
    const user = userEvent.setup();
    render(<SettingsForm />);

    // Select option
    await user.selectOptions(screen.getByRole('combobox'), ['dark']);

    // Toggle checkbox
    await user.click(screen.getByRole('checkbox', { name: /notifications/i }));

    expect(screen.getByRole('checkbox')).toBeChecked();
  });
});

Async Testing

describe('UserProfile', () => {
  it('shows loading then user data', async () => {
    render(<UserProfile userId="123" />);

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

    // Wait for data to load
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });

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

// Using findBy (combines getBy + waitFor)
it('loads and displays items', async () => {
  render(<ItemList />);

  // findBy waits for element
  const items = await screen.findAllByRole('listitem');
  expect(items).toHaveLength(3);
});

// waitForElementToBeRemoved
it('removes loading indicator', async () => {
  render(<DataLoader />);

  await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));

  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

Mocking

Mock Functions
import { vi } from 'vitest';

const mockFn = vi.fn();
mockFn.mockReturnValue('default');
mockFn.mockReturnValueOnce('first call');
mockFn.mockImplementation((x) => x * 2);
mockFn.mockResolvedValue({ data: [] });
mockFn.mockRejectedValue(new Error('Failed'));

// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('last arg');
Mock Modules
// Mock a module
vi.mock('@/lib/api', () => ({
  fetchUsers: vi.fn(() => Promise.resolve([{ id: 1, name: 'John' }])),
}));

// Import after mocking
import { fetchUsers } from '@/lib/api';

it('fetches users', async () => {
  render(<UserList />);

  expect(fetchUsers).toHaveBeenCalled();
  await screen.findByText('John');
});

Common Pitfalls

IssueProblemSolution
Test not finding elementElement rendered asyncUse findBy or waitFor
State not updatingMissing act()Use userEvent (handles act)
Tests affecting each otherShared stateClean up in afterEach
Flaky testsRace conditionsUse proper async patterns

Best Practices

  • Test behavior, not implementation
  • Use accessible queries (getByRole)
  • Use userEvent over fireEvent
  • Test error states
  • Use MSW for API mocking
  • Don't test implementation details
  • Don't test third-party libraries
  • Don't overuse snapshots

When NOT to Use This Skill

  • End-to-end testing - Use Playwright skill for full E2E flows
  • Backend testing - Use framework-specific testing skills
  • Performance testing - Use specialized performance testing tools
  • Visual regression testing - Use tools like Percy or Chromatic

Anti-Patterns

Anti-PatternProblemSolution
Testing implementation detailsBrittle testsTest user-facing behavior
Using getByTestId firstNot testing accessibilityPrefer getByRole, getByLabelText
Using fireEvent instead of userEventDoesn't simulate real user interactionUse userEvent for realistic tests
Not waiting for async updatesFlaky testsUse findBy or waitFor
Snapshot testing everythingHard to maintainUse sparingly for stable UI
Testing third-party librariesWasted effortTrust library tests, test your integration
Not testing error statesMissing edge casesTest loading, error, empty states
Shallow renderingMissing integration issuesUse full render

Quick Troubleshooting

IssueLikely CauseFix
Element not foundQuery timing, wrong queryUse findBy for async or check query
Test timeoutAsync operation not completingCheck waitFor timeout, fix async code
Act warningState update outside actUse userEvent or wrap in act()
Flaky testsRace conditions, timingUse proper async queries (findBy, waitFor)
Can't test hooksTesting implementationExtract to component or use renderHook
Mock not workingMock after importMock before importing component
Tests affecting each otherShared stateClean up in afterEach

Reference Documentation

Comments

Markdown supported