# Async Utilities

Wait for asynchronous changes with automatic act() wrapping.

## API

### waitFor Function

Waits for a condition to be met, repeatedly calling the callback until it succeeds or times out. Useful for waiting for asynchronous state updates, API calls, or side effects.

```typescript { .api }
/**
 * Wait for a condition to be met by repeatedly calling the callback
 * @param callback - Function to call repeatedly until it doesn't throw
 * @param options - Configuration options
 * @returns Promise resolving to callback's return value
 * @throws When timeout is reached or onTimeout callback returns error
 */
function waitFor<T>(
  callback: () => T | Promise<T>,
  options?: {
    /**
     * Maximum time to wait in milliseconds (default: 1000)
     */
    timeout?: number;

    /**
     * Time between callback calls in milliseconds (default: 50)
     */
    interval?: number;

    /**
     * Custom error handler when timeout is reached
     */
    onTimeout?: (error: Error) => Error;

    /**
     * Suppress timeout errors for debugging (default: false)
     */
    mutationObserverOptions?: MutationObserverInit;
  }
): Promise<T>;
```

### waitForElementToBeRemoved Function

Waits for an element to be removed from the DOM. Useful for testing loading states, modals closing, or elements being unmounted.

```typescript { .api }
/**
 * Wait for element(s) to be removed from the DOM
 * @param callback - Element, elements, or function returning element(s) to wait for removal
 * @param options - Configuration options
 * @returns Promise that resolves when element(s) are removed
 * @throws When timeout is reached
 */
function waitForElementToBeRemoved<T>(
  callback: T | (() => T),
  options?: {
    /**
     * Maximum time to wait in milliseconds (default: 1000)
     */
    timeout?: number;

    /**
     * Time between checks in milliseconds (default: 50)
     */
    interval?: number;

    /**
     * Suppress timeout errors for debugging (default: false)
     */
    mutationObserverOptions?: MutationObserverInit;
  }
): Promise<void>;
```

### findBy* Queries

Async query variants that wait for elements to appear. These are convenience wrappers around `waitFor` + `getBy*` queries.

```typescript { .api }
/**
 * Async queries that wait for elements to appear
 * All getBy* queries have findBy* async equivalents
 */
findByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement>;
findAllByRole(role: string, options?: ByRoleOptions): Promise<HTMLElement[]>;

findByLabelText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement>;
findAllByLabelText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement[]>;

findByPlaceholderText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
findAllByPlaceholderText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

findByText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement>;
findAllByText(text: string | RegExp, options?: SelectorMatcherOptions): Promise<HTMLElement[]>;

findByDisplayValue(value: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
findAllByDisplayValue(value: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

findByAltText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
findAllByAltText(text: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

findByTitle(title: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
findAllByTitle(title: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;

findByTestId(testId: string | RegExp, options?: MatcherOptions): Promise<HTMLElement>;
findAllByTestId(testId: string | RegExp, options?: MatcherOptions): Promise<HTMLElement[]>;
```

## Common Patterns

### Wait for Element to Appear
```typescript
// Using findBy* (recommended for single element)
const message = await screen.findByText(/loaded/i);

// Using waitFor (for complex assertions)
await waitFor(() => {
  expect(screen.getByText(/loaded/i)).toBeInTheDocument();
});
```

### Wait for Element to Disappear
```typescript
// Wait for removal
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));

// Or use waitFor with queryBy
await waitFor(() => {
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
```

### Wait for Multiple Conditions
```typescript
await waitFor(() => {
  expect(screen.getByText('Title')).toBeInTheDocument();
  expect(screen.getByText('Content')).toBeInTheDocument();
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
```

### Custom Timeout
```typescript
await waitFor(
  () => expect(screen.getByText('Slow load')).toBeInTheDocument(),
  { timeout: 5000 }  // 5 seconds
);
```

## Testing Patterns

