Locator Management in Cross-Platform Mobile Testing: A Unified Approach
Introduction
Cross-platform mobile testing can be challenging, particularly when it comes to managing locators for elements that vary across platforms. In traditional testing setups, you may need to write conditional logic (like if-else
statements) to handle platform-specific locators, leading to complex and difficult-to-maintain code. But what if there was a simpler way to manage locators across different platforms?
In this story, I’ll walk you through a unified approach for managing locators in cross-platform mobile testing that eliminates the need for complex conditionals, streamlining your test scripts and making them more scalable and maintainable.
The Problem with Traditional Approaches
When testing mobile apps on both iOS and Android, it’s common to define separate locators for each platform. Traditionally, you might end up writing platform-specific logic like this:
if (driver.isAndroid) {
return $('~password_field'); // Android-specific locator
} else if (driver.isIOS) {
return $('~password_field'); // iOS-specific locator
}
This approach leads to repetitive code and introduces complexity when maintaining locators across multiple platforms. As your app evolves and your testing needs grow, this kind of code becomes harder to manage.
The Unified Locator Management Solution
To address these issues, I propose a centralized, platform-agnostic approach that simplifies locator management. Instead of writing if-else
statements for each platform, we can load locators dynamically from a single JSON file based on the running platform. Here’s how it works:
Step 1: Structure the Locator Data
The locators for both iOS and Android are stored in a single JSON file. Each locator is mapped to its platform-specific value. For example:
{
"login_screen": {
"ios": "~login_screen_ios",
"android": "~login_screen_android"
},
"email_input": {
"ios": "~email_input_ios",
"android": "~email_input_android"
},
"submit_button": {
"ios": "~submit_button_ios",
"android": "~submit_button_android"
}
}
This structure allows you to maintain the same locator names for both platforms, simplifying the test scripts and making the code more readable.
Step 2: Implement a Locator Loader
Now, we need to implement a Locator Loader class to load the appropriate locator based on the current platform. Here’s how we can do it:
import * as fs from 'fs';
import * as path from 'path';
import { PLATFORM } from '../conf/platformConfig.ts';
// Custom Exception for Locator Not Found
class LocatorNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'LocatorNotFoundError';
}
}
// Define an interface for the structure of your JSON file
interface LocatorData {
[key: string]: {
ios: string;
android: string;
};
}
class Locators {
private readonly locators: LocatorData | null = null;
constructor(fileName: string) {
this.locators = this.loadLocators(fileName);
}
private loadLocators(fileName: string): LocatorData | null {
const resourcePath = path.resolve(__dirname, '../../src/locators');
const filePath = path.join(resourcePath, fileName);
try {
const data = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(data);
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === 'ENOENT') {
throw new Error(`File "${fileName}" not found.`);
} else {
throw new Error(`Error reading or parsing the file "${fileName}": ${err.message}`);
}
}
}
public getLocator(locator: string): string {
if (this.locators && this.locators[locator]) {
const platformLocator = this.locators[locator][PLATFORM];
if (!platformLocator) {
throw new LocatorNotFoundError(
`Locator for platform "${PLATFORM}" not found for "${locator}".`
);
}
return platformLocator;
}
throw new LocatorNotFoundError(`Locator "${locator}" not found.`);
}
}
export default Locators;
In this class:
- We load the locators from a JSON file that includes platform-specific mappings for each element.
- The
getLocator()
method checks the current platform and retrieves the corresponding locator dynamically, removing the need for conditionals.
Step 3: Use the Locator in Page Objects
With the Locators
class in place, you can now use it in your page objects to get the correct locator based on the platform.
Here’s an example using a LoginScreen
page object:
import { $ } from '@wdio/globals';
import Locators from '../../helpers/locators.ts';
import WdCommands from '../../wd/wdCommands.ts';
class LoginScreen extends Locators {
private wdCommands: WdCommands;
constructor(locatorsFile: string) {
super(locatorsFile);
this.wdCommands = new WdCommands();
}
public get loginScreen() {
return $(this.getLocator('login_screen')).getElement();
}
public get emailInput() {
return $(this.getLocator('email_input')).getElement();
}
public get submitButton() {
return $(this.getLocator('submit_button')).getElement();
}
public async isLoginScreenDisplayed(): Promise<boolean> {
return this.wdCommands.isDisplayed(await this.loginScreen);
}
public async typeEmail(email: string): Promise<void> {
await this.wdCommands.setValue(await this.emailInput, email);
}
public async clickSubmitButton(): Promise<void> {
await this.wdCommands.click(await this.submitButton);
}
public async isSubmitButtonEnabled(): Promise<boolean> {
return await this.wdCommands.isEnabled(await this.submitButton);
}
}
export default new LoginScreen('login_screen_locators.json');
In this LoginScreen
class:
- We instantiate the
Locators
class and pass thelogin_screen_locators.json
file that contains the platform-specific locators. - The locators are retrieved dynamically in the getters and used to interact with elements like the email input and submit button.
Benefits of This Approach
- No Conditional Logic: You avoid using
if-else
statements to handle platform-specific locators. The correct locator is automatically selected based on the platform. - Maintainability: By keeping locators in a separate JSON file, it’s easier to update locators as the app evolves, without changing the test scripts.
- Scalability: Adding new platforms or elements is simple. Just update the JSON file with new platform mappings.
- Cleaner Code: Test scripts become more concise and easier to read because you no longer need to write redundant platform-specific code.
Example Project
For a better understanding of this approach, I encourage you to check out this example project on GitHub. It demonstrates how to manage locators dynamically in a cross-platform testing environment using Appium and WebDriverIO.
Conclusion
This unified approach to managing locators for cross-platform mobile testing significantly reduces complexity and improves maintainability. By using a dynamic locator loader and organizing locators in a JSON file, you can ensure that your tests are platform-agnostic and easier to scale as your app grows.
If you’re looking for a cleaner and more maintainable way to handle locators in your cross-platform tests, I highly recommend adopting this strategy.