End-to-End Testing with Detox

Detox 12,028 (https://github.com/wix/Detox 12,028 ) (MIT, from Wix 630 , npm 20.51.4 2,036 ) is a gray-box runner: a native library in a test build of the app reports when the app is idle (no pending requests, timers, animations or JavaScript work), and each step waits for that. On Android it drives the app with Espresso 234 from a test APK, so an Expo 6,418 project adds @config-plugins/detox to plugins in app.json:

Adding Detox to an Expo projectShell
npm install --save-dev detox@20.51.4
npm install --save-dev --legacy-peer-deps @config-plugins/detox@11.0.0
npx detox init
npx expo prebuild --platform android

The plugin's latest release, 11.0.0 from June 2025, still declares expo@^53 as its peer, hence --legacy-peer-deps. It worked, but that install silently removed @react-native/jest-preset, and Jest 62,362 failed until it was added back as a dev dependency. BookNest's .detoxrc.js keeps one configuration, android.att.release, for an attached device with adbName: 'emulator-5554'; the generated default, '.*', takes whichever device adb lists first. Building the release APK and the test APK took 35 minutes on this shared machine. The test is Jest with Detox's globals:

e2e/cart.test.js (excerpt)JSX
  it('adds a book from the catalog and removes it in the cart', async () => {
    await waitFor(element(by.text('Salt and Saffron'))).toBeVisible().withTimeout(20000);
    await element(by.text('Salt and Saffron')).tap();
    await element(by.text('Add to cart · $24.00')).tap();
    await expect(element(by.text('Add to cart · $24.00 · 1 in cart'))).toBeVisible();
    await device.pressBack();
    await element(by.label('Cart, 1 book in your cart')).tap();
    await expect(element(by.text('1 book in your cart'))).toBeVisible();
    await element(by.label('Remove Salt and Saffron')).tap();
    await expect(element(by.text('Your cart is empty.'))).toBeVisible();
  });
Output
PASS e2e/cart.test.js (35.764 s)
  BookNest cart
    ✓ adds a book from the catalog and removes it in the cart (7312 ms)

After the app had launched, the same journey took Maestro 54 seconds and Detox 7.3, because Detox acts the moment the app is idle rather than re-reading the screen. by.text() matches exact strings, so the $ needs no escaping.