mirror of
https://github.com/hanzoai/Detox.git
synced 2026-09-24 16:55:12 +00:00
docs: add internal architecture documentation (#4909)
This commit is contained in:
+344
@@ -0,0 +1,344 @@
|
|||||||
|
# Detox Architecture
|
||||||
|
|
||||||
|
This document provides an overview of Detox's internal architecture for contributors and maintainers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Detox is a **gray-box end-to-end testing framework** for React Native mobile applications. Unlike black-box testing frameworks, Detox has visibility into the app's internal state, enabling automatic synchronization between tests and the app.
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌───────────────────────────────────────────────────────────┐
|
||||||
|
│ Test Environment (Node.js) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ Test Runner │ │DetoxContext │ │ ArtifactsManager │ │
|
||||||
|
│ │(Jest/Mocha) │ │ (Realms) │ │ │ │
|
||||||
|
│ └──────┬──────┘ └──────┬──────┘ └─────────┬─────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └────────────────┼───────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────▼─────────┐ │
|
||||||
|
│ │ DetoxWorker │ │
|
||||||
|
│ │ - device │ │
|
||||||
|
│ │ - element/by │ │
|
||||||
|
│ │ - expect │ │
|
||||||
|
│ └─────────┬─────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────▼─────────┐ │
|
||||||
|
│ │ Client │ │
|
||||||
|
│ │ (WebSocket) │ │
|
||||||
|
│ └─────────┬─────────┘ │
|
||||||
|
└──────────────────────────┼────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────▼─────────┐
|
||||||
|
│ DetoxServer │
|
||||||
|
│ (WebSocket) │
|
||||||
|
│ localhost:port │
|
||||||
|
└─────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────────────┼────────────────────────────────┐
|
||||||
|
│ Mobile Device/Simulator │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────▼─────────┐ │
|
||||||
|
│ │ Native Client │ │
|
||||||
|
│ │ (iOS/Android) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ - Synchronizer │ │
|
||||||
|
│ │ - Matchers │ │
|
||||||
|
│ │ - Actions │ │
|
||||||
|
│ └───────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Components
|
||||||
|
|
||||||
|
### 1. Realms (`src/realms/`)
|
||||||
|
|
||||||
|
Detox uses a "realm" pattern to manage different execution contexts:
|
||||||
|
|
||||||
|
| Realm | Class | Purpose |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| **Primary** | `DetoxPrimaryContext` | Full-featured context with device allocation, used in test runner process |
|
||||||
|
| **Secondary** | `DetoxSecondaryContext` | Lightweight config-snapshot context for worker processes |
|
||||||
|
|
||||||
|
**Key files:**
|
||||||
|
|
||||||
|
- `DetoxContext.js` - Base class exposing `device`, `element`, `expect`, `by`, `waitFor`, `web`, `system`
|
||||||
|
- `DetoxPrimaryContext.js` - Handles initialization, device allocation, server lifecycle
|
||||||
|
- `DetoxSecondaryContext.js` - Receives config snapshots, no device management
|
||||||
|
|
||||||
|
The base `DetoxContext` uses **funpermaproxy** to lazily delegate API calls to the worker:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
device = funpermaproxy(() => this[symbols.worker].device);
|
||||||
|
element = funpermaproxy.callable(() => this[symbols.worker].element);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Worker (`src/DetoxWorker.js`)
|
||||||
|
|
||||||
|
Each test worker creates a `DetoxWorker` instance that:
|
||||||
|
|
||||||
|
- Connects to the Detox server via `Client`
|
||||||
|
- Creates the `InvocationManager` for command serialization
|
||||||
|
- Instantiates the `RuntimeDevice` via environment factories
|
||||||
|
- Creates matchers (`by`, `element`, `expect`, `waitFor`)
|
||||||
|
- Manages the `ArtifactsManager`
|
||||||
|
|
||||||
|
**Lifecycle hooks:**
|
||||||
|
|
||||||
|
- `onRunDescribeStart` / `onRunDescribeFinish`
|
||||||
|
- `onTestStart` / `onTestDone`
|
||||||
|
- `onHookFailure` / `onTestFnFailure`
|
||||||
|
|
||||||
|
### 3. Client-Server Communication (`src/client/`, `src/server/`)
|
||||||
|
|
||||||
|
See [docs/architecture/client-server.md](docs/architecture/client-server.md) for details.
|
||||||
|
|
||||||
|
**Key components:**
|
||||||
|
|
||||||
|
- `Client` - WebSocket client connecting to server, sends actions to app
|
||||||
|
- `AsyncWebSocket` - Promise-based WebSocket wrapper
|
||||||
|
- `DetoxServer` - WebSocket server mediating tester ↔ app communication
|
||||||
|
- `DetoxSession` / `DetoxSessionManager` - Per-session state management
|
||||||
|
|
||||||
|
### 4. Device Management (`src/devices/`)
|
||||||
|
|
||||||
|
See [docs/architecture/devices.md](docs/architecture/devices.md) for details.
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
devices/
|
||||||
|
├── allocation/ # Device allocation strategies
|
||||||
|
│ ├── DeviceAllocator.js
|
||||||
|
│ ├── DeviceRegistry.js
|
||||||
|
│ └── drivers/
|
||||||
|
│ ├── ios/ # SimulatorAllocDriver
|
||||||
|
│ └── android/ # Emulator, Attached, Genycloud
|
||||||
|
├── runtime/ # Runtime device abstractions
|
||||||
|
│ ├── RuntimeDevice.js
|
||||||
|
│ └── drivers/
|
||||||
|
│ ├── ios/ # IosDriver, SimulatorDriver
|
||||||
|
│ └── android/ # EmulatorDriver, AttachedAndroidDriver, GenyCloudDriver
|
||||||
|
├── cookies/ # Device state serialization
|
||||||
|
└── validation/ # Environment validators
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Artifacts (`src/artifacts/`)
|
||||||
|
|
||||||
|
See [docs/architecture/artifacts.md](docs/architecture/artifacts.md) for details.
|
||||||
|
|
||||||
|
**Plugins:**
|
||||||
|
|
||||||
|
- `screenshot` - Captures screenshots on failure or on-demand
|
||||||
|
- `video` - Records test execution video
|
||||||
|
- `log` - Aggregates device and app logs
|
||||||
|
- `instruments` - iOS performance profiling
|
||||||
|
- `uiHierarchy` - Dumps view hierarchy for debugging (iOS only)
|
||||||
|
|
||||||
|
### 6. Invocation System (`src/invoke.js`, `src/client/actions/`)
|
||||||
|
|
||||||
|
Commands are serialized as JSON and sent to the native app:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Test code
|
||||||
|
await element(by.id('button')).tap();
|
||||||
|
|
||||||
|
// Serialized to invocation
|
||||||
|
{
|
||||||
|
type: 'invoke',
|
||||||
|
params: {
|
||||||
|
target: { type: 'Class', value: 'com.wix.detox.Detox' },
|
||||||
|
method: 'perform',
|
||||||
|
args: [{ /* matcher */ }, { /* action */ }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `InvocationManager` handles serialization/deserialization and batching.
|
||||||
|
|
||||||
|
### 7. Matchers & Expectations (`src/ios/`, `src/android/`)
|
||||||
|
|
||||||
|
Platform-specific implementations:
|
||||||
|
|
||||||
|
| Component | iOS | Android |
|
||||||
|
|-----------|-----|---------|
|
||||||
|
| Matchers | `src/matchers/` | `src/android/matchers/` |
|
||||||
|
| Actions | `src/ios/` | `src/android/actions/` |
|
||||||
|
| Expectations | `src/ios/expectTwo.js` | `src/android/AndroidExpect.js` |
|
||||||
|
|
||||||
|
Note: Shared matcher factory logic is in `src/matchers/`. iOS-specific expectations and test runner code are in `src/ios/`.
|
||||||
|
|
||||||
|
### 8. Test Runner Integration (`runners/`)
|
||||||
|
|
||||||
|
```text
|
||||||
|
runners/
|
||||||
|
├── jest/
|
||||||
|
│ ├── testEnvironment/ # Jest environment setup
|
||||||
|
│ ├── reporters/ # Custom reporters
|
||||||
|
│ ├── globalSetup.js
|
||||||
|
│ └── globalTeardown.js
|
||||||
|
├── jest-circus/ # Circus event handling
|
||||||
|
└── mocha/ # Mocha adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### Test Execution Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. Jest runs test file
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
2. DetoxEnvironment.setup()
|
||||||
|
└── detox.init()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
3. DetoxPrimaryContext initializes
|
||||||
|
├── Starts DetoxServer
|
||||||
|
├── Allocates device
|
||||||
|
└── Installs worker
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
4. DetoxWorker.init()
|
||||||
|
├── Connects Client to server
|
||||||
|
├── Creates RuntimeDevice
|
||||||
|
├── Creates matchers
|
||||||
|
└── Installs app (if configured)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
5. Test executes
|
||||||
|
├── element(by.id('x')).tap()
|
||||||
|
│ └── Client.execute(invocation)
|
||||||
|
│ └── Server relays to app
|
||||||
|
│ └── App executes action
|
||||||
|
│ └── App responds
|
||||||
|
│ └── Client resolves
|
||||||
|
└── expect(element).toBeVisible()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
6. DetoxEnvironment.teardown()
|
||||||
|
└── detox.cleanup()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
7. Artifacts collected, device deallocated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synchronization Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test sends action
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Native client receives
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Synchronizer checks app state:
|
||||||
|
├── Pending network requests?
|
||||||
|
├── Main thread busy?
|
||||||
|
├── Animations running?
|
||||||
|
├── React Native bridge busy?
|
||||||
|
└── JavaScript thread idle?
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
When idle: Execute action
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Return result to test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Patterns
|
||||||
|
|
||||||
|
### 1. Symbol-Based Privacy
|
||||||
|
|
||||||
|
Internal members use Symbols to prevent accidental access:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const $worker = Symbol('worker');
|
||||||
|
class DetoxContext {
|
||||||
|
[$worker] = null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Factory Pattern
|
||||||
|
|
||||||
|
Environment-specific factories create platform-appropriate instances:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const factories = environmentFactory.createFactories(deviceConfig);
|
||||||
|
// Returns: envValidatorFactory, artifactsManagerFactory,
|
||||||
|
// matchersFactory, runtimeDeviceFactory
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Cancellable Async Flow (CAF)
|
||||||
|
|
||||||
|
Long-running operations use CAF for cancellation:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
this._reinstallAppsOnDevice = CAF(this._reinstallAppsOnDevice.bind(this));
|
||||||
|
// Can be cancelled via: this._initToken.abort('CLEANUP')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Event-Driven Architecture
|
||||||
|
|
||||||
|
`ArtifactsManager` and `RuntimeDevice` use event emitters for loose coupling:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
deviceEmitter.on('bootDevice', this.onBootDevice.bind(this));
|
||||||
|
deviceEmitter.on('launchApp', this.onLaunchApp.bind(this));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration flows through composition:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.detoxrc.js / detox.config.js
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
loadExternalConfig()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
CLI arguments (--configuration, --device-name, etc.)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
composeDetoxConfig()
|
||||||
|
├── composeAppsConfig()
|
||||||
|
├── composeDeviceConfig()
|
||||||
|
├── composeArtifactsConfig()
|
||||||
|
├── composeBehaviorConfig()
|
||||||
|
└── composeSessionConfig()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
RuntimeConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
detox/
|
||||||
|
├── src/
|
||||||
|
│ ├── realms/ # Context management
|
||||||
|
│ ├── client/ # WebSocket client
|
||||||
|
│ ├── server/ # WebSocket server
|
||||||
|
│ ├── devices/ # Device management
|
||||||
|
│ ├── artifacts/ # Artifact collection
|
||||||
|
│ ├── configuration/ # Config composition
|
||||||
|
│ ├── ios/ # iOS-specific code
|
||||||
|
│ ├── android/ # Android-specific code
|
||||||
|
│ ├── matchers/ # Matcher factories
|
||||||
|
│ ├── invoke.js # Invocation manager
|
||||||
|
│ └── DetoxWorker.js # Per-test worker
|
||||||
|
├── runners/ # Test runner integrations
|
||||||
|
├── local-cli/ # CLI commands
|
||||||
|
├── ios/ # Native iOS SDK
|
||||||
|
└── android/ # Native Android SDK
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [docs/architecture/client-server.md](docs/architecture/client-server.md) - Client-server protocol details
|
||||||
|
- [docs/architecture/devices.md](docs/architecture/devices.md) - Device management internals
|
||||||
|
- [docs/architecture/artifacts.md](docs/architecture/artifacts.md) - Artifact collection system
|
||||||
|
- [docs/articles/how-detox-works.md](docs/articles/how-detox-works.md) - User-facing explanation
|
||||||
|
- [docs/articles/design-principles.md](docs/articles/design-principles.md) - Design philosophy
|
||||||
@@ -0,0 +1,560 @@
|
|||||||
|
# Artifacts System
|
||||||
|
|
||||||
|
The artifacts subsystem collects test artifacts like screenshots, videos, logs, and performance data during test execution.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Artifacts Subsystem │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ArtifactsManager │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ - Registers plugins │ │
|
||||||
|
│ │ - Subscribes to device events │ │
|
||||||
|
│ │ - Orchestrates lifecycle callbacks │ │
|
||||||
|
│ │ - Manages idle callback queue │ │
|
||||||
|
│ └────────────────────────┬──────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────┼───────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ │
|
||||||
|
│ │Screenshot │ │ Video │ │ Log │ │
|
||||||
|
│ │ Plugin │ │ Plugin │ │ Plugin │ │
|
||||||
|
│ └───────────┘ └───────────┘ └───────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────┐ ┌───────────┐ │
|
||||||
|
│ │Instruments│ │UIHierarchy│ │
|
||||||
|
│ │ Plugin │ │ (iOS) │ │
|
||||||
|
│ └───────────┘ └───────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ArtifactPathBuilder │ │
|
||||||
|
│ │ Generates paths: artifacts/{config}/{test}/artifact │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/artifacts/
|
||||||
|
├── ArtifactsManager.js # Main orchestrator
|
||||||
|
├── providers/
|
||||||
|
│ └── index.js # Exports: AndroidArtifactPluginsProvider,
|
||||||
|
│ # IosArtifactPluginsProvider,
|
||||||
|
│ # IosSimulatorArtifactPluginsProvider
|
||||||
|
├── templates/
|
||||||
|
│ ├── artifact/
|
||||||
|
│ │ ├── Artifact.js # Base artifact class
|
||||||
|
│ │ └── FileArtifact.js # File-based artifact
|
||||||
|
│ └── plugin/
|
||||||
|
│ ├── ArtifactPlugin.js # Base plugin class
|
||||||
|
│ ├── StartupAndTestRecorderPlugin.js
|
||||||
|
│ ├── TwoSnapshotsPerTestPlugin.js
|
||||||
|
│ └── WholeTestRecorderPlugin.js
|
||||||
|
├── screenshot/
|
||||||
|
│ ├── ScreenshotArtifactPlugin.js
|
||||||
|
│ ├── SimulatorScreenshotPlugin.js # iOS
|
||||||
|
│ └── ADBScreencapPlugin.js # Android
|
||||||
|
├── video/
|
||||||
|
│ ├── VideoArtifactPlugin.js
|
||||||
|
│ ├── SimulatorRecordVideoPlugin.js # iOS
|
||||||
|
│ └── ADBScreenrecorderPlugin.js # Android
|
||||||
|
├── log/
|
||||||
|
│ ├── LogArtifactPlugin.js
|
||||||
|
│ ├── ios/
|
||||||
|
│ │ └── SimulatorLogPlugin.js
|
||||||
|
│ └── android/
|
||||||
|
│ └── ADBLogcatPlugin.js
|
||||||
|
├── instruments/
|
||||||
|
│ ├── InstrumentsArtifactPlugin.js
|
||||||
|
│ ├── ios/
|
||||||
|
│ │ └── SimulatorInstrumentsPlugin.js
|
||||||
|
│ └── android/
|
||||||
|
│ └── AndroidInstrumentsPlugin.js
|
||||||
|
├── uiHierarchy/
|
||||||
|
│ └── IosUIHierarchyPlugin.js # iOS only
|
||||||
|
└── utils/
|
||||||
|
├── ArtifactPathBuilder.js
|
||||||
|
└── temporaryPath.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## ArtifactsManager
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ArtifactsManager extends EventEmitter {
|
||||||
|
constructor({ rootDir, pathBuilder, plugins }) {
|
||||||
|
this._pluginConfigs = plugins;
|
||||||
|
this._artifactPlugins = {};
|
||||||
|
this._pathBuilder = new ArtifactPathBuilder({ rootDir });
|
||||||
|
this._idlePromise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerArtifactPlugins(artifactPluginFactoriesMap) {
|
||||||
|
for (const [key, factory] of Object.entries(artifactPluginFactoriesMap)) {
|
||||||
|
const config = this._pluginConfigs[key];
|
||||||
|
this._artifactPlugins[key] = this._instantiateArtifactPlugin(factory, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Instantiation
|
||||||
|
|
||||||
|
Each plugin receives an API object for interacting with the manager:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
_instantiateArtifactPlugin(pluginFactory, pluginUserConfig) {
|
||||||
|
const artifactsApi = {
|
||||||
|
plugin: null,
|
||||||
|
userConfig: { ...pluginUserConfig },
|
||||||
|
|
||||||
|
preparePathForArtifact: async (artifactName, testSummary) => {
|
||||||
|
const artifactPath = this._pathBuilder.buildPathForTestArtifact(artifactName, testSummary);
|
||||||
|
const artifactDir = path.dirname(artifactPath);
|
||||||
|
await fs.ensureDir(artifactDir);
|
||||||
|
return artifactPath;
|
||||||
|
},
|
||||||
|
|
||||||
|
trackArtifact: (artifact) => this.emit('trackArtifact', artifact),
|
||||||
|
untrackArtifact: (artifact) => this.emit('untrackArtifact', artifact),
|
||||||
|
|
||||||
|
requestIdleCallback: (callback) => {
|
||||||
|
// Queue for execution during idle time
|
||||||
|
this._idleCallbackRequests.push({ caller: artifactsApi.plugin, callback });
|
||||||
|
return this._idlePromise;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const plugin = pluginFactory(artifactsApi);
|
||||||
|
artifactsApi.plugin = plugin;
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Event Subscription
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
subscribeToDeviceEvents(deviceEmitter) {
|
||||||
|
deviceEmitter.on('bootDevice', this.onBootDevice.bind(this));
|
||||||
|
deviceEmitter.on('beforeShutdownDevice', this.onBeforeShutdownDevice.bind(this));
|
||||||
|
deviceEmitter.on('shutdownDevice', this.onShutdownDevice.bind(this));
|
||||||
|
deviceEmitter.on('beforeLaunchApp', this.onBeforeLaunchApp.bind(this));
|
||||||
|
deviceEmitter.on('launchApp', this.onLaunchApp.bind(this));
|
||||||
|
deviceEmitter.on('appReady', this.onAppReady.bind(this));
|
||||||
|
deviceEmitter.on('beforeUninstallApp', this.onBeforeUninstallApp.bind(this));
|
||||||
|
deviceEmitter.on('beforeTerminateApp', this.onBeforeTerminateApp.bind(this));
|
||||||
|
deviceEmitter.on('terminateApp', this.onTerminateApp.bind(this));
|
||||||
|
deviceEmitter.on('createExternalArtifact', this.onCreateExternalArtifact.bind(this));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lifecycle Events
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Test lifecycle
|
||||||
|
async onRunDescribeStart(suite) {
|
||||||
|
await this._callPlugins('ascending', 'onRunDescribeStart', suite);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestStart(testSummary) {
|
||||||
|
await this._callPlugins('ascending', 'onTestStart', testSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestDone(testSummary) {
|
||||||
|
await this._callPlugins('descending', 'onTestDone', testSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRunDescribeFinish(suite) {
|
||||||
|
await this._callPlugins('descending', 'onRunDescribeFinish', suite);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failure hooks
|
||||||
|
async onHookFailure(testSummary) {
|
||||||
|
await this._callPlugins('plain', 'onHookFailure', testSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestFnFailure(testSummary) {
|
||||||
|
await this._callPlugins('plain', 'onTestFnFailure', testSummary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Execution Strategy
|
||||||
|
|
||||||
|
Plugins can be called in different orders based on priority:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async _callPlugins(strategy, methodName, ...args) {
|
||||||
|
for (const pluginGroup of this._groupPlugins(strategy)) {
|
||||||
|
await Promise.all(pluginGroup.map(async (plugin) => {
|
||||||
|
try {
|
||||||
|
await plugin[methodName](...args);
|
||||||
|
} catch (e) {
|
||||||
|
this._unhandledPluginExceptionHandler(e, { plugin, methodName });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_groupPlugins(strategy) {
|
||||||
|
if (strategy === 'plain') {
|
||||||
|
return [_.values(this._artifactPlugins)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const byPriority = _.chain(this._artifactPlugins)
|
||||||
|
.values()
|
||||||
|
.groupBy('priority')
|
||||||
|
.entries()
|
||||||
|
.sortBy(([priority]) => Number(priority))
|
||||||
|
.map(1)
|
||||||
|
.value();
|
||||||
|
|
||||||
|
return strategy === 'descending' ? byPriority.reverse() : byPriority;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Types
|
||||||
|
|
||||||
|
### Base Plugin (`src/artifacts/templates/plugin/ArtifactPlugin.js`)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ArtifactPlugin {
|
||||||
|
constructor({ api }) {
|
||||||
|
this.api = api;
|
||||||
|
this.enabled = api.userConfig.enabled !== false;
|
||||||
|
this.keepOnlyFailedTestsArtifacts = api.userConfig.keepOnlyFailedTestsArtifacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle hooks (override in subclasses)
|
||||||
|
async onBootDevice(deviceInfo) {}
|
||||||
|
async onBeforeLaunchApp(appLaunchInfo) {}
|
||||||
|
async onLaunchApp(appLaunchInfo) {}
|
||||||
|
async onTestStart(testSummary) {}
|
||||||
|
async onTestDone(testSummary) {}
|
||||||
|
async onBeforeCleanup() {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screenshot Plugin
|
||||||
|
|
||||||
|
Captures screenshots on test failure or on-demand:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ScreenshotArtifactPlugin extends TwoSnapshotsPerTestPlugin {
|
||||||
|
async preparePathForSnapshot(testSummary, name) {
|
||||||
|
return this.api.preparePathForArtifact(`${name}.png`, testSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async takeSnapshot(name) {
|
||||||
|
// Delegate to device driver
|
||||||
|
return this.driver.takeScreenshot(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
artifacts: {
|
||||||
|
plugins: {
|
||||||
|
screenshot: {
|
||||||
|
enabled: true,
|
||||||
|
shouldTakeAutomaticSnapshots: true,
|
||||||
|
keepOnlyFailedTestsArtifacts: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video Plugin
|
||||||
|
|
||||||
|
Records test execution:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class VideoArtifactPlugin extends WholeTestRecorderPlugin {
|
||||||
|
async onTestStart(testSummary) {
|
||||||
|
await this.startRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestDone(testSummary) {
|
||||||
|
await this.stopRecording();
|
||||||
|
|
||||||
|
if (testSummary.status === 'passed' && this.keepOnlyFailedTestsArtifacts) {
|
||||||
|
await this.discardRecording();
|
||||||
|
} else {
|
||||||
|
await this.saveRecording(testSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Plugin
|
||||||
|
|
||||||
|
Aggregates device and app logs:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class LogArtifactPlugin extends StartupAndTestRecorderPlugin {
|
||||||
|
async onLaunchApp(appLaunchInfo) {
|
||||||
|
await this.startCapturingLogs(appLaunchInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestDone(testSummary) {
|
||||||
|
const logs = await this.stopCapturingLogs();
|
||||||
|
await this.saveLogs(logs, testSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instruments Plugin (iOS)
|
||||||
|
|
||||||
|
Records performance profiles:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class InstrumentsArtifactPlugin extends ArtifactPlugin {
|
||||||
|
async startRecording({ recordingPath, samplingInterval }) {
|
||||||
|
await this.client.startInstrumentsRecording({ recordingPath, samplingInterval });
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopRecording() {
|
||||||
|
await this.client.stopInstrumentsRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI Hierarchy Plugin (iOS)
|
||||||
|
|
||||||
|
Captures view hierarchy for debugging (iOS only via `IosUIHierarchyPlugin`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class IosUIHierarchyPlugin extends ArtifactPlugin {
|
||||||
|
async onTestFnFailure(testSummary) {
|
||||||
|
const hierarchy = await this.client.captureViewHierarchy();
|
||||||
|
await this.saveHierarchy(hierarchy, testSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-Specific Implementations
|
||||||
|
|
||||||
|
Each artifact type has platform-specific implementations:
|
||||||
|
|
||||||
|
| Artifact | iOS (Simulator) | Android |
|
||||||
|
|----------|-----------------|---------|
|
||||||
|
| Screenshot | `SimulatorScreenshotPlugin` | `ADBScreencapPlugin` |
|
||||||
|
| Video | `SimulatorRecordVideoPlugin` | `ADBScreenrecorderPlugin` |
|
||||||
|
| Log | `SimulatorLogPlugin` | `ADBLogcatPlugin` |
|
||||||
|
| Instruments | `SimulatorInstrumentsPlugin` | `AndroidInstrumentsPlugin` |
|
||||||
|
| UI Hierarchy | `IosUIHierarchyPlugin` | Not available |
|
||||||
|
|
||||||
|
The appropriate plugin is selected by the provider based on the device type:
|
||||||
|
|
||||||
|
- `IosSimulatorArtifactPluginsProvider` for iOS simulators
|
||||||
|
- `AndroidArtifactPluginsProvider` for Android devices
|
||||||
|
|
||||||
|
## Artifact Templates
|
||||||
|
|
||||||
|
### Two Snapshots Per Test
|
||||||
|
|
||||||
|
Takes snapshots at test start and end:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class TwoSnapshotsPerTestPlugin extends ArtifactPlugin {
|
||||||
|
async onTestStart(testSummary) {
|
||||||
|
if (this.shouldTakeAutomaticSnapshots) {
|
||||||
|
await this.takeSnapshot('testStart');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestDone(testSummary) {
|
||||||
|
if (this.shouldTakeAutomaticSnapshots) {
|
||||||
|
await this.takeSnapshot('testDone');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Whole Test Recorder
|
||||||
|
|
||||||
|
Records throughout entire test:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class WholeTestRecorderPlugin extends ArtifactPlugin {
|
||||||
|
async onTestStart(testSummary) {
|
||||||
|
await this.startRecording(testSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestDone(testSummary) {
|
||||||
|
await this.stopRecording();
|
||||||
|
await this.saveOrDiscard(testSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Building
|
||||||
|
|
||||||
|
### ArtifactPathBuilder
|
||||||
|
|
||||||
|
Generates consistent artifact paths:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ArtifactPathBuilder {
|
||||||
|
constructor({ rootDir }) {
|
||||||
|
this._rootDir = rootDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPathForTestArtifact(artifactName, testSummary) {
|
||||||
|
// Path format: rootDir/{statusSign}{fullName}({invocations})/{artifactName}
|
||||||
|
// Example: artifacts/✓ Login should work/screenshot.png
|
||||||
|
// Example: artifacts/✗ Login should work (2)/screenshot.png (on retry)
|
||||||
|
const testDir = this._constructDirectoryNameForCurrentRunningTest(testSummary);
|
||||||
|
return path.join(this._rootDir, testDir, artifactName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_constructDirectoryNameForCurrentRunningTest(testSummary) {
|
||||||
|
const prefix = this._buildTestDirectoryPrefix(testSummary);
|
||||||
|
const suffix = testSummary.invocations > 1 ? ` (${testSummary.invocations})` : '';
|
||||||
|
return constructSafeFilename(prefix, testSummary.fullName, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildTestDirectoryPrefix(testSummary) {
|
||||||
|
return testSummary.status === 'passed' ? '✓ ' : '✗ ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom path builders can be provided:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// detox.config.js
|
||||||
|
module.exports = {
|
||||||
|
artifacts: {
|
||||||
|
rootDir: 'e2e-artifacts',
|
||||||
|
pathBuilder: './my-path-builder.js',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
artifacts: {
|
||||||
|
rootDir: 'artifacts',
|
||||||
|
pathBuilder: undefined, // or custom path builder
|
||||||
|
plugins: {
|
||||||
|
screenshot: {
|
||||||
|
enabled: true,
|
||||||
|
shouldTakeAutomaticSnapshots: true,
|
||||||
|
keepOnlyFailedTestsArtifacts: true,
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
enabled: true,
|
||||||
|
keepOnlyFailedTestsArtifacts: true,
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
enabled: true,
|
||||||
|
keepOnlyFailedTestsArtifacts: false,
|
||||||
|
},
|
||||||
|
instruments: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
uiHierarchy: { // iOS only
|
||||||
|
enabled: true,
|
||||||
|
keepOnlyFailedTestsArtifacts: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test Suite Starts
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
onRunDescribeStart(suite)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ For each test: │
|
||||||
|
│ │
|
||||||
|
│ onTestStart(testSummary) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ [Test executes] │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────┴────┐ │
|
||||||
|
│ │ Failure │ │
|
||||||
|
│ └────┬────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ onHookFailure() or │
|
||||||
|
│ onTestFnFailure() │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ onTestDone(testSummary) │
|
||||||
|
│ │ │
|
||||||
|
│ [Save or discard artifacts] │
|
||||||
|
│ │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
onRunDescribeFinish(suite)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
onBeforeCleanup()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[Final artifact processing]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Idle Callbacks
|
||||||
|
|
||||||
|
Plugins can defer non-critical work:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In plugin
|
||||||
|
async onTestDone(testSummary) {
|
||||||
|
// Critical work first
|
||||||
|
await this.stopRecording();
|
||||||
|
|
||||||
|
// Defer compression to idle time
|
||||||
|
this.api.requestIdleCallback(async () => {
|
||||||
|
await this.compressVideo();
|
||||||
|
await this.uploadToCloud();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## External Artifacts
|
||||||
|
|
||||||
|
Plugins can create artifacts from external sources:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// During test
|
||||||
|
device.createExternalArtifact({
|
||||||
|
pluginId: 'screenshot',
|
||||||
|
artifactName: 'custom-screenshot',
|
||||||
|
artifactPath: '/tmp/screenshot.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
// In ArtifactsManager
|
||||||
|
async onCreateExternalArtifact({ pluginId, artifactName, artifactPath }) {
|
||||||
|
await this._callSinglePlugin(pluginId, 'onCreateExternalArtifact', {
|
||||||
|
artifact: new FileArtifact({ temporaryPath: artifactPath }),
|
||||||
|
name: artifactName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [ARCHITECTURE.md](../../ARCHITECTURE.md) - Main architecture overview
|
||||||
|
- [docs/config/artifacts.mdx](../config/artifacts.mdx) - Artifacts configuration
|
||||||
|
- [docs/guide/taking-screenshots.md](../guide/taking-screenshots.md) - Screenshot guide
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
# Client-Server Communication
|
||||||
|
|
||||||
|
Detox uses a WebSocket-based client-server architecture for communication between the test process (Node.js) and the mobile app.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
|
│ Test Process │ │ DetoxServer │ │ Mobile App │
|
||||||
|
│ (Node.js) │ │ (localhost) │ │ (iOS/Android) │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌────────────┐ │ WS │ ┌────────────┐ │ WS │ ┌────────────┐ │
|
||||||
|
│ │ Client │◄─┼───────►│ │ Session │◄─┼───────►│ │ Native │ │
|
||||||
|
│ │ │ │ │ │ Manager │ │ │ │ Client │ │
|
||||||
|
│ └────────────┘ │ │ └────────────┘ │ │ └────────────┘ │
|
||||||
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why a Mediator Server?
|
||||||
|
|
||||||
|
The server acts as a mediator rather than having direct client-to-app communication:
|
||||||
|
|
||||||
|
1. **Resilience** - Either side can disconnect (app restart, simulator boot) without losing the other's state
|
||||||
|
2. **Session management** - Multiple test workers can share device sessions
|
||||||
|
3. **Debugging** - Server can log all messages for troubleshooting
|
||||||
|
4. **Flexibility** - Server can run on a different machine for CI/CD
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Client (`src/client/Client.js`)
|
||||||
|
|
||||||
|
The test-side WebSocket client that:
|
||||||
|
|
||||||
|
- Connects to the Detox server
|
||||||
|
- Sends actions to the mobile app
|
||||||
|
- Handles responses and events
|
||||||
|
- Manages synchronization state
|
||||||
|
|
||||||
|
**Key methods:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Connect to server and login to session
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Send an invocation (tap, type, scroll, etc.)
|
||||||
|
await client.execute(invocation);
|
||||||
|
|
||||||
|
// Wait for app to be ready (synchronized)
|
||||||
|
await client.waitUntilReady();
|
||||||
|
|
||||||
|
// Send action and wait for response
|
||||||
|
await client.sendAction(new actions.Invoke(invocation));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Event handling:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.setEventCallback('appConnected', this._onAppConnected);
|
||||||
|
client.setEventCallback('ready', this._onAppReady);
|
||||||
|
client.setEventCallback('AppNonresponsiveDetected', this._onAppUnresponsive);
|
||||||
|
client.setEventCallback('AppWillTerminateWithError', this._onBeforeAppCrash);
|
||||||
|
client.setEventCallback('appDisconnected', this._onAppDisconnected);
|
||||||
|
client.setEventCallback('serverError', this._onUnhandledServerError);
|
||||||
|
```
|
||||||
|
|
||||||
|
### AsyncWebSocket (`src/client/AsyncWebSocket.js`)
|
||||||
|
|
||||||
|
Promise-based wrapper around the WebSocket API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class AsyncWebSocket {
|
||||||
|
async open() { /* ... */ }
|
||||||
|
async close() { /* ... */ }
|
||||||
|
async send(action, options) { /* ... */ }
|
||||||
|
|
||||||
|
// Track in-flight requests for timeout handling
|
||||||
|
inFlightPromises = {};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DetoxServer (`src/server/DetoxServer.js`)
|
||||||
|
|
||||||
|
WebSocket server that mediates all communication:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class DetoxServer {
|
||||||
|
constructor({ port, standalone }) {
|
||||||
|
this._sessionManager = new DetoxSessionManager();
|
||||||
|
this._wss = new WebSocket.Server({ port });
|
||||||
|
}
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
// Start listening
|
||||||
|
}
|
||||||
|
|
||||||
|
_onConnection(ws, req) {
|
||||||
|
this._sessionManager.registerConnection(ws, req.socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DetoxSession (`src/server/DetoxSession.js`)
|
||||||
|
|
||||||
|
Per-session state management:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class DetoxSession {
|
||||||
|
constructor(id) {
|
||||||
|
this.id = id;
|
||||||
|
this.tester = null; // Test process connection
|
||||||
|
this.app = null; // Mobile app connection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route messages between tester and app
|
||||||
|
// Handle connection/disconnection events
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DetoxSessionManager (`src/server/DetoxSessionManager.js`)
|
||||||
|
|
||||||
|
Manages multiple concurrent sessions:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class DetoxSessionManager {
|
||||||
|
constructor() {
|
||||||
|
this._sessions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerConnection(ws, socket) {
|
||||||
|
// Identify session from login message
|
||||||
|
// Create or join existing session
|
||||||
|
}
|
||||||
|
|
||||||
|
getSession(sessionId) {
|
||||||
|
return this._sessions.get(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Message Protocol
|
||||||
|
|
||||||
|
All messages are JSON objects with a standard structure.
|
||||||
|
|
||||||
|
**Platform differences:** iOS and Android implement the protocol quite differently:
|
||||||
|
|
||||||
|
- **Android** is reflection-centric — the `generation/` project maps Java classes to JS invocation objects (see `src/android/espressoapi/`), allowing the test side to call Android APIs via reflection-based dispatch
|
||||||
|
- **iOS** has a more handcrafted and formalized protocol, with explicit message types handled on the native side
|
||||||
|
|
||||||
|
**Note:** Invocation targets vary by operation type:
|
||||||
|
|
||||||
|
- High-level operations use `com.wix.detox.Detox` as the entry point
|
||||||
|
- Action-specific invocations may use `com.wix.detox.espresso.DetoxAction` (Android) or platform-specific targets
|
||||||
|
|
||||||
|
### Request (Test → App)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "invoke",
|
||||||
|
"messageId": 1,
|
||||||
|
"params": {
|
||||||
|
"target": {
|
||||||
|
"type": "Class",
|
||||||
|
"value": "com.wix.detox.espresso.DetoxAction"
|
||||||
|
},
|
||||||
|
"method": "perform",
|
||||||
|
"args": [
|
||||||
|
{ "matcher": "..." },
|
||||||
|
{ "action": "..." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (App → Test)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "invokeResult",
|
||||||
|
"messageId": 1,
|
||||||
|
"params": {
|
||||||
|
"result": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event (App → Test)
|
||||||
|
|
||||||
|
Events typically use fixed negative `messageId` values (e.g., `-1000`, `-0xc1ea`) rather than the incrementing positive IDs used for request-response pairs.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ready",
|
||||||
|
"messageId": -1000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "AppWillTerminateWithError",
|
||||||
|
"params": {
|
||||||
|
"errorDetails": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actions (`src/client/actions/actions.js`)
|
||||||
|
|
||||||
|
Predefined action classes for common operations:
|
||||||
|
|
||||||
|
| Action | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `Login` | Authenticate with session ID |
|
||||||
|
| `Ready` | Query if app is ready |
|
||||||
|
| `Invoke` | Execute a command on the app |
|
||||||
|
| `Cleanup` | Signal test completion |
|
||||||
|
| `ReloadReactNative` | Reload the RN bundle |
|
||||||
|
| `CurrentStatus` | Get app synchronization status |
|
||||||
|
| `Shake` | Trigger device shake |
|
||||||
|
| `SetOrientation` | Change device orientation |
|
||||||
|
| `SetSyncSettings` | Configure synchronization settings |
|
||||||
|
| `DeliverPayload` | Send push notification, deep link |
|
||||||
|
| `CaptureViewHierarchy` | Dump UI hierarchy |
|
||||||
|
| `GenerateViewHierarchyXml` | Generate UI hierarchy as XML (iOS and Android) |
|
||||||
|
| `SetInstrumentsRecordingState` | Control iOS performance profiling |
|
||||||
|
| `WaitForBackground` | Wait for app to background |
|
||||||
|
| `WaitForActive` | Wait for app to foreground |
|
||||||
|
|
||||||
|
## Connection Lifecycle
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. Test starts
|
||||||
|
│
|
||||||
|
├── Client.connect()
|
||||||
|
│ └── WebSocket connects to server
|
||||||
|
│
|
||||||
|
├── Client.sendAction(Login)
|
||||||
|
│ └── Server creates/joins session
|
||||||
|
│
|
||||||
|
└── Server waits for app connection
|
||||||
|
|
||||||
|
2. App launches (with server URL in launch args)
|
||||||
|
│
|
||||||
|
├── Native client connects to server
|
||||||
|
│ └── Server associates with session
|
||||||
|
│
|
||||||
|
└── Server notifies test: 'appConnected'
|
||||||
|
|
||||||
|
3. App becomes ready
|
||||||
|
│
|
||||||
|
└── Native client sends 'ready' event
|
||||||
|
|
||||||
|
4. Test executes
|
||||||
|
│
|
||||||
|
├── Client.execute(invocation)
|
||||||
|
│ ├── Server routes to app
|
||||||
|
│ ├── App executes
|
||||||
|
│ └── App responds via server
|
||||||
|
│
|
||||||
|
└── Client receives result
|
||||||
|
|
||||||
|
5. Test completes
|
||||||
|
│
|
||||||
|
├── Client.cleanup()
|
||||||
|
│ └── Sends Cleanup action
|
||||||
|
│
|
||||||
|
└── Client.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Synchronization
|
||||||
|
|
||||||
|
The client monitors app readiness through deferred promises (simplified):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Track connection state
|
||||||
|
this._whenAppIsConnected = new Deferred();
|
||||||
|
this._whenAppIsReady = new Deferred();
|
||||||
|
|
||||||
|
// Wait for synchronization (simplified - actual implementation has additional checks)
|
||||||
|
async waitUntilReady() {
|
||||||
|
await this._whenAppIsConnected.promise;
|
||||||
|
await this.sendAction(new actions.Ready());
|
||||||
|
this._whenAppIsReady.resolve();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Slow Invocation Detection
|
||||||
|
|
||||||
|
When `debugSynchronization` is enabled, the client periodically queries app status during long operations:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
_scheduleSlowInvocationQuery() {
|
||||||
|
this._slowInvocationStatusHandle = setTimeout(async () => {
|
||||||
|
const status = await this.currentStatus();
|
||||||
|
log.info({ event: 'APP_STATUS' }, status);
|
||||||
|
// Reschedule if still waiting
|
||||||
|
}, this._slowInvocationTimeout);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### App Crash Detection
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
_onBeforeAppCrash({ params }) {
|
||||||
|
this._pendingAppCrash = new DetoxRuntimeError({
|
||||||
|
message: 'The app has crashed',
|
||||||
|
debugInfo: params.errorDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reject pending promises
|
||||||
|
// Schedule app termination
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disconnect Handling
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
_onAppDisconnected() {
|
||||||
|
if (this._pendingAppCrash) {
|
||||||
|
this._whenAppDisconnected.reject(this._pendingAppCrash);
|
||||||
|
this._asyncWebSocket.rejectAll(this._pendingAppCrash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Stack Traces
|
||||||
|
|
||||||
|
The client preserves user test stack traces for better error messages:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async _doSendAction(action, options) {
|
||||||
|
const errorWithUserStack = createErrorWithUserStack();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this._asyncWebSocket.send(action, options);
|
||||||
|
return await action.handle(response);
|
||||||
|
} catch (err) {
|
||||||
|
throw replaceErrorStack(errorWithUserStack, asError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [ARCHITECTURE.md](../../ARCHITECTURE.md) - Main architecture overview
|
||||||
|
- [docs/architecture/devices.md](devices.md) - Device management
|
||||||
|
- [docs/config/session.mdx](../config/session.mdx) - Session configuration
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
# Device Management
|
||||||
|
|
||||||
|
The device subsystem handles allocation, lifecycle, and interaction with iOS simulators, Android emulators, and cloud devices.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Device Subsystem │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────┐ │
|
||||||
|
│ │ DeviceAllocator │─►│ RuntimeDevice │─►│ Driver │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ - allocate() │ │ - launchApp() │ │ - boot() │ │
|
||||||
|
│ │ - free() │ │ - terminateApp()│ │ - install()│ │
|
||||||
|
│ │ │ │ - reloadRN() │ │ - execute()│ │
|
||||||
|
│ └────────┬────────┘ └────────┬────────┘ └────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌────────▼────────┐ ┌────────▼────────┐ │
|
||||||
|
│ │ DeviceRegistry │ │ DeviceCookie │ │
|
||||||
|
│ │ (Lock files) │ │ (Serialization) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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
|
||||||
|
|
||||||
|
```text
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Instrumentation {
|
||||||
|
async launch(deviceId, bundleId, testRunner, args) {
|
||||||
|
// am instrument -w -r ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Device Lifecycle
|
||||||
|
|
||||||
|
### Allocation Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
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
|
||||||
|
|
||||||
|
```text
|
||||||
|
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 Cookie
|
||||||
|
|
||||||
|
Device cookies serialize device state for passing between processes:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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
|
||||||
|
|
||||||
|
- [ARCHITECTURE.md](../../ARCHITECTURE.md) - Main architecture overview
|
||||||
|
- [docs/config/devices.mdx](../config/devices.mdx) - Device configuration reference
|
||||||
|
- [docs/guide/android-dev-env.md](../guide/android-dev-env.md) - Android setup guide
|
||||||
Reference in New Issue
Block a user