A React Testing Library test renders a table where every row has a Delete button, and screen.getByRole('button', { name: /delete/i }) fails with "found multiple elements". How do you target the button in one specific row without dropping to a CSS selector?
answer
- the search area is wrong, not the query
- screen is document.body; bind a smaller node
- find the row, then query inside it
- index into getAllBy is order-coupled
- ambiguous for the test, ambiguous for the user
basics
~20 sScope the query instead of widening it: find the row with an accessible query, pass that element to within(), then run the role query inside it — within(row).getByRole('button', { name: /delete/i }). Indexing getAllBy results is the fragile fallback.
solid answer
~50 sThe ambiguity is real, so the fix is to narrow the search area rather than the query text. `screen` searches the whole document body; `within(element)` returns the same query set bound to one subtree. So I find the row by something a user would recognise — `screen.getByRole('row', { name: /ada lovelace/i })`, or a list item found by its heading — and then run `within(row).getByRole('button', { name: /delete/i })`. That keeps every query on the accessible tier and it reads like the user's mental model: "the Delete button in Ada's row". `getAllByRole('button', { name: /delete/i })[2]` also compiles, but it encodes row ordering the component is free to change, so it silently starts deleting a different row after a sort change. If the rows are genuinely indistinguishable to a user too, that is a product problem — the button needs a more specific accessible name.
code
javascript · 16 linesimport { screen, within } from '@testing-library/dom'
document.body.innerHTML = `
<table>
<tbody>
<tr><td>Ada Lovelace</td><td><button>Delete</button></td></tr>
<tr><td>Grace Hopper</td><td><button>Delete</button></td></tr>
</tbody>
</table>
`
// throws: two buttons match
// screen.getByRole('button', { name: /delete/i })
const row = screen.getByRole('row', { name: /ada lovelace/i })
const deleteAda = within(row).getByRole('button', { name: /delete/i })go deeper
Know that screen searches the whole page and that within(element) runs the same queries inside one element. Be able to write the two-step find-the-row-then-find-the-button pattern.
Explain why indexing into getAllBy is a different kind of test: it encodes ordering, so a reordering bug keeps the suite green while the wrong record gets deleted.
Show that you read multiplicity as a signal about the UI — repeated identical labels are a real problem for screen-reader and voice-control users, and the durable fix is often a better accessible name rather than a cleverer query.
Push the concern upstream: landmark and region markup gives every test stable anchors, so the scoping pattern is only as good as the component library's structural conventions.
## Why the query failed `getBy*` throws on multiple matches by design. The error is not telling you the query is wrong; it is telling you the *search area* is wrong. Five Delete buttons in a table are all legitimately named "Delete" — the thing that distinguishes them is which row they live in. There are three responses, and only one of them is durable. ## Response 1: widen the query (bad) ```javascript // fragile: depends on row order screen.getAllByRole('button', { name: /delete/i })[2] ``` This passes today. It keeps passing after a change that reorders the rows — but now it deletes the wrong record, and the test still says green. Positional indexing turns a behavioural test into a structural one. It is acceptable in exactly one case: when the position *is* the thing under test ("the third item in the sorted list is Pears"), and even then you usually want to assert on the list's content rather than click by index. ## Response 2: reach for a CSS selector (worse) ```javascript container.querySelector('[data-row-id="42"] .delete-btn') ``` Now the test is coupled to a class name and an internal id. It also silently opts out of the accessibility check that a role query gives you: this passes whether or not the button is reachable by keyboard. ## Response 3: scope with within (right) `within(element)` is the counterpart to `screen`. `screen` is simply the whole query set bound to `document.body`; `within(node)` binds the identical set to any node you hand it. Everything you know about `getBy`/`queryBy`/`findBy` and the query ladder applies unchanged inside it. ```javascript import { screen, within } from '@testing-library/dom' const row = screen.getByRole('row', { name: /ada lovelace/i }) within(row).getByRole('button', { name: /delete/i }) ``` Two things make this good. First, the anchor is chosen by meaning, not position: the row is identified by content a user reads. Second, the assertion reads like the user story. Reordering rows, restyling the table, or swapping `<table>` for a grid of `<div role="row">` all leave the test alone. The same pattern generalises far beyond tables: ```javascript const dialog = screen.getByRole('dialog', { name: /confirm deletion/i }) within(dialog).getByRole('button', { name: 'Confirm' }) const nav = screen.getByRole('navigation', { name: 'Main' }) within(nav).getByRole('link', { name: 'Settings' }) ``` Any landmark, region, dialog, list item or table row can serve as a scope. This is one of the practical reasons to care about landmark markup: it gives your tests stable anchors for free. ## Choosing the anchor A good scope anchor is (a) named by user-visible content and (b) at the smallest level that is still unambiguous. Prefer `getByRole('row', { name: /ada/i })` over `getAllByRole('row')[2]` for the same reason as above. If your rows have no accessible name — a common situation when a table is built from unlabelled `<div>`s — that is worth fixing in the component, because a screen-reader user navigating that table has exactly the problem your test has. ## When the answer is to change the markup Sometimes scoping is not enough, or the scope itself is ambiguous. Then the honest conclusion is that the UI does not distinguish these controls for anyone. A card grid of five "Edit" buttons is announced to a screen-reader user as "Edit, Edit, Edit, Edit, Edit", and voice-control users cannot say "click Edit" and be understood. Giving each button a more specific accessible name fixes the product and your test at once. ## What not to conclude The wrong lesson from "found multiple elements" is "role queries are impractical, use test ids". The multiplicity is information: it is the test telling you that the handle you picked is not unique, and the fix is to say *where*, not to switch to an attribute that only tests can see.
- When is indexing into getAllBy* results actually the right call?When order is the property under test — asserting a sorted list renders "Apples, Pears, Plums" in that sequence, or that the first row of a leaderboard is the winner. There the index is the assertion, not an accident. For clicking a control, prefer scoping, because an index quietly survives a reordering bug that should have failed the test.
- What does within() give you that a plain container-scoped query does not?The same thing, expressed at the level you care about: within takes any element you already found with an accessible query, so the scope itself is chosen by meaning rather than by structure. It also composes — you can nest it — and it works on elements from queries, refs or portals, without needing a handle on the render result.
- How would you target a control inside a modal that is rendered through a portal outside your component's DOM?Query from `screen` rather than from a component-local container, since screen searches the whole document body where the portal content actually lands. Then scope with `within(screen.getByRole('dialog', { name: /…/ }))`. That is the standard reason to prefer screen as the default entry point for queries.
saying these in an interview costs you the question
- Adds a data-testid per row to defeat the ambiguity
- Indexes into getAllBy and calls it deterministic
- Says role queries do not work for repeated components
- Switches to container.querySelector with a row class
- Treats "found multiple elements" as a Testing Library bug