@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,144 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, test } from "node:test";
3
- import { Browser } from "../browser/browser.js";
4
- import type Page from "../browser/page.js";
5
- import { setContent } from "../testing/browser-fixture.js";
6
- import type { BiDiConnector } from "../transport/bidi-connection.js";
7
- import { callFunction } from "./call-function.js";
8
- import { HELPER_SANDBOX } from "./helpers.js";
9
-
10
- let browser: Browser;
11
- let page: Page;
12
- let connector: BiDiConnector;
13
- /** Commands sent to the browser, by method, to count registrations */
14
- const sentMethods: string[] = [];
15
-
16
- before(async () => {
17
- ({ browser, page } = await Browser.launch("firefox", { port: 9234, headless: true }));
18
- connector = (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
19
- const send = connector.send.bind(connector);
20
- connector.send = ((method: string, params: never, options: never) => {
21
- sentMethods.push(method);
22
- return send(method as never, params, options);
23
- }) as typeof connector.send;
24
- await page.navigateTo("about:blank", "complete");
25
- });
26
-
27
- after(async () => {
28
- await page.dispose();
29
- await browser?.close();
30
- });
31
-
32
- const OPTIONS = { timeout: 20000 };
33
- const evaluate = <T>(fn: string, sandbox?: string) =>
34
- callFunction<T>(connector, page.contextId, fn, [], sandbox === undefined ? {} : { sandbox });
35
-
36
- test("an init script runs before page scripts of the next document", OPTIONS, async () => {
37
- const handle = await page.addInitScript(() => {
38
- (globalThis as any).__x = 1;
39
- });
40
- await page.navigateTo(
41
- "data:text/html,<script>window.seenByPage = window.__x</script>",
42
- "complete",
43
- );
44
- assert.equal(await evaluate("() => window.seenByPage"), 1);
45
- await handle.dispose();
46
- });
47
-
48
- test("an init script registered after load applies to the current document and the next one", OPTIONS, async () => {
49
- await page.navigateTo("about:blank", "complete");
50
- const handle = await page.addInitScript(() => {
51
- (globalThis as any).__late = "yes";
52
- });
53
- assert.equal(await evaluate("() => window.__late"), "yes"); // current document, via the immediate run
54
- await page.navigateTo("about:blank?again", "complete");
55
- assert.equal(await evaluate("() => window.__late"), "yes"); // next document, via the registration
56
- await handle.dispose();
57
- });
58
-
59
- test("an init script receives its argument", OPTIONS, async () => {
60
- const handle = await page.addInitScript(
61
- (flags: { beta: boolean; name: string }) => {
62
- (globalThis as any).__flags = flags;
63
- },
64
- { beta: true, name: `q"uote 'x' \\` },
65
- );
66
- await page.navigateTo("about:blank?args", "complete");
67
- assert.deepEqual(await evaluate("() => window.__flags"), { beta: true, name: `q"uote 'x' \\` });
68
- await handle.dispose();
69
- });
70
-
71
- test("a string init script runs as statements", OPTIONS, async () => {
72
- const handle = await page.addInitScript(`window.__a = 1; window.__b = window.__a + 1;`);
73
- await page.navigateTo("about:blank?string", "complete");
74
- assert.equal(await evaluate("() => window.__b"), 2);
75
- await handle.dispose();
76
- });
77
-
78
- test("dispose stops the effect on the next navigation", OPTIONS, async () => {
79
- const handle = await page.addInitScript(() => {
80
- (globalThis as any).__gone = true;
81
- });
82
- await page.navigateTo("about:blank?with", "complete");
83
- assert.equal(await evaluate("() => window.__gone"), true);
84
- await handle.dispose();
85
- await page.navigateTo("about:blank?without", "complete");
86
- assert.equal(await evaluate("() => window.__gone"), undefined);
87
- });
88
-
89
- test("an init script also applies to iframes", OPTIONS, async () => {
90
- const handle = await page.addInitScript(() => {
91
- (globalThis as any).__inFrame = "yes";
92
- });
93
- await page.navigateTo("about:blank?frames", "complete");
94
- const seen = await callFunction<Promise<unknown>>(
95
- connector,
96
- page.contextId,
97
- `async () => {
98
- const frame = document.body.appendChild(document.createElement("iframe"));
99
- frame.src = "about:blank";
100
- await new Promise((resolve) => frame.addEventListener("load", resolve, { once: true }));
101
- return frame.contentWindow.__inFrame;
102
- }`,
103
- );
104
- assert.equal(seen, "yes");
105
- await handle.dispose();
106
- });
107
-
108
- test("a throwing init script does not break registration or the page", OPTIONS, async () => {
109
- const handle = await page.addInitScript(`throw new Error("init script boom");`);
110
- await page.navigateTo("about:blank?boom", "complete");
111
- assert.equal(await evaluate("() => 1 + 1"), 2);
112
- await handle.dispose();
113
- });
114
-
115
- test("the helper realm is registered once and invisible to the page", OPTIONS, async () => {
116
- await page.navigateTo("about:blank?helpers", "complete");
117
- await setContent(page, `<button id="b">Go</button>`);
118
- const button = page.locator("button[@id='b']");
119
- assert.equal(await button.isEnabled(), true);
120
-
121
- // not on the page's own window ...
122
- assert.equal(await evaluate("() => typeof window.__samurai"), "undefined");
123
- // ... only in the sandbox realm
124
- assert.equal(await evaluate("() => typeof globalThis.__samurai", HELPER_SANDBOX), "object");
125
-
126
- const registrations = () =>
127
- sentMethods.filter((method) => method === "script.addPreloadScript").length;
128
- const before = registrations();
129
- // more probes, after navigations: the registration is reused, and the helpers are there in each new document
130
- for (const url of ["about:blank?one", "about:blank?two"]) {
131
- await page.navigateTo(url, "complete");
132
- await setContent(page, `<button id="b">Go</button>`);
133
- assert.equal(await page.locator("button[@id='b']").isEnabled(), true);
134
- }
135
- assert.equal(registrations(), before);
136
- });
137
-
138
- test("page scripts cannot tamper with the helpers", OPTIONS, async () => {
139
- await page.navigateTo("about:blank?tamper", "complete");
140
- await setContent(page, `<button id="b">Go</button>`);
141
- await evaluate("() => { window.__samurai = { probeElement: () => 'hacked' }; }");
142
- const state = await page.locator("button[@id='b']").isEnabled();
143
- assert.equal(state, true);
144
- });
@@ -1,180 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { autoReply, FakeWebSocket } from "../testing/fake-websocket.js";
4
- import { BiDiConnector } from "../transport/bidi-connection.js";
5
- import { addPreload } from "./preload.js";
6
- import { ScriptError } from "./call-function.js";
7
-
8
- const ok = { type: "success", realm: "r", result: { type: "undefined" } };
9
- const frame = (context: string, children: object[] = []) => ({ context, parent: null, children });
10
-
11
- interface Setup {
12
- tree?: (params: any) => object[];
13
- callFunction?: (params: any) => object | Error;
14
- removePreload?: () => object | Error;
15
- }
16
-
17
- function setup({ tree, callFunction, removePreload }: Setup = {}) {
18
- const ws = new FakeWebSocket();
19
- const connector = new BiDiConnector(ws as unknown as WebSocket);
20
- const stop = autoReply(ws, {
21
- "script.addPreloadScript": () => ({ script: "preload-1" }),
22
- "browsingContext.getTree": (params) => ({ contexts: tree?.(params) ?? [] }),
23
- "script.callFunction": callFunction ?? (() => ok),
24
- "script.removePreloadScript": removePreload ?? (() => ({})),
25
- });
26
- const methods = () => ws.sent.map((m) => m.method);
27
- return { ws, connector, stop, methods };
28
- }
29
-
30
- describe("addPreload", () => {
31
- it("registers the script with its contexts and sandbox", async () => {
32
- const { ws, connector, stop } = setup();
33
- const handle = await addPreload(connector, {
34
- source: "() => { globalThis.x = 1 }",
35
- contexts: ["top"],
36
- sandbox: "samurai",
37
- });
38
- assert.equal(handle.id, "preload-1");
39
- assert.deepEqual(ws.sent[0]!.params, {
40
- functionDeclaration: "() => { globalThis.x = 1 }",
41
- contexts: ["top"],
42
- sandbox: "samurai",
43
- });
44
- stop();
45
- });
46
-
47
- it("omits contexts and sandbox when not given", async () => {
48
- const { ws, connector, stop } = setup();
49
- await addPreload(connector, { source: "() => {}" });
50
- assert.deepEqual(ws.sent[0]!.params, { functionDeclaration: "() => {}" });
51
- stop();
52
- });
53
-
54
- it("also runs the source in every loaded context under the given roots, same sandbox", async () => {
55
- const { ws, connector, stop, methods } = setup({
56
- tree: (params) =>
57
- params.root === "top" ? [frame("top", [frame("child", [frame("grandchild")])])] : [],
58
- });
59
- await addPreload(connector, { source: "() => 1", contexts: ["top"], sandbox: "samurai" });
60
- // registered first, so a document created meanwhile is not missed
61
- assert.equal(methods()[0], "script.addPreloadScript");
62
- const getTrees = ws.sent.filter((m) => m.method === "browsingContext.getTree");
63
- assert.deepEqual(getTrees.map((m) => m.params), [{ root: "top" }]);
64
- const calls = ws.sent.filter((m) => m.method === "script.callFunction").map((m) => m.params as any);
65
- assert.deepEqual(calls.map((c) => c.target).sort((a, b) => a.context.localeCompare(b.context)), [
66
- { context: "child", sandbox: "samurai" },
67
- { context: "grandchild", sandbox: "samurai" },
68
- { context: "top", sandbox: "samurai" },
69
- ]);
70
- assert.ok(calls.every((c) => c.functionDeclaration === "() => 1" && c.awaitPromise === false));
71
- stop();
72
- });
73
-
74
- it("without contexts it covers every top-level context", async () => {
75
- const { ws, connector, stop } = setup({ tree: () => [frame("a"), frame("b", [frame("b-frame")])] });
76
- await addPreload(connector, { source: "() => 1" });
77
- assert.deepEqual(ws.sent.find((m) => m.method === "browsingContext.getTree")!.params, {});
78
- const targets = ws.sent
79
- .filter((m) => m.method === "script.callFunction")
80
- .map((m) => (m.params as any).target.context)
81
- .sort();
82
- assert.deepEqual(targets, ["a", "b", "b-frame"]);
83
- stop();
84
- });
85
-
86
- it("ignores a context that disappeared while running", async () => {
87
- const ws = new FakeWebSocket();
88
- const connector = new BiDiConnector(ws as unknown as WebSocket);
89
- const timer = setInterval(() => {
90
- for (const m of ws.sent.splice(0)) {
91
- if (m.method === "script.addPreloadScript") ws.reply(m.id, { script: "p" });
92
- else if (m.method === "browsingContext.getTree") ws.reply(m.id, { contexts: [frame("top")] });
93
- else ws.replyError(m.id, "no such frame", "frame is gone");
94
- }
95
- }, 1).unref();
96
- await assert.doesNotReject(addPreload(connector, { source: "() => 1", contexts: ["top"] }));
97
- clearInterval(timer);
98
- });
99
-
100
- it("throws a ScriptError from the immediate run by default", async () => {
101
- const { connector, stop } = setup({
102
- tree: () => [frame("top")],
103
- callFunction: () => ({
104
- type: "exception",
105
- realm: "r",
106
- exceptionDetails: { text: "ReferenceError: nope", stackTrace: { callFrames: [] } },
107
- }),
108
- });
109
- await assert.rejects(addPreload(connector, { source: "() => nope" }), ScriptError);
110
- stop();
111
- });
112
-
113
- it("removes the registration again when the immediate run throws", async () => {
114
- const { ws, connector, stop, methods } = setup({
115
- tree: () => [frame("top")],
116
- callFunction: () => ({
117
- type: "exception",
118
- realm: "r",
119
- exceptionDetails: { text: "boom", stackTrace: { callFrames: [] } },
120
- }),
121
- });
122
- await assert.rejects(addPreload(connector, { source: "() => 1" }), ScriptError);
123
- assert.ok(methods().includes("script.removePreloadScript"));
124
- assert.deepEqual(
125
- ws.sent.find((m) => m.method === "script.removePreloadScript")!.params,
126
- { script: "preload-1" },
127
- );
128
- stop();
129
- });
130
-
131
- it("with onRunError log the immediate run's exception does not fail the registration", async () => {
132
- const { connector, stop } = setup({
133
- tree: () => [frame("top")],
134
- callFunction: () => ({
135
- type: "exception",
136
- realm: "r",
137
- exceptionDetails: { text: "boom", stackTrace: { callFrames: [] } },
138
- }),
139
- });
140
- const handle = await addPreload(connector, { source: "() => 1", onRunError: "log" });
141
- assert.equal(handle.id, "preload-1");
142
- stop();
143
- });
144
-
145
- it("rethrows when registering fails", async () => {
146
- const ws = new FakeWebSocket();
147
- const connector = new BiDiConnector(ws as unknown as WebSocket);
148
- const timer = setInterval(() => {
149
- for (const m of ws.sent.splice(0)) ws.replyError(m.id, "invalid argument", "bad script");
150
- }, 1).unref();
151
- await assert.rejects(addPreload(connector, { source: "not a function" }), { code: "invalid argument" });
152
- clearInterval(timer);
153
- });
154
-
155
- it("dispose removes the registration once, however often it is called", async () => {
156
- const { ws, connector, stop } = setup();
157
- const handle = await addPreload(connector, { source: "() => 1" });
158
- await handle.dispose();
159
- await handle.dispose();
160
- const removals = ws.sent.filter((m) => m.method === "script.removePreloadScript");
161
- assert.equal(removals.length, 1);
162
- assert.deepEqual(removals[0]!.params, { script: "preload-1" });
163
- stop();
164
- });
165
-
166
- it("dispose swallows no such script", async () => {
167
- const ws = new FakeWebSocket();
168
- const connector = new BiDiConnector(ws as unknown as WebSocket);
169
- const timer = setInterval(() => {
170
- for (const m of ws.sent.splice(0)) {
171
- if (m.method === "script.addPreloadScript") ws.reply(m.id, { script: "p" });
172
- else if (m.method === "browsingContext.getTree") ws.reply(m.id, { contexts: [] });
173
- else ws.replyError(m.id, "no such script", "already removed");
174
- }
175
- }, 1).unref();
176
- const handle = await addPreload(connector, { source: "() => 1" });
177
- await assert.doesNotReject(handle.dispose());
178
- clearInterval(timer);
179
- });
180
- });
@@ -1,102 +0,0 @@
1
- import logger from "../logger/index.js";
2
- import type { BiDiConnector } from "../transport/bidi-connection.js";
3
- import { BiDiError } from "../transport/bidi-error.js";
4
- import type { Info } from "../types/bidi-modules/browsing-context.js";
5
- import { callFunction, ScriptError } from "./call-function.js";
6
-
7
- export interface PreloadHandle {
8
- readonly id: string;
9
- /** Idempotent. Documents that are already loaded keep the effect until they navigate */
10
- dispose(): Promise<void>;
11
- }
12
-
13
- export interface RunOptions {
14
- /** A function declaration, e.g. `() => { … }` */
15
- source: string;
16
- /** Top-level contexts to cover, including their iframes. Default: every top-level context */
17
- contexts?: string[];
18
- /** Named sandbox realm; without it the script runs in the page's own realm */
19
- sandbox?: string;
20
- /**
21
- * What to do when the immediate run in an already-loaded document throws.
22
- * "log" suits user init scripts (the browser reports such errors for later documents itself)
23
- * @default "throw"
24
- */
25
- onRunError?: "throw" | "log";
26
- }
27
-
28
- /**
29
- * Registers `source` to run before any page script in every future document of `contexts`, and
30
- * also runs it once in the documents that are loaded now: preload scripts only affect documents
31
- * created after registration, and callers should not have to care about that ordering.
32
- */
33
- export async function addPreload(
34
- connector: BiDiConnector,
35
- options: RunOptions,
36
- ): Promise<PreloadHandle> {
37
- const { script } = await connector.send("script.addPreloadScript", {
38
- functionDeclaration: options.source,
39
- ...(options.contexts && { contexts: options.contexts }),
40
- ...(options.sandbox !== undefined && { sandbox: options.sandbox }),
41
- });
42
-
43
- let removed = false;
44
- const dispose = async () => {
45
- if (removed) return;
46
- removed = true;
47
- try {
48
- await connector.send("script.removePreloadScript", { script });
49
- } catch (err) {
50
- if (err instanceof BiDiError && err.code === "no such script") return;
51
- throw err;
52
- }
53
- };
54
-
55
- try {
56
- // Registered first, so a document created while this runs is covered by the registration
57
- await runInLoadedContexts(connector, options);
58
- } catch (err) {
59
- await dispose().catch((disposeErr) =>
60
- logger.debug("Could not remove preload script after a failed run", { disposeErr }),
61
- );
62
- throw err;
63
- }
64
- return { id: script, dispose };
65
- }
66
-
67
- /** Runs `source` once in every loaded context (iframes included) under `contexts` */
68
- export async function runInLoadedContexts(
69
- connector: BiDiConnector,
70
- { source, contexts, sandbox, onRunError = "throw" }: RunOptions,
71
- ): Promise<void> {
72
- const trees = contexts
73
- ? await Promise.all(
74
- contexts.map((root) => connector.send("browsingContext.getTree", { root })),
75
- )
76
- : [await connector.send("browsingContext.getTree", {})];
77
- const loaded = trees.flatMap((tree) => tree.contexts.flatMap(flatten));
78
-
79
- const outcomes = await Promise.allSettled(
80
- loaded.map((context) =>
81
- callFunction(connector, context, source, [], {
82
- awaitPromise: false,
83
- ...(sandbox !== undefined && { sandbox }),
84
- }),
85
- ),
86
- );
87
- for (const outcome of outcomes) {
88
- if (outcome.status === "fulfilled") continue;
89
- const err: unknown = outcome.reason;
90
- // The context closed between listing and running: nothing left to cover
91
- if (err instanceof BiDiError && err.code === "no such frame") continue;
92
- if (err instanceof ScriptError && onRunError === "log") {
93
- logger.warn("Script threw while running in a loaded document: %s", err.text);
94
- continue;
95
- }
96
- throw err;
97
- }
98
- }
99
-
100
- function flatten(info: Info): string[] {
101
- return [info.context, ...(info.children ?? []).flatMap(flatten)];
102
- }
@@ -1,182 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { RemoteValue } from "../types/bidi-modules/script.js";
4
- import { ElementHandle } from "./element-handle.js";
5
- import { fromRemoteValue, RemoteObject, toLocalValue } from "./serialize.js";
6
-
7
- describe("toLocalValue", () => {
8
- it("serializes primitives", () => {
9
- assert.deepEqual(toLocalValue(undefined), { type: "undefined" });
10
- assert.deepEqual(toLocalValue(null), { type: "null" });
11
- assert.deepEqual(toLocalValue(true), { type: "boolean", value: true });
12
- assert.deepEqual(toLocalValue("a\"b'c\\d\n"), { type: "string", value: "a\"b'c\\d\n" });
13
- assert.deepEqual(toLocalValue(12.5), { type: "number", value: 12.5 });
14
- assert.deepEqual(toLocalValue(10n), { type: "bigint", value: "10" });
15
- });
16
-
17
- it("serializes the special numbers", () => {
18
- assert.deepEqual(toLocalValue(NaN), { type: "number", value: "NaN" });
19
- assert.deepEqual(toLocalValue(Infinity), { type: "number", value: "Infinity" });
20
- assert.deepEqual(toLocalValue(-Infinity), { type: "number", value: "-Infinity" });
21
- assert.deepEqual(toLocalValue(-0), { type: "number", value: "-0" });
22
- assert.deepEqual(toLocalValue(0), { type: "number", value: 0 });
23
- });
24
-
25
- it("serializes dates and regular expressions", () => {
26
- assert.deepEqual(toLocalValue(new Date("2026-09-29T10:00:00.000Z")), {
27
- type: "date",
28
- value: "2026-09-29T10:00:00.000Z",
29
- });
30
- assert.deepEqual(toLocalValue(/ab+c/gi), { type: "regexp", value: { pattern: "ab+c", flags: "gi" } });
31
- });
32
-
33
- it("serializes arrays, sets, maps and plain objects, nested", () => {
34
- assert.deepEqual(toLocalValue([1, "a"]), {
35
- type: "array",
36
- value: [
37
- { type: "number", value: 1 },
38
- { type: "string", value: "a" },
39
- ],
40
- });
41
- assert.deepEqual(toLocalValue(new Set([1])), { type: "set", value: [{ type: "number", value: 1 }] });
42
- assert.deepEqual(toLocalValue(new Map<unknown, unknown>([["k", true], [2, null]])), {
43
- type: "map",
44
- value: [
45
- ["k", { type: "boolean", value: true }],
46
- [{ type: "number", value: 2 }, { type: "null" }],
47
- ],
48
- });
49
- assert.deepEqual(toLocalValue({ a: { b: [undefined] } }), {
50
- type: "object",
51
- value: [
52
- [
53
- "a",
54
- {
55
- type: "object",
56
- value: [["b", { type: "array", value: [{ type: "undefined" }] }]],
57
- },
58
- ],
59
- ],
60
- });
61
- assert.deepEqual(toLocalValue(Object.create(null)), { type: "object", value: [] });
62
- });
63
-
64
- it("serializes an ElementHandle as a shared reference", () => {
65
- assert.deepEqual(toLocalValue(new ElementHandle("node-1")), { sharedId: "node-1" });
66
- assert.deepEqual(toLocalValue(new ElementHandle("node-1", "h1")), { sharedId: "node-1", handle: "h1" });
67
- });
68
-
69
- it("rejects what cannot cross the wire", () => {
70
- assert.throws(() => toLocalValue(() => 1), /cannot serialize function/);
71
- assert.throws(() => toLocalValue(Symbol("s")), /cannot serialize symbol/);
72
- assert.throws(() => toLocalValue(new (class Foo {})()), /cannot serialize Foo/);
73
- assert.throws(() => toLocalValue(Buffer.from("a")), /cannot serialize Buffer/);
74
- assert.throws(() => toLocalValue({ nested: () => 1 }), /cannot serialize function/);
75
- });
76
-
77
- it("rejects cycles and absurd depth", () => {
78
- const loop: Record<string, unknown> = {};
79
- loop.self = loop;
80
- assert.throws(() => toLocalValue(loop), /circular/);
81
- const list: unknown[] = [];
82
- list.push(list);
83
- assert.throws(() => toLocalValue(list), /circular/);
84
- let deep: unknown = 1;
85
- for (let i = 0; i < 150; i++) deep = [deep];
86
- assert.throws(() => toLocalValue(deep), /too deeply nested/);
87
- });
88
-
89
- it("allows the same object twice when it is not a cycle", () => {
90
- const shared = { a: 1 };
91
- assert.doesNotThrow(() => toLocalValue([shared, shared]));
92
- });
93
- });
94
-
95
- describe("fromRemoteValue", () => {
96
- it("deserializes primitives and special numbers", () => {
97
- assert.equal(fromRemoteValue({ type: "undefined" }), undefined);
98
- assert.equal(fromRemoteValue({ type: "null" }), null);
99
- assert.equal(fromRemoteValue({ type: "string", value: "x" }), "x");
100
- assert.equal(fromRemoteValue({ type: "boolean", value: false }), false);
101
- assert.equal(fromRemoteValue({ type: "number", value: 3 }), 3);
102
- assert.ok(Number.isNaN(fromRemoteValue({ type: "number", value: "NaN" } as never)));
103
- assert.equal(fromRemoteValue({ type: "number", value: "Infinity" } as never), Infinity);
104
- assert.equal(fromRemoteValue({ type: "number", value: "-Infinity" } as never), -Infinity);
105
- assert.ok(Object.is(fromRemoteValue({ type: "number", value: "-0" } as never), -0));
106
- assert.equal(fromRemoteValue({ type: "bigint", value: "12345678901234567890" } as never), 12345678901234567890n);
107
- });
108
-
109
- it("deserializes dates and regular expressions", () => {
110
- const date = fromRemoteValue({ type: "date", value: "2026-09-29T10:00:00.000Z" }) as Date;
111
- assert.equal(date.toISOString(), "2026-09-29T10:00:00.000Z");
112
- const regexp = fromRemoteValue({ type: "regexp", value: { pattern: "a+", flags: "i" } } as never) as RegExp;
113
- assert.equal(regexp.source, "a+");
114
- assert.equal(regexp.flags, "i");
115
- });
116
-
117
- it("deserializes collections, nested", () => {
118
- const value: RemoteValue = {
119
- type: "object",
120
- value: [
121
- ["list", { type: "array", value: [{ type: "number", value: 1 }, { type: "string", value: "a" }] }],
122
- [{ type: "number", value: 5 }, { type: "null" }],
123
- ["set", { type: "set", value: [{ type: "boolean", value: true }] } as never],
124
- ["map", { type: "map", value: [["k", { type: "number", value: 2 }]] } as never],
125
- ],
126
- };
127
- const result = fromRemoteValue(value) as Record<string, unknown>;
128
- assert.deepEqual(result.list, [1, "a"]);
129
- assert.equal(result["5"], null);
130
- assert.deepEqual([...(result.set as Set<unknown>)], [true]);
131
- assert.deepEqual([...(result.map as Map<unknown, unknown>)], [["k", 2]]);
132
- });
133
-
134
- it("turns a node into an ElementHandle", () => {
135
- const handle = fromRemoteValue({ type: "node", sharedId: "n1", handle: "h1", value: {} } as never);
136
- assert.ok(handle instanceof ElementHandle);
137
- assert.equal(handle.sharedId, "n1");
138
- assert.equal(handle.handle, "h1");
139
- });
140
-
141
- it("returns an opaque RemoteObject for values that cannot be copied", () => {
142
- for (const type of ["function", "window", "promise", "symbol", "error", "weakmap"]) {
143
- const value = fromRemoteValue({ type, handle: "h" } as never);
144
- assert.ok(value instanceof RemoteObject, type);
145
- assert.equal((value as RemoteObject).type, type);
146
- assert.equal((value as RemoteObject).handle, "h");
147
- }
148
- });
149
-
150
- it("returns a RemoteObject for an object cut off by the serialization depth", () => {
151
- const value = fromRemoteValue({ type: "object", handle: "h" } as never);
152
- assert.ok(value instanceof RemoteObject);
153
- });
154
-
155
- it("resolves internalId back references, cycles included", () => {
156
- const value = {
157
- type: "object",
158
- internalId: "1",
159
- value: [["self", { type: "object", internalId: "1" }]],
160
- } as RemoteValue;
161
- const result = fromRemoteValue(value) as { self: unknown };
162
- assert.equal(result.self, result);
163
- });
164
-
165
- it("keeps handles when asked, attaching the deserialized value", () => {
166
- const value = { type: "object", handle: "h", value: [["a", { type: "number", value: 1 }]] } as RemoteValue;
167
- const kept = fromRemoteValue(value, { keepHandles: true }) as RemoteObject;
168
- assert.ok(kept instanceof RemoteObject);
169
- assert.equal(kept.handle, "h");
170
- assert.deepEqual(kept.value, { a: 1 });
171
- assert.deepEqual(fromRemoteValue(value), { a: 1 });
172
- });
173
- });
174
-
175
- describe("round trip", () => {
176
- it("survives toLocalValue → fromRemoteValue for plain data", () => {
177
- const data = { a: [1, "two", { three: true }], n: null, d: new Date(0), big: 5n };
178
- // A local value has the same shape a remote value has for these types, except `value` keys
179
- const remote = toLocalValue(data) as unknown as RemoteValue;
180
- assert.deepEqual(fromRemoteValue(remote), data);
181
- });
182
- });