TestKase Docs
AutomationTest Frameworks

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-junit

Install the Detox CLI globally:

npm install -g detox-cli

For iOS, install applesimutils via Homebrew:

brew tap wix/brew
brew install applesimutils

Project 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 TestKase
  • 48272 → linked to the "invalid password" test case in TestKase
  • 48273 → 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.debug

Run the tests:

detox test --configuration ios.sim.debug

Since 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 CodeExtracted 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.debug

3. 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

Troubleshooting

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 available

Update 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);