The challenge in robust UI automation often lies in locating an element based not just on its own attributes, but on the characteristics of its related nodes — siblings, ancestors, or descendants. Traditional XPath forces context switching (/../following-sibling::*
), leading to brittle, unreadable locators.
XPathy addresses this by separating DOM Traversal from Relationship Predication. This article explores how XPathy combines explicit DOM Navigation methods ($up()
, $descendant()
, etc.) with the powerful, context-preserving Having Operations (byHaving()
) to create readable, maintainable, and precisely targeted locators.
1. Foundation: Explicit DOM Traversal
XPathy simplifies the core XPath axes into intuitive, chained methods prefixed with $
for visual clarity. These methods are designed to change the context of the expression, moving from one node to another.
XPathy Method | XPath Axis/Shorthand | Function |
---|---|---|
$parent() / $up(n)
|
parent::* / .. |
Moves up one or more levels. |
$ancestor(tag) |
ancestor::tag |
Matches any element above the current node. |
$child(tag) |
child::tag / ./tag |
Matches immediate children. |
$descendant(tag) |
descendant::tag / //tag |
Matches elements anywhere below the current node. |
$followingSibling(tag) |
following-sibling::tag |
Matches elements after the current node at the same level. |
$precedingSibling(tag) |
preceding-sibling::tag |
Matches elements before the current node at the same level. |
Example: Chaining Traversal
XPathy locator = div.byAttribute(id).equals("main")
.$parent()
.$followingSibling(div)
.$descendant(span)
.byText().contains("Discount");
Result:
"//div[@id='main']/../following-sibling::div/descendant::span[contains(text(), 'Discount')]"
Purpose: Start at div#main
, go up one parent, then across to the next sibling div
, and finally find any span
below it.
2. Advanced Relationship Predication: Having Operations
While traversal changes context, Having Operations use traversal axes to check an attribute, text, or characteristic of a related node — without changing the locator's final target.
The byHaving()
method acts as a predicate (a filter [ ... ]
) that encapsulates a relationship test. This is essential when verifying a structural or data-driven relationship before acting on the target element.
A. Checking for a Descendant's Existence (Deep Check)
A common case is locating a container only if it holds a specific button or status message deep within.
Scenario: Find div.card
that contains a descendant button with text "Add to Cart".
div.byAttribute(class_).equals("card")
.and()
.byHaving().descendant(button.byText().equals("Add to Cart"));
Resulting XPath:
"//div[@class='card' and ( .//button[text() = 'Add to Cart'] )]"
Benefit: The final locator targets the outer div
, ensuring WebDriver interacts with the correct container.
B. Checking an Ancestor's Attribute (Context Check)
Ensure that a target element is rendered within the correct structural context — a critical safeguard against false positives.
Scenario: Find a span.price
only if its ancestor <section>
has id="featured-product"
.
span.byAttribute(class_).equals("price")
.and()
.byHaving().ancestor(section.byAttribute(id).equals("featured-product"));
Resulting XPath:
"//span[@class='price' and ( ancestor::section[@id='featured-product'] )]"
Benefit: If another span.price
exists outside the featured section, it’s ignored, ensuring accuracy.
C. Checking a Sibling's Content (Relational Data Check)
Use this when elements depend on their immediate neighbors (e.g., a label followed by an input field).
Scenario: Find an input field preceded by a label with text "Email".
input.byAttribute(type).equals("text")
.and()
.byHaving().precedingSibling(label.byText().equals("Email"));
Resulting XPath:
"//input[@type='text' and ( preceding-sibling::label[text() = 'Email'] )]"
Benefit: Precisely target the input using the label text — a stable reference for UI automation.
3. The Power of Composition: Traversal and Having Combined
XPathy allows Traversal and Having to be mixed with logical operators and transformations, unlocking advanced, human-readable locators.
Use Case: Find a div.form-container
that contains an enabled Login button.
XPathy locator = div.byAttribute(class_).equals("form-container")
.and()
.byHaving().descendant(
button.byText().contains("Login")
.and()
.byAttribute(class_).not().contains("disabled")
);
Resulting XPath:
"//div[@class='form-container' and ( .//button[contains(text(), 'Login') and not(contains(@class, 'disabled'))] )]"
Explanation: The locator targets the main form container while verifying that it contains a Login button that is not disabled.
Final Thoughts
By separating context traversal from relationship validation, XPathy empowers testers to write structural assertions directly into locators — reducing fragility and increasing readability.
This approach turns XPath from a brittle string expression into a fluent, logical representation of the DOM’s relationships, making UI automation both expressive and maintainable.
XPathy provides a lot more: Read the Full Documentation:
The Repository:
https://github.com/Volta-Jebaprashanth/xpathy
Happy Testing! 🚀
Top comments (0)