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".
baseURLis set in this project's config, sopage.goto('/products')openshttps://practice.devtutorials.ai/products.
What You Will Learn
- What a locator is, and why it is lazy and strict.
- The built-in
getBy...locators. - When to use CSS and XPath, and why they break easily.
- How to narrow a locator: chaining,
filter(),nth(),and(),or()andwithin(). - How to choose a good locator.
- 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()andfill()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()andtoHaveCount()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.
| Locator | Finds an element by | Example |
|---|---|---|
getByRole() | Its role and accessible name | getByRole('button', { name: 'Log in' }) |
getByText() | Its visible text | getByText('No products found') |
getByLabel() | Its form label | getByLabel('Email') |
getByPlaceholder() | Its placeholder | getByPlaceholder('Search by name') |
getByAltText() | An image's alt text | getByAltText('Blue running shoes') |
getByTitle() | Its title attribute | getByTitle('Yoga Mat details') |
getByTestId() | Its data-testid | getByTestId('cart-count') |
getByRole()
- Role = what an element is for:
button,link,heading,checkbox,textboxand 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:
| Option | Example | Matches |
|---|---|---|
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 theiralttext.getByTitle()finds elements by theirtitleattribute (the tooltip text).getByTestId('cart-count')findsdata-testid="cart-count". Developers add these on purpose, just for tests.
Customizing the Test ID Attribute
- Some teams use another attribute, such as
data-qaordata-test. - Tell Playwright in the config, under
use:testIdAttribute: 'data-qa'. - This lab's config is editable. Open
playwright.config.tsand look at theuseblock.
- 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
pricetoamount, 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
| Filter | Keeps 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()andnth(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 goodgetByRole()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-testidonly 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, thengetByLabel, then text, thengetByTestId, then CSS or XPath. - Narrow with chaining,
filter(),and(),or()andwithin(). Usenth()only when order matters. testIdAttributechanges the attributegetByTestId()reads.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Using .first() to silence a strict mode error | The test clicks the wrong element when the order changes | Use filter() to say which element you mean |
| Copying long CSS selectors from DevTools | Tests break after small layout changes | Use getByRole or getByLabel |
getByText() for a button | It may match other text with the same words | Use 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 everything | Tests stop checking what users see | Add 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. Avoidnth()unless order is the point. - Read the aka suggestions in strict mode errors.
describe()makes long locators readable.
Next: Actions and Auto-Waiting