@e2edev/e2e 0.5.1 → 0.6.1

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 (337) hide show
  1. package/README.md +24 -11
  2. package/dist/agent/act.d.ts +1 -1
  3. package/dist/agent/act.js +4 -2
  4. package/dist/agent/act.js.map +1 -1
  5. package/dist/agent/ai-sdk.d.ts +1 -2
  6. package/dist/agent/ai-sdk.d.ts.map +1 -1
  7. package/dist/agent/ai-sdk.js +1 -2
  8. package/dist/agent/ai-sdk.js.map +1 -1
  9. package/dist/agent/default-agent.d.ts +7 -3
  10. package/dist/agent/default-agent.d.ts.map +1 -1
  11. package/dist/agent/default-agent.js +2 -2
  12. package/dist/agent/default-agent.js.map +1 -1
  13. package/dist/agent/derived.d.ts +1 -1
  14. package/dist/agent/derived.js +1 -1
  15. package/dist/agent/error.d.ts +2 -2
  16. package/dist/agent/error.d.ts.map +1 -1
  17. package/dist/agent/error.js +2 -2
  18. package/dist/agent/error.js.map +1 -1
  19. package/dist/agent/executor.d.ts +8 -3
  20. package/dist/agent/executor.d.ts.map +1 -1
  21. package/dist/agent/executor.js +1 -1
  22. package/dist/agent/executor.js.map +1 -1
  23. package/dist/agent/index.d.ts +1 -1
  24. package/dist/agent/index.d.ts.map +1 -1
  25. package/dist/agent/index.js +8 -5
  26. package/dist/agent/index.js.map +1 -1
  27. package/dist/agent/invocation.d.ts +2 -2
  28. package/dist/agent/invocation.d.ts.map +1 -1
  29. package/dist/agent/invocation.js +9 -3
  30. package/dist/agent/invocation.js.map +1 -1
  31. package/dist/agent/ledger.d.ts +1 -1
  32. package/dist/agent/ledger.js +1 -1
  33. package/dist/agent/model/adapter.d.ts +7 -4
  34. package/dist/agent/model/adapter.d.ts.map +1 -1
  35. package/dist/agent/model/adapter.js +2 -2
  36. package/dist/agent/model/adapter.js.map +1 -1
  37. package/dist/agent/model/router.d.ts +1 -1
  38. package/dist/agent/model/router.d.ts.map +1 -1
  39. package/dist/agent/model/router.js +1 -1
  40. package/dist/agent/model/router.js.map +1 -1
  41. package/dist/agent/model/sdk.d.ts +11 -1
  42. package/dist/agent/model/sdk.d.ts.map +1 -1
  43. package/dist/agent/model/sdk.js +54 -10
  44. package/dist/agent/model/sdk.js.map +1 -1
  45. package/dist/agent/observation.d.ts +1 -1
  46. package/dist/agent/observation.d.ts.map +1 -1
  47. package/dist/agent/observation.js +2 -2
  48. package/dist/agent/observation.js.map +1 -1
  49. package/dist/agent/prompts.d.ts +1 -1
  50. package/dist/agent/prompts.js +1 -1
  51. package/dist/agent/protocol.d.ts +2 -3
  52. package/dist/agent/protocol.d.ts.map +1 -1
  53. package/dist/agent/protocol.js +2 -3
  54. package/dist/agent/protocol.js.map +1 -1
  55. package/dist/agent/public.d.ts +1 -1
  56. package/dist/agent/public.js +1 -1
  57. package/dist/agent/replay.d.ts +1 -1
  58. package/dist/agent/replay.js +1 -1
  59. package/dist/agent/secrets.d.ts +1 -1
  60. package/dist/agent/secrets.d.ts.map +1 -1
  61. package/dist/agent/secrets.js +1 -1
  62. package/dist/agent/secrets.js.map +1 -1
  63. package/dist/agent/step-cache.d.ts +1 -1
  64. package/dist/agent/step-cache.d.ts.map +1 -1
  65. package/dist/agent/step-cache.js +7 -1
  66. package/dist/agent/step-cache.js.map +1 -1
  67. package/dist/agent/tool-loop.d.ts +7 -3
  68. package/dist/agent/tool-loop.d.ts.map +1 -1
  69. package/dist/agent/tool-loop.js +6 -5
  70. package/dist/agent/tool-loop.js.map +1 -1
  71. package/dist/agent/usage.d.ts +6 -1
  72. package/dist/agent/usage.d.ts.map +1 -1
  73. package/dist/agent/usage.js +12 -0
  74. package/dist/agent/usage.js.map +1 -1
  75. package/dist/cache/anchors.d.ts +1 -1
  76. package/dist/cache/anchors.js +1 -1
  77. package/dist/cache/context.d.ts +6 -1
  78. package/dist/cache/context.d.ts.map +1 -1
  79. package/dist/cache/context.js +2 -1
  80. package/dist/cache/context.js.map +1 -1
  81. package/dist/cache/decide.d.ts +1 -1
  82. package/dist/cache/decide.js +1 -1
  83. package/dist/cache/identity.d.ts +1 -1
  84. package/dist/cache/identity.js +1 -1
  85. package/dist/cache/recorder.d.ts +4 -2
  86. package/dist/cache/recorder.d.ts.map +1 -1
  87. package/dist/cache/recorder.js +10 -1
  88. package/dist/cache/recorder.js.map +1 -1
  89. package/dist/cache/relocate.d.ts +1 -1
  90. package/dist/cache/relocate.js +1 -1
  91. package/dist/cache/store.d.ts +1 -1
  92. package/dist/cache/store.js +1 -1
  93. package/dist/cache/trace.d.ts +16 -1
  94. package/dist/cache/trace.d.ts.map +1 -1
  95. package/dist/cache/trace.js +22 -1
  96. package/dist/cache/trace.js.map +1 -1
  97. package/dist/cli/bin.js +9 -1
  98. package/dist/cli/bin.js.map +1 -1
  99. package/dist/cli/cache.d.ts +25 -0
  100. package/dist/cli/cache.d.ts.map +1 -0
  101. package/dist/cli/cache.js +249 -0
  102. package/dist/cli/cache.js.map +1 -0
  103. package/dist/cli/docs-url.d.ts +3 -0
  104. package/dist/cli/docs-url.d.ts.map +1 -0
  105. package/dist/cli/docs-url.js +3 -0
  106. package/dist/cli/docs-url.js.map +1 -0
  107. package/dist/cli/guide.d.ts +4 -0
  108. package/dist/cli/guide.d.ts.map +1 -0
  109. package/dist/cli/guide.js +16 -0
  110. package/dist/cli/guide.js.map +1 -0
  111. package/dist/cli/index.d.ts +1 -1
  112. package/dist/cli/index.d.ts.map +1 -1
  113. package/dist/cli/index.js +210 -41
  114. package/dist/cli/index.js.map +1 -1
  115. package/dist/cli/init/agent-skill.d.ts +34 -0
  116. package/dist/cli/init/agent-skill.d.ts.map +1 -0
  117. package/dist/cli/init/agent-skill.js +44 -0
  118. package/dist/cli/init/agent-skill.js.map +1 -0
  119. package/dist/cli/init/engines.d.ts +31 -23
  120. package/dist/cli/init/engines.d.ts.map +1 -1
  121. package/dist/cli/init/engines.js +34 -24
  122. package/dist/cli/init/engines.js.map +1 -1
  123. package/dist/cli/init/package.d.ts +19 -3
  124. package/dist/cli/init/package.d.ts.map +1 -1
  125. package/dist/cli/init/package.js +20 -20
  126. package/dist/cli/init/package.js.map +1 -1
  127. package/dist/cli/init/scaffold.d.ts.map +1 -1
  128. package/dist/cli/init/scaffold.js +5 -3
  129. package/dist/cli/init/scaffold.js.map +1 -1
  130. package/dist/cli/init.d.ts +14 -2
  131. package/dist/cli/init.d.ts.map +1 -1
  132. package/dist/cli/init.js +94 -16
  133. package/dist/cli/init.js.map +1 -1
  134. package/dist/cli/signals.d.ts +4 -1
  135. package/dist/cli/signals.d.ts.map +1 -1
  136. package/dist/cli/signals.js +8 -2
  137. package/dist/cli/signals.js.map +1 -1
  138. package/dist/cli/skill.d.ts +16 -0
  139. package/dist/cli/skill.d.ts.map +1 -0
  140. package/dist/cli/skill.js +57 -0
  141. package/dist/cli/skill.js.map +1 -0
  142. package/dist/collect/collect.d.ts +9 -1
  143. package/dist/collect/collect.d.ts.map +1 -1
  144. package/dist/collect/collect.js +50 -5
  145. package/dist/collect/collect.js.map +1 -1
  146. package/dist/collect/registry.d.ts +1 -1
  147. package/dist/collect/registry.d.ts.map +1 -1
  148. package/dist/collect/registry.js +1 -1
  149. package/dist/collect/registry.js.map +1 -1
  150. package/dist/collect/select.d.ts +1 -1
  151. package/dist/collect/select.d.ts.map +1 -1
  152. package/dist/collect/select.js +77 -8
  153. package/dist/collect/select.js.map +1 -1
  154. package/dist/config/agent.d.ts +14 -2
  155. package/dist/config/agent.d.ts.map +1 -1
  156. package/dist/config/agent.js +53 -16
  157. package/dist/config/agent.js.map +1 -1
  158. package/dist/config/app.d.ts +2 -2
  159. package/dist/config/app.d.ts.map +1 -1
  160. package/dist/config/app.js +3 -3
  161. package/dist/config/app.js.map +1 -1
  162. package/dist/config/diagnose.d.ts +15 -0
  163. package/dist/config/diagnose.d.ts.map +1 -0
  164. package/dist/config/diagnose.js +122 -0
  165. package/dist/config/diagnose.js.map +1 -0
  166. package/dist/config/load.d.ts +9 -1
  167. package/dist/config/load.d.ts.map +1 -1
  168. package/dist/config/load.js +35 -3
  169. package/dist/config/load.js.map +1 -1
  170. package/dist/config/resolve.d.ts +2 -2
  171. package/dist/config/resolve.d.ts.map +1 -1
  172. package/dist/config/resolve.js +51 -16
  173. package/dist/config/resolve.js.map +1 -1
  174. package/dist/config/validate.d.ts +6 -2
  175. package/dist/config/validate.d.ts.map +1 -1
  176. package/dist/config/validate.js +20 -3
  177. package/dist/config/validate.js.map +1 -1
  178. package/dist/credentials.d.ts +1 -1
  179. package/dist/credentials.d.ts.map +1 -1
  180. package/dist/credentials.js +1 -1
  181. package/dist/credentials.js.map +1 -1
  182. package/dist/engine/contract.d.ts +2 -2
  183. package/dist/engine/contract.js +1 -1
  184. package/dist/engine/index.d.ts +2 -2
  185. package/dist/engine/index.js +3 -3
  186. package/dist/engine/index.js.map +1 -1
  187. package/dist/engine/session.d.ts +1 -1
  188. package/dist/engine/session.d.ts.map +1 -1
  189. package/dist/engine/session.js +5 -2
  190. package/dist/engine/session.js.map +1 -1
  191. package/dist/expect/async.d.ts +1 -1
  192. package/dist/expect/async.d.ts.map +1 -1
  193. package/dist/expect/async.js +37 -1
  194. package/dist/expect/async.js.map +1 -1
  195. package/dist/expect/values.d.ts +1 -1
  196. package/dist/expect/values.d.ts.map +1 -1
  197. package/dist/expect/values.js +1 -1
  198. package/dist/expect/values.js.map +1 -1
  199. package/dist/index.d.ts +0 -3
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +0 -4
  202. package/dist/index.js.map +1 -1
  203. package/dist/internal/errors.d.ts +21 -3
  204. package/dist/internal/errors.d.ts.map +1 -1
  205. package/dist/internal/errors.js +55 -5
  206. package/dist/internal/errors.js.map +1 -1
  207. package/dist/internal/globs.d.ts +2 -2
  208. package/dist/internal/globs.d.ts.map +1 -1
  209. package/dist/internal/globs.js +2 -2
  210. package/dist/internal/globs.js.map +1 -1
  211. package/dist/internal/ids.d.ts +3 -3
  212. package/dist/internal/ids.d.ts.map +1 -1
  213. package/dist/internal/ids.js +3 -3
  214. package/dist/internal/ids.js.map +1 -1
  215. package/dist/internal/node-version.d.ts +10 -0
  216. package/dist/internal/node-version.d.ts.map +1 -0
  217. package/dist/internal/node-version.js +28 -0
  218. package/dist/internal/node-version.js.map +1 -0
  219. package/dist/internal/package-manager.d.ts +10 -0
  220. package/dist/internal/package-manager.d.ts.map +1 -0
  221. package/dist/internal/package-manager.js +43 -0
  222. package/dist/internal/package-manager.js.map +1 -0
  223. package/dist/internal/redact.d.ts +1 -1
  224. package/dist/internal/redact.d.ts.map +1 -1
  225. package/dist/internal/redact.js +1 -1
  226. package/dist/internal/redact.js.map +1 -1
  227. package/dist/internal/suggest.d.ts +16 -0
  228. package/dist/internal/suggest.d.ts.map +1 -0
  229. package/dist/internal/suggest.js +61 -0
  230. package/dist/internal/suggest.js.map +1 -0
  231. package/dist/internal/text.d.ts +1 -1
  232. package/dist/internal/text.d.ts.map +1 -1
  233. package/dist/internal/text.js +1 -1
  234. package/dist/internal/text.js.map +1 -1
  235. package/dist/internal/urls.d.ts +2 -2
  236. package/dist/internal/urls.d.ts.map +1 -1
  237. package/dist/internal/urls.js +2 -2
  238. package/dist/internal/urls.js.map +1 -1
  239. package/dist/locator/engine.d.ts +1 -1
  240. package/dist/locator/engine.d.ts.map +1 -1
  241. package/dist/locator/engine.js +1 -1
  242. package/dist/locator/engine.js.map +1 -1
  243. package/dist/locator/expression.d.ts +1 -1
  244. package/dist/locator/expression.d.ts.map +1 -1
  245. package/dist/locator/expression.js +1 -1
  246. package/dist/locator/expression.js.map +1 -1
  247. package/dist/locator/screen.js +1 -1
  248. package/dist/locator/screen.js.map +1 -1
  249. package/dist/report/build.d.ts +1 -1
  250. package/dist/report/build.d.ts.map +1 -1
  251. package/dist/report/build.js +4 -4
  252. package/dist/report/build.js.map +1 -1
  253. package/dist/report/format.d.ts +9 -1
  254. package/dist/report/format.d.ts.map +1 -1
  255. package/dist/report/format.js +17 -4
  256. package/dist/report/format.js.map +1 -1
  257. package/dist/report/junit.d.ts +1 -1
  258. package/dist/report/junit.d.ts.map +1 -1
  259. package/dist/report/junit.js +1 -1
  260. package/dist/report/junit.js.map +1 -1
  261. package/dist/report/list-model.d.ts +79 -0
  262. package/dist/report/list-model.d.ts.map +1 -0
  263. package/dist/report/list-model.js +10 -0
  264. package/dist/report/list-model.js.map +1 -0
  265. package/dist/report/list-steps.d.ts +27 -0
  266. package/dist/report/list-steps.d.ts.map +1 -0
  267. package/dist/report/list-steps.js +65 -0
  268. package/dist/report/list-steps.js.map +1 -0
  269. package/dist/report/list.d.ts +26 -24
  270. package/dist/report/list.d.ts.map +1 -1
  271. package/dist/report/list.js +89 -157
  272. package/dist/report/list.js.map +1 -1
  273. package/dist/report/live-window.d.ts +11 -0
  274. package/dist/report/live-window.d.ts.map +1 -1
  275. package/dist/report/live-window.js +12 -5
  276. package/dist/report/live-window.js.map +1 -1
  277. package/dist/report/running-tree.d.ts +46 -0
  278. package/dist/report/running-tree.d.ts.map +1 -0
  279. package/dist/report/running-tree.js +182 -0
  280. package/dist/report/running-tree.js.map +1 -0
  281. package/dist/report/write.d.ts +1 -1
  282. package/dist/report/write.d.ts.map +1 -1
  283. package/dist/report/write.js +1 -1
  284. package/dist/report/write.js.map +1 -1
  285. package/dist/run/budget.d.ts +2 -2
  286. package/dist/run/budget.js +2 -2
  287. package/dist/run/declared-processes.d.ts +1 -1
  288. package/dist/run/declared-processes.js +1 -1
  289. package/dist/run/events.d.ts +2 -0
  290. package/dist/run/events.d.ts.map +1 -1
  291. package/dist/run/events.js.map +1 -1
  292. package/dist/run/execute.d.ts +2 -3
  293. package/dist/run/execute.d.ts.map +1 -1
  294. package/dist/run/execute.js +4 -5
  295. package/dist/run/execute.js.map +1 -1
  296. package/dist/run/fixtures.d.ts +1 -1
  297. package/dist/run/fixtures.d.ts.map +1 -1
  298. package/dist/run/fixtures.js +41 -5
  299. package/dist/run/fixtures.js.map +1 -1
  300. package/dist/run/managed-process.d.ts +8 -1
  301. package/dist/run/managed-process.d.ts.map +1 -1
  302. package/dist/run/managed-process.js +35 -17
  303. package/dist/run/managed-process.js.map +1 -1
  304. package/dist/run/realm.d.ts +2 -2
  305. package/dist/run/realm.js +2 -2
  306. package/dist/run/runner.d.ts +22 -0
  307. package/dist/run/runner.d.ts.map +1 -1
  308. package/dist/run/runner.js +47 -9
  309. package/dist/run/runner.js.map +1 -1
  310. package/dist/run/scheduler.d.ts +1 -2
  311. package/dist/run/scheduler.d.ts.map +1 -1
  312. package/dist/run/scheduler.js +2 -3
  313. package/dist/run/scheduler.js.map +1 -1
  314. package/dist/run/serial.d.ts +1 -1
  315. package/dist/run/serial.d.ts.map +1 -1
  316. package/dist/run/serial.js +3 -3
  317. package/dist/run/serial.js.map +1 -1
  318. package/dist/run/sessions.d.ts +1 -1
  319. package/dist/run/sessions.d.ts.map +1 -1
  320. package/dist/run/sessions.js +1 -1
  321. package/dist/run/sessions.js.map +1 -1
  322. package/dist/run/steps.d.ts +8 -2
  323. package/dist/run/steps.d.ts.map +1 -1
  324. package/dist/run/steps.js +1 -1
  325. package/dist/run/steps.js.map +1 -1
  326. package/dist/run/units.d.ts +1 -1
  327. package/dist/run/units.js +1 -1
  328. package/dist/types.d.ts +27 -8
  329. package/dist/types.d.ts.map +1 -1
  330. package/dist/types.js +2 -2
  331. package/package.json +13 -12
  332. package/skills/e2e/SKILL.md +110 -0
  333. package/skills/e2e/references/agent.md +210 -0
  334. package/skills/e2e/references/debugging.md +81 -0
  335. package/skills/e2e/references/running.md +153 -0
  336. package/skills/e2e/references/setup.md +239 -0
  337. package/skills/e2e/references/writing-tests.md +289 -0
