How does Playwright getByRole's exact: true match the accessible name?
PlaywrightPlaywright's getByRole() locator uses the browser's accessibility tree to find elements by their semantic role. When you add exact: true to match against the name parameter, Playwright looks for an exact string match on the element's computed accessible name—not its visible text or HTML attributes alone. The accessible name is built by the browser following a specific cascade of sources, and mismatches often occur because of whitespace normalization, invisible elements, or text that looks identical visually but differs at the string level.
How accessible names are computed
The browser builds an element's accessible name by checking sources in this order:
- aria-label attribute – Direct text override; highest priority
- aria-labelledby attribute – Points to another element's text via its id
- Visible text content – Child text nodes and allowed form controls
- alt attribute – For images and image buttons
- title attribute – Fallback for images; lower priority for interactive elements
Once a source is found, the browser stops and uses that name. Critically, the accessible name is then trimmed of leading/trailing whitespace and normalized – consecutive internal spaces collapse to a single space, and newlines/tabs are treated as spaces.
What exact: true actually compares
By default, getByRole() with a name does substring matching, case-insensitive, with whitespace normalization:
// Matches because "Log in" is inside "Click to Log in today"
await page.getByRole('button', { name: 'Log in' }).click();
When you add exact: true, the match becomes an exact string comparison after normalization:
// Will NOT match if button text is "Log in" (double space)
// even though both render visually the same
await page.getByRole('button', { name: 'Log in', exact: true }).click();
The browser normalizes both the element's accessible name and your search string before comparing:
- Trim leading/trailing whitespace
- Collapse consecutive spaces to single spaces
- Preserve case (the comparison is case-sensitive)
Common causes of mismatch
Extra whitespace in markup:
<button>
Log in
</button>
The text node includes a newline and spaces before "Log in" and after. The browser's normalization trims these, yielding "Log in". This works fine.
But if you have:
<button>
{' '} {/* Extra space node */}
Log in
</button>
The accessible name becomes " Log in" (two spaces before "Log in"), which after normalization is "Log in". Still works. However, case matters:
// Will fail with exact: true
await page.getByRole('button', { name: 'log in', exact: true }).click();
Icon buttons relying on aria-label:
<button aria-label="Save document">
<SaveIcon />
</button>
The accessible name is "Save document" (from aria-label). Without aria-label, the button would have no accessible name because the icon is not text. The fix is always to include aria-label or aria-labelledby on icon-only buttons.
Hidden text affecting the name:
<button>
Submit
<span className="hidden">form</span>
</button>
Even if .hidden { display: none } hides the span visually, the accessible name includes "Submit form" because elements with display: none are excluded from the accessibility tree. However, elements with visibility: hidden or opacity: 0 are included, so their text gets added to the accessible name.
Why it works
Normalization ensures that visually similar text always produces the same accessible name in the browser's accessibility tree. By enforcing exact matching with exact: true, your tests verify that an element's true accessible label matches what users with assistive technology will hear or read—not just what sighted users see. This catches subtle misconfigurations like missing aria-label on icon buttons or stray whitespace from JSX formatting.
Tips
- Always include aria-label or aria-labelledby on icon-only buttons; visible text alone will not generate an accessible name
- Use exact: true when you want precise semantic assertions; use the default substring match for flexible locating
- Remember that whitespace is normalized (consecutive spaces become one, leading/trailing is trimmed), but case is not
- If a button looks correct visually but exact: true fails, inspect its accessible name in your browser's DevTools accessibility panel or use page.getByRole().all() to list all roles and see what names are computed
- For debugging, use Playwright Inspector or print await page.locator('role=button').getByText(/.../).getAttribute('aria-label') to verify the actual accessible name
- For the broader pattern of how visible and hidden text affects accessible-name matches, see hidden accessibility name mismatch.