Locators

By Syncfusion®, Inc. · Last updated

What a locator is, why locators are lazy and strict, the built-in getBy locators (role, text, label, placeholder, alt text, title, test id), CSS and XPath, narrowing with chaining, filter, nth, and, or and within, choosing resilient locators from the accessibility tree, and naming locators with describe().

Locators

  • Before a test can click a button, it must find the button.
  • A locator tells Playwright how to find an element on the page.
  • Good locators find elements the way a user does: by role, label or text.
  • In this lesson, you will learn every kind of locator, and how to pick the best one.

Simple lesson: a locator is a description of how to find an element.

A Real-World Example

  • The Practice Shop lists products. Every card has an Add to cart button.
  • Priya wants to add the Yoga Mat, not just any product.
  • So her locator says: "the product card that contains Yoga Mat, then its Add to cart button".
  • baseURL is set in this project's config, so page.goto('/products') opens https://practice.devtutorials.ai/products.

What You Will Learn

  1. What a locator is, and why it is lazy and strict.
  2. The built-in getBy... locators.
  3. When to use CSS and XPath, and why they break easily.
  4. How to narrow a locator: chaining, filter(), nth(), and(), or() and within().
  5. How to choose a good locator.
  6. How to give a locator a readable name.

Locator Fundamentals

What Is a Locator?

  • page.getByRole('button', { name: 'Log in' }) is a locator.
  • It does not search the page when you write it. It is a recipe for finding the element later.
  • page.locator() is the general form. It takes a CSS or XPath selector: page.locator('#search').

Locators Are Lazy and Re-Evaluated

  • Lazy = Playwright searches only when you act or assert.
  • Re-evaluated = it searches again every time. If the page changes, the locator finds the new element.
  • So you can create a locator once and reuse it, even after the page updates.

Animation: a locator is written once. Each time the test uses it, Playwright searches the page again. The page grows from 6 cards to 8, and the second count finds 8

const cards = getByTestId('product-card')

count() → 6 count() → 8

Load more added 2 cards

The same locator, used twice. Each use searches the page as it is now.

Strictness and the Strict Mode Violation Error

  • Actions such as click() and fill() need exactly one element.
  • If a locator finds two or more, Playwright stops with a strict mode violation.
  • This protects you. Without it, the test might click the wrong button and still pass.
  • Run this test and read the error. Playwright even suggests better locators after aka.
  • Reading and counting are not strict: count() and toHaveCount() are happy with many elements.

Simple lesson: a locator searches when used, searches again every time, and must match one element for an action.

Built-in Locators

  • Playwright has seven getBy... methods. They find elements the way people see them.
LocatorFinds an element byExample
getByRole()Its role and accessible namegetByRole('button', { name: 'Log in' })
getByText()Its visible textgetByText('No products found')
getByLabel()Its form labelgetByLabel('Email')
getByPlaceholder()Its placeholdergetByPlaceholder('Search by name')
getByAltText()An image's alt textgetByAltText('Blue running shoes')
getByTitle()Its title attributegetByTitle('Yoga Mat details')
getByTestId()Its data-testidgetByTestId('cart-count')

getByRole()

  • Role = what an element is for: button, link, heading, checkbox, textbox and many more.
  • Accessible name = what a screen reader says for it: a button's text, a field's label, an image's alt text.
  • Options make it more exact:
OptionExampleMatches
name{ name: 'Log in' }The accessible name contains "Log in", any case
exact{ name: 'Log in', exact: true }The name is exactly "Log in"
checked{ checked: true }Ticked checkboxes and radios
expanded{ expanded: false }Closed menus and combo boxes
level{ level: 1 }<h1> headings
pressed{ pressed: true }Toggle buttons that are on
selected{ selected: true }Selected options and tabs

getByText(), getByLabel() and getByPlaceholder()

  • getByText('Yoga Mat') matches text that contains "Yoga Mat", ignoring case and extra spaces.
  • getByText('Yoga Mat', { exact: true }) needs the exact text.
  • getByLabel('Email') finds the input that the label "Email" belongs to. Use it for form fields.
  • getByPlaceholder() is a fallback for fields with no label.

A string in getByText(), getByLabel() or a role's name matches part of the text and ignores upper and lower case. Add exact: true for a whole, case-sensitive match. A regular expression gives you full control.

getByAltText(), getByTitle() and getByTestId()

  • getByAltText() finds images by their alt text.
  • getByTitle() finds elements by their title attribute (the tooltip text).
  • getByTestId('cart-count') finds data-testid="cart-count". Developers add these on purpose, just for tests.

Customizing the Test ID Attribute

  • Some teams use another attribute, such as data-qa or data-test.
  • Tell Playwright in the config, under use: testIdAttribute: 'data-qa'.
  • This lab's config is editable. Open playwright.config.ts and look at the use block.
  • You can also set it in code with selectors.setTestIdAttribute('data-qa'), imported from @playwright/test.

Simple lesson: prefer getByRole, then labels and text. Use test ids when nothing user-facing is stable.

CSS and XPath Locators

  • page.locator() takes a CSS selector ('#search', '.price') or an XPath ('//button[@type="submit"]').
  • Use them when no user-facing locator works, for example for a layout element with no role or text.
  • They are fragile: they depend on how the page is built, not on what the user sees.
  • A designer renames a class from price to amount, and the test breaks, though the page still works.
