Simple and complete DOM testing utilities that encourage good testing practices.
—
Wait for conditions or elements to appear/disappear in the DOM for testing asynchronous behavior.
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}`)
}
);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}
);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');findBy* for async content// Good
const element = await screen.findByText('Async content');
// Works but less idiomatic
await waitFor(() => {
expect(screen.getByText('Async content')).toBeInTheDocument();
});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');
});waitForElementToBeRemoved for removal// Good - explicit removal check
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
// Less clear
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});fireEvent.click(screen.getByRole('button', {name: /load/i}));
await waitFor(() => {
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});// 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();// 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}
);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@10.4.2