# React Testing Library

Testing utility for React emphasizing user-centric testing. Renders components in a test environment and provides queries for finding elements by accessible roles, labels, and text content.

## Package Information

- **Package Name**: @testing-library/react
- **Package Type**: npm
- **Language**: JavaScript/TypeScript
- **Installation**: `npm install --save-dev @testing-library/react @testing-library/dom`

## Peer Dependencies

- `react` (^18.0.0 || ^19.0.0)
- `react-dom` (^18.0.0 || ^19.0.0)
- `@testing-library/dom` (^10.0.0)
- `@types/react` (optional, ^18.0.0 || ^19.0.0)
- `@types/react-dom` (optional, ^18.0.0 || ^19.0.0)

## Core Imports

```typescript
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react';
```

**Pure version** (without auto-cleanup):
```typescript
import { render, screen } from '@testing-library/react/pure';
```

**CommonJS:**
```javascript
const { render, screen, fireEvent } = require('@testing-library/react');
```

## Quick Reference

### Essential Pattern
```typescript
// Render component
render(<Component />);

// Find element (prefer role queries)
const button = screen.getByRole('button', { name: /submit/i });

// Interact
fireEvent.click(button);

// Assert
expect(screen.getByText(/success/i)).toBeInTheDocument();

// Async wait
await screen.findByText(/loaded/i);
```

### Test Hook
```typescript
const { result } = renderHook(() => useCustomHook());
act(() => result.current.action());
expect(result.current.value).toBe(expected);
```

## Architecture

React Testing Library is built around several key concepts:

- **Rendering Utilities**: Core `render()` and `renderHook()` functions for mounting React components and hooks in a test environment
- **Query System**: Inherited from @testing-library/dom, provides accessible queries (getBy*, queryBy*, findBy*) that encourage testing from a user's perspective
- **Event System**: Enhanced `fireEvent` with React-specific behaviors for simulating user interactions
- **act() Wrapper**: Automatic wrapping of renders and updates in React's act() to ensure proper state updates
- **Auto-cleanup**: Automatic unmounting and cleanup between tests (can be disabled via pure import)
- **Configuration**: Global and per-test configuration for strict mode, queries, wrappers, and error handling

## Core Capabilities

### Rendering: [rendering.md](./rendering.md)

```typescript { .api }
function render(ui: React.ReactNode, options?: RenderOptions): RenderResult;

interface RenderOptions {
  container?: HTMLElement;              // Custom container (e.g., for <tbody>)
  baseElement?: HTMLElement;            // Base element for queries (defaults to container)
  wrapper?: React.ComponentType<{ children: React.ReactNode }>;  // Provider wrapper
  hydrate?: boolean;                    // SSR hydration mode
  legacyRoot?: boolean;                 // React 18 only, not supported in React 19+
  queries?: Queries;                    // Custom query set
  reactStrictMode?: boolean;            // Enable StrictMode
  onCaughtError?: (error: Error, errorInfo: { componentStack?: string }) => void;  // React 19+ only
  onRecoverableError?: (error: Error, errorInfo: { componentStack?: string }) => void;  // React 18+
}

interface RenderResult {
  container: HTMLElement;               // DOM container element
  baseElement: HTMLElement;             // Base element for queries
  rerender: (ui: React.ReactNode) => void;  // Re-render with new UI
  unmount: () => void;                  // Unmount and cleanup
  debug: (element?, maxLength?, options?) => void;  // Pretty-print DOM
  asFragment: () => DocumentFragment;   // Snapshot testing helper
  // All query functions bound to baseElement:
  getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
  getAllByRole: (role: string, options?: ByRoleOptions) => HTMLElement[];
  queryByRole: (role: string, options?: ByRoleOptions) => HTMLElement | null;
  queryAllByRole: (role: string, options?: ByRoleOptions) => HTMLElement[];
  findByRole: (role: string, options?: ByRoleOptions) => Promise<HTMLElement>;
  findAllByRole: (role: string, options?: ByRoleOptions) => Promise<HTMLElement[]>;
  // Plus: getBy/queryBy/findBy LabelText, PlaceholderText, Text, DisplayValue, AltText, Title, TestId
}
```

**Key Patterns:**
```typescript
// Basic render
const { container, rerender } = render(<App />);

// With providers
render(<App />, {
  wrapper: ({ children }) => <Provider>{children}</Provider>
});

// Re-render with props
rerender(<App count={2} />);
```

### Querying: [queries.md](./queries.md)

**Query Priority:** Role > Label > Placeholder > Text > DisplayValue > AltText > Title > TestId

