Supercharge Your E2E Tests: Cross-Platform Text Selectors in Webdriver.IO
Hey Testers! Ever feel like you’re writing the same test logic three times just to cover Web, Android, and iOS? Read my article!
Supercharge Your E2E Tests: Cross-Platform Text Selectors in Webdriver.IO
Hey Testers! Ever feel like you’re writing the same test logic three times just to cover Web, Android, and iOS? Managing platform-specific selectors can be a major headache, bloating your codebase and slowing down test creation. What if there was a smoother way?
Today, let’s dive into a practical approach using Webdriver.IO to streamline your end-to-end (E2E) automation with cross-platform text-based selectors. We’ll look at real code examples that demonstrate how you can write cleaner, more maintainable tests that run seamlessly across different environments.
The Challenge: Platform-Specific Selectors
Typically, finding an element requires different strategies: CSS selectors or XPath for the web, UIAutomator (resource IDs, text, etc.) for Android, and Predicate Strings or Class Chains for iOS. Maintaining separate selectors for each element on each platform quickly becomes cumbersome.
The Solution: A Unified Selector Strategy
The core idea is to abstract away the platform differences. We define a single CrossSelector object that holds the appropriate selector string for each platform (Web, Android, iOS). Then, a base utility determines the current platform and picks the correct selector automatically.
1. The Foundation: BaseObject and CrossSelector
Let’s start with a base class that our Page Objects will inherit from. It introduces the CrossSelector type and the logic to resolve the correct selector at runtime.
// Define the structure for our cross-platform selectors
type CrossSelector = {
web: string;
android: string;
ios: string;
};
// Base class to handle selector resolution and common actions
export default class BaseObject {
// Determines the correct selector based on the current platform
private getSelector(selector: CrossSelector): string {
if (browser.isAndroid) {
return selector.android;
} else if (browser.isIOS) {
return selector.ios;
} else {
return selector.web;
}
}
// Gets the WebdriverIO element using the resolved selector
private select(selector: CrossSelector): ChainablePromiseElement {
return browser.$(this.getSelector(selector));
}
// Example common action using the cross-platform selector
public async click(selector: CrossSelector) {
await this.select(selector).click({ force: true }); // Simplified click
}
// Example wait action
public async waitForDisplayed(selector: CrossSelector, timeout: number = 10000) {
await this.select(selector).waitForDisplayed({ timeout });
}
// ... other common methods like fill, getElementText, isDisplayed etc.
}
Benefit: Your common actions (click, fill, waitForDisplayed) now work with a single CrossSelector object, regardless of the platform your test is running on.
2. Handling Text Variations: TranslatableObject
Text is tricky. “Log in” might be “Se connecter” in French. Even in the same language, button text could differ slightly between web and mobile. We can extend our BaseObject to handle this.
// Structure to hold translations for different languages
export type Translations = Record<string, Record<"en" | "fr", string>>;
// Extends BaseObject to add translation capabilities
export class TranslatableObject extends BaseObject {
_translations: Translations;
constructor(translations: Translations) {
super();
this._translations = translations;
}
// Gets the translated text for a given key and the current test language
getTranslationItem(item: keyof typeof this._translations): string {
const testLanguage = envConfig.testLanguage; // Assumes language is configured elsewhere
const targetItem = this._translations[item];
if (!targetItem) throw new Error(`Translation item not found: ${item}`);
const targetTrad: string = targetItem[testLanguage];
if (!targetTrad) throw new Error(`Translation not found: ${item}.${testLanguage}`);
return targetTrad;
}
}
(Note: We often combine this with helper functions like crossTxtSel shown in the original SignIn object, which would build a CrossSelector using the translated text for text-based selectors like Android's androidTxtSel or iOS's iosStaticTextSel.)
Benefit: Your selectors can automatically adapt to the language configured for the test run, making your tests more robust for internationalized applications.
3. Putting it Together: The SignIn Page/Screen Object
Now let’s see how a Page Object (or Screen Object for mobile) utilizes these base classes.
import { crossTxtSel, androidResourceIdSel, iosClassChainSel, iosNameSel } from "../utils/selectors.ts"; // Assuming selector helpers
import { TranslatableObject } from "@/framework/_/TranslatableObject.ts";
// Define translations specific to the Sign In screen
const translations = {
signIn: { en: "Log in", fr: "Se connecter" },
title: { en: "Connect to MyApp", fr: "Se connecter à MyApp" },
// ... other translations
};
class SignIn extends TranslatableObject {
constructor() {
super(translations); // Pass translations to the parent
}
// Selector using translated text (via a helper like crossTxtSel)
get title() {
return crossTxtSel(this.getTranslationItem("title"));
}
// Selector using specific IDs/paths per platform
get emailField() {
return {
android: androidResourceIdSel("login.email"), // e.g., `~resourceId/login.email`
ios: iosClassChainSel('**/XCUIElementTypeTextField[`name == "login.email"`]'),
web: "input#email" // CSS selector
};
}
get passwordField() {
return {
android: androidResourceIdSel("login.password"),
ios: iosClassChainSel('**/XCUIElementTypeSecureTextField[`name == "login.password"`]'),
web: "input#password"
};
}
get signInButton() {
return {
android: androidResourceIdSel("login.signinbutton"),
ios: iosNameSel('login.signinbutton'), // Accessibility ID
web: "//button[@type='submit']" // XPath
};
}
// High-level action using the cross-platform selectors
public async fillAndSubmit(email: string, password: string) {
await this.fillCharByChar(this.emailField, email, this.title); // Uses BaseObject's fill method
// ... (handle potential differences like iOS focus tap)
await this.fillCharByChar(this.passwordField, password, this.title);
// ... (handle potential captcha)
await this.waitForButtonToBe(this.signInButton, true); // Uses BaseObject's wait method
await this.click(this.signInButton); // Uses BaseObject's click method
}
// ... other page-specific actions and waits
}
// Export a single instance for tests to use
const signIn = new SignIn();
export default signIn;
Benefit: The Page Object clearly defines elements using the CrossSelector structure. High-level methods like fillAndSubmit encapsulate the interaction logic, hiding the platform-specific details from the actual test scripts.
4 & 5. The Payoff: Cleaner Test Scripts
Look how much cleaner the actual test becomes! It focuses on the what (the test steps and assertions) rather than the how (finding elements on each platform).
import signIn from "@/framework/signIn.ts";
import dashboard from "@/framework/dashboard";
import testAccounts from "@/data/testAccounts.ts";
import envConfig from "@/environment"; // For language setting
describe("signIn", () => {
it("should fail with bad password", async () => {
// Parameters
const email = testAccounts.default.email;
const password = "wrongPassword123";
// Preconditions (simplified - open app/page and wait for sign in)
await openAppOrPageAndWaitForSignIn(); // Abstracted precondition
// Actions - using the Page Object's high-level method
await signIn.fillAndSubmit(email, password);
// Assertions - using Page Object methods for verification
const expectedError = envConfig.testLanguage === "fr" ? "Identifiant et/ou mot de passe invalides" : "Invalid";
await signIn.waitForErrorMessageContaining(expectedError);
});
it("should succeed and keep session", async () => {
// Parameters
const email = testAccounts.adrian_default.email;
const password = testAccounts.adrian_default.password;
// Preconditions
await openAppOrPageAndWaitForSignIn();
// Actions
await signIn.fillAndSubmit(email, password);
// ... (handle potential mobile-specific alerts like biometrics)
await dashboard.wait(); // Verify login succeeded by waiting for dashboard
// More Actions - Relaunch app / Refresh page
await relaunchAppOrRefreshPage(); // Abstracted action
// Assertions - Still logged in?
// ... (handle potential mobile-specific alerts again)
await dashboard.wait(); // Verify dashboard is still visible
});
});
// Placeholder functions for clarity
async function openAppOrPageAndWaitForSignIn() { /* ... platform specific setup ... */ await signIn.wait(); }
async function relaunchAppOrRefreshPage() { /* ... platform specific relaunch/refresh ... */ }
Benefit:
- Readability: Tests are much easier to read and understand.
- Maintainability: If a selector changes, you update it in one place (the Page Object’s
CrossSelector) instead of three. If text changes, you update thetranslations. - Efficiency: Writing new cross-platform tests becomes significantly faster.
- Robustness: Centralizing selector logic reduces the chance of errors.
Wrapping Up
By abstracting platform differences using CrossSelector objects within a BaseObject, potentially adding a translation layer, and encapsulating interactions in Page Objects, you can create a powerful and maintainable E2E testing framework with Webdriver.IO. Your tests become cleaner, more resilient, and easier to manage across Web, Android, and iOS.
Give this approach a try and see how it can enhance your cross-platform testing efforts! Happy testing!
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don’t receive any funding, we do this to support the community. ❤️
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, **Instagram. You can also subscribe to our [weekly newsletter](https://newsletter.plainenglish.io/)**.
And before you go, don’t forget to clap and follow the writer️!
메타데이터
- post_id
- 25508e40fc86
- slug
- supercharge-your-e2e-tests-cross-platform-text-selectors-in-webdriver-io-25508e40fc86
- url
- https://medium.com/@adrianpothuaud/supercharge-your-e2e-tests-cross-platform-text-selectors-in-webdriver-io-25508e40fc86
- canonical_url
- https://medium.com/@adrianpothuaud/supercharge-your-e2e-tests-cross-platform-text-selectors-in-webdriver-io-25508e40fc86
- author_url
- https://medium.com/@adrianpothuaud
- status
- ok
- fetched_at
- 2026-07-15 04:21:51