Element Identification
Overview
Element identification is the foundation of GUI test automation. This guide covers all the methods available in SHAFT Engine to locate and interact with web elements, including traditional locators, the SHAFT Locator Builder, relative locators, shadow DOM elements, and elements within iframes.
Supported Locator Types
SHAFT Engine supports all standard Selenium locator strategies through the By class:
1. ID
Locates elements by their unique id attribute - the most reliable and fastest method when available.
By elementLocator = By.id("username");
2. Name
Locates elements by their name attribute, commonly used for form fields.
By elementLocator = By.name("email");
3. Class Name
Locates elements by their CSS class name.
By elementLocator = By.className("btn-primary");
4. Tag Name
Locates elements by their HTML tag name (e.g., button, input, div).
By elementLocator = By.tagName("button");
5. CSS Selector
Locates elements using CSS selector syntax - powerful and flexible.
By elementLocator = By.cssSelector(".form-control[type='text']");
By elementLocator = By.cssSelector("#login-form > input.username");
6. XPath
Locates elements using XPath expressions - very powerful but can be slower than CSS selectors.
By elementLocator = By.xpath("//input[@id='username']");
By elementLocator = By.xpath("//button[contains(text(),'Submit')]");
7. Link Text
Locates anchor (<a>) elements by their exact text content.
By elementLocator = By.linkText("Click Here");
8. Partial Link Text
Locates anchor elements by partial text match.
By elementLocator = By.partialLinkText("Click");
Traditional Locators vs. SHAFT Locator Builder
SHAFT Engine provides a fluent API for building locators that's more readable and maintainable than traditional approaches.
Example 1: Simple Button
HTML:
<button id="submit-btn" class="btn-primary" data-test="submit">Submit</button>
Native Selenium Approach:
// Using ID
By button = By.id("submit-btn");
// Using CSS Selector
By button = By.cssSelector("button[data-test='submit']");
// Using XPath
By button = By.xpath("//button[@data-test='submit']");
SHAFT Locator Builder Approach:
// More readable and self-documenting
By button = SHAFT.GUI.Locator.hasTagName("button")
.hasAttribute("data-test", "submit")
.build();
// Or using ID
By button = SHAFT.GUI.Locator.hasId("submit-btn").build();
Example 2: Complex Element
HTML:
<div class="card product-card">
<span class="price" data-currency="USD">$99.99</span>
</div>
Native Selenium Approach:
By priceElement = By.cssSelector(".product-card .price[data-currency='USD']");
By priceElement = By.xpath("//div[contains(@class,'product-card')]//span[@data-currency='USD']");
SHAFT Locator Builder Approach:
By priceElement = SHAFT.GUI.Locator.hasTagName("span")
.containsClass("price")
.hasAttribute("data-currency", "USD")
.build();
Example 3: Element with Text
HTML:
<button class="action-button">Add to Cart</button>
Native Selenium Approach:
By button = By.xpath("//button[contains(text(),'Add to Cart')]");
By button = By.cssSelector("button.action-button"); // Can't filter by text with CSS
SHAFT Locator Builder Approach:
By button = SHAFT.GUI.Locator.hasTagName("button")
.containsText("Add to Cart")
.build();
SHAFT Locator Builder Methods
The SHAFT Locator Builder provides a fluent API with the following methods:
Tag-Based Methods
// Exact tag name
SHAFT.GUI.Locator.hasTagName("button").build();
// Any tag name (when you want to filter by other attributes)
SHAFT.GUI.Locator.hasAnyTagName().hasAttribute("data-test", "submit").build();
Attribute-Based Methods
// Element with specific attribute
SHAFT.GUI.Locator.hasAttribute("data-test").build();
// Element with attribute and value
SHAFT.GUI.Locator.hasAttribute("data-test", "add-to-cart").build();
// Element with ID
SHAFT.GUI.Locator.hasId("username").build();
// Element containing a class
SHAFT.GUI.Locator.containsClass("btn-primary").build();
Text-Based Methods
// Element containing specific text
SHAFT.GUI.Locator.containsText("Submit").build();
// Element containing ID substring
SHAFT.GUI.Locator.containsId("user").build();
Chaining Methods
// Combine multiple conditions
By complexLocator = SHAFT.GUI.Locator.hasTagName("input")
.hasAttribute("type", "text")
.containsClass("form-control")
.containsId("user")
.build();
Relative (Location-Based) Locators
Selenium 4 introduced relative locators that allow you to locate elements based on their position relative to other elements. SHAFT Engine fully supports these.
Available Relative Locator Methods
above()- Locates elements above the reference elementbelow()- Locates elements below the reference elementtoLeftOf()- Locates elements to the left of the reference elementtoRightOf()- Locates elements to the right of the reference elementnear()- Locates elements near (within approximately 50 pixels) of the reference element
Example: Login Form
HTML:
<form>
<label>Username:</label>
<input id="username" type="text">
<label>Password:</label>
<input id="password" type="password">
<button>Login</button>
</form>
Using Relative Locators:
import static org.openqa.selenium.support.locators.RelativeLocator.with;
// Locate password field relative to username field
By usernameField = By.id("username");
By passwordField = with(By.tagName("input")).below(usernameField);
// Locate the login button below password field
By loginButton = with(By.tagName("button")).below(passwordField);
// Locate label to the left of username field
By usernameLabel = with(By.tagName("label")).toLeftOf(usernameField);
Example: Product Grid
HTML:
<div class="grid">
<div class="product" id="product-1">Product 1</div>
<div class="product" id="product-2">Product 2</div>
<div class="product" id="product-3">Product 3</div>
</div>
Using Relative Locators:
// Locate product-2 relative to product-1
By product1 = By.id("product-1");
By product2 = with(By.className("product")).toRightOf(product1);
// Locate product near product-1 (within ~50px)
By nearbyProduct = with(By.className("product")).near(product1);
Combining Relative Locators
You can chain multiple relative locator conditions:
By referenceElement = By.id("reference");
By targetElement = with(By.tagName("input"))
.below(referenceElement)
.toRightOf(By.id("another-reference"));
Using Relative Locators with SHAFT
driver = new SHAFT.GUI.WebDriver();
driver.browser().navigateToURL("https://example.com");
// Define reference element
By referenceElement = By.id("username");
// Use relative locator
By relativeElement = with(By.tagName("input")).below(referenceElement);
// Interact with the element
driver.element().type(relativeElement, "test@example.com");
Interacting with Shadow DOM
Shadow DOM encapsulates parts of a web component's DOM tree. SHAFT Locator Builder provides special support for locating elements within shadow DOM.
What is Shadow DOM?
Shadow DOM allows developers to attach a hidden, separate DOM tree to an element. Elements within the shadow DOM are not accessible through normal DOM traversal methods.
Locating Shadow DOM Elements
Example:
<shop-app>
#shadow-root
<header>
<a href="/list/mens_outerwear">Men's Outerwear</a>
</header>
</shop-app>
Native Selenium Approach:
// Requires JavaScript execution
WebElement shadowHost = driver.findElement(By.tagName("shop-app"));
WebElement shadowRoot = (WebElement) ((JavascriptExecutor) driver)
.executeScript("return arguments[0].shadowRoot", shadowHost);
WebElement link = shadowRoot.findElement(By.cssSelector("a[href='/list/mens_outerwear']"));
SHAFT Locator Builder Approach:
driver = new SHAFT.GUI.WebDriver();
driver.browser().navigateToURL("https://shop.polymer-project.org/");
// Define the shadow host
By shadowHost = SHAFT.GUI.Locator.hasTagName("shop-app").build();
// Locate element inside shadow DOM
By shadowElement = SHAFT.GUI.Locator.hasTagName("a")
.hasAttribute("href", "/list/mens_outerwear")
.insideShadowDom(shadowHost)
.build();
// Interact with the element
driver.element().click(shadowElement);
Nested Shadow DOM
For deeply nested shadow DOM structures:
// First shadow host
By shadowHost1 = SHAFT.GUI.Locator.hasTagName("outer-component").build();
// Second shadow host inside the first shadow DOM
By shadowHost2 = SHAFT.GUI.Locator.hasTagName("inner-component")
.insideShadowDom(shadowHost1)
.build();
// Target element inside the nested shadow DOM
By targetElement = SHAFT.GUI.Locator.hasTagName("button")
.hasAttribute("id", "submit")
.insideShadowDom(shadowHost2)
.build();
driver.element().click(targetElement);
Complete Shadow DOM Example
public class ShadowDomTest {
SHAFT.GUI.WebDriver driver;
@Test
public void testShadowDomElements() {
driver = new SHAFT.GUI.WebDriver();
driver.browser().navigateToURL("https://shop.polymer-project.org/");
// Locate shadow host
By shadowHost = SHAFT.GUI.Locator.hasTagName("shop-app").build();
// Locate element inside shadow DOM
By menuLink = SHAFT.GUI.Locator.hasTagName("a")
.containsText("Men's Outerwear")
.insideShadowDom(shadowHost)
.build();
// Click the element
driver.element().click(menuLink);
// Verify navigation
driver.browser().assertThat().url().contains("/list/mens_outerwear");
driver.quit();
}
}
For more examples, visit ShadowDomTest on GitHub.
Interacting with IFrames
IFrames (Inline Frames) are HTML elements that embed another HTML page within the current page. To interact with elements inside an iframe, you must first switch the driver's focus to that iframe.
Basic IFrame Handling
Switching to an IFrame:
By iframeLocator = By.id("iframe-id");
driver.element().switchToIframe(iframeLocator);
Switching Back to Main Content:
driver.element().switchToDefaultContent();
Example: Text Editor in IFrame
HTML:
<iframe id="mce_0_ifr">
<body>
<p id="tinymce">Editable content</p>
</body>
</iframe>
Native Selenium Approach:
WebDriver driver = new ChromeDriver();
driver.get("https://the-internet.herokuapp.com/tinymce");
// Switch to iframe
WebElement iframe = driver.findElement(By.id("mce_0_ifr"));
driver.switchTo().frame(iframe);
// Interact with element inside iframe
WebElement textField = driver.findElement(By.id("tinymce"));
textField.clear();
textField.sendKeys("New text");
// Switch back to main content
driver.switchTo().defaultContent();
SHAFT Approach:
driver = new SHAFT.GUI.WebDriver();
driver.browser().navigateToURL("https://the-internet.herokuapp.com/tinymce");
// Locate the iframe
By textIframe = By.id("mce_0_ifr");
By textField = By.id("tinymce");
// Switch focus to iframe
driver.element().switchToIframe(textIframe);
// Interact with elements inside the iframe
driver.element().typeAppend(textField, "This text is added");
driver.element().clipboardActions(textField, "select all");
driver.element().clipboardActions(textField, "copy");
// Switch back to default content
driver.element().switchToDefaultContent();
Nested IFrames
For nested iframes, switch to each iframe sequentially:
// Switch to outer iframe
By outerIframe = By.id("outer-iframe");
driver.element().switchToIframe(outerIframe);
// Switch to inner iframe
By innerIframe = By.id("inner-iframe");
driver.element().switchToIframe(innerIframe);
// Interact with element in nested iframe
By targetElement = By.id("target");
driver.element().click(targetElement);
// Switch back to default content (exits all iframes)
driver.element().switchToDefaultContent();
IFrame Switching by Index
You can also switch to an iframe by its index (zero-based):
// Switch to the first iframe on the page
driver.element().switchToIframe(By.xpath("(//iframe)[1]"));
Complete IFrame Example
public class IFrameTest {
SHAFT.GUI.WebDriver driver;
@Test
public void testIFrameInteraction() {
driver = new SHAFT.GUI.WebDriver();
driver.browser().navigateToURL("https://the-internet.herokuapp.com/tinymce");
// Define locators
By textIframe = By.id("mce_0_ifr");
By textField = By.id("tinymce");
// Switch to iframe
driver.element().switchToIframe(textIframe);
// Get current text
String originalText = driver.element().getText(textField);
System.out.println("Original text: " + originalText);
// Modify text
driver.element().type(textField, "New content added by SHAFT");
// Verify the change
String newText = driver.element().getText(textField);
System.out.println("New text: " + newText);
// Switch back to main content
driver.element().switchToDefaultContent();
driver.quit();
}
}
IFrame Best Practices
- Always switch back to default content when done interacting with iframe elements
- Use explicit locators for iframes rather than relying on index-based switching when possible
- Handle iframe loading by ensuring the iframe is present before switching
- Be aware of nested iframes - you need to switch through each level
- Consider using unique IDs for iframes when you control the HTML
Combining Techniques
You can combine different element identification techniques for complex scenarios:
Example: Element in IFrame with SHAFT Locator Builder
By iframeLocator = SHAFT.GUI.Locator.hasTagName("iframe")
.hasAttribute("id", "content-frame")
.build();
driver.element().switchToIframe(iframeLocator);
By buttonInIframe = SHAFT.GUI.Locator.hasTagName("button")
.containsText("Submit")
.hasAttribute("data-test", "submit-btn")
.build();
driver.element().click(buttonInIframe);
driver.element().switchToDefaultContent();
Example: Relative Locator for Element in Shadow DOM
By shadowHost = SHAFT.GUI.Locator.hasTagName("custom-component").build();
By referenceElement = SHAFT.GUI.Locator.hasId("reference")
.insideShadowDom(shadowHost)
.build();
// Use relative locator for element below the reference
By targetElement = with(By.tagName("input")).below(referenceElement);
driver.element().type(targetElement, "value");
XPath Axis Navigation
The SHAFT Locator Builder exposes a fluent XPath axis API that lets you navigate DOM relationships — parent, sibling, child, ancestor, and more — without writing raw XPath strings.
Available Axes
| Axis | Description |
|---|---|
parent() | The direct parent element |
ancestor(tag) | The nearest ancestor with the given tag |
child(tag) | A direct child element with the given tag |
followingSibling(tag) | The next sibling element with the given tag |
precedingSibling(tag) | The previous sibling element with the given tag |
following(tag) | Any element that follows in document order |
preceding(tag) | Any element that precedes in document order |
descendant(tag) | Any descendant element with the given tag |
Example: Locate Input via Associated Label
// Find the input field that follows the "Email" label
By emailInput = SHAFT.GUI.Locator.hasTagName("label")
.hasText("Email")
.byAxis().followingSibling("input")
.build();
driver.element().type(emailInput, "user@example.com");
Example: Navigate to an Ancestor
// Find the wrapping <div> that contains an error <span>
By errorWrapper = SHAFT.GUI.Locator.hasTagName("span")
.containsText("Required field")
.byAxis().ancestor("div")
.build();
driver.assertThat().element(errorWrapper).attribute("class").contains("error").perform();
Example: Navigate to a Child Element
// First <li> inside the navigation <ul>
By firstNavItem = SHAFT.GUI.Locator.hasTagName("ul")
.containsClass("nav-menu")
.byAxis().child("li")
.build();
driver.element().click(firstNavItem);
Example: Preceding Sibling
// The <h2> heading that appears before a specific section
By sectionHeading = SHAFT.GUI.Locator.hasTagName("section")
.hasId("pricing")
.byAxis().precedingSibling("h2")
.build();
String title = driver.element().getText(sectionHeading);
Axis navigation generates optimised XPath expressions internally. You still benefit from SHAFT's smart waits and retry logic when interacting with axis-resolved elements.
ARIA Role-Based Locators
ARIA roles give you resilient, accessibility-driven locators that survive DOM refactors. Use Locator.hasRole() with the Role enum to target elements by their semantic purpose rather than fragile CSS classes or XPath.
Available Roles
// Some commonly used ARIA roles
Role.BUTTON // <button>, role="button"
Role.LINK // <a>, role="link"
Role.TEXTBOX // <input type="text">, role="textbox"
Role.SEARCHBOX // <input type="search">, role="searchbox"
Role.CHECKBOX // <input type="checkbox">, role="checkbox"
Role.RADIO // <input type="radio">, role="radio"
Role.COMBOBOX // <select>, role="combobox"
Role.NAVIGATION // <nav>, role="navigation"
Role.MAIN // <main>, role="main"
Role.DIALOG // role="dialog"
Role.ALERT // role="alert"
Role.HEADING // <h1>–<h6>, role="heading"
Role.LIST // <ul>/<ol>, role="list"
Role.LISTITEM // <li>, role="listitem"
Role.TABLE // <table>, role="table"
Example: Locate by Role
import com.shaft.driver.SHAFT;
import com.shaft.enums.internal.Role;
// Locate a submit button by role and visible text
By submitButton = SHAFT.GUI.Locator.hasRole(Role.BUTTON).hasText("Submit").build();
// Locate a search input by role
By searchInput = SHAFT.GUI.Locator.hasRole(Role.SEARCHBOX).build();
// Locate the navigation landmark
By mainNav = SHAFT.GUI.Locator.hasRole(Role.NAVIGATION).build();
driver.element().click(submitButton);
driver.element().type(searchInput, "SHAFT Engine");
Example: Role + Additional Conditions
// Dialog with a specific title
By confirmDialog = SHAFT.GUI.Locator.hasRole(Role.DIALOG)
.containsText("Confirm deletion")
.build();
// Alert message containing error text
By errorAlert = SHAFT.GUI.Locator.hasRole(Role.ALERT)
.containsText("Invalid credentials")
.build();
driver.assertThat().element(confirmDialog).exists().perform();
driver.assertThat().element(errorAlert).attribute("class").contains("error").perform();
Role-based locators improve accessibility coverage in your tests and make your test suite act as a living accessibility audit.
Smart Locators
SHAFT provides two high-level smart locators — inputField() and clickableField() — that automatically resolve to the most relevant element based on visible labels, placeholders, and ARIA attributes.
inputField()
Locates an editable field (text input, textarea, etc.) by its associated label text, placeholder, or aria-label:
// Resolves to the input associated with the "Email" label
By emailField = SHAFT.GUI.Locator.inputField("Email");
// Resolves to the input with placeholder "Search..."
By searchField = SHAFT.GUI.Locator.inputField("Search...");
// Resolves to the input with aria-label "Password"
By passwordField = SHAFT.GUI.Locator.inputField("Password");
driver.element()
.type(emailField, "user@example.com")
.and().type(passwordField, "secret");
clickableField()
Locates a clickable element (button, link, etc.) by its visible text, value attribute, or aria-label:
// Resolves to a button or link with visible text "Log In"
By loginButton = SHAFT.GUI.Locator.clickableField("Log In");
// Resolves to a button with value="Sign Up"
By signUpButton = SHAFT.GUI.Locator.clickableField("Sign Up");
driver.element().click(loginButton);
Complete Example
driver.browser().navigateToURL("https://example.com/register");
driver.element()
.type(SHAFT.GUI.Locator.inputField("First Name"), "Alice")
.and().type(SHAFT.GUI.Locator.inputField("Last Name"), "Smith")
.and().type(SHAFT.GUI.Locator.inputField("Email"), "alice@example.com")
.and().type(SHAFT.GUI.Locator.inputField("Password"), "Secure@123")
.and().click(SHAFT.GUI.Locator.clickableField("Create Account"));
Smart locators are the most resilient option for forms: they target semantic meaning rather than brittle selectors. When the DOM changes, the label or button text usually stays the same.
Best Practices
- Prefer ID locators when available - they're the fastest and most reliable
- Use SHAFT Locator Builder for complex locators - it's more readable and maintainable
- Avoid XPath when possible - CSS selectors are generally faster
- Use relative locators for dynamic layouts where absolute positions might change
- Keep locators simple - complex locators are fragile and hard to maintain
- Use data attributes (e.g.,
data-test) specifically for testing when you control the HTML - Document complex locators - add comments explaining why a particular strategy was chosen
- Test locators in isolation - verify your locators work before building tests around them