@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
@@ -0,0 +1,205 @@
1
+ import assert from "assert";
2
+ import { glob } from "node:fs/promises";
3
+ import path from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { Browser } from "../browser/browser.js";
6
+ import { projectDir, readConfig } from "../config/config.js";
7
+ import logger from "../logger/index.js";
8
+ import { assertModuleProject } from "./module-type.js";
9
+ import { clearRegistry, registeredTests } from "./registry.js";
10
+ import { createEnvFixture } from "../config/variables.js";
11
+ import { createSecretsFixture } from "../config/secrets.js";
12
+ import TestReporter, {} from "./reporter.js";
13
+ import Page from "../browser/page.js";
14
+ import { takePendingAssertions } from "../assert/expect.js";
15
+ import { toTestError } from "./test-error.js";
16
+ import { judgePageLogs } from "./page-logs-report.js";
17
+ /** Whether a test with this full name is selected by the options */
18
+ export function isSelected(name, { grep, testNames }) {
19
+ if (testNames && !testNames.includes(name))
20
+ return false;
21
+ if (grep === undefined)
22
+ return true;
23
+ if (typeof grep === "string")
24
+ return name.includes(grep);
25
+ grep.lastIndex = 0;
26
+ return grep.test(name);
27
+ }
28
+ export default class TestRunner {
29
+ constructor(testFiles, run, group, options = {}) {
30
+ this.testFiles = testFiles;
31
+ this.run = run;
32
+ this.group = group;
33
+ this.options = options;
34
+ }
35
+ static async init(run, group, options = {}) {
36
+ logger.verbose("Initializing test runner");
37
+ let srcDir = "";
38
+ if (group && group.src) {
39
+ srcDir = group.src;
40
+ }
41
+ else {
42
+ srcDir = (await readConfig("srcDir")) ?? "./src";
43
+ }
44
+ assert(srcDir, "No srcDir declared!");
45
+ const cwd = path.resolve(projectDir(), srcDir);
46
+ if (options.files) {
47
+ return new TestRunner(options.files.map((file) => path.resolve(projectDir(), file)), run, group, options);
48
+ }
49
+ logger.verbose("Reading test files from: %s", cwd);
50
+ const testFiles = [];
51
+ for await (const entry of glob("**/*.spec.ts", {
52
+ cwd,
53
+ })) {
54
+ testFiles.push(entry);
55
+ }
56
+ logger.debug("Found %d tests in srcDir", testFiles.length);
57
+ return new TestRunner(testFiles.map((file) => path.join(cwd, file)), run, group, options);
58
+ }
59
+ /** Runs the selected tests and returns the summary that was also written to the report */
60
+ /**
61
+ * Imports the spec files and returns the tests the options select. A fresh registry and a fresh import of each
62
+ * file, so a second run in this process registers its tests again (and sees edited specs, for files loaded as
63
+ * ES modules; CommonJS files stay cached until the process ends)
64
+ */
65
+ async register() {
66
+ clearRegistry();
67
+ const stamp = `?run=${Date.now().toString(36)}`;
68
+ for (const file of this.testFiles)
69
+ assertModuleProject(file);
70
+ for (const file of this.testFiles) {
71
+ logger.verbose("Trying to register file: %s", file);
72
+ await import(pathToFileURL(file).href + stamp);
73
+ }
74
+ // TODO: Implement test grouping
75
+ return registeredTests().filter((test) => isSelected(test.name, this.options));
76
+ }
77
+ /** Runs the selected tests and returns the summary that was also written to the report */
78
+ async start() {
79
+ const { reportPath, onEvent, signal } = this.options;
80
+ const reporter = new TestReporter({
81
+ environment: this.run.settings.environment,
82
+ ...(reportPath !== undefined && { output: reportPath }),
83
+ ...(onEvent && { onEvent }),
84
+ });
85
+ const selected = await this.register();
86
+ reporter.onStart(selected.length);
87
+ for (const test of selected) {
88
+ if (signal?.aborted) {
89
+ reporter.onTestStart(test);
90
+ reporter.onTestEnd(test, { message: "Run aborted", type: "error" });
91
+ continue;
92
+ }
93
+ await this.executeTestCase(test, reporter);
94
+ }
95
+ return reporter.onEnd();
96
+ }
97
+ async executeTestCase(test, reporter) {
98
+ logger.verbose("Starting test: %s", test.name);
99
+ reporter.onTestStart(test);
100
+ const selectedBrowser = (await readConfig("browser")) ?? "firefox";
101
+ const { settings, secrets } = this.run;
102
+ const timeout = settings.timeout;
103
+ let browser;
104
+ let page;
105
+ let timeoutTimer;
106
+ let onRunAborted;
107
+ const timeoutController = new AbortController();
108
+ await Promise.race([
109
+ new Promise((resolve) => {
110
+ timeoutTimer = setTimeout(() => {
111
+ reporter.onTestEnd(test, {
112
+ message: `Test timed out after ${timeout / 1000} seconds`,
113
+ type: "timeout",
114
+ });
115
+ timeoutController.abort(new Error("Test timed out"));
116
+ resolve();
117
+ }, timeout);
118
+ onRunAborted = () => {
119
+ if (timeoutController.signal.aborted)
120
+ return;
121
+ reporter.onTestEnd(test, { message: "Run aborted", type: "error" });
122
+ timeoutController.abort(new Error("Run aborted"));
123
+ resolve();
124
+ };
125
+ this.options.signal?.addEventListener("abort", onRunAborted, {
126
+ once: true,
127
+ });
128
+ }),
129
+ new Promise(async (resolve, reject) => {
130
+ try {
131
+ const launched = await Browser.launch(selectedBrowser, {
132
+ port: this.options.port ?? 9223,
133
+ headless: this.options.headless ?? false,
134
+ }, timeoutController.signal);
135
+ browser = launched.browser;
136
+ page = launched.page;
137
+ const fixtures = {
138
+ page: launched.page,
139
+ browser: launched.browser,
140
+ env: createEnvFixture(settings.environment, settings.variables),
141
+ secrets: createSecretsFixture(settings.environment, secrets),
142
+ };
143
+ await test.function(fixtures);
144
+ // Already reported as timed out
145
+ if (timeoutController.signal.aborted)
146
+ return resolve();
147
+ const unawaited = takePendingAssertions();
148
+ if (unawaited.length > 0) {
149
+ reporter.onTestEnd(test, {
150
+ message: unawaited
151
+ .map((matcher) => `expect(locator).${matcher}() was not awaited`)
152
+ .join("\n"),
153
+ type: "assertion",
154
+ }, await judgeLogs(page, true));
155
+ return resolve();
156
+ }
157
+ const verdict = await judgeLogs(page, false);
158
+ reporter.onTestEnd(test, verdict.error, verdict);
159
+ resolve();
160
+ }
161
+ catch (err) {
162
+ if (timeoutController.signal.aborted)
163
+ return resolve();
164
+ reporter.onTestEnd(test, toTestError(err), await judgeLogs(page, true));
165
+ resolve();
166
+ }
167
+ }),
168
+ ]);
169
+ clearTimeout(timeoutTimer);
170
+ if (onRunAborted)
171
+ this.options.signal?.removeEventListener("abort", onRunAborted);
172
+ takePendingAssertions();
173
+ logger.verbose("Closing browser for test: %s", test.name);
174
+ await browser?.close();
175
+ }
176
+ }
177
+ /** Reads the page's log once the test body is done and applies config `logs` (see judgePageLogs) */
178
+ async function judgeLogs(page, testFailed) {
179
+ if (!page)
180
+ return {};
181
+ try {
182
+ // Events and command replies share one ordered connection: after this, everything logged so far is in
183
+ await page.syncLogs();
184
+ }
185
+ catch (err) {
186
+ logger.debug("Could not sync the page's logs", { err });
187
+ }
188
+ let config;
189
+ try {
190
+ config = await readConfig("logs");
191
+ }
192
+ catch (err) {
193
+ logger.debug("Could not read logs config, using defaults", { err });
194
+ }
195
+ return judgePageLogs({
196
+ config,
197
+ entries: page.getLogs(),
198
+ errors: page.pageErrors(),
199
+ dropped: page.logsDropped,
200
+ testFailed,
201
+ allowPageErrors: page.pageErrorsAllowed,
202
+ routeErrors: page.routeErrors(),
203
+ });
204
+ }
205
+ export { describe, test } from "./registry.js";
@@ -0,0 +1,34 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import type { StackTrace } from "../types/bidi-modules/script.js";
3
+ import { type Arg } from "./serialize.js";
4
+ export interface CallOptions {
5
+ /** @default true. The auto-wait probes pass false */
6
+ awaitPromise?: boolean;
7
+ /**
8
+ * "root" keeps a handle alive in the browser; the result then comes back as a `RemoteObject`
9
+ * carrying it, and the caller must `disown` it. @default "none"
10
+ */
11
+ ownership?: "none" | "root";
12
+ /** Named sandbox realm to run in (see the preload-scripts spec) */
13
+ sandbox?: string;
14
+ thisArg?: Arg;
15
+ }
16
+ /** The page function threw */
17
+ export declare class ScriptError extends Error {
18
+ /** The browser's description, e.g. "TypeError: el is null" */
19
+ readonly text: string;
20
+ readonly stackTrace: StackTrace | undefined;
21
+ /** Start of the function that threw */
22
+ readonly functionSource: string;
23
+ constructor(text: string, functionSource: string, stackTrace?: StackTrace);
24
+ }
25
+ /** A function cannot be sent as source */
26
+ export declare class ScriptSerializationError extends Error {
27
+ constructor(message: string);
28
+ }
29
+ /** The source to send for `fn`; a function object must not depend on transpiler helpers */
30
+ export declare function toFunctionDeclaration(fn: string | ((...args: any[]) => unknown)): string;
31
+ /** Calls `fn` in the page with serialized arguments and returns its deserialized result */
32
+ export declare function callFunction<T = unknown>(connector: BiDiConnector, context: string, fn: string | ((...args: any[]) => unknown), args?: Arg[], options?: CallOptions): Promise<T>;
33
+ /** Releases handles obtained with `ownership: "root"`; a handle that is already gone is fine */
34
+ export declare function disown(connector: BiDiConnector, context: string, handles: string[]): Promise<void>;
@@ -0,0 +1,71 @@
1
+ import { BiDiError } from "../transport/bidi-error.js";
2
+ import { fromRemoteValue, toLocalValue } from "./serialize.js";
3
+ /** How much of a function's source `ScriptError` keeps */
4
+ const SOURCE_PREVIEW = 200;
5
+ /** The page function threw */
6
+ export class ScriptError extends Error {
7
+ constructor(text, functionSource, stackTrace) {
8
+ super(`Script threw: ${text}`);
9
+ this.name = "ScriptError";
10
+ this.text = text;
11
+ this.stackTrace = stackTrace;
12
+ this.functionSource =
13
+ functionSource.length > SOURCE_PREVIEW
14
+ ? functionSource.slice(0, SOURCE_PREVIEW) + "…"
15
+ : functionSource;
16
+ }
17
+ }
18
+ /** A function cannot be sent as source */
19
+ export class ScriptSerializationError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "ScriptSerializationError";
23
+ }
24
+ }
25
+ /** The source to send for `fn`; a function object must not depend on transpiler helpers */
26
+ export function toFunctionDeclaration(fn) {
27
+ if (typeof fn === "string")
28
+ return fn;
29
+ const source = fn.toString();
30
+ // tsx/esbuild wrap named functions in a __name() helper that does not exist in the page
31
+ if (source.includes("__name(")) {
32
+ throw new ScriptSerializationError("The function's source uses the transpiler's __name helper, which does not exist in the page. " +
33
+ "Use an arrow function without inner named functions, or pass the function as a string.");
34
+ }
35
+ return source;
36
+ }
37
+ /** Calls `fn` in the page with serialized arguments and returns its deserialized result */
38
+ export async function callFunction(connector, context, fn, args = [], options = {}) {
39
+ const functionDeclaration = toFunctionDeclaration(fn);
40
+ const ownership = options.ownership ?? "none";
41
+ // Serialize first: a bad argument must fail before anything is sent
42
+ const serialized = args.map(toLocalValue);
43
+ const thisValue = "thisArg" in options ? toLocalValue(options.thisArg) : undefined;
44
+ const result = await connector.send("script.callFunction", {
45
+ functionDeclaration,
46
+ awaitPromise: options.awaitPromise ?? true,
47
+ target: options.sandbox === undefined
48
+ ? { context }
49
+ : { context, sandbox: options.sandbox },
50
+ ...(serialized.length > 0 && { arguments: serialized }),
51
+ resultOwnership: ownership,
52
+ ...(thisValue !== undefined && { this: thisValue }),
53
+ });
54
+ if (result.type === "exception") {
55
+ throw new ScriptError(result.exceptionDetails.text, functionDeclaration, result.exceptionDetails.stackTrace);
56
+ }
57
+ return fromRemoteValue(result.result, { keepHandles: ownership === "root" });
58
+ }
59
+ /** Releases handles obtained with `ownership: "root"`; a handle that is already gone is fine */
60
+ export async function disown(connector, context, handles) {
61
+ if (handles.length === 0)
62
+ return;
63
+ try {
64
+ await connector.send("script.disown", { handles, target: { context } });
65
+ }
66
+ catch (err) {
67
+ if (err instanceof BiDiError && err.code === "no such handle")
68
+ return;
69
+ throw err;
70
+ }
71
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A reference to a DOM node in the browser. The `sharedId` stays valid across scripts in the
3
+ * same document; after navigation or removal the browser answers `no such node`.
4
+ * Internal: user code keeps using `Locator`.
5
+ */
6
+ export declare class ElementHandle {
7
+ readonly sharedId: string;
8
+ readonly handle?: string | undefined;
9
+ constructor(sharedId: string, handle?: string | undefined);
10
+ }
@@ -4,8 +4,8 @@
4
4
  * Internal: user code keeps using `Locator`.
5
5
  */
6
6
  export class ElementHandle {
7
- constructor(
8
- public readonly sharedId: string,
9
- public readonly handle?: string,
10
- ) {}
7
+ constructor(sharedId, handle) {
8
+ this.sharedId = sharedId;
9
+ this.handle = handle;
10
+ }
11
11
  }
@@ -0,0 +1,28 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ /** Sandbox realm holding the framework's helpers: same DOM as the page, separate JS globals */
3
+ export declare const HELPER_SANDBOX = "samurai";
4
+ /** What `CALL_PROBE` answers with when the helpers are not installed in the document */
5
+ export declare const HELPERS_MISSING = "samurai:helpers-missing";
6
+ /** Installs `globalThis.__samurai` in the sandbox realm of a document */
7
+ export declare const HELPERS_SOURCE: string;
8
+ /** Calls the installed probe, or reports that the helpers are missing instead of throwing */
9
+ export declare const CALL_PROBE: string;
10
+ /** What the locator needs from the helper realm */
11
+ export interface HelperInstaller {
12
+ /** Resolves once the helpers are registered for the page; installs on first use */
13
+ ensureInstalled(): Promise<void>;
14
+ /** Runs the helpers again in the loaded documents; used when a call found them missing */
15
+ reinstall(): Promise<void>;
16
+ }
17
+ /** The framework's helper library for one page: registered once, applied to every document */
18
+ export declare class HelperRealm implements HelperInstaller {
19
+ private connector;
20
+ private contextId;
21
+ private installed;
22
+ constructor(connector: BiDiConnector, contextId: string);
23
+ ensureInstalled(): Promise<void>;
24
+ reinstall(): Promise<void>;
25
+ /** Removes the registration; nothing is sent when the helpers were never installed */
26
+ dispose(): Promise<void>;
27
+ private options;
28
+ }
@@ -0,0 +1,50 @@
1
+ import { PROBE_ELEMENT } from "../locator/element-state.js";
2
+ import { addPreload, runInLoadedContexts } from "./preload.js";
3
+ /** Sandbox realm holding the framework's helpers: same DOM as the page, separate JS globals */
4
+ export const HELPER_SANDBOX = "samurai";
5
+ /** What `CALL_PROBE` answers with when the helpers are not installed in the document */
6
+ export const HELPERS_MISSING = "samurai:helpers-missing";
7
+ /** Installs `globalThis.__samurai` in the sandbox realm of a document */
8
+ export const HELPERS_SOURCE = `() => {
9
+ globalThis.__samurai = { probeElement: ${PROBE_ELEMENT} };
10
+ }`;
11
+ /** Calls the installed probe, or reports that the helpers are missing instead of throwing */
12
+ export const CALL_PROBE = `(...args) => {
13
+ const helpers = globalThis.__samurai;
14
+ return helpers ? helpers.probeElement(...args) : ${JSON.stringify(HELPERS_MISSING)};
15
+ }`;
16
+ /** The framework's helper library for one page: registered once, applied to every document */
17
+ export class HelperRealm {
18
+ constructor(connector, contextId) {
19
+ this.connector = connector;
20
+ this.contextId = contextId;
21
+ }
22
+ ensureInstalled() {
23
+ this.installed ??= addPreload(this.connector, this.options()).catch((err) => {
24
+ this.installed = undefined;
25
+ throw err;
26
+ });
27
+ return this.installed.then(() => undefined);
28
+ }
29
+ async reinstall() {
30
+ if (!this.installed)
31
+ return this.ensureInstalled();
32
+ await this.installed;
33
+ await runInLoadedContexts(this.connector, this.options());
34
+ }
35
+ /** Removes the registration; nothing is sent when the helpers were never installed */
36
+ async dispose() {
37
+ const installed = this.installed;
38
+ this.installed = undefined;
39
+ if (!installed)
40
+ return;
41
+ await (await installed).dispose();
42
+ }
43
+ options() {
44
+ return {
45
+ source: HELPERS_SOURCE,
46
+ contexts: [this.contextId],
47
+ sandbox: HELPER_SANDBOX,
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,31 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import { type ChannelArg } from "./serialize.js";
3
+ export interface PreloadHandle {
4
+ readonly id: string;
5
+ /** Idempotent. Documents that are already loaded keep the effect until they navigate */
6
+ dispose(): Promise<void>;
7
+ }
8
+ export interface RunOptions {
9
+ /** A function declaration, e.g. `() => { … }` */
10
+ source: string;
11
+ /** Top-level contexts to cover, including their iframes. Default: every top-level context */
12
+ contexts?: string[];
13
+ /** Named sandbox realm; without it the script runs in the page's own realm */
14
+ sandbox?: string;
15
+ /** Channels the function receives as arguments, in the preload registration and in the immediate run */
16
+ arguments?: ChannelArg[];
17
+ /**
18
+ * What to do when the immediate run in an already-loaded document throws.
19
+ * "log" suits user init scripts (the browser reports such errors for later documents itself)
20
+ * @default "throw"
21
+ */
22
+ onRunError?: "throw" | "log";
23
+ }
24
+ /**
25
+ * Registers `source` to run before any page script in every future document of `contexts`, and
26
+ * also runs it once in the documents that are loaded now: preload scripts only affect documents
27
+ * created after registration, and callers should not have to care about that ordering.
28
+ */
29
+ export declare function addPreload(connector: BiDiConnector, options: RunOptions): Promise<PreloadHandle>;
30
+ /** Runs `source` once in every loaded context (iframes included) under `contexts` */
31
+ export declare function runInLoadedContexts(connector: BiDiConnector, { source, contexts, sandbox, onRunError, arguments: args, }: RunOptions): Promise<void>;
@@ -0,0 +1,71 @@
1
+ import logger from "../logger/index.js";
2
+ import { BiDiError } from "../transport/bidi-error.js";
3
+ import { callFunction, ScriptError } from "./call-function.js";
4
+ import { toLocalValue } from "./serialize.js";
5
+ /**
6
+ * Registers `source` to run before any page script in every future document of `contexts`, and
7
+ * also runs it once in the documents that are loaded now: preload scripts only affect documents
8
+ * created after registration, and callers should not have to care about that ordering.
9
+ */
10
+ export async function addPreload(connector, options) {
11
+ const { script } = await connector.send("script.addPreloadScript", {
12
+ functionDeclaration: options.source,
13
+ ...(options.contexts && { contexts: options.contexts }),
14
+ ...(options.sandbox !== undefined && { sandbox: options.sandbox }),
15
+ ...(options.arguments && {
16
+ arguments: options.arguments.map((arg) => toLocalValue(arg)),
17
+ }),
18
+ });
19
+ let removed = false;
20
+ const dispose = async () => {
21
+ if (removed)
22
+ return;
23
+ removed = true;
24
+ try {
25
+ await connector.send("script.removePreloadScript", { script });
26
+ }
27
+ catch (err) {
28
+ if (err instanceof BiDiError && err.code === "no such script")
29
+ return;
30
+ throw err;
31
+ }
32
+ };
33
+ try {
34
+ // Registered first, so a document created while this runs is covered by the registration
35
+ await runInLoadedContexts(connector, options);
36
+ }
37
+ catch (err) {
38
+ await dispose().catch((disposeErr) => logger.debug("Could not remove preload script after a failed run", {
39
+ disposeErr,
40
+ }));
41
+ throw err;
42
+ }
43
+ return { id: script, dispose };
44
+ }
45
+ /** Runs `source` once in every loaded context (iframes included) under `contexts` */
46
+ export async function runInLoadedContexts(connector, { source, contexts, sandbox, onRunError = "throw", arguments: args = [], }) {
47
+ const trees = contexts
48
+ ? await Promise.all(contexts.map((root) => connector.send("browsingContext.getTree", { root })))
49
+ : [await connector.send("browsingContext.getTree", {})];
50
+ const loaded = trees.flatMap((tree) => tree.contexts.flatMap(flatten));
51
+ const outcomes = await Promise.allSettled(loaded.map((context) => callFunction(connector, context, source, args, {
52
+ awaitPromise: false,
53
+ ...(sandbox !== undefined && { sandbox }),
54
+ })));
55
+ for (const outcome of outcomes) {
56
+ if (outcome.status === "fulfilled")
57
+ continue;
58
+ const err = outcome.reason;
59
+ // The context closed between listing and running: nothing left to cover
60
+ if (err instanceof BiDiError && err.code === "no such frame")
61
+ continue;
62
+ if (err instanceof ScriptError && onRunError === "log") {
63
+ logger.warn("Script threw while running in a loaded document: %s", err.text);
64
+ continue;
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+ function flatten(info) {
70
+ return [info.context, ...(info.children ?? []).flatMap(flatten)];
71
+ }
@@ -0,0 +1,29 @@
1
+ import type { ArgumentValue, RemoteValue } from "../types/bidi-modules/script.js";
2
+ /** A value passed to a page function */
3
+ export type Arg = unknown;
4
+ /** A remote value that cannot be copied to plain JS (function, window, promise, …), kept by type and handle */
5
+ export declare class RemoteObject {
6
+ readonly type: string;
7
+ readonly handle?: string | undefined;
8
+ /** The deserialized value, when the browser sent one and handles were kept */
9
+ readonly value?: unknown | undefined;
10
+ constructor(type: string, handle?: string | undefined,
11
+ /** The deserialized value, when the browser sent one and handles were kept */
12
+ value?: unknown | undefined);
13
+ }
14
+ /**
15
+ * A channel the page function can call as `send(value)`; the browser delivers each value as a
16
+ * `script.message` event. Only meaningful as a preload script or function argument.
17
+ */
18
+ export declare class ChannelArg {
19
+ readonly channel: string;
20
+ constructor(channel: string);
21
+ }
22
+ /** Serializes `value` for `script.callFunction` arguments; throws TypeError for what cannot cross the wire */
23
+ export declare function toLocalValue(value: Arg): ArgumentValue;
24
+ export interface DeserializeOptions {
25
+ /** Return objects that carry a handle as `RemoteObject`s (with `.value`), so the caller can disown them later */
26
+ keepHandles?: boolean;
27
+ }
28
+ /** Converts a remote value to plain JS; what cannot be copied becomes a `RemoteObject` */
29
+ export declare function fromRemoteValue(value: RemoteValue, options?: DeserializeOptions): unknown;