@itmetsam/samurai-framework 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/README.md +129 -0
  2. package/bin/samurai.mjs +31 -0
  3. package/dist/api.js +3 -0
  4. package/dist/assert/assertion-error.d.ts +23 -0
  5. package/dist/assert/assertion-error.js +45 -0
  6. package/dist/assert/expect.d.ts +55 -0
  7. package/dist/assert/expect.js +188 -0
  8. package/dist/browser/browser-context.d.ts +91 -0
  9. package/dist/browser/browser-context.js +146 -0
  10. package/dist/browser/browser-finder.d.ts +2 -0
  11. package/dist/browser/browser-finder.js +95 -0
  12. package/dist/browser/browser.d.ts +95 -0
  13. package/dist/browser/browser.js +372 -0
  14. package/dist/browser/context-tree.d.ts +23 -0
  15. package/dist/browser/context-tree.js +60 -0
  16. package/dist/browser/dialog.d.ts +37 -0
  17. package/dist/browser/dialog.js +61 -0
  18. package/dist/browser/download.d.ts +71 -0
  19. package/dist/browser/download.js +137 -0
  20. package/dist/browser/downloads-dir.d.ts +8 -0
  21. package/dist/browser/downloads-dir.js +38 -0
  22. package/dist/browser/emulation.d.ts +61 -0
  23. package/dist/browser/emulation.js +144 -0
  24. package/dist/browser/file-chooser.d.ts +39 -0
  25. package/dist/browser/file-chooser.js +86 -0
  26. package/dist/browser/file-paths.d.ts +4 -0
  27. package/dist/browser/file-paths.js +18 -0
  28. package/dist/browser/index.d.ts +4 -0
  29. package/dist/browser/index.js +4 -0
  30. package/dist/browser/navigation-error.d.ts +13 -0
  31. package/dist/browser/navigation-error.js +19 -0
  32. package/dist/browser/page-logs.d.ts +86 -0
  33. package/dist/browser/page-logs.js +149 -0
  34. package/dist/browser/page-wait.d.ts +9 -0
  35. package/dist/browser/page-wait.js +17 -0
  36. package/dist/browser/page.d.ts +370 -0
  37. package/dist/browser/page.js +1038 -0
  38. package/dist/browser/pdf.d.ts +72 -0
  39. package/dist/browser/pdf.js +37 -0
  40. package/dist/browser/screenshot.d.ts +29 -0
  41. package/dist/browser/screenshot.js +42 -0
  42. package/dist/cli/args.d.ts +35 -0
  43. package/dist/cli/args.js +69 -0
  44. package/dist/cli/cli.cli-test.d.ts +1 -0
  45. package/dist/cli/cli.cli-test.js +170 -0
  46. package/dist/cli/console-reporter.d.ts +15 -0
  47. package/dist/cli/console-reporter.js +52 -0
  48. package/dist/cli/init.d.ts +7 -0
  49. package/dist/cli/init.js +61 -0
  50. package/dist/cli/main.d.ts +12 -0
  51. package/dist/cli/main.js +156 -0
  52. package/dist/config/config.d.ts +24 -0
  53. package/dist/config/config.js +42 -0
  54. package/dist/config/default.d.ts +2 -0
  55. package/dist/config/default.js +3 -0
  56. package/dist/config/mask.d.ts +11 -0
  57. package/dist/config/mask.js +85 -0
  58. package/dist/config/run-settings.d.ts +28 -0
  59. package/dist/config/run-settings.js +61 -0
  60. package/dist/config/secrets.d.ts +10 -0
  61. package/dist/config/secrets.js +67 -0
  62. package/dist/config/variables.d.ts +11 -0
  63. package/dist/config/variables.js +26 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +15 -0
  66. package/dist/locator/action-timeout-error.d.ts +25 -0
  67. package/dist/locator/action-timeout-error.js +35 -0
  68. package/dist/locator/element-state.d.ts +44 -0
  69. package/dist/locator/element-state.js +109 -0
  70. package/dist/locator/label-locator.d.ts +13 -0
  71. package/dist/locator/label-locator.js +49 -0
  72. package/dist/locator/locator.d.ts +168 -0
  73. package/dist/locator/locator.js +591 -0
  74. package/dist/locator/selector-errors.d.ts +9 -0
  75. package/dist/locator/selector-errors.js +15 -0
  76. package/dist/locator/selector.d.ts +60 -0
  77. package/dist/locator/selector.js +113 -0
  78. package/dist/locator/text-locator.d.ts +13 -0
  79. package/dist/logger/index.d.ts +2 -0
  80. package/dist/logger/index.js +30 -0
  81. package/dist/network/data-collector.d.ts +26 -0
  82. package/dist/network/data-collector.js +77 -0
  83. package/dist/network/network-tracker.d.ts +94 -0
  84. package/dist/network/network-tracker.js +198 -0
  85. package/dist/network/network-wait.d.ts +16 -0
  86. package/dist/network/network-wait.js +24 -0
  87. package/dist/network/response.d.ts +22 -0
  88. package/dist/network/response.js +42 -0
  89. package/dist/network/router.d.ts +120 -0
  90. package/dist/network/router.js +343 -0
  91. package/dist/network/url-match.d.ts +8 -0
  92. package/dist/network/url-match.js +38 -0
  93. package/dist/recorder/apply.d.ts +3 -0
  94. package/dist/recorder/apply.js +10 -0
  95. package/dist/recorder/capture.d.ts +18 -0
  96. package/dist/recorder/capture.js +198 -0
  97. package/dist/recorder/index.d.ts +3 -0
  98. package/dist/recorder/index.js +3 -0
  99. package/dist/recorder/normalise.d.ts +68 -0
  100. package/dist/recorder/normalise.js +104 -0
  101. package/dist/recorder/ranking.d.ts +9 -0
  102. package/dist/recorder/ranking.js +21 -0
  103. package/dist/recorder/recorder.d.ts +52 -0
  104. package/dist/recorder/recorder.js +143 -0
  105. package/dist/recorder/session.d.ts +49 -0
  106. package/dist/recorder/session.js +113 -0
  107. package/dist/runner/module-type.d.ts +6 -0
  108. package/dist/runner/module-type.js +29 -0
  109. package/dist/runner/page-logs-report.d.ts +40 -0
  110. package/dist/runner/page-logs-report.js +44 -0
  111. package/dist/runner/prepare-run.d.ts +14 -0
  112. package/dist/runner/prepare-run.js +20 -0
  113. package/dist/runner/project.d.ts +17 -0
  114. package/dist/runner/project.js +33 -0
  115. package/dist/runner/registry.d.ts +7 -0
  116. package/dist/runner/registry.js +40 -0
  117. package/dist/runner/reporter.d.ts +43 -0
  118. package/dist/runner/reporter.js +83 -0
  119. package/dist/runner/run.d.ts +27 -0
  120. package/dist/runner/run.js +34 -0
  121. package/dist/runner/test-error.d.ts +3 -0
  122. package/dist/runner/test-error.js +18 -0
  123. package/dist/runner/test-runner.d.ts +44 -0
  124. package/dist/runner/test-runner.js +205 -0
  125. package/dist/script/call-function.d.ts +34 -0
  126. package/dist/script/call-function.js +71 -0
  127. package/dist/script/element-handle.d.ts +10 -0
  128. package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
  129. package/dist/script/helpers.d.ts +28 -0
  130. package/dist/script/helpers.js +50 -0
  131. package/dist/script/preload.d.ts +31 -0
  132. package/dist/script/preload.js +71 -0
  133. package/dist/script/serialize.d.ts +29 -0
  134. package/dist/script/serialize.js +207 -0
  135. package/dist/steps/build.d.ts +5 -0
  136. package/dist/steps/build.js +38 -0
  137. package/dist/steps/edit.d.ts +30 -0
  138. package/dist/steps/edit.js +80 -0
  139. package/dist/steps/emit.d.ts +6 -0
  140. package/dist/steps/emit.js +73 -0
  141. package/dist/steps/index.d.ts +5 -0
  142. package/dist/steps/index.js +4 -0
  143. package/dist/steps/model.d.ts +103 -0
  144. package/dist/steps/model.js +1 -0
  145. package/dist/steps/parse.d.ts +3 -0
  146. package/dist/steps/parse.js +272 -0
  147. package/dist/transport/bidi-connection.d.ts +46 -0
  148. package/dist/transport/bidi-connection.js +176 -0
  149. package/dist/transport/bidi-error.d.ts +10 -0
  150. package/dist/transport/bidi-error.js +10 -0
  151. package/dist/transport/subscriptions.d.ts +36 -0
  152. package/dist/transport/subscriptions.js +81 -0
  153. package/{src → dist}/types/test.d.ts +10 -4
  154. package/dist/wait/wait-until.d.ts +20 -0
  155. package/dist/wait/wait-until.js +63 -0
  156. package/package.json +44 -7
  157. package/.claude/settings.local.json +0 -7
  158. package/.env.example +0 -3
  159. package/.github/actions/setup/action.yml +0 -16
  160. package/.github/workflows/ci.yml +0 -71
  161. package/.trunk/configs/.markdownlint.yaml +0 -2
  162. package/.trunk/configs/.yamllint.yaml +0 -7
  163. package/.trunk/configs/svgo.config.mjs +0 -14
  164. package/.trunk/trunk.yaml +0 -34
  165. package/docs/superpowers/bidi-roadmap.md +0 -60
  166. package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
  167. package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
  168. package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
  169. package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
  170. package/docs/superpowers/recorder-roadmap.md +0 -52
  171. package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
  172. package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
  173. package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
  174. package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
  175. package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
  176. package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
  177. package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
  178. package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
  179. package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
  180. package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
  181. package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
  182. package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
  183. package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
  184. package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
  185. package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
  186. package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
  187. package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
  188. package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
  189. package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
  190. package/public/logo-dark.svg +0 -236
  191. package/public/logo-light.svg +0 -262
  192. package/samurai.config.ts +0 -7
  193. package/src/assert/assertion-error.test.ts +0 -55
  194. package/src/assert/assertion-error.ts +0 -62
  195. package/src/assert/expect-locator.test.ts +0 -217
  196. package/src/assert/expect.test.ts +0 -73
  197. package/src/assert/expect.ts +0 -299
  198. package/src/browser/browser-context.browser-test.ts +0 -201
  199. package/src/browser/browser-context.test.ts +0 -407
  200. package/src/browser/browser-context.ts +0 -208
  201. package/src/browser/browser-finder.ts +0 -110
  202. package/src/browser/browser.test.ts +0 -88
  203. package/src/browser/browser.ts +0 -515
  204. package/src/browser/context-tree.test.ts +0 -86
  205. package/src/browser/context-tree.ts +0 -70
  206. package/src/browser/dialog.browser-test.ts +0 -144
  207. package/src/browser/dialog.test.ts +0 -322
  208. package/src/browser/dialog.ts +0 -83
  209. package/src/browser/download.test.ts +0 -161
  210. package/src/browser/download.ts +0 -172
  211. package/src/browser/downloads-dir.test.ts +0 -75
  212. package/src/browser/downloads-dir.ts +0 -39
  213. package/src/browser/emulation.browser-test.ts +0 -186
  214. package/src/browser/emulation.test.ts +0 -200
  215. package/src/browser/emulation.ts +0 -204
  216. package/src/browser/file-chooser.test.ts +0 -142
  217. package/src/browser/file-chooser.ts +0 -99
  218. package/src/browser/file-paths.ts +0 -18
  219. package/src/browser/goto.browser-test.ts +0 -44
  220. package/src/browser/navigation-error.ts +0 -26
  221. package/src/browser/navigation.browser-test.ts +0 -172
  222. package/src/browser/page-api.browser-test.ts +0 -228
  223. package/src/browser/page-logs.browser-test.ts +0 -119
  224. package/src/browser/page-logs.test.ts +0 -212
  225. package/src/browser/page-logs.ts +0 -208
  226. package/src/browser/page-wait.ts +0 -19
  227. package/src/browser/page.test.ts +0 -1567
  228. package/src/browser/page.ts +0 -1176
  229. package/src/browser/pdf.ts +0 -62
  230. package/src/browser/screenshot.ts +0 -65
  231. package/src/browser/transfers.browser-test.ts +0 -180
  232. package/src/config/config.ts +0 -33
  233. package/src/config/default.ts +0 -5
  234. package/src/config/fixtures.test.ts +0 -137
  235. package/src/config/mask.test.ts +0 -143
  236. package/src/config/mask.ts +0 -87
  237. package/src/config/run-settings.test.ts +0 -163
  238. package/src/config/run-settings.ts +0 -114
  239. package/src/config/secrets.ts +0 -70
  240. package/src/config/variables.ts +0 -31
  241. package/src/index.ts +0 -10
  242. package/src/locator/action-timeout-error.test.ts +0 -83
  243. package/src/locator/action-timeout-error.ts +0 -72
  244. package/src/locator/element-state.test.ts +0 -181
  245. package/src/locator/element-state.ts +0 -159
  246. package/src/locator/locate-nodes.browser-test.ts +0 -169
  247. package/src/locator/locator-actions.test.ts +0 -355
  248. package/src/locator/locator-chain.test.ts +0 -292
  249. package/src/locator/locator.browser-test.ts +0 -137
  250. package/src/locator/locator.test.ts +0 -179
  251. package/src/locator/locator.ts +0 -656
  252. package/src/locator/selector-errors.ts +0 -18
  253. package/src/locator/selector.test.ts +0 -124
  254. package/src/locator/selector.ts +0 -107
  255. package/src/locator/set-files.browser-test.ts +0 -141
  256. package/src/locator/set-files.test.ts +0 -191
  257. package/src/locator/text-locator.test.ts +0 -118
  258. package/src/logger/index.test.ts +0 -28
  259. package/src/logger/index.ts +0 -40
  260. package/src/network/data-collector.test.ts +0 -111
  261. package/src/network/data-collector.ts +0 -92
  262. package/src/network/network-data.browser-test.ts +0 -192
  263. package/src/network/network-tracker.browser-test.ts +0 -151
  264. package/src/network/network-tracker.test.ts +0 -307
  265. package/src/network/network-tracker.ts +0 -286
  266. package/src/network/network-wait.ts +0 -28
  267. package/src/network/response.test.ts +0 -93
  268. package/src/network/response.ts +0 -58
  269. package/src/network/router.browser-test.ts +0 -260
  270. package/src/network/router.test.ts +0 -528
  271. package/src/network/router.ts +0 -420
  272. package/src/network/url-match.test.ts +0 -69
  273. package/src/network/url-match.ts +0 -41
  274. package/src/runner/page-logs-report.test.ts +0 -144
  275. package/src/runner/page-logs-report.ts +0 -82
  276. package/src/runner/prepare-run.test.ts +0 -33
  277. package/src/runner/prepare-run.ts +0 -36
  278. package/src/runner/registry.test.ts +0 -59
  279. package/src/runner/registry.ts +0 -46
  280. package/src/runner/reporter.test.ts +0 -73
  281. package/src/runner/reporter.ts +0 -109
  282. package/src/runner/test-error.test.ts +0 -39
  283. package/src/runner/test-error.ts +0 -20
  284. package/src/runner/test-runner.ts +0 -184
  285. package/src/script/call-function.browser-test.ts +0 -112
  286. package/src/script/call-function.test.ts +0 -174
  287. package/src/script/call-function.ts +0 -113
  288. package/src/script/helpers.test.ts +0 -116
  289. package/src/script/helpers.ts +0 -68
  290. package/src/script/preload.browser-test.ts +0 -144
  291. package/src/script/preload.test.ts +0 -180
  292. package/src/script/preload.ts +0 -102
  293. package/src/script/serialize.test.ts +0 -182
  294. package/src/script/serialize.ts +0 -203
  295. package/src/testing/browser-fixture.ts +0 -18
  296. package/src/testing/fake-websocket.ts +0 -72
  297. package/src/testing/stub-connector.ts +0 -154
  298. package/src/tests/index.spec.ts +0 -30
  299. package/src/transport/bidi-connection.test.ts +0 -155
  300. package/src/transport/bidi-connection.ts +0 -256
  301. package/src/transport/bidi-error.ts +0 -23
  302. package/src/transport/foundation.browser-test.ts +0 -73
  303. package/src/transport/subscriptions.test.ts +0 -140
  304. package/src/transport/subscriptions.ts +0 -110
  305. package/src/types/bidi-commands.test-d.ts +0 -55
  306. package/src/wait/wait-until.test.ts +0 -117
  307. package/src/wait/wait-until.ts +0 -86
  308. package/tsconfig.json +0 -44
  309. /package/{src/api.ts → dist/api.d.ts} +0 -0
  310. /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
  311. /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
  312. /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
  313. /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
  314. /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
  315. /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
  316. /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
  317. /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
  318. /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
  319. /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
  320. /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
  321. /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
  322. /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
  323. /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
  324. /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
  325. /package/{src → dist}/types/bidi.d.ts +0 -0
  326. /package/{src → dist}/types/browser.d.ts +0 -0
  327. /package/{src → dist}/types/config.d.ts +0 -0
  328. /package/{src → dist}/types/env.d.ts +0 -0
