Element Identification Best Practices
Choosing the right element identification strategy is critical to building
stable tests. SHAFT encourages Selenium By objects and its
Locator Builder.
Use By Objects, Not @FindBy
The @FindBy annotation (from Selenium's PageFactory) has several drawbacks:
@FindBy | By Objects |
|---|---|
| Evaluated at runtime via reflection — errors appear late | Evaluated immediately — compile-time safety |
| Cannot be reused across methods easily | Standard Java objects — pass, store, and compose freely |
Tied to PageFactory.initElements() lifecycle | No initialization ceremony required |
| Hard to make dynamic or conditional | Easy to build dynamically with logic |
@FindBy with PageFactory can lead to StaleElementReferenceException issues and adds unnecessary complexity. SHAFT's built-in element handling with By objects already provides smart waits, auto-scrolling, and retry logic.
Recommended Pattern
Define your locators as By constants in your Page Object class:
import org.openqa.selenium.By;
public class LoginPage {
// Locators as By objects
private final By usernameInput = By.id("username");
private final By passwordInput = By.id("password");
private final By loginButton = By.cssSelector("button[type='submit']");
private final By errorMessage = By.className("error-message");
}
Dynamic Element Identification
Sometimes a locator depends on runtime data — a product name, a row index, or a user-provided value. With By objects you can build locators dynamically:
public By getProductAddToCartButton(String productName) {
return By.cssSelector("div[data-product='" + productName + "'] button.add-to-cart");
}
public By getTableCell(int row, int col) {
return By.cssSelector("table tr:nth-child(" + row + ") td:nth-child(" + col + ")");
}
Using SHAFT Locator Builder for Dynamic Locators
The SHAFT Locator Builder provides a fluent API for building locators without raw XPath or CSS:
public By getProductButton(String productName) {
return SHAFT.GUI.Locator.hasTagName("button")
.hasAttribute("data-product", productName)
.build();
}
Cross-Platform Locators (Android and iOS)
When testing the same app on both Android and iOS, the locators are often different. The recommended pattern is to use a helper method or enum that returns the correct locator based on the current platform:
import org.openqa.selenium.By;
import org.openqa.selenium.Platform;
import com.shaft.driver.SHAFT;
public class LoginPage {
private By getUsernameInput() {
if (SHAFT.Properties.platform.targetPlatform().equals(Platform.ANDROID.name())) {
return By.id("com.example.app:id/username_input");
} else {
return By.name("username_field");
}
}
private By getLoginButton() {
if (SHAFT.Properties.platform.targetPlatform().equals(Platform.ANDROID.name())) {
return By.id("com.example.app:id/login_btn");
} else {
return By.xpath("//XCUIElementTypeButton[@name='Login']");
}
}
}
For a cleaner approach, you can centralize platform-specific locators in a constants file or use an enum-based strategy to avoid if-else blocks throughout your page objects. See the Cross-Platform Strategy page for more details.
Best Practices Summary
- Use
Byobjects — They are simple, composable, and work naturally with SHAFT's element handling. - Avoid
@FindBy/PageFactory— It adds complexity without benefits when using SHAFT. - Build locators dynamically when they depend on runtime data.
- Use SHAFT Locator Builder for readable, maintainable locators without raw XPath or CSS.
- Abstract platform differences behind helper methods when supporting both Android and iOS.