XPathy: A Fluent API for Writing Smarter, Cleaner XPath in Selenium
XPathy is a lightweight Java library that simplifies the creation of XPath expressions to be used in Selenium. Instead of manually writing long, error‑prone strings, XPathy allows you to build expressions using a fluent API. This makes your locators more readable, maintainable, and scalable. XPathy takes away the frustration of balancing brackets, quotes, and functions, letting developers focus on expressing intent clearly.
When you create an XPathy object, you can call .getLocator() to return a Selenium By object, or call .toString() to get the XPath, making it directly usable in your automation scripts. XPathy is compatible with any Selenium version 3.0 or higher with any Java or Kotlin versions.
.getLocator() // Returns Selenium By object
.toString() // Returns raw XPath string
Repository, Author & Package
- Repository: https://github.com/Volta-Jebaprashanth/xpathy
- Author: Volta Jebaprashanth ([email protected])
- Package:
package com.xpathy;
Installation (via JitPack)
To use this library in your Maven project (pom.xml):
1. Add the JitPack repository
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
2. Add the XPathy dependency
<dependencies>
<dependency>
<groupId>com.github.Volta-Jebaprashanth</groupId>
<artifactId>xpathy</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
A - Basic Operations
Attributes are the most common entry point for XPath locators. XPathy exposes all HTML attributes as objects, each with chainable methods.
import static com.xpathy.Attribute.*;
1. Working with Attributes
-
Contains on id
XPathy locator = id.contains("login-button"); // Result: //*[contains(@id, 'login-button')] -
Equals on class
XPathy locator = class_.equals("active"); // Result: //*[@class='active'] -
StartsWith on data-testid
XPathy locator = data_testid.startsWith("menu-"); // Result: //*[starts-with(@data-testid, 'menu-')] -
Numeric comparisons on value
XPathy locator = value.greaterThan(100); // Result: //*[@value > 100] XPathy locator = value.lessThan(50); // Result: //*[@value < 50]
Additional methods include:
haveIt()→ checks whether an attribute existsisEmpty()→ confirms an attribute is present but emptyisNumeric()→ ensures an attribute’s value is numeric
2. Attributes within Specific Tags
XPathy allows scoping attributes inside specific HTML tags, making locators more precise.
import static com.xpathy.Tag.*;
// Find a div by id
XPathy locator = div.byAttribute(id).equals("main-container");
// Result: //div[@id='main-container']
// Find a h2 by class
XPathy locator = h2.byAttribute(class_).equals("section-title");
// Result: //h2[@class='section-title']
// Find a p by data-testid starting with text
XPathy locator = p.byAttribute(data_testid).startsWith("paragraph-");
// Result: //p[starts-with(@data-testid, 'paragraph-')]
Note: Every attribute method (equals, contains, startsWith, greaterThan, etc.) works with every supported tag.
3. Working with Text Content
XPathy provides intuitive methods for targeting visible text inside elements.
// Text contains
XPathy locator = div.byText().contains("Welcome");
// Result: //div[contains(text(), 'Welcome')]
// Text starts with
XPathy locator = h2.byText().startsWith("Chapter");
// Result: //h2[starts-with(text(), 'Chapter')]
// Global Text usage
XPathy locator = Text.contains("Error");
// Result: //*[contains(text(), 'Error')]
4. Numeric Values Inside Elements
Some elements display numbers, such as counters or prices. XPathy lets you build conditions around them.
// Greater than numeric content
XPathy locator = td.byNumber().greaterThan(10);
// Result: //td[number(text()) > 10]
// Between numeric values
XPathy locator = span.byNumber().between(5, 15);
// Result: //span[number(text()) >= 5 and number(text()) <= 15]
5. Working with Styles
Inline styles can be targeted when attributes or text are insufficient.
// Check inline style for background colour within a tag
XPathy locator = div.byStyle(backgroundColor).equals("#000000");
// Result: //div[contains(translate(@style, ' ', ''), 'background-color:#000000;')]
// Check inline style directly
import static com.xpathy.Style.*;
XPathy locator = backgroundColor.equals("#000000");
// Result: //*[contains(translate(@style, ' ', ''), 'background-color:#000000;')]
B - Understanding the Architecture Flow
XPathy follows a layered architecture for building locators. Each starting point such as
.byText(), .byAttribute(), .byNumber(), or
.byStyle() returns a builder object that knows how to handle that context:
.byText()→ switches context to element text, allowing operations like.equals(),.contains(),.startsWith()..byAttribute(attribute)→ switches context to a specific attribute, enabling methods such as.equals(),.contains(),.startsWith(),.greaterThan(),.lessThan()..byNumber()→ converts inner text into a number, making numeric methods like.greaterThan(),.lessThan(),.between()available..byStyle(styleAttribute)→ inspects inline CSS properties inside the style attribute, and supports.equals(),.haveIt().
How methods are chained
When you call .equals(), .contains(), .startsWith(), etc., you are
finalizing the condition on the selected context. For example:
XPathy locator = div.byAttribute(id).equals("header");
Flow:
divsets the base tag..byAttribute(id)selects the id attribute..equals("header")finalizes the expression as//div[@id='header'].
C - Basic Logical Operations
XPathy also supports combining multiple conditions with logical operators. These map directly to XPath
and(), or(), and not() constructs, but with a fluent, chainable
API that preserves readability.
1. and()
XPathy locator = div.byAttribute(id).equals("main-container")
.and()
.byText().contains("Hello World");
// Result: //div[@id='main-container' and contains(text(), 'Hello World')]
2. or()
XPathy locator = div.byAttribute(id).equals("main-container")
.or()
.byText().contains("Hello World");
// Result: //div[@id='main-container' or contains(text(), 'Hello World')]
3. not()
XPathy locator = div.byText().contains("Hello World")
.and()
.byAttribute(id).not().equals("main-container");
// Result: //div[contains(text(), 'Hello World') and not(@id='main-container')]
4. Chaining Multiple Logical Operations
XPathy locator = span.byText().contains("Discount")
.and()
.byAttribute(class_).not().equals("expired")
.or()
.byNumber().greaterThan(50);
// Result: //span[contains(text(), 'Discount') and not(@class='expired') or number(text()) > 50]
D - DOM Navigation
XPathy provides intuitive methods for navigating the DOM tree. These methods allow you to traverse relationships between elements—moving up, down, or sideways—while still chaining into text, attributes, numbers, or styles.
All navigation methods starts with $ made you easy to extend it with a traversal operator.
// 1. $tag(tag)
XPathy locator = div.byAttribute(class_).equals("container")
.$tag(button)
.byText().equals("Submit");
// Result: //div[@class='container']//button[text()='Submit']
// 2. $child()
XPathy locator = ul.byAttribute(id).equals("menu")
.$child()
.byText().contains("Home");
// Result: //ul[@id='menu']/child::*[contains(text(), 'Home')]
// Specific child tag
XPathy locator = ul.byAttribute(id).equals("menu")
.$child(li)
.byText().contains("Contact");
// Result: //ul[@id='menu']/child::li[contains(text(), 'Contact')]
// 3. $ancestor()
XPathy locator = a.byAttribute(href).contains("profile")
.$ancestor()
.byAttribute(id).equals("navbar");
// Result: //a[contains(@href, 'profile')]/ancestor::*[@id='navbar']
// 4. $descendant()
XPathy locator = section.byAttribute(id).equals("content")
.$descendant(p)
.byText().contains("Welcome");
// Result: //section[@id='content']/descendant::p[contains(text(), 'Welcome')]
// 5. $parent() and $up()
XPathy locator = span.byText().equals("$19.99")
.$parent(div)
.byAttribute(class_).equals("product");
// Result: //span[text()='$19.99']/parent::div[@class='product']
// 6. $followingSibling()
XPathy locator = label.byText().equals("Username")
.$followingSibling(input)
.byAttribute(type).equals("text");
// Result: //label[text()='Username']/following-sibling::input[@type='text']
// 7. $precedingSibling()
XPathy locator = li.byText().equals("Contact")
.$precedingSibling()
.byText().equals("About");
// Result: //li[text()='Contact']/preceding-sibling::*[text()='About']
// 8. Multiple Navigations
XPathy locator = div.byAttribute(id).contains("main-container")
.$parent()
.$followingSibling(div)
.$descendant()
.byText().contains("Hello World");
// Result: //div[contains(@id, 'main-container')]/../following-sibling::div/descendant::*[contains(text(), 'Hello World')]
E - Value Transformations
One of the most powerful features of XPathy is the ability to transform values before applying conditions. Transformations make locators more robust against variations in casing, whitespace, special characters, numbers, or accented characters.
1. Case Transformations
import static com.xpathy.Case.*;
// Ignore Case
XPathy locator = button.byAttribute(id)
.withCase(IGNORED)
.contains("login-button");
// Result: //button[contains(translate(@id, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'login-button')]
// Force Uppercase
XPathy locator = label.byText()
.withCase(UPPER)
.equals("USERNAME");
// Result: //label[translate(text(), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')='USERNAME']
// Force Lowercase
XPathy locator = div.byAttribute(class_)
.withCase(LOWER)
.equals("active");
// Result: //div[translate(@class, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')='active']
2. Whitespace Handling
// Normalize Space
XPathy locator = div.byText()
.withNormalizeSpace()
.equals("Invalid password");
// Result: //div[normalize-space(text())='Invalid password']
3. Character Filtering (Keep or Remove)
import static com.xpathy.Only.*;
// Keep Only
XPathy locator = span.byText()
.withKeepOnly(ENGLISH_ALPHABETS)
.contains("ProductABC");
// Result: //span[contains(translate(text(), translate(text(), 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ''), ''), 'ProductABC')]
// Keep Only with Multiple Only enums
XPathy locator = td.byText()
.withKeepOnly(ENGLISH_ALPHABETS, NUMBERS)
.equals("ORD1234");
// Result: //td[translate(text(), translate(text(), 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ''), '')='ORD1234']
// Remove Only
XPathy locator = span.byText()
.withRemoveOnly(SPECIAL_CHARACTERS)
.contains("1999");
// Result: //span[contains(translate(text(), concat('!@#$%^&*()_+-=[]{}|;:,./<>?`~' , "'", '"'), ''), '1999')]
4. Character Translation
XPathy locator = h1.byText()
.withTranslate("éàè", "eae")
.contains("Cafe");
// Result: //h1[contains(translate(text(), 'éàè', 'eae'), 'Cafe')]
5. Combining Multiple Transformations
XPathy locator = div.byText()
.withNormalizeSpace()
.withRemoveOnly(NUMBERS)
.withTranslate("éàè", "eae")
.withCase(IGNORED)
.contains("premium cafe");
// Result: //div[contains(translate(translate(translate(normalize-space(text()), 'éàè', 'eae'), '0123456789', ''), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'premium cafe')]
F - Union and Intersect Logical Operations
XPathy goes beyond simple and(), or(), and not() chaining by introducing union and intersect operations. These allow you to group multiple conditions into clean, reusable blocks.
1. Union (union(Or...))
XPathy locator = button.byAttribute(id)
.union(
Or.equals("login-btn"),
Or.equals("signin-btn"),
Or.contains("auth")
);
// Result: //button[@id='login-btn' or @id='signin-btn' or contains(@id, 'auth')]
2. Intersect (intersect(And...))
XPathy locator = div.byText()
.intersect(
And.startsWith("Order #"),
And.contains("Confirmed"),
And.not().contains("Cancelled")
);
// Result: //div[starts-with(text(), 'Order #') and contains(text(), 'Confirmed') and not(contains(text(), 'Cancelled'))]
3. Using Transformations with Union and Intersect
// Union with Transformation
XPathy locator = li.byAttribute(class_)
.union(
Or.withRemoveOnly(SPECIAL_CHARACTERS).contains("active"),
Or.withCase(IGNORED).equals("selected")
);
// Intersect with Transformation
XPathy locator = span.byText()
.intersect(
And.withNormalizeSpace().contains("Premium"),
And.withCase(LOWER).contains("subscription")
);
G - Nested Logical Conditions
Instead of chaining and(), or(), and not() inline, you can use the Condition helper methods to group multiple conditions explicitly.
import static com.xpathy.Condition.*;
// Nested Login Validation
XPathy locator = div.byCondition(
and(
text().startsWith("Login"),
or(
text().contains("Button"),
attribute(id).contains("auth-btn")
),
not(attribute(class_).withCase(IGNORED).contains("disabled"))
)
);
H - Having Operations
XPathy introduces Having operations, which allow you to define conditions on related elements (child, parent, ancestor, sibling, etc.) inside the same expression.
1. Basic Having with Direct Condition
XPathy locator = div.byAttribute(class_).equals("product-card").and()
.byHaving(
span.byText().contains("In Stock")
);
// Result: //div[@class='product-card' and ( span[contains(text(), 'In Stock')] )]
2. Having with Child
XPathy locator = table.byHaving().child(
tr.byAttribute(class_).equals("total-row")
);
// Result: //table[( ./tr[@class='total-row'] )]
3. Having with Descendant
XPathy locator = section.byAttribute(id).equals("checkout").and()
.byHaving().descendant(
button.byText().contains("Place Order")
);
// Result: //section[@id='checkout' and ( .//button[contains(text(), 'Place Order')] )]
4. Having with Ancestor
XPathy locator = div.byAttribute(class_).equals("price-tag").and()
.byHaving().ancestor(
section.byAttribute(id).equals("product-details")
);
// Result: //div[@class='price-tag' and ( ancestor::section[@id='product-details'] )]
5. Having with Parent
XPathy locator = ul.byAttribute(class_).equals("menu-items").and()
.byHaving().parent(
nav.byAttribute(role).equals("navigation")
);
// Result: //ul[@class='menu-items' and ( parent::nav[@role='navigation'] )]
6. Having with Following Sibling
XPathy locator = h2.byText().equals("Features").and()
.byHaving().followingSibling(
div.byAttribute(class_).equals("description")
);
// Result: //h2[text()='Features' and ( following-sibling::div[@class='description'] )]
7. Having with Preceding Sibling
XPathy locator = li.byText().equals("Contact").and()
.byHaving().precedingSibling(
li.byText().equals("About")
);
// Result: //li[text()='Contact' and ( preceding-sibling::li[text()='About'] )]
8. Having with Simplified workflow
XPathy locator = table.byAttribute(id).equals("invoice").and()
.byHaving().child(td).byText().contains("Subtotal");
// Result: //table[@id='invoice' and ./td[contains(text(), 'Subtotal')]]
Conclusion
XPathy turns brittle, hand-written XPath into a fluent, readable DSL that scales with your UI and your team. From attribute/text/number/style contexts to robust value transformations, DOM navigation, logical composition (and/or/not, union/intersect), nested conditions, and Having operations—everything is designed to express intent clearly while compiling to pure XPath under the hood. The result is faster authoring, easier reviews, fewer flaky locators, and a test suite you can actually trust.