Skip to contentPlayForge

What is the difference between page.locator() and getByRole() in Playwright?

Written by Jatin Thacker, Automation Architect · Updated

page.locator() takes a CSS or XPath selector, so it finds elements by how the page is built. getByRole() finds elements by their role and accessible name, the way a user or a screen reader perceives them, which is why Playwright recommends it first.

This answer is one piece of Chapter 9: Locators I: What the User Sees.

  • getByRole and the Accessible Name
  • getByText, getByLabel, getByPlaceholder, getByTestId
  • Strict Mode — One Locator, One Element

Both return a locator, and both wait and retry the same way when you act on them. The difference is what you describe. page.locator('.ui-a1b2 .ui-w3x4 > button') describes the markup: these wrapper classes, then a button directly inside. getByRole('button', { name: 'Reconcile ledger' }) describes the thing a user sees: a button called Reconcile ledger.

Playwright's locator guide recommends prioritising role locators because they are the closest way to how users and assistive technology perceive the page. It warns that CSS and XPath can be tied to the DOM structure and break when that structure changes.

The example uses the legacy nesting block on the embeds page, where twelve wrapper divs with build-generated class names surround one button. Both locators find it today. If a developer removes the outer wrapper, or the build renames ui-w3x4, the CSS chain finds nothing, while the role locator still finds the button, because its role and its name have not changed.

Keep page.locator() for the cases the built-in locators cannot express, such as a stable data- attribute.

qa-041.spec.js(https://practice.learnplayforge.com)
import { test, expect } from '@playwright/test'; test('a CSS chain and a role locator reach the same button', async ({ page }) => { await page.goto('/embeds.html?mock=static&seed=42'); // page.locator() takes a selector: here, a path through two wrapper classes const byStructure = page.locator('.ui-a1b2 .ui-w3x4 > button'); await expect(byStructure).toHaveText('Reconcile ledger'); // getByRole() describes what the user sees: a button with this name const byRole = page.getByRole('button', { name: 'Reconcile ledger' }); await expect(byRole).toBeVisible(); });
Output
1 passed (2.0s)
Test yourself
The next build renames the class ui-w3x4 on one of the wrappers around the Reconcile ledger button. Which locator still finds the button?

Practised in the full course: the drill Twelve divs deep.

See the chapter

AI can write the test now. The job is knowing whether it checks anything. That's what the course teaches, one chapter at a time.

163 lessons · 226 drills · start free

← All questions

Training a team? Talk to us.