e2e 0.15.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +51 -24
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +3 -3
  4. package/dist/agent/act.js.map +1 -1
  5. package/dist/agent/error.d.ts +16 -3
  6. package/dist/agent/error.d.ts.map +1 -1
  7. package/dist/agent/error.js +25 -6
  8. package/dist/agent/error.js.map +1 -1
  9. package/dist/agent/model/tool-choice.d.ts +24 -1
  10. package/dist/agent/model/tool-choice.d.ts.map +1 -1
  11. package/dist/agent/model/tool-choice.js +27 -1
  12. package/dist/agent/model/tool-choice.js.map +1 -1
  13. package/dist/agent/step-cache.d.ts +14 -3
  14. package/dist/agent/step-cache.d.ts.map +1 -1
  15. package/dist/agent/step-cache.js +13 -3
  16. package/dist/agent/step-cache.js.map +1 -1
  17. package/dist/agent/tool-loop.d.ts.map +1 -1
  18. package/dist/agent/tool-loop.js +62 -9
  19. package/dist/agent/tool-loop.js.map +1 -1
  20. package/dist/cache/context.d.ts +19 -6
  21. package/dist/cache/context.d.ts.map +1 -1
  22. package/dist/cache/context.js +15 -21
  23. package/dist/cache/context.js.map +1 -1
  24. package/dist/cli/init/scaffold.d.ts +1 -0
  25. package/dist/cli/init/scaffold.d.ts.map +1 -1
  26. package/dist/cli/init/scaffold.js +2 -1
  27. package/dist/cli/init/scaffold.js.map +1 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/expect/values.js +1 -1
  30. package/dist/expect/values.js.map +1 -1
  31. package/dist/run/execute.d.ts.map +1 -1
  32. package/dist/run/execute.js +21 -8
  33. package/dist/run/execute.js.map +1 -1
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/docs/agent-steps.mdx +126 -0
  37. package/docs/agents.mdx +217 -0
  38. package/docs/api-testing.mdx +65 -0
  39. package/docs/authentication.mdx +345 -0
  40. package/docs/browser.mdx +440 -0
  41. package/docs/bug-bash.mdx +90 -0
  42. package/docs/cache.mdx +306 -0
  43. package/docs/ci.mdx +210 -0
  44. package/docs/coding-agents.mdx +183 -0
  45. package/docs/debugging.mdx +291 -0
  46. package/docs/executors.mdx +243 -0
  47. package/docs/explore.mdx +174 -0
  48. package/docs/github.mdx +184 -0
  49. package/docs/index.mdx +82 -0
  50. package/docs/integrations/eas.mdx +216 -0
  51. package/docs/integrations/index.mdx +24 -0
  52. package/docs/integrations/kernel.mdx +163 -0
  53. package/docs/migrate/cypress.mdx +124 -0
  54. package/docs/migrate/detox.mdx +140 -0
  55. package/docs/migrate/maestro.mdx +229 -0
  56. package/docs/migrate/playwright.mdx +427 -0
  57. package/docs/migrate/selenium.mdx +110 -0
  58. package/docs/mobile-ci.mdx +567 -0
  59. package/docs/mobile.mdx +555 -0
  60. package/docs/models.mdx +241 -0
  61. package/docs/quickstart.mdx +260 -0
  62. package/docs/reference/agent.mdx +524 -0
  63. package/docs/reference/app.mdx +72 -0
  64. package/docs/reference/cli.mdx +817 -0
  65. package/docs/reference/config.mdx +986 -0
  66. package/docs/reference/engine.mdx +1362 -0
  67. package/docs/reference/environment.mdx +113 -0
  68. package/docs/reference/errors.mdx +143 -0
  69. package/docs/reference/expect.mdx +513 -0
  70. package/docs/reference/mcp.mdx +202 -0
  71. package/docs/reference/mobile.mdx +522 -0
  72. package/docs/reference/reporters.mdx +216 -0
  73. package/docs/reference/screen.mdx +688 -0
  74. package/docs/reference/test.mdx +449 -0
  75. package/docs/reference/web.mdx +611 -0
  76. package/docs/security.mdx +292 -0
  77. package/docs/starting-your-app.mdx +231 -0
  78. package/docs/subscriptions.mdx +157 -0
  79. package/docs/telemetry.mdx +185 -0
  80. package/docs/tools.mdx +150 -0
  81. package/docs/viewports.mdx +146 -0
  82. package/docs/writing-an-engine.mdx +316 -0
  83. package/docs/writing-tests.mdx +256 -0
  84. package/package.json +2 -1
  85. package/skills/e2e/SKILL.md +4 -1
  86. package/skills/e2e/references/agent.md +3 -1
  87. package/skills/e2e/references/setup.md +13 -4
