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