@@ -0,0 +1,153 @@
1
+ # Running tests
2
+
3
+ ## Commands
4
+
5
+ ```bash
6
+ npx --no-install e2e run [files...] [options] # run tests
7
+ npx --no-install e2e list [files...] [options] # print what run would select, without running
8
+ npx --no-install e2e init [--yes] # scaffold a project, refresh the agent skill
9
+ npx --no-install e2e guide [topic] # print this skill: setup, writing-tests, agent, running, debugging
10
+ npx --no-install e2e cache ls|clear|stats # read or empty the trace cache
11
+ ```
12
+
13
+ `run` flags:
14
+
15
+ | Flag | Effect |
16
+ | --- | --- |
17
+ | `[files...]` | Files, directories, or quoted globs relative to the project root. They narrow the config `tests` glob, never bypass it. |
18
+ | `--config <path>` | Explicit config file. Default: `e2e.config.ts` or `.mts` found upward from the working directory. |
19
+ | `--target <ids>` | Comma-separated target names. |
20
+ | `--tag <tag>` | Repeatable tag filter; `--tag-mode all` requires every tag. |
21
+ | `--headed` | Visible browser or simulator when the engine supports it. |
22
+ | `--workers <n>`, `--retries <n>` | Override the resolved values. |
23
+ | `--reporter <ids>` | `list`, `json`, `junit`, comma-separated. `json` cannot combine with `list`. |
24
+ | `--artifacts <dir>` | Artifact root, default `.e2e/artifacts`. |
25
+ | `--no-cache` | Run with the trace cache off. |
26
+ | `--pass-with-no-tests` | Exit 0 when nothing matches instead of `NO_TESTS`. |
27
+ | `--debug` | Phase timings and an agent step table on stderr; step transcripts saved as artifacts. |
28
+ | `--ai-trace` | Record every model call to `.e2e/ai-trace.json`. |
29
+
30
+ ```bash
31
+ npx --no-install e2e run tests/signup.e2e.ts
32
+ npx --no-install e2e run tests/agent --tag smoke
33
+ npx --no-install e2e run 'tests/**/*.smoke.e2e.ts' --target chromium --workers 1 --retries 0
34
+ CI=1 npx --no-install e2e run # reproduce the CI defaults locally
35
+ ```
36
+
37
+ `list` takes the same files and the selection flags (`--config`, `--target`,
38
+ `--tag`, `--tag-mode`, `--pass-with-no-tests`) and prints one line per
39
+ test-target pair, `file › title [target]`, then exits without starting the
40
+ app, an engine, or a worker. `--reporter json` prints `{ "pairs": [...] }`.
41
+ Use it to check a filter before a run.
42
+
43
+ ```bash
44
+ npx --no-install e2e list --tag smoke
45
+ npx --no-install e2e list tests/signup.e2e.ts --reporter json
46
+ ```
47
+
48
+ A `package.json` script keeps it short: `"test:e2e": "e2e run"`, then
49
+ `pnpm test:e2e tests/signup.e2e.ts`.
50
+
51
+ ## The trace cache
52
+
53
+ Entries live under `.e2e/cache/`, one file per key, named after the key
54
+ digest. `cache` commands read the same config as `run`, so `--config` and
55
+ `cache.dir` point them at the right store.
56
+
57
+ | Command | Prints |
58
+ | --- | --- |
59
+ | `e2e cache ls` | One row per entry: test, target, instruction digest, age, action count. |
60
+ | `e2e cache stats` | Directory, entry count, total size. |
61
+ | `e2e cache clear` | Deletes the entries and the directory; files the runner never wrote stay. |
62
+
63
+ Use `ls` to see what a committed cache would replay, and `clear` when a
64
+ recorded flow is stale — `--no-cache` only skips the cache for one run.
65
+
66
+ ## Output
67
+
68
+ - `list` (default): one line per file and target, a `Failed Tests` section
69
+ with each error, its code, the failing line and a code frame, then a
70
+ summary (`Test Files`, `Tests`, `AI`, `Duration`, `Report`).
71
+ - `.e2e/report.json` is written on every run whatever the reporters:
72
+ `run.status`, `run.exitCode`, `run.errors[]` (run-level failures such as
73
+ `APP_UNREACHABLE`), and `run.results[]`, one per test and target, with
74
+ `titlePath`, `file`, `source`, `status`, and `attempts[]` holding `steps[]`,
75
+ `artifacts[]`, and `error`.
76
+ - `junit`: `.e2e/junit.xml` beside the report, for CI test summaries.
77
+ Combine it with the terminal output: `--reporter list,junit`.
78
+ - `json`: the report document on stdout.
79
+ - Artifacts (screenshots, Playwright traces, `--debug` transcripts,
80
+ downloads) live under `.e2e/artifacts/`; every path is recorded in the
81
+ report.
82
+
83
+ ## Exit codes
84
+
85
+ | Code | Meaning |
86
+ | ---: | --- |
87
+ | 0 | Every selected test passed, was flaky, or was skipped |
88
+ | 1 | A test or setup test failed or timed out |
89
+ | 2 | CLI, config, collection, credential, model-config, or policy error |
90
+ | 3 | Engine, app process, model provider, artifact, or cleanup failure |
91
+ | 4 | Internal runner error |
92
+ | 130 | Interrupted |
93
+
94
+ The highest code present wins for a mixed run (`130 > 4 > 3 > 2 > 1 > 0`).
95
+ Do not retry a job on exit 2: it is deterministic. Exit 3 is the only one
96
+ where a job-level retry makes sense.
97
+
98
+ Ctrl-C once interrupts and still writes the report; twice forces teardown;
99
+ three times exits at once.
100
+
101
+ ## Continuous integration
102
+
103
+ CI mode is on when `CI` is set (and not `0` or `false`). It changes
104
+ `retries` to 1, `workers` to 1, rejects `test.only` with `ONLY_IN_CI`, makes
105
+ the trace cache `read-only`, and ignores `reuseExisting`.
106
+
107
+ ```yaml
108
+ # .github/workflows/e2e.yml
109
+ name: e2e
110
+ on:
111
+ pull_request:
112
+ push:
113
+ branches: [main]
114
+ permissions:
115
+ contents: read
116
+ jobs:
117
+ e2e:
118
+ runs-on: ubuntu-latest
119
+ steps:
120
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
121
+ - uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
122
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
123
+ with:
124
+ node-version: 26
125
+ cache: pnpm
126
+ - run: pnpm install --frozen-lockfile
127
+ - run: npx playwright install chromium --with-deps
128
+ - run: npx --no-install e2e run --reporter list,junit
129
+ env:
130
+ E2E_USER_ADMIN_USERNAME: ${{ secrets.E2E_USER_ADMIN_USERNAME }}
131
+ E2E_USER_ADMIN_PASSWORD: ${{ secrets.E2E_USER_ADMIN_PASSWORD }}
132
+ - if: ${{ !cancelled() }}
133
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
134
+ with:
135
+ name: e2e-report
136
+ path: |
137
+ .e2e/report.json
138
+ .e2e/junit.xml
139
+ - if: failure()
140
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
141
+ with:
142
+ name: e2e-artifacts
143
+ path: .e2e/artifacts
144
+ retention-days: 7
145
+ ```
146
+
147
+ - Install browsers as their own step so the download never counts against a
148
+ launch timeout.
149
+ - Start the app through the engine's `command`; the runner tears it down on
150
+ every exit path.
151
+ - Agentic suites: a separate config, run on `schedule` or
152
+ `workflow_dispatch`, `E2E_MODEL` as a CI variable and `E2E_MODEL_API_KEY`
153
+ as a secret, never a required check.
@@ -0,0 +1,239 @@
1
+ # Setting up e2e
2
+
3
+ ## Requirements
4
+
5
+ - Node.js 22.12 or newer.
6
+ - ES modules. The `package.json` nearest to the config and the tests must set
7
+ `"type": "module"`. An `e2e.config.mts` is ESM on its own, but `.ts` tests
8
+ still follow their package. In a CommonJS package either run
9
+ `npm pkg set type=module` (this also changes how its `.js` files run) or put
10
+ the tests in their own ESM package directory.
11
+ - For browser tests, `@e2edev/playwright`. Missing browsers download when the
12
+ engine first boots. In CI install them up front:
13
+ `npx playwright install chromium --with-deps`.
14
+
15
+ ## Scaffold
16
+
17
+ ```bash
18
+ npx @e2edev/e2e@beta init # npm
19
+ pnpm dlx @e2edev/e2e@beta init # pnpm
20
+ ```
21
+
22
+ When `@e2edev/e2e` is already installed, run `npx --no-install e2e init`
23
+ instead, so the installed version scaffolds.
24
+
25
+ The wizard asks for the engine (Playwright by default; None and agent-device
26
+ are the alternatives), whether to enable AI testing (adds `ai@^7.0.0` and
27
+ `createAgent` to the config), which agent directories receive this skill
28
+ (`.agents/skills/` and `.claude/skills/`), a confirmation of the files it
29
+ will write, and whether to install. `--yes` skips every prompt (use it from
30
+ scripts and from a shell without a TTY): Playwright, AI on, no installation,
31
+ skill in both directories. The closing line prints the run command,
32
+ `APP_URL=http://localhost:3000 npx --no-install e2e run` for Playwright, and
33
+ suggests a `tsconfig.json` when the project has none.
34
+
35
+ Init writes `package.json` (a private ESM package when missing; otherwise
36
+ only the missing dev dependencies and the `test:e2e` script are added),
37
+ `e2e.config.ts`, `tests/example.e2e.ts`, `.gitignore` entries for the `.e2e/`
38
+ output, and the skill. Existing config and test files are never touched. Re-run it after an
39
+ upgrade to refresh the skill; it changes nothing else in an initialized
40
+ project.
41
+
42
+ Without the wizard:
43
+
44
+ ```bash
45
+ npm install --save-dev @e2edev/e2e@beta @e2edev/playwright@beta ai@^7
46
+ ```
47
+
48
+ `ai` (the Vercel AI SDK, v7) is only needed for `agent.*` steps.
49
+
50
+ ## The config
51
+
52
+ `e2e.config.ts` sits at the project root and default-exports an object
53
+ literal ending in `satisfies E2EConfig`. The runner validates it at load:
54
+ unknown keys are `INVALID_CONFIG`.
55
+
56
+ ```ts
57
+ import type { E2EConfig } from '@e2edev/e2e';
58
+ import { createAgent } from '@e2edev/e2e/agent';
59
+ import { playwright } from '@e2edev/playwright';
60
+
61
+ export default {
62
+ tests: 'tests/**/*.e2e.ts',
63
+ targets: [
64
+ {
65
+ name: 'web',
66
+ platform: 'web',
67
+ engine: playwright({
68
+ url: 'http://127.0.0.1:3000',
69
+ command: { executable: 'pnpm', args: ['dev'], log: '.e2e/logs/app.log' },
70
+ }),
71
+ },
72
+ ],
73
+ // Only for agent.* steps. The model comes from E2E_MODEL unless set here.
74
+ agent: createAgent({ system: 'You are a thorough QA agent. Verify every outcome on screen.' }),
75
+ credentials: {
76
+ admin: { username: 'admin@example.test', password: process.env.ADMIN_PASSWORD ?? '' },
77
+ },
78
+ } satisfies E2EConfig;
79
+ ```
80
+
81
+ | Key | Default | Notes |
82
+ | --- | --- | --- |
83
+ | `targets` | required | Non-empty. Each target: `name` (used by `--target` and in reports), `platform` (`web`, `ios`, `android`, or any label), `engine`. |
84
+ | `tests` | `'tests/**/*.e2e.ts'` | A glob or an array of globs, `/` separators. |
85
+ | `timeout` | `120000` | Per test attempt, in ms. Also the default `agent.act` deadline. |
86
+ | `actionTimeout` | `30000` | Every locator action and engine operation, including each observation inside an agent step. Raise it for slow model providers. |
87
+ | `assertionTimeout` | `5000` | `expect` polling window. |
88
+ | `retries` | `0`, `1` in CI | 0 to 10. |
89
+ | `workers` | half the cores, `1` in CI | Test files run in parallel across workers. Use `1` for device targets. |
90
+ | `reporters` | `['list']` | `list`, `json`, `junit`. `json` excludes `list`. |
91
+ | `cache` | `'read-write'`, `'read-only'` in CI | The trace cache for `agent.act`; `'off'` disables it. |
92
+ | `agent` | unset | `createAgent(...)`, an options block `{ model, context, visionModel, maxSteps, maxModelCalls, vision, providerOptions }`, or a custom `StepExecutor`. Omitted, the built-in agent runs with `E2E_MODEL`. |
93
+ | `credentials` | `{}` | Named `{ username, password, allowedOrigins? }` entries; `password` may be a function returning the value. |
94
+ | `screen.testIdAttribute` | `'data-testid'` | Attribute read by `getByTestId`. |
95
+ | `artifacts` | `['screenshot', 'trace']` | Kinds to keep, or `{ kinds, store }`. |
96
+ | `projectId` | the package name | Report and cache identity. |
97
+
98
+ ## The app under test
99
+
100
+ The engine declares the app; the runner owns navigation policy, the app
101
+ process, and identity. `playwright()` accepts:
102
+
103
+ | Option | Meaning |
104
+ | --- | --- |
105
+ | `url` | Base URL for `app.open()` and relative navigation. A missing scheme becomes `https://`, or `http://` for a loopback host. Required once a test navigates. |
106
+ | `command` | The process that serves `url`. See below. |
107
+ | `readyUrl` | Readiness probe when it differs from `url`. |
108
+ | `services` | Dependency processes started before `command`, in order. |
109
+ | `allowedOrigins` | Origins tests and the agent may navigate to. Default: the origin of `url`. |
110
+ | `environment` | `'test'`, `'staging'`, `'production'`. Inferred from the host; a label for the report and the cache key. |
111
+ | `identity` | Stable app identity for cache and session keys when the origin changes per deploy (preview URLs). |
112
+ | `browser` | `'chromium'` (default), `'firefox'`, `'webkit'`. |
113
+ | `viewport` | `{ width, height }`, default 1280x720. |
114
+ | `connect` | `{ cdpEndpoint }` to attach to a remote Chromium over CDP instead of launching. |
115
+
116
+ Two browsers are two targets sharing one app declaration:
117
+
118
+ ```ts
119
+ const app = { url: 'http://127.0.0.1:3000' };
120
+ export default {
121
+ targets: [
122
+ { name: 'chromium', platform: 'web', engine: playwright(app) },
123
+ { name: 'mobile-webkit', platform: 'web', engine: playwright({ ...app, browser: 'webkit', viewport: { width: 390, height: 844 } }) },
124
+ ],
125
+ } satisfies E2EConfig;
126
+ ```
127
+
128
+ ### Let the runner start the app
129
+
130
+ Prefer `command` over a hand-started dev server: the run is then
131
+ self-contained locally and in CI.
132
+
133
+ ```ts
134
+ engine: playwright({
135
+ url: 'http://127.0.0.1:3000',
136
+ services: [
137
+ {
138
+ name: 'postgres',
139
+ executable: 'docker',
140
+ args: ['compose', 'up', '--wait', 'postgres'],
141
+ waitForExit: true,
142
+ teardown: { executable: 'docker', args: ['compose', 'down'] },
143
+ },
144
+ { name: 'migrate', executable: 'pnpm', args: ['db:migrate'], waitForExit: true },
145
+ ],
146
+ command: {
147
+ executable: 'pnpm',
148
+ args: ['dev'],
149
+ env: { PORT: '3000', DATABASE_URL: process.env.DATABASE_URL ?? '' },
150
+ startupTimeout: 120_000,
151
+ log: '.e2e/logs/app.log',
152
+ },
153
+ }),
154
+ ```
155
+
156
+ How it behaves:
157
+
158
+ - The runner spawns `command`, polls `readyUrl` (default `url`) until a
159
+ 200 to 499 status arrives within `startupTimeout` (default 60 s), and stops
160
+ the process when the run ends, fails, or is interrupted. Never ready is
161
+ `APP_UNREACHABLE`; `.e2e/report.json` is still written.
162
+ - The child inherits only `PATH`, `HOME`, and the temp-directory variables,
163
+ plus `command.env`. Anything the app needs (database URL, API keys) must be
164
+ passed through `env` explicitly. Model keys and `E2E_USER_*` values are
165
+ never inherited.
166
+ - Output is discarded unless `log` names a file. Set it; a server that dies
167
+ on boot is otherwise invisible. Keep the file under an ignored directory
168
+ such as `.e2e/logs/`.
169
+ - If `url` already answers before the spawn, the run fails with
170
+ `APP_ALREADY_RUNNING`. Set `reuseExisting: true` for local development to
171
+ attach to a dev server that is already up; CI ignores the flag.
172
+ - `services` start one at a time in declaration order. Each declares
173
+ `readyUrl` (polled) or `waitForExit: true` (ready when it exits 0). On
174
+ every exit path the runner stops the app, stops the services in reverse,
175
+ then runs their `teardown` commands in reverse.
176
+ - `executable` is resolved on `PATH` and never shell-interpreted. Point it at
177
+ the server itself rather than at a wrapper script.
178
+
179
+ To test an app started elsewhere, point `url` at it and start it yourself,
180
+ or read the address from the environment:
181
+ `url: process.env.APP_URL ?? 'http://localhost:3000'`. The runner reads no
182
+ `APP_URL` itself; the config does.
183
+
184
+ ## Environment variables the runner reads
185
+
186
+ | Variable | Effect |
187
+ | --- | --- |
188
+ | `E2E_MODEL` | `provider/model-id` for `agent.*` steps when the config sets no model. Routed through the Vercel AI Gateway. |
189
+ | `E2E_MODEL_API_KEY` | Credential for that model. `AI_GATEWAY_API_KEY` is the fallback. |
190
+ | `E2E_MODEL_ENDPOINT` | Any OpenAI-compatible endpoint URL for that model; the gateway is the default. HTTPS unless loopback. `agent.model.endpoint` wins. |
191
+ | `E2E_VISION_MODEL` | Model for calls with `vision`; falls back to the main model. |
192
+ | `E2E_USER_<NAME>_USERNAME`, `E2E_USER_<NAME>_PASSWORD` | Override `credentials.<name>`. `<NAME>` is the credential name uppercased, other characters as `_`. |
193
+ | `CI` | Turns on CI defaults: `retries: 1`, `workers: 1`, `test.only` rejected, cache read-only, `reuseExisting` ignored. |
194
+
195
+ ## Mobile targets
196
+
197
+ `@e2edev/agent-device` drives iOS simulators and Android emulators through
198
+ [agent-device](https://github.com/callstack/agent-device). It needs Xcode
199
+ with a simulator runtime, or the Android SDK with an emulator; run
200
+ `npx agent-device doctor` once.
201
+
202
+ ```ts
203
+ import type { E2EConfig } from '@e2edev/e2e';
204
+ import { createAgent } from '@e2edev/e2e/agent';
205
+ import { agentDevice } from '@e2edev/agent-device';
206
+ import { agentDeviceTools } from '@e2edev/agent-device/tools';
207
+
208
+ const iphone = agentDevice({ platform: 'ios', app: 'com.example.app' });
209
+
210
+ export default {
211
+ targets: [{ name: 'ios', platform: 'ios', engine: iphone }],
212
+ workers: 1,
213
+ agent: createAgent({ tools: agentDeviceTools(iphone) }),
214
+ } satisfies E2EConfig;
215
+ ```
216
+
217
+ - `app` is a bundle id, package name, or display name opened fresh per
218
+ attempt. `appPath` installs a `.app` or `.apk` once per worker; without
219
+ `app`, the installed bundle is the one opened.
220
+ - `workers: 1`: workers share one simulator.
221
+ - `screen`, `expect`, `app`, and `agent` work unchanged. Import `test` from
222
+ `@e2edev/agent-device` to type the `device` fixture (`setAppearance`,
223
+ `setNetwork`, `setPermission`, `installApp`, `locator('role=... id=...')`,
224
+ and more). Portable suites declare `requires: ['device']`.
225
+ - No `state` capability: `test.setup` and `session` are unavailable on a
226
+ device. Sign in with deterministic `screen` actions; `agent.act` cannot
227
+ fill a `Secret` on a device.
228
+ - A deterministic check that names a platform label runs on one platform
229
+ only: `test('...', { platforms: ['ios'] }, ...)`.
230
+ - `selectOption`, `setInputFiles`, and `scrollIntoView` are
231
+ `UNSUPPORTED_CAPABILITY` on a device.
232
+
233
+ ## Done when
234
+
235
+ - `npx --no-install e2e run tests/example.e2e.ts` passes against the app.
236
+ - `package.json` has a script such as `"test:e2e": "e2e run"`.
237
+ - `.gitignore` lists the `.e2e/` outputs (init adds them). Committing
238
+ `.e2e/cache/` is opt-in: remove that line to share `agent.act` replays.
239
+ - CI runs the deterministic suite on pull requests; see `running`.
@@ -0,0 +1,289 @@
1
+ # Writing tests
2
+
3
+ ## A complete file
4
+
5
+ ```ts
6
+ // tests/todos.e2e.ts
7
+ import { test } from '@e2edev/playwright';
8
+ import { expect } from '@e2edev/e2e';
9
+
10
+ test.describe('todos', { tags: ['todos'] }, () => {
11
+ test.beforeEach(async ({ app }) => {
12
+ await app.open('/todos');
13
+ });
14
+
15
+ test('adds and completes a todo', async ({ screen, web }) => {
16
+ await screen.getByLabel('New todo').fill('Write the release notes');
17
+ await screen.getByRole('button', { name: 'Add' }).tap();
18
+
19
+ await expect(screen.getByRole('listitem')).toHaveCount(1);
20
+ await expect(screen.getByRole('status', { name: 'Remaining' })).toHaveText('1 remaining');
21
+
22
+ await screen.getByRole('checkbox', { name: 'Write the release notes' }).check();
23
+ await expect(screen.getByRole('status', { name: 'Remaining' })).toHaveText('0 remaining');
24
+ await expect(web).toHaveURL('/todos');
25
+ });
26
+
27
+ test('ignores an empty submission', async ({ screen }) => {
28
+ await screen.getByRole('button', { name: 'Add' }).tap();
29
+ await expect(screen.getByRole('listitem')).toHaveCount(0);
30
+ });
31
+ });
32
+ ```
33
+
34
+ Files match the config `tests` glob, default `tests/**/*.e2e.ts`. Every test
35
+ starts from clean state: a fresh browser context and no page open, so a test
36
+ calls `app.open()` first (here in `beforeEach`).
37
+
38
+ ## Registration
39
+
40
+ `test` is the only registration surface; everything registers while the file
41
+ is imported, so a `describe` body is synchronous (an `async` body is a
42
+ `COLLECTION_ERROR`).
43
+
44
+ ```ts
45
+ test('title', async ({ app, screen }) => {});
46
+ test('title', { tags: ['smoke'], retries: 2, timeout: 60_000 }, async ({ app }) => {});
47
+ test.describe('group', { tags: ['billing'] }, () => { /* tests and hooks */ });
48
+ test.describe('checkout flow', { serial: true }, () => { /* ordered, shared app state */ });
49
+ test.beforeEach(async ({ app }) => {}); // per attempt, with test fixtures
50
+ test.afterEach(async ({ screen }) => {}); // runs after failures too, with its own cleanup budget
51
+ test.beforeAll(async ({ platform }) => {}); // per suite realm, no app fixtures
52
+ test.afterAll(async () => {});
53
+ test.skip('later', async () => {});
54
+ test.only('focus', async () => {}); // local only: CI fails with ONLY_IN_CI
55
+ test.setup('sign in', { sessions: ['admin'] }, async ({ app, screen, session }) => {}); // see Sign-in sessions
56
+ ```
57
+
58
+ | Option | Default | Notes |
59
+ | --- | --- | --- |
60
+ | `timeout` | `config.timeout`, 120 s | Covers `beforeEach` and the body. |
61
+ | `retries` | `config.retries` | 0 to 10. On a serial group, the group's value applies. |
62
+ | `tags` | `[]` | Union across layers. Select with `--tag smoke`; `--tag-mode all` requires every tag. |
63
+ | `skip` | unset | `true` or a reason string. |
64
+ | `platforms` | unset | Run only on targets with these platforms, e.g. `['ios']`. |
65
+ | `requires` | `[]` | Capabilities the engine must contribute, e.g. `['web']`. Otherwise the test is skipped at selection instead of failing with `UNSUPPORTED_CAPABILITY`. |
66
+ | `session` | unset | Restore state saved by a setup test. |
67
+ | `agentContext` | unset | Extra context for `agent.*` calls in this test or group. |
68
+ | `serial` | `false` | Groups only. Members share one app state, run in order on one worker, and retry as a whole. Inside, per-member `retries`, `session`, `platforms`, `requires`, and `skip` are errors. |
69
+
70
+ Hook order follows nesting, not position: outer `beforeEach` first, inner
71
+ `afterEach` first. `beforeAll` runs again for every retry and every serial
72
+ group, because each is a fresh module realm.
73
+
74
+ ## Fixtures
75
+
76
+ Fixtures are lazy; destructure them in the callback.
77
+
78
+ | Fixture | Type | Available |
79
+ | --- | --- | --- |
80
+ | `app` | `App` | Always. |
81
+ | `screen` | `Screen` | Always. |
82
+ | `agent` | `Agent` | Needs a configured model, else `MODEL_UNAVAILABLE`. See the `agent` topic. |
83
+ | `platform` | `'web' \| 'ios' \| 'android' \| string` | Always; also in `beforeAll` and `afterAll`. |
84
+ | `web` | `Web` | Browser targets. Import `test` from `@e2edev/playwright`. |
85
+ | `device` | `Device` | Device targets. Import `test` from `@e2edev/agent-device`. |
86
+ | `session` | `SetupSession` | Only in `test.setup`. |
87
+
88
+ ### app
89
+
90
+ | Method | Does |
91
+ | --- | --- |
92
+ | `open(path?)` | Opens the engine's `url`, a path relative to it, or an absolute URL inside `allowedOrigins`. |
93
+ | `back()` | One history step back. |
94
+ | `restart()` | Recreates the context and keeps persisted state, including a restored session. |
95
+ | `clearState()` | Clears cookies and storage, then relaunches. Not inside a serial group. |
96
+ | `deepLink(url)` | Opens an allowed deep or universal link. |
97
+ | `screenshot(label?)` | Saves a redacted screenshot as an artifact and returns its path. |
98
+
99
+ ## Locators
100
+
101
+ `screen.getBy*` builds a lazy query; nothing resolves until an action, read,
102
+ or assertion runs. Every query also exists on a locator, scoped to its
103
+ subtree.
104
+
105
+ | Query | Matches |
106
+ | --- | --- |
107
+ | `getByRole(role, { name?, exact?, checked?, disabled?, selected?, expanded?, hidden?, visible? })` | Semantic role, optionally by accessible name and state. First choice. |
108
+ | `getByLabel(text, { exact?, visible? })` | Form controls by label. |
109
+ | `getByPlaceholder(text)` | Inputs by placeholder. |
110
+ | `getByText(text, { exact?, visible? })` | Visible text. |
111
+ | `getByDisplayValue(value)` | Inputs by current value. |
112
+ | `getByTestId(id, { visible? })` | `data-testid` (or `screen.testIdAttribute`). Last resort. |
113
+
114
+ Roles: `button`, `link`, `textbox`, `searchbox`, `combobox`, `listbox`,
115
+ `option`, `checkbox`, `radio`, `switch`, `slider`, `image`, `heading`, `tab`,
116
+ `menuitem`, `list`, `listitem`, `table`, `row`, `cell`, `columnheader`,
117
+ `status`, `alert`, `dialog`, `alertdialog`, `main`, `navigation`, `banner`,
118
+ `contentinfo`, `complementary`, `region`. The union is closed; anything else
119
+ is a type error.
120
+
121
+ Text matching is exact by default after whitespace normalization.
122
+ `exact: false` is a case-insensitive substring match; a `RegExp` matches as
123
+ written.
124
+
125
+ Rules:
126
+
127
+ - An action, read, or assertion needs exactly one match. Two matches fail
128
+ immediately with `LOCATOR_AMBIGUOUS`; zero matches poll until the timeout,
129
+ then `LOCATOR_NOT_FOUND`. `toHaveCount` and `toBeHidden` are the
130
+ exceptions.
131
+ - Narrow with `filter({ hasText })`, `filter({ has: locator })`, `first()`,
132
+ `last()`, `nth(i)`, or by scoping under another locator.
133
+ - `visible: true` drops nodes the page hides (a closed drawer, a prerendered
134
+ duplicate) before the exactly-one rule. Reach for it when a query is
135
+ ambiguous even though one element is on screen.
136
+
137
+ ```ts
138
+ const row = screen.getByRole('listitem').filter({ hasText: 'Invoice 42' });
139
+ await row.getByRole('button', { name: 'Void' }).tap();
140
+ await screen.getByText('Save', { visible: true }).first().tap();
141
+ await screen.scrollUntilVisible(screen.getByRole('button', { name: 'Accept' }));
142
+ ```
143
+
144
+ ### Actions
145
+
146
+ Each action resolves one node, waits for it to be actionable within
147
+ `config.actionTimeout` (30 s, or `{ timeout }`), and performs one operation.
148
+
149
+ `tap()` (alias `click()`), `doubleTap()`, `longPress({ durationMs? })`,
150
+ `fill(value | Secret)`, `clear()`, `press(key)`, `check()`, `uncheck()`,
151
+ `selectOption(label | { label } | { index })`, `focus()`, `hover()`,
152
+ `setInputFiles(paths)` (relative to the project root), `dragTo(locator)`,
153
+ `scrollIntoView()`, `swipe({ direction, momentum? })`.
154
+
155
+ ### Reads
156
+
157
+ Reads resolve once and do not retry: `textContent()`, `inputValue()`,
158
+ `getAttribute(name)`, `isVisible()`, `isEnabled()`, `isChecked()`,
159
+ `boundingBox()`, `count()`. `waitFor({ state?: 'visible' | 'hidden', timeout? })`
160
+ waits for a state. When a value has to settle, use `expect` instead of a
161
+ read. Reading a password field's value is `POLICY_DENIED`.
162
+
163
+ ## expect
164
+
165
+ `expect(locator)` polls for up to `config.assertionTimeout` (5 s) or
166
+ `{ timeout }`; `.not` inverts. `expect(web)` gives the browser matchers.
167
+ `expect(value)` is synchronous.
168
+
169
+ ```ts
170
+ await expect(screen.getByRole('status')).toHaveText('Saved');
171
+ await expect(screen.getByRole('dialog')).not.toBeVisible({ timeout: 10_000 });
172
+ await expect(screen.getByTestId('todo')).toHaveCount(3);
173
+ await expect(web).toHaveURL('/dashboard'); // relative to the base URL, or a RegExp
174
+ await expect(web).toHaveTitle(/Dashboard/);
175
+ expect(await screen.getByTestId('total').textContent()).toContain('$');
176
+ ```
177
+
178
+ | Locator matchers | Web matchers | Value matchers |
179
+ | --- | --- | --- |
180
+ | `toBeVisible`, `toBeHidden`, `toBeEnabled`, `toBeDisabled`, `toBeChecked`, `toBeSelected`, `toBeExpanded`, `toHaveText`, `toContainText`, `toHaveValue`, `toHaveCount`, `toHaveAccessibleName` | `toHaveURL`, `toHaveTitle` | `toBe`, `toEqual`, `toBeTruthy`, `toBeFalsy`, `toBeNull`, `toBeUndefined`, `toBeDefined`, `toContain`, `toMatch`, `toBeGreaterThan`, `toBeLessThan` |
181
+
182
+ `toHaveText` compares the whole normalized text; `toContainText` a
183
+ substring or a RegExp. A failed matcher is `ASSERTION_FAILED`, exit code 1.
184
+
185
+ ## Sign-in sessions
186
+
187
+ Sign in once in a setup test, save the state under a name, and let other
188
+ tests declare it. Selecting a dependent test alone still runs its setup.
189
+
190
+ ```ts
191
+ // tests/auth.setup.e2e.ts
192
+ import { test } from '@e2edev/playwright';
193
+ import { expect, credentials } from '@e2edev/e2e';
194
+
195
+ test.setup('authenticate as admin', { sessions: ['admin'] }, async ({ app, screen, session, web }) => {
196
+ const admin = credentials.user('admin');
197
+ await app.open('/login');
198
+ await screen.getByLabel('Email').fill(admin.username);
199
+ await screen.getByLabel('Password').fill(admin.password);
200
+ await screen.getByRole('button', { name: 'Sign in' }).tap();
201
+ await expect(web).toHaveURL('/dashboard'); // prove the sign-in worked before saving
202
+ await session.save('admin');
203
+ });
204
+ ```
205
+
206
+ ```ts
207
+ // tests/dashboard.e2e.ts
208
+ import { test, expect } from '@e2edev/e2e';
209
+
210
+ test('the dashboard opens directly', { session: 'admin' }, async ({ app, screen }) => {
211
+ await app.open('/dashboard');
212
+ await expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
213
+ });
214
+ ```
215
+
216
+ - Setup tests are top-level (not inside `describe`); exactly one setup saves
217
+ a given name; the body saves every declared name once (`SESSION_CONTRACT`
218
+ otherwise).
219
+ - A session holds cookies, local storage, and IndexedDB, for one run only.
220
+ The files are encrypted and deleted at cleanup. Server state is not part of
221
+ it.
222
+ - Credentials live in the config; values come from the environment:
223
+
224
+ ```ts
225
+ credentials: {
226
+ admin: { username: 'admin@example.test', password: process.env.ADMIN_PASSWORD ?? '' },
227
+ },
228
+ ```
229
+
230
+ `E2E_USER_ADMIN_USERNAME` and `E2E_USER_ADMIN_PASSWORD` override either
231
+ field per run. `credentials.user('admin').password` is a `Secret` with no
232
+ plaintext accessor; only `fill()` and `agent.act` params accept it. Once a
233
+ secret is filled, screenshots stop being attached for the rest of that
234
+ attempt, so sign in inside a setup test and keep the evidence in the tests
235
+ that matter.
236
+
237
+ ## The web fixture (browser only)
238
+
239
+ Import `test` from `@e2edev/playwright`. Prefer `app` and `screen`; use
240
+ `web` for what only a browser has. Portable suites declare
241
+ `requires: ['web']` so device targets skip the test instead of failing.
242
+
243
+ | Method | Does |
244
+ | --- | --- |
245
+ | `goto(url, { waitUntil? })`, `reload()`, `back()`, `forward()` | Navigation. `goto` accepts a path relative to the base URL. |
246
+ | `url()`, `title()`, `waitForURL(url \| RegExp)` | Reads and a URL wait. |
247
+ | `locator(css)` | Raw CSS or XPath. Not portable; a last resort. |
248
+ | `frameLocator(css)` | A `Screen` scoped to one iframe: `web.frameLocator('#payment').getByLabel('Card number')`. |
249
+ | `evaluate(fn, arg?)` | Runs serialized code in the page. JSON in and out only, no closures. |
250
+ | `route(pattern, handler)`, `unroute(pattern)` | Intercept requests: `route.fulfill({ json })`, `route.continue()`, `route.abort()`. |
251
+ | `waitForResponse(pattern)` | Resolves with `{ status, headers, json(), text() }`. |
252
+ | `cookies()`, `setCookies([...])` | Cookies within `allowedOrigins`. |
253
+ | `setViewport({ width, height })` | Resize. |
254
+ | `onDialog('accept' \| 'dismiss' \| handler)` | Returns an unsubscribe function. Register it before the tap that opens the dialog. |
255
+ | `waitForDownload(() => trigger)` | Returns `{ path, suggestedFilename }`. |
256
+ | `keyboard.press(key)`, `keyboard.type(text)`, `mouse.*` | Unfocused input. Prefer `locator.press` and `locator.fill`. |
257
+
258
+ ```ts
259
+ await web.route('**/api/quote', (route) => route.fulfill({ json: { cents: 4200 } }));
260
+ const response = await web.waitForResponse('**/api/orders');
261
+ expect(response.status).toBe(201);
262
+ ```
263
+
264
+ ## Patterns that keep suites honest
265
+
266
+ - Find selectors in the source, not by guessing: read the component or
267
+ template for labels, roles, and text. Add an `aria-label` or a heading
268
+ where the app has no accessible name, rather than falling back to CSS.
269
+ - Create the data a test needs under a name unique to the run
270
+ (`Invoice ${Date.now()}`) and clean up in `afterEach`. Replays and retries
271
+ then never trip over leftovers.
272
+ - One flow across several tests: `test.describe('...', { serial: true })`.
273
+ Otherwise tests are independent and may run on different workers.
274
+ - Tag by area and by cost (`smoke`, `billing`, `agent`) and run subsets with
275
+ `--tag`.
276
+ - Mix agent steps in where the path varies and pin the outcome with
277
+ `expect`; see the `agent` topic.
278
+
279
+ ## Mistakes to avoid
280
+
281
+ - Sleeps or manual polling loops. Use a matcher with a longer `timeout`.
282
+ - `web.locator('.btn-primary')` when `getByRole('button', { name })` exists.
283
+ - `expect(await locator.textContent()).toBe(...)` for a value that is still
284
+ changing; use `toHaveText`.
285
+ - Hardcoded passwords or tokens.
286
+ - Sharing state between tests without `serial`.
287
+ - `test.only` left in a file: CI fails with `ONLY_IN_CI`.
288
+ - Asserting an exact sentence a model produced; assert the fact with
289
+ `toContain`.