Skip to main content

Command Palette

Search for a command to run...

🎯 Mastering getByRole in Playwright: Write Reliable and Accessible Tests

Updated
•4 min read
🎯 Mastering getByRole in Playwright: Write Reliable and Accessible Tests

When writing UI tests, choosing the right selector can make or break your test suite. Fragile selectors like class names or IDs often lead to flaky tests that break with minor UI changes. That’s where getByRole comes in—a powerful, accessibility-first query method that helps you write robust, readable, and maintainable tests.

In this post, we’ll explore how getByRole works, why it’s better than traditional selectors, and how to use it effectively in your Playwright tests.


🔍 What is getByRole?

getByRole is a query method from the Testing Library family, integrated into Playwright via @playwright/experimental-ct-* packages. It allows you to select elements based on their ARIA roles, which describe the purpose of an element in the UI.

âś… Why Use getByRole?

Here are some compelling reasons to use getByRole:

  • Accessibility-first: Encourages semantic HTML and accessible components.

  • Resilient to UI changes: Doesn’t break when class names or IDs change.

  • Readable: Clearly communicates the intent of the test.

  • Built-in filtering: Supports options like name, level, and hidden.

đź§Ş Basic Usage

Here’s a simple example using Playwright Component Testing with React:

import { test, expect } from '@playwright/experimental-ct-react';
import LoginForm from './LoginForm';

test('should submit login form', async ({ mount }) => {
  const component = await mount(<LoginForm />);

  const usernameInput = component.getByRole('textbox', { name: 'Username' });
  const passwordInput = component.getByRole('textbox', { name: 'Password' });
  const submitButton = component.getByRole('button', { name: 'Login' });

  await usernameInput.fill('gunasekaran');
  await passwordInput.fill('securepassword');
  await submitButton.click();

  await expect(component).toContainText('Welcome, gunasekaran!'

Did you know the name parameter can come from multiple sources, not just visible text?. here are the list of scenarios that name can handle.

  1. Visible text
<button>Submit</button>

await page.getByRole('button', { name: 'Submit' }).click();
  1. aria-label
<button aria-label="Submit Form"></button>
await page.getByRole('button', { name: 'Submit Form' }).click();
  1. aria-labelledby
<label id="lblUser">Username</label>
<input type="text" aria-labelledby="lblUser">

await page.getByRole('textbox', { name: 'Username' }).fill('admin');
  1. Alt text for images
<img src="logo.png" alt="Company Logo">
await page.getByRole('img', { name: 'Company Logo' }).click();
DOM ExampleAccessible NamePlaywright LocatorNote
<button>Submit</button>"Submit"page.getByRole('button', { name: 'Submit' })Simple button with visible text.
<button>Submit <label>form</lable> </button>"Submit form"page.getByRole('button', { name: 'Submit form' })button tag contains nested label tag. both can be concatenated in the name and used for identifying
<input type="text" aria-labelledby="lblUser"><label id="lblUser">Username</label>"Username"page.getByRole('textbox', { name: 'Username' })Label references input using aria-labelledby.
<button aria-label="Save Changes"></button>"Save Changes"page.getByRole('button', { name: 'Save Changes' })aria-label provides accessible name, visible text not required.
<img src="logo.png" alt="Company Logo">"Company Logo"page.getByRole('img', { name: 'Company Logo' })alt attribute used for accessible name.
<div role="checkbox" aria-checked="true">Accept Terms</div>"Accept Terms"page.getByRole('checkbox', { name: 'Accept Terms', checked: true })Checkbox with text inside div, state can be filtered.
<h1>Welcome User</h1>"Welcome User"page.getByRole('heading', { name: 'Welcome User', level: 1 })Heading with plain text, no nested tags.
<h1 name='greeting'>Welcome User</h1>"Welcome User"

"greeting" —> wrong | page.getByRole('heading', { name: 'Welcome User' }) | Dont get confused with name attribute
name attribute ≠ accessible name in Playwright. |

There are html elements that has implicit role so it won’t have the role attribute manually but still can be identified using role

âś… Implicit ARIA Roles in Native HTML Elements

Implicit ARIA RoleHTML Elements
link<a href>, <area href>
button<button>, <input type="button">, <input type="submit">, <input type="reset">, <input type="image">
checkbox<input type="checkbox">
radio<input type="radio">
slider<input type="range">
textbox<input type="email">, <input type="tel">, <input type="text">, <input type="url">, <textarea>
listbox<select>
option<option>
progressbar<progress>
text<b>, <i>, <u>, <strong>, <em>, <dfn>, <abbr>, <span>
meter<meter>
form<form>
table<table>
columnheader / rowheader<th>
cell<td>
row<tr>
rowgroup<thead>, <tbody>, <tfoot>
list<ul>, <ol>
listitem<li>
dialog<dialog>
heading<h1>, <h2>, <h3>, <h4>, <h5>, <h6>
document<iframe>, <code>, <pre>, <cite>, <time>
img<img> (if alt attribute is present)
banner<header>
contentinfo<footer>
main<main>
article<article>
complementary<aside>
navigation<nav>
region<section>, <blockquote>, <address>
figure<figure>
caption<figcaption>
group<summary>, <details>

📚 Conclusion

Using getByRole in Playwright helps you write tests that are not only more reliable but also promote better accessibility. It’s a win-win for developers and users alike. Whether you're testing components or full pages, adopting role-based queries will elevate the quality of your test suite.