testing-library / testing-library/eslint-plugin-testing-library

New Rule Proposal: no-unsettled-absence-query

Open
#1,237 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
1.1k
Forks
168
Avg merge
15h 44m
Merged PRs (30d)
14

Description

Name for new rule

no-unsettled-absence-query

Description of the new rule

Asserting absence with queryBy* + .not.toBeInTheDocument() immediately after render(), before the component has settled, can produce a false positive. The element isn't there yet, not because it won't be there. The community calls this Testing Ghosts.

Testing Library's render() does flush the initial synchronous render (it's wrapped in act() internally), so for a plain component with no async behavior, checking absence right after render() is technically correct. The problem shows up when the component has async side effects like data fetching, lazy loading, state updates from useEffect, etc. In those cases the UI hasn't finished updating, and the absence check passes vacuously. Since the rule can't know whether a component has async behavior, it flags all unsettled absence assertions and lets the developer confirm intent by adding a settling expression.

This rule would enforce that an absence assertion (expect(screen.queryBy*(...)).not.toBeInTheDocument() / .not.toBeVisible()) must be preceded by a "settling" expression within the test body.

Why existing rules don't cover this:

Existing Rule Why it doesn't solve this
prefer-presence-queries Enforces correct query type, but doesn't track timing relative to render
no-await-sync-queries Only flags await directly on sync queries, not timing of assertions
await-async-queries / await-async-utils About promise handling, not assertion ordering
prefer-find-by Simplifies waitFor + getBy -> findBy, doesn't detect premature absence checks

Related issues: #269, #411, #518 - all adjacent but none address this specific pattern.

Testing Library feature

This rule relates to the absence/presence assertion pattern described in the Appearance and Disappearance guide. Specifically, it enforces correct ordering of queryBy* absence assertions relative to async settling utilities (findBy*, waitFor, act) and sync settling queries (getBy*).

Testing Library framework(s)

All frameworks: React, Angular, Vue, Svelte, etc. The pattern applies anywhere screen.queryBy* + .not.toBeInTheDocument() is used.

What category of rule is this?

Warns about a potential error. Less valuable as an error given the call-out above of vanilla components.

Code examples

Fail:

// Absence assertion before component has settled, "Testing a Ghost"
test('shows no error', () => {
  render(<AsyncComponent />);
  expect(screen.queryByText('error')).not.toBeInTheDocument();
});

// Absence assertion BEFORE the awai, order matters
test('shows no error', async () => {
  render(<AsyncComponent />);
  expect(screen.queryByText('error')).not.toBeInTheDocument();
  await screen.findByText('loaded');
});

// queryAllBy variant
test('shows no alerts', () => {
  render(<AsyncComponent />);
  expect(screen.queryAllByRole('alert')).not.toBeInTheDocument();
});

// Absence assertion inside waitFor - passes on first retry, still a ghost
test('shows no error', async () => {
  render(<AsyncComponent />);
  await waitFor(() => {
    expect(screen.queryByText('error')).not.toBeInTheDocument();
  });
});

Pass:

// findBy* settles the component first
test('shows no error', async () => {
  render(<AsyncComponent />);
  await screen.findByText('loaded');
  expect(screen.queryByText('error')).not.toBeInTheDocument();
});

// waitFor settles the component first
test('shows no error', async () => {
  render(<AsyncComponent />);
  await waitFor(() => expect(something).toBe(true));
  expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});

// act settles the component first
test('shows no error', async () => {
  await act(() => render(<AsyncComponent />));
  expect(screen.queryByText('error')).not.toBeInTheDocument();
});

// getBy* proves sync render completed
test('shows no error', () => {
  render(<Component />);
  screen.getByText('visible heading');
  expect(screen.queryByText('error')).not.toBeInTheDocument();
});

What counts as "settled":

  1. Any await expression on a preceding statement — covers findBy*, waitFor, act, custom async helpers
  2. A screen.getBy* / screen.getAllBy* call — proves the sync render produced expected output

Matchers checked: .not.toBeInTheDocument(), .not.toBeVisible()

Anything else?

References:

Do you want to submit a pull request to make the new rule?

I have a working implementation with full test coverage that I'm happy to submit as a PR if this proposal is accepted.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No repository files, tests, or entry points are named. Start by reviewing the existing Testing Library ESLint rules and their tests, then compare the proposed failing and passing examples. Done means the new warning consistently detects unsettled absence assertions while allowing the documented settling expressions.

Written by the indexing model from the issue text.

Assessment

Tech stack
eslint, javascript, typescript
Domain
testing-qa, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.