# Component Rendering

Renders React components in a test DOM environment with queries and lifecycle control.

## API

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

interface RenderOptions {
  /**
   * Custom container element. If not provided, a div appended to baseElement is used.
   * For testing elements like <tbody>, provide a table element as container.
   */
  container?: HTMLElement;

  /**
   * Base element for queries. Defaults to container if specified, otherwise document.body.
   * Also used as the element printed by debug().
   */
  baseElement?: HTMLElement;

  /**
   * Wrapper component to render around the UI.
   * Useful for providers (Context, Router, Theme, etc.).
   */
  wrapper?: React.JSXElementConstructor<{ children: React.ReactNode }>;

  /**
   * Use hydration instead of normal render (ReactDOM.hydrateRoot).
   * Useful for server-side rendering scenarios.
   */
  hydrate?: boolean;

  /**
   * Force synchronous ReactDOM.render instead of concurrent mode.
   * Only supported in React 18. Not supported in React 19+.
   * Throws an error if used with React 19 or later.
   */
  legacyRoot?: boolean;

  /**
   * Custom query set to bind. Overrides default queries from @testing-library/dom.
   */
  queries?: Queries;

  /**
   * Enable React.StrictMode wrapper around the component.
   * Overrides global reactStrictMode config if specified.
   */
  reactStrictMode?: boolean;

  /**
   * React 19+ only: Callback when React catches an error in an Error Boundary.
   * Receives the error and errorInfo with componentStack.
   * Only available in React 19 and later.
   */
  onCaughtError?: (error: Error, errorInfo: { componentStack?: string }) => void;

  /**
   * Callback when React automatically recovers from errors.
   * Receives error and errorInfo with componentStack.
   * Some recoverable errors may include original error cause as error.cause.
   * Available in React 18 and later.
   */
  onRecoverableError?: (error: Error, errorInfo: { componentStack?: string }) => void;
}

interface RenderResult {
  /**
   * The DOM container element where the component was rendered
   */
  container: HTMLElement;

  /**
   * The base element used for queries (container or document.body)
   */
  baseElement: HTMLElement;

  /**
   * Re-renders the component with new UI
   */
  rerender: (ui: React.ReactNode) => void;

  /**
   * Unmounts the component and cleans up
   */
  unmount: () => void;

  /**
   * Pretty-prints the DOM for debugging
   */
  debug: (
    element?: HTMLElement | HTMLElement[],
    maxLength?: number,
    options?: any
  ) => void;

  /**
   * Returns a DocumentFragment of the container's innerHTML
   */
  asFragment: () => DocumentFragment;

  // All query functions from @testing-library/dom 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[]>;

  getByLabelText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement;
  getAllByLabelText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement[];
  queryByLabelText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement | null;
  queryAllByLabelText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement[];
  findByLabelText: (text: string | RegExp, options?: SelectorMatcherOptions) => Promise<HTMLElement>;
  findAllByLabelText: (text: string | RegExp, options?: SelectorMatcherOptions) => Promise<HTMLElement[]>;

