Skip to navigation

XPath selectors

XPath (XML Path Language) selectors provide a powerful alternative to CSS selectors for targeting elements in Doc Detective tests. While more complex than CSS selectors, XPath offers advanced capabilities for navigating complex HTML structures and selecting elements based on their content or relationships.

When to use XPath selectors

Use XPath selectors in Doc Detective actions like find, click, and others that accept a selector property. XPath is useful when:

  • You need to select elements based on their text content
  • You need to navigate up the DOM tree (select parent elements)
  • CSS selectors aren’t powerful enough for your use case
  • You need to select elements by their position relative to other elements

For most use cases, CSS selectors are simpler and more readable. Use XPath only when CSS selectors can’t accomplish what you need.

XPath syntax basics

On browser surfaces, Doc Detective treats a selector starting with //, /, or ( as XPath (otherwise it treats the selector as CSS). The ( case covers grouped expressions like (//button)[2], used to index or filter a node set. See Position-based selection.

On native app surfaces, Doc Detective recognizes only // and ( as XPath. A bare single / doesn’t count. Stick to // (or a (...) grouped expression) if the same selector needs to work on both browser and app surfaces.

Absolute vs. relative paths

  • Absolute path: Starts with a single / and specifies the complete path from the root

    /html/body/div/form/input
    Avoid absolute paths

    They’re fragile and break when the page structure changes.

  • Relative path: Starts with // and finds elements anywhere in the document

    //input[@type='password']
    Prefer relative paths

    They’re more flexible and maintainable.

Basic XPath patterns

Select by element type

Find all elements of a specific type anywhere in the document:

{
"find": {
"selector": "//button"
}
}

Select by attribute

Find elements with a specific attribute value using the [@attribute='value'] syntax:

{
"find": {
"selector": "//input[@type='password']"
}
}

Select by ID

Target elements by their id attribute using the @id selector:

{
"find": {
"selector": "//*[@id='username']"
}
}

The * matches any element type. You can also be more specific: //input[@id='username'].

Select by class

Use the contains() function to match elements by their class attribute:

{
"find": {
"selector": "//*[contains(@class, 'submit-button')]"
}
}

Use contains() because an element can have multiple classes. This matches any element whose class attribute contains “submit-button”.

Select by text content

One of XPath’s most powerful features is selecting elements by their visible text:

{
"find": {
"selector": "//button[text()='Submit']"
}
}

This finds a button element with the exact text “Submit”.

Select by partial text

Use the contains() function to match elements that contain specific text:

{
"find": {
"selector": "//button[contains(text(), 'Submit')]"
}
}

This finds a button element whose text contains “Submit” (like “Submit Form” or “Click to Submit”).

Combining conditions

Multiple attributes

Use the and operator to combine multiple attribute conditions:

{
"find": {
"selector": "//input[@type='text' and @name='email']"
}
}

OR conditions

Use the or operator to match elements that satisfy any of the specified conditions:

{
"find": {
"selector": "//button[@type='submit' or @type='button']"
}
}

Element type and attribute

Combine element types with attribute matching and text content for highly specific selectors:

{
"find": {
"selector": "//button[@class='primary' and contains(text(), 'Sign Up')]"
}
}

Parent elements

Select the parent of an element using the parent:: axis or the .. shorthand:

{
"find": {
"selector": "//input[@type='email']/parent::div"
}
}

Or use the shorter form:

{
"find": {
"selector": "//input[@type='email']/.."
}
}

Ancestor elements

Find any ancestor element (not just the direct parent) using the ancestor:: axis:

{
"find": {
"selector": "//input[@type='email']/ancestor::form"
}
}

Following siblings

Select sibling elements that come after the current element using the following-sibling:: axis:

{
"find": {
"selector": "//label[text()='Email']/following-sibling::input"
}
}

This finds an input that comes after a label with text “Email”.

Preceding siblings

Select sibling elements that come before the current element using the preceding-sibling:: axis:

{
"find": {
"selector": "//button[@type='submit']/preceding-sibling::input"
}
}

Descendant elements

Find elements nested at any depth within other elements using the // operator:

{
"find": {
"selector": "//div[@class='form-group']//input"
}
}

This finds any input within a div with class “form-group”, regardless of nesting depth.

Position-based selection

Select by index

Select a specific occurrence using square brackets with a number (XPath indexes start at 1):

{
"find": {
"selector": "(//button)[2]"
}
}

This selects the second button element in the document.

Use parentheses to select from all matching elements. Without them, //button[2] means “a button that is the second button child of its parent.”

First element

Select the first matching element using index [1]:

{
"find": {
"selector": "(//input[@type='text'])[1]"
}
}

