Files
Detox/docs/architecture/devices.md
T

12 KiB

Device Management

The device subsystem handles allocation, lifecycle, and interaction with iOS simulators, Android emulators, and cloud devices.

Architecture Overview

┌────────────────────────────────────────────────────────────┐
│                      Device Subsystem                      │
│                                                            │
│  ┌─────────────────┐  ┌─────────────────┐  ┌────────────┐  │
│  │ DeviceAllocator │─►│  RuntimeDevice  │─►│   Driver   │  │
│  │                 │  │                 │  │            │  │
│  │ - allocate()    │  │ - launchApp()   │  │ - boot()   │  │
│  │ - free()        │  │ - terminateApp()│  │ - install()│  │
│  │                 │  │ - reloadRN()    │  │ - execute()│  │
│  └────────┬────────┘  └────────┬────────┘  └────────────┘  │
│           │                    │                           │
│  ┌────────▼────────┐  ┌────────▼────────┐                  │
│  │ DeviceRegistry  │  │  DeviceCookie   │                  │
│  │  (Lock files)   │  │ (Serialization) │                  │
│  └─────────────────┘  └─────────────────┘                  │
└────────────────────────────────────────────────────────────┘

Directory Structure

src/devices/
├── allocation/
│   ├── DeviceAllocator.js      # Main allocation coordinator
│   ├── DeviceList.js           # Available devices tracking
│   ├── DeviceRegistry.js       # Lock-based device registry
│   ├── drivers/
│   │   ├── ios/
│   │   │   └── SimulatorAllocDriver.js
│   │   └── android/
│   │       ├── emulator/
│   │       │   └── EmulatorAllocDriver.js
│   │       ├── attached/
│   │       │   └── AttachedAndroidAllocDriver.js
│   │       └── genycloud/
│   │           └── GenyAllocDriver.js
│   └── factories/
│       ├── ios.js
│       ├── android.js
│       └── external.js
├── runtime/
│   ├── RuntimeDevice.js        # Main device abstraction
│   └── drivers/
│       ├── DeviceDriverBase.js
│       ├── ios/
│       │   ├── IosDriver.js
│       │   └── SimulatorDriver.js
│       └── android/
│           ├── AndroidDriver.js
│           ├── emulator/
│           │   └── EmulatorDriver.js
│           ├── attached/
│           │   └── AttachedAndroidDriver.js
│           └── genycloud/
│               └── GenyCloudDriver.js
├── common/
│   └── drivers/
│       ├── ios/tools/
│       │   └── AppleSimUtils.js
│       └── android/
│           ├── exec/
│           │   ├── ADB.js
│           │   └── AAPT.js
│           └── tools/
│               ├── Instrumentation.js
│               └── AppInstallHelper.js
├── cookies/
│   └── index.js                # Device state serialization
└── validation/
    └── ...                     # Environment validators

Device Types

Type Config Value Platform Description
iOS Simulator ios.simulator iOS Xcode simulators
Android Emulator android.emulator Android AVD emulators
Android Attached android.attached Android USB-connected devices
Genycloud android.genycloud Android Genymotion cloud instances

Allocation System

DeviceAllocator (src/devices/allocation/DeviceAllocator.js)

Coordinates device allocation with concurrency control:

class DeviceAllocator {
  constructor(allocationDriver, deviceRegistry) {
    this._driver = allocationDriver;
    this._registry = deviceRegistry;
  }

  async allocate(deviceConfig) {
    // Find or create a device matching the config
    // Register ownership in the registry
    // Return a device cookie
  }

  async postAllocate(deviceCookie, configs) {
    // Platform-specific device setup after allocation
    // e.g., boot simulator, disable animations, unlock screen
    // Returns an enriched device cookie
  }

  async free(deviceCookie) {
    // Release the device
    // Remove from registry
  }
}

DeviceRegistry (src/devices/allocation/DeviceRegistry.js)

Uses lock files for cross-process device ownership:

class DeviceRegistry {
  constructor({ lockfilePath }) {
    this._lockfile = new ExclusiveLockfile(lockfilePath);
  }

  async allocateDevice(deviceQuery) {
    return this._lockfile.exclusively(async () => {
      // Find available device
      // Mark as allocated
      // Return device handle
    });
  }
}

Allocation Drivers

Each device type has a specialized allocation driver:

SimulatorAllocDriver (src/devices/allocation/drivers/ios/SimulatorAllocDriver.js):

  • Queries available simulators via xcrun simctl
  • Creates simulators if needed
  • Handles simulator lifecycle

EmulatorAllocDriver (src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js):

  • Validates AVD existence
  • Launches emulators with appropriate flags
  • Assigns unique ports

GenyAllocDriver (src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.js):

  • Communicates with Genymotion cloud API
  • Provisions cloud instances
  • Handles instance lifecycle

Runtime Device

RuntimeDevice (src/devices/runtime/RuntimeDevice.js)

The main device abstraction exposed to tests:

class RuntimeDevice {
  constructor(deviceCookie, deps, configs) {
    this._driver = createDriver(deviceCookie, deps);
    this._appsConfig = configs.appsConfig;
  }

