10 Best Playwright Locator Strategies for Reliable Tests

📋 Table of Contents

Why Locator Strategy Matters

Choosing the right locator strategy can mean the difference between tests that run reliably for years and tests that break every time the UI changes. After analyzing thousands of test failures, we've identified clear patterns in what makes locators stable.

In this comprehensive guide, you'll learn:

💡 Pro Tip

LocatorLab's Locator Capture feature automatically selects the most stable locator strategy based on these exact principles, giving each locator a quality score from 0-10.

Priority Ranking of Locator Strategies

Playwright recommends this priority order (from most stable to least stable):

  1. data-testid - Explicit test attributes (Score: 10/10)
  2. getByRole() - ARIA roles and accessible names (Score: 9/10)
  3. getByLabel() - Form field labels (Score: 9/10)
  4. getByPlaceholder() - Input placeholders (Score: 8/10)
  5. getByText() - Visible text content (Score: 7/10)
  6. getByAltText() - Image alt attributes (Score: 8/10)
  7. getByTitle() - Element title attributes (Score: 7/10)
  8. CSS Selectors - Classes, IDs (Score: 5-7/10)
  9. XPath - DOM traversal (Score: 3-5/10)
  10. Locator Chaining - Combine multiple strategies (Score: 8-9/10)

1. data-testid - The Gold Standard

Quality Score: 10/10 ⭐

The data-testid attribute is specifically added for testing purposes and won't change when designers update the UI. This is the most stable locator strategy.

HTML Example:

<button data-testid="submit-button">Submit Form</button>
<input data-testid="email-input" type="email" />

Playwright Code:

// Click submit button
await page.getByTestId('submit-button').click();

// Fill email field
await page.getByTestId('email-input').fill('test@example.com');

✅ When to Use

Always prefer data-testid when you control the HTML. Work with your development team to add these attributes to critical elements.

Best Practices:

2. getByRole() - Accessibility First

Quality Score: 9/10 ⭐

Locating by ARIA role ensures your tests mirror how assistive technologies (screen readers) interact with your app. Bonus: it forces you to build accessible UIs!

Playwright Code:

// Click a button with accessible name
await page.getByRole('button', { name: 'Submit' }).click();

// Fill a textbox
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');

// Select from dropdown
await page.getByRole('combobox', { name: 'Country' }).selectOption('USA');

// Navigate using links
await page.getByRole('link', { name: 'Contact Us' }).click();

Common ARIA Roles:

🎯 Pro Tip

Use Playwright's codegen tool to discover the correct role: npx playwright codegen your-url

3. getByLabel() - Perfect for Forms

Quality Score: 9/10 ⭐

Locates form controls by their associated <label> text. This is how real users find form fields!

HTML Example:

<label for="email">Email Address</label>
<input id="email" type="email" />

Playwright Code:

await page.getByLabel('Email Address').fill('test@example.com');
await page.getByLabel('Password').fill('SecurePass123');
await page.getByLabel('Remember me').check();

⚠️ Watch Out

This only works if your HTML properly associates labels with inputs using for attribute or nesting. Always validate your HTML structure!

4. getByPlaceholder() - Input Hints

Quality Score: 8/10 ⭐

When inputs don't have labels but do have placeholder text, this is your next best option.

Playwright Code:

await page.getByPlaceholder('Search...').fill('playwright tutorial');
await page.getByPlaceholder('Enter your email').fill('test@example.com');

⚠️ Caution

Placeholders can change more frequently than labels. Use getByLabel() when possible.

5. getByText() - Visible Content

Quality Score: 7/10 ⭐

Locate elements by their visible text content. Great for buttons, links, and headings.

Playwright Code:

// Exact match
await page.getByText('Submit Order').click();

// Partial match (case-insensitive)
await page.getByText('submit', { exact: false }).click();

// Regular expression
await page.getByText(/submit order/i).click();

Best Practices:

6. getByAltText() - For Images

Quality Score: 8/10 ⭐

Locate images by their alt text (which should exist for accessibility!).

Playwright Code:

await page.getByAltText('Company Logo').click();
await page.getByAltText('User Avatar').isVisible();

7. getByTitle() - Tooltips

Quality Score: 7/10 ⭐

Locate elements by their title attribute (shown as tooltips).

Playwright Code:

await page.getByTitle('Close dialog').click();
await page.getByTitle('More options').click();

8. CSS Selectors - When You Need Precision

Quality Score: 5-7/10 ⭐

CSS selectors are powerful but can be brittle if they rely on implementation details like class names.

Playwright Code:

// By ID (stable)
await page.locator('#submit-button').click();

// By class (less stable)
await page.locator('.btn-primary').click();

// By attribute
await page.locator('[name="email"]').fill('test@example.com');

// Combining selectors
await page.locator('input[type="email"][name="email"]').fill('test@example.com');

💡 When to Use CSS

9. XPath - Last Resort Only

Quality Score: 3-5/10 ⭐

XPath is powerful but extremely brittle. Use only when absolutely necessary.

Playwright Code:

// Avoid this if possible
await page.locator('//button[text()="Submit"]').click();

// Better: use text locator instead
await page.getByText('Submit').click();

⚠️ Why Avoid XPath?

When XPath is Acceptable:

10. Locator Chaining - Combine Strategies

Quality Score: 8-9/10 ⭐

Combine multiple locator strategies for more precise targeting, especially for duplicate elements.

Playwright Code:

// Find submit button within a specific form
await page.getByRole('form', { name: 'Login' })
  .getByRole('button', { name: 'Submit' })
  .click();

// Find input within a section
await page.getByRole('region', { name: 'Contact Info' })
  .getByLabel('Email')
  .fill('test@example.com');

// Narrow down by parent container
await page.locator('#checkout-form')
  .getByTestId('credit-card-input')
  .fill('4111111111111111');

Benefits of Chaining:

Best Practices & Common Pitfalls

✅ DO:

❌ DON'T:

Common Pitfalls:

⚠️ Pitfall #1: Dynamic Content

Problem: getByText('Items: 5') breaks when count changes
Solution: getByText(/Items: \d+/) or use data-testid

⚠️ Pitfall #2: Multiple Matches

Problem: Multiple "Submit" buttons on the page
Solution: Use locator chaining or more specific locators

⚠️ Pitfall #3: Internationalization

Problem: getByText('Submit') breaks in other languages
Solution: Use data-testid or getByRole with accessible names

Conclusion: Choose Wisely, Test Confidently

Selecting the right locator strategy is one of the most important decisions you'll make when writing automated tests. Follow this priority order:

  1. Add data-testid for critical elements
  2. Use getByRole() for semantic elements
  3. Use getByLabel() for form fields
  4. Fall back to getByText() for content
  5. Use CSS selectors when semantic options don't work
  6. Avoid XPath unless absolutely necessary
  7. Chain locators for precision and clarity

🚀 Try LocatorLab

LocatorLab automatically analyzes your page and suggests the best locator strategy for each element, complete with quality scores. It follows these exact principles to ensure your tests are stable and maintainable.

Install Free Chrome Extension →

Remember: The best locator is the one that will still work 6 months from now when the UI inevitably changes.

Found this helpful? Share it!