skip to content

Detox

Detox drives a real app build on a simulator or device and waits for the app to go idle before each step instead of sleeping. Interviewers ask what it catches that Jest cannot and why E2E flakes.

on this pageshow

explore

questions

21

In Detox, what does element(by.id('...')) match in a React Native app, and why is it preferred over by.text?

level: juniorimportance: must knowfreq 55%

answer

  1. matched in the native hierarchy
  2. testID prop becomes the identifier
  3. copy and locale changes
  4. custom components must forward testID
  5. by.label concatenates child labels

basics

~20 s

Detox's by.id matches a native view's test identifier, which React Native sets from the testID prop. Unlike by.text, it survives copy changes and translations and is unique by design, so Detox recommends it as the default matcher.

solid answer

~40 s

`element(by.id('signup.submit'))` is resolved inside the app against the native view hierarchy: the `testID` prop becomes the accessibility identifier on iOS and the view tag on Android. I prefer it to `by.text` because text changes with copy edits and locales and repeats across screens, while an ID is stable and unique. `by.label` matches `accessibilityLabel`, which React Native can compute from child labels, so it surprises people too. The catch is that `testID` only works on built-in components; a custom `PrimaryButton` must forward it to a `Pressable` or `View`, or `by.id` finds nothing. I still use `toHaveText` to assert copy on an element found by ID.

code

typescript · 8 lines
typescript
import { by, element, expect } from 'detox';

it('signs up a new meal-kit customer', async () => {
  await element(by.id('signup.email')).typeText('[email protected]');
  await element(by.id('signup.password')).typeText('correct-horse-42');
  await element(by.id('signup.submit')).tap();
  await expect(element(by.id('plans.screen'))).toBeVisible();
});

go deeper

for a junior

Recall that by.id matches the testID prop, and give two reasons it beats by.text: copy changes and translations.

for a middle

Explain that matching happens in the native hierarchy, what testID becomes per platform, and why custom components must forward it.

for a senior

Set ID naming conventions for a team, including derived child IDs and stable row keys, and debug a missing ID through the native hierarchy.

for a principal

Decide whether test IDs ship in production builds, and how the team keeps them consistent as screens are redesigned.

## What a Detox matcher does A **Detox** test finds UI with `element(matcher)`, then either acts on it (`tap()`, `typeText()`) or asserts on it through `expect(...)`. The matcher is evaluated **inside the running app**, against the native view hierarchy that React Native rendered — not against React components or JSX. That detail explains almost everything about which matcher to choose. | Matcher | Matches on iOS | Matches on Android | Set in React Native by | |---|---|---|---| | `by.id('x')` | accessibility identifier | the view's tag | the `testID` prop | | `by.text('x')` | the element's text | the element's text | the rendered string inside `Text` or an input | | `by.label('x')` | accessibility label | content description | the `accessibilityLabel` prop | | `by.type('Cls')` | native class name | canonical class name | not set by you; differs per platform | `by.id`, `by.text` and `by.label` also accept regular expressions (`by.id(/^dish\.[0-9]+$/)`) with the `i`, `s` and `m` flags. ## Why `by.id` is the default choice The Detox docs recommend matching by unique test IDs, and interviewers expect you to say why: - **Decoupled from copy.** A product team renaming "Continue" to "Next" breaks every `by.text('Continue')` test but no `by.id` test. - **Locale-agnostic.** A suite run under another language, or with a pseudo-locale, still finds `signup.submit`; text matchers do not. - **Unique by construction.** Text such as "Continue" or "Add" appears on many screens; a well-named ID appears once, avoiding ambiguous matches. - **Stable across platforms.** `testID` maps to the platform's identifier on both iOS and Android, while `by.type` class names differ per platform. - **Fewer label surprises.** Detox's own docs warn that React Native computes accessibility labels in a non-standard way: a view with no explicit label can match the **concatenated labels of its children**, and iOS gives text elements their text as a label automatically. `by.text` still has a place — asserting that a visible string is correct, for example `expect(element(by.id('signup.error'))).toHaveText('Enter a valid postcode')` — but as an assertion on an element found by ID, not as the way to find it. ## `by.label`, `by.type` and `by.traits` in practice - **`by.label`** is tempting because it doubles as an accessibility check, but the Detox docs show the trap: a `View` with `testID='title-root'` and no label of its own, containing two `Text` children labelled `title` and `subtitle`, is matched by the label `title subtitle`. Detox aligns Android with iOS here, so the concatenation happens on both. - **`by.type`** matches a native class name, and those differ per platform — `RCTImageView` on iOS against `android.widget.ImageView` on Android — so a shared test needs a `device.getPlatform()` branch. - **`by.traits`** matches iOS accessibility traits such as `button` or `header`; it is iOS-only. These are tools for the cases IDs cannot reach, such as a third-party component that does not accept a `testID`. ## The composite-component trap `testID` is a prop of React Native's **built-in components** (`View`, `Text`, `TextInput`, `Pressable` and so on). A custom component such as `PrimaryButton` does nothing with a `testID` prop unless it forwards it to a built-in child. The Detox troubleshooting guide lists this as the first thing to check when `by.id` finds nothing: ```tsx import { Pressable, Text } from 'react-native'; type Props = { title: string; onPress: () => void; testID?: string }; export function PrimaryButton({ title, onPress, testID }: Props) { return ( <Pressable testID={testID} onPress={onPress}> <Text testID={testID ? `${testID}.label` : undefined}>{title}</Text> </Pressable> ); } ``` Deriving child IDs (`signup.submit.label`) lets a test reach a specific part of a composite without resorting to text. ## Naming IDs so tests read well 1. Prefix by screen or feature: `signup.email`, `signup.password`, `signup.submit`. 2. For repeated rows, add a stable key rather than a position: `plan.card.family-4`, not `plan.card.2`. 3. Keep IDs out of user-visible strings, so copy and translation changes never touch tests. ## The meal-kit sign-up, matched by ID ```js await element(by.id('welcome.getStarted')).tap(); await element(by.id('signup.email')).typeText('[email protected]'); await element(by.id('signup.password')).typeText('correct-horse-42'); await element(by.id('signup.submit')).tap(); await expect(element(by.id('plans.screen'))).toBeVisible(); ``` Every lookup here survives a copy change, a translation and a redesign that keeps the same controls. When a lookup fails, inspecting the native hierarchy (Xcode's view debugger on iOS) shows whether the ID reached a native view at all — React Native test IDs appear there as accessibility identifiers.

  • Why does by.id sometimes find nothing even though the testID is in the JSX?
    The prop is on a custom component that never passes it to a built-in React Native component, so no native view receives the identifier. Forward `testID` to the `Pressable`, `View` or `TextInput` the component renders, and give children derived IDs such as `signup.submit.label` if tests need them.
  • When is by.text still the right matcher?
    When the text itself is what the test is about and it is unique on screen — for example a one-off confirmation message. More often the better pattern is to find the element by ID and assert its copy with `toHaveText`, so the lookup does not break when the wording changes.

