Locators & Selectors
Master Playwright's built-in locators — getByRole, getByText, getByLabel, CSS, XPath — and learn to chain and filter them.
Why Locators Matter
Choosing the right locator strategy is the single biggest factor in test reliability. Locators that are tightly coupled to implementation details — such as generated class names or element indices — break whenever a developer refactors the markup. Playwright's semantic locators like getByRole and getByLabel mirror how assistive technologies navigate the page, making tests resilient and accessible-by-design.
Recommended Locator Priority
The Playwright team recommends a priority order: getByRole first (matches ARIA roles), then getByLabel (for form controls), then getByPlaceholder, getByText, getByAltText, getByTitle, and finally getByTestId when you control the markup. Fall back to CSS or XPath only when semantic locators are not feasible.
data-testid attributes to important UI components as part of your development workflow. This gives tests a stable, intent-revealing handle that survives design changes.Chaining and Filtering
Locators can be chained to narrow scope — for example, find the table row that contains a specific customer name and then click the delete button within that row. The .filter() method refines a locator set by inner text or another nested locator, while .nth() picks a specific item from a list. These patterns eliminate the need for brittle index-based CSS selectors.
| Locator | Best for | Example |
|---|---|---|
| getByRole | Buttons, links, headings, inputs | getByRole('button', {name:'Save'}) |
| getByLabel | Form fields with a label | getByLabel('Email') |
| getByText | Paragraphs, spans, list items | getByText('Welcome back') |
| getByTestId | Custom data-testid attributes | getByTestId('submit-btn') |
| locator(css) | Specific CSS selectors | locator('.card .title') |
| locator(xpath) | Complex DOM traversal | locator('//tr[td[text()="Alice"]]') |
import { test, expect } from '@playwright/test';
test('locator strategies showcase', async ({ page }) => {
await page.goto('/components');
// By ARIA role — most reliable
await page.getByRole('button', { name: 'Submit' }).click();
// By associated label element
await page.getByLabel('Search').fill('playwright');
// By visible text content
await expect(page.getByText('No results found')).toBeHidden();
// By data-testid attribute
await page.getByTestId('user-avatar').click();
// CSS selector fallback
await page.locator('.notification-badge').waitFor();
// Chaining — find delete button inside a specific row
const row = page.getByRole('row').filter({ hasText: 'Alice' });
await row.getByRole('button', { name: 'Delete' }).click();
// nth() — second item in a list
await page.getByRole('listitem').nth(1).click();
// XPath for complex traversal
await page.locator('xpath=//td[normalize-space()="Active"]/../td[1]').click();
});
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.AriaRole;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
class LocatorsTest {
void showcaseLocators(Page page) {
page.navigate("http://localhost:3000/components");
// By ARIA role
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")).click();
// By label
page.getByLabel("Search").fill("playwright");
// By test ID
page.getByTestId("user-avatar").click();
// Chaining — delete button inside specific row
Locator row = page.getByRole(AriaRole.ROW)
.filter(new Locator.FilterOptions().setHasText("Alice"));
row.getByRole(AriaRole.BUTTON,
new Locator.GetByRoleOptions().setName("Delete")).click();
// CSS selector fallback
assertThat(page.locator(".notification-badge")).isVisible();
}
}
from playwright.sync_api import Page, expect
def test_locator_strategies(page: Page):
page.goto("/components")
# By ARIA role — most robust
page.get_by_role("button", name="Submit").click()
# By associated label
page.get_by_label("Search").fill("playwright")
# By visible text
expect(page.get_by_text("No results found")).to_be_hidden()
# By data-testid
page.get_by_test_id("user-avatar").click()
# Chaining — delete button in row containing "Alice"
row = page.get_by_role("row").filter(has_text="Alice")
row.get_by_role("button", name="Delete").click()
# nth() picks item by index
page.get_by_role("listitem").nth(1).click()
# XPath fallback
page.locator("xpath=//td[normalize-space()='Active']/../td[1]").click()
Written by PV
© 2026 All Rights Reserved