e2e 0.21.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act-validation.js +1 -0
  4. package/dist/agent/act.js +7 -3
  5. package/dist/agent/action-dispatcher.js +30 -8
  6. package/dist/agent/actions.d.ts +2 -0
  7. package/dist/agent/actions.js +5 -2
  8. package/dist/agent/error.js +1 -0
  9. package/dist/agent/executor.d.ts +14 -6
  10. package/dist/agent/index.js +5 -1
  11. package/dist/agent/invocation.d.ts +3 -0
  12. package/dist/agent/model/sdk.d.ts +5 -0
  13. package/dist/agent/model/sdk.js +18 -1
  14. package/dist/agent/observation.js +1 -0
  15. package/dist/agent/primitives.js +14 -3
  16. package/dist/agent/replay.js +9 -3
  17. package/dist/agent/step-cache.d.ts +3 -2
  18. package/dist/agent/step-cache.js +7 -4
  19. package/dist/agent/tool-loop.js +4 -1
  20. package/dist/cache/context.js +1 -1
  21. package/dist/cache/recorder.d.ts +2 -1
  22. package/dist/cache/recorder.js +11 -2
  23. package/dist/cache/trace-fields.d.ts +10 -0
  24. package/dist/cache/trace-fields.js +17 -0
  25. package/dist/cache/trace.d.ts +9 -1
  26. package/dist/cache/trace.js +26 -7
  27. package/dist/cli/feedback.js +2 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/collect/select.js +12 -2
  30. package/dist/config/agent.d.ts +1 -1
  31. package/dist/config/diagnose.js +9 -3
  32. package/dist/config/esm-hooks.js +3 -2
  33. package/dist/config/resolve.js +3 -2
  34. package/dist/config/typescript.js +52 -2
  35. package/dist/engine/contract.d.ts +3 -2
  36. package/dist/engine/manifest.js +2 -2
  37. package/dist/internal/errors.js +5 -2
  38. package/dist/internal/source.js +2 -0
  39. package/dist/internal/unserved-model.d.ts +5 -0
  40. package/dist/internal/unserved-model.js +22 -0
  41. package/dist/locator/screen.js +1 -2
  42. package/dist/oauth/copilot.js +23 -5
  43. package/dist/oauth/device-code.js +35 -19
  44. package/dist/oauth/errors.d.ts +3 -0
  45. package/dist/oauth/errors.js +26 -11
  46. package/dist/oauth/providers/github-copilot.d.ts +4 -0
  47. package/dist/oauth/providers/github-copilot.js +64 -16
  48. package/dist/oauth/providers/openai.d.ts +2 -2
  49. package/dist/oauth/providers/openai.js +47 -19
  50. package/dist/oauth/providers/opencode-console.js +36 -15
  51. package/dist/oauth/providers/xai.js +18 -9
  52. package/dist/oauth/token-endpoint.d.ts +15 -6
  53. package/dist/oauth/token-endpoint.js +18 -6
  54. package/dist/report/code-frame.js +9 -3
  55. package/dist/report/failure-text.d.ts +10 -2
  56. package/dist/report/failure-text.js +24 -8
  57. package/dist/report/list-model.d.ts +13 -0
  58. package/dist/report/list-steps.d.ts +6 -1
  59. package/dist/report/list-steps.js +29 -0
  60. package/dist/report/list.d.ts +1 -0
  61. package/dist/report/list.js +16 -7
  62. package/dist/report/live-window.js +2 -1
  63. package/dist/report/markdown.js +1 -1
  64. package/dist/run/fixtures.js +2 -0
  65. package/dist/run/host-trace.d.ts +3 -2
  66. package/dist/run/host-trace.js +2 -1
  67. package/dist/run/last-run.d.ts +9 -0
  68. package/dist/run/last-run.js +28 -0
  69. package/dist/run/open-session.d.ts +3 -1
  70. package/dist/run/open-session.js +1 -1
  71. package/dist/run/runner.js +6 -2
  72. package/dist/run/sessions.js +3 -3
  73. package/dist/run/standalone-attempt.d.ts +2 -1
  74. package/dist/run/standalone-attempt.js +3 -1
  75. package/dist/run/steps.d.ts +3 -1
  76. package/dist/run/units.d.ts +8 -0
  77. package/dist/run/units.js +14 -0
  78. package/dist/run/worker-models.d.ts +2 -0
  79. package/dist/run/worker-models.js +7 -0
  80. package/dist/types.d.ts +2 -2
  81. package/docs/authentication.mdx +26 -23
  82. package/docs/browser.mdx +7 -4
  83. package/docs/bug-bash.mdx +2 -2
  84. package/docs/cache.mdx +6 -1
  85. package/docs/ci/bitrise.mdx +1 -1
  86. package/docs/ci/codemagic.mdx +2 -2
  87. package/docs/ci/eas-workflows.mdx +1 -1
  88. package/docs/coding-agents.mdx +7 -35
  89. package/docs/debugging.mdx +24 -36
  90. package/docs/executors.mdx +1 -1
  91. package/docs/github.mdx +2 -1
  92. package/docs/integrations/eas.mdx +1 -1
  93. package/docs/integrations/index.mdx +6 -0
  94. package/docs/integrations/lightpanda.mdx +192 -0
  95. package/docs/integrations/testmu.mdx +152 -0
  96. package/docs/mcp.mdx +123 -0
  97. package/docs/migrate/detox.mdx +1 -1
  98. package/docs/migrate/maestro.mdx +101 -105
  99. package/docs/migrate/playwright.mdx +65 -70
  100. package/docs/mobile.mdx +18 -3
  101. package/docs/models.mdx +4 -2
  102. package/docs/quickstart.mdx +7 -0
  103. package/docs/reference/agent.mdx +369 -277
  104. package/docs/reference/app.mdx +141 -32
  105. package/docs/reference/cli.mdx +674 -558
  106. package/docs/reference/config.mdx +1406 -752
  107. package/docs/reference/engine.mdx +1062 -256
  108. package/docs/reference/environment.mdx +156 -84
  109. package/docs/reference/errors.mdx +106 -72
  110. package/docs/reference/expect.mdx +752 -372
  111. package/docs/reference/mcp.mdx +366 -141
  112. package/docs/reference/mobile.mdx +844 -403
  113. package/docs/reference/reporters.mdx +312 -106
  114. package/docs/reference/runner.mdx +373 -174
  115. package/docs/reference/screen.mdx +931 -398
  116. package/docs/reference/security.mdx +106 -40
  117. package/docs/reference/test.mdx +626 -273
  118. package/docs/reference/web.mdx +956 -533
  119. package/docs/security.mdx +6 -5
  120. package/docs/snippets/model-provider.jsx +763 -0
  121. package/docs/snippets/setup-prompt-card.jsx +22 -0
  122. package/docs/snippets/show-more.jsx +22 -0
  123. package/docs/snippets/video-card.jsx +34 -0
  124. package/docs/snippets/video.jsx +25 -0
  125. package/docs/subscriptions.mdx +10 -3
  126. package/docs/telemetry.mdx +39 -39
  127. package/package.json +3 -2
  128. package/schema/report-v1.schema.json +6 -0
  129. package/skills/e2e/references/agent.md +8 -3
  130. package/skills/e2e/references/debugging.md +1 -1
  131. package/skills/e2e/references/mcp.md +1 -1
  132. package/skills/e2e/references/setup.md +12 -3
  133. package/skills/e2e/references/writing-tests.md +1 -1