```typescript { .api }
// Synchronous queries
getByRole(role: string, options?: ByRoleOptions): HTMLElement;
getAllByRole(role: string, options?: ByRoleOptions): HTMLElement[];

queryByRole(role: string, options?: ByRoleOptions): HTMLElement | null;
queryAllByRole(role: string, options?: ByRoleOptions): HTMLElement[];

// Async queries (wait for element to appear)
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;
findAllByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement[]>;

// Available query types: Role, LabelText, PlaceholderText, Text, DisplayValue, AltText, Title, TestId
```

**Common Patterns:**
```typescript
// Prefer role queries (most accessible)
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });

// Form inputs
screen.getByLabelText('Email');
screen.getByPlaceholderText('Enter email');

// Text content
screen.getByText(/hello world/i);

// Negative assertions
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

// Scoped queries
const modal = screen.getByRole('dialog');
within(modal).getByRole('button', { name: /close/i });
```

### Events: [events.md](./events.md)

```typescript { .api }
// Common events (automatically wrapped in act())
fireEvent.click(element, options?: MouseEventInit);
fireEvent.change(element, { target: { value: 'text' } });
fireEvent.submit(form);
fireEvent.focus(element);
fireEvent.blur(element);
fireEvent.keyDown(element, { key: 'Enter', code: 'Enter' });
```

**Patterns:**
```typescript
// Click interaction
fireEvent.click(screen.getByRole('button'));

// Input change
fireEvent.change(screen.getByLabelText('Email'), {
  target: { value: 'user@example.com' }
});

// Keyboard
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });

// Hover
fireEvent.mouseEnter(element);
fireEvent.mouseLeave(element);
```

### Async Operations: [async.md](./async.md)

```typescript { .api }
// Wait for condition
function waitFor<T>(callback: () => T | Promise<T>, options?: {
  timeout?: number;  // default: 1000ms
  interval?: number; // default: 50ms
}): Promise<T>;

// Wait for removal
function waitForElementToBeRemoved<T>(
  element: T | (() => T),
  options?: { timeout?: number }
): Promise<void>;

// Async queries (shorthand for waitFor + getBy)
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;
```

**Patterns:**
```typescript
// Wait for element to appear
const element = await screen.findByText(/loaded/i);

// Wait for condition
await waitFor(() => {
  expect(screen.getByText(/success/i)).toBeInTheDocument();
}, { timeout: 3000 });

// Wait for disappearance
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
```

### Hook Testing: [hooks.md](./hooks.md)

```typescript { .api }
function renderHook<Result, Props>(
  callback: (props: Props) => Result,
  options?: RenderHookOptions<Props>
): RenderHookResult<Result, Props>;

interface RenderHookResult<Result, Props> {
  result: { current: Result };
  rerender: (props?: Props) => void;
  unmount: () => void;
}
```

**Patterns:**
```typescript
// Test hook
const { result } = renderHook(() => useCounter(0));

// Trigger updates (wrap in act)
act(() => result.current.increment());
expect(result.current.count).toBe(1);

// With context
const { result } = renderHook(() => useUser(), {
  wrapper: ({ children }) => <UserProvider>{children}</UserProvider>
});

// Re-render with props
const { result, rerender } = renderHook(
  ({ id }) => useFetch(id),
  { initialProps: { id: 1 } }
);
rerender({ id: 2 });
```

### Configuration: [configuration.md](./configuration.md)

```typescript { .api }
function configure(config: Partial<Config>): void;

interface Config {
  reactStrictMode: boolean;        // default: false
  testIdAttribute: string;         // default: 'data-testid'
  asyncUtilTimeout: number;        // default: 1000ms
}
```

**Setup Pattern:**
```typescript
// test-setup.js
import { configure } from '@testing-library/react';

configure({
  reactStrictMode: true,
  asyncUtilTimeout: 2000,
});
```

## Common Type Definitions

```typescript { .api }
interface ByRoleOptions {
  name?: string | RegExp;              // Accessible name filter
  description?: string | RegExp;       // Accessible description filter
  hidden?: boolean;                    // Include hidden elements (default: false)
  selected?: boolean;                  // Filter by selected state
  checked?: boolean;                   // Filter by checked state
  pressed?: boolean;                   // Filter by pressed state (toggle buttons)
  current?: boolean | string;          // Filter by current state (navigation)
  expanded?: boolean;                  // Filter by expanded state
  level?: number;                      // Filter by heading level (1-6)
  exact?: boolean;                     // Enable exact matching (default: true)
  normalizer?: (text: string) => string;  // Custom text normalizer
  queryFallbacks?: boolean;            // Enable query fallbacks
}

interface MatcherOptions {
  exact?: boolean;                     // Enable exact matching (default: true)
  normalizer?: (text: string) => string;  // Custom text normalizer
}

interface SelectorMatcherOptions extends MatcherOptions {
  selector?: string;                   // CSS selector to filter results
}

interface Queries {
  [key: string]: (...args: any[]) => any;
}
```