Last element

Select the last matching element using the last() function:

{
"find": {
"selector": "(//button)[last()]"
}
}

Position greater than

Select elements beyond a certain position using the position() function:

{
"find": {
"selector": "(//li)[position()>2]"
}
}

This selects all li elements from the third onwards.

Advanced functions

Attribute contains

Match elements whose attributes contain a specific substring:

{
"find": {
"selector": "//*[contains(@data-testid, 'user')]"
}
}

Attribute starts with

Match elements whose attributes begin with a specific value:

{
"find": {
"selector": "//*[starts-with(@id, 'user-')]"
}
}

Case-insensitive text matching

XPath 2.0+ supports case-insensitive matching, but browser support varies. For compatibility, use:

{
"find": {
"selector": "//button[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'submit')]"
}
}

This converts text to lowercase before matching.

Not operator

Exclude elements with specific attributes using the not() function:

{
"find": {
"selector": "//button[not(@disabled)]"
}
}

This finds enabled buttons (those without a disabled attribute).

Best practices

Prefer CSS selectors when possible

XPath is powerful but complex. Use CSS selectors for more straightforward cases:

CSS

#username or [data-testid='login-button']

XPath

//*[@id='username'] or //*[@data-testid='login-button']

Use relative paths

Good

//input[@type='password']

Avoid

/html/body/div[1]/form/div[2]/input[3]

Combine text and attributes for specificity

Create highly specific selectors by combining text content with attributes:

{
"find": {
"selector": "//button[@type='submit' and contains(text(), 'Sign Up')]"
}
}

Test selectors in browser console

Use the $x() function in the browser console to test XPath selectors:

$x("//button[contains(text(), 'Submit')]")

This returns an array of matching elements.

Avoid brittle position-based selectors

Fragile

//div[3]/span[2]/a[1]

Better

//a[contains(@class, 'download-link')]

Common patterns

Find input by label text

Target an input field based on its associated label text:

{
"find": {
"selector": "//label[text()='Email']/following-sibling::input"
}
}

Or if the input is nested inside the label:

{
"find": {
"selector": "//label[contains(text(), 'Email')]//input"
}
}

Find element by placeholder

Target input fields by their placeholder text:

{
"find": {
"selector": "//input[@placeholder='Enter your email']"
}
}

Find button by aria-label

Target buttons and other elements by their ARIA accessibility labels:

{
"find": {
"selector": "//button[@aria-label='Close dialog']"
}
}

Target links that contain specific URL patterns:

{
"find": {
"selector": "//a[contains(@href, '/downloads')]"
}
}

Examples

Click a button by text

{
"steps": [
{
"description": "Click the Sign Up button",
"click": {
"selector": "//button[text()='Sign Up']"
}
}
]
}

Find input by following a label

{
"steps": [
{
"description": "Enter email address",
"find": {
"selector": "//label[text()='Email Address']/following-sibling::input",
"click": true,
"type": "user@example.com"
}
}
]
}

Find enabled submit button

{
"steps": [
{
"description": "Click the enabled submit button",
"find": {
"selector": "//button[@type='submit' and not(@disabled)]",
"click": true
}
}
]
}

Select third item in a list

{
"steps": [
{
"description": "Click the third menu item",
"click": {
"selector": "(//nav//li)[3]//a"
}
}
]
}

Find element with specific data attribute

{
"steps": [
{
"description": "Find element by test ID",
"find": {
"selector": "//*[@data-testid='user-profile']"
}
}
]
}

Debugging XPath selectors

Test in browser console

Use the $x() function (available in most browser consoles) to test and debug your XPath selectors:

// Test your XPath
$x("//button[contains(text(), 'Submit')]")
// Count matches
$x("//button[contains(text(), 'Submit')]").length
// Click the first match
$x("//button[contains(text(), 'Submit')]")[0].click()

Browser developer tools

  1. Open Developer Tools (F12)
  2. Go to the Console tab
  3. Type $x("your-xpath-here") and press Enter
  4. The console shows all matching elements
  5. Hover over results to highlight them on the page

Common issues

No elements found
  • Verify the element exists in the DOM
  • Check for typos in attribute names or values
  • Ensure the element is loaded (you may need to add a wait action)
Multiple elements found
  • Add more specific conditions
  • Combine multiple attributes
  • Use text content to narrow the match
XPath treated as CSS
  • Doc Detective detects the selector type automatically from the leading characters.
  • On browser surfaces, start your XPath with //, /, or (. Doc Detective treats anything else as CSS.
  • On native app surfaces, only // and ( count as XPath. A bare single / is treated as CSS.