@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,161 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
4
- import { BiDiConnector } from "../transport/bidi-connection.js";
5
- import { ContextTree } from "./context-tree.js";
6
- import { Download, DownloadError, DownloadTracker } from "./download.js";
7
-
8
- const TREE = [
9
- { context: "top", parent: null, children: [{ context: "frame", parent: "top", children: [] }] },
10
- { context: "other", parent: null, children: [] },
11
- ];
12
- const stops: Array<() => void> = [];
13
- afterEach(() => stops.splice(0).forEach((stop) => stop()));
14
-
15
- async function setup(options: { timeout?: number } = {}) {
16
- const ws = new FakeWebSocket();
17
- const connector = new BiDiConnector(ws as unknown as WebSocket);
18
- stops.push(autoReply(ws, { "browsingContext.getTree": () => ({ contexts: TREE }) }));
19
- const tree = await ContextTree.create(connector);
20
- const tracker = await DownloadTracker.start(connector, tree, "top", { timeout: options.timeout ?? 1000 });
21
- const seen: Download[] = [];
22
- tracker.on("download", (download) => seen.push(download));
23
- const begin = (navigation: string, extra: { context?: string; url?: string; filename?: string } = {}) =>
24
- ws.emitEvent("browsingContext.downloadWillBegin", {
25
- context: extra.context ?? "top",
26
- navigation,
27
- timestamp: 1,
28
- url: extra.url ?? `https://example.test/${navigation}`,
29
- suggestedFilename: extra.filename ?? `${navigation}.csv`,
30
- });
31
- const complete = (navigation: string, filepath = `/tmp/dl/${navigation}.csv`, context = "top") =>
32
- ws.emitEvent("browsingContext.downloadEnd", { context, navigation, timestamp: 2, url: "u", status: "complete", filepath });
33
- const cancel = (navigation: string, context = "top") =>
34
- ws.emitEvent("browsingContext.downloadEnd", { context, navigation, timestamp: 2, url: "u", status: "canceled" });
35
- return { ws, tracker, seen, begin, complete, cancel };
36
- }
37
-
38
- describe("DownloadTracker", () => {
39
- it("announces a download with its url and suggested file name", async () => {
40
- const { seen, begin } = await setup();
41
- begin("n1", { url: "https://example.test/file", filename: "report.csv" });
42
- assert.equal(seen.length, 1);
43
- assert.ok(seen[0] instanceof Download);
44
- assert.equal(seen[0]!.url(), "https://example.test/file");
45
- assert.equal(seen[0]!.suggestedFilename(), "report.csv");
46
- });
47
-
48
- it("path() resolves with the file the browser wrote once the download completed", async () => {
49
- const { seen, begin, complete } = await setup();
50
- begin("n1");
51
- const pending = seen[0]!.path();
52
- complete("n1", "/tmp/dl/report.csv");
53
- assert.equal(await pending, "/tmp/dl/report.csv");
54
- assert.equal(await seen[0]!.failure(), null);
55
- });
56
-
57
- it("path() also works when the download finished before it was asked", async () => {
58
- const { seen, begin, complete } = await setup();
59
- begin("n1");
60
- complete("n1", "/tmp/dl/a.csv");
61
- await tick(5);
62
- assert.equal(await seen[0]!.path(), "/tmp/dl/a.csv");
63
- assert.equal(await seen[0]!.path(), "/tmp/dl/a.csv", "and again");
64
- });
65
-
66
- it("a cancelled download rejects path() with a DownloadError and reports the failure", async () => {
67
- const { seen, begin, cancel } = await setup();
68
- begin("n1", { url: "https://example.test/big" });
69
- const pending = seen[0]!.path();
70
- const rejection = assert.rejects(pending, (err: unknown) => {
71
- assert.ok(err instanceof DownloadError);
72
- assert.equal(err.url, "https://example.test/big");
73
- assert.equal(err.reason, "canceled");
74
- assert.match(err.message, /^Download of https:\/\/example\.test\/big failed: canceled/);
75
- return true;
76
- });
77
- cancel("n1");
78
- await rejection;
79
- assert.equal(await seen[0]!.failure(), "canceled");
80
- });
81
-
82
- it("failure() waits for the end, too", async () => {
83
- const { seen, begin, cancel } = await setup();
84
- begin("n1");
85
- let failure: string | null | undefined;
86
- void seen[0]!.failure().then((value) => (failure = value));
87
- await tick(10);
88
- assert.equal(failure, undefined);
89
- cancel("n1");
90
- await tick(10);
91
- assert.equal(failure, "canceled");
92
- });
93
-
94
- it("path() times out when the download never ends", async () => {
95
- const { seen, begin } = await setup({ timeout: 50 });
96
- begin("n1", { url: "https://example.test/slow" });
97
- await assert.rejects(seen[0]!.path(), /Download of https:\/\/example\.test\/slow did not finish within 50ms/);
98
- await assert.rejects(seen[0]!.path({ timeout: 20 }), /did not finish within 20ms/);
99
- });
100
-
101
- it("matches ends to downloads by navigation, however they interleave", async () => {
102
- const { seen, begin, complete, cancel } = await setup();
103
- begin("a");
104
- begin("b");
105
- cancel("b");
106
- complete("a", "/tmp/dl/a.csv");
107
- assert.equal(await seen[0]!.path(), "/tmp/dl/a.csv");
108
- await assert.rejects(seen[1]!.path(), DownloadError);
109
- });
110
-
111
- it("an end without a beginning is ignored", async () => {
112
- const { seen, complete } = await setup();
113
- complete("ghost");
114
- assert.equal(seen.length, 0);
115
- });
116
-
117
- it("takes downloads of iframes, ignores those of other pages", async () => {
118
- const { seen, begin } = await setup();
119
- begin("f", { context: "frame" });
120
- begin("x", { context: "other" });
121
- assert.deepEqual(seen.map((d) => d.suggestedFilename()), ["f.csv"]);
122
- });
123
-
124
- it("a throwing listener does not stop the others, and off removes one", async () => {
125
- const { tracker, begin } = await setup();
126
- let second = 0;
127
- let removed = 0;
128
- const gone = () => removed++;
129
- tracker.on("download", () => {
130
- throw new Error("listener failed");
131
- });
132
- tracker.on("download", () => second++);
133
- tracker.on("download", gone);
134
- tracker.off("download", gone);
135
- begin("n1");
136
- assert.equal(second, 1);
137
- assert.equal(removed, 0);
138
- });
139
-
140
- it("subscribes to both events and lets go of them on dispose", async () => {
141
- const { ws, tracker, begin, seen } = await setup();
142
- const subscribed = ws.sent.filter((m) => m.method === "session.subscribe").map((m) => (m.params as any).events[0]);
143
- assert.ok(subscribed.includes("browsingContext.downloadWillBegin"));
144
- assert.ok(subscribed.includes("browsingContext.downloadEnd"));
145
- await tracker.dispose();
146
- await tick(10);
147
- begin("late");
148
- assert.equal(seen.length, 0);
149
- assert.ok(ws.sent.filter((m) => m.method === "session.unsubscribe").length >= 2);
150
- await assert.doesNotReject(tracker.dispose());
151
- });
152
-
153
- it("dispose rejects downloads still in progress", async () => {
154
- const { seen, begin, tracker } = await setup();
155
- begin("n1");
156
- const pending = seen[0]!.path();
157
- const rejection = assert.rejects(pending, /page closed/);
158
- await tracker.dispose();
159
- await rejection;
160
- });
161
- });
@@ -1,172 +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 {
6
- DownloadEndParams,
7
- DownloadWillBeginParams,
8
- } from "../types/bidi-modules/browsing-context.js";
9
- import { WaitTimeoutError } from "../wait/wait-until.js";
10
- import type { ContextTree } from "./context-tree.js";
11
-
12
- /** A download did not complete */
13
- export class DownloadError extends Error {
14
- public readonly url: string;
15
- /** "canceled": the browser stopped it or the server broke the connection; "page closed" */
16
- public readonly reason: string;
17
-
18
- constructor(url: string, reason: string) {
19
- super(`Download of ${url} failed: ${reason}`);
20
- this.name = "DownloadError";
21
- this.url = url;
22
- this.reason = reason;
23
- }
24
- }
25
-
26
- /** A download did not finish in time */
27
- export class DownloadTimeoutError extends WaitTimeoutError<undefined> {
28
- constructor(url: string, timeout: number) {
29
- super(timeout, undefined);
30
- this.name = "DownloadTimeoutError";
31
- this.message = `Download of ${url} did not finish within ${timeout}ms`;
32
- }
33
- }
34
-
35
- type Outcome = { status: "complete"; path: string } | { status: "canceled" } | { status: "closed" };
36
-
37
- /** A file the page started to download */
38
- export class Download {
39
- private settle!: (outcome: Outcome) => void;
40
- private outcome = new Promise<Outcome>((resolve) => (this.settle = resolve));
41
-
42
- constructor(
43
- private readonly address: string,
44
- private readonly filename: string,
45
- private readonly defaultTimeout: number,
46
- ) {}
47
-
48
- public url(): string {
49
- return this.address;
50
- }
51
-
52
- /** The name the server (or the link) suggested */
53
- public suggestedFilename(): string {
54
- return this.filename;
55
- }
56
-
57
- /**
58
- * Waits until the download finished and returns where the browser saved it. Rejects with DownloadError when it was
59
- * cancelled, and with DownloadTimeoutError after `timeout` ms (default: the navigation timeout).
60
- */
61
- public async path(options: { timeout?: number } = {}): Promise<string> {
62
- const timeout = options.timeout ?? this.defaultTimeout;
63
- let timer: ReturnType<typeof setTimeout> | undefined;
64
- const late = new Promise<never>((_, reject) => {
65
- timer = setTimeout(() => reject(new DownloadTimeoutError(this.address, timeout)), timeout);
66
- });
67
- try {
68
- const outcome = await Promise.race([this.outcome, late]);
69
- if (outcome.status === "complete") return outcome.path;
70
- throw new DownloadError(this.address, outcome.status === "closed" ? "page closed" : "canceled");
71
- } finally {
72
- clearTimeout(timer);
73
- }
74
- }
75
-
76
- /** Waits for the end of the download: null when it completed, otherwise why it did not ("canceled") */
77
- public async failure(): Promise<string | null> {
78
- const outcome = await this.outcome;
79
- if (outcome.status === "complete") return null;
80
- return outcome.status === "closed" ? "page closed" : "canceled";
81
- }
82
-
83
- /** @internal */
84
- public finish(outcome: Outcome): void {
85
- this.settle(outcome);
86
- }
87
- }
88
-
89
- interface EventMap {
90
- download: [Download];
91
- }
92
-
93
- /** Follows the downloads of one page (frames included) */
94
- export class DownloadTracker {
95
- private events = new EventEmitter();
96
- private active = new Map<string, Download>();
97
- private subscription: Subscription | undefined;
98
- private disposed = false;
99
-
100
- private constructor(
101
- private connector: BiDiConnector,
102
- private tree: ContextTree,
103
- private contextId: string,
104
- private timeout: number,
105
- ) {}
106
-
107
- public static async start(
108
- connector: BiDiConnector,
109
- tree: ContextTree,
110
- contextId: string,
111
- options: { timeout: number },
112
- ): Promise<DownloadTracker> {
113
- const tracker = new DownloadTracker(connector, tree, contextId, options.timeout);
114
- connector.onEvent("browsingContext.downloadWillBegin", tracker.onBegin);
115
- connector.onEvent("browsingContext.downloadEnd", tracker.onEnd);
116
- try {
117
- tracker.subscription = await connector.subscribe([
118
- "browsingContext.downloadWillBegin",
119
- "browsingContext.downloadEnd",
120
- ]);
121
- } catch (err) {
122
- tracker.removeListeners();
123
- throw err;
124
- }
125
- return tracker;
126
- }
127
-
128
- public on(event: "download", listener: (...args: EventMap["download"]) => void): void {
129
- this.events.on(event, listener as (...args: unknown[]) => void);
130
- }
131
-
132
- public off(event: "download", listener: (...args: EventMap["download"]) => void): void {
133
- this.events.off(event, listener as (...args: unknown[]) => void);
134
- }
135
-
136
- /** Stops tracking; downloads still going on reject with "page closed". Idempotent */
137
- public async dispose(): Promise<void> {
138
- if (this.disposed) return;
139
- this.disposed = true;
140
- this.removeListeners();
141
- for (const download of this.active.values()) download.finish({ status: "closed" });
142
- this.active.clear();
143
- await this.subscription?.unsubscribe();
144
- }
145
-
146
- private removeListeners() {
147
- this.connector.offEvent("browsingContext.downloadWillBegin", this.onBegin);
148
- this.connector.offEvent("browsingContext.downloadEnd", this.onEnd);
149
- }
150
-
151
- private onBegin = (params: DownloadWillBeginParams) => {
152
- if (!this.tree.isWithin(params.context, this.contextId)) return;
153
- const download = new Download(params.url, params.suggestedFilename, this.timeout);
154
- this.active.set(params.navigation, download);
155
- for (const listener of this.events.rawListeners("download")) {
156
- try {
157
- listener(download);
158
- } catch (err) {
159
- logger.error("A download listener threw", { err });
160
- }
161
- }
162
- };
163
-
164
- private onEnd = (params: DownloadEndParams) => {
165
- const download = this.active.get(params.navigation);
166
- if (!download) return;
167
- this.active.delete(params.navigation);
168
- download.finish(
169
- params.status === "complete" ? { status: "complete", path: params.filepath } : { status: "canceled" },
170
- );
171
- };
172
- }
@@ -1,75 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, 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 } from "../testing/fake-websocket.js";
7
- import { BiDiConnector } from "../transport/bidi-connection.js";
8
- import { prepareDownloadsDir, removeIfEmpty } from "./downloads-dir.js";
9
-
10
- const stops: Array<() => void> = [];
11
- afterEach(() => stops.splice(0).forEach((stop) => stop()));
12
- const base = mkdtempSync(path.join(tmpdir(), "samurai-downloads-"));
13
- after(() => rmSync(base, { recursive: true, force: true }));
14
-
15
- function setup(handlers: Record<string, (params: any) => object | Error> = {}) {
16
- const ws = new FakeWebSocket();
17
- const connector = new BiDiConnector(ws as unknown as WebSocket);
18
- stops.push(autoReply(ws, { "browser.setDownloadBehavior": () => ({}), ...handlers }));
19
- return { ws, connector };
20
- }
21
- const failing = (code: string) => Object.assign(new Error(code), { code });
22
-
23
- describe("prepareDownloadsDir", () => {
24
- it("makes a folder below the base and tells the browser to save there", async () => {
25
- const { ws, connector } = setup();
26
- const dir = await prepareDownloadsDir(connector, path.join(base, "a"));
27
- assert.ok(dir);
28
- assert.equal(path.dirname(dir), path.join(base, "a"));
29
- assert.ok(existsSync(dir));
30
- assert.deepEqual(ws.sent[0]!.params, { downloadBehavior: { type: "allowed", destinationFolder: dir } });
31
- });
32
-
33
- it("every call gets a folder of its own", async () => {
34
- const { connector } = setup();
35
- const first = await prepareDownloadsDir(connector, path.join(base, "b"));
36
- const second = await prepareDownloadsDir(connector, path.join(base, "b"));
37
- assert.notEqual(first, second);
38
- });
39
-
40
- it("a browser that cannot be told gets no folder, and none is left behind", async () => {
41
- const { connector } = setup({ "browser.setDownloadBehavior": () => failing("unknown command") });
42
- const target = path.join(base, "c");
43
- assert.equal(await prepareDownloadsDir(connector, target), undefined);
44
- assert.deepEqual(readdirSync(target), []);
45
- });
46
-
47
- it("other errors are thrown, folder removed", async () => {
48
- const { connector } = setup({ "browser.setDownloadBehavior": () => failing("invalid argument") });
49
- const target = path.join(base, "d");
50
- await assert.rejects(prepareDownloadsDir(connector, target), { name: "BiDiError", code: "invalid argument" });
51
- assert.deepEqual(readdirSync(target), []);
52
- });
53
- });
54
-
55
- describe("removeIfEmpty", () => {
56
- it("removes an empty folder, keeps one that has files", () => {
57
- const empty = path.join(base, "empty");
58
- const full = path.join(base, "full");
59
- for (const dir of [empty, full]) {
60
- rmSync(dir, { recursive: true, force: true });
61
- }
62
- mkdirSync(empty);
63
- mkdirSync(full);
64
- writeFileSync(path.join(full, "report.csv"), "x");
65
- removeIfEmpty(empty);
66
- removeIfEmpty(full);
67
- assert.equal(existsSync(empty), false);
68
- assert.equal(existsSync(path.join(full, "report.csv")), true);
69
- });
70
-
71
- it("does nothing for a missing folder or no folder", () => {
72
- assert.doesNotThrow(() => removeIfEmpty(path.join(base, "never-existed")));
73
- assert.doesNotThrow(() => removeIfEmpty(undefined));
74
- });
75
- });
@@ -1,39 +0,0 @@
1
- import { mkdirSync, rmdirSync } from "node:fs";
2
- import path from "node:path";
3
- import logger from "../logger/index.js";
4
- import type { BiDiConnector } from "../transport/bidi-connection.js";
5
- import { BiDiError } from "../transport/bidi-error.js";
6
-
7
- let counter = 0;
8
-
9
- /**
10
- * Makes downloads land in a folder of their own, so a test knows where to look and runs do not mix: a new folder
11
- * below `baseDir` is created and the browser told to save there. Returns it, or undefined when the browser cannot be told.
12
- */
13
- export async function prepareDownloadsDir(connector: BiDiConnector, baseDir: string): Promise<string | undefined> {
14
- const dir = path.join(path.resolve(baseDir), `dl-${Date.now().toString(36)}-${process.pid}-${counter++}`);
15
- mkdirSync(dir, { recursive: true });
16
- try {
17
- await connector.send("browser.setDownloadBehavior", {
18
- downloadBehavior: { type: "allowed", destinationFolder: dir },
19
- });
20
- } catch (err) {
21
- removeIfEmpty(dir);
22
- if (err instanceof BiDiError && (err.code === "unknown command" || err.code === "unsupported operation")) {
23
- logger.warn("The browser cannot be told where to save downloads; they go to its own folder");
24
- return undefined;
25
- }
26
- throw err;
27
- }
28
- return dir;
29
- }
30
-
31
- /** Removes `dir` when nothing was downloaded into it. Never throws */
32
- export function removeIfEmpty(dir: string | undefined): void {
33
- if (!dir) return;
34
- try {
35
- rmdirSync(dir);
36
- } catch {
37
- // Not empty, or already gone: leave it
38
- }
39
- }
@@ -1,186 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createServer, type Server } from "node:http";
3
- import type { AddressInfo, Socket } from "node:net";
4
- import { after, before, test } from "node:test";
5
- import { UnsupportedOperationError } from "../locator/selector-errors.js";
6
- import { callFunction } from "../script/call-function.js";
7
- import type { BiDiConnector } from "../transport/bidi-connection.js";
8
- import { Browser } from "./browser.js";
9
- import type { BrowserContext } from "./browser-context.js";
10
- import type Page from "./page.js";
11
-
12
- let browser: Browser;
13
- let page: Page;
14
- let server: Server;
15
- let base: string;
16
- const sockets = new Set<Socket>();
17
- const userAgents: string[] = [];
18
- const contexts: BrowserContext[] = [];
19
-
20
- before(async () => {
21
- server = createServer((req, res) => {
22
- if (req.url?.startsWith("/ping")) {
23
- userAgents.push(String(req.headers["user-agent"]));
24
- res.setHeader("content-type", "text/plain").end("pong");
25
- return;
26
- }
27
- res.setHeader("content-type", "text/html").end(`<!doctype html><body>emulation<script>window.__inline = "ran"</script></body>`);
28
- });
29
- server.on("connection", (socket) => {
30
- sockets.add(socket);
31
- socket.on("close", () => sockets.delete(socket));
32
- });
33
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
34
- base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
35
- ({ browser, page } = await Browser.launch("firefox", { port: 9248, headless: true }));
36
- await page.navigateTo(`${base}/`);
37
- });
38
-
39
- after(async () => {
40
- for (const context of contexts) await context.close().catch(() => undefined);
41
- await page.dispose();
42
- await browser?.close();
43
- sockets.forEach((socket) => socket.destroy());
44
- await new Promise((resolve) => server.close(resolve));
45
- });
46
-
47
- const OPTIONS = { timeout: 40000 };
48
- const connector = (p: Page) => (p as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
49
- const evaluate = <T>(p: Page, source: string) => callFunction<T>(connector(p), p.contextId, source);
50
-
51
- async function freshPage(options?: Parameters<Browser["newContext"]>[0]) {
52
- const context = await browser.newContext(options);
53
- contexts.push(context);
54
- const tab = await context.newPage();
55
- await tab.navigateTo(`${base}/`);
56
- return { context, tab };
57
- }
58
-
59
- test("locale changes navigator.language and Intl formatting, and resets", OPTIONS, async () => {
60
- const { tab } = await freshPage();
61
- const read = () => evaluate<{ language: string; number: string }>(tab, `() => ({ language: navigator.language, number: new Intl.NumberFormat().format(1234.5) })`);
62
- // The starting locale is the machine's own, so it is read rather than assumed
63
- const before = await read();
64
- await tab.emulate({ locale: "nl-NL" });
65
- assert.deepEqual(await read(), { language: "nl-NL", number: "1.234,5" });
66
- await tab.emulate({ locale: null });
67
- assert.deepEqual(await read(), before);
68
- });
69
-
70
- test("timezone changes what Date and Intl report, and resets", OPTIONS, async () => {
71
- const { tab } = await freshPage();
72
- const read = () => evaluate<{ zone: string; offset: number }>(tab, `() => ({ zone: Intl.DateTimeFormat().resolvedOptions().timeZone, offset: new Date(0).getTimezoneOffset() })`);
73
- const before = await read();
74
- await tab.emulate({ timezone: "Asia/Tokyo" });
75
- assert.deepEqual(await read(), { zone: "Asia/Tokyo", offset: -540 });
76
- await tab.emulate({ timezone: "America/New_York" });
77
- assert.deepEqual(await read(), { zone: "America/New_York", offset: 300 });
78
- await tab.emulate({ timezone: null });
79
- assert.deepEqual(await read(), before);
80
- });
81
-
82
- test("userAgent changes navigator.userAgent and the header the server receives", OPTIONS, async () => {
83
- const { tab } = await freshPage();
84
- await tab.emulate({ userAgent: "Samurai/1.0 (test)" });
85
- assert.equal(await evaluate<string>(tab, "() => navigator.userAgent"), "Samurai/1.0 (test)");
86
- userAgents.length = 0;
87
- await evaluate(tab, `async () => { await fetch(${JSON.stringify(base + "/ping")}); }`);
88
- assert.deepEqual(userAgents, ["Samurai/1.0 (test)"]);
89
- await tab.emulate({ userAgent: null });
90
- assert.match(await evaluate<string>(tab, "() => navigator.userAgent"), /Firefox/);
91
- });
92
-
93
- test("offline stops the network, and going online again restores it", OPTIONS, async () => {
94
- const { tab } = await freshPage();
95
- const read = () => evaluate<{ online: boolean; fetch: string }>(tab, `async () => ({ online: navigator.onLine, fetch: await fetch(${JSON.stringify(base + "/ping")}).then(() => "ok", () => "failed") })`);
96
- assert.deepEqual(await read(), { online: true, fetch: "ok" });
97
- await tab.emulate({ offline: true });
98
- assert.deepEqual(await read(), { online: false, fetch: "failed" });
99
- await tab.emulate({ offline: false });
100
- assert.deepEqual(await read(), { online: true, fetch: "ok" });
101
- });
102
-
103
- test("orientation changes screen.orientation", OPTIONS, async () => {
104
- const { tab } = await freshPage();
105
- await tab.emulate({ orientation: "portrait" });
106
- assert.equal(await evaluate<string>(tab, "() => screen.orientation.type"), "portrait-primary");
107
- await tab.emulate({ orientation: "landscape-secondary" });
108
- assert.equal(await evaluate<string>(tab, "() => screen.orientation.type"), "landscape-secondary");
109
- await tab.emulate({ orientation: null });
110
- assert.equal(await evaluate<string>(tab, "() => screen.orientation.type"), "landscape-primary");
111
- });
112
-
113
- test("screen changes screen.width and screen.height", OPTIONS, async () => {
114
- const { tab } = await freshPage();
115
- await tab.emulate({ screen: { width: 800, height: 600 } });
116
- assert.deepEqual(await evaluate<number[]>(tab, "() => [screen.width, screen.height]"), [800, 600]);
117
- await tab.emulate({ screen: null });
118
- assert.notDeepEqual(await evaluate<number[]>(tab, "() => [screen.width, screen.height]"), [800, 600]);
119
- });
120
-
121
- test("geolocation answers once the permission is granted", OPTIONS, async () => {
122
- const { tab } = await freshPage();
123
- const position = () =>
124
- evaluate<{ lat?: number; lon?: number; accuracy?: number; error?: string }>(
125
- tab,
126
- `() => new Promise((resolve) => navigator.geolocation.getCurrentPosition(
127
- (p) => resolve({ lat: p.coords.latitude, lon: p.coords.longitude, accuracy: p.coords.accuracy }),
128
- (e) => resolve({ error: e.code + " " + e.message }),
129
- { timeout: 4000, maximumAge: 0 }))`,
130
- );
131
- await tab.grantPermission("geolocation");
132
- await tab.emulate({ geolocation: { latitude: 52.37, longitude: 4.9, accuracy: 10 } });
133
- assert.deepEqual(await position(), { lat: 52.37, lon: 4.9, accuracy: 10 });
134
- await tab.emulate({ geolocation: { latitude: -33.87, longitude: 151.21 } });
135
- const sydney = await position();
136
- assert.deepEqual([sydney.lat, sydney.lon], [-33.87, 151.21]);
137
- });
138
-
139
- test("a context's emulation reaches its pages, also the ones opened later; other contexts are untouched", OPTIONS, async () => {
140
- const language = (p: Page) => evaluate<string>(p, "() => navigator.language");
141
- const ownLanguage = await language(page);
142
- const { context, tab } = await freshPage({ locale: "de-DE" });
143
- assert.equal(await language(tab), "de-DE", "options given to newContext are in place for its first page");
144
- await context.emulate({ locale: "es-ES" });
145
- assert.equal(await language(tab), "es-ES", "an open page follows a change");
146
- const later = await context.newPage();
147
- await later.navigateTo(`${base}/`);
148
- assert.equal(await language(later), "es-ES", "a page opened later is emulated too");
149
- assert.equal(await language(page), ownLanguage, "the default context is not");
150
- });
151
-
152
- test("a page's own emulation wins over its context's, and null falls back to the context", OPTIONS, async () => {
153
- const { context, tab } = await freshPage({ locale: "de-DE" });
154
- const language = () => evaluate<string>(tab, "() => navigator.language");
155
- await tab.emulate({ locale: "fr-FR" });
156
- assert.equal(await language(), "fr-FR");
157
- await tab.emulate({ locale: null });
158
- assert.equal(await language(), "de-DE");
159
- void context;
160
- });
161
-
162
- test("touch and javaScriptEnabled are reported as unsupported by this Firefox, after what came before them was applied", OPTIONS, async () => {
163
- const { tab } = await freshPage();
164
- await assert.rejects(tab.emulate({ locale: "it-IT", touch: 5 }), (err: unknown) => {
165
- assert.ok(err instanceof UnsupportedOperationError);
166
- assert.equal((err as { option?: string }).option, "touch");
167
- assert.deepEqual((err as { applied?: string[] }).applied, ["locale"]);
168
- return true;
169
- });
170
- assert.equal(await evaluate<string>(tab, "() => navigator.language"), "it-IT", "nothing is rolled back");
171
- await assert.rejects(tab.emulate({ javaScriptEnabled: false }), UnsupportedOperationError);
172
- });
173
-
174
- test("newContext refuses an unsupported option and leaves no context behind", OPTIONS, async () => {
175
- const before = browser.contexts().length;
176
- await assert.rejects(browser.newContext({ locale: "nl-NL", touch: 3 }), UnsupportedOperationError);
177
- assert.equal(browser.contexts().length, before);
178
- await assert.rejects(browser.newContext({ timezone: "Not/AZone" }), RangeError);
179
- assert.equal(browser.contexts().length, before);
180
- });
181
-
182
- test("invalid values are refused by the framework before the browser is asked", OPTIONS, async () => {
183
- const { tab } = await freshPage();
184
- await assert.rejects(tab.emulate({ geolocation: { latitude: 100, longitude: 0 } }), RangeError);
185
- await assert.rejects(tab.emulate({ timezone: "Mars/Olympus" }), RangeError);
186
- });