Command Palette

Search for a command to run...

Hectal
PHASE 17Advanced ~26 min· topic 3 of 7

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 getByRole or queryByText.
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.

SignupForm.test.jsxwhole filejsx
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).

Pick the query by prioritydiagram
Rendering diagram…

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.

Choose get, query or find by what you expectdiagram
Rendering diagram…

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.

Team.test.jsxwhole filejsx
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. 1

    Break the label link

    In the form example, change htmlFor="email" to htmlFor="mail". Predict which lines fail and what the error says. (The textbox loses its name, so both getByRole("textbox", { name: "Email" }) and getByLabelText("Email") throw.) That is exactly the bug a screen reader user would meet.

  2. 2

    Make findBy give up

    In the getBy/queryBy/findBy example, change the timer from 300 to 1500 and run. Predict: does findByRole still find it? (Its default timeout is 1000 ms.) Then pass { timeout: 2000 } as the third argument: screen.findByRole("status", {}, { timeout: 2000 }).

  3. 3

    Query inside Asha's row

    In the within() example, log within(screen.getByRole("row", { name: /Asha/ })).getByRole("cell", { name: /Admin/ }).textContent. Then try screen.getAllByRole("row").length: predict the number (the header row counts).

Code & diagrams

One form, every kind of query New tab

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

Press Run to see the page.

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")                          span
getBy, queryBy and findBy New tab

The status appears after 300 ms. getBy and queryBy look once, right now, so they miss it. findBy waits and finds it.

Preview

Press Run to see the page.

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/)  → []
within(), and printing what the test sees New tab

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

Press Run to see the page.

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 />

--------------------------------------------------
The same queries in a Vitest file with jest-domjsx
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).

terminal
$ Console
── what you'll see ──
TestingLibraryElementError: Unable to find an element with the text: Saved!. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

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.

terminal
$ Console
── what you'll see ──
Found multiple elements with the role "button" and name "Remove"

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.

terminal
$ Console
── what you'll see ──
TypeError: Cannot read properties of null (reading 'click')

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-api and the ARIA spec, so they are slower than text queries on very large trees (hundreds of rows). If a test is slow, scope with within, or pass hidden: true to skip the visibility check (it calls getComputedStyle per element).

  • ▸

    A string name is an exact match on the full computed name, after whitespace normalisation. An icon button with aria-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* is waitFor + 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 with configure({ asyncUtilTimeout: 2000 }) rather than per call.

  • ▸

    Implicit roles depend on context: <header> is banner only when it isn't inside article/section/main; <section> and <form> only get the region/form role when they have an accessible name; <input type="email"> is textbox but type="number" is spinbutton and type="search" is searchbox.

Remember this

  1. 1

    render(<Component />) creates a container div, appends it to document.body and renders your component into it with React (wrapped in act(), 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 to document.body.

  2. 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 role button, <h2> is heading, <input type="text"> is textbox, <select> is combobox, <a href> is link, <input type="checkbox"> is checkbox, <ul> is list and <li> is listitem. The accessible name is what a screen reader announces for it: a button's text, an input's <label>, an image's alt, or aria-label. So getByRole("button", { name: "Save" }) finds exactly what a blind user would hear as "Save, button".

  3. 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 finds data-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. 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...** returns null instead 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 with await for 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. 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. getByRole options narrow further: name, level for headings, checked, pressed, selected, expanded, current, and hidden: true to 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. 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, and logRoles(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

01

Why does Testing Library recommend getByRole first? What is the full priority?

02

Explain getBy, queryBy and findBy and when to use each.

03

What does within do? Give an example where you need it.

04

A getByRole call fails although you can see the button in the browser. How do you debug it?

Practice

01

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.

02

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 getByRole with name first 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.debug and logRoles.