This rule raises an issue when a Testing Library waitFor callback either runs a synchronous query for an element that is expected to appear, checks that query’s presence, or unnecessarily wraps an asynchronous query that already waits.

Why is this an issue?

waitFor is a general retry utility. It reruns its callback until the callback stops throwing or the timeout elapses. Testing Library’s findBy* and findAllBy* queries already combine that waiting behavior with the corresponding query, so they express an element-appearance wait directly.

Wrapping a synchronous getBy* or getAllBy* call in waitFor duplicates that behavior. A standalone queryBy* or queryAllBy* callback has the opposite problem: those queries return null or an empty array when the element is absent instead of throwing, so the waitFor callback can resolve before the element appears. In both cases, the asynchronous query is the appropriate API when the test expects an element to become available.

Using an asynchronous query directly makes the test’s intent clearer: the test is waiting for an element to become available. It also produces the query-specific error message when the element does not appear.

The rule applies to concise arrow callbacks containing a single synchronous query or a presence assertion around one, and to asynchronous arrow callbacks that await a findBy* or findAllBy* query. Assertions that check another condition, such as whether an element is disabled, and utilities such as waitForElementToBeRemoved remain valid uses of waitFor.

Code examples

Noncompliant code example

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

test('shows the confirmation', async () => {
  render(<Form />);

  await waitFor( // Noncompliant: prefer "findByRole" query over using "waitFor" + "getByRole"
    () => screen.getByRole('status', { name: 'Saved' }),
  );
});

Compliant solution

import { render, screen } from '@testing-library/react';

test('shows the confirmation', async () => {
  render(<Form />);

  await screen.findByRole('status', { name: 'Saved' });
});

Noncompliant code example

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

test('shows the confirmation', async () => {
  await waitFor(async () => { // Noncompliant: prefer "findByRole" query over using "waitFor" + "findByRole"
    const status = await screen.findByRole('status', { name: 'Saved' });
    expect(status).toBeVisible();
  });
});

Compliant solution

import { screen } from '@testing-library/react';

test('shows the confirmation', async () => {
  const status = await screen.findByRole('status', { name: 'Saved' });
  expect(status).toBeVisible();
});

Noncompliant code example

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

test('shows the confirmation', async () => {
  render(<Form />);

  await waitFor( // Noncompliant: prefer "findByRole" query over using "waitFor" + "queryByRole"
    () => screen.queryByRole('status', { name: 'Saved' }),
  );
});

Compliant solution

import { render, screen } from '@testing-library/react';

test('shows the confirmation', async () => {
  render(<Form />);

  await screen.findByRole('status', { name: 'Saved' });
});

Resources

Documentation

Articles & blog posts