@@ -51,15 +51,15 @@ A Playwright key such as `testDir`, `baseURL`, `use`, `webServer`, or
51
51
  | `webServer` | `app: { url, command }` on the target, see [Starting your app](/web#starting-your-app) | renamed |
52
52
  | `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 |
53
53
  | `webServer.reuseExistingServer` | `app.command: { reuseExisting: true }`; CI ignores it | renamed |
54
- | `globalSetup`, `globalTeardown` | not yet; `test.setup` for sign-in, hooks and fixtures for setup code | missing |
54
+ | `globalSetup`, `globalTeardown` | `test.setup` for sign-in, hooks and fixtures for setup code | missing |
55
55
  | `projects` | `targets`, one engine instance each | renamed |
56
56
  | `testDir`, `testMatch` | `tests`, globs relative to the project root | renamed |
57
57
  | `testIgnore` | a `tests` entry with a leading `!`: `tests: ['tests/**/*.e2e.ts', '!tests/wip/**']` | renamed |
58
- | `fullyParallel` | not yet; a file's tests run in order on one worker, files run in parallel | missing |
58
+ | `fullyParallel` | a file's tests run in order on one worker, files run in parallel | missing |
59
59
  | `maxFailures` | `--max-failures <n>`; no config key | renamed |
60
60
  | `globalTimeout` | `maxDuration`, or `--max-duration <ms>`; the clock starts with the tests, not app startup | renamed |
61
61
  | `reporter` | `reporters: ['list', 'junit']`; also `json` and `markdown` | renamed |
62
- | `reporter: 'html'` | no equivalent; `markdown` writes `.e2e/summary.md`, and each failed test gets a `trace.md` page under `.e2e/results/` | missing |
62
+ | `reporter: 'html'` | the `markdown` reporter writes `.e2e/summary.md` | missing |
63
63
  | `outputDir` | `output`, default `.e2e`, holding `results/` (cleared when a run starts, like `outputDir`) beside the report | renamed |
64
64
  | `metadata` | not yet | missing |
65
65
 
@@ -71,22 +71,22 @@ A Playwright key such as `testDir`, `baseURL`, `use`, `webServer`, or
71
71
  | `use.testIdAttribute` | `web({ testIdAttribute })` | renamed |
72
72
  | `use.httpCredentials` | `web({ basicAuth })` | renamed |
73
73
  | `use.extraHTTPHeaders` | `web({ headers })`, sent to the app's site only | renamed |
74
- | `use.connectOptions`, `connectOverCDP` | `web({ connect })` | renamed |
74
+ | `chromium.connectOverCDP(url)` | `web({ connect: { cdpEndpoint: () => url } })`, chromium only | renamed |
75
+ | `use.connectOptions` (`wsEndpoint`) | `web({ connect })` attaches over CDP only | missing |
75
76
  | `retries`, `workers`, `timeout` | the same names at the top level; the defaults differ, see [below](#defaults-that-differ) | same |
76
77
  | `expect.timeout` | `assertionTimeout` | renamed |
77
78
  | `use.actionTimeout` | `actionTimeout` | renamed |
78
79
  | `use.navigationTimeout` | `timeout`, the test budget; `browser.goto(url, { timeout })` overrides one call | renamed |
79
- | `use.storageState`, `projects[].dependencies` for sign-in | `test.setup` and `{ session }`, see [Signing in](/authentication); no file import, see [What differs on purpose](#what-differs-on-purpose) | by design |
80
+ | `use.storageState`, a `setup` project in `projects[].dependencies` | `test.setup(title, { sessions }, fn)` with `session.save(name)`, then `{ session }` on a test, see [Signing in](/authentication); no file, see [What differs on purpose](#what-differs-on-purpose) | by design |
80
81
  | `use.storageState: { cookies }` with fixed values | `browser.setCookies(cookies)` in a fixture | renamed |
81
- | `use.trace` | `trace` with the modes `'off'`, `'on'`, `'retain-on-failure'`, `'on-first-retry'`, and `'on-all-retries'`, at the top of the config, per target, `test('x', { trace }, ...)`, or `--trace [mode]` for one run. A trace is a markdown page, `trace.md`, in each test's directory under `.e2e/results/`, not a zip; the default differs, see [below](#defaults-that-differ) | same |
82
+ | `use.trace` | `trace`: `'off'`, `'on'`, `'retain-on-failure'`, `'on-first-retry'`, or `'on-all-retries'`, at the top of the config, per target, per test, or `--trace [mode]`. A trace is a markdown page, `trace.md`, in each test's directory under `.e2e/results/`, not a zip. The default differs, see [below](#defaults-that-differ) | same |
82
83
  | `use.video` | `video`, with the modes and places of `trace`, and `--video [mode]` | same |
83
84
  | `use.headless: false` | `--headed` on the CLI | renamed |
84
85
  | `use.locale`, `use.timezoneId` | `web({ locale, timezoneId })` | renamed |
85
86
  | `use.colorScheme`, `use.reducedMotion` | `web({ colorScheme, reducedMotion })` | renamed |
86
- | `geolocation`, `permissions` | not yet | missing |
87
87
  | `use.userAgent` | `web({ userAgent })` | renamed |
88
- | `devices['iPhone 15']`, `deviceScaleFactor`, `isMobile`, `hasTouch` | not yet; `web({ viewport })` sets the size only | missing |
89
- | `ignoreHTTPSErrors`, `bypassCSP`, `offline`, `launchOptions`, `proxy` | not yet | missing |
88
+ | `devices['iPhone 15']`, `deviceScaleFactor`, `isMobile`, `hasTouch` | `web({ viewport })` sets the size only | missing |
89
+ | `geolocation`, `permissions`, `ignoreHTTPSErrors`, `bypassCSP`, `offline`, `launchOptions`, `proxy` | not yet | missing |
90
90
 
91
91
  ## Rewrite one spec
92
92
 
@@ -136,21 +136,19 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
136
136
  | Playwright | e2e | Status |
137
137
  | --- | --- | --- |
138
138
  | `page.getByRole`, `getByLabel`, `getByText`, `getByTestId`, `getByPlaceholder` | the same on `screen`; `getByRole` also takes the name positionally, `getByRole('button', 'Save')` | same |
139
- | `getByRole(role, { name })`, `getByLabel`, `getByPlaceholder`, `getByText` with a string | matches the whole string, case-sensitive; `{ exact: false }` for Playwright's substring match, see [What differs on purpose](#what-differs-on-purpose) | by design |
139
+ | `getByRole(role, { name })`, `getByLabel`, `getByPlaceholder`, `getByText` with a string | an exact, case-sensitive match, see [What differs on purpose](#what-differs-on-purpose) | by design |
140
140
  | `getByRole('img')` | the same; `img` is an alias of `image` | same |
141
141
  | `getByAltText(alt)` | `getByRole('image', alt)` | renamed |
142
- | `getByTitle` | not yet; the node carries no title | missing |
142
+ | `getByTitle` | the node carries no title | missing |
143
143
  | `getByRole(role, { includeHidden })` | a role query never matches a hidden node; `{ visible: true }` on the other queries drops hidden twins | by design |
144
- | `locator.filter({ hasText, has })` | the same | same |
145
- | `filter({ hasNot, hasNotText })` | not yet; a positive filter or `nth`. `INVALID_LOCATOR`, alone or beside `hasText` or `has` | missing |
146
- | `first()`, `last()`, `nth(i)` | the same | same |
144
+ | `locator.filter({ hasText, has })`, `first()`, `last()`, `nth(i)`, `count()` | the same | same |
145
+ | `filter({ hasNot, hasNotText })` | a positive filter or `nth`. `INVALID_LOCATOR`, alone or beside `hasText` or `has` | missing |
147
146
  | `locator.and(other)`, `locator.or(other)` | not yet | missing |
148
- | `locator.count()` | the same | same |
149
147
  | `locator.all()`, `allTextContents()` | the same; `allInnerTexts()` is `allTextContents()` | same |
150
148
  | `page.locator('css')`, `page.locator('xpath=...')` | `browser.locator('css')`: the string is a Playwright selector, so `xpath=` (or a leading `//`) and `>>` chains work too | web only |
151
- | `page.waitForSelector(selector)` | `browser.locator(selector).waitFor()`; a locator is strict, so narrow a selector with several matches with a `>> visible=true` step or `first()` | renamed |
152
- | `page.click(selector)`, `page.fill(selector, value)`, `page.getAttribute(selector, name)`, and the other `page` methods that take a selector | `browser.locator(selector).tap()`, `.fill(value)`, `.getAttribute(name)`. Playwright runs these on the first match; a locator is strict, so narrow a selector with several matches with `>> visible=true` or `first()`, which is what Playwright did | renamed |
153
- | `locator.locator('css')` | not yet; a semantic query inside the scope (`cell.getByRole('textbox')` for `cell.locator('input')`), `browser.locator('css').getByRole(...)` the other way round, or one `browser.locator` selector for the whole path | missing |
149
+ | `page.waitForSelector(selector)` | `browser.locator(selector).waitFor()`; a locator is strict, see [What differs on purpose](#what-differs-on-purpose) | renamed |
150
+ | `page.click(selector)`, `page.fill(selector, value)`, `page.getAttribute(selector, name)`, and the other `page` methods that take a selector | `browser.locator(selector).tap()`, `.fill(value)`, `.getAttribute(name)`. Playwright acts on the first match; a locator is strict, so add `first()` for the old behavior, see [What differs on purpose](#what-differs-on-purpose) | renamed |
151
+ | `locator.locator('css')` | a semantic query inside the scope (`cell.getByRole('textbox')` for `cell.locator('input')`), `browser.locator('css').getByRole(...)` the other way round, or one `browser.locator` selector for the whole path | missing |
154
152
  | `page.frameLocator('#frame')` | `browser.frameLocator('#frame')`, then `screen` queries inside it | web only |
155
153
 
156
154
  </Accordion>
@@ -162,29 +160,28 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
162
160
  | `click({ button: 'right' })` | `secondaryTap()` | renamed |
163
161
  | `dblclick()` | `doubleTap()`; `dblclick()` is an alias | same |
164
162
  | `click({ position })` | `tap({ position })`, an offset from the node's top-left corner | renamed |
165
- | `click({ force })` | not yet; `tap({ position })` skips every actionability check but the scroll into view | missing |
163
+ | `click({ force })` | `tap({ position })` skips every actionability check but the scroll into view | missing |
166
164
  | `click({ modifiers })`, `dblclick({ modifiers })` | `tap({ modifiers })`, `doubleTap({ modifiers })` or `dblclick({ modifiers })`, `secondaryTap({ modifiers })`; not beside `position` yet | same |
167
165
  | `click({ clickCount, delay, trial })` | not yet | missing |
168
- | `{ trial }`, `{ force }`, `{ noWaitAfter }` on `check()`, `fill()`, and the other actions | not yet; an unknown option is `INVALID_ARGUMENT`, never ignored | missing |
166
+ | `{ trial }`, `{ force }`, `{ noWaitAfter }` on `check()`, `fill()`, and the other actions | an unknown option is `INVALID_ARGUMENT`, never ignored | missing |
169
167
  | `fill(value)` | the same; also takes a `Secret` | same |
170
168
  | `pressSequentially(text, { delay })` | the same; keystrokes through the platform keyboard, a `Secret` is refused | same |
171
169
  | `press(key)` | the same; modifiers join with `+`, see the [key grammar](/reference/engine#keys) | same |
172
170
  | `clear()`, `focus()`, `hover()`, `setInputFiles()`, `dragTo()` | the same | same |
173
171
  | `check()`, `uncheck()` | the same, except when the state updates late or the click removes the control, see [What differs on purpose](#what-differs-on-purpose) | by design |
174
172
  | `selectOption('pro')` | `selectOption('Pro')`: a string matches the option's label only, where Playwright matches its value or its label; `selectOption({ value: 'pro' })` for the value | by design |
175
- | `selectOption(['a', 'b'])` | not yet; one option per call. An array is `INVALID_ARGUMENT` | missing |
176
- | `setInputFiles({ name, buffer })` | not yet; paths only, resolved from the project root | missing |
173
+ | `selectOption(['a', 'b'])` | one option per call. An array is `INVALID_ARGUMENT` | missing |
174
+ | `setInputFiles({ name, buffer })` | paths only, resolved from the project root | missing |
177
175
  | `scrollIntoViewIfNeeded()` | `scrollIntoView()` | renamed |
178
176
  | `locator.innerText()` | `textContent()`, whitespace collapsed to single spaces | renamed |
179
177
  | `locator.textContent()` | the rendered text, what `innerText()` reads, see [What differs on purpose](#what-differs-on-purpose) | by design |
180
- | `inputValue()`, `getAttribute()`, `isVisible()`, `isEnabled()`, `boundingBox()` | the same | same |
178
+ | `inputValue()`, `getAttribute()`, `isVisible()`, `isHidden()`, `isEnabled()`, `isDisabled()`, `boundingBox()` | the same | same |
181
179
  | `isChecked()` | the same on a checkbox, radio, or `aria-checked` node; `false` on any other, see [What differs on purpose](#what-differs-on-purpose) | by design |
182
- | `isHidden()`, `isDisabled()` | the same | same |
183
180
  | `isEditable()` | not yet | missing |
184
181
  | `waitFor({ state: 'visible' })`, `'hidden'`, `'attached'`, `'detached'` | the same | same |
185
- | `locator.evaluate(fn)`, `evaluateAll(fn)` | not yet; `browser.evaluate(fn, selector)` with a CSS selector argument | missing |
186
- | `locator.screenshot()` | not yet; `app.screenshot(label)` captures the viewport | missing |
187
- | `innerHTML()`, `dispatchEvent()`, `selectText()`, `blur()`, `highlight()` | not yet; `browser.evaluate` | missing |
182
+ | `locator.evaluate(fn)`, `evaluateAll(fn)` | `browser.evaluate(fn, selector)` with a CSS selector argument | missing |
183
+ | `locator.screenshot()` | `app.screenshot(label)` captures the viewport | missing |
184
+ | `innerHTML()`, `dispatchEvent()`, `selectText()`, `blur()`, `highlight()` | `browser.evaluate` | missing |
188
185
  | `ariaSnapshot()` | not yet | missing |
189
186
  | `page.keyboard.press`, `keyboard.type` | `browser.keyboard.press`, `browser.keyboard.type`; plain text only, never a `Secret` | web only |
190
187
  | `keyboard.down`, `keyboard.up` | `browser.keyboard.down`, `browser.keyboard.up`; keys and modifiers stay held until released | web only |
@@ -199,36 +196,33 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
199
196
 
200
197
  | Playwright | e2e | Status |
201
198
  | --- | --- | --- |
202
- | `expect(locator).toBeVisible()`, `toBeHidden()`, `toBeEnabled()`, `toBeDisabled()`, `toBeFocused()` | the same | same |
199
+ | `expect(locator).toBeVisible()`, `toBeHidden()`, `toBeEnabled()`, `toBeDisabled()`, `toBeFocused()`, `toHaveValue()`, `toHaveAttribute()`, `toHaveCount()`, `toHaveAccessibleName()` | the same | same |
203
200
  | `toBeChecked()` | the same on a checkable node; on any other node `.not.toBeChecked()` passes, see [What differs on purpose](#what-differs-on-purpose) | by design |
204
- | `toHaveText()`, `toContainText()` | compare the rendered text, as `{ useInnerText: true }` does in Playwright, see [What differs on purpose](#what-differs-on-purpose) | by design |
205
- | `toHaveValue()`, `toHaveAttribute()`, `toHaveCount()`, `toHaveAccessibleName()` | the same | same |
201
+ | `toHaveText()`, `toContainText()` | compare the rendered text, see [What differs on purpose](#what-differs-on-purpose) | by design |
206
202
  | `toHaveText(['a', 'b'])`, `toContainText([...])` on several matches | the same: `toHaveText` needs the count to match, `toContainText` an ordered subsequence | same |
207
203
  | `toBeChecked({ checked: false })`, `toBeEnabled({ enabled })`, `toBeVisible({ visible })`, `toBeAttached({ attached })` | the same | same |
208
204
  | `{ ignoreCase }` on `toHaveText()`, `toContainText()`, `toHaveAccessibleName()`, `toHaveAttribute(name, value)` | the same | same |
209
- | `toBeChecked({ indeterminate })`, `{ useInnerText }` | not yet; `INVALID_ARGUMENT`, so the matcher never passes on its default | missing |
205
+ | `toBeChecked({ indeterminate })`, `{ useInnerText }` | `INVALID_ARGUMENT`, so the matcher never passes on its default | missing |
210
206
  | `toHaveAttribute('aria-selected', 'true')`, `'aria-expanded'` | `toBeSelected()`, `toBeExpanded()` | renamed |
211
- | a positive matcher on several matches | `LOCATOR_AMBIGUOUS` at once; narrow with `filter`, `first`, or `{ visible: true }` | by design |
207
+ | a positive matcher on several matches | `LOCATOR_AMBIGUOUS` at once, see [What differs on purpose](#what-differs-on-purpose) | by design |
212
208
  | `toBeAttached()` | the same; passes on a hidden match too | same |
213
209
  | `toBeEmpty()`, `toBeInViewport()`, `toBeEditable()`, `toHaveRole()`, `toHaveAccessibleDescription()` | not yet | missing |
214
210
  | `expect(locator).toHaveClass(cls)` | `expect(browser).toHaveClass(locator, cls)`; a DOM fact is asserted through `browser`; reads the `class` attribute, so `''` matches `class=""` and not a missing attribute | web only |
215
- | `toHaveCSS()`, `toHaveId()`, `toHaveJSProperty()`, `toContainClass()`, `toHaveValues()` | not yet; `browser.evaluate` plus a value matcher | missing |
211
+ | `toHaveCSS()`, `toHaveId()`, `toHaveJSProperty()`, `toContainClass()`, `toHaveValues()` | `browser.evaluate` plus a value matcher | missing |
216
212
  | `expect(page).toHaveURL()`, `toHaveTitle()` | `expect(browser).toHaveURL()`, `toHaveTitle()`; `toHaveURL` takes `ignoreCase` | web only |
217
213
  | `expect(page).toHaveScreenshot()`, `expect(locator).toHaveScreenshot()` | `expect(screen).toHaveScreenshot()`, `expect(locator).toHaveScreenshot()` on every target, a device included, with `threshold`, `maxDiffPixels`, `maxDiffPixelRatio`, `mask`, `maskColor`, and `timeout`. See [Visual comparison](/visual-testing) | same |
218
- | `toHaveScreenshot({ fullPage, clip, animations, caret, scale, stylePath })` | not yet; the viewport, at CSS scale, animations stopped and the caret hidden | missing |
219
- | `toMatchSnapshot()` | not yet | missing |
220
- | `toMatchAriaSnapshot()` | not yet | missing |
221
- | `expect(value).toBe()`, `toEqual()`, `toMatch()`, `toBeTruthy()`, `toBeCloseTo()` and the rest | the same, except `toContain`, below, see [value matchers](/reference/expect#value-matchers) | same |
214
+ | `toHaveScreenshot({ fullPage, clip, animations, caret, scale, stylePath })` | always the viewport, at CSS scale, animations stopped and the caret hidden | missing |
215
+ | `toMatchSnapshot()`, `toMatchAriaSnapshot()` | not yet | missing |
216
+ | `expect(value).toBe()`, `toEqual()`, `toMatch()`, `toBeTruthy()`, `toBeCloseTo()`, `toHaveLength()`, `toMatchObject()`, `toHaveProperty()`, and the rest | the same, except `toContain`, below, see [value matchers](/reference/expect#value-matchers) | same |
222
217
  | `expect(collection).toContain()` | compares items by deep equality, like Playwright's `toContainEqual`, so `toContain({ id: 1 })` passes on `[{ id: 1 }]`. Containment in a string is the same | by design |
223
- | `toHaveLength()`, `toMatchObject()`, `toHaveProperty()` | the same | same |
224
- | `toStrictEqual()`, `toThrow()`, `toBeInstanceOf()` | not yet; `toEqual` on the shape you need | missing |
218
+ | `toStrictEqual()`, `toThrow()`, `toBeInstanceOf()` | `toEqual` on the shape you need | missing |
225
219
  | `expect.any()`, `objectContaining()`, `arrayContaining()`, `stringMatching()` | the same, plus `anything()` and `stringContaining()`, see [asymmetric matchers](/reference/expect#asymmetric-matchers) | same |
226
220
  | `expect(async () => { ... }).toPass()` | `expect.poll(read).toBe(...)`; poll an array with `toEqual([a, b])` for several values at once | renamed |
227
221
  | `expect.poll()` | the same | same |
228
222
  | `expect.soft()` | the same; kept failures fail the attempt once the body settles, see [soft assertions](/reference/expect#soft-assertions) | same |
229
223
  | `expect(locator, 'message')` | `expect(locator)`; a locator failure already names the locator, the matcher, and what was observed. Only a value expectation takes a message | by design |
230
224
  | `expect.extend()` | not yet | missing |
231
- | `expect(promise).resolves`, `.rejects` | not yet; `await` first, then `expect(value)` | missing |
225
+ | `expect(promise).resolves`, `.rejects` | `await` first, then `expect(value)` | missing |
232
226
 
233
227
  </Accordion>
234
228
  <Accordion title="Navigation and app">
@@ -240,15 +234,15 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
240
234
  | `page.goBack()` | `app.back()`, or `browser.back()` | renamed |
241
235
  | `page.goForward()`, `page.reload()` | `browser.forward()`, `browser.reload()` | web only |
242
236
  | `page.url()`, `page.title()` | `browser.url()`, `browser.title()` | web only |
243
- | `page.waitForURL()` | `browser.waitForURL()`; a string or `RegExp`, no predicate function, and `timeout` is its only option, so `waitUntil` is `INVALID_ARGUMENT`. A string is one URL, a relative one resolved against `app.url`, matched exactly and not as a glob: `'**/orders'` never matches, so pass a `RegExp` | web only |
244
- | `page.waitForLoadState()` | not yet; `browser.goto(url, { waitUntil })` on the navigation itself | missing |
237
+ | `page.waitForURL()` | `browser.waitForURL()`, with a string or `RegExp` and `timeout` as the only option. A string is an absolute URL, or a relative one resolved against `app.url`, matched exactly, not a glob: `'**/orders'` never matches, so pass a `RegExp` | web only |
238
+ | `page.waitForLoadState()` | `browser.goto(url, { waitUntil })` on the navigation itself | missing |
245
239
  | `page.waitForFunction(fn)` | `expect.poll(() => browser.evaluate(fn)).toBeTruthy()` | missing |
246
240
  | `page.waitForTimeout(ms)` | a locator assertion on the state you are waiting for: `toBeVisible()` on what appears, `toBeHidden()` on what goes away | by design |
247
241
  | `page.setViewportSize()` | `browser.setViewport()`, before or after the first navigation | web only |
248
- | `page.viewportSize()` | not yet; `browser.evaluate(() => ({ width: innerWidth, height: innerHeight }))` | missing |
242
+ | `page.viewportSize()` | `browser.evaluate(() => ({ width: innerWidth, height: innerHeight }))` | missing |
249
243
  | `page.screenshot()` | `app.screenshot(label)`: the viewport, redacted, attached to the report; no `fullPage` | renamed |
250
244
  | a fresh `context` per test | every attempt starts in a fresh context; `app.restart()` recreates it, `app.clearState()` also clears storage | same |
251
- | `page.pause()` | not yet; `--headed` to watch, [`e2e mcp`](/reference/mcp) to drive the app from a coding agent | missing |
245
+ | `page.pause()` | `--headed` to watch, [`e2e mcp`](/mcp) to drive the app from a coding agent | missing |
252
246
 
253
247
  </Accordion>
254
248
  <Accordion title="Network and browser">
@@ -258,22 +252,22 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
258
252
  | `page.route(pattern, handler)` | `browser.route(pattern, handler)`; a string or `RegExp` pattern, no predicate function; the handler decides exactly once | web only |
259
253
  | `route.fulfill({ json, body, path, status, headers, contentType })` | the same; one of `json`, `body`, or `path`, and `path` is relative to the project root | web only |
260
254
  | `route.continue({ url, method, headers, postData })`, `route.fallback()` | the same; a `url` goes through the navigation rule, and `fallback()` takes no overrides | web only |
261
- | `route.fetch()`, `route.abort(code)`, `route.fallback(overrides)` | not yet; `abort()` takes no error code | missing |
255
+ | `route.fetch()`, `route.abort(code)`, `route.fallback(overrides)` | none yet; `abort()` takes no error code | missing |
262
256
  | `page.unroute()` | `browser.unroute()` | web only |
263
257
  | `page.waitForResponse(pattern)` | `browser.waitForResponse(pattern)`: a string or `RegExp`, no predicate function; `url`, `status`, `headers`, `json()`, `text()`. A string is a glob tested against the whole URL, query included, and never resolved against `app.url`: Playwright's `'/api/orders'` is the full URL or `'**/api/orders'`, and `?` matches any one character (write `'\\?'` for a literal one) | web only |
264
- | `response.ok()`, `response.body()`, `response.request()` | not yet; `status` and `text()` | missing |
265
- | `page.waitForRequest()`, `request.postDataJSON()` | not yet; `route.request.postData` is a string | missing |
258
+ | `response.ok()`, `response.body()`, `response.request()` | `status` and `text()` | missing |
259
+ | `page.waitForRequest()`, `request.postDataJSON()` | none yet for `waitForRequest`; `route.request.postData` is a string | missing |
266
260
  | `routeFromHAR()`, `routeWebSocket()` | not yet | missing |
267
261
  | `page.on('dialog')` | `browser.onDialog('accept')`, or a handler that calls `accept` or `dismiss` | web only |
268
262
  | `page.waitForEvent('download')` | `browser.waitForDownload(() => trigger)` | web only |
269
- | `page.waitForEvent('popup')`, `context.on('page')` | not yet; a window the app opens is not driven | missing |
270
- | `page.on('console')`, `page.on('pageerror')` | not yet; assert on what the page shows | missing |
263
+ | `page.waitForEvent('popup')`, `context.on('page')` | a window the app opens is not driven | missing |
264
+ | `page.on('console')`, `page.on('pageerror')` | assert on what the page shows | missing |
271
265
  | `page.evaluate(fn, arg)` | `browser.evaluate(fn, arg)`; JSON in and out, no handles | web only |
272
266
  | `page.addInitScript(script, arg)`, `context.addInitScript()` | `browser.addInitScript(script, arg)`, or `web({ initScripts })` for every test; pass `{ content }` as a plain string | web only |
273
- | `page.emulateMedia({ colorScheme, reducedMotion })` | `browser.emulateMedia()`, for the active tab and every tab the attempt opens after it; `media`, `forcedColors`, and `contrast` are not yet supported | web only |
274
- | `page.exposeFunction()`, `addStyleTag()`, `setExtraHTTPHeaders()`, `clock` | not yet; `browser.evaluate` can append a `<style>` once the page is open | missing |
267
+ | `page.emulateMedia({ colorScheme, reducedMotion })` | `browser.emulateMedia()`, for every open tab and every tab opened after it; `media`, `forcedColors`, and `contrast` are not yet supported | web only |
268
+ | `page.exposeFunction()`, `addStyleTag()`, `setExtraHTTPHeaders()`, `clock` | none yet; for `addStyleTag`, `browser.evaluate` can append a `<style>` once the page is open | missing |
275
269
  | `context.addCookies()`, `context.cookies()` | `browser.setCookies()`, `browser.cookies()` | web only |
276
- | `context.clearCookies()` | not yet; `app.clearState()` also clears other persisted state and recreates the context | missing |
270
+ | `context.clearCookies()` | `app.clearState()` also clears other persisted state and recreates the context | missing |
277
271
  | `request` fixture | Node `fetch`, see [API](/api-testing) | by design |
278
272
  | `context.storageState({ path })` | no file, see [What differs on purpose](#what-differs-on-purpose) | by design |
279
273
 
@@ -288,20 +282,19 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
288
282
  | `{ page }` in a test | `{ screen, app, browser }`; no raw `Page` in tests, see [What differs on purpose](#what-differs-on-purpose) | by design |
289
283
  | `test.skip(title, fn)`, `test.skip(condition, reason)` | the same | same |
290
284
  | `test.only` | the same; CI rejects it with `ONLY_IN_CI` | same |
291
- | `test.describe.only` | not yet; `test.only` on each member | missing |
285
+ | `test.describe.only` | `test.only` on each member | missing |
292
286
  | `test.describe.skip` | `test.describe(title, { skip: true }, fn)` | renamed |
293
287
  | `test.describe.serial`, `describe.configure({ mode: 'serial' })` | `test.describe(title, { serial: true }, fn)`: one retry unit with shared app state | renamed |
294
- | `describe.configure({ mode: 'parallel' })` | not yet; a file's tests run in order on one worker | missing |
288
+ | `describe.configure({ mode: 'parallel' })` | a file's tests run in order on one worker | missing |
295
289
  | `describe.configure({ retries, timeout })` | `{ retries, timeout }` on the group | renamed |
296
290
  | `test.setTimeout(ms)`, `test.slow()` | `test(title, { timeout }, fn)` | renamed |
297
- | `test.fixme()`, `test.fail()` | not yet; `{ skip: 'reason' }` | missing |
298
- | `test.step(title, fn)` | not yet; every `screen`, `browser`, and `agent` call is already a named step in the report, with its arguments redacted | missing |
299
- | `test.info()`, `attach()`, `annotations` | not yet; `app.screenshot(label)` attaches a screenshot | missing |
291
+ | `test.fixme()`, `test.fail()` | `{ skip: 'reason' }` | missing |
292
+ | `test.step(title, fn)` | every `screen`, `browser`, and `agent` call is already a named step in the report, with its arguments redacted | missing |
293
+ | `test.info()`, `attach()`, `annotations` | `app.screenshot(label)` attaches a screenshot | missing |
300
294
  | `{ tag: '@smoke' }` | `{ tags: ['smoke'] }` on a test or group | renamed |
301
295
  | `test.use({ ... })` | per target in the config; `browser.setViewport` at runtime | renamed |
302
296
  | `test.extend({ fixture })` | the same shape; a fixture that needs another goes in a chained `extend`. Not yet on demand: every fixture of the chain sets up for every test on that `test`, named or not. Give each setup its own `test` | missing |
303
- | `{ scope: 'worker' }` fixtures, option fixtures | not yet; `beforeAll` for per-file setup | missing |
304
- | a `setup` project with `storageState` | `test.setup(title, { sessions }, fn)` and `session.save(name)` | renamed |
297
+ | `{ scope: 'worker' }` fixtures, option fixtures | `beforeAll` for per-file setup | missing |
305
298
 
306
299
  </Accordion>
307
300
  <Accordion title="CLI">
@@ -311,12 +304,11 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
311
304
  | `npx playwright test` | `npx e2e run` | renamed |
312
305
  | `npx playwright test tests/login.spec.ts` | `npx e2e run tests/login.e2e.ts`; a directory, a bare name, or a glob also selects | same |
313
306
  | `--project web` | `--target web` | renamed |
314
- | `-g`, `--grep`, `--grep-invert` | `--grep` and `--grep-invert`, matched against the describe titles and the test title joined by spaces; tags are `--tag` and `--exclude-tag` | same |
307
+ | `-g`, `--grep`, `--grep-invert` | `--grep` and `--grep-invert`, no `-g`; matched against the describe titles and the test title joined by spaces; tags are `--tag` and `--exclude-tag` | renamed |
315
308
  | `tests/login.spec.ts:12` | `tests/login.e2e.ts:12`, the test whose `test(` call opens on that line | same |
316
- | `--headed` | the same | same |
317
- | `--debug` | not yet; `--headed` to watch. `e2e run --debug` prints phase timings and the agent step table, not an inspector | missing |
318
- | `--ui` | not yet; `--watch` reruns on save with vitest's keys, `--headed` to watch, `e2e mcp` to drive the app from a coding agent | missing |
319
- | `--retries`, `--workers`, `--config`, `--pass-with-no-tests` | the same | same |
309
+ | `--debug` | `--headed` to watch. `e2e run --debug` prints phase timings and the agent step table, not an inspector | missing |
310
+ | `--ui` | `--watch` reruns on save with vitest's keys, `--headed` to watch, `e2e mcp` to drive the app from a coding agent | missing |
311
+ | `--headed`, `--retries`, `--workers`, `--config`, `--pass-with-no-tests` | the same | same |
320
312
  | `--reporter` | the same flag; the ids are `list`, `json`, `junit`, `markdown` | same |
321
313
  | `--output` | `--output`, the results directory; each test's trace page and artifacts are in its `results/` | same |
322
314
  | `--trace <mode>` | `--trace [mode]`, the same modes; a bare `--trace` is `on` | same |
@@ -324,9 +316,9 @@ uses it declares `requires: ['browser']` and other targets skip it. Calls on
324
316
  | `--quiet` | not yet | missing |
325
317
  | `-u`, `--update-snapshots` | the same; rewrites the stored screenshots that differ | same |
326
318
  | `--list` | `npx e2e list`, with the same selection flags | renamed |
327
- | `npx playwright codegen` | `npx e2e explore` walks the app from a goal and reports what it found; `npx e2e mcp` lets a coding agent drive the app while it writes the test, see [Coding agents](/coding-agents) | renamed |
319
+ | `npx playwright codegen` | `npx e2e explore` walks the app from a goal and reports what it found; `npx e2e mcp` lets a coding agent drive the app while it writes the test, see [MCP server](/mcp) | renamed |
328
320
  | `npx playwright show-trace` | no viewer; open the `trace.md` under `.e2e/results/` that the terminal names | by design |
329
- | `npx playwright show-report` | not yet; `.e2e/summary.md` from the `markdown` reporter | missing |
321
+ | `npx playwright show-report` | `.e2e/summary.md` from the `markdown` reporter | missing |
330
322
  | `npx playwright install` | `npx @e2e-dev/web install`, for the Playwright version the engine pins; a run also installs a missing browser on its own | renamed |
331
323
 
332
324
  </Accordion>
@@ -397,9 +389,10 @@ context.
397
389
  screen.getByLabel('Email'); // no match
398
390
  ```
399
391
  - Text reads are rendered text. On the web, `textContent()` and `toHaveText`
400
- read what `innerText` returns, where Playwright reads the DOM's
401
- `textContent`. Queries still match the source text: `getByText('Draft')`
402
- finds the tag below, `getByText('DRAFT')` does not. `browser.evaluate`
392
+ read what `innerText` returns, like Playwright's `{ useInnerText: true }`.
393
+ Playwright reads the DOM's `textContent` by default. Queries still match
394
+ the source text: `getByText('Draft')` finds the tag below,
395
+ `getByText('DRAFT')` does not. `browser.evaluate`
403
396
  reads the raw DOM text. See [locator reads](/reference/screen#locator-reads).
404
397
 
405
398
  ```ts
@@ -422,7 +415,9 @@ context.
422
415
  so a controlled checkbox that updates late passes. A click that removes the
423
416
  control passes too, where Playwright fails. Assert on what replaced it.
424
417
  - One match or `LOCATOR_AMBIGUOUS`, `isVisible()` included, where Playwright
425
- reads the first match silently. See [screen](/reference/screen).
418
+ reads the first match silently. Narrow with `filter`, `first()`, or
419
+ `{ visible: true }`, or add a `>> visible=true` step to a
420
+ `browser.locator` selector. See [screen](/reference/screen).
426
421
  - `browser.evaluate` takes and returns JSON only, so the report can record
427
422
  and redact the result. Pass values in through `arg`.
428
423
  - A password is a `Secret` with no string value, and the model never sees
package/docs/mobile.mdx CHANGED
@@ -169,6 +169,10 @@ on Android. Each UI framework sets them its own way:
169
169
  | Compose Multiplatform | `Modifier.testTag()` | [with-kotlin-multiplatform](https://github.com/tester-army/e2e/tree/main/examples/with-kotlin-multiplatform) |
170
170
  | Flutter | `Semantics(identifier:)` | [with-flutter](https://github.com/tester-army/e2e/tree/main/examples/with-flutter) |
171
171
 
172
+ - On Android, a native `android:id` and a .NET MAUI `AutomationId` read as
173
+ `<package>:id/<id>`. `getByTestId('greet')` finds them without the app's
174
+ package, as on iOS. Framework ids such as `android:id/content` match only
175
+ in full.
172
176
  - On Android, Compose hides test tags until a root composable sets
173
177
  `Modifier.semantics { testTagsAsResourceId = true }`. Without it,
174
178
  `getByTestId` finds nothing. iOS reads a Compose Multiplatform tag as is,
@@ -421,7 +425,8 @@ or a phone works. The provider leases one device per worker slot when the
421
425
  run starts, and releases it when the run ends.
422
426
 
423
427
  For [EAS Simulators](/integrations/eas), use `easSimulators()` from
424
- `@e2e-dev/eas`. For another service, write a provider of a few dozen lines,
428
+ `@e2e-dev/eas`. For [TestMu AI](/integrations/testmu), use `testmu()` from
429
+ `@e2e-dev/testmu`. For another service, write a provider of a few dozen lines,
425
430
  such as `hostedDevices` in the example below:
426
431
 
427
432
  ```ts
@@ -464,7 +469,7 @@ export interface DeviceLease {
464
469
  ```
465
470
 
466
471
  **`acquire`** runs for every worker slot at once, in the runner process.
467
- The request carries `platform`, `runId`, `targetName`, `slot`, `app` (the
472
+ The request carries `platform`, `runId`, `targetName`, `slot`, `slots`, `app` (the
468
473
  target's `app.bundleId`), the resolved `appPath`, `agentDeviceVersion` (for
469
474
  a service that starts a daemon of a chosen version), `projectRoot`, `env`,
470
475
  an interrupt `signal`, and a `log` that reaches the reporter.
@@ -562,7 +567,8 @@ export function hostedDevices(options: { serviceUrl: string; image: string }): D
562
567
  <Accordion title="Example: a device cloud that agent-device supports">
563
568
  A device cloud that agent-device itself speaks to needs no session API of
564
569
  your own. The provider allocates a lease through a daemon it starts for the
565
- run, and gives the worker the lease scope as `client`.
570
+ run, and gives the worker the lease scope as `client`. `testmu()` from
571
+ `@e2e-dev/testmu` works this way for [TestMu AI](/integrations/testmu).
566
572
 
567
573
  ```ts title="device-cloud-provider.ts" expandable
568
574
  import { createAgentDeviceClient } from 'agent-device';
@@ -663,6 +669,15 @@ the Android SDK's `aapt`, found under `ANDROID_SDK_ROOT`, `ANDROID_HOME`,
663
669
  `app.bundleId`. Or export `ANDROID_HOME`, then stop the agent-device daemon
664
670
  so the next run starts it with the variable.
665
671
 
672
+ **Android reports insufficient foreground app content.** agent-device
673
+ couldn't read the screen yet, often an app still loading after a cold
674
+ start. A waiting locator action or `expect` waits through it, as for an
675
+ element not there yet. If the screen is still unreadable when the wait runs
676
+ out, the step fails with this message. Immediate reads like `count()` and
677
+ `isVisible()` see no matches instead, so `not.toBeVisible()` can pass while
678
+ the screen is unreadable. An observation, such as the agent's next turn,
679
+ fails on it at once.
680
+
666
681
  **A timeout on the device.** A simulator boot, runner connection, or app
667
682
  launch that times out on the device is `ENGINE_FAILURE` with agent-device's
668
683
  message and hint, never `LAUNCH_TIMEOUT`, `ACTION_FAILED`, or
package/docs/models.mdx CHANGED
@@ -9,7 +9,7 @@ icon: "brain"
9
9
  import { ModelProvider } from './snippets/model-provider.jsx';
10
10
 
11
11
  Agent steps use the model you set in `e2e.config.ts`. There is no default.
12
- The model comes from an [AI SDK](https://ai-sdk.dev/providers) provider: a
12
+ The model comes from an [AI SDK](https://ai-sdk.dev/providers/ai-sdk-providers) provider: a
13
13
  gateway, a direct API key, a local server, or your
14
14
  [subscription](/subscriptions). For a first setup, `e2e init` writes the
15
15
  config. See the [quickstart](/quickstart#use-a-subscription-or-api-key).
@@ -140,7 +140,8 @@ when your shell exports it, or when the config loads the file with
140
140
 
141
141
  ## AI SDK providers
142
142
 
143
- Every AI SDK provider that supports tool calls works. Select yours:
143
+ Every AI SDK provider that supports tool calls works. Each provider's
144
+ package and variables are on its [AI SDK provider page](https://ai-sdk.dev/providers/ai-sdk-providers). Select yours:
144
145
 
145
146
  <ModelProvider show="select" />
146
147
 
@@ -250,6 +251,7 @@ them.
250
251
  | --- | --- | --- |
251
252
  | A model is a string instead of an AI SDK instance | `INVALID_CONFIG` | Construct it, for example `gateway('openai/gpt-6-luna-fast')`. |
252
253
  | The built-in agent has no model | `MODEL_UNAVAILABLE` | Set `agents.<name>.model`. |
254
+ | GitHub Copilot doesn't serve the model to your account | `MODEL_UNAVAILABLE` | Pick an id your plan serves. See [Pick a model](/subscriptions#pick-a-model). |
253
255
  | A provider that reads its key at config load has none | `CONFIG_LOAD_FAILED` | Set the key. |
254
256
  | Authentication fails on the first model call | `MODEL_PROVIDER_FAILED` | Check the provider's API key, or run `npx e2e login` for a subscription. For Vercel OIDC the message names only `AI_GATEWAY_API_KEY`. Check `vercel login` and `.vercel/project.json`. |
255
257
  | The `ai` package is missing | `MODEL_UNAVAILABLE` | Install `ai`. See [Pick a gateway or a provider](#pick-a-gateway-or-a-provider). |
@@ -9,6 +9,7 @@ icon: "rocket"
9
9
  import { ModelProvider } from './snippets/model-provider.jsx';
10
10
  import { SetupPrompt } from './snippets/setup-prompt-card.jsx';
11
11
  import { Video } from './snippets/video.jsx';
12
+ import { VideoCard } from './snippets/video-card.jsx';
12
13
 
13
14
  export const setupPrompt = `Set up e2e end-to-end tests in this project. Docs: \`https://e2e.tester.army/docs/quickstart.md\`
14
15
 
@@ -19,6 +20,12 @@ export const setupPrompt = `Set up e2e end-to-end tests in this project. Docs: \
19
20
  5. Install dependencies, run the example test, and fix setup errors until it passes.
20
21
  6. Write one test for the most important user flow in this app, run it, and iterate until it passes.`;
21
22
 
23
+ <VideoCard
24
+ id="4_9z6QWShLc"
25
+ title="Getting Started with Agentic e2e in 4 Minutes"
26
+ subtitle="In this video, Kacper takes a simple Next.js app from zero to a working agentic test suite."
27
+ />
28
+
22
29
  ## Set up with your coding agent
23
30
 
24
31
  Paste this prompt into Claude Code, Codex, Cursor, or another coding agent