Testing Tools selenium java python csharp javascript

XPath in Selenium

By.xpath in Java, Python, C# and JavaScript, quoting helpers, relative searches from an element, Selenium 4 relative locators, and where XPath sits in a page object.

XPath in Selenium

Selenium hands your XPath to the browser’s own evaluator, so everything on this site applies unchanged. What Selenium adds is the API around it: how you pass the string, how you search within an element, and how you keep locators maintainable across a large suite.

The basic call

// Java
WebElement save = driver.findElement(By.xpath("//button[normalize-space()='Save']"));
List<WebElement> rows = driver.findElements(By.xpath("//table[@id='orders']/tbody/tr"));
# Python
from selenium.webdriver.common.by import By

save = driver.find_element(By.XPATH, "//button[normalize-space()='Save']")
rows = driver.find_elements(By.XPATH, "//table[@id='orders']/tbody/tr")
// C#
IWebElement save = driver.FindElement(By.XPath("//button[normalize-space()='Save']"));
var rows = driver.FindElements(By.XPath("//table[@id='orders']/tbody/tr"));
// JavaScript (selenium-webdriver)
const { By } = require('selenium-webdriver');
const save = await driver.findElement(By.xpath("//button[normalize-space()='Save']"));
const rows = await driver.findElements(By.xpath("//table[@id='orders']/tbody/tr"));

findElement throws NoSuchElementException on zero matches and returns the first match if there are several. findElements returns an empty list on zero matches, which makes it the right call for “assert not present”.

Quoting

XPath strings can use single or double quotes. Pick the one that is not your language’s string delimiter:

By.xpath("//a[@title='Home']")          // Java string uses ", XPath uses '
By.XPATH, "//a[@title='Home']"
By.XPATH, '//a[@title="Home"]'          # either is fine in Python

When the value itself contains an apostrophe (Don't save), neither works alone. Use concat() or a helper:

static String q(String s) {
  if (!s.contains("'")) return "'" + s + "'";
  if (!s.contains("\"")) return "\"" + s + "\"";
  return "concat('" + s.replace("'", "', \"'\", '") + "')";
}
By.xpath("//button[normalize-space()=" + q("Don't save") + "]");
def q(s: str) -> str:
    if "'" not in s:
        return f"'{s}'"
    if '"' not in s:
        return f'"{s}"'
    return "concat('" + s.replace("'", "', \"'\", '") + "')"

Never interpolate user-provided text into an XPath without a helper like this; a stray quote breaks the expression.

Searching within an element

WebElement.findElement searches the subtree of that element, but only if the XPath is relative:

WebElement card = driver.findElement(By.xpath("//article[.//h3[normalize-space()='USB-C Hub']]"));
card.findElement(By.xpath(".//button"));     // inside the card
card.findElement(By.xpath("//button"));      // WRONG: searches the whole document

The leading . is the difference. This mistake is common and silent.

Selenium 4 relative locators

Selenium 4 added RelativeLocator for “the input below this label” style queries:

import static org.openqa.selenium.support.locators.RelativeLocator.with;

WebElement email = driver.findElement(
  with(By.tagName("input")).below(By.xpath("//label[normalize-space()='Email']")));

They work on rendered position, so they are useful when the DOM relationship is messy. XPath axes (following::input[1]) do the same job from structure, and are usually more predictable. Use whichever describes the intent more clearly.

Page objects

Keep XPath strings in one place per page, as constants or By fields, with meaningful names:

public class CheckoutPage {
  private static final By CARD_NUMBER = By.xpath("//form[@data-testid='checkout']//input[@name='cardNumber']");
  private static final By PAY_BUTTON  = By.xpath("//form[@data-testid='checkout']//button[normalize-space()='Pay now']");

  private By rowRemoveButton(String product) {
    return By.xpath("//tr[td[normalize-space()=" + q(product) + "]]//button[@aria-label='Remove']");
  }
}

Parameterised locator methods (rowRemoveButton) are where XPath’s text matching shines.

Waits

XPath does not wait. Combine it with WebDriverWait:

new WebDriverWait(driver, Duration.ofSeconds(10))
  .until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@role='dialog']")));
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.XPATH, "//*[@role='dialog']")))

Frames and shadow roots

Switch into iframes before locating: driver.switchTo().frame(frameElement). For shadow DOM use element.getShadowRoot() and then CSS, because Chrome’s shadow root search context does not support XPath. See Shadow DOM and iframes.

Executing XPath in JavaScript instead

Occasionally you need something WebDriver cannot express, such as counting or reading attribute values in one round trip:

Long count = (Long) ((JavascriptExecutor) driver).executeScript(
  "return document.evaluate(arguments[0], document, null, XPathResult.NUMBER_TYPE, null).numberValue;",
  "count(//tr[td[normalize-space()='Pending']])");

Common Selenium-specific mistakes

  • Forgetting the . in .// on element searches.
  • Using By.xpath for id or name when By.id / By.name are clearer. XPath is not wrong there, just noisier.
  • Building XPath with string concatenation and no quoting helper.
  • Locating hidden duplicates (mobile nav) and getting ElementNotInteractableException. Anchor to the visible container.

Next Steps

  1. XPath in Playwright - The same locators in a locator-first API
  2. Anchoring to Stable Context - The page-object locator style
  3. Code Generator - Turn any XPath into Selenium code in five languages