@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,256 +0,0 @@
1
- import EventEmitter from "node:events";
2
- import type { BiDiCommands, BiDiEvents } from "../types/bidi.js";
3
- import logger from "../logger/index.js";
4
- import { WaitTimeoutError } from "../wait/wait-until.js";
5
- import { BiDiError } from "./bidi-error.js";
6
- import {
7
- SubscriptionManager,
8
- type EventName,
9
- type Subscription,
10
- type SubscribeOptions,
11
- } from "./subscriptions.js";
12
-
13
- /** Time (ms) a command may wait for its reply before it fails with code "timeout" */
14
- const DEFAULT_COMMAND_TIMEOUT = 30000;
15
-
16
- export interface ConnectorOptions {
17
- /** @default 30000 */
18
- commandTimeout?: number;
19
- }
20
-
21
- export interface SendOptions {
22
- /** Overrides the connector's commandTimeout for this command */
23
- timeout?: number;
24
- }
25
-
26
- interface BiDiMessage {
27
- id: number;
28
- method: string;
29
- params?: object | undefined;
30
- }
31
-
32
- export class BiDiConnector {
33
- private resolveMap: Map<
34
- number,
35
- {
36
- method: string;
37
- resolve: <M extends keyof BiDiCommands>(
38
- value: BiDiCommands[M]["result"],
39
- ) => void;
40
- reject: (reason: unknown) => void;
41
- }
42
- > = new Map();
43
- private currentId: number = 0;
44
- private webSocket: WebSocket;
45
- private eventEmitter: EventEmitter;
46
- private commandTimeout: number;
47
- private subscriptions: SubscriptionManager;
48
-
49
- constructor(ws: WebSocket, options: ConnectorOptions = {}) {
50
- this.commandTimeout = options.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT;
51
- this.subscriptions = new SubscriptionManager({
52
- subscribe: (params) => this.send("session.subscribe", params),
53
- unsubscribe: (params) => this.send("session.unsubscribe", params),
54
- });
55
- this.webSocket = ws;
56
- this.webSocket.addEventListener("message", this.messageListener);
57
- this.webSocket.addEventListener("close", this.onWebsocketClose);
58
- this.webSocket.addEventListener("error", this.onWebsocketError);
59
- this.eventEmitter = new EventEmitter();
60
- console.info("BiDiConnector: Websocket handshake finished");
61
- }
62
-
63
- public static async connect(url: string, options?: ConnectorOptions) {
64
- console.log("Connecting to WebSocket");
65
- const ws = new WebSocket(url);
66
- return new Promise<BiDiConnector>((resolve, reject) => {
67
- console.log("Waiting for WebSocket handshake");
68
- ws.addEventListener("open", () => {
69
- console.log("WebSocket handshake finished");
70
- resolve(new BiDiConnector(ws, options));
71
- });
72
- ws.addEventListener("error", (err) => {
73
- console.error(err);
74
- reject(new Error("WebSocket connection failed"));
75
- });
76
- });
77
- }
78
-
79
- private onWebsocketClose = () => {
80
- this.rejectAll(
81
- (pending) => new Error(`Websocket closed${pendingSuffix(pending)}`),
82
- );
83
- };
84
-
85
- private onWebsocketError = (ev: Event) => {
86
- this.rejectAll((pending) =>
87
- Object.assign(new Error(`Websocket error${pendingSuffix(pending)}`), {
88
- cause: ev,
89
- }),
90
- );
91
- };
92
-
93
- /** Rejects every pending command; `reason` gets the pending method names for its message */
94
- private rejectAll(reason: (pendingMethods: string[]) => unknown) {
95
- const entries = [...this.resolveMap.values()];
96
- const error = reason(entries.map((entry) => entry.method));
97
- this.resolveMap.clear();
98
- entries.forEach(({ reject }) => reject(error));
99
- }
100
-
101
- private getId() {
102
- return this.currentId++;
103
- }
104
-
105
- public send<M extends keyof BiDiCommands>(
106
- method: M,
107
- params?: BiDiCommands[M]["params"],
108
- options: SendOptions = {},
109
- ): Promise<BiDiCommands[M]["result"]> {
110
- return new Promise((resolve, reject) => {
111
- try {
112
- const id = this.getId();
113
- const timeout = options.timeout ?? this.commandTimeout;
114
- const timer = setTimeout(() => {
115
- this.resolveMap.delete(id);
116
- reject(
117
- new BiDiError(method, "timeout", `no reply within ${timeout}ms`),
118
- );
119
- }, timeout);
120
- this.resolveMap.set(id, {
121
- method,
122
- resolve: (value) => {
123
- clearTimeout(timer);
124
- resolve(value);
125
- },
126
- reject: (reason) => {
127
- clearTimeout(timer);
128
- reject(reason);
129
- },
130
- });
131
- const commandBody: BiDiMessage = {
132
- id,
133
- method,
134
- params,
135
- };
136
- logger.debug("Sending message to BiDi Websocket", commandBody);
137
- this.webSocket.send(JSON.stringify(commandBody));
138
- } catch (err: unknown) {
139
- reject(err);
140
- }
141
- });
142
- }
143
-
144
- /**
145
- * Subscribes to events. Unscoped subscriptions are refcounted per event, so features can
146
- * subscribe independently; scoped ones (`contexts`) always send their own command.
147
- */
148
- public subscribe(
149
- events: EventName[],
150
- options?: SubscribeOptions,
151
- ): Promise<Subscription> {
152
- return this.subscriptions.subscribe(events, options);
153
- }
154
-
155
- /**
156
- * Resolves with the params of the first `event` for which `predicate` is true.
157
- * The listener is registered before this returns, so start the triggering command afterwards.
158
- * Rejects with WaitTimeoutError after `timeout` ms, or with the abort reason.
159
- */
160
- public waitForEvent<E extends keyof BiDiEvents>(
161
- event: E,
162
- predicate: (params: BiDiEvents[E]["params"]) => boolean,
163
- options: { timeout: number; signal?: AbortSignal },
164
- ): Promise<BiDiEvents[E]["params"]> {
165
- const { timeout, signal } = options;
166
- return new Promise((resolve, reject) => {
167
- if (signal?.aborted) {
168
- reject(signal.reason);
169
- return;
170
- }
171
- const cleanup = () => {
172
- clearTimeout(timer);
173
- this.offEvent(event, listener);
174
- signal?.removeEventListener("abort", onAbort);
175
- };
176
- const listener = (params: BiDiEvents[E]["params"]) => {
177
- if (!predicate(params)) return;
178
- cleanup();
179
- resolve(params);
180
- };
181
- const onAbort = () => {
182
- cleanup();
183
- reject(signal?.reason);
184
- };
185
- const timer = setTimeout(() => {
186
- cleanup();
187
- reject(new WaitTimeoutError<undefined>(timeout, undefined));
188
- }, timeout);
189
- this.onEvent(event, listener);
190
- signal?.addEventListener("abort", onAbort, { once: true });
191
- });
192
- }
193
-
194
- /** Register an event listener */
195
- public onEvent<E extends keyof BiDiEvents>(
196
- event: E,
197
- listener: (params: BiDiEvents[E]["params"]) => void,
198
- ): void {
199
- this.eventEmitter.on(event, listener);
200
- }
201
-
202
- /** Remove an event listener */
203
- public offEvent<E extends keyof BiDiEvents>(
204
- event: E,
205
- listener: (params: BiDiEvents[E]["params"]) => void,
206
- ): void {
207
- this.eventEmitter.off(event, listener);
208
- }
209
-
210
- private messageListener = (ev: MessageEvent) => {
211
- const message = JSON.parse(ev.data);
212
- logger.debug("Received message from BiDi Websocket: ", message);
213
- if (message.type === "success" && message.id !== undefined) {
214
- const targetToResolve = this.resolveMap.get(message.id);
215
- if (targetToResolve) {
216
- this.resolveMap.delete(message.id);
217
- targetToResolve.resolve(message.result);
218
- }
219
- } else if (message.type === "error") {
220
- const target =
221
- message.id === null || message.id === undefined
222
- ? undefined
223
- : this.resolveMap.get(message.id);
224
- if (!target) {
225
- logger.debug("BiDi error reply without a pending command", message);
226
- return;
227
- }
228
- this.resolveMap.delete(message.id);
229
- target.reject(
230
- new BiDiError(
231
- target.method,
232
- message.error,
233
- message.message,
234
- message.stacktrace,
235
- ),
236
- );
237
- } else if (message.type === "event") {
238
- this.eventEmitter.emit(message.method, message.params);
239
- }
240
- };
241
-
242
- public kill() {
243
- this.webSocket.removeEventListener("message", this.messageListener);
244
- this.webSocket.removeEventListener("close", this.onWebsocketClose);
245
- this.webSocket.removeEventListener("error", this.onWebsocketError);
246
- this.rejectAll(
247
- (pending) => new Error(`BiDiConnector killed${pendingSuffix(pending)}`),
248
- );
249
- this.eventEmitter.removeAllListeners();
250
- this.webSocket.close();
251
- }
252
- }
253
-
254
- function pendingSuffix(pending: string[]) {
255
- return pending.length > 0 ? ` (pending: ${pending.join(", ")})` : "";
256
- }
@@ -1,23 +0,0 @@
1
- import type { ErrorCode } from "../types/bidi-protocols/error.js";
2
-
3
- /** A failed BiDi command: an error reply from the browser, or `"timeout"` when none came in time */
4
- export class BiDiError extends Error {
5
- /** BiDi error code such as "no such node", or "timeout" */
6
- public readonly code: ErrorCode | "timeout";
7
- /** The command that failed, e.g. "script.callFunction" */
8
- public readonly command: string;
9
- public readonly remoteStack?: string | undefined;
10
-
11
- constructor(
12
- command: string,
13
- code: ErrorCode | "timeout",
14
- message: string,
15
- remoteStack?: string,
16
- ) {
17
- super(`${command} failed (${code}): ${message}`);
18
- this.name = "BiDiError";
19
- this.command = command;
20
- this.code = code;
21
- this.remoteStack = remoteStack;
22
- }
23
- }
@@ -1,73 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, test } from "node:test";
3
- import { Browser } from "../browser/browser.js";
4
- import type Page from "../browser/page.js";
5
- import { BiDiError } from "./bidi-error.js";
6
-
7
- let browser: Browser;
8
- let page: Page;
9
-
10
- before(async () => {
11
- ({ browser, page } = await Browser.launch("firefox", { port: 9232, headless: true }));
12
- await page.navigateTo("about:blank", "complete");
13
- });
14
-
15
- after(async () => {
16
- await browser?.close();
17
- });
18
-
19
- const OPTIONS = { timeout: 15000 };
20
-
21
- test("newPage returns a page that can navigate", OPTIONS, async () => {
22
- const created = await browser.newPage({ type: "tab" });
23
- assert.notEqual(created.contextId, "");
24
- assert.notEqual(created.contextId, page.contextId);
25
- await created.navigateTo("about:blank", "complete");
26
- });
27
-
28
- test("a failing command rejects with the BiDi error code instead of hanging", OPTIONS, async () => {
29
- const connector = (page as unknown as { biDiConnector: import("./bidi-connection.js").BiDiConnector })
30
- .biDiConnector;
31
- await assert.rejects(
32
- connector.send("script.evaluate", {
33
- expression: "1",
34
- awaitPromise: false,
35
- target: { context: "does-not-exist" },
36
- }),
37
- (err: unknown) => err instanceof BiDiError && err.code === "no such frame",
38
- );
39
- });
40
-
41
- test("context tree attributes iframe contexts to the page", OPTIONS, async () => {
42
- await page.navigateTo("about:blank", "complete");
43
- const tree = await page.tree();
44
- const connector = (page as unknown as { biDiConnector: import("./bidi-connection.js").BiDiConnector })
45
- .biDiConnector;
46
- const created = connector.waitForEvent("browsingContext.contextCreated", (info) => info.parent === page.contextId, {
47
- timeout: 5000,
48
- });
49
- await connector.send("script.evaluate", {
50
- expression: `document.body.appendChild(document.createElement("iframe")).src = "about:blank"; 1`,
51
- awaitPromise: false,
52
- target: { context: page.contextId },
53
- });
54
- const frame = await created;
55
- assert.equal(tree.isWithin(frame.context, page.contextId), true);
56
- assert.equal(tree.rootOf(frame.context), page.contextId);
57
- });
58
-
59
- test("refcounted subscriptions deliver events to every subscriber", OPTIONS, async () => {
60
- const connector = (page as unknown as { biDiConnector: import("./bidi-connection.js").BiDiConnector })
61
- .biDiConnector;
62
- const a = await connector.subscribe(["browsingContext.load"]);
63
- const b = await connector.subscribe(["browsingContext.load"]);
64
- const seen = connector.waitForEvent("browsingContext.load", (p) => p.context === page.contextId, { timeout: 5000 });
65
- await page.navigateTo("about:blank?again", "none");
66
- await seen;
67
- await a.unsubscribe();
68
- // b still holds the subscription, so events keep flowing
69
- const again = connector.waitForEvent("browsingContext.load", (p) => p.context === page.contextId, { timeout: 5000 });
70
- await page.navigateTo("about:blank?third", "none");
71
- await again;
72
- await b.unsubscribe();
73
- });
@@ -1,140 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { tick } from "../testing/fake-websocket.js";
4
- import { SubscriptionManager, type SessionCommands } from "./subscriptions.js";
5
-
6
- function setup(options: { failSubscribe?: boolean; delay?: number } = {}) {
7
- const calls: Array<{ command: "subscribe" | "unsubscribe"; params: any }> = [];
8
- let next = 0;
9
- const commands: SessionCommands = {
10
- async subscribe(params) {
11
- calls.push({ command: "subscribe", params });
12
- await tick(options.delay ?? 0);
13
- if (options.failSubscribe) throw new Error("subscribe failed");
14
- return { subscription: `s${++next}` };
15
- },
16
- async unsubscribe(params) {
17
- calls.push({ command: "unsubscribe", params });
18
- await tick(options.delay ?? 0);
19
- return {};
20
- },
21
- };
22
- return { calls, manager: new SubscriptionManager(commands) };
23
- }
24
-
25
- describe("SubscriptionManager", () => {
26
- it("shares one subscribe between concurrent first subscribers", async () => {
27
- const { calls, manager } = setup({ delay: 5 });
28
- const [a, b] = await Promise.all([
29
- manager.subscribe(["browsingContext.load"]),
30
- manager.subscribe(["browsingContext.load"]),
31
- ]);
32
- assert.equal(calls.filter((c) => c.command === "subscribe").length, 1);
33
- await a.unsubscribe();
34
- await b.unsubscribe();
35
- assert.deepEqual(calls[calls.length - 1], { command: "unsubscribe", params: { subscriptions: ["s1"] } });
36
- });
37
-
38
- it("subscribes each event separately and releases them together", async () => {
39
- const { calls, manager } = setup();
40
- const sub = await manager.subscribe(["network.beforeRequestSent", "network.fetchError"]);
41
- assert.deepEqual(
42
- calls.map((c) => c.params.events),
43
- [["network.beforeRequestSent"], ["network.fetchError"]],
44
- );
45
- await sub.unsubscribe();
46
- assert.equal(calls.filter((c) => c.command === "unsubscribe").length, 2);
47
- });
48
-
49
- it("keeps an event subscribed while another subscriber still needs it", async () => {
50
- const { calls, manager } = setup();
51
- const a = await manager.subscribe(["network.fetchError", "network.responseCompleted"]);
52
- const b = await manager.subscribe(["network.fetchError"]);
53
- await a.unsubscribe();
54
- // only responseCompleted was released
55
- assert.deepEqual(
56
- calls.filter((c) => c.command === "unsubscribe").map((c) => c.params.subscriptions),
57
- [["s2"]],
58
- );
59
- await b.unsubscribe();
60
- assert.equal(calls.filter((c) => c.command === "unsubscribe").length, 2);
61
- });
62
-
63
- it("keeps the subscription when a new subscriber arrives while the last one is leaving", async () => {
64
- const { calls, manager } = setup({ delay: 5 });
65
- const first = await manager.subscribe(["log.entryAdded"]);
66
- const leaving = first.unsubscribe();
67
- const again = manager.subscribe(["log.entryAdded"]); // arrives before the queued unsubscribe runs
68
- const second = await again;
69
- await leaving;
70
- assert.deepEqual(calls.map((c) => c.command), ["subscribe"]);
71
- await second.unsubscribe();
72
- assert.deepEqual(calls.map((c) => c.command), ["subscribe", "unsubscribe"]);
73
- });
74
-
75
- it("resubscribes when a subscriber arrives after the last one fully left", async () => {
76
- const { calls, manager } = setup();
77
- const first = await manager.subscribe(["log.entryAdded"]);
78
- await first.unsubscribe();
79
- const again = await manager.subscribe(["log.entryAdded"]);
80
- assert.deepEqual(calls.map((c) => c.command), ["subscribe", "unsubscribe", "subscribe"]);
81
- await again.unsubscribe();
82
- });
83
-
84
- it("unsubscribe is idempotent", async () => {
85
- const { calls, manager } = setup();
86
- const a = await manager.subscribe(["log.entryAdded"]);
87
- const b = await manager.subscribe(["log.entryAdded"]);
88
- await a.unsubscribe();
89
- await a.unsubscribe();
90
- assert.equal(calls.filter((c) => c.command === "unsubscribe").length, 0); // b still holds it
91
- await b.unsubscribe();
92
- assert.equal(calls.filter((c) => c.command === "unsubscribe").length, 1);
93
- });
94
-
95
- it("scoped subscriptions send their own command and are not shared", async () => {
96
- const { calls, manager } = setup();
97
- const a = await manager.subscribe(["log.entryAdded"], { contexts: ["c1"] });
98
- const b = await manager.subscribe(["log.entryAdded"], { contexts: ["c1"] });
99
- assert.equal(calls.filter((c) => c.command === "subscribe").length, 2);
100
- assert.deepEqual(calls[0]!.params, { events: ["log.entryAdded"], contexts: ["c1"] });
101
- await a.unsubscribe();
102
- await a.unsubscribe();
103
- assert.deepEqual(
104
- calls.filter((c) => c.command === "unsubscribe").map((c) => c.params.subscriptions),
105
- [["s1"]],
106
- );
107
- await b.unsubscribe();
108
- });
109
-
110
- it("a failed subscribe rolls back and lets a later subscribe try again", async () => {
111
- const { calls, manager } = setup({ failSubscribe: true });
112
- await assert.rejects(manager.subscribe(["log.entryAdded"]), /subscribe failed/);
113
- await assert.rejects(manager.subscribe(["log.entryAdded"]), /subscribe failed/);
114
- assert.equal(calls.filter((c) => c.command === "subscribe").length, 2);
115
- assert.equal(calls.filter((c) => c.command === "unsubscribe").length, 0);
116
- });
117
-
118
- it("rolls back events already acquired when a later one fails", async () => {
119
- const calls: Array<{ command: string; params: any }> = [];
120
- let count = 0;
121
- const flaky = new SubscriptionManager({
122
- async subscribe(params) {
123
- calls.push({ command: "subscribe", params });
124
- if (++count === 2) throw new Error("second failed");
125
- return { subscription: "ok" };
126
- },
127
- async unsubscribe(params) {
128
- calls.push({ command: "unsubscribe", params });
129
- return {};
130
- },
131
- });
132
- await assert.rejects(
133
- flaky.subscribe(["network.fetchError", "network.responseCompleted"]),
134
- /second failed/,
135
- );
136
- assert.deepEqual(calls.filter((c) => c.command === "unsubscribe").map((c) => c.params), [
137
- { subscriptions: ["ok"] },
138
- ]);
139
- });
140
- });
@@ -1,110 +0,0 @@
1
- import type { BiDiEvents } from "../types/bidi.js";
2
-
3
- export type EventName = keyof BiDiEvents;
4
-
5
- export interface Subscription {
6
- /** Idempotent */
7
- unsubscribe(): Promise<void>;
8
- }
9
-
10
- export interface SubscribeOptions {
11
- /** Scope to these browsing contexts. Scoped subscriptions are never shared */
12
- contexts?: string[];
13
- }
14
-
15
- /** Sends the two session commands; the connector provides it */
16
- export interface SessionCommands {
17
- subscribe(params: { events: string[]; contexts?: string[] }): Promise<{ subscription: string }>;
18
- unsubscribe(params: { subscriptions: string[] }): Promise<unknown>;
19
- }
20
-
21
- interface Shared {
22
- count: number;
23
- id: string | undefined;
24
- /** Serialises subscribe/unsubscribe commands for this event so they never interleave */
25
- chain: Promise<void>;
26
- }
27
-
28
- /**
29
- * Global (unscoped) subscriptions are refcounted per event name: the first subscriber sends
30
- * `session.subscribe`, the last one leaving sends `session.unsubscribe`.
31
- */
32
- export class SubscriptionManager {
33
- private shared = new Map<EventName, Shared>();
34
-
35
- constructor(private commands: SessionCommands) {}
36
-
37
- public async subscribe(events: EventName[], options: SubscribeOptions = {}): Promise<Subscription> {
38
- if (options.contexts) return this.subscribeScoped(events, options.contexts);
39
-
40
- const acquired: EventName[] = [];
41
- try {
42
- for (const event of events) {
43
- await this.acquire(event);
44
- acquired.push(event);
45
- }
46
- } catch (err) {
47
- await Promise.allSettled(acquired.map((event) => this.release(event)));
48
- throw err;
49
- }
50
-
51
- let released = false;
52
- return {
53
- unsubscribe: async () => {
54
- if (released) return;
55
- released = true;
56
- await Promise.all(acquired.map((event) => this.release(event)));
57
- },
58
- };
59
- }
60
-
61
- private async subscribeScoped(events: EventName[], contexts: string[]): Promise<Subscription> {
62
- const { subscription } = await this.commands.subscribe({ events, contexts });
63
- let released = false;
64
- return {
65
- unsubscribe: async () => {
66
- if (released) return;
67
- released = true;
68
- await this.commands.unsubscribe({ subscriptions: [subscription] });
69
- },
70
- };
71
- }
72
-
73
- private state(event: EventName): Shared {
74
- let state = this.shared.get(event);
75
- if (!state) {
76
- state = { count: 0, id: undefined, chain: Promise.resolve() };
77
- this.shared.set(event, state);
78
- }
79
- return state;
80
- }
81
-
82
- private acquire(event: EventName): Promise<void> {
83
- const state = this.state(event);
84
- state.count++;
85
- const step = state.chain.then(async () => {
86
- if (state.count > 0 && state.id === undefined) {
87
- state.id = (await this.commands.subscribe({ events: [event] })).subscription;
88
- }
89
- });
90
- state.chain = step.catch(() => {});
91
- return step.catch((err) => {
92
- state.count--;
93
- throw err;
94
- });
95
- }
96
-
97
- private release(event: EventName): Promise<void> {
98
- const state = this.state(event);
99
- state.count--;
100
- const step = state.chain.then(async () => {
101
- if (state.count === 0 && state.id !== undefined) {
102
- const id = state.id;
103
- state.id = undefined;
104
- await this.commands.unsubscribe({ subscriptions: [id] });
105
- }
106
- });
107
- state.chain = step.catch(() => {});
108
- return step;
109
- }
110
- }
@@ -1,55 +0,0 @@
1
- // Compile-time check, run by `tsc --noEmit`: every command the BiDi specs rely on is typed.
2
- import type { BiDiCommands } from "./bidi.js";
3
-
4
- type Expect<T extends true> = T;
5
- type HasCommand<M extends string> = M extends keyof BiDiCommands
6
- ? BiDiCommands[M] extends { params: unknown; result: unknown }
7
- ? true
8
- : false
9
- : false;
10
-
11
- type Commands = [
12
- Expect<HasCommand<"script.callFunction">>,
13
- Expect<HasCommand<"script.disown">>,
14
- Expect<HasCommand<"script.addPreloadScript">>,
15
- Expect<HasCommand<"script.removePreloadScript">>,
16
- Expect<HasCommand<"browsingContext.locateNodes">>,
17
- Expect<HasCommand<"browsingContext.handleUserPrompt">>,
18
- Expect<HasCommand<"browsingContext.setViewport">>,
19
- Expect<HasCommand<"browsingContext.reload">>,
20
- Expect<HasCommand<"browsingContext.traverseHistory">>,
21
- Expect<HasCommand<"browsingContext.close">>,
22
- Expect<HasCommand<"browsingContext.print">>,
23
- Expect<HasCommand<"input.setFiles">>,
24
- Expect<HasCommand<"network.addIntercept">>,
25
- Expect<HasCommand<"network.removeIntercept">>,
26
- Expect<HasCommand<"network.continueRequest">>,
27
- Expect<HasCommand<"network.continueResponse">>,
28
- Expect<HasCommand<"network.continueWithAuth">>,
29
- Expect<HasCommand<"network.provideResponse">>,
30
- Expect<HasCommand<"network.failRequest">>,
31
- Expect<HasCommand<"network.setCacheBehavior">>,
32
- Expect<HasCommand<"network.addDataCollector">>,
33
- Expect<HasCommand<"network.removeDataCollector">>,
34
- Expect<HasCommand<"network.getData">>,
35
- Expect<HasCommand<"network.disownData">>,
36
- Expect<HasCommand<"browser.createUserContext">>,
37
- Expect<HasCommand<"browser.removeUserContext">>,
38
- Expect<HasCommand<"browser.getUserContexts">>,
39
- Expect<HasCommand<"browser.setDownloadBehavior">>,
40
- Expect<HasCommand<"session.unsubscribe">>,
41
- Expect<HasCommand<"emulation.setForcedColorsModeThemeOverride">>,
42
- Expect<HasCommand<"emulation.setGeolocationOverride">>,
43
- Expect<HasCommand<"emulation.setLocaleOverride">>,
44
- Expect<HasCommand<"emulation.setNetworkConditions">>,
45
- Expect<HasCommand<"emulation.setScreenOrientationOverride">>,
46
- Expect<HasCommand<"emulation.setScreenSettingsOverride">>,
47
- Expect<HasCommand<"emulation.setScriptingEnabled">>,
48
- Expect<HasCommand<"emulation.setScrollbarTypeOverride">>,
49
- Expect<HasCommand<"emulation.setTimezoneOverride">>,
50
- Expect<HasCommand<"emulation.setTouchOverride">>,
51
- Expect<HasCommand<"emulation.setUserAgentOverride">>,
52
- Expect<HasCommand<"permissions.setPermission">>,
53
- ];
54
-
55
- export type { Commands };