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,185 @@
1
+ ---
2
+ title: Telemetry
3
+ description: What the CLI sends, what it never sends, and how to turn it off.
4
+ ---
5
+
6
+ The `e2e` CLI sends anonymous usage data so we can see which commands,
7
+ engines, and features are used and where runs fail. It is on by default.
8
+ Telemetry includes counts and version information, but no test content, app
9
+ content, or credentials.
10
+
11
+ ## Opt out
12
+
13
+ Any one of these turns it off:
14
+
15
+ <CodeGroup>
16
+ ```bash npm
17
+ npx e2e telemetry disable # saved in ~/.config/e2e/telemetry.json
18
+ export E2E_TELEMETRY_DISABLED=1 # for a shell or a CI job
19
+ export DO_NOT_TRACK=1 # the cross-tool convention
20
+ ```
21
+
22
+ ```bash pnpm
23
+ pnpm exec e2e telemetry disable # saved in ~/.config/e2e/telemetry.json
24
+ export E2E_TELEMETRY_DISABLED=1 # for a shell or a CI job
25
+ export DO_NOT_TRACK=1 # the cross-tool convention
26
+ ```
27
+
28
+ ```bash bun
29
+ bunx e2e telemetry disable # saved in ~/.config/e2e/telemetry.json
30
+ export E2E_TELEMETRY_DISABLED=1 # for a shell or a CI job
31
+ export DO_NOT_TRACK=1 # the cross-tool convention
32
+ ```
33
+ </CodeGroup>
34
+
35
+ `npx e2e telemetry` prints the status and, when it is off, why.
36
+ `enable` reverses a saved `disable`. Environment variables take precedence.
37
+ Running commands check the saved setting again before sending, so disabling
38
+ telemetry also applies to commands already running in another terminal.
39
+
40
+ ## See what would be sent
41
+
42
+ <CodeGroup>
43
+ ```bash npm
44
+ E2E_TELEMETRY_DEBUG=1 npx e2e run
45
+ ```
46
+
47
+ ```bash pnpm
48
+ E2E_TELEMETRY_DEBUG=1 pnpm exec e2e run
49
+ ```
50
+
51
+ ```bash bun
52
+ E2E_TELEMETRY_DEBUG=1 bunx e2e run
53
+ ```
54
+ </CodeGroup>
55
+
56
+ This prints the exact event payloads to stderr as `[telemetry] {...}` and
57
+ sends nothing.
58
+
59
+ ## What is sent
60
+
61
+ Each command sends a session event. `run` and `explore` also send a run event,
62
+ and `init` an init event. `--help` and `--version` send nothing, also when
63
+ asked on a command. Events go in one request when the command ends, with a
64
+ two-second deadline for all telemetry work.
65
+
66
+ Every event carries these properties.
67
+
68
+ Who ran it:
69
+
70
+ | Property | Value |
71
+ | --- | --- |
72
+ | `distinct_id` | A random id stored in the preferences file. CI uses `ci:<vendor>` instead, and a platform that runs e2e for its users uses `fleet:<name>` when it sets `E2E_TELEMETRY_FLEET` |
73
+ | `project_id` | A hash derived from the repository's root commit, shared across full clones. Outside git or in a shallow clone, a hash of the project path with a random local salt. `null` in CI when no root commit is available |
74
+ | `session_id` | A random id for this one invocation |
75
+ | `first_run`, `days_since_first_run` | Whether this command created the preferences file, and whole days since it was created. `null` in CI and in a fleet, where no machine persists, and for a preferences file from before the time was recorded |
76
+
77
+ Where it ran:
78
+
79
+ | Property | Value |
80
+ | --- | --- |
81
+ | `os`, `os_release`, `arch`, `cpu_count`, `memory_gb`, `docker`, `wsl`, `tty` | The machine class |
82
+ | `sandbox` | The microVM the kernel announces itself as (`firecracker`), else `null` |
83
+ | `ci`, `ci_name` | Whether `CI` is set, and the vendor when one announces itself (`github-actions`, `gitlab`, `circleci`, ...) |
84
+ | `fleet` | The value of `E2E_TELEMETRY_FLEET` when it is a plain token, `other` when it is not, `null` when unset |
85
+ | `coding_agent` | The coding agent whose shell ran the command, when one announces itself (`claude-code`, `cursor`, `codex`, ...) |
86
+
87
+ Versions:
88
+
89
+ | Property | Value |
90
+ | --- | --- |
91
+ | `e2e_version`, `package_manager` | The e2e version and the package manager that invoked the CLI |
92
+ | `runtime`, `runtime_version`, `node_version` | `node`, `bun`, or `deno`, and its version; `node_version` is Node's own version, or the Node compatibility version under Bun or Deno |
93
+ | `$lib`, `$lib_version`, `$process_person_profile`, `$geoip_disable` | `e2e`, its version, `false`, and `true`: PostHog keeps no person profile and derives no location |
94
+
95
+ `e2e_cli_session`, once per command:
96
+
97
+ | Property | Value |
98
+ | --- | --- |
99
+ | `command` | `init`, `run`, `list`, `cache ls`, `guide`, `telemetry`, ... |
100
+ | `flags` | The names of the flags given, such as `--headed` or `--tag`. Never their values |
101
+ | `exit_code`, `duration_ms` | How the command ended and how long it took |
102
+ | `error_code` | The runner code of the failure that ended the command before it could run (`CONFIG_NOT_FOUND`, `INVALID_CONFIG`, ...), `CLI_USAGE` for a flag or argument the CLI rejected, `null` otherwise. A code that is not an upper-case token is reported as `OTHER` |
103
+
104
+ `e2e_init_completed`, once per `e2e init`:
105
+
106
+ | Property | Value |
107
+ | --- | --- |
108
+ | `result` | `scaffolded`, `already-initialized`, `cancelled`, `install-failed`, `not-interactive`, or `invalid-project` |
109
+ | `yes`, `existing_config` | Whether `--yes` was given, and whether a config already existed so no engine or gateway was asked |
110
+ | `engine`, `gateway` | The ids of the engine and the model gateway chosen for a new config (`web`, `mobile`, `none`; `vercel`, `openrouter`, `openai-compatible`, `chatgpt`, `copilot`, `grok`, `none`), `null` when not asked. Never the endpoint typed for an OpenAI-compatible gateway |
111
+ | `skill`, `mcp`, `install` | Whether the agent skill, the MCP registration, and the dependency install were chosen |
112
+
113
+ `e2e_run_completed`, once per `e2e run` or `e2e explore`, built from the numbers in `report.json`:
114
+
115
+ | Property | Value |
116
+ | --- | --- |
117
+ | `status`, `exit_code`, `duration_ms` | The run's outcome |
118
+ | `flags` | The names of the flags given to the run command, never their values |
119
+ | `tests_discovered`, `tests_selected`, `tests_executed`, `tests_passed`, `tests_failed`, `tests_flaky`, `tests_skipped` | The summary counts |
120
+ | `attempts_total`, `tests_retried` | How many attempts the run made across tests and serial groups, and how many of them needed more than one |
121
+ | `targets`, `platforms`, `engines` | How many targets, on which platforms, and with which engines and versions, as your config declares them: `web`, `ios`, `web@0.6.1`, or your own engine's name and version. A name that is not a plain token of letters, digits, dots, dashes, and underscores is reported as `other` |
122
+ | `steps_total`, `steps_agent`, `steps_locator`, `steps_assertion`, `steps_screen`, `steps_app`, `steps_session`, `steps_resource` | Step counts by kind |
123
+ | `agent_steps_replayed`, `agent_steps_partial`, `agent_steps_missed`, `agent_steps_vision` | How the replay cache served the agent steps, and how many used pixel evidence |
124
+ | `agent_actions` | How often the agent took each action, by the runner's own names: `{"tap": 12, "typeText": 3, "scroll": 2}`. A project's own tools are counted together as `tool` and anything else as `other`; no other name is sent |
125
+ | `model_gateway`, `model_provider`, `model_id`, `model_calls`, `model_tokens`, `model_cached_tokens`, `estimated_cost_usd` | For the first model-backed step: the AI SDK provider that served it (`gateway` for the Vercel AI Gateway, `openrouter.chat`, `openai.chat`, ...), the vendor (the first segment of a `vendor/model` id, else the provider), and the public model id (a fine-tuned or routed id is reported as `other`); then the run's totals |
126
+ | `cost_source` | `provider` when the provider priced the calls, `none` when the run made model calls nobody priced, `null` for a run without model calls |
127
+ | `artifact_bytes` | Size of the artifacts the run wrote |
128
+ | `errors`, `error_codes` | How many run-level errors, and the distinct error codes seen anywhere in the run (`LOCATOR_NOT_FOUND`, `APP_UNREACHABLE`, ...), at most 20. A code that is not an upper-case token, the shape of every runner code, is reported as `OTHER` |
129
+ | `primary_error_code` | The first error the run recorded, which is the one that decided its status: a run-level error, else the failed attempt's verdict, else a step's; `null` for a clean run |
130
+ | `error_kinds` | For `ENGINE_FAILURE`, `MODEL_PROVIDER_FAILED`, and `ERROR`, whose code says where a failure surfaced but not what it was: the code and a kind read off the message against a closed list (`auth`, `rate-limit`, `overloaded`, `timeout`, `network`, `server`, `bad-request`, `device`, `daemon`, `browser`, `permission`, `not-found`, `memory`, `other`), as `MODEL_PROVIDER_FAILED:rate-limit`. Only the kind's name leaves the machine, never the message |
131
+
132
+ ## What is never sent
133
+
134
+ Test titles, file paths, URLs, origins, instructions, observations, page
135
+ content, screenshots, error messages, stack traces, environment variable
136
+ values, credentials, or file contents. Events include only the properties
137
+ listed above.
138
+
139
+ ## In CI
140
+
141
+ CI runs are attributed to the CI vendor rather than to a machine, no
142
+ preferences file is written on the runner, and no notice is printed. Set
143
+ `E2E_TELEMETRY_DISABLED: '1'` in the job's `env` to send nothing from CI.
144
+
145
+ ## In a fleet
146
+
147
+ A platform that runs e2e on its users' behalf starts each run on a fresh
148
+ machine, and every fresh machine would otherwise count as a new user. Set
149
+ `E2E_TELEMETRY_FLEET=<name>` in those machines and they are attributed to
150
+ `fleet:<name>` instead, like CI: no preferences file, no notice, and no
151
+ project id outside git. Or set `E2E_TELEMETRY_DISABLED=1` and send nothing.
152
+
153
+ ## Where it goes
154
+
155
+ Events go to a PostHog project we operate, in one request to
156
+ `eu.i.posthog.com`. PostHog keeps no person profile for the ids and stores
157
+ no request address; every event also asks it to skip its GeoIP step, so no
158
+ country, city, or coordinates are derived from where the request came from.
159
+ The data is read in aggregate. Telemetry falls under the
160
+ [security policy](https://github.com/tester-army/e2e/blob/main/SECURITY.md).
161
+ [Outbound connections](/security#outbound-connections) lists every other
162
+ connection a run can make.
163
+
164
+ ## Feedback
165
+
166
+ [`e2e feedback`](/reference/cli#e2e-feedback) is not telemetry. It sends
167
+ nothing until you, or the coding agent in your shell, run it, and then it
168
+ sends the words written into its flags as one `e2e_feedback` event to the
169
+ same PostHog project, with the machine facts above. Because it is an explicit
170
+ request, a saved `e2e telemetry disable` does not stop it, but
171
+ `E2E_TELEMETRY_DISABLED=1` and `DO_NOT_TRACK=1` do: an environment that set
172
+ either sends nothing, feedback included. `E2E_TELEMETRY_DEBUG=1` and
173
+ `--dry-run` print it instead of sending it. When telemetry is on, the
174
+ event carries the same anonymous id as your telemetry events. Secret-named
175
+ environment variable values and well-known token shapes are redacted before
176
+ it is sent.
177
+
178
+ ## The notice
179
+
180
+ The first command prints a notice on stderr explaining how to opt out.
181
+ `e2e init` is exempt, so a scaffold starts with its prompts rather than a
182
+ disclaimer; the first command after it prints the notice. `e2e telemetry`
183
+ is exempt too, since it is where you act on the notice. The notice leaves
184
+ `--reporter json` output intact. The notice appears again after a
185
+ material change to the data collected.
package/docs/tools.mdx ADDED
@@ -0,0 +1,150 @@
1
+ ---
2
+ title: Project tools
3
+ description: Add tools for test APIs, data setup, and device operations.
4
+ ---
5
+
6
+ Put tools in an agent's `tools` so the agent can call your test API or perform
7
+ other project-specific operations during a goal. Wrap each AI SDK tool with
8
+ `defineTool` to declare whether it changes state and which platforms support it.
9
+
10
+ ```ts title="e2e.config.ts"
11
+ import type { E2EConfig } from 'e2e';
12
+ import { defineTool } from 'e2e/agent';
13
+ import { web } from '@e2e-dev/web';
14
+ import { gateway, tool } from 'ai';
15
+ import { z } from 'zod';
16
+
17
+ const APP_URL = process.env.APP_URL ?? 'http://localhost:3000';
18
+
19
+ const seedCart = defineTool(
20
+ tool({
21
+ description: 'Seed the cart with a SKU via the store test API',
22
+ inputSchema: z.object({ sku: z.string() }),
23
+ execute: async ({ sku }) => {
24
+ const response = await fetch(`${APP_URL}/api/test/cart?sku=${encodeURIComponent(sku)}`);
25
+ if (!response.ok) throw new Error(`Cart seeding failed: HTTP ${response.status}`);
26
+ return response.text();
27
+ },
28
+ }),
29
+ { mutates: true },
30
+ );
31
+
32
+ export default {
33
+ targets: [{ engine: web(), app: { url: APP_URL } }],
34
+ agents: { default: { model: gateway('openai/gpt-6-luna-fast'), tools: { seedCart } } },
35
+ } satisfies E2EConfig;
36
+ ```
37
+
38
+ The agent can now seed a cart before checking out:
39
+
40
+ ```ts
41
+ await agent.act('seed the cart with SKU {sku}, then check out', {
42
+ params: { sku: 'SHIRT-BLUE-M' },
43
+ });
44
+ ```
45
+
46
+ Built-in tool names are reserved. Loading the config fails with
47
+ `INVALID_CONFIG` for a project tool named like a grammar verb (`tap`,
48
+ `screenshot`), a tool the harness adds (`complete_step` in every step,
49
+ `locate`, `start_recording`, and `stop_recording` in an `e2e mcp` session,
50
+ `report_finding` under `e2e explore`), and for a value `defineTool` did not
51
+ build. Tools belong to
52
+ the built-in agent: an entry with a custom `executor` cannot also set
53
+ `tools`.
54
+
55
+ ## Declare what a tool does
56
+
57
+ `defineTool` takes the AI SDK tool and these annotations:
58
+
59
+ | Annotation | Meaning |
60
+ | --- | --- |
61
+ | `mutates: true` | Changes state. The call counts against the step's action budget and runs in order with the other actions. The [replay cache](/cache) cannot replay it, so the agent runs that part of the step live. |
62
+ | `mutates: false` | Read-only. Runs without an action slot and may observe the screen. |
63
+ | `platforms: ['ios', 'android']` | Offers the tool only on those platforms. Omit this to offer it on every target. |
64
+
65
+ A failed call still consumes its action slot. The runner checks the budget
66
+ before dispatch, including when the model requests several calls at once.
67
+
68
+ ## Observe from a tool
69
+
70
+ A read-only tool can observe the redacted screen. This example saves a
71
+ masked screenshot as report evidence when pixels are available:
72
+
73
+ ```ts
74
+ import { randomUUID } from 'node:crypto';
75
+ import { defineTool, getToolContext } from 'e2e/agent';
76
+ import { tool } from 'ai';
77
+ import { z } from 'zod';
78
+
79
+ const captureEvidence = defineTool(
80
+ tool({
81
+ description: 'Save a screenshot as evidence in the report',
82
+ inputSchema: z.object({}),
83
+ execute: async (_input, executionOptions) => {
84
+ const context = getToolContext(executionOptions);
85
+ const observation = await context.observe({ pixels: true });
86
+ const pixels = observation.pixels;
87
+ if (pixels === undefined) {
88
+ return { withheld: observation.pixelsWithheld ?? 'UNSUPPORTED_CAPABILITY' };
89
+ }
90
+ const id = await context.attachScreenshot(pixels, `evidence-${randomUUID()}`);
91
+ return { evidence: id };
92
+ },
93
+ }),
94
+ { mutates: false },
95
+ );
96
+ ```
97
+
98
+ `observe()` returns a redacted text snapshot. Request `tree: true` for the
99
+ node tree or `pixels: true` for a masked screenshot. The built-in agent's
100
+ restrictions apply, including no pixels
101
+ [after a secret fill](/security#screenshots-after-a-secret-fill).
102
+
103
+ Observe in a separate read-only tool. A mutating tool already occupies the
104
+ action queue, so observing from inside it would wait on its own operation.
105
+
106
+ `attachScreenshot(pixels, label)` adds the image to the step's artifacts and
107
+ returns its artifact id. Use a unique label for each capture to avoid
108
+ overwriting an earlier image.
109
+
110
+ ## Device toolkits
111
+
112
+ `@e2e-dev/mobile` ships a ready tool pack for its engine. Pass every device
113
+ engine to one `mobileTools` call, and spread the pack to add your own tools
114
+ beside it:
115
+
116
+ ```ts
117
+ import type { E2EConfig } from 'e2e';
118
+ import { mobile } from '@e2e-dev/mobile';
119
+ import { mobileTools } from '@e2e-dev/mobile/tools';
120
+ import { gateway } from 'ai';
121
+
122
+ const iphone = mobile({ platform: 'ios' });
123
+ const pixel = mobile({ platform: 'android' });
124
+
125
+ export default {
126
+ targets: [
127
+ { name: 'iphone', engine: iphone, app: { bundleId: 'Settings' } },
128
+ { name: 'pixel', engine: pixel, app: { bundleId: 'com.android.settings' } },
129
+ ],
130
+ agents: {
131
+ default: {
132
+ model: gateway('openai/gpt-6-luna-fast'),
133
+ tools: { ...mobileTools(iphone, pixel) /* , ...your own tools */ },
134
+ },
135
+ },
136
+ } satisfies E2EConfig;
137
+ ```
138
+
139
+ The pack adds `open_app`, `swipe`, and `alert` on device targets.
140
+ Screenshots and point actions come from the engine's built-in tools.
141
+ See the [Mobile engine reference](/reference/mobile#agent-tools).
142
+
143
+ <CardGroup cols={2}>
144
+ <Card title="Custom executors" icon="gears" href="/executors">
145
+ Replace the prompt or decision loop.
146
+ </Card>
147
+ <Card title="How agent steps work" icon="stairs" href="/agent-steps">
148
+ What the model sees and what it may do.
149
+ </Card>
150
+ </CardGroup>
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: Testing viewports
3
+ description: Run a suite at several sizes, resize inside a test, and know what the agent sees at each.
4
+ ---
5
+
6
+ Set a viewport on the target to run a suite at that size. Use
7
+ `browser.setViewport()` to resize during a test. The agent uses the current size
8
+ and cannot resize it.
9
+
10
+ ## One size per target
11
+
12
+ `viewport` sets the initial size of each attempt, in pixels. The default is
13
+ 1280 by 720. Add one target for each size you want to test:
14
+
15
+ ```ts title="e2e.config.ts"
16
+ import type { E2EConfig } from 'e2e';
17
+ import { web } from '@e2e-dev/web';
18
+
19
+ const app = { url: 'http://localhost:3000' };
20
+
21
+ export default {
22
+ targets: [
23
+ { name: 'desktop', engine: web(), app },
24
+ { name: 'tablet', engine: web({ viewport: { width: 820, height: 1180 } }), app },
25
+ { name: 'phone', engine: web({ browser: 'webkit', viewport: { width: 390, height: 844 } }), app },
26
+ ],
27
+ } satisfies E2EConfig;
28
+ ```
29
+
30
+ Each test runs once per target, with a separate result labeled `desktop`,
31
+ `tablet`, or `phone`. Run one size with `npx e2e run --target phone`, or list
32
+ the selected test-target pairs with `npx e2e list`.
33
+
34
+ To have the runner start one dev server for every size, give every target
35
+ the same `app.command`: targets declaring one command share one process; see
36
+ [several targets](/starting-your-app#several-targets). Every retry starts at
37
+ the target's configured size.
38
+
39
+ ## Fill the window
40
+
41
+ `viewport: null` sets no size, so the page fills the browser window. The
42
+ agent, screenshots, and videos use that size. Use it in a headed run, or
43
+ with a hosted browser whose live view would show a fixed 1280 by 720 page
44
+ in one corner:
45
+
46
+ ```ts title="e2e.config.ts"
47
+ { name: 'window', engine: web({ viewport: null }), app }
48
+ ```
49
+
50
+ The page takes the window's size, minus the toolbar in a headed browser. A
51
+ local headed launch opens a small window; a hosted browser uses the window
52
+ size you requested from the service. `browser.setViewport` still works.
53
+
54
+ ## Resize inside a test
55
+
56
+ `browser.setViewport` resizes the page for the rest of the attempt, through
57
+ `app.restart()` and `app.clearState()`:
58
+
59
+ ```ts
60
+ import { test } from '@e2e-dev/web';
61
+ import { expect } from 'e2e';
62
+
63
+ test('the menu collapses on a narrow screen', async ({ app, screen, browser }) => {
64
+ await app.open('/');
65
+ await expect(screen.getByRole('navigation')).toBeVisible();
66
+
67
+ await browser.setViewport({ width: 390, height: 844 });
68
+ await expect(screen.getByRole('button', 'Menu')).toBeVisible();
69
+ await expect(screen.getByRole('navigation')).toBeHidden();
70
+ });
71
+ ```
72
+
73
+ Use this when one test needs to check a layout before and after resizing.
74
+ For a whole suite, configure separate targets instead.
75
+
76
+ Call it before `app.open` to load the first page at that size, so the app
77
+ never renders its desktop layout first:
78
+
79
+ ```ts
80
+ await browser.setViewport({ width: 390, height: 844 });
81
+ await app.open('/');
82
+ ```
83
+
84
+ The resize appears as a step in the report. To check the exact width, read
85
+ it from the page:
86
+
87
+ ```ts
88
+ const width = await browser.evaluate(() => window.innerWidth);
89
+ expect(width).toBe(390);
90
+ ```
91
+
92
+ ## What the agent sees
93
+
94
+ The agent receives the current viewport size with each observation.
95
+ Screenshots cover that viewport. Off-screen elements can still appear in
96
+ the tree, and tapping them scrolls them into view. The agent can scroll to
97
+ reveal lazy-loaded content.
98
+
99
+ Resize before calling `agent.act`:
100
+
101
+ ```ts
102
+ test('the mobile menu reaches pricing', async ({ app, agent, browser }) => {
103
+ await browser.setViewport({ width: 390, height: 844 });
104
+ await app.open('/');
105
+ await agent.act('open the navigation menu and go to Pricing');
106
+ await expect(browser).toHaveURL('/pricing');
107
+ });
108
+ ```
109
+
110
+ ## Scoping a test to one size
111
+
112
+ `platforms` and `requires` cannot select a viewport; all Playwright targets
113
+ use platform `web`. To skip a test at some sizes, check the width in the
114
+ test:
115
+
116
+ ```ts
117
+ test('the sidebar stays open', async ({ app, screen, browser }) => {
118
+ await app.open('/');
119
+ test.skip((await browser.evaluate(() => window.innerWidth)) < 768, 'desktop layout only');
120
+ await expect(screen.getByRole('complementary')).toBeVisible();
121
+ });
122
+ ```
123
+
124
+ To keep a whole group of size-specific tests apart, give them their own
125
+ config and run it with `--config`.
126
+
127
+ ## Limits
128
+
129
+ A narrow viewport tests responsive layout. It still uses a desktop browser
130
+ with a scale factor of 1, desktop pointer behavior, and its usual user agent.
131
+ It does not emulate touch, color scheme, or reduced-motion preferences.
132
+ Use the [device engine](/mobile) for tests on a simulator or emulator.
133
+
134
+ A video keeps the size the page had when it started recording; set
135
+ `web({ screencast: { size } })` for one fixed size. A browser provider that
136
+ records, such as [Kernel](/integrations/kernel#recordings), records the whole
137
+ screen instead.
138
+
139
+ <CardGroup cols={2}>
140
+ <Card title="Working with the browser" icon="globe" href="/browser">
141
+ Routes, cookies, dialogs, downloads, and the rest of the `browser` fixture.
142
+ </Card>
143
+ <Card title="Web engine reference" icon="masks-theater" href="/reference/web">
144
+ Every engine option and `browser` method, with timeouts and errors.
145
+ </Card>
146
+ </CardGroup>