@@ -1,52 +0,0 @@
1
- # Recorder — roadmap
2
-
3
- Date: 2026-10-01
4
- Status: planning. No spec written yet.
5
- UI: the Electron app at `~/dev/itmetsam/projects/samurai` (designs: "SAMURAI" design canvas, boards "Test editor & recorder" and "Test editor · code view").
6
-
7
- Each item gets its own spec in `docs/superpowers/specs/` and its own plan. Order respects dependencies (`←`).
8
-
9
- ## Decisions so far
10
-
11
- - **Audience: non-technical testers.** Recording, editing and replaying happen in the Electron UI; nobody has to touch code.
12
- - **Source of truth: the `.spec.ts` file.** The steps view is parsed from it and edits are written back with minimal diffs ("edit either one and the other updates"). The recorder emits a restricted set of known step calls so parsing is reliable; any statement outside that set becomes a "Custom code" step.
13
- - **Recording browser: external window for now.** The engine launches the browser over BiDi; the tester interacts in that window while the Electron app shows steps live. In-page overlay handles the Alt+click assertion picker. Embedding the page in the Electron editor panel (as the designs show) is **required later** — see item 7.
14
-
15
- ## Todo
16
-
17
- ### Phase 1 — framework prerequisites
18
- - [x] **1. Test API alignment** — `describe`, `test(name, async ({ page, browser, env, secrets }) => …)` fixtures, `page.goto` resolving relative URLs against the environment's `baseURL`; environment selection, per-environment variables and timeout precedence (run > environment > project); a secrets provider interface (env-var/`.env` provider now, keychain/vault plugged in by the Electron app later) with masking in logs and reports. — [spec](specs/2026-10-01-test-api-alignment-design.md)
19
- - [ ] **2. Semantic locators** — `getByRole`, `getByLabel`, `getByTestId`, `getByText`, plus an ordered fallback-locator list per step (input for self-healing).
20
-
21
- ### Phase 2 — recorder core
22
- - [ ] **3. Step codec** ← 1, 2 — steps model, parser (TS AST → steps, unknown → "Custom code"), minimal-diff writer.
23
- - [ ] **4. Recorder engine** ← 2, 3 — preload capture over a `script.addPreloadScript` channel, locator ranking (verified to match exactly one element), step normalisation (merge typing into `fill`, drop focus clicks, navigation waits), record-from-line N, Alt+click assertions.
24
-
25
- ### Phase 3 — UI
26
- - [ ] **5. Recorder UI (Electron)** ← 3, 4 — steps panel, step inspector, IPC to the engine, live step stream while recording.
27
-
28
- ### Phase 4 — browsers
29
- - [ ] **6. Chrome support** — likely via the `chromium-bidi` mapper (BiDi over CDP). Separate from the recorder, but item 7 builds on it.
30
- - [ ] **7. Embedded recording browser (Electron + chromium-bidi)** ← 6 — **required**: the designs show the recorded page inside the editor panel.
31
- - Idea: render the page under test in a `WebContentsView`, expose CDP (`remote-debugging-port` or `webContents.debugger`), run the `chromium-bidi` mapper in the main process, and add a framework transport that talks BiDi to it. Everything above the transport stays unchanged.
32
- - Risks to settle in a spike first:
33
- - The debugging port exposes every Electron window over CDP, including SAMURAI's own UI. It must be filtered to the recording view, and a local open port is a security concern (prefer a pipe or `webContents.debugger`).
34
- - The mapper expects browser-level CDP (`Target.*`, `Browser.*`); a page-level `webContents.debugger` session may not be enough.
35
- - User contexts (`createBrowserContext`), downloads and some emulation may behave differently in Electron.
36
- - Firefox can't be embedded: in-app recording is Chromium-only. Replay still runs in Firefox and Chrome.
37
- - Spike (throwaway): the mapper drives one `WebContentsView` in Electron 41 and runs `browsingContext.navigate` and `script.callFunction`.
38
- - Considered and rejected: **Tauri**. It uses the OS webview (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux); only WebView2 exposes CDP, so in-app recording would work on Windows only.
39
-
40
- ### Later (not scheduled)
41
- - AI-suggested assertions, self-healing (selector fixes), AI agent recording. All build on item 4.
42
- - `api.<name>` fixture: per-environment HTTP client with auth from a secret (designs: "Environment settings" → APIs). Builds on item 1.
43
- - Masking secrets in screenshots, videos and traces (item 1 only masks logs and reports).
44
-
45
- ## Dependency graph
46
-
47
- ```
48
- 1 ─┐
49
- ├─ 3 ─┬─ 4 ── 5
50
- 2 ─┴─────┘
51
- 6 ── 7
52
- ```
@@ -1,185 +0,0 @@
1
- # Auto-waiting actions — design
2
-
3
- Date: 2026-09-29
4
- Status: approved in brainstorming, pending spec review
5
- Depends on: the `expect` feature (`docs/superpowers/specs/2026-09-29-expect-design.md`), including its final fix wave (commits 6b5ed5b, e802a96, 0a377cf).
6
-
7
- ## Goal
8
-
9
- Locator actions (`click`, `fill`, `focus`) wait until the element is ready for that action instead of firing once and throwing. Waiting uses one retry mechanism shared with `expect`, with a clear error naming the unmet check.
10
-
11
- ```ts
12
- await page.locator("button[@type='submit']").click(); // waits: attached, visible, stable, enabled, not covered
13
- await page.locator("input[@id='name']").fill("Sam", { timeout: 10000 });
14
- await page.locator("div[@class='spinner']").waitFor({ state: "hidden" });
15
- await page.locator("button[@id='hidden-debug']").click({ force: true });
16
- ```
17
-
18
- ## Non-goals
19
-
20
- - Waiting in reads (`textContent`, `inputValue`, `isVisible`, …) — they stay instant; `expect()` does the waiting for reads.
21
- - New actions (`hover`, `press`, `check`, `selectOption`, …) — later work; they reuse this mechanism.
22
- - `toBeEnabled` / `toBeEditable` matchers — enabled by the new reads, added later.
23
- - Shadow DOM, iframes, CSS selectors.
24
-
25
- ## Components
26
-
27
- ### `src/wait/wait-until.ts` (new) — the one retry mechanism
28
-
29
- ```ts
30
- export interface WaitOptions { timeout: number; interval?: number } // interval default 100
31
-
32
- export async function waitUntil<T>(
33
- probe: () => Promise<T>,
34
- isDone: (current: T, previous: T | undefined) => boolean,
35
- options: WaitOptions,
36
- ): Promise<T>;
37
-
38
- export async function resolveTimeout(perCall?: number): Promise<number>;
39
- // perCall → config `expect.timeout` → 5000
40
-
41
- export class WaitTimeoutError<T> extends Error {
42
- readonly timeout: number;
43
- readonly last: T | undefined; // last completed probe result, undefined if none completed
44
- }
45
- ```
46
-
47
- Behaviour (carries over the `expect` loop semantics after fix F3, plus the clamp from that fix's open concern):
48
-
49
- 1. Always probes at least once.
50
- 2. `timeout === 0`: the single probe is awaited un-raced; `isDone` false → `WaitTimeoutError` with that result.
51
- 3. `timeout > 0`: each probe races the time remaining to the deadline. If the deadline wins, throw `WaitTimeoutError` with `last` = the last completed result (or `undefined`).
52
- 4. After a completed probe: `isDone(current, previous)` true → resolve with `current`. Otherwise, if the deadline has passed → `WaitTimeoutError`; else sleep `min(interval, remaining)`. A probe is only started while time remains, so after the final clamped sleep the wait ends at the deadline instead of starting a probe that could only lose the race (resolves the open concern from expect fix F3).
53
- 5. Errors thrown by `probe` are rethrown immediately.
54
- 6. Race timers are cleared when the probe wins; no timer outlives the call.
55
-
56
- `LocatorAssertions.assertEventually` in `src/assert/expect.ts` is rewritten on top of `waitUntil` with identical observable behaviour: `WaitTimeoutError` with `last` → `AssertionError` with `actual = last`; without `last` → `Error("expect(locator).<name>: could not read <selector> within <timeout>ms")`. `resolveTimeout` moves from `expect.ts` to `wait-until.ts`. The existing `expect` tests must pass unchanged.
57
-
58
- ### `src/locator/element-state.ts` (new) — browser-side probe
59
-
60
- ```ts
61
- export interface ElementBox { x: number; y: number; width: number; height: number } // viewport coords
62
- export interface ElementState {
63
- attached: boolean;
64
- visible: boolean;
65
- enabled: boolean;
66
- editable: boolean;
67
- box: ElementBox | null; // after any scroll; null when not attached
68
- hitTarget: "self" | string | null; // "self", a description of the covering element, or null when not checked / not attached
69
- }
70
- export interface ProbeOptions { scroll: boolean; hitTest: boolean }
71
-
72
- export function elementStateScript(elementExpression: string, options: ProbeOptions): string;
73
- export function parseElementState(value: RemoteValue): ElementState;
74
- ```
75
-
76
- One `script.evaluate` per poll returns the whole state as a JSON string (parsed by `parseElementState`, which throws on a malformed result).
77
-
78
- Check definitions (browser side):
79
-
80
- | Check | Definition |
81
- |---|---|
82
- | attached | the xpath matches an element |
83
- | visible | box width and height > 0 and computed style not `display:none`, `visibility:hidden`, `opacity:"0"` (same rule as `isVisible()`) |
84
- | enabled | `!el.matches(":disabled")` and `el.getAttribute("aria-disabled") !== "true"` |
85
- | editable | enabled, and not `el.readOnly`, and (a text-type `<input>` — type in text, search, email, url, tel, password, number, or missing — or `<textarea>` or `el.isContentEditable`) |
86
- | stable | computed in Node: `box` equal (all four numbers) to the previous poll's `box`, both non-null |
87
- | hit target | with `hitTest`: `document.elementFromPoint(centerX, centerY)` is `el` or `el.contains(it)` → `"self"`; otherwise a description `tag#id.class1.class2` of the topmost element (`"nothing"` when `elementFromPoint` returns null) |
88
-
89
- Scrolling: with `scroll`, when attached and the box is not fully inside the viewport, call `el.scrollIntoView({ block: "center", inline: "center" })` before measuring `box` and hit target.
90
-
91
- ### `src/locator/locator.ts` changes
92
-
93
- ```ts
94
- export interface ActionOptions { timeout?: number; force?: boolean }
95
- export type WaitForState = "attached" | "detached" | "visible" | "hidden";
96
-
97
- click(options?: ActionOptions): Promise<void>;
98
- fill(value: string, options?: ActionOptions): Promise<void>;
99
- focus(options?: ActionOptions): Promise<void>;
100
- waitFor(options?: { state?: WaitForState; timeout?: number }): Promise<void>; // state default "visible"
101
- isEnabled(): Promise<boolean>; // instant; false when missing
102
- isEditable(): Promise<boolean>; // instant; false when missing
103
- ```
104
-
105
- - `click()` and `focus()` now return `Promise<void>` (previously leaked the raw BiDi `RemoteValue`).
106
- - `clickRect(rect)` stays public and unchanged.
107
- - The actions click with real BiDi pointer input at the centre of the **latest** measured box.
108
-
109
- ### `src/locator/action-timeout-error.ts` (new)
110
-
111
- ```ts
112
- export class ActionTimeoutError extends Error {
113
- readonly action: string; // "click" | "fill" | "focus" | "waitFor"
114
- readonly selector: string;
115
- readonly timeout: number;
116
- readonly checks: Record<string, "pass" | "fail" | "pending"> | undefined;
117
- readonly coveredBy: string | undefined;
118
- }
119
- ```
120
-
121
- The runner reports it through the existing `toTestError` path as `type: "error"` (not an assertion).
122
-
123
- ## Per-action flow
124
-
125
- Every poll is one probe round trip. Checks are evaluated in the listed order; the first failing check is the reason in the error.
126
-
127
- | Action | Probe options | Done when | Then |
128
- |---|---|---|---|
129
- | `click()` | scroll, hitTest | attached, visible, stable, enabled, hit = self | pointer move/down/up at centre of latest box |
130
- | `fill()` | scroll, hitTest | attached, visible, enabled, editable, hit = self | pointer click at centre (focus), select existing value, type (existing `fill` behaviour; `""` → Backspace) |
131
- | `focus()` | none | attached | `el.focus()` |
132
- | `waitFor({ state })` | none | attached: attached · detached: not attached · visible: attached and visible · hidden: not attached or not visible | resolve |
133
-
134
- With `timeout: 0` the *stable* check is dropped from `click()`'s required checks (it needs two probes); there is still exactly one probe.
135
-
136
- `force: true` (click, fill, focus): skip every check except *attached*; still scroll into view (click, fill) and use the real pointer.
137
-
138
- ## Errors
139
-
140
- Timeout message, built from the last completed probe:
141
-
142
- ```
143
- click(): //button[@type="submit"] was not actionable within 5000ms
144
- attached ✓ visible ✓ stable ✓ enabled ✗ hit target —
145
- ```
146
-
147
- - Symbols: `✓` pass, `✗` fail, `—` not evaluated because an earlier check failed.
148
- - Covered element: the hit-target entry reads `hit target ✗ (covered by div#cookie-banner.overlay)`.
149
- - Never attached: `click(): //button[@type="submit"] was not attached within 5000ms`.
150
- - No probe completed before the deadline: `click(): could not read //button[@type="submit"] within 5000ms`.
151
- - `waitFor`: `waitFor(): //div[@class="spinner"] did not become hidden within 5000ms`.
152
-
153
- Probe errors (invalid xpath, navigation destroying the execution context) are rethrown immediately.
154
-
155
- ## Timeouts
156
-
157
- - Resolution: per-call `{ timeout }` → config `expect.timeout` → `5000` (shared with `expect` via `resolveTimeout`).
158
- - `timeout: 0` checks once without waiting; the stability check is skipped because it needs two probes.
159
- - A test timeout closes the browser; the in-flight probe then fails or never settles, and `waitUntil`'s deadline race ends the action within its own timeout.
160
-
161
- ## Behaviour changes (intended, user-visible)
162
-
163
- - `click()` is a real pointer click (trusted events; hover/mousedown/mouseup fire) instead of the synthetic `el.click()`.
164
- - Actions on a missing element wait up to the timeout instead of throwing immediately.
165
- - `click()` / `focus()` resolve to `void`.
166
-
167
- ## Testing
168
-
169
- - Unit (`npm test`, no browser):
170
- - `src/wait/wait-until.test.ts` — fake probes: at least one probe; probe until done; `previous` passed to `isDone`; timeout with last value; `timeout: 0` single un-raced probe; probe error rethrown without retry; never-settling probe stops at the deadline; final sleep clamped to remaining time; no leftover timers.
171
- - `src/locator/element-state.test.ts` — script compiles for all option combinations; embeds the element expression; `parseElementState` round-trips and rejects malformed values.
172
- - `src/locator/locator-actions.test.ts` — stub connector with scripted `ElementState` sequences:
173
- - `click()` retries through not attached → hidden → moving → disabled → covered → actionable, then sends exactly one pointer click at the latest box centre;
174
- - timeout error text for each failing check, covered-by, never-attached and no-probe cases;
175
- - `force` skips checks but still requires attached;
176
- - `fill()` waits for editable, then keeps the select-then-type sequence;
177
- - `focus()` waits for attached only;
178
- - `waitFor` for all four states and its error text;
179
- - `isEnabled()` / `isEditable()`.
180
- - Existing `expect` tests pass unchanged (regression net for the `waitUntil` refactor).
181
- - Browser (opt-in, `npm run test:browser` → `tsx --test "src/**/*.browser-test.ts"`, headless Firefox; not matched by `npm test`'s `src/**/*.test.ts` glob because the suffix is `-test.ts`, not `.test.ts`):
182
- - Fixtures injected into a blank page via `script.evaluate` (no external site, no real form submissions): button enabled after 300 ms; overlay removed after 300 ms; element sliding for 300 ms; button below the fold; readonly input; an `opacity:0` button for `force` (not "visible" by our rule, yet still hit-testable — a truly hidden or covered element cannot receive a real pointer click, with or without `force`); an element removed after 300 ms for `waitFor({ state: "hidden" })`; disabled and covered buttons for the timeout errors.
183
- - The fixtures load `about:blank`, which needs `Page.navigateTo` to keep URLs that already have a scheme (today it turns any non-`http` URL into the literal `https://url`); the plan fixes that as part of the browser-test task.
184
- - Each asserts the action succeeds or fails with the specified message.
185
- - TDD: each test before its implementation.
@@ -1,130 +0,0 @@
1
- # BiDi foundation — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 0 (prerequisite of every other BiDi spec)
6
-
7
- ## Goal
8
-
9
- Fix the transport gaps that make every later feature unreliable, and add the shared primitives (typed commands, refcounted subscriptions, event waiting, context tree) the other specs build on.
10
-
11
- ## Problems found in the current code
12
-
13
- 1. `BiDiConnector.messageListener` handles only `type: "success"` and `"event"`. A `type: "error"` reply is dropped and its promise stays in `resolveMap` forever, so a failing command hangs until some outer timeout.
14
- 2. `Browser.newPage()` sends `browsingContext.create` but returns `new Page(connector, "")`, an empty context id.
15
- 3. The subscribed events are a hardcoded list in `Browser.launch` (`["browsingContext.load"]`); `onEvent` for anything else silently never fires.
16
- 4. `Page.waitForNetworkIdle` leaks its listener and never resolves if `load` already fired (fixed properly in the network-tracking spec, but needs the primitives here).
17
- 5. `BiDiCommands` lacks most commands the other specs use (see Typing).
18
-
19
- ## Non-goals
20
-
21
- - Multi-browser support, reconnecting after socket loss, BiDi over pipes.
22
- - Public API for the primitives below; they are internal to `src/transport` and `src/browser`.
23
-
24
- ## Components
25
-
26
- ### Error replies — `src/transport/bidi-error.ts` (new)
27
-
28
- ```ts
29
- export class BiDiError extends Error {
30
- readonly code: string; // BiDi error code: "no such node", "invalid argument", "unknown error", …
31
- readonly command: string; // e.g. "script.callFunction"
32
- readonly remoteStack?: string;
33
- }
34
- ```
35
-
36
- `messageListener` handles `{ type: "error", id, error, message, stacktrace }`: look up `id`, reject with `BiDiError`, delete the entry. `resolveMap` entries store the method name for the error. A reply with an unknown `id` is logged at debug level and ignored. `rejectAll` (socket close/error) keeps rejecting with plain errors, now including the pending method names in the message.
37
-
38
- ### Send timeout
39
-
40
- `send(method, params, { timeout? })`: default `commandTimeout` from config (`bidi.commandTimeout`, default 30000). On expiry: delete the map entry and reject with `BiDiError` code `"timeout"`. A late reply for a deleted id is ignored. Commands known to block legitimately (`browsingContext.navigate` with `wait: "complete"`, `browsingContext.print`) pass their own timeout.
41
-
42
- ### Refcounted subscriptions — `src/transport/subscriptions.ts` (new)
43
-
44
- ```ts
45
- connector.subscribe(events: (keyof BiDiEvents)[], options?: { contexts?: string[] }): Promise<Subscription>;
46
- interface Subscription { readonly id: string; unsubscribe(): Promise<void> }
47
- ```
48
-
49
- - Refcount per event name (global scope). First subscriber of an event sends `session.subscribe { events: [name] }` and stores the returned `subscription` id; last unsubscriber sends `session.unsubscribe { subscriptions: [id] }`.
50
- - Context-scoped subscriptions send `contexts` and are not shared; they always issue their own command.
51
- - `Browser.launch` stops subscribing to a hardcoded list; `browsingContext.load` is subscribed by the feature that needs it.
52
- - `unsubscribe()` is idempotent.
53
- - Module-level events (e.g. `network`) subscribe to all their events on `session.subscribe`; the connector's typed event map stays per-event.
54
-
55
- ### Event waiting
56
-
57
- ```ts
58
- connector.waitForEvent<E extends keyof BiDiEvents>(
59
- event: E,
60
- predicate: (params: BiDiEvents[E]["params"]) => boolean,
61
- options: { timeout: number; signal?: AbortSignal },
62
- ): Promise<BiDiEvents[E]["params"]>;
63
- ```
64
-
65
- Registers the listener before returning (so callers can start the triggering command afterwards without racing), removes it on match, timeout or abort. Timeout rejects with `WaitTimeoutError` (from `src/wait/wait-until.ts`).
66
-
67
- ### Context tree — `src/browser/context-tree.ts` (new)
68
-
69
- ```ts
70
- class ContextTree {
71
- static create(connector): Promise<ContextTree>; // getTree + subscribe browsingContext.contextCreated/contextDestroyed
72
- parentOf(context: string): string | null;
73
- rootOf(context: string): string; // the top-level context
74
- isWithin(context: string, root: string): boolean;
75
- dispose(): Promise<void>;
76
- }
77
- ```
78
-
79
- Events for iframes carry the child's context id; features that filter by page (`network`, `log`, prompts) call `tree.isWithin(event.context, page.contextId)`.
80
-
81
- ### `Browser.newPage` / `Page`
82
-
83
- - `newPage` returns `new Page(connector, result.context)`. `background`/`userContext` keep passing through.
84
- - `Page` exposes a read-only `contextId` getter (tests currently reach into private fields).
85
- - `Page` owns a `ContextTree` instance created lazily and shared by features.
86
-
87
- ### Typing
88
-
89
- Add to `src/types/bidi-modules/*.d.ts` and `BiDiCommands`/`BiDiEvents` (params/result per the spec), each with a one-line doc comment:
90
-
91
- | Module | Commands |
92
- |---|---|
93
- | script | `callFunction`, `disown`, `addPreloadScript`, `removePreloadScript` |
94
- | browsingContext | `locateNodes`, `handleUserPrompt`, `setViewport`, `reload`, `traverseHistory`, `close`, `print` |
95
- | input | `setFiles` |
96
- | network | `addIntercept`, `removeIntercept`, `continueRequest`, `continueResponse`, `continueWithAuth`, `provideResponse`, `failRequest`, `setCacheBehavior`, `addDataCollector`, `removeDataCollector`, `getData`, `disownData` |
97
- | browser | `createUserContext`, `removeUserContext`, `getUserContexts`, `setDownloadBehavior` |
98
- | session | `unsubscribe` |
99
- | emulation | all `emulation.set*` (types exist; only wire into `BiDiCommands`) |
100
-
101
- The `network` and `emulation` modules are currently missing from `BiDiCommands` entirely. A compile-time test (`src/types/bidi-commands.test-d.ts` checked by `tsc --noEmit`) asserts each method above resolves to a params/result pair.
102
-
103
- ## Errors
104
-
105
- | Situation | Behaviour |
106
- |---|---|
107
- | `type:"error"` reply | reject with `BiDiError` |
108
- | No reply within timeout | reject with `BiDiError` code `"timeout"` |
109
- | Socket closed | reject all pending; message names the methods |
110
- | Command not supported by the browser | `BiDiError` code `"unknown command"` / `"unsupported operation"`, unchanged (features wrap where they need friendlier text) |
111
-
112
- ## Testing
113
-
114
- - Unit (stub connector): error reply rejects the right promise only; unknown-id reply ignored; timeout rejects and a late reply does not throw; refcount sends one `session.subscribe` for two subscribers and one `unsubscribe` after both leave; double `unsubscribe()` is a no-op; `waitForEvent` registers before resolving, cleans up on all three exits; `ContextTree` parent/root/`isWithin` including destroyed contexts.
115
- - Browser: `newPage()` returns a page whose `navigateTo("about:blank")` works; a deliberately invalid command (`script.evaluate` with bad target) rejects with `code: "no such frame"` within a second.
116
- - TDD: tests first.
117
-
118
- ## Implementation notes
119
-
120
- Deviations and details settled while implementing:
121
-
122
- - `session.unsubscribe` takes `{ subscriptions }` or `{ events }`; the connector only uses subscription ids.
123
- - A new subscriber that arrives while the last one is still unsubscribing keeps the existing subscription (no unsubscribe/subscribe churn); the per-event command queue makes this safe.
124
- - Multi-event `subscribe([...])` sends one `session.subscribe` per event so refcounts stay per event; a failure part-way rolls back the events already acquired.
125
- - `commandTimeout` is a `BiDiConnector` option, read once at launch from config `bidi.commandTimeout` (a missing config falls back to 30000), instead of a config read per `send`.
126
- - `Page.waitForNetworkIdle` now subscribes to `browsingContext.load` itself, waits with `waitForEvent` (30000 ms cap) and unsubscribes; its real replacement is the network-tracking spec.
127
- - `Page.tree()` is the lazily created, shared `ContextTree`; nothing creates it until a feature needs it.
128
- - Type fix: `Info.children` is `InfoList | null` (was `InfoList[]`), `parent` may be `null`, `originalOpener` may be `null`.
129
- - `src/testing/fake-websocket.ts` (`FakeWebSocket`, `autoReply`, `tick`) is the new unit-test seam for the real connector; `stubConnector` is unchanged.
130
- - Tests: `transport/{bidi-connection,subscriptions}.test.ts`, `browser/{context-tree,browser}.test.ts`, additions to `browser/page.test.ts`, `types/bidi-commands.test-d.ts` (compile-time, via `tsc --noEmit`), and `transport/foundation.browser-test.ts` (real Firefox).
@@ -1,88 +0,0 @@
1
- # User contexts and storage — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 12
6
- Depends on: bidi-foundation
7
-
8
- ## Goal
9
-
10
- Cheap isolated browser sessions (separate cookies, storage, cache) inside one browser process, and a cookie API that is scoped to them. Base for parallel tests and multi-user scenarios.
11
-
12
- ```ts
13
- const alice = await browser.newContext();
14
- const bob = await browser.newContext();
15
- const pageA = await alice.newPage();
16
- await alice.addCookies([{ name: "sid", value: "1", domain: "example.com" }]);
17
- await bob.close();
18
- ```
19
-
20
- ## Non-goals
21
-
22
- - Parallel test execution (only the primitive it needs; the runner currently launches one browser per test and keeps doing so).
23
- - Persistent profiles per context, proxy per context (`createUserContext.proxy` is left out until needed).
24
- - Cross-context storage copying.
25
-
26
- ## Components
27
-
28
- ### `BrowserContext` (rewrite of the test helper in `src/browser/browser-context.ts`)
29
-
30
- The existing file holds only the test-only `setContent`; it moves to `src/testing/set-content.ts` (imports updated) and the file becomes the real class.
31
-
32
- ```ts
33
- class BrowserContext {
34
- readonly id: string; // BiDi userContext, "default" for the default one
35
- newPage(): Promise<Page>; // browsingContext.create { type: "tab", userContext }
36
- pages(): Page[];
37
- addCookies(cookies: Cookie[]): Promise<void>; // storage.setCookie, partition { type: "context", userContext }
38
- cookies(filter?: { name?: string; domain?: string; path?: string }): Promise<Cookie[]>; // storage.getCookies
39
- clearCookies(filter?): Promise<void>; // storage.deleteCookies
40
- close(): Promise<void>; // browser.removeUserContext, closes its pages
41
- }
42
- ```
43
-
44
- `Browser` additions:
45
-
46
- ```ts
47
- newContext(options?: { acceptInsecureCerts?: boolean; unhandledPromptBehavior?: … }): Promise<BrowserContext>; // browser.createUserContext
48
- contexts(): BrowserContext[]; // browser.getUserContexts
49
- defaultContext: BrowserContext
50
- ```
51
-
52
- - `Browser.newPage()` delegates to `defaultContext.newPage()` and returns a working `Page` (bug fix owned by the foundation spec).
53
- - `Browser.getCookie` / `setCookie` (existing) are kept and delegate to `defaultContext` with the `partition` argument still accepted.
54
- - Cookie value conversion: BiDi cookies carry `value: { type: "string" | "base64", value }`; the public `Cookie` uses plain strings (base64 only when `valueEncoding: "base64"`). `sameSite`, `httpOnly`, `secure`, `expiry` map 1:1. `domain` is mandatory in BiDi; `addCookies` throws `TypeError` when missing and no `url` was given (`url` is parsed for domain/path/secure).
55
- - `BrowserContext.close()` removes the user context; the browser closes its tabs. `Page`s of that context reject further use with `Error("page closed")`.
56
- - Context ids for `Page`: a `Page` remembers its `BrowserContext` so `page.context()` works and cookie helpers on `Page` (`page.context().addCookies`) are scoped correctly.
57
- - Cleanup: `Browser.close()` closes all contexts first (best effort), then the browser.
58
-
59
- ### Storage partitions
60
-
61
- `storage.*` commands take `partition`: `{ type: "context", context }` (by top-level context) or `{ type: "storageKey", userContext, sourceOrigin }`. `BrowserContext` cookies use `{ type: "storageKey", userContext }`; the returned `partitionKey` is ignored on read.
62
-
63
- ## Errors
64
-
65
- | Situation | Error |
66
- |---|---|
67
- | Removing the default context | `Error("cannot close the default context")` |
68
- | Cookie without domain/url | `TypeError` |
69
- | `no such user context` | `BiDiError` (context already removed) |
70
-
71
- ## Testing
72
-
73
- - Unit (stub connector): create/remove payloads; `newPage` passes `userContext`; cookie mapping both ways incl. base64 and `url` parsing; `close` order (pages, then remove); default-context guard; `Browser.getCookie/setCookie` still work.
74
- - Browser: two contexts; a cookie set in one is invisible to the other (via `document.cookie` on a local server origin); `clearCookies` with filter; closing a context closes its pages; the old `newPage()` bug (empty context id) has a regression test navigating the returned page.
75
- - TDD: tests first.
76
-
77
- ## Implementation notes
78
-
79
- - **Where:** `browser/browser-context.ts` (`BrowserContext`, `Cookie`, `CookieInput`, `CookieFilter`), `Browser.newContext/contexts/defaultContext`, `Page.context()`. The old contents of that file (a test-only `setContent` copy nobody imported; tests use `testing/browser-fixture.ts`) were replaced, so nothing had to move.
80
- - **`browser.newPage(options)` keeps its BiDi-shaped signature** and sends exactly what it is given (existing tests depend on that); it attaches the page to the context named by `userContext` (the default one when absent or `"default"`). `context.newPage()` calls it with `userContext` for a user context and without for the default one. The page `Browser.launch` returns belongs to the default context. `Browser.getCookie/setCookie` stay as raw partition-based calls.
81
- - **`browser.contexts()` is sync and lists the default context plus the contexts this browser created** (not `browser.getUserContexts`): Firefox keeps user contexts in its profile, so that command also lists contexts left by earlier runs. A page opened with a `userContext` id this browser did not create adopts a context object for it.
82
- - **Contexts are removed with the browser.** `Browser.close()` closes the contexts still open before it closes the browser (failures ignored), because otherwise every context a test forgets stays in the test profile. A browser test relaunches the browser after closing and checks none of the ids it created remain.
83
- - **Cookies:** `addCookies` takes plain string values, a `domain` or a `url` (then domain, path and `secure` come from it unless given; `path` defaults to `/`), and checks every cookie before storing any. `cookies(filter?)` returns plain cookies (`value` a string, `expiry` only for persistent ones); `clearCookies(filter?)`. All use the partition `{ type: "storageKey", userContext }`, which for the default context is `"default"`. The local partition typing said `"storagekey"`; the browser only accepts `"storageKey"`.
84
- - **Base64 cookie values were left out:** Firefox returns a base64 value as a Latin-1 string (`héllo` comes back as `héllo`), so a round trip would garble text; `cookies()` still decodes a `base64` value if a browser sends one.
85
- - **`BrowserContext.close()`** removes the user context (the browser closes its tabs), then marks its pages closed (`Page.detach()`, which releases what they registered) and drops it from `browser.contexts()`. `no such user context` counts as closed; the default context cannot be closed (`cannot close the default context`); a second call does nothing; other errors leave it open. After closing, `newPage`, `addCookies`, `cookies` and `clearCookies` throw `context closed`.
86
- - **Firefox 153 facts:** `createUserContext` (also `acceptInsecureCerts`), `getUserContexts`, `removeUserContext` (closes the tabs; the default context answers `invalid argument`), cookies isolated per user context (`document.cookie` in the other context is empty), `setCookie` without a partition writes to the default context; a new context inherits the session's `unhandledPromptBehavior`, so dialogs there are handled by the same page policy.
87
- - **Not done:** per-context proxy, persistent profiles per context, cross-context storage copying (non-goals); `emulation` options on `newContext` come with the emulation spec.
88
- - Tests: `browser/browser-context.test.ts` (24), `browser/browser-context.browser-test.ts` (port 9245; isolation, cookies through `document.cookie`, closing, dialog and viewport in a new context, leftovers checked after the browser closed with a second launch on port 9246).
@@ -1,94 +0,0 @@
1
- # Downloads and file dialogs — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 14
6
- Depends on: bidi-foundation, set-files (file chooser reuses `setFiles`)
7
-
8
- ## Goal
9
-
10
- Test downloads and native file pickers.
11
-
12
- ```ts
13
- const download = await page.waitForDownload({ trigger: () => link.click() });
14
- console.log(download.suggestedFilename(), download.url());
15
- const file = await download.path(); // resolves once the download completed
16
- await download.cancel?.(); // only if the browser supports it (see below)
17
-
18
- const chooser = await page.waitForFileChooser({ trigger: () => button.click() });
19
- await chooser.setFiles("./fixtures/a.png");
20
- ```
21
-
22
- ## Non-goals
23
-
24
- - Streaming download contents, resuming, progress events beyond start/end.
25
- - Downloads from other tabs.
26
- - Saving to arbitrary user-chosen folders per download (folder is per context/browser).
27
-
28
- ## Downloads
29
-
30
- ### Events and command
31
-
32
- - `browsingContext.downloadWillBegin { context, navigation, url, suggestedFilename, timestamp }` and `browsingContext.downloadEnd { …, status: "complete" | "canceled", filepath? }`.
33
- - `browser.setDownloadBehavior { downloadBehavior: { type: "allowed", destinationFolder } | { type: "denied" } | null, userContexts? }` sets where files land. The framework sets `allowed` with a per-test temp folder (`<config downloadsDir, default result/downloads>/<testId>/`) on page creation so downloads are deterministic and cleaned by the runner with the other artifacts. `null` restores the browser default.
34
- - Browser support gate: `setDownloadBehavior` is newer than the events. If Firefox lacks it (`unknown command`), `page.waitForDownload` still works using the events, `download.path()` returns the `filepath` from `downloadEnd`, and the folder option throws `UnsupportedOperationError`. Confirmed by a browser test before the destination-folder feature ships.
35
-
36
- ### `src/browser/download.ts` (new)
37
-
38
- ```ts
39
- class Download {
40
- url(): string;
41
- suggestedFilename(): string;
42
- path(): Promise<string>; // resolves on downloadEnd complete; rejects on canceled
43
- failure(): Promise<string | null>; // "canceled" or null
44
- }
45
- page.waitForDownload(options: { trigger?: () => Promise<unknown>; timeout?: number }): Promise<Download>;
46
- page.on("download", (d: Download) => …);
47
- ```
48
-
49
- - Correlation: `downloadEnd` matched to `downloadWillBegin` by `navigation` id.
50
- - `waitForDownload` registers its `waitForEvent` **before** running `trigger`, then resolves on `downloadWillBegin` (not on end). `timeout` defaults to the navigation timeout (30000).
51
- - `path()` has its own timeout (same default). A canceled download rejects with `DownloadError { url, reason: "canceled" }`.
52
- - Subscription (refcounted) to both events from `Page` creation so `page.on("download")` and early downloads are not missed; events filtered by `ContextTree`.
53
-
54
- ## File dialogs
55
-
56
- - `input.fileDialogOpened { context, element?, multiple }` fires when a click would open the native picker. Subscribed from page creation. Firefox may open the OS dialog unless it is suppressed; the browser test verifies that headless Firefox does not hang (the dialog is not shown headless).
57
- - `page.waitForFileChooser({ trigger?, timeout? }): Promise<FileChooser>`.
58
-
59
- ```ts
60
- class FileChooser {
61
- isMultiple(): boolean;
62
- element(): Locator | null; // Locator pinned to the element (locate-nodes `handle` selector) when `element` was provided
63
- setFiles(files: string | string[]): Promise<void>; // input.setFiles on that element; reuses set-files validation
64
- }
65
- ```
66
-
67
- - If `element` is absent in the event (browser omitted it), `setFiles` throws `Error("file chooser has no element reference")`.
68
- - Unhandled dialogs are not answered; a picker that is opened while nobody waits is ignored (the headless browser closes it).
69
-
70
- ## Errors
71
-
72
- | Situation | Error |
73
- |---|---|
74
- | No download within timeout | `WaitTimeoutError` → `waitForDownload(): no download started within <t>ms` |
75
- | Download canceled | `DownloadError` from `path()` |
76
- | `setDownloadBehavior` unsupported | `UnsupportedOperationError` (events still work) |
77
- | Chooser without element | `Error` as above |
78
-
79
- ## Testing
80
-
81
- - Unit (stub connector): behaviour set on page creation with per-test folder; `waitForDownload` registers before `trigger`; begin/end correlation by navigation id; canceled → rejection; `path()` timeout; iframe event accepted, foreign ignored; `FileChooser` mapping to `setFiles` with the event's element and `multiple` guard; missing element error.
82
- - Browser (local server with `Content-Disposition: attachment`): download completes and file content matches; `suggestedFilename`; canceled/failed download (server aborts) rejects; `<input type=file>` click yields a chooser and `setFiles` populates `input.files`; behaviour of unsupported `setDownloadBehavior` recorded by an explicit skip-with-reason.
83
- - TDD: tests first.
84
-
85
- ## Implementation notes
86
-
87
- - **Where:** `browser/download.ts` (`Download`, `DownloadTracker`, `DownloadError`, `DownloadTimeoutError`), `browser/file-chooser.ts` (`FileChooser`, `FileChooserTracker`), `browser/downloads-dir.ts` (`prepareDownloadsDir`, `removeIfEmpty`), `browser/file-paths.ts` (`resolveFiles`, `assertIsFile`, shared with `Locator.setInputFiles`), `browser/page-wait.ts` (`DownloadWaitTimeoutError`, `FileChooserTimeoutError`), and on `Page`: `startDownloadTracking`, `startFileChooserTracking`, `waitForDownload`, `waitForFileChooser`, `page.on/once/off("download" | "filechooser")`. `Browser.downloadsDir`, config `downloadsDir` (default `result/downloads`).
88
- - **Everything in the draft works on Firefox 153, including `browser.setDownloadBehavior`** (probed, then browser-tested), so no fallback was needed. The framework calls it when the browser starts (`allowed`, into a new folder `<downloadsDir>/dl-<time>-<pid>-<n>` of the browser's own), so a test knows where downloads land and runs do not mix. A browser without the command gets no folder (`browser.downloadsDir` is undefined, a `warn` is logged) and the events still work. `Browser.close()` removes the folder again when nothing was downloaded into it, so runs do not pile up empty folders; a folder with downloads is left for the test artifacts.
89
- - **`Download`:** `url()`, `suggestedFilename()`, `path({ timeout? })` (waits for the end, resolves with the file the browser wrote, rejects with `DownloadError` when cancelled and `DownloadTimeoutError` after `timeout`, default the navigation timeout), `failure()` (null when complete, otherwise "canceled" or "page closed"). Ends are matched to beginnings by navigation id; an end nobody began is ignored; disposing the page fails downloads still going on with "page closed".
90
- - **`waitForDownload({ trigger, timeout })`** resolves when the download *starts*; it listens before `trigger` runs, and a trigger that throws rejects the wait with that error. The generic `Page.waitForPageEvent` now serves the request, response, download and file-chooser waits.
91
- - **Firefox 153 facts:** `downloadEnd`'s field is `filepath` (the local typing said `filePath`; fixed); a download the server breaks off, or one refused with `downloadBehavior: denied`, ends as `canceled`; when a name is taken Firefox saves under `report(1).csv` and its `suggestedFilename` already carries the suffix (`report(2).csv`); iframe downloads reach the page; `fileDialogOpened` fires for a click on the input and on its label, carries the input as a full node (`sharedId` is all that is used) and `multiple`, and nothing is shown in headless mode.
92
- - **`FileChooser`:** `isMultiple()` and `setFiles(files)` (resolves paths, checks every file exists and is a file before sending, refuses several files for a single-file input, sends `input.setFiles` for the element in the frame that opened the dialog; an empty list clears; `file chooser has no element reference` when the browser omitted it). The draft's `element(): Locator` was left out: locators no longer have a pinned-handle form (see the locate-nodes notes) and `setFiles` is what a test needs. A picker nobody waits for is simply ignored and does not hang the page.
93
- - **Cleanup in tests:** the browser test removes its downloads folder afterwards.
94
- - Tests: `browser/download.test.ts` (12), `browser/file-chooser.test.ts` (11), `browser/downloads-dir.test.ts`, the "Page downloads and file choosers" block in `browser/page.test.ts`, `browser/transfers.browser-test.ts` (port 9250, 13 tests).