No history yet

Advanced Selectors

Better Selectors for Sturdier Tests

You already know how to find elements using basic selectors. But relying on complex CSS paths or the exact structure of the HTML can lead to fragile tests. A small change in the user interface, like wrapping a button in a <div>, could break your test script even if the functionality remains the same.

To build more resilient tests, we should select elements the way a user would. Users don't see CSS classes or XPath; they see text, labels, and buttons. Playwright encourages this by prioritizing selectors that are tied to accessibility and user visibility.

Tests that rely on user-facing attributes are less likely to break when the implementation details of the UI change.

This approach has a double benefit. It makes your tests more stable, and it encourages you to build more accessible websites. Let's look at the best locators to use.

Find Elements by Role

The most powerful locator is getByRole(). It finds elements based on their ARIA role, which describes what an element is or does. For example, a link has the role link, and a button has the role button. These roles are fundamental to how screen readers and other assistive technologies understand a web page.

// Finds the element with the role of 'button' and the name 'Sign in'
await page.getByRole('button', { name: 'Sign in' }).click();

// Finds the element with the role of 'checkbox' and the name 'Remember me'
await page.getByRole('checkbox', { name: 'Remember me' }).check();

// Finds a heading with the text 'Welcome back!'
const heading = page.getByRole('heading', { name: 'Welcome back!' });

Using roles makes your tests incredibly readable and resistant to change. The developer can change the underlying tag from <button> to <a role="button"> and your test will still pass, because the element's purpose for the user hasn't changed.

Using Labels and Test IDs

What if an element doesn't have a clear role? The next best options are getByLabelText() and getByTestId().

getByLabelText() is perfect for form fields. It finds an <input> element associated with a <label> tag. This is exactly how a user would identify where to type their information.

// Finds the input associated with the label 'Password'
await page.getByLabelText('Password').fill('s3cr3t');

For elements that have no user-visible text or role, the last resort is a test ID. You can add a data-testid attribute to your HTML specifically for testing purposes. This creates a stable hook that is completely independent of styling or element structure.

<!-- In your application's HTML -->
<div data-testid="main-content">...</div>
// In your Playwright test
const mainContent = page.getByTestId('main-content');

✅ Minimize test ID usage – prefer semantic selectors first

While getByTestId() is reliable, it's best used sparingly. Overusing it can lead to tests that don't reflect the true user experience. Always try to use accessibility-first locators like getByRole() and getByLabelText() before reaching for a test ID.

Chaining and Filtering

Sometimes a single locator isn't specific enough. Playwright allows you to chain locators to narrow down your search within a specific part of the page.

Imagine you have a product card with its own "Add to Cart" button. To avoid clicking the button for the wrong product, you can first locate the card, then find the button inside that card.

// Locate the list item containing 'Product A'
const productA = page.getByRole('listitem', { name: 'Product A' });

// Now, find the 'Add to Cart' button only within that list item
await productA.getByRole('button', { name: 'Add to Cart' }).click();

You can also use filters to refine your selection. The filter() method lets you select from a set of elements based on what they contain or their text.

For example, if you have a list of users and you want to find the one named "Alice" that also has a "View Profile" link:

// Get all list items
const users = page.getByRole('listitem');

// Filter for the one that has text 'Alice' AND a 'View Profile' link
const aliceProfile = users.filter({ hasText: 'Alice' })
                          .filter({ has: page.getByRole('link', { name: 'View Profile' }) });

await aliceProfile.click();

Chaining and filtering give you precise control, allowing you to create selectors that are both highly specific and incredibly resilient to UI changes. By adopting these advanced strategies, your tests will become more reliable and easier to maintain.

Quiz Questions 1/6

Why can relying on complex CSS paths or the exact HTML structure lead to fragile tests?

Quiz Questions 2/6

What is the primary advantage of using Playwright's accessibility-first locators like getByRole()?