## Production Patterns

### Custom Render with Providers
```typescript
import { render, RenderOptions } from '@testing-library/react';
import { ThemeProvider } from './theme';
import { QueryClientProvider } from '@tanstack/react-query';

export function renderWithProviders(
  ui: React.ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) {
  return render(ui, {
    wrapper: ({ children }) => (
      <QueryClientProvider client={queryClient}>
        <ThemeProvider>{children}</ThemeProvider>
      </QueryClientProvider>
    ),
    ...options,
  });
}
```

### Testing Async Data Fetching
```typescript
test('loads and displays data', async () => {
  render(<DataComponent />);

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

  const data = await screen.findByText(/data loaded/i);
  expect(data).toBeInTheDocument();

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

### Testing Form Submission
```typescript
test('submits form with validation', async () => {
  const handleSubmit = jest.fn();
  render(<Form onSubmit={handleSubmit} />);

  const emailInput = screen.getByLabelText(/email/i);
  const submitButton = screen.getByRole('button', { name: /submit/i });

  fireEvent.change(emailInput, { target: { value: 'user@example.com' } });
  fireEvent.click(submitButton);

  await waitFor(() => {
    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'user@example.com'
    });
  });
});
```

### Testing Modal Interactions
```typescript
test('opens and closes modal', async () => {
  render(<App />);

  const openButton = screen.getByRole('button', { name: /open modal/i });
  fireEvent.click(openButton);

  const modal = await screen.findByRole('dialog');
  expect(modal).toBeInTheDocument();

  const closeButton = within(modal).getByRole('button', { name: /close/i });
  fireEvent.click(closeButton);

  await waitForElementToBeRemoved(modal);
});
```

## Best Practices

1. **Query Priority**: Use getByRole when possible for accessibility
2. **Async Queries**: Use findBy* for async elements, waitFor for complex conditions
3. **Negative Assertions**: Use queryBy* with .not.toBeInTheDocument()
4. **act() Wrapping**: render/fireEvent/waitFor handle this automatically
5. **screen vs destructuring**: Prefer screen for better error messages
6. **Scoped Queries**: Use within() to limit query scope
7. **Real User Behavior**: Test what users see and do, not implementation

## Entry Points

### Main Entry (`@testing-library/react`)

Default import with auto-cleanup and automatic act() environment setup. Use for standard test setups with Jest, Vitest, or similar test runners.

```typescript
import { render, screen } from '@testing-library/react';
// Auto-cleanup runs after each test
```

### Pure Entry (`@testing-library/react/pure`)

No auto-cleanup or automatic setup. Use when you need manual control over cleanup or test lifecycle.

```typescript
import { render, screen, cleanup } from '@testing-library/react/pure';

afterEach(() => {
  cleanup(); // Manual cleanup
});
```

### Dont-Cleanup-After-Each Entry

Alternative method to disable auto-cleanup by importing this file before the main import. Sets `RTL_SKIP_AUTO_CLEANUP=true` environment variable.

```typescript
import '@testing-library/react/dont-cleanup-after-each';
import { render, screen, cleanup } from '@testing-library/react';

// Auto-cleanup is now disabled, manual cleanup required
afterEach(() => {
  cleanup();
});
```

## Common Utility Functions

### screen Object

Pre-bound queries to document.body for convenient access without destructuring render results.

```typescript { .api }
const screen: {
  // All query functions
  getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
  // ... all other query functions

  // Debug utilities
  debug: (element?: HTMLElement, maxLength?: number) => void;
  logTestingPlaygroundURL: (element?: HTMLElement) => void;
};
```

### within Function

Get queries bound to a specific element for scoped searches.

```typescript { .api }
function within(element: HTMLElement): {
  getByRole: (role: string, options?: ByRoleOptions) => HTMLElement;
  // ... all other query functions
};
```

### Debug Utilities

```typescript { .api }
function prettyDOM(
  element?: HTMLElement,
  maxLength?: number,
  options?: any
): string;

function logRoles(container: HTMLElement): void;
```

### Lifecycle Management

```typescript { .api }
// Manual cleanup (auto-runs by default)
function cleanup(): void;

// Manual act() wrapping (usually automatic)
function act(callback: () => void | Promise<void>): Promise<void>;
```

**Note:** Most operations are automatically wrapped in act(), so manual use is rarely needed.
