Profile
Volta Jebaprashanth.

XPathy: A Fluent API for Writing Smarter, Cleaner XPath in Selenium

#selenium #automation #xpath
10 min read

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

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

Additional methods include:

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:

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:

  1. div sets the base tag.
  2. .byAttribute(id) selects the id attribute.
  3. .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.