# 🎯 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:

```typescript
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**
    

```html
<button>Submit</button>

await page.getByRole('button', { name: 'Submit' }).click();

```

2. `aria-label`
    

```html
<button aria-label="Submit Form"></button>
await page.getByRole('button', { name: 'Submit Form' }).click();
```

3. `aria-labelledby`
    

```html
<label id="lblUser">Username</label>
<input type="text" aria-labelledby="lblUser">

await page.getByRole('textbox', { name: 'Username' }).fill('admin');
```

4. **Alt text for images**
    

```html
<img src="logo.png" alt="Company Logo">
await page.getByRole('img', { name: 'Company Logo' }).click();
```

| **DOM Example** | **Accessible Name** | **Playwright Locator** | **Note** |
| --- | --- | --- | --- |
| `<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"` —&gt; 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 Role** | **HTML 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.