// Fragile: depends on page structure
await page.locator('div.row > div:nth-child(2) select').selectOption('name');

// Resilient: depends on what the user sees
await page.getByLabel('Sort by').selectOption('name');

Simple lesson: CSS and XPath work, but break when the page structure changes. Use them last.

Narrowing Locators

Chaining with locator.locator()

  • Call a locator method on another locator to search inside it.
  • page.getByTestId('product-card').getByRole('button') finds buttons inside cards.

filter() by Text and by Child Locator

FilterKeeps elements that
filter({ hasText: 'Yoga' })contain the text "Yoga"
filter({ hasNotText: 'Out of stock' })do not contain that text
filter({ has: page.getByRole('button', { name: 'Add to cart', disabled: true }) })contain a matching child
filter({ hasNot: page.getByText('Out of stock') })do not contain a matching child
filter({ visible: true }) or .visible()are visible

first(), last() and nth()

  • first(), last() and nth(2) pick by position. nth() counts from 0.
  • Use them only when the order is the point, for example "the first search result".
  • Position breaks easily when items are added or sorted.

and(), or() and within()

  • a.and(b) matches elements that match both locators.
  • a.or(b) matches elements that match either one. Useful when a page shows one of two things.
  • button.within(card) (Playwright 1.64) finds the button inside each element the container matches.

Simple lesson: chain to search inside, filter() to keep the right ones, and nth() only when order matters.

Choosing Good Locators

Locator Priority

A ladder of locators from most resilient to most fragile: role, label, text, test id, CSS and XPath. A marker climbs to the top

1. getByRole 2. getByLabel 3. getByText / getByPlaceholder / getByAltText 4. getByTestId 5. CSS and XPath

most resilient most fragile Start at the top. Move down only when the step above cannot find the element.

Prefer locators that describe what users see. Fall back to test ids, then CSS.

User-Facing vs Implementation Attributes

  • User-facing = what people see or hear: roles, labels, text. These change only when the product changes.
  • Implementation = how the page is built: ids, classes, element order. These change in every refactor.
  • A test built on user-facing locators breaks only when users would notice a change too.

The Accessibility Tree and Role-Based Locators

  • Browsers build an accessibility tree from the page: a simple list of roles and names that screen readers use.
  • getByRole() searches this tree. So a good getByRole() locator also proves the page is accessible.
  • If you cannot find a button by role, a screen reader user probably cannot either.

Try the Locator Picker

  • Click any element in the page below. The picker shows the locator Playwright would choose, and other options.
  • Type your own locator in Try a locator to see how many elements it matches.

Pick locator

If a locator matches more than one element, read the aka suggestions in the error. They are locators Playwright already checked for you.

Simple lesson: pick locators by what users see. They survive redesigns and help accessibility.

Readable Locators

Naming Locators with describe()

  • locator.describe('Add Yoga Mat button') gives a locator a human name.
  • It does not change what the locator finds.

How Descriptions Appear in Reports and Traces

  • The name appears in error messages, the HTML report and the Trace Viewer.
  • A long chain such as getByTestId('product-card').filter({ hasText: 'Yoga Mat' }).getByRole('button') becomes easy to read.
const addYogaMat = page
  .getByTestId('product-card')
  .filter({ hasText: 'Yoga Mat' })
  .getByRole('button', { name: 'Add to cart' })
  .describe('Add Yoga Mat button');

Store a locator in a const when you use it more than once. Because locators are lazy, the const stays correct as the page changes.

Simple lesson: describe() gives a long locator a short, readable name.

Try It in Your Project

  • These steps take the lesson from the lab to your own app.
  • Open your app in Codegen's locator picker and note which elements have no good role or label.
  • Ask your team to add a data-testid only where no role, label or text works.
  • Rewrite one old CSS or XPath locator in your suite with getByRole().

Simple lesson: use one idea from this lesson on your own app this week.

TL;DR

  • A locator is a recipe for finding elements. It searches each time you use it.
  • Actions need exactly one match. Many matches cause a strict mode violation.
  • Prefer getByRole, then getByLabel, then text, then getByTestId, then CSS or XPath.
  • Narrow with chaining, filter(), and(), or() and within(). Use nth() only when order matters.
  • testIdAttribute changes the attribute getByTestId() reads.

Common mistakes

MistakeWhat happensFix
Using .first() to silence a strict mode errorThe test clicks the wrong element when the order changesUse filter() to say which element you mean
Copying long CSS selectors from DevToolsTests break after small layout changesUse getByRole or getByLabel
getByText() for a buttonIt may match other text with the same wordsUse getByRole('button', { name })
Forgetting exact: true'Log in' also matches "Log in with Google"Add exact: true when names overlap
Putting a test id on everythingTests stop checking what users seeAdd test ids only where user-facing locators fail

Check Yourself

Exercises

What to Remember

  • Locators are lazy, re-evaluated and strict.
  • Prefer role, label and text over test ids, CSS and XPath.
  • Narrow with filter() and chaining. Avoid nth() unless order is the point.
  • Read the aka suggestions in strict mode errors.
  • describe() makes long locators readable.

Next: Actions and Auto-Waiting