← All skills

Detox Skill

Mobile testingJavaScriptTypeScript

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill detox-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Detox — Advanced Implementation Playbook

§1 Project Setup & Configuration

.detoxrc.js — Production Config

/** @type {Detox.DetoxConfig} */
module.exports = {
  testRunner: {
    args: {
      config: 'e2e/jest.config.js',
      maxWorkers: 1,
      _: ['e2e'],
    },
    jest: {
      setupTimeout: 120000,
      teardownTimeout: 30000,
    },
  },
  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',
    },
    'ios.release': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/MyApp.app',
      build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -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',
      reversePorts: [8081],
    },
    'android.release': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
      build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
    },
  },
  devices: {
    simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } },
    emulator: { type: 'android.emulator', device: { avdName: 'Pixel_6_API_34' } },
    'attached.android': { type: 'android.attached', device: { adbName: '.*' } },
  },
  configurations: {
    'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
    'ios.sim.release': { device: 'simulator', app: 'ios.release' },
    'android.emu.debug': { device: 'emulator', app: 'android.debug' },
    'android.emu.release': { device: 'emulator', app: 'android.release' },
    'android.att.debug': { device: 'attached.android', app: 'android.debug' },
  },
  artifacts: {
    rootDir: './e2e/artifacts',
    plugins: {
      screenshot: { shouldTakeAutomaticSnapshots: true, keepOnlyFailedTestsArtifacts: true },
      video: { enabled: true },
      log: { enabled: true },
    },
  },
  behavior: {
    init: { exposeGlobals: true },
    cleanup: { shutdownDevice: false },
  },
};

e2e/jest.config.js

module.exports = {
  maxWorkers: 1,
  testTimeout: 120000,
  rootDir: '..',
  testMatch: ['<rootDir>/e2e/**/*.test.js'],
  verbose: true,
  reporters: [
    'default',
    ['jest-junit', { outputDirectory: './e2e/artifacts', outputName: 'junit.xml' }],
  ],
};

§2 Page Object Pattern

BasePage

class BasePage {
  async waitForVisible(testID, timeout = 5000) {
    await waitFor(element(by.id(testID)))
      .toBeVisible()
      .withTimeout(timeout);
  }

  async tapById(testID) {
    await element(by.id(testID)).tap();
  }

  async typeById(testID, text) {
    await element(by.id(testID)).clearText();
    await element(by.id(testID)).typeText(text);
  }

  async takeScreenshot(name) {
    await device.takeScreenshot(name);
  }
}

LoginPage

class LoginPage extends BasePage {
  get emailInput() { return element(by.id('email_input')); }
  get passwordInput() { return element(by.id('password_input')); }
  get loginButton() { return element(by.id('login_button')); }
  get errorMessage() { return element(by.id('error_message')); }

  async enterEmail(email) {
    await this.emailInput.clearText();
    await this.emailInput.typeText(email);
  }

  async enterPassword(password) {
    await this.passwordInput.clearText();
    await this.passwordInput.typeText(password);
  }

  async tapLogin() {
    await this.loginButton.tap();
  }

  async login(email, password) {
    await this.enterEmail(email);
    await this.enterPassword(password);
    await this.tapLogin();
    return new DashboardPage();
  }

  async expectError(message) {
    await expect(this.errorMessage).toBeVisible();
    await expect(this.errorMessage).toHaveText(message);
  }
}

module.exports = { LoginPage };

DashboardPage

class DashboardPage extends BasePage {
  get welcomeText() { return element(by.id('welcome_text')); }
  get productList() { return element(by.id('product_list')); }
  get cartBadge() { return element(by.id('cart_badge')); }

  async verifyOnScreen() {
    await waitFor(this.welcomeText).toBeVisible().withTimeout(10000);
  }

  async selectProduct(index) {
    await element(by.id(`product_item_${index}`)).tap();
    return new ProductDetailPage();
  }
}

module.exports = { DashboardPage };

§3 Test Patterns

