Playwright: Simple Ways to Find the Right Elements

Playwright: Simple Ways to Find the Right Elements

If you’ve been using Playwright but still rely on long, fragile XPaths, you’re not really using its power. One small UI refactor, and suddenly half your tests start failing with “locator not found” or “timeout exceeded.” The feature still works fine — but your locators don’t. 

The good news? You can escape this locator hell by shifting from brittle XPaths to stable data-testid hooks with getByTestId and other smart Playwright locators. 

In this blog, let’s look at how to design a clean, future-proof locator strategy in Playwright that drastically reduces flakiness and maintenance. 

Playwright online training

Why old-school XPaths are a problem in Playwright 

Even though Playwright supports CSS and XPath, using them like we did in older tools is asking for trouble. Common anti-patterns: 

Locator Anti-Patterns That Break Tests

  • Absolute XPaths: 

  • //div[2]/div[1]/form/div[3]/button 

  • Structure-dependent XPaths: 

  • //div[@class='form-group'][3]//button 

  • Over-specific CSS selectors: 

  • .btn.btn-primary:nth-child(4) 

These locators depend heavily on DOM structure and CSS classes, both of which change frequently: 

  • Designers shuffle layouts. 

  • Developers add or remove wrapper divs. 

  • Class names get refactored or minified. 

Result? Your Playwright tests start failing even though the user journey is still correct. Over time: 

  • You spend more effort fixing locators than writing new tests. 

  • CI becomes noisy and unreliable. 

  • Stakeholders lose trust in automation. 

Playwright gives you a better way — especially with getByTestId and other “semantic” locators. 

Playwright Webinar
Also read: Automated Tests: Turning Reported Defects into a QA Best Practice

What is data-testid and why is it ideal for Playwright? 

data-testid is a custom attribute that exists purely for testing and automation. It doesn’t affect UX, styling, or behavior — only makes the UI easier to test. 

Example markup: 

<input 

  type="email" 

  data-testid="login-email-input" 

  placeholder="Enter your email" 

/> 

<button data-testid="login-submit-button"> 

  Login 

</button> 

Playwright code: 

await page.getByTestId('login-email-input').fill('user@example.com'); 

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

Why this is powerful: 

  • Independent of layout and CSS. 

  • Expresses intent: this is the login email input, not “the third input inside that form”. 

  • Easy to search and refactor in code. 

  • Works consistently across React, Angular, Vue, or any SPA. 

Also read: From Federer to Sinner: Are You Still Automated Testing Like It’s 2020?

From XPaths to getByTestId: a practical migration path 

You don’t have to rewrite your entire suite in one go. Move gradually and strategically. 

From XPaths to getByTestId: a practical migration path

1. Identify the worst offenders 

Start with: 

  • Tests that frequently fail after UI changes. 

  • Locators that are: 

  • Long XPaths 

  • Position-based selectors (nth-child, [3], etc.) 

  • Deeply nested CSS like .page .container .form .btn.primary:nth-child(2) 

These are your prime candidates for replacement. 

2. Collaborate with developers on test hooks 

Talk to your dev team and agree on: 

  • Adding data-testid to key UI elements: 

  • Login/sign-up flows 

  • Cart and checkout 

  • Search and filters 

  • Critical dashboard actions 

  • A naming convention, for example: 

  • login-email-input, login-password-input, login-submit-button 

  • cart-icon, cart-item, checkout-cta 

  • profile-menu-toggle, profile-logout-link 

This is your Locator Contract between Dev and QA. 

3. Start replacing locators in your Playwright tests 

As soon as hooks are available, refactor your tests. 

Before (brittle): 

await page.locator("//form/div[3]//button").click(); 

After (stable): 

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

You’ll notice that your test code becomes self-explanatory and far easier to maintain. 

Writing clean and readable Playwright locators 

Your goal should be: any tester can understand the intent just by reading the code

Example login flow: 

await page.getByTestId('login-email-input').fill('tester@example.com'); 

await page.getByTestId('login-password-input').fill('P@ssw0rd!'); 

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

Compare that with: 

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

await page.locator("(//input[@type='password'])[1]").fill('P@ssw0rd!'); 

await page.locator("//form/div[3]/button").click(); 

Both might work, but only the first clearly tells a story

Some guidelines: 

  • Use clear, descriptive names

  • ✅ reset-password-link 

  • ✅ order-history-tab 

  • ❌ link1, btn2, tab_3 

  • Keep them consistent across modules. 

  • Avoid reusing the same data-testid for multiple elements (unless it’s a list where you expect multiple items, like product-card). 

Combine getByTestId with other smart Playwright locators 

Playwright gives you more than just getByTestId. For many flows, you can mix semantic locators: 

  • getByRole – aligns with accessibility roles: 

  • await page.getByRole('button', { name: 'Login' }).click(); 

  • getByLabel – great for form inputs: 

  • getByPlaceholder – useful during prototyping: 

  • await page.getByPlaceholder('Search products').fill('laptop'); 

A good strategy: 

  1. Prefer getByRole / getByLabel when UX text is stable and accessibility is set correctly. 

  1. Use getByTestId when: 

  • Text may change (marketing copy, button text, etc.). 

  • Elements are icons, toggles, or have no clear text. 

  1. Use raw locator('css=...') or XPath only when necessary

Handling dynamic UIs with getByTestId 

Modern SPAs have: 

  • Dynamic modals 

  • Toasts and snackbars 

  • Infinite scroll lists 

  • Conditional components 

data-testid makes these easier to anchor to. 

Examples: 

// Waiting for success toast 

await expect(page.getByTestId('toast-success')).toBeVisible(); 

// Selecting a product card 

const firstProduct = page.getByTestId('product-card').first(); 

await firstProduct.click(); 

// Filtering with a toggle 

await page.getByTestId('filter-in-stock').click(); 

Because you’re not depending on DOM depth or nth-child positions, these tests are much more resistant to layout changes. 

“But do we really need test-only attributes?” 

This is a common pushback from developers. Here’s how to respond: 

  • Test hooks are like APIs for your UI. They don’t affect users, but they let automation and debugging work reliably. 

  • Without them, test code gets coupled to implementation details that change often. 

  • Over time, stable automation saves developer time too, because fewer “tests are broken again” complaints land in their lap. 

Think of it as designing your UI for testability, just like you’d design backend services for observability. 

Also read: How API Integration Helped Us Handle 3rd Party Failures in UI Tests

Long-term benefits of using getByTestId in Playwright 

Once you’ve migrated a good portion of your suite to getByTestId and semantic locators, you’ll notice: 

Long-term benefits of using getByTestId in Playwright

  • Massive reduction in flaky tests caused by locator issues. 

  • Faster onboarding — new team members understand tests quickly. 

  • Easier refactors — you can move components around without rewriting tests. 

  • More trust in CI results — failures are more likely to be real issues, not “UI moved by 10px.” 

Playwright is built for modern automation, and its locator system is one of its strongest advantages. But you only feel that power when you stop treating it like an old-school Selenium clone and start using getByTestId, roles, and labels as first-class citizens. 

If you’re still wrestling with brittle XPaths, this is your sign:  standardise on data-testid + getByTestId, and let Playwright do what it does best — stable, readable, modern UI automation. 

Testleaf is where you learn the practical automation habits that reduce locator breakages and cut maintenance time. If you’re ready to work smarter (not re-fix the same tests), you’re in the right place.

To view or add a comment, sign in

More articles by Testleaf Software Solutions Private Limited

Others also viewed

Explore content categories