---
name: mobile-testing
description: >-
Test mobile applications with Appium 2.0 and Detox for React Native. Covers device
farm setup (BrowserStack, Sauce Labs), gesture simulation, deep link testing, push
notification testing, offline/poor network simulation, and permission dialog handling.
Use when: "mobile test," "Appium," "Detox," "iOS test," "Android test," "device farm,"
"React Native test."
Related: ci-cd-integration, cross-browser-testing, performance-testing.
license: MIT
metadata:
author: kindlmann
version: "1.0"
category: automation
---
Test native, React Native, and hybrid mobile applications with production-grade tooling and real device strategies.
**Before starting:** Check for `.agents/qa-project-context.md` in the project root. It contains tech stack details, target platforms, and device coverage requirements that shape every decision below.
---
## Discovery Questions
1. **App type:** Native iOS/Android, React Native, Flutter, or hybrid (Cordova/Capacitor)? This determines the framework choice -- Appium for native/hybrid, Detox for React Native, Patrol for Flutter.
2. **Real devices or emulators?** Real devices for release validation and performance, emulators/simulators for development speed. Most teams need both.
3. **Device farm:** BrowserStack App Automate, Sauce Labs, AWS Device Farm, or self-hosted? Check budget and CI integration requirements.
4. **OS coverage:** Minimum iOS and Android versions? Check analytics for actual user distribution before building the device matrix.
5. **Existing CI pipeline:** Where do mobile tests run? Local machines, CI runners with emulators, or cloud device farms?
6. **App distribution:** How are test builds distributed? TestFlight, Firebase App Distribution, direct APK/IPA?
---
## Core Principles
1. **Real devices for release, emulators for speed.** Emulators miss touch latency, GPS drift, camera quirks, push notification timing, and battery behavior. Use emulators in development and PR checks; reserve real device farms for nightly and release pipelines.
2. **Gesture simulation is framework-specific.** Appium W3C Actions, Detox device APIs, and platform-native gesture recognizers each handle swipes, pinches, and long-presses differently. Do not assume cross-framework portability.
3. **Deep links and push notifications are unique to mobile.** Web testing frameworks cannot test these. Dedicated patterns exist for each -- treat them as first-class test scenarios, not afterthoughts.
4. **Permission dialogs break assumptions.** iOS and Android handle runtime permissions differently. Camera, location, contacts, and notification permissions require explicit handling in test setup or the test will hang waiting for a dialog it cannot dismiss.
5. **Network conditions matter more on mobile.** Users switch between WiFi, LTE, 3G, and offline. Test behavior under degraded and absent connectivity -- not just happy-path WiFi.
---
## Appium 2.0
### Architecture
Appium 2.0 uses a driver-based plugin architecture. The server is a thin shell; drivers provide platform-specific automation.
```bash
# Install Appium 2.0 and drivers
npm install -g appium
appium driver install uiautomator2 # Android
appium driver install xcuitest # iOS
# Verify installation
appium driver list --installed
```
### Capabilities (W3C Format)
```typescript
// Android capabilities
const androidCaps: Record = {
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': 'Pixel 7',
'appium:platformVersion': '14',
'appium:app': '/path/to/app.apk',
'appium:autoGrantPermissions': true,
'appium:newCommandTimeout': 300,
'appium:noReset': false,
};
// iOS capabilities
const iosCaps: Record = {
platformName: 'iOS',
'appium:automationName': 'XCUITest',
'appium:deviceName': 'iPhone 15 Pro',
'appium:platformVersion': '17.4',
'appium:app': '/path/to/app.ipa',
'appium:autoAcceptAlerts': false, // Handle alerts explicitly
'appium:newCommandTimeout': 300,
};
```
### Element Location Strategies
```typescript
// Accessibility ID (preferred -- cross-platform, stable)
const loginButton = await driver.$('~login-button');
// iOS class chain (iOS-specific, fast)
const cell = await driver.$('-ios class chain:**/XCUIElementTypeCell[`name == "Settings"`]');
// Android UIAutomator (Android-specific, powerful)
const scrollTarget = await driver.$(
'android=new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Terms"))'
);
// XPath (last resort -- slow, brittle)
// Avoid unless no other strategy works
```
**Priority:** Accessibility ID > platform-specific selector > XPath.
### Gesture Simulation
```typescript
// Scroll down
await driver.execute('mobile: scroll', { direction: 'down' });
// Swipe from point A to point B
await driver.execute('mobile: swipeGesture', {
left: 100, top: 500, width: 200, height: 400,
direction: 'up', percent: 0.75,
});
// Pinch to zoom (iOS)
await driver.execute('mobile: pinch', {
elementId: mapElement.elementId,
scale: 2.0,
velocity: 1.5,
});
// Long press
await driver.execute('mobile: longClickGesture', {
elementId: menuItem.elementId,
duration: 1500,
});
// Double tap
await driver.execute('mobile: doubleClickGesture', {
elementId: imageElement.elementId,
});
```
---
## Detox for React Native
### Architecture
Detox is a gray-box testing framework. It synchronizes with the React Native bridge, waiting for animations, network requests, and timers to settle before acting. This eliminates most flakiness caused by timing.
### Setup
```javascript
// .detoxrc.js
module.exports = {
testRunner: {
args: { $0: 'jest', config: 'e2e/jest.config.js' },
jest: { setupTimeout: 120000 },
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app',
build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
reversePorts: [8081],
},
},
devices: {
simulator: { type: 'ios.simulator', device: { type: 'iPhone 15 Pro' } },
emulator: { type: 'android.emulator', device: { avdName: 'Pixel_7_API_34' } },
},
configurations: {
'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
'android.emu.debug': { device: 'emulator', app: 'android.debug' },
},
};
```
### Test Patterns
```javascript
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should login with valid credentials', async () => {
await element(by.id('email-input')).typeText('user@example.com');
await element(by.id('password-input')).typeText('securePass123');
await element(by.id('login-button')).tap();
// Detox auto-waits for navigation and animations
await expect(element(by.id('dashboard-screen'))).toBeVisible();
await expect(element(by.text('Welcome back'))).toBeVisible();
});
it('should show error for invalid credentials', async () => {
await element(by.id('email-input')).typeText('wrong@example.com');
await element(by.id('password-input')).typeText('wrongpass');
await element(by.id('login-button')).tap();
await expect(element(by.id('error-message'))).toHaveText('Invalid email or password');
await expect(element(by.id('dashboard-screen'))).not.toBeVisible();
});
});
```
### Device APIs
```javascript
// Biometric authentication
await device.setBiometricEnrollment(true);
await device.matchBiometric(); // Simulate successful Face ID / fingerprint
await device.unmatchBiometric(); // Simulate failed biometric
// Shake gesture (e.g., to trigger feedback dialog)
await device.shake();
// Change device orientation
await device.setOrientation('landscape');
await device.setOrientation('portrait');
// Set location
await device.setLocation(37.7749, -122.4194); // San Francisco
// Open URL (deep link)
await device.openURL({ url: 'myapp://profile/settings' });
// Send user notification (iOS)
await device.sendUserNotification({
trigger: { type: 'push' },
title: 'New message',
body: 'You have a new message from Alice',
payload: { screen: 'chat', chatId: '123' },
});
```
### CI Integration
```bash
# Build and test on CI (iOS)
detox build --configuration ios.sim.debug
detox test --configuration ios.sim.debug --cleanup --headless --record-logs all
# Parallel test execution
detox test --configuration ios.sim.debug --workers 3
```
---
## Device Farm Integration
### BrowserStack App Automate
```typescript
// browserstack.config.ts
export const bsCapabilities = {
'bstack:options': {
userName: process.env.BROWSERSTACK_USERNAME,
accessKey: process.env.BROWSERSTACK_ACCESS_KEY,
projectName: 'MyApp Mobile Tests',
buildName: `build-${process.env.CI_BUILD_NUMBER}`,
sessionName: 'Login Flow',
debug: true,
networkLogs: true,
appiumVersion: '2.0.0',
},
platformName: 'Android',
'appium:deviceName': 'Samsung Galaxy S24',
'appium:platformVersion': '14.0',
'appium:app': process.env.BROWSERSTACK_APP_URL, // Upload via API: POST api-cloud.browserstack.com/app-automate/upload
};
```
### Sauce Labs
```typescript
export const sauceCapabilities = {
platformName: 'iOS',
'appium:deviceName': 'iPhone 15 Pro',
'appium:platformVersion': '17',
'appium:app': 'storage:filename=MyApp.ipa',
'sauce:options': {
name: 'Login Flow',
build: `build-${process.env.CI_BUILD_NUMBER}`,
appiumVersion: '2.0',
},
};
```
### Device Matrix Strategy
```yaml
# GitHub Actions matrix for device farm
strategy:
fail-fast: false
matrix:
include:
# P0: Top devices from analytics
- platform: android
device: Samsung Galaxy S24
os_version: "14"
- platform: ios
device: iPhone 15 Pro
os_version: "17"
# P1: Previous generation
- platform: android
device: Google Pixel 8
os_version: "14"
- platform: ios
device: iPhone 14
os_version: "16"
# P2: Oldest supported
- platform: android
device: Samsung Galaxy A54
os_version: "13"
- platform: ios
device: iPhone SE 3rd Gen
os_version: "16"
```
Build the matrix from analytics data. Typical split: 60% of tests on P0 devices, 30% on P1, 10% on P2.
---
## Mobile-Specific Testing Patterns
### Deep Link Testing
```typescript
// Appium: launch app via deep link
await driver.execute('mobile: deepLink', {
url: 'myapp://products/widget-123',
package: 'com.mycompany.myapp', // Android only
});
// Verify correct screen loaded
const productTitle = await driver.$('~product-title');
await expect(productTitle).toHaveText('Widget');
// Test deep link when app is not running (cold start)
await driver.terminateApp('com.mycompany.myapp');
await driver.execute('mobile: deepLink', {
url: 'myapp://products/widget-123',
package: 'com.mycompany.myapp',
});
await expect(driver.$('~product-title')).toBeDisplayed();
// Test deep link with authentication required
// App should redirect to login, then forward to deep link target after auth
await driver.execute('mobile: deepLink', {
url: 'myapp://settings/billing',
package: 'com.mycompany.myapp',
});
await expect(driver.$('~login-screen')).toBeDisplayed();
```
### Push Notification Testing
```javascript
// Detox: send push notification and verify handling
await device.sendUserNotification({
trigger: { type: 'push' },
title: 'Order shipped',
body: 'Your order #1234 has been shipped',
payload: { screen: 'order-detail', orderId: '1234' },
});
await expect(element(by.id('order-detail-screen'))).toBeVisible();
await expect(element(by.id('order-id'))).toHaveText('#1234');
// Appium: use Firebase Cloud Messaging test API for real push
// Send via backend test endpoint, then verify notification appears
await fetch(`${API_BASE}/test/send-push`, {
method: 'POST',
body: JSON.stringify({ userId: testUser.id, title: 'Order shipped' }),
});
// Wait for notification in notification shade (Android)
await driver.openNotifications();
const notification = await driver.$('android=new UiSelector().text("Order shipped")');
await notification.click();
```
### Offline and Poor Network Simulation
```typescript
// Appium: toggle airplane mode (Android)
await driver.execute('mobile: shell', {
command: 'cmd connectivity airplane-mode enable',
});
// Verify offline UI
await expect(driver.$('~offline-banner')).toBeDisplayed();
// Perform action while offline
await driver.$('~save-draft-button').click();
// Re-enable network
await driver.execute('mobile: shell', {
command: 'cmd connectivity airplane-mode disable',
});
// Verify queued action syncs
await expect(driver.$('~sync-complete-indicator')).toBeDisplayed();
// BrowserStack: throttle network
// Set in capabilities:
// 'browserstack.networkProfile': '3g-lossy'
// Options: 'no-network', '2g-gprs', '3g-lossy', '4g-lte', 'reset'
```
```javascript
// Detox: WiFi toggle (iOS simulator)
await device.setStatusBar({ dataNetwork: 'wifi' });
// Note: Detox does not directly simulate offline. Use a proxy or
// mock the network layer in the app with a test-only flag.
```
### Permission Dialog Handling
```typescript
// Android: set 'appium:autoGrantPermissions': true in capabilities
// iOS: handle permission dialogs explicitly
const allowButton = await driver.$('-ios predicate string:label == "Allow"');
if (await allowButton.isDisplayed()) {
await allowButton.click();
}
// Or use the mobile: alert command
await driver.execute('mobile: alert', { action: 'accept' });
// Detox
await systemDialog.accept(); // Tap "Allow"
await systemDialog.deny(); // Tap "Don't Allow"
```
### App Lifecycle Testing
```typescript
// Background and foreground
await driver.execute('mobile: backgroundApp', { seconds: 5 });
await expect(driver.$('~dashboard-screen')).toBeDisplayed();
// Terminate and relaunch (cold start)
await driver.terminateApp('com.mycompany.myapp');
await driver.activateApp('com.mycompany.myapp');
await expect(driver.$('~last-viewed-screen')).toBeDisplayed();
```
```javascript
// Detox lifecycle
await device.sendToHome();
await device.launchApp({ newInstance: false }); // Resume from background
await expect(element(by.id('dashboard'))).toBeVisible();
await device.launchApp({ newInstance: true, delete: true }); // Fresh install
await expect(element(by.id('onboarding-screen'))).toBeVisible();
```
---
## Anti-Patterns
**Running all tests on emulators only.** Emulators do not reproduce touch latency, camera behavior, GPS drift, or push notification timing. Use emulators for development velocity; run release suites on real devices via a device farm.
**Hardcoded device names in tests.** `await driver.$('Samsung Galaxy S24 - Home')` breaks when the device changes. Use accessibility IDs and platform-agnostic selectors.
**Ignoring app permissions.** Tests that assume permissions are pre-granted will fail on first install or when testing permission denial flows. Handle permissions explicitly.
**Testing only portrait orientation.** Many apps break in landscape. Test critical flows in both orientations, especially on tablets.
**Skipping offline scenarios.** Mobile users lose connectivity constantly. If the app does not handle offline gracefully, test it. If it does, verify the behavior works.
**Using `sleep()` instead of framework synchronization.** Detox auto-waits. Appium has implicit and explicit waits. Sleep-based synchronization is slow and flaky on both.
**Ignoring app size and startup time.** A 200MB app with a 6-second cold start is a real user experience issue. Include non-functional checks for app binary size and launch time in the test suite.
---
## Done When
- Device matrix defined and documented: real devices + emulators per platform, prioritized by analytics (P0/P1/P2 tiers)
- Test suite runnable against both iOS and Android with a single CI configuration (matrix strategy or separate jobs)
- Gesture tests (swipe, scroll, long-press) and deep link tests (cold start + authenticated redirect) cover the app's primary flows
- Push notification tests exist or are explicitly deferred with a documented rationale (e.g. "deferred until FCM test endpoint available")
- CI pipeline runs tests on at least one emulator per platform (iOS simulator + Android emulator) on every PR, with real device farm runs gated to nightly or release branches
## Related Skills
- **ci-cd-integration** -- Pipeline configuration for mobile test execution, artifact management, device farm CI connectors.
- **cross-browser-testing** -- Browser matrix design methodology applies to device matrix design.
- **performance-testing** -- Mobile-specific performance: app startup time, memory usage, battery drain.
- **test-data-management** -- Seed data strategies for mobile apps, backend state setup via API.
- **test-reliability** -- Flaky test patterns specific to mobile: timing, device state, network conditions.