@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
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 recordedagent.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
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.
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.