saying these in an interview costs you the question

  • by.id matches the React component's name in the JSX tree.
  • Any custom component accepts testID automatically.
  • by.text is safer because users see the text.
  • Using the same testID on several list rows is fine for tapping one.
  • by.type class names are identical on iOS and Android.
open as a page

In a Detox test, why is no sleep() needed between tapping a Draw button and asserting the results screen, and what is Detox waiting for?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Detox is gray-box: code inside the app tracks in-flight network requests, short timers, animations, UI layout, the main queue and the React Native JS thread, and Detox runs each action or expectation only once all of them are idle.

open as a page

In Detox, how do expect's toBeVisible() and toExist() differ, and when is waitFor(...).withTimeout() the right tool instead?

level: middleimportance: must knowfreq 50%

basics

~20 s

toExist() passes when the element is in the native view hierarchy; toBeVisible() also needs at least 75 % of it on screen by default. expect checks once after Detox's idle sync, while waitFor polls until a timeout, which must be set with withTimeout().

open as a page

In Detox, how do the devices, apps and configurations sections of .detoxrc.js let one suite run on an iOS simulator and an Android emulator?

level: middleimportance: must knowfreq 48%

basics

~20 s

A Detox config lists devices and apps as separate named dictionaries, then defines configurations that each pair one device with one app. The same test files run against whichever pair detox build -c and detox test -c select.

open as a page

A Detox suite for a hotel-booking app passes locally but fails on the CI runner — how do you diagnose it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Turn on Detox artifacts for failing tests (logs, screenshots, videos) and trace logging, upload them from CI, then compare the runner's conditions — speed, headless device, workers, data, binary — and reproduce locally with the same configuration before touching retries.

open as a page

A Detox test in a lottery app hangs after launch because a 'drawing numbers' loader animates endlessly; how do you diagnose and fix it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Read Detox's synchronization debug log to confirm animations are the busy resource, then fix the app if the loader should have ended; if it loops by design, swap it for a static mock in the e2e build, or disable synchronization only around it.

open as a page

What do Detox's --record-logs, --take-screenshots and --record-videos flags produce, and what does their failing value mean?

level: juniorimportance: should knowfreq 35%

basics

~20 s

They record device logs, before-and-after screenshots and screen videos for each test. Logs and videos default to none and screenshots to manual; the value failing records them but keeps files only for tests that failed, which suits CI uploads.

open as a page

In a Detox 20 project, what does the detox test command actually run, and what does Jest contribute that Detox itself does not?

level: juniorimportance: should knowfreq 30%

basics

~20 s

detox test is a wrapper: it resolves the chosen configuration, starts a Detox session and spawns Jest, by default jest --config e2e/jest.config.js. Jest finds and runs the test files; Detox's Jest environment boots the device, installs the app and supplies device, element and expect.

open as a page

