@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
@@ -0,0 +1,120 @@
1
+ import type { ContextTree } from "../browser/context-tree.js";
2
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
3
+ import { type NetworkRequest } from "./network-tracker.js";
4
+ import { type UrlPattern } from "./url-match.js";
5
+ export type RouteHandler = (route: Route) => void | Promise<void>;
6
+ export interface FulfillOptions {
7
+ /** @default 200 */
8
+ status?: number;
9
+ headers?: Record<string, string>;
10
+ /** Sets `content-type` unless `headers` has one; `json` implies "application/json" */
11
+ contentType?: string;
12
+ body?: string | Buffer;
13
+ /** Sent as JSON text; cannot be combined with `body` */
14
+ json?: unknown;
15
+ }
16
+ export interface ContinueOptions {
17
+ /**
18
+ * Sends the request to another URL. Firefox applies this to navigations only: the page's `fetch`, XHR and
19
+ * image requests fail when their URL is changed, so mock those with `fulfill` instead
20
+ */
21
+ url?: string;
22
+ method?: string;
23
+ /** Replaces all request headers */
24
+ headers?: Record<string, string>;
25
+ postData?: string | Buffer;
26
+ }
27
+ interface RouteEntry {
28
+ pattern: UrlPattern;
29
+ handler: RouteHandler;
30
+ }
31
+ /** Answers `network.*` commands for one blocked request */
32
+ interface RouteDeps {
33
+ send: BiDiConnector["send"];
34
+ /** Passes the request to the next matching route, or lets it continue */
35
+ next: () => Promise<void>;
36
+ /** Called once the route is answered, whichever way */
37
+ settled: (route: Route) => void;
38
+ }
39
+ /**
40
+ * A request the browser holds until it is answered. Answer it exactly once with `fulfill`, `continue`,
41
+ * `abort` or `fallback`; a second call throws.
42
+ */
43
+ export declare class Route {
44
+ private readonly info;
45
+ /** @internal */
46
+ readonly entry: RouteEntry;
47
+ private readonly deps;
48
+ private answered;
49
+ /** @internal */
50
+ timer: ReturnType<typeof setTimeout> | undefined;
51
+ constructor(info: NetworkRequest,
52
+ /** @internal */
53
+ entry: RouteEntry, deps: RouteDeps);
54
+ request(): NetworkRequest;
55
+ /** @internal */
56
+ get isAnswered(): boolean;
57
+ /** Answers the request without sending it: status 200 and no body unless told otherwise */
58
+ fulfill(options?: FulfillOptions): Promise<void>;
59
+ /**
60
+ * Lets the request go on, optionally changed. `headers` replaces all request headers. A new `postData` brings its
61
+ * own `content-length` (the request's old one would cut the new body off or leave the server waiting).
62
+ */
63
+ continue(options?: ContinueOptions): Promise<void>;
64
+ /** Makes the request fail (the browser reports a network error to the page) */
65
+ abort(): Promise<void>;
66
+ /** Hands the request to the next route that matches it, or lets it continue when there is none */
67
+ fallback(): Promise<void>;
68
+ /** @internal Lets the request continue unchanged; failures are logged, not thrown */
69
+ release(): Promise<void>;
70
+ /** @internal Fails the request; failures are logged, not thrown */
71
+ failQuietly(): Promise<void>;
72
+ private claim;
73
+ private answer;
74
+ }
75
+ /**
76
+ * Lets a page mock, block and change requests. One `network.addIntercept` serves all routes; matching
77
+ * happens here. Every request the browser holds is answered exactly once, so nothing hangs.
78
+ */
79
+ export declare class Router {
80
+ private connector;
81
+ private tree;
82
+ private contextId;
83
+ private routes;
84
+ private pending;
85
+ private failures;
86
+ private queue;
87
+ private interceptIds;
88
+ private currentKey;
89
+ private currentId;
90
+ private subscription;
91
+ private disposed;
92
+ private routeTimeout;
93
+ constructor(connector: BiDiConnector, tree: ContextTree, contextId: string, options?: {
94
+ routeTimeout?: number;
95
+ });
96
+ /** Errors thrown by route handlers, oldest first */
97
+ get errors(): readonly Error[];
98
+ /** Sends matching requests to `handler`. Later routes are asked first */
99
+ route(pattern: UrlPattern, handler: RouteHandler): Promise<void>;
100
+ /** Removes the routes for `pattern` (only the one with `handler`, when given) */
101
+ unroute(pattern: UrlPattern, handler?: RouteHandler): Promise<void>;
102
+ unrouteAll(): Promise<void>;
103
+ /** Lets held requests continue, removes the intercept and stops listening. Idempotent */
104
+ dispose(): Promise<void>;
105
+ private remove;
106
+ private enqueue;
107
+ /** Makes the browser-side intercept match the routes: none, one for everything, or one for exact URLs */
108
+ private sync;
109
+ /** The exact URLs to block, or undefined to block everything and match here */
110
+ private urlPatterns;
111
+ private addIntercept;
112
+ private removeIntercept;
113
+ private listen;
114
+ private stop;
115
+ private onBeforeRequestSent;
116
+ /** Runs the `index`-th matching route; past the last one the request continues unchanged */
117
+ private dispatch;
118
+ private record;
119
+ }
120
+ export {};
@@ -0,0 +1,343 @@
1
+ import { STATUS_CODES } from "node:http";
2
+ import logger from "../logger/index.js";
3
+ import { BiDiError } from "../transport/bidi-error.js";
4
+ import { toRequest } from "./network-tracker.js";
5
+ import { isExactString, matchUrl, samePattern } from "./url-match.js";
6
+ /** Time (ms) a handler may take to answer a request before the router lets it continue */
7
+ const DEFAULT_ROUTE_TIMEOUT = 30000;
8
+ /** How many handler errors the router keeps */
9
+ const MAX_ERRORS = 100;
10
+ function toHeaders(headers) {
11
+ return Object.entries(headers).map(([name, value]) => ({ name, value: { type: "string", value } }));
12
+ }
13
+ function toBytes(data) {
14
+ return typeof data === "string"
15
+ ? { type: "string", value: data }
16
+ : { type: "base64", value: data.toString("base64") };
17
+ }
18
+ const hasHeader = (headers, name) => Object.keys(headers).some((key) => key.toLowerCase() === name);
19
+ /**
20
+ * A request the browser holds until it is answered. Answer it exactly once with `fulfill`, `continue`,
21
+ * `abort` or `fallback`; a second call throws.
22
+ */
23
+ export class Route {
24
+ constructor(info,
25
+ /** @internal */
26
+ entry, deps) {
27
+ this.info = info;
28
+ this.entry = entry;
29
+ this.deps = deps;
30
+ this.answered = false;
31
+ }
32
+ request() {
33
+ return this.info;
34
+ }
35
+ /** @internal */
36
+ get isAnswered() {
37
+ return this.answered;
38
+ }
39
+ /** Answers the request without sending it: status 200 and no body unless told otherwise */
40
+ async fulfill(options = {}) {
41
+ if (options.json !== undefined && options.body !== undefined) {
42
+ throw new TypeError("route.fulfill(): use either json or body, not both");
43
+ }
44
+ const text = options.json !== undefined ? JSON.stringify(options.json) : undefined;
45
+ const body = options.body ?? text;
46
+ const headers = { ...options.headers };
47
+ const contentType = options.contentType ?? (text !== undefined ? "application/json" : undefined);
48
+ if (contentType && !hasHeader(headers, "content-type"))
49
+ headers["content-type"] = contentType;
50
+ if (!hasHeader(headers, "content-length")) {
51
+ headers["content-length"] = String(body === undefined ? 0 : Buffer.byteLength(body));
52
+ }
53
+ const status = options.status ?? 200;
54
+ await this.answer("network.provideResponse", {
55
+ request: this.info.id,
56
+ statusCode: status,
57
+ reasonPhrase: STATUS_CODES[status] ?? "",
58
+ headers: toHeaders(headers),
59
+ ...(body !== undefined && { body: toBytes(body) }),
60
+ });
61
+ }
62
+ /**
63
+ * Lets the request go on, optionally changed. `headers` replaces all request headers. A new `postData` brings its
64
+ * own `content-length` (the request's old one would cut the new body off or leave the server waiting).
65
+ */
66
+ async continue(options = {}) {
67
+ let headers = options.headers;
68
+ if (options.postData !== undefined) {
69
+ const withoutLength = Object.fromEntries(Object.entries(headers ?? this.info.headers).filter(([name]) => name.toLowerCase() !== "content-length"));
70
+ headers = { ...withoutLength, "content-length": String(Buffer.byteLength(options.postData)) };
71
+ }
72
+ await this.answer("network.continueRequest", {
73
+ request: this.info.id,
74
+ ...(options.url !== undefined && { url: options.url }),
75
+ ...(options.method !== undefined && { method: options.method }),
76
+ ...(headers !== undefined && { headers: toHeaders(headers) }),
77
+ ...(options.postData !== undefined && { body: toBytes(options.postData) }),
78
+ });
79
+ }
80
+ /** Makes the request fail (the browser reports a network error to the page) */
81
+ async abort() {
82
+ await this.answer("network.failRequest", { request: this.info.id });
83
+ }
84
+ /** Hands the request to the next route that matches it, or lets it continue when there is none */
85
+ async fallback() {
86
+ this.claim();
87
+ await this.deps.next();
88
+ }
89
+ /** @internal Lets the request continue unchanged; failures are logged, not thrown */
90
+ async release() {
91
+ if (this.answered)
92
+ return;
93
+ try {
94
+ await this.continue();
95
+ }
96
+ catch (err) {
97
+ logger.debug("Could not let %s continue", this.info.url, { err });
98
+ }
99
+ }
100
+ /** @internal Fails the request; failures are logged, not thrown */
101
+ async failQuietly() {
102
+ if (this.answered)
103
+ return;
104
+ try {
105
+ await this.abort();
106
+ }
107
+ catch (err) {
108
+ logger.debug("Could not fail %s", this.info.url, { err });
109
+ }
110
+ }
111
+ claim() {
112
+ if (this.answered)
113
+ throw new Error("route already handled");
114
+ this.answered = true;
115
+ clearTimeout(this.timer);
116
+ this.deps.settled(this);
117
+ }
118
+ async answer(method, params) {
119
+ this.claim();
120
+ await this.deps.send(method, params);
121
+ }
122
+ }
123
+ /** Whether a browser accepts `urlPatterns` in `network.addIntercept`; asked once per connection */
124
+ const patternsUnsupported = new WeakSet();
125
+ /**
126
+ * Lets a page mock, block and change requests. One `network.addIntercept` serves all routes; matching
127
+ * happens here. Every request the browser holds is answered exactly once, so nothing hangs.
128
+ */
129
+ export class Router {
130
+ constructor(connector, tree, contextId, options = {}) {
131
+ this.connector = connector;
132
+ this.tree = tree;
133
+ this.contextId = contextId;
134
+ this.routes = [];
135
+ this.pending = new Set();
136
+ this.failures = [];
137
+ this.queue = Promise.resolve();
138
+ this.interceptIds = new Set();
139
+ this.disposed = false;
140
+ this.onBeforeRequestSent = (params) => {
141
+ if (!params.isBlocked || params.context === null)
142
+ return;
143
+ if (!this.tree.isWithin(params.context, this.contextId))
144
+ return;
145
+ // Held by an intercept of ours, not someone else's
146
+ if (!(params.intercepts ?? []).some((id) => this.interceptIds.has(id)))
147
+ return;
148
+ const info = toRequest(params.request, params.navigation, params.isBlocked, null);
149
+ const matching = this.routes.filter((entry) => matchUrl(entry.pattern, info.url)).reverse();
150
+ this.dispatch(info, matching, 0).catch((err) => logger.error("Handling the request to %s failed", info.url, { err }));
151
+ };
152
+ this.routeTimeout = options.routeTimeout ?? DEFAULT_ROUTE_TIMEOUT;
153
+ }
154
+ /** Errors thrown by route handlers, oldest first */
155
+ get errors() {
156
+ return [...this.failures];
157
+ }
158
+ /** Sends matching requests to `handler`. Later routes are asked first */
159
+ async route(pattern, handler) {
160
+ if (this.disposed)
161
+ throw new Error("router disposed");
162
+ const entry = { pattern, handler };
163
+ this.routes.push(entry);
164
+ try {
165
+ await this.enqueue(() => this.sync());
166
+ }
167
+ catch (err) {
168
+ this.routes = this.routes.filter((route) => route !== entry);
169
+ throw err;
170
+ }
171
+ }
172
+ /** Removes the routes for `pattern` (only the one with `handler`, when given) */
173
+ async unroute(pattern, handler) {
174
+ const removed = this.routes.filter((route) => samePattern(route.pattern, pattern) && (handler === undefined || route.handler === handler));
175
+ await this.remove(removed);
176
+ }
177
+ async unrouteAll() {
178
+ await this.remove([...this.routes]);
179
+ }
180
+ /** Lets held requests continue, removes the intercept and stops listening. Idempotent */
181
+ async dispose() {
182
+ if (this.disposed)
183
+ return;
184
+ this.disposed = true;
185
+ await this.remove([...this.routes]);
186
+ }
187
+ async remove(removed) {
188
+ if (removed.length === 0)
189
+ return;
190
+ this.routes = this.routes.filter((route) => !removed.includes(route));
191
+ // Requests a removed route still holds would wait for nobody
192
+ await Promise.all([...this.pending].filter((route) => removed.includes(route.entry)).map((route) => route.release()));
193
+ await this.enqueue(() => this.sync());
194
+ }
195
+ enqueue(work) {
196
+ const run = this.queue.then(work);
197
+ this.queue = run.catch(() => undefined);
198
+ return run;
199
+ }
200
+ /** Makes the browser-side intercept match the routes: none, one for everything, or one for exact URLs */
201
+ async sync() {
202
+ if (this.routes.length === 0)
203
+ return this.stop();
204
+ await this.listen();
205
+ const patterns = this.urlPatterns();
206
+ const key = patterns ? JSON.stringify(patterns) : "all";
207
+ if (key === this.currentKey)
208
+ return;
209
+ const id = await this.addIntercept(patterns);
210
+ const previous = this.currentId;
211
+ this.currentId = id;
212
+ this.currentKey = patterns && !patternsUnsupported.has(this.connector) ? key : "all";
213
+ this.interceptIds.add(id);
214
+ // The new intercept is in place before the old one goes: nothing slips through in between
215
+ if (previous !== undefined)
216
+ await this.removeIntercept(previous);
217
+ }
218
+ /** The exact URLs to block, or undefined to block everything and match here */
219
+ urlPatterns() {
220
+ if (patternsUnsupported.has(this.connector))
221
+ return undefined;
222
+ const patterns = [];
223
+ for (const { pattern } of this.routes) {
224
+ // The browser reads a URL string in normal form; anything else could mean something else to it
225
+ if (!isExactString(pattern) || !isNormalUrl(pattern))
226
+ return undefined;
227
+ if (!patterns.some((existing) => existing.type === "string" && existing.pattern === pattern)) {
228
+ patterns.push({ type: "string", pattern });
229
+ }
230
+ }
231
+ return patterns;
232
+ }
233
+ async addIntercept(patterns) {
234
+ const params = {
235
+ phases: ["beforeRequestSent"],
236
+ contexts: [this.contextId],
237
+ };
238
+ try {
239
+ const { intercept } = await this.connector.send("network.addIntercept", {
240
+ ...params,
241
+ ...(patterns && { urlPatterns: patterns }),
242
+ });
243
+ return intercept;
244
+ }
245
+ catch (err) {
246
+ if (patterns && err instanceof BiDiError && err.code === "invalid argument") {
247
+ patternsUnsupported.add(this.connector);
248
+ return this.addIntercept(undefined);
249
+ }
250
+ throw err;
251
+ }
252
+ }
253
+ async removeIntercept(id) {
254
+ try {
255
+ await this.connector.send("network.removeIntercept", { intercept: id });
256
+ }
257
+ catch (err) {
258
+ if (!(err instanceof BiDiError && err.code === "no such intercept"))
259
+ throw err;
260
+ }
261
+ finally {
262
+ this.interceptIds.delete(id);
263
+ }
264
+ }
265
+ async listen() {
266
+ if (this.subscription)
267
+ return;
268
+ this.connector.onEvent("network.beforeRequestSent", this.onBeforeRequestSent);
269
+ try {
270
+ this.subscription = await this.connector.subscribe(["network.beforeRequestSent"]);
271
+ }
272
+ catch (err) {
273
+ this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
274
+ throw err;
275
+ }
276
+ }
277
+ async stop() {
278
+ const id = this.currentId;
279
+ this.currentId = undefined;
280
+ this.currentKey = undefined;
281
+ if (id !== undefined)
282
+ await this.removeIntercept(id);
283
+ if (this.subscription) {
284
+ this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
285
+ const subscription = this.subscription;
286
+ this.subscription = undefined;
287
+ await subscription.unsubscribe();
288
+ }
289
+ }
290
+ /** Runs the `index`-th matching route; past the last one the request continues unchanged */
291
+ async dispatch(info, matching, index) {
292
+ const entry = matching[index];
293
+ const deps = {
294
+ send: (method, params) => this.connector.send(method, params),
295
+ next: () => this.dispatch(info, matching, index + 1),
296
+ settled: (settled) => {
297
+ this.pending.delete(settled);
298
+ },
299
+ };
300
+ if (!entry) {
301
+ // Nobody (left) to ask: an unmatched request, or every route fell back
302
+ await new Route(info, { pattern: "", handler: () => { } }, deps).release();
303
+ return;
304
+ }
305
+ const route = new Route(info, entry, deps);
306
+ this.pending.add(route);
307
+ route.timer = setTimeout(() => {
308
+ if (route.isAnswered)
309
+ return;
310
+ logger.error("A route for %s did not answer within %dms; letting the request continue", info.url, this.routeTimeout);
311
+ void route.release();
312
+ }, this.routeTimeout);
313
+ route.timer.unref?.();
314
+ try {
315
+ await entry.handler(route);
316
+ }
317
+ catch (err) {
318
+ this.record(info, err);
319
+ await route.failQuietly();
320
+ return;
321
+ }
322
+ if (!route.isAnswered) {
323
+ logger.warn("The route for %s did not answer the request; letting it continue", info.url);
324
+ await route.release();
325
+ }
326
+ }
327
+ record(info, err) {
328
+ const message = err instanceof Error ? err.message : String(err);
329
+ const error = Object.assign(new Error(`route handler for ${info.url} threw: ${message}`), { cause: err });
330
+ this.failures.push(error);
331
+ if (this.failures.length > MAX_ERRORS)
332
+ this.failures.shift();
333
+ logger.error("A route handler threw for %s", info.url, { err });
334
+ }
335
+ }
336
+ function isNormalUrl(value) {
337
+ try {
338
+ return new URL(value).href === value;
339
+ }
340
+ catch {
341
+ return false;
342
+ }
343
+ }
@@ -0,0 +1,8 @@
1
+ /** A URL to match: an exact string, a glob (`*` = anything but `/`, `**` = anything) or a RegExp */
2
+ export type UrlPattern = string | RegExp;
3
+ /** True for a string without `*`: it must equal the URL */
4
+ export declare function isExactString(pattern: UrlPattern): pattern is string;
5
+ /** Whether `url` matches `pattern` */
6
+ export declare function matchUrl(pattern: UrlPattern, url: string): boolean;
7
+ /** Same pattern, so `unroute` finds the route a `route` call made */
8
+ export declare function samePattern(a: UrlPattern, b: UrlPattern): boolean;
@@ -0,0 +1,38 @@
1
+ /** True for a string without `*`: it must equal the URL */
2
+ export function isExactString(pattern) {
3
+ return typeof pattern === "string" && !pattern.includes("*");
4
+ }
5
+ function globToRegExp(glob) {
6
+ let source = "";
7
+ for (let i = 0; i < glob.length; i++) {
8
+ const char = glob[i];
9
+ if (char === "*") {
10
+ if (glob[i + 1] === "*") {
11
+ source += ".*";
12
+ i++;
13
+ }
14
+ else {
15
+ source += "[^/]*";
16
+ }
17
+ }
18
+ else {
19
+ source += char.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
20
+ }
21
+ }
22
+ return new RegExp(`^${source}$`);
23
+ }
24
+ /** Whether `url` matches `pattern` */
25
+ export function matchUrl(pattern, url) {
26
+ if (typeof pattern === "string") {
27
+ return isExactString(pattern) ? pattern === url : globToRegExp(pattern).test(url);
28
+ }
29
+ // A global or sticky RegExp remembers where it stopped; every match must start fresh
30
+ pattern.lastIndex = 0;
31
+ return pattern.test(url);
32
+ }
33
+ /** Same pattern, so `unroute` finds the route a `route` call made */
34
+ export function samePattern(a, b) {
35
+ if (typeof a === "string" || typeof b === "string")
36
+ return a === b;
37
+ return a.source === b.source && a.flags === b.flags;
38
+ }
@@ -0,0 +1,3 @@
1
+ import type { RecorderEvent } from "./normalise.js";
2
+ /** Writes a recorder event into the spec source, as an edit of test number `test` */
3
+ export declare function applyRecorderEvent(source: string, test: number, event: RecorderEvent): string;
@@ -0,0 +1,10 @@
1
+ import { applyEdit } from "../steps/edit.js";
2
+ /** Writes a recorder event into the spec source, as an edit of test number `test` */
3
+ export function applyRecorderEvent(source, test, event) {
4
+ return applyEdit(source, {
5
+ op: event.op,
6
+ test,
7
+ index: event.index,
8
+ step: event.step,
9
+ });
10
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Function declaration installed as a preload script, called as `(send)` where `send` is the recorder's
3
+ * channel. Reports what the tester does in the top-level document:
4
+ *
5
+ * - `{ type: "click", target, candidates, textEntry }`: a click, retargeted to the nearest interactive
6
+ * element (a label click reports its control; the click the browser forwards to the control is dropped).
7
+ * - `{ type: "input", target, candidates, value, secret, secretName }`: text typed into a field. Password
8
+ * values never leave the page: `secret` is set and `value` is left out.
9
+ * - `{ type: "assert", target, candidates, text, value }`: an Alt+click, which does not reach the page.
10
+ * While Alt is held, the element under the pointer is outlined.
11
+ *
12
+ * `candidates` are locator calls for the element, most stable first (test id, role and name, label,
13
+ * text, css, and an absolute xpath that always exists). The recorder checks each one against the page.
14
+ * Runs in the page's own realm; a string, so no transpiler helper can end up in it.
15
+ */
16
+ export declare const CAPTURE_SOURCE: string;
17
+ /** Run in every loaded document to stop its capture script reporting and swallowing Alt+clicks */
18
+ export declare const CAPTURE_OFF = "() => { window[Symbol.for(\"samurai.recorder.off\")] = true; }";