Skip to content

Testing React components with Testing Library

Express the interaction through a meaningful query, realistic input, and an observation that matches the UI lifecycle.

Predict first

Before you read: predict

The ExamHall navigator renders this control.

Which lookup follows Testing Library's recommended priority for this interaction?

Code for this question
<button aria-label="Question 7" data-testid="question-7" className="question">7</button>
Pick one

The 30-second version

Testing Library tests work through rendered elements. Prefer a query that reflects how the target is exposed, then scope repeated controls. Await user interactions and asynchronous observations. Choosing an API involves its contract: one result or several, an immediate answer or retries, and a fresh lookup or a previously captured node. Queries

Express the interaction through a meaningful query, realistic input, and an observation that matches the UI lifecycle.

How it works

Step 1 of 7

Choose a contract. Prefer role and accessible name for an exposed control; successful matching alone does not establish query priority. Priority

Analogy: Wildlife field-guide station

A birdwatcher identifies a marked perch by its public location and label, rather than a private inventory number. They narrow their search to a particular enclosure, perform the agreed feeding action, and record what returns to the perch. That connects meaningful queries, scoped searches, realistic interactions and subsequent observations.

The analogy stops at the interface boundary. It cannot determine accessible-name computation, the browser's focus rules, or React's rendering behavior. A perch also remains the same physical object, while rendering can replace a DOM node. A saved reference therefore answers a different question from a lookup against the current document. The linked packs own those underlying mechanics; this lesson concerns the testing APIs used to observe them.

Examples, run for real

4.1 ExamHall: operate the review marker

EXH-1890 and MR !2090 identify authored local fixtures. The candidate's review marker begins as an unpressed button. A user-event click operates it; the component renders a pressed state and a confirmation beside it. The fixture checks those exposed changes, and separately records the pointer events received by the button. These observations explain what this interaction exercised without claiming coverage of the rest of the exam interface.

await userEvent.setup().click(screen.getByRole('button', { name: 'Mark for review' }));
expect(screen.getByRole('button', { name: 'Mark for review', pressed: true })).toBeTruthy();
expect(screen.getByText('Review mark recorded')).toBeTruthy();
teaching ExamHall: pressed=true; confirmation=Review mark recorded; events=pointerdown,pointerup,click

A complete user action can involve focus and several events. Dispatching one named event is sometimes useful when that event itself is the test's subject, but it does not stand in for the whole sequence. In this button fixture, the click reaches pointer-down, pointer-up and click handlers. Keyboard actions and text editing have their own sequences; the API name describes the user's action rather than a universal list of browser events.

A selector's success and its suitability are separate observations. Several selectors may return the same element today. The preferred query can still protect a more meaningful contract, such as the control's role and exposed name. A private hook remains available when the user-facing alternatives cannot identify the intended target. This preference comes from Testing Library's guidance, so the opening question labels that judgment as cited.

4.2 Media streaming: inspect a persistent buffer gauge

The second fixture renders a progress gauge with an initial value of 20. A local response is held until the harness releases it; the same gauge then reports 80. The test captures that gauge and uses a throwing assertion inside waitFor to observe its changed value. It checks both the new value and whether the captured object is still the current gauge. This particular component updates an existing node.

await waitFor(() => expect(gauge.value).toBe(80));
teaching media: buffer=20->80; initial assertion failed=true; same node=true; releases=1

The harness starts the assertion before releasing its response and observes an initial failure. Releasing the response lets React apply the update; a later assertion succeeds. This is an explicit ordering relationship. It does not infer readiness from a sleep, a machine's speed or a screenshot taken at an arbitrary moment. Timeout limits remain harness failure guards, not lesson answers.

The examples do not install a network server or exercise a browser. React renders into jsdom, so the claims concern these Testing Library operations on the authored components. The evidence also does not determine where production dependencies should be substituted, which test mix a team should buy, or whether the original interface is accessible. Those decisions remain in the linked lanes.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 5

A controlled textbox labelled Search clips stores its input through React's onChange; the result reads No search initially. search refers to that textbox.

Which action updates its state to trailer, producing Searching trailer?

Pick one

Question 2 of 5

A table lists Opening scene followed by Night train. Each row has a Queue button beside its title. row is the second row.

Which lookup selects the control beside Night train?

Pick one

Question 3 of 5

The activity component starts with an empty list. A deferred response supplies the entries Opened and Assigned; React renders each as a list item. The test starts its lookup and the harness releases the response.

Which expression supplies the resulting entries for assertions?

Pick one

Question 4 of 5

The component renders the following subtree. ready becomes true when the held response resolves. In the test, saved references the status captured after the initial render.

Which replacement completes a check of the synchronization result?

Code for this question
<>  <blockquote>Synced</blockquote>  {ready    ? <p key="done" role="status">Synced</p>    : <p key="busy" role="status">Syncing</p>}</>// Current test line, before the response is released:await waitFor(() => screen.getByRole('status').textContent === 'Synced');
Pick one

Question 5 of 5

The rendered player layout and test are shown below.

What does the last expression produce?

Code for this question
<>  <section aria-label="Studio">    <button>Pause</button>    <section aria-label="Preview"><button>Pause</button></section>  </section>  <section aria-label="Archive"><button>Pause</button></section></>// Test:within(screen.getByRole('region', { name: 'Studio' }))  .getAllByRole('button', { name: 'Pause' }).length;
Pick one

Go deeper

Pro tip

Separate the selector's result from the test's claim. A lookup may return a real element while targeting the wrong region, an old node or incidental text. Check what the assertion reads, and match the query's cardinality and waiting contract to that observation. Queries

Sources

The pinned jsdom/Vitest fixtures provide the executable item evidence. The preference in t1 comes from the query-priority source; execution establishes matching behavior, not that preference.

Checked against the tools and docs as of 2026-09-30.