@@ -0,0 +1,522 @@
1
+ ---
2
+ title: Mobile
3
+ description: Mobile engine options and the device fixture for iOS and Android.
4
+ ---
5
+
6
+ `@e2e-dev/mobile` drives iOS simulators and Android emulators through
7
+ [agent-device](https://github.com/callstack/agent-device). `agent`, `app`,
8
+ `screen`, and `expect` work as they do everywhere; this page covers what is
9
+ specific to a device target. Import `test` from the package to have `device`
10
+ typed, and declare `requires: ['device']` in a suite that also runs on the
11
+ web.
12
+
13
+ Choose **Mobile (iOS/Android)** in `e2e init` to add the package with a
14
+ Settings example. iOS needs Xcode and a simulator; Android needs the Android
15
+ SDK and an emulator.
16
+
17
+ The package includes a compatible version of `agent-device`; no separate
18
+ installation is needed.
19
+
20
+ ## Engine
21
+
22
+ ```ts title="e2e.config.ts"
23
+ import type { E2EConfig } from 'e2e';
24
+ import { mobile } from '@e2e-dev/mobile';
25
+ import { mobileTools } from '@e2e-dev/mobile/tools';
26
+ import { gateway } from 'ai';
27
+
28
+ const iphone = mobile({ platform: 'ios' });
29
+ const pixel = mobile({ platform: 'android' });
30
+
31
+ export default {
32
+ targets: [
33
+ { name: 'iphone', engine: iphone, app: { bundleId: 'Settings' } },
34
+ { name: 'pixel', engine: pixel, app: { bundleId: 'com.android.settings' } },
35
+ ],
36
+ workers: 1,
37
+ agents: {
38
+ default: {
39
+ model: gateway('openai/gpt-6-luna-fast'),
40
+ tools: mobileTools(iphone, pixel),
41
+ },
42
+ },
43
+ } satisfies E2EConfig;
44
+ ```
45
+
46
+ Create an engine for each platform you test. Use `platforms: ['ios']` or
47
+ `platforms: ['android']` on tests with platform-specific labels or operations.
48
+ `app.open()` launches the target's app fresh; an attempt launches nothing
49
+ on its own, so a test starts where the previous one left the app. `app.open`
50
+ takes no path on a device: a link goes through `device.openLink`.
51
+
52
+ The app under test is the target's `app`, not an engine option: `bundleId`,
53
+ `appPath`, `launchArguments`, `permissions`, `identity`, and `environment`
54
+ are documented under [the app under test](/reference/config#the-app-under-test).
55
+ A device target needs `app.bundleId` or `app.appPath`. An `app.url` (not
56
+ supported yet), a link in `bundleId` (a test opens one with
57
+ `device.openLink`), and an unknown permission name are `INVALID_CONFIG` at
58
+ config load. `mobile({ app })` and the other old app options fail naming
59
+ their key under `app`.
60
+
61
+ `app.appPath` is the build the suite runs against. The engine installs
62
+ nothing on its own: `device.installApp()` with no path installs it, once per
63
+ device, where the suite says so, and a device provider installs it on the
64
+ device it leases. Without `bundleId`, the installed bundle id or package
65
+ becomes the app `app.open()` launches. Declare an `identity` when the build
66
+ path changes per run (a version in it) so recordings survive the rename.
67
+
68
+ `mobile()` takes only how the device is driven:
69
+
70
+ ```ts
71
+ /**
72
+ * Options of the device engine: which devices it drives and how. The app
73
+ * under test is the target's `app` (`bundleId`, `appPath`,
74
+ * `launchArguments`, `permissions`). Every optional value also accepts
75
+ * `undefined`, so values read straight from `process.env` need no
76
+ * conditional spread.
77
+ */
78
+ export interface MobileOptions {
79
+ /** Platform the target's device runs. */
80
+ readonly platform: MobilePlatform;
81
+ /**
82
+ * Simulator or emulator to use, by name, simulator UDID, or emulator
83
+ * serial (`emulator-5554`). A list is a pool: the
84
+ * engine declares one worker per entry and worker slot `n` drives the
85
+ * `n`th, so `workers` at or above the pool size runs the target's files
86
+ * across every device at once. Omitted, the pool is every booted device of
87
+ * the platform at `prepare`, as many as the run has slots; with none booted,
88
+ * agent-device boots one. A `DeviceProvider` leases hosted devices instead:
89
+ * one per worker slot at `prepare`, each driven through the daemon and
90
+ * client configuration the lease names, all released when the run ends.
91
+ */
92
+ readonly device?: string | readonly string[] | DeviceProvider | undefined;
93
+ /**
94
+ * agent-device session name, before the worker slot: slot `n` drives its
95
+ * device under `<session>-<n>`, `e2e-<target name>-<n>` by default. One run
96
+ * per session at a time: concurrent runs on the same session interleave
97
+ * taps.
98
+ */
99
+ readonly session?: string | undefined;
100
+ /**
101
+ * What an observation captures. `full` (default) includes static text, so
102
+ * judgments can read values; `interactive` keeps only actionable nodes and
103
+ * is cheaper on screens with long lists.
104
+ */
105
+ readonly snapshot?: 'full' | 'interactive' | undefined;
106
+ /**
107
+ * UI stability window after a tap, fill, screen scroll, or back, in
108
+ * milliseconds. Default 150. Raise it for slow transitions. `0` keeps only
109
+ * the first re-observation; `false` skips the wait.
110
+ */
111
+ readonly settle?: number | false | undefined;
112
+ /**
113
+ * Wait before acting on a control that just appeared or moved, in
114
+ * milliseconds. Default 500. Accessibility bounds cannot tell a sliding
115
+ * control from a landed one (iOS reports the final frame at once, Android
116
+ * the frame in flight), so the control is found again in a fresh snapshot
117
+ * once the wait is over and acted on where it landed. Unmoved controls are
118
+ * acted on immediately. Raise this for slower transitions.
119
+ */
120
+ readonly transition?: number | undefined;
121
+ /**
122
+ * Whether video recordings draw agent-device's touch indicator where a
123
+ * test tapped. Default `true`. `false` records the plain screen and skips
124
+ * drawing the indicator when the recording stops, which on a hosted daemon
125
+ * such as EAS Simulators' can outlast `cleanupTimeout`.
126
+ */
127
+ readonly videoTouches?: boolean | undefined;
128
+ }
129
+
130
+ /** A permission's state: held, refused, or not asked for yet, so the OS asks again. */
131
+ export type PermissionState = 'grant' | 'deny' | 'reset';
132
+
133
+ /** Permissions an app holds when it launches, by name. */
134
+ export type LaunchPermissions = Readonly<Partial<Record<DevicePermission, PermissionState>>>;
135
+ ```
136
+
137
+ An empty `device` list is `INVALID_CONFIG`, and so is an option `mobile()`
138
+ does not take, naming the nearest known option when it is a likely typo and
139
+ every option otherwise. A `device` provider object
140
+ leases hosted devices; see [Hosted devices](/mobile#hosted-devices) for the
141
+ `DeviceProvider` contract and an example provider.
142
+
143
+ Devices boot in `prepare`, once per run and before the run's clock starts, so
144
+ a cold boot on CI is never charged against `launchTimeout`. agent-device
145
+ commands take no abort signal, so a timed-out command keeps running on the
146
+ device; the next attempt waits for it to settle. On iOS, `prepare` starts the
147
+ automation runner on every device with agent-device's `prepare ios-runner`,
148
+ so its first launch is paid before the first test, then brings the target's
149
+ `app.bundleId` up once per device when it is installed; a build the suite
150
+ installs itself comes up with the first `app.open()`, which relaunches it
151
+ fresh whenever a test asks. A runner that does not start in `prepare` is logged and left
152
+ to the first attempt, and that device's app is not opened there.
153
+
154
+ `app.launchArguments` and `app.permissions` ride every fresh launch of the
155
+ pinned app: `app.open()`, `app.restart()`, and `app.clearState()`, which
156
+ resets the permissions with the data. iOS hands the arguments to the app
157
+ process (`ProcessInfo.processInfo.arguments`); Android appends them to
158
+ `am start`, so `['--ez', 'e2e', 'true']` is a boolean intent extra. `deny` is
159
+ a recorded refusal on iOS, where the app is told no without a prompt, and a
160
+ revocation on Android, where the OS asks again when the app requests the
161
+ permission. The permissions are set before the app starts,
162
+ since iOS terminates a running app whose permission changed and Android one
163
+ whose permission was revoked. agent-device sets them on the app its session
164
+ is on, and only an open puts a session on an app: the warm-up's open in
165
+ `prepare` does that under the session the worker resumes, so a worker's
166
+ launches set them straight away. A run without `prepare`, the first launch
167
+ after `device.closeApp`, and an app the session has not opened get one
168
+ foreground open first; that open is the one process start no permission can
169
+ precede, and the relaunch after it has them. The warm-up passes no launch
170
+ argument and touches no permission.
171
+ `device.openApp(app, { relaunch: true, launchArguments, permissions })` does
172
+ the same for one launch of any app. A permission name outside
173
+ `DevicePermission` (naming the nearest one when it is a likely typo) or a
174
+ state other than `grant`, `deny`, or `reset` fails before any device command:
175
+ `INVALID_CONFIG` in `app.permissions`, `INVALID_ARGUMENT` in
176
+ `device.openApp`.
177
+
178
+ The engine declares one worker per configured device. With no `device`,
179
+ `prepare` uses the booted devices it finds, capped by the run's slots, and
180
+ boots one when none are. Two workers never drive the same simulator. To
181
+ split a target's files across devices, pass a pool and set `workers` to at
182
+ least its size.
183
+
184
+ ```ts
185
+ import type { E2EConfig } from 'e2e';
186
+ import { mobile } from '@e2e-dev/mobile';
187
+
188
+ const iphones = mobile({
189
+ platform: 'ios',
190
+ device: ['iPhone 17', 'iPhone 17 Pro'],
191
+ });
192
+
193
+ export default {
194
+ targets: [{ name: 'ios', engine: iphones, app: { bundleId: 'com.example.app' } }],
195
+ workers: 2,
196
+ } satisfies E2EConfig;
197
+ ```
198
+
199
+ ### Capabilities
200
+
201
+ | Contract member | On a device |
202
+ | --- | --- |
203
+ | `observe` | The accessibility tree with rects. iOS: `Button` is `button`, `TextField` and `SearchField` are `textbox`, `SecureTextField` is a secure `textbox`, `Cell` is `listitem`, `StaticText` is `text`, `Switch` is `switch` with `checked`, `TabBar` and `SegmentedControl` are `tablist` and the buttons inside them `tab`, `ProgressIndicator` is `progressbar`, `ActivityIndicator` is `status`, `Stepper` is `spinbutton`, `Toolbar`, `Menu`, and `RadioGroup` are `toolbar`, `menu`, and `radiogroup`. Android: `TextView` is `text`, `EditText` is `textbox` and one UIAutomator flags as `password` is a secure `textbox` like `SecureTextField`, `Switch` and `CheckBox` keep their roles and `RadioButton` is `radio`, each with the `checked` state the device reports, layouts are `group`, `RecyclerView` and `AbsListView` (React Native's `FlatList`) are `list`, `TabLayout` and `BottomNavigationView` are `tablist` with their items as `tab`, `ProgressBar` is `progressbar`, `NumberPicker` is `spinbutton`, `Toolbar` and `RadioGroup` keep their names. Any other type is kept verbatim in lower case (`picker-wheel`), so it answers no vocabulary role, unless the platform reports it editable, which makes it a `textbox`. React Native on iOS has no trait for `checkbox`, `radio`, and a few other roles and spells the role and state into the accessibility value instead (`checkbox, unchecked`, `radio button, checked`); such a node takes that role, `checked` and `expanded` from the state words, and reports only what follows them as its value. An iOS field keeps a value equal to its label (an unlabeled field is named by its text); Android's echo of a text view's label as its value is dropped. The accessibility identifier (iOS) or resource id (Android) is the node's `testId`. An Android node is named by the content description its app set (React Native's `accessibilityLabel`), else by its text, and its `text` is what it shows, so a labeled text view or a filled field answers `getByLabel` by its label and `getByText` by its text; a view flagged as a heading is a `heading`, and the role description React Native writes for a role Android has no class for (`Tab`, `Tab List`, `Radio Group`) gives the node that role. A field's hint is its `placeholder` attribute, showing or not. One Android fact is agent-device's, not the platform's: `rect` and every point are the screen's physical pixels; the display density it reports is not applied yet. Android's status bar and navigation bar, the full-width `com.android.systemui` windows along the screen's edges and the narrow status icons (clock, signal, battery) agent-device reports on their own inside that edge band, are left out; the notification shade, system dialogs, and a heads-up notification or other popup inset from the sides stay. Pixels on request, with every secure node's bounds painted black. |
204
+ | `perform` | `tap`, `doubleTap`, `longPress`, `fill`, `clear`, `hover`, `check`, `uncheck`, `press` (`Enter` or one character), `swipe`, `dragTo`, and `focus` on editable fields. A tap on a `switch` or `checkbox` lands on its innermost control, since UIKit reports a settings row as one labelled switch with the real toggle as a child. `check` and `uncheck` need a `checked` state, which Android switches lack and a React Native checkbox or radio on iOS has. `doubleTap` is two presses, 284 to 285 ms apart on iOS (the cadence of agent-device's XCTest tap): agent-device's own double-tap gesture reaches a React Native `Pressable` as one press. A detector with a window of 300 ms or less can read the pair as two single taps. `longPress` holds for one second unless the test passes `duration`, past a React Native `Pressable`'s `delayLongPress` of 500 ms and the longer thresholds apps set; agent-device's own default hold is shorter and registers as a tap on those. `selectOption`, `setInputFiles`, `scrollIntoView`, and `secondaryTap` are not declared, and neither is `tapModifiers`, so `tap` or `doubleTap` with `modifiers` is `UNSUPPORTED_CAPABILITY`. |
205
+ | `performAt` | `tap`, `doubleTap`, `longPress`, and `swipeTo` at a bare screen point, so `screen.tapAt`, `tap({ position })`, `screen.swipe({ from, to })`, and the agent's `tap_at` reach a control the tree does not list. No `hover`, `secondaryTap`, or `dragTo` at a point: touch has none without a node, so `hover_at` needs a listed control. |
206
+ | `keyboard` | `type` and `press` into the focused field, the agent's `type` and `press` without a target and the fallback of `type_at` and `press_at`, and `dismiss`, the agent's `dismiss_keyboard`. |
207
+ | `locate` | Every `screen` query. Text and label queries answer with the innermost match, since a device echoes both up the tree (a React Native input is a host view and the field inside it, both labelled). |
208
+ | `validateApp` | Requires `app.bundleId` or `app.appPath` and refuses `app.url` for now, with a note on reaching this machine from the platform's device. A target with either is a `native-app` target, so `requires: ['native-app']` selects device targets. |
209
+ | `session` | `back`, `restart`, and `reset`. No `open`: `app.open()` takes no path and the runner serves it with `restart`, the fresh launch of the pinned app; the agent's `navigate` verb is unavailable, and `device.openLink(url)` opens a deep link or web link. |
210
+ | `artifacts` | Redacted screenshots under `screenshots/NNN-<label>.png`; a screenshot that cannot be redacted fails instead of being written. Video, when the attempt's [video mode](/reference/config#video) records, as `video/video.mp4`, or a device provider's own recording. No traces. |
211
+ | `location` | `<bundle id> / <screen title>`, the replay cache's anchor. The title is the navigation bar's on iOS and the toolbar's on Android. |
212
+ | `state` | Not declared. `test.setup` and `session:` are unavailable. |
213
+
214
+ ## Replay cache on a device
215
+
216
+ A step recorded on `com.apple.Preferences / General` replays only when
217
+ Settings is on General again. A mutating project-tool call such as
218
+ `open_app` ends a replay. Declare `app.bundleId` and call `app.open()` first, so steps
219
+ start on the same screen, and switch apps through the `device` fixture
220
+ between steps rather than inside one.
221
+
222
+ ## Device
223
+
224
+ ```ts
225
+ /** Deterministic device management exposed to tests as `device`. */
226
+ export interface Device {
227
+ /**
228
+ * A locator from an agent-device selector (`'id=SW_VERSION_SPECIFIER'`,
229
+ * `'role=StaticText label="26.2"'`), for nodes the closed `screen` query
230
+ * vocabulary cannot name. Same polling and strictness as any locator.
231
+ */
232
+ locator(selector: string): Locator;
233
+ /** Toggles the device's Wi-Fi. */
234
+ setNetwork(state: 'online' | 'offline'): Promise<void>;
235
+ /** Toggles airplane mode. */
236
+ setAirplaneMode(enabled: boolean): Promise<void>;
237
+ /**
238
+ * Grants, denies, or resets one permission for the pinned app, brought to
239
+ * the foreground first when the session is on no app. A change terminates
240
+ * a running app on iOS, a revoke one on Android, so it goes before the
241
+ * `app.open()` a test starts with.
242
+ */
243
+ setPermission(permission: DevicePermission, state: PermissionState): Promise<void>;
244
+ /** Sets the simulated location, switching location services on first on Android. */
245
+ setLocation(coordinates: { latitude: number; longitude: number }): Promise<void>;
246
+ /** Turns simulated location off; on Android that is location services off, until the next `setLocation`. */
247
+ clearLocation(): Promise<void>;
248
+ /** Sets the system appearance. */
249
+ setAppearance(mode: 'light' | 'dark'): Promise<void>;
250
+ /** Rotates the device. */
251
+ setOrientation(orientation: DeviceOrientation): Promise<void>;
252
+ /**
253
+ * Puts a foldable iOS simulator (iPhone Duo) in a hinge pose and resolves
254
+ * once agent-device reads the angle back from CoreDevice at that pose,
255
+ * typically 10 to 16 seconds later. `closed` lights the outer display;
256
+ * `half-open` (a 130° book) and `open` light the inner one, whose app
257
+ * window has a different size, so the next observation reads a new screen.
258
+ * Any device without a hinge is `UNSUPPORTED_CAPABILITY`.
259
+ */
260
+ fold(pose: FoldPose): Promise<void>;
261
+ /** Simulates one biometric attempt on the open app. */
262
+ setBiometrics(sensor: BiometricSensor, result: 'match' | 'nonmatch'): Promise<void>;
263
+ /** Enrolls or unenrolls the simulator's Face ID or Touch ID. */
264
+ enrollBiometrics(sensor: 'faceid' | 'touchid', enrolled: boolean): Promise<void>;
265
+ /**
266
+ * Installs a build (an iOS `.app` bundle or an Android `.apk`, resolved
267
+ * against the project root) on the device; without a path, the target's
268
+ * `app.appPath`. The engine installs nothing on its own, so a suite that runs
269
+ * against a build calls this once per device, in a fixture or a test.
270
+ * `reinstall: true` removes the app first so it starts with no data; a
271
+ * plain install replaces the binary and keeps its data. Resolves to the
272
+ * identity to `openApp` it by, which becomes the app `app.open()` launches
273
+ * when the target's build is installed and no `app.bundleId` is pinned.
274
+ */
275
+ installApp(appPath?: string, options?: InstallAppOptions): Promise<InstalledApp>;
276
+ /**
277
+ * Brings an app to the foreground by bundle id, package, or display name;
278
+ * `relaunch` restarts it fresh. `launchArguments` and `permissions` apply
279
+ * to this launch alone; the target's configured ones apply to a `relaunch`
280
+ * of the pinned app, and to no foreground-only open. A link is
281
+ * `INVALID_ARGUMENT` (`file:`, `data:`, and `javascript:` are
282
+ * `POLICY_DENIED`): `openLink` opens one.
283
+ */
284
+ openApp(app: string, options?: OpenAppOptions): Promise<void>;
285
+ /**
286
+ * Opens a deep link (`myapp://orders/42`) or a web link (`https://...`)
287
+ * into `app` (default: the pinned app), which the session observes
288
+ * afterwards. iOS launches the app for a web link and then opens the URL;
289
+ * a web link the app does not claim goes to Safari, and the next
290
+ * observation brings the app back. Android starts the link on that
291
+ * package. Without an app, Android lets the OS route the link; iOS needs
292
+ * one (`INVALID_ARGUMENT` without). `file:`, `data:`, and `javascript:`
293
+ * links are `POLICY_DENIED`.
294
+ */
295
+ openLink(url: string, options?: { app?: string }): Promise<void>;
296
+ /**
297
+ * Terminates the session's app and ends the session, so the next
298
+ * `openApp` launches it fresh. Until then the screen is unobservable and
299
+ * `foregroundApp` is `APP_NOT_OPEN`.
300
+ */
301
+ closeApp(): Promise<void>;
302
+ /**
303
+ * Resets the simulator's keychain, every app's, since simctl has no
304
+ * per-app reset: what `app.clearState()` leaves behind (a session token, a
305
+ * Firebase sign-in). iOS simulator only; Android is
306
+ * `UNSUPPORTED_CAPABILITY`. Launch the app afterwards to see it signed out.
307
+ */
308
+ clearKeychain(): Promise<void>;
309
+ /**
310
+ * The app the session is on. iOS answers from the session, not the
311
+ * device, so it names the app the session opened even after `home()`;
312
+ * Android reads the device's foreground activity. `APP_NOT_OPEN` once
313
+ * `closeApp` ended the session.
314
+ */
315
+ foregroundApp(): Promise<ForegroundApp>;
316
+ /** Sends the device to its home screen. */
317
+ home(): Promise<void>;
318
+ /** Navigates back once (in-app back). */
319
+ back(): Promise<void>;
320
+ /** Accepts or dismisses the visible system alert. */
321
+ alert(action: 'accept' | 'dismiss'): Promise<void>;
322
+ /**
323
+ * Dismisses the soft keyboard: the keyboard's own dismiss key where it has
324
+ * one (Android, iPad), else the way a user does it on an iPhone, a short
325
+ * drag at the centre of the screen that a scroll view turns into a
326
+ * dismissal, horizontal first and vertical second, stopping as soon as the
327
+ * keyboard is gone. The simulator's "Speed up your typing" tip goes through
328
+ * its Continue button first. A keyboard that outlives both drags fails with
329
+ * `UNSUPPORTED_CAPABILITY`; the message names the alternatives (the app's
330
+ * own Done or close control, or pressing Enter on the field).
331
+ */
332
+ dismissKeyboard(): Promise<void>;
333
+ /** Reads the clipboard. */
334
+ clipboard(): Promise<string>;
335
+ /** Writes the clipboard. */
336
+ setClipboard(text: string): Promise<void>;
337
+ }
338
+
339
+ /** How `device.openApp` launches an app. */
340
+ export interface OpenAppOptions {
341
+ /** Terminate the app first, so it starts fresh; without it a running app is brought forward as it is. */
342
+ readonly relaunch?: boolean;
343
+ /**
344
+ * Arguments this launch hands the app: its process arguments on iOS,
345
+ * `am start` arguments on Android. Replaces the target's `app.launchArguments`
346
+ * for a relaunch of the pinned app.
347
+ */
348
+ readonly launchArguments?: readonly string[];
349
+ /**
350
+ * Permissions the app holds before this launch, each granted, denied, or
351
+ * reset. Replaces the target's `app.permissions` for a relaunch of the
352
+ * pinned app.
353
+ */
354
+ readonly permissions?: LaunchPermissions;
355
+ }
356
+
357
+ /** Permissions agent-device can grant, deny, or reset on an app. */
358
+ export type DevicePermission =
359
+ | 'camera'
360
+ | 'microphone'
361
+ | 'photos'
362
+ | 'contacts'
363
+ | 'notifications'
364
+ | 'calendar'
365
+ | 'location'
366
+ | 'reminders'
367
+ | 'motion'
368
+ | 'siri'
369
+ | 'media-library';
370
+
371
+ /** Orientations `setOrientation` accepts. */
372
+ export type DeviceOrientation = 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
373
+
374
+ /** Hinge poses `device.fold` accepts: `closed` lights the outer display, `half-open` and `open` the inner one. */
375
+ export type FoldPose = 'closed' | 'half-open' | 'open';
376
+
377
+ /** How `device.installApp` installs a build. */
378
+ export interface InstallAppOptions {
379
+ /**
380
+ * Bundle id or package of the build. `reinstall` needs one: agent-device
381
+ * removes that app before installing. Defaults to the pinned app.
382
+ */
383
+ readonly app?: string;
384
+ /** Remove the installed app first so the build starts with no data. */
385
+ readonly reinstall?: boolean;
386
+ }
387
+
388
+ /** What an install put on the device, as agent-device identified it. */
389
+ export interface InstalledApp {
390
+ /** The bundle id or package to open the app by. */
391
+ readonly app: string;
392
+ readonly bundleId?: string;
393
+ }
394
+
395
+ /** The app in the foreground, as `foregroundApp` reports it. */
396
+ export interface ForegroundApp {
397
+ readonly name: string;
398
+ readonly bundleId?: string;
399
+ }
400
+
401
+ /** Sensors `setBiometrics` simulates. */
402
+ export type BiometricSensor = 'faceid' | 'touchid' | 'fingerprint';
403
+ ```
404
+
405
+ Every async method is a recorded step named `device.<method>`, bounded by
406
+ `config.actionTimeout`. A lost session is `APP_NOT_OPEN`, that budget
407
+ expiring is `ACTION_FAILED`, a command the platform cannot run is
408
+ `UNSUPPORTED_CAPABILITY`, anything else `ENGINE_FAILURE` with agent-device's
409
+ message: a timeout the device itself reports (a boot, the runner connecting)
410
+ keeps its text and hint there, and a busy or wedged iOS automation runner
411
+ names itself and the recovery (see
412
+ [Troubleshooting](/mobile#troubleshooting)).
413
+
414
+ `back`, `home`, `alert`, `dismissKeyboard`, `setOrientation`, and `fold`
415
+ change the screen, so they count as actions for the `transition` budget the way a tap
416
+ does: a control that arrives with the screen they reveal waits out the budget
417
+ before the next action lands on it.
418
+
419
+ `closeApp` terminates the app the session is on and ends the session, so
420
+ `openApp` after it launches the app fresh, on its first screen; a screen
421
+ query or `foregroundApp` in between is `APP_NOT_OPEN`. `foregroundApp` on
422
+ iOS reads the session, not the device: agent-device's `appstate` has no
423
+ device-level answer there, so after `home()` it still names the app the
424
+ session opened. Android reads the device's foreground activity.
425
+
426
+ `installApp` installs a build from inside a test or a fixture: the run's build once per device (no path installs `app.appPath`), or another build for an upgrade path. A plain
427
+ install keeps the app's data; `reinstall: true` removes the app named by
428
+ `app` (default: the pinned app) first. It resolves to the id to `openApp`
429
+ the build by.
430
+
431
+ ```ts
432
+ const { app } = await device.installApp('./build/MyApp.app', { reinstall: true });
433
+ await device.openApp(app);
434
+ ```
435
+
436
+ `clearKeychain` resets the simulator's keychain, which `app.clearState()`
437
+ leaves alone: simctl resets the whole simulator's, so every app on it loses
438
+ its keychain-backed credentials. iOS only; on Android it is
439
+ `UNSUPPORTED_CAPABILITY` before any device command. Launch the app afterwards
440
+ to see it signed out.
441
+
442
+ `openApp` brings an app to the foreground by bundle id, package, or display
443
+ name (`Settings`); `relaunch: true` restarts it fresh, and `launchArguments`
444
+ and `permissions` apply to that launch alone (the target's own apply to a
445
+ `relaunch` of the pinned app). It takes no link:
446
+ agent-device opens any `scheme:rest` string as a URL, so a link is refused
447
+ before the device sees it. A `file:`, `data:`, or `javascript:` link is
448
+ `POLICY_DENIED`, the rule every navigation follows; any other link is
449
+ `INVALID_ARGUMENT` pointing at `openLink`.
450
+
451
+ `openLink` opens a deep link or a web link into `app` (default: the pinned
452
+ app), for magic-link sign-in and deep-link routes, and the session observes
453
+ that app afterwards. iOS launches the app for a web link and then opens the
454
+ URL; a web link the app does not claim goes to Safari, and the next
455
+ observation brings the app back. Android starts the link on that package.
456
+ Without an app, Android lets the OS route the link, to a verified app link
457
+ owner or the browser, and the session follows the package that took it. iOS
458
+ needs an app: an open bound to none leaves no app session to observe, so it
459
+ is `INVALID_ARGUMENT`. `file:`, `data:`, and `javascript:` links are
460
+ `POLICY_DENIED`, as on the web. The report shows the link cut before its
461
+ query and fragment, where a magic link carries its token. The replay cache
462
+ anchors on the location the link lands on: a step recorded after `openLink`
463
+ replays when the link lands on the same screen again.
464
+
465
+ ```ts
466
+ await device.openLink('myapp://orders/42');
467
+ await expect(screen.getByText('Order #42')).toBeVisible();
468
+ ```
469
+
470
+ Use `device.locator` for platform-specific selectors that `screen` queries
471
+ cannot express.
472
+
473
+ ```ts
474
+ await expect(device.locator('role=NavigationBar id=About')).toBeVisible();
475
+ await expect(device.locator('role=StaticText label="iOS Version"')).toHaveText('iOS Version');
476
+ await device.locator('id=SW_VERSION_SPECIFIER role=Button').tap();
477
+ ```
478
+
479
+ Selector terms are agent-device's: `id`, `role`, `text`, `label`, `value`,
480
+ `appname`, `windowtitle`, and the flags `visible`, `hidden`, `editable`,
481
+ `selected`, `focused`, `enabled`, `hittable`. `role` accepts the platform
482
+ type (`StaticText`) or the contract role (`text`).
483
+
484
+ ## Agent tools
485
+
486
+ `mobileTools(...engines)` adds device tools to an agent's `tools`. It takes at
487
+ least one engine; pass all device engines in one call, and the tools use the
488
+ current attempt's engine.
489
+
490
+ | Tool | Replay | Purpose |
491
+ | --- | --- | --- |
492
+ | `open_app` | gap | Bring an app to the foreground by id, optionally relaunched. A link is refused, as it is for `device.openApp`. |
493
+ | `swipe` | gap | Free-form swipe between two points in logical pixels. |
494
+ | `alert` | gap | Accept or dismiss a system alert; prefer a listed button, whose tap replays. |
495
+
496
+ The grammar verbs need no tool pack: the engine's declared actions give the
497
+ agent `tap`, `double_tap`, `long_press`, `hover`, `type`, `press`, `check`,
498
+ `scroll`, `drag`, `scroll_to` by `text` (paging the list until a row reading it
499
+ is drawn), `back`, and `dismiss_keyboard`; there is no `right_click`,
500
+ `select`, `scroll_to` by node id, `upload`, or `navigate` on a device. The
501
+ pixel tools are `screenshot`, `tap_at`, `hover_at` (on a listed control only),
502
+ `type_at`, and `press_at`; `select_at` is not offered. Point actions can tap
503
+ controls absent from the accessibility tree. `type_secret` is offered when
504
+ the step declares a secret.
505
+
506
+ After a secret fill the agent offers neither `screenshot` nor `tap_at`,
507
+ including in later members of the same serial group. Pixels whose masking the
508
+ engine cannot prove are withheld as `MASKING_UNPROVEN`.
509
+
510
+ An accessibility capture failure still stops the observation. Device
511
+ screenshot masks depend on secure-field bounds from that capture, so the
512
+ engine cannot prove a fallback screenshot safe when those bounds are missing.
513
+
514
+ ## Exported types
515
+
516
+ `MobileOptions`, `MobilePlatform`, `DevicePermission`, `LaunchPermissions`,
517
+ `PermissionState`, `DeviceClientConfig`, `DeviceConnection`, `DeviceDaemon`,
518
+ `DeviceProvider`, `DeviceLease`, `DeviceReleaseContext`, `DeviceRequest`,
519
+ `Device`, `DeviceOrientation`, `BiometricSensor`, `ForegroundApp`,
520
+ `InstallAppOptions`, `InstalledApp`, and `OpenAppOptions` are exported from
521
+ `@e2e-dev/mobile`. The runtime exports are `mobile` and `test`, typed with the
522
+ `device` fixture; `mobileTools` comes from `@e2e-dev/mobile/tools`.