Testland
Browse all skills & agents

detox-testing

Authors React Native E2E tests with Detox (Wix) - gray-box architecture (runs in-process with the app), `element(by.id|by.text|by.label)` matchers, `waitFor()` for explicit sync beyond Detox's automatic async tracking, Jest runner. Use when the app is React Native and speed matters. For Flutter use flutter-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows; for non-RN native use xcuitest-suite or espresso-suite.

Install with skills.sh (any agent)

npx skills add testland/qa --skill detox-testing
View source

detox-testing

Detox's "gray-box" architecture is load-bearing: unlike Appium (black-box, external server), it runs in the app's process (per detox-docs (opens in new window)), so it "automatically monitors asynchronous operations to eliminate test flakiness at its core" (detox-docs (opens in new window)) - tracking network calls, animations, and timers without explicit sync hooks.

When to use

  • The app is React Native (the framework's intended use case).
  • Speed matters more than cross-stack reuse (Detox is faster than Appium for RN apps).
  • The team uses Jest (Detox's default runner; built-in integration).

If the app is native iOS/Android (not RN), use xcuitest-suite or espresso-suite. For non-RN-specific cross-platform, see appium-testing.

How to use

  1. Install Detox and scaffold with npx detox init (Step 1).
  2. Add testID props to the RN components the tests will target (Step 4).
  3. Build the app for the target device configuration with detox build (Step 2).
  4. Author e2e/*.test.js specs using element(by.id(...)) matchers, actions, and assertions (Step 3, references/element-api.md).
  5. Add waitFor(...).withTimeout(N) only where Detox's auto-sync misses a genuine async condition (Step 5).
  6. Run detox test --configuration <cfg> and iterate until green (Step 6).
  7. Wire build + test into CI as a headless job (Step 7).

Step 1 - Install

npm install --save-dev detox
npx detox init   # scaffolds .detoxrc.js + e2e/ directory

.detoxrc.js configures device + app + runner; the default template is sensible.

Step 2 - Build the app for testing

# Android
detox build --configuration android.emu.debug

# iOS
detox build --configuration ios.sim.debug

The build configuration in .detoxrc.js references the project's existing build commands (Gradle / xcodebuild) - Detox doesn't introduce a new build pipeline.

Verify: detox build must finish and emit the app binary before Step 6. If it fails, fix the underlying native build error (Gradle / xcodebuild) and re-run - the fix lives in the RN build config, not Detox.

Step 3 - Author tests with matchers

Match elements with element(by.id(...)); prefer by.id (the RN testID prop) for stable, translation-proof lookups. The full matcher catalog, combinators, action verbs, and assertions are in references/element-api.md.

Example test:

describe('Cart flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

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

  it('adds item to cart', async () => {
    await element(by.id('product-BOOK-001')).tap();
    await element(by.id('add-to-cart-button')).tap();
    await expect(element(by.id('cart-count'))).toHaveText('1');
  });

  it('applies promo code', async () => {
    await element(by.id('promo-input')).typeText('WELCOME10');
    await element(by.id('apply-promo-button')).tap();
    await expect(element(by.id('subtotal'))).toHaveText('$22.49');
  });
});

Step 4 - Production code: set testID

In RN production code:

<TouchableOpacity testID="add-to-cart-button" onPress={addToCart}>
  <Text>Add to cart</Text>
</TouchableOpacity>

<TextInput testID="promo-input" value={code} onChangeText={setCode} />

testID is React Native's prop for accessibilityIdentifier (iOS) / resource-id (Android). Detox finds elements by it.

Step 5 - waitFor for explicit sync

When Detox's automatic tracking misses something:

await waitFor(element(by.id('async-result')))
  .toBeVisible()
  .withTimeout(10000);

await waitFor(element(by.id('progress-bar')))
  .not.toBeVisible()
  .whileElement(by.id('list')).scroll(100, 'down');

waitFor(...).withTimeout(N) polls the condition for up to N ms. whileElement(...) performs an action (scroll) while waiting - useful for "scroll until visible."

Step 6 - Run

detox test --configuration android.emu.debug
detox test --configuration ios.sim.debug

# Specific test file
detox test e2e/cart.test.js --configuration ios.sim.debug

# Headless mode
detox test --headless

Verify: assert every spec reports green. If one fails, read the Detox error (element not found / timeout), fix the matcher or add a waitFor (Step 5), and re-run that spec before wiring CI. Detox then runs headless on CI platforms such as Travis CI, CircleCI, and Jenkins (per detox-docs (opens in new window)).

Step 7 - CI integration

jobs:
  detox-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - run: npm ci
      - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          script: |
            detox build --configuration android.emu.debug
            detox test --configuration android.emu.debug --headless

  detox-ios:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v5
      - run: npm ci
      - run: npx pod-install
      - run: |
          detox build --configuration ios.sim.debug
          detox test --configuration ios.sim.debug

Worked example

A React Native store app needs an E2E check that adding a book to the cart updates the badge count.

  1. The product card already renders <TouchableOpacity testID="product-BOOK-001"> and the cart badge renders <Text testID="cart-count"> (Step 4).
  2. Build once: detox build --configuration ios.sim.debug (Step 2).
  3. Author e2e/cart.test.js with the 'adds item to cart' spec from Step 3.
  4. Run detox test e2e/cart.test.js --configuration ios.sim.debug (Step 6).
  5. Detox launches the simulator, taps the product and the add button, and its auto-sync waits for the state update before the assertion runs. The spec passes: the badge reads 1, with no sleep anywhere in the test.

Anti-patterns

Anti-patternWhy it failsFix
Querying by by.text for translatable stringsTranslation breaks tests.Use by.id (testID) for stable lookups (Step 3).
await sleep(2000) between actionsDetox's auto-sync is the point; sleeps mask real flake.waitFor(...) for genuine async; trust auto-sync otherwise.
Skipping device.reloadReactNative() between testsState leaks; tests pollute each other.beforeEach reload (Step 3 example).
by.type('RCTView') (iOS-specific class)Tests break on Android (different class name).Use by.id (cross-platform) or platform-conditional code.
Per-test app reinstallSlow; Detox's reload is faster.device.reloadReactNative() over device.launchApp().
Long-press without device wakeSimulator may be in screensaver; tap misses.device.shake() / device.openURL() to wake.

Limitations

  • React Native only. Native non-RN apps need xcuitest-suite or appium-testing.
  • iOS support depends on RN version. Per detox-docs (opens in new window), some iOS features may have caveats per RN version.
  • Test runner choice. Detox works with any runner; Jest is built-in. Mocha / Jasmine require additional config.
  • In-process gray-box trade-off. Detox knows about the JS bridge; native iOS/Android crashes that don't surface to JS may be invisible until next interaction.

References

  • det (opens in new window) - Detox overview: gray-box architecture, automatic async monitoring, JS test syntax, CI-ready, RN-specific.
  • references/element-api.md - matcher catalog (by.id, by.text, by.label, by.type, by.traits), combinators, actions, and assertions.
  • xcuitest-suite, espresso-suite, appium-testing, maestro-flows - alternatives.

Detox element API - matchers, actions, assertions

View source (opens in new window)

Detox element API - matchers, actions, assertions

Lookup reference for element(...) matchers, the actions callable on a matched element, and the assertions expect(...) supports. Referenced from Step 3 of the detox-testing skill.

Matchers

Per detox-matchers (opens in new window):

MatcherUse
by.id('tap_me')React Native testID prop (preferred default).
by.text('Tap Me')Visible text content.
by.label('...')iOS accessibility label / Android content description.
by.type('RCTImageView')Component class name (iOS / Android-specific).
by.traits(['button'])iOS only - accessibility traits.

Each accepts strings or regex (by.id(/^tap_[a-z]+$/)).

Combinators per detox-matchers (opens in new window):

withAncestor(matcher)    // child element within a parent
withDescendant(matcher)  // parent containing children
and(matcher)             // combine matchers
atIndex(index)           // when matcher returns multiple

Actions

await element(by.id('btn')).tap();
await element(by.id('btn')).longPress();
await element(by.id('btn')).multiTap(2);

await element(by.id('input')).typeText('hello');
await element(by.id('input')).clearText();
await element(by.id('input')).replaceText('new text');

await element(by.id('list')).scroll(200, 'down');
await element(by.id('list')).scrollTo('bottom');
await element(by.id('list')).swipe('left', 'fast');

await element(by.id('toggle')).pinch(1.5);

Assertions

await expect(element(by.id('toast'))).toBeVisible();
await expect(element(by.id('cart-count'))).toHaveText('1');
await expect(element(by.id('error'))).not.toBeVisible();
await expect(element(by.id('field'))).toHaveValue('expected');

expect(...) from Detox auto-waits up to a default timeout (typically 5s) - no explicit waitFor needed for normal synchronization-tracked work.

Related skills

appium-testing

Wires Appium for cross-platform mobile UI automation - uses the WebDriver protocol, picks a driver per platform (XCUITest for iOS, UiAutomator2 / Espresso for Android, Mac2 for macOS, Windows for desktop), authors tests in JS / Python / Java / Ruby / .NET, configures `desiredCapabilities`, runs against simulators / emulators / device farms. Use when a single test suite must cover both iOS and Android, or when the team's stack is multi-platform (iOS + Android + Mac + Windows).

espresso-suite

Authors Espresso UI tests for Android - uses `onView(withId(...)).perform(...).check(matches(...))`, leans on Espresso's automatic synchronization (no `Thread.sleep`), wires `IdlingResource` for app-specific async, runs via `./gradlew connectedAndroidTest` and parses the JUnit XML output. Use when an Android app needs UI tests in Google's first-party framework.

flutter-testing

Authors Flutter tests across the three-layer pyramid - unit (`flutter test`), widget (`testWidgets` + `WidgetTester`), integration (`integration_test` on simulator/emulator/device). Picks the right layer per change, mocks via `mockito` + `build_runner`, LCOV coverage, CI with the Flutter Action. Use when the app is Flutter and the team wants its first-party stack. For React Native use detox-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows.

maestro-flows

Authors Maestro YAML flow files (`.maestro/*.yaml`) for mobile + web UI automation: declarative `tapOn`, `inputText`, `assertVisible`, `swipe`, supported targets (iOS, Android, Flutter, React Native, web), nested flow imports, JavaScript hooks for complex conditions. Use when the team has already chosen Maestro, is coming from an existing `.maestro/` directory, or explicitly wants YAML-declarative tests readable by non-engineers without a compile step. For framework selection or authoring tests in XCUITest / Espresso / Detox / Appium / Flutter, use a mobile driver-selection or per-flow mobile test-authoring step instead.

mobile-a11y-test-author

Authors native mobile accessibility tests covering iOS (Accessibility Inspector, XCUITest `performAccessibilityAudit()` introduced in iOS 17, VoiceOver label/trait/hint verification) and Android (Espresso `AccessibilityChecks.enable()`, Accessibility Scanner, TalkBack traversal, `contentDescription` labelling) with WCAG-aligned checks for element labels, 44pt/48dp touch targets, contrast ratios, and focus order. Use when an iOS or Android app needs automated and manual accessibility test coverage beyond what `xcuitest-suite` or `espresso-suite` provide.

mobile-device-matrix-toolkit

Dispatches mobile UI test runs across a 3-tier device matrix (smoke per-PR, regression per-merge, full farm at release) to control CI cost: generates per-target Appium capability configs from a central YAML, parallelises via GitHub Actions matrix strategy, and aggregates JUnit XML into a cross-device pass/fail table. Use when deciding which iOS / Android devices and OS versions to run tests on and at which stage (smoke / regression / full farm), not how to configure a specific test framework (for that, use xcuitest-suite, espresso-suite, etc.).

mobile-web-emulation-runner

Builds a workflow to run web E2E tests under mobile viewports + DPRs (device pixel ratios): Playwright's `devices` catalog (iPhone 15, Pixel 7), suite run per-device as matrix shards, per-device screenshots, mobile assertions (`.tap()`, viewport-conditional layout). Use when a responsive web app needs mobile-breakpoint regression without a real-device farm. Mobile WEB only - for native apps use appium-testing, detox-testing, or flutter-testing; for cross-shard aggregation use mobile-device-matrix-toolkit; for gesture sequences use touch-gesture-tester.

mobile-web-perf-budget

Pure-reference skill for mobile-web performance budgets - Core Web Vitals at the 75th percentile mobile (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1; FID retired March 2024 in favor of INP), Lighthouse mobile profile config, per-route resource budgets (JS bundle, image weight, font load). Use as the team's reference for "what should the mobile perf gate enforce" - paired with `lighthouse-perf` (the runner) and `lighthouse-budget-author` (the per-route author).

touch-gesture-tester

Verifies touch-gesture handlers (tap, double-tap, long-press, swipe, pinch, rotate, pan) work as expected under both mobile-emulation (Playwright) and native (XCUITest / Espresso / Detox) - distinguishes "mouse click handler also fires on tap" from "real touch event fired with correct properties." Use when the app has bespoke gesture handlers (custom carousels, sliders, drag-drop, pull-to-refresh) and the team needs targeted gesture verification beyond generic UI assertions.

xcuitest-suite

Authors XCUIest UI tests for iOS / iPadOS / tvOS - uses the three-class XCUIApplication / XCUIElement / XCUIElementQuery pattern, sets accessibility identifiers on production code, runs via `xcodebuild test` with destination, parses the `xcresult` bundle. Use when an iOS app needs UI tests in Apple's first-party framework (no external runtime; native to Xcode).