  // App management
  async launchApp(params) { /* ... */ }
  async terminateApp(bundleId) { /* ... */ }
  async installApp() { /* ... */ }
  async uninstallApp() { /* ... */ }

  // React Native specific
  async reloadReactNative() { /* ... */ }

  // Device control
  async sendToHome() { /* ... */ }
  async shake() { /* ... */ }
  async setLocation(lat, lon) { /* ... */ }
  async setURLBlacklist(urls) { /* ... */ }

  // State
  get id() { return this._driver.deviceId; }
  get name() { return this._driver.deviceName; }
}

Driver Hierarchy

DeviceDriverBase
├── IosDriver
│   └── SimulatorDriver
└── AndroidDriver
    ├── EmulatorDriver
    ├── AttachedAndroidDriver
    └── GenyCloudDriver

DeviceDriverBase (src/devices/runtime/drivers/DeviceDriverBase.js):

  • Abstract base class
  • Defines common interface
  • Provides utility methods

Platform Drivers:

  • Implement platform-specific operations
  • Handle app installation/launch
  • Manage device state

Platform-Specific Tools

iOS Tools

AppleSimUtils (src/devices/common/drivers/ios/tools/AppleSimUtils.js):

class AppleSimUtils {
  async boot(deviceId) { /* xcrun simctl boot */ }
  async shutdown(deviceId) { /* xcrun simctl shutdown */ }
  async install(deviceId, path) { /* xcrun simctl install */ }
  async launch(deviceId, bundleId, args) { /* xcrun simctl launch */ }
  async setPermissions(deviceId, bundleId, permissions) { /* ... */ }
}

Android Tools

ADB (src/devices/common/drivers/android/exec/ADB.js):

class ADB {
  async devices() { /* adb devices */ }
  async install(deviceId, apkPath) { /* adb install */ }
  async shell(deviceId, command) { /* adb shell */ }
  async forward(deviceId, local, remote) { /* adb forward */ }
}

Instrumentation (src/devices/common/drivers/android/tools/Instrumentation.js):

class Instrumentation {
  async launch(deviceId, bundleId, testRunner, args) {
    // am instrument -w -r ...
  }
}

Device Lifecycle

Allocation Flow

1. Test starts
   │
   ▼
2. DeviceAllocator.allocate(deviceConfig)
   │
   ├── Query available devices
   │   └── SimulatorQuery / FreeDeviceFinder
   │
   ├── Acquire lock
   │   └── DeviceRegistry.allocateDevice()
   │
   └── Return DeviceCookie

3. DeviceAllocator.postAllocate(cookie, configs)
   │
   ├── Platform-specific device setup
   │   ├── iOS: Boot simulator, clean app cache
   │   └── Android: Wait for boot, disable animations,
   │       unlock screen, configure System UI
   │
   └── Return enriched DeviceCookie

4. RuntimeDevice created with cookie
   │
   ▼
5. Tests execute
   │
   ▼
6. DeviceAllocator.free(cookie)
   │
   ├── Shutdown (if configured)
   │
   └── Release lock

App Launch Flow

1. device.launchApp(params)
   │
   ├── Terminate if running
   │
   ├── Build launch args
   │   ├── Detox server URL
   │   ├── Session ID
   │   └── User-provided args
   │
   ├── Platform-specific launch
   │   ├── iOS: xcrun simctl launch
   │   └── Android: am instrument
   │
   └── Wait for app connection
       └── client.waitUntilReady()

Device cookies serialize device state for passing between processes:

// src/devices/cookies/index.js
const DeviceCookie = {
  serialize(device) {
    return {
      id: device.id,
      type: device.type,
      // Platform-specific data
    };
  },

  deserialize(cookie) {
    // Reconstruct device handle
  }
};

Environment Validation

Validators ensure the environment is properly configured:

// iOS validator
class IosSimulatorEnvValidator {
  async validate() {
    // Check Xcode installation
    // Verify xcrun availability
    // Validate simulator runtime
  }
}

// Android validator
class AndroidEmulatorEnvValidator {
  async validate() {
    // Check ANDROID_HOME
    // Verify ADB installation
    // Validate emulator availability
  }
}

Event Emission

RuntimeDevice emits lifecycle events for artifact collection:

class RuntimeDevice {
  async launchApp(params) {
    await this._eventEmitter.emit('beforeLaunchApp', launchInfo);

    // ... launch logic ...

    await this._eventEmitter.emit('launchApp', launchInfo);
    await this._eventEmitter.emit('appReady', appInfo);
  }

  async uninstallApp() {
    await this._eventEmitter.emit('beforeUninstallApp', appInfo);
    // ... uninstall logic ...
  }

  async terminateApp() {
    await this._eventEmitter.emit('beforeTerminateApp', appInfo);
    // ... terminate logic ...
    await this._eventEmitter.emit('terminateApp', appInfo);
  }
}

Configuration

Device configuration in .detoxrc.js:

module.exports = {
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 15 Pro',
      },
    },
    emulator: {
      type: 'android.emulator',
      device: {
        avdName: 'Pixel_4_API_30',
      },
    },
    genycloud: {
      type: 'android.genycloud',
      device: {
        recipeUUID: '...',
      },
    },
  },
};

See Also