In Detox, when should a test call device.launchApp({ newInstance: true }), device.reloadReactNative() or device.openURL() in a sign-up suite?

level: middleimportance: should knowfreq 35%

basics

~20 s

device.launchApp({ newInstance: true }) restarts the app process; device.reloadReactNative() only reloads the JS bundle, fast but leaving native state and sometimes misbehaving; device.openURL() delivers a link to the running app, while launchApp({ url }) tests a link at launch.

open as a page

In a Detox test of a React Native sign-up form, why can replaceText leave the screen in a different state than typeText?

level: middleimportance: should knowfreq 35%

basics

~20 s

Detox's typeText enters text through the system keyboard, firing the same change events a user would, while replaceText sets the value directly and may skip input callbacks. Validation, masks or submit-enable logic driven by those callbacks can then disagree with what the field shows.

open as a page

How does `detox test --retries` decide what to re-run, and why can relying on it hide real problems?

level: middleimportance: should knowfreq 40%

basics

~20 s

detox test --retries N re-spawns Jest for the test files that failed, re-running each whole file up to N more times. It keeps pipelines green despite flakiness, but a retried pass can hide intermittent product bugs, so retried files must be tracked and fixed.

open as a page

In Detox, what does the default app reinstall do for isolation between test files, and when is --reuse acceptable?

level: middleimportance: should knowfreq 35%

basics

~20 s

By default Detox uninstalls and reinstalls the app for each test file, so every file starts without stored data. --reuse skips that for speed, letting app data and a stale binary carry over, which suits a local JS-only loop but not CI.

open as a page

Why does a Detox Android app config need a second test APK and a DetoxTest.java class when an iOS app config needs only the .app?

level: middleimportance: should knowfreq 32%

basics

~20 s

On Android, Detox's native code runs as instrumentation: a separate test APK, built with assembleAndroidTest and started through the single DetoxTest.java JUnit test, runs inside the app process. On an iOS simulator Detox injects its framework into the .app at launch instead.

open as a page

In Detox, what do device.disableSynchronization() and device.enableSynchronization() do, and what must the test do differently while synchronization is off?

level: middleimportance: should knowfreq 35%

basics

~20 s

disableSynchronization() makes Detox stop waiting for the app to be idle; enableSynchronization() turns waiting back on and resolves only when the app next goes idle. While off, the test must wait explicitly for each element with waitFor(...).withTimeout(...).

open as a page

A Detox sign-up test's element(by.text('Continue')).tap() fails on iOS with 'Multiple elements found' — what causes it, and what is the robust fix?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The matcher resolves to several native views, often including a Continue button on a screen still mounted underneath, and Detox fails rather than guess. Give the button a unique testID or scope the match with withAncestor; atIndex is a last resort.

open as a page

A Detox test cannot tap a meal card far down a React Native FlatList — why, and how do scroll, scrollTo and whileElement reach it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A FlatList renders only rows near the viewport, so a distant card does not exist yet, and a nearer one exists but is off screen. Scroll the list element: waitFor(card).toBeVisible().whileElement(by.id('list')).scroll(200, 'down') scrolls until the card shows, then tap it.

open as a page

How does a Detox suite run with several workers on CI, and what must hold for the hotel-booking tests to run in parallel safely?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Detox parallelism is Jest's maxWorkers: each worker allocates its own simulator or emulator, created or launched as needed. It is safe only when test files are order-independent and use separate backend data, and the runner has resources for every device.

open as a page

In Detox, what changes when a configuration uses an android.attached device instead of android.emulator, and why does CI usually choose the emulator?

level: seniorimportance: should knowfreq 24%

basics

~20 s

An android.emulator device names an AVD that Detox can boot, prepare and shut down itself; an android.attached device is an adb serial pattern Detox can only pick from already-connected devices. CI prefers emulators because they are reproducible and Detox manages them.

open as a page

A Detox suite passes on android.emu.debug but the android.emu.release configuration hangs at launch — why, and why test release builds at all?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A release build with shrinking on strips React Native classes Detox reaches by reflection, so the app hangs or crashes; adding Detox's proguard-rules-app.pro fixes it. CI still tests release because it is Metro-free, deterministic and closest to what ships.

open as a page

A lottery app polls draw status with a recursive setTimeout every second, and every Detox test after launch blocks; why, and what changes fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Detox treats a pending setTimeout of 1.5 seconds or less as busy and ignores setInterval. A recursive one-second setTimeout always has the next timer queued, so the app never idles. Switch the loop to setInterval, lengthen it, or stop polling when not needed.

open as a page

In Detox, when do you use device.setURLBlacklist() or the detoxURLBlacklistRegex launch argument, and how do the patterns have to be written?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Use them when requests that never finish, such as long polling or background uploads, keep Detox's network synchronization busy. setURLBlacklist works mid-test; detoxURLBlacklistRegex applies from launch. Both take regex strings or RegExp objects with only the i, m and s flags.

open as a page