Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Playwright›Working with selectors and locators

Core Playwright Concepts

Working with selectors and locators

Locators represent a central concept in Playwright, serving as the abstraction for finding and interacting with elements on a webpage at any given moment. They are essential because they employ built-in auto-waiting and retry-ability, ensuring that tests remain resilient against the asynchronous nature of modern web applications. You should reach for locators whenever you need to simulate user actions, such as clicking buttons or inputting text, rather than relying on brittle manual element queries.

Understanding the Locator Philosophy

A locator is not an element itself, but a strategy for finding one. Unlike traditional approaches where you query the DOM and receive an object immediately, a Playwright locator is a lazy representation of the query. This distinction is critical because it allows Playwright to defer execution until the very moment an action is performed. When you call an action like .click() on a locator, Playwright automatically performs a series of internal checks. It verifies that the element is attached to the DOM, visible, stable (not animating), and capable of receiving events. This mechanism is the bedrock of test stability, as it eliminates the need for manual sleeps or arbitrary wait times. By defining the 'how' and 'where' upfront without executing the search until necessary, your test suite becomes significantly more robust against dynamic content updates and slow network responses, ensuring that the test fails only when the application is truly broken rather than just slow.

// Locators are lazy: this line does not find the element yet.
const submitButton = page.locator('button[type="submit"]');

// The search and checks only happen when the action is called.
await submitButton.click();

User-Facing Locators

The most effective way to locate elements is by using user-facing attributes, which reflect how a human interacts with the page. Playwright provides built-in methods like getByRole, getByLabel, and getByText to prioritize accessibility and semantic structure over brittle CSS selectors or XPath expressions. When you use getByRole, for example, Playwright searches the accessibility tree for elements matching specific roles like 'button' or 'textbox', combined with their accessible name. This approach is superior because it inherently tests the accessibility of your application while simultaneously decoupling your tests from implementation details like specific IDs or classes that frequently change during refactoring. If your developer changes a button from a <div> to a <button> element, a user-facing locator will likely continue to work without modification, whereas a CSS selector based on class names would inevitably break, forcing costly maintenance cycles. Prioritizing these locators ensures your tests behave like real users.

// Preferred: targets elements based on their accessibility role.
const loginButton = page.getByRole('button', { name: /log in/i });

// Semantic: targets inputs by their associated label text.
const emailField = page.getByLabel('Email Address');

await emailField.fill('test@example.com');
await loginButton.click();

Combining and Refining Locators

Often, a single attribute is insufficient to uniquely identify an element on a complex page. Playwright allows you to refine your locators by chaining them, effectively narrowing down the search scope. By using .locator() on an existing locator, you create a parent-child relationship that confines the search for the child element to within the DOM subtree of the parent. This technique is extremely powerful for handling forms, navigation bars, or lists where multiple similar elements exist. For instance, if you have multiple 'Edit' buttons on a page, you can create a locator for a specific row in a data table and then chain a locator for the button inside that row. This ensures the test remains precise, avoiding accidental interaction with the wrong element. Mastering this chaining pattern allows you to build highly specific queries that remain readable and maintainable, reducing the likelihood of test flakiness caused by ambiguous or duplicate selectors across your application interface.

// Chain locators to narrow down the search area.
const productRow = page.locator('.product-item').filter({ hasText: 'Blue Widget' });

// This only clicks the 'Buy' button inside the specific product row.
await productRow.getByRole('button', { name: 'Buy' }).click();

Handling Lists and Multiple Elements

When a locator matches multiple elements, Playwright provides methods to interact with them as a collection. Using .first(), .last(), or .nth() allows you to select specific instances within a list without needing a complex loop. This is particularly useful for handling tables, dropdowns, or repeating components in a dynamic dashboard. It is important to remember that these methods still maintain the underlying auto-waiting behavior. If you are uncertain about the index of an element, you can also use .all() to resolve the current list into an array for more complex logic. However, you should generally prefer index-based selection or filtering by text to keep your tests declarative. By treating elements as ordered sets, you gain the ability to write expressive tests that handle dynamic lists efficiently, ensuring that even if the order of elements changes or a new item is inserted, your test logic remains sound by targeting specific indices or identifiable contents.

// Select the second item in a list of navigation links.
const navLink = page.locator('nav ul li').nth(1);

// Perform actions on all elements in a matched collection.
const allInputs = page.locator('input[type="checkbox"]');
await allInputs.first().check();

