Mobile Automation
Production-ready Appium, XCUITest, and Espresso automation for iOS and Android applications
Advanced Appium Gesture Automation
Complex gesture automation including swipe, pinch, zoom, long press, and multi-touch gestures for iOS and Android.
// Advanced Appium Gesture Framework
import { remote, RemoteOptions, Element } from 'webdriverio';
interface Point {
x: number;
y: number;
}
class AdvancedMobileGestures {
private driver: WebdriverIO.Browser;
constructor(driver: WebdriverIO.Browser) {
this.driver = driver;
}
// Advanced swipe with custom speed and direction
async swipe(
startPoint: Point,
endPoint: Point,
duration: number = 800
): Promise<void> {
if (this.driver.isIOS) {
await this.driver.execute('mobile: swipe', {
startX: startPoint.x,
startY: startPoint.y,
endX: endPoint.x,
endY: endPoint.y,
duration,
});
} else {
await this.driver.touchPerform([
{ action: 'press', options: startPoint },
{ action: 'wait', options: { ms: duration } },
{ action: 'moveTo', options: endPoint },
{ action: 'release' },
]);
}
}
// Swipe element in specific direction
async swipeElement(
element: Element,
direction: 'up' | 'down' | 'left' | 'right',
distance: number = 0.8
): Promise<void> {
const location = await element.getLocation();
const size = await element.getSize();
const centerX = location.x + size.width / 2;
const centerY = location.y + size.height / 2;
let startPoint: Point, endPoint: Point;
switch (direction) {
case 'up':
startPoint = { x: centerX, y: centerY + size.height * distance / 2 };
endPoint = { x: centerX, y: centerY - size.height * distance / 2 };
break;
case 'down':
startPoint = { x: centerX, y: centerY - size.height * distance / 2 };
endPoint = { x: centerX, y: centerY + size.height * distance / 2 };
break;
case 'left':
startPoint = { x: centerX + size.width * distance / 2, y: centerY };
endPoint = { x: centerX - size.width * distance / 2, y: centerY };
break;
case 'right':
startPoint = { x: centerX - size.width * distance / 2, y: centerY };
endPoint = { x: centerX + size.width * distance / 2, y: centerY };
break;
}
await this.swipe(startPoint, endPoint);
}
// Pinch to zoom
async pinchZoom(
element: Element,
scale: number = 2,
duration: number = 500
): Promise<void> {
const location = await element.getLocation();
const size = await element.getSize();
const centerX = location.x + size.width / 2;
const centerY = location.y + size.height / 2;
if (this.driver.isIOS) {
await this.driver.execute('mobile: pinch', {
x: centerX,
y: centerY,
scale,
velocity: 1.0,
});
} else {
const offset = 100;
await this.driver.multiTouchPerform([
[
{ action: 'press', options: { x: centerX - offset, y: centerY } },
{ action: 'moveTo', options: { x: centerX - offset * scale, y: centerY } },
{ action: 'release' },
],
[
{ action: 'press', options: { x: centerX + offset, y: centerY } },
{ action: 'moveTo', options: { x: centerX + offset * scale, y: centerY } },
{ action: 'release' },
],
]);
}
}
// Drag and drop
async dragAndDrop(
sourceElement: Element,
targetElement: Element,
duration: number = 1000
): Promise<void> {
const sourceLoc = await sourceElement.getLocation();
const sourceSize = await sourceElement.getSize();
const targetLoc = await targetElement.getLocation();
const targetSize = await targetElement.getSize();
const startPoint = {
x: sourceLoc.x + sourceSize.width / 2,
y: sourceLoc.y + sourceSize.height / 2,
};
const endPoint = {
x: targetLoc.x + targetSize.width / 2,
y: targetLoc.y + targetSize.height / 2,
};
await this.swipe(startPoint, endPoint, duration);
}
// Long press with custom duration
async longPress(element: Element, duration: number = 2000): Promise<void> {
if (this.driver.isIOS) {
await this.driver.execute('mobile: touchAndHold', {
elementId: element.elementId,
duration: duration / 1000,
});
} else {
const location = await element.getLocation();
const size = await element.getSize();
await this.driver.touchPerform([
{
action: 'press',
options: {
x: location.x + size.width / 2,
y: location.y + size.height / 2,
},
},
{ action: 'wait', options: { ms: duration } },
{ action: 'release' },
]);
}
}
// Circular swipe (for unlock patterns, etc.)
async circularSwipe(
centerPoint: Point,
radius: number,
duration: number = 1000
): Promise<void> {
const points: Point[] = [];
const steps = 20;
for (let i = 0; i <= steps; i++) {
const angle = (i / steps) * 2 * Math.PI;
points.push({
x: centerPoint.x + radius * Math.cos(angle),
y: centerPoint.y + radius * Math.sin(angle),
});
}
const actions = [
{ action: 'press', options: points[0] },
...points.slice(1).map(point => ({
action: 'moveTo',
options: point,
})),
{ action: 'release' },
];
await this.driver.touchPerform(actions);
}
// Shake device (iOS only)
async shakeDevice(): Promise<void> {
if (this.driver.isIOS) {
await this.driver.execute('mobile: shake');
}
}
// Scroll until element is visible
async scrollToElement(
element: Element,
maxScrolls: number = 10
): Promise<boolean> {
for (let i = 0; i < maxScrolls; i++) {
const isDisplayed = await element.isDisplayed();
if (isDisplayed) return true;
const windowSize = await this.driver.getWindowSize();
await this.swipe(
{ x: windowSize.width / 2, y: windowSize.height * 0.8 },
{ x: windowSize.width / 2, y: windowSize.height * 0.2 },
300
);
await this.driver.pause(500);
}
return false;
}
}
// Usage Example
const options: RemoteOptions = {
capabilities: {
platformName: 'iOS',
'appium:deviceName': 'iPhone 14 Pro',
'appium:platformVersion': '16.0',
'appium:app': '/path/to/app.ipa',
'appium:automationName': 'XCUITest',
},
};
const driver = await remote(options);
const gestures = new AdvancedMobileGestures(driver);
// Swipe to next screen
const element = await driver.$('~nextButton');
await gestures.swipeElement(element, 'left');
// Pinch to zoom on image
const image = await driver.$('~photoImage');
await gestures.pinchZoom(image, 2.5);
// Long press to open context menu
await gestures.longPress(element, 3000);
await driver.deleteSession();Cross-Platform Mobile Test Framework
Unified test framework supporting iOS and Android with page object model and automatic retry logic.
// Cross-Platform Mobile Testing Framework
import { remote, RemoteOptions } from 'webdriverio';
interface TestConfig {
platform: 'iOS' | 'Android';
deviceName: string;
platformVersion: string;
app: string;
}
abstract class BasePage {
protected driver: WebdriverIO.Browser;
constructor(driver: WebdriverIO.Browser) {
this.driver = driver;
}
// Platform-agnostic selectors
protected getSelector(iosSelector: string, androidSelector: string): string {
return this.driver.isIOS ? iosSelector : androidSelector;
}
async waitForElement(selector: string, timeout: number = 10000) {
const element = await this.driver.$(selector);
await element.waitForDisplayed({ timeout });
return element;
}
async tapElement(selector: string, retry: number = 3) {
for (let i = 0; i < retry; i++) {
try {
const element = await this.waitForElement(selector);
await element.click();
return;
} catch (error) {
if (i === retry - 1) throw error;
await this.driver.pause(1000);
}
}
}
async typeText(selector: string, text: string, clearFirst: boolean = true) {
const element = await this.waitForElement(selector);
if (clearFirst) {
await element.clearValue();
}
await element.setValue(text);
// Hide keyboard
if (this.driver.isIOS) {
await this.driver.execute('mobile: hideKeyboard');
} else {
await this.driver.hideKeyboard();
}
}
async getText(selector: string): Promise<string> {
const element = await this.waitForElement(selector);
return await element.getText();
}
}
class LoginPage extends BasePage {
private get usernameInput() {
return this.getSelector('~username-input', 'id:username');
}
private get passwordInput() {
return this.getSelector('~password-input', 'id:password');
}
private get loginButton() {
return this.getSelector('~login-button', 'id:login-btn');
}
private get errorMessage() {
return this.getSelector('~error-message', 'id:error-msg');
}
async login(username: string, password: string) {
await this.typeText(this.usernameInput, username);
await this.typeText(this.passwordInput, password);
await this.tapElement(this.loginButton);
}
async getErrorMessage(): Promise<string> {
return await this.getText(this.errorMessage);
}
}
class TestRunner {
private driver: WebdriverIO.Browser | null = null;
private config: TestConfig;
constructor(config: TestConfig) {
this.config = config;
}
async setup() {
const options: RemoteOptions = {
logLevel: 'info',
capabilities: {
platformName: this.config.platform,
'appium:deviceName': this.config.deviceName,
'appium:platformVersion': this.config.platformVersion,
'appium:app': this.config.app,
'appium:automationName':
this.config.platform === 'iOS' ? 'XCUITest' : 'UiAutomator2',
'appium:noReset': false,
'appium:fullReset': false,
'appium:newCommandTimeout': 300,
},
};
this.driver = await remote(options);
return this.driver;
}
async teardown() {
if (this.driver) {
await this.driver.deleteSession();
}
}
// Screenshot on failure
async takeScreenshot(testName: string) {
if (this.driver) {
const screenshot = await this.driver.takeScreenshot();
const fs = require('fs').promises;
await fs.writeFile(
`./screenshots/${testName}-${Date.now()}.png`,
screenshot,
'base64'
);
}
}
// Get device info
async getDeviceInfo() {
if (!this.driver) return null;
return {
platform: await this.driver.getPlatform(),
orientation: await this.driver.getOrientation(),
battery: this.config.platform === 'Android'
? await this.driver.execute('mobile: batteryInfo', {})
: null,
};
}
}
// Test Suite Example
describe('Login Tests', () => {
let runner: TestRunner;
let driver: WebdriverIO.Browser;
let loginPage: LoginPage;
beforeAll(async () => {
runner = new TestRunner({
platform: 'iOS',
deviceName: 'iPhone 14',
platformVersion: '16.0',
app: '/path/to/app.ipa',
});
driver = await runner.setup();
loginPage = new LoginPage(driver);
});
afterAll(async () => {
await runner.teardown();
});
afterEach(async function() {
if (this.currentTest?.state === 'failed') {
await runner.takeScreenshot(this.currentTest.title);
}
});
it('should login successfully with valid credentials', async () => {
await loginPage.login('testuser@example.com', 'password123');
// Add assertions
});
it('should show error with invalid credentials', async () => {
await loginPage.login('invalid@example.com', 'wrongpass');
const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});
});Mobile App Performance Testing
Monitor app performance metrics including CPU, memory, battery, network, and FPS during automation.
// Mobile Performance Monitoring
import { remote } from 'webdriverio';
interface PerformanceMetrics {
cpu: number;
memory: number;
battery?: number;
network: {
sent: number;
received: number;
};
fps?: number;
timestamp: number;
}
class MobilePerformanceMonitor {
private driver: WebdriverIO.Browser;
private metrics: PerformanceMetrics[] = [];
private monitoringInterval: NodeJS.Timeout | null = null;
constructor(driver: WebdriverIO.Browser) {
this.driver = driver;
}
async startMonitoring(intervalMs: number = 1000) {
this.monitoringInterval = setInterval(async () => {
try {
const metric = await this.collectMetrics();
this.metrics.push(metric);
} catch (error) {
console.error('Failed to collect metrics:', error);
}
}, intervalMs);
}
stopMonitoring() {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
}
}
private async collectMetrics(): Promise<PerformanceMetrics> {
const isAndroid = this.driver.isAndroid;
if (isAndroid) {
return await this.collectAndroidMetrics();
} else {
return await this.collectiOSMetrics();
}
}
private async collectAndroidMetrics(): Promise<PerformanceMetrics> {
// Get CPU usage
const cpu = await this.driver.execute('mobile: getPerformanceData', {
packageName: 'com.example.app',
dataType: 'cpuinfo',
});
// Get memory usage
const memory = await this.driver.execute('mobile: getPerformanceData', {
packageName: 'com.example.app',
dataType: 'memoryinfo',
});
// Get battery info
const battery = await this.driver.execute('mobile: batteryInfo', {});
// Get network stats
const network = await this.driver.execute('mobile: getPerformanceData', {
packageName: 'com.example.app',
dataType: 'networkinfo',
});
return {
cpu: parseFloat(cpu[0] || 0),
memory: parseFloat(memory[0] || 0),
battery: battery.level,
network: {
sent: parseFloat(network[0] || 0),
received: parseFloat(network[1] || 0),
},
timestamp: Date.now(),
};
}
private async collectiOSMetrics(): Promise<PerformanceMetrics> {
// iOS performance data collection
const perfData = await this.driver.execute('mobile: performanceData', {
timeout: 2000,
pid: 'current',
});
return {
cpu: perfData.cpuUsage || 0,
memory: perfData.memoryUsage || 0,
network: {
sent: perfData.networkSent || 0,
received: perfData.networkReceived || 0,
},
fps: perfData.fps || 0,
timestamp: Date.now(),
};
}
getMetrics(): PerformanceMetrics[] {
return this.metrics;
}
getAverageMetrics(): Partial<PerformanceMetrics> {
if (this.metrics.length === 0) return {};
const sum = this.metrics.reduce(
(acc, metric) => ({
cpu: acc.cpu + metric.cpu,
memory: acc.memory + metric.memory,
networkSent: acc.networkSent + metric.network.sent,
networkReceived: acc.networkReceived + metric.network.received,
}),
{ cpu: 0, memory: 0, networkSent: 0, networkReceived: 0 }
);
const count = this.metrics.length;
return {
cpu: sum.cpu / count,
memory: sum.memory / count,
network: {
sent: sum.networkSent / count,
received: sum.networkReceived / count,
},
};
}
exportToCSV(filename: string) {
const fs = require('fs');
const header = 'timestamp,cpu,memory,battery,network_sent,network_received,fps\n';
const rows = this.metrics.map(m =>
`${m.timestamp},${m.cpu},${m.memory},${m.battery || ''},${m.network.sent},${m.network.received},${m.fps || ''}\n`
);
fs.writeFileSync(filename, header + rows.join(''));
}
clearMetrics() {
this.metrics = [];
}
}
// Usage Example
const driver = await remote({
capabilities: {
platformName: 'Android',
'appium:deviceName': 'Pixel 7',
'appium:app': '/path/to/app.apk',
},
});
const perfMonitor = new MobilePerformanceMonitor(driver);
// Start monitoring
await perfMonitor.startMonitoring(1000);
// Run your test actions
await driver.$('~login-button').click();
await driver.$('~username').setValue('test@example.com');
// ... more actions
// Stop monitoring
perfMonitor.stopMonitoring();
// Get results
const avgMetrics = perfMonitor.getAverageMetrics();
console.log('Average CPU:', avgMetrics.cpu);
console.log('Average Memory:', avgMetrics.memory);
// Export to CSV
perfMonitor.exportToCSV('./performance-report.csv');
await driver.deleteSession();More Mobile Automation Topics
Deep Link Testing
Push Notification Automation
Biometric Authentication Testing
Camera and Photo Gallery Automation
Location and GPS Mocking
Network Condition Simulation
Device Rotation Testing
Multi-Language Testing
Accessibility Testing
iOS Simulator & Android Emulator Management
Need Custom Mobile Automation?
We build comprehensive mobile testing frameworks for iOS and Android applications.
Get Free Consultation