@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,515 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { setTimeout as sleep } from "node:timers/promises";
3
- import type { ChildProcessWithoutNullStreams } from "node:child_process";
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
- import type { SupportedBrowser } from "../types/browser.js";
6
- import { findBrowser } from "./browser-finder.js";
7
- import path from "node:path";
8
- import { BiDiConnector } from "../transport/bidi-connection.js";
9
- import Page from "./page.js";
10
- import { BrowserContext, type ContextHost } from "./browser-context.js";
11
- import type { UserPromptHandler } from "../types/bidi-modules/session.js";
12
- import { validateEmulation, type EmulationOptions } from "./emulation.js";
13
- import { prepareDownloadsDir, removeIfEmpty } from "./downloads-dir.js";
14
- import type { BiDiCommands } from "../types/bidi.js";
15
- import logger from "../logger/index.js";
16
- import { readConfig } from "../config/config.js";
17
- import type {
18
- CookieFilter,
19
- PartialCookie,
20
- PartitionDescriptor,
21
- } from "../types/bidi-modules/storage.js";
22
-
23
- /**
24
- * Sent with session.new. With "ignore" the browser leaves a JavaScript dialog open and reports it
25
- * (`browsingContext.userPromptOpened`), so the framework decides how to answer instead of the browser.
26
- */
27
- export const SESSION_CAPABILITIES = {
28
- alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
29
- };
30
-
31
- /** Time (ms) to wait for Firefox to answer browser.close before killing the process */
32
- const BROWSER_CLOSE_GRACE = 2000;
33
-
34
- const browserProfilePath: Record<SupportedBrowser, string> = {
35
- // chrome: path.resolve("browsers/profiles/chrome"),
36
- firefox: path.resolve("browsers/profiles/firefox/user.js"),
37
- };
38
-
39
- const baseBrowserLaunchFlags: Record<SupportedBrowser, string[]> = {
40
- // chrome: [],
41
- firefox: ["--no-sandbox", "--no-remote"],
42
- };
43
-
44
- const defaultLaunchOptions: Record<SupportedBrowser, BrowserLaunchOptions> = {
45
- firefox: {
46
- port: 9222,
47
- profileDir: path.dirname(browserProfilePath.firefox),
48
- headless: true,
49
- },
50
- // chrome: {
51
- // port: 9222,
52
- // profileDir: path.dirname(browserProfilePath.chrome),
53
- // headless: true,
54
- // },
55
- };
56
-
57
- const regexPrefix: Record<SupportedBrowser, RegExp> = {
58
- firefox: /WebDriver\sBiDi\slistening\son\s/,
59
- // chrome: /DevTools\slistening\son/,
60
- };
61
-
62
- export const browserLaunchFlag = (
63
- browserName: SupportedBrowser,
64
- launchOptions: Partial<BrowserLaunchOptions>,
65
- ) => {
66
- const defLaunchOpts = defaultLaunchOptions[browserName];
67
- const port = launchOptions.port || defLaunchOpts.port;
68
- const profileDir = launchOptions.profileDir || defLaunchOpts.profileDir;
69
- const headless = launchOptions.headless ?? defLaunchOpts.headless;
70
-
71
- const baseLaunchFlags = [...baseBrowserLaunchFlags[browserName]];
72
-
73
- if (headless) {
74
- baseLaunchFlags.push("--headless");
75
- }
76
- baseLaunchFlags.push(`--remote-debugging-port=${port}`);
77
-
78
- switch (browserName) {
79
- // case "chrome":
80
- // baseLaunchFlags.push(`--user-data-dir=${profileDir}`);
81
- // break;
82
- case "firefox":
83
- baseLaunchFlags.push("--profile", profileDir);
84
- // The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
85
- baseLaunchFlags.push("about:blank");
86
- break;
87
- }
88
- logger.verbose("Launch flags for browser %s: ", browserName, {
89
- baseLaunchFlags,
90
- });
91
- return baseLaunchFlags;
92
- };
93
-
94
- /**
95
- * Stops what a failed launch started. A browser left running holds the profile (the next launch
96
- * fails with "already open") and keeps the test process alive.
97
- */
98
- export function abandonLaunch(
99
- browserProc: { kill(): boolean },
100
- biDiConnector: { kill(): void } | undefined,
101
- ): void {
102
- biDiConnector?.kill();
103
- browserProc.kill();
104
- }
105
-
106
- // On macOS a spawned browser is attributed to the terminal, which is denied access to
107
- // ~/Library/Application Support/Firefox (profiles.ini). Point Firefox at a local app data dir instead.
108
- const browserAppDataPath: Record<SupportedBrowser, string> = {
109
- firefox: path.resolve("browsers/app-data/firefox"),
110
- };
111
-
112
- const browserLaunchEnv = (browserName: SupportedBrowser) => {
113
- const appDataPath = browserAppDataPath[browserName];
114
- mkdirSync(appDataPath, { recursive: true });
115
- logger.debug("Using app data dir for %s: %s", browserName, appDataPath);
116
-
117
- switch (browserName) {
118
- case "firefox":
119
- return { MOZ_APP_DATA: appDataPath };
120
- }
121
- };
122
-
123
- const browserProfiles: Record<SupportedBrowser, string> = {
124
- // chrome: "",
125
- firefox: `
126
- user_pref("devtools.debugger.remote-enabled", true);
127
- user_pref("devtools.debugger.prompt-connection", false);
128
- user_pref("devtools.chrome.enabled", true);
129
- // The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
130
- user_pref("browser.startup.page", 0);
131
- user_pref("browser.startup.homepage", "about:blank");
132
- user_pref("browser.newtabpage.enabled", false);
133
- `,
134
- };
135
-
136
- // Firefox logs "WebDriver BiDi listening on ws://<host>:<port>" without the endpoint path
137
- const browserWsPath: Record<SupportedBrowser, string> = {
138
- firefox: "/session",
139
- };
140
-
141
- const browserWsRegex: Record<SupportedBrowser, RegExp> = {
142
- firefox: /ws:\/\/(\d){1,3}.(\d){1,3}.(\d){1,3}.(\d){1,3}:(\d){4}/g,
143
- // chrome:
144
- // /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,
145
- };
146
-
147
- export interface BrowserLaunchOptions {
148
- /** Which port to use for the WebDeveloper tools */
149
- port: number;
150
- /** Firefox only */
151
- profileDir: string;
152
- /** Start the browser in headless mode or in UI mode */
153
- headless: boolean;
154
- }
155
-
156
- /** Options of `browser.newContext()` */
157
- export interface ContextOptions extends EmulationOptions {
158
- acceptInsecureCerts?: boolean;
159
- /** How this context's pages answer JavaScript dialogs; the session default is to leave them for the framework */
160
- unhandledPromptBehavior?: UserPromptHandler;
161
- }
162
-
163
- export class Browser {
164
- private browserProc: ChildProcessWithoutNullStreams;
165
- private biDiConnector: BiDiConnector;
166
- private userContexts = new Map<string, BrowserContext>();
167
- private host: ContextHost;
168
- /** The browser's own context, the one `Browser.launch`'s page lives in */
169
- public readonly defaultContext: BrowserContext;
170
- /** Where downloads are saved; undefined when the browser could not be told (it then uses its own folder) */
171
- public readonly downloadsDir: string | undefined;
172
-
173
- constructor({
174
- browserProc,
175
- biDiConnector,
176
- downloadsDir,
177
- }: {
178
- browserProc: ChildProcessWithoutNullStreams;
179
- biDiConnector: BiDiConnector;
180
- downloadsDir?: string | undefined;
181
- }) {
182
- this.browserProc = browserProc;
183
- this.biDiConnector = biDiConnector;
184
- this.downloadsDir = downloadsDir;
185
- this.host = {
186
- connector: biDiConnector,
187
- newPage: (options) => this.newPage(options),
188
- forget: (context) => void this.userContexts.delete(context.id),
189
- };
190
- this.defaultContext = new BrowserContext(this.host, "default", true);
191
- }
192
-
193
- static async launch(
194
- browserName: SupportedBrowser,
195
- launchOptions: Partial<BrowserLaunchOptions> = defaultLaunchOptions[
196
- browserName
197
- ],
198
- /** Aborting kills the browser process and rejects the launch */
199
- signal?: AbortSignal,
200
- ) {
201
- signal?.throwIfAborted();
202
- logger.verbose("Trying to launch browser %s", browserName);
203
- const browserLocation = findBrowser(browserName);
204
- logger.debug("Found browser install location at: %s", browserLocation);
205
- ensureBrowserProfile(browserName);
206
-
207
- logger.verbose("Trying to spawn %s process", browserName);
208
-
209
- const browserProc = spawn(
210
- browserLocation,
211
- browserLaunchFlag(browserName, launchOptions),
212
- { env: { ...process.env, ...browserLaunchEnv(browserName) } },
213
- );
214
-
215
- return new Promise<{ browser: Browser; page: Page }>((resolve, reject) => {
216
- const onUnexpectedClose = (code: number | null) => {
217
- reject(
218
- new Error(`Browser process exited unexpectedly with code ${code}`),
219
- );
220
- };
221
-
222
- browserProc.on("close", onUnexpectedClose);
223
-
224
- const onAbort = () => {
225
- logger.verbose("Launch of %s aborted, killing process", browserName);
226
- browserProc.kill();
227
- reject(signal?.reason);
228
- };
229
- signal?.addEventListener("abort", onAbort, { once: true });
230
-
231
- browserProc.stdout.on("data", (data: any) => {
232
- logger.debug(`${browserName} stdout: ${data}`);
233
- if (
234
- String(data).match(
235
- /!!!\scould\snot\sstart\sserver\son\sport\s(\d){4}/g,
236
- )
237
- ) {
238
- browserProc.kill();
239
- reject(`PORT ${launchOptions.port} is already in use!`);
240
- }
241
- });
242
-
243
- browserProc.stderr.on("data", async (data: any) => {
244
- logger.error(`${browserName} stderr: ${data}`);
245
-
246
- const urlMatch = String(data).match(browserWsRegex[browserName]);
247
- console.log({ urlMatch });
248
- if (!Array.isArray(urlMatch) || urlMatch.length === 0) {
249
- return;
250
- }
251
- // Killing the process mid-handshake rejects pending BiDi calls; surface that via reject
252
- // instead of an unhandled rejection from this async listener
253
- let biDiConnector: BiDiConnector | undefined;
254
- try {
255
- let url = urlMatch[0] + browserWsPath[browserName];
256
- logger.verbose("Websocket URL match: %s", url);
257
- biDiConnector = await BiDiConnector.connect(url, {
258
- commandTimeout: await readCommandTimeout(),
259
- });
260
- logger.verbose("Starting new session");
261
- const status = await biDiConnector.send("session.status", {});
262
- logger.verbose("Session status", status);
263
- await biDiConnector.send("session.new", {
264
- capabilities: SESSION_CAPABILITIES,
265
- });
266
- logger.verbose("Session started successfully");
267
-
268
- const downloadsDir = await prepareDownloadsDir(
269
- biDiConnector,
270
- (await readOptionalConfig("downloadsDir")) ?? "result/downloads",
271
- );
272
- const browser = new Browser({
273
- browserProc,
274
- biDiConnector,
275
- downloadsDir,
276
- });
277
- logger.verbose("Requesting current browser tree");
278
- const browserTree = await biDiConnector.send(
279
- "browsingContext.getTree",
280
- {},
281
- );
282
- logger.verbose("Received current browser tree");
283
- logger.debug("Browser tree: ", { browserTree });
284
-
285
- const pageContext = browserTree.contexts[0];
286
-
287
- let page = undefined;
288
-
289
- if (pageContext) {
290
- page = new Page(biDiConnector, pageContext.context);
291
- browser.defaultContext.register(page);
292
- } else {
293
- page = await browser.newPage({ type: "tab" });
294
- }
295
- await page.applyDefaultViewport();
296
- // Requests made before tracking starts are invisible to it, and a dialog nobody watches would hang the page
297
- await page.startNetworkTracking();
298
- await page.startDialogHandling();
299
- await page.startLogging();
300
- await page.startDownloadTracking();
301
- await page.startFileChooserTracking();
302
- browserProc.off("close", onUnexpectedClose);
303
- signal?.removeEventListener("abort", onAbort);
304
- resolve({
305
- browser,
306
- page,
307
- });
308
- } catch (err) {
309
- browserProc.off("close", onUnexpectedClose);
310
- signal?.removeEventListener("abort", onAbort);
311
- abandonLaunch(browserProc, biDiConnector);
312
- reject(err);
313
- }
314
- });
315
- });
316
- }
317
-
318
- /** Closes the browser via BiDi browser.close, then makes sure the process has exited */
319
- public async close() {
320
- if (this.browserProc.exitCode !== null || this.browserProc.signalCode) {
321
- this.biDiConnector.kill();
322
- removeIfEmpty(this.downloadsDir);
323
- return;
324
- }
325
- const exited = new Promise((resolve) =>
326
- this.browserProc.once("exit", resolve),
327
- );
328
- // User contexts outlive the browser in its profile unless they are removed
329
- await Promise.allSettled(
330
- [...this.userContexts.values()].map((context) => context.close()),
331
- );
332
- try {
333
- // Firefox may exit without answering; a lost reply must not hold up teardown
334
- await Promise.race([
335
- this.biDiConnector.send("browser.close", {}),
336
- sleep(BROWSER_CLOSE_GRACE, undefined, { ref: false }),
337
- ]);
338
- } catch {
339
- // browser may already be shutting down
340
- }
341
- this.biDiConnector.kill();
342
- this.browserProc.kill();
343
- await exited;
344
- // A folder nothing was downloaded into would only pile up
345
- removeIfEmpty(this.downloadsDir);
346
- }
347
-
348
- /** Alias of close() */
349
- public async kill() {
350
- await this.close();
351
- }
352
-
353
- public async newPage({
354
- type,
355
- background,
356
- userContext,
357
- }: BiDiCommands["browsingContext.create"]["params"]): Promise<Page> {
358
- const createBrowserContext = await this.biDiConnector.send(
359
- "browsingContext.create",
360
- {
361
- type,
362
- background,
363
- userContext,
364
- },
365
- );
366
- logger.debug("Created browsing context", { createBrowserContext });
367
- const page = new Page(this.biDiConnector, createBrowserContext.context);
368
- this.contextFor(userContext).register(page);
369
- await page.applyDefaultViewport();
370
- await page.startNetworkTracking();
371
- await page.startDialogHandling();
372
- await page.startLogging();
373
- await page.startDownloadTracking();
374
- await page.startFileChooserTracking();
375
- return page;
376
- }
377
-
378
- /**
379
- * Creates an isolated context: its own cookies, storage and cache. `browser.close()` removes the contexts
380
- * that are still open, as Firefox would otherwise keep them in its profile.
381
- * @example
382
- * const alice = await browser.newContext();
383
- * const page = await alice.newPage();
384
- */
385
- public async newContext(
386
- options: ContextOptions = {},
387
- ): Promise<BrowserContext> {
388
- const { acceptInsecureCerts, unhandledPromptBehavior, ...emulation } =
389
- options;
390
- // A wrong value must not leave a context behind, so it is refused before one is created
391
- validateEmulation(emulation);
392
- const { userContext } = await this.biDiConnector.send(
393
- "browser.createUserContext",
394
- {
395
- ...(acceptInsecureCerts !== undefined && { acceptInsecureCerts }),
396
- ...(unhandledPromptBehavior && { unhandledPromptBehavior }),
397
- },
398
- );
399
- const context = new BrowserContext(this.host, userContext, false);
400
- this.userContexts.set(userContext, context);
401
- if (Object.keys(emulation).length > 0) {
402
- try {
403
- await context.emulate(emulation);
404
- } catch (err) {
405
- // Firefox keeps a context in its profile until it is removed
406
- await context
407
- .close()
408
- .catch((closeErr) =>
409
- logger.debug("Could not remove a context whose emulation failed", {
410
- closeErr,
411
- }),
412
- );
413
- throw err;
414
- }
415
- }
416
- return context;
417
- }
418
-
419
- /** The default context, then the contexts this browser created that are still open */
420
- public contexts(): BrowserContext[] {
421
- return [this.defaultContext, ...this.userContexts.values()];
422
- }
423
-
424
- private contextFor(userContext: string | undefined): BrowserContext {
425
- if (userContext === undefined || userContext === "default")
426
- return this.defaultContext;
427
- return this.userContexts.get(userContext) ?? this.adopt(userContext);
428
- }
429
-
430
- /** A user context this browser did not create (Firefox keeps them in its profile): usable, never closed by `close()` */
431
- private adopt(userContext: string): BrowserContext {
432
- const context = new BrowserContext(this.host, userContext, false);
433
- this.userContexts.set(userContext, context);
434
- return context;
435
- }
436
-
437
- public async getCookie(filter: CookieFilter, partition: PartitionDescriptor) {
438
- return await this.biDiConnector.send("storage.getCookies", {
439
- filter,
440
- partition,
441
- });
442
- }
443
-
444
- public async setCookie(
445
- cookie: PartialCookie,
446
- partition?: PartitionDescriptor,
447
- ) {
448
- return await this.biDiConnector.send("storage.setCookie", {
449
- partition,
450
- cookie,
451
- });
452
- }
453
- }
454
-
455
- /** A config value, or undefined when it is not set or the config cannot be read */
456
- async function readOptionalConfig<K extends "downloadsDir">(key: K) {
457
- try {
458
- return await readConfig(key);
459
- } catch (err) {
460
- logger.debug("Could not read %s from config, using the default", key, {
461
- err,
462
- });
463
- return undefined;
464
- }
465
- }
466
-
467
- /** The configured BiDi command timeout; a missing or unreadable config falls back to the connector default */
468
- async function readCommandTimeout() {
469
- try {
470
- return (await readConfig("bidi"))?.commandTimeout;
471
- } catch (err) {
472
- logger.debug("Could not read bidi config, using defaults", { err });
473
- return undefined;
474
- }
475
- }
476
-
477
- /** Writes the browser's user.js unless it already holds the current prefs; an outdated one is replaced */
478
- export function ensureBrowserProfile(
479
- browserName: SupportedBrowser,
480
- profilePath: string | undefined = browserProfilePath[browserName],
481
- ) {
482
- logger.verbose("Checking browser profile for: %s", browserName);
483
- if (!profilePath) {
484
- logger.verbose("No browser profile path declared, skipping...");
485
- return;
486
- }
487
- if (existsSync(profilePath)) {
488
- if (readFileSync(profilePath, "utf8") === browserProfiles[browserName]) {
489
- logger.verbose("Browser profile is up to date at: %s", profilePath);
490
- return;
491
- }
492
- logger.verbose("Browser profile at %s is outdated, rewriting it", profilePath);
493
- } else {
494
- logger.verbose("No browser profile created yet for %s", browserName);
495
- }
496
-
497
- createBrowserProfile(browserName, profilePath);
498
- }
499
-
500
- function createBrowserProfile(
501
- browserName: SupportedBrowser,
502
- profilePath: string,
503
- ) {
504
- logger.verbose(
505
- "Creating browser profile for %s at %s",
506
- browserName,
507
- profilePath,
508
- );
509
- mkdirSync(path.dirname(profilePath), { recursive: true });
510
-
511
- writeFileSync(profilePath, browserProfiles[browserName], {
512
- encoding: "utf8",
513
- });
514
- logger.verbose("Browser profile created");
515
- }
@@ -1,86 +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 { ContextTree } from "./context-tree.js";
6
-
7
- /** Answers subscribe/unsubscribe and getTree commands as they come in */
8
- function setup(tree: object[] = [{ context: "root", parent: null, children: [{ context: "frame", parent: "root", children: [] }] }]) {
9
- const ws = new FakeWebSocket();
10
- const connector = new BiDiConnector(ws as unknown as WebSocket);
11
- let answered = 0;
12
- const pump = setInterval(() => {
13
- while (answered < ws.sent.length) {
14
- const message = ws.sent[answered++]!;
15
- if (message.method === "session.subscribe") ws.reply(message.id, { subscription: `s${answered}` });
16
- else if (message.method === "browsingContext.getTree") ws.reply(message.id, { contexts: tree });
17
- else ws.reply(message.id);
18
- }
19
- }, 1).unref();
20
- return { ws, connector, stop: () => clearInterval(pump) };
21
- }
22
-
23
- describe("ContextTree", () => {
24
- it("reads the initial tree, including nested frames", async () => {
25
- const { connector, stop } = setup();
26
- const tree = await ContextTree.create(connector);
27
- assert.equal(tree.parentOf("frame"), "root");
28
- assert.equal(tree.parentOf("root"), null);
29
- assert.equal(tree.rootOf("frame"), "root");
30
- assert.equal(tree.isWithin("frame", "root"), true);
31
- assert.equal(tree.isWithin("root", "root"), true);
32
- await tree.dispose();
33
- stop();
34
- });
35
-
36
- it("unknown contexts are not within anything", async () => {
37
- const { connector, stop } = setup();
38
- const tree = await ContextTree.create(connector);
39
- assert.equal(tree.isWithin("nope", "root"), false);
40
- assert.equal(tree.parentOf("nope"), null);
41
- assert.equal(tree.rootOf("nope"), "nope");
42
- await tree.dispose();
43
- stop();
44
- });
45
-
46
- it("tracks created contexts, also deeper than one level", async () => {
47
- const { ws, connector, stop } = setup();
48
- const tree = await ContextTree.create(connector);
49
- ws.emitEvent("browsingContext.contextCreated", { context: "grandchild", parent: "frame", children: [] });
50
- ws.emitEvent("browsingContext.contextCreated", { context: "other", parent: null, children: [] });
51
- assert.equal(tree.rootOf("grandchild"), "root");
52
- assert.equal(tree.isWithin("grandchild", "root"), true);
53
- assert.equal(tree.isWithin("other", "root"), false);
54
- await tree.dispose();
55
- stop();
56
- });
57
-
58
- it("still answers for destroyed contexts (late events)", async () => {
59
- const { ws, connector, stop } = setup();
60
- const tree = await ContextTree.create(connector);
61
- ws.emitEvent("browsingContext.contextDestroyed", { context: "frame", parent: "root", children: [] });
62
- assert.equal(tree.isWithin("frame", "root"), true);
63
- await tree.dispose();
64
- stop();
65
- });
66
-
67
- it("dispose removes listeners and unsubscribes", async () => {
68
- const { ws, connector, stop } = setup();
69
- const tree = await ContextTree.create(connector);
70
- await tree.dispose();
71
- await tick(5);
72
- ws.emitEvent("browsingContext.contextCreated", { context: "late", parent: "root", children: [] });
73
- assert.equal(tree.parentOf("late"), null);
74
- assert.equal(ws.sent.filter((m) => m.method === "session.unsubscribe").length, 2);
75
- stop();
76
- });
77
-
78
- it("subscribes before reading the tree so no creation is missed", async () => {
79
- const { ws, connector, stop } = setup();
80
- const tree = await ContextTree.create(connector);
81
- const order = ws.sent.map((m) => m.method).filter((m) => m !== "session.unsubscribe");
82
- assert.deepEqual(order, ["session.subscribe", "session.subscribe", "browsingContext.getTree"]);
83
- await tree.dispose();
84
- stop();
85
- });
86
- });
@@ -1,70 +0,0 @@
1
- import type { BiDiConnector } from "../transport/bidi-connection.js";
2
- import type { Subscription } from "../transport/subscriptions.js";
3
- import type { BrowsingContext, Info } from "../types/bidi-modules/browsing-context.js";
4
-
5
- /**
6
- * Tracks which browsing contexts (iframes) belong to which top-level context, so events that
7
- * carry a child context id can be attributed to the page that owns it.
8
- * Destroyed contexts stay known: late events for them must still resolve to their page.
9
- */
10
- export class ContextTree {
11
- private parents = new Map<BrowsingContext, BrowsingContext | null>();
12
-
13
- private constructor(
14
- private connector: BiDiConnector,
15
- private subscription: Subscription,
16
- ) {}
17
-
18
- public static async create(connector: BiDiConnector): Promise<ContextTree> {
19
- // Subscribe first: a context created between getTree and subscribing would be missed
20
- const subscription = await connector.subscribe([
21
- "browsingContext.contextCreated",
22
- "browsingContext.contextDestroyed",
23
- ]);
24
- const tree = new ContextTree(connector, subscription);
25
- connector.onEvent("browsingContext.contextCreated", tree.onContext);
26
- connector.onEvent("browsingContext.contextDestroyed", tree.onContext);
27
- try {
28
- const { contexts } = await connector.send("browsingContext.getTree", {});
29
- contexts.forEach((info) => tree.add(info));
30
- } catch (err) {
31
- await tree.dispose();
32
- throw err;
33
- }
34
- return tree;
35
- }
36
-
37
- private onContext = (info: Info) => this.add(info);
38
-
39
- private add(info: Info) {
40
- this.parents.set(info.context, info.parent ?? null);
41
- info.children?.forEach((child) => this.add(child));
42
- }
43
-
44
- /** The direct parent, or null for a top-level or unknown context */
45
- public parentOf(context: BrowsingContext): BrowsingContext | null {
46
- return this.parents.get(context) ?? null;
47
- }
48
-
49
- /** The top-level context that contains `context`; an unknown context is its own root */
50
- public rootOf(context: BrowsingContext): BrowsingContext {
51
- let current = context;
52
- for (let parent = this.parentOf(current); parent !== null; parent = this.parentOf(current)) {
53
- current = parent;
54
- }
55
- return current;
56
- }
57
-
58
- /** True when `context` is `root` or one of its descendants; false for unknown contexts */
59
- public isWithin(context: BrowsingContext, root: BrowsingContext): boolean {
60
- if (context === root) return true;
61
- return this.parents.has(context) && this.rootOf(context) === root;
62
- }
63
-
64
- public async dispose(): Promise<void> {
65
- this.connector.offEvent("browsingContext.contextCreated", this.onContext);
66
- this.connector.offEvent("browsingContext.contextDestroyed", this.onContext);
67
- this.parents.clear();
68
- await this.subscription.unsubscribe();
69
- }
70
- }