2

🔎 Mastering Playwright Locators: Your Guide to Finding Elements

Learn how to reliably find elements in Playwright using recommended locators like getByRole, getByText, getByLabel, and best practices for CSS/XPath selectors.

Introduction

In test automation, reliably finding and interacting with elements on a web page is fundamental. Playwright, a powerful browser automation library, offers a robust set of locators to make this process efficient and resilient. This guide will walk you through the different types of Playwright locators, best practices for using them, and how to choose the right one for the job.

What are Locators in Playwright?

Locators are the heart of Playwright's element selection strategy. They represent a way to find element(s) on the page at any moment. Unlike traditional approaches where you query for an element immediately, a Playwright Locator object captures the logic of how to find an element. The actual search for the element happens when you perform an action on the locator (like click(), fill(), textContent(), etc.). This makes locators more resilient to timing issues in dynamic web applications.

Why Choosing Good Locators Matters

  • Readability: Well-chosen locators make your tests easier to understand and maintain.
  • Resilience: Good locators are less likely to break when the UI structure changes.
  • Speed: While Playwright is fast, overly complex locators can sometimes be slower.
  • Accuracy: Ensures your tests interact with the intended elements.

Playwright prioritizes user-facing attributes and roles for locating elements, as these are less prone to change with code refactoring.

1. page.getByRole()

This is often the preferred way to locate elements. It targets elements based on their ARIA role, which describes the element's purpose to assistive technologies (and to your tests!).

  • How it works: Locates elements by role (e.g., button, link, textbox, heading) and optional attributes like name (accessible name/label) or level (for headings).
  • Example (TypeScript):
    // Click a button with the name "Submit"
    await page.getByRole('button', { name: 'Submit' }).click();
     
    // Get a heading with the name "Welcome" and level 1
    const mainHeading = page.getByRole('heading', { name: 'Welcome', level: 1 });
    await expect(mainHeading).toBeVisible();
     
    // Find a checkbox
    await page.getByRole('checkbox', { name: 'I agree to the terms' }).check();
  • Pros: Most resilient to DOM changes, aligns with accessibility best practices, very readable.
  • Cons: Relies on developers implementing correct ARIA roles (though many HTML elements have implicit roles).

2. page.getByText()

Locates elements based on the text content they contain.

  • How it works: Finds an element that contains the specified text. By default, it looks for an exact match. For substring matching, you can use a regular expression or set the exact: false option.
  • Example (TypeScript):
    // Click an element containing the exact text "Login" (default behavior)
    await page.getByText('Login').click();
     
    // Find an element containing "Product Details" (explicit substring match)
    const productInfo = page.getByText('Product Details', { exact: false }); 
    await expect(productInfo).toBeVisible();
     
    // Find by regex
    await page.getByText(/User Agreement/i).click(); // case-insensitive
  • Pros: Intuitive for elements defined by their text (buttons, links, paragraphs).
  • Cons: Can be brittle if text changes frequently (e.g., due to internationalization or A/B testing). Use exact: true when possible for more precision.

3. page.getByLabel()

Locates form control elements by their associated label text.

  • How it works: Finds an input field, textarea, select, etc., connected to a <label> element containing the specified text.
  • Example (TypeScript):
    // Fill an input field labeled "Email"
    await page.getByLabel('Email').fill('test@example.com');
     
    // Select an option from a dropdown labeled "Country"
    await page.getByLabel('Country').selectOption('United States');
  • Pros: Good for forms, readable, resilient if label text is stable.
  • Cons: Requires properly associated <label> elements (using for attribute or by nesting).

4. page.getByPlaceholder()

Locates input elements by their placeholder text.

  • How it works: Finds an input that has a placeholder attribute matching the specified text.
  • Example (TypeScript):
    await page.getByPlaceholder('Enter your username').fill('john.doe');
  • Pros: Useful when labels are missing or for quick identification.
  • Cons: Placeholder text can change, less robust than labels.

5. page.getByAltText()

