[ Documentation ]
Detox Driver

Detox Driver

Drop-in replacement for import { by, element, expect, waitFor, device } from 'detox'. Your existing Detox specs run against Peach’s canvas with no native build.

Running your suite

You don’t wire this up by hand. peach detox runs your own Jest, forcing a preset that remaps ^detox$ to the Peach driver, so import ... from 'detox' resolves to Peach without touching your test files:

terminal
peach detox # discover e2e/ (or test/e2e, detox/) and run
peach detox e2e/login.test.ts # run specific files
peach detox init # scaffold peach-detox.config.cjs + e2e/example.test.ts
peach detox --headed # watch it run in a visible browser
peach detox -t "login" # jest --testNamePattern (also --grep)

The driver creates its own browser context. --no-launch skips the running-shell reachability check; it does not attach to an existing sim. The runner picks up config from --config <path>, else peach-detox.config.cjs / jest.config.cjs / jest.config.js, else a generated temp config. Your Jest version, transforms, and setup files are preserved.

Matchers

by.id(testID) // match by testID
by.text(text) // match by text content
by.label(label) // match by accessibility label
by.role(role) // match by accessibility role
by.type(componentType) // match by component type

by.text('Welcome') matches the full text exactly. Use a regular expression for partial text or case-insensitive matching. by.id, by.text, and by.label accept regular expressions:

by.text(/Welcome/)
by.id(/^login-/)
by.label(/^sign in$/i)

The i, m, and s flags are supported. Matching always uses Unicode; g and y are ignored, so repeated lookups do not advance the expression.

Element actions

element(matcher).tap(point?) // optional {x,y} offset
element(matcher).multiTap(times)
element(matcher).longPress(duration?) // default 1000ms
element(matcher).longPressAndDrag(duration, startX, startY, target, endX, endY)
element(matcher).typeText(text)
element(matcher).replaceText(text)
element(matcher).clearText()
element(matcher).scroll(pixels, direction) // 'up' | 'down' | 'left' | 'right'
element(matcher).scrollTo(edge) // 'top' | 'bottom' | 'left' | 'right'
element(matcher).swipe(direction, speed?, percentage?)
element(matcher).pinch(scale, speed?, angle?) // angle in radians
element(matcher).rotate(radians, speed?)
element(matcher).takeScreenshot(name)
element(matcher).getAttributes()
element(matcher).atIndex(i) // select the zero-based matching node

Assertions

expect(element(matcher)).toExist()
expect(element(matcher)).toBeVisible() // 75% visible-area threshold
expect(element(matcher)).toBeNotVisible()
expect(element(matcher)).toHaveText(text) // exact or substring
expect(element(matcher)).toHaveId(id)
expect(element(matcher)).toHaveLabel(label)
expect(element(matcher)).toHaveAccessibilityRole(role)
expect(element(matcher)).toBeEnabled()
expect(element(matcher)).toHaveValue(value)
// all support the .not prefix

toBeFocused checks focus. toHaveSliderPosition and toHaveToggleValue currently only assert existence.

waitFor

waitFor(element(matcher)).toExist().withTimeout(ms)
waitFor(element(matcher)).toBeVisible().withTimeout(ms)
waitFor(element(matcher)).toBeNotVisible().withTimeout(ms)
waitFor(element(matcher)).toHaveText(text).withTimeout(ms)
waitFor(element(matcher)).toHaveValue(value).withTimeout(ms)
waitFor(element(matcher)).whileElement(by.id('list')).scroll(200, 'down')

The waitFor chain intentionally exposes a narrower set of conditions than expect.

Device

device.launchApp(opts?)
device.reloadReactNative()
device.terminateApp()
device.openURL({ url })
device.takeScreenshot(name)
device.takeScreenshotFast(name) // no stability wait
device.startBridgeRecording(opts?)
device.stopAndSaveBridgeRecording(outPath)
device.getPlatform() // 'ios' | 'android'
device.pressBack() // android only
device.setLocation(latitude, longitude) // update this simulator's location

No-ops kept for source compatibility (there is no native process in the browser): installApp, uninstallApp, shake, setURLBlacklist, sendToHome.