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,292 @@
1
+ ---
2
+ title: Security model
3
+ description: What the model can access, how the runner handles secrets, and the limits of its protections.
4
+ ---
5
+
6
+ Test code and configuration run with your operating-system permissions.
7
+ This includes engines, tools, reporters, stores, secret providers, and app
8
+ commands. e2e does not sandbox them. Run untrusted pull request code in an
9
+ external sandbox without secrets or write tokens.
10
+
11
+ The model receives app content as untrusted evidence. It requests actions
12
+ through tools; the runner validates and performs those actions. Cached
13
+ actions also run with your permission, so review shared cache entries as
14
+ you would test code.
15
+
16
+ ## What the model sees
17
+
18
+ Model requests separate runner policy and trusted project context from app
19
+ content. App content cannot grant tools, credentials, or a larger budget.
20
+ [How agent steps work](/agent-steps) explains what a step shows the model and
21
+ what it may do; this page lists the trust boundary.
22
+
23
+ | In the prompt | Form |
24
+ | --- | --- |
25
+ | Your instruction and params | Quoted inside `<instruction>` and marked as data. A `Secret` param is projected to its name and purpose. |
26
+ | The screen | An accessibility tree with node ids, roles, names, text, test ids, link targets, and states, cut at a size limit. No selectors. A link's query and fragment, where tokens ride, show only as `?…` and `#…`. |
27
+ | The location | The current URL, redacted. |
28
+ | A screenshot | When needed or requested, only if the engine proves secure fields are masked and no secret has been filled in the attempt. |
29
+ | Prior steps | `agent.act` receives bounded records from the same test or serial group. Judgments use the current screen and instruction, without prior-step records. |
30
+
31
+ Secure fields appear as `value=<secure>`. Known secret values in model input
32
+ and reports become `<secret:name>`. This [redaction](#redaction) has limits
33
+ for transformed values, images, and app logs.
34
+
35
+ ## What the model may do
36
+
37
+ Built-in tools have closed schemas and are offered only when the engine
38
+ supports them. Project tools must use `defineTool` and cannot replace a
39
+ built-in name. Only read-only project tools may request an observation.
40
+
41
+ A call to a tool the step does not offer, or with an argument its schema does
42
+ not declare, never runs: the model reads the refusal and tries again. A node
43
+ id that is not on the current screen fails the action with
44
+ `LOCATOR_NOT_FOUND`. The runner never evaluates model text as code,
45
+ selectors, shell commands, or config.
46
+
47
+ A tool call that breaks a rule fails with `POLICY_DENIED`:
48
+
49
+ | Denied | Why |
50
+ | --- | --- |
51
+ | Navigation to `file:`, `data:`, or `javascript:` | These URL schemes are forbidden. |
52
+ | A malformed URL | Nothing is repaired. |
53
+ | A `Secret` reaching a JSON sink | Handles carry no value, and no value may be serialized. |
54
+ | Reading a value, text, or attribute from a secure field, with a getter or a `toHaveValue`, `toHaveText`, `toContainText`, or `toHaveAttribute` matcher | Assert the outcome, not the secret. A withheld value is redacted, not empty or absent. |
55
+ | `type_secret` with an undeclared name | Only secrets in the step's params can be filled. |
56
+ | `type_secret` into a disabled or non-editable node | Sinks are `textbox`, `searchbox`, and `combobox`. |
57
+ | A password into a field that is not a password field | Purpose must match; a generic secret fills any editable input. |
58
+ | A project tool not made with `defineTool`, or without `execute` | Undeclared semantics are not trusted. |
59
+ | A mutating project tool requesting an observation | Observe in a separate call. |
60
+ | `vision: 'only'` when pixels are withheld | There is nothing to fall back to. |
61
+ | `app.screenshot()` after a secret fill | The app may display the secret outside a secure field. |
62
+
63
+ ## Navigation and origins
64
+
65
+ Navigation and secret fills have no origin allowlist. The agent can follow
66
+ the app to other sites and use the secrets supplied to the current step
67
+ there. Password fills still require a password field.
68
+
69
+ The browser engine uses a hostname-based site check for two purposes:
70
+
71
+ - Configured `headers` are added to requests on the target's site.
72
+ - Child frames outside that site, and `data:` frames, are omitted from
73
+ observations.
74
+
75
+ The site check approximates the registrable domain without a public suffix
76
+ list. Shared hosting domains such as `vercel.app` count as one site, so
77
+ headers can reach other hosts under that domain. `basicAuth` is broader:
78
+ it answers a `401` challenge from any origin.
79
+
80
+ The runner checks forbidden schemes on explicit navigation: `app.open`, the
81
+ agent's `navigate` verb, and `device.openLink`. `device.openApp` and the
82
+ `open_app` tool take an app id and refuse a link, since the device would
83
+ open one as a URL. The runner does not re-check redirects or attach to popup
84
+ pages. Register `browser.onDialog` to handle browser dialogs.
85
+
86
+ ## Secrets
87
+
88
+ A `Secret` is an opaque handle with no plaintext accessor. Three sinks
89
+ accept one: a locator's `fill`, the params of an agent step, and an engine
90
+ option that declares it (`web({ basicAuth: { password } })`). The
91
+ [Signing in](/authentication) guide covers usage; the rules are here.
92
+
93
+ Before a model-directed fill, the runner checks that the step declared the
94
+ secret, the secret is configured, and the target is an enabled editable
95
+ input. Passwords require a password field. It then resolves the value on the
96
+ host and passes it to the engine. Replay repeats these checks.
97
+
98
+ A deterministic `fill(secret)` uses the field chosen by test code without
99
+ those model-directed field checks. It still registers the value for
100
+ redaction and disables pixels for the rest of the attempt.
101
+
102
+ ### Screenshots after a secret fill
103
+
104
+ After any secret fill, for the rest of the attempt, screenshot and pixel
105
+ tools are unavailable, `app.screenshot()` is denied, and the runner omits
106
+ assertion and failure screenshots. The app could display the value anywhere,
107
+ beyond the field's masked rectangle.
108
+
109
+ A test that restores a [session](#sessions) whose setup filled a secret keeps
110
+ this protection. A setup that signs in without filling a secret, for example
111
+ by setting a session cookie with `browser.setCookies`, leaves screenshots
112
+ available to the tests that restore it. A value it uses that is not a
113
+ configured secret is not redacted.
114
+
115
+ ### Secrets in engine options
116
+
117
+ A secret an engine option holds (`web({ basicAuth: { password: secrets.get(name) } })`)
118
+ is resolved on the host when each attempt starts and registered for redaction
119
+ then, together with what the engine derives from it (for basic auth, the
120
+ base64 `user:password` the `Authorization` header carries). It is not a fill
121
+ and is protected as text only: reports, `screen.txt`, failure pages, what the
122
+ model and an `e2e mcp` session read, text downloads, and the Playwright
123
+ trace's text (the options the browser opened with, and the `Authorization:
124
+ Basic` header of every request) are redacted, while screenshots, the trace's
125
+ screencast frames, and the model's pixels are kept. A page that renders the
126
+ password on screen is not masked in pixels.
127
+
128
+ ### Redaction
129
+
130
+ The runner replaces every occurrence of a registered secret value with
131
+ `<secret:name>` in model input, reports, a worker process's console output,
132
+ and the trace text of a session a secret was filled on or an engine option
133
+ holds. It matches the value as written, JSON-escaped, HTML-escaped, and
134
+ percent-encoded, in any letter case (so a CSS `text-transform` does not hide
135
+ it), and with its whitespace collapsed. Text cut at a length limit is masked
136
+ too when it ends with the first 8 or more characters of a value (half of a
137
+ value shorter than 16). Redaction rewrites only what the runner reports;
138
+ assertions still compare the text the page shows. Secret values never enter
139
+ cache entries.
140
+
141
+ A static value shorter than 6 characters is `INVALID_CONFIG`, and a provider
142
+ returning one fails the fill: every occurrence of so short a value would take
143
+ ordinary text with it.
144
+
145
+ ### Where redaction stops
146
+
147
+ - A transformed form of the value, such as its last four characters, is not
148
+ the secret and passes through.
149
+ - A value encoded as a whole rather than character by character, base64 or
150
+ a hash, is a different string and passes through.
151
+ - A value the app renders in another Unicode normalization form, decomposed
152
+ accents where the secret has composed ones, passes through.
153
+ - Text that widens the whitespace inside the value, or drops it, passes
154
+ through.
155
+ - A value spread over several nodes, one character per cell as a PIN pad
156
+ shows it, is not one string in the tree and passes through.
157
+ - Text cut at a length limit is matched by its prefix only in the value's
158
+ own form: a case mapping that changes length (`ß` shown as `SS`) or
159
+ whitespace collapsed to one space escapes the prefix rule.
160
+ - When tests run in the runner's own process, with the config or tests
161
+ passed in memory by an embedding host, their console output is not
162
+ redacted.
163
+ - A Playwright trace from a session a secret reached, filled or held by the
164
+ engine, is rewritten before it is kept: any run of 8 or more characters of
165
+ a value becomes `<secret:name>`, so unrelated text that shares 8 characters
166
+ with a value is masked too. A base64 or base64url run that decodes to a
167
+ value or such a fragment (a basic-auth header, a cookie, a token segment)
168
+ is replaced whole. After a fill, screencast frames are dropped; with only
169
+ an engine-held secret they are kept. An image the app served that
170
+ draws the secret survives. A trace the runner cannot rewrite is deleted and
171
+ the attempt records `TRACE_WITHHELD`.
172
+ - A video masks nothing and is labeled `redaction: "incomplete"`. It is
173
+ never recorded unless you ask. A recording a hosted browser or
174
+ device service keeps stays with that service; the report holds only its
175
+ URL.
176
+ - A download is what the app served. Once a secret reached the session,
177
+ filled or held by the engine, a text download (`text/*` or JSON by file
178
+ extension) is rewritten the way trace text is and labeled
179
+ `redaction: "complete"`. Any other download, a binary or one from a
180
+ session no secret reached, is kept as served and labeled
181
+ `redaction: "incomplete"`.
182
+ - The `headers` a protected preview needs are recorded in the trace as
183
+ request headers. Share that trace as you would the bypass secret.
184
+ - `command.log` is captured as the process writes it.
185
+ The runner does not redact them. Keep secrets out of your app's stdout.
186
+
187
+ ## Sessions
188
+
189
+ Saved sessions contain authentication state. They are encrypted and last
190
+ for one run. A session also carries the secret values the setup test resolved
191
+ from a provider before saving it, inside the same ciphertext, and whether a
192
+ secret was filled on it. The test that restores the session redacts those
193
+ values and keeps its pixels withheld, so a stored token the app echoes back
194
+ never reaches a failure message, a report, or the model. The envelope names
195
+ the secrets in the clear, authenticated; the values are never written
196
+ unencrypted.
197
+
198
+ | Property | Value |
199
+ | --- | --- |
200
+ | Location | `sessions/<runId>/` under the [output directory](/reference/config#output) (default `.e2e/sessions/<runId>/`), directory mode `0700`, files `0600` |
201
+ | Cipher | AES-256-GCM, a fresh 12-byte IV per save |
202
+ | Key | 32 random bytes generated per run and held in memory. Workers receive it over IPC, never through disk or the environment. |
203
+ | Bound to | Run id, target, engine name and version, platform, and app identity, all authenticated as additional data |
204
+ | Lifetime | At most 24 hours, sooner if the saved state declares an earlier expiry |
205
+ | Cleanup | The directory is removed when the run ends |
206
+
207
+ Invalid, expired, missing, or mismatched sessions are refused. See the
208
+ [session errors](/reference/errors) for the corresponding codes.
209
+
210
+ ## Cache trust
211
+
212
+ Shared cache entries deserve the same review as test code. They can replay
213
+ actions without consulting a model and are not signed.
214
+
215
+ The runner validates entries before use. Invalid entries become misses.
216
+ Files are named by a hash of the expected key and refused if larger than
217
+ 1 MiB. Entries store actions and a redacted summary; replay executes only
218
+ the actions. Secret fills store a name and authorize the fill again.
219
+
220
+ `e2e init` ignores `.e2e/cache/` in Git. If you choose to
221
+ [commit recordings](/cache#commit-the-replay-cache), review their actions and typed
222
+ values. CI defaults to read-only when no cache mode is configured.
223
+
224
+ ## Artifacts and logs
225
+
226
+ By default everything a run writes lands under `.e2e/`. The
227
+ [`output`](/reference/config#output) option or `--output` moves it, the
228
+ replay cache stays at `cache.dir`, and `command.log` is whatever
229
+ project-relative path you configure.
230
+
231
+ | Path | Contents | Redaction |
232
+ | --- | --- | --- |
233
+ | `report.json` | The run record. Errors, reasoning excerpts, and action summaries pass the redactor. | complete |
234
+ | `artifacts/.../*.png` | Screenshots. Secure fields are masked before screenshots are returned or saved as artifacts. | complete |
235
+ | `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 |
236
+ | `artifacts/.../*.webm`, `*.mp4` | Video, opt-in through `video` or `--video`; `retain-on-failure` keeps only the attempts that did not pass. | incomplete |
237
+ | `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 |
238
+ | `artifacts/.../failure/screen.txt` | The redacted tree at the moment of failure. | complete |
239
+ | `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 |
240
+ | `sessions/<runId>/` | Encrypted sessions, deleted at cleanup. | encrypted |
241
+ | `videos/<session>/*.webm`, `*.mp4` | Videos an `e2e mcp` session recorded with `start_recording`, kept until you delete them. Never handed to `artifacts.store`. | none |
242
+ | `cache.dir` (default `.e2e/cache/`) | Replay cache entries. Not under `output`. | secret-free by construction |
243
+ | The path in `command.log` | Your app's stdout and stderr, verbatim. Must be inside the project root. | none |
244
+
245
+ An `artifacts.store` receives each artifact's bytes as they are produced,
246
+ after trace and download redaction, with the `redaction` label the report
247
+ records, so a store can export only what the runner vouches for. A
248
+ `cache.store` receives every recorded entry and supplies every replayed one;
249
+ what comes back is validated again before use.
250
+
251
+ ## Outbound connections
252
+
253
+ Built-in features may connect to:
254
+
255
+ - one telemetry request per CLI invocation to `eu.i.posthog.com`, unless
256
+ opted out; see [Telemetry](/telemetry)
257
+ - one request to `eu.i.posthog.com` per `e2e feedback` you run; see
258
+ [Feedback](/telemetry#feedback)
259
+ - the provider endpoint used by your configured model. Deterministic tests
260
+ and fully replayed action steps make no model call.
261
+ - readiness probes against the `readyUrl` of the app the runner starts
262
+ - a Playwright browser download, once, when the browser is missing
263
+ - the DevTools endpoint you name in `web({ connect })`
264
+ - the GitHub API, only from the opt-in `@e2e-dev/github` reporter
265
+ - the Kernel API and its hosted browsers, only from
266
+ [`kernel()`](/integrations/kernel)
267
+ - the Expo API and the simulators' agent-device daemons, only from
268
+ [`easSimulators()`](/integrations/eas). It authenticates with `EXPO_TOKEN`,
269
+ else with the eas-cli login in `~/.expo/state.json`: on a CI runner, set
270
+ `EXPO_TOKEN` or keep that login out of the runner's home
271
+ - the app under test, and whatever that app itself loads
272
+
273
+ There is no crash reporting or update check. `--ai-trace` writes a local
274
+ file without uploading it. Your test code, plugins, and app can make
275
+ additional connections.
276
+
277
+ ## MCP and explore
278
+
279
+ `e2e mcp` uses stdio and the same action authorization as a test run. It
280
+ allows four open sessions at once by default
281
+ ([`--max-sessions`](/reference/mcp), 1 through 16) and closes idle sessions.
282
+
283
+ `e2e explore` offers every configured credential to every step; see
284
+ [Exploring without a test](/explore#your-config-applies).
285
+
286
+ ## Reporting a vulnerability
287
+
288
+ Report privately through
289
+ [GitHub security advisories](https://github.com/tester-army/e2e/security/advisories/new)
290
+ or by mail to security@tester.army. The
291
+ [security policy](https://github.com/tester-army/e2e/blob/main/SECURITY.md)
292
+ states response times and supported versions.
@@ -0,0 +1,231 @@
1
+ ---
2
+ title: Starting your app
3
+ description: Point the runner at a running app, or let it start the app for the run.
4
+ ---
5
+
6
+ Set your app's URL in the target's `app`. If you also provide a start
7
+ command, the runner starts the app, waits until it is ready, and stops it
8
+ when the run ends.
9
+
10
+ ## Point at a running app
11
+
12
+ Import `web` from `@e2e-dev/web` and put this in the exported
13
+ config object:
14
+
15
+ ```ts title="e2e.config.ts"
16
+ export default {
17
+ targets: [{
18
+ engine: web(),
19
+ app: { url: process.env.APP_URL ?? 'http://localhost:3000' },
20
+ }],
21
+ } satisfies E2EConfig;
22
+ ```
23
+
24
+ `app.open('/path')` resolves against `app.url`. This config reads `APP_URL`, so
25
+ you can change the address for a run without editing the file. URLs without
26
+ a scheme use HTTPS, or HTTP for loopback hosts.
27
+
28
+ ## Let the runner start it
29
+
30
+ Add `app.command` and the runner spawns the dev server, waits until `app.url`
31
+ answers, and stops it when the run ends, fails, or is interrupted:
32
+
33
+ ```ts title="e2e.config.ts"
34
+ import type { E2EConfig } from 'e2e';
35
+ import { web } from '@e2e-dev/web';
36
+ import { gateway } from 'ai';
37
+
38
+ export default {
39
+ agents: {
40
+ default: {
41
+ model: gateway('openai/gpt-6-luna-fast'),
42
+ system: 'You are a thorough QA agent. Verify every outcome.',
43
+ },
44
+ },
45
+ targets: [{
46
+ engine: web(),
47
+ app: {
48
+ url: process.env.APP_URL ?? 'http://localhost:3000',
49
+ command: {
50
+ executable: 'npm',
51
+ args: ['run', 'dev'],
52
+ reuseExisting: true,
53
+ log: '.e2e/logs/app.log',
54
+ },
55
+ },
56
+ }],
57
+ } satisfies E2EConfig;
58
+ ```
59
+
60
+ ```text
61
+ ✓ target "web" command ready 1.20s
62
+ ```
63
+
64
+ The main command options are:
65
+
66
+ | Option | Purpose |
67
+ | --- | --- |
68
+ | `reuseExisting: true` | Use an already-running server locally. CI ignores this and reports `APP_ALREADY_RUNNING`. |
69
+ | `log` | Save the app's stdout and stderr to a project-relative file. Startup errors include the last 20 lines, with `command.env` values redacted. The log file itself is unredacted. |
70
+ | `env` | Environment variables for the app. The child inherits `PATH`, `HOME`, and the temp-directory variables, plus these. |
71
+ | `startupTimeout` | How long to wait for readiness, 60 seconds by default. |
72
+
73
+ To probe a health endpoint instead of the base URL, set `readyUrl` beside
74
+ `command`: `app: { url, command, readyUrl: 'http://localhost:3000/health' }`.
75
+
76
+ The command runs without a shell. `executable` is resolved on `PATH` and
77
+ `args` are passed as written. The child inherits only `PATH`, `HOME`, the
78
+ temp-directory variables, and `env`, so pass what the app needs explicitly.
79
+ See the
80
+ [command reference](/reference/config#command) for all options.
81
+
82
+ ### Let the runner pick the port
83
+
84
+ Use port `0` to run multiple checkouts without a port conflict. The runner
85
+ chooses a free port and replaces `{port}` in the command's `args` and `env`.
86
+ Your app command must use that value:
87
+
88
+ ```ts
89
+ engine: web(),
90
+ app: {
91
+ url: 'http://127.0.0.1:0',
92
+ command: { executable: 'pnpm', args: ['dev', '--port', '{port}'], env: { PORT: '{port}' } },
93
+ },
94
+ ```
95
+
96
+ Tests can read the allocated URL from `app.baseUrl`. Cache entries remain
97
+ valid when the port changes. Use `127.0.0.1` or `[::1]`, matching the address
98
+ the command binds; `localhost:0` is rejected. See
99
+ [free ports](/reference/config#free-ports) for details.
100
+
101
+ ### Next.js dev targets
102
+
103
+ Next.js 16 blocks cross-origin requests to its dev resources. A target that
104
+ opens `127.0.0.1` or `[::1]` while `next dev` identifies as `localhost`
105
+ renders and never hydrates. Locators resolve and clicks land, but no React
106
+ handler runs, so the test fails and every retry fails the same way.
107
+
108
+ Add the host the target opens to `allowedDevOrigins`:
109
+
110
+ ```ts title="next.config.ts"
111
+ import type { NextConfig } from 'next';
112
+
113
+ const nextConfig: NextConfig = {
114
+ allowedDevOrigins: ['127.0.0.1'],
115
+ };
116
+
117
+ export default nextConfig;
118
+ ```
119
+
120
+ Use `'[::1]'` in the list when `app.url` opens `http://[::1]:0`.
121
+
122
+ A target on `localhost` needs no entry, because `next dev` identifies as
123
+ `localhost`. A production `next start` target is unaffected either way.
124
+
125
+ ## Dependencies
126
+
127
+ <Note>
128
+ This version starts only `app.command` (one process for every target that
129
+ declares the same command). Databases, mocks, and other processes the app
130
+ needs are not started by the runner yet: bring them up before the run, or
131
+ have `app.command` start a script that brings them up and serves the app. A
132
+ services API returns in a later release.
133
+ </Note>
134
+
135
+ ## Several targets
136
+
137
+ Two browsers on one app are two targets that each name it:
138
+
139
+ ```ts title="e2e.config.ts"
140
+ import type { E2EConfig } from 'e2e';
141
+ import { web } from '@e2e-dev/web';
142
+
143
+ const app = { url: 'http://localhost:3000' };
144
+
145
+ export default {
146
+ targets: [
147
+ { name: 'chromium', engine: web(), app },
148
+ { name: 'webkit-mobile', engine: web({ browser: 'webkit', viewport: { width: 390, height: 844 } }), app },
149
+ ],
150
+ } satisfies E2EConfig;
151
+ ```
152
+
153
+ Two targets that declare the same `app.command` share one process. Every
154
+ test runs on every target unless it is scoped with `platforms` or
155
+ `requires`; `--target <name>` selects at run time. A device target sits
156
+ beside a browser target in the same list; see the
157
+ [Mobile engine reference](/reference/mobile). One target per screen size is
158
+ how a suite runs at several [viewports](/viewports).
159
+
160
+ ## Protected previews
161
+
162
+ Use `headers` to access protected previews or skip a tunnel's interstitial
163
+ page. For example, a Vercel preview can use a protection bypass header:
164
+
165
+ ```ts title="Target fragment"
166
+ engine: web({
167
+ headers: {
168
+ 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET ?? '',
169
+ },
170
+ }),
171
+ app: {
172
+ url: process.env.PREVIEW_URL ?? 'http://localhost:3000',
173
+ identity: 'storefront',
174
+ },
175
+ ```
176
+
177
+ `headers` apply to requests on the target's site, determined by its hostname.
178
+ The check also includes subdomains and treats shared suffixes such as
179
+ `vercel.app` as one site. See [header scope](/security#navigation-and-origins)
180
+ before using a bypass secret. For ngrok, use
181
+ `'ngrok-skip-browser-warning': '1'`.
182
+
183
+ `app.identity` gives the app a stable cache identity when its preview URL changes.
184
+
185
+ Use `basicAuth` for HTTP basic authentication. The browser sends these
186
+ credentials when any origin requests them with a `401` challenge:
187
+
188
+ ```ts title="Target fragment"
189
+ engine: web({
190
+ basicAuth: { username: 'preview', password: process.env.PREVIEW_PASSWORD ?? '' },
191
+ }),
192
+ app: { url: 'https://staging.example.com' },
193
+ ```
194
+
195
+ To keep the password out of reports, logs, and what the agent reads, declare
196
+ it under `secrets` and pass `secrets.get(name)`. The engine resolves it when
197
+ each attempt starts, and the runner redacts it like any configured secret; a
198
+ name that `secrets` does not declare fails the config load with `INVALID_CONFIG`:
199
+
200
+ ```ts title="e2e.config.ts"
201
+ import type { E2EConfig } from 'e2e';
202
+ import { secrets } from 'e2e';
203
+ import { web } from '@e2e-dev/web';
204
+
205
+ export default {
206
+ targets: [
207
+ {
208
+ engine: web({
209
+ basicAuth: { username: 'preview', password: secrets.get('previewPassword') },
210
+ }),
211
+ app: { url: 'https://staging.example.com' },
212
+ },
213
+ ],
214
+ secrets: { previewPassword: () => process.env.PREVIEW_PASSWORD ?? '' },
215
+ } satisfies E2EConfig;
216
+ ```
217
+
218
+ <Note>
219
+ Setting `headers` disables the browser's HTTP cache and blocks service
220
+ workers. Playwright traces include request headers, so a protected-preview
221
+ trace can contain your bypass secret. Restrict access to those traces.
222
+ </Note>
223
+
224
+ <CardGroup cols={2}>
225
+ <Card title="Config reference" icon="sliders" href="/reference/config#the-app-under-test">
226
+ `app`, `command`, and every app key.
227
+ </Card>
228
+ <Card title="Web engine reference" icon="masks-theater" href="/reference/web">
229
+ Browser, viewport, `connect`, `headers`, `basicAuth`.
230
+ </Card>
231
+ </CardGroup>
@@ -0,0 +1,157 @@
1
+ ---
2
+ title: Subscriptions
3
+ description: Use your ChatGPT, GitHub Copilot, or SuperGrok subscription for agent steps.
4
+ ---
5
+
6
+ Use an existing subscription without a separate API key. Choose your plan
7
+ in `e2e init`, install dependencies, then sign in:
8
+
9
+ | Subscription | Command |
10
+ | --- | --- |
11
+ | ChatGPT Plus or Pro | `npx e2e login openai` |
12
+ | GitHub Copilot | `npx e2e login github-copilot` |
13
+ | SuperGrok or X Premium+ | `npx e2e login spacexai` |
14
+
15
+ Follow the terminal prompts, then run `npx e2e run`. Agent calls use your
16
+ plan's usage limits. For a new project, start with the [quickstart](/quickstart).
17
+
18
+ Claude subscriptions are not supported. To use Claude, configure an
19
+ [API provider](/models) or a model available through your Copilot plan.
20
+
21
+ ## Set up an existing project
22
+
23
+ `e2e init` leaves existing configs intact. To switch an existing project to
24
+ a subscription, install the AI SDK provider package the model wraps and
25
+ replace the model in your config. The sign-in flows ship with `e2e` itself:
26
+
27
+ <Tabs>
28
+ <Tab title="ChatGPT">
29
+ <CodeGroup>
30
+ ```bash npm
31
+ npm install -D ai @ai-sdk/openai
32
+ npx e2e login openai
33
+ ```
34
+
35
+ ```bash pnpm
36
+ pnpm add -D ai @ai-sdk/openai
37
+ pnpm exec e2e login openai
38
+ ```
39
+
40
+ ```bash bun
41
+ bun add -d ai @ai-sdk/openai
42
+ bunx e2e login openai
43
+ ```
44
+ </CodeGroup>
45
+
46
+ ```ts title="Model setup"
47
+ import { chatgpt } from 'e2e/oauth/chatgpt';
48
+
49
+ const agent = { model: chatgpt('gpt-6-luna') };
50
+ ```
51
+ </Tab>
52
+ <Tab title="GitHub Copilot">
53
+ <CodeGroup>
54
+ ```bash npm
55
+ npm install -D ai @ai-sdk/openai-compatible
56
+ npx e2e login github-copilot
57
+ ```
58
+
59
+ ```bash pnpm
60
+ pnpm add -D ai @ai-sdk/openai-compatible
61
+ pnpm exec e2e login github-copilot
62
+ ```
63
+
64
+ ```bash bun
65
+ bun add -d ai @ai-sdk/openai-compatible
66
+ bunx e2e login github-copilot
67
+ ```
68
+ </CodeGroup>
69
+
70
+ ```ts title="Model setup"
71
+ import { copilot } from 'e2e/oauth/copilot';
72
+
73
+ const agent = { model: copilot('claude-sonnet-5') };
74
+ ```
75
+ </Tab>
76
+ <Tab title="SuperGrok">
77
+ <CodeGroup>
78
+ ```bash npm
79
+ npm install -D ai @ai-sdk/xai
80
+ npx e2e login spacexai
81
+ ```
82
+
83
+ ```bash pnpm
84
+ pnpm add -D ai @ai-sdk/xai
85
+ pnpm exec e2e login spacexai
86
+ ```
87
+
88
+ ```bash bun
89
+ bun add -d ai @ai-sdk/xai
90
+ bunx e2e login spacexai
91
+ ```
92
+ </CodeGroup>
93
+
94
+ ```ts title="Model setup"
95
+ import { grok } from 'e2e/oauth/grok';
96
+
97
+ const agent = { model: grok('grok-4') };
98
+ ```
99
+ </Tab>
100
+ </Tabs>
101
+
102
+ Set `agents: { default: agent }` in `e2e.config.ts`. Choose a model
103
+ available to your account.
104
+
105
+ ## Pick a model
106
+
107
+ The constructor takes the id the vendor uses, which is not always the name
108
+ the app shows. Ask the vendor which ids your login serves:
109
+
110
+ <CodeGroup>
111
+ ```bash npm
112
+ npx e2e models # every stored login
113
+ npx e2e models openai # one provider
114
+ ```
115
+
116
+ ```bash pnpm
117
+ pnpm exec e2e models # every stored login
118
+ pnpm exec e2e models openai # one provider
119
+ ```
120
+
121
+ ```bash bun
122
+ bunx e2e models # every stored login
123
+ bunx e2e models openai # one provider
124
+ ```
125
+ </CodeGroup>
126
+
127
+ Each line is the id to pass to `chatgpt()`, `copilot()`, or `grok()`, the
128
+ vendor's display name, and what it says about the model: reasoning levels,
129
+ vision, preview, or hidden. ChatGPT lists what the Codex backend serves your
130
+ plan; Copilot lists the chat models of your plan; SuperGrok lists the
131
+ language models the token reaches.
132
+
133
+ ## Sign-in help
134
+
135
+ - ChatGPT opens a browser to sign in. Use `npx e2e login openai --device`
136
+ to sign in with a device code instead.
137
+ - Copilot reuses your GitHub CLI login. If you do not use `gh`, pass
138
+ `--client-id` for your own GitHub OAuth app with device flow enabled.
139
+ Use `--enterprise-url` for GitHub Enterprise.
140
+ - SuperGrok prints a device code and the URL where you enter it.
141
+
142
+ Run `npx e2e login` to choose a provider and see which accounts are signed
143
+ in. Run `npx e2e logout` to remove a saved login.
144
+
145
+ ## Saved credentials and CI
146
+
147
+ Logins are stored in `~/.config/e2e/oauth.json`, or under
148
+ `$XDG_CONFIG_HOME`, with file permissions restricted to your user. Tokens
149
+ refresh automatically. When a vendor rejects the saved token, or its refresh
150
+ token, the run fails with `LOGIN_REQUIRED` and names the
151
+ `npx e2e login <provider>` command to run.
152
+
153
+ Use API keys in CI. `E2E_OAUTH_CREDENTIALS` can supply credential JSON
154
+ instead of the local file, but refreshed tokens are not saved back to that
155
+ variable. A value containing a rotating refresh token cannot be reused
156
+ indefinitely. See [CLI reference](/reference/cli#e2e-login) for all sign-in
157
+ options.