Why Your Test Automation Keeps Failing: 5 xPath Mistakes We Had to Unlearn

By Eduard Dublyer, CTO of Skipper Soft

A Quick Disclaimer Before We Begin

At Skipper Soft, we generally do not recommend using xPath as the default way to locate elements in UI automation.

If you’re building a modern, scalable automation suite, you’re much better off with:

  • data-testid
  • aria-label
  • name, id
  • or even robust CSS selectors

They’re faster, more readable, and often more maintainable.

However, we don’t live in a perfect world.

In many of the startups and engineering teams we work with, testers don’t always control the frontend. You might not be able to convince developers to add semantic test hooks in time. Or you’re working with legacy components where CSS isn’t an option.

In those cases, xPath becomes necessary — but it has to be used with care.

So, let me share the 5 most common xPath mistakes we’ve made (and fixed) in real projects and how your team can avoid them.


Mistake #1: Absolute xPaths

I still remember a junior engineer proudly showing me their first working test:

await page.locator('/html/body/div[1]/main/section[2]/button[3]').click(); 

It worked — until someone added a banner above the main section. Then the button was no longer in the same place, and the test broke.

Why it fails:

Absolute xPaths depend entirely on the DOM’s structure. Any layout shift—even cosmetic—will break the locator.

A better approach:

await page.locator("//button[@id='submit-invoice']").click(); 

or

await page.locator("//div[contains(@class, 'modal') and @aria-hidden='false']//button").click(); 

Use meaningful attributes. And avoid paths that start at <html> — they rarely end well.


Mistake #2: Exact Text Matching — Especially in Localized Apps

Here’s a classic:

await page.locator("//*[text()='Login']").click(); 

It’s clean. It’s readable. It’s also dangerous.

Why it fails:

Text is content, not structure. It changes frequently:

  • Marketing updates the CTA from “Login” to “Log in”
  • Product team changes it again to “Sign in”
  • And in localized apps? Now it’s “Connexion”, “Anmelden”, “התחברות”, or “Войти”

And just like that, your locator is useless.

A better approach:

await page.locator("//*[contains(text(), 'Log')]").click(); 

or (to strip extra spaces):

await page.locator("//button[normalize-space()='Sign Up']").click(); 

But the best fix is this:

Don’t use visible text as a locator in apps that support multiple languages.

Instead, ask your developers for:

  • data-testid
  • aria-label
  • role or name attributes

These are not translated. They’re semantic. And they’re stable.


Mistake #3: Using Dynamic IDs

This one comes up more than I’d like:

await page.locator("//div[@id='user-8932']").click(); 

It works one day. Fails the next. Why? Because that ID was randomly generated and changes every session.

Why it fails:

If an attribute value changes between test runs — it’s not a good anchor. It might look unique, but it’s not dependable.

A better approach:

await page.locator("//div[starts-with(@id, 'user-')]").click(); 

or

await page.locator("//div[contains(@class, 'profile-card')]//button").click(); 

If the ID is dynamic, fall back to class names, roles, or neighboring elements.


Mistake #4: Locating by Position in the DOM

Here’s another locator we’ve inherited from client projects:

await page.locator("//form/div[2]/input").fill('[email protected]'); 

It breaks the moment someone adds a new field or wrapper. And debugging it is never fun.

Why it fails:

DOM structure is not static. Developers refactor components, change layout wrappers, and insert new elements frequently. Tests that depend on div[2] are fragile by nature.

A better approach:

await page.locator("//input[@name='email']").fill('[email protected]'); 

Or, if inside a form:

await page.locator("//form[@id='login-form']//input[@type='password']").fill('securePassword123'); 

Avoid indexes. Choose intent-based attributes instead.


Mistake #5: The xPath Monster

This one’s real. And it made it into a pull request:

//div[@class='container'][2]//span[text()='OK'][not(@disabled)] 

It worked. But it wasn’t readable. And nobody wanted to be the one maintaining it.

Why it fails:

Complex xPaths are error-prone. They’re hard to read, debug, and refactor. And they usually mean something is missing in the UI that should have been added for testability.

A better approach:

//div[@data-testid='modal']//button[.='Confirm'] 

Or better yet, work with your frontend team to add:

<button data-testid="confirm-button">Confirm</button> 

Automation is a team sport. The better the markup, the simpler the test.


Final Thoughts from the Field

At Skipper Soft, we help fast-moving startups scale their test automation efforts. And over time, we’ve learned this:

  • Bad locators are one of the most common and preventable reasons tests fail
  • xPath is not the enemy — but lazy xPath is
  • Teams that care about testability build more reliable products

So the next time a test fails unexpectedly, before blaming infrastructure or flaky frameworks — check the locator.

It’s often where the real bug lives.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *