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: getByRolegetByLabelgetByTextgetByTestId → 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

  1. Log in using only getByRole and getByLabel locators.
  2. Add the first inventory item to the cart without using CSS selectors.
  3. 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.