e2e 0.17.0 → 0.18.0

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 (166) hide show
  1. package/README.md +10 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/ai-sdk.js +5 -4
  4. package/dist/agent/error.js +3 -3
  5. package/dist/agent/invocation.js +3 -4
  6. package/dist/agent/model/tool-choice.d.ts +9 -0
  7. package/dist/agent/model/tool-choice.js +14 -0
  8. package/dist/agent/observation-feed.js +3 -4
  9. package/dist/agent/phases.d.ts +7 -1
  10. package/dist/agent/phases.js +12 -3
  11. package/dist/agent/prompts.js +3 -0
  12. package/dist/agent/replay.js +18 -15
  13. package/dist/agent/step-cache.d.ts +2 -1
  14. package/dist/agent/step-cache.js +9 -5
  15. package/dist/agent/tool-loop.js +92 -5
  16. package/dist/cache/trace.js +6 -2
  17. package/dist/cli/bin.js +2 -2
  18. package/dist/cli/index.js +35 -24
  19. package/dist/cli/init/mcp-config.d.ts +2 -1
  20. package/dist/cli/init/mcp-config.js +8 -3
  21. package/dist/cli/init/sibling-versions.json +1 -1
  22. package/dist/cli/mcp.d.ts +3 -2
  23. package/dist/cli/mcp.js +52 -16
  24. package/dist/cli/run-output.d.ts +17 -0
  25. package/dist/cli/run-output.js +45 -0
  26. package/dist/collect/collect.js +8 -4
  27. package/dist/collect/registry.js +27 -16
  28. package/dist/config/compiled-files.d.ts +36 -0
  29. package/dist/config/compiled-files.js +50 -0
  30. package/dist/config/diagnose.js +21 -19
  31. package/dist/config/esm-hooks.d.ts +39 -44
  32. package/dist/config/esm-hooks.js +238 -63
  33. package/dist/config/load.d.ts +1 -7
  34. package/dist/config/load.js +23 -66
  35. package/dist/config/register.d.ts +8 -0
  36. package/dist/config/register.js +9 -0
  37. package/dist/config/specifiers.d.ts +19 -0
  38. package/dist/config/specifiers.js +33 -0
  39. package/dist/config/tsconfig.d.ts +56 -0
  40. package/dist/config/tsconfig.js +115 -0
  41. package/dist/config/typescript.d.ts +19 -0
  42. package/dist/config/typescript.js +253 -0
  43. package/dist/engine/contract.d.ts +1 -0
  44. package/dist/engine/index.d.ts +2 -0
  45. package/dist/engine/session.js +2 -2
  46. package/dist/expect/async.js +2 -1
  47. package/dist/expect/poll.js +2 -2
  48. package/dist/internal/errors.d.ts +10 -3
  49. package/dist/internal/errors.js +23 -8
  50. package/dist/internal/node-version.d.ts +16 -7
  51. package/dist/internal/node-version.js +50 -15
  52. package/dist/internal/package-manager.js +1 -1
  53. package/dist/internal/realm-slot.d.ts +5 -4
  54. package/dist/internal/redact.d.ts +2 -0
  55. package/dist/internal/redact.js +4 -0
  56. package/dist/internal/time.d.ts +21 -5
  57. package/dist/internal/time.js +35 -3
  58. package/dist/locator/engine.js +47 -11
  59. package/dist/locator/screen.js +31 -5
  60. package/dist/mcp/output.d.ts +59 -0
  61. package/dist/mcp/output.js +129 -0
  62. package/dist/mcp/server.d.ts +4 -0
  63. package/dist/mcp/server.js +9 -12
  64. package/dist/mcp/session.d.ts +8 -1
  65. package/dist/mcp/session.js +55 -14
  66. package/dist/mcp/tools.js +7 -5
  67. package/dist/report/list.d.ts +11 -0
  68. package/dist/report/list.js +23 -6
  69. package/dist/run/artifacts.d.ts +4 -4
  70. package/dist/run/artifacts.js +3 -2
  71. package/dist/run/execute.d.ts +25 -5
  72. package/dist/run/execute.js +44 -20
  73. package/dist/run/failure-evidence.js +6 -2
  74. package/dist/run/in-process.js +51 -10
  75. package/dist/run/process-output.d.ts +73 -0
  76. package/dist/run/process-output.js +130 -0
  77. package/dist/run/provision.d.ts +2 -0
  78. package/dist/run/provision.js +1 -0
  79. package/dist/run/records.d.ts +4 -4
  80. package/dist/run/retry.d.ts +7 -0
  81. package/dist/run/retry.js +27 -11
  82. package/dist/run/runner.d.ts +8 -0
  83. package/dist/run/runner.js +32 -9
  84. package/dist/run/scheduler.js +255 -19
  85. package/dist/run/secrecy.d.ts +22 -19
  86. package/dist/run/secrecy.js +22 -18
  87. package/dist/run/serial.d.ts +73 -2
  88. package/dist/run/serial.js +239 -58
  89. package/dist/run/standalone.js +1 -1
  90. package/dist/run/trace-redaction.d.ts +5 -6
  91. package/dist/run/trace-redaction.js +5 -6
  92. package/dist/run/worker/entry.js +35 -14
  93. package/dist/run/worker/handle.js +10 -5
  94. package/dist/run/worker/protocol.d.ts +83 -3
  95. package/dist/run/worker/session.js +28 -0
  96. package/dist/telemetry/environment.d.ts +2 -1
  97. package/dist/telemetry/environment.js +12 -2
  98. package/dist/telemetry/project.d.ts +5 -6
  99. package/dist/telemetry/project.js +6 -11
  100. package/dist/telemetry/store.d.ts +5 -5
  101. package/dist/telemetry/store.js +5 -7
  102. package/dist/telemetry/telemetry.d.ts +2 -3
  103. package/dist/telemetry/telemetry.js +2 -4
  104. package/dist/types.d.ts +6 -3
  105. package/docs/agent-steps.mdx +1 -0
  106. package/docs/agents.mdx +1 -0
  107. package/docs/api-testing.mdx +2 -1
  108. package/docs/authentication.mdx +3 -2
  109. package/docs/browser.mdx +7 -6
  110. package/docs/bug-bash.mdx +2 -1
  111. package/docs/cache.mdx +2 -1
  112. package/docs/ci.mdx +3 -2
  113. package/docs/coding-agents.mdx +2 -1
  114. package/docs/core-concepts.mdx +208 -0
  115. package/docs/debugging.mdx +19 -9
  116. package/docs/decision-models.mdx +142 -0
  117. package/docs/example-projects.mdx +32 -0
  118. package/docs/executors.mdx +3 -0
  119. package/docs/explore.mdx +1 -0
  120. package/docs/github.mdx +2 -1
  121. package/docs/index.mdx +9 -5
  122. package/docs/integrations/eas.mdx +1 -0
  123. package/docs/integrations/index.mdx +1 -0
  124. package/docs/integrations/kernel.mdx +2 -1
  125. package/docs/migrate/cypress.mdx +26 -3
  126. package/docs/migrate/detox.mdx +24 -1
  127. package/docs/migrate/maestro.mdx +28 -4
  128. package/docs/migrate/playwright.mdx +27 -4
  129. package/docs/migrate/selenium.mdx +27 -3
  130. package/docs/mobile-ci.mdx +6 -5
  131. package/docs/mobile.mdx +79 -8
  132. package/docs/models.mdx +7 -2
  133. package/docs/quickstart.mdx +26 -15
  134. package/docs/reference/agent.mdx +13 -3
  135. package/docs/reference/app.mdx +1 -0
  136. package/docs/reference/cli.mdx +11 -5
  137. package/docs/reference/config.mdx +42 -6
  138. package/docs/reference/engine.mdx +22 -10
  139. package/docs/reference/environment.mdx +2 -1
  140. package/docs/reference/errors.mdx +11 -8
  141. package/docs/reference/expect.mdx +2 -1
  142. package/docs/reference/mcp.mdx +21 -8
  143. package/docs/reference/mobile.mdx +1 -0
  144. package/docs/reference/reporters.mdx +1 -0
  145. package/docs/reference/screen.mdx +19 -9
  146. package/docs/reference/test.mdx +21 -1
  147. package/docs/reference/web.mdx +8 -2
  148. package/docs/security.mdx +33 -27
  149. package/docs/subscriptions.mdx +1 -0
  150. package/docs/telemetry.mdx +8 -5
  151. package/docs/tools.mdx +1 -0
  152. package/docs/{starting-your-app.mdx → web.mdx} +157 -11
  153. package/docs/writing-an-engine.mdx +2 -1
  154. package/package.json +11 -7
  155. package/skills/e2e/SKILL.md +3 -1
  156. package/skills/e2e/references/agent.md +11 -0
  157. package/skills/e2e/references/debugging.md +1 -1
  158. package/skills/e2e/references/mcp.md +8 -3
  159. package/skills/e2e/references/running.md +2 -1
  160. package/skills/e2e/references/setup.md +9 -1
  161. package/skills/e2e/references/writing-tests.md +1 -1
  162. package/docs/assertions.mdx +0 -94
  163. package/docs/goals.mdx +0 -176
  164. package/docs/locators.mdx +0 -64
  165. package/docs/physical-devices.mdx +0 -76
  166. package/docs/viewports.mdx +0 -146
