Detox
01 / 02

Matchers, Actions & Synchronization

Detox: Matchers, Actions & Synchronization

Detox is a gray-box end-to-end testing framework for React Native apps. Its core differentiator is automatic synchronization: it waits for the app to reach a genuinely idle state (network settled, animations done, no pending timers) before each test step, eliminating the flaky sleep()-based waits common in other E2E tools.

Matcher -> Action -> Expectation

describe('Login flow', () => {
  beforeEach(async () => {
    await device.launchApp({ newInstance: true });
  });

  it('should log in with valid credentials', async () => {
    // Match by stable testID, not visible text (breaks on copy/localization
    // changes) or screen coordinates (breaks on layout changes)
    await element(by.id('emailInput')).typeText('user@example.com');
    await element(by.id('passwordInput')).typeText('secret123');
    await element(by.id('loginButton')).tap();

    // Detox already waited for the login request to resolve before
    // this assertion runs -- no manual sleep() needed
    await expect(element(by.id('welcomeText'))).toBeVisible();
  });
});

// <Button testID="loginButton" onPress={handleLogin}>Log In</Button>

Disambiguating Repeated Elements

// A list with several similarly-structured rows -- combinators
// pinpoint exactly which instance to interact with
await element(by.id('deleteButton').withAncestor(by.id('row-3'))).tap();
await element(by.text('Delete')).atIndex(2).tap();

// toExist() vs toBeVisible():
// toExist() -- present in the view hierarchy, even if off-screen/hidden
// toBeVisible() -- actually visible on screen (not occluded, non-zero size)
await expect(element(by.id('offscreenItem'))).toExist();
await expect(element(by.id('modalTitle'))).toBeVisible();

waitFor: The Escape Hatch

// Automatic sync covers what Detox can detect (network, animations,
// timers). For app-specific delays outside that visibility, waitFor
// gives an explicit, bounded poll instead of a flat sleep()
await waitFor(element(by.id('slowLoadedBanner')))
  .toBeVisible()
  .withTimeout(5000);

Device Lifecycle & Permissions

// Native permission dialogs aren't part of the RN view tree --
// can't be tapped like a regular element. Pre-grant instead.
await device.launchApp({
  newInstance: true,
  permissions: { camera: 'YES', location: 'always' },
});

// Fast JS-only reload -- skips a full native app relaunch
await device.reloadReactNative();

// Simulate background/foreground transitions
await device.sendToHome();
await device.launchApp({ newInstance: false });

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free