Skip to Content

Testing Library

Testing Library test source rules from eslint-plugin-testing-library.

They apply to TypeScript test sources that use any @testing-library/* package. They detect Testing Library anti-patterns such as container access, ByTestId overuse, missing await on async queries.

Source: eslint-plugin-testing-library (MIT).

Rule index

Each rule name links to the detailed section below.

Examples come from the checked lint corpus or package-level rule tests when project layout matters.

Disallow

Prefer

Consistency

Other checks

Rules

testing-library/await-async-events

Require awaiting async user-event methods (userEvent.click, userEvent.type, …) under the v14+ Promise-returning API.

Without await the next assertion runs against the pre-interaction DOM, which masks effects the user action was meant to trigger.

Example:

import userEvent from "@testing-library/user-event"; function testCase() { // reports: testing-library/await-async-events (error) userEvent.click(document.body); }

testing-library/await-async-queries

Require awaiting findBy* and findAllBy* queries.

They return a Promise that resolves once the element appears; an unawaited query yields a pending Promise that no matcher can assert against meaningfully.

Example:

import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/await-async-queries (error) screen.findByText("Saved"); }

testing-library/await-async-utils

Require awaiting waitFor, waitForElementToBeRemoved, and the other async Testing Library utilities.

Skipping the await means the test moves on before the predicate settles, so subsequent assertions race against the wait.

Example:

import { screen, waitFor } from "@testing-library/react"; function testCase() { // reports: testing-library/await-async-utils (error) waitFor(() => screen.getByText("Done")); }

testing-library/consistent-data-testid

Validate JSX data-testid attribute values against a regex pattern.

The options object names the attribute and the pattern, keeping test ids consistently formed across components.

Options:

  • testIdPattern: string

    Regular expression string every configured test-id attribute value must match. {fileName} is replaced with the basename before the first dot.

  • testIdAttribute?: string | readonly string[]

    Test-id attribute name, or names, to validate. Default: "data-testid".

Example:

import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/consistent-data-testid (error) return screen.getByTestId("Bad Value"); }

testing-library/no-await-sync-events

Reject unnecessary await before synchronous event helpers (fireEvent.click(...)).

The helpers return boolean rather than a Promise, so the await is a no-op that misleads readers into thinking the helper is async.

Example:

import { fireEvent, screen } from "@testing-library/react"; async function testCase() { // reports: testing-library/no-await-sync-events (error) await fireEvent.click(screen.getByText("Save")); }

testing-library/no-await-sync-queries

Reject unnecessary await before synchronous queries (getBy*, queryBy*).

These queries return DOM nodes directly, so the await misleads readers and can shadow a genuine missing await on an adjacent findBy*.

Example:

import { screen } from "@testing-library/react"; async function testCase() { // reports: testing-library/no-await-sync-queries (error) await screen.getByText("Ready"); }

testing-library/no-container

Reject container destructuring and DOM query methods on the render result.

screen queries the same document but matches how the user sees the page, keeping tests accessibility-first and resilient to layout refactors that move nodes inside the tree.

Example:

import { render } from "@testing-library/react"; function testCase() { // reports: testing-library/no-container (error) const { container } = render(<button>Save</button>); }

testing-library/no-debugging-utils

Reject debug, prettyDOM, logTestingPlaygroundURL, and related debugging utilities in committed tests.

They print large DOM dumps to CI logs and only exist to help during local authoring.

Example:

import { render } from "@testing-library/react"; function testCase() { const { debug } = render(<button>Save</button>); // reports: testing-library/no-debugging-utils (error) debug(); }

testing-library/no-dom-import

Reject direct @testing-library/dom imports when a framework-specific package is installed.

Framework packages (@testing-library/react, …) re-export the same surface plus a render that wires the framework’s lifecycle, importing dom directly skips that wiring.

Example:

// reports: testing-library/no-dom-import (error) // @ts-ignore import { prettyDOM } from "@testing-library/dom"; // @ts-ignore import { render } from "@testing-library/react";

testing-library/no-global-regexp-flag-in-query

Reject global RegExp flags (/foo/g) inside query text matchers.

The matcher reuses the regex across nodes, so a global regex’s persistent lastIndex state causes the second call to skip matches the first one found.

Example:

import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/no-global-regexp-flag-in-query (error) return screen.getByText(/save/g); }

testing-library/no-manual-cleanup

Reject manual cleanup() calls.

Framework wrappers (@testing-library/react, …) register automatic cleanup, so explicit calls duplicate the unmount and can race against the runner’s between-test reset.

Example:

import { cleanup } from "@testing-library/react"; function testCase() { // reports: testing-library/no-manual-cleanup (error) cleanup(); }

testing-library/no-node-access

Reject direct DOM node traversal from query results (.parentElement, .firstChild, .children, …).

Traversal couples the test to incidental markup; another semantic query names what the assertion actually cares about.

Example:

import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/no-node-access (error) return screen.getByText("Save").parentElement; }

testing-library/no-promise-in-fire-event

Reject Promise-producing expressions passed to fireEvent, since fireEvent is synchronous and the Promise is dropped.

Example:

import { fireEvent, screen } from "@testing-library/react"; async function testCase() { // reports: testing-library/no-promise-in-fire-event (error) fireEvent.click(await screen.findByRole("button")); }

testing-library/no-render-in-lifecycle

Reject render(...) inside lifecycle hooks (beforeEach, etc.).

Each test should render its component directly so the arrange step is visible in-place and auto-cleanup runs between cases without sharing state.

Example:

import { beforeEach } from "vitest"; import { render } from "@testing-library/react"; beforeEach(() => { // reports: testing-library/no-render-in-lifecycle (error) render(<button>Save</button>); });

testing-library/no-test-id-queries

Reject *ByTestId queries.

Test ids couple the test to incidental markup and skip the accessibility tree that real users navigate; queries by role, label, or text describe the UI in user-visible terms instead.

Example:

import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/no-test-id-queries (error) return screen.getByTestId("save"); }

testing-library/no-unnecessary-act

Reject unnecessary act(...) wrappers around Testing Library helpers.

render, fireEvent, and userEvent already wrap their work in act, so the extra wrapper is dead code that obscures the real state change.

Example:

import { act, fireEvent, screen } from "@testing-library/react"; function testCase() { // reports: testing-library/no-unnecessary-act (error) act(() => { fireEvent.click(screen.getByRole("button")); }); }

testing-library/no-wait-for-multiple-assertions

Reject multiple assertions inside one waitFor callback, split into separate waitFors so each retry boundary is narrow.

Example:

import { expect } from "vitest"; import { screen, waitFor } from "@testing-library/react"; async function testCase() { // reports: testing-library/no-wait-for-multiple-assertions (error) await waitFor(() => { expect(screen.queryByText("A")).toBeTruthy(); expect(screen.queryByText("B")).toBeTruthy(); }); }

testing-library/no-wait-for-side-effects

Reject side effects inside waitFor callbacks, waitFor retries the callback, so side effects fire repeatedly.

Example:

import { fireEvent, screen, waitFor } from "@testing-library/react"; async function testCase() { // reports: testing-library/no-wait-for-side-effects (error) await waitFor(() => { fireEvent.click(screen.getByText("Go")); }); }

testing-library/no-wait-for-snapshot

Reject snapshot assertions inside waitFor.

waitFor retries until the callback stops throwing, so the captured snapshot is whichever pass happened to match, usually an intermediate render rather than the settled UI the test cares about.

Example:

import { expect } from "vitest"; import { screen, waitFor } from "@testing-library/react"; async function testCase() { // reports: testing-library/no-wait-for-snapshot (error) await waitFor(() => { expect(screen.getByText("B")).toMatchSnapshot(); }); }

testing-library/prefer-explicit-assert

Require explicit assertions on the result of standalone queries.

A bare screen.getByRole(...) looks like an assertion but only checks presence (and only for getBy*); adding expect(...) makes the intent and the matched property obvious.

Example:

import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/prefer-explicit-assert (error) screen.getByText("Save"); }

testing-library/prefer-find-by

Prefer findBy* over waitFor wrapping a getBy*.

findBy* is the dedicated retry-aware query; the manual combination duplicates its semantics and is easy to misconfigure (wrong timeout, missing await).

Example:

import { screen, waitFor } from "@testing-library/react"; async function testCase() { // reports: testing-library/prefer-find-by (error) await waitFor(() => screen.getByText("Saved")); }

testing-library/prefer-implicit-assert

Avoid redundant toBeInTheDocument() around getBy* queries.

getBy* already throws when nothing is found, so the extra matcher only restates what the query promises and adds noise to the failure trace.

Example:

import { expect } from "vitest"; import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/prefer-implicit-assert (error) expect(screen.getByText("Save")).toBeInTheDocument(); }

testing-library/prefer-presence-queries

Match presence and absence assertions to the query variant that already encodes the same semantic: getBy* for presence (throws when missing), queryBy* for absence (returns null when missing).

Mixing the two yields confusing failure modes.

Example:

import { expect } from "vitest"; import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/prefer-presence-queries (error) expect(screen.queryByText("Save")).toBeInTheDocument(); }

testing-library/prefer-query-by-disappearance

Prefer queryBy* inside disappearance waits.

waitFor retries the callback against the disappearing element, but getBy* throws on the very state being waited for, which produces a noisy error in the trace each retry.

Example:

import { expect } from "vitest"; import { screen, waitFor } from "@testing-library/react"; async function testCase() { // reports: testing-library/prefer-query-by-disappearance (error) await waitFor(() => expect(screen.getByText("Saved")).not.toBeInTheDocument(), ); }

testing-library/prefer-query-matchers

Prefer jest-dom document matchers (toBeVisible, toHaveTextContent, …) over generic equality checks on Testing Library queries.

The dedicated matchers explain failures in terms of the DOM property they assert on, not a structural diff of nodes.

Example:

import { expect } from "vitest"; import { screen } from "@testing-library/react"; function testCase() { // reports: testing-library/prefer-query-matchers (error) expect(screen.queryByText("Save")).toBeNull(); }

testing-library/prefer-screen-queries

Prefer screen.* over queries on the render-result object.

screen is the single global query target, so tests stay consistent regardless of which component is mounted and the render call’s return value rarely needs destructuring.

Example:

import { render } from "@testing-library/react"; function testCase() { const { getByText } = render(<button>Save</button>); // reports: testing-library/prefer-screen-queries (error) getByText("Save"); }

testing-library/prefer-user-event

Prefer userEvent over fireEvent.

userEvent simulates the full sequence of DOM events a real user triggers (focus, keydown, input, change, …) while fireEvent dispatches a single event and skips intermediate state.

Example:

import { fireEvent, screen } from "@testing-library/react"; function testCase() { // reports: testing-library/prefer-user-event (error) fireEvent.click(screen.getByText("Save")); }

testing-library/prefer-user-event-setup

Prefer userEvent.setup() (the v14+ instance pattern) over the static userEvent.* calls.

The instance binds fresh pointer state per test, removing the cross-test focus and click bookkeeping that the static API leaks.

Example:

import userEvent from "@testing-library/user-event"; function testCase() { // reports: testing-library/prefer-user-event-setup (error) userEvent.click(document.body); }

testing-library/render-result-naming-convention

Require the variable assigned from render(...) to use one of the conventional names (view, result, …).

The name is a reading cue: a non-conventional one usually signals that the destructured queries are being treated as a component surface instead of a render artifact.

Example:

import { render } from "@testing-library/react"; function testCase() { // reports: testing-library/render-result-naming-convention (error) const wrapper = render(<button>Save</button>); }
Last updated on