In test automation, locators allow test scripts to interact with elements on a page, mimicking real user actions. Choosing the right locators can make tests more robust, reliable, and less prone to breaking with minor UI changes. Poor locator strategies can lead to flaky tests that fail for the wrong reasons, wasting time and resources. By choosing the right locator strategy you can create tests that are stable, easy to understand, and closely aligned with how users interact with your application, ultimately improving test effectiveness and maintainability.
Playwright comes with a very wide and flexible way of locators, and in this blog post we will dive deep into how to define resilient locators and I will propose a locator strategy that I personally use on my projects.
Core Playwright locators
There are several core Playwright locators and for completeness in the blog, the code snippet bellow contains the playwright recommended built-in locators, which you can also find on their documentation page.
From my personal experience, the ones that I use the most are getByRole() and getByText(). Roles are beneficial because they closely align with how screen readers interpret a page, creating accessibility-friendly tests. Roles make it easier to locate common elements (like buttons and headings) without relying on brittle selectors, which can change frequently. On the other hand, locating by text content is highly readable, as it’s immediately clear what element you’re targeting. It’s also robust, as it targets user-facing content rather than specific HTML structures.
await page.getByRole('button', { name: 'Submit' }).click()
await page.getByRole('heading', { name: 'Welcome' }).click()
await page.getByText('Sign In').click()
await page.getByLabel('Username').fill('John Doe')
await page.getByPlaceholder('Enter your email').fill('user@example.com')
await page.getByAltText('Company logo').isVisible()
await page.getByTitle('Delete item').click()
await page.getByTestId('submit-button').click()Traditional Playwright locators
Traditional locators, like CSS classes, IDs, and xPath, are powerful and flexible, but they may introduce test instability and flakiness since they rely heavily on the DOM structure, which changes frequently. I wouldn’t recommend using them if not necessary, but they can come quite handy in cases where you need to locate elements in complex layouts, temporary or code generated layouts, third party layouts that you don’t have control on, and when you want to access styles where roles are not defined properly. Again, more details can be found in the official documentation page.
await page.locator('.submit-button') // by class
await page.locator('#login-form') // by ID
await page.locator('button[type="submit"]') // by attribute
await page.locator('div > button') // by hierarchy
await page.locator('//button') // basic XPath
await page.locator('//button[contains(@class, "submit")]') // with condition
await page.locator('//div[@id="main"]//button') // nested elements
await page.locator('text=Submit') // by text content
await page.locator(':has-text("Submit")') // CSS with text</code></pre>User centric locator strategy
Playwright encourages using locators that reflect how users interact with your application. This user-centric approach not only makes tests more stable but also promotes accessible UI design. When selecting a locator strategy, prioritize role-based and text-based locators over traditional testID, CSS or xPath locators.
Why it is important to use user-centric locators? [Example]
I will use a short example to point out why you should really consider user-centric locators. Imagine simple test that validates a login form, you have two input fields for username/password and a login button. Here is a snippet on how would that code look like if we were using data-testids to locate the elements:
await page.getByTestId('username-input').fill('John Doe')
await page.getByTestId('password-input').fill('secure_password')
await page.getByTestId('login-button').click()Can you think of potential problems with this way of locating the elements?
I can definitely think of few, but the main one, is again related to how the user is interacting with the application under test. Imagine the login text is miss-spelled, or not translated properly based on your translation files. What will your test do?
It will pass, even though your user might see just random text for the login button, because your locators are not considering it at all. Same applies if there was a CSS class or basic xPath. Yes, you can always have expect that will validate the text, but why having extra lines of code when it can be part of how you write your tests.
Consider the example below, just by replacing the data-testids, with getByRole locators, we will do complete user-centric validation.
await page.getByRole('textbox', { name: 'Username' }).fill('John Doe')
await page.getByRole('textbox', { name: 'Password' }).fill('secure_password')
await page.getByRole('button', { name: 'Log In' }).click()The true power of the user-centric locator strategy lies in its alignment with real user behavior. Our users don’t see IDs, testIds, or CSS classes - they interact with our application by clicking buttons with text they can see, filling forms based on visible labels, and finding elements by their meaningful text. This approach serves a dual purpose: it ensures our tests are in line with the UI changes while simultaneously validating that we test from the user perspective. This will also remove the need for developers or SDETs to add test-ids on their own, which often becomes a nightmare to maintain and a frequent source of friction that leads to pushback and heated discussions with the development team.
Remember, the best test automation strategy isn’t about finding elements in the DOM, it’s about replicating and verifying the genuine user journey.
Best way to locate accessible elements
- Navigate to Chrome Dev Tools by right-click and Inspect on your page
- Select the Accessibility tab on the right
- Enable full-page accessibility tree
- Reload DevTools
- Inspect any element on the page
Result: You will be able to see the full accessibility tree, the role of the element and the content that can be used to locate the element.
See the video below for more details:
Going step further in your strategy
Don’t get me wrong, I am not saying that you should be using roles, and only roles, and forget about all other locators, they have their own use as well. All I am saying is that we should focus on the user interactions when writing UI automation and be careful about how we locate elements.
The proposed user-centric locator strategy integrates well with the popular Page Object Model (POM) pattern. There are various “flavors” of POM, and I generally structure it with pages and components, or page functions, where components are subsets of the pages. Diving into the specifics of my implementation could fill an entire new blog post, but focusing on locators here, I typically set up what I call a parent locator for each component. This parent locator defines the component boundaries and allows me to narrow the scope of locators within the component. Parent locators can utilize roles, test data IDs, or even xPath (rarely), as their main purpose is to introduce separation between components. This setup enables you to leverage all combinations of locators that Playwright offers.
const parentContainer = page.getByTestId('address-component')
const firstNameInput = parentContainer.getByRole('textbox', {
name: 'First name'
})
const lastNameInput = parentContainer.getByRole('textbox', {
name: 'Last name'
})Avoiding common pitfalls
Setting the right locator strategy for your test automation is often challenging to get right the first time. Here are some common mistakes teams make when implementing Playwright locators and how to avoid them.
Using locators inspired by old-school Selenium patterns
It’s not uncommon for teams transitioning from Selenium to bring over old habits like, relying heavily on CSS selectors or xPath locators. While Playwright supports these, they can lead to flaky tests. Examples include:
- Deeply nested CSS selectors that can break with minor DOM changes.
div.container > div.row > button.primary- Overly complex xPath expressions that adds unnecessary complexity compared to accessible locators:
//div[@class='container']/div[@class='row']/div/button[text()='Submit']Overusing getByTestId
While getByTestId is excellent for targeting specific elements, overusing it can reduce the accessibility and readability of your tests. Reserve getByTestId for cases where semantic or accessible attributes are unavailable, such as uniquely identifying dynamic or non-interactive elements.
Failing to narrow locator scopes
Not scoping locators properly can lead to ambiguous matches, especially when there are multiple similar elements on a page. Using parent containers or composite locators (e.g., chaining getByRole with locator) can help mitigate this.
Assuming a “one-size-fits-all” approach
There are always exceptions to recommended strategies. For instance:
- If a component lacks accessible attributes, a robust CSS or xPath locator might be necessary.
- Some legacy systems may require creative locator strategies due to limitations in the HTML structure.
Overlooking debugging features
Playwright’s debugging tools (e.g., Playwright Inspector and locator suggestions) are underutilized. These can greatly assist in choosing the best locator strategy for a given element.
Neglecting cross-browser consistency
A locator that works in one browser may not behave identically in another if it depends on browser-specific quirks. Always validate your locators across all targeted browsers.
Conclusion
Creating a robust locator strategy requires true collaboration between SDETs and developers, with test engineers taking the lead in educating teams about the benefits of user-centric approaches. By participating in code reviews and embracing a mindset of continuous improvement through failures, teams can develop more stable automation that accurately reflects real user interactions. This approach naturally promotes accessibility best practices, as role-based locators inherently validate that your application works well with assistive technologies.
From my experience, prioritizing user-centric locators significantly reduces test flakiness and improves stability - common problems that hurt many automation projects and damage team trust. In the end, investing time in a thoughtful locator strategy pays dividends in more maintainable tests, faster debugging, and a testing approach that truly verifies what matters most - the actual user experience.