Using Filter for Precise Targeting

The .filter() method is a powerful utility that allows you to refine a locator based on its internal content or properties, such as containing specific text or another locator. This is significantly more readable and robust than writing complicated CSS pseudo-selectors to traverse the DOM tree. By filtering a list of elements based on a required child element, you can effectively locate complex components that lack unique identifiers. For example, if you have a card component that only contains a specific icon, you can filter the card list by that icon's presence. This capability enables you to write highly specific tests that verify structural integrity without needing to know the exact HTML implementation details. When you combine filters with role-based locators, you create a resilient suite that remains functional even as the internal structure of your components evolves, shifting the burden of stability from the test code to the clear intent of the user interactions.

// Filter a list of articles to find one containing a specific heading.
const article = page.locator('article').filter({ hasText: 'Breaking News' });

// Further restrict the selection to an article that contains a specific link.
const specificArticle = article.filter({ has: page.getByRole('link', { name: 'Read more' }) });

await specificArticle.click();

Key points

  • Locators are lazy definitions of how to find an element, ensuring actions wait until the element is ready.
  • Playwright automatically performs stability checks like visibility and hit-testing before every action.
  • Always prefer user-facing locators like getByRole or getByLabel to mimic real user behavior.
  • Chaining locators allows you to scope searches to specific parts of the DOM, increasing precision.
  • Using .nth() helps interact with specific elements in lists without relying on brittle structure.
  • The .filter() method provides a clean way to narrow down elements based on their internal children or text.
  • Decoupling your tests from implementation details like CSS classes makes them significantly easier to maintain.
  • Always ensure that your locators are unique enough to avoid accidental interaction with unintended elements.

Common mistakes

  • Mistake: Relying on auto-generated CSS selectors like div > div > span. Why it's wrong: These are fragile and break whenever the UI structure changes. Fix: Use user-facing locators like getByRole or getByText which mimic how users interact with the page.
  • Mistake: Overusing XPath because it's familiar from older tools. Why it's wrong: XPath is slow and often verbose, making tests hard to maintain. Fix: Prefer Playwright's built-in locator engines like getByLabel or getByTestId for better performance and readability.
  • Mistake: Chaining locators without understanding the scope. Why it's wrong: Chaining with the '>' combinator or multiple .locator() calls can lead to ambiguous matches. Fix: Define the scope clearly using locators as the base, such as 'const row = page.getByRole('row').filter({ hasText: 'Item' })'.
  • Mistake: Using strictly the test ID as the only locator strategy. Why it's wrong: While reliable, it ignores accessibility issues that user-facing locators would otherwise catch. Fix: Use getByRole first to ensure accessibility, and fallback to getByTestId only when an element lacks semantic meaning.
  • Mistake: Neglecting to wait for elements using assertions. Why it's wrong: Manually checking visibility or presence leads to race conditions and flaky tests. Fix: Use web-first assertions like expect(locator).toBeVisible(), which handle retries automatically.

Interview questions

What is the basic syntax for locating an element in Playwright, and why is it preferred over manual XPath?

In Playwright, the primary method for locating elements is the `page.locator()` method. You pass a selector string, such as a CSS selector or a user-facing locator like `getByRole`, to this method. This is preferred over manual XPath because Playwright locators are designed to be resilient. They automatically wait for the element to be actionable, such as visible and stable, before interacting. Manual XPath is often brittle, breaking when the DOM structure changes slightly, whereas Playwright locators focus on how a user perceives the page.

Can you explain the difference between 'getByRole' and 'getByTestId', and when you should prioritize one over the other?

The `getByRole` locator is the recommended approach because it matches the accessibility tree, ensuring your tests verify that your application is usable by everyone, including those using screen readers. It checks for ARIA roles and accessible names. In contrast, `getByTestId` is used for internal testing purposes by adding 'data-testid' attributes to elements. You should prioritize `getByRole` to enforce good accessibility practices, but use `getByTestId` when an element lacks a unique role or name, or when the content is highly dynamic and prone to frequent text changes.

How do you handle locators that need to be scoped to a specific area of the page, such as a modal or a sidebar?

To scope locators in Playwright, you can chain them by creating a locator for the parent container first and then using that object to find child elements. For example, you might define `const modal = page.getByTestId('login-modal');` and then perform actions like `modal.getByRole('button', { name: 'Submit' }).click();`. This approach is powerful because it enforces structural scoping, ensuring your tests only interact with the intended component, which prevents false positives if multiple identical elements exist elsewhere on the page.

