keepcalculator

06 Numbers and Powers

Playwright Locators: Stable Tests Guide

Card No. 06·68065 · Updated · Standard formula, shown below

Reusable Playwright locators are a practical way to build stable tests by centralizing how you find elements, reducing duplication, and making updates easier when your UI changes. This guide explains what reusable locators are, how to structure them, and how to avoid common pitfalls that lead to flaky tests.

Why Reusable Locators Matter

In Playwright, a locator is an object that points to one or more elements on a page. If you write the same locator in every test, a small change to the DOM (like adding a wrapper div or changing a class name) forces you to edit every test. Reusable locators solve this by defining the locator once and importing it where needed.

This is especially useful for large applications with many tests. Instead of scattering page.locator('.submit-btn') across 50 files, you define export const submitButton = page.locator('.submit-btn') in a single module. When the class changes, you update one place.

How to Structure Reusable Locators

There are two common patterns: page object models and separate locator modules. Both work; choose based on your team’s preference.

  • Page Object Model (POM) – Each page or component has a class that exposes locators and actions. For example, a LoginPage class might have usernameInput, passwordInput, and submitButton as properties.
  • Locator module – A plain file that exports locator constants, often grouped by feature. This is simpler and works well for small projects.

Example of a locator module:

// locators/login.js
const login = (page) => ({
  username: page.getByLabel('Username'),
  password: page.getByLabel('Password'),
  submit: page.getByRole('button', { name: 'Sign in' }),
});
export default login;

Then in a test:

import login from './locators/login';
const loginPage = login(page);
await loginPage.username.fill('testuser');

Choosing the Right Locator Strategy

Playwright offers several ways to locate elements. The most stable are user-facing attributes: getByRole, getByLabel, getByText, and getByPlaceholder. These mimic how a real user sees the page and are less likely to break when CSS classes change.

  • getByRole – best for buttons, links, and headings.
  • getByLabel – ideal for form inputs with associated <label>.
  • getByText – useful for non-interactive text elements.
  • CSS or XPath – fallback when user-facing selectors aren’t available, but they’re more brittle.

Avoid locators that rely on deeply nested CSS paths or positional indexes (like div > div > span:nth-child(2)). They break on minor layout changes.

Common Problems and How to Avoid Them

Even with reusable locators, tests can become flaky. Here are the most frequent issues:

  • Overly strict locators – Using page.locator('button') when there are many buttons. Always narrow with getByRole and a name.
  • Not using strict mode – Playwright throws if a locator matches multiple elements. Use .first() or .nth() only when you’re sure, or better, make the locator more specific.
  • Ignoring iframes – If your element is inside an iframe, you must chain frameLocator first. Reusable locators should include that context.
  • Hard-coding waits – Instead of page.waitForTimeout(2000), use Playwright’s auto-waiting. Locators automatically wait for the element to be actionable.

A common misconception is that reusable locators are only for large teams. Even a solo developer benefits from centralizing locators, because it makes refactoring safer and tests easier to read.

Comparing with Other Approaches

Some teams use CSS custom attributes (like data-testid) to make locators even more stable. Playwright supports this via page.getByTestId('submit'). The trade-off is that you add extra attributes to your markup, which some developers see as clutter. However, they decouple tests from both visual and structural changes.

Another alternative is writing locators directly in each test without reuse. That’s faster to start but becomes a maintenance nightmare as the suite grows. Reusable locators sit between raw locators and full page objects—they give you structure without the overhead of a full POM.

Costs and Trade-offs

The main cost of reusable locators is upfront design time. You need to decide where to put them and how to name them. There’s also a risk of over-engineering: creating a locator for every tiny element can make your codebase harder to navigate.

But the long-term benefit is reduced maintenance. When the UI changes, you fix one file instead of dozens. This saves time and reduces the chance of missing a spot.

Alternatives to Reusable Locators

If you don’t want to create your own locator modules, consider using Playwright’s built-in test.step to organize actions, or use a third-party library like @playwright/test’s fixtures to inject locators. Some teams also use visual regression testing to catch UI changes, which complements locator-based testing.

For very small projects, you might skip reusable locators entirely and write locators inline. That’s fine if you have fewer than a dozen tests and the UI is stable.

Frequently Asked Questions

What is the best way to store reusable locators?

There’s no single best way. Page object models are popular for large apps, while simple locator modules work well for smaller ones. The key is consistency—pick one pattern and stick to it.

Should I use data-testid attributes?

They are a good practice if you want to isolate tests from styling and structure. However, they add extra attributes to your HTML. Use them sparingly, mainly for interactive elements that lack accessible names.

Why does my locator match multiple elements?

Playwright’s strict mode throws an error when a locator resolves to more than one element. To fix it, make the locator more specific—for example, add a name to getByRole or use filter().

Can reusable locators slow down my tests?

No. Locators are just descriptions; they don’t perform any action until you call a method like click() or fill(). The performance impact is negligible.

How do I handle dynamic elements?

Use locators that target stable attributes, and rely on Playwright’s auto-waiting. Avoid locators that depend on dynamic class names or indexes. If an element appears only after an action, use expect(locator).toBeVisible() to wait.

Is it okay to mix CSS and user-facing locators?

Yes, but prefer user-facing ones when possible. If you must use CSS, keep it simple and avoid deep nesting. Reusable locators make it easy to swap the strategy later without touching every test.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →