@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,144 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, test } from "node:test";
3
- import { callFunction } from "../script/call-function.js";
4
- import { setContent } from "../testing/browser-fixture.js";
5
- import type { BiDiConnector } from "../transport/bidi-connection.js";
6
- import { Browser } from "./browser.js";
7
- import type { Dialog } from "./dialog.js";
8
- import type Page from "./page.js";
9
-
10
- let browser: Browser;
11
- let page: Page;
12
-
13
- before(async () => {
14
- ({ browser, page } = await Browser.launch("firefox", { port: 9239, headless: true }));
15
- await page.navigateTo("about:blank");
16
- });
17
-
18
- after(async () => {
19
- await page.dispose();
20
- await browser?.close();
21
- });
22
-
23
- const OPTIONS = { timeout: 30000 };
24
- const connector = () => (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
25
- const run = <T>(source: string) => callFunction<T>(connector(), page.contextId, source, [], { awaitPromise: false });
26
- const read = <T>(name: string) => run<T>(`() => window.${name}`);
27
-
28
- /** Starts `code` in the page without waiting for it: a dialog inside would block the command */
29
- const start = (code: string) => run(`() => { setTimeout(() => { ${code} }, 0); }`);
30
-
31
- async function until<T>(read: () => Promise<T | undefined>, what: string, ms = 5000): Promise<T> {
32
- const deadline = Date.now() + ms;
33
- for (;;) {
34
- const value = await read();
35
- if (value !== undefined) return value;
36
- if (Date.now() > deadline) throw new Error(`timed out waiting for ${what}`);
37
- await new Promise((resolve) => setTimeout(resolve, 25));
38
- }
39
- }
40
-
41
- test("an alert is reported and holds the page until it is accepted", OPTIONS, async () => {
42
- const seen: string[] = [];
43
- const onDialog = async (dialog: Dialog) => {
44
- seen.push(`${dialog.type()}: ${dialog.message()}`);
45
- assert.equal(await read("__afterAlert"), undefined, "the page is still stuck in alert()");
46
- await dialog.accept();
47
- };
48
- page.on("dialog", onDialog);
49
- await start(`alert("Saved!"); window.__afterAlert = true;`);
50
- assert.equal(await until(() => read<boolean | undefined>("__afterAlert"), "the page to go on"), true);
51
- assert.deepEqual(seen, ["alert: Saved!"]);
52
- page.off("dialog", onDialog);
53
- });
54
-
55
- test("confirm returns true when accepted and false when dismissed", OPTIONS, async () => {
56
- const answers = [true, false];
57
- const onDialog = (dialog: Dialog) => (answers.shift() ? dialog.accept() : dialog.dismiss());
58
- page.on("dialog", onDialog);
59
- await start(`window.__yes = confirm("Sure?");`);
60
- assert.equal(await until(() => read<boolean | undefined>("__yes"), "the first confirm"), true);
61
- await start(`window.__no = confirm("Sure?");`);
62
- assert.equal(await until(() => read<boolean | undefined>("__no"), "the second confirm"), false);
63
- page.off("dialog", onDialog);
64
- });
65
-
66
- test("a prompt shows its default value and returns the text that was typed", OPTIONS, async () => {
67
- let shown = "";
68
- const onDialog = (dialog: Dialog) => {
69
- shown = `${dialog.message()} / ${dialog.defaultValue()}`;
70
- return dialog.accept("Sam");
71
- };
72
- page.on("dialog", onDialog);
73
- await start(`window.__name = prompt("Name?", "Anonymous");`);
74
- assert.equal(await until(() => read<string | undefined>("__name"), "the prompt"), "Sam");
75
- assert.equal(shown, "Name? / Anonymous");
76
- page.off("dialog", onDialog);
77
- });
78
-
79
- test("a dismissed prompt returns null", OPTIONS, async () => {
80
- const onDialog = (dialog: Dialog) => dialog.dismiss();
81
- page.on("dialog", onDialog);
82
- await start(`window.__cancelled = prompt("Name?") === null;`);
83
- assert.equal(await until(() => read<boolean | undefined>("__cancelled"), "the prompt"), true);
84
- page.off("dialog", onDialog);
85
- });
86
-
87
- test("closed resolves with what the browser reports", OPTIONS, async () => {
88
- let closed: Promise<unknown> | undefined;
89
- const onDialog = async (dialog: Dialog) => {
90
- closed = dialog.closed;
91
- await dialog.accept("typed");
92
- };
93
- page.on("dialog", onDialog);
94
- await start(`window.__r = prompt("x");`);
95
- await until(() => read<string | undefined>("__r"), "the prompt");
96
- assert.deepEqual(await closed, { accepted: true, userText: "typed" });
97
- page.off("dialog", onDialog);
98
- });
99
-
100
- test("an alert nobody handles is dismissed, so a click on its button does not hang", OPTIONS, async () => {
101
- await setContent(
102
- page,
103
- `<button id="b">Go</button>`,
104
- `document.getElementById("b").addEventListener("click", () => { alert("Unhandled"); window.__afterUnhandled = true; });`,
105
- );
106
- await page.locator("button[@id='b']").click({ timeout: 10000 });
107
- assert.equal(await until(() => read<boolean | undefined>("__afterUnhandled"), "the page to go on"), true);
108
- });
109
-
110
- test("a dialog opened by an iframe is handled too", OPTIONS, async () => {
111
- await page.navigateTo("about:blank?frame");
112
- const seen: string[] = [];
113
- const onDialog = async (dialog: Dialog) => {
114
- seen.push(dialog.message());
115
- await dialog.accept();
116
- };
117
- page.on("dialog", onDialog);
118
- await run(`() => {
119
- const frame = document.body.appendChild(document.createElement("iframe"));
120
- frame.src = "about:blank";
121
- frame.addEventListener("load", () => {
122
- frame.contentWindow.setTimeout(() => { frame.contentWindow.alert("from the frame"); window.__frameDone = true; }, 0);
123
- }, { once: true });
124
- }`);
125
- await until(() => read<boolean | undefined>("__frameDone"), "the frame's alert");
126
- assert.deepEqual(seen, ["from the frame"]);
127
- page.off("dialog", onDialog);
128
- });
129
-
130
- test("a beforeunload prompt does not keep the page from navigating away", OPTIONS, async (t) => {
131
- await page.navigateTo("about:blank?unload");
132
- await setContent(page, `<button id="b">x</button>`, `window.addEventListener("beforeunload", (e) => { e.preventDefault(); e.returnValue = "leave?"; });`);
133
- await page.locator("button[@id='b']").click({ timeout: 10000 }); // user activation, or the browser skips the prompt
134
- const seen: string[] = [];
135
- const onDialog = (dialog: Dialog) => {
136
- seen.push(dialog.type());
137
- };
138
- page.on("dialog", onDialog);
139
- await page.navigateTo("about:blank?after", { timeout: 10000 });
140
- page.off("dialog", onDialog);
141
- assert.equal(await run<string>("() => location.search"), "?after");
142
- assert.deepEqual(seen, ["beforeunload"], "the prompt appeared, and the default policy accepted it");
143
- void t;
144
- });
@@ -1,322 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, describe, it, mock } from "node:test";
3
- import logger from "../logger/index.js";
4
- import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
5
- import { BiDiConnector } from "../transport/bidi-connection.js";
6
- import { SESSION_CAPABILITIES } from "./browser.js";
7
- import type { Dialog } from "./dialog.js";
8
- import Page from "./page.js";
9
-
10
- const TREE = [
11
- { context: "ctx-1", parent: null, children: [{ context: "frame", parent: "ctx-1", children: [] }] },
12
- { context: "other", parent: null, children: [] },
13
- ];
14
-
15
- const stops: Array<() => void> = [];
16
- let warn: ReturnType<typeof mock.method>;
17
- let error: ReturnType<typeof mock.method>;
18
- beforeEach(() => {
19
- warn = mock.method(logger, "warn", () => logger);
20
- error = mock.method(logger, "error", () => logger);
21
- });
22
- afterEach(() => {
23
- mock.restoreAll();
24
- stops.splice(0).forEach((stop) => stop());
25
- });
26
-
27
- function setup(handlers: Record<string, (params: any) => object | Error> = {}) {
28
- const ws = new FakeWebSocket();
29
- const connector = new BiDiConnector(ws as unknown as WebSocket);
30
- stops.push(
31
- autoReply(ws, {
32
- "browsingContext.getTree": () => ({ contexts: TREE }),
33
- "browsingContext.handleUserPrompt": () => ({}),
34
- ...handlers,
35
- }),
36
- );
37
- const page = new Page(connector, "ctx-1");
38
- const opened = (extra: object = {}) =>
39
- ws.emitEvent("browsingContext.userPromptOpened", {
40
- context: "ctx-1",
41
- handler: "ignore",
42
- message: "Are you sure?",
43
- type: "confirm",
44
- ...extra,
45
- });
46
- const closed = (extra: object = {}) =>
47
- ws.emitEvent("browsingContext.userPromptClosed", { context: "ctx-1", accepted: true, type: "confirm", ...extra });
48
- const handled = () =>
49
- ws.sent.filter((m) => m.method === "browsingContext.handleUserPrompt").map((m) => m.params as any);
50
- return { ws, page, opened, closed, handled };
51
- }
52
-
53
- describe("session capability", () => {
54
- it("asks the browser to leave prompts open until the framework answers", () => {
55
- assert.deepEqual(SESSION_CAPABILITIES, {
56
- alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
57
- });
58
- });
59
- });
60
-
61
- describe("Dialog", () => {
62
- it("describes the prompt", async () => {
63
- const { page, opened } = setup();
64
- let seen!: Dialog;
65
- page.on("dialog", (dialog) => {
66
- seen = dialog;
67
- return dialog.dismiss();
68
- });
69
- await page.startDialogHandling();
70
- opened({ type: "prompt", message: "Name?", defaultValue: "Sam" });
71
- await tick(20);
72
- assert.equal(seen.type(), "prompt");
73
- assert.equal(seen.message(), "Name?");
74
- assert.equal(seen.defaultValue(), "Sam");
75
- });
76
-
77
- it("defaultValue is empty for other prompts", async () => {
78
- const { page, opened } = setup();
79
- let seen!: Dialog;
80
- page.on("dialog", (dialog) => {
81
- seen = dialog;
82
- return dialog.dismiss();
83
- });
84
- await page.startDialogHandling();
85
- opened({ type: "alert" });
86
- await tick(20);
87
- assert.equal(seen.defaultValue(), "");
88
- });
89
-
90
- it("accept sends the text, dismiss does not accept", async () => {
91
- const { page, opened, handled } = setup();
92
- page.on("dialog", (dialog) => (dialog.type() === "prompt" ? dialog.accept("Sam") : dialog.dismiss()));
93
- await page.startDialogHandling();
94
- opened({ type: "prompt" });
95
- opened({ type: "confirm" });
96
- await tick(30);
97
- assert.deepEqual(handled(), [
98
- { context: "ctx-1", accept: true, userText: "Sam" },
99
- { context: "ctx-1", accept: false },
100
- ]);
101
- });
102
-
103
- it("accept without text sends no userText", async () => {
104
- const { page, opened, handled } = setup();
105
- page.on("dialog", (dialog) => dialog.accept());
106
- await page.startDialogHandling();
107
- opened({ type: "confirm" });
108
- await tick(20);
109
- assert.deepEqual(handled(), [{ context: "ctx-1", accept: true }]);
110
- });
111
-
112
- it("can be handled once; a second call throws", async () => {
113
- const { page, opened } = setup();
114
- let second: unknown;
115
- page.on("dialog", async (dialog) => {
116
- await dialog.accept();
117
- second = await dialog.dismiss().catch((err: Error) => err);
118
- });
119
- await page.startDialogHandling();
120
- opened();
121
- await tick(30);
122
- assert.match((second as Error).message, /dialog already handled/);
123
- });
124
-
125
- it("swallows no such alert: the dialog is gone, which is what the caller wanted", async () => {
126
- const gone = Object.assign(new Error("no alert open"), { code: "no such alert" });
127
- const { page, opened } = setup({ "browsingContext.handleUserPrompt": () => gone });
128
- let outcome = "not run";
129
- page.on("dialog", async (dialog) => {
130
- await dialog.accept();
131
- outcome = "accepted";
132
- });
133
- await page.startDialogHandling();
134
- opened();
135
- await tick(30);
136
- assert.equal(outcome, "accepted");
137
- });
138
-
139
- it("other browser errors reach the caller", async () => {
140
- const broken = Object.assign(new Error("boom"), { code: "unknown error" });
141
- const { page, opened } = setup({ "browsingContext.handleUserPrompt": () => broken });
142
- let outcome: unknown;
143
- page.on("dialog", async (dialog) => {
144
- outcome = await dialog.accept().catch((err: Error) => err);
145
- });
146
- await page.startDialogHandling();
147
- opened();
148
- await tick(30);
149
- assert.equal((outcome as Error).name, "BiDiError");
150
- });
151
-
152
- it("closed resolves with the answer from userPromptClosed", async () => {
153
- const { page, opened, closed } = setup();
154
- let seen!: Dialog;
155
- page.on("dialog", (dialog) => {
156
- seen = dialog;
157
- });
158
- await page.startDialogHandling();
159
- opened({ type: "prompt" });
160
- await tick(20);
161
- let result: unknown;
162
- void seen.closed.then((value) => (result = value));
163
- closed({ type: "prompt", accepted: true, userText: "typed" });
164
- await tick(10);
165
- assert.deepEqual(result, { accepted: true, userText: "typed" });
166
- });
167
- });
168
-
169
- describe("default policy", () => {
170
- it("dismisses an alert nobody handled and says so", async () => {
171
- const { page, opened, handled } = setup();
172
- await page.startDialogHandling();
173
- opened({ type: "alert", message: "Saved!" });
174
- await tick(30);
175
- assert.deepEqual(handled(), [{ context: "ctx-1", accept: false }]);
176
- assert.equal(warn.mock.callCount(), 1);
177
- const [template, message, outcome] = warn.mock.calls[0]!.arguments as string[];
178
- assert.match(template!, /Dialog "%s" was %s automatically; add page\.on\("dialog"\) to handle it/);
179
- assert.deepEqual([message, outcome], ["Saved!", "dismissed"]);
180
- });
181
-
182
- it("dismisses confirm and prompt too", async () => {
183
- const { page, opened, handled } = setup();
184
- await page.startDialogHandling();
185
- opened({ type: "confirm" });
186
- opened({ type: "prompt" });
187
- await tick(30);
188
- assert.deepEqual(handled().map((h) => h.accept), [false, false]);
189
- });
190
-
191
- it("accepts beforeunload so navigation can go on", async () => {
192
- const { page, opened, handled } = setup();
193
- await page.startDialogHandling();
194
- opened({ type: "beforeunload", message: "" });
195
- await tick(30);
196
- assert.deepEqual(handled(), [{ context: "ctx-1", accept: true }]);
197
- assert.equal((warn.mock.calls[0]!.arguments as string[])[2], "accepted");
198
- });
199
-
200
- it("applies when a listener returns without handling the dialog", async () => {
201
- const { page, opened, handled } = setup();
202
- page.on("dialog", () => {});
203
- await page.startDialogHandling();
204
- opened({ type: "confirm" });
205
- await tick(30);
206
- assert.deepEqual(handled(), [{ context: "ctx-1", accept: false }]);
207
- });
208
-
209
- it("waits for an async listener before applying, and not at all if it handled the dialog", async () => {
210
- const { page, opened, handled } = setup();
211
- page.on("dialog", async (dialog) => {
212
- await tick(40);
213
- await dialog.accept();
214
- });
215
- await page.startDialogHandling();
216
- opened({ type: "confirm" });
217
- await tick(15);
218
- assert.deepEqual(handled(), [], "still waiting for the listener");
219
- await tick(60);
220
- assert.deepEqual(handled(), [{ context: "ctx-1", accept: true }]);
221
- assert.equal(warn.mock.callCount(), 0);
222
- });
223
-
224
- it("applies after a listener threw, and logs the error", async () => {
225
- const { page, opened, handled } = setup();
226
- page.on("dialog", () => {
227
- throw new Error("listener failed");
228
- });
229
- await page.startDialogHandling();
230
- opened({ type: "confirm" });
231
- await tick(30);
232
- assert.deepEqual(handled(), [{ context: "ctx-1", accept: false }]);
233
- assert.ok(error.mock.callCount() >= 1);
234
- });
235
-
236
- it("does not answer a prompt the browser already handled, but still tells the listeners", async () => {
237
- const { page, opened, handled } = setup();
238
- let notified: Dialog | undefined;
239
- page.on("dialog", (dialog) => {
240
- notified = dialog;
241
- });
242
- await page.startDialogHandling();
243
- opened({ handler: "dismiss" });
244
- await tick(30);
245
- assert.deepEqual(handled(), []);
246
- assert.ok(notified);
247
- await assert.rejects(notified.accept(), /dialog already handled/);
248
- });
249
- });
250
-
251
- describe("which prompts belong to the page", () => {
252
- it("handles a prompt from an iframe, answering through the top-level context", async () => {
253
- const { page, opened, handled } = setup();
254
- page.on("dialog", (dialog) => dialog.accept());
255
- await page.startDialogHandling();
256
- opened({ context: "frame" });
257
- await tick(20);
258
- assert.deepEqual(handled(), [{ context: "ctx-1", accept: true }]);
259
- });
260
-
261
- it("ignores prompts of other pages", async () => {
262
- const { page, opened, handled } = setup();
263
- let count = 0;
264
- page.on("dialog", () => count++);
265
- await page.startDialogHandling();
266
- opened({ context: "other" });
267
- await tick(30);
268
- assert.equal(count, 0);
269
- assert.deepEqual(handled(), []);
270
- });
271
- });
272
-
273
- describe("listener management", () => {
274
- it("off removes a listener, once fires a single time", async () => {
275
- const { page, opened } = setup();
276
- let onceCount = 0;
277
- let offCount = 0;
278
- const off = () => {
279
- offCount++;
280
- };
281
- page.on("dialog", off);
282
- page.off("dialog", off);
283
- page.once("dialog", (dialog) => {
284
- onceCount++;
285
- return dialog.dismiss();
286
- });
287
- await page.startDialogHandling();
288
- opened();
289
- await tick(20);
290
- opened();
291
- await tick(20);
292
- assert.equal(onceCount, 1);
293
- assert.equal(offCount, 0);
294
- });
295
-
296
- it("page.on starts dialog handling by itself", async () => {
297
- const { ws, page } = setup();
298
- page.on("dialog", () => {});
299
- await tick(40);
300
- assert.ok(ws.sent.some((m) => m.method === "session.subscribe" && (m.params as any).events.includes("browsingContext.userPromptOpened")));
301
- });
302
-
303
- it("dispose stops handling and unsubscribes", async () => {
304
- const { ws, page, opened, handled } = setup();
305
- await page.startDialogHandling();
306
- await page.dispose();
307
- await tick(20);
308
- opened();
309
- await tick(20);
310
- assert.deepEqual(handled(), []);
311
- assert.ok(ws.sent.filter((m) => m.method === "session.unsubscribe").length >= 2);
312
- });
313
-
314
- it("startDialogHandling is idempotent", async () => {
315
- const { ws, page } = setup();
316
- await Promise.all([page.startDialogHandling(), page.startDialogHandling()]);
317
- const prompts = ws.sent.filter(
318
- (m) => m.method === "session.subscribe" && (m.params as any).events[0].startsWith("browsingContext.userPrompt"),
319
- );
320
- assert.equal(prompts.length, 2, "opened + closed, once");
321
- });
322
- });
@@ -1,83 +0,0 @@
1
- import type { BiDiConnector } from "../transport/bidi-connection.js";
2
- import { BiDiError } from "../transport/bidi-error.js";
3
- import type {
4
- UserPromptOpenedParameters,
5
- UserPromptType,
6
- } from "../types/bidi-modules/browsing-context.js";
7
-
8
- export interface DialogResult {
9
- accepted: boolean;
10
- /** What was typed into a `prompt` */
11
- userText?: string;
12
- }
13
-
14
- /**
15
- * A JavaScript dialog (`alert`, `confirm`, `prompt`) or a `beforeunload` prompt that is open in the page.
16
- * Answer it once with `accept()` or `dismiss()`; `page.on("dialog")` hands these out.
17
- */
18
- export class Dialog {
19
- /** Resolves when the browser reports the prompt closed */
20
- public readonly closed: Promise<DialogResult>;
21
- private resolveClosed!: (result: DialogResult) => void;
22
- private answered: boolean;
23
-
24
- constructor(
25
- private connector: BiDiConnector,
26
- /** The top-level context of the page; prompts are answered there, also when an iframe opened them */
27
- private context: string,
28
- private info: UserPromptOpenedParameters,
29
- ) {
30
- this.closed = new Promise((resolve) => (this.resolveClosed = resolve));
31
- // With another handler than "ignore" the browser has answered already
32
- this.answered = info.handler !== "ignore";
33
- }
34
-
35
- public type(): UserPromptType {
36
- return this.info.type;
37
- }
38
-
39
- public message(): string {
40
- return this.info.message;
41
- }
42
-
43
- /** The text a `prompt` starts with; empty for the other kinds */
44
- public defaultValue(): string {
45
- return this.info.defaultValue ?? "";
46
- }
47
-
48
- /** True once the dialog is answered, by you or by the browser */
49
- public get handled(): boolean {
50
- return this.answered;
51
- }
52
-
53
- /** Clicks OK; `promptText` is typed into a `prompt` first */
54
- public accept(promptText?: string): Promise<void> {
55
- return this.answer({ accept: true, ...(promptText !== undefined && { userText: promptText }) });
56
- }
57
-
58
- /** Clicks Cancel */
59
- public dismiss(): Promise<void> {
60
- return this.answer({ accept: false });
61
- }
62
-
63
- /** Called by the page when the browser reports the prompt closed */
64
- public markClosed(result: DialogResult): void {
65
- this.answered = true;
66
- this.resolveClosed(result);
67
- }
68
-
69
- private async answer(params: { accept: boolean; userText?: string }): Promise<void> {
70
- if (this.answered) throw new Error("dialog already handled");
71
- this.answered = true;
72
- try {
73
- await this.connector.send("browsingContext.handleUserPrompt", {
74
- context: this.context,
75
- ...params,
76
- });
77
- } catch (err) {
78
- // Closed meanwhile (a navigation, another handler): the dialog is gone, which is what was wanted
79
- if (err instanceof BiDiError && err.code === "no such alert") return;
80
- throw err;
81
- }
82
- }
83
- }