Complete Flow Test

const { LoginPage } = require('./pages/LoginPage');
const { DashboardPage } = require('./pages/DashboardPage');

describe('Purchase Flow', () => {
  beforeAll(async () => {
    await device.launchApp({ newInstance: true, permissions: { notifications: 'YES' } });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should complete login → browse → purchase', async () => {
    const loginPage = new LoginPage();
    const dashboard = await loginPage.login('user@test.com', 'password123');

    await dashboard.verifyOnScreen();
    await device.takeScreenshot('dashboard_loaded');

    const productPage = await dashboard.selectProduct(0);
    await productPage.addToCart();
    await expect(dashboard.cartBadge).toBeVisible();

    await productPage.goToCart();
    await element(by.id('checkout_button')).tap();
    await waitFor(element(by.text('Order Confirmed')))
      .toBeVisible()
      .withTimeout(15000);
  });

  it('should show error for invalid credentials', async () => {
    const loginPage = new LoginPage();
    await loginPage.enterEmail('wrong@test.com');
    await loginPage.enterPassword('wrong');
    await loginPage.tapLogin();
    await loginPage.expectError('Invalid credentials');
  });
});

§4 Advanced Interactions

Scroll & Search

// Scroll until element is visible
await waitFor(element(by.text('Load More')))
  .toBeVisible()
  .whileElement(by.id('scroll_view'))
  .scroll(200, 'down');

// Scroll to edge
await element(by.id('scroll_view')).scrollTo('bottom');
await element(by.id('scroll_view')).scrollTo('top');

// FlatList — scroll by index
await element(by.id('product_list')).scrollTo('bottom');

Gestures

// Swipe
await element(by.id('card')).swipe('left', 'fast', 0.75);
await element(by.id('card')).swipe('right', 'slow', 0.5);

// Long press
await element(by.id('item')).longPress(2000);

// Multi-tap
await element(by.id('zoom_target')).multiTap(2); // double-tap

// Pinch (iOS only)
await element(by.id('map_view')).pinch(1.5); // zoom in
await element(by.id('map_view')).pinch(0.5); // zoom out

Device Operations

// Location
await device.setLocation(37.7749, -122.4194);

// Deep links
await device.openURL('myapp://products/123');

// Shake device
await device.shake();

// Background & foreground
await device.sendToHome();
await device.launchApp({ newInstance: false });

// Biometric (iOS simulator)
await device.matchFace(); // or device.unmatchFace()
await device.matchFinger(); // or device.unmatchFinger()

// Set orientation
await device.setOrientation('landscape');
await device.setOrientation('portrait');

Synchronization Control

// Disable for animations that never settle
await device.disableSynchronization();
await element(by.id('lottie_animation')).tap();
await device.enableSynchronization();

// Wait with timeout for async elements
await waitFor(element(by.id('loaded_content')))
  .toBeVisible()
  .withTimeout(15000);

§5 Matchers & Assertions

// Visibility
await expect(element(by.id('header'))).toBeVisible();
await expect(element(by.id('hidden'))).not.toBeVisible();
await expect(element(by.id('deleted'))).not.toExist();

// Text
await expect(element(by.id('title'))).toHaveText('Welcome');
await expect(element(by.id('label'))).toHaveLabel('Submit');

// Toggles
await expect(element(by.id('switch'))).toHaveToggleValue(true);

// Slider
await expect(element(by.id('slider'))).toHaveSliderPosition(0.5, 0.1);

// Multiple matching elements
await expect(element(by.text('Item')).atIndex(0)).toBeVisible();
await expect(element(by.text('Item')).atIndex(2)).toBeVisible();

// Compound matchers
await element(by.id('save').and(by.text('Save'))).tap();
await element(by.id('cell').withAncestor(by.id('list_section_1'))).tap();
await element(by.id('icon').withDescendant(by.text('Edit'))).tap();

§6 Network & Mock Server Integration

