Detox plus React Native testing on real cloud devices
Detox plus React Native testing on real cloud devices
if you build with React Native and want to run Detox e2e tests against real Android devices, cloud phones are the cleanest answer. Detox was built around emulator-style configurations, but the same toolchain works on any device that can talk ADB, including cloud phones connected over ADB-over-network. this post walks the full setup: configure Detox for cloud phones, build the test APK, run a smoke flow, and parallelize across a Singapore cloud-phone pool.
cloud phones beat emulators for React Native specifically because RN apps depend on real-arm64 native modules: react-native-firebase, react-native-iap, react-native-camera, and any module that wraps an Android-only SDK. emulators handle some of these, but the failure modes (FCM push not arriving, IAP attestation failing, camera initialization race conditions) only show up on real hardware.
why Detox is opinionated about devices
Detox synchronizes test execution with the React Native bridge. it waits for the bridge to idle before issuing the next action, which gives you flake-free e2e tests without adding Thread.sleep everywhere. this synchronization works on emulators and real devices identically as long as the Detox build hooks land in your app’s debug variant.
the catch is the device configuration. Detox device blocks in .detoxrc.js historically referenced AVD names. for cloud phones, you specify the device by ADB serial, which is just the network endpoint.
for context on cloud phone basics, see cloud phone vs physical Android device and how to set up ADB on cloudf.one.
prerequisites
your React Native project should be on RN 0.71 or newer. Detox 20 or newer (Detox 19 is end-of-life). Java 17, Android platform-tools, Node.js 20, and Gradle.
# install Detox CLI
npm install -g detox-cli
# install Detox into your RN project
cd your-rn-app
npm install --save-dev detox jest
# scaffold Detox config
detox init -r jest
this creates .detoxrc.js, e2e/ folder, and a Jest setup.
step 1: configure Detox for cloud phones
edit .detoxrc.js:
module.exports = {
testRunner: { args: { config: 'e2e/jest.config.js' } },
apps: {
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
testBinaryPath: 'android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk',
},
},
devices: {
'cloud-phone-sg': {
type: 'android.attached',
device: { adbName: 'adb-sg.cloudf.one:5555' },
},
},
configurations: {
'android.cloud': {
device: 'cloud-phone-sg',
app: 'android.debug',
},
},
};
the android.attached device type tells Detox to use a device already visible in adb devices rather than spinning up an emulator. adbName matches the ADB serial.
step 2: connect to the cloud phone
adb connect adb-sg.cloudf.one:5555
adb devices
confirm the cloud phone appears with the network endpoint as its serial.
step 3: write your first Detox test
e2e/login.test.js:
describe('login flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should show login screen', async () => {
await expect(element(by.id('loginScreen'))).toBeVisible();
});
it('should log in with valid credentials', async () => {
await element(by.id('usernameInput')).typeText('test');
await element(by.id('passwordInput')).typeText('test123');
await element(by.id('loginButton')).tap();
await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(5000);
});
});
your React components must have testID props matching the Detox locators. if your team has not yet adopted testID discipline, add it before scaling Detox.
step 4: build and run
detox build --configuration android.cloud
detox test --configuration android.cloud
Detox builds the debug APK, builds the androidTest APK (which contains the Detox test runner), installs both on the cloud phone, and runs the Jest test files. results appear in the terminal. on failure, screenshots and view-hierarchy dumps land in artifacts/.
step 5: parallelize across multiple cloud phones
Detox has built-in worker support via --workers. each worker drives one device.
// add a second device to .detoxrc.js
devices: {
'cloud-phone-sg-1': {
type: 'android.attached',
device: { adbName: 'adb-sg.cloudf.one:5555' },
},
'cloud-phone-sg-2': {
type: 'android.attached',
device: { adbName: 'adb-sg.cloudf.one:5556' },
},
},
connect both:
adb connect adb-sg.cloudf.one:5555
adb connect adb-sg.cloudf.one:5556
detox test --configuration android.cloud --workers 2
Detox shards the test files across workers. with 2 cloud phones, wall-clock test time drops by roughly half.
step 6: handle the synchronization model
Detox waits for the React Native bridge to idle before each action. that works for most flows but breaks when your app uses long-running animations, infinite loops, or polling timers. tell Detox to ignore them:
// in your app code, before tests run
import {NativeModules} from 'react-native';
NativeModules.DetoxRNDebugClient?.setSyncMode('off');
or use Detox’s device.disableSynchronization() for specific test cases. the Detox synchronization docs cover the full API.
handling Firebase, push notifications, and native modules
Detox on cloud phones makes one category of tests possible that emulators struggle with: Firebase Cloud Messaging end-to-end. on a real cloud phone, FCM push tokens register correctly, the FCM sender ID resolves, and push delivery works. on emulators, push delivery is brittle.
it('should handle push notification', async () => {
await device.sendUserNotification({
trigger: { type: 'push' },
title: 'test',
body: 'hello',
payload: { type: 'order_update', orderId: '123' },
});
await waitFor(element(by.id('orderUpdateScreen'))).toBeVisible().withTimeout(3000);
});
device.sendUserNotification works on attached cloud phones the same as on emulators.
CI integration
Detox runs in CI via the same pattern as Espresso or Appium: lock cloud phone, connect ADB, run Detox, capture artifacts, unlock. our CI/CD integration guide covers the full GitHub Actions flow.
- name: run Detox
run: |
adb connect ${{ steps.lock.outputs.adb_endpoint }}
detox build --configuration android.cloud
detox test --configuration android.cloud --headless
common pitfalls
three issues catch teams new to Detox on cloud phones.
first, animation duration. disable system animations on the cloud phone:
adb -s adb-sg.cloudf.one:5555 shell settings put global window_animation_scale 0
adb -s adb-sg.cloudf.one:5555 shell settings put global transition_animation_scale 0
adb -s adb-sg.cloudf.one:5555 shell settings put global animator_duration_scale 0
second, the test APK build. Detox needs assembleAndroidTest to produce the runner APK. CI must build both debug and androidTest variants.
third, ADB connection drops on long runs. add a heartbeat in CI:
while true; do adb -s $UDID shell echo ok; sleep 30; done &
try Detox on a real Singapore cloud phone
register for a free trial for a one-hour token. that’s enough time to run the login example above against your own RN app. once confirmed, scale to a paid plan and add cloud phones to your test pool.
frequently asked questions
does Detox work with Expo?
yes, but you need the bare workflow or a custom Expo dev client with Detox’s native config. the managed Expo workflow does not support Detox out of the box.
can I run Detox tests on Android and iOS in the same CI pipeline?
yes, but iOS Detox needs a real macOS runner with Xcode. cloudf.one is Android-first; for iOS see AWS Device Farm or Firebase Test Lab.
how is Detox different from Maestro?
Detox is React Native-aware and synchronizes with the bridge. Maestro is platform-agnostic, drives any app via UI taps, and is simpler to write. for RN-specific apps, Detox catches more bugs. for cross-stack apps, Maestro is easier. our Maestro tutorial covers the alternative.
does Detox capture screen recordings?
yes, via the recordVideos artifact configuration in .detoxrc.js. set recordVideos: 'failing' to keep video only for failed tests.
can I use Detox with Hermes?
yes. Detox supports both Hermes and the standard JSC JavaScript engine. no special config required.