CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-testing-library--dom

Simple and complete DOM testing utilities that encourage good testing practices.

Pending
Overview
Eval results
Files

async.mddocs/

Async Utilities

Wait for conditions or elements to appear/disappear in the DOM for testing asynchronous behavior.

waitFor

Wait for a callback to succeed without throwing, with polling and MutationObserver optimization.

function waitFor<T>(
  callback: () => Promise<T> | T,
  options?: waitForOptions
): Promise<T>;

interface waitForOptions {
  container?: HTMLElement;  // Element to observe for mutations (default: document)
  timeout?: number;  // Max wait time in ms (default: 1000)
  interval?: number;  // Polling interval in ms (default: 50)
  onTimeout?: (error: Error) => Error;  // Custom timeout error
  mutationObserverOptions?: MutationObserverInit;
}

Usage:

import {waitFor, screen} from '@testing-library/dom';

// Wait for element to appear
await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeInTheDocument();
});

// Wait for specific condition
await waitFor(() => {
  const counter = screen.getByTestId('counter');
  expect(counter.textContent).toBe('5');
});

// Custom timeout
await waitFor(
  () => expect(screen.getByText('Slow')).toBeInTheDocument(),
  {timeout: 5000}
);

// With MutationObserver optimization
await waitFor(
  () => expect(screen.getByText('Content')).toBeInTheDocument(),
  {container: screen.getByTestId('dynamic-content')}
);

// Custom error
await waitFor(
  () => expect(screen.getByText('Ready')).toBeInTheDocument(),
  {
    timeout: 5000,
    onTimeout: (error) => new Error(`Custom: ${error.message}`)
  }
);

waitForElementToBeRemoved

Wait for elements to be removed from the DOM. Throws immediately if elements not present.

function waitForElementToBeRemoved<T>(
  callback: T | (() => T),
  options?: waitForOptions
): Promise<void>;

Usage:

import {waitForElementToBeRemoved, screen} from '@testing-library/dom';

// Wait for specific element
const spinner = screen.getByRole('status', {name: /loading/i});
await waitForElementToBeRemoved(spinner);

// Using query function
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));

// Multiple elements
const errors = screen.getAllByRole('alert');
await waitForElementToBeRemoved(errors);

// With timeout
await waitForElementToBeRemoved(
  () => screen.queryByTestId('modal'),
  {timeout: 3000}
);

Find Queries (Built-in Async)

All findBy* and findAllBy* queries are async and use waitFor internally:

// Using findBy (recommended for async content)
const element = await screen.findByText('Loaded content');

// Equivalent to:
const element = await waitFor(() => screen.getByText('Loaded content'));

// With options
const element = await screen.findByRole(
  'alert',
  {name: /error/i},
  {timeout: 3000}  // pass to waitForOptions
);

// findAllBy for multiple elements
const items = await screen.findAllByRole('listitem');

Best Practices

Use findBy* for async content

// Good
const element = await screen.findByText('Async content');

// Works but less idiomatic
await waitFor(() => {
  expect(screen.getByText('Async content')).toBeInTheDocument();
});

Use waitFor for complex conditions

// Multiple related assertions
await waitFor(() => {
  expect(screen.getByText('Username')).toBeInTheDocument();
  expect(screen.getByText('Email')).toBeInTheDocument();
  expect(screen.getByText('Status: Active')).toBeInTheDocument();
});

// Complex logic
await waitFor(() => {
  const items = screen.getAllByRole('listitem');
  expect(items.length).toBe(5);
  expect(items[0]).toHaveTextContent('First item');
});

Use waitForElementToBeRemoved for removal

// Good - explicit removal check
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));

// Less clear
await waitFor(() => {
  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});

Common Patterns

Waiting after user interaction

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

await waitFor(() => {
  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

API response handling

// Submit form
fireEvent.submit(screen.getByRole('form'));

// Wait for loading to finish
await waitForElementToBeRemoved(() =>
  screen.queryByRole('status', {name: /loading/i})
);

// Verify result
expect(screen.getByText('Success!')).toBeInTheDocument();

Polling for state changes

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

// Poll for completion
await waitFor(
  () => {
    const progress = screen.getByTestId('progress');
    expect(progress).toHaveTextContent('100%');
  },
  {interval: 200, timeout: 10000}
);

Error Handling

try {
  await waitFor(
    () => expect(screen.getByText('Never appears')).toBeInTheDocument(),
    {timeout: 1000}
  );
} catch (error) {
  console.error('Element did not appear within 1 second');
}

// Custom timeout messages
await waitFor(
  () => {
    const element = screen.queryByTestId('status');
    if (!element || element.textContent !== 'ready') {
      throw new Error('Status is not ready');
    }
  },
  {
    timeout: 5000,
    onTimeout: (error) => {
      const status = screen.queryByTestId('status');
      const currentStatus = status?.textContent || 'not found';
      return new Error(
        `Timeout waiting for ready status. Current: ${currentStatus}`
      );
    }
  }
);

Install with Tessl CLI

npx tessl i tessl/npm-testing-library--dom

docs

async.md

config.md

debugging.md

events.md

index.md

queries.md

query-helpers.md

role-utilities.md

screen.md

within.md

tile.json