In Detox, how do expect's toBeVisible() and toExist() differ, and when is waitFor(...).withTimeout() the right tool instead?
answer
- in the hierarchy vs on screen
- 75% default visibility threshold
- expect checks once after idle
- waitFor polls, needs withTimeout
- last resort for transient UI
basics
~20 stoExist() 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().
solid answer
~40 s`toExist()` only needs the element in the current native hierarchy, so a row below the fold or a view under a modal passes. `toBeVisible()` needs at least 75 % of the element visible by default, or the percentage you pass; on iOS the element must be topmost at its activation point. A plain `expect` runs once, after Detox has waited for the app to go idle, and never retries. `waitFor(element(...)).toBeVisible().withTimeout(3000)` polls instead, and without `withTimeout` the call does nothing. I keep `waitFor` for what synchronization cannot cover — a toast on a timer, work Detox does not track, or waiting for something to disappear — because sprinkling it everywhere hides wrong matchers and slows the suite.
code
typescript · 8 linesimport { by, element, expect, waitFor } from 'detox';
it('shows the plan picker after sign-up', async () => {
await element(by.id('signup.submit')).tap();
await expect(element(by.id('plans.screen'))).toBeVisible();
await expect(element(by.id('signup.form'))).not.toExist();
await waitFor(element(by.id('plans.promoBanner'))).toBeVisible().withTimeout(4000);
});go deeper
Recall that toExist means in the hierarchy, toBeVisible means on screen, and that waitFor needs withTimeout.
Explain the 75 % threshold, the activation-point rule on iOS, and why a plain expect relies on idle synchronization instead of polling.
Justify each waitFor in a suite, spot the missing-withTimeout false pass, and fix wrong matchers instead of adding waits.
Set suite-wide rules for when polling is allowed, so waits do not grow into slow, flaky tests across teams.
## Two kinds of checks in Detox **Detox** gives a test two ways to check UI state: `expect(element(...))`, which checks once, and `waitFor(element(...))`, which polls until a condition holds or a timeout expires. Both use the same matchers and the same expectation methods; what differs is **when** the check runs and **how long** it keeps trying. Before every action and every `expect`, Detox waits for the app to become idle — its gray-box synchronization. That is why most Detox tests need no explicit waits: when the "Create account" tap has been handled and the next screen has settled, the next line runs. The details of what counts as busy belong to Detox's synchronization; what matters here is that `expect` relies on it and does not retry. ## `toBeVisible()` versus `toExist()` | Expectation | Passes when | Typical use | |---|---|---| | `toExist()` | the element is in the app's current native view hierarchy | a view was mounted or unmounted | | `toBeVisible()` | at least 75 % of the element is visible on screen (or the percentage you pass, 1–100) | the user can actually see it | | `not.toExist()` | no matching element is in the hierarchy | a dismissed modal is gone | | `not.toBeVisible()` | the visible area is below the threshold | it scrolled away or is covered | Key points: - **An element can exist but not be visible**: a row rendered below the fold of a list, a view behind a modal, a button pushed under the keyboard. `toExist` passes; `toBeVisible` fails. - **On iOS, visibility is decided at the element's activation point**: the view (or a subview) must be topmost there, so an overlay covering the centre fails the check even if the edges show. - `toBeVisible(35)` lowers the threshold for elements that are legitimately partly clipped, such as a card peeking from a carousel. ## Other expectations on a found element `toBeVisible` and `toExist` answer "is it there"; the rest of the expectation API checks **what** it shows: - **`toHaveText(text)`** — exact string, or a regex that must match the **entire** text on both platforms (`/.*Welcome.*/` for "contains"). - **`toHaveId(id)`** and **`toHaveLabel(label)`** — the element's `testID` or accessibility label. - **`toHaveValue(value)`** — the accessibility value, such as a stepper's count. - **`toHaveToggleValue(true)`** — a `Switch` or checkbox state, for the meal-kit's "Send me recipes" opt-in. - **`toBeFocused()`** — the element has focus, useful after `tapReturnKey()` moves to the next field. Every one of them can be negated with `.not` and wrapped in `waitFor` the same way. ## `waitFor(...).withTimeout(ms)` `waitFor` wraps an expectation and **polls** it: ```js await waitFor(element(by.id('signup.welcomeToast'))).toBeVisible().withTimeout(3000); ``` Three facts interviewers probe: 1. **`withTimeout` is mandatory.** The Detox typings say a `waitFor` call without `withTimeout()` does nothing — the chain is only executed when the timeout is set. 2. **It is a last resort, not a default.** Detox's docs describe `waitFor` as manual synchronization; adding it everywhere hides real problems and slows the suite. 3. **It pairs with `whileElement`** to scroll until something appears: `waitFor(el).toBeVisible().whileElement(by.id('plans.list')).scroll(200, 'down')`. ## When `waitFor` is the right tool - **Transient UI.** A toast or success animation that appears and disappears on a timer can be gone before, or after, the moment `expect` checks; polling with a timeout catches it. - **Work Detox does not track.** When the app is idle from Detox's point of view but a result still arrives later — for example over a channel synchronization ignores, or while synchronization is turned off for a screen — the test must poll. - **Waiting for something to disappear.** `waitFor(el).not.toExist().withTimeout(5000)` expresses "the spinner goes away within five seconds". What `waitFor` should **not** replace is a fixed sleep for everything; a test full of `waitFor` calls usually means the matcher is wrong or the app never becomes idle, and both deserve a real fix. ## Putting it into the meal-kit sign-up ```js await element(by.id('signup.submit')).tap(); await expect(element(by.id('plans.screen'))).toBeVisible(); // synchronized, one check await expect(element(by.id('signup.form'))).not.toExist(); // previous form unmounted await waitFor(element(by.id('plans.promoBanner'))) .toBeVisible() .withTimeout(4000); // banner slides in on a timer ``` The first two lines lean on synchronization; only the timed banner needs polling.
- What happens if you write waitFor(element(by.id('toast'))).toBeVisible() without withTimeout()?Nothing is waited for. The Detox typings state that a `waitFor` chain is only executed once `withTimeout()` sets its timeout, so the line silently checks nothing and the test moves on — a quiet source of false passes.
- How would you assert that a card partly clipped by a carousel is on screen?Pass a lower threshold: `toBeVisible(35)` passes when at least 35 % of the element's area is visible. The default of 75 % suits ordinary controls; lowering it deliberately for peeking cards is better than switching to `toExist`, which would also pass when the card is fully off screen.
expect is a single photo taken once the room has gone quiet; waitFor is a security camera you leave running for a set time, useful only when something happens after the room seems quiet.
saying these in an interview costs you the question
- toExist() proves the user can see the element.
- expect() keeps retrying until the element appears.
- waitFor() without withTimeout() waits a sensible default time.
- Every expectation should be wrapped in waitFor to avoid flakiness.
- toBeVisible() passes if any single pixel is on screen.