Compare the use of 'getByText' versus 'getByRole' when selecting buttons or links on a page.

Using `getByRole` is significantly superior to `getByText` because `getByRole` validates the semantic nature of the element. If you use `getByRole('button', { name: 'Submit' })`, Playwright confirms that the element is actually a button tag or has a button role. `getByText` merely looks for a string pattern and could inadvertently match a paragraph or a span that happens to contain that text. `getByRole` ensures your test fails if the developer accidentally changes a button into a div, which protects the functional integrity of your interface.

How can you use CSS selectors in Playwright when user-facing locators like 'getByRole' are insufficient?

When standard locators are insufficient, you can provide a CSS selector directly into the `page.locator()` method. For example, you might use `page.locator('div.card > .active-item')`. While CSS selectors allow for precise DOM traversal based on class or attribute hierarchies, they should be a secondary choice. They are less resilient to UI refactors compared to semantic locators. Use them sparingly for highly specific custom components or legacy elements that do not provide standard accessibility attributes or stable IDs.

How does Playwright's 'locators' approach handle dynamic content and state changes during execution?

Playwright locators are inherently lazy, meaning they do not search for the element until an action is performed. This is a critical feature for handling dynamic content. When you call a method like `.click()`, Playwright performs a series of 'actionability' checks, including verifying that the element is attached to the DOM, visible, and stable. If the element is currently loading or undergoing a transition, Playwright will automatically poll the DOM until the condition is met or the timeout expires, effectively eliminating the need for manual 'sleep' commands.

All Playwright interview questions →

Check yourself

1. When testing a login form, which locator strategy is most resilient to design changes?

  • A.A CSS selector targeting the class name of the input field
  • B.An XPath expression pointing to the third input on the page
  • C.A getByLabel locator targeting the label 'Username'
  • D.Selecting the element by its computed pixel coordinates
Show answer

C. A getByLabel locator targeting the label 'Username'
getByLabel is best because it relies on the accessible name, which stays consistent even if the CSS class or DOM hierarchy changes. CSS and XPath are fragile, and coordinates are highly prone to breaking across screen resolutions.

2. What happens when you use multiple locator chains, such as 'page.locator('.sidebar').locator('button')'?

  • A.Playwright throws an error because locators cannot be chained
  • B.It limits the scope of the second locator to descendants of the first
  • C.It forces the browser to find all buttons first, then filter by sidebar
  • D.It creates a race condition between the two elements
Show answer

B. It limits the scope of the second locator to descendants of the first
Chaining locators narrows the search context to the sub-tree defined by the first locator. The other options are incorrect because chaining is a core feature, it is efficient, and it does not cause race conditions.

3. Why is 'getByRole' preferred over other locator methods?

  • A.It is the only method that supports regular expressions
  • B.It is the fastest method to execute in the browser engine
  • C.It encourages accessible web design while providing stable test locators
  • D.It bypasses the need for the browser to render the page
Show answer

C. It encourages accessible web design while providing stable test locators
getByRole forces developers to consider accessibility (ARIA roles). It is stable because roles rarely change. The other options are false; other methods support regex, it is not inherently faster than others, and it definitely requires the page to be rendered.

4. If an element is located inside a shadow DOM, how does Playwright handle it?

  • A.You must manually switch the context to the shadow root
  • B.Playwright locators penetrate shadow roots by default
  • C.You must use a special XPath extension to reach it
  • D.Shadow DOM elements are invisible to Playwright locators
Show answer

B. Playwright locators penetrate shadow roots by default
Playwright's locators transparently cross shadow DOM boundaries, so no extra configuration is required. The other options suggest manual intervention or limitations that do not exist in Playwright.

5. What is the primary benefit of using 'filter' on a locator?

  • A.It increases the speed of the browser's JavaScript execution
  • B.It allows you to narrow down a large list of elements based on child content
  • C.It forces the element to become visible before the test proceeds
  • D.It replaces the need for writing custom assertion logic
Show answer

B. It allows you to narrow down a large list of elements based on child content
Filter is used to refine a locator when multiple elements match (e.g., finding a specific row in a table by its text). It does not affect browser speed, force visibility, or replace assertions.

Take the full Playwright quiz →

← PreviousGenerating and viewing test reportsNext →Handling different types of elements (buttons, inputs, dropdowns)

Playwright

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app