e2e 0.21.1 → 0.21.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.
- package/README.md +2 -0
- package/dist/.build.json +1 -1
- package/dist/agent/act-validation.js +1 -0
- package/dist/agent/act.js +7 -3
- package/dist/agent/action-dispatcher.js +30 -8
- package/dist/agent/actions.d.ts +2 -0
- package/dist/agent/actions.js +5 -2
- package/dist/agent/error.js +1 -0
- package/dist/agent/executor.d.ts +14 -6
- package/dist/agent/index.js +5 -1
- package/dist/agent/invocation.d.ts +3 -0
- package/dist/agent/model/sdk.d.ts +5 -0
- package/dist/agent/model/sdk.js +18 -1
- package/dist/agent/observation.js +1 -0
- package/dist/agent/primitives.js +14 -3
- package/dist/agent/replay.js +9 -3
- package/dist/agent/step-cache.d.ts +3 -2
- package/dist/agent/step-cache.js +7 -4
- package/dist/agent/tool-loop.js +4 -1
- package/dist/cache/context.js +1 -1
- package/dist/cache/recorder.d.ts +2 -1
- package/dist/cache/recorder.js +11 -2
- package/dist/cache/trace-fields.d.ts +10 -0
- package/dist/cache/trace-fields.js +17 -0
- package/dist/cache/trace.d.ts +9 -1
- package/dist/cache/trace.js +26 -7
- package/dist/cli/feedback.js +2 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/collect/select.js +12 -2
- package/dist/config/agent.d.ts +1 -1
- package/dist/config/diagnose.js +9 -3
- package/dist/config/esm-hooks.js +3 -2
- package/dist/config/resolve.js +3 -2
- package/dist/config/typescript.js +52 -2
- package/dist/engine/contract.d.ts +3 -2
- package/dist/engine/manifest.js +2 -2
- package/dist/internal/errors.js +5 -2
- package/dist/internal/source.js +2 -0
- package/dist/internal/unserved-model.d.ts +5 -0
- package/dist/internal/unserved-model.js +22 -0
- package/dist/locator/screen.js +1 -2
- package/dist/oauth/copilot.js +23 -5
- package/dist/oauth/device-code.js +35 -19
- package/dist/oauth/errors.d.ts +3 -0
- package/dist/oauth/errors.js +26 -11
- package/dist/oauth/providers/github-copilot.d.ts +4 -0
- package/dist/oauth/providers/github-copilot.js +64 -16
- package/dist/oauth/providers/openai.d.ts +2 -2
- package/dist/oauth/providers/openai.js +47 -19
- package/dist/oauth/providers/opencode-console.js +36 -15
- package/dist/oauth/providers/xai.js +18 -9
- package/dist/oauth/token-endpoint.d.ts +15 -6
- package/dist/oauth/token-endpoint.js +18 -6
- package/dist/report/code-frame.js +9 -3
- package/dist/report/failure-text.d.ts +10 -2
- package/dist/report/failure-text.js +24 -8
- package/dist/report/list-model.d.ts +13 -0
- package/dist/report/list-steps.d.ts +6 -1
- package/dist/report/list-steps.js +29 -0
- package/dist/report/list.d.ts +1 -0
- package/dist/report/list.js +16 -7
- package/dist/report/live-window.js +2 -1
- package/dist/report/markdown.js +1 -1
- package/dist/run/fixtures.js +2 -0
- package/dist/run/host-trace.d.ts +3 -2
- package/dist/run/host-trace.js +2 -1
- package/dist/run/last-run.d.ts +9 -0
- package/dist/run/last-run.js +28 -0
- package/dist/run/open-session.d.ts +3 -1
- package/dist/run/open-session.js +1 -1
- package/dist/run/runner.js +6 -2
- package/dist/run/sessions.js +3 -3
- package/dist/run/standalone-attempt.d.ts +2 -1
- package/dist/run/standalone-attempt.js +3 -1
- package/dist/run/steps.d.ts +3 -1
- package/dist/run/units.d.ts +8 -0
- package/dist/run/units.js +14 -0
- package/dist/run/worker-models.d.ts +2 -0
- package/dist/run/worker-models.js +7 -0
- package/dist/types.d.ts +2 -2
- package/docs/authentication.mdx +26 -23
- package/docs/browser.mdx +7 -4
- package/docs/bug-bash.mdx +2 -2
- package/docs/cache.mdx +6 -1
- package/docs/ci/bitrise.mdx +1 -1
- package/docs/ci/codemagic.mdx +2 -2
- package/docs/ci/eas-workflows.mdx +1 -1
- package/docs/coding-agents.mdx +7 -35
- package/docs/debugging.mdx +24 -36
- package/docs/executors.mdx +1 -1
- package/docs/github.mdx +2 -1
- package/docs/integrations/eas.mdx +1 -1
- package/docs/integrations/index.mdx +6 -0
- package/docs/integrations/lightpanda.mdx +192 -0
- package/docs/integrations/testmu.mdx +152 -0
- package/docs/mcp.mdx +123 -0
- package/docs/migrate/detox.mdx +1 -1
- package/docs/migrate/maestro.mdx +101 -105
- package/docs/migrate/playwright.mdx +65 -70
- package/docs/mobile.mdx +18 -3
- package/docs/models.mdx +4 -2
- package/docs/quickstart.mdx +7 -0
- package/docs/reference/agent.mdx +369 -277
- package/docs/reference/app.mdx +141 -32
- package/docs/reference/cli.mdx +674 -558
- package/docs/reference/config.mdx +1406 -752
- package/docs/reference/engine.mdx +1062 -256
- package/docs/reference/environment.mdx +156 -84
- package/docs/reference/errors.mdx +106 -72
- package/docs/reference/expect.mdx +752 -372
- package/docs/reference/mcp.mdx +366 -141
- package/docs/reference/mobile.mdx +844 -403
- package/docs/reference/reporters.mdx +312 -106
- package/docs/reference/runner.mdx +373 -174
- package/docs/reference/screen.mdx +931 -398
- package/docs/reference/security.mdx +106 -40
- package/docs/reference/test.mdx +626 -273
- package/docs/reference/web.mdx +956 -533
- package/docs/security.mdx +6 -5
- package/docs/snippets/model-provider.jsx +763 -0
- package/docs/snippets/setup-prompt-card.jsx +22 -0
- package/docs/snippets/show-more.jsx +22 -0
- package/docs/snippets/video-card.jsx +34 -0
- package/docs/snippets/video.jsx +25 -0
- package/docs/subscriptions.mdx +10 -3
- package/docs/telemetry.mdx +39 -39
- package/package.json +3 -2
- package/schema/report-v1.schema.json +6 -0
- package/skills/e2e/references/agent.md +8 -3
- package/skills/e2e/references/debugging.md +1 -1
- package/skills/e2e/references/mcp.md +1 -1
- package/skills/e2e/references/setup.md +12 -3
- package/skills/e2e/references/writing-tests.md +1 -1
|
@@ -6,22 +6,50 @@ icon: "mobile-screen"
|
|
|
6
6
|
"twitter:image": "https://e2e.tester.army/docs/images/og/reference/mobile.png?v=3da450961d"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
`@e2e-dev/mobile`
|
|
10
|
-
phones through
|
|
11
|
-
[agent-device](https://github.com/callstack/agent-device).
|
|
12
|
-
`screen`, and `expect` work as they do
|
|
13
|
-
|
|
14
|
-
typed, and declare `requires: ['device']` in a suite that also runs on the
|
|
15
|
-
web.
|
|
9
|
+
`@e2e-dev/mobile` is the engine for iOS simulators, Android emulators, and
|
|
10
|
+
connected phones, driven through
|
|
11
|
+
[agent-device](https://github.com/callstack/agent-device). It contributes the
|
|
12
|
+
`device` fixture. `agent`, `app`, `screen`, and `expect` work as they do
|
|
13
|
+
everywhere; this page covers what is specific to a device target.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
```ts title="e2e.config.ts"
|
|
16
|
+
import type { E2EConfig } from 'e2e';
|
|
17
|
+
import { mobile } from '@e2e-dev/mobile';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
targets: [
|
|
21
|
+
{ name: 'iphone', engine: mobile({ platform: 'ios' }), app: { bundleId: 'com.apple.Preferences' } },
|
|
22
|
+
],
|
|
23
|
+
} satisfies E2EConfig;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts title="tests/settings.e2e.ts"
|
|
27
|
+
import { expect } from 'e2e';
|
|
28
|
+
import { test } from '@e2e-dev/mobile';
|
|
29
|
+
|
|
30
|
+
test('General opens in dark mode', async ({ app, device, screen }) => {
|
|
31
|
+
await device.setAppearance('dark');
|
|
32
|
+
await app.open();
|
|
33
|
+
await screen.getByText('General').tap();
|
|
34
|
+
await expect(screen.getByText('About')).toBeVisible();
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Import `test` from `@e2e-dev/mobile` to have `device` typed. Declare
|
|
39
|
+
`requires: ['device']` in a suite that also runs on the web.
|
|
40
|
+
- Choose **Mobile (iOS/Android)** in `e2e init` to add the package with a
|
|
41
|
+
Settings example. iOS needs Xcode and a simulator; Android needs the Android
|
|
42
|
+
SDK and an emulator.
|
|
43
|
+
- The package includes a compatible version of `agent-device`; no separate
|
|
44
|
+
installation is needed.
|
|
45
|
+
|
|
46
|
+
## Reference
|
|
20
47
|
|
|
21
|
-
|
|
22
|
-
installation is needed.
|
|
48
|
+
### Engine
|
|
23
49
|
|
|
24
|
-
|
|
50
|
+
`mobile(options)` creates an engine for one platform. Create one for each
|
|
51
|
+
platform you test, and use `platforms: ['ios']` or `platforms: ['android']`
|
|
52
|
+
on tests with platform-specific labels or operations.
|
|
25
53
|
|
|
26
54
|
```ts title="e2e.config.ts"
|
|
27
55
|
import type { E2EConfig } from 'e2e';
|
|
@@ -47,140 +75,73 @@ export default {
|
|
|
47
75
|
} satisfies E2EConfig;
|
|
48
76
|
```
|
|
49
77
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
on its own, so a test starts where the previous one left the app. `app.open`
|
|
54
|
-
takes no path on a device: a link goes through `device.openLink`.
|
|
78
|
+
`mobile()` takes `MobileOptions`: which devices the engine drives and how.
|
|
79
|
+
The app under test is the target's `app`, not an engine option; see
|
|
80
|
+
[The app under test](#the-app-under-test).
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
| Option | Type | Default |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| [`platform`](#platform) | `'ios' \| 'android'` | required |
|
|
85
|
+
| [`device`](#device-option) | `string \| readonly string[] \| DeviceProvider` | every booted device |
|
|
86
|
+
| [`session`](#session) | `string` | `e2e-<target name>` |
|
|
87
|
+
| [`snapshot`](#snapshot) | `'full' \| 'interactive'` | `'full'` |
|
|
88
|
+
| [`settle`](#settle) | `number \| false` | `150` |
|
|
89
|
+
| [`transition`](#transition) | `number` | `500` |
|
|
90
|
+
| [`videoTouches`](#videotouches) | `boolean` | `true` |
|
|
91
|
+
|
|
92
|
+
Every optional value also accepts `undefined`, so values read straight from
|
|
93
|
+
`process.env` need no conditional spread. Each entry below shows its member's
|
|
94
|
+
declaration.
|
|
95
|
+
|
|
96
|
+
| Throws | When |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `INVALID_CONFIG` | An empty `device` list. |
|
|
99
|
+
| `INVALID_CONFIG` | An option `mobile()` does not take, such as `mobile({ app })` and the other old app options. The message names the nearest known option for a likely typo, else every option. |
|
|
100
|
+
| `INVALID_CONFIG` | `settle` or `transition` is not a non-negative integer of milliseconds (`settle` may also be `false`). |
|
|
71
101
|
|
|
72
|
-
`
|
|
102
|
+
#### `platform`
|
|
73
103
|
|
|
74
104
|
```ts
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* `launchArguments`, `permissions`). Every optional value also accepts
|
|
79
|
-
* `undefined`, so values read straight from `process.env` need no
|
|
80
|
-
* conditional spread.
|
|
81
|
-
*/
|
|
82
|
-
export interface MobileOptions {
|
|
83
|
-
/** Platform the target's device runs. */
|
|
84
|
-
readonly platform: MobilePlatform;
|
|
85
|
-
/**
|
|
86
|
-
* Device to use: a simulator or emulator by name, simulator UDID, or
|
|
87
|
-
* emulator serial (`emulator-5554`), or a connected phone by name. A list
|
|
88
|
-
* is a pool: the
|
|
89
|
-
* engine declares one worker per entry and worker slot `n` drives the
|
|
90
|
-
* `n`th, so `workers` at or above the pool size runs the target's files
|
|
91
|
-
* across every device at once. Omitted, the pool is every booted device of
|
|
92
|
-
* the platform at `prepare`, as many as the run has slots; with none booted,
|
|
93
|
-
* agent-device boots one. A `DeviceProvider` leases hosted devices instead:
|
|
94
|
-
* one per worker slot at `prepare`, each driven through the daemon and
|
|
95
|
-
* client configuration the lease names, all released when the run ends.
|
|
96
|
-
*/
|
|
97
|
-
readonly device?: string | readonly string[] | DeviceProvider | undefined;
|
|
98
|
-
/**
|
|
99
|
-
* agent-device session name, before the worker slot: slot `n` drives its
|
|
100
|
-
* device under `<session>-<n>`, `e2e-<target name>-<n>` by default. One run
|
|
101
|
-
* per session at a time: concurrent runs on the same session interleave
|
|
102
|
-
* taps.
|
|
103
|
-
*/
|
|
104
|
-
readonly session?: string | undefined;
|
|
105
|
-
/**
|
|
106
|
-
* What an observation captures. `full` (default) includes static text, so
|
|
107
|
-
* judgments can read values; `interactive` keeps only actionable nodes and
|
|
108
|
-
* is cheaper on screens with long lists.
|
|
109
|
-
*/
|
|
110
|
-
readonly snapshot?: 'full' | 'interactive' | undefined;
|
|
111
|
-
/**
|
|
112
|
-
* UI stability window after a tap, fill, screen scroll, or back, in
|
|
113
|
-
* milliseconds. Default 150. Raise it for slow transitions. `0` keeps only
|
|
114
|
-
* the first re-observation; `false` skips the wait.
|
|
115
|
-
*/
|
|
116
|
-
readonly settle?: number | false | undefined;
|
|
117
|
-
/**
|
|
118
|
-
* Wait before acting on a control that just appeared or moved, in
|
|
119
|
-
* milliseconds. Default 500. Accessibility bounds cannot tell a sliding
|
|
120
|
-
* control from a landed one (iOS reports the final frame at once, Android
|
|
121
|
-
* the frame in flight), so the control is found again in a fresh snapshot
|
|
122
|
-
* once the wait is over and acted on where it landed. Unmoved controls are
|
|
123
|
-
* acted on immediately. Raise this for slower transitions.
|
|
124
|
-
*/
|
|
125
|
-
readonly transition?: number | undefined;
|
|
126
|
-
/**
|
|
127
|
-
* Whether video recordings draw agent-device's touch indicator where a
|
|
128
|
-
* test tapped. Default `true`. `false` records the plain screen and skips
|
|
129
|
-
* drawing the indicator when the recording stops, which on a hosted daemon
|
|
130
|
-
* such as EAS Simulators' can outlast `cleanupTimeout`.
|
|
131
|
-
*/
|
|
132
|
-
readonly videoTouches?: boolean | undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** A permission's state: held, refused, or not asked for yet, so the OS asks again. */
|
|
136
|
-
export type PermissionState = 'grant' | 'deny' | 'reset';
|
|
105
|
+
/** Platform the target's device runs. */
|
|
106
|
+
readonly platform: MobilePlatform;
|
|
107
|
+
```
|
|
137
108
|
|
|
138
|
-
|
|
139
|
-
|
|
109
|
+
```ts
|
|
110
|
+
mobile({ platform: 'android' });
|
|
140
111
|
```
|
|
141
112
|
|
|
142
|
-
|
|
143
|
-
does not take, naming the nearest known option when it is a likely typo and
|
|
144
|
-
every option otherwise. A `device` provider object
|
|
145
|
-
leases hosted devices; see [Hosted devices](/mobile#hosted-devices) for the
|
|
146
|
-
`DeviceProvider` contract and an example provider.
|
|
113
|
+
- `'ios'` or `'android'`. Required.
|
|
147
114
|
|
|
148
|
-
|
|
149
|
-
a cold boot on CI is never charged against `launchTimeout`. agent-device
|
|
150
|
-
commands take no abort signal, so a timed-out command keeps running on the
|
|
151
|
-
device; the next attempt waits for it to settle. On iOS, `prepare` starts the
|
|
152
|
-
automation runner on every device with agent-device's `prepare ios-runner`,
|
|
153
|
-
so its first launch is paid before the first test, then brings the target's
|
|
154
|
-
`app.bundleId` up once per device when it is installed; a build the suite
|
|
155
|
-
installs itself comes up with the first `app.open()`, which relaunches it
|
|
156
|
-
fresh whenever a test asks. A runner that does not start in `prepare` is logged and left
|
|
157
|
-
to the first attempt, and that device's app is not opened there.
|
|
115
|
+
#### `device` option
|
|
158
116
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
state other than `grant`, `deny`, or `reset` fails before any device command:
|
|
174
|
-
`INVALID_CONFIG` in `app.permissions`, `INVALID_ARGUMENT` in
|
|
175
|
-
`device.openApp`.
|
|
176
|
-
|
|
177
|
-
The engine declares one worker per configured device. With no `device`,
|
|
178
|
-
`prepare` uses the booted devices it finds, capped by the run's slots, and
|
|
179
|
-
boots one when none are. Two workers never drive the same simulator. To
|
|
180
|
-
split a target's files across devices, pass a pool and set `workers` to at
|
|
181
|
-
least its size.
|
|
117
|
+
```ts
|
|
118
|
+
/**
|
|
119
|
+
* Device to use: a simulator or emulator by name, simulator UDID, or
|
|
120
|
+
* emulator serial (`emulator-5554`), or a connected phone by name. A list
|
|
121
|
+
* is a pool: the engine declares one worker per entry and worker slot `n`
|
|
122
|
+
* drives the `n`th, so `workers` at or above the pool size runs the
|
|
123
|
+
* target's files across every device at once. Omitted, the pool is every booted device of
|
|
124
|
+
* the platform at `prepare`, as many as the run has slots; with none booted,
|
|
125
|
+
* agent-device boots one. A `DeviceProvider` leases hosted devices instead:
|
|
126
|
+
* one per worker slot at `prepare`, each driven through the daemon and
|
|
127
|
+
* client configuration the lease names, all released when the run ends.
|
|
128
|
+
*/
|
|
129
|
+
readonly device?: string | readonly string[] | DeviceProvider | undefined;
|
|
130
|
+
```
|
|
182
131
|
|
|
183
132
|
```ts
|
|
133
|
+
mobile({ platform: 'ios' }); // default: every booted iOS device
|
|
134
|
+
mobile({ platform: 'ios', device: 'iPhone 17' });
|
|
135
|
+
mobile({ platform: 'android', device: 'emulator-5554' });
|
|
136
|
+
mobile({ platform: 'ios', device: ['iPhone 17', 'iPhone 17 Pro'] });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- Two workers never drive the same device, as long as each `device` entry
|
|
140
|
+
names a different one.
|
|
141
|
+
- [Hosted devices](/mobile#hosted-devices) has the `DeviceProvider` contract
|
|
142
|
+
and an example provider.
|
|
143
|
+
|
|
144
|
+
```ts title="e2e.config.ts"
|
|
184
145
|
import type { E2EConfig } from 'e2e';
|
|
185
146
|
import { mobile } from '@e2e-dev/mobile';
|
|
186
147
|
|
|
@@ -195,160 +156,761 @@ export default {
|
|
|
195
156
|
} satisfies E2EConfig;
|
|
196
157
|
```
|
|
197
158
|
|
|
198
|
-
|
|
159
|
+
#### `session`
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
/**
|
|
163
|
+
* agent-device session name, before the worker slot: slot `n` drives its
|
|
164
|
+
* device under `<session>-<n>`, `e2e-<target name>-<n>` by default. One run
|
|
165
|
+
* per session at a time: concurrent runs on the same session interleave
|
|
166
|
+
* taps.
|
|
167
|
+
*/
|
|
168
|
+
readonly session?: string | undefined;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
mobile({ platform: 'ios', session: 'checkout' }); // default: e2e-<target name>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### `snapshot`
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
/**
|
|
179
|
+
* What an observation captures. `full` (default) includes static text, so
|
|
180
|
+
* judgments can read values; `interactive` keeps only actionable nodes and
|
|
181
|
+
* is cheaper on screens with long lists.
|
|
182
|
+
*/
|
|
183
|
+
readonly snapshot?: 'full' | 'interactive' | undefined;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
mobile({ platform: 'android', snapshot: 'interactive' }); // default: 'full'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `settle`
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
/**
|
|
194
|
+
* UI stability window after a tap, fill, screen scroll, or back, in
|
|
195
|
+
* milliseconds. Default 150. Raise it for slow transitions. `0` keeps only
|
|
196
|
+
* the first re-observation; `false` skips the wait.
|
|
197
|
+
*/
|
|
198
|
+
readonly settle?: number | false | undefined;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
mobile({ platform: 'ios', settle: 300 }); // default: 150
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### `transition`
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
/**
|
|
209
|
+
* Wait before acting on a control that just appeared or moved, in
|
|
210
|
+
* milliseconds. Default 500. Accessibility bounds cannot tell a sliding
|
|
211
|
+
* control from a landed one (iOS reports the final frame at once, Android
|
|
212
|
+
* the frame in flight), so the control is found again in a fresh snapshot
|
|
213
|
+
* once the wait is over and acted on where it landed. Unmoved controls are
|
|
214
|
+
* acted on immediately. Raise this for slower transitions.
|
|
215
|
+
*/
|
|
216
|
+
readonly transition?: number | undefined;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
mobile({ platform: 'ios', transition: 800 }); // default: 500
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`back`, `home`, `alert`, `dismissKeyboard`, `setOrientation`, and `fold` on
|
|
224
|
+
[`device`](#device) change the screen, so they count as actions for this
|
|
225
|
+
budget the way a tap does. A control that arrives with the screen they reveal
|
|
226
|
+
waits out the budget before the next action lands on it.
|
|
227
|
+
|
|
228
|
+
#### `videoTouches`
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
/**
|
|
232
|
+
* Whether video recordings draw agent-device's touch indicator where a
|
|
233
|
+
* test tapped. Default `true`. `false` records the plain screen and skips
|
|
234
|
+
* drawing the indicator when the recording stops, which on a hosted daemon
|
|
235
|
+
* such as EAS Simulators' can outlast `cleanupTimeout`.
|
|
236
|
+
*/
|
|
237
|
+
readonly videoTouches?: boolean | undefined;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
mobile({ platform: 'ios', videoTouches: false }); // default: true
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### The app under test
|
|
245
|
+
|
|
246
|
+
The app is the target's `app`: `bundleId`, `appPath`, `launchArguments`,
|
|
247
|
+
`permissions`, `identity`, and `environment` are documented under
|
|
248
|
+
[the app under test](/reference/config#the-app-under-test).
|
|
249
|
+
|
|
250
|
+
```ts title="e2e.config.ts"
|
|
251
|
+
export default {
|
|
252
|
+
targets: [{ name: 'ios', engine: mobile({ platform: 'ios' }), app: { appPath: './build/MyApp.app' } }],
|
|
253
|
+
} satisfies E2EConfig;
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- A device target needs `app.bundleId` or `app.appPath`.
|
|
257
|
+
- `app.open()` launches the target's app fresh. An attempt launches nothing
|
|
258
|
+
on its own, so a test starts where the previous one left the app.
|
|
259
|
+
- `app.open` takes no path on a device: a link goes through
|
|
260
|
+
[`device.openLink`](#openlink).
|
|
261
|
+
- `app.appPath` is the build the suite runs against. The engine installs
|
|
262
|
+
nothing on its own: [`device.installApp()`](#installapp) installs it where
|
|
263
|
+
the suite says so. A device provider installs it on the device it leases.
|
|
264
|
+
- Without `bundleId`, the installed bundle id or package becomes the app
|
|
265
|
+
`app.open()` launches.
|
|
266
|
+
- Declare an `identity` when the build path changes per run (a version in
|
|
267
|
+
it), so recordings survive the rename.
|
|
268
|
+
|
|
269
|
+
| Throws | When |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| `INVALID_CONFIG` | At config load: no `app.bundleId` and no `app.appPath`; an `app.url` (not supported yet); a link in `bundleId` (a test opens one with `device.openLink`); an unknown permission name. |
|
|
272
|
+
|
|
273
|
+
#### Launch arguments and permissions
|
|
274
|
+
|
|
275
|
+
`app.launchArguments` and `app.permissions` ride every fresh launch of the
|
|
276
|
+
pinned app: `app.open()`, `app.restart()`, and `app.clearState()`, which
|
|
277
|
+
resets the permissions with the data.
|
|
278
|
+
|
|
279
|
+
```ts title="e2e.config.ts"
|
|
280
|
+
export default {
|
|
281
|
+
targets: [
|
|
282
|
+
{
|
|
283
|
+
name: 'pixel',
|
|
284
|
+
engine: mobile({ platform: 'android' }),
|
|
285
|
+
app: {
|
|
286
|
+
bundleId: 'com.example.app',
|
|
287
|
+
launchArguments: ['--ez', 'e2e', 'true'],
|
|
288
|
+
permissions: { camera: 'grant', notifications: 'deny' },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
} satisfies E2EConfig;
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
- iOS hands the arguments to the app process
|
|
296
|
+
(`ProcessInfo.processInfo.arguments`). Android appends them to `am start`,
|
|
297
|
+
so `['--ez', 'e2e', 'true']` is a boolean intent extra.
|
|
298
|
+
- Each permission is `grant`, `deny`, or `reset`. `deny` is a recorded
|
|
299
|
+
refusal on iOS, where the app is told no without a prompt, and a revocation
|
|
300
|
+
on Android, where the OS asks again when the app requests the permission.
|
|
301
|
+
- The permissions are set on the app by name before it starts, with no open
|
|
302
|
+
first, since iOS terminates a running app whose permission changed and
|
|
303
|
+
Android one whose permission was revoked.
|
|
304
|
+
- The warm-up passes no launch argument and touches no permission.
|
|
305
|
+
- [`device.openApp(app, { relaunch: true, launchArguments, permissions })`](#openapp)
|
|
306
|
+
does the same for one launch of any app.
|
|
307
|
+
|
|
308
|
+
| Throws | When |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| `INVALID_CONFIG` | In `app.permissions`: a permission name outside `DevicePermission` (naming the nearest one when it is a likely typo), or a state other than `grant`, `deny`, or `reset`. Fails before any device command. |
|
|
311
|
+
| `INVALID_ARGUMENT` | The same, in `device.openApp`. |
|
|
312
|
+
|
|
313
|
+
#### Boot and prepare
|
|
314
|
+
|
|
315
|
+
Devices boot in `prepare`, once per run and before the run's clock starts,
|
|
316
|
+
so a cold boot on CI is never charged against `launchTimeout`.
|
|
317
|
+
|
|
318
|
+
- [`device`](#device-option) says which devices `prepare` uses and how many
|
|
319
|
+
workers the engine declares.
|
|
320
|
+
- On iOS, `prepare` starts the automation runner on every device with
|
|
321
|
+
agent-device's `prepare ios-runner`, so its first launch is paid before the
|
|
322
|
+
first test. Then it brings the target's `app.bundleId` up once per device
|
|
323
|
+
when it is installed. A build the suite installs itself comes up with the
|
|
324
|
+
first `app.open()`, which relaunches it fresh whenever a test asks.
|
|
325
|
+
- A device that cannot boot, or a runner that is busy, wedged, or past its
|
|
326
|
+
watchdog, ends the run in `prepare`.
|
|
327
|
+
- A runner that does not start for another reason is logged and left to the
|
|
328
|
+
first attempt, and that device's app is not opened there.
|
|
329
|
+
|
|
330
|
+
<Note>
|
|
331
|
+
agent-device commands take no abort signal, so a timed-out command keeps
|
|
332
|
+
running on the device. The next attempt waits for it to settle.
|
|
333
|
+
</Note>
|
|
334
|
+
|
|
335
|
+
#### Capabilities
|
|
199
336
|
|
|
200
337
|
| Contract member | On a device |
|
|
201
338
|
| --- | --- |
|
|
202
339
|
| `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. |
|
|
203
|
-
| `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`. |
|
|
340
|
+
| `perform` | `tap`, `doubleTap`, `longPress`, `fill`, `clear`, `hover`, `check`, `uncheck`, `press` (`Enter`, `Space`, 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`. |
|
|
204
341
|
| `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. |
|
|
205
342
|
| `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`. |
|
|
206
|
-
| `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). |
|
|
343
|
+
| `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). On Android a test id query, a string or a RegExp, also matches a resource id without its `<package>:id/` prefix, for any package but the framework's `android`, when nothing on the screen matches the full id. `screen.locator('id=…')` is agent-device's selector and takes the full id. |
|
|
207
344
|
| `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. |
|
|
208
345
|
| `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. |
|
|
209
346
|
| `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. |
|
|
210
347
|
| `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. |
|
|
211
348
|
| `state` | Not declared. `test.setup` and `session:` are unavailable. |
|
|
212
349
|
|
|
350
|
+
### Device
|
|
351
|
+
|
|
352
|
+
The `device` fixture is deterministic device management. A test calls it
|
|
353
|
+
directly to arrange or assert device state.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
test('offline banner', { platforms: ['android'] }, async ({ app, device, screen }) => {
|
|
357
|
+
await app.open();
|
|
358
|
+
await device.setNetwork('offline');
|
|
359
|
+
await expect(screen.getByText('You are offline')).toBeVisible();
|
|
360
|
+
await device.setNetwork('online');
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
| Member | Returns |
|
|
365
|
+
| --- | --- |
|
|
366
|
+
| [`locator`](#locator) | `Locator` |
|
|
367
|
+
| [`setNetwork`](#setnetwork) | `Promise<void>` |
|
|
368
|
+
| [`setAirplaneMode`](#setairplanemode) | `Promise<void>` |
|
|
369
|
+
| [`setPermission`](#setpermission) | `Promise<void>` |
|
|
370
|
+
| [`setLocation`](#setlocation) | `Promise<void>` |
|
|
371
|
+
| [`clearLocation`](#clearlocation) | `Promise<void>` |
|
|
372
|
+
| [`setAppearance`](#setappearance) | `Promise<void>` |
|
|
373
|
+
| [`setOrientation`](#setorientation) | `Promise<void>` |
|
|
374
|
+
| [`fold`](#fold) | `Promise<void>` |
|
|
375
|
+
| [`setBiometrics`](#setbiometrics) | `Promise<void>` |
|
|
376
|
+
| [`enrollBiometrics`](#enrollbiometrics) | `Promise<void>` |
|
|
377
|
+
| [`installApp`](#installapp) | `Promise<InstalledApp>` |
|
|
378
|
+
| [`openApp`](#openapp) | `Promise<void>` |
|
|
379
|
+
| [`openLink`](#openlink) | `Promise<void>` |
|
|
380
|
+
| [`closeApp`](#closeapp) | `Promise<void>` |
|
|
381
|
+
| [`clearKeychain`](#clearkeychain) | `Promise<void>` |
|
|
382
|
+
| [`foregroundApp`](#foregroundapp) | `Promise<ForegroundApp>` |
|
|
383
|
+
| [`home`](#home) | `Promise<void>` |
|
|
384
|
+
| [`back`](#back) | `Promise<void>` |
|
|
385
|
+
| [`alert`](#alert) | `Promise<void>` |
|
|
386
|
+
| [`dismissKeyboard`](#dismisskeyboard) | `Promise<void>` |
|
|
387
|
+
| [`clipboard`](#clipboard) | `Promise<string>` |
|
|
388
|
+
| [`setClipboard`](#setclipboard) | `Promise<void>` |
|
|
389
|
+
|
|
390
|
+
Every async method is a recorded step named `device.<method>`, bounded by
|
|
391
|
+
`config.actionTimeout`. `back`, `home`, `alert`, `dismissKeyboard`,
|
|
392
|
+
`setOrientation`, and `fold` count as actions for the
|
|
393
|
+
[`transition`](#transition) budget. Each entry below shows its member's
|
|
394
|
+
declaration; the types they use are under [Types](#device-fixture).
|
|
395
|
+
|
|
396
|
+
| Throws | When |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| `APP_NOT_OPEN` | The session is lost. |
|
|
399
|
+
| `ACTION_FAILED` | `actionTimeout` expires. |
|
|
400
|
+
| `UNSUPPORTED_CAPABILITY` | The platform cannot run the command. |
|
|
401
|
+
| `ENGINE_FAILURE` | Anything else, with agent-device's message. A timeout the device itself reports (a boot, the runner connecting) keeps its text and hint there. A busy or wedged iOS automation runner names itself and the recovery (see [Troubleshooting](/mobile#troubleshooting)). |
|
|
402
|
+
|
|
403
|
+
#### `locator`
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
/**
|
|
407
|
+
* A locator from an agent-device selector (`'id=SW_VERSION_SPECIFIER'`,
|
|
408
|
+
* `'role=text label="26.2"'`), for nodes the closed `screen` query
|
|
409
|
+
* vocabulary cannot name. Same polling and strictness as any locator.
|
|
410
|
+
*/
|
|
411
|
+
locator(selector: string): Locator;
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
await expect(device.locator('role=navigation-bar id=About')).toBeVisible();
|
|
416
|
+
await expect(device.locator('role=text label="iOS Version"')).toHaveText('iOS Version');
|
|
417
|
+
await device.locator('id=SW_VERSION_SPECIFIER role=button').tap();
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Selector terms and their rules are agent-device's, the ones `agent-device
|
|
421
|
+
find ... list` applies:
|
|
422
|
+
|
|
423
|
+
- Terms: `id`, `role`, `text`, `label`, `value`, `appname`, `windowtitle`,
|
|
424
|
+
and the flags `visible`, `hidden`, `editable`, `selected`, `focused`,
|
|
425
|
+
`enabled`, `hittable`.
|
|
426
|
+
- `role` takes agent-device's kind, the role `agent-device snapshot` prints:
|
|
427
|
+
`button`, `text`, `text-field`, `navigation-bar`, `scroll-area`, `switch`,
|
|
428
|
+
`image`. It is not the `getByRole` role (`textbox`).
|
|
429
|
+
- Text terms compare case-insensitively after collapsing whitespace.
|
|
430
|
+
- `visible` means the node is hittable or has a non-empty frame; `hidden` is
|
|
431
|
+
its opposite.
|
|
432
|
+
- `text` reads the first non-empty label, value, or identifier.
|
|
433
|
+
|
|
434
|
+
<Note>
|
|
435
|
+
An action on a selector that matches several nodes fails, where an
|
|
436
|
+
agent-device action skips an ambiguous `||` alternative.
|
|
437
|
+
</Note>
|
|
438
|
+
|
|
439
|
+
#### `setNetwork`
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
/**
|
|
443
|
+
* Switches an Android device's Wi-Fi. Cellular data stays on, so an
|
|
444
|
+
* emulator hands the app over to its cellular link; `setAirplaneMode(true)`
|
|
445
|
+
* cuts both. On iOS `offline` is `UNSUPPORTED_CAPABILITY`, since a
|
|
446
|
+
* simulator shares the Mac's network and simctl can only repaint its status
|
|
447
|
+
* bar; `online` sends nothing there, so a cleanup hook shared with Android
|
|
448
|
+
* passes.
|
|
449
|
+
*/
|
|
450
|
+
setNetwork(state: 'online' | 'offline'): Promise<void>;
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
await device.setNetwork('offline');
|
|
455
|
+
await device.setNetwork('online');
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
| Throws | When |
|
|
459
|
+
| --- | --- |
|
|
460
|
+
| `UNSUPPORTED_CAPABILITY` | `offline` on iOS. |
|
|
461
|
+
|
|
462
|
+
#### `setAirplaneMode`
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
/**
|
|
466
|
+
* Switches an Android device's airplane mode, Wi-Fi and cellular together;
|
|
467
|
+
* Android 11 or newer. On iOS, as for `setNetwork`, `true` is
|
|
468
|
+
* `UNSUPPORTED_CAPABILITY` and `false` sends nothing.
|
|
469
|
+
*/
|
|
470
|
+
setAirplaneMode(enabled: boolean): Promise<void>;
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
await device.setAirplaneMode(true);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
| Throws | When |
|
|
478
|
+
| --- | --- |
|
|
479
|
+
| `UNSUPPORTED_CAPABILITY` | `true` on iOS. |
|
|
480
|
+
|
|
481
|
+
#### `setPermission`
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
/**
|
|
485
|
+
* Grants, denies, or resets one permission for the pinned app, which need
|
|
486
|
+
* not be running. A change terminates a running app on iOS, a revoke one
|
|
487
|
+
* on Android, so it goes before the `app.open()` a test starts with.
|
|
488
|
+
*/
|
|
489
|
+
setPermission(permission: DevicePermission, state: PermissionState): Promise<void>;
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
await device.setPermission('location', 'deny');
|
|
494
|
+
await app.open();
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
- `permission` is a [`DevicePermission`](#device-fixture); `state` is
|
|
498
|
+
`grant`, `deny`, or `reset`.
|
|
499
|
+
|
|
500
|
+
#### `setLocation`
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
/** Sets the simulated location, switching location services on first on Android. */
|
|
504
|
+
setLocation(coordinates: { latitude: number; longitude: number }): Promise<void>;
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
await device.setLocation({ latitude: 52.23, longitude: 21.01 });
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
#### `clearLocation`
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
/** Turns simulated location off; on Android that is location services off, until the next `setLocation`. */
|
|
515
|
+
clearLocation(): Promise<void>;
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
await device.clearLocation();
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
#### `setAppearance`
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
/** Sets the system appearance. */
|
|
526
|
+
setAppearance(mode: 'light' | 'dark'): Promise<void>;
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
await device.setAppearance('dark');
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### `setOrientation`
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
/** Rotates the device. */
|
|
537
|
+
setOrientation(orientation: DeviceOrientation): Promise<void>;
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
await device.setOrientation('landscape-left');
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
- `'portrait'`, `'portrait-upside-down'`, `'landscape-left'`, or
|
|
545
|
+
`'landscape-right'`.
|
|
546
|
+
|
|
547
|
+
#### `fold`
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
/**
|
|
551
|
+
* Puts a foldable simulator or emulator in a hinge pose. On the iPhone Duo
|
|
552
|
+
* it resolves once agent-device reads the angle back from CoreDevice at
|
|
553
|
+
* that pose, typically 10 to 16 seconds later; `closed` lights the outer
|
|
554
|
+
* display, `half-open` (a 130° book) and `open` light the inner one. On a
|
|
555
|
+
* foldable Android emulator it sets the emulator console posture and
|
|
556
|
+
* resolves once the guest commits the matching device state, within a few
|
|
557
|
+
* seconds; `half-open` is the profile's own angle there (90° on the Pixel
|
|
558
|
+
* folds), and a fold to the cover display dismisses the lock screen it
|
|
559
|
+
* raises. Either way the app window changes size, so the next observation
|
|
560
|
+
* reads a new screen. Any device without a hinge is `UNSUPPORTED_CAPABILITY`.
|
|
561
|
+
*/
|
|
562
|
+
fold(pose: FoldPose): Promise<void>;
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
await device.fold('half-open');
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
| Throws | When |
|
|
570
|
+
| --- | --- |
|
|
571
|
+
| `UNSUPPORTED_CAPABILITY` | The device has no hinge. |
|
|
572
|
+
|
|
573
|
+
#### `setBiometrics`
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
/** Simulates one biometric attempt on the open app. */
|
|
577
|
+
setBiometrics(sensor: BiometricSensor, result: 'match' | 'nonmatch'): Promise<void>;
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
await device.setBiometrics('faceid', 'nonmatch');
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
- `sensor` is `'faceid'`, `'touchid'`, or `'fingerprint'`.
|
|
585
|
+
|
|
586
|
+
#### `enrollBiometrics`
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
/** Enrolls or unenrolls the simulator's Face ID or Touch ID. */
|
|
590
|
+
enrollBiometrics(sensor: 'faceid' | 'touchid', enrolled: boolean): Promise<void>;
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
await device.enrollBiometrics('touchid', true);
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
#### `installApp`
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
/**
|
|
601
|
+
* Installs a build (an iOS `.app` bundle or an Android `.apk`, resolved
|
|
602
|
+
* against the project root) on the device; without a path, the target's
|
|
603
|
+
* `app.appPath`. The engine installs nothing on its own, so a suite that runs
|
|
604
|
+
* against a build calls this once per device, in a fixture or a test.
|
|
605
|
+
* `reinstall: true` removes the app first so it starts with no data; a
|
|
606
|
+
* plain install replaces the binary and keeps its data. Resolves to the
|
|
607
|
+
* identity to `openApp` it by, which becomes the app `app.open()` launches
|
|
608
|
+
* when the target's build is installed and no `app.bundleId` is pinned.
|
|
609
|
+
*/
|
|
610
|
+
installApp(appPath?: string, options?: InstallAppOptions): Promise<InstalledApp>;
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
await device.installApp(); // the target's app.appPath
|
|
615
|
+
const { app } = await device.installApp('./build/MyApp.app', { reinstall: true });
|
|
616
|
+
await device.openApp(app);
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
- Use it for the run's build, or for another build on an upgrade path.
|
|
620
|
+
- The resolved id is the bundle id or package agent-device read from the
|
|
621
|
+
build, else the `app` passed in.
|
|
622
|
+
|
|
623
|
+
| Throws | When |
|
|
624
|
+
| --- | --- |
|
|
625
|
+
| `ENGINE_FAILURE` | agent-device reported no id and no `app` was passed, instead of a file path posing as an id. See [Troubleshooting](/mobile#troubleshooting). |
|
|
626
|
+
|
|
627
|
+
#### `openApp`
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
/**
|
|
631
|
+
* Brings an app to the foreground by bundle id, package, or display name;
|
|
632
|
+
* `relaunch` restarts it fresh. `launchArguments` and `permissions` apply
|
|
633
|
+
* to this launch alone; the target's configured ones apply to a `relaunch`
|
|
634
|
+
* of the pinned app, and to no foreground-only open. A link is
|
|
635
|
+
* `INVALID_ARGUMENT` (`file:`, `data:`, `javascript:`, `view-source:`,
|
|
636
|
+
* `blob:`, and `filesystem:` are `POLICY_DENIED`): `openLink` opens one.
|
|
637
|
+
*/
|
|
638
|
+
openApp(app: string, options?: OpenAppOptions): Promise<void>;
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
await device.openApp('Settings');
|
|
643
|
+
await device.openApp('com.example.app', { relaunch: true, permissions: { camera: 'grant' } });
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
- A link is refused before the device sees it, since agent-device opens any
|
|
647
|
+
`scheme:rest` string as a URL.
|
|
648
|
+
|
|
649
|
+
| Throws | When |
|
|
650
|
+
| --- | --- |
|
|
651
|
+
| `POLICY_DENIED` | A `file:`, `data:`, `javascript:`, `view-source:`, `blob:`, or `filesystem:` link. |
|
|
652
|
+
| `INVALID_ARGUMENT` | Any other link, with a message pointing at `openLink`; or an invalid permission name or state (see [Launch arguments and permissions](#launch-arguments-and-permissions)). |
|
|
653
|
+
|
|
654
|
+
#### `openLink`
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
/**
|
|
658
|
+
* Opens a deep link (`myapp://orders/42`) or a web link (`https://...`)
|
|
659
|
+
* into `app` (default: the pinned app), which the session observes
|
|
660
|
+
* afterwards. iOS launches the app for a web link and then opens the URL;
|
|
661
|
+
* a web link the app does not claim goes to Safari, and the next
|
|
662
|
+
* observation brings the app back. Android starts the link on that
|
|
663
|
+
* package. Without an app, Android lets the OS route the link; iOS needs
|
|
664
|
+
* one (`INVALID_ARGUMENT` without). `file:`, `data:`, `javascript:`,
|
|
665
|
+
* `view-source:`, `blob:`, and `filesystem:` links are `POLICY_DENIED`.
|
|
666
|
+
*/
|
|
667
|
+
openLink(url: string, options?: { app?: string }): Promise<void>;
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
await device.openLink('myapp://orders/42');
|
|
672
|
+
await expect(screen.getByText('Order #42')).toBeVisible();
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
- Use it for magic-link sign-in and deep-link routes.
|
|
676
|
+
- Without an app, Android routes the link to a verified app link owner or
|
|
677
|
+
the browser, and the session follows the package that took it.
|
|
678
|
+
- A device link may use any app's custom scheme, so the rule refuses the
|
|
679
|
+
schemes below rather than admitting http(s) alone as the web does.
|
|
680
|
+
- The report shows the link cut before its query and fragment, where a magic
|
|
681
|
+
link carries its token.
|
|
682
|
+
- The replay cache anchors on the location the link lands on; see
|
|
683
|
+
[Replay cache on a device](#replay-cache-on-a-device).
|
|
684
|
+
|
|
685
|
+
| Throws | When |
|
|
686
|
+
| --- | --- |
|
|
687
|
+
| `POLICY_DENIED` | A `file:`, `data:`, `javascript:`, `view-source:`, `blob:`, or `filesystem:` link. |
|
|
688
|
+
| `INVALID_ARGUMENT` | No app on iOS: an open bound to none leaves no app session to observe. |
|
|
689
|
+
| `INVALID_ARGUMENT` | A string that does not parse as an absolute URL. The message names no part of the input, which may carry a magic-link token in its query, path, or userinfo. |
|
|
690
|
+
|
|
691
|
+
#### `closeApp`
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
/**
|
|
695
|
+
* Terminates the session's app and ends the session, so the next
|
|
696
|
+
* `openApp` launches it fresh. Until then the screen is unobservable and
|
|
697
|
+
* `foregroundApp` is `APP_NOT_OPEN`.
|
|
698
|
+
*/
|
|
699
|
+
closeApp(): Promise<void>;
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
await device.closeApp();
|
|
704
|
+
await device.openApp('com.example.app'); // launches fresh, on its first screen
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
- A screen query before the next `openApp` is `APP_NOT_OPEN` too.
|
|
708
|
+
|
|
709
|
+
#### `clearKeychain`
|
|
710
|
+
|
|
711
|
+
```ts
|
|
712
|
+
/**
|
|
713
|
+
* Resets the simulator's keychain, every app's, since simctl has no
|
|
714
|
+
* per-app reset: what `app.clearState()` leaves behind (a session token, a
|
|
715
|
+
* Firebase sign-in). iOS simulator only; Android is
|
|
716
|
+
* `UNSUPPORTED_CAPABILITY`. Launch the app afterwards to see it signed out.
|
|
717
|
+
*/
|
|
718
|
+
clearKeychain(): Promise<void>;
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
await device.clearKeychain();
|
|
723
|
+
await app.open(); // see it signed out
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
| Throws | When |
|
|
727
|
+
| --- | --- |
|
|
728
|
+
| `UNSUPPORTED_CAPABILITY` | On Android, before any device command. |
|
|
729
|
+
|
|
730
|
+
#### `foregroundApp`
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
/**
|
|
734
|
+
* The app the session is on. iOS answers from the session, not the
|
|
735
|
+
* device, so it names the app the session opened even after `home()`;
|
|
736
|
+
* Android reads the device's foreground activity. `APP_NOT_OPEN` once
|
|
737
|
+
* `closeApp` ended the session.
|
|
738
|
+
*/
|
|
739
|
+
foregroundApp(): Promise<ForegroundApp>;
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
```ts
|
|
743
|
+
const { name, bundleId } = await device.foregroundApp();
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
- On iOS, agent-device's `appstate` has no device-level answer.
|
|
747
|
+
|
|
748
|
+
| Throws | When |
|
|
749
|
+
| --- | --- |
|
|
750
|
+
| `APP_NOT_OPEN` | `closeApp` ended the session. |
|
|
751
|
+
|
|
752
|
+
#### `home`
|
|
753
|
+
|
|
754
|
+
```ts
|
|
755
|
+
/** Sends the device to its home screen. */
|
|
756
|
+
home(): Promise<void>;
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
await device.home();
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
#### `back`
|
|
764
|
+
|
|
765
|
+
```ts
|
|
766
|
+
/** Navigates back once (in-app back). */
|
|
767
|
+
back(): Promise<void>;
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
```ts
|
|
771
|
+
await device.back();
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
#### `alert`
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
/** Accepts or dismisses the visible system alert. */
|
|
778
|
+
alert(action: 'accept' | 'dismiss'): Promise<void>;
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
await device.alert('accept');
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
#### `dismissKeyboard`
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
/**
|
|
789
|
+
* Dismisses the soft keyboard: the keyboard's own dismiss key where it has
|
|
790
|
+
* one (Android, iPad), else the way a user does it on an iPhone, a short
|
|
791
|
+
* drag at the centre of the screen that a scroll view turns into a
|
|
792
|
+
* dismissal, horizontal first and vertical second, stopping as soon as the
|
|
793
|
+
* keyboard is gone. The simulator's "Speed up your typing" tip goes through
|
|
794
|
+
* its Continue button first. A keyboard that outlives both drags fails with
|
|
795
|
+
* `UNSUPPORTED_CAPABILITY`; the message names the alternatives (the app's
|
|
796
|
+
* own Done or close control, or pressing Enter on the field).
|
|
797
|
+
*/
|
|
798
|
+
dismissKeyboard(): Promise<void>;
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
await device.dismissKeyboard();
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
| Throws | When |
|
|
806
|
+
| --- | --- |
|
|
807
|
+
| `UNSUPPORTED_CAPABILITY` | The keyboard outlives both drags. |
|
|
808
|
+
|
|
809
|
+
#### `clipboard`
|
|
810
|
+
|
|
811
|
+
```ts
|
|
812
|
+
/** Reads the clipboard. */
|
|
813
|
+
clipboard(): Promise<string>;
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
```ts
|
|
817
|
+
const copied = await device.clipboard();
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
#### `setClipboard`
|
|
821
|
+
|
|
822
|
+
```ts
|
|
823
|
+
/** Writes the clipboard. */
|
|
824
|
+
setClipboard(text: string): Promise<void>;
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
```ts
|
|
828
|
+
await device.setClipboard('SAVE20');
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Agent tools
|
|
832
|
+
|
|
833
|
+
`mobileTools(...engines)` from `@e2e-dev/mobile/tools` adds device tools to
|
|
834
|
+
an agent's `tools`.
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
tools: mobileTools(iphone, pixel),
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
- It takes at least one engine. Pass all device engines in one call; the
|
|
841
|
+
tools use the current attempt's engine.
|
|
842
|
+
|
|
843
|
+
| Tool | Replay | Purpose |
|
|
844
|
+
| --- | --- | --- |
|
|
845
|
+
| `open_app` | gap | Bring an app to the foreground by id, optionally relaunched. A link is refused, as it is for `device.openApp`. |
|
|
846
|
+
| `swipe` | gap | Free-form swipe between two points in logical pixels. |
|
|
847
|
+
| `alert` | gap | Accept or dismiss a system alert; prefer a listed button, whose tap replays. |
|
|
848
|
+
|
|
849
|
+
The grammar verbs need no tool pack. The engine's declared actions give the
|
|
850
|
+
agent:
|
|
851
|
+
|
|
852
|
+
- `tap`, `double_tap`, `long_press`, `hover`, `type`, `press`, `check`,
|
|
853
|
+
`scroll`, `drag`, `scroll_to` by `text` (paging the list until a row
|
|
854
|
+
reading it is drawn), `back`, and `dismiss_keyboard`.
|
|
855
|
+
- The pixel tools `screenshot`, `tap_at`, `hover_at` (on a listed control
|
|
856
|
+
only), `type_at`, and `press_at`. Point actions can tap controls absent
|
|
857
|
+
from the accessibility tree.
|
|
858
|
+
- `type_secret`, when the step declares a secret.
|
|
859
|
+
|
|
860
|
+
There is no `right_click`, `select`, `scroll_to` by node id, `upload`, or
|
|
861
|
+
`navigate` on a device, and `select_at` is not offered.
|
|
862
|
+
|
|
863
|
+
<Note>
|
|
864
|
+
After a secret fill the agent offers neither `screenshot` nor `tap_at`,
|
|
865
|
+
including in later members of the same serial group. Pixels whose masking
|
|
866
|
+
the engine cannot prove are withheld as `MASKING_UNPROVEN`. An accessibility
|
|
867
|
+
capture failure still stops the observation. Device screenshot masks depend
|
|
868
|
+
on secure-field bounds from that capture, so without them the engine cannot
|
|
869
|
+
prove a fallback screenshot safe.
|
|
870
|
+
</Note>
|
|
871
|
+
|
|
213
872
|
## Replay cache on a device
|
|
214
873
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`
|
|
218
|
-
|
|
219
|
-
|
|
874
|
+
The replay cache anchors on the [`location`](#capabilities) a device reports,
|
|
875
|
+
`<bundle id> / <screen title>`. A step recorded on
|
|
876
|
+
`com.apple.Preferences / General` replays only when Settings is on General
|
|
877
|
+
again.
|
|
878
|
+
|
|
879
|
+
```ts
|
|
880
|
+
await app.open();
|
|
881
|
+
await screen.getByText('General').tap();
|
|
882
|
+
await agent.act('Open About and read the iOS version');
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
- A mutating project-tool call such as `open_app` ends a replay.
|
|
886
|
+
- Declare `app.bundleId` and call `app.open()` first, so steps start on the
|
|
887
|
+
same screen.
|
|
888
|
+
- Switch apps through the `device` fixture between steps rather than inside
|
|
889
|
+
one.
|
|
890
|
+
- A step recorded after [`device.openLink`](#openlink) replays when the link
|
|
891
|
+
lands on the same screen again.
|
|
892
|
+
|
|
893
|
+
## Types
|
|
220
894
|
|
|
221
|
-
|
|
895
|
+
### Engine options
|
|
896
|
+
|
|
897
|
+
Each `MobileOptions` member is declared, with its JSDoc, in its
|
|
898
|
+
[Engine](#engine) entry.
|
|
222
899
|
|
|
223
900
|
```ts
|
|
224
|
-
/**
|
|
225
|
-
export
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
* cuts both. On iOS `offline` is `UNSUPPORTED_CAPABILITY`, since a
|
|
236
|
-
* simulator shares the Mac's network and simctl can only repaint its status
|
|
237
|
-
* bar; `online` sends nothing there, so a cleanup hook shared with Android
|
|
238
|
-
* passes.
|
|
239
|
-
*/
|
|
240
|
-
setNetwork(state: 'online' | 'offline'): Promise<void>;
|
|
241
|
-
/**
|
|
242
|
-
* Switches an Android device's airplane mode, Wi-Fi and cellular together;
|
|
243
|
-
* Android 11 or newer. On iOS, as for `setNetwork`, `true` is
|
|
244
|
-
* `UNSUPPORTED_CAPABILITY` and `false` sends nothing.
|
|
245
|
-
*/
|
|
246
|
-
setAirplaneMode(enabled: boolean): Promise<void>;
|
|
247
|
-
/**
|
|
248
|
-
* Grants, denies, or resets one permission for the pinned app, which need
|
|
249
|
-
* not be running. A change terminates a running app on iOS, a revoke one
|
|
250
|
-
* on Android, so it goes before the `app.open()` a test starts with.
|
|
251
|
-
*/
|
|
252
|
-
setPermission(permission: DevicePermission, state: PermissionState): Promise<void>;
|
|
253
|
-
/** Sets the simulated location, switching location services on first on Android. */
|
|
254
|
-
setLocation(coordinates: { latitude: number; longitude: number }): Promise<void>;
|
|
255
|
-
/** Turns simulated location off; on Android that is location services off, until the next `setLocation`. */
|
|
256
|
-
clearLocation(): Promise<void>;
|
|
257
|
-
/** Sets the system appearance. */
|
|
258
|
-
setAppearance(mode: 'light' | 'dark'): Promise<void>;
|
|
259
|
-
/** Rotates the device. */
|
|
260
|
-
setOrientation(orientation: DeviceOrientation): Promise<void>;
|
|
261
|
-
/**
|
|
262
|
-
* Puts a foldable simulator or emulator in a hinge pose. On the iPhone Duo
|
|
263
|
-
* it resolves once agent-device reads the angle back from CoreDevice at
|
|
264
|
-
* that pose, typically 10 to 16 seconds later; `closed` lights the outer
|
|
265
|
-
* display, `half-open` (a 130° book) and `open` light the inner one. On a
|
|
266
|
-
* foldable Android emulator it sets the emulator console posture and
|
|
267
|
-
* resolves once the guest commits the matching device state, within a few
|
|
268
|
-
* seconds; `half-open` is the profile's own angle there (90° on the Pixel
|
|
269
|
-
* folds), and a fold to the cover display dismisses the lock screen it
|
|
270
|
-
* raises. Either way the app window changes size, so the next observation
|
|
271
|
-
* reads a new screen. Any device without a hinge is `UNSUPPORTED_CAPABILITY`.
|
|
272
|
-
*/
|
|
273
|
-
fold(pose: FoldPose): Promise<void>;
|
|
274
|
-
/** Simulates one biometric attempt on the open app. */
|
|
275
|
-
setBiometrics(sensor: BiometricSensor, result: 'match' | 'nonmatch'): Promise<void>;
|
|
276
|
-
/** Enrolls or unenrolls the simulator's Face ID or Touch ID. */
|
|
277
|
-
enrollBiometrics(sensor: 'faceid' | 'touchid', enrolled: boolean): Promise<void>;
|
|
278
|
-
/**
|
|
279
|
-
* Installs a build (an iOS `.app` bundle or an Android `.apk`, resolved
|
|
280
|
-
* against the project root) on the device; without a path, the target's
|
|
281
|
-
* `app.appPath`. The engine installs nothing on its own, so a suite that runs
|
|
282
|
-
* against a build calls this once per device, in a fixture or a test.
|
|
283
|
-
* `reinstall: true` removes the app first so it starts with no data; a
|
|
284
|
-
* plain install replaces the binary and keeps its data. Resolves to the
|
|
285
|
-
* identity to `openApp` it by, which becomes the app `app.open()` launches
|
|
286
|
-
* when the target's build is installed and no `app.bundleId` is pinned.
|
|
287
|
-
*/
|
|
288
|
-
installApp(appPath?: string, options?: InstallAppOptions): Promise<InstalledApp>;
|
|
289
|
-
/**
|
|
290
|
-
* Brings an app to the foreground by bundle id, package, or display name;
|
|
291
|
-
* `relaunch` restarts it fresh. `launchArguments` and `permissions` apply
|
|
292
|
-
* to this launch alone; the target's configured ones apply to a `relaunch`
|
|
293
|
-
* of the pinned app, and to no foreground-only open. A link is
|
|
294
|
-
* `INVALID_ARGUMENT` (`file:`, `data:`, `javascript:`, `view-source:`,
|
|
295
|
-
* `blob:`, and `filesystem:` are `POLICY_DENIED`): `openLink` opens one.
|
|
296
|
-
*/
|
|
297
|
-
openApp(app: string, options?: OpenAppOptions): Promise<void>;
|
|
298
|
-
/**
|
|
299
|
-
* Opens a deep link (`myapp://orders/42`) or a web link (`https://...`)
|
|
300
|
-
* into `app` (default: the pinned app), which the session observes
|
|
301
|
-
* afterwards. iOS launches the app for a web link and then opens the URL;
|
|
302
|
-
* a web link the app does not claim goes to Safari, and the next
|
|
303
|
-
* observation brings the app back. Android starts the link on that
|
|
304
|
-
* package. Without an app, Android lets the OS route the link; iOS needs
|
|
305
|
-
* one (`INVALID_ARGUMENT` without). `file:`, `data:`, `javascript:`,
|
|
306
|
-
* `view-source:`, `blob:`, and `filesystem:` links are `POLICY_DENIED`.
|
|
307
|
-
*/
|
|
308
|
-
openLink(url: string, options?: { app?: string }): Promise<void>;
|
|
309
|
-
/**
|
|
310
|
-
* Terminates the session's app and ends the session, so the next
|
|
311
|
-
* `openApp` launches it fresh. Until then the screen is unobservable and
|
|
312
|
-
* `foregroundApp` is `APP_NOT_OPEN`.
|
|
313
|
-
*/
|
|
314
|
-
closeApp(): Promise<void>;
|
|
315
|
-
/**
|
|
316
|
-
* Resets the simulator's keychain, every app's, since simctl has no
|
|
317
|
-
* per-app reset: what `app.clearState()` leaves behind (a session token, a
|
|
318
|
-
* Firebase sign-in). iOS simulator only; Android is
|
|
319
|
-
* `UNSUPPORTED_CAPABILITY`. Launch the app afterwards to see it signed out.
|
|
320
|
-
*/
|
|
321
|
-
clearKeychain(): Promise<void>;
|
|
322
|
-
/**
|
|
323
|
-
* The app the session is on. iOS answers from the session, not the
|
|
324
|
-
* device, so it names the app the session opened even after `home()`;
|
|
325
|
-
* Android reads the device's foreground activity. `APP_NOT_OPEN` once
|
|
326
|
-
* `closeApp` ended the session.
|
|
327
|
-
*/
|
|
328
|
-
foregroundApp(): Promise<ForegroundApp>;
|
|
329
|
-
/** Sends the device to its home screen. */
|
|
330
|
-
home(): Promise<void>;
|
|
331
|
-
/** Navigates back once (in-app back). */
|
|
332
|
-
back(): Promise<void>;
|
|
333
|
-
/** Accepts or dismisses the visible system alert. */
|
|
334
|
-
alert(action: 'accept' | 'dismiss'): Promise<void>;
|
|
335
|
-
/**
|
|
336
|
-
* Dismisses the soft keyboard: the keyboard's own dismiss key where it has
|
|
337
|
-
* one (Android, iPad), else the way a user does it on an iPhone, a short
|
|
338
|
-
* drag at the centre of the screen that a scroll view turns into a
|
|
339
|
-
* dismissal, horizontal first and vertical second, stopping as soon as the
|
|
340
|
-
* keyboard is gone. The simulator's "Speed up your typing" tip goes through
|
|
341
|
-
* its Continue button first. A keyboard that outlives both drags fails with
|
|
342
|
-
* `UNSUPPORTED_CAPABILITY`; the message names the alternatives (the app's
|
|
343
|
-
* own Done or close control, or pressing Enter on the field).
|
|
344
|
-
*/
|
|
345
|
-
dismissKeyboard(): Promise<void>;
|
|
346
|
-
/** Reads the clipboard. */
|
|
347
|
-
clipboard(): Promise<string>;
|
|
348
|
-
/** Writes the clipboard. */
|
|
349
|
-
setClipboard(text: string): Promise<void>;
|
|
350
|
-
}
|
|
901
|
+
/** A permission's state: held, refused, or not asked for yet, so the OS asks again. */
|
|
902
|
+
export type PermissionState = 'grant' | 'deny' | 'reset';
|
|
903
|
+
|
|
904
|
+
/** Permissions an app holds when it launches, by name. */
|
|
905
|
+
export type LaunchPermissions = Readonly<Partial<Record<DevicePermission, PermissionState>>>;
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### Device fixture
|
|
909
|
+
|
|
910
|
+
Each `Device` member is declared, with its JSDoc, in its [Device](#device)
|
|
911
|
+
entry.
|
|
351
912
|
|
|
913
|
+
```ts
|
|
352
914
|
/** How `device.openApp` launches an app. */
|
|
353
915
|
export interface OpenAppOptions {
|
|
354
916
|
/** Terminate the app first, so it starts fresh; without it a running app is brought forward as it is. */
|
|
@@ -415,139 +977,18 @@ export interface ForegroundApp {
|
|
|
415
977
|
export type BiometricSensor = 'faceid' | 'touchid' | 'fingerprint';
|
|
416
978
|
```
|
|
417
979
|
|
|
418
|
-
|
|
419
|
-
`config.actionTimeout`. A lost session is `APP_NOT_OPEN`, that budget
|
|
420
|
-
expiring is `ACTION_FAILED`, a command the platform cannot run is
|
|
421
|
-
`UNSUPPORTED_CAPABILITY`, anything else `ENGINE_FAILURE` with agent-device's
|
|
422
|
-
message: a timeout the device itself reports (a boot, the runner connecting)
|
|
423
|
-
keeps its text and hint there, and a busy or wedged iOS automation runner
|
|
424
|
-
names itself and the recovery (see
|
|
425
|
-
[Troubleshooting](/mobile#troubleshooting)).
|
|
426
|
-
|
|
427
|
-
`back`, `home`, `alert`, `dismissKeyboard`, `setOrientation`, and `fold`
|
|
428
|
-
change the screen, so they count as actions for the `transition` budget the way a tap
|
|
429
|
-
does: a control that arrives with the screen they reveal waits out the budget
|
|
430
|
-
before the next action lands on it.
|
|
431
|
-
|
|
432
|
-
`closeApp` terminates the app the session is on and ends the session, so
|
|
433
|
-
`openApp` after it launches the app fresh, on its first screen; a screen
|
|
434
|
-
query or `foregroundApp` in between is `APP_NOT_OPEN`. `foregroundApp` on
|
|
435
|
-
iOS reads the session, not the device: agent-device's `appstate` has no
|
|
436
|
-
device-level answer there, so after `home()` it still names the app the
|
|
437
|
-
session opened. Android reads the device's foreground activity.
|
|
438
|
-
|
|
439
|
-
`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
|
|
440
|
-
install keeps the app's data; `reinstall: true` removes the app named by
|
|
441
|
-
`app` (default: the pinned app) first. It resolves to the id to `openApp`
|
|
442
|
-
the build by: the bundle id or package agent-device read from the build, else
|
|
443
|
-
the `app` passed in. With no reported id and no `app`, the install is
|
|
444
|
-
`ENGINE_FAILURE` instead of a file path posing as an id; see
|
|
445
|
-
[Troubleshooting](/mobile#troubleshooting).
|
|
446
|
-
|
|
447
|
-
```ts
|
|
448
|
-
const { app } = await device.installApp('./build/MyApp.app', { reinstall: true });
|
|
449
|
-
await device.openApp(app);
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
`clearKeychain` resets the simulator's keychain, which `app.clearState()`
|
|
453
|
-
leaves alone: simctl resets the whole simulator's, so every app on it loses
|
|
454
|
-
its keychain-backed credentials. iOS only; on Android it is
|
|
455
|
-
`UNSUPPORTED_CAPABILITY` before any device command. Launch the app afterwards
|
|
456
|
-
to see it signed out.
|
|
457
|
-
|
|
458
|
-
`openApp` brings an app to the foreground by bundle id, package, or display
|
|
459
|
-
name (`Settings`); `relaunch: true` restarts it fresh, and `launchArguments`
|
|
460
|
-
and `permissions` apply to that launch alone (the target's own apply to a
|
|
461
|
-
`relaunch` of the pinned app). It takes no link:
|
|
462
|
-
agent-device opens any `scheme:rest` string as a URL, so a link is refused
|
|
463
|
-
before the device sees it. A `file:`, `data:`, `javascript:`,
|
|
464
|
-
`view-source:`, `blob:`, or `filesystem:` link is `POLICY_DENIED`; any other
|
|
465
|
-
link is `INVALID_ARGUMENT` pointing at `openLink`.
|
|
466
|
-
|
|
467
|
-
`openLink` opens a deep link or a web link into `app` (default: the pinned
|
|
468
|
-
app), for magic-link sign-in and deep-link routes, and the session observes
|
|
469
|
-
that app afterwards. iOS launches the app for a web link and then opens the
|
|
470
|
-
URL; a web link the app does not claim goes to Safari, and the next
|
|
471
|
-
observation brings the app back. Android starts the link on that package.
|
|
472
|
-
Without an app, Android lets the OS route the link, to a verified app link
|
|
473
|
-
owner or the browser, and the session follows the package that took it. iOS
|
|
474
|
-
needs an app: an open bound to none leaves no app session to observe, so it
|
|
475
|
-
is `INVALID_ARGUMENT`. `file:`, `data:`, `javascript:`, `view-source:`,
|
|
476
|
-
`blob:`, and `filesystem:` links are `POLICY_DENIED`. A device link may use
|
|
477
|
-
any app's custom scheme, so the rule refuses these schemes rather than
|
|
478
|
-
admitting http(s) alone as the web does. A string that does not parse as an
|
|
479
|
-
absolute URL is `INVALID_ARGUMENT` too, and its message names no part of the
|
|
480
|
-
input, which may carry a magic-link token in its query, path, or userinfo.
|
|
481
|
-
The report shows the link cut before its
|
|
482
|
-
query and fragment, where a magic link carries its token. The replay cache
|
|
483
|
-
anchors on the location the link lands on: a step recorded after `openLink`
|
|
484
|
-
replays when the link lands on the same screen again.
|
|
485
|
-
|
|
486
|
-
```ts
|
|
487
|
-
await device.openLink('myapp://orders/42');
|
|
488
|
-
await expect(screen.getByText('Order #42')).toBeVisible();
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
Use `device.locator` for platform-specific selectors that `screen` queries
|
|
492
|
-
cannot express.
|
|
493
|
-
|
|
494
|
-
```ts
|
|
495
|
-
await expect(device.locator('role=navigation-bar id=About')).toBeVisible();
|
|
496
|
-
await expect(device.locator('role=text label="iOS Version"')).toHaveText('iOS Version');
|
|
497
|
-
await device.locator('id=SW_VERSION_SPECIFIER role=button').tap();
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
Selector terms and their rules are agent-device's (the ones `agent-device
|
|
501
|
-
find ... list` applies): `id`, `role`, `text`, `label`, `value`, `appname`,
|
|
502
|
-
`windowtitle`, and the flags `visible`, `hidden`, `editable`, `selected`,
|
|
503
|
-
`focused`, `enabled`, `hittable`. Strictness stays the locator's: an action
|
|
504
|
-
on a selector that matches several nodes fails, where an agent-device action
|
|
505
|
-
skips an ambiguous `||` alternative.
|
|
506
|
-
|
|
507
|
-
- `role` takes agent-device's kind, the role `agent-device snapshot` prints:
|
|
508
|
-
`button`, `text`, `text-field`, `navigation-bar`, `scroll-area`, `switch`,
|
|
509
|
-
`image`. It is not the `getByRole` role (`textbox`).
|
|
510
|
-
- Text terms compare case-insensitively after collapsing whitespace.
|
|
511
|
-
- `visible` means the node is hittable or has a non-empty frame;
|
|
512
|
-
`hidden` is its opposite.
|
|
513
|
-
- `text` reads the first non-empty label, value, or identifier.
|
|
514
|
-
|
|
515
|
-
## Agent tools
|
|
516
|
-
|
|
517
|
-
`mobileTools(...engines)` adds device tools to an agent's `tools`. It takes at
|
|
518
|
-
least one engine; pass all device engines in one call, and the tools use the
|
|
519
|
-
current attempt's engine.
|
|
520
|
-
|
|
521
|
-
| Tool | Replay | Purpose |
|
|
522
|
-
| --- | --- | --- |
|
|
523
|
-
| `open_app` | gap | Bring an app to the foreground by id, optionally relaunched. A link is refused, as it is for `device.openApp`. |
|
|
524
|
-
| `swipe` | gap | Free-form swipe between two points in logical pixels. |
|
|
525
|
-
| `alert` | gap | Accept or dismiss a system alert; prefer a listed button, whose tap replays. |
|
|
526
|
-
|
|
527
|
-
The grammar verbs need no tool pack: the engine's declared actions give the
|
|
528
|
-
agent `tap`, `double_tap`, `long_press`, `hover`, `type`, `press`, `check`,
|
|
529
|
-
`scroll`, `drag`, `scroll_to` by `text` (paging the list until a row reading it
|
|
530
|
-
is drawn), `back`, and `dismiss_keyboard`; there is no `right_click`,
|
|
531
|
-
`select`, `scroll_to` by node id, `upload`, or `navigate` on a device. The
|
|
532
|
-
pixel tools are `screenshot`, `tap_at`, `hover_at` (on a listed control only),
|
|
533
|
-
`type_at`, and `press_at`; `select_at` is not offered. Point actions can tap
|
|
534
|
-
controls absent from the accessibility tree. `type_secret` is offered when
|
|
535
|
-
the step declares a secret.
|
|
536
|
-
|
|
537
|
-
After a secret fill the agent offers neither `screenshot` nor `tap_at`,
|
|
538
|
-
including in later members of the same serial group. Pixels whose masking the
|
|
539
|
-
engine cannot prove are withheld as `MASKING_UNPROVEN`.
|
|
540
|
-
|
|
541
|
-
An accessibility capture failure still stops the observation. Device
|
|
542
|
-
screenshot masks depend on secure-field bounds from that capture, so the
|
|
543
|
-
engine cannot prove a fallback screenshot safe when those bounds are missing.
|
|
544
|
-
|
|
545
|
-
## Exported types
|
|
980
|
+
### Exported types
|
|
546
981
|
|
|
547
982
|
`MobileOptions`, `MobilePlatform`, `DevicePermission`, `LaunchPermissions`,
|
|
548
983
|
`PermissionState`, `DeviceClientConfig`, `DeviceConnection`, `DeviceDaemon`,
|
|
549
984
|
`DeviceProvider`, `DeviceLease`, `DeviceReleaseContext`, `DeviceRequest`,
|
|
550
985
|
`Device`, `DeviceOrientation`, `BiometricSensor`, `ForegroundApp`,
|
|
551
|
-
`InstallAppOptions`, `InstalledApp`, and `OpenAppOptions` are exported from
|
|
552
|
-
`@e2e-dev/mobile`. The runtime exports are `mobile
|
|
553
|
-
`
|
|
986
|
+
`FoldPose`, `InstallAppOptions`, `InstalledApp`, and `OpenAppOptions` are exported from
|
|
987
|
+
`@e2e-dev/mobile`. The runtime exports are `mobile`, and `test`, `describe`,
|
|
988
|
+
`beforeEach`, `afterEach`, `beforeAll`, and `afterAll`, typed with the `device`
|
|
989
|
+
fixture; `mobileTools` comes from `@e2e-dev/mobile/tools`.
|
|
990
|
+
|
|
991
|
+
## Related
|
|
992
|
+
|
|
993
|
+
The [Mobile](/mobile) guide covers setup, hosted devices, and troubleshooting.
|
|
994
|
+
The portable lifecycle methods are on [`app`](/reference/app).
|