Playwright Locators - getByRole, Test IDs, fill() & Dropdowns
A locator tells Playwright how to find an element — and, unlike a one-off element handle, it's re-evaluated every time you use it and waits automatically until the element is ready. Picking the right kind of locator is the biggest single factor in how stable a Playwright suite is. This guide covers the built-in locators in priority order, filtering and chaining, strict mode, and the actions you'll use with them.
The Recommended Order
| Priority | Locator | Finds by | Example |
|---|---|---|---|
| 1 | getByRole() |
ARIA role + accessible name | getByRole('button', { name: 'Submit transfer' }) |
| 2 | getByLabel() |
The form field's label | getByLabel('Amount') |
| 3 | getByPlaceholder() |
Placeholder text | getByPlaceholder('Search payees') |
| 4 | getByText() |
Visible text (non-interactive elements) | getByText('Transfer successful') |
| 5 | getByAltText() / getByTitle() |
Image alt text / title attribute | getByAltText('Company logo') |
| 6 | getByTestId() |
data-testid (configurable) |
getByTestId('submit-transfer') |
| 7 | locator() |
CSS or XPath | locator('#amt') |
Why this order? Role, label and text locators find elements the way a user (or a screen reader) does, so they keep working through CSS and layout refactors — and a failing role locator often reveals a real accessibility problem. Test IDs are the most stable of all but invisible to users; many teams use them whenever user-facing attributes are ambiguous. CSS and XPath are the fallback, because they depend on page structure.
All of Them on One Form
import { test, expect } from '@playwright/test';
test('locator strategies on one form', async ({ page }) => {
await page.setContent(`
<h1>Transfer money</h1>
<label for="amt">Amount</label><input id="amt" value="100">
<input placeholder="Search payees">
<label><input type="checkbox"> Save as template</label>
<select id="country" aria-label="Country">
<option value="in">India</option><option value="us">United States</option>
</select>
<ul class="payees">
<li><span>Asha</span><button>Pay</button></li>
<li><span>Ravi</span><button>Pay</button></li>
</ul>
<button data-testid="submit-transfer">Submit transfer</button>`);
await expect(page.getByRole('heading', { name: 'Transfer money' })).toBeVisible();
await page.getByLabel('Amount').fill('250'); // replaces the existing "100"
await page.getByPlaceholder('Search payees').fill('Ravi');
await page.getByRole('checkbox', { name: 'Save as template' }).check();
await page.getByLabel('Country').selectOption({ label: 'United States' });
await expect(page.getByLabel('Country')).toHaveValue('us');
// Two "Pay" buttons — narrow down to Ravi's row
await page.getByRole('listitem').filter({ hasText: 'Ravi' })
.getByRole('button', { name: 'Pay' }).click();
await page.getByTestId('submit-transfer').click();
});
The HTML is inline, so you can paste this test into any Playwright project and run it.
getByRole in Detail
page.getByRole('button', { name: 'Login' })
page.getByRole('link', { name: 'Forgot Password?' })
page.getByRole('checkbox', { name: 'Remember me' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('heading', { name: 'Dashboard', level: 1 })
page.getByRole('row', { name: /Invoice 1043/ }) // regex for partial names
page.getByRole('button', { name: 'Save', exact: true }) // exact match (default is case-insensitive substring)
The name is the accessible name — usually the visible text, the label, or aria-label. Not sure what an element's role and name are? Use the locator picker in UI mode or npx playwright codegen, which suggests role locators automatically.
Filtering and Chaining ⭐
Real pages repeat things — a "Pay" button per payee, an "Edit" link per row. Narrow down instead of reaching for XPath indexes:
const row = page.getByRole('row').filter({ hasText: 'Invoice 1043' });
await row.getByRole('button', { name: 'Edit' }).click(); // chain inside the row
page.getByRole('listitem').filter({ has: page.getByText('Out of stock') }); // contains an element
page.getByRole('listitem').filter({ hasNotText: 'Archived' }); // exclude
page.getByRole('button', { name: 'Pay' }).first(); // position — use sparingly
page.getByRole('button', { name: 'Pay' }).nth(1); // 0-based
await expect(page.getByRole('button', { name: 'Pay' })).toHaveCount(2);
Strict Mode
Actions on a locator require it to match exactly one element. If getByRole('button', { name: 'Pay' }) matches two buttons, click() fails with a strict mode violation listing both. That's a feature: it stops the test from silently clicking the wrong one. Fix it by making the locator more specific (filter, chain, exact name) — use first() or nth() only when position really is what you mean.
Actions
| Action | Use |
|---|---|
click(), dblclick() |
Click after actionability checks |
fill('text') |
Replace a field's value in one step ⭐ |
clear() or fill('') |
Empty a field |
pressSequentially('text') |
Type key by key — for fields that react to each keystroke (autocomplete) |
press('Enter'), press('Control+A') |
Keys and shortcuts |
check() / uncheck() |
Set a checkbox or radio state — idempotent, unlike click |
selectOption() |
Native <select> by value, label or index |
hover(), focus() |
Menus, tooltips, blur validation |
setInputFiles() |
File uploads |
dragTo() |
Drag and drop |
fill() vs pressSequentially(): fill() sets the value at once and fires an input event — fast and right for most fields. Use pressSequentially() (the replacement for the deprecated type()) only when the page needs real keystrokes, such as a search box that fetches suggestions as you type.
Dropdowns: Native vs Custom
// Native <select>
await page.getByLabel('Country').selectOption('in'); // by value
await page.getByLabel('Country').selectOption({ label: 'India' }); // by visible text
await page.getByLabel('Country').selectOption({ index: 1 });
// Custom dropdown (div/ul-based, common in React/Angular)
await page.getByRole('combobox', { name: 'State' }).click();
await page.getByRole('option', { name: 'Karnataka' }).click();
await expect(page.getByRole('combobox', { name: 'State' })).toHaveText('Karnataka');
selectOption() only works on real <select> elements. For dependent dropdowns (Country → State loaded by an API), just select and continue: the option locator auto-waits until the new options appear. More: Playwright Auto-Wait.
Configuring Test IDs
// playwright.config.ts — if your app uses data-test or data-qa instead of data-testid
export default defineConfig({ use: { testIdAttribute: 'data-test' } });
Coming From Selenium
| Selenium | Playwright |
|---|---|
driver.findElement(By.id("amt")) |
page.locator('#amt') — or getByLabel('Amount') |
findElements(...).size() |
expect(locator).toHaveCount(n) |
clear() + sendKeys() |
fill() |
new Select(el).selectByVisibleText("India") |
selectOption({ label: 'India' }) |
if (!box.isSelected()) box.click() |
check() |
XPath //li[.//span='Ravi']//button |
getByRole('listitem').filter({ hasText: 'Ravi' }).getByRole('button') |
Convert more of your existing locators in the Selenium ⇄ Playwright ⇄ Cypress Translator.
From Real Projects
My own automation experience is with Selenium WebDriver, Java and TestNG in a POM-based hybrid framework, running in batch, parallel and cross-browser mode. The concepts on this page carry straight over to Playwright — page objects, independent tests, parallel execution and reliable synchronisation — which makes it a natural next tool for Selenium testers. The biggest difference you'll notice is how much waiting Playwright handles for you. Testing Testsigma — a platform where users write automated tests in plain English — meant thinking like its users, who are testers themselves. Prefer role- and label-based locators; they read like the user's view of the page.
📚 Official documentation: Playwright docs: Locators · Playwright docs: Actions
FAQs
What are locators in Playwright?
Objects that describe how to find elements. They're re-evaluated on every use and auto-wait for the element before acting, which makes them more reliable than stored element handles.
What is the recommended locator strategy?
User-facing locators first — getByRole, getByLabel, getByPlaceholder, getByText — then getByTestId, and CSS/XPath only as a fallback.
What is a strict mode violation?
An action on a locator that matches more than one element. Make the locator more specific with filters, chaining or an exact name.
How do you clear a text field?
locator.clear() or fill(''); fill('new value') replaces the old value directly.
fill() or pressSequentially()?
fill() for almost everything; pressSequentially() when the page reacts to individual keystrokes.
How do you handle a custom dropdown?
Click the combobox, click the option by role and name, then assert the displayed value — selectOption() works only on native selects.
Related
- The Complete Playwright Tutorial
- Playwright Cheat Sheet
- Playwright Assertions & Screenshots
- Top 20 Playwright Interview Questions