This document provides an overview of Detox's internal architecture for contributors and maintainers.
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.
┌───────────────────────────────────────────────────────────┐
│ 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 │ │
│ └───────────────────┘ │
└───────────────────────────────────────────────────────────┘
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 exposingdevice,element,expect,by,waitFor,web,systemDetoxPrimaryContext.js- Handles initialization, device allocation, server lifecycleDetoxSecondaryContext.js- Receives config snapshots, no device management
The base DetoxContext uses funpermaproxy to lazily delegate API calls to the worker:
device = funpermaproxy(() => this[symbols.worker].device);
element = funpermaproxy.callable(() => this[symbols.worker].element);Each test worker creates a DetoxWorker instance that:
- Connects to the Detox server via
Client - Creates the
InvocationManagerfor command serialization - Instantiates the
RuntimeDevicevia environment factories - Creates matchers (
by,element,expect,waitFor) - Manages the
ArtifactsManager
Lifecycle hooks:
onRunDescribeStart/onRunDescribeFinishonTestStart/onTestDoneonHookFailure/onTestFnFailure
See docs/architecture/client-server.md for details.
Key components:
Client- WebSocket client connecting to server, sends actions to appAsyncWebSocket- Promise-based WebSocket wrapperDetoxServer- WebSocket server mediating tester ↔ app communicationDetoxSession/DetoxSessionManager- Per-session state management
See docs/architecture/devices.md for details.
Structure:
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
See docs/architecture/artifacts.md for details.
Plugins:
screenshot- Captures screenshots on failure or on-demandvideo- Records test execution videolog- Aggregates device and app logsinstruments- iOS performance profilinguiHierarchy- Dumps view hierarchy for debugging (iOS only)
Commands are serialized as JSON and sent to the native app:
// 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.
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/.
runners/
├── jest/
│ ├── testEnvironment/ # Jest environment setup
│ ├── reporters/ # Custom reporters
│ ├── globalSetup.js
│ └── globalTeardown.js
├── jest-circus/ # Circus event handling
└── mocha/ # Mocha adapter
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
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
Internal members use Symbols to prevent accidental access:
const $worker = Symbol('worker');
class DetoxContext {
[$worker] = null;
}Environment-specific factories create platform-appropriate instances:
const factories = environmentFactory.createFactories(deviceConfig);
// Returns: envValidatorFactory, artifactsManagerFactory,
// matchersFactory, runtimeDeviceFactoryLong-running operations use CAF for cancellation:
this._reinstallAppsOnDevice = CAF(this._reinstallAppsOnDevice.bind(this));
// Can be cancelled via: this._initToken.abort('CLEANUP')ArtifactsManager and RuntimeDevice use event emitters for loose coupling:
deviceEmitter.on('bootDevice', this.onBootDevice.bind(this));
deviceEmitter.on('launchApp', this.onLaunchApp.bind(this));Configuration flows through composition:
.detoxrc.js / detox.config.js
│
▼
loadExternalConfig()
│
▼
CLI arguments (--configuration, --device-name, etc.)
│
▼
composeDetoxConfig()
├── composeAppsConfig()
├── composeDeviceConfig()
├── composeArtifactsConfig()
├── composeBehaviorConfig()
└── composeSessionConfig()
│
▼
RuntimeConfig
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
- docs/architecture/client-server.md - Client-server protocol details
- docs/architecture/devices.md - Device management internals
- docs/architecture/artifacts.md - Artifact collection system
- docs/articles/how-detox-works.md - User-facing explanation
- docs/articles/design-principles.md - Design philosophy