Sometimes you'd describe an element by its text: 'click the link that says Terms of Service.'
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.
// 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:
$x("//button[text()='Add to cart']")
$x("//div[text()='Sauce Labs Backpack']/ancestor::div[@class='inventory_item']")> $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 titleThat 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.
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.
/html/body/div[1]/div/div[2]/… that break on any layout change. Write short relative XPaths by hand instead — you now know how.Fix: text()= is an exact match — invisible leading/trailing whitespace breaks it. Use normalize-space()='Add to cart' or contains(text(),'Add to cart').
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.
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.
On the Products page, write and verify with $x():
pricebar? inspect first)shopping_cart (expect 1)//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')]