level 02 / locators
Locators
getByRole, getByText, CSS, XPath, test IDs — and which to choose for tests that survive refactors.
practice site: SauceDemo
Locator priority
Playwright’s guidance, in order: getByRole → getByLabel → getByText →
getByTestId → CSS → XPath. Role-based locators assert accessibility for free
and survive markup refactors.
// Resilient: tied to user-visible semantics
await page.getByRole('button', { name: 'Login' }).click();
// Fragile: tied to implementation details
await page.locator('#root > div > div:nth-child(2) > button').click();
page.getByRole('button', { name: 'Login' })
Accessible role + name. First choice.
page.getByLabel('Username')
Form fields by their label.
page.getByText('Welcome back')
Visible text. Good for non-interactive elements.
page.getByTestId('cart-badge')
Stable hook when semantics are unavailable.
page.locator('.btn-primary')
CSS. Last resort — couples test to styling.
Key insight: Role-based locators double as accessibility checks — if
getByRole cannot find your button, neither can a screen reader.Strict mode
Every Playwright locator must resolve to exactly one element when actioned.
Two matches throw — by design. Disambiguate with filter(), nth(), or a
more specific role/name.
Practice on SauceDemo
- Log in using only
getByRoleandgetByLabellocators. - Add the first inventory item to the cart without using CSS selectors.
- Find a locator that matches two elements and fix it with
filter().
One flow, three layers
import { test, expect } from '@playwright/test';
import { LoginPage } from './login.page';
test('standard user can log in', async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.signIn('standard_user', 'secret_sauce');
await expect(page.getByText('Products')).toBeVisible();
});import type { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async goto() { await this.page.goto('https://www.saucedemo.com'); }
async signIn(user: string, pass: string) {
await this.page.getByPlaceholder('Username').fill(user);
await this.page.getByPlaceholder('Password').fill(pass);
await this.page.getByRole('button', { name: 'Login' }).click();
}
}import { defineConfig } from '@playwright/test';
export default defineConfig({
use: { baseURL: 'https://www.saucedemo.com', trace: 'on-first-retry' },
retries: process.env.CI ? 2 : 0,
});Try it: TypeScript Locator Patterns
Try it: LocatorLab
Interact with a live practice page. Type a locator below and see which elements match in real time.
Try it: PwRunner Simulation
Watch a simulated Playwright test script run step by step against a mock login page.