Skip to main content
@e2edev/agent-device drives iOS simulators and Android emulators through agent-device, against the same engine contract the browser engine implements. agent, app, screen, and expect work as they do everywhere; this page covers what is specific to a device target. Import test from the package to have device typed, and declare requires: ['device'] in a suite that also runs on the web. Choose Mobile (iOS/Android) in e2e init to add the package with a Settings example. It defaults to iOS on macOS and Android elsewhere, uses one worker, and needs no APP_URL. iOS needs Xcode and a simulator; Android needs the Android SDK and an emulator. Bring your own agent-device: the package declares agent-device as a peer dependency (0.21.x) and does not install it. A project that already drives devices with the agent-device CLI keeps its version and one copy in node_modules; e2e init adds agent-device only to a project that has none. agent-device is 0.x and its minors break, so the range pins the minor the engine was built and tested against and moves with each engine release. A version outside the range may be rejected by your package manager as an unmet peer (npm’s ERESOLVE).

Engine

e2e.config.ts
One engine per device family; a config declares as many as it needs. A test using the shared device APIs can run on iOS and Android. Platform-specific labels or operations need platforms: ['ios'] or platforms: ['android']. Device attempts open their configured app; app.open() requires a URL and is unavailable on these targets. Every optional value also accepts undefined, so a config passes device: process.env.E2E_DEVICE or appPath: process.env.E2E_APP_PATH straight through, with no conditional spread. The devices boot in prepare, once per run and outside every launch budget: each worker slot the run will use boots its device and opens the pinned app once, so its automation runner is up before any worker starts, one slot after another. init then finds a booted device. On a CI runner, where a cold boot and a runner launch take a minute or more, that time is spent before the run’s clock starts instead of inside launchTimeout. agent-device commands take no abort signal, so a cancelled or timed-out command is abandoned by its caller but keeps running on the device; the next attempt waits for every abandoned command to settle before it opens anything, within the launch timeout. Each attempt relaunches app when one is pinned; without it the surface observes whatever is in the foreground and the model opens apps with the open_app tool. The engine declares one worker per configured device: a single device is one worker and a pool is one per entry. With no device, prepare uses the booted devices it finds, capped by the run’s slots. When none are booted, it boots one and reports a one-worker cap. The scheduler respects this cap, so two workers never drive the same simulator. To split a target’s test files across several devices, pass a pool and let workers be at least its size, or omit device to discover booted devices. Four booted simulators can run four files at a time when workers is at least four. The discovered pool reaches workers through the run’s environment, E2E_AGENT_DEVICE_POOL_<TARGET>_<digest>, so each slot resumes the device prepare booted for it.

Capabilities

Trace cache on a device

The cache anchors a recorded agent.act step on the path the surface reports when the step begins and ends. This engine reports app://device/<bundle id>/<title>, with the title read off the navigation bar (its label, its inner text, or its identifier). A step recorded on /com.apple.preferences/General replays only when Settings is on General again. Recorded node actions replay through perform; viewport scrolling uses swipe. A mutating project-tool call such as open_app ends a replay, as does a target that no longer relocates. The read-only screenshot tool creates no replay gap. Pin app so attempts start on the same screen, and keep in-step app switching to the device fixture between steps.

Device

Every async method is a recorded step named device.<method>, bounded by config.actionTimeout and the attempt signal, and never involves a model. A lost session is APP_NOT_OPEN, a command that timed out is ACTION_FAILED, a command the platform cannot run is UNSUPPORTED_CAPABILITY, and anything else is ENGINE_FAILURE carrying agent-device’s message prefixed with the operation. installApp puts a build on the device from inside a test, for upgrade and fresh-install paths the appPath option cannot express. A plain install replaces the binary and keeps its data; reinstall: true removes the app named by app (default: the pinned app) first, and is APP_NOT_OPEN when neither names one. It resolves to the bundle id or package to openApp the build by.
locator is a synchronous accessor: it mints a core Locator from an agent-device selector, for nodes the closed screen vocabulary cannot name.
Selector terms are agent-device’s: id, role, text, label, value, appname, windowtitle, and the flags visible, hidden, editable, selected, focused, enabled, hittable. Text terms compare exactly after whitespace collapsing; role accepts either the platform type (StaticText) or the contract role (text). Polling and strictness are the runner’s, as for any locator.

Agent tools

agentDeviceTools(...engines) returns a defineTool pack for createAgent. Pass every device engine the config declares: tool names are fixed, so two packs cannot be merged; the pack is scoped to the platforms of those engines and dispatches each call to the engine whose attempt is running (a worker runs one attempt at a time): The grammar verbs (tap, type, scroll) come from the engine manifest and need no tool. screenshot and tap_at are the agent’s own pixel tools, offered while no secret has been filled; the engine declares tapAt, so a tap_at whose point lands on nothing the tree lists taps the bare point in logical pixels. type_secret is not offered: secret fills require an origin the runner can check, and app:// has none. After a secret fill the agent offers neither screenshot nor tap_at and captures no pixels, including in later members of the same serial group. Pixels whose masking the engine cannot prove are withheld as MASKING_UNPROVEN.

Project tools

Writing an engine

app

Config