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