Playwright

Locators and Web-first Assertions

Thirdy Gayares
16 min read

🎯 What You Will Learn

Choose resilient role, label, text, and test-id locators, then verify behavior with auto-retrying assertions.

  • Choose locators in a reliable priority order
  • Narrow ambiguous matches without fragile DOM paths
  • Use assertions that retry until the UI reaches the expected state
  • Diagnose strictness and timeout failures
Prerequisites: A Playwright Test project and one working end-to-end test.

Treat a Locator as a Live Query

A locator describes how to find an element at action time. Playwright resolves it again before each action, so it can survive a component re-render.

Use a User-facing Priority Order

Start with role, label, placeholder, text, alt text, or title. Use a test id when the interface needs an explicit testing contract.

LocatorTypical use
getByRoleButtons, links, headings, dialogs, and landmarks
getByLabelForm controls with visible labels
getByTextVisible messages and content
getByTestIdStable explicit contract when user-facing attributes are insufficient

Locate by Role and Name

Role locators follow accessibility semantics and make the intended control clear.

tests/account.spec.ts
await page
  .getByRole('button', { name: 'Save changes' })
  .click();

await expect(
  page.getByRole('status')
).toHaveText('Changes saved');

Locate Form Controls by Label

Labels remain understandable to users and tests. They are normally more stable than class names generated by styling systems.

tests/login.spec.ts
await page.getByLabel('Email address').fill('[email protected]');
await page.getByLabel('Password').fill('example-password');
await page.getByRole('button', { name: 'Sign in' }).click();

Narrow Ambiguous Matches

Locators are strict for single-element actions. Scope to a card, row, or dialog before selecting the nested control.

tests/cart.spec.ts
const product = page
  .getByRole('listitem')
  .filter({ hasText: 'Product 2' });

await product.getByRole('button', { name: 'Add to cart' }).click();

Use Web-first Assertions

Locator assertions retry until the expected state appears or the assertion timeout expires. Always await them.

tests/status.spec.ts
await expect(page.getByTestId('status')).toHaveText('Submitted');
await expect(page.getByRole('dialog')).toBeVisible();
await expect(page).toHaveURL(/\/account$/);

Avoid Immediate Value Checks

Reading textContent and immediately comparing it does not retry. Prefer toHaveText when the UI changes asynchronously.

tests/status.spec.ts
// Fragile: one immediate read.
expect(await page.getByTestId('status').textContent()).toBe('Ready');

// Reliable: retries until the expected state.
await expect(page.getByTestId('status')).toHaveText('Ready');

Debug Strictness and Timeouts

A strictness error means more than one element matched. A timeout means the expected actionable or asserted state did not arrive.

  1. Read the error's matching elements.
  2. Add a role name or scope to a parent region.
  3. Confirm the UI state in headed or UI mode.
  4. Fix the application or locator before increasing timeouts.

Avoid long CSS chains: DOM structure and generated class names change easily. Prefer user-facing attributes or an intentional test id.

Recap

  • Locators are live queries resolved before actions.
  • Prefer role, label, text, and explicit test-id contracts.
  • Web-first assertions retry and must be awaited.
  • Narrow ambiguous matches by scoping to meaningful UI regions.

Checkpoint: You have completed the workflow and have a repeatable reference for your next Playwright project.

Official Resources

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.