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.
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:
These locators depend heavily on DOM structure and CSS classes, both of which change frequently:
Result? Your Playwright tests start failing even though the user journey is still correct. Over time:
Playwright gives you a better way — especially with getByTestId and other “semantic” locators.
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:
From XPaths to getByTestId: a practical migration path
You don’t have to rewrite your entire suite in one go. Move gradually and strategically.
1. Identify the worst offenders
Start with:
These are your prime candidates for replacement.
2. Collaborate with developers on test hooks
Talk to your dev team and agree on:
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):
Recommended by LinkedIn
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:
Combine getByTestId with other smart Playwright locators
Playwright gives you more than just getByTestId. For many flows, you can mix semantic locators:
A good strategy:
Handling dynamic UIs with getByTestId
Modern SPAs have:
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:
Think of it as designing your UI for testability, just like you’d design backend services for observability.
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:
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.