Topic 17.3
Rendering and Querying
In one line
render() mounts a component into the test page and screen finds elements the way people do: by role and accessible name first, then label, text and the rest, with getBy, queryBy and findBy for present, absent and not-yet-there elements.
Think of it like this
Finding a friend at a railway station. You don't look for "the person standing on tile 4,382". You look for who they are: "my friend Asha, the one with the red bag". Testing Library queries work the same way: you find "the button called Save", not "the third div inside .toolbar". If the station is rebuilt, you can still find Asha; if the markup changes, your test still finds the button.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- render()
- Testing Library's function that mounts a component into the test page.
- screen
- An object with every query, searching the whole test page.
- Query
- A function that finds elements on the page, like
getByRoleorqueryByText. - Role
- What an element is to assistive technology: button, heading, textbox, link, checkbox...
- Accessible name
- What a screen reader says for an element: a button's text, an input's label, an image's alt text.
- Screen reader
- Software that reads the page aloud for people who can't see the screen.
- getBy / queryBy / findBy
- Find now or throw / find now or return null / wait until it appears (a promise).
- within()
- Limits queries to inside one element, like one row of a table.
- data-testid
- An attribute only tests use to find an element. The last resort query.
Step by step
01Render, then look at the page like a user
Render the component, then describe what you want in user words: "the Email text box", "the Sign up button". Turn each into a getByRole with a name.
render(<SignupForm />);
const email = screen.getByRole("textbox", { name: "Email" });
const submit = screen.getByRole("button", { name: "Sign up" });02Pick the query by priority
Work down the list and stop at the first that fits. Interactive things almost always have a role and a name. Use ByText for plain text (a paragraph, a message), ByLabelText for fields when you prefer the label wording, and ByTestId only for things with no accessible handle (a chart canvas, a decorative wrapper).
03Choose get, query or find by what you expect
Expect it there now: getBy. Expect it not there: queryBy and check for null (with jest-dom: expect(...).not.toBeInTheDocument()). Expect it to appear soon: await findBy.
04Narrow with within()
A table of team members has a Remove button on every row, so getByRole("button", { name: "Remove" }) throws "Found multiple elements". Find the row first (rows get their accessible name from their cells' text), then search inside it.
const row = screen.getByRole("row", { name: /Ravi/ });
await user.click(within(row).getByRole("button", { name: "Remove" }));05When a query fails, read the error, then debug
The error lists the roles and names that do exist. Often the fix is obvious: the button's name is Sign Up not Sign up, or the input has no label at all. If not, screen.debug(element) or logRoles(element) show you what Testing Library sees.
Try it yourself
- 1
Break the label link
In the form example, change
htmlFor="email"tohtmlFor="mail". Predict which lines fail and what the error says. (The textbox loses its name, so bothgetByRole("textbox", { name: "Email" })andgetByLabelText("Email")throw.) That is exactly the bug a screen reader user would meet. - 2
Make findBy give up
In the getBy/queryBy/findBy example, change the timer from
300to1500and run. Predict: doesfindByRolestill find it? (Its default timeout is 1000 ms.) Then pass{ timeout: 2000 }as the third argument:screen.findByRole("status", {}, { timeout: 2000 }). - 3
Query inside Asha's row
In the within() example, log
within(screen.getByRole("row", { name: /Asha/ })).getByRole("cell", { name: /Admin/ }).textContent. Then tryscreen.getAllByRole("row").length: predict the number (the header row counts).
Code & diagrams
Each line shows a query and the tag it found. Note the roles: an email input is a textbox, a select is a combobox, and a form only has the form role when it has a name (here from aria-label).
Preview
Console
Nothing logged yet.Expected console output
getByRole("heading", { level: 2 }) h2
getByRole("textbox", { name: "Email" }) input
getByRole("combobox", { name: "Plan" }) select
getByRole("checkbox", { name: /news/ }) input
getByRole("button", { name: "Sign up" }) button
getByRole("form", { name: "Sign up" }) form
getByLabelText("Email") input
getByPlaceholderText("you@example.com") input
getByText("Create your account") h2
getByDisplayValue("Free") select
getByAltText("Hectal logo") img
getByTitle("Help") span
getByTestId("hint") spanThe status appears after 300 ms. getBy and queryBy look once, right now, so they miss it. findBy waits and finds it.
Preview
Console
Nothing logged yet.Expected console output
getBy → TestingLibraryElementError: Unable to find an accessible element with the role "status"
queryBy → null
findBy → Saved!
getAllByText(/Sav/) → 1
queryAllByText(/oops/) → []A row's accessible name is the text of its cells, so { name: /Ravi/ } finds it. prettyDOM prints the HTML; logRoles prints every role and accessible name inside the element.
Preview
Console
Nothing logged yet.Expected console output
Found multiple elements with the role "button" and name "Remove"
Editor
BUTTON
<tr>
<td>
Ravi
</td>
<td>
Editor
</td>
<td>
<button>
Remove
</button>
</td>
</tr>
row:
Name "Ravi Editor Remove":
<tr />
--------------------------------------------------
cell:
Name "Ravi":
<td />
Name "Editor":
<td />
Name "Remove":
<td />
--------------------------------------------------
button:
Name "Remove":
<button />
--------------------------------------------------import { render, screen, within } from "@testing-library/react";
import Team from "./Team";
test("lists each person with their role", () => {
render(<Team />);
const ravisRow = screen.getByRole("row", { name: /Ravi/ });
expect(within(ravisRow).getByRole("cell", { name: "Editor" })).toBeInTheDocument();
});
test("has no Delete-all button for editors", () => {
render(<Team />);
expect(screen.queryByRole("button", { name: /delete all/i })).not.toBeInTheDocument();
});
test("shows the saved message once saving finishes", async () => {
render(<Notice />);
expect(await screen.findByRole("status")).toHaveTextContent("Saved!");
});Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use getBy to check something is gone
To check a dismissed message is gone, write expect(screen.getByText("Saved!")).toBe(null).
Break #2
A query that matches more than one element
Call screen.getByRole("button", { name: "Remove" }) on a table with a Remove button on every row.
Break #3
Query by CSS selector out of habit
Write container.querySelector(".btn-primary") instead of a role query, then a designer renames the class.
Myth vs fact
Myth
data-testid is the most reliable query, so use it everywhere.
Fact
It is stable but tests nothing a user experiences: a button with a test id and no text passes. Role and name queries are just as stable and also check accessibility. Keep test ids for the few elements with no accessible handle.
Myth
findBy is just a slower getBy.
Fact
findBy returns a promise and retries until the element appears or the timeout passes. It is the right tool for anything that appears after an effect, a timer or a request; getBy would fail there.
Myth
screen and the queries returned from render are different.
Fact
They are the same queries; screen is bound to document.body, the returned ones to the container. Prefer screen: no destructuring to keep in sync, and portals (Topic 8.5) render outside the container but inside the body.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Role queries compute the accessibility tree using
dom-accessibility-apiand the ARIA spec, so they are slower than text queries on very large trees (hundreds of rows). If a test is slow, scope withwithin, or passhidden: trueto skip the visibility check (it callsgetComputedStyleper element). - ▸
A string
nameis an exact match on the full computed name, after whitespace normalisation. An icon button witharia-label="Close dialog"is found by{ name: "Close dialog" }, not by its SVG. A function matcher receives the computed name, which makes generic checks possible, such as "no button has an empty name" (Topic 17.7). - ▸
findBy*iswaitFor+getBy*. The default polling is every 50 ms with a 1000 ms timeout, and it also re-checks on every DOM mutation, so it usually resolves as soon as the element appears. Raise the default globally withconfigure({ asyncUtilTimeout: 2000 })rather than per call. - ▸
Implicit roles depend on context:
<header>isbanneronly when it isn't insidearticle/section/main;<section>and<form>only get theregion/formrole when they have an accessible name;<input type="email">istextboxbuttype="number"isspinbuttonandtype="search"issearchbox.
Remember this
- 1
render(<Component />)creates a containerdiv, appends it todocument.bodyand renders your component into it with React (wrapped inact(), so effects and updates are flushed before the next line). It returns helpers:container(the div),rerender(newElement)to render again with new props,unmount(), and the queries bound to the container. Most of the time you ignore the return value and use **screen**, which has every query bound todocument.body. - 2
A query is a function that finds elements. The best one is **
getByRole. A role** is what an element *is* to assistive technology such as a screen reader (software that reads the page aloud for blind users): a<button>has the rolebutton,<h2>isheading,<input type="text">istextbox,<select>iscombobox,<a href>islink,<input type="checkbox">ischeckbox,<ul>islistand<li>islistitem. The accessible name is what a screen reader announces for it: a button's text, an input's<label>, an image'salt, oraria-label. SogetByRole("button", { name: "Save" })finds exactly what a blind user would hear as "Save, button". - 3
Testing Library recommends a query priority. First, queries everyone can use:
ByRole,ByLabelText(form fields by their label),ByPlaceholderText(only when there is no label),ByText(non-interactive text),ByDisplayValue(the current value of a filled field). Then semantic queries:ByAltText(images),ByTitle. Last, **ByTestId**, which findsdata-testid="...": a hidden hook users can't see, for when nothing else works. A nice side effect: if you can't find your button by role and name, a screen reader user can't either, so your tests push you towards accessible markup (Phase 18). - 4
Every query comes in three kinds. **
getBy...returns the element or throws a helpful error if there is none (or more than one).queryBy...** returnsnullinstead of throwing, so use it only to prove something is not there. **findBy...returns a promise** that keeps retrying (every 50 ms, up to 1000 ms by default) until the element appears, so use it withawaitfor things that show up later, like data from an API (Topic 17.5). Each also has an All version (getAllBy,queryAllBy,findAllBy) that returns an array, for lists. - 5
Text matchers can be a string (exact, whole text, whitespace trimmed and collapsed), a regular expression like
/save/i(case-insensitive, partial), or a function(content, element) => boolean.getByRoleoptions narrow further:name,levelfor headings,checked,pressed,selected,expanded,current, andhidden: trueto include elements hidden from the accessibility tree. **within(element)returns all queries limited to that element, which you need when the page has many similar things: "the Remove button in Ravi's row**". - 6
When a query fails, the error prints the page's DOM (and for
ByRole, every accessible role and name it found), which is often enough to see the mistake. You can also print on purpose:screen.debug()prints the whole page,screen.debug(element)one part,prettyDOM(element)returns the same text as a string, andlogRoles(element)lists every role and name.screen.logTestingPlaygroundURL()prints a link to the Testing Playground site, which suggests the best query for each element.
Explain it without notes
Why does Testing Library recommend getByRole first? What is the full priority?
Explain getBy, queryBy and findBy and when to use each.
What does within do? Give an example where you need it.
A getByRole call fails although you can see the button in the browser. How do you debug it?
Practice
Write queries for: a search field labelled "Search products", a level-1 heading "Shop", and an image with alt "Cart". Use the best query for each.
A list shows three products, each with an "Add to cart" button. Write a test (live helper style) that clicks the button for "Mango" only.
Trade-offs
- ↔
Role queries are the most meaningful and a little slower; text queries are fast but can match non-interactive text by accident. Use roles for anything interactive.
- ↔
Test ids never break on copy changes, which is why teams with frequently changing text like them; but they let inaccessible UI pass. A middle ground: role queries with regex names (
/save/i).
Done when you can
I can render a component and find elements with
screen.I use
getByRolewithnamefirst and know the query priority.I pick getBy, queryBy or findBy based on what I expect.
I can scope queries with
within.I can debug a failing query with the error,
screen.debugandlogRoles.