@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,112 +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 { BiDiError } from "../transport/bidi-error.js";
8
- import { callFunction, disown, ScriptError } from "./call-function.js";
9
- import { ElementHandle } from "./element-handle.js";
10
- import { RemoteObject } from "./serialize.js";
11
-
12
- let browser: Browser;
13
- let page: Page;
14
- let connector: BiDiConnector;
15
-
16
- before(async () => {
17
- ({ browser, page } = await Browser.launch("firefox", { port: 9233, headless: true }));
18
- connector = (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
19
- await page.navigateTo("about:blank", "complete");
20
- });
21
-
22
- after(async () => {
23
- await browser?.close();
24
- });
25
-
26
- const OPTIONS = { timeout: 15000 };
27
- const call = <T>(fn: string | ((...args: any[]) => unknown), args: unknown[] = [], options = {}) =>
28
- callFunction<T>(connector, page.contextId, fn, args, options);
29
-
30
- test("plain values, Map, Set, Date and RegExp round trip", OPTIONS, async () => {
31
- const input = {
32
- text: `quote " apostrophe ' backslash \\ newline \n`,
33
- numbers: [1, -0, NaN, Infinity],
34
- map: new Map<unknown, unknown>([["k", 1]]),
35
- set: new Set([1, 2]),
36
- date: new Date("2026-09-29T10:00:00.000Z"),
37
- pattern: /a+b/gi,
38
- big: 12345678901234567890n,
39
- nothing: null,
40
- missing: undefined,
41
- };
42
- const output = await call<typeof input>("(value) => value", [input]);
43
- assert.equal(output.text, input.text);
44
- assert.ok(Object.is(output.numbers[1], -0));
45
- assert.ok(Number.isNaN(output.numbers[2]));
46
- assert.equal(output.numbers[3], Infinity);
47
- assert.deepEqual([...output.map], [["k", 1]]);
48
- assert.deepEqual([...output.set], [1, 2]);
49
- assert.equal(output.date.toISOString(), input.date.toISOString());
50
- assert.equal(output.pattern.source, "a+b");
51
- assert.equal(output.pattern.flags, "gi");
52
- assert.equal(output.big, input.big);
53
- assert.equal(output.nothing, null);
54
- assert.equal(output.missing, undefined);
55
- });
56
-
57
- test("an xpath with every quote style and a newline is located without escaping", OPTIONS, async () => {
58
- await setContent(page, `<a id="x">Don't "stop"</a>`);
59
- const xpath = `//a[@id="x" and contains(., "Don't")]\n`;
60
- const link = page.locator(xpath);
61
- assert.equal(await link.count(), 1);
62
- assert.equal(await link.textContent(), `Don't "stop"`);
63
- });
64
-
65
- test("awaitPromise resolves async functions", OPTIONS, async () => {
66
- assert.equal(await call("async () => { await new Promise((r) => setTimeout(r, 50)); return 7; }"), 7);
67
- });
68
-
69
- test("a function object is sent as its source", OPTIONS, async () => {
70
- assert.equal(await call((a: number, b: number) => a + b, [2, 3]), 5);
71
- });
72
-
73
- test("a throwing function becomes a ScriptError", OPTIONS, async () => {
74
- await assert.rejects(call(`() => { null.x; }`), (err: unknown) => {
75
- assert.ok(err instanceof ScriptError);
76
- assert.match(err.text, /TypeError/);
77
- return true;
78
- });
79
- });
80
-
81
- test("nodes come back as ElementHandles and can be passed in again", OPTIONS, async () => {
82
- await setContent(page, `<p id="p">hello</p>`);
83
- const handle = await call<ElementHandle>(`() => document.getElementById("p")`);
84
- assert.ok(handle instanceof ElementHandle);
85
- assert.equal(await call<string>(`(el) => el.textContent`, [handle]), "hello");
86
- });
87
-
88
- test("a removed node stays usable, a node from a previous document fails with no such node", OPTIONS, async () => {
89
- await setContent(page, `<p id="p">hello</p>`);
90
- const handle = await call<ElementHandle>(`() => document.getElementById("p")`);
91
- await call(`() => document.getElementById("p").remove()`);
92
- // Detached but alive: the browser still resolves the reference
93
- assert.equal(await call<string>(`(el) => el.textContent`, [handle]), "hello");
94
- await page.navigateTo("about:blank?next", "complete");
95
- await assert.rejects(
96
- call(`(el) => el.textContent`, [handle]),
97
- (err: unknown) => err instanceof BiDiError && err.code === "no such node",
98
- );
99
- });
100
-
101
- test("ownership root keeps a handle that disown releases, twice without error", OPTIONS, async () => {
102
- const kept = await call<RemoteObject>(`() => ({ a: 1 })`, [], { ownership: "root" });
103
- assert.ok(kept instanceof RemoteObject);
104
- assert.ok(kept.handle);
105
- assert.deepEqual(kept.value, { a: 1 });
106
- await disown(connector, page.contextId, [kept.handle!]);
107
- await assert.doesNotReject(disown(connector, page.contextId, [kept.handle!]));
108
- });
109
-
110
- test("unserializable arguments fail before anything is sent", OPTIONS, async () => {
111
- await assert.rejects(call(`(f) => f`, [() => 1]), TypeError);
112
- });
@@ -1,174 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { FakeWebSocket, tick } from "../testing/fake-websocket.js";
4
- import { BiDiConnector } from "../transport/bidi-connection.js";
5
- import { callFunction, disown, ScriptError, ScriptSerializationError } from "./call-function.js";
6
- import { ElementHandle } from "./element-handle.js";
7
- import { RemoteObject } from "./serialize.js";
8
-
9
- function setup(reply: (params: any) => object | Error) {
10
- const ws = new FakeWebSocket();
11
- const connector = new BiDiConnector(ws as unknown as WebSocket);
12
- let answered = 0;
13
- const pump = setInterval(() => {
14
- while (answered < ws.sent.length) {
15
- const { id, method, params } = ws.sent[answered++]!;
16
- const result = method === "script.callFunction" || method === "script.disown" ? reply(params) : {};
17
- if (result instanceof Error) ws.replyError(id, "no such handle", result.message);
18
- else ws.reply(id, result);
19
- }
20
- }, 1);
21
- return { ws, connector, stop: () => clearInterval(pump) };
22
- }
23
-
24
- const success = (result: object) => ({ type: "success", realm: "r", result });
25
-
26
- describe("callFunction", () => {
27
- it("sends the declaration, serialized arguments, target and defaults", async () => {
28
- const { ws, connector, stop } = setup(() => success({ type: "number", value: 3 }));
29
- const result = await callFunction(connector, "ctx", "(a, b) => a + b", [1, "x"]);
30
- assert.equal(result, 3);
31
- assert.deepEqual(ws.sent[0]!.params, {
32
- functionDeclaration: "(a, b) => a + b",
33
- awaitPromise: true,
34
- target: { context: "ctx" },
35
- arguments: [
36
- { type: "number", value: 1 },
37
- { type: "string", value: "x" },
38
- ],
39
- resultOwnership: "none",
40
- });
41
- stop();
42
- });
43
-
44
- it("omits arguments when there are none and passes options through", async () => {
45
- const { ws, connector, stop } = setup(() => success({ type: "undefined" }));
46
- await callFunction(connector, "ctx", "function() { return this; }", [], {
47
- awaitPromise: false,
48
- sandbox: "samurai",
49
- ownership: "root",
50
- thisArg: { a: 1 },
51
- });
52
- assert.deepEqual(ws.sent[0]!.params, {
53
- functionDeclaration: "function() { return this; }",
54
- awaitPromise: false,
55
- target: { context: "ctx", sandbox: "samurai" },
56
- resultOwnership: "root",
57
- this: { type: "object", value: [["a", { type: "number", value: 1 }]] },
58
- });
59
- stop();
60
- });
61
-
62
- it("serializes an ElementHandle argument as a shared reference", async () => {
63
- const { ws, connector, stop } = setup(() => success({ type: "null" }));
64
- await callFunction(connector, "ctx", "(el) => el.id", [new ElementHandle("n1")]);
65
- assert.deepEqual((ws.sent[0]!.params as any).arguments, [{ sharedId: "n1" }]);
66
- stop();
67
- });
68
-
69
- it("accepts a function and sends its source", async () => {
70
- const { ws, connector, stop } = setup(() => success({ type: "number", value: 2 }));
71
- await callFunction(connector, "ctx", (n: number) => n * 2, [1]);
72
- assert.match((ws.sent[0]!.params as any).functionDeclaration, /n\s*\*\s*2/);
73
- stop();
74
- });
75
-
76
- it("rejects a function whose source depends on the transpiler's __name helper", async () => {
77
- const { connector, stop } = setup(() => success({ type: "null" }));
78
- const named = () => {
79
- function inner() {
80
- return 1;
81
- }
82
- return inner();
83
- };
84
- // Under tsx (keepNames) `inner` is wrapped in __name(); the guard turns that into a clear error
85
- if (named.toString().includes("__name(")) {
86
- await assert.rejects(callFunction(connector, "ctx", named), ScriptSerializationError);
87
- }
88
- const guarded = "(x) => { __name(x, 'x'); }";
89
- // A string is used as written, the guard is only for function objects
90
- await assert.doesNotReject(callFunction(connector, "ctx", guarded));
91
- const fake = Object.assign(() => 1, { toString: () => "() => { __name(a, 'a'); }" });
92
- await assert.rejects(callFunction(connector, "ctx", fake), ScriptSerializationError);
93
- stop();
94
- });
95
-
96
- it("fails before sending when an argument cannot be serialized", async () => {
97
- const { ws, connector, stop } = setup(() => success({ type: "null" }));
98
- await assert.rejects(callFunction(connector, "ctx", "(f) => f", [() => 1]), TypeError);
99
- assert.equal(ws.sent.length, 0);
100
- stop();
101
- });
102
-
103
- it("maps an exception result to ScriptError", async () => {
104
- const { connector, stop } = setup(() => ({
105
- type: "exception",
106
- realm: "r",
107
- exceptionDetails: {
108
- text: "TypeError: x is null",
109
- lineNumber: 1,
110
- columnNumber: 2,
111
- exception: { type: "error" },
112
- stackTrace: { callFrames: [] },
113
- },
114
- }));
115
- const longSource = "() => {" + " ".repeat(300) + "}";
116
- await assert.rejects(callFunction(connector, "ctx", longSource), (err: unknown) => {
117
- assert.ok(err instanceof ScriptError);
118
- assert.equal(err.text, "TypeError: x is null");
119
- assert.deepEqual(err.stackTrace, { callFrames: [] });
120
- assert.ok(err.functionSource.length <= 201);
121
- assert.match(err.message, /TypeError: x is null/);
122
- return true;
123
- });
124
- stop();
125
- });
126
-
127
- it("returns nodes as ElementHandles", async () => {
128
- const { connector, stop } = setup(() => success({ type: "node", sharedId: "n9", value: {} }));
129
- const result = await callFunction<ElementHandle>(connector, "ctx", "() => document.body");
130
- assert.ok(result instanceof ElementHandle);
131
- assert.equal(result.sharedId, "n9");
132
- stop();
133
- });
134
-
135
- it("keeps handles for ownership root", async () => {
136
- const { connector, stop } = setup(() =>
137
- success({ type: "object", handle: "h1", value: [["a", { type: "number", value: 1 }]] }),
138
- );
139
- const result = await callFunction<RemoteObject>(connector, "ctx", "() => ({a: 1})", [], { ownership: "root" });
140
- assert.ok(result instanceof RemoteObject);
141
- assert.equal(result.handle, "h1");
142
- assert.deepEqual(result.value, { a: 1 });
143
- stop();
144
- });
145
-
146
- it("does not keep handles for ownership none", async () => {
147
- const { connector, stop } = setup(() => success({ type: "object", value: [["a", { type: "number", value: 1 }]] }));
148
- assert.deepEqual(await callFunction(connector, "ctx", "() => ({a: 1})"), { a: 1 });
149
- stop();
150
- });
151
- });
152
-
153
- describe("disown", () => {
154
- it("sends the handles for the context", async () => {
155
- const { ws, connector, stop } = setup(() => ({}));
156
- await disown(connector, "ctx", ["h1", "h2"]);
157
- assert.deepEqual(ws.sent[0]!.params, { handles: ["h1", "h2"], target: { context: "ctx" } });
158
- stop();
159
- });
160
-
161
- it("sends nothing for no handles", async () => {
162
- const { ws, connector, stop } = setup(() => ({}));
163
- await disown(connector, "ctx", []);
164
- await tick(5);
165
- assert.equal(ws.sent.length, 0);
166
- stop();
167
- });
168
-
169
- it("swallows no such handle", async () => {
170
- const { connector, stop } = setup(() => new Error("gone"));
171
- await assert.doesNotReject(disown(connector, "ctx", ["h1"]));
172
- stop();
173
- });
174
- });
@@ -1,113 +0,0 @@
1
- import type { BiDiConnector } from "../transport/bidi-connection.js";
2
- import { BiDiError } from "../transport/bidi-error.js";
3
- import type { StackTrace } from "../types/bidi-modules/script.js";
4
- import { fromRemoteValue, toLocalValue, type Arg } from "./serialize.js";
5
-
6
- /** How much of a function's source `ScriptError` keeps */
7
- const SOURCE_PREVIEW = 200;
8
-
9
- export interface CallOptions {
10
- /** @default true. The auto-wait probes pass false */
11
- awaitPromise?: boolean;
12
- /**
13
- * "root" keeps a handle alive in the browser; the result then comes back as a `RemoteObject`
14
- * carrying it, and the caller must `disown` it. @default "none"
15
- */
16
- ownership?: "none" | "root";
17
- /** Named sandbox realm to run in (see the preload-scripts spec) */
18
- sandbox?: string;
19
- thisArg?: Arg;
20
- }
21
-
22
- /** The page function threw */
23
- export class ScriptError extends Error {
24
- /** The browser's description, e.g. "TypeError: el is null" */
25
- public readonly text: string;
26
- public readonly stackTrace: StackTrace | undefined;
27
- /** Start of the function that threw */
28
- public readonly functionSource: string;
29
-
30
- constructor(text: string, functionSource: string, stackTrace?: StackTrace) {
31
- super(`Script threw: ${text}`);
32
- this.name = "ScriptError";
33
- this.text = text;
34
- this.stackTrace = stackTrace;
35
- this.functionSource =
36
- functionSource.length > SOURCE_PREVIEW
37
- ? functionSource.slice(0, SOURCE_PREVIEW) + "…"
38
- : functionSource;
39
- }
40
- }
41
-
42
- /** A function cannot be sent as source */
43
- export class ScriptSerializationError extends Error {
44
- constructor(message: string) {
45
- super(message);
46
- this.name = "ScriptSerializationError";
47
- }
48
- }
49
-
50
- /** The source to send for `fn`; a function object must not depend on transpiler helpers */
51
- export function toFunctionDeclaration(fn: string | ((...args: any[]) => unknown)): string {
52
- if (typeof fn === "string") return fn;
53
- const source = fn.toString();
54
- // tsx/esbuild wrap named functions in a __name() helper that does not exist in the page
55
- if (source.includes("__name(")) {
56
- throw new ScriptSerializationError(
57
- "The function's source uses the transpiler's __name helper, which does not exist in the page. " +
58
- "Use an arrow function without inner named functions, or pass the function as a string.",
59
- );
60
- }
61
- return source;
62
- }
63
-
64
- /** Calls `fn` in the page with serialized arguments and returns its deserialized result */
65
- export async function callFunction<T = unknown>(
66
- connector: BiDiConnector,
67
- context: string,
68
- fn: string | ((...args: any[]) => unknown),
69
- args: Arg[] = [],
70
- options: CallOptions = {},
71
- ): Promise<T> {
72
- const functionDeclaration = toFunctionDeclaration(fn);
73
- const ownership = options.ownership ?? "none";
74
- // Serialize first: a bad argument must fail before anything is sent
75
- const serialized = args.map(toLocalValue);
76
- const thisValue = "thisArg" in options ? toLocalValue(options.thisArg) : undefined;
77
-
78
- const result = await connector.send("script.callFunction", {
79
- functionDeclaration,
80
- awaitPromise: options.awaitPromise ?? true,
81
- target:
82
- options.sandbox === undefined
83
- ? { context }
84
- : { context, sandbox: options.sandbox },
85
- ...(serialized.length > 0 && { arguments: serialized }),
86
- resultOwnership: ownership,
87
- ...(thisValue !== undefined && { this: thisValue }),
88
- });
89
-
90
- if (result.type === "exception") {
91
- throw new ScriptError(
92
- result.exceptionDetails.text,
93
- functionDeclaration,
94
- result.exceptionDetails.stackTrace,
95
- );
96
- }
97
- return fromRemoteValue(result.result, { keepHandles: ownership === "root" }) as T;
98
- }
99
-
100
- /** Releases handles obtained with `ownership: "root"`; a handle that is already gone is fine */
101
- export async function disown(
102
- connector: BiDiConnector,
103
- context: string,
104
- handles: string[],
105
- ): Promise<void> {
106
- if (handles.length === 0) return;
107
- try {
108
- await connector.send("script.disown", { handles, target: { context } });
109
- } catch (err) {
110
- if (err instanceof BiDiError && err.code === "no such handle") return;
111
- throw err;
112
- }
113
- }
@@ -1,116 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
4
- import { BiDiConnector } from "../transport/bidi-connection.js";
5
- import {
6
- CALL_PROBE,
7
- HELPER_SANDBOX,
8
- HELPERS_MISSING,
9
- HELPERS_SOURCE,
10
- HelperRealm,
11
- } from "./helpers.js";
12
-
13
- const ok = { type: "success", realm: "r", result: { type: "undefined" } };
14
-
15
- function setup(overrides: Record<string, (params: any) => object | Error> = {}) {
16
- const ws = new FakeWebSocket();
17
- const connector = new BiDiConnector(ws as unknown as WebSocket);
18
- const stop = autoReply(ws, {
19
- "script.addPreloadScript": () => ({ script: "helpers-1" }),
20
- "browsingContext.getTree": () => ({ contexts: [{ context: "top", parent: null, children: [] }] }),
21
- "script.callFunction": () => ok,
22
- ...overrides,
23
- });
24
- const count = (method: string) => ws.sent.filter((m) => m.method === method).length;
25
- return { ws, connector, stop, count, realm: new HelperRealm(connector, "top") };
26
- }
27
-
28
- describe("helper source", () => {
29
- it("compiles and installs probeElement on the global", () => {
30
- const fakeGlobal: Record<string, any> = {};
31
- new Function("globalThis", `(${HELPERS_SOURCE})()`)(fakeGlobal);
32
- assert.equal(typeof fakeGlobal.__samurai.probeElement, "function");
33
- });
34
-
35
- it("the call wrapper forwards to probeElement, or answers with the missing marker", () => {
36
- const wrapper = (globalScope: object) =>
37
- new Function("globalThis", `return (${CALL_PROBE})`)(globalScope) as (...args: unknown[]) => unknown;
38
- const probeElement = (xpath: string, options: unknown) => ({ xpath, options });
39
- assert.deepEqual(wrapper({ __samurai: { probeElement } })("//a", { scroll: true }), {
40
- xpath: "//a",
41
- options: { scroll: true },
42
- });
43
- assert.equal(wrapper({})("//a", {}), HELPERS_MISSING);
44
- });
45
-
46
- it("uses the samurai sandbox", () => {
47
- assert.equal(HELPER_SANDBOX, "samurai");
48
- });
49
- });
50
-
51
- describe("HelperRealm", () => {
52
- it("registers once for the page context and runs in the sandbox", async () => {
53
- const { ws, realm, stop } = setup();
54
- await realm.ensureInstalled();
55
- assert.deepEqual(ws.sent[0]!.params, {
56
- functionDeclaration: HELPERS_SOURCE,
57
- contexts: ["top"],
58
- sandbox: "samurai",
59
- });
60
- stop();
61
- });
62
-
63
- it("concurrent and repeated ensureInstalled share one registration", async () => {
64
- const { realm, count, stop } = setup();
65
- await Promise.all([realm.ensureInstalled(), realm.ensureInstalled()]);
66
- await realm.ensureInstalled();
67
- assert.equal(count("script.addPreloadScript"), 1);
68
- stop();
69
- });
70
-
71
- it("a failed install can be retried", async () => {
72
- let attempts = 0;
73
- const { realm, count, stop } = setup({
74
- "script.addPreloadScript": () => (++attempts === 1 ? new Error("first fails") : { script: "helpers-1" }),
75
- });
76
- await assert.rejects(realm.ensureInstalled());
77
- await realm.ensureInstalled();
78
- assert.equal(count("script.addPreloadScript"), 2);
79
- stop();
80
- });
81
-
82
- it("reinstall runs the helpers in the current contexts without registering again", async () => {
83
- const { ws, realm, count, stop } = setup();
84
- await realm.ensureInstalled();
85
- const before = count("script.addPreloadScript");
86
- await realm.reinstall();
87
- assert.equal(count("script.addPreloadScript"), before);
88
- const last = ws.sent.filter((m) => m.method === "script.callFunction").pop()!.params as any;
89
- assert.equal(last.functionDeclaration, HELPERS_SOURCE);
90
- assert.deepEqual(last.target, { context: "top", sandbox: "samurai" });
91
- stop();
92
- });
93
-
94
- it("reinstall before the first install installs first", async () => {
95
- const { realm, count, stop } = setup();
96
- await realm.reinstall();
97
- assert.equal(count("script.addPreloadScript"), 1);
98
- stop();
99
- });
100
-
101
- it("dispose removes the registration", async () => {
102
- const { realm, count, stop } = setup({ "script.removePreloadScript": () => ({}) });
103
- await realm.ensureInstalled();
104
- await realm.dispose();
105
- await tick(5);
106
- assert.equal(count("script.removePreloadScript"), 1);
107
- stop();
108
- });
109
-
110
- it("dispose without an install sends nothing", async () => {
111
- const { realm, count, stop } = setup();
112
- await realm.dispose();
113
- assert.equal(count("script.removePreloadScript"), 0);
114
- stop();
115
- });
116
- });
@@ -1,68 +0,0 @@
1
- import type { BiDiConnector } from "../transport/bidi-connection.js";
2
- import { PROBE_ELEMENT } from "../locator/element-state.js";
3
- import { addPreload, runInLoadedContexts, type PreloadHandle } from "./preload.js";
4
-
5
- /** Sandbox realm holding the framework's helpers: same DOM as the page, separate JS globals */
6
- export const HELPER_SANDBOX = "samurai";
7
-
8
- /** What `CALL_PROBE` answers with when the helpers are not installed in the document */
9
- export const HELPERS_MISSING = "samurai:helpers-missing";
10
-
11
- /** Installs `globalThis.__samurai` in the sandbox realm of a document */
12
- export const HELPERS_SOURCE = `() => {
13
- globalThis.__samurai = { probeElement: ${PROBE_ELEMENT} };
14
- }`;
15
-
16
- /** Calls the installed probe, or reports that the helpers are missing instead of throwing */
17
- export const CALL_PROBE = `(...args) => {
18
- const helpers = globalThis.__samurai;
19
- return helpers ? helpers.probeElement(...args) : ${JSON.stringify(HELPERS_MISSING)};
20
- }`;
21
-
22
- /** What the locator needs from the helper realm */
23
- export interface HelperInstaller {
24
- /** Resolves once the helpers are registered for the page; installs on first use */
25
- ensureInstalled(): Promise<void>;
26
- /** Runs the helpers again in the loaded documents; used when a call found them missing */
27
- reinstall(): Promise<void>;
28
- }
29
-
30
- /** The framework's helper library for one page: registered once, applied to every document */
31
- export class HelperRealm implements HelperInstaller {
32
- private installed: Promise<PreloadHandle> | undefined;
33
-
34
- constructor(
35
- private connector: BiDiConnector,
36
- private contextId: string,
37
- ) {}
38
-
39
- public ensureInstalled(): Promise<void> {
40
- this.installed ??= addPreload(this.connector, this.options()).catch((err) => {
41
- this.installed = undefined;
42
- throw err;
43
- });
44
- return this.installed.then(() => undefined);
45
- }
46
-
47
- public async reinstall(): Promise<void> {
48
- if (!this.installed) return this.ensureInstalled();
49
- await this.installed;
50
- await runInLoadedContexts(this.connector, this.options());
51
- }
52
-
53
- /** Removes the registration; nothing is sent when the helpers were never installed */
54
- public async dispose(): Promise<void> {
55
- const installed = this.installed;
56
- this.installed = undefined;
57
- if (!installed) return;
58
- await (await installed).dispose();
59
- }
60
-
61
- private options() {
62
- return {
63
- source: HELPERS_SOURCE,
64
- contexts: [this.contextId],
65
- sandbox: HELPER_SANDBOX,
66
- };
67
- }
68
- }