In Playwright, why does page.getByRole('button', { name: 'Save' }) also match a button named 'Save and close'?
answer
- Default is looser than equality
- Two knobs change the comparison
- One knob flips two switches at once
- Whitespace normalised before comparing
- Patterns make exact irrelevant
basics
~10 sThe name option is a case-insensitive substring test against the accessible name by default, so any name containing Save matches. Pass exact: true for a case-sensitive whole-string match, or a RegExp for full control.
solid answer
~40 sBy default `name` matches a **substring**, case-insensitively, after whitespace on both sides is normalised. So `{ name: 'Save' }` matches `Save`, `save` and `Save and close` alike. Two knobs change that: `exact: true` makes the comparison a whole-string, case-sensitive one, and passing a `RegExp` lets the pattern decide, in which case `exact` is ignored. Note that `exact: true` still trims and collapses whitespace, so a label split across source lines is compared as one normalised string. The practical consequence on a page with `Save`, `Save and close` and `Save as draft` is an ambiguous locator: Playwright refuses to act when a locator resolves to several elements. Tighten with `exact: true`, anchor with a pattern such as `/^Save$/`, or scope the query to a container.
code
typescript · 14 linesimport { test, expect } from '@playwright/test';
test('narrowing an ambiguous name on the issue composer', async ({ page }) => {
await page.goto('/issues/42');
// Page has "Save", "Save and close" and "Save as draft".
await expect(page.getByRole('button', { name: 'Save' })).toHaveCount(3);
// Whole-string, case-sensitive: exactly one match.
await expect(page.getByRole('button', { name: 'Save', exact: true })).toHaveCount(1);
// A pattern anchors without hard-coding the whole label.
await page.getByRole('button', { name: /^Save and close$/ }).click();
});go deeper
Remember the name option is not string equality. It matches any accessible name that contains your text, ignoring case, so a short label can match several longer buttons on the same screen.
Explain that exact: true switches to a whole-string case-sensitive comparison, that whitespace is normalised either way, and that a RegExp overrides exact entirely.
Show judgment about which knob to reach for: substring for volatile decorations such as count badges, exact for prefix collisions, patterns for dynamic labels, scoping when the page repeats a control.
Own the tradeoff between locators that survive copy edits and locators that pin exact user-facing strings. Decide where that line sits so a wording change does not become a suite-wide breakage.
By default the `name` option of `page.getByRole` is a **case-insensitive substring** test against the computed accessible name, so `{ name: 'Save' }` matches a button whose name is `Save`, one whose name is `save`, and one whose name is `Save and close`. This is deliberate: labels change case and gain suffixes, and a substring match survives that. It is also the single most common source of a locator that matches more elements than the author expected. ## The three matching modes | `name` value | Matching | Case | |---|---|---| | `'Save'` | substring | insensitive | | `'Save'` with `exact: true` | whole string | sensitive | | `/^Save$/` | whatever the pattern says | pattern's own flags | - `exact: true` flips **both** switches at once — there is no option that makes matching case-sensitive while keeping it a substring. - `exact` is ignored when `name` is a `RegExp`; the pattern already expresses everything, including anchoring and the `i` flag. - `exact` also governs the `description` option, which matches the accessible *description* in the same way. ## Whitespace is normalised either way Before comparing, Playwright normalises whitespace on both sides: it trims the ends and collapses internal runs of whitespace to a single space. So a comment composer button written as ```html <button> Add comment </button> ``` has the accessible name `Add comment`, and `{ name: 'Add comment', exact: true }` matches it. `exact: true` means "the whole normalised string", not "the raw characters in the source". ## What goes wrong on a real page An issue board that renders `Save`, `Save and close` and `Save as draft` gives `page.getByRole('button', { name: 'Save' })` three matches. Playwright will happily count or assert over the set, but an action on it fails with an error listing the candidates rather than clicking the first one. The fixes, in order of preference: 1. Tighten the name: `{ name: 'Save', exact: true }` picks only the button named exactly `Save`. 2. Anchor with a pattern when the label is dynamic: `{ name: /^Save( \(\d+\))?$/ }`. 3. Scope the query to a container first, so the extra candidates are outside the search root. ## Choosing between exact and a pattern - Prefer the **default substring** when the label carries a volatile decoration — a count badge, a trailing icon whose text is part of the name, a trailing ellipsis. - Prefer **`exact: true`** when a shorter label is a prefix of a longer one on the same screen; that is precisely the `Save` / `Save and close` case. - Prefer a **`RegExp`** when you need anchoring plus tolerance, for example `/^Issue \d+$/` for a heading whose number changes per test run. - Avoid `exact: true` for user-visible copy that a product team rewords; the whole-string test is what breaks first when `Save` becomes `Save changes`. ## Case-insensitivity is a real behaviour, not a nicety Because the default is case-insensitive, `{ name: 'save' }` and `{ name: 'SAVE' }` behave identically. A candidate who assumes the string is compared with `===` will be surprised by extra matches and, worse, will "fix" an ambiguity by changing the case of the string, which changes nothing. The two knobs that actually change the match are `exact` and switching to a pattern. ## Reading the name, not the text One more trap sits behind this option: the value being compared is the **accessible name**, not `textContent`. An icon-only button labelled with `aria-label="Save"` has the name `Save` even though it contains no text at all, and a button whose visible text is `Save` but which carries `aria-label="Persist draft"` has the name `Persist draft` — the label wins, so `{ name: 'Save' }` does not match it. When a name-based match surprises you, the first thing to check is which of `aria-labelledby`, `aria-label`, a native label or the content actually produced the name.
- Does exact: true compare the raw source text of the label?No. Playwright normalises whitespace on both the computed accessible name and the string you pass, trimming the ends and collapsing internal runs to a single space. A button whose label is split across three source lines has the name `Add comment`, and `exact: true` matches that normalised form.
- When would you prefer a RegExp over exact: true?When the label is partly dynamic. A heading that reads `Issue 42` today and `Issue 137` tomorrow is matched by `/^Issue \d+$/`, which anchors both ends while tolerating the number. `exact: true` would force the test to know the exact value, and the plain substring would be too loose.
- Does exact affect anything besides the name option?Yes, it also governs the `description` option, which matches the accessible description in the same way. One `exact` flag applies to both, so you cannot make the name comparison strict while leaving the description loose within a single getByRole call.
saying these in an interview costs you the question
- Thinks the name option is an exact string equality test
- Believes matching is case-sensitive unless told otherwise
- Says exact: true compares raw untrimmed source text
- Thinks exact still applies when name is a RegExp
- Fixes an ambiguous match by changing the string's case