@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,372 @@
1
+ import { spawn } from "node:child_process";
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { findBrowser } from "./browser-finder.js";
5
+ import path from "node:path";
6
+ import { BiDiConnector } from "../transport/bidi-connection.js";
7
+ import Page from "./page.js";
8
+ import { BrowserContext } from "./browser-context.js";
9
+ import { validateEmulation } from "./emulation.js";
10
+ import { prepareDownloadsDir, removeIfEmpty } from "./downloads-dir.js";
11
+ import logger from "../logger/index.js";
12
+ import { browsersDir, projectDir, readConfig } from "../config/config.js";
13
+ /**
14
+ * Sent with session.new. With "ignore" the browser leaves a JavaScript dialog open and reports it
15
+ * (`browsingContext.userPromptOpened`), so the framework decides how to answer instead of the browser.
16
+ */
17
+ export const SESSION_CAPABILITIES = {
18
+ alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
19
+ };
20
+ /** Time (ms) to wait for Firefox to answer browser.close before killing the process */
21
+ const BROWSER_CLOSE_GRACE = 2000;
22
+ const browserProfilePath = (browserName) => path.join(browsersDir(), "profiles", browserName, "user.js");
23
+ const baseBrowserLaunchFlags = {
24
+ // chrome: [],
25
+ firefox: ["--no-sandbox", "--no-remote"],
26
+ };
27
+ /** Read when a browser launches, since the profile folder depends on the active project */
28
+ const defaultLaunchOptions = (browserName) => ({
29
+ port: 9222,
30
+ profileDir: path.dirname(browserProfilePath(browserName)),
31
+ headless: true,
32
+ });
33
+ const regexPrefix = {
34
+ firefox: /WebDriver\sBiDi\slistening\son\s/,
35
+ // chrome: /DevTools\slistening\son/,
36
+ };
37
+ export const browserLaunchFlag = (browserName, launchOptions) => {
38
+ const defLaunchOpts = defaultLaunchOptions(browserName);
39
+ const port = launchOptions.port || defLaunchOpts.port;
40
+ const profileDir = launchOptions.profileDir || defLaunchOpts.profileDir;
41
+ const headless = launchOptions.headless ?? defLaunchOpts.headless;
42
+ const baseLaunchFlags = [...baseBrowserLaunchFlags[browserName]];
43
+ if (headless) {
44
+ baseLaunchFlags.push("--headless");
45
+ }
46
+ baseLaunchFlags.push(`--remote-debugging-port=${port}`);
47
+ switch (browserName) {
48
+ // case "chrome":
49
+ // baseLaunchFlags.push(`--user-data-dir=${profileDir}`);
50
+ // break;
51
+ case "firefox":
52
+ baseLaunchFlags.push("--profile", profileDir);
53
+ // The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
54
+ baseLaunchFlags.push("about:blank");
55
+ break;
56
+ }
57
+ logger.verbose("Launch flags for browser %s: ", browserName, {
58
+ baseLaunchFlags,
59
+ });
60
+ return baseLaunchFlags;
61
+ };
62
+ /**
63
+ * Stops what a failed launch started. A browser left running holds the profile (the next launch
64
+ * fails with "already open") and keeps the test process alive.
65
+ */
66
+ export function abandonLaunch(browserProc, biDiConnector) {
67
+ biDiConnector?.kill();
68
+ browserProc.kill();
69
+ }
70
+ // On macOS a spawned browser is attributed to the terminal, which is denied access to
71
+ // ~/Library/Application Support/Firefox (profiles.ini). Point Firefox at a local app data dir instead.
72
+ const browserAppDataPath = (browserName) => path.join(browsersDir(), "app-data", browserName);
73
+ const browserLaunchEnv = (browserName) => {
74
+ const appDataPath = browserAppDataPath(browserName);
75
+ mkdirSync(appDataPath, { recursive: true });
76
+ logger.debug("Using app data dir for %s: %s", browserName, appDataPath);
77
+ switch (browserName) {
78
+ case "firefox":
79
+ return { MOZ_APP_DATA: appDataPath };
80
+ }
81
+ };
82
+ const browserProfiles = {
83
+ // chrome: "",
84
+ firefox: `
85
+ user_pref("devtools.debugger.remote-enabled", true);
86
+ user_pref("devtools.debugger.prompt-connection", false);
87
+ user_pref("devtools.chrome.enabled", true);
88
+ // The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
89
+ user_pref("browser.startup.page", 0);
90
+ user_pref("browser.startup.homepage", "about:blank");
91
+ user_pref("browser.newtabpage.enabled", false);
92
+ `,
93
+ };
94
+ // Firefox logs "WebDriver BiDi listening on ws://<host>:<port>" without the endpoint path
95
+ const browserWsPath = {
96
+ firefox: "/session",
97
+ };
98
+ const browserWsRegex = {
99
+ firefox: /ws:\/\/(\d){1,3}.(\d){1,3}.(\d){1,3}.(\d){1,3}:(\d){4}/g,
100
+ // chrome:
101
+ // /ws:\/\/(\d){1,3}.(\d){1,3}.(\d){1,3}.(\d){1,3}:(\d){4}\/devtools\/browser\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g,
102
+ };
103
+ export class Browser {
104
+ constructor({ browserProc, biDiConnector, downloadsDir, }) {
105
+ this.userContexts = new Map();
106
+ this.browserProc = browserProc;
107
+ this.biDiConnector = biDiConnector;
108
+ this.downloadsDir = downloadsDir;
109
+ this.host = {
110
+ connector: biDiConnector,
111
+ newPage: (options) => this.newPage(options),
112
+ forget: (context) => void this.userContexts.delete(context.id),
113
+ };
114
+ this.defaultContext = new BrowserContext(this.host, "default", true);
115
+ }
116
+ /** Resolves when the browser process has exited, whether `close()` or a person closed it */
117
+ get exited() {
118
+ this.exitedPromise ??= new Promise((resolve) => {
119
+ if (this.browserProc.exitCode !== null || this.browserProc.signalCode)
120
+ return resolve();
121
+ this.browserProc.once("exit", () => resolve());
122
+ });
123
+ return this.exitedPromise;
124
+ }
125
+ static async launch(browserName, launchOptions = defaultLaunchOptions(browserName),
126
+ /** Aborting kills the browser process and rejects the launch */
127
+ signal) {
128
+ signal?.throwIfAborted();
129
+ logger.verbose("Trying to launch browser %s", browserName);
130
+ const browserLocation = findBrowser(browserName);
131
+ logger.debug("Found browser install location at: %s", browserLocation);
132
+ ensureBrowserProfile(browserName);
133
+ logger.verbose("Trying to spawn %s process", browserName);
134
+ const browserProc = spawn(browserLocation, browserLaunchFlag(browserName, launchOptions), { env: { ...process.env, ...browserLaunchEnv(browserName) } });
135
+ return new Promise((resolve, reject) => {
136
+ const onUnexpectedClose = (code) => {
137
+ reject(new Error(`Browser process exited unexpectedly with code ${code}`));
138
+ };
139
+ browserProc.on("close", onUnexpectedClose);
140
+ const onAbort = () => {
141
+ logger.verbose("Launch of %s aborted, killing process", browserName);
142
+ browserProc.kill();
143
+ reject(signal?.reason);
144
+ };
145
+ signal?.addEventListener("abort", onAbort, { once: true });
146
+ browserProc.stdout.on("data", (data) => {
147
+ logger.debug(`${browserName} stdout: ${data}`);
148
+ if (String(data).match(/!!!\scould\snot\sstart\sserver\son\sport\s(\d){4}/g)) {
149
+ browserProc.kill();
150
+ reject(`PORT ${launchOptions.port} is already in use!`);
151
+ }
152
+ });
153
+ browserProc.stderr.on("data", async (data) => {
154
+ logger.debug(`${browserName} stderr: ${data}`);
155
+ const urlMatch = String(data).match(browserWsRegex[browserName]);
156
+ logger.debug("Browser announced its endpoint", { urlMatch });
157
+ if (!Array.isArray(urlMatch) || urlMatch.length === 0) {
158
+ return;
159
+ }
160
+ // Killing the process mid-handshake rejects pending BiDi calls; surface that via reject
161
+ // instead of an unhandled rejection from this async listener
162
+ let biDiConnector;
163
+ try {
164
+ let url = urlMatch[0] + browserWsPath[browserName];
165
+ logger.verbose("Websocket URL match: %s", url);
166
+ biDiConnector = await BiDiConnector.connect(url, {
167
+ commandTimeout: await readCommandTimeout(),
168
+ });
169
+ logger.verbose("Starting new session");
170
+ const status = await biDiConnector.send("session.status", {});
171
+ logger.verbose("Session status", status);
172
+ await biDiConnector.send("session.new", {
173
+ capabilities: SESSION_CAPABILITIES,
174
+ });
175
+ logger.verbose("Session started successfully");
176
+ const downloadsDir = await prepareDownloadsDir(biDiConnector, path.resolve(projectDir(), (await readOptionalConfig("downloadsDir")) ?? "result/downloads"));
177
+ const browser = new Browser({
178
+ browserProc,
179
+ biDiConnector,
180
+ downloadsDir,
181
+ });
182
+ logger.verbose("Requesting current browser tree");
183
+ const browserTree = await biDiConnector.send("browsingContext.getTree", {});
184
+ logger.verbose("Received current browser tree");
185
+ logger.debug("Browser tree: ", { browserTree });
186
+ const pageContext = browserTree.contexts[0];
187
+ let page = undefined;
188
+ if (pageContext) {
189
+ page = new Page(biDiConnector, pageContext.context);
190
+ browser.defaultContext.register(page);
191
+ }
192
+ else {
193
+ page = await browser.newPage({ type: "tab" });
194
+ }
195
+ await page.applyDefaultViewport();
196
+ // Requests made before tracking starts are invisible to it, and a dialog nobody watches would hang the page
197
+ await page.startNetworkTracking();
198
+ await page.startDialogHandling();
199
+ await page.startLogging();
200
+ await page.startDownloadTracking();
201
+ await page.startFileChooserTracking();
202
+ browserProc.off("close", onUnexpectedClose);
203
+ signal?.removeEventListener("abort", onAbort);
204
+ resolve({
205
+ browser,
206
+ page,
207
+ });
208
+ }
209
+ catch (err) {
210
+ browserProc.off("close", onUnexpectedClose);
211
+ signal?.removeEventListener("abort", onAbort);
212
+ abandonLaunch(browserProc, biDiConnector);
213
+ reject(err);
214
+ }
215
+ });
216
+ });
217
+ }
218
+ /** Closes the browser via BiDi browser.close, then makes sure the process has exited */
219
+ async close() {
220
+ if (this.browserProc.exitCode !== null || this.browserProc.signalCode) {
221
+ this.biDiConnector.kill();
222
+ removeIfEmpty(this.downloadsDir);
223
+ return;
224
+ }
225
+ const exited = new Promise((resolve) => this.browserProc.once("exit", resolve));
226
+ // User contexts outlive the browser in its profile unless they are removed
227
+ await Promise.allSettled([...this.userContexts.values()].map((context) => context.close()));
228
+ try {
229
+ // Firefox may exit without answering; a lost reply must not hold up teardown
230
+ await Promise.race([
231
+ this.biDiConnector.send("browser.close", {}),
232
+ sleep(BROWSER_CLOSE_GRACE, undefined, { ref: false }),
233
+ ]);
234
+ }
235
+ catch {
236
+ // browser may already be shutting down
237
+ }
238
+ this.biDiConnector.kill();
239
+ this.browserProc.kill();
240
+ await exited;
241
+ // A folder nothing was downloaded into would only pile up
242
+ removeIfEmpty(this.downloadsDir);
243
+ }
244
+ /** Alias of close() */
245
+ async kill() {
246
+ await this.close();
247
+ }
248
+ async newPage({ type, background, userContext, }) {
249
+ const createBrowserContext = await this.biDiConnector.send("browsingContext.create", {
250
+ type,
251
+ background,
252
+ userContext,
253
+ });
254
+ logger.debug("Created browsing context", { createBrowserContext });
255
+ const page = new Page(this.biDiConnector, createBrowserContext.context);
256
+ this.contextFor(userContext).register(page);
257
+ await page.applyDefaultViewport();
258
+ await page.startNetworkTracking();
259
+ await page.startDialogHandling();
260
+ await page.startLogging();
261
+ await page.startDownloadTracking();
262
+ await page.startFileChooserTracking();
263
+ return page;
264
+ }
265
+ /**
266
+ * Creates an isolated context: its own cookies, storage and cache. `browser.close()` removes the contexts
267
+ * that are still open, as Firefox would otherwise keep them in its profile.
268
+ * @example
269
+ * const alice = await browser.newContext();
270
+ * const page = await alice.newPage();
271
+ */
272
+ async newContext(options = {}) {
273
+ const { acceptInsecureCerts, unhandledPromptBehavior, ...emulation } = options;
274
+ // A wrong value must not leave a context behind, so it is refused before one is created
275
+ validateEmulation(emulation);
276
+ const { userContext } = await this.biDiConnector.send("browser.createUserContext", {
277
+ ...(acceptInsecureCerts !== undefined && { acceptInsecureCerts }),
278
+ ...(unhandledPromptBehavior && { unhandledPromptBehavior }),
279
+ });
280
+ const context = new BrowserContext(this.host, userContext, false);
281
+ this.userContexts.set(userContext, context);
282
+ if (Object.keys(emulation).length > 0) {
283
+ try {
284
+ await context.emulate(emulation);
285
+ }
286
+ catch (err) {
287
+ // Firefox keeps a context in its profile until it is removed
288
+ await context.close().catch((closeErr) => logger.debug("Could not remove a context whose emulation failed", {
289
+ closeErr,
290
+ }));
291
+ throw err;
292
+ }
293
+ }
294
+ return context;
295
+ }
296
+ /** The default context, then the contexts this browser created that are still open */
297
+ contexts() {
298
+ return [this.defaultContext, ...this.userContexts.values()];
299
+ }
300
+ contextFor(userContext) {
301
+ if (userContext === undefined || userContext === "default")
302
+ return this.defaultContext;
303
+ return this.userContexts.get(userContext) ?? this.adopt(userContext);
304
+ }
305
+ /** A user context this browser did not create (Firefox keeps them in its profile): usable, never closed by `close()` */
306
+ adopt(userContext) {
307
+ const context = new BrowserContext(this.host, userContext, false);
308
+ this.userContexts.set(userContext, context);
309
+ return context;
310
+ }
311
+ async getCookie(filter, partition) {
312
+ return await this.biDiConnector.send("storage.getCookies", {
313
+ filter,
314
+ partition,
315
+ });
316
+ }
317
+ async setCookie(cookie, partition) {
318
+ return await this.biDiConnector.send("storage.setCookie", {
319
+ partition,
320
+ cookie,
321
+ });
322
+ }
323
+ }
324
+ /** A config value, or undefined when it is not set or the config cannot be read */
325
+ async function readOptionalConfig(key) {
326
+ try {
327
+ return await readConfig(key);
328
+ }
329
+ catch (err) {
330
+ logger.debug("Could not read %s from config, using the default", key, {
331
+ err,
332
+ });
333
+ return undefined;
334
+ }
335
+ }
336
+ /** The configured BiDi command timeout; a missing or unreadable config falls back to the connector default */
337
+ async function readCommandTimeout() {
338
+ try {
339
+ return (await readConfig("bidi"))?.commandTimeout;
340
+ }
341
+ catch (err) {
342
+ logger.debug("Could not read bidi config, using defaults", { err });
343
+ return undefined;
344
+ }
345
+ }
346
+ /** Writes the browser's user.js unless it already holds the current prefs; an outdated one is replaced */
347
+ export function ensureBrowserProfile(browserName, profilePath = browserProfilePath(browserName)) {
348
+ logger.verbose("Checking browser profile for: %s", browserName);
349
+ if (!profilePath) {
350
+ logger.verbose("No browser profile path declared, skipping...");
351
+ return;
352
+ }
353
+ if (existsSync(profilePath)) {
354
+ if (readFileSync(profilePath, "utf8") === browserProfiles[browserName]) {
355
+ logger.verbose("Browser profile is up to date at: %s", profilePath);
356
+ return;
357
+ }
358
+ logger.verbose("Browser profile at %s is outdated, rewriting it", profilePath);
359
+ }
360
+ else {
361
+ logger.verbose("No browser profile created yet for %s", browserName);
362
+ }
363
+ createBrowserProfile(browserName, profilePath);
364
+ }
365
+ function createBrowserProfile(browserName, profilePath) {
366
+ logger.verbose("Creating browser profile for %s at %s", browserName, profilePath);
367
+ mkdirSync(path.dirname(profilePath), { recursive: true });
368
+ writeFileSync(profilePath, browserProfiles[browserName], {
369
+ encoding: "utf8",
370
+ });
371
+ logger.verbose("Browser profile created");
372
+ }
@@ -0,0 +1,23 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
3
+ /**
4
+ * Tracks which browsing contexts (iframes) belong to which top-level context, so events that
5
+ * carry a child context id can be attributed to the page that owns it.
6
+ * Destroyed contexts stay known: late events for them must still resolve to their page.
7
+ */
8
+ export declare class ContextTree {
9
+ private connector;
10
+ private subscription;
11
+ private parents;
12
+ private constructor();
13
+ static create(connector: BiDiConnector): Promise<ContextTree>;
14
+ private onContext;
15
+ private add;
16
+ /** The direct parent, or null for a top-level or unknown context */
17
+ parentOf(context: BrowsingContext): BrowsingContext | null;
18
+ /** The top-level context that contains `context`; an unknown context is its own root */
19
+ rootOf(context: BrowsingContext): BrowsingContext;
20
+ /** True when `context` is `root` or one of its descendants; false for unknown contexts */
21
+ isWithin(context: BrowsingContext, root: BrowsingContext): boolean;
22
+ dispose(): Promise<void>;
23
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Tracks which browsing contexts (iframes) belong to which top-level context, so events that
3
+ * carry a child context id can be attributed to the page that owns it.
4
+ * Destroyed contexts stay known: late events for them must still resolve to their page.
5
+ */
6
+ export class ContextTree {
7
+ constructor(connector, subscription) {
8
+ this.connector = connector;
9
+ this.subscription = subscription;
10
+ this.parents = new Map();
11
+ this.onContext = (info) => this.add(info);
12
+ }
13
+ static async create(connector) {
14
+ // Subscribe first: a context created between getTree and subscribing would be missed
15
+ const subscription = await connector.subscribe([
16
+ "browsingContext.contextCreated",
17
+ "browsingContext.contextDestroyed",
18
+ ]);
19
+ const tree = new ContextTree(connector, subscription);
20
+ connector.onEvent("browsingContext.contextCreated", tree.onContext);
21
+ connector.onEvent("browsingContext.contextDestroyed", tree.onContext);
22
+ try {
23
+ const { contexts } = await connector.send("browsingContext.getTree", {});
24
+ contexts.forEach((info) => tree.add(info));
25
+ }
26
+ catch (err) {
27
+ await tree.dispose();
28
+ throw err;
29
+ }
30
+ return tree;
31
+ }
32
+ add(info) {
33
+ this.parents.set(info.context, info.parent ?? null);
34
+ info.children?.forEach((child) => this.add(child));
35
+ }
36
+ /** The direct parent, or null for a top-level or unknown context */
37
+ parentOf(context) {
38
+ return this.parents.get(context) ?? null;
39
+ }
40
+ /** The top-level context that contains `context`; an unknown context is its own root */
41
+ rootOf(context) {
42
+ let current = context;
43
+ for (let parent = this.parentOf(current); parent !== null; parent = this.parentOf(current)) {
44
+ current = parent;
45
+ }
46
+ return current;
47
+ }
48
+ /** True when `context` is `root` or one of its descendants; false for unknown contexts */
49
+ isWithin(context, root) {
50
+ if (context === root)
51
+ return true;
52
+ return this.parents.has(context) && this.rootOf(context) === root;
53
+ }
54
+ async dispose() {
55
+ this.connector.offEvent("browsingContext.contextCreated", this.onContext);
56
+ this.connector.offEvent("browsingContext.contextDestroyed", this.onContext);
57
+ this.parents.clear();
58
+ await this.subscription.unsubscribe();
59
+ }
60
+ }
@@ -0,0 +1,37 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import type { UserPromptOpenedParameters, UserPromptType } from "../types/bidi-modules/browsing-context.js";
3
+ export interface DialogResult {
4
+ accepted: boolean;
5
+ /** What was typed into a `prompt` */
6
+ userText?: string;
7
+ }
8
+ /**
9
+ * A JavaScript dialog (`alert`, `confirm`, `prompt`) or a `beforeunload` prompt that is open in the page.
10
+ * Answer it once with `accept()` or `dismiss()`; `page.on("dialog")` hands these out.
11
+ */
12
+ export declare class Dialog {
13
+ private connector;
14
+ /** The top-level context of the page; prompts are answered there, also when an iframe opened them */
15
+ private context;
16
+ private info;
17
+ /** Resolves when the browser reports the prompt closed */
18
+ readonly closed: Promise<DialogResult>;
19
+ private resolveClosed;
20
+ private answered;
21
+ constructor(connector: BiDiConnector,
22
+ /** The top-level context of the page; prompts are answered there, also when an iframe opened them */
23
+ context: string, info: UserPromptOpenedParameters);
24
+ type(): UserPromptType;
25
+ message(): string;
26
+ /** The text a `prompt` starts with; empty for the other kinds */
27
+ defaultValue(): string;
28
+ /** True once the dialog is answered, by you or by the browser */
29
+ get handled(): boolean;
30
+ /** Clicks OK; `promptText` is typed into a `prompt` first */
31
+ accept(promptText?: string): Promise<void>;
32
+ /** Clicks Cancel */
33
+ dismiss(): Promise<void>;
34
+ /** Called by the page when the browser reports the prompt closed */
35
+ markClosed(result: DialogResult): void;
36
+ private answer;
37
+ }
@@ -0,0 +1,61 @@
1
+ import { BiDiError } from "../transport/bidi-error.js";
2
+ /**
3
+ * A JavaScript dialog (`alert`, `confirm`, `prompt`) or a `beforeunload` prompt that is open in the page.
4
+ * Answer it once with `accept()` or `dismiss()`; `page.on("dialog")` hands these out.
5
+ */
6
+ export class Dialog {
7
+ constructor(connector,
8
+ /** The top-level context of the page; prompts are answered there, also when an iframe opened them */
9
+ context, info) {
10
+ this.connector = connector;
11
+ this.context = context;
12
+ this.info = info;
13
+ this.closed = new Promise((resolve) => (this.resolveClosed = resolve));
14
+ // With another handler than "ignore" the browser has answered already
15
+ this.answered = info.handler !== "ignore";
16
+ }
17
+ type() {
18
+ return this.info.type;
19
+ }
20
+ message() {
21
+ return this.info.message;
22
+ }
23
+ /** The text a `prompt` starts with; empty for the other kinds */
24
+ defaultValue() {
25
+ return this.info.defaultValue ?? "";
26
+ }
27
+ /** True once the dialog is answered, by you or by the browser */
28
+ get handled() {
29
+ return this.answered;
30
+ }
31
+ /** Clicks OK; `promptText` is typed into a `prompt` first */
32
+ accept(promptText) {
33
+ return this.answer({ accept: true, ...(promptText !== undefined && { userText: promptText }) });
34
+ }
35
+ /** Clicks Cancel */
36
+ dismiss() {
37
+ return this.answer({ accept: false });
38
+ }
39
+ /** Called by the page when the browser reports the prompt closed */
40
+ markClosed(result) {
41
+ this.answered = true;
42
+ this.resolveClosed(result);
43
+ }
44
+ async answer(params) {
45
+ if (this.answered)
46
+ throw new Error("dialog already handled");
47
+ this.answered = true;
48
+ try {
49
+ await this.connector.send("browsingContext.handleUserPrompt", {
50
+ context: this.context,
51
+ ...params,
52
+ });
53
+ }
54
+ catch (err) {
55
+ // Closed meanwhile (a navigation, another handler): the dialog is gone, which is what was wanted
56
+ if (err instanceof BiDiError && err.code === "no such alert")
57
+ return;
58
+ throw err;
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,71 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import { WaitTimeoutError } from "../wait/wait-until.js";
3
+ import type { ContextTree } from "./context-tree.js";
4
+ /** A download did not complete */
5
+ export declare class DownloadError extends Error {
6
+ readonly url: string;
7
+ /** "canceled": the browser stopped it or the server broke the connection; "page closed" */
8
+ readonly reason: string;
9
+ constructor(url: string, reason: string);
10
+ }
11
+ /** A download did not finish in time */
12
+ export declare class DownloadTimeoutError extends WaitTimeoutError<undefined> {
13
+ constructor(url: string, timeout: number);
14
+ }
15
+ type Outcome = {
16
+ status: "complete";
17
+ path: string;
18
+ } | {
19
+ status: "canceled";
20
+ } | {
21
+ status: "closed";
22
+ };
23
+ /** A file the page started to download */
24
+ export declare class Download {
25
+ private readonly address;
26
+ private readonly filename;
27
+ private readonly defaultTimeout;
28
+ private settle;
29
+ private outcome;
30
+ constructor(address: string, filename: string, defaultTimeout: number);
31
+ url(): string;
32
+ /** The name the server (or the link) suggested */
33
+ suggestedFilename(): string;
34
+ /**
35
+ * Waits until the download finished and returns where the browser saved it. Rejects with DownloadError when it was
36
+ * cancelled, and with DownloadTimeoutError after `timeout` ms (default: the navigation timeout).
37
+ */
38
+ path(options?: {
39
+ timeout?: number;
40
+ }): Promise<string>;
41
+ /** Waits for the end of the download: null when it completed, otherwise why it did not ("canceled") */
42
+ failure(): Promise<string | null>;
43
+ /** @internal */
44
+ finish(outcome: Outcome): void;
45
+ }
46
+ interface EventMap {
47
+ download: [Download];
48
+ }
49
+ /** Follows the downloads of one page (frames included) */
50
+ export declare class DownloadTracker {
51
+ private connector;
52
+ private tree;
53
+ private contextId;
54
+ private timeout;
55
+ private events;
56
+ private active;
57
+ private subscription;
58
+ private disposed;
59
+ private constructor();
60
+ static start(connector: BiDiConnector, tree: ContextTree, contextId: string, options: {
61
+ timeout: number;
62
+ }): Promise<DownloadTracker>;
63
+ on(event: "download", listener: (...args: EventMap["download"]) => void): void;
64
+ off(event: "download", listener: (...args: EventMap["download"]) => void): void;
65
+ /** Stops tracking; downloads still going on reject with "page closed". Idempotent */
66
+ dispose(): Promise<void>;
67
+ private removeListeners;
68
+ private onBegin;
69
+ private onEnd;
70
+ }
71
+ export {};