Detox
Set up Detox for React Native testing and integrate test results with TestKase.
Overview
Detox is a gray-box end-to-end testing framework for React Native applications, built and maintained by Wix. It runs tests on real devices and simulators, synchronizing automatically with the app to eliminate flakiness. Detox uses Jest as its test runner, so the test authoring experience is familiar to JavaScript developers.
To integrate Detox results with TestKase, generate JUnit XML output using the jest-junit package (since
Detox runs on top of Jest) and report with --format junit.
Prerequisites
- Node.js 18+ and npm (or yarn/pnpm)
- A React Native project
- Xcode (for iOS testing) or Android SDK (for Android testing)
- applesimutils (required for iOS simulator control)
Installation
Install Detox, its Jest adapter, and the JUnit reporter:
npm install --save-dev detox jest-circus jest-junitInstall the Detox CLI globally:
npm install -g detox-cliFor iOS, install applesimutils via Homebrew:
brew tap wix/brew
brew install applesimutilsProject Setup
Detox Configuration
Create a .detoxrc.js file in your project root:
// .detoxrc.js
module.exports = {
testRunner: {
args: {
config: 'e2e/jest.config.js',
_: ['e2e'],
},
},
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',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: { type: 'iPhone 15' },
},
emulator: {
type: 'android.emulator',
device: { avdName: 'Pixel_6_API_34' },
},
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug',
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug',
},
},
};Jest Configuration for Detox
Create the Jest config at e2e/jest.config.js:
// e2e/jest.config.js
module.exports = {
rootDir: '..',
testMatch: ['<rootDir>/e2e/**/*.e2e.js'],
testTimeout: 120000,
maxWorkers: 1,
globalSetup: 'detox/runners/jest/globalSetup',
globalTeardown: 'detox/runners/jest/globalTeardown',
testEnvironment: 'detox/runners/jest/testEnvironment',
reporters: [
'default',
['jest-junit', {
outputDirectory: 'test-results',
outputName: 'junit.xml',
}],
],
};Writing Tests
Create an end-to-end test file (e.g., e2e/login.e2e.js):
// e2e/login.e2e.js
describe('Login Screen', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative();
});
test('[48271] should login with valid credentials', async () => {
await element(by.id('emailInput')).typeText('user@example.com');
await element(by.id('passwordInput')).typeText('password123');
await element(by.id('loginButton')).tap();
await expect(element(by.id('dashboardTitle'))).toBeVisible();
});
test('[48272] should show error for invalid password', async () => {
await element(by.id('emailInput')).typeText('user@example.com');
await element(by.id('passwordInput')).typeText('wrong');
await element(by.id('loginButton')).tap();
await expect(element(by.text('Invalid credentials'))).toBeVisible();
});
test('[48273] should navigate to forgot password', async () => {
await element(by.id('forgotPasswordLink')).tap();
await expect(element(by.id('resetPasswordTitle'))).toBeVisible();
});
});Each test name includes a 5-digit Automation ID in square brackets. The @testkase/reporter CLI
extracts these IDs using the regex \[(\d{5})\]. For the tests above:
48271→ linked to the "valid login" test case in TestKase48272→ linked to the "invalid password" test case in TestKase48273→ linked to the "forgot password" test case in TestKase
Generate Automation IDs in TestKase first, then embed them in your test names. The [XXXXX] pattern
can appear anywhere in the test name — the reporter extracts all 5-digit IDs found in brackets.
Running Tests
Build the app for testing:
detox build --configuration ios.sim.debugRun the tests:
detox test --configuration ios.sim.debugSince jest-junit is configured as a reporter in the Jest config, the JUnit XML file is automatically
generated at test-results/junit.xml after the test run completes.
TestKase Integration
After tests complete and the JUnit XML is generated, report results to TestKase:
npx @testkase/reporter report \
--token $TESTKASE_PAT \
--project-id PRJ-1 \
--org-id 1173 \
--cycle-id TCYCLE-5 \
--format junit \
--results-file test-results/junit.xml--cycle-id is optional. If not provided, results are reported to TCYCLE-1 — the master test cycle for the project.
Automation ID Mapping
The reporter extracts 5-digit Automation IDs from Detox test names using the [XXXXX] bracket pattern:
| Test Code | Extracted ID |
|---|---|
test('[48271] should login with valid credentials', ...) | 48271 |
test('[48272] should show error for invalid password', ...) | 48272 |
describe('App', () => { test('[48273] adds item', ...) }) | 48273 |
The [XXXXX] pattern can appear anywhere in the test() name. The describe block structure does
not affect the Automation ID — only the 5-digit number inside brackets matters.
Complete Example
1. Test File
// e2e/login.e2e.js
describe('Login Screen', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative();
});
test('[48271] should login with valid credentials', async () => {
await element(by.id('emailInput')).typeText('user@example.com');
await element(by.id('passwordInput')).typeText('password123');
await element(by.id('loginButton')).tap();
await expect(element(by.id('dashboardTitle'))).toBeVisible();
});
test('[48272] should show error for invalid password', async () => {
await element(by.id('emailInput')).typeText('user@example.com');
await element(by.id('passwordInput')).typeText('wrong');
await element(by.id('loginButton')).tap();
await expect(element(by.text('Invalid credentials'))).toBeVisible();
});
});2. Build and Run Tests
# Build the app
detox build --configuration ios.sim.debug
# Run the tests (jest-junit generates the XML automatically)
detox test --configuration ios.sim.debug3. Report Results to TestKase
npx @testkase/reporter report \
--token $TESTKASE_PAT \
--project-id PRJ-1 \
--org-id 1173 \
--cycle-id TCYCLE-5 \
--format junit \
--results-file test-results/junit.xmlTroubleshooting
Build failed
Ensure all native dependencies are linked and CocoaPods are installed for iOS:
cd ios && pod install && cd ..For Android, verify the Gradle build succeeds independently:
cd android && ./gradlew assembleDebug && cd ..Also check that the binaryPath in .detoxrc.js points to the correct build output location.
Simulator not found
Verify the device configuration in .detoxrc.js matches an available simulator. List available simulators:
xcrun simctl list devices availableUpdate the device.type field in the devices.simulator section to match an installed simulator name
(e.g., iPhone 15, iPhone 15 Pro).
Test timeout
Detox tests may time out if the app takes too long to synchronize. Increase the testTimeout value in
your e2e/jest.config.js:
module.exports = {
testTimeout: 300000, // 5 minutes
// ...
};If a specific interaction is slow, you can disable automatic synchronization temporarily and add manual
waits using waitFor:
await waitFor(element(by.id('loadingIndicator')))
.not.toBeVisible()
.withTimeout(10000);