CSS selectors
#login-button by id (best)
.form_input by class
.input_error.form_input both classes (no space)
[data-test="username"] by attribute
form input descendant (space)
form > input direct child
[id^="login"] [id$="btn"] [class*="err"] partial
.item:nth-child(2) position — LAST resort
XPath
//input[@id='user-name'] by attribute
//button[text()='Checkout'] by exact text
//a[contains(text(),'Terms')] partial text
//*[contains(@class,'error')] any tag
//span[text()='X']/../.. up two levels
//div[text()='X']/ancestor::div[@class='card']//button
(//button[@class='b'])[3] position — last resort
Test live in DevTools console
$$("#login-button") CSS matches
$x("//button[text()='Go']") XPath matches
$$(".form_input").length uniqueness check -> want 1
Elements panel + Ctrl+F accepts both dialects
Locator priority
- 1. id → 2. data-test → 3. meaningful class/attr
- 4. hierarchy → 5. position (avoid)
- Never trust DevTools 'Copy XPath' — absolute = brittle
- Good locator = unique + stable + readable
Choosing the dialect
know the id / attribute? CSS #id [data-test='x']
need the visible text? XPath //b[text()='Checkout']
need to climb UP? XPath ancestor::div[...]
repeated cards / tables? text -> ancestor -> descendant
everything else CSS (shorter, faster)
saucedemo locator inventory
#user-name #password #login-button
[data-test='error'] login error box
.title page header
#add-to-cart-sauce-labs-backpack
.shopping_cart_badge .shopping_cart_link
#checkout #first-name #last-name #postal-code
#continue #finish .complete-header
When 0 matches lies to you
- element inside an <iframe> — console targets the top document
- page re-rendered after you looked — run the query again
- typo or case — ids are case-sensitive
- $$ / $x exist ONLY in the DevTools console, never in code
Practice targets
saucedemo.com standard_user / secret_sauce
locked_out_user (error flows)
the-internet.herokuapp.com/dynamic_loading (waits)