JSONPath Online logojsonpath.online
Selenium XPath · Playwright friendly

Selenium XPath Guide

Battle-tested XPath patterns for Selenium WebDriver and Playwright. Stop guessing selectors—validate them, highlight matches, and ship stable tests.

Locator checklist

  • Use data-test-id or aria-label when available.
  • Combine contains() with tag scopes to avoid over-matching.
  • Prefer relative selectors scoped to a stable parent.
  • Minimize reliance on index-based predicates unless the list is stable.

High-value patterns

  • //button[normalize-space()="Save"] — resilient CTA matching
  • //input[@name="email" or @id="email"] — tolerate differing attributes
  • //table//tr[td[contains(., "Active")]]//td[3] — scoped cell lookup
  • //div[contains(@class, "error") and not(contains(@class, "hidden"))] — visible errors

Anti-patterns to avoid

  • Absolute paths like /html/body/div[3]/div[2] — break when layout changes.
  • Text-only selectors on unstable copy—pair with attributes when possible.
  • Complex chained indexes in dynamic lists—prefer attribute beacons.
  • Ignoring wait conditions—wrap clicks in waits to avoid flaky tests.

Copy-ready patterns

Buttons and links

Exact text

//button[normalize-space(text())='Submit']

Partial text

//a[contains(text(),'Learn More')]

CTA by data-test-id

//button[@data-test-id='cta-primary']

Forms and inputs

By name

//input[@name='email']

Placeholder

//input[@placeholder='Enter email']

Label + input

//label[text()='Email']/following-sibling::input

Checkbox checked

//input[@type='checkbox' and @checked]

Dynamic content

Dynamic ID

//div[starts-with(@id, 'react-select')]

Toast message

//div[contains(@class,'toast') and contains(text(),'Saved')]

Loading finished

//*[contains(@class,'loading')][not(@hidden)]/following::*[1]

Tables

Cell in row

//table[@id='orders']//tr[3]//td[2]

Row by text

//table//tr[td[contains(text(),'Paid')]]

Column header

//table//th[normalize-space()='Status']

Language snippets

Python + Selenium

Copy & adapt
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example.com")

login_btn = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.XPATH, "//button[normalize-space()='Login']"))
)
login_btn.click()

rows = driver.find_elements(By.XPATH, "//table[@id='orders']//tr[td[contains(., 'Active')]]")
for row in rows:
    email = row.find_element(By.XPATH, ".//td[2]").text
    print(email)

driver.quit()

JavaScript + Playwright

Copy & adapt
import { chromium } from "playwright";

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto("https://example.com");

const cta = page.getByRole("button", { name: "Start free trial" });
await cta.click();

const statusCells = page.locator("xpath=//table//tr[td[contains(., 'Paid')]]//td[3]");
const statuses = await statusCells.allTextContents();
console.log(statuses);

await browser.close();

Java + Selenium

Copy & adapt
WebDriver driver = new ChromeDriver();
driver.get("https://example.com");
WebElement searchBox = driver.findElement(By.xpath("//input[@name='q']"));
searchBox.sendKeys("xpath");
List<WebElement> links = driver.findElements(By.xpath("//a[contains(text(),'XPath')]"));
for (WebElement link : links) {
  System.out.println(link.getAttribute("href"));
}
driver.quit();

C# + Selenium

Copy & adapt
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

var driver = new ChromeDriver();
driver.Navigate().GoToUrl("https://example.com");
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));

var submit = wait.Until(drv => drv.FindElement(By.XPath("//button[@type='submit' or @class='submit-btn']")));
submit.Click();

var alerts = driver.FindElements(By.XPath("//div[contains(@class,'alert') and not(contains(@class,'hidden'))]"));
foreach (var alert in alerts)
{
    Console.WriteLine(alert.Text);
}
driver.Quit();

Best practices for stability

  • Prefer semantic attributes over positional selectors. Reach for @data-test-id, @aria-label, and stable IDs.
  • Normalize whitespace when matching text: normalize-space() handles trailing spaces and line breaks.
  • Avoid absolute paths that start at /html/body; prefer short, scoped selectors under a stable container.
  • Use contains() and starts-with() for dynamic attributes instead of brittle equality checks.
  • In tables, filter rows first, then drill into cells: //tr[td[contains(.,'Active')]]//td[3].
  • Combine axes to avoid flakiness: label[text()='Email']/following-sibling::input is sturdier than //input[1].
  • Keep locators readable—future maintainers should understand intent without comments.

Fast path:

Use the XPath playground to test each locator before merging. Pair it with the cheat sheet for syntax and the DevTools guide to debug in-browser.