Start with the observable contract
Before choosing a testing library, write down the relationship you want to observe. In the counter, one Review next interaction should change the heading from zero to one. The test need not know the variable name count or call setCount directly.
Select the button by role and accessible name, then observe the heading after interaction. A CSS-class or internal-structure change therefore need not change test intent. This does not make every change irrelevant: if the button loses an identifiable name, the query should fail and require investigation. Testing the contract does not mean accepting arbitrary output.
Presence, absence and ambiguity
getBy looks for a current match and fails for zero or multiple matches. queryBy supports absence assertions by returning null for zero matches, but also fails for multiple matches. findBy returns a Promise and retries until it finds the element or reaches the waiting limit.
None of these choices should hide two Save buttons when the test intends only the billing-region button. In that case, scope the region with within or query a specific accessible name if that is the interface contract. A singular query failing through ambiguity reveals useful information.
Interactions and pending updates
user-event v14 describes interactions that can produce several events and additional checks. Create a setup instance inside the test and await asynchronous methods before asserting the result. An isolated fireEvent remains useful for a concrete event, but does not itself demonstrate focus, keyboard and value-change sequencing.
React provides act to apply updates related to an interaction unit before assertions; the recommended form is asynchronous with await. React Testing Library helpers already integrate act. None of these mechanisms invents a network response or replaces waiting for the correct asynchronous condition.
Read the example and preserve isolation
ReviewCounter does not contact a server: it increments local state. inspectCounterContract contains mounting, an initial assertion, the awaited interaction and the final assertion. The finally block calls cleanup, which unmounts trees created by render, including when an assertion throws.
Some runners configure cleanup automatically through global afterEach; others need explicit configuration. Here the excerpt states the intent explicitly. The environment also needs a DOM and JSX transformation.
Importing node:assert identifies the inspection function as Node test code; that import should not be copied into the application’s public bundle.
Control response order
An interface can pass every test where A finishes before B and still display the wrong response when the network reverses that order. To examine the contract, use two controlled responses: start A, change selection to B, resolve B and only then A.
The relevant observation is that B’s result remains associated with current selection. Counting two requests or two HTTP successes does not check that property. An Effect cleanup guard can prevent A from being applied, as in the synchronization lesson.
Evidence must distinguish rejecting an obsolete result from canceling the remote operation, since those are different contracts.
Interpret a simulation without expanding the conclusion
Consider a form whose save function is replaced by a Promise resolved by the test. Seeing Saved after the click supports that the interface reacted to the interaction and prepared response. It does not demonstrate real-service persistence, valid session permissions or layout behavior in every browser.
Similarly, role and name queries help evaluate the interface, but one interaction does not certify complete accessibility. Record the environment, replaced dependencies and observed assertion. This boundary allows complementary evidence to be combined without attributing results to a component test that it never observed.
Diagnosis and test summary
On failure, first ask whether the element should exist now, appear later or be absent. Then check query scope and whether the interaction was awaited. A fixed sleep can make a test occasionally pass without correcting the observed condition; findBy expresses waiting for concrete presence with a limit.
Do not remove the assertion to obtain a green suite. Finally, confirm cleanup of trees and resources from the previous case. The summary should state the checked behavior and what still depends on integration, a real browser or evaluation with users.
// Reading example. A JSX-capable DOM test environment is required.
// Dependencies: React, React Testing Library and user-event v14.
// Node provides assert; this is not a browser entry point.
import {useState} from 'react';
import {render, screen, cleanup} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import assert from 'node:assert/strict';
function ReviewCounter() {
const [count, setCount] = useState(0);
return <section>
<h2>Reviewed: {count}</h2>
<button type="button" onClick={() => setCount(n => n + 1)}>
Review next
</button>
</section>;
}
export async function inspectCounterContract() {
const user = userEvent.setup();
try {
render(<ReviewCounter />);
assert.equal(screen.getByRole('heading', {name: 'Reviewed: 0'}).textContent,
'Reviewed: 0');
await user.click(screen.getByRole('button', {name: 'Review next'}));
assert.equal(screen.getByRole('heading', {name: 'Reviewed: 1'}).textContent,
'Reviewed: 1');
} finally {
cleanup();
}
}The example presents the component and inspection function in the same excerpt. The function awaits the click and checks the observed heading; finally unmounts the tree even if an assertion fails. It is a reading contract for a DOM environment with JSX support, not a file directly executable by a browser or proof of remote persistence.
Common pitfalls
Using CSS selectors as business identity; not awaiting user.click; using getBy for absence; always resolving requests in start order; leaving previous trees mounted; turning a DOM simulation into a guarantee of complete accessibility.
Related topics: Forms, DOM contracts and accessibility · Effects, refs and external synchronization · Async data, Suspense and error boundaries
A useful test links an action to an observable result and declares replaced dependencies. Queries, synchronization and cleanup make that evidence more precise; they do not automatically expand its scope to services, browsers or users not observed.
Reference: Queries, presence and scope · React 19.3.0; JSX