@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,88 +0,0 @@
1
- # Emulation — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 13
6
- Depends on: contexts-and-storage (overrides can target user contexts), bidi-foundation
7
-
8
- ## Goal
9
-
10
- Expose the `emulation.*` commands (already typed in `src/types/bidi-modules/emulation.d.ts`, not wired into `BiDiCommands`) as page/context options.
11
-
12
- ```ts
13
- const ctx = await browser.newContext({
14
- locale: "nl-NL", timezone: "Europe/Amsterdam",
15
- geolocation: { latitude: 52.37, longitude: 4.9, accuracy: 10 },
16
- userAgent: "Samurai/1.0", colorScheme: "dark", offline: false,
17
- });
18
- await page.emulate({ geolocation: null }); // reset one override
19
- ```
20
-
21
- ## Non-goals
22
-
23
- - Device descriptors (iPhone 15, …); a list of presets can be layered on later.
24
- - Permissions API grants (`permissions.setPermission` is a separate BiDi module, not in scope).
25
- - CPU throttling.
26
-
27
- ## Options and mapping
28
-
29
- | Option | Command | Reset |
30
- |---|---|---|
31
- | `locale` | `emulation.setLocaleOverride { locale }` | `null` |
32
- | `timezone` | `emulation.setTimezoneOverride { timezone }` | `null` |
33
- | `geolocation` `{ latitude, longitude, accuracy? }` | `emulation.setGeolocationOverride { coordinates }` | `null` |
34
- | `userAgent` | `emulation.setUserAgentOverride { userAgent }` | `null` |
35
- | `colorScheme` `"light" \| "dark"` | `emulation.setForcedColorsModeThemeOverride`-adjacent; see Caveat | `null` |
36
- | `offline` | `emulation.setNetworkConditions { networkConditions: { type: "offline" } }` | `null` |
37
- | `orientation` | `emulation.setScreenOrientationOverride` | `null` |
38
- | `touch` (max touch points) | `emulation.setTouchOverride { maxTouchPoints }` | `null` |
39
- | `javaScriptEnabled` | `emulation.setScriptingEnabled { enabled }` (`null` resets) | `null` |
40
-
41
- Each command accepts `contexts` or `userContexts` (mutually exclusive). Context-level options (`browser.newContext({…})`) use `userContexts: [id]` so every page of the context inherits them, including pages created later; `page.emulate({…})` uses `contexts: [pageContext]`. A page override wins over its context's override (browser behaviour; asserted in a browser test).
42
-
43
- ### Caveat: colour scheme
44
-
45
- BiDi only has `setForcedColorsModeThemeOverride` (forced-colors theme), which is not the same as `prefers-color-scheme`. `colorScheme` is included only if the browser test proves it drives `matchMedia("(prefers-color-scheme: dark)")` in Firefox; otherwise it is dropped from v1 and documented as unsupported (`UnsupportedOperationError`). The same proof-first rule applies to every row: each option ships only if its browser test passes on Firefox; the rest throw `UnsupportedOperationError` naming the option.
46
-
47
- ## Components
48
-
49
- - `src/browser/emulation.ts`: `applyEmulation(connector, target: { contexts?: string[]; userContexts?: string[] }, options)` runs the mapped commands sequentially, skipping `undefined` options, sending `null` to reset. Validation before sending: latitude −90..90, longitude −180..180, accuracy ≥ 0, IANA timezone via `Intl.DateTimeFormat(undefined, { timeZone })` (throws `RangeError` on unknown), locale via `Intl.getCanonicalLocales`.
50
- - `Page.emulate(options)`; `Browser.newContext(options)` accepts the same options object (contexts spec) and applies it right after `createUserContext`, before the first page exists.
51
- - `BrowserContext.emulate(options)` for changing a live context.
52
- - Partial failure: if command N of M fails, previously applied commands are **not** rolled back; the thrown `EmulationError` lists `applied` and `failed` option names so the caller knows the state.
53
-
54
- ## Errors
55
-
56
- `EmulationError extends Error { applied: string[]; failed: string; cause: BiDiError }`; `UnsupportedOperationError` per option as above; `RangeError` for invalid values.
57
-
58
- ## Testing
59
-
60
- - Unit: option → command/payload table for each row, `null` reset, contexts vs userContexts targeting, validation errors, partial failure report, context options applied before first page.
61
- - Browser (each row is its own test and gates whether the option ships): `navigator.language`, `Intl.DateTimeFormat().resolvedOptions().timeZone`, `navigator.geolocation.getCurrentPosition` result, `navigator.userAgent`, `navigator.onLine` plus a failing fetch when offline, `matchMedia` for colour scheme, `screen.orientation.type`, `navigator.maxTouchPoints`, scripting disabled leaves inline script unexecuted; page override beats context override.
62
- - TDD: tests first.
63
-
64
- ## Implementation notes
65
-
66
- - **Where:** `browser/emulation.ts` (`EmulationOptions`, `applyEmulation`, `validateEmulation`, `EmulationError`, `EmulationUnsupportedError`), `Page.emulate`, `BrowserContext.emulate`, `Browser.newContext(options)` (its options now extend `EmulationOptions`), `permissions.setPermission` typed (`types/bidi-modules/permissions.d.ts`, wired into `BiDiCommands`).
67
- - **Proof-first result on Firefox 153** (probed, then browser-tested; each row is its own test):
68
-
69
- | Option | Firefox 153 |
70
- |---|---|
71
- | `locale` | works: `navigator.language`, `Intl` formatting; `null` resets |
72
- | `timezone` | works: `Intl` zone and `getTimezoneOffset`; `null` resets |
73
- | `userAgent` | works: `navigator.userAgent` and the request header the server receives |
74
- | `offline` | works: `navigator.onLine` false and fetches fail |
75
- | `orientation` | works: `screen.orientation.type` |
76
- | `screen` (new: `{ width, height }` → `setScreenSettingsOverride`) | works: `screen.width`/`height` |
77
- | `geolocation` | works, **once the permission is granted for that user context** (without it `getCurrentPosition` never answers) |
78
- | `touch`, `javaScriptEnabled` | `unknown command` → `UnsupportedOperationError` |
79
- | `colorScheme` | **dropped**: BiDi has no `prefers-color-scheme` override, only forced colours, which Firefox 153 lacks (`unknown command`) |
80
-
81
- `setTouchOverride`, `setScriptingEnabled` and the forced-colours / scrollbar commands are unknown to Firefox 153; the framework maps `touch` and `javaScriptEnabled` anyway and turns `unknown command` / `unsupported operation` into `UnsupportedOperationError` (so they work where a browser has them). The forced-colours and scrollbar commands are not exposed.
82
- - **Permissions (an addition to the draft, which listed them as a non-goal):** geolocation is unusable without a grant, so `BrowserContext.setPermission(name, state, origin)` and `Page.grantPermission(name, { origin? })` (default: the origin the page is on; `about:blank` and `data:` pages need an explicit one) send `permissions.setPermission`, naming the user context for non-default contexts.
83
- - **Targets:** `page.emulate` uses `contexts: [pageId]`, `context.emulate` and `newContext` options use `userContexts: [id]` (the default context is `"default"`). Firefox rejects both or neither. Verified: a context's setting reaches open pages and pages opened later, not other contexts; a page's own setting wins; `null` on the page falls back to the context's value.
84
- - **Options:** `undefined` leaves an option alone, `null` resets it; `offline: false` and `javaScriptEnabled: true` mean "back to normal" (a `null` command); `orientation` takes `"portrait"` / `"landscape"` (primary) or one of the four BiDi types. Commands go out in a fixed order (locale, timezone, userAgent, geolocation, offline, orientation, screen, touch, javaScriptEnabled).
85
- - **Validation before anything is sent** (`RangeError`): latitude −90..90, longitude −180..180, accuracy ≥ 0, timezone via `Intl.DateTimeFormat`, locale via `Intl.getCanonicalLocales`, non-empty user agent, positive integers for screen and touch, known orientation. `newContext` validates before it creates the context.
86
- - **Failures:** nothing is rolled back. `EmulationError` (`applied`, `failed`, `cause`) for an ordinary failure; `EmulationUnsupportedError extends UnsupportedOperationError` (`option`, `applied`) for an unsupported option. `newContext` removes the context it just created when its emulation fails, so nothing stays in the profile.
87
- - **Typing fixes:** `SetLocaleOverrideParameters` no longer requires both `contexts` and `userContexts`; `SetTouchOverrideParameters.maxTouchPoints` may be `null`.
88
- - Tests: `browser/emulation.test.ts` (34), the "emulation" block in `browser/browser-context.test.ts`, `browser/emulation.browser-test.ts` (port 9248, 12 tests).
@@ -1,130 +0,0 @@
1
- # `expect` assertions — design
2
-
3
- Date: 2026-09-29
4
- Status: approved in brainstorming, pending spec review
5
-
6
- ## Goal
7
-
8
- Give tests a single `expect()` API for:
9
-
10
- - **Locators** — web-first assertions that auto-retry until the DOM matches or a timeout expires.
11
- - **Plain values** — synchronous, Jest-style matchers.
12
-
13
- A failing assertion throws, stops the test, and is reported as `type: "assertion"` with `expected` / `actual`.
14
-
15
- ```ts
16
- import { expect } from "../assert/expect.js";
17
-
18
- await expect(page.locator("div[@class='alert']")).toBeVisible();
19
- await expect(page.locator("h1")).not.toHaveText(/error/i, { timeout: 2000 });
20
-
21
- expect(count).toBeGreaterThan(2);
22
- expect(result).toEqual({ ok: true });
23
- ```
24
-
25
- ## Non-goals (v1)
26
-
27
- - Soft assertions (`expect.soft`).
28
- - Custom matchers / `expect.extend()`.
29
- - Page-level matchers (`toHaveURL`, `toHaveTitle`).
30
-
31
- ## Components
32
-
33
- ### `src/assert/expect.ts`
34
-
35
- - `expect` overloads:
36
- - `expect(locator: Locator): LocatorAssertions`
37
- - `expect<T>(value: T): ValueAssertions<T>`
38
- - Dispatch by `value instanceof Locator`.
39
- - `LocatorAssertions` — every matcher returns `Promise<void>` and takes an optional last argument `{ timeout?: number }`:
40
- - `toBeVisible()`
41
- - `toHaveText(expected: string | RegExp)` — string compares against trimmed `textContent` exactly.
42
- - `toContainText(expected: string | RegExp)` — string is a substring check.
43
- - `toHaveValue(expected: string | RegExp)`
44
- - `toHaveAttribute(name: string, expected: string | RegExp)`
45
- - `toHaveCount(expected: number)`
46
- - `ValueAssertions<T>` — every matcher returns `void`:
47
- - `toBe(expected)` — `Object.is`.
48
- - `toEqual(expected)` — `util.isDeepStrictEqual`.
49
- - `toBeTruthy()`, `toBeFalsy()`
50
- - `toContain(item)` — arrays (`includes`) and strings (substring).
51
- - `toMatch(pattern: string | RegExp)` — strings only.
52
- - `toBeGreaterThan`, `toBeGreaterThanOrEqual`, `toBeLessThan`, `toBeLessThanOrEqual` — numbers/bigints.
53
- - Both classes expose a `.not` getter returning a negated instance.
54
- - Type misuse (e.g. `toMatch` on a non-string, number comparison on a non-number) throws a `TypeError`, not an `AssertionError`.
55
-
56
- ### `src/assert/assertion-error.ts`
57
-
58
- `AssertionError extends Error` with:
59
-
60
- - `matcher: string` — e.g. `"toHaveText"` or `"not.toHaveText"`.
61
- - `expected: unknown`, `actual: unknown`
62
- - `locator?: string` — the XPath, for locator assertions.
63
-
64
- Message format:
65
-
66
- ```
67
- expect(locator).toHaveText
68
- locator: //h1
69
- expected: "Contact"
70
- received: "Home"
71
- ```
72
-
73
- Values are formatted by a shared `format()`: strings quoted, RegExp as `/src/flags`, other values via `util.inspect` with `depth: 2`.
74
-
75
- ### `src/locator/locator.ts` — new public reads
76
-
77
- None of these throw when the element is missing; an evaluate exception (e.g. invalid XPath) still throws.
78
-
79
- | Method | Returns | Missing element |
80
- |---|---|---|
81
- | `isVisible()` | `boolean` — exists, bounding box width and height > 0, computed style not `display:none`, `visibility:hidden`, `opacity:0` | `false` |
82
- | `textContent()` | trimmed `string` | `null` |
83
- | `inputValue()` | `string` (`.value`) | `null` |
84
- | `getAttribute(name)` | `string` | `null` (also when the attribute is absent) |
85
- | `count()` | `number` via XPath `count(...)` with `XPathResult.NUMBER_TYPE` | `0` |
86
-
87
- ## Retry behaviour (locator matchers only)
88
-
89
- - Internal `poll(read, check, timeout)`:
90
- 1. `actual = await read()`; with `timeout > 0` the read is raced against the time left until the deadline (start + timeout), and the race timer is cleared when the read wins.
91
- 2. If `check(actual) !== negated` → resolve.
92
- 3. If elapsed ≥ timeout → throw `AssertionError` with the last `actual`.
93
- 4. Otherwise wait 100 ms and repeat.
94
- - The timeout is a hard bound: if the deadline expires while a read is still pending, the matcher throws `AssertionError` with the last completed `actual`, or, when no read ever completed, a plain `Error`: `expect(locator).<matcher>: could not read <selector> within <timeout>ms`.
95
- - A matcher always reads at least once, even with `timeout: 0`; with `timeout: 0` that single read is awaited without a race.
96
- - Errors thrown by `read()` (invalid XPath, BiDi socket closed after the browser was killed) are rethrown immediately, not retried.
97
- - Timeout resolution: per-call `{ timeout }` → config `expect.timeout` → default `5000` ms.
98
- - `SamuraiTestConfig` gains `expect?: { timeout?: number }`.
99
-
100
- ## Failure reporting
101
-
102
- - `TestError.type` becomes `"timeout" | "error" | "assertion"`, with optional `expected?: unknown` and `actual?: unknown`. The reporter stores them as-is (JSON-serialisable values); a RegExp, bigint, symbol or function, or any value `JSON.stringify` rejects (e.g. a circular object), is stored as its `format()` string.
103
- - `test-runner.ts`: in the test's `catch`, `err instanceof AssertionError` → `type: "assertion"` with `expected` / `actual`; everything else stays `"error"`.
104
-
105
- ### Missing-`await` guard
106
-
107
- - Every locator assertion is added to a module-level `pendingAssertions` registry in `expect.ts`. It is removed as soon as anyone subscribes to it (`await`, `.then`, `.catch`, `.finally`, `Promise.all`, `assert.rejects`) or when it passes. An un-awaited assertion that fails stays in the registry: a failure nobody observed is the same silent-pass trap as one still running.
108
- - To detect `await`, matchers return a `Promise` subclass (still typed `Promise<void>`) whose overridden `then` removes it from the registry; `await` calls that `then` because the constructor is not the native `Promise`. Promises derived from it are plain promises.
109
- - `expect.ts` exports `takePendingAssertions(): string[]`, which returns the matcher names of un-awaited assertions that are still running or already failed, and clears the registry.
110
- - After the test function resolves, the runner calls it; if the result is non-empty, the test fails with `type: "assertion"` and message `expect(locator).<matcher>() was not awaited`.
111
- - Each assertion gets an internal rejection handler (which does not count as awaiting), so an un-awaited failure is never an unhandled rejection.
112
-
113
- ## Testing
114
-
115
- - Runner: Node's built-in `node:test` via tsx. `package.json`: `"test": "tsx --test \"src/**/*.test.ts\""`.
116
- - `src/assert/expect.test.ts`:
117
- - Each value matcher: pass, fail, `.not`, `AssertionError` fields, `TypeError` on misuse.
118
- - Locator matchers against a stub `BiDiConnector` whose `send("script.evaluate")` returns a scripted sequence of results:
119
- - passes after N polls,
120
- - times out with the last `actual`,
121
- - rethrows on an evaluate exception without retrying,
122
- - `.not` variants,
123
- - per-call `timeout` overrides the default.
124
- - Missing-`await` guard: an un-awaited locator assertion shows up in `takePendingAssertions()`, also when it already failed; an awaited one never does, pass or fail.
125
- - End-to-end: `src/tests/index.spec.ts` asserts `toHaveValue` on the filled fields and `toBeVisible` on the name field; verified with `bun run dev` against Firefox.
126
- - TDD: write each test before its implementation.
127
-
128
- ## Out of scope, noted
129
-
130
- - `Page.navigateTo` builds `` `${protocol}://url` `` (literal `url`) for scheme-less URLs — separate fix.
@@ -1,90 +0,0 @@
1
- # `browsingContext.locateNodes` — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 3
6
- Depends on: script-call-function (preload-scripts optional)
7
-
8
- ## Goal
9
-
10
- Add CSS, text and accessibility locators, and move element resolution from ad-hoc `document.evaluate` calls to the protocol's own locator command. Existing `page.locator(xpath)` behaviour is unchanged.
11
-
12
- ```ts
13
- page.locator("//button[@type='submit']") // unchanged
14
- page.getByCss("form.login input[name=user]")
15
- page.getByText("Sign in", { match: "full" })
16
- page.getByRole("button", { name: "Submit" })
17
- page.locator("//form").getByRole("textbox") // chained, scoped to the form
18
- ```
19
-
20
- ## Non-goals
21
-
22
- - Shadow-DOM piercing beyond what the browser's `locateNodes` does natively.
23
- - A custom selector-string mini-language (`role=button[name=…]`); constructors only.
24
- - `getByLabel`, `getByPlaceholder`, `getByTestId`; each is a one-line CSS/xpath wrapper, added later.
25
-
26
- ## Components
27
-
28
- ### Selector model — `src/locator/selector.ts` (new)
29
-
30
- ```ts
31
- export type Selector =
32
- | { kind: "xpath"; value: string }
33
- | { kind: "css"; value: string }
34
- | { kind: "text"; value: string; match: "full" | "partial"; ignoreCase: boolean }
35
- | { kind: "role"; role: string; name?: string };
36
-
37
- export function toBiDiLocator(selector: Selector): BrowsingContext.Locator;
38
- export function describeSelector(selector: Selector): string; // used in every error message
39
- ```
40
-
41
- `describeSelector` output: xpath → the normalised xpath (unchanged from today, so existing error texts and tests keep passing); css → `css=form input`; text → `text="Sign in"`; role → `role=button[name="Submit"]`. A chain is described `parent >> child`.
42
-
43
- Mapping: xpath → `{type:"xpath"}`, css → `{type:"css"}`, text → `{type:"innerText", value, matchType, ignoreCase}`, role → `{type:"accessibility", role, name}`.
44
-
45
- ### `Locator` changes
46
-
47
- - Constructor takes `Selector[]` (a chain) instead of an xpath string; `new Locator(xpath, …)` callers keep working via `Page.locator(xpath)` which builds `[{kind:"xpath", …}]`.
48
- - New: `getByCss`, `getByText`, `getByRole`, `locator(xpath)` on `Locator` (chaining). Each returns a new `Locator` whose chain is the parent chain plus one selector.
49
- - `resolveOne(): Promise<ElementHandle | null>`: for each selector in the chain call `browsingContext.locateNodes { context, locator, maxNodeCount: 1, startNodes }` where `startNodes` is the previous result as a `SharedReference`; return null when any step is empty.
50
- - `resolveAll(): Promise<ElementHandle[]>`: intermediate steps use every match as `startNodes`; the last step has no `maxNodeCount`. Results de-duplicated by `sharedId`, document order kept as returned.
51
- - `count()`, `all()` use `resolveAll`. `all()` returns locators pinned to an element via a `{kind: "handle"}` internal selector (never exposed) instead of the `(xpath)[n]` trick, so non-xpath selectors also support `all()`.
52
- - The actionability probe takes the element as a `callFunction` argument (`probeElement(el, options)`) instead of resolving the xpath in page code. A `no such node` reply (element replaced between resolve and probe) is treated as "not attached" for that poll.
53
- - One poll = `locateNodes` + one `callFunction`: two round trips instead of one. To keep the cost down, `resolveOne` caches the last `ElementHandle` per action; the next poll reuses it and only re-resolves after `no such node`.
54
-
55
- ### Accessibility locator caveats
56
-
57
- - `accessibility` requires a role and/or name; both empty throws `TypeError` at construction.
58
- - Support and exact name computation differ per browser. If Firefox rejects the locator type with `unsupported operation`, `getByRole` throws `UnsupportedOperationError("getByRole is not supported by <browser>")` instead of retrying until the timeout.
59
-
60
- ## Errors
61
-
62
- | Situation | Behaviour |
63
- |---|---|
64
- | Invalid CSS / xpath | `BiDiError` `invalid selector` → thrown immediately (not retried), message includes `describeSelector` |
65
- | No match | as today: retried until the action/assert timeout, then the existing `ActionTimeoutError` (“was not attached”) |
66
- | Unsupported locator type | `UnsupportedOperationError` |
67
-
68
- ## Testing
69
-
70
- - Unit: `toBiDiLocator` / `describeSelector` for every kind and for chains; chain resolution sends `startNodes` from the previous step; multi-match parents are unioned and de-duplicated; cached handle reused then re-resolved after a `no such node` stub reply; invalid selector not retried.
71
- - Existing `locator-actions`, `expect-locator`, `element-state` tests pass unchanged (xpath behaviour, messages).
72
- - Browser: css, text (full vs partial, case), role+name against a fixture page; chained locator scoped to one of two forms; `all()` on a css locator; element replaced mid-wait is re-resolved and clicked.
73
- - TDD: tests first.
74
-
75
- ## Implementation notes
76
-
77
- Deviations and details settled while implementing:
78
-
79
- - **No handle cache across polls (replaces the "cache the last `ElementHandle`" idea).** Firefox keeps a removed node resolvable while it is alive, so a cached handle would keep probing a detached element, and it would also miss that a different element became the first match. Every poll searches again: one `locateNodes` (limited to 1 node) plus one `callFunction`. The probe checks `el.isConnected`, so a resolvable-but-removed node reads as not attached. `no such node` between the search and the call also reads as missing/not attached.
80
- - **Locating asks for references only:** `serializationOptions: { maxDomDepth: 0 }`.
81
- - **`nth` steps replace pinned handles for `all()`.** `all()` on a locator that is a single xpath keeps returning positional xpaths (`(//li)[n]`, unchanged); every other locator gets a `nth=<index>` step that re-resolves the whole chain and picks the n-th match (0-based), so items survive re-renders. The step before `nth` is never limited by `maxNodeCount`. No internal `handle` selector exists.
82
- - **Chained xpaths are scoped:** in a chain, `input` and `//input` become `.//input`; absolute (`/…`), grouped (`(…)`) and `./…` paths are kept.
83
- - **Defaults:** `getByText` matches fully and case-sensitively (BiDi's defaults); `match: "partial"` and `ignoreCase: true` are opt-in. `describeSelector` adds `(partial, ignoring case)` only for non-default options.
84
- - **`Locator` takes `string | Selector[]`**, so `new Locator(xpath, …)` keeps working; `selector` prints the chain (`//form >> role=button[name="Save"]`).
85
- - **New API:** `Locator.locator/getByCss/getByText/getByRole` (chaining) and `Page.getByCss/getByText/getByRole`; `InvalidSelectorError` (browser answered `invalid selector`, thrown at once, never retried) and `UnsupportedOperationError` are exported from `locator.ts`.
86
- - **What Firefox (153) supports:** `css`, `xpath` and `accessibility` natively; **`innerText` is `unsupported operation`.** So `getByRole` (role and/or name) works as designed, and text search has a fallback: `TEXT_LOCATE` (`src/locator/text-locator.ts`), an in-page function that searches the descendants of the start nodes (or the whole document), compares `innerText` with whitespace collapsed (`full` = equal, `partial` = substring, optional lower-casing), returns only the innermost matches, unique and in document order. It is tried only after the browser said `unsupported operation`; the answer is remembered per connection, so later text locators go straight to the page. Other locator types that come back unsupported still raise `UnsupportedOperationError`.
87
- - Cost of the text fallback: it reads `innerText` of every element under the roots, so it is linear in page size times depth; fine for test pages, not for very large DOMs.
88
- - The actionability probe is now `PROBE_ELEMENT = (el, options) => …` (element as argument, `DETACHED_STATE` for a missing element) and the framework helpers install it unchanged.
89
- - Test infrastructure: the stub connector answers `browsingContext.locateNodes` (`nodeCounts`, `failLocate`, `unsupportedLocators`, `{ error }` responses). Tests that read the xpath from script text now read it from the locate command.
90
- - Tests: `locator/{selector,text-locator,locator-chain}.test.ts`, updated `locator*.test.ts`/`element-state.test.ts`/`expect-locator.test.ts`, `locator/locate-nodes.browser-test.ts` (real Firefox, port 9235).
@@ -1,87 +0,0 @@
1
- # Logs and page errors (`log.entryAdded`) — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 8
6
- Depends on: bidi-foundation, script-call-function
7
-
8
- ## Goal
9
-
10
- Capture browser console output and uncaught page exceptions, attach them to failing tests, and optionally fail tests on page errors.
11
-
12
- ```ts
13
- page.on("console", (msg) => console.log(msg.type(), msg.text()));
14
- page.on("pageerror", (err) => console.log(err.message));
15
- ```
16
-
17
- ```ts
18
- // samurai.config.ts
19
- logs: { capture: "failures", failOnPageError: true }
20
- ```
21
-
22
- ## Non-goals
23
-
24
- - `script.message` channels and `page.exposeFunction`.
25
- - Console line-level source mapping.
26
- - Browser-level (non-page) log entries.
27
-
28
- ## Components
29
-
30
- ### `src/browser/page-logs.ts` (new)
31
-
32
- ```ts
33
- class PageLogs {
34
- static async start(connector, tree: ContextTree, contextId: string, options?: { maxEntries?: number }): Promise<PageLogs>;
35
- readonly entries: readonly LogEntry[]; // ring buffer, default 1000
36
- readonly errors: readonly PageError[]; // uncaught exceptions only
37
- on/off("console" | "pageerror", …);
38
- clear(): void;
39
- dispose(): Promise<void>;
40
- }
41
- ```
42
-
43
- - Subscribes (refcounted) to `log.entryAdded`; keeps entries where `source.context` is set and within the page (`ContextTree`). Entries without a context (worker realms) are ignored in v1.
44
- - Started at page creation so early logs are not lost.
45
- - BiDi entry → wrapper:
46
- - `type: "console"`: `ConsoleMessage { type() /* method: log|info|warn|error|debug|… */, text(), args: unknown[] /* fromRemoteValue */, timestamp, location, stack }`.
47
- - `type: "javascript"`: `PageError extends Error { message: text, stack from stackTrace, timestamp }`; also emitted as `pageerror`.
48
- - `level` is kept on the raw entry (`debug | info | warn | error`); `console.error` is `level: "error"` but is **not** a page error.
49
- - Ring buffer drops oldest entries and counts `dropped` for the report.
50
-
51
- ### Runner integration (`src/runner/*`)
52
-
53
- - Config:
54
- ```ts
55
- logs?: {
56
- capture?: "off" | "failures" | "all"; // default "failures"
57
- failOnPageError?: boolean; // default false
58
- ignoreErrors?: (string | RegExp)[]; // messages never counted as failures
59
- }
60
- ```
61
- - After each test, `capture: "failures"` attaches `entries` to the `TestResult` only when the test failed; `"all"` always; `"off"` never. The reporter prints them under the failure and writes them into `result/report.json` (`logs: [{ level, type, text, timestamp }]`).
62
- - `failOnPageError`: after the test body succeeds, if `errors` contains any not matching `ignoreErrors`, the test fails with `PageErrorsError` listing the messages. A test that already failed keeps its own error; page errors are attached as logs.
63
- - Per-test escape hatch: `page.allowPageErrors()` (this test only).
64
- - A fresh `Browser` per test (current runner) means no manual clearing is needed; `clear()` stays for tests that reuse a page.
65
-
66
- ## Errors
67
-
68
- `PageErrorsError extends Error { errors: PageError[] }`, message `<n> uncaught page error(s): <first message>` plus the rest one per line.
69
-
70
- ## Testing
71
-
72
- - Unit: entry mapping for each console method and for a javascript entry; args deserialized via `fromRemoteValue`; ring buffer overflow and `dropped`; iframe entry kept, foreign context dropped; `pageerror` emitted only for `javascript` type; `console.error` not a page error; `ignoreErrors` string and RegExp; runner attaches logs per `capture` mode and `failOnPageError` behaviour incl. `allowPageErrors`; report JSON shape.
73
- - Browser: page calls `console.log({a:1})`, `console.error("x")`, and throws in a `setTimeout`; assert entries, args `{a:1}`, one page error with a stack.
74
- - TDD: tests first.
75
-
76
- ## Implementation notes
77
-
78
- - **Where:** `browser/page-logs.ts` (`PageLogs`, `ConsoleMessage`, `PageError`, `LogRecord`), `Page.startLogging/getLogs/pageErrors/clearLogs/syncLogs/allowPageErrors/pageErrorsAllowed/logsDropped` and `page.on/once/off("console" | "pageerror")`; `runner/page-logs-report.ts` (`judgePageLogs`, `PageErrorsError`); config `logs: { capture, failOnPageError, ignoreErrors }`; `logs` / `logsDropped` on test results in the report.
79
- - **Typing fix:** the BiDi log entry text field is `text` (the local typing called it `string`).
80
- - **Start:** `Browser.launch` / `newPage` start logging with the page, so output of the first document is kept (a browser test loads a `data:` page that logs at once). A hand-built `Page` starts on first `page.on("console" | "pageerror")` or `startLogging()`.
81
- - **`ConsoleMessage`:** `type()` (the console method), `text()` (the browser's rendering, e.g. `hello 42 Object(2)` in Firefox), `args` (plain values through `fromRemoteValue`), `level`, `timestamp`, `location()` (top stack frame), `stackTrace`. **`PageError`** is an `Error` whose `stack` is the text plus one `at fn (url:line:col)` line per frame.
82
- - **Buffer:** `entries` and `errors` are copies (a snapshot); the buffer keeps the newest `maxEntries` (1000) and counts `dropped`. Entries without a context (workers) and of other pages are ignored; iframes count. Entries of other types (e.g. deprecation) are recorded without an event.
83
- - **Firefox facts:** `console.error` is `level: "error"` and not a page error; an **unhandled promise rejection is reported as a `javascript` entry**, so it is a page error and counts for `failOnPageError`.
84
- - **`syncLogs()`** sends `session.status`: events and replies share one ordered connection, so everything logged before the call has arrived when it resolves. The runner calls it before it reads the logs.
85
- - **Runner integration:** `judgePageLogs` is a pure function (config, entries, errors, dropped, testFailed, allowPageErrors) that returns the error to fail a passing test with, and the entries for the report; `test-runner.ts` calls it through a small `judgeLogs(page, testFailed)` helper on the three ways a test ends (passed, failed, unawaited assertions). A timed-out test ends before that and carries no logs. `capture` defaults to `"failures"`; a test that fails because of page errors counts as failed for that. `ignoreErrors` strings match as substrings.
86
- - **Not done:** the draft said the reporter "prints" the logs under a failure; the reporter only writes `result/report.json` today, and that is where the logs go.
87
- - Tests: `browser/page-logs.test.ts`, "Page logs" block in `browser/page.test.ts`, `runner/page-logs-report.test.ts`, `browser/page-logs.browser-test.ts` (port 9240).
@@ -1,83 +0,0 @@
1
- # Navigation waits — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 6
6
- Depends on: bidi-foundation, network-tracking
7
-
8
- ## Goal
9
-
10
- Make navigation deterministic: pick when `navigateTo` resolves, fail with a real error when navigation fails, and expose load-state waiting for actions that trigger navigation (a click on a link).
11
-
12
- ```ts
13
- await page.navigateTo("example.com"); // wait: "complete" (default)
14
- await page.navigateTo("example.com", { wait: "interactive" });
15
- await page.navigateTo("example.com", { wait: "none" });
16
- await page.locator("//a[@id='next']").click();
17
- await page.waitForLoadState("domcontentloaded"); // "load" | "domcontentloaded" | "networkidle"
18
- await page.reload();
19
- ```
20
-
21
- ## Non-goals
22
-
23
- - `waitForURL` and `waitForNavigation` (small follow-ups on the events added here).
24
- - History traversal and `reload` details (page-api spec owns those commands; this spec only defines the shared wait semantics).
25
-
26
- ## API
27
-
28
- ```ts
29
- navigateTo(url: string, options?: NavigateOptions): Promise<{ navigation: string | null; url: string }>;
30
- interface NavigateOptions {
31
- wait?: "none" | "interactive" | "complete"; // default config navigation.waitUntil, else "complete"
32
- timeout?: number; // default config navigation.timeout, else 30000
33
- protocol?: "http" | "https"; // existing third param, kept
34
- }
35
- waitForLoadState(state?: "load" | "domcontentloaded" | "networkidle", options?: { timeout?: number }): Promise<void>; // default "load"
36
- ```
37
-
38
- The current positional signature `navigateTo(url, wait?, protocol?)` stays accepted for one release: a string second argument is treated as `wait`.
39
-
40
- Config additions (`SamuraiTestConfig`):
41
-
42
- ```ts
43
- navigation?: { timeout?: number /* 30000 */; waitUntil?: "none" | "interactive" | "complete" /* "complete" */ };
44
- ```
45
-
46
- The navigation timeout is deliberately separate from `expect.timeout` (5000, for actions/assertions): page loads legitimately take longer.
47
-
48
- ## Behaviour
49
-
50
- - `browsingContext.navigate { context, url, wait }` runs with the navigation timeout as its command timeout (foundation spec). The result `{ navigation, url }` is returned (`navigation` is null for same-document navigations).
51
- - The `wait` value is passed to the browser, replacing the existing `if (!result)` check (which could never fire).
52
- - Failure detection: on a `BiDiError` from `navigate` (DNS failure, connection refused, TLS error surface as `unknown error` with the net error in the message), throw `NavigationError { url, reason, code }`. `navigationFailed` / `navigationAborted` events matching the `navigation` id also reject a still-pending wait (covers `wait: "none"` followed by `waitForLoadState`).
53
- - Timeout: throw `NavigationError` with `reason: "timeout after <t>ms"`. A best-effort `browsingContext.navigate` to the current URL is *not* attempted; the caller decides.
54
- - Existing scheme handling (`about:blank`, `data:`, `localhost:3000`) is kept.
55
- - `waitForLoadState(state)`:
56
- 1. Read `document.readyState` via `callFunction` (already reached → resolve). Mapping: `load` ⇔ `complete`; `domcontentloaded` ⇔ `interactive` or `complete`.
57
- 2. Otherwise `waitForEvent("browsingContext.load" | "browsingContext.domContentLoaded", context === page)`, listener registered *before* step 1 completes to avoid the read/event race.
58
- 3. `networkidle` = `load` then `NetworkTracker.waitForIdle`.
59
- - Navigation triggered by a click: `click()` does not wait for navigation itself; callers use `waitForLoadState`. (Auto-waiting for navigations after actions is out of scope.)
60
-
61
- ## Errors
62
-
63
- `NavigationError extends Error { url: string; reason: string; code?: string }` with message `navigateTo(): <url> failed: <reason>`. The runner maps it to a normal test error.
64
-
65
- ## Testing
66
-
67
- - Unit (stub connector): `wait` param forwarded, default resolved from config; `BiDiError` → `NavigationError`; `navigationFailed` rejects a pending wait for the matching navigation id only; timeout path; positional-argument compatibility; `waitForLoadState` already-reached (no event wait), not-yet-reached (event), event-before-read race (event registered first), `networkidle` composition.
68
- - Browser (local `node:http` server): `complete` resolves after a delayed subresource, `interactive` before it, `none` immediately; connection-refused port yields `NavigationError`; slow server hits the navigation timeout; click on a link then `waitForLoadState("load")`.
69
- - TDD: tests first.
70
-
71
- ## Implementation notes
72
-
73
- - **Where:** `Page.navigateTo` / `Page.waitForLoadState`, `browser/navigation-error.ts` (`NavigationError`, `LoadStateTimeoutError`), config `navigation: { timeout, waitUntil }` in `types/config.d.ts`, `PageSettings` (`Pick<SamuraiTestConfig, "network" | "navigation">`). `navigateTo(url, wait?, protocol?)` still works; the second argument may be a string (the wait) or an options object.
74
- - **Default wait is now `"complete"`** (was the browser default, `"none"`), from config `navigation.waitUntil` when set.
75
- - **`NavigationError`** for every BiDi error from `browsingContext.navigate` (`code` = the BiDi code, or `"timeout"` with reason `timeout after <t>ms`). The BiDi command itself carries the navigation timeout.
76
- - **`LoadStateTimeoutError extends WaitTimeoutError`**: `waitForLoadState("load") did not finish within <t>ms`. `waitForNetworkIdle`'s default timeout is now the navigation timeout too.
77
- - **Progress per navigation (not in the draft).** A document that is still the old one reports `readyState === "complete"`, so `navigateTo(url, { wait: "none" })` followed by `waitForLoadState()` would have returned at once. For navigations that do not wait for the load, `Page` records which navigation ids reached DOMContentLoaded and load (`browsingContext.domContentLoaded`/`load`, subscribed from the first such navigation until `dispose()`, latest 20 ids kept). `waitForLoadState` trusts that record when it says the last `navigateTo` navigation is short of the state, and reads `document.readyState` otherwise. Failure events count when they belong to that navigation, or, when no navigation is known, `navigationFailed` in this page's context.
78
- - **`waitForLoadState` after a click** cannot know a navigation is coming: if the click's navigation has not started yet, the old document counts as loaded. Wait for a sign of the navigation first (an element of the new page, the URL) and then for the load state; the browser test does this.
79
- - **Firefox 153 facts** (browser tests): the `browsingContext.navigationAborted` event is unknown to it (`session.subscribe` answers `invalid argument`), so a wait subscribes to it only when the browser knows it and remembers per connection when it does not; `navigate` fails with `NS_ERROR_CONNECTION_REFUSED` even with `wait: "none"`; `wait: "none"` returns once the navigation has committed (response headers received), so a server that never answers blocks it until the navigation timeout.
80
- - **`networkidle`** = load, then `NetworkTracker.waitForIdle` (idle time from config `network.idleTime`); the timeout applies to each of the two stages.
81
- - `reload` is left to the page-api spec.
82
- - **Test infra:** `autoReply` handlers may now answer `session.subscribe` (an Error with a `code` property answers with that BiDi code). A few timing assertions in `wait-until.test.ts` and `expect-locator.test.ts` got 5 ms of slack (`setTimeout` can fire a millisecond early).
83
- - Tests: the "Page navigation" block in `browser/page.test.ts`, `browser/navigation.browser-test.ts` (port 9238; local server with a slow image, refused port, hanging route).
@@ -1,78 +0,0 @@
1
- # Network data (cache control and response bodies) — design
2
-
3
- Date: 2026-09-29
4
- Status: implemented (see "Implementation notes")
5
- Roadmap item: 10
6
- Depends on: network-tracking
7
-
8
- ## Goal
9
-
10
- Read response bodies and control the HTTP cache.
11
-
12
- ```ts
13
- const response = await page.waitForResponse("**/api/users");
14
- expect(response.status).toBe(200);
15
- const users = await response.json();
16
-
17
- await page.setCacheDisabled(true); // network.setCacheBehavior { cacheBehavior: "bypass" }
18
- ```
19
-
20
- ## Non-goals
21
-
22
- - Streaming bodies, request body capture beyond what `dataType: "request"` returns (added if Firefox supports it; otherwise out of scope).
23
- - HAR export.
24
- - Modifying responses (interception spec).
25
-
26
- ## API
27
-
28
- ```ts
29
- page.waitForResponse(match: string | RegExp | ((r: Response) => boolean), options?: { timeout?: number }): Promise<Response>;
30
- page.waitForRequest(match: …, options?): Promise<Request>;
31
- page.setCacheDisabled(disabled: boolean): Promise<void>;
32
-
33
- interface Response {
34
- url; status; statusText; headers; fromCache; request; // from network-tracking spec
35
- body(): Promise<Buffer>;
36
- text(): Promise<string>;
37
- json<T = unknown>(): Promise<T>;
38
- }
39
- ```
40
-
41
- `match` strings/RegExps use the URL matching from the interception spec (shared `matchUrl` util in `src/network/url-match.ts`). `waitForResponse` uses `waitForEvent`-style registration: call it before triggering the request, or pass the trigger: `page.waitForResponse(match, { trigger: () => button.click() })` (registers first, runs the trigger, then awaits).
42
-
43
- ## Components
44
-
45
- ### Data collector — `src/network/data-collector.ts` (new)
46
-
47
- - BiDi only retains bodies for requests that a **collector** was registered for *before* the request. `network.addDataCollector { dataTypes: ["response"], maxEncodedDataSize, contexts: [pageContext] }` returns a `collector` id.
48
- - The collector is created lazily on the first use of `waitForResponse`, `response.body()`, `page.on("response")` body access, or explicitly by `page.collectResponseBodies()`. Bodies of requests that completed before that are unavailable: `response.body()` throws `ResponseBodyUnavailableError`.
49
- - Trade-off (documented): always-on collection would retain every body in browser memory, so it is opt-in by first use, plus config `network.collectBodies: true` to start with the page.
50
- - `maxEncodedDataSize` from config `network.maxBodySize` (default 10 MiB). Larger responses: `body()` throws `ResponseBodyUnavailableError("exceeds maxBodySize")`.
51
- - `Response.body()`: waits for `responseCompleted` of that request if not yet seen, then `network.getData { dataType: "response", collector, request }` → `bytes` (`{ type: "string" | "base64", value }`) → `Buffer`. The Buffer is cached on the `Response` so repeated calls work. After a successful read: `network.disownData { dataType, collector, request }` to free browser memory (guarded; failures logged, not thrown).
52
- - `dispose()`: `network.removeDataCollector`.
53
- - Redirect responses have no body: `body()` throws `ResponseBodyUnavailableError("redirect response")`.
54
-
55
- ### Cache
56
-
57
- `setCacheDisabled(true)` → `network.setCacheBehavior { cacheBehavior: "bypass", contexts: [pageContext] }`; `false` → `"default"`. Scoped to the page. If unsupported: `UnsupportedOperationError`.
58
-
59
- ## Errors
60
-
61
- `ResponseBodyUnavailableError extends Error { url: string; reason: string }` with reasons: `collector not registered before the request`, `exceeds maxBodySize`, `redirect response`, `no data available`.
62
-
63
- ## Testing
64
-
65
- - Unit (stub connector): lazy collector creation and payload; `getData` mapping for string/base64 bytes; caching of the buffer; `disownData` after read and failure tolerance; `redirect` and size errors; `waitForResponse` with `trigger` registers before running it; timeout error text; `setCacheDisabled` payload both ways; `dispose` removes the collector.
66
- - Browser (local server): JSON endpoint body via `json()`; binary body round trip via `base64`; second `fetch` with `setCacheDisabled(true)` hits the server twice while default hits it once (server counts requests, cacheable headers set); a response completed before the collector exists rejects with the documented error.
67
- - TDD: tests first.
68
-
69
- ## Implementation notes
70
-
71
- - **Where:** `network/data-collector.ts` (`DataCollector`, `ResponseBodyUnavailableError`), `network/response.ts` (`Response`), `network/network-wait.ts` (`RequestTimeoutError`, `ResponseTimeoutError`, `toPredicate`), and on `Page`: `startBodyCollection`, `waitForResponse`, `waitForRequest`, `setCacheDisabled`. Config `network.collectBodies` (default false) and `network.maxBodySize` (default 10485760).
72
- - **`page.on("response")` now hands out `Response` objects** (a superset of the tracker's plain response: `url`, `status`, `statusText`, `headers`, `fromCache`, `request`, plus `id`, `body()`, `text()`, `json()`). There is exactly one `Response` per network response however many listeners or `waitForResponse` calls see it, so a body is read from the browser once, kept, and released with `network.disownData`.
73
- - **Collection starts on first use** (`page.on("response")`, `waitForResponse`, `startBodyCollection()`), or with the page when `collectBodies` is true. A response that completed before collection began cannot be read. `page.on("response")` starts collection asynchronously, so a response that arrives within the first few milliseconds after registering may still miss it; register listeners before triggering traffic.
74
- - **Error reasons:** the browser answers `no such network data` for both "collector started after the request" and "larger than maxBodySize", and they cannot be told apart, so `ResponseBodyUnavailableError` says: `no body was collected: the collector started after the request, or the response is larger than maxBodySize`. A 3xx response with a `Location` header is a `redirect response` (checked locally, the browser is not asked; a 304 is asked for). `unavailable network data` maps to `the body is not available yet`. Other errors pass through.
75
- - **`waitForResponse(match, { timeout, trigger })` / `waitForRequest`:** `match` is a URL pattern (same rules as `page.route`) or a function of the response/request. The wait listens first, then runs `trigger`; a trigger that throws rejects the wait with that error and leaves nothing listening. `timeout` defaults to the navigation timeout. The timeout error names what was awaited: `waitForResponse(): no response matching "**/api/users" within 5000ms` (`/regex/` and `the given function` likewise).
76
- - **`setCacheDisabled(disabled)`** is `network.setCacheBehavior` with `bypass` / `default` for this page; an unsupported browser gives `UnsupportedOperationError`.
77
- - **Firefox 153 facts** (probed, then browser-tested): `addDataCollector`, `getData` (text as `string`, binary as `base64`, readable repeatedly until disowned), `disownData`, `removeDataCollector` and `setCacheBehavior` all work; `getData` for `dataType: "request"` answers `no such network data` for a plain GET, so request bodies were left out; with the cache on, two fetches of a `max-age` resource hit the server once, with `bypass` twice.
78
- - Tests: `network/data-collector.test.ts`, `network/response.test.ts`, the "Page network data" block in `browser/page.test.ts`, `network/network-data.browser-test.ts` (port 9243: JSON, text, binary, empty, document and redirect bodies, click trigger, size limit through a second `Page` view with a small `maxBodySize`, cache on/off against a request-counting server).