getByRole
Get every DOM element with the given role, optionally narrowed to the elements
whose accessible name matches. The role is what a screen reader announces an
element as, such as a button or a link, and the accessible name is the text it
announces for it. A test written with cy.getByRole() finds elements the way
assistive technology does, and fails if the role or text is changed.
Browsers expose accessible roles for many elements by default, such as a
<button> or a <nav>. A role attribute can also set one, which is the only
way to give an element a widget role such as tab. For the full list, see the
implicit roles of HTML elements
and the roles that a role attribute can set.
By default, cy.getByRole() skips two kinds of elements:
- Elements hidden from the accessibility tree
- Elements that duplicate a native browser role with an ARIA
roleattribute, such as a<div role="button">
The command has options to find both, but the defaults favor the elements most likely to be accessible to users. This follows the first rule of ARIA, which advises using native browser elements over a custom re-implementation. To change the defaults for your project, overwrite the query or add a custom query of your own.
Syntax
.getByRole(role)
.getByRole(role, options)
// ---or---
cy.getByRole(role)
cy.getByRole(role, options)
Usage
Correct Usage
cy.getByRole('button', { name: 'Save' }) // Yield the button named "Save"
cy.get('nav').getByRole('link') // Yield every link inside <nav>
Incorrect Usage
cy.getByRole(/button/) // Errors, the role must be a string
cy.title().getByRole('heading') // Errors, 'title' does not yield DOM element
cy.getByRole('switch checkbox') // Errors, query one role at a time
cy.getByRole('tab', { native: true }) // Errors, HTML has no native tab element
cy.getByRole('heading', { level: 2 }) // Errors, narrow by level with .filter()
Arguments
role (String)
The role to find, such as 'button', 'heading', 'navigation', or
'tab'. An element has a role when:
- Its tag implies one, such as
<button>forbutton,<nav>fornavigation, or<input type="checkbox">forcheckbox. - Its
roleattribute sets one, such as<div role="tab">. The attribute replaces the implied role. When it lists several roles as fallbacks, the first valid one counts, as in browsers:role="switch checkbox"is aswitch, androle="foo button"is abutton. With no valid role in the attribute, the element keeps its implied role.
options (Object)
Pass in an options object to change the default behavior of cy.getByRole().
| Option | Default | Description |
|---|---|---|
name | undefined | Match only elements whose accessible name matches. Accepts a string, number, regular expression, or function. |
hidden | false | Include elements hidden from the accessibility tree, such as those with aria-hidden="true". |
native | true for roles that HTML has an element for, otherwise false | Match only elements whose HTML tag gives them the role. Pass false to also match elements that have the role only through a role attribute. |
log | true | Displays the command in the Command log |
timeout | defaultCommandTimeout | Time, in milliseconds, to wait for cy.getByRole() to resolve before timing out. Must be a number. |
includeShadowDom | includeShadowDom config option value | Whether to traverse shadow DOM boundaries and include elements within the shadow DOM in the yielded results. |
cy.getByRole() throws an error for any other option. To narrow the results by
an ARIA state such as checked, expanded, or level, chain
.filter().
Yields
cy.getByRole()yields every matching element, in document order.cy.getByRole()is a query, and it is safe to chain further commands.
Examples
Role
Find an element by the role the browser gives it
<main>
<h1>Account settings</h1>
<button>Save changes</button>
</main>
cy.getByRole('main').should('match', 'main')
cy.getByRole('heading').should('have.text', 'Account settings')
cy.getByRole('button').click()
Find a widget by its ARIA role attribute
HTML has no element for widget roles such as tab, menuitem, or switch, so
a page gives an element one with a role attribute. Roles applied this way
follow the
Accessible Rich Internet Applications (ARIA) spec,
so assistive technology can represent custom UI that follows a standard pattern
but isn't part of the HTML spec.
Native matching doesn't apply to these roles, so cy.getByRole() finds the
element by its role attribute.
<div role="tablist">
<button role="tab" aria-selected="true">Profile</button>
<button role="tab" aria-selected="false">Billing</button>
</div>
cy.getByRole('tab', { name: 'Billing' }).click()
Get every element with a role
cy.getByRole() yields every match, like cy.get(), so
assert on the length or pick one with .first() or
.eq().
<ul>
<li><a href="/profile">Profile</a></li>
<li><a href="/billing">Billing</a></li>
<li><a>Coming soon</a></li>
</ul>
// An <a> without an href has no link role, so only two match
cy.getByRole('link').should('have.length', 2)
cy.getByRole('link').eq(1).should('have.attr', 'href', '/billing')
Check every element with a role
Chain .each() to run the same check against every
match, or pass a callback to .should() to assert on
the whole set at once. A .should() callback retries until it passes, so it
also waits for a list that is still loading.
<nav aria-label="Account">
<a href="/profile">Profile</a>
<a href="/billing">Billing</a>
<a href="/security">Security</a>
</nav>
// Runs once for each of the three links
cy.getByRole('link').each(($link) => {
cy.wrap($link).should('have.attr', 'href').and('match', /^\//)
})
// Retries until the links have these names, in this order
cy.getByRole('link').should(($links) => {
const names = $links.toArray().map((link) => link.textContent)
expect(names).to.deep.equal(['Profile', 'Billing', 'Security'])
})
Name
Match by accessible name
The accessible name comes from the element's content, a <label>,
aria-label, aria-labelledby, an image's alt text, or a title
attribute. A string must match the whole name, and it is case-sensitive.
Whitespace in the name is collapsed to a single space and trimmed before it's
compared.
<button>Save changes</button>
<button aria-label="Close dialog">×</button>
<label>Email <input type="email" /></label>
cy.getByRole('button', { name: 'Save changes' }).click()
cy.getByRole('button', { name: 'Close dialog' }).click()
// Neither matches: a string matches the whole name, case-sensitively
cy.getByRole('button', { name: 'Save' }).should('not.exist')
cy.getByRole('button', { name: 'save changes' }).should('not.exist')
Content hidden with aria-hidden="true" is left out of the name. So is CSS
::before and ::after content, so that an icon-font glyph, which lives in a
pseudo-element, doesn't become part of a button's name. Browsers include this
content in the accessible name, so an element named only by ::before or
::after content has a name in the browser but not in cy.getByRole(). Match
it with an aria-label or by another role and name instead.
<button>Send<span aria-hidden="true"> ✉️</span></button>
// Yields and clicks the <button>, whose accessible name is "Send" without the ✉️
cy.getByRole('button', { name: 'Send' }).click()
Match part of a name with a regular expression
Pass a regular expression to match part of the name, or to ignore case.
<button>Delete account</button> <button>Delete draft</button>
// Yields both buttons, since each name starts with "Delete"
cy.getByRole('button', { name: /^delete/i }).should('have.length', 2)
// Yields and clicks only the "Delete draft" button
cy.getByRole('button', { name: /draft$/ }).click()
Match a name with a function
A function receives the accessible name and the element, and returns true
for a match.
<button>Pay $20.00</button> <button>Pay $120.00</button>
cy.getByRole('button', {
name: (name) => Number(name.replace(/[^\d.]/g, '')) > 100,
}).should('have.text', 'Pay $120.00')
Hidden elements
Include elements hidden from the accessibility tree
By default, cy.getByRole() skips an element that assistive technology can't
reach:
- The element or an ancestor has the
hiddenattribute,aria-hidden="true", ordisplay: none. - The element's computed
visibilityishidden. It inherits this from avisibility: hiddenancestor unless it setsvisibility: visibleitself.
Pass hidden: true to include them, such as to find a closed menu.
<nav aria-label="Account" hidden>
<a href="/settings">Settings</a>
</nav>
cy.getByRole('link', { name: 'Settings' }).should('not.exist')
cy.getByRole('link', { name: 'Settings', hidden: true }).should('exist')
An element that is only visually hidden, such as one clipped to a single pixel
or set to opacity: 0, is still in the accessibility tree, so
cy.getByRole() finds it without hidden: true. The hidden option is about
what a screen reader can reach, not what Cypress considers
visible.
Native elements
Native elements by default
A <div role="button"> has the button role, but it doesn't get a button's
keyboard behavior unless the page adds it. For a role that HTML has an element
for, such as button, link, heading, dialog, or checkbox,
cy.getByRole() matches only elements whose tag gives them the role. The test
fails if a native control is replaced with an imitation.
<button>Save</button>
<div role="button" tabindex="0">Cancel</div>
// Yields and clicks the native <button>
cy.getByRole('button', { name: 'Save' }).click()
// The <div> has the role only through its role attribute, so it's skipped
cy.getByRole('button', { name: 'Cancel' }).should('not.exist')
For widget roles that HTML has no element for, such as tab, menuitem, or
switch, a role attribute is the only way to give an element the role, so
cy.getByRole() always matches the role attribute. Passing native: true
with one of these roles throws an error.
Include elements with a role attribute
Pass native: false to also match elements that have the role only through a
role attribute, such as a <div role="dialog"> rendered by a component
library.
// Yields the <div role="button">
cy.getByRole('button', { name: 'Cancel', native: false }).click()
Some widely used patterns need native: false even though they follow the ARIA
authoring practices, because HTML has a different element for the role:
- An autocomplete
<input role="combobox">. The native combobox is a<select>or an<input>with alistattribute. - An icon
<svg role="img">. The native image is an<img>.
When an element was skipped only because it has the role through a role
attribute, the error says so and suggests passing { native: false }.
Change the native default for your project
If your project relies on a component library that re-implements browser roles
with role attributes, such as a <div role="button">, you can make
native: false the default. Overwrite the query with
Cypress.Commands.overwriteQuery()
in your support file. A test can still pass native: true to require a native
element.
Cypress.Commands.overwriteQuery(
'getByRole',
function (originalFn, role, options = {}) {
return originalFn.call(this, role, { native: false, ...options })
}
)
Scope
Search inside another element
Chained off a command that yields DOM elements, cy.getByRole() searches the
descendants of each of them. It never matches the subject element(s) it is
chained off.
<header>
<nav aria-label="Main">
<a href="/">Home</a>
</nav>
</header>
<footer>
<a href="/privacy">Privacy</a>
</footer>
cy.getByRole('navigation', { name: 'Main' })
.getByRole('link')
.should('have.length', 1)
.and('have.text', 'Home')
Act inside a dialog with .within()
Inside a .within() callback, cy.getByRole() searches
only the .within() subject.
<button>Delete</button>
<dialog open aria-label="Delete user">
<button>Cancel</button>
<button>Delete</button>
</dialog>
cy.getByRole('dialog', { name: 'Delete user' }).within(() => {
cy.getByRole('button', { name: 'Delete' }).click()
})
Narrow the results by state with .filter()
cy.getByRole() matches on role and name only. To narrow the results by a state
or property, chain .filter(), or chain
.not() to leave matches out.
// The tab that is currently selected
cy.getByRole('tab').filter('[aria-selected="true"]')
// Level 2 headings
cy.getByRole('heading').filter('[aria-level="2"], h2:not([aria-level])')
// A checked native checkbox
cy.getByRole('checkbox').filter(':checked')
// A checked checkbox that gets its role from a role attribute
cy.getByRole('checkbox', { native: false }).filter('[aria-checked="true"]')
// The menu button whose menu is open
cy.getByRole('button').filter('[aria-expanded="true"]')
// A toggle button that is switched on
cy.getByRole('button').filter('[aria-pressed="true"]')
// The link to the page the reader is on
cy.getByRole('link').filter('[aria-current="page"]')
// Every button except disabled ones
cy.getByRole('button').not(':disabled, [aria-disabled="true"]')
Waiting for elements
Wait for an element to appear
cy.getByRole() retries until a matching element exists and every chained
assertion passes, so it waits for an alert that renders after a save.
cy.getByRole('button', { name: 'Save' }).click()
cy.getByRole('alert').should('have.text', 'Changes saved')
Assert an element doesn't exist
cy.getByRole() retries a negative assertion too, until the element is gone.
cy.getByRole('dialog').should('not.exist')
Options
Find elements inside shadow DOM with includeShadowDom
By default, cy.getByRole() doesn't search inside shadow roots. Pass
includeShadowDom: true to search them, including nested shadow roots, or set
the
includeShadowDom configuration option
to search them in every query.
cy.getByRole('button', { name: 'Checkout', includeShadowDom: true }).click()
To search one shadow root only, chain cy.getByRole() off
.shadow():
cy.get('checkout-panel').shadow().getByRole('button', { name: 'Checkout' })
TypeScript
Type the yielded element
The role argument autocompletes role names, and its type allows any other
string. At runtime, a role that contains a space still
throws an error.
Pass a type argument to type the yielded element.
cy.getByRole<HTMLInputElement>('textbox', { name: 'Email' }).then(($input) => {
// $input is typed as JQuery<HTMLInputElement>
expect($input[0].value).to.eq('')
})
Notes
What a cy.getByRole() error looks like
When no element matches before the timeout, the error says what it looked for and lists the roles and accessible names it did find. Here, the name was misspelled:
Timed out retrying after 4000ms: Expected to find an accessible native element with the role "button" and name "Svae", but never did.
Here are the accessible roles that were found, with the accessible name of each element:
- main: (no name)
- heading: "Account settings"
- button: "Save", "Cancel"
How cy.getByRole() decides an element's role
cy.getByRole() maps elements to roles with
aria-query. Some roles depend on attributes: an <a> needs an href
to be a link, a <section> needs an aria-label or aria-labelledby to be a
region, and an
<input> with a missing or unknown type is a textbox.
Some roles depend on the surrounding table: a <td> is a cell in a table,
a gridcell in a grid or treegrid, and has no role in a table with
role="presentation". Otherwise, the mapping doesn't consider where an element
sits in the page, so every <header> is a banner and every <footer> a
contentinfo, even inside an <article>, where a browser gives them no
landmark role.
Rename an existing getByRole custom command
If your project already defines its own getByRole with
Cypress.Commands.add() or Cypress.Commands.addQuery(), that call now throws
an error, because getByRole is a built-in Cypress query. Rename your command,
or remove it and use the built-in cy.getByRole().
How to migrate from Testing Library findByRole() to cy.getByRole()
cy.getByRole() does what the findByRole() and findAllByRole() queries from
Cypress Testing Library
do for role queries, without a plugin. Most calls translate by renaming the
query, and the state options become a chained
.filter().
cy.findByRole('button', { name: 'Save' }).click()
cy.findByRole('button', { name: /save/i }).click()
cy.findAllByRole('listitem').should('have.length', 3)
cy.findByRole('tab', { selected: true }).should('have.text', 'Billing')
cy.findByRole('checkbox', { checked: true }).should(
'have.attr',
'name',
'terms'
)
cy.getByRole()
cy.getByRole('button', { name: 'Save' }).click()
cy.getByRole('button', { name: /save/i }).click()
cy.getByRole('listitem').should('have.length', 3)
cy.getByRole('tab')
.filter('[aria-selected="true"]')
.should('have.text', 'Billing')
cy.getByRole('checkbox').filter(':checked').should('have.attr', 'name', 'terms')
Keep these differences in mind as you migrate:
- Native elements only, by default. For a role that HTML has an element
for,
cy.getByRole()skips elements that have the role only through aroleattribute, so a test that found a<div role="button">withfindByRole()fails after the migration. Decide for each one whether the application should use the native element, or whether the test should passnative: false. To keep your current behavior while you migrate, overwrite the query to default tonative: false, or wrap it in a custom query of your own. - Every match, not one.
cy.getByRole()yields every match, likefindAllByRole(), instead of failing when more than one element matches. Narrow thename, or chain.first(), to act on one. - State options. The
checked,selected,pressed,expanded,current,busy,level, andvalueoptions become a chained.filter(). The error for each of these options names the filter to use.
When no other Testing Library queries remain in your tests, remove
@testing-library/cypress from your dependencies, its import from your support
file, and its entry in the types of your tsconfig.json.
Migrate from Testing Library findByRole() to cy.getByRole()
Move your role queries to the built-in cy.getByRole() and get a list of the elements that need a decision about native elements.
Migrate this project's Cypress tests from Testing Library's findByRole() and findAllByRole() queries to the built-in cy.getByRole() query. First read https://docs.cypress.io/api/commands/getbyrole.md, then work through these steps: 1. Find every cy.findByRole() and cy.findAllByRole() call in the Cypress specs, custom commands, and support files. If the project defines its own getByRole custom command, stop and tell me, since it conflicts with the built-in query. 2. Replace each call with cy.getByRole(), keeping the role, the command it's chained from, and the name, timeout, log, and hidden options as they are. 3. Replace the checked, selected, pressed, expanded, current, busy, level, and value options with a chained .filter(), as the page describes. Use :checked only for native checkboxes and radios, and [aria-checked="true"] for elements that get their role from a role attribute. 4. Run the migrated specs. cy.getByRole() matches only native elements for roles that HTML has an element for, so a test that found an element such as a <div role="button"> now fails. Don't add native: false to every call. List each of these elements with its spec file and line, so I can decide whether the application should use the native element or the test should pass native: false. 5. If no other Testing Library queries remain, remove @testing-library/cypress from package.json, its import from the support file, and its entry in the types of tsconfig.json. Keep it if any other Testing Library query, such as findByLabelText(), is still in use. 6. Flag any option or pattern with no direct equivalent instead of guessing. Finish with a summary of what changed and the list of elements that need a decision.
Rules
Requirements
cy.getByRole()can be chained off ofcyor off a command that yields DOM element(s). Chained offcy.window()orcy.document(), it searches the whole page.
Assertions
cy.getByRole()automatically retries until the element(s) exist in the DOM.cy.getByRole()automatically retries until all chained assertions have passed.
Timeouts
cy.getByRole()can time out waiting for the element(s) to exist in the DOM.cy.getByRole()can time out waiting for assertions you've added to pass.
Command Log
Find a button by its role and name
cy.getByRole('button', { name: 'Save changes' }).click()
The Command Log shows a getByRole row with the role and options, followed by
the click. When clicking on the getByRole command within the command log,
the console outputs the role, the options, the element the query was applied
to, the yielded element(s), and the number of elements found.
History
| Version | Changes |
|---|---|
| 16.2.0 | cy.getByRole() command added |