// In your React Native app, use launch arguments for mock mode:
// if (__DEV__ && process.argv.includes('--mock-api')) { useMockServer(); }

// Detox can launch with arguments:
await device.launchApp({
  newInstance: true,
  launchArgs: {
    mockServerPort: '3001',
    detoxEnableSync: 0, // if needed for certain animations
  },
});

// Set up mock server with beforeAll
const { MockServer } = require('./helpers/mockServer');

beforeAll(async () => {
  MockServer.start(3001);
  MockServer.stub('GET', '/api/products', {
    status: 200,
    body: [{ id: 1, name: 'Test Product', price: 9.99 }],
  });
});

afterAll(async () => {
  MockServer.stop();
});

§7 CI/CD Integration

GitHub Actions (iOS)

name: Detox E2E
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }

jobs:
  ios-e2e:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: 20 }

      - name: Install deps
        run: |
          npm ci
          cd ios && pod install

      - name: Boot Simulator
        run: |
          xcrun simctl boot "iPhone 15" || true
          xcrun simctl status_bar "iPhone 15" override --time "9:41"

      - name: Build for Detox
        run: npx detox build --configuration ios.sim.debug

      - name: Run E2E tests
        run: npx detox test --configuration ios.sim.debug --cleanup --headless

      - name: Upload artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: detox-artifacts
          path: e2e/artifacts/

  android-e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci

      - name: Android E2E
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          arch: x86_64
          profile: Pixel 6
          script: |
            npx detox build --configuration android.emu.debug
            npx detox test --configuration android.emu.debug --headless

§8 Debugging Table

#ProblemCauseFix
1element(by.id(...)) not foundtestID prop not setAdd testID="identifier" to React Native component
2pumpAndSettle-style timeoutInfinite animation (Lottie, ActivityIndicator)Use device.disableSynchronization() around animated sections
3device.reloadReactNative() crashesMetro bundler not runningStart Metro: npx react-native start before running tests
4Build fails for DetoxCocoaPods or Gradle cache stalecd ios && pod install --repo-update; cd android && ./gradlew clean
5Test passes locally, fails on CITiming differencesIncrease withTimeout() values; use explicit waitFor
6Keyboard covers input fieldAuto-scroll not triggeredAppend \n to typeText() to dismiss; or scroll manually
7clearText() doesn't clearFocus not on fieldCall tap() before clearText()
8Android emulator extremely slowNo hardware accelerationUse -gpu host and x86_64 image; enable KVM on CI
9scrollTo scrolls past elementScroll offset too largeUse whileElement().scroll(100, 'down') with smaller increments
10Multiple elements matchNon-unique testIDUse unique IDs; or use .atIndex(n) or compound matchers
11launchApp hangsPrevious app instance stuckUse { newInstance: true, delete: true } to force fresh start
12Artifacts not generatedMissing artifacts configAdd artifacts.plugins in .detoxrc.js with screenshot/video enabled

§9 Best Practices Checklist

  1. ✅ Use testID prop on all interactive/assertable React Native elements
  2. ✅ Use device.reloadReactNative() in beforeEach for clean state
  3. ✅ Use waitFor().toBeVisible().withTimeout() — never sleep or fixed delays
  4. ✅ Disable synchronization only for animations, re-enable immediately
  5. ✅ Use --reuse flag during development to skip app rebuild
  6. ✅ Use page objects for readable, maintainable tests
  7. ✅ Use device.takeScreenshot() at key points for debugging and CI evidence
  8. ✅ Handle keyboard by appending \n to typeText() or dismissing explicitly
  9. ✅ Use compound matchers for elements inside lists: by.id().withAncestor()
  10. ✅ Configure artifact collection (screenshots, videos, logs) for CI
  11. ✅ Run iOS tests on macOS runners; Android tests with hardware acceleration
  12. ✅ Use launch arguments for mock mode: launchArgs: { mockServerPort: '3001' }
  13. ✅ Test on both platforms — matcher behavior differs between iOS and Android