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:

//tagname[@attribute='value']
// 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

TypeExampleVerdict
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.
Note: Always prefer relative XPath. Absolute XPath is tied to the exact DOM structure at the moment you recorded it — any layout change (a wrapping div, a new banner, a reordered section) silently breaks every absolute path, and you won't find out until your suite turns red. Relative XPath survives most of those changes because it only cares about the target element's attributes.

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();
Explanation:
  • 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 / FunctionWhat it does
following-sibling::tagMoves to a sibling that comes after the current node at the same level.
preceding-sibling::tagMoves to a sibling that comes before the current node.
parent::tagMoves up one level to the immediate parent.
ancestor::tagMoves 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).
Next: Locators are only half the job — once you've found an element, you need to act on it. Head to WebElement Actions to fill in and submit RecipeBox's Add Recipe form.