This is an enterprise-grade mobile automation framework built with TypeScript, WebdriverIO, and Appium, designed for scalable, maintainable, and robust mobile testing across Android and iOS platforms.
- WebdriverIO v9.18.x - Modern test automation framework with built-in services and reporters
- Appium v2.19.0 - Cross-platform mobile automation server with UiAutomator2 driver
- TypeScript v5.x - Type-safe JavaScript with advanced IDE support and compile-time error detection
- Mocha - Feature-rich JavaScript test framework with BDD/TDD assertion library
- Allure Reporting - Comprehensive test reporting with rich visualizations and analytics
- BrowserStack - Cloud-based device testing platform for parallel execution
- Appium UiAutomator2 Driver - Native Android automation driver for enhanced performance
- Node.js ESM - Modern module system for better performance and tree-shaking
- Visual Testing Service - Screenshot comparison and visual regression testing
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install --save-dev eslint eslint-config-prettier eslint-plugin-prettier
npm install --save-dev @wdio/eslint-plugin-wdioRecommended ESLint Configuration:
{
"extends": [
"@typescript-eslint/recommended",
"plugin:@wdio/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@wdio/no-debug": "error"
}
}npm install --save-dev prettierPrettier Configuration (.prettierrc):
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
}npm install --save-dev husky lint-staged- GitHub Actions / Jenkins / Azure DevOps for pipeline automation
- Docker containers for consistent test execution environments
- Kubernetes for scalable test execution in enterprise environments
npm install --save-dev faker @types/faker
npm install --save-dev dotenv @types/dotenvnpm install --save-dev @wdio/junit-reporter
npm install --save-dev @wdio/json-reporter
npm install --save-dev jest-html-reportersnpm install --save-dev @wdio/timeline-service
npm install --save-dev wdio-performance-serviceπ― Goal: Establish compile-time safety and developer experience through strong typing. This prevents runtime errors and provides intelligent autocomplete/IntelliSense for the development team.
// interfaces/LoginTypes.ts
export interface LoginCredentials {
readonly username: string; // Line 2: readonly prevents accidental modification of credentials
readonly password: string; // Line 3: ensures password is always string type, no undefined/null
}
export interface TestUser {
credentials: LoginCredentials; // Line 6: references our LoginCredentials for type consistency
profile: UserProfile; // Line 7: contains user profile info (name, email, etc.)
permissions: UserPermission[]; // Line 8: array of permissions for role-based testing
}
// Use strict types for page objects
export interface LoginPageElements {
readonly usernameField: string; // Line 12: selector for username input - readonly prevents changes
readonly passwordField: string; // Line 13: selector for password input during test execution
readonly loginButton: string; // Line 14: selector for login submit button
readonly errorMessage: string; // Line 15: selector for error message display
}Why This Matters:
- Catches typos in property names at compile time, not during test execution
- Provides autocomplete when writing tests, reducing development time
- Creates a contract that all implementations must follow
- Prevents accidental modification of critical test data
π― Goal: Extend WebdriverIO's built-in types with custom methods specific to mobile automation. This adds our custom functionality while maintaining type safety.
// types/WebDriverExtensions.ts
declare global { // Line 2: extends global namespace
namespace WebdriverIO { // Line 3: targets WebdriverIO's type system
interface Browser { // Line 4: extends the Browser interface
waitForAppState(state: 'foreground' | 'background'): Promise<void>; // Line 5: custom method for app state checking
performAppAction(action: AppAction): Promise<void>; // Line 6: generic app action executor
}
interface Element { // Line 9: extends Element interface for all elements
waitForClickable(timeout?: number): Promise<void>; // Line 10: enhanced clickable wait with timeout
performSecureInput(value: string): Promise<void>; // Line 11: secure input for sensitive data
}
}
}
// Use mapped types for configuration
type TestConfig = { // Line 16: creates type-safe configuration mapping
readonly [K in keyof typeof environments]: EnvironmentConfig; // Line 17: maps each environment to its config
};Why This Matters:
- Global Declaration: Makes custom methods available throughout the entire test suite
- Type Safety: TypeScript will validate these methods exist and have correct signatures
- IntelliSense: Developers get autocomplete for custom methods like
waitForAppState() - Mapped Types: Creates configuration objects that are validated against available environments
π― Goal: Create meaningful, actionable error messages that help developers quickly identify and fix test failures. Replace generic errors with context-rich exceptions.
// utils/TestExceptions.ts
export class ElementNotFoundError extends Error { // Line 2: extends built-in Error class
constructor(selector: string, timeout: number) { // Line 3: accepts selector and timeout context
super(`Element '${selector}' not found within ${timeout}ms`); // Line 4: creates descriptive error message
this.name = 'ElementNotFoundError'; // Line 5: sets custom error name for filtering
}
}
export class TestDataValidationError extends Error { // Line 8: handles test data validation failures
constructor(field: string, expectedType: string, actualType: string) { // Line 9: accepts validation context
super(`Invalid test data: ${field} expected ${expectedType}, got ${actualType}`); // Line 10: detailed validation message
this.name = 'TestDataValidationError'; // Line 11: custom error name for categorization
}
}Why This Matters:
- Debugging Speed: Clear error messages reduce time spent investigating failures
- Test Maintenance: Specific error types help identify systemic issues vs. individual test problems
- Error Categorization: Custom error names enable filtering and grouping in reports
- Context Preservation: Includes relevant information (selectors, timeouts, data types) in error messages
π― Goal: Implement a service container for dependency management, making tests more modular and easier to mock/stub for unit testing. This follows the Dependency Inversion Principle.
// core/ServiceContainer.ts
export class ServiceContainer { // Line 2: singleton service container class
private static instance: ServiceContainer; // Line 3: static instance for singleton pattern
private services: Map<string, any> = new Map(); // Line 4: Map to store service instances by key
static getInstance(): ServiceContainer { // Line 6: singleton getter method
if (!ServiceContainer.instance) { // Line 7: lazy initialization check
ServiceContainer.instance = new ServiceContainer(); // Line 8: create instance if doesn't exist
}
return ServiceContainer.instance; // Line 10: return the singleton instance
}
register<T>(key: string, service: T): void { // Line 13: generic method to register services
this.services.set(key, service); // Line 14: store service in Map with string key
}
resolve<T>(key: string): T { // Line 17: generic method to retrieve services
return this.services.get(key); // Line 18: get service from Map by key
}
}Usage Example:
// Register services at startup
const container = ServiceContainer.getInstance();
container.register('apiClient', new ApiClient());
container.register('testDataManager', new TestDataManager());
// Use in tests
const apiClient = container.resolve<ApiClient>('apiClient');Why This Matters:
- Loose Coupling: Tests don't directly instantiate dependencies, making them more flexible
- Easy Mocking: Can replace real services with mocks for unit testing
- Singleton Management: Ensures shared services are reused across tests
- Type Safety: Generic methods provide compile-time type checking
π― Goal: Create a robust, priority-based locator strategy that automatically selects the most stable element identifiers. This reduces test maintenance when app elements change.
// utils/LocatorStrategy.ts
export class LocatorStrategy {
private static readonly PRIORITY_ORDER = [ // Line 3: defines locator priority from most to least stable
'accessibility-id', // Line 4: highest priority - accessibility IDs are most stable
'id', // Line 5: resource IDs are second most stable
'class', // Line 6: class names are moderately stable
'xpath' // Line 7: XPath is least stable, used as last resort
] as const;
static generateLocator(element: ElementMetadata): string { // Line 10: main method to generate optimal locator
// AI-powered locator generation based on element properties
if (element.accessibilityId) return `~${element.accessibilityId}`; // Line 12: accessibility ID selector (tilde prefix)
if (element.resourceId) return `id=${element.resourceId}`; // Line 13: resource ID selector
// Fallback to dynamic XPath generation
return this.generateSmartXPath(element); // Line 16: fallback to XPath when stable locators unavailable
}
private static generateSmartXPath(element: ElementMetadata): string { // Line 19: generates intelligent XPath
const attributes = []; // Line 20: array to collect XPath attributes
if (element.text) attributes.push(`@text="${element.text}"`); // Line 21: adds text attribute if available
if (element.contentDesc) attributes.push(`@content-desc="${element.contentDesc}"`); // Line 22: adds content description
return `//*[${attributes.join(' and ')}]`; // Line 24: combines attributes with AND logic
}
}Real-World Usage:
// Element metadata from page inspection
const buttonMetadata = {
accessibilityId: 'login_button',
resourceId: 'com.app:id/btn_login',
text: 'Log In',
contentDesc: 'Login button'
};
// Generates: ~login_button (most stable)
const locator = LocatorStrategy.generateLocator(buttonMetadata);Why This Matters:
- Stability: Accessibility IDs rarely change, reducing test maintenance
- Fallback Strategy: Automatically uses best available locator when preferred ones aren't available
- Cross-Platform: Works for both Android and iOS with appropriate metadata
- Maintainability: Centralized locator logic makes updates easier across the entire test suite
π― Goal: Write tests once and run them on both Android and iOS platforms. This eliminates code duplication and ensures consistent test behavior across platforms.
// core/PlatformHandler.ts
export class PlatformHandler {
static async getElement(locators: PlatformLocators): Promise<WebdriverIO.Element> { // Line 3: accepts platform-specific locators
const platform = await driver.isAndroid() ? 'android' : 'ios'; // Line 4: detects current platform
const selector = locators[platform]; // Line 5: selects appropriate locator
return await $(selector); // Line 7: returns WebdriverIO element
}
static async handlePlatformSpecificAction(action: PlatformAction): Promise<void> { // Line 10: handles platform differences
if (await driver.isAndroid()) { // Line 11: checks if running on Android
await this.handleAndroidAction(action); // Line 12: executes Android-specific logic
} else { // Line 13: else clause for iOS
await this.handleIOSAction(action); // Line 14: executes iOS-specific logic
}
}
}Real-World Usage:
// Define platform-specific locators
const loginButtonLocators = {
android: '~login_button', // Android accessibility ID
ios: 'name="Login"' // iOS predicate string
};
// Use in test - works on both platforms
const loginButton = await PlatformHandler.getElement(loginButtonLocators);
await loginButton.click();
// Platform-specific actions
await PlatformHandler.handlePlatformSpecificAction({
type: 'hideKeyboard',
androidMethod: 'back', // Android uses back button
iosMethod: 'done' // iOS uses done button
});Why This Matters:
- Code Reuse: Same test code runs on both platforms without modification
- Maintenance: Single point of change for platform-specific logic
- Consistency: Ensures identical test behavior regardless of platform
- Abstraction: Hides platform complexity from test developers
π― Goal: Implement intelligent waiting mechanisms that handle real-world mobile app behaviors like network delays, animations, and state transitions. This reduces flaky tests caused by timing issues.
// utils/WaitStrategies.ts
export class WaitStrategies {
static async waitForAppState(expectedState: 'foreground' | 'background'): Promise<void> { // Line 3: waits for specific app state
await browser.waitUntil( // Line 4: WebdriverIO waitUntil method
async () => { // Line 5: async condition function
const state = await driver.queryAppState('com.example.app'); // Line 6: queries current app state
return state === (expectedState === 'foreground' ? 4 : 1); // Line 7: compares with expected state (4=foreground, 1=background)
},
{ // Line 9: wait configuration object
timeout: 30000, // Line 10: maximum wait time (30 seconds)
timeoutMsg: `App did not reach ${expectedState} state within 30 seconds` // Line 11: custom timeout error message
}
);
}
static async waitForNetworkIdle(): Promise<void> { // Line 15: waits for network requests to complete
// Implementation for network idle detection
await driver.pause(2000); // Temporary implementation // Line 17: placeholder - would implement actual network monitoring
}
}Real-World Usage:
// Wait for app to be in foreground before interacting
await WaitStrategies.waitForAppState('foreground');
// Wait for network requests to complete before validation
await WaitStrategies.waitForNetworkIdle();
const data = await apiClient.getData();
expect(data).toBeDefined();
// Custom wait for element with loading animation
await browser.waitUntil(async () => {
const loadingSpinner = await $('~loading_spinner');
return !(await loadingSpinner.isDisplayed());
}, { timeout: 15000, timeoutMsg: 'Loading never completed' });Why This Matters:
- Flaky Test Reduction: Proper waits eliminate timing-based test failures
- Real-World Conditions: Handles network delays and app state transitions
- Meaningful Errors: Custom timeout messages help diagnose actual problems
- Performance: Avoids unnecessary hard waits (driver.pause) in favor of condition-based waits
π― Goal: Optimize test execution speed and app performance monitoring. This reduces overall test suite execution time and provides insights into app performance during automated testing.
// utils/PerformanceOptimizer.ts
export class PerformanceOptimizer {
static async optimizeAppLaunch(): Promise<void> { // Line 3: optimizes app startup for faster tests
// Clear app cache before critical tests
await driver.executeScript('mobile: clearApp', [{ appId: 'com.example.app' }]); // Line 5: clears app data/cache for clean state
// Warm up the app
await driver.activateApp('com.example.app'); // Line 8: brings app to foreground
await driver.pause(1000); // Line 9: allows app initialization to complete
}
static async capturePerformanceMetrics(): Promise<PerformanceMetrics> { // Line 12: captures performance data during tests
const startTime = Date.now(); // Line 13: records start timestamp
// Performance measurement logic
const endTime = Date.now(); // Line 15: records end timestamp
return { // Line 17: returns performance metrics object
executionTime: endTime - startTime, // Line 18: calculates total execution time
memoryUsage: await this.getMemoryUsage(), // Line 19: gets current memory consumption
cpuUsage: await this.getCpuUsage() // Line 20: gets current CPU utilization
};
}
}Real-World Usage:
// Before critical test scenarios
await PerformanceOptimizer.optimizeAppLaunch();
// During performance-sensitive tests
const startMetrics = await PerformanceOptimizer.capturePerformanceMetrics();
// Execute test steps
await loginPage.performLogin(credentials);
await dashboardPage.loadData();
// Capture end metrics
const endMetrics = await PerformanceOptimizer.capturePerformanceMetrics();
// Validate performance thresholds
expect(endMetrics.executionTime - startMetrics.executionTime).toBeLessThan(5000);
expect(endMetrics.memoryUsage).toBeLessThan(500); // MB thresholdWhy This Matters:
- Test Speed: Optimized app launches reduce overall test execution time
- Clean State: Clearing app cache ensures consistent test starting conditions
- Performance Monitoring: Captures real performance data during automated testing
- Threshold Validation: Enables performance regression detection in CI/CD pipelines
π― Goal: Create a flexible, environment-aware configuration system that dynamically generates device capabilities and manages environment-specific settings. This supports multiple environments (dev, staging, prod) with different device matrices.
// config/ConfigManager.ts
export class ConfigManager {
private static config: TestConfiguration; // Line 3: stores merged configuration
static initialize(environment: Environment): void { // Line 5: initializes config for specific environment
this.config = { // Line 6: merges configuration objects
...baseConfig, // Line 7: spreads base configuration (timeouts, URLs, etc.)
...environmentConfigs[environment], // Line 8: overlays environment-specific settings
capabilities: this.generateCapabilities(environment) // Line 9: generates dynamic device capabilities
};
}
static getConfig(): TestConfiguration { // Line 12: getter for current configuration
return this.config; // Line 13: returns the merged configuration object
}
private static generateCapabilities(env: Environment): Capabilities[] { // Line 16: generates WebdriverIO capabilities array
return deviceMatrix[env].map(device => ({ // Line 17: maps each device to capability object
platformName: device.platform, // Line 18: sets platform (Android/iOS)
'appium:deviceName': device.name, // Line 19: sets device name/model
'appium:platformVersion': device.version, // Line 20: sets OS version
'appium:automationName': device.automationName, // Line 21: sets automation driver (UiAutomator2/XCUITest)
'bstack:options': { // Line 22: BrowserStack-specific options
projectName: 'Mobile Automation Framework', // Line 23: project identifier for reporting
buildName: `Build-${process.env.BUILD_NUMBER || 'local'}`, // Line 24: build name from CI/CD or 'local'
sessionName: `${device.platform}-${device.name}` // Line 25: session name for identification
}
}));
}
}Real-World Usage:
// Initialize for different environments
ConfigManager.initialize('staging'); // Uses staging device matrix
const config = ConfigManager.getConfig();
// Environment-specific device matrices
const deviceMatrix = {
dev: [{ platform: 'Android', name: 'Pixel 3', version: '10' }],
staging: [
{ platform: 'Android', name: 'Galaxy S21', version: '12' },
{ platform: 'iOS', name: 'iPhone 13', version: '15.0' }
],
prod: [/* full device matrix */]
};
// Access configuration in tests
const baseUrl = ConfigManager.getConfig().baseUrl;
const timeout = ConfigManager.getConfig().defaultTimeout;Why This Matters:
- Environment Flexibility: Easy switching between dev, staging, and production configurations
- Dynamic Capabilities: Automatically generates device matrices based on environment
- CI/CD Integration: Uses environment variables for build identification
- Centralized Management: Single source of truth for all configuration settings
π― Goal: Establish a consistent test lifecycle with proper setup, teardown, and error handling. This base class ensures all tests follow the same pattern and include necessary cleanup operations.
// framework/BaseTest.ts
export abstract class BaseTest { // Line 2: abstract base class for all tests
protected testData: TestDataManager; // Line 3: manages test data (users, credentials, etc.)
protected reporter: TestReporter; // Line 4: handles test reporting and artifacts
async setup(): Promise<void> { // Line 6: executed before each test
this.testData = new TestDataManager(); // Line 7: initializes test data management
this.reporter = new TestReporter(); // Line 8: initializes reporting system
await this.initializeApp(); // Line 10: prepares app for testing (launch, reset, etc.)
await this.loadTestData(); // Line 11: loads required test data from fixtures/APIs
}
async teardown(): Promise<void> { // Line 14: executed after each test (pass or fail)
await this.captureTestArtifacts(); // Line 15: captures screenshots, logs, performance data
await this.cleanupTestData(); // Line 16: cleans up created test data
}
abstract executeTest(): Promise<void>; // Line 19: forces child classes to implement test logic
}Real-World Usage:
// Example implementation
export class LoginTest extends BaseTest {
async executeTest(): Promise<void> {
// Test-specific logic here
const credentials = this.testData.getValidCredentials();
await loginPage.performLogin(credentials);
await dashboardPage.verifyWelcomeMessage();
}
// Override setup for specific test needs
async setup(): Promise<void> {
await super.setup(); // Call parent setup
await this.clearAppData(); // Additional setup
}
}
// Usage in test file
describe('Login Tests', () => {
let loginTest: LoginTest;
beforeEach(async () => {
loginTest = new LoginTest();
await loginTest.setup();
});
afterEach(async () => {
await loginTest.teardown();
});
it('should login successfully', async () => {
await loginTest.executeTest();
});
});Why This Matters:
- Consistency: All tests follow the same lifecycle pattern
- Cleanup: Ensures proper cleanup even when tests fail
- Artifact Collection: Automatically captures debugging information
- Extensibility: Child classes can override methods for specific needs
π― Goal: Create a service layer that orchestrates parallel test execution across multiple devices and manages resource allocation. This enables horizontal scaling and efficient device utilization.
// services/TestOrchestrator.ts
export class TestOrchestrator {
private devicePool: DevicePool; // Line 3: manages available devices for testing
private testQueue: TestQueue; // Line 4: organizes and prioritizes tests
private resultAggregator: ResultAggregator; // Line 5: combines results from parallel executions
async executeTestSuite(suite: TestSuite): Promise<TestResults> { // Line 7: main orchestration method
const devices = await this.devicePool.allocateDevices(suite.parallelism); // Line 8: allocates requested number of devices
const testBatches = this.testQueue.createBatches(suite.tests, devices.length); // Line 9: splits tests into batches per device
const results = await Promise.allSettled( // Line 11: executes all batches in parallel
testBatches.map((batch, index) => // Line 12: maps each batch to a device
this.executeBatch(batch, devices[index]) // Line 13: executes batch on assigned device
)
);
return this.resultAggregator.combine(results); // Line 17: aggregates all execution results
}
}Real-World Usage:
// Initialize orchestrator with services
const orchestrator = new TestOrchestrator();
await orchestrator.initialize({
maxDevices: 5,
deviceTypes: ['Android', 'iOS'],
retryFailedTests: true
});
// Execute test suite with parallelism
const testSuite = {
tests: [LoginTest, DashboardTest, CheckoutTest],
parallelism: 3,
priority: 'high'
};
const results = await orchestrator.executeTestSuite(testSuite);
// Results include timing, device allocation, and failure analysis
console.log(`Tests completed in ${results.totalTime}ms`);
console.log(`Success rate: ${results.successRate}%`);
console.log(`Device utilization: ${results.deviceUtilization}`);Why This Matters:
- Parallel Execution: Dramatically reduces total test execution time
- Resource Management: Efficiently allocates and manages device resources
- Scalability: Easily scales from 1 to N devices based on availability
- Fault Tolerance: Handles device failures and test retries gracefully
// reporting/CustomReporter.ts
export class CustomReporter {
async onTestStart(test: Test): Promise<void> {
await this.logTestStart(test);
await this.captureEnvironmentSnapshot();
}
async onTestComplete(test: Test, result: TestResult): Promise<void> {
await this.attachArtifacts(test, result);
await this.updateDashboard(result);
await this.sendNotifications(result);
}
private async attachArtifacts(test: Test, result: TestResult): Promise<void> {
if (!result.passed) {
await this.captureFailureArtifacts(test);
}
await this.attachPerformanceMetrics(test);
await this.attachDeviceLogs(test);
}
}project/
βββ src/
β βββ pages/ # Page Object Models
β β βββ base/ # Base page classes
β β βββ android/ # Android-specific pages
β β βββ ios/ # iOS-specific pages
β βββ tests/
β β βββ smoke/ # Critical path tests
β β βββ regression/ # Full regression suite
β β βββ integration/ # Integration tests
β βββ utils/
β β βββ helpers/ # Test utilities
β β βββ fixtures/ # Test data
β β βββ services/ # External service integrations
β βββ config/
β β βββ environments/ # Environment configurations
β β βββ capabilities/ # Device capabilities
β βββ types/ # TypeScript definitions
βββ reports/ # Test execution reports
βββ artifacts/ # Screenshots, logs, videos
βββ docker/ # Containerization files
βββ ci/ # CI/CD pipeline configurations
export class UserFactory {
static createTestUser(type: UserType): TestUser {
return {
credentials: CredentialsFactory.create(type),
profile: ProfileFactory.create(type),
permissions: PermissionFactory.create(type)
};
}
}- Device-level parallelism: Run tests across multiple real devices and simulators
- Test-level parallelism: Execute independent test classes simultaneously
- Feature-level parallelism: Split large test suites by feature areas
- Container orchestration with Kubernetes for scalable test execution
- Device farm integration with AWS Device Farm, Firebase Test Lab
- CI/CD pipeline optimization with parallel stages and smart test selection
- Real-time dashboards for test execution monitoring
- Flaky test detection and automatic retry mechanisms
- Performance baseline tracking and regression detection
This framework provides a solid foundation for enterprise-scale mobile automation with modern TypeScript practices, robust Appium integration, and scalable WebdriverIO configuration. Each component is designed for maintainability, extensibility, and team collaboration.