@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,200 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import { UnsupportedOperationError } from "../locator/selector-errors.js";
4
- import { autoReply, FakeWebSocket } from "../testing/fake-websocket.js";
5
- import { BiDiConnector } from "../transport/bidi-connection.js";
6
- import { applyEmulation, EmulationError, type EmulationOptions } from "./emulation.js";
7
-
8
- const stops: Array<() => void> = [];
9
- afterEach(() => stops.splice(0).forEach((stop) => stop()));
10
-
11
- const failing = (code: string, message = "nope") => Object.assign(new Error(message), { code });
12
-
13
- function setup(handlers: Record<string, (params: any) => object | Error> = {}) {
14
- const ws = new FakeWebSocket();
15
- const connector = new BiDiConnector(ws as unknown as WebSocket);
16
- stops.push(autoReply(ws, handlers));
17
- const sent = () => ws.sent.map((m) => [m.method, m.params] as [string, any]);
18
- return { ws, connector, sent };
19
- }
20
- const PAGE = { contexts: ["ctx-1"] };
21
-
22
- describe("applyEmulation: what each option sends", () => {
23
- const cases: Array<[string, EmulationOptions, string, object]> = [
24
- ["locale", { locale: "nl-NL" }, "emulation.setLocaleOverride", { locale: "nl-NL" }],
25
- ["timezone", { timezone: "Europe/Amsterdam" }, "emulation.setTimezoneOverride", { timezone: "Europe/Amsterdam" }],
26
- ["userAgent", { userAgent: "Samurai/1.0" }, "emulation.setUserAgentOverride", { userAgent: "Samurai/1.0" }],
27
- [
28
- "geolocation",
29
- { geolocation: { latitude: 52.37, longitude: 4.9, accuracy: 10 } },
30
- "emulation.setGeolocationOverride",
31
- { coordinates: { latitude: 52.37, longitude: 4.9, accuracy: 10 } },
32
- ],
33
- [
34
- "geolocation without accuracy",
35
- { geolocation: { latitude: 1, longitude: 2 } },
36
- "emulation.setGeolocationOverride",
37
- { coordinates: { latitude: 1, longitude: 2 } },
38
- ],
39
- ["offline", { offline: true }, "emulation.setNetworkConditions", { networkConditions: { type: "offline" } }],
40
- [
41
- "orientation",
42
- { orientation: "portrait" },
43
- "emulation.setScreenOrientationOverride",
44
- { screenOrientation: { natural: "portrait", type: "portrait-primary" } },
45
- ],
46
- [
47
- "orientation (full type)",
48
- { orientation: "landscape-secondary" },
49
- "emulation.setScreenOrientationOverride",
50
- { screenOrientation: { natural: "landscape", type: "landscape-secondary" } },
51
- ],
52
- ["screen", { screen: { width: 800, height: 600 } }, "emulation.setScreenSettingsOverride", { screenArea: { width: 800, height: 600 } }],
53
- ["touch", { touch: 5 }, "emulation.setTouchOverride", { maxTouchPoints: 5 }],
54
- ["javaScriptEnabled false", { javaScriptEnabled: false }, "emulation.setScriptingEnabled", { enabled: false }],
55
- ];
56
- for (const [name, options, method, params] of cases) {
57
- it(`${name} → ${method}`, async () => {
58
- const { connector, sent } = setup();
59
- await applyEmulation(connector, PAGE, options);
60
- assert.deepEqual(sent(), [[method, { ...params, ...PAGE }]]);
61
- });
62
- }
63
- });
64
-
65
- describe("applyEmulation: resetting", () => {
66
- it("null puts the real value back", async () => {
67
- const { connector, sent } = setup();
68
- await applyEmulation(connector, PAGE, {
69
- locale: null,
70
- timezone: null,
71
- userAgent: null,
72
- geolocation: null,
73
- offline: null,
74
- orientation: null,
75
- screen: null,
76
- touch: null,
77
- javaScriptEnabled: null,
78
- });
79
- assert.deepEqual(sent().map(([method, params]) => [method, Object.values(params).filter((v) => v === null).length]), [
80
- ["emulation.setLocaleOverride", 1],
81
- ["emulation.setTimezoneOverride", 1],
82
- ["emulation.setUserAgentOverride", 1],
83
- ["emulation.setGeolocationOverride", 1],
84
- ["emulation.setNetworkConditions", 1],
85
- ["emulation.setScreenOrientationOverride", 1],
86
- ["emulation.setScreenSettingsOverride", 1],
87
- ["emulation.setTouchOverride", 1],
88
- ["emulation.setScriptingEnabled", 1],
89
- ]);
90
- });
91
-
92
- it("offline false is back online, javaScriptEnabled true is back to the default", async () => {
93
- const { connector, sent } = setup();
94
- await applyEmulation(connector, PAGE, { offline: false, javaScriptEnabled: true });
95
- assert.deepEqual(sent(), [
96
- ["emulation.setNetworkConditions", { networkConditions: null, ...PAGE }],
97
- ["emulation.setScriptingEnabled", { enabled: null, ...PAGE }],
98
- ]);
99
- });
100
-
101
- it("undefined leaves an option alone; no options send nothing", async () => {
102
- const { connector, sent } = setup();
103
- await applyEmulation(connector, PAGE, {});
104
- await applyEmulation(connector, PAGE, { locale: undefined, timezone: "UTC" });
105
- assert.deepEqual(sent().map(([method]) => method), ["emulation.setTimezoneOverride"]);
106
- });
107
- });
108
-
109
- describe("applyEmulation: targets and order", () => {
110
- it("a user context target is sent as userContexts", async () => {
111
- const { connector, sent } = setup();
112
- await applyEmulation(connector, { userContexts: ["u1"] }, { locale: "de-DE" });
113
- assert.deepEqual(sent(), [["emulation.setLocaleOverride", { locale: "de-DE", userContexts: ["u1"] }]]);
114
- });
115
-
116
- it("options are applied in a fixed order, whatever the order they were given in", async () => {
117
- const { connector, sent } = setup();
118
- await applyEmulation(connector, PAGE, { offline: true, timezone: "UTC", locale: "en-GB", userAgent: "x" });
119
- assert.deepEqual(sent().map(([method]) => method), [
120
- "emulation.setLocaleOverride",
121
- "emulation.setTimezoneOverride",
122
- "emulation.setUserAgentOverride",
123
- "emulation.setNetworkConditions",
124
- ]);
125
- });
126
- });
127
-
128
- describe("applyEmulation: validation happens before anything is sent", () => {
129
- const invalid: Array<[string, EmulationOptions, RegExp]> = [
130
- ["latitude too big", { geolocation: { latitude: 91, longitude: 0 } }, /latitude must be between -90 and 90/],
131
- ["latitude too small", { geolocation: { latitude: -90.5, longitude: 0 } }, /latitude/],
132
- ["longitude too big", { geolocation: { latitude: 0, longitude: 181 } }, /longitude must be between -180 and 180/],
133
- ["longitude not a number", { geolocation: { latitude: 0, longitude: NaN } }, /longitude/],
134
- ["negative accuracy", { geolocation: { latitude: 0, longitude: 0, accuracy: -1 } }, /accuracy must be 0 or more/],
135
- ["unknown timezone", { timezone: "Not/AZone" }, /unknown timezone "Not\/AZone"/],
136
- ["malformed locale", { locale: "not a locale!" }, /invalid locale "not a locale!"/],
137
- ["empty user agent", { userAgent: "" }, /userAgent must not be empty/],
138
- ["fractional screen", { screen: { width: 800.5, height: 600 } }, /screen width and height must be positive integers/],
139
- ["zero screen", { screen: { width: 0, height: 600 } }, /screen width and height/],
140
- ["unknown orientation", { orientation: "diagonal" as never }, /unknown orientation "diagonal"/],
141
- ["touch below one", { touch: 0 }, /touch must be a positive integer/],
142
- ["fractional touch", { touch: 1.5 }, /touch must be a positive integer/],
143
- ];
144
- for (const [name, options, message] of invalid) {
145
- it(`${name} is a RangeError`, async () => {
146
- const { connector, sent } = setup();
147
- await assert.rejects(applyEmulation(connector, PAGE, { locale: "en-GB", ...options }), (err: unknown) => {
148
- assert.ok(err instanceof RangeError, String(err));
149
- assert.match((err as Error).message, message);
150
- return true;
151
- });
152
- assert.deepEqual(sent(), [], "the valid locale was not applied either");
153
- });
154
- }
155
-
156
- it("accepts UTC offsets as timezones, and locales with extensions", async () => {
157
- const { connector, sent } = setup();
158
- await applyEmulation(connector, PAGE, { timezone: "Europe/Berlin", locale: "de-DE-u-nu-latn" });
159
- assert.equal(sent().length, 2);
160
- });
161
- });
162
-
163
- describe("applyEmulation: when the browser refuses", () => {
164
- it("a command the browser does not know is UnsupportedOperationError naming the option, with what was applied", async () => {
165
- const { connector } = setup({ "emulation.setTouchOverride": () => failing("unknown command") });
166
- await assert.rejects(applyEmulation(connector, PAGE, { locale: "nl-NL", touch: 5 }), (err: unknown) => {
167
- assert.ok(err instanceof UnsupportedOperationError);
168
- assert.match(err.message, /emulate\(\{ touch \}\) is not supported by the browser/);
169
- assert.equal((err as { option?: string }).option, "touch");
170
- assert.deepEqual((err as { applied?: string[] }).applied, ["locale"]);
171
- return true;
172
- });
173
- });
174
-
175
- it("unsupported operation counts the same", async () => {
176
- const { connector } = setup({ "emulation.setScriptingEnabled": () => failing("unsupported operation") });
177
- await assert.rejects(applyEmulation(connector, PAGE, { javaScriptEnabled: false }), UnsupportedOperationError);
178
- });
179
-
180
- it("another failure is an EmulationError listing what was applied and what failed; nothing is rolled back", async () => {
181
- const { connector, sent } = setup({ "emulation.setUserAgentOverride": () => failing("invalid argument", "bad ua") });
182
- await assert.rejects(
183
- applyEmulation(connector, PAGE, { locale: "nl-NL", timezone: "Europe/Amsterdam", userAgent: "x", offline: true }),
184
- (err: unknown) => {
185
- assert.ok(err instanceof EmulationError);
186
- assert.deepEqual(err.applied, ["locale", "timezone"]);
187
- assert.equal(err.failed, "userAgent");
188
- assert.equal((err.cause as { code: string }).code, "invalid argument");
189
- assert.match(err.message, /emulate\(\): userAgent failed after applying locale, timezone: .*bad ua/);
190
- return true;
191
- },
192
- );
193
- assert.equal(sent().length, 3, "the offline option after the failure was not sent");
194
- });
195
-
196
- it("with nothing applied before the failure the message says so", async () => {
197
- const { connector } = setup({ "emulation.setLocaleOverride": () => failing("invalid argument", "bad") });
198
- await assert.rejects(applyEmulation(connector, PAGE, { locale: "nl-NL" }), /emulate\(\): locale failed: /);
199
- });
200
- });
@@ -1,204 +0,0 @@
1
- import { UnsupportedOperationError } from "../locator/selector-errors.js";
2
- import type { BiDiConnector } from "../transport/bidi-connection.js";
3
- import { BiDiError } from "../transport/bidi-error.js";
4
- import type { BiDiCommands } from "../types/bidi.js";
5
-
6
- const ORIENTATIONS = [
7
- "portrait-primary",
8
- "portrait-secondary",
9
- "landscape-primary",
10
- "landscape-secondary",
11
- ] as const;
12
- type OrientationType = (typeof ORIENTATIONS)[number];
13
-
14
- /**
15
- * What to pretend about the browser. `undefined` leaves an option as it is, `null` puts the real value back.
16
- * Which options a browser supports differs: an unsupported one throws UnsupportedOperationError.
17
- */
18
- export interface EmulationOptions {
19
- /** A BCP 47 tag, e.g. "nl-NL": `navigator.language`, `Intl` formatting */
20
- locale?: string | null;
21
- /** An IANA timezone, e.g. "Europe/Amsterdam" */
22
- timezone?: string | null;
23
- userAgent?: string | null;
24
- /** Needs the geolocation permission: see `page.grantPermission("geolocation")` */
25
- geolocation?: { latitude: number; longitude: number; accuracy?: number } | null;
26
- /** `true`: no network (`navigator.onLine` is false and requests fail); `false` or `null`: back online */
27
- offline?: boolean | null;
28
- /** "portrait" and "landscape" mean the primary orientation of that kind */
29
- orientation?: "portrait" | "landscape" | OrientationType | null;
30
- /** The size `screen.width` and `screen.height` report */
31
- screen?: { width: number; height: number } | null;
32
- /** How many touch points `navigator.maxTouchPoints` reports */
33
- touch?: number | null;
34
- /** `false` turns JavaScript off; `true` or `null` on again */
35
- javaScriptEnabled?: boolean | null;
36
- }
37
-
38
- export type EmulationTarget = { contexts: string[] } | { userContexts: string[] };
39
-
40
- /** A part of the options could not be applied; what was applied before it stays applied */
41
- export class EmulationError extends Error {
42
- /** The options that were applied before the failure, in the order they were applied */
43
- public readonly applied: string[];
44
- public readonly failed: string;
45
- public readonly cause: unknown;
46
-
47
- constructor(failed: string, applied: string[], cause: unknown) {
48
- const detail = cause instanceof Error ? cause.message : String(cause);
49
- super(`emulate(): ${failed} failed${applied.length ? ` after applying ${applied.join(", ")}` : ""}: ${detail}`);
50
- this.name = "EmulationError";
51
- this.failed = failed;
52
- this.applied = applied;
53
- this.cause = cause;
54
- }
55
- }
56
-
57
- /** The browser does not implement an emulation command */
58
- export class EmulationUnsupportedError extends UnsupportedOperationError {
59
- public readonly option: string;
60
- public readonly applied: string[];
61
-
62
- constructor(option: string, applied: string[]) {
63
- super(`emulate({ ${option} }) is not supported by the browser`);
64
- this.name = "EmulationUnsupportedError";
65
- this.option = option;
66
- this.applied = applied;
67
- }
68
- }
69
-
70
- interface Step {
71
- option: string;
72
- method: keyof BiDiCommands & `emulation.${string}`;
73
- params: object;
74
- }
75
-
76
- const isNumber = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
77
- const isPositiveInteger = (value: unknown): value is number => Number.isInteger(value) && (value as number) > 0;
78
-
79
- /** Turns the options into commands, in a fixed order; throws RangeError for an invalid value, before anything is sent */
80
- function plan(options: EmulationOptions): Step[] {
81
- const steps: Step[] = [];
82
-
83
- if (options.locale !== undefined) {
84
- if (options.locale !== null) {
85
- try {
86
- if (options.locale === "" || Intl.getCanonicalLocales(options.locale).length !== 1) throw new RangeError();
87
- } catch {
88
- throw new RangeError(`emulate(): invalid locale "${options.locale}"`);
89
- }
90
- }
91
- steps.push({ option: "locale", method: "emulation.setLocaleOverride", params: { locale: options.locale } });
92
- }
93
-
94
- if (options.timezone !== undefined) {
95
- if (options.timezone !== null) {
96
- try {
97
- new Intl.DateTimeFormat(undefined, { timeZone: options.timezone });
98
- } catch {
99
- throw new RangeError(`emulate(): unknown timezone "${options.timezone}"`);
100
- }
101
- }
102
- steps.push({ option: "timezone", method: "emulation.setTimezoneOverride", params: { timezone: options.timezone } });
103
- }
104
-
105
- if (options.userAgent !== undefined) {
106
- if (options.userAgent === "") throw new RangeError("emulate(): userAgent must not be empty");
107
- steps.push({ option: "userAgent", method: "emulation.setUserAgentOverride", params: { userAgent: options.userAgent } });
108
- }
109
-
110
- if (options.geolocation !== undefined) {
111
- const place = options.geolocation;
112
- if (place !== null) {
113
- if (!isNumber(place.latitude) || place.latitude < -90 || place.latitude > 90) {
114
- throw new RangeError("emulate(): latitude must be between -90 and 90");
115
- }
116
- if (!isNumber(place.longitude) || place.longitude < -180 || place.longitude > 180) {
117
- throw new RangeError("emulate(): longitude must be between -180 and 180");
118
- }
119
- if (place.accuracy !== undefined && (!isNumber(place.accuracy) || place.accuracy < 0)) {
120
- throw new RangeError("emulate(): accuracy must be 0 or more");
121
- }
122
- }
123
- steps.push({
124
- option: "geolocation",
125
- method: "emulation.setGeolocationOverride",
126
- params: { coordinates: place },
127
- });
128
- }
129
-
130
- if (options.offline !== undefined) {
131
- steps.push({
132
- option: "offline",
133
- method: "emulation.setNetworkConditions",
134
- params: { networkConditions: options.offline ? { type: "offline" } : null },
135
- });
136
- }
137
-
138
- if (options.orientation !== undefined) {
139
- let screenOrientation: { natural: "portrait" | "landscape"; type: OrientationType } | null = null;
140
- if (options.orientation !== null) {
141
- const type = (
142
- options.orientation === "portrait" || options.orientation === "landscape"
143
- ? `${options.orientation}-primary`
144
- : options.orientation
145
- ) as OrientationType;
146
- if (!ORIENTATIONS.includes(type)) throw new RangeError(`emulate(): unknown orientation "${options.orientation}"`);
147
- screenOrientation = { natural: type.startsWith("portrait") ? "portrait" : "landscape", type };
148
- }
149
- steps.push({ option: "orientation", method: "emulation.setScreenOrientationOverride", params: { screenOrientation } });
150
- }
151
-
152
- if (options.screen !== undefined) {
153
- const area = options.screen;
154
- if (area !== null && !(isPositiveInteger(area.width) && isPositiveInteger(area.height))) {
155
- throw new RangeError("emulate(): screen width and height must be positive integers");
156
- }
157
- steps.push({ option: "screen", method: "emulation.setScreenSettingsOverride", params: { screenArea: area } });
158
- }
159
-
160
- if (options.touch !== undefined) {
161
- if (options.touch !== null && !isPositiveInteger(options.touch)) {
162
- throw new RangeError("emulate(): touch must be a positive integer");
163
- }
164
- steps.push({ option: "touch", method: "emulation.setTouchOverride", params: { maxTouchPoints: options.touch } });
165
- }
166
-
167
- if (options.javaScriptEnabled !== undefined) {
168
- steps.push({
169
- option: "javaScriptEnabled",
170
- method: "emulation.setScriptingEnabled",
171
- params: { enabled: options.javaScriptEnabled === false ? false : null },
172
- });
173
- }
174
-
175
- return steps;
176
- }
177
-
178
- /** Throws RangeError when a value is invalid, without sending anything */
179
- export function validateEmulation(options: EmulationOptions): void {
180
- plan(options);
181
- }
182
-
183
- /**
184
- * Applies the options to `target`, one command after the other. Nothing is rolled back when one fails:
185
- * the error says what was applied.
186
- */
187
- export async function applyEmulation(
188
- connector: BiDiConnector,
189
- target: EmulationTarget,
190
- options: EmulationOptions,
191
- ): Promise<void> {
192
- const applied: string[] = [];
193
- for (const step of plan(options)) {
194
- try {
195
- await connector.send(step.method, { ...step.params, ...target } as never);
196
- } catch (err) {
197
- if (err instanceof BiDiError && (err.code === "unknown command" || err.code === "unsupported operation")) {
198
- throw new EmulationUnsupportedError(step.option, applied);
199
- }
200
- throw new EmulationError(step.option, applied, err);
201
- }
202
- applied.push(step.option);
203
- }
204
- }
@@ -1,142 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import path from "node:path";
5
- import { after, afterEach, describe, it } from "node:test";
6
- import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
7
- import { BiDiConnector } from "../transport/bidi-connection.js";
8
- import { ContextTree } from "./context-tree.js";
9
- import { FileChooser, FileChooserTracker } from "./file-chooser.js";
10
-
11
- const TREE = [
12
- { context: "top", parent: null, children: [{ context: "frame", parent: "top", children: [] }] },
13
- { context: "other", parent: null, children: [] },
14
- ];
15
- const stops: Array<() => void> = [];
16
- afterEach(() => stops.splice(0).forEach((stop) => stop()));
17
- const dir = mkdtempSync(path.join(tmpdir(), "samurai-chooser-"));
18
- after(() => rmSync(dir, { recursive: true, force: true }));
19
- const file = (name: string) => {
20
- const full = path.join(dir, name);
21
- writeFileSync(full, name);
22
- return full;
23
- };
24
-
25
- async function setup(handlers: Record<string, (params: any) => object | Error> = {}) {
26
- const ws = new FakeWebSocket();
27
- const connector = new BiDiConnector(ws as unknown as WebSocket);
28
- stops.push(autoReply(ws, { "browsingContext.getTree": () => ({ contexts: TREE }), "input.setFiles": () => ({}), ...handlers }));
29
- const tree = await ContextTree.create(connector);
30
- const tracker = await FileChooserTracker.start(connector, tree, "top");
31
- const seen: FileChooser[] = [];
32
- tracker.on("filechooser", (chooser) => seen.push(chooser));
33
- const open = (extra: { context?: string; multiple?: boolean; element?: object | null } = {}) =>
34
- ws.emitEvent("input.fileDialogOpened", {
35
- context: extra.context ?? "top",
36
- multiple: extra.multiple ?? false,
37
- ...(extra.element !== null && { element: extra.element ?? { type: "node", sharedId: "input-1", value: {} } }),
38
- });
39
- const setFiles = () => ws.sent.filter((m) => m.method === "input.setFiles").map((m) => m.params as any);
40
- return { ws, tracker, seen, open, setFiles };
41
- }
42
-
43
- describe("FileChooserTracker", () => {
44
- it("announces a chooser for this page, with what the browser says about it", async () => {
45
- const { seen, open } = await setup();
46
- open({ multiple: true });
47
- open({ multiple: false });
48
- assert.equal(seen.length, 2);
49
- assert.ok(seen[0] instanceof FileChooser);
50
- assert.deepEqual(seen.map((chooser) => chooser.isMultiple()), [true, false]);
51
- });
52
-
53
- it("takes iframes, ignores other pages", async () => {
54
- const { seen, open } = await setup();
55
- open({ context: "frame" });
56
- open({ context: "other" });
57
- assert.equal(seen.length, 1);
58
- });
59
-
60
- it("a throwing listener does not stop the others; off removes one", async () => {
61
- const { tracker, open } = await setup();
62
- let second = 0;
63
- let removed = 0;
64
- const gone = () => removed++;
65
- tracker.on("filechooser", () => {
66
- throw new Error("listener failed");
67
- });
68
- tracker.on("filechooser", () => second++);
69
- tracker.on("filechooser", gone);
70
- tracker.off("filechooser", gone);
71
- open();
72
- assert.equal(second, 1);
73
- assert.equal(removed, 0);
74
- });
75
-
76
- it("subscribes to the event and lets go of it on dispose", async () => {
77
- const { ws, tracker, open, seen } = await setup();
78
- assert.ok(ws.sent.some((m) => m.method === "session.subscribe" && (m.params as any).events[0] === "input.fileDialogOpened"));
79
- await tracker.dispose();
80
- await tick(10);
81
- open();
82
- assert.equal(seen.length, 0);
83
- await assert.doesNotReject(tracker.dispose());
84
- });
85
- });
86
-
87
- describe("FileChooser.setFiles", () => {
88
- it("sets the files on the element the browser named, in the frame that opened the dialog", async () => {
89
- const { seen, open, setFiles } = await setup();
90
- open({ context: "frame", element: { type: "node", sharedId: "in-frame" } });
91
- const a = file("a.txt");
92
- await seen[0]!.setFiles(a);
93
- assert.deepEqual(setFiles(), [{ context: "frame", element: { sharedId: "in-frame" }, files: [a] }]);
94
- });
95
-
96
- it("takes a list, and resolves relative paths", async () => {
97
- const { seen, open, setFiles } = await setup();
98
- open({ multiple: true });
99
- const [a, b] = [file("m1.txt"), file("m2.txt")];
100
- await seen[0]!.setFiles([path.relative(process.cwd(), a), b]);
101
- assert.deepEqual(setFiles()[0].files, [a, b]);
102
- });
103
-
104
- it("refuses several files for a chooser that takes one, and sends nothing", async () => {
105
- const { seen, open, setFiles } = await setup();
106
- open({ multiple: false });
107
- await assert.rejects(seen[0]!.setFiles([file("x1.txt"), file("x2.txt")]), /file chooser does not accept multiple files/);
108
- assert.deepEqual(setFiles(), []);
109
- });
110
-
111
- it("an empty list clears the selection", async () => {
112
- const { seen, open, setFiles } = await setup();
113
- open();
114
- await seen[0]!.setFiles([]);
115
- assert.deepEqual(setFiles()[0].files, []);
116
- });
117
-
118
- it("checks the files exist first, before sending", async () => {
119
- const { seen, open, setFiles } = await setup();
120
- open();
121
- const missing = path.join(dir, "nope.txt");
122
- await assert.rejects(seen[0]!.setFiles(missing), new Error(`setFiles(): file not found: ${missing}`));
123
- const folder = path.join(dir, "folder");
124
- mkdirSync(folder);
125
- await assert.rejects(seen[0]!.setFiles(folder), new Error(`setFiles(): not a file: ${folder}`));
126
- assert.deepEqual(setFiles(), []);
127
- });
128
-
129
- it("without an element reference it cannot set anything", async () => {
130
- const { seen, open, setFiles } = await setup();
131
- open({ element: null });
132
- await assert.rejects(seen[0]!.setFiles(file("e.txt")), /file chooser has no element reference/);
133
- assert.deepEqual(setFiles(), []);
134
- });
135
-
136
- it("rethrows what the browser answers", async () => {
137
- const failing = Object.assign(new Error("read-only"), { code: "unable to set file input" });
138
- const { seen, open } = await setup({ "input.setFiles": () => failing });
139
- open();
140
- await assert.rejects(seen[0]!.setFiles(file("f.txt")), { name: "BiDiError", code: "unable to set file input" });
141
- });
142
- });
@@ -1,99 +0,0 @@
1
- import EventEmitter from "node:events";
2
- import logger from "../logger/index.js";
3
- import type { BiDiConnector } from "../transport/bidi-connection.js";
4
- import type { Subscription } from "../transport/subscriptions.js";
5
- import type { FileDialogInfo } from "../types/bidi-modules/input.js";
6
- import type { ContextTree } from "./context-tree.js";
7
- import { assertIsFile, resolveFiles } from "./file-paths.js";
8
-
9
- /**
10
- * A native file picker the page asked for (a click on an `<input type=file>` or its label). Nothing appears on screen:
11
- * answer it with `setFiles()`.
12
- */
13
- export class FileChooser {
14
- constructor(
15
- private connector: BiDiConnector,
16
- private info: FileDialogInfo,
17
- ) {}
18
-
19
- /** Whether the input lets the user pick several files */
20
- public isMultiple(): boolean {
21
- return this.info.multiple;
22
- }
23
-
24
- /**
25
- * Picks the files (an empty list picks none). Paths resolve against the working directory and must exist on the
26
- * machine the browser runs on.
27
- */
28
- public async setFiles(files: string | string[]): Promise<void> {
29
- const sharedId = (this.info.element as { sharedId?: string } | undefined)?.sharedId;
30
- if (!sharedId) throw new Error("file chooser has no element reference");
31
- const paths = resolveFiles(files);
32
- for (const file of paths) await assertIsFile(file, "setFiles");
33
- if (paths.length > 1 && !this.info.multiple) {
34
- throw new Error("file chooser does not accept multiple files");
35
- }
36
- await this.connector.send("input.setFiles", {
37
- context: this.info.context,
38
- element: { sharedId },
39
- files: paths,
40
- });
41
- }
42
- }
43
-
44
- interface EventMap {
45
- filechooser: [FileChooser];
46
- }
47
-
48
- /** Follows the file pickers of one page (frames included) */
49
- export class FileChooserTracker {
50
- private events = new EventEmitter();
51
- private subscription: Subscription | undefined;
52
- private disposed = false;
53
-
54
- private constructor(
55
- private connector: BiDiConnector,
56
- private tree: ContextTree,
57
- private contextId: string,
58
- ) {}
59
-
60
- public static async start(connector: BiDiConnector, tree: ContextTree, contextId: string): Promise<FileChooserTracker> {
61
- const tracker = new FileChooserTracker(connector, tree, contextId);
62
- connector.onEvent("input.fileDialogOpened", tracker.onOpened);
63
- try {
64
- tracker.subscription = await connector.subscribe(["input.fileDialogOpened"]);
65
- } catch (err) {
66
- connector.offEvent("input.fileDialogOpened", tracker.onOpened);
67
- throw err;
68
- }
69
- return tracker;
70
- }
71
-
72
- public on(event: "filechooser", listener: (...args: EventMap["filechooser"]) => void): void {
73
- this.events.on(event, listener as (...args: unknown[]) => void);
74
- }
75
-
76
- public off(event: "filechooser", listener: (...args: EventMap["filechooser"]) => void): void {
77
- this.events.off(event, listener as (...args: unknown[]) => void);
78
- }
79
-
80
- /** Idempotent */
81
- public async dispose(): Promise<void> {
82
- if (this.disposed) return;
83
- this.disposed = true;
84
- this.connector.offEvent("input.fileDialogOpened", this.onOpened);
85
- await this.subscription?.unsubscribe();
86
- }
87
-
88
- private onOpened = (params: FileDialogInfo) => {
89
- if (!this.tree.isWithin(params.context, this.contextId)) return;
90
- const chooser = new FileChooser(this.connector, params);
91
- for (const listener of this.events.rawListeners("filechooser")) {
92
- try {
93
- listener(chooser);
94
- } catch (err) {
95
- logger.error("A filechooser listener threw", { err });
96
- }
97
- }
98
- };
99
- }
@@ -1,18 +0,0 @@
1
- import { stat } from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- /** Absolute paths for `files` (relative ones resolve against the working directory) */
5
- export function resolveFiles(files: string | string[]): string[] {
6
- return (Array.isArray(files) ? files : [files]).map((file) => path.resolve(file));
7
- }
8
-
9
- /** Throws unless `file` is an existing regular file; `operation` names the caller in the message */
10
- export async function assertIsFile(file: string, operation: string): Promise<void> {
11
- let info;
12
- try {
13
- info = await stat(file);
14
- } catch {
15
- throw new Error(`${operation}(): file not found: ${file}`);
16
- }
17
- if (!info.isFile()) throw new Error(`${operation}(): not a file: ${file}`);
18
- }