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
testing-library/no-await-sync-events: Reject unnecessaryawaitbefore synchronous event helpers.testing-library/no-await-sync-queries: Reject unnecessaryawaitbefore synchronous queries.testing-library/no-container: Rejectcontainerdestructuring and DOM query methods on the render result.testing-library/no-debugging-utils: Rejectdebug,prettyDOM,logTestingPlaygroundURL, and related debugging utilities in committed tests.testing-library/no-dom-import: Reject direct@testing-library/domimports.testing-library/no-global-regexp-flag-in-query: Reject global RegExp flags inside query text matchers.testing-library/no-manual-cleanup: Reject manualcleanup()calls.testing-library/no-node-access: Reject direct DOM node traversal from query results.testing-library/no-promise-in-fire-event: Reject Promise-producing expressions passed tofireEvent.testing-library/no-render-in-lifecycle: Rejectrender(...)inside lifecycle hooks.testing-library/no-test-id-queries: Reject*ByTestIdqueries.testing-library/no-unnecessary-act: Reject unnecessaryact(...)wrappers around Testing Library helpers.testing-library/no-wait-for-multiple-assertions: Reject multiple assertions inside onewaitForcallback, split into separatewaitFors.testing-library/no-wait-for-side-effects: Reject side effects insidewaitForcallbacks,waitForretries the callback.testing-library/no-wait-for-snapshot: Reject snapshot assertions insidewaitFor.
Prefer
testing-library/prefer-explicit-assert: Require explicit assertions on the result of standalone queries.testing-library/prefer-find-by: PreferfindBy*overwaitForwrapping agetBy*.testing-library/prefer-implicit-assert: Avoid redundanttoBeInTheDocument()aroundgetBy*queries.testing-library/prefer-presence-queries: Match presence and absence assertions to the query variant that already encodes the same semantic.testing-library/prefer-query-by-disappearance: PreferqueryBy*inside disappearance waits.testing-library/prefer-query-matchers: Prefer jest-dom document matchers over generic equality checks on Testing Library queries.testing-library/prefer-screen-queries: Preferscreen.*over queries on the render-result object.testing-library/prefer-user-event: PreferuserEventoverfireEvent.testing-library/prefer-user-event-setup: PreferuserEvent.setup()(the v14+ instance pattern) over the staticuserEvent.*calls.
Consistency
testing-library/consistent-data-testid: Validate JSXdata-testidattribute values against a regex pattern.
Other checks
testing-library/await-async-events: Require awaiting async user-event methods under the v14+ Promise-returning API.testing-library/await-async-queries: Require awaitingfindBy*andfindAllBy*queries.testing-library/await-async-utils: Require awaitingwaitFor,waitForElementToBeRemoved, and the other async Testing Library utilities.testing-library/render-result-naming-convention: Require the variable assigned fromrender(...)to use one of the conventional names.
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: stringRegular 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>);
}