assert_
glossary ↓ downloads
track: java · selenium
suite 03 · web fundamentals

XPath — when CSS isn't enough

Web fundamentals15 min
as a manual tester

Sometimes you'd describe an element by its text: 'click the link that says Terms of Service.'

in automation

CSS can't select by text. XPath can — that's its superpower, and the main reason it's still in every automation engineer's kit.

XPath is a query language for walking the DOM tree. Anything CSS can locate, XPath can too — plus a few things CSS can't, most importantly matching by visible text and walking upward to a parent.

XPath cheatsheet
// anywhere in the document, any depth — your usual start
//input                              every <input>
//input[@id='user-name']             by attribute (@)
//input[@type='submit' and @value='Login']

// by visible text — the thing CSS cannot do
//button[text()='Checkout']
//a[contains(text(),'Terms')]

// partial attribute match
//*[contains(@class,'error')]        * = any tag

// hierarchy: / child, // any descendant, .. parent
//form//input
//span[text()='Sauce Labs Backpack']/../..   up two levels

// position — same warning as CSS: last resort
(//button[@class='btn_inventory'])[3]

Test XPath in the DevTools console with $x("…") (single dollar, x for XPath), or with Ctrl+F in the Elements panel — the same search box accepts both CSS and XPath:

DevTools console — on the saucedemo Products page
$x("//button[text()='Add to cart']")
$x("//div[text()='Sauce Labs Backpack']/ancestor::div[@class='inventory_item']")
console output
> $x("//button[text()='Add to cart']")
(6) [button, button, button, button, button, button]

> $x("//div[text()='Sauce Labs Backpack']/ancestor::div[@class='inventory_item']")
[div.inventory_item]      // found the whole product card from its title

That second query shows the classic real-world pattern: find by text, then climb to the container, then act inside it ("click Add to cart inside the Backpack card"). It's the pattern for tables and product grids everywhere, and it's pure XPath territory — challenge 8 in the locator sandbox makes you do exactly this.

CSS or XPath — the honest rule

Default to CSS: shorter, faster, easier to read. Reach for XPath when you need text matching, parent/ancestor traversal, or complex conditions. Fluent automation engineers use both without ideology; interviews love asking the difference, and now you can answer it from experience.

Never copy from "Copy XPath": DevTools' right-click → Copy → XPath produces brittle absolute paths like /html/body/div[1]/div/div[2]/… that break on any layout change. Write short relative XPaths by hand instead — you now know how.
⚠ when it breaks — the classics
My text() match returns nothing, but I can see the text
> $x("//button[text()='Add to cart ']")
[]

Fix: text()= is an exact match — invisible leading/trailing whitespace breaks it. Use normalize-space()='Add to cart' or contains(text(),'Add to cart').

$x throws 'not a function'
Uncaught TypeError: $x is not a function

Fix: Some pages override the console shortcuts. Fall back to Ctrl+F in the Elements panel — it evaluates XPath too and shows the match count.

My copied XPath broke after a tiny UI change
/html/body/div[1]/div/div[2]/div[1]/div/form/input[1]

Fix: That's an absolute path from Copy XPath — it encodes the whole page structure. Rewrite it as a short relative XPath (//input[@id='user-name']) and it survives redesigns.

⚡ exercise · four XPaths on saucedemo

On the Products page, write and verify with $x():

  1. The "Sauce Labs Onesie" product name, by its text (expect 1)
  2. All product price spans (expect 6 — hint: class contains pricebar? inspect first)
  3. The Add-to-cart button inside the Onesie's card, via text + ancestor + descendant (expect 1)
  4. Any element whose class contains shopping_cart (expect 1)
show a solution
//div[text()='Sauce Labs Onesie']
//div[@class='inventory_item_price']
//div[text()='Sauce Labs Onesie']/ancestor::div[@class='inventory_item']//button
//*[contains(@class,'shopping_cart')]
key takeaways
  • XPath adds what CSS lacks: text() matching and upward (ancestor) traversal.
  • $x() in the console tests XPath; Ctrl+F in Elements accepts both CSS and XPath.
  • Default to CSS; use XPath for text/parent cases. Never trust auto-copied absolute XPaths.
← CSS selectors Locator strategy — the practice lesson →