Q1 of 42 · Playwright

What are user-facing locators and why does Playwright recommend them?

PlaywrightMidplaywrightlocatorsaccessibilitybest-practices

Short answer

Short answer: User-facing locators (getByRole, getByLabel, getByText) target elements the way a user or assistive tech would. They're more resilient to markup churn and reward accessible HTML, so tests stay green when CSS class names change.

Detail

Playwright's recommended locator hierarchy is opinionated. At the top sit semantic locators — page.getByRole('button', { name: 'Submit' }), page.getByLabel('Email'), page.getByPlaceholder(...), page.getByText(...). These match the accessible name, label, or visible content rather than implementation details.

The case for them is twofold. First, they're stable under refactor: a designer renaming a CSS class from .btn-primary to .btn-action doesn't break a getByRole('button', { name: 'Submit' }) query. Second, they create a forcing function for accessibility: if you can't locate a button by its accessible name, neither can a screen reader user. Tests that fail because there's no aria-label are tests that caught a real a11y bug.

When semantic locators don't work — because the markup is genuinely opaque, third-party widgets, or canvas content — fall back to getByTestId with a stable data-testid attribute. CSS and XPath selectors should be a last resort because they couple tests to internal structure that has no contract to remain stable.

In an interview, the right framing is: the locator is a contract. Semantic locators express the user's contract; data-testid expresses an explicit testing contract; raw CSS expresses no contract at all and should be avoided.

// EXAMPLE

checkout.spec.ts

import { test, expect } from '@playwright/test';

test('user can submit checkout', async ({ page }) => {
  await page.goto('/checkout');

  // ✅ Preferred: semantic, accessibility-first
  await page.getByLabel('Card number').fill('4242 4242 4242 4242');
  await page.getByLabel('Expiry').fill('12/30');
  await page.getByRole('button', { name: 'Pay now' }).click();

  await expect(
    page.getByRole('heading', { name: 'Order confirmed' }),
  ).toBeVisible();

  // ⚠️  Fallback for opaque widgets — still better than CSS classes
  // await page.getByTestId('payment-iframe').click();
});

// WHAT INTERVIEWERS LOOK FOR

Knowledge of the locator hierarchy and the reasoning (stability + a11y forcing function). Strong candidates frame the locator as a contract.

// COMMON PITFALL

Defaulting to CSS or XPath selectors and treating data-testid as the first choice. Or claiming semantic locators are 'slower' — they aren't.