package/docs/security.mdx CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Security model
3
3
  description: What the model can access, how the runner handles secrets, and the limits of its protections.
4
+ icon: "shield-halved"
4
5
  ---
5
6
 
6
7
  Test code and configuration run with your operating-system permissions.
@@ -106,7 +107,9 @@ redaction and disables pixels for the rest of the attempt.
106
107
  After any secret fill, for the rest of the attempt, screenshot and pixel
107
108
  tools are unavailable, `app.screenshot()` is denied, and the runner omits
108
109
  assertion and failure screenshots. The app could display the value anywhere,
109
- beyond the field's masked rectangle.
110
+ beyond the field's masked rectangle. An `act` or `vision: true` step whose
111
+ semantic capture times out before the step does fails with `POLICY_DENIED`
112
+ instead of reading a fallback screenshot.
110
113
 
111
114
  A test that restores a [session](#sessions) whose setup filled a secret keeps
112
115
  this protection. A setup that signs in without filling a secret, for example
@@ -130,13 +133,14 @@ password on screen is not masked in pixels.
130
133
  ### Redaction
131
134
 
132
135
  The runner replaces every occurrence of a registered secret value with
133
- `<secret:name>` in model input, reports, a worker process's console output,
134
- and the trace text of a session a secret was filled on or an engine option
135
- holds. That includes a value the test passes as a plain string rather than
136
- a `secrets.get()` handle: test and `describe` titles, step labels (an
137
- `app.open` URL, a locator's text), an `agent.act` or `agent.assert`
138
- instruction, the strings in `agent.act` params, `agentContext`, and an
139
- `e2e explore` goal. Titles are redacted as the test file is collected, so
136
+ `<secret:name>` in model input, reports, what your code prints during
137
+ `e2e run` (tests, the config's and test files' top-level code, reporters),
138
+ trace text, and text downloads. That includes a value the test passes as a
139
+ plain string rather than a `secrets.get()` handle: test and `describe`
140
+ titles, step labels (an `app.open` URL, a locator's text), a `fill`, an
141
+ `agent.act` or `agent.assert` instruction, the strings in `agent.act`
142
+ params, `agentContext`, and an `e2e explore` goal. A plain string is not a
143
+ secret fill: it is protected as text only, and screenshots stay available. Titles are redacted as the test file is collected, so
140
144
  the test id, `--grep`, the reporters, and the artifact and failure-page file
141
145
  names all see `<secret:name>`, and two titles in one file that differ only
142
146
  in a secret value are a duplicate title path. A custom executor's
@@ -155,6 +159,9 @@ replay cache descriptor, or an end anchor reads it. Redaction rewrites only
155
159
  what the runner reports; assertions still compare the text the page shows.
156
160
  Secret values and their cut parts never enter cache entries.
157
161
 
162
+ Output printed while the config loads appears once it has loaded. If the
163
+ load fails, that output is withheld, since the config's secrets are unknown.
164
+
158
165
  A static value shorter than 6 characters is `INVALID_CONFIG`, and a provider
159
166
  returning one fails the fill: every occurrence of so short a value would take
160
167
  ordinary text with it.
@@ -176,31 +183,30 @@ ordinary text with it.
176
183
  rule for text cut at a length limit.
177
184
  - A title is redacted when its file is collected, before any provider
178
185
  runs, so a value a secret provider returns is not redacted from titles.
179
- - A Playwright trace from a session no secret reached is kept as recorded,
180
- so a value the test only spelled into a URL or a locator stays in its
181
- text.
182
186
  - When tests run in the runner's own process, with the config or tests
183
187
  passed in memory by an embedding host, their console output is not
184
188
  redacted.
185
- - A Playwright trace from a session a secret reached, filled or held by the
186
- engine, is rewritten before it is kept: any run of 8 or more characters of
187
- a value, its whitespace collapsed or not, becomes `<secret:name>`, so unrelated text that shares 8 characters
188
- with a value is masked too. A base64 or base64url run that decodes to a
189
- value or such a fragment (a basic-auth header, a cookie, a token segment)
190
- is replaced whole. After a fill, screencast frames are dropped; with only
191
- an engine-held secret they are kept. An image the app served that
192
- draws the secret survives. A trace the runner cannot rewrite is deleted and
189
+ - A Playwright trace is rewritten before it is kept whenever its session
190
+ knows a secret value, filled or not: every static one, and a provider's
191
+ once the session resolved it. Any run of 8 or more characters of a value,
192
+ its whitespace collapsed or not, becomes `<secret:name>`, so unrelated
193
+ text that shares 8 characters with a value is masked too. A base64 or
194
+ base64url run that decodes to a value or such a fragment (a basic-auth
195
+ header, a cookie, a token segment) is replaced whole. After a fill,
196
+ screencast frames are dropped; otherwise they are kept. An image the app
197
+ served that draws the secret survives. A trace the runner cannot rewrite is deleted and
193
198
  the attempt records `TRACE_WITHHELD`.
194
199
  - A video masks nothing and is labeled `redaction: "incomplete"`. It is
195
200
  never recorded unless you ask. A recording a hosted browser or
196
201
  device service keeps stays with that service; the report holds only its
197
202
  URL.
198
- - A download is what the app served. Once a secret reached the session,
199
- filled or held by the engine, a text download (`text/*` or JSON by file
200
- extension) is rewritten the way trace text is and labeled
201
- `redaction: "complete"`. Any other download, a binary or one from a
202
- session no secret reached, is kept as served and labeled
203
- `redaction: "incomplete"`.
203
+ - A download is what the app served. When the session knows a secret
204
+ value, every whole occurrence of it in a text download (`text/*` or JSON
205
+ by file extension) is replaced, as in reports, and the download is labeled
206
+ `redaction: "complete"`. Fragments and base64 runs are matched in traces
207
+ only. Any other download, a binary, one that is not valid UTF-8 or cannot
208
+ be rewritten, or one from a session that knows no secret value, is kept
209
+ as served and labeled `redaction: "incomplete"`.
204
210
  - The `headers` a protected preview needs are recorded in the trace as
205
211
  request headers. Share that trace as you would the bypass secret.
206
212
  - `command.log` is captured as the process writes it.
@@ -258,9 +264,9 @@ project-relative path you configure.
258
264
  | --- | --- | --- |
259
265
  | `report.json` | The run record. Errors, reasoning excerpts, and action summaries pass the redactor. | complete |
260
266
  | `artifacts/.../*.png` | Screenshots. Secure fields are masked before screenshots are returned or saved as artifacts. | complete |
261
- | `artifacts/.../trace.zip` | Playwright trace, rewritten after a secret fill or when an engine option holds a secret; screencast frames are dropped after a fill only. | not-required, complete, or deleted |
267
+ | `artifacts/.../trace.zip` | Playwright trace, rewritten whenever its session knows a secret value; screencast frames are dropped after a fill only. | not-required (no secret value), complete, or deleted |
262
268
  | `artifacts/.../*.webm`, `*.mp4` | Video, opt-in through `video` or `--video`; `retain-on-failure` keeps only the attempts that did not pass. | incomplete |
263
- | `artifacts/.../downloads/*` | Files the app served through a download. Text ones are rewritten after a secret fill or when an engine option holds a secret; the rest are kept as served. | complete or incomplete |
269
+ | `artifacts/.../downloads/*` | Files the app served through a download. Text ones are rewritten whenever the session knows a secret value; the rest are kept as served. | complete or incomplete |
264
270
  | `artifacts/.../failure/screen.txt` | The redacted tree at the moment of failure. | complete |
265
271
  | `ai-trace.json` | Every model call, opt-in via `--ai-trace`. Prompts are the redacted ones the model saw; inline image bytes are replaced by their size. | as the prompt |
266
272
  | `sessions/<runId>/` | Encrypted sessions, deleted at cleanup. | encrypted |
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Subscriptions
3
3
  description: Use your ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription for agent steps.
4
+ icon: "id-card"
4
5
  ---
5
6
 
6
7
  Use an existing subscription without a separate API key. Choose your plan
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Telemetry
3
3
  description: What the CLI sends, what it never sends, and how to turn it off.
4
+ icon: "chart-line"
4
5
  ---
5
6
 
6
7
  The `e2e` CLI sends anonymous usage data so we can see which commands,
@@ -72,7 +73,7 @@ Who ran it:
72
73
  | Property | Value |
73
74
  | --- | --- |
74
75
  | `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` |
75
- | `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 |
76
+ | `project_id` | A hash derived from the repository's root commit, shared across full clones. `null` outside git and in a shallow clone |
76
77
  | `session_id` | A random id for this one invocation |
77
78
  | `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 |
78
79
 
@@ -181,16 +182,18 @@ listed above.
181
182
  ## In CI
182
183
 
183
184
  CI runs are attributed to the CI vendor rather than to a machine, no
184
- preferences file is written on the runner, and no notice is printed. Set
185
- `E2E_TELEMETRY_DISABLED: '1'` in the job's `env` to send nothing from CI.
185
+ preferences file is written on the runner, and no notice is printed. A coding
186
+ agent that sets `CI` in its shell, with no CI vendor around it, counts as the
187
+ machine it runs on. Set `E2E_TELEMETRY_DISABLED: '1'` in the job's `env` to
188
+ send nothing from CI.
186
189
 
187
190
  ## In a fleet
188
191
 
189
192
  A platform that runs e2e on its users' behalf starts each run on a fresh
190
193
  machine, and every fresh machine would otherwise count as a new user. Set
191
194
  `E2E_TELEMETRY_FLEET=<name>` in those machines and they are attributed to
192
- `fleet:<name>` instead, like CI: no preferences file, no notice, and no
193
- project id outside git. Or set `E2E_TELEMETRY_DISABLED=1` and send nothing.
195
+ `fleet:<name>` instead, like CI: no preferences file and no notice. Or set
196
+ `E2E_TELEMETRY_DISABLED=1` and send nothing.
194
197
 
195
198
  ## Where it goes
196
199
 
package/docs/tools.mdx CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Project tools
3
3
  description: Add tools for test APIs, data setup, and device operations.
4
+ icon: "screwdriver-wrench"
4
5
  ---
5
6
 
6
7
  Put tools in an agent's `tools` so the agent can call your test API or perform
@@ -1,13 +1,21 @@
1
1
  ---
2
- title: Starting your app
3
- description: Point the runner at a running app, or let it start the app for the run.
2
+ title: Web
3
+ description: Point the web engine at your app or let the runner start it, and run a suite at several viewport sizes.
4
+ icon: "globe"
4
5
  ---
5
6
 
7
+ `@e2e-dev/web` runs tests in a browser against your app's URL. This page
8
+ covers getting the app running for a run and testing it at different
9
+ sizes. For routes, cookies, dialogs, downloads, and the rest of the
10
+ `browser` fixture, see [Working with the browser](/browser).
11
+
12
+ ## Starting your app
13
+
6
14
  Set your app's URL in the target's `app`. If you also provide a start
7
15
  command, the runner starts the app, waits until it is ready, and stops it
8
16
  when the run ends.
9
17
 
10
- ## Point at a running app
18
+ ### Point at a running app
11
19
 
12
20
  Import `web` from `@e2e-dev/web` and put this in the exported
13
21
  config object:
@@ -25,7 +33,7 @@ export default {
25
33
  you can change the address for a run without editing the file. URLs without
26
34
  a scheme use HTTPS, or HTTP for loopback hosts.
27
35
 
28
- ## Let the runner start it
36
+ ### Let the runner start it
29
37
 
30
38
  Add `app.command` and the runner spawns the dev server, waits until `app.url`
31
39
  answers, and stops it when the run ends, fails, or is interrupted:
@@ -79,7 +87,7 @@ temp-directory variables, and `env`, so pass what the app needs explicitly.
79
87
  See the
80
88
  [command reference](/reference/config#command) for all options.
81
89
 
82
- ### Let the runner pick the port
90
+ #### Let the runner pick the port
83
91
 
84
92
  Use port `0` to run multiple checkouts without a port conflict. The runner
85
93
  chooses a free port and replaces `{port}` in the command's `args` and `env`.
@@ -98,7 +106,7 @@ valid when the port changes. Use `127.0.0.1` or `[::1]`, matching the address
98
106
  the command binds; `localhost:0` is rejected. See
99
107
  [free ports](/reference/config#free-ports) for details.
100
108
 
101
- ### Next.js dev targets
109
+ #### Next.js dev targets
102
110
 
103
111
  Next.js 16 blocks cross-origin requests to its dev resources. A target that
104
112
  opens `127.0.0.1` or `[::1]` while `next dev` identifies as `localhost`
@@ -122,7 +130,7 @@ Use `'[::1]'` in the list when `app.url` opens `http://[::1]:0`.
122
130
  A target on `localhost` needs no entry, because `next dev` identifies as
123
131
  `localhost`. A production `next start` target is unaffected either way.
124
132
 
125
- ## Dependencies
133
+ ### Dependencies
126
134
 
127
135
  <Note>
128
136
  This version starts only `app.command` (one process for every target that
@@ -132,7 +140,7 @@ have `app.command` start a script that brings them up and serves the app. A
132
140
  services API returns in a later release.
133
141
  </Note>
134
142
 
135
- ## Several targets
143
+ ### Several targets
136
144
 
137
145
  Two browsers on one app are two targets that each name it:
138
146
 
@@ -155,9 +163,9 @@ test runs on every target unless it is scoped with `platforms` or
155
163
  `requires`; `--target <name>` selects at run time. A device target sits
156
164
  beside a browser target in the same list; see the
157
165
  [Mobile engine reference](/reference/mobile). One target per screen size is
158
- how a suite runs at several [viewports](/viewports).
166
+ how a suite runs at several [viewports](#viewports).
159
167
 
160
- ## Protected previews
168
+ ### Protected previews
161
169
 
162
170
  Use `headers` to access protected previews or skip a tunnel's interstitial
163
171
  page. For example, a Vercel preview can use a protection bypass header:
@@ -221,11 +229,149 @@ workers. Playwright traces include request headers, so a protected-preview
221
229
  trace can contain your bypass secret. Restrict access to those traces.
222
230
  </Note>
223
231
 
232
+ ## Viewports
233
+
234
+ Set a viewport on the target to run a suite at that size. Use
235
+ `browser.setViewport()` to resize during a test. The agent uses the current size
236
+ and cannot resize it.
237
+
238
+ ### One size per target
239
+
240
+ `viewport` sets the initial size of each attempt, in pixels. The default is
241
+ 1280 by 720. Add one target for each size you want to test:
242
+
243
+ ```ts title="e2e.config.ts"
244
+ import type { E2EConfig } from 'e2e';
245
+ import { web } from '@e2e-dev/web';
246
+
247
+ const app = { url: 'http://localhost:3000' };
248
+
249
+ export default {
250
+ targets: [
251
+ { name: 'desktop', engine: web(), app },
252
+ { name: 'tablet', engine: web({ viewport: { width: 820, height: 1180 } }), app },
253
+ { name: 'phone', engine: web({ browser: 'webkit', viewport: { width: 390, height: 844 } }), app },
254
+ ],
255
+ } satisfies E2EConfig;
256
+ ```
257
+
258
+ Each test runs once per target, with a separate result labeled `desktop`,
259
+ `tablet`, or `phone`. Run one size with `npx e2e run --target phone`, or list
260
+ the selected test-target pairs with `npx e2e list`.
261
+
262
+ To have the runner start one dev server for every size, give every target
263
+ the same `app.command`: targets declaring one command share one process; see
264
+ [several targets](#several-targets). Every retry starts at
265
+ the target's configured size.
266
+
267
+ ### Fill the window
268
+
269
+ `viewport: null` sets no size, so the page fills the browser window. The
270
+ agent, screenshots, and videos use that size. Use it in a headed run, or
271
+ with a hosted browser whose live view would show a fixed 1280 by 720 page
272
+ in one corner:
273
+
274
+ ```ts title="e2e.config.ts"
275
+ { name: 'window', engine: web({ viewport: null }), app }
276
+ ```
277
+
278
+ The page takes the window's size, minus the toolbar in a headed browser. A
279
+ local headed launch opens a small window; a hosted browser uses the window
280
+ size you requested from the service. `browser.setViewport` still works.
281
+
282
+ ### Resize inside a test
283
+
284
+ `browser.setViewport` resizes the page for the rest of the attempt, through
285
+ `app.restart()` and `app.clearState()`:
286
+
287
+ ```ts
288
+ import { test } from '@e2e-dev/web';
289
+ import { expect } from 'e2e';
290
+
291
+ test('the menu collapses on a narrow screen', async ({ app, screen, browser }) => {
292
+ await app.open('/');
293
+ await expect(screen.getByRole('navigation')).toBeVisible();
294
+
295
+ await browser.setViewport({ width: 390, height: 844 });
296
+ await expect(screen.getByRole('button', 'Menu')).toBeVisible();
297
+ await expect(screen.getByRole('navigation')).toBeHidden();
298
+ });
299
+ ```
300
+
301
+ Use this when one test needs to check a layout before and after resizing.
302
+ For a whole suite, configure separate targets instead.
303
+
304
+ Call it before `app.open` to load the first page at that size, so the app
305
+ never renders its desktop layout first:
306
+
307
+ ```ts
308
+ await browser.setViewport({ width: 390, height: 844 });
309
+ await app.open('/');
310
+ ```
311
+
312
+ The resize appears as a step in the report. To check the exact width, read
313
+ it from the page:
314
+
315
+ ```ts
316
+ const width = await browser.evaluate(() => window.innerWidth);
317
+ expect(width).toBe(390);
318
+ ```
319
+
320
+ ### What the agent sees
321
+
322
+ The agent receives the current viewport size with each observation.
323
+ Screenshots cover that viewport. Off-screen elements can still appear in
324
+ the tree, and tapping them scrolls them into view. The agent can scroll to
325
+ reveal lazy-loaded content.
326
+
327
+ Resize before calling `agent.act`:
328
+
329
+ ```ts
330
+ test('the mobile menu reaches pricing', async ({ app, agent, browser }) => {
331
+ await browser.setViewport({ width: 390, height: 844 });
332
+ await app.open('/');
333
+ await agent.act('open the navigation menu and go to Pricing');
334
+ await expect(browser).toHaveURL('/pricing');
335
+ });
336
+ ```
337
+
338
+ ### Scoping a test to one size
339
+
340
+ `platforms` and `requires` cannot select a viewport; all Playwright targets
341
+ use platform `web`. To skip a test at some sizes, check the width in the
342
+ test:
343
+
344
+ ```ts
345
+ test('the sidebar stays open', async ({ app, screen, browser }) => {
346
+ await app.open('/');
347
+ test.skip((await browser.evaluate(() => window.innerWidth)) < 768, 'desktop layout only');
348
+ await expect(screen.getByRole('complementary')).toBeVisible();
349
+ });
350
+ ```
351
+
352
+ To keep a whole group of size-specific tests apart, give them their own
353
+ config and run it with `--config`.
354
+
355
+ ### Limits
356
+
357
+ A narrow viewport tests responsive layout. It still uses a desktop browser
358
+ with a scale factor of 1, desktop pointer behavior, and its usual user agent.
359
+ It does not emulate touch, color scheme, or reduced-motion preferences.
360
+ Use the [device engine](/mobile) for tests on a simulator or emulator.
361
+
362
+ A video keeps the size the page had when it started recording; set
363
+ `web({ screencast: { size } })` for one fixed size. A browser provider that
364
+ records, such as [Kernel](/integrations/kernel#recordings), records the whole
365
+ screen instead.
366
+
224
367
  <CardGroup cols={2}>
368
+ <Card title="Working with the browser" icon="globe" href="/browser">
369
+ Routes, cookies, dialogs, downloads, and the rest of the `browser` fixture.
370
+ </Card>
225
371
  <Card title="Config reference" icon="sliders" href="/reference/config#the-app-under-test">
226
372
  `app`, `command`, and every app key.
227
373
  </Card>
228
374
  <Card title="Web engine reference" icon="masks-theater" href="/reference/web">
229
- Browser, viewport, `connect`, `headers`, `basicAuth`.
375
+ Browser, viewport, `connect`, `headers`, `basicAuth`, and every `browser` method.
230
376
  </Card>
231
377
  </CardGroup>
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Writing an engine
3
3
  description: Implement the e2e engine contract for another UI platform.
4
+ icon: "gear"
4
5
  ---
5
6
 
6
7
  An engine connects `screen`, `app`, and agent calls to a UI platform through
@@ -40,7 +41,7 @@ the engine's capabilities when the config loads.
40
41
  This skeleton assumes a `createClient` adapter for your platform. Its nodes
41
42
  must follow the semantic tree contract described below.
42
43
 
43
- ```ts title="src/index.ts"
44
+ ```ts title="src/index.ts" expandable
44
45
  import { ConfigurationError, defineEngine, resolveExpression, type EngineHandle } from 'e2e/engine';
45
46
 
46
47
  export function hyperdrive(options: { device: string }): EngineHandle {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "e2e",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "SDK, runner, and CLI for agentic end-to-end testing",
5
5
  "keywords": [
6
6
  "e2e",
@@ -79,14 +79,18 @@
79
79
  "NOTICE"
80
80
  ],
81
81
  "dependencies": {
82
- "@ai-sdk/provider": "4.0.18",
82
+ "@ai-sdk/provider": "4.0.21",
83
83
  "@clack/prompts": "1.8.1",
84
- "@modelcontextprotocol/server": "2.2.0",
85
- "@vitest/expect": "5.0.2",
84
+ "@jridgewell/remapping": "2.3.5",
85
+ "@modelcontextprotocol/server": "2.3.0",
86
+ "@oxc-project/runtime": "0.152.0",
87
+ "@vitest/expect": "5.0.3",
86
88
  "commander": "15.0.0",
89
+ "get-tsconfig": "4.14.3",
90
+ "oxc-parser": "0.152.0",
91
+ "oxc-transform": "0.152.0",
87
92
  "picocolors": "1.1.1",
88
93
  "pngjs": "7.0.0",
89
- "tsx": "4.23.14",
90
94
  "zod": "4.6.1"
91
95
  },
92
96
  "peerDependencies": {
@@ -123,7 +127,7 @@
123
127
  "@ai-sdk/openai": "4.0.71",
124
128
  "@ai-sdk/openai-compatible": "3.0.53",
125
129
  "@ai-sdk/xai": "5.0.4",
126
- "@e2e-dev/web": "0.12.0",
130
+ "@e2e-dev/web": "0.13.0",
127
131
  "@modelcontextprotocol/client": "2.2.0",
128
132
  "@openrouter/ai-sdk-provider": "3.1.0",
129
133
  "@types/node": "26.6.2",
@@ -135,7 +139,7 @@
135
139
  "vitest": "5.0.1"
136
140
  },
137
141
  "engines": {
138
- "node": ">=22.12.0"
142
+ "node": "^22.22.3 || >=24.8.0"
139
143
  },
140
144
  "scripts": {
141
145
  "build": "node scripts/prepare-build.ts && tsc --project tsconfig.build.json && node scripts/write-sibling-versions.ts && node ../../scripts/stamp-dist.ts",
@@ -61,7 +61,9 @@ one; the installed CLI prints the same text with `npx e2e guide <topic>`
61
61
  (`e2e guide` alone prints this page). For anything the topics do not cover,
62
62
  the full documentation ships in the `docs/` directory of the installed `e2e`
63
63
  package (`node_modules/e2e/docs` in a single-package project); a link such as
64
- `/reference/cli` is `docs/reference/cli.mdx`.
64
+ `/reference/cli` is `docs/reference/cli.mdx`. Complete projects for Vite,
65
+ Next.js, Expo, and SwiftUI, each with its config, scripts, and a passing
66
+ suite, are in https://github.com/tester-army/e2e/tree/main/examples.
65
67
 
66
68
  | Topic | File | Read it when |
67
69
  | --- | --- | --- |
@@ -29,6 +29,14 @@ Subscription logins and local models:
29
29
  [setup](setup.md#subscriptions-and-api-keys). Keep `ai@^7` installed with
30
30
  any provider.
31
31
 
32
+ Decision models pick actions from choices the executor builds: install
33
+ `@e2e-dev/decision` and put `decisionExecutor({ model:
34
+ typeSafeAi.decisionModel('jev-latest'), textModel: openrouter('inception/mercury-2.5') })`
35
+ under `executor`. Tests stay plain language with no params; the text model
36
+ writes field values. See the shipped `docs/decision-models.mdx` or
37
+ [the online guide](https://e2e.tester.army/docs/decision-models) for setup,
38
+ gates, and limits.
39
+
32
40
  - Pass a model instance, not a string (`INVALID_CONFIG`).
33
41
  - An agents entry is one plain object of `model`, `judge`, `system`,
34
42
  `context`, `tools`, `maxSteps`, `maxModelCalls`, `judgmentTimeout`,
@@ -162,6 +170,9 @@ expect(data.titles).toContain('Buy milk');
162
170
  - Judgments see the assertion and the current screen only, never prior
163
171
  steps or the act loop's summaries; malformed output gets one repair
164
172
  round, then `MODEL_OUTPUT_INVALID`.
173
+ - A value shown in more than one place (a total in the summary and on the
174
+ pay button) must agree everywhere, or the judgment fails. Name the one
175
+ you mean (`'the order summary total is $42.00'`) when only it matters.
165
176
  - `waitFor` observes every `interval` (default 3 s), judges only when the
166
177
  screen changed, and is `STEP_TIMEOUT` after `timeout` (default 30 s).
167
178
  - `extract` takes any Standard Schema validator (zod works); the model sees
@@ -65,7 +65,7 @@ jq '.run.results[] | select(.selected and .status != "passed") | .attempts[-1]
65
65
  | `AUTOMATION_UNSUPPORTED` | The step needs an interaction the engine's toolset lacks (drag on a device, say); blocked, exit 1 | Do that step with `screen` actions, or run the test on a target that supports it |
66
66
  | `STEP_TIMEOUT`, `STEP_BUDGET_EXHAUSTED` | Goal too big or ambiguous, or a slow provider | Split the goal, use on-screen wording, add vocabulary via `agents.<name>.context` or `agentContext`; raise `maxSteps` or `maxModelCalls` (budget), the config `timeout` (`act`), `judgmentTimeout` (judgments); `--debug` |
67
67
  | `CONTEXT_OVERFLOW` | Screen plus step history did not fit the model's context window (`act` after one shrink-and-retry; a judgment on the first overflow) | Lower `agents.<name>.maxObservationBytes`, split the step, or pick a larger-window model |
68
- | `POLICY_DENIED` | A URL whose scheme is not `http:` or `https:` (`file:`, `view-source:`, `data:`); a password `Secret` into a non-password sink; reading a secure field, `toHaveValue`, `toHaveText`, `toContainText`, `toHaveAttribute` included; `app.screenshot()` after a secret fill | http(s) or `about:blank` only; passwords into password inputs only; assert the outcome, not the value; screenshot before filling secrets |
68
+ | `POLICY_DENIED` | A URL whose scheme is not `http:` or `https:` (`file:`, `view-source:`, `data:`); a password `Secret` into a non-password sink; reading a secure field, `toHaveValue`, `toHaveText`, `toContainText`, `toHaveAttribute` included; `app.screenshot()` after a secret fill, or an `act` or `vision: true` step whose tree read timed out after one | http(s) or `about:blank` only; passwords into password inputs only; assert the outcome, not the value; screenshot before filling secrets |
69
69
  | `UNSUPPORTED_CAPABILITY` | A fixture the engine lacks (`browser` on a device), `schema` or `vision` on `act`, an action the surface lacks | Declare `requires: ['browser']`; drop the option or action |
70
70
  | `SESSION_UNAVAILABLE`, `SESSION_CONTRACT` | `session: 'x'` with no setup saving `x`; a setup that skipped a declared name. `SESSION_MISMATCH`, `SESSION_EXPIRED`, `SESSION_INVALID`: the stored session is another run's or app's, expired, or corrupt | Add or fix the `test.setup`; rerun it |
71
71
  | `ONLY_IN_CI` | `test.only` reached CI | Remove it |
@@ -24,8 +24,9 @@ No model is needed: without the optional `ai` package, sessions open and the
24
24
  deterministic tools work.
25
25
 
26
26
  Flags: `--config <path>` names the default config file, `--target <name>`
27
- fixes the target every session opens on, `--headless` hides the browser or
28
- simulator (sessions are headed by default outside CI), `--max-sessions <n>`
27
+ fixes the target every session opens on, `--headed` shows the browser or
28
+ simulator when the engine supports it in sessions that do not set `headed`
29
+ (sessions are headless by default, as in `e2e run`), `--max-sessions <n>`
29
30
  sets how many sessions may be open at once (default 4, 1 through 16).
30
31
 
31
32
  ## Tools
@@ -34,7 +35,7 @@ Four tools; everything a session can do is a catalog behind `call`.
34
35
 
35
36
  | Tool | Does |
36
37
  | --- | --- |
37
- | `open_session` | Loads the config (`config` names another file; default the nearest `e2e.config.ts`), starts the declared app command if any, boots the engine, opens the app URL, and returns the session id, the catalog, and the first observation. `target` is required when the config declares several. Each session has its own browser or device. |
38
+ | `open_session` | Loads the config (`config` names another file; default the nearest `e2e.config.ts`), starts the declared app command if any, boots the engine, opens the app URL, and returns the session id, the catalog, and the first observation. `target` is required when the config declares several. `headed: true` shows the browser or simulator when the user wants to watch; without it the session follows the server's `--headed`, headless by default. Each session has its own browser or device. |
38
39
  | `tools` | The catalog: one line per tool with its argument names (`?` marks optional), the first sentence of its description, and `[read-only]` where it changes nothing. `tools {tool}` shows the full description and the JSON Schema of its arguments. |
39
40
  | `call` | Runs one catalog tool: `call {tool: "tap", args: {target: "n42"}}`. Arguments are checked against the tool's schema first; a wrong one fails with `INVALID_ARGUMENT` naming the field. |
40
41
  | `close_session` | Saves a recording still running, ends the attempt, disposes the engine, stops the app processes the session started once no other session uses them. |
@@ -57,6 +58,10 @@ The catalog, per session:
57
58
  | `stop_recording` | Stops it and returns the absolute path of each video file, under `<output>/videos/<session>/` (`.e2e` by default), or the URL of a provider's own recording. |
58
59
  | Project tools | Every `defineTool` in the agent's `tools` that applies to the target's platform, under its own name; an engine pack such as `mobileTools` adds `open_app`, `swipe`, `alert`. |
59
60
 
61
+ Project tools use their `toModelOutput` renderer for strings as well as
62
+ structured results. Its `error-text` and `error-json` outputs set the MCP
63
+ result's `isError: true`; treat them as failed calls.
64
+
60
65
  Once a secret is filled in the session, `screenshot` and the point tools
61
66
  (`tap_at`, `hover_at`, `type_at`, `press_at`, `select_at`) answer with a
62
67
  `PIXEL_TAINTED` line for the rest of it; act on listed nodes by id (topic
@@ -67,6 +67,7 @@ npx e2e list tests/signup.e2e.ts --tag smoke --reporter json
67
67
  With a `package.json` script `"test:e2e": "e2e run"`, pnpm forwards `--`
68
68
  literally: `pnpm test:e2e -- --headed` reaches e2e as `run -- --headed` and
69
69
  exits 2. Write `pnpm test:e2e --headed` or `pnpm exec e2e run --headed`.
70
+ With npm, use `npm exec -- e2e run --headed` to pass flags to e2e.
70
71
 
71
72
  ## The replay cache
72
73
 
@@ -176,7 +177,7 @@ jobs:
176
177
  - uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
177
178
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
178
179
  with:
179
- node-version: 26 # any Node >= 22.12
180
+ node-version: 26 # Node >= 24.8, or >= 22.22.3 on 22
180
181
  cache: pnpm
181
182
  - run: pnpm install --frozen-lockfile
182
183
  - run: pnpm exec e2e-web install chromium --with-deps
@@ -2,10 +2,13 @@
2
2
 
3
3
  ## Requirements
4
4
 
5
- - Node.js 22.12 or newer.
5
+ - Node.js 24.8 or newer, or 22.22.3 or newer on Node.js 22.
6
6
  - ES modules: `.ts` config, tests, helpers, and workspace packages exporting
7
7
  `.ts` source load as ESM regardless of the nearest `package.json` `type`
8
8
  (CommonJS packages need no change); never `require` or `module.exports`.
9
+ Imports follow TypeScript: `./x.js` or `./x` loads `x.ts`, and the nearest
10
+ `tsconfig.json` `paths` and `baseUrl` apply. Decorators need
11
+ `experimentalDecorators`; CommonJS TypeScript goes in `.cts`.
9
12
  - Browser tests: `@e2e-dev/web`, pinning `playwright-core` exactly; do not
10
13
  add `playwright` for it. Missing browsers download on first boot; in CI run
11
14
  `npx @e2e-dev/web install chromium --with-deps` (pnpm: `pnpm exec e2e-web
@@ -354,3 +357,8 @@ export default {
354
357
  - `.gitignore` lists the `.e2e/` outputs (init adds them); drop the
355
358
  `.e2e/cache/` line to commit replays.
356
359
  - CI runs the whole suite, agent steps included, on PRs; see `running`.
360
+
361
+ ### Existing MCP registration
362
+
363
+ Re-running `init` preserves the e2e MCP server's environment and client settings
364
+ while repairing its launch command. A current registration is left unchanged.
@@ -203,7 +203,7 @@ field and types one character per `delay`, plain string only (a `Secret` is
203
203
  Coordinates are CSS pixels, for what the tree does not list: `tap({ position:
204
204
  { x, y } })` offsets from the node's top-left corner (with `modifiers` it is
205
205
  `INVALID_ARGUMENT`), `screen.tapAt({ x, y })` taps a viewport point,
206
- `screen.swipe({ from, to })` swipes along a path,
206
+ `screen.swipe({ from, to, duration? })` swipes along a path,
207
207
  `screen.swipe({ direction, momentum? })` swipes the viewport. Prefer a
208
208
  locator; a point moves with the layout.
209
209