Day 2: Locator Strategies — Why getByRole Wins and XPath Loses
This is Day 2 of the 21-Day Playwright with TypeScript Challenge. One lesson per day. Zero to production-ready in 3 weeks.
🎭 Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Locators determine how your tests find elements. Wrong strategy = tests break every sprint. Right strategy = tests survive UI redesigns. Playwright offers 8+ locator types. Here is which to use and when.
Contents
Locator Resilience Ranking
| Tier | Locator | Resilience | Example |
|---|---|---|---|
| 1 (Best) | getByRole | Survives all UI changes | page.getByRole('button', {name: 'Submit'}) |
| 2 | getByLabel | Survives class/ID changes | page.getByLabel('Email') |
| 3 | getByTestId | Explicit contract | page.getByTestId('checkout-btn') |
| 4 | getByText | Breaks on text change only | page.getByText('Add to cart') |
| 5 | getByPlaceholder | Moderate | page.getByPlaceholder('Search') |
| 6 (Worst) | CSS/XPath | Breaks on any DOM change | page.locator('.btn-primary') |
Each Locator Explained
// 1. getByRole — ALWAYS try first
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByRole('link', { name: 'Documentation' }).click();
await page.getByRole('heading', { name: 'Welcome' });
await page.getByRole('textbox', { name: 'Email' }).fill('test@test.com');
// 2. getByLabel — for labeled form fields
await page.getByLabel('Password').fill('secret123');
await page.getByLabel('Remember me').check();
// 3. getByTestId — when semantic locators insufficient
await page.getByTestId('submit-button').click();
// Requires: data-testid="submit-button" in HTML
// 4. getByText — for visible text content
await page.getByText('Welcome back').click();
await page.getByText(/total: \$/i); // Regex support
// 5. getByPlaceholder — for placeholder text
await page.getByPlaceholder('Enter your email').fill('x@y.com');
// 6. CSS selector — last resort
await page.locator('.nav-item.active').click();
await page.locator('#main-content').isVisible();
🚀 Level Up Your Playwright
From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.
Chaining and Filtering
// filter() — narrow within results
await page.getByRole('listitem')
.filter({ hasText: 'Active' })
.click();
// .and() — both conditions must match
const btn = page.getByRole('button')
.and(page.getByText('Submit'));
// .or() — either matches
const cta = page.getByRole('button', { name: 'Buy' })
.or(page.getByRole('button', { name: 'Purchase' }));
// Chaining parent > child
await page.getByTestId('product-card')
.filter({ hasText: 'Laptop' })
.getByRole('button', { name: 'Add to cart' })
.click();
Decision Tree
- Has visible role? →
getByRole() - Labeled form field? →
getByLabel() - Has data-testid? →
getByTestId() - Unique visible text? →
getByText() - None above? → CSS selector (never XPath)
Tomorrow (Day 3): Assertions that actually catch bugs — expect() deep dive.
🎓 Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
