XPath
CSS selectors cover most locators, but they can't do everything. XPath (XML Path Language) can locate an element by its visible text, walk upward from a child to a parent, or hop sideways to a sibling — none of which plain CSS selectors can do. In Selenium, XPath locators go through By.xpath(). We'll keep using our RecipeBox app: a recipe list where each item is a <div class="recipe-card"> containing an <h3> title and a "Delete" button.
By.xpath() Basics
WebElement title = driver.findElement(By.xpath("//h3[@class='recipe-title']"));
System.out.println(title.getText());
The syntax reads as: // (search anywhere in the document) + h3 (tag name) + [@class='recipe-title'] (attribute filter). The general pattern testers use constantly is:
// Login page locators, written as XPath instead of By.id()
WebElement email = driver.findElement(By.xpath("//input[@id='email']"));
WebElement password = driver.findElement(By.xpath("//input[@id='password']"));
WebElement loginBtn = driver.findElement(By.xpath("//button[@id='loginBtn']"));
Absolute vs Relative XPath
| Type | Example | Verdict |
|---|---|---|
| Absolute | /html/body/div[2]/div/div[3]/ul/li[2]/h3 | Starts with a single / and walks the entire DOM tree from the root. Breaks the instant a developer adds one extra <div> anywhere above it. |
| Relative | //h3[@class='recipe-title'] | Starts with // and searches from anywhere in the document. Depends only on the element's own attributes, not on the page structure around it. |
Locating by Visible Text: text() and contains()
This is where XPath earns its place in your toolkit — CSS selectors have no way to match on the text a user actually sees. Say the recipe list has a card titled "Paneer Butter Masala" and we want to click it without knowing its exact position in the list:
// Exact match on visible text
WebElement recipe = driver.findElement(
By.xpath("//h3[text()='Paneer Butter Masala']"));
// Partial match - safer when text might have extra whitespace or a badge appended
WebElement recipeLoose = driver.findElement(
By.xpath("//h3[contains(text(), 'Paneer')]"));
recipeLoose.click();
- text()='...' requires the element's text to match exactly, character for character.
- contains(text(), '...') matches if the text merely contains the given substring — more forgiving, and the more commonly used of the two in real suites.
- You can combine both attribute and text conditions with and: //h3[@class='recipe-title' and contains(text(),'Paneer')].
Useful XPath Axes
Axes let you navigate relative to an element you've already identified — handy when the element you can actually reference (say, a recipe title) isn't the element you need to act on (say, its "Delete" button sitting next to it in the same card).
// Find the Delete button that sits right after the "Paneer Butter Masala" title
WebElement deleteBtn = driver.findElement(By.xpath(
"//h3[contains(text(),'Paneer')]/following-sibling::button[@class='delete-btn']"));
deleteBtn.click();
// Find the parent recipe-card div, starting from the title
WebElement card = driver.findElement(By.xpath(
"//h3[contains(text(),'Paneer')]/parent::div[@class='recipe-card']"));
// Find every recipe title on the page, then loop through them
List<WebElement> allTitles = driver.findElements(By.xpath("//h3[@class='recipe-title']"));
for (WebElement t : allTitles) {
System.out.println(t.getText());
}
| Axis / Function | What it does |
|---|---|
| following-sibling::tag | Moves to a sibling that comes after the current node at the same level. |
| preceding-sibling::tag | Moves to a sibling that comes before the current node. |
| parent::tag | Moves up one level to the immediate parent. |
| ancestor::tag | Moves up to any matching ancestor, however many levels up. |
| contains(@attr, 'val') | Matches if an attribute's value contains a substring (great for dynamic/generated class names). |