Playwright Locators Masterclass: All 18 Strategies With Real Code Examples
Playwright offers 18 different locator strategies — but most engineers only use 3 or 4. Choosing the right locator is the difference between a test suite that survives UI changes and one that breaks every sprint.
Contents
Tier 1: Semantic Locators (Always Prefer These)
These locators are based on accessibility roles and visible content. They are the most resilient to DOM changes.
// 1. getByRole — BEST choice, ARIA-based
await page.getByRole('button', { name: 'Submit' }).click();
// 2. getByLabel — for form inputs with labels
await page.getByLabel('Email address').fill('test@example.com');
// 3. getByText — matches visible text
await page.getByText('Welcome back').click();
// 4. getByPlaceholder — for inputs with placeholder text
await page.getByPlaceholder('Search...').fill('playwright');
// 5. getByAltText — for images
await page.getByAltText('Company logo').click();
// 6. getByTitle — for elements with title attribute
await page.getByTitle('Close dialog').click();
Tier 2: Test ID Locators (Best for Dynamic Content)
// 7. getByTestId — uses data-testid attribute
await page.getByTestId('submit-button').click();
// Configure custom test ID attribute
// playwright.config.ts: use: { testIdAttribute: 'data-qa' }
Tier 3: CSS and XPath (Use Sparingly)
// 8. CSS selector
await page.locator('.btn-primary').click();
// 9. CSS with pseudo-classes
await page.locator('button:has-text("Submit")').click();
await page.locator('div:visible').click();
// 10. XPath
await page.locator('xpath=//button[@type="submit"]').click();
// 11. CSS with nth-child
await page.locator('li:nth-child(3)').click();
Tier 4: Advanced Locators
// 12. filter() — chain conditions
await page.getByRole('listitem').filter({ hasText: 'Active' }).click();
// 13. .and() — combine locators
const button = page.getByRole('button').and(page.getByText('Save'));
// 14. .or() — match either
const cta = page.getByRole('button', { name: 'Buy' }).or(page.getByRole('button', { name: 'Purchase' }));
// 15. frameLocator() — for iframes
await page.frameLocator('#payment-iframe').getByRole('button', { name: 'Pay' }).click();
// 16. Shadow DOM — pierces automatically
await page.locator('my-component').getByRole('button').click();
// 17. Layout-based (left-of, right-of, above, below, near)
await page.getByText('Price').locator('xpath=..').getByRole('textbox').fill('100');
// 18. has/hasNot filtering
await page.locator('div', { has: page.getByText('Error') }).screenshot();
Locator Decision Tree
- Does the element have a visible role? Use
getByRole() - Is it a labeled form field? Use
getByLabel() - Does it have unique visible text? Use
getByText() - Does your team use data-testid? Use
getByTestId() - None of the above? Use CSS selector as last resort
- Never use XPath unless absolutely necessary
