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-testingdetox-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
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
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.debugThe 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 --headlessVerify: 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.debugWorked example
A React Native store app needs an E2E check that adding a book to the cart updates the badge count.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Querying by by.text for translatable strings | Translation breaks tests. | Use by.id (testID) for stable lookups (Step 3). |
await sleep(2000) between actions | Detox's auto-sync is the point; sleeps mask real flake. | waitFor(...) for genuine async; trust auto-sync otherwise. |
Skipping device.reloadReactNative() between tests | State 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 reinstall | Slow; Detox's reload is faster. | device.reloadReactNative() over device.launchApp(). |
| Long-press without device wake | Simulator may be in screensaver; tap misses. | device.shake() / device.openURL() to wake. |
Limitations
References
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):
| Matcher | Use |
|---|---|
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 multipleActions
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).