  getByPlaceholderText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement;
  getAllByPlaceholderText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  queryByPlaceholderText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement | null;
  queryAllByPlaceholderText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  findByPlaceholderText: (text: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement>;
  findAllByPlaceholderText: (text: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement[]>;

  getByText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement;
  getAllByText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement[];
  queryByText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement | null;
  queryAllByText: (text: string | RegExp, options?: SelectorMatcherOptions) => HTMLElement[];
  findByText: (text: string | RegExp, options?: SelectorMatcherOptions) => Promise<HTMLElement>;
  findAllByText: (text: string | RegExp, options?: SelectorMatcherOptions) => Promise<HTMLElement[]>;

  getByDisplayValue: (value: string | RegExp, options?: MatcherOptions) => HTMLElement;
  getAllByDisplayValue: (value: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  queryByDisplayValue: (value: string | RegExp, options?: MatcherOptions) => HTMLElement | null;
  queryAllByDisplayValue: (value: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  findByDisplayValue: (value: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement>;
  findAllByDisplayValue: (value: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement[]>;

  getByAltText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement;
  getAllByAltText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  queryByAltText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement | null;
  queryAllByAltText: (text: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  findByAltText: (text: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement>;
  findAllByAltText: (text: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement[]>;

  getByTitle: (title: string | RegExp, options?: MatcherOptions) => HTMLElement;
  getAllByTitle: (title: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  queryByTitle: (title: string | RegExp, options?: MatcherOptions) => HTMLElement | null;
  queryAllByTitle: (title: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  findByTitle: (title: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement>;
  findAllByTitle: (title: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement[]>;

  getByTestId: (testId: string | RegExp, options?: MatcherOptions) => HTMLElement;
  getAllByTestId: (testId: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  queryByTestId: (testId: string | RegExp, options?: MatcherOptions) => HTMLElement | null;
  queryAllByTestId: (testId: string | RegExp, options?: MatcherOptions) => HTMLElement[];
  findByTestId: (testId: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement>;
  findAllByTestId: (testId: string | RegExp, options?: MatcherOptions) => Promise<HTMLElement[]>;
}
```

## Common Patterns

### Basic Rendering
```typescript
const { container, getByRole, rerender } = render(<Counter initial={0} />);

expect(getByRole('button')).toBeInTheDocument();

// Re-render with new props
rerender(<Counter initial={5} />);
```

### With Providers
```typescript
const wrapper = ({ children }) => (
  <ThemeProvider theme="dark">
    <AuthProvider user={mockUser}>
      {children}
    </AuthProvider>
  </ThemeProvider>
);

render(<App />, { wrapper });
```

### Custom Container
```typescript
// For testing <tr>, <td>, etc.
const table = document.createElement('table');
const tbody = document.createElement('tbody');
table.appendChild(tbody);

render(<TableRow />, { container: tbody });
```

### Hydration (SSR)
```typescript
const container = document.getElementById('root');
container.innerHTML = serverRenderedHTML;

render(<App />, { container, hydrate: true });
```

### StrictMode
```typescript
render(<App />, { reactStrictMode: true });
```

## Testing Patterns

### Testing Re-renders
```typescript
test('updates on prop change', () => {
  const { rerender, getByText } = render(<Display value={1} />);
  expect(getByText('1')).toBeInTheDocument();

  rerender(<Display value={2} />);
  expect(getByText('2')).toBeInTheDocument();
});
```

### Testing Unmount/Cleanup
```typescript
test('cleans up on unmount', () => {
  const cleanup = jest.fn();

  function Component() {
    useEffect(() => cleanup, []);
    return <div>Content</div>;
  }

  const { unmount } = render(<Component />);
  unmount();

  expect(cleanup).toHaveBeenCalled();
});
```

### Snapshot Testing
```typescript
test('matches snapshot', () => {
  const { asFragment } = render(<App />);
  expect(asFragment()).toMatchSnapshot();
});
```

### Debug Utilities
```typescript
const { debug, getByRole } = render(<App />);

debug();                           // Print entire DOM
debug(getByRole('button'));        // Print specific element
debug(getByRole('button'), 1000);  // Limit output length
```

## Query Access

All queries available three ways:

```typescript
// 1. From render result
const { getByRole } = render(<Component />);
const button = getByRole('button');

// 2. From screen (recommended)
import { screen } from '@testing-library/react';
render(<Component />);
const button = screen.getByRole('button');

// 3. From within (scoped)
const modal = screen.getByRole('dialog');
const button = within(modal).getByRole('button');
```

## Render Modes

### Concurrent (Default React 18+)
```typescript
render(<App />);  // Uses ReactDOMClient.createRoot()
```

### Legacy (React 18 only)
```typescript
render(<App />, { legacyRoot: true });  // Error in React 19+
```

### Hydration
```typescript
render(<App />, { hydrate: true });  // Uses hydrateRoot()
```

## Production Setup

### Custom Render Utility
```typescript
// test-utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } }
});

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

// Use in tests
import { renderWithProviders } from './test-utils';

test('renders with providers', () => {
  renderWithProviders(<MyComponent />);
});
```

## Additional Types

```typescript { .api }
type RendererableContainer = Element | DocumentFragment;

interface MatcherOptions {
  exact?: boolean;
  normalizer?: (text: string) => string;
}

interface SelectorMatcherOptions extends MatcherOptions {
  selector?: string;
}

interface ByRoleOptions extends MatcherOptions {
  name?: string | RegExp;
  description?: string | RegExp;
  hidden?: boolean;
  selected?: boolean;
  checked?: boolean;
  pressed?: boolean;
  current?: boolean | string;
  expanded?: boolean;
  level?: number;
  queryFallbacks?: boolean;
}

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

## Deprecated Types

The following type aliases are deprecated and provided for backward compatibility:

```typescript { .api }
/** @deprecated Use RenderOptions instead */
type BaseRenderOptions<Q, Container, BaseElement> = RenderOptions;

/** @deprecated Use RenderOptions with hydrate: false instead */
interface ClientRenderOptions<Q, Container, BaseElement> extends RenderOptions {
  hydrate?: false | undefined;
}

/** @deprecated Use RenderOptions with hydrate: true instead */
interface HydrateOptions<Q, Container, BaseElement> extends RenderOptions {
  hydrate: true;
}
```

## Key Notes

- Auto-cleanup runs after each test (unless using /pure import)
- All operations automatically wrapped in act()
- Use screen queries for better error messages
- Prefer getByRole for accessibility testing
- legacyRoot option only works in React 18, throws error in React 19+
- onCaughtError callback only available in React 19+
