diff --git a/.changeset/browser-install-verdict.md b/.changeset/browser-install-verdict.md
new file mode 100644
index 000000000..7f29699fd
--- /dev/null
+++ b/.changeset/browser-install-verdict.md
@@ -0,0 +1,5 @@
+---
+"@e2e-dev/web": patch
+---
+
+A headless run no longer downloads the full Chromium when the headless shell is installed, and the install no longer removes other browsers in the cache. Set `PLAYWRIGHT_SKIP_BROWSER_GC=0` to restore cleanup.
diff --git a/.changeset/cache-empty-typed-value.md b/.changeset/cache-empty-typed-value.md
new file mode 100644
index 000000000..a71198858
--- /dev/null
+++ b/.changeset/cache-empty-typed-value.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+A recorded step that types an empty value, which clears a field, replays instead of failing with `REPLAY_STALE` (`invalid-entry`). The cache wrote the entry but its reader rejected the empty `type` or `typeText` value, so such a step could never replay from a recording.
diff --git a/.changeset/engine-prepare-headed.md b/.changeset/engine-prepare-headed.md
new file mode 100644
index 000000000..f8baab3a4
--- /dev/null
+++ b/.changeset/engine-prepare-headed.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+`EnginePrepareInfo.headed` is now required, alongside the `headed` an engine already receives on `init`. A host that builds a `prepare` call now passes the run's mode, so an engine provisions for the browser that run launches.
diff --git a/.changeset/error-default-redactor.md b/.changeset/error-default-redactor.md
new file mode 100644
index 000000000..0a5672b5c
--- /dev/null
+++ b/.changeset/error-default-redactor.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+Run-level errors in reports (hook, collection, provisioning, and worker failures) are now redacted with the config's secrets.
diff --git a/.changeset/evaluate-string-expression.md b/.changeset/evaluate-string-expression.md
new file mode 100644
index 000000000..f1207b24a
--- /dev/null
+++ b/.changeset/evaluate-string-expression.md
@@ -0,0 +1,5 @@
+---
+'@e2e-dev/web': patch
+---
+
+`browser.evaluate` evaluates a string as an expression: `browser.evaluate('document.title')` and `browser.evaluate('(() => document.title)()')` return the title instead of failing with `EVALUATE_FAILED: ... is not a function`. A string that evaluates to a function, such as `'() => document.title'`, is still called with the argument.
diff --git a/.changeset/forced-conclusion-wrong-tool.md b/.changeset/forced-conclusion-wrong-tool.md
new file mode 100644
index 000000000..81a774a7a
--- /dev/null
+++ b/.changeset/forced-conclusion-wrong-tool.md
@@ -0,0 +1,5 @@
+---
+'e2e': patch
+---
+
+An agent step whose model answers a forced `complete_step` turn with another tool no longer fails as `MODEL_PROVIDER_FAILED`. The call goes back to the model as an error and the turn is asked again within the step's turn budget; a model that never concludes fails the step with `STEP_NO_CONCLUSION`.
diff --git a/.changeset/foreign-engine-errors.md b/.changeset/foreign-engine-errors.md
new file mode 100644
index 000000000..4bdb24c64
--- /dev/null
+++ b/.changeset/foreign-engine-errors.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+An error an engine throws with a runner code, such as `INVALID_LOCATOR` for an unsupported selector, keeps its code and category when the engine is loaded from `e2e.config.ts`. It was reported as an infrastructure `ENGINE_FAILURE`.
diff --git a/.changeset/in-process-stray-rejections.md b/.changeset/in-process-stray-rejections.md
new file mode 100644
index 000000000..db9aca57f
--- /dev/null
+++ b/.changeset/in-process-stray-rejections.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+A run that executes in the host process, such as `e2e explore`, no longer crashes that process when a test leaves a promise rejection or an exception uncaught. It handles them as a worker process does: a rejection fails the test running when it surfaces, or is recorded as `UNHANDLED_REJECTION` between tests. An uncaught exception, or a rejection that surfaces with no test running before any test has finished, is a run error that fails the test on its way with `WORKER_CRASH` and skips the rest of its file. The handlers are removed when the run ends.
diff --git a/.changeset/mcp-error-redaction.md b/.changeset/mcp-error-redaction.md
new file mode 100644
index 000000000..53f8ed958
--- /dev/null
+++ b/.changeset/mcp-error-redaction.md
@@ -0,0 +1,5 @@
+---
+'e2e': patch
+---
+
+`e2e mcp` now redacts secret values from everything `open_session` and `close_session` return and from its log lines. Before, an engine or app error that carried a configured secret, in a failed open or in a `close_session` "Cleanup:" line, reached the client and stderr verbatim.
diff --git a/.changeset/mcp-headless-default.md b/.changeset/mcp-headless-default.md
new file mode 100644
index 000000000..fcb8d361e
--- /dev/null
+++ b/.changeset/mcp-headless-default.md
@@ -0,0 +1,5 @@
+---
+"e2e": minor
+---
+
+`e2e mcp` sessions are now headless by default, the same as `e2e run` and `e2e explore`, and the agent decides per session: `open_session {headed: true}` shows the browser or simulator, `headed: false` hides it. `--headed` makes headed the default for sessions that do not say. Before, sessions were headed outside CI and only the server's `--headless` could hide them; `--headless` is still accepted, since it now asks for the default, but it is no longer documented.
diff --git a/.changeset/mcp-user-output-redaction.md b/.changeset/mcp-user-output-redaction.md
new file mode 100644
index 000000000..861fede6d
--- /dev/null
+++ b/.changeset/mcp-user-output-redaction.md
@@ -0,0 +1,5 @@
+---
+'e2e': patch
+---
+
+`e2e mcp` now redacts secret values from what your code prints through `console`, `process.stdout`, or `process.stderr`, such as a `console.log` in a project tool or in the config's top-level code. Before, it reached stderr verbatim. Output printed while a config loads appears once the config has loaded; if the load fails, that output is withheld, since the config's secrets are unknown. An uncaught error or unhandled rejection is logged redacted and the server shuts down its sessions and exits with code 1, instead of crashing with the raw error.
diff --git a/.changeset/npm-exec-cli-hint.md b/.changeset/npm-exec-cli-hint.md
new file mode 100644
index 000000000..a98ba22e1
--- /dev/null
+++ b/.changeset/npm-exec-cli-hint.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+Include npm's argument separator in CLI hints so flags such as `--headed` reach the installed `e2e` binary.
diff --git a/.changeset/retry-crash-attempts.md b/.changeset/retry-crash-attempts.md
new file mode 100644
index 000000000..8757ed80a
--- /dev/null
+++ b/.changeset/retry-crash-attempts.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+A worker that crashes during a retry no longer drops the attempts before it. Before, a test whose first attempt failed and whose retry crashed the worker showed a single attempt 0 with `WORKER_CRASH`, and the first attempt's error was lost. The report now keeps every finished attempt and adds the crash as the next one. A crash after the last attempt (in an `afterAll`) is recorded on that attempt instead of as a retry. For a serial group, the finished group attempts and the members that finished in the crashed attempt stay in the report, the member that was running fails with `WORKER_CRASH`, and a crash after a member finished fails the group attempt without blaming that member. A forced interrupt keeps the attempts that finished and the verdict they reach, `flaky` included.
diff --git a/.changeset/run-user-output-redaction.md b/.changeset/run-user-output-redaction.md
new file mode 100644
index 000000000..a156abcab
--- /dev/null
+++ b/.changeset/run-user-output-redaction.md
@@ -0,0 +1,5 @@
+---
+'e2e': patch
+---
+
+`e2e run` redacts secret values from what the config's top-level code, a test file's top level, and reporters print, as it already did for tests, including what a timer or a reporter the run stopped waiting for prints after the run. Output printed while the config loads appears once it has loaded, and is withheld if the load fails. With the `list` reporter, output printed during the run prints under `stdout | runner` or `stderr | runner`, on stdout like a test's output, so the live view no longer paints over it.
diff --git a/.changeset/strict-mode-reresolve.md b/.changeset/strict-mode-reresolve.md
new file mode 100644
index 000000000..69907e501
--- /dev/null
+++ b/.changeset/strict-mode-reresolve.md
@@ -0,0 +1,5 @@
+---
+'@e2e-dev/web': patch
+---
+
+An action whose locator gains a second match just before it runs, such as a screen transition that briefly shows the old and new page together, no longer fails with `ENGINE_FAILURE` and a Playwright strict mode violation. The locator resolves again: two matches still fail with `LOCATOR_AMBIGUOUS`, and a duplicate that went away lets the action run. A drag whose drop target gains a second match after the press fails with `ACTION_FAILED` and is not repeated.
diff --git a/.changeset/swipe-path-duration.md b/.changeset/swipe-path-duration.md
new file mode 100644
index 000000000..df8c58dbe
--- /dev/null
+++ b/.changeset/swipe-path-duration.md
@@ -0,0 +1,7 @@
+---
+"e2e": minor
+"@e2e-dev/mobile": minor
+"@e2e-dev/web": minor
+---
+
+`screen.swipe({ from, to, duration })` adds an optional `duration` for path swipes, controlling how long the gesture takes in milliseconds.
diff --git a/.changeset/tainted-fallback-policy-denied.md b/.changeset/tainted-fallback-policy-denied.md
new file mode 100644
index 000000000..45f2269e0
--- /dev/null
+++ b/.changeset/tainted-fallback-policy-denied.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+After a secret fill, an agent step whose screen can only be read from a fallback screenshot (`act`, or a judgment with `vision: true`) now fails with `POLICY_DENIED` naming the secret fill, instead of the engine's timeout.
diff --git a/.changeset/telemetry-identity.md b/.changeset/telemetry-identity.md
new file mode 100644
index 000000000..b3a538709
--- /dev/null
+++ b/.changeset/telemetry-identity.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+Telemetry no longer sends a project id outside git or in a shallow clone, and a coding agent that sets `CI` outside a CI vendor counts as the machine it runs on. EAS Build, Bitrise, Codemagic, and Xcode Cloud are recognized as CI vendors.
diff --git a/.changeset/web-hung-page-timeouts.md b/.changeset/web-hung-page-timeouts.md
new file mode 100644
index 000000000..41bbc6154
--- /dev/null
+++ b/.changeset/web-hung-page-timeouts.md
@@ -0,0 +1,6 @@
+---
+'@e2e-dev/web': patch
+'e2e': patch
+---
+
+A page whose script never yields no longer holds a web test until its test timeout: a tap, a locate, or a navigation on it now fails with `ACTION_FAILED` at its action or navigation timeout, also while a trace records. A tap or keystroke cut off that way may have reached the page and is never repeated by a replay. `expect(browser)` matchers on such a page end at their own timeout instead of the action timeout.
diff --git a/.changeset/worker-hang-watchdog.md b/.changeset/worker-hang-watchdog.md
new file mode 100644
index 000000000..07a7b271a
--- /dev/null
+++ b/.changeset/worker-hang-watchdog.md
@@ -0,0 +1,5 @@
+---
+'e2e': patch
+---
+
+A test that blocks its worker's event loop (`while (true) {}`) no longer hangs the run until Ctrl-C. When the worker has not responded `cleanupTimeout` (at least 5 s) after the test timeout, the runner kills it, reports the test `timed-out` with `TEST_TIMEOUT`, and runs the rest of the file on a new worker. A worker with a debugger attached when the test starts (an editor's auto-attach, or `NODE_OPTIONS=--inspect=0`) is never killed this way. Runs with an inline config or in-memory tests run in the runner's own process and still hang.
diff --git a/.fallowrc.json b/.fallowrc.json
index 7df1d7782..92733e667 100644
--- a/.fallowrc.json
+++ b/.fallowrc.json
@@ -25,6 +25,7 @@
],
"ignorePatterns": [
"docs/**",
+ "examples/**",
"**/next-env.d.ts",
"**/.e2e/**",
"packages/e2e/tests/tmp-projects/**"
diff --git a/.github/filters.yml b/.github/filters.yml
index dd3936055..de0304975 100644
--- a/.github/filters.yml
+++ b/.github/filters.yml
@@ -29,6 +29,7 @@ web:
mobile:
- *runner
- 'packages/mobile/**'
+ - 'packages/github/**'
- 'apps/mobile-benchmark/**'
- '.github/workflows/mobile.yml'
- '.github/actions/expo-app/**'
diff --git a/.oxlintrc.json b/.oxlintrc.json
index 5369d67fd..82795af8c 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -16,7 +16,9 @@
"packages/*/tests/fixtures/**",
// Vendored conformance data, kept byte for byte.
"packages/*/tests/conformance/*/**",
- "packages/*/tests/tmp-projects/**"
+ "packages/*/tests/tmp-projects/**",
+ // Standalone user-facing projects with their own installs; nothing here builds them.
+ "examples/**"
],
"rules": {
// Sequential execution is the contract of a test runner, not a perf bug.
diff --git a/AGENTS.md b/AGENTS.md
index 493187f2b..19c961701 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -66,6 +66,14 @@ suites that consume the built packages the way a user would.
`e2e/engine` only: the semantics every engine must reproduce
(error taxonomy, text and URL matching, assertion polling, JSON-value rules)
are exported there, and there is no `e2e/internal` subpath.
+- `packages/mobile` — the published `@e2e-dev/mobile` package: the
+ iOS/Android engine on agent-device, built with the same public
+ `defineEngine`, contributing the `device` fixture. Like the web engine it
+ depends on `e2e` (peer), never the reverse; the engine implementation
+ imports from `e2e/engine` only, and a target names it as `engine: mobile()`.
+ The `@e2e-dev/mobile/tools` subpath holds the agent-side `open_app`, `swipe`,
+ and `alert` tools; focused-field typing uses the engine's keyboard grammar.
+ The main entry never loads the AI SDK.
- `packages/kernel` - the published `@e2e-dev/kernel` package: Kernel hosted
browsers for the web engine. An official integration with a hosted service
is one package per service, named after it (`@e2e-dev/`), with the
@@ -77,7 +85,19 @@ suites that consume the built packages the way a user would.
hosted iOS simulators and Android emulators for the mobile engine
(`DeviceProvider`). Expo publishes no SDK for the sessions API, so it calls
Expo's GraphQL API with `fetch`, and `@e2e-dev/mobile` is its only peer.
-- `packages/testmu` - the published `@e2e-dev/testmu` package: TestMu AI
+- `packages/decision` — the published `@e2e-dev/decision` package: a
+ `StepExecutor` (`decisionExecutor()`) that drives `agent.act` and
+ `agent.assert` through an AI SDK *evaluation* model answering `choice`
+ questions with probability distributions, plus an optional small language
+ model that writes field values when the decision model picks `type`.
+ `minProbability` and `minConfidence` gate a chosen operation, target,
+ secret, or assertion verdict.
+- `packages/github` — the published `@e2e-dev/github` package: the reporter
+ that posts the run as one pull request comment from GitHub Actions and
+ keeps it current on reruns, writing the same text to the job summary. It
+ reads the token, event, and repository when the run finishes, never at
+ config load, and renders the page with `renderMarkdownReport` from `e2e`.
+- `packages/testmu` — the published `@e2e-dev/testmu` package: TestMu AI
(formerly LambdaTest) hosted Android emulators, iOS simulators, and real
devices for the mobile engine (`DeviceProvider`). agent-device's `testmu`
cloud provider is the vendor client: the package allocates agent-device
@@ -120,6 +140,14 @@ suites that consume the built packages the way a user would.
"Committed recordings" under Gotchas. Scenario files are copies: keep
diffs against the source minimal, and name no company a scenario was
distilled from.
+- `examples/` — standalone user-facing projects, one per technology
+ (`with-vite`, `with-next`, `with-expo`, `with-swiftui`), each the same
+ one-screen greeter demo with deterministic and agent tests. They install
+ the published packages from npm, sit outside the pnpm workspace, commit no
+ lockfile, and run in no CI; oxlint and fallow ignore them. A change runs
+ the example's suite by hand and updates the "Last checked" line in its
+ README. A SwiftUI example keeps its tests in an `e2e/` folder beside the
+ native project, as a user would.
- `docs/` (the Mintlify docs site; pages are the `.mdx` files under `docs/`,
navigation, theme, and redirects in `docs/docs.json`, extra CSS in
`docs/style.css`; `docs/examples/` is typechecked and shown verbatim on
@@ -218,7 +246,7 @@ thread handled, and labeled `Ready for Human Review`. "It compiles" and
## Testing quirks
-- Vitest 4, `pool: 'forks'`, two projects. `integration` is capped at
+- Vitest 5, `pool: 'forks'`, two projects. `integration` is capped at
`maxWorkers: 3` and runs in a later group — do not raise it; CPU starvation
produces timeouts indistinguishable from real failures.
- Integration tests write throwaway projects into
diff --git a/README.md b/README.md
index ee0caf714..5ccbb722b 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,10 @@
+
+
+
+
# e2e
[e2e](https://tester.army/e2e?utm_source=e2e&utm_medium=github&utm_campaign=readme_intro) is an end-to-end testing framework for web and mobile apps. Describe a goal in natural language and an agent drives the app to reach it. Check the result with locators and assertions in the same test.
@@ -40,6 +44,11 @@ npx e2e init
config and an example test. The
[quickstart](https://e2e.tester.army/docs/quickstart) covers the rest.
+To see a finished setup in your stack, open
+[`examples/`](https://github.com/tester-army/e2e/tree/main/examples):
+Vite, Next.js, Expo, and SwiftUI, each a standalone project with a passing
+suite.
+
## Packages
| Package | What it does |
diff --git a/apps/mobile-benchmark/package.json b/apps/mobile-benchmark/package.json
index ff74ce114..7043fa2e3 100644
--- a/apps/mobile-benchmark/package.json
+++ b/apps/mobile-benchmark/package.json
@@ -16,9 +16,9 @@
"capture:snapshots": "node node_modules/e2e/dist/cli/bin.js run --config e2e.capture.config.ts"
},
"dependencies": {
- "@react-navigation/bottom-tabs": "7.19.2",
- "@react-navigation/native": "7.4.1",
- "@react-navigation/native-stack": "7.19.2",
+ "@react-navigation/bottom-tabs": "7.20.0",
+ "@react-navigation/native": "7.5.0",
+ "@react-navigation/native-stack": "7.20.0",
"@stripe/stripe-react-native": "0.64.0",
"expo": "57.0.22",
"expo-audio": "57.0.5",
diff --git a/apps/web-benchmark/package.json b/apps/web-benchmark/package.json
index 887ee826c..34a73a73b 100644
--- a/apps/web-benchmark/package.json
+++ b/apps/web-benchmark/package.json
@@ -16,7 +16,7 @@
"dependencies": {
"e2e": "workspace:*",
"@e2e-dev/web": "workspace:*",
- "next": "16.3.6",
+ "next": "16.3.8",
"react": "19.3.0",
"react-dom": "19.3.0"
},
diff --git a/docs/agent-steps.mdx b/docs/agent-steps.mdx
index d8d4d0b61..4e3494913 100644
--- a/docs/agent-steps.mdx
+++ b/docs/agent-steps.mdx
@@ -1,6 +1,7 @@
---
title: How agent steps work
description: What the model sees, what it may do, how a step ends, and what it costs.
+icon: "list-check"
---
An agent step is one `agent.act`, `agent.assert`, `agent.waitFor`, or
diff --git a/docs/agents.mdx b/docs/agents.mdx
index 0ddaca16a..66fb7f7ae 100644
--- a/docs/agents.mdx
+++ b/docs/agents.mdx
@@ -1,6 +1,7 @@
---
title: Agents and personas
description: Configure several agents in one suite, pin tests to them, and run one flow as each persona.
+icon: "users"
---
Define named agents in `agents` to give tests different models, instructions,
diff --git a/docs/api-testing.mdx b/docs/api-testing.mdx
index 3002e20a2..b9373ff39 100644
--- a/docs/api-testing.mdx
+++ b/docs/api-testing.mdx
@@ -1,6 +1,7 @@
---
-title: Testing APIs
+title: API
description: Check your API with fetch and value matchers, in the same files and run as UI tests.
+icon: "server"
---
API tests run in the same files and the same run as UI tests. A test that
diff --git a/docs/assertions.mdx b/docs/assertions.mdx
deleted file mode 100644
index 2ab2d4dcc..000000000
--- a/docs/assertions.mdx
+++ /dev/null
@@ -1,94 +0,0 @@
----
-title: Assertions
-description: Check the screen with agent.assert, wait for a condition, and extract data.
----
-
-An assertion checks what is on screen. The agent has three judgment calls:
-`assert` checks the screen now, `waitFor` waits until a condition holds, and
-`extract` reads structured data. When you know the exact value, use an
-`expect` [locator assertion](#locator-assertions) instead, with no model call.
-
-```ts
-await agent.assert('the dashboard shows a trial badge');
-```
-
-## `agent.assert`
-
-`assert` checks a statement against the current screen. A false statement
-fails with `ASSERTION_FAILED`. If the screen does not provide enough evidence,
-it fails with `ASSERTION_INCONCLUSIVE`.
-
-The judging model sees the statement and the current screen. It does not see
-earlier steps or the acting agent's summaries.
-
-## `agent.waitFor`
-
-`assert` checks the screen now. Use `waitFor` when the condition may become true
-later. It polls the screen and only calls the model when the observation changes.
-
-```ts
-await agent.waitFor('the export finishes and a download link appears', {
- interval: 500,
- timeout: 120_000,
-});
-```
-
-## `agent.extract`
-
-`extract` reads structured data from the screen. Provide a
-[Standard Schema](https://standardschema.dev) validator, such as Zod, to define
-the result.
-
-```ts
-import { expect } from 'e2e';
-import { z } from 'zod';
-
-const data = await agent.extract('every todo title and how many remain', {
- schema: z.object({ titles: z.array(z.string()), remaining: z.number().int() }),
-});
-expect(data.titles).toContain('Buy milk');
-```
-
-
-For model-generated descriptions, check the relevant meaning rather than the
-entire sentence. Use a [locator assertion](/locators) when the exact text matters.
-
-
-## Judging pixels
-
-By default, judgments read a redacted text snapshot of the screen. Add
-`vision: true` for visual checks, such as charts or layout. Use
-`vision: 'only'` to judge the screenshot alone.
-
-```ts
-await agent.assert('the chart trends upward', { vision: true });
-await agent.assert('the search form is not covered by a banner', { vision: 'only' });
-```
-
-Images add input tokens. Screenshots are withheld for the rest of an attempt
-after a secret is filled. The [agent reference](/reference/agent#vision)
-lists the modes and restrictions.
-
-## Locator assertions
-
-`expect` on a locator checks an exact value and calls no model. It retries
-until the check passes or the assertion timeout runs out.
-
-```ts
-await expect(screen.getByRole('status')).toHaveText('2 remaining');
-await expect(screen.getByRole('heading', 'Dashboard')).toBeVisible();
-```
-
-[Locators](/locators) covers queries and actions. The
-[expect reference](/reference/expect#locator-matchers) lists every matcher.
-
-## Next
-
-
-
- Exact actions and checks with `screen` and `expect`.
-
-
- Verdicts, exit codes, and what the model sees.
-
-
diff --git a/docs/authentication.mdx b/docs/authentication.mdx
index 9a1cabd9e..aabeb896d 100644
--- a/docs/authentication.mdx
+++ b/docs/authentication.mdx
@@ -1,6 +1,7 @@
---
title: Signing in
description: Sign in once, reuse the session across tests, and pass secrets without exposing them to the model.
+icon: "key"
---
Sign in once in a setup test and save a named session. Other tests restore
@@ -164,7 +165,7 @@ runner keeps it away from test code and the model altogether.
A `Secret` has no `.value`. Three sinks accept one: the two below in a
test, and an engine option that declares it, as
-[`basicAuth`](/starting-your-app#protected-previews) does:
+[`basicAuth`](/web#protected-previews) does:
@@ -279,7 +280,7 @@ account, a username with the secrets that sign it in, while a secret is one
value, so `credentials.admin` and `secrets.admin` can both exist.
In `e2e.config.ts`, `secrets.get()` hands an engine option a secret it needs
-before any test runs, such as a [basic-auth password](/starting-your-app#protected-previews).
+before any test runs, such as a [basic-auth password](/web#protected-previews).
The config load checks the name, and the value is redacted once the engine
resolves it, together with the base64 `Authorization` credential it becomes.
The protection is text only: screenshots and the model's pixels are kept, so
diff --git a/docs/browser.mdx b/docs/browser.mdx
index 1079652bb..ec77a493c 100644
--- a/docs/browser.mdx
+++ b/docs/browser.mdx
@@ -1,6 +1,7 @@
---
title: Working with the browser
description: Mock an API, handle a dialog, download a file, reach into an iframe, and pick a browser.
+icon: "window-maximize"
---
`@e2e-dev/web` runs tests in Chromium, Firefox, and WebKit. It is built on
@@ -144,7 +145,7 @@ await browser.setCookies([{ url: 'http://localhost:3000', name: 'tz', value: 'UT
A cookie URL that does not parse, or uses a scheme other than `http:` or
`https:` (`about:blank` included), is `POLICY_DENIED`; a domain cookie is checked as the origin
it would be sent to. If any entry is refused, no cookies are set. To resize
-the page, see [Testing viewports](/viewports).
+the page, see [Viewports](/web#viewports).
## Pick a browser
@@ -183,7 +184,7 @@ bunx @e2e-dev/web install webkit --with-deps
`connect` attaches to a remote Chromium over CDP instead of launching
one; a [browser provider](#hosted-browsers) does the same and also leases
-and releases the browser. [Testing viewports](/viewports) covers sizes.
+and releases the browser. [Viewports](/web#viewports) covers sizes.
## Hosted browsers
@@ -198,7 +199,7 @@ provider, for [TestMu AI](/integrations/testmu-browsers),
`testmuBrowsers()` from `@e2e-dev/testmu/web` is the provider, and for any other
service a provider is a few dozen lines in your project. Add `viewport: null` when
the service shows the window in a live view: the page then
-[fills the window](/viewports#fill-the-window) instead of a fixed 1280 by
+[fills the window](/web#fill-the-window) instead of a fixed 1280 by
720 area of it.
```ts
@@ -286,7 +287,7 @@ same browser.
A provider against a typical session API has three requests: create a
session, poll it until it exposes a DevTools endpoint, stop it.
-```ts title="browser-provider.ts"
+```ts title="browser-provider.ts" expandable
import type { BrowserProvider } from '@e2e-dev/web';
/**
@@ -471,7 +472,7 @@ root declared in HTML (``) is out of reach.
Every `browser` method, its timeout, and what it throws.
-
- Let the runner start the dev server and reach protected previews.
+
+ Let the runner start the dev server, reach protected previews, and test viewports.
diff --git a/docs/bug-bash.mdx b/docs/bug-bash.mdx
index 56afda95b..ac10cb86c 100644
--- a/docs/bug-bash.mdx
+++ b/docs/bug-bash.mdx
@@ -1,6 +1,7 @@
---
title: Bug bashes
description: Run many explorations at once, then prove each bug with a failing test before anyone reads about it.
+icon: "bug"
---
A bug bash runs several `e2e explore` sessions at once, each on one area
@@ -50,7 +51,7 @@ itself: `npx e2e guide bug-bash`.
`reuseExisting: true` on the target's `command`, or declare the URL with
port `0` so each run starts its own copy. `reuseExisting` does not apply
when `CI` is set; use port `0` there. See
- [Starting your app](/starting-your-app).
+ [Starting your app](/web#starting-your-app).
- When the target's `app.command` brings up a stack of its own, such as a
database from `docker compose` that it removes on exit, start the stack once
and explore with a config that points at the running app and declares no
diff --git a/docs/cache.mdx b/docs/cache.mdx
index 03cab32e1..b236cdc29 100644
--- a/docs/cache.mdx
+++ b/docs/cache.mdx
@@ -1,6 +1,7 @@
---
-title: Caching agent steps
+title: Caching
description: Replay verified agent actions without model calls, and let the agent take over when the app changes.
+icon: "box-archive"
---
The replay cache records the actions in an `agent.act()` step after a later
diff --git a/docs/ci.mdx b/docs/ci.mdx
index d9d44987d..46546cf91 100644
--- a/docs/ci.mdx
+++ b/docs/ci.mdx
@@ -1,13 +1,14 @@
---
title: Continuous integration
description: Run tests on pull requests, upload reports, and configure model credentials in CI.
+icon: "rotate"
---
Run deterministic and agent tests in the same CI job. This workflow assumes
-your config includes a [command](/starting-your-app) to start the app and
+your config includes a [command](/web#starting-your-app) to start the app and
uses AI Gateway for model calls:
-```yaml title=".github/workflows/e2e.yml"
+```yaml title=".github/workflows/e2e.yml" expandable
name: e2e
on:
diff --git a/docs/coding-agents.mdx b/docs/coding-agents.mdx
index e8a992fc6..ccbf08c63 100644
--- a/docs/coding-agents.mdx
+++ b/docs/coding-agents.mdx
@@ -1,6 +1,7 @@
---
title: Coding agents
description: Give a coding agent the skill, a live app session, and reports it can use to write and debug tests.
+icon: "robot"
---
`e2e init` installs a skill for writing and running tests, plus MCP
@@ -168,7 +169,7 @@ model call in the flow.
Do not edit recordings by hand. A failed read-write run removes implicated
entries so a later verified run can replace them. A failure where no model
answered implicates nothing and removes none. See
-[Caching agent steps](/cache).
+[Caching](/cache).
diff --git a/docs/core-concepts.mdx b/docs/core-concepts.mdx
new file mode 100644
index 000000000..95dad8f3c
--- /dev/null
+++ b/docs/core-concepts.mdx
@@ -0,0 +1,208 @@
+---
+title: Core concepts
+description: Goals, assertions, and locators, and how to structure and run tests with them.
+icon: "lightbulb"
+---
+
+A test has three types of step:
+
+| Step | API | Model call |
+| --- | --- | --- |
+| [Goal](#goals) | `agent.act` | Yes, unless the [cache](/cache) replays it |
+| [Assertion](#assertions) | `agent.assert`, `agent.waitFor`, `agent.extract` | Yes |
+| [Locator](#locators) | `screen` and `expect` | No |
+
+The model column is for the built-in agent. A
+[custom executor](/executors) can run `act` and `assert` without a model.
+
+You can use all three in one test:
+
+```ts title="tests/todos.e2e.ts"
+import { test, expect } from 'e2e';
+
+test('todos survive a filter round-trip', async ({ app, agent, screen }) => {
+ await app.open('/todos');
+
+ await agent.act('add todos "Buy milk" and "Walk the dog", complete the first');
+ await agent.assert('only one todo remains open');
+
+ await screen.getByRole('tab', 'Done').tap();
+ await expect(screen.getByTestId('todo')).toHaveCount(1);
+});
+```
+
+Await each agent call, screen call, and `expect.poll`. If a test returns
+while a step still runs, the test fails with `STEP_NOT_AWAITED`.
+
+## Goals
+
+`agent.act` takes one goal. The agent selects and does the actions. It
+returns when the goal is complete. It throws when the goal fails.
+
+```ts
+await agent.act('complete checkout with the test card');
+await agent.act('invite {email} as an editor', { params: { email: 'ada@example.test' } });
+```
+
+- Write one goal for each call. The agent controls the flow in a goal. You
+ control the order of the goals.
+- Use the words that the screen shows.
+- Give test data in `params`. For a password, give a `Secret`. The model
+ does not see a secret. See [Signing in](/authentication).
+- For a value that changes on each run, use `unique()`. Then the
+ [replay cache](/cache#what-must-match) can replay the step:
+
+```ts
+import { unique } from 'e2e';
+
+const email = `ada+${Date.now()}@example.test`;
+await agent.act('sign up with email {email}', { params: { email: unique(email) } });
+```
+
+If a goal fails, read the report. Then make the instruction more specific.
+Put words that many goals use in the agent's `context`; see
+[Agents and personas](/agents). For verdicts and exit codes, see
+[How agent steps work](/agent-steps#how-a-step-ends).
+
+`act` gets screenshots when it needs them. On some engines, it can tap and
+type at points in the image.
+
+## Assertions
+
+An assertion examines the screen. The judging model sees the statement, the
+current screen, and the agent's context. It does not see the earlier steps
+or the summaries of `act`.
+
+- `agent.assert` examines the screen now. A false statement fails with
+ `ASSERTION_FAILED`. Not sufficient evidence fails with
+ `ASSERTION_INCONCLUSIVE`.
+- `agent.waitFor` waits until a condition is true. It calls the model only
+ when the screen changes.
+- `agent.extract` reads data from the screen into a
+ [Standard Schema](https://standardschema.dev) type, for example Zod.
+
+```ts
+import { expect } from 'e2e';
+import { z } from 'zod';
+
+await agent.assert('the dashboard shows a trial badge');
+await agent.waitFor('a download link appears', { timeout: 120_000 });
+
+const data = await agent.extract('every todo title', {
+ schema: z.object({ titles: z.array(z.string()) }),
+});
+expect(data.titles).toContain('Buy milk');
+```
+
+For text that the model wrote, examine the meaning, not the full sentence.
+When the exact text is important, use a [locator](#locators).
+
+### Judging pixels
+
+Judgments read a redacted text snapshot of the screen. For a visual check,
+such as a chart or a layout, add `vision: true`. To judge only the
+screenshot, use `vision: 'only'`.
+
+```ts
+await agent.assert('the chart trends upward', { vision: true });
+```
+
+Images add input tokens. After a secret fill, the attempt sends no more
+screenshots. See the [agent reference](/reference/agent#vision).
+
+## Locators
+
+A locator finds a known control. Locator steps do not call a model. Use them
+when you know the exact action or the exact value.
+
+```ts
+// Role and name first, label second, test ID last.
+await screen.getByRole('button', 'Sign in').tap();
+await screen.getByLabel('Email').fill('ada@example.test');
+
+// Scope into a row.
+const row = screen.getByRole('listitem').filter({ hasText: 'Design review' });
+await row.getByRole('button', 'Archive').tap();
+
+// Gestures and coordinates.
+await screen.swipe({ direction: 'up' });
+await screen.getByRole('image', 'Seat map').tap({ position: { x: 120, y: 48 } });
+
+// Assertions retry until they pass or time out.
+await expect(screen.getByRole('status')).toHaveText('2 remaining');
+```
+
+- An action, a read, or a single-node assertion fails when its query finds
+ more than one node. Use `.filter()`, `.first()`, or `.nth()` to find one
+ node.
+- A direct read, such as `textContent()`, does not retry.
+- Put an `expect` after an `agent.act` goal. It examines the exact result.
+ The [replay cache](/cache) records a goal only after a later check passes.
+
+See the [screen reference](/reference/screen) and the
+[expect reference](/reference/expect#locator-matchers). For routes, cookies,
+dialogs, and downloads, see [Working with the browser](/browser).
+
+## Structure
+
+```ts
+import { beforeEach, describe, test } from 'e2e';
+
+describe('billing', { tags: ['billing'] }, () => {
+ beforeEach(async ({ app }) => { await app.open('/billing'); });
+
+ test('upgrades', { retries: 2, timeout: 60_000 }, async ({ agent }) => {
+ await agent.act('upgrade to Pro');
+ });
+});
+
+test('needs two organizations', async ({ app }) => {
+ test.skip((await countOrganizations()) < 2, 'nothing to switch to');
+ await app.open('/organizations');
+});
+```
+
+- Each test runs in its own attempt. Prepare its data in fixtures or hooks.
+- `test.skip(condition, reason)` stops the test when the condition is true.
+ The report shows the test as `skipped`.
+- For a flow across many tests, set `{ serial: true }` on the group. The
+ tests run in sequence on one worker, and retry together.
+- For setup and cleanup, use `test.extend`. See
+ [Your own fixtures](/reference/test#your-own-fixtures).
+- Import `test` from `@e2e-dev/web` or `@e2e-dev/mobile` to have the
+ `browser` or `device` fixture typed.
+
+The [test reference](/reference/test) shows all options.
+
+## Run one test
+
+
+```bash npm
+npx e2e run tests/signup.e2e.ts:12 # the test at line 12
+npx e2e run --grep checkout # tests with a matching title
+npx e2e run --target web # one target
+```
+
+```bash pnpm
+pnpm exec e2e run tests/signup.e2e.ts:12 # the test at line 12
+pnpm exec e2e run --grep checkout # tests with a matching title
+pnpm exec e2e run --target web # one target
+```
+
+```bash bun
+bunx e2e run tests/signup.e2e.ts:12 # the test at line 12
+bunx e2e run --grep checkout # tests with a matching title
+bunx e2e run --target web # one target
+```
+
+
+The [CLI reference](/reference/cli#e2e-run) shows all flags.
+
+
+
+ Replay passed goals with no model calls.
+
+
+ Verdicts, exit codes, and what the model sees.
+
+
diff --git a/docs/debugging.mdx b/docs/debugging.mdx
index 96a7f4b43..5c693f374 100644
--- a/docs/debugging.mdx
+++ b/docs/debugging.mdx
@@ -1,6 +1,7 @@
---
title: Debugging a run
description: Read the report, open the evidence, watch the browser, and see what the model was sent.
+icon: "magnifying-glass"
---
Start with the failure in the terminal. Every run also writes
@@ -198,9 +199,16 @@ stdout | web tests/todos.e2e.ts > adds a todo
hello from the test
```
-Nothing is buffered or dropped: every line appears when it is written, in a
-pipe or a CI log as much as in a terminal. The output is not part of the
-report; use `expect` or an artifact for anything a later reader needs.
+Output from outside a test, such as a test file's top level or a reporter,
+prints under `stdout | runner` or `stderr | runner`. Both streams print on
+stdout, in a pipe or a CI log too. Without the `list` reporter, output
+goes to its own stream as written.
+
+Secret values are [redacted](/security#redaction). Output printed while the
+config loads appears once it has loaded, before the run header, and is
+dropped for a warning if the load fails. Every other line appears when it
+is written. The output is not part of the report; use `expect` or an
+artifact for anything a later reader needs.
## Agent steps
@@ -266,13 +274,13 @@ large raw JSON file.
Replayed steps make no model call and leave no AI trace. Add `--no-cache`
when you need to inspect the whole flow or rule out a stale recording. See
-[Caching agent steps](/cache).
+[Caching](/cache).
## Common errors
| Code | Usual cause | Fix |
| --- | --- | --- |
-| `APP_UNREACHABLE` | Nothing answers at `app.url`, or `app.command` never became ready; on a device, the message may name the iOS automation runner instead (busy, wedged, or a snapshot it could not present) | Set `app.command.log` and read it; check the port; raise `app.command.startupTimeout`. For the runner, follow the recovery in the message: `npx agent-device daemon stop`, reboot the simulator (see [Testing iOS and Android](/mobile#troubleshooting)) |
+| `APP_UNREACHABLE` | Nothing answers at `app.url`, or `app.command` never became ready; on a device, the message may name the iOS automation runner instead (busy, wedged, or a snapshot it could not present) | Set `app.command.log` and read it; check the port; raise `app.command.startupTimeout`. For the runner, follow the recovery in the message: `npx agent-device daemon stop`, reboot the simulator (see [Mobile](/mobile#troubleshooting)) |
| `APP_ALREADY_RUNNING` | Something already serves `app.url` when the runner wanted to start `app.command` | Stop it, or set `reuseExisting: true` for local runs |
| `LOCATOR_NOT_FOUND` | Wrong role or name, text not exact, element inside an iframe, page not open | Read the markup for the accessible name; try `exact: false`; `browser.frameLocator` for iframes; `--headed` to look |
| `LOCATOR_AMBIGUOUS` | Two matches: a hidden duplicate, a repeated label | Add `{ name }`, scope under a container, `filter`, `first()`, or `{ visible: true }` |
diff --git a/docs/decision-models.mdx b/docs/decision-models.mdx
index 23b84ef12..93e3f6c58 100644
--- a/docs/decision-models.mdx
+++ b/docs/decision-models.mdx
@@ -1,6 +1,7 @@
---
title: Decision models
description: Run bounded actions with a decision model and a small text model instead of a full LLM agent.
+icon: "scale-balanced"
---
`@e2e-dev/decision` runs `agent.act` and `agent.assert` through a decision
diff --git a/docs/docs.json b/docs/docs.json
index cbe567e77..7343293ff 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -130,82 +130,41 @@
"groups": [
{
"group": "Get started",
- "icon": "rocket",
"pages": [
"index",
"quickstart",
- "goals",
- "assertions",
- "locators"
+ "core-concepts",
+ "explore"
]
},
{
- "group": "Web",
- "icon": "globe",
- "pages": [
- "starting-your-app",
- "browser",
- "viewports"
- ]
- },
- {
- "group": "Mobile",
- "icon": "mobile-screen",
+ "group": "Platforms",
"pages": [
+ "web",
"mobile",
- "physical-devices"
- ]
- },
- {
- "group": "API",
- "icon": "server",
- "pages": [
"api-testing"
]
},
{
"group": "Models and agents",
- "icon": "wand-magic-sparkles",
"pages": [
"models",
"decision-models",
"subscriptions",
- "agents",
- "agent-steps",
"cache"
]
},
- {
- "group": "Sign-in and security",
- "icon": "shield-halved",
- "pages": [
- "authentication",
- "security",
- "telemetry"
- ]
- },
- {
- "group": "Debug and explore",
- "icon": "bug",
- "pages": [
- "debugging",
- "explore",
- "bug-bash"
- ]
- },
{
"group": "Automate",
- "icon": "gears",
"pages": [
+ "coding-agents",
"ci",
"mobile-ci",
- "github",
- "coding-agents"
+ "github"
]
},
{
"group": "Integrations",
- "icon": "plug",
"pages": [
"integrations/index",
"integrations/kernel",
@@ -214,9 +173,29 @@
"integrations/testmu-browsers"
]
},
+ {
+ "group": "Extend",
+ "pages": [
+ "tools",
+ "executors",
+ "writing-an-engine"
+ ]
+ },
+ {
+ "group": "Deep dive",
+ "pages": [
+ "browser",
+ "bug-bash",
+ "authentication",
+ "security",
+ "telemetry",
+ "debugging",
+ "agents",
+ "agent-steps"
+ ]
+ },
{
"group": "Migrate",
- "icon": "arrow-right-arrow-left",
"pages": [
"migrate/playwright",
"migrate/cypress",
@@ -224,15 +203,6 @@
"migrate/detox",
"migrate/maestro"
]
- },
- {
- "group": "Extend",
- "icon": "puzzle-piece",
- "pages": [
- "tools",
- "executors",
- "writing-an-engine"
- ]
}
]
},
@@ -242,7 +212,6 @@
"groups": [
{
"group": "Test API",
- "icon": "flask",
"pages": [
"reference/test",
"reference/expect",
@@ -253,7 +222,6 @@
},
{
"group": "Project",
- "icon": "folder-gear",
"pages": [
"reference/config",
"reference/cli",
@@ -265,26 +233,30 @@
},
{
"group": "Web",
- "icon": "globe",
"pages": [
"reference/web"
]
},
{
"group": "Mobile",
- "icon": "mobile-screen",
"pages": [
"reference/mobile"
]
},
{
"group": "Engine contract",
- "icon": "gear",
"pages": [
"reference/engine"
]
}
]
+ },
+ {
+ "tab": "Examples",
+ "icon": "folder-open",
+ "pages": [
+ "example-projects"
+ ]
}
]
},
@@ -298,10 +270,6 @@
"source": "/introduction",
"destination": "/"
},
- {
- "source": "/web",
- "destination": "/browser"
- },
{
"source": "/reference/device",
"destination": "/reference/mobile"
@@ -348,7 +316,31 @@
},
{
"source": "/writing-tests",
- "destination": "/goals"
+ "destination": "/core-concepts"
+ },
+ {
+ "source": "/starting-your-app",
+ "destination": "/web#starting-your-app"
+ },
+ {
+ "source": "/viewports",
+ "destination": "/web#viewports"
+ },
+ {
+ "source": "/physical-devices",
+ "destination": "/mobile#physical-devices"
+ },
+ {
+ "source": "/goals",
+ "destination": "/core-concepts#goals"
+ },
+ {
+ "source": "/assertions",
+ "destination": "/core-concepts#assertions"
+ },
+ {
+ "source": "/locators",
+ "destination": "/core-concepts#locators"
}
]
}
diff --git a/docs/example-projects.mdx b/docs/example-projects.mdx
new file mode 100644
index 000000000..12f4b19f0
--- /dev/null
+++ b/docs/example-projects.mdx
@@ -0,0 +1,32 @@
+---
+title: Example projects
+description: Run standalone e2e examples with Vite, Next.js, Expo, and SwiftUI.
+icon: "folder-open"
+---
+
+Each example is a standalone greeting app with three locator tests and two
+agent tests. Enter a name, press Greet, and check the result. The projects
+install published e2e packages from npm and work outside this repository.
+
+
+
+ React with Vite. Run locator and agent tests in Chromium.
+
+
+ Next.js App Router. The runner starts the app and tests it in Chromium.
+
+
+ React Native with Expo. Run the same tests on web, an iOS simulator, or an Android emulator.
+
+
+ A native iOS app with an Xcode project. Run tests on an iPhone simulator.
+
+
+
+Follow each project's README for installation, builds, and run commands.
+The config and tests live beside the app. The SwiftUI project keeps them
+in its `e2e/` folder.
+
+Locator tests need no model. Set `AI_GATEWAY_API_KEY` to run the agent
+tests; without it, those tests skip. See [Models](/models) for model setup
+and the [Quickstart](/quickstart) to add e2e to your own app.
diff --git a/docs/executors.mdx b/docs/executors.mdx
index 8ec3fb785..63ae9e7a3 100644
--- a/docs/executors.mdx
+++ b/docs/executors.mdx
@@ -1,6 +1,7 @@
---
title: Custom executors
description: Customize the agent loop or implement your own step executor.
+icon: "microchip"
---
Use a custom executor when a `system` prompt and [project tools](/tools) are
diff --git a/docs/explore.mdx b/docs/explore.mdx
index 9c998a4b9..22e5c04d0 100644
--- a/docs/explore.mdx
+++ b/docs/explore.mdx
@@ -1,6 +1,7 @@
---
title: Exploring without a test
description: Give the agent a goal instead of a test file. It plans its own steps, drives the app, and reports what it finds.
+icon: "compass"
---
Use `e2e explore` to investigate a flow without writing a test. Give the
diff --git a/docs/github.mdx b/docs/github.mdx
index 1c35b9d1a..a224a1100 100644
--- a/docs/github.mdx
+++ b/docs/github.mdx
@@ -1,6 +1,7 @@
---
title: Pull request comments
description: The @e2e-dev/github reporter posts one comment per run from GitHub Actions and keeps it current.
+icon: "code-pull-request"
---
`@e2e-dev/github` posts test results as a pull request comment, including
@@ -73,7 +74,7 @@ that path. The checkout root is the nearest directory holding `.git` inside
## The comment
-```markdown
+```markdown expandable
### 🔴 e2e: 1 failed, 1 flaky, 3 passed, 1 skipped
9 agent steps · 4 replayed from cache · 31 model calls · 128k tokens (62% cached) · $0.31
diff --git a/docs/goals.mdx b/docs/goals.mdx
deleted file mode 100644
index 217b6c868..000000000
--- a/docs/goals.mdx
+++ /dev/null
@@ -1,176 +0,0 @@
----
-title: Goals
-description: Write test steps as goals with agent.act, and structure the tests around them.
----
-
-Use agent calls for goals and questions about the screen. Use
-[locators](/locators) when you know the exact action or value to check. A test
-can freely mix both.
-
-```ts title="tests/todos.e2e.ts"
-import { test, expect } from 'e2e';
-
-test('todos survive a filter round-trip', async ({ app, agent, screen }) => {
- await app.open('/todos');
-
- await agent.act('add todos "Buy milk" and "Walk the dog", complete the first');
- await agent.assert('only one todo remains open');
-
- await screen.getByRole('tab', 'Done').tap();
- await expect(screen.getByTestId('todo')).toHaveCount(1);
-});
-```
-
-## Goals: `agent.act`
-
-Give `act` one goal. The agent chooses and performs the actions, then returns
-when the goal passes or throws when it fails. Await every agent and screen
-call, and every `expect.poll`; a test that returns while one is still running
-fails with `STEP_NOT_AWAITED`.
-
-```ts
-await agent.act('complete checkout with the test card');
-await agent.act('invite {email} as an editor', { params: { email: 'ada@example.test' } });
-```
-
-Pass values through `params` so the agent uses your test data. For passwords,
-use a `Secret`, this prevents the model from seeing your secrets. See [Signing in](/authentication).
-
-```ts
-import { credentials } from 'e2e';
-
-const member = credentials.user('member');
-await agent.act('Sign in', {
- params: { username: member.username, password: member.password },
-});
-```
-
-
-**Write instructions as you would say them out loud**
-
-Name what is visible and prefer the exact wording on screen. One goal per
-call. The flow inside a goal is the model's job; the order of goals is yours.
-
-
-For a value that changes on every run, wrap it in `unique()` so the
-[replay cache](/cache#what-must-match) can still replay the step:
-
-```ts
-import { expect, unique } from 'e2e';
-
-const email = `ada+${Date.now()}@example.test`;
-await agent.act('sign up with email {email}', { params: { email: unique(email) } });
-await expect(screen.getByText(email)).toBeVisible();
-```
-
-If a goal fails, check the report and make the instruction more specific.
-Use the labels on screen. Put vocabulary shared by several goals in the
-agent's `context`; see [Agents and personas](/agents).
-
-A step can pass, fail, or be blocked by something such as missing credentials.
-[How agent steps work](/agent-steps#how-a-step-ends) explains the verdicts and
-exit codes.
-
-`act` requests screenshots itself when it needs them, so unlike the
-[judgments](/assertions#judging-pixels) it has no `vision` option. On
-supported engines, it can tap and type at points in the image:
-
-```ts
-await agent.act('enter the code 3141 on the drawn keypad and press OK');
-```
-
-Images add input tokens. Screenshots are withheld for the rest of an attempt
-after a secret is filled. The [agent reference](/reference/agent#vision)
-lists the modes and restrictions.
-
-To check what a goal left on screen, follow it with an
-[assertion](/assertions) or a [locator check](/locators).
-
-## Tests without a screen
-
-A test that takes only `app` calls no model and drives no screen. Use it to
-check your API with `fetch` in the same run as UI tests. See
-[Testing APIs](/api-testing).
-
-## Structure
-
-```ts
-import { beforeEach, describe, test } from 'e2e';
-
-describe('billing', { tags: ['billing'] }, () => {
- beforeEach(async ({ app }) => { await app.open('/billing'); });
-
- test('upgrades', { retries: 2, timeout: 60_000 }, async ({ agent }) => {
- await agent.act('upgrade to Pro');
- });
-});
-
-test.skip('not ready yet', async () => {});
-test.only('just this one while I work', async () => {}); // rejected in CI
-
-test('needs two organizations', async ({ app }) => {
- test.skip((await countOrganizations()) < 2, 'one organization: nothing to switch to');
- await app.open('/organizations');
-});
-```
-
-`test.skip(condition, reason)` inside a body throws when the condition holds:
-the test stops at that line and is reported `skipped`, as in Playwright. Call
-it before the first step when it guards a precondition.
-
-`describe` and the hooks are also `test.describe`, `test.beforeEach`, and so
-on. A `beforeEach` imported from `e2e` sees the core fixtures; import
-`test`, `describe`, and the hooks from `@e2e-dev/web` (or `@e2e-dev/mobile`)
-to have `browser` (or `device`) typed from one import, and use
-`test.beforeEach` on a `test` you extended with your own fixtures.
-
-Common options include `timeout`, `retries`, and `tags`. Use `session` for
-state from a setup test, `agentContext` for extra instructions, and `agent`
-to choose a configured [agent](/agents). `trace` and `video` record one test
-without recording the suite; see [Watch it happen](/debugging#watch-it-happen).
-Group options apply to their tests. The [test reference](/reference/test)
-lists all options and defaults.
-
-Each test runs in its own attempt. Prepare the app data it needs through
-fixtures or hooks. When a flow must span several tests, mark the group
-`{ serial: true }`. Members share app state, run in order on one worker,
-and retry as a whole.
-
-Share setup and cleanup through `test.extend`. A fixture can create a test
-workspace before the test and delete it afterward, including when the test
-fails. See [Your own fixtures](/reference/test#your-own-fixtures).
-
-## Run one test
-
-
-```bash npm
-npx e2e run tests/signup.e2e.ts:12 # the test declared at line 12
-npx e2e run --grep checkout # tests whose title matches
-npx e2e run --target web # one target, by name
-```
-
-```bash pnpm
-pnpm exec e2e run tests/signup.e2e.ts:12 # the test declared at line 12
-pnpm exec e2e run --grep checkout # tests whose title matches
-pnpm exec e2e run --target web # one target, by name
-```
-
-```bash bun
-bunx e2e run tests/signup.e2e.ts:12 # the test declared at line 12
-bunx e2e run --grep checkout # tests whose title matches
-bunx e2e run --target web # one target, by name
-```
-
-
-The [CLI reference](/reference/cli#e2e-run) lists every flag.
-
-## Next
-
-
-
- Check the screen with `assert`, `waitFor`, and `extract`.
-
-
- Exact actions and checks with `screen` and `expect`.
-
-
diff --git a/docs/images/examples/README.md b/docs/images/examples/README.md
new file mode 100644
index 000000000..2a741d6da
--- /dev/null
+++ b/docs/images/examples/README.md
@@ -0,0 +1,9 @@
+# Technology logos
+
+These assets identify the technologies used by the example projects.
+
+- `vite.svg`: https://vite.dev/logo-without-border.svg
+- `nextjs.svg`: https://github.com/vercel/next.js/blob/canary/packages/create-next-app/templates/app/ts/public/next.svg
+- `swiftui.png`: https://developer.apple.com/assets/elements/icons/swiftui/swiftui-96x96_2x.png
+
+The Expo card reuses `../integrations/expo.svg`.
diff --git a/docs/images/examples/nextjs.svg b/docs/images/examples/nextjs.svg
new file mode 100644
index 000000000..5174b28c5
--- /dev/null
+++ b/docs/images/examples/nextjs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/images/examples/swiftui.png b/docs/images/examples/swiftui.png
new file mode 100644
index 000000000..342ccb146
Binary files /dev/null and b/docs/images/examples/swiftui.png differ
diff --git a/docs/images/examples/vite.svg b/docs/images/examples/vite.svg
new file mode 100644
index 000000000..0f2bdacd0
--- /dev/null
+++ b/docs/images/examples/vite.svg
@@ -0,0 +1,130 @@
+
diff --git a/docs/images/icons/cypress.svg b/docs/images/icons/cypress.svg
new file mode 100644
index 000000000..2c4272298
--- /dev/null
+++ b/docs/images/icons/cypress.svg
@@ -0,0 +1 @@
+
diff --git a/docs/images/icons/detox.svg b/docs/images/icons/detox.svg
new file mode 100644
index 000000000..e86436472
--- /dev/null
+++ b/docs/images/icons/detox.svg
@@ -0,0 +1,6 @@
+
diff --git a/docs/images/icons/expo.svg b/docs/images/icons/expo.svg
new file mode 100644
index 000000000..5f25571f1
--- /dev/null
+++ b/docs/images/icons/expo.svg
@@ -0,0 +1 @@
+
diff --git a/docs/images/icons/kernel.svg b/docs/images/icons/kernel.svg
new file mode 100644
index 000000000..fff0f68d5
--- /dev/null
+++ b/docs/images/icons/kernel.svg
@@ -0,0 +1 @@
+
diff --git a/docs/images/icons/maestro.svg b/docs/images/icons/maestro.svg
new file mode 100644
index 000000000..53f299edc
--- /dev/null
+++ b/docs/images/icons/maestro.svg
@@ -0,0 +1 @@
+
diff --git a/docs/images/icons/playwright.svg b/docs/images/icons/playwright.svg
new file mode 100644
index 000000000..f495f9e59
--- /dev/null
+++ b/docs/images/icons/playwright.svg
@@ -0,0 +1 @@
+
diff --git a/docs/images/icons/selenium.svg b/docs/images/icons/selenium.svg
new file mode 100644
index 000000000..2eee34556
--- /dev/null
+++ b/docs/images/icons/selenium.svg
@@ -0,0 +1 @@
+
diff --git a/docs/index.mdx b/docs/index.mdx
index 0a810e814..8f67a7e8a 100644
--- a/docs/index.mdx
+++ b/docs/index.mdx
@@ -1,6 +1,7 @@
---
title: Introduction
description: Open source AI testing for web and mobile apps. Spend less time on tests and more time building.
+icon: "house"
---
import { VideoCard } from './snippets/video-card.jsx';
@@ -9,9 +10,9 @@ e2e is a next-generation testing framework for web and mobile
apps, built to keep up with the speed we ship software today.
You choose how agentic each test is: fully deterministic with
-[locators](/locators) and
+[locators](/core-concepts#locators) and
[assertions](/reference/expect#locator-matchers), fully driven by an agent
-from [goals](/goals) in natural language, or a mix of
+from [goals](/core-concepts#goals) in natural language, or a mix of
both in the same test.
One prompt for your coding agent, or set up by hand.
-
- Write test steps as goals in natural language.
+
+ Run a standalone app with Vite, Next.js, Expo, or SwiftUI.
+
+
+ Goals, assertions, and locators.
Let your coding agent write, run, and debug tests.
diff --git a/docs/integrations/eas.mdx b/docs/integrations/eas.mdx
index a2ca2bcc6..f2b17fb3b 100644
--- a/docs/integrations/eas.mdx
+++ b/docs/integrations/eas.mdx
@@ -2,6 +2,7 @@
title: EAS Simulators
sidebarTitle: EAS Simulators
description: Run mobile targets on EAS Simulators' hosted iOS simulators and Android emulators with the easSimulators() device provider from @e2e-dev/eas.
+icon: "/images/icons/expo.svg"
---
`easSimulators()` from `@e2e-dev/eas` is a
diff --git a/docs/integrations/index.mdx b/docs/integrations/index.mdx
index 838815e40..85f8ff050 100644
--- a/docs/integrations/index.mdx
+++ b/docs/integrations/index.mdx
@@ -2,6 +2,7 @@
title: Integrations
sidebarTitle: Overview
description: Official integrations with hosted services, one package per service.
+icon: "plug"
---
Official integrations connect e2e to hosted services through the engines'
diff --git a/docs/integrations/kernel.mdx b/docs/integrations/kernel.mdx
index 5e3107398..a70f10b49 100644
--- a/docs/integrations/kernel.mdx
+++ b/docs/integrations/kernel.mdx
@@ -2,6 +2,7 @@
title: Kernel browsers
sidebarTitle: Kernel
description: Run web targets in Kernel's hosted Chromium with the kernel() browser provider from @e2e-dev/kernel.
+icon: "/images/icons/kernel.svg"
---
`kernel()` from `@e2e-dev/kernel` is a
@@ -60,7 +61,7 @@ web({
```
Each attempt's window then opens maximized, and the page
-[fills it](/viewports#fill-the-window): 1920 by 993 on a 1920 by 1080 screen,
+[fills it](/web#fill-the-window): 1920 by 993 on a 1920 by 1080 screen,
after the tab strip and address bar. With a fixed web `viewport`, the page is
emulated at that size inside a smaller window, so Kernel's live view shows it
cut off or in part of the window.
diff --git a/docs/locators.mdx b/docs/locators.mdx
deleted file mode 100644
index 1cdabe914..000000000
--- a/docs/locators.mdx
+++ /dev/null
@@ -1,64 +0,0 @@
----
-title: Locators
-description: Deterministic actions and checks with screen and expect.
----
-
-Use locators to interact with known controls and check exact values. Locator
-steps call no model.
-
-```ts title="tests/todos.e2e.ts"
-import { test, expect } from 'e2e';
-
-test('adds a todo', async ({ app, screen }) => {
- await app.open('/todos');
- await screen.getByLabel('New todo').fill('Buy milk');
- await screen.getByRole('button', 'Add').tap();
- await expect(screen.getByTestId('todo')).toHaveCount(1);
-});
-```
-
-An `expect` after an [`agent.act` goal](/goals) checks the exact result. It is
-also what lets the [replay cache](/cache) record the step and keep it.
-
-## Queries, actions, and assertions
-
-```ts
-// Semantic queries: role and name first, label second, test ID last.
-await screen.getByRole('button', 'Sign in').tap();
-await screen.getByLabel('Email').fill('ada@example.test');
-await screen.getByTestId('todo').first().tap();
-
-// Scope into a row, gesture like a user:
-const row = screen.getByRole('listitem').filter({ hasText: 'Design review' });
-await row.getByRole('button', 'Archive').tap();
-await screen.swipe({ direction: 'up', momentum: 'fast' });
-
-// Coordinates, for what the tree does not list:
-await screen.getByRole('image', 'Seat map').tap({ position: { x: 120, y: 48 } });
-await screen.swipe({ from: { x: 300, y: 400 }, to: { x: 40, y: 400 } });
-
-// Polling assertions:
-await expect(screen.getByRole('status')).toHaveText('2 remaining');
-await expect(screen.getByRole('heading', 'Dashboard')).toBeVisible();
-```
-
-Actions and single-node reads or assertions fail when a query matches more
-than one node. Narrow it with `.filter()`, `.first()`, or `.nth()`.
-Locator assertions retry until they pass or time out. Direct reads such as
-`textContent()` do not retry a mismatched value.
-
-Use the `browser` fixture for browser features such as routes, cookies, dialogs,
-frames, and downloads; see [Working with the browser](/browser). The
-[screen reference](/reference/screen) lists every query and action, and the
-[expect reference](/reference/expect#locator-matchers) lists every matcher.
-
-## Next
-
-
-
- Sessions and secrets.
-
-
- Several agents in one suite, and running a flow as each of them.
-
-
diff --git a/docs/migrate/cypress.mdx b/docs/migrate/cypress.mdx
index 686a21cc4..490a46f61 100644
--- a/docs/migrate/cypress.mdx
+++ b/docs/migrate/cypress.mdx
@@ -1,8 +1,31 @@
---
title: From Cypress
description: Replace Cypress command chains with async tests and keep your data-cy attributes.
+icon: "/images/icons/cypress.svg"
---
+import { SetupPrompt } from '../snippets/setup-prompt-card.jsx';
+
+export const migrationPrompt = `Migrate this project's Cypress tests to e2e, one spec at a time. Docs: \`https://e2e.tester.army/docs/migrate/cypress.md\`
+
+1. Read the migration guide above. It maps Cypress config, commands, and assertions to e2e.
+2. If the project has no e2e config (\`e2e.config.ts\` or \`e2e.config.mts\`), run \`npx e2e init --yes\` (or the pnpm or bun equivalent). It writes a config and an example test, and installs nothing yet.
+3. Read the e2e skill (\`npx e2e guide\` prints it, \`npx e2e guide \` prints a topic) and follow it for every step below.
+4. If you ran init, ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
+5. Port \`cypress.config.*\` into the e2e config: \`baseUrl\` and the dev server command become the target's \`app\`, and if the specs select by \`data-cy\`, set \`web({ testIdAttribute: 'data-cy' })\` so \`screen.getByTestId\` reads it. Keep Cypress installed while both suites run.
+6. Install dependencies.
+7. List the Cypress end-to-end specs and ask me which one to move first. Suggest one that breaks often or waits with \`cy.wait(ms)\`. Component tests have no e2e equivalent; leave them in Cypress.
+8. Rewrite that spec as a \`.e2e.ts\` file under \`tests/\` (e2e runs \`tests/**/*.e2e.ts\` by default), with one awaited call per line and no chains. \`cy.contains\` matches a substring and e2e queries match the whole text, so pass \`{ exact: false }\` where the spec relied on a fragment. Replace each \`cy.wait(ms)\` with an assertion on the state the test waits for. Move \`cy.task\`, \`cy.exec\`, and \`cy.request\` into plain Node code, and custom commands into functions or fixtures.
+9. Keep navigation and the final assertions deterministic, with \`app.open()\` and locator assertions. Where a run of scripted interactions breaks often, propose an \`agent.act\` goal for it instead.
+10. Run the new file with \`npx e2e run \` and fix it until it passes. Keep the Cypress spec until I confirm it can go.
+11. Summarize which tests moved, which stayed in Cypress, and why.`;
+
+
+
+
+
+
+
e2e tests run in Node. Use `await` for UI actions and call Node functions
directly for work previously handled by `cy.task`. Both suites can stay in
the project while you migrate one spec at a time.
@@ -15,7 +38,7 @@ for the agent calls shown below.
| Cypress | e2e |
| --- | --- |
| `baseUrl` | `app: { url }` on the target |
-| `start-server-and-test` | `app: { url, command }`, see [Starting your app](/starting-your-app) |
+| `start-server-and-test` | `app: { url, command }`, see [Starting your app](/web#starting-your-app) |
| `viewportWidth`, `viewportHeight` | `web({ viewport })` |
| `data-cy` selectors | `web({ testIdAttribute: 'data-cy' })`, then `screen.getByTestId` |
| `retries` | `retries` |
@@ -115,8 +138,8 @@ Start with a spec that often breaks and whose fixed `cy.wait` calls can
become assertions on the condition the test needs.
-
- Write test steps as goals instead of scripted selectors.
+
+ Goals, assertions, and locators instead of scripted selectors.
Routes, dialogs, downloads, iframes, and page evaluation.
diff --git a/docs/migrate/detox.mdx b/docs/migrate/detox.mdx
index 7cdf61642..0232354cd 100644
--- a/docs/migrate/detox.mdx
+++ b/docs/migrate/detox.mdx
@@ -1,8 +1,31 @@
---
title: From Detox
description: Use your existing app build and testIDs with screen, expect, and agent goals.
+icon: "/images/icons/detox.svg"
---
+import { SetupPrompt } from '../snippets/setup-prompt-card.jsx';
+
+export const migrationPrompt = `Migrate this project's Detox tests to e2e, one spec at a time. Docs: \`https://e2e.tester.army/docs/migrate/detox.md\` and \`https://e2e.tester.army/docs/mobile.md\`
+
+1. Read the migration guide above. It maps \`.detoxrc.js\` and the Detox API to e2e. The mobile guide covers devices and app builds.
+2. If the project has no e2e config (\`e2e.config.ts\` or \`e2e.config.mts\`), run \`npx e2e init --yes\` (or the pnpm or bun equivalent). It writes a web config and an example test, and installs nothing yet. Replace @e2e-dev/web with @e2e-dev/mobile, and rewrite \`tests/example.e2e.ts\` for the mobile engine or delete it.
+3. Read the e2e skill (\`npx e2e guide\` prints it, \`npx e2e guide \` prints a topic) and follow it for every step below.
+4. If you ran init, ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
+5. Port \`.detoxrc.js\` into the e2e config: one target per platform with \`mobile({ platform })\`, the app's \`bundleId\` and \`appPath\` on the target, and the device type as \`mobile({ device })\`. e2e uses the normal debug or release build, with no Detox framework.
+6. Install dependencies, then run \`npx agent-device doctor\` and fix what it reports.
+7. List the Detox specs and ask me which one to move first. Suggest a flow that needs frequent maintenance.
+8. Rewrite that spec as a \`.e2e.ts\` file under \`tests/\` (e2e runs \`tests/**/*.e2e.ts\` by default), not in the Detox folder. Start it with \`app.open()\`, since nothing launches on its own. \`testID\` props work with \`screen.getByTestId\`. Replace \`waitFor(...).withTimeout()\` with a locator assertion and its \`timeout\`. e2e fails when a query matches several nodes, so narrow those queries.
+9. Keep the launch and the final assertions deterministic, with \`app.open()\` and locator assertions. Where a run of scripted interactions breaks often, propose an \`agent.act\` goal for it instead.
+10. Build and install the app, run the new file with \`npx e2e run \` on each platform target, and fix it until it passes. Keep the Detox spec until I confirm it can go.
+11. Summarize which tests moved, which stayed in Detox, and why.`;
+
+
+
+
+
+
+
`@e2e-dev/mobile` drives a simulator or emulator using your normal debug
or release build. It does not need the Detox framework. React Native
`testID` props work with `getByTestId` on both platforms.
@@ -131,7 +154,7 @@ to settle, configure the engine's
[`settle` and `transition` options](/reference/mobile#engine).
-
+
Configure your app and devices.
diff --git a/docs/migrate/maestro.mdx b/docs/migrate/maestro.mdx
index c293363c7..8b7dad919 100644
--- a/docs/migrate/maestro.mdx
+++ b/docs/migrate/maestro.mdx
@@ -1,8 +1,32 @@
---
title: From Maestro
description: Move Maestro flows to TypeScript with locators, assertions, and agent goals.
+icon: "/images/icons/maestro.svg"
---
+import { SetupPrompt } from '../snippets/setup-prompt-card.jsx';
+
+export const migrationPrompt = `Migrate this project's Maestro flows to e2e, one flow at a time. Docs: \`https://e2e.tester.army/docs/migrate/maestro.md\` and \`https://e2e.tester.army/docs/mobile.md\`
+
+1. Read the migration guide above. It maps every Maestro config option and command to e2e and marks what e2e does not have yet. The mobile guide covers devices and app builds.
+2. e2e tests are TypeScript. If the project has no \`package.json\`, as in a native iOS or Android project, create the test project in an \`e2e/\` folder beside the app.
+3. If the test project has no e2e config (\`e2e.config.ts\` or \`e2e.config.mts\`), run \`npx e2e init --yes\` (or the pnpm or bun equivalent) there. It writes a web config and an example test, and installs nothing yet. Replace @e2e-dev/web with @e2e-dev/mobile, and rewrite \`tests/example.e2e.ts\` for the mobile engine or delete it.
+4. Read the e2e skill (\`npx e2e guide\` prints it, \`npx e2e guide \` prints a topic) and follow it for every step below.
+5. If you ran init, ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
+6. Port the flows' \`appId\` and \`.maestro/config.yaml\` into the e2e config: one target per platform with \`mobile({ platform })\`, \`app: { bundleId }\` on each target, and tags as \`{ tags }\` on tests. Move passwords and tokens from \`env\` into \`credentials\` or \`secrets\`.
+7. Install dependencies, then run \`npx agent-device doctor\` and fix what it reports.
+8. List the flows and ask me which one to move first. Suggest one whose \`optional\` steps and \`runFlow: { when }\` branches often need updating.
+9. Rewrite that flow as a \`.e2e.ts\` test under \`tests/\` (e2e runs \`tests/**/*.e2e.ts\` by default), with one awaited call per command. Start it with \`app.open()\`, since nothing launches on its own. Maestro matches text and ids as regular expressions and taps the first of several matches; e2e matches the whole string and fails on several, so use a RegExp, \`.first()\`, or \`.nth()\` where the flow relied on that. \`optional\` and \`runFlow: { when }\` become \`if (await locator.isVisible())\`. Relative selectors such as \`below\` have no port; use a label, a test id, or a goal. Where the guide marks a command missing, use the workaround it gives, or leave that flow in Maestro and tell me why.
+10. Keep the launch and the final assertions deterministic, with \`app.open()\` and locator assertions. Where a run of scripted taps breaks often, propose an \`agent.act\` goal for it instead.
+11. Build and install the app, run the new file with \`npx e2e run \` on each platform target, and fix it until it passes. Keep the Maestro flow until I confirm it can go.
+12. Summarize which flows moved, which stayed in Maestro, and why.`;
+
+
+
+
+
+
+
Translate Maestro commands into awaited TypeScript calls. Keep exact
interactions as locator calls, or give the agent a goal that covers several
of them. Both runners can use the same simulator or emulator.
@@ -167,7 +191,7 @@ declares `requires: ['device']` so other targets skip it. `Status` is `same`,
| `optional: true` on a command | the same `isVisible()` check | renamed |
| `repeat: { times: n }`, `repeat: { while }` | a `for` or `while` loop | renamed |
| `runScript`, `evalScript`, `${output.x}` | TypeScript in the test | renamed |
-| `http.get`, `http.post`, `json()` | Node `fetch`, see [Testing APIs](/api-testing) | renamed |
+| `http.get`, `http.post`, `json()` | Node `fetch`, see [API](/api-testing) | renamed |
| `onFlowStart`, `onFlowComplete` | `beforeEach`, `afterEach` | renamed |
| `startRecording`, `stopRecording` | `test('x', { video: 'on' }, ...)` records that test's whole attempts, on a device the mobile engine records; no start and stop inside a test | renamed |
@@ -220,10 +244,10 @@ Start with a flow whose `optional` steps and `runFlow: { when }` branches
often need updating.
-
+
Configure your app and devices.
-
- Write test steps as goals instead of scripted selectors.
+
+ Goals, assertions, and locators instead of scripted selectors.
diff --git a/docs/migrate/playwright.mdx b/docs/migrate/playwright.mdx
index bc953d776..928fa9490 100644
--- a/docs/migrate/playwright.mdx
+++ b/docs/migrate/playwright.mdx
@@ -1,8 +1,31 @@
---
title: From Playwright
description: Run e2e alongside Playwright and migrate tests one file at a time.
+icon: "/images/icons/playwright.svg"
---
+import { SetupPrompt } from '../snippets/setup-prompt-card.jsx';
+
+export const migrationPrompt = `Migrate this project's Playwright tests to e2e, one spec at a time. Docs: \`https://e2e.tester.army/docs/migrate/playwright.md\`
+
+1. Read the migration guide above. It maps every Playwright config key, API, and CLI flag to e2e and marks what e2e does not have yet.
+2. If the project has no e2e config (\`e2e.config.ts\` or \`e2e.config.mts\`), run \`npx e2e init --yes\` (or the pnpm or bun equivalent). It writes a config and an example test, and installs nothing yet.
+3. Read the e2e skill (\`npx e2e guide\` prints it, \`npx e2e guide \` prints a topic) and follow it for every step below.
+4. If you ran init, ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
+5. Port \`playwright.config.ts\` into the e2e config with the guide's Config tables. \`baseURL\` and \`webServer\` become the target's \`app\`, \`projects\` become \`targets\`, and browser options under \`use\` (viewport, locale, testIdAttribute) become \`web(...)\` options; \`trace\`, \`video\`, and the timeouts are top-level keys. Keep \`@playwright/test\` installed and the two file patterns apart.
+6. Install dependencies.
+7. List the Playwright specs and ask me which one to move first. Suggest a flow whose steps change often, such as a wizard or a checkout.
+8. Rewrite that spec as a \`.e2e.ts\` file under \`tests/\` (e2e runs \`tests/**/*.e2e.ts\` by default). Map each \`page\` call to \`screen\`, \`app\`, or \`browser\` with the guide. String names in e2e queries match the whole text, case-sensitive, so add \`{ exact: false }\` or a RegExp where the Playwright query matched a fragment. Where the guide marks a feature missing, use the workaround it gives, or leave that test in Playwright and tell me why.
+9. Keep navigation and the final assertions deterministic, with \`app.open()\` and locator assertions. Where a run of scripted interactions breaks often, propose an \`agent.act\` goal for it instead.
+10. Run the new file with \`npx e2e run \` and fix it until it passes. Keep the Playwright spec until I confirm it can go.
+11. Summarize which tests moved, which stayed in Playwright, and why.`;
+
+
+
+
+
+
+
`@e2e-dev/web` brings its own pinned `playwright-core`, so your
`@playwright/test` version is yours to keep. Both runners can stay in one
project. e2e reads `e2e.config.ts` and finds `tests/**/*.e2e.ts` by default,
@@ -27,7 +50,7 @@ A Playwright key the runner recognizes (`testDir`, `baseURL`, `webServer`,
| Playwright | e2e | Status |
| --- | --- | --- |
| `use.baseURL` | `app: { url }` on the target | renamed |
-| `webServer` | `app: { url, command }` on the target, see [Starting your app](/starting-your-app) | renamed |
+| `webServer` | `app: { url, command }` on the target, see [Starting your app](/web#starting-your-app) | renamed |
| `webServer` array | `app: { url, command }` on each target; targets declaring the same command share one process. Other dependency processes are not started by this version | missing |
| `webServer.reuseExistingServer` | `app.command: { reuseExisting: true }`; CI ignores it | renamed |
| `globalSetup`, `globalTeardown` | not yet; `test.setup` for sign-in, hooks and fixtures for setup code | missing |
@@ -238,7 +261,7 @@ what to do today.
| `page.exposeFunction()`, `addStyleTag()`, `setExtraHTTPHeaders()`, `emulateMedia()`, `clock` | not yet; `browser.evaluate` appends a `