📋 Table of Contents
- Introduction
- Priority Ranking of Locator Strategies
- 1. data-testid (Best Practice)
- 2. getByRole() - Accessibility First
- 3. getByLabel() - Form Fields
- 4. getByPlaceholder() - Input Hints
- 5. getByText() - Visible Content
- 6. getByAltText() - Images
- 7. getByTitle() - Tooltips
- 8. CSS Selectors - When Needed
- 9. XPath - Last Resort
- 10. Locator Chaining - Combine Strategies
- Best Practices & Common Pitfalls
- Conclusion
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:
- The official Playwright priority ranking for locators
- When to use each locator strategy
- Real-world examples with code
- Common pitfalls and how to avoid them
- How LocatorLab automatically chooses the best locator
💡 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):
- data-testid - Explicit test attributes (Score: 10/10)
- getByRole() - ARIA roles and accessible names (Score: 9/10)
- getByLabel() - Form field labels (Score: 9/10)
- getByPlaceholder() - Input placeholders (Score: 8/10)
- getByText() - Visible text content (Score: 7/10)
- getByAltText() - Image alt attributes (Score: 8/10)
- getByTitle() - Element title attributes (Score: 7/10)
- CSS Selectors - Classes, IDs (Score: 5-7/10)
- XPath - DOM traversal (Score: 3-5/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:
- Use descriptive, meaningful names:
submit-order-buttonnotbtn1 - Use kebab-case:
user-profile-form - Be specific:
login-email-inputnot justemail - Keep them unique on the page
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:
button- Buttons and clickable elementstextbox- Input fieldscheckbox- Checkboxesradio- Radio buttonscombobox- Dropdowns/select elementslink- Hyperlinksheading- H1, H2, etc.list/listitem- Lists
🎯 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:
- Use exact matches when possible:
{ exact: true } - Be cautious with partial matches - they can be ambiguous
- Consider that text content changes with internationalization
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
- ID selectors - very stable if IDs are meaningful
- Attribute selectors for name, type, etc.
- When semantic locators don't work
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?
- Breaks when DOM structure changes
- Hard to read and maintain
- Slower performance than CSS
- Doesn't reflect user interaction patterns
When XPath is Acceptable:
- Table navigation:
//tr[td[contains(text(), "John")]]/td[3] - Complex parent/sibling relationships
- When all other strategies fail
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:
- Resolves ambiguity when multiple elements match
- More resilient to page structure changes
- Mirrors how users think about page sections
- Creates self-documenting test code
Best Practices & Common Pitfalls
✅ DO:
- Work with developers - Get data-testid added to critical elements
- Use semantic locators - Prefer getByRole, getByLabel over CSS/XPath
- Test accessibility - Good locators = accessible UIs
- Be specific - Use unique, descriptive identifiers
- Use locator chaining - Combine strategies for precision
- Auto-wait - Playwright locators auto-wait, use them!
❌ DON'T:
- Don't use generated class names -
.css-1x2y3zwill break - Don't rely on DOM structure -
div > div > buttonis brittle - Don't use positional selectors -
nth-child(5)breaks when order changes - Don't hardcode indices -
.first(),.nth(2)unless necessary - Don't use XPath for simple cases - There's always a better way
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:
- Add
data-testidfor critical elements - Use
getByRole()for semantic elements - Use
getByLabel()for form fields - Fall back to
getByText()for content - Use CSS selectors when semantic options don't work
- Avoid XPath unless absolutely necessary
- 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.
Remember: The best locator is the one that will still work 6 months from now when the UI inevitably changes.