### API Data Loading
```typescript
test('loads user data', async () => {
  render(<UserProfile userId="123" />);

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

  // Wait for data
  const name = await screen.findByText(/john doe/i);
  expect(name).toBeInTheDocument();

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

### Async State Updates
```typescript
test('updates after async operation', async () => {
  render(<AsyncCounter />);

  fireEvent.click(screen.getByRole('button', { name: /increment/i }));

  // Wait for state update
  await waitFor(() => {
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
});
```

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

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

  fireEvent.click(closeButton);

  await waitForElementToBeRemoved(modal);
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
```

### Form Validation
```typescript
test('shows validation after submit', async () => {
  render(<Form />);

  fireEvent.click(screen.getByRole('button', { name: /submit/i }));

  const error = await screen.findByRole('alert');
  expect(error).toHaveTextContent('Email is required');
});
```

### Polling/Retries
```typescript
test('retries failed request', async () => {
  render(<DataFetcher />);

  // Shows initial error
  expect(await screen.findByText(/error/i)).toBeInTheDocument();

  // Retries and succeeds
  const data = await screen.findByText(/success/i, {}, { timeout: 5000 });
  expect(data).toBeInTheDocument();
});
```

## Advanced Patterns

### Wait for Multiple Elements
```typescript
test('loads all items', async () => {
  render(<ItemList />);

  const items = await screen.findAllByRole('listitem');
  expect(items).toHaveLength(5);
});
```

### Async Callback Return Value
```typescript
test('returns value from waitFor', async () => {
  render(<Component />);

  const element = await waitFor(() => screen.getByText('Value'));
  expect(element).toHaveAttribute('data-loaded', 'true');
});
```

### Custom Error Messages
```typescript
test('provides context on timeout', async () => {
  render(<Component />);

  await waitFor(
    () => expect(screen.getByText('Expected')).toBeInTheDocument(),
    {
      timeout: 2000,
      onTimeout: (error) => {
        screen.debug();
        return new Error(`Element not found after 2s: ${error.message}`);
      },
    }
  );
});
```

### Waiting for Hook Updates
```typescript
test('hook updates async state', async () => {
  const { result } = renderHook(() => useAsyncData());

  expect(result.current.loading).toBe(true);

  await waitFor(() => {
    expect(result.current.loading).toBe(false);
  });

  expect(result.current.data).toBeDefined();
});
```

## findBy* vs waitFor

### Use findBy* when:
- Waiting for single element to appear
- Simple query without complex assertions

```typescript
const button = await screen.findByRole('button');
```

### Use waitFor when:
- Multiple assertions needed
- Complex conditions
- Checking element properties

```typescript
await waitFor(() => {
  const button = screen.getByRole('button');
  expect(button).toBeEnabled();
  expect(button).toHaveTextContent('Submit');
});
```

## Common Pitfalls

### ❌ Wrong: Using getBy without waiting
```typescript
// Will fail if element not immediately present
fireEvent.click(button);
expect(screen.getByText('Success')).toBeInTheDocument();  // ❌
```

### ✅ Correct: Use findBy or waitFor
```typescript
fireEvent.click(button);
const success = await screen.findByText('Success');  // ✅
expect(success).toBeInTheDocument();
```

### ❌ Wrong: Return value instead of assertion
```typescript
await waitFor(() => element !== null);  // ❌ Never retries
```

### ✅ Correct: Use assertions that throw
```typescript
await waitFor(() => {
  expect(element).toBeInTheDocument();  // ✅ Throws until true
});
```

### ❌ Wrong: waitFor for synchronous state
```typescript
fireEvent.click(button);
await waitFor(() => {  // ❌ Unnecessary
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
```

### ✅ Correct: Direct assertion for sync updates
```typescript
fireEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();  // ✅
```

## Configuration

### Global Timeout
```typescript
import { configure } from '@testing-library/react';

configure({ asyncUtilTimeout: 2000 });  // 2 seconds default
```

### Per-Test Timeout
```typescript
test('slow operation', async () => {
  await waitFor(
    () => expect(screen.getByText('Done')).toBeInTheDocument(),
    { timeout: 10000 }  // Override for this test
  );
});
```

## Debugging

### Print State on Timeout
```typescript
await waitFor(
  () => {
    screen.debug();  // Print current DOM
    expect(screen.getByText('Expected')).toBeInTheDocument();
  },
  {
    onTimeout: (error) => {
      screen.debug();  // Print final state
      return error;
    },
  }
);
```

## Important Notes

### Automatic act() Wrapping

All async utilities automatically wrap operations in React's `act()`, ensuring state updates are properly flushed:

```typescript
// No manual act() needed
await waitFor(() => {
  expect(screen.getByText('Updated')).toBeInTheDocument();
});
```

### Assertions in waitFor

The callback passed to `waitFor` should contain assertions or throw errors. `waitFor` will retry until the callback doesn't throw:

```typescript
// CORRECT: Assertion that throws
await waitFor(() => {
  expect(element).toBeInTheDocument();
});

// WRONG: Always returns true, never retries
await waitFor(() => {
  return element !== null;
});
```

### Default Timeouts

- Default timeout: 1000ms (1 second)
- Default interval: 50ms
- Can be configured globally via `configure()` or per-call via options

### waitFor vs findBy

Both work similarly, but have different use cases:

```typescript
// findBy: Convenient for single queries
const element = await screen.findByText('Hello');

// waitFor: Better for complex assertions
await waitFor(() => {
  expect(screen.getByText('Hello')).toBeInTheDocument();
  expect(screen.getByText('World')).toBeInTheDocument();
});
```

### Error Messages

When `waitFor` times out, it includes the last error thrown by the callback:

```typescript
// Timeout error will include "Expected element to be in document"
await waitFor(() => {
  expect(screen.getByText('Missing')).toBeInTheDocument();
});
```

### MutationObserver

`waitFor` uses MutationObserver to efficiently wait for DOM changes. It will check the callback:

1. After every DOM mutation
2. At the specified interval (default 50ms)
3. Until timeout is reached (default 1000ms)

This makes it efficient for most async scenarios without constant polling.

## Best Practices

1. **Prefer findBy*** for simple waits
2. **Use waitFor** for complex conditions
3. **Set realistic timeouts** - default 1s usually sufficient
4. **Don't waitFor sync operations** - fireEvent updates are synchronous
5. **Assert in callbacks** - waitFor needs thrown errors to retry
6. **Use queryBy in waitFor** - for checking absence
7. **Avoid waitFor for everything** - only use when actually async