Locates elements (usually images) by their alt text.

  • How it works: Finds an element with an alt attribute matching the text.
  • Example (TypeScript):
    await page.getByAltText('Company Logo').click();
  • Pros: Good for images, aligns with accessibility.
  • Cons: Relies on alt text being present and unique.

6. page.getByTitle()

Locates elements by their title attribute.

  • How it works: Finds an element with a title attribute matching the text.
  • Example (TypeScript):
    await page.getByTitle('Close dialog').click();
  • Pros: Can be useful for tooltips or elements where other attributes are not descriptive.
  • Cons: title attributes are often not unique or consistently used.

7. page.getByTestId()

Locates elements by a data-testid attribute (or a custom test ID attribute configured in Playwright).

  • How it works: Finds an element with data-testid="your-id".
  • Example (TypeScript):
    await page.getByTestId('login-button').click();
  • Pros: Explicitly for testing, can be very robust if developers add and maintain them. Decouples tests from DOM structure and styling.
  • Cons: Requires developers to add these test IDs to the codebase. Overuse can lead to a test-specific DOM.

Other Locator Strategies

While the getBy methods are recommended, Playwright also supports other strategies:

CSS Selectors: page.locator('css=...') or page.locator('...')

  • How it works: Uses CSS selector syntax to find elements.
  • Example (TypeScript):
    // Using a class
    await page.locator('.submit-button').click();
     
    // Using an ID
    await page.locator('#username').fill('user');
     
    // Complex CSS selector
    await page.locator('div.product-card[data-available="true"] button.add-to-cart').click();
  • Pros: Familiar to web developers, very flexible.
  • Cons: Can be brittle if class names or DOM structure change for styling or non-functional reasons. Less readable for complex selectors compared to getByRole.

XPath Selectors: page.locator('xpath=...')

  • How it works: Uses XPath expressions to navigate and select elements in the XML/HTML document.
  • Example (TypeScript):
    await page.locator('xpath=//button[normalize-space(.)="Save Changes"]').click();
  • Pros: Extremely powerful for complex DOM traversal when no other locator works.
  • Cons: Can be very complex and hard to read, highly brittle to DOM changes. Generally a last resort.

Chaining Locators

Playwright allows you to chain locators to narrow down the search context.

  • Example (TypeScript):
    const productCard = page.locator('.product-item').filter({ hasText: 'My Awesome Product' });
    await productCard.getByRole('button', { name: 'Add to cart' }).click();
     
    // Find a button within a specific form
    const form = page.getByTestId('user-settings-form');
    await form.getByRole('button', { name: 'Save' }).click();

Filtering Locators

You can filter a set of locators:

  • .first(): Selects the first element matched by the locator.
  • .last(): Selects the last element.
  • .nth(index): Selects the element at the specified (0-based) index.
  • .filter({ hasText: '...' }): Filters by text content.
  • .filter({ has: page.locator('...') }): Filters by a descendant locator.

Best Practices for Writing Good Locators

  1. Prioritize User-Facing Attributes: Use getByRole, getByLabel, getByText whenever possible. These reflect how users interact with the page.
  2. Prefer Test IDs for Test-Specific Hooks: When user-facing attributes are not suitable or stable, getByTestId is a great alternative if you can add them to your application.
  3. Avoid Brittle CSS/XPath: Use CSS and XPath sparingly. If you must, aim for simple, direct selectors. Avoid long, complex paths that depend heavily on DOM structure.
  4. Write Readable Locators: Your future self (and your team) will thank you.
  5. Test Your Locators: Use Playwright's highlight() method (await page.locator('...').highlight();) or the Playwright Inspector to verify your locators are selecting the correct elements.
  6. Strive for Uniqueness (but not too specific): The locator should ideally match only one element if you intend to interact with one. However, overly specific locators are brittle. Find the balance.

Conclusion

Choosing the right Playwright locators is crucial for creating robust, readable, and maintainable automated tests. By prioritizing user-facing attributes and understanding the strengths of each locator type, you can build tests that are resilient to UI changes and effectively validate your application's functionality.

Happy testing!