QA Engineer Skills 2026QA-2026Locator Strategies

Locator Strategies

Choosing the right locator strategy is one of the highest-leverage decisions in test automation. Fragile locators are the leading cause of test maintenance burden. Playwright provides user-centric locators that mirror how real users find elements — and they are dramatically more stable than the CSS/XPath selectors used in Selenium.


The Locator Hierarchy

Playwright recommends locators in this priority order:

Priority Locator Example Why
1 Role getByRole('button', { name: 'Submit' }) Mirrors accessibility tree; resilient to markup changes
2 Text getByText('Sign in') User-visible; breaks only when copy changes
3 Test ID getByTestId('login-form') Explicit contract between dev and test
4 Label getByLabel('Email address') Form fields via associated label
5 Placeholder getByPlaceholder('Enter email') Inputs without visible labels
6 CSS/XPath page.locator('.btn-primary') Last resort for complex DOM traversal

Role-Based Locators (Preferred)

Role locators use the accessibility tree, not the DOM structure. They find elements the way a screen reader would — which means they are resilient to HTML refactoring.

// Finds <button>, <input type="submit">, or any element with role="button"
await page.getByRole('button', { name: 'Submit' }).click();

// Finds <a> elements or elements with role="link"
await page.getByRole('link', { name: 'Sign up' }).click();

// Finds <h1>–<h6> or elements with role="heading"
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

// Checkboxes and radio buttons
await page.getByRole('checkbox', { name: 'Remember me' }).check();

Text Locators

// Exact text
await page.getByText('Welcome back', { exact: true }).click();

// Substring match (default)
await page.getByText('Welcome').click();

// Regex
await page.getByText(/welcome/i).click();

Test ID Locators

When role and text locators are not viable (e.g., elements without meaningful text or multiple identical elements), test IDs provide a stable contract.

// In the application: <div data-testid="user-profile">...</div>
await page.getByTestId('user-profile').click();

// Configure the attribute name in playwright.config.ts:
// use: { testIdAttribute: 'data-qa' }

Label and Placeholder Locators

// Finds input by its associated <label>
await page.getByLabel('Email address').fill('user@example.com');

// Finds input by placeholder text
await page.getByPlaceholder('Search products').fill('laptop');

CSS and XPath: When You Need Them

Playwright supports CSS and XPath for cases where semantic locators are insufficient:

// CSS selector
await page.locator('.product-card >> .price').textContent();

// XPath (prefix with //)
await page.locator('//div[@class="sidebar"]//a').click();

When CSS/XPath Is Acceptable

  • Third-party widgets without accessible roles or test IDs
  • Complex DOM traversal (parent → child → sibling relationships)
  • Legacy applications where adding test IDs is not possible

When to Avoid CSS/XPath

  • Positional selectors like div:nth-child(3) — break when markup order changes
  • Deep nesting like #app > div > main > section > form > button — break on any refactor
  • Class-based selectors that target styling (.btn-primary) rather than purpose

Locator Strategy Comparison

Strategy Stability Readability Maintenance Best For
getByRole() High High Low Buttons, links, headings, form controls
getByText() Medium High Low Static content, labels
getByTestId() High Medium Low Dynamic content, complex components
getByLabel() High High Low Form inputs
CSS selector Low–Medium Low High Third-party widgets, legacy apps
XPath Low Low High Complex traversal, text matching

Practical Example: Login Form

// BEST: role-based locators
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();

// GOOD: test ID locators
await page.getByTestId('email-input').fill('user@example.com');
await page.getByTestId('password-input').fill('secret');
await page.getByTestId('login-button').click();

// AVOID: CSS selectors tied to implementation
await page.locator('input[type="email"]').fill('user@example.com');
await page.locator('input[type="password"]').fill('secret');
await page.locator('form.login-form button.btn-primary').click();

Key Takeaways

  • Prefer role-based locators (getByRole) — they mirror user behavior and survive refactoring
  • Use getByTestId when semantic locators are not available — it creates a stable test contract
  • Reserve CSS/XPath for third-party widgets and legacy applications
  • Avoid positional selectors, deep nesting, and styling-based class selectors
  • A good locator strategy reduces test maintenance by 60–80% compared to CSS/XPath-heavy tests