@itmetsam/samurai-framework 0.10.0 → 0.12.1

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 (326) hide show
  1. package/README.md +3 -4
  2. package/dist/assert/assertion-error.d.ts +1 -1
  3. package/dist/assert/expect.d.ts +5 -50
  4. package/dist/assert/expect.js +5 -182
  5. package/dist/assert/locator-assertions.d.ts +22 -0
  6. package/dist/assert/locator-assertions.js +61 -0
  7. package/dist/assert/pending-assertions.d.ts +7 -0
  8. package/dist/assert/pending-assertions.js +40 -0
  9. package/dist/assert/text-match.d.ts +4 -0
  10. package/dist/assert/text-match.js +11 -0
  11. package/dist/assert/value-assertions.d.ts +23 -0
  12. package/dist/assert/value-assertions.js +82 -0
  13. package/dist/browser/browser-context.d.ts +0 -1
  14. package/dist/browser/browser-context.js +13 -5
  15. package/dist/browser/browser-finder.js +5 -9
  16. package/dist/browser/browser.d.ts +3 -31
  17. package/dist/browser/browser.js +36 -219
  18. package/dist/browser/chrome-launch.d.ts +2 -1
  19. package/dist/browser/dialog-handling.d.ts +32 -0
  20. package/dist/browser/dialog-handling.js +85 -0
  21. package/dist/browser/document-watcher.d.ts +44 -0
  22. package/dist/browser/document-watcher.js +160 -0
  23. package/dist/browser/download-folders.d.ts +0 -1
  24. package/dist/browser/download-folders.js +7 -15
  25. package/dist/browser/download.js +5 -10
  26. package/dist/browser/downloads-dir.js +2 -2
  27. package/dist/browser/emit.d.ts +7 -0
  28. package/dist/browser/emit.js +16 -0
  29. package/dist/browser/emulation.d.ts +1 -1
  30. package/dist/browser/emulation.js +54 -18
  31. package/dist/browser/file-chooser.js +4 -11
  32. package/dist/browser/history.d.ts +6 -0
  33. package/dist/browser/history.js +89 -0
  34. package/dist/browser/index.d.ts +2 -0
  35. package/dist/browser/index.js +2 -0
  36. package/dist/browser/init-script.d.ts +2 -0
  37. package/dist/browser/init-script.js +16 -0
  38. package/dist/browser/launch-firefox.d.ts +20 -0
  39. package/dist/browser/launch-firefox.js +66 -0
  40. package/dist/browser/launch-flags.d.ts +18 -0
  41. package/dist/browser/launch-flags.js +100 -0
  42. package/dist/browser/lazy.d.ts +21 -0
  43. package/dist/browser/lazy.js +44 -0
  44. package/dist/browser/page-events.d.ts +44 -0
  45. package/dist/browser/page-events.js +58 -0
  46. package/dist/browser/page-logs.js +2 -9
  47. package/dist/browser/page-navigation.d.ts +58 -0
  48. package/dist/browser/page-navigation.js +108 -0
  49. package/dist/browser/page-network.d.ts +69 -0
  50. package/dist/browser/page-network.js +163 -0
  51. package/dist/browser/page-settings.d.ts +35 -0
  52. package/dist/browser/page-settings.js +51 -0
  53. package/dist/browser/page-transfers.d.ts +36 -0
  54. package/dist/browser/page-transfers.js +50 -0
  55. package/dist/browser/page.d.ts +39 -154
  56. package/dist/browser/page.js +134 -758
  57. package/dist/browser/pdf.d.ts +1 -1
  58. package/dist/browser/session.d.ts +16 -0
  59. package/dist/browser/session.js +32 -0
  60. package/dist/browser/viewport.d.ts +19 -0
  61. package/dist/browser/viewport.js +72 -0
  62. package/dist/browser/warm-up.d.ts +1 -1
  63. package/dist/browser/warm-up.js +2 -1
  64. package/dist/browsers/index.d.ts +1 -1
  65. package/dist/browsers/index.js +1 -1
  66. package/dist/browsers/pins.d.ts +0 -2
  67. package/dist/browsers/pins.js +0 -2
  68. package/dist/browsers/store.d.ts +0 -2
  69. package/dist/browsers/store.js +1 -2
  70. package/dist/cli/args.d.ts +13 -6
  71. package/dist/cli/args.js +13 -10
  72. package/dist/cli/browsers.d.ts +1 -2
  73. package/dist/cli/browsers.js +2 -2
  74. package/dist/cli/console-reporter.js +3 -2
  75. package/dist/cli/flow-reporter.js +2 -1
  76. package/dist/cli/flow.d.ts +1 -2
  77. package/dist/cli/flow.js +25 -22
  78. package/dist/cli/init.js +2 -7
  79. package/dist/cli/io.d.ts +10 -0
  80. package/dist/cli/io.js +1 -0
  81. package/dist/cli/main.d.ts +3 -10
  82. package/dist/cli/main.js +10 -143
  83. package/dist/cli/paint.d.ts +2 -0
  84. package/dist/cli/paint.js +4 -0
  85. package/dist/cli/record.d.ts +4 -0
  86. package/dist/cli/record.js +93 -0
  87. package/dist/cli/usage.d.ts +1 -0
  88. package/dist/cli/usage.js +58 -0
  89. package/dist/cli/version.d.ts +2 -0
  90. package/dist/cli/version.js +7 -0
  91. package/dist/config/browsers.d.ts +11 -0
  92. package/dist/config/browsers.js +32 -0
  93. package/dist/config/config.js +4 -1
  94. package/dist/config/mask.d.ts +1 -1
  95. package/dist/config/mask.js +7 -2
  96. package/dist/config/run-settings.d.ts +1 -2
  97. package/dist/config/run-settings.js +1 -6
  98. package/dist/config/timeouts.d.ts +2 -0
  99. package/dist/config/timeouts.js +13 -0
  100. package/dist/config/workers.d.ts +14 -0
  101. package/dist/config/workers.js +29 -0
  102. package/dist/flows/run/branch-context.d.ts +14 -0
  103. package/dist/flows/run/branch-context.js +35 -0
  104. package/dist/flows/run/handlers/condition.d.ts +1 -1
  105. package/dist/flows/run/handlers/end.d.ts +1 -1
  106. package/dist/flows/run/handlers/group.d.ts +1 -1
  107. package/dist/flows/run/handlers/index.d.ts +2 -2
  108. package/dist/flows/run/handlers/parallel.d.ts +1 -1
  109. package/dist/flows/run/handlers/start.d.ts +1 -1
  110. package/dist/flows/run/handlers/support.d.ts +3 -27
  111. package/dist/flows/run/handlers/test.d.ts +1 -1
  112. package/dist/flows/run/handlers/types.d.ts +27 -0
  113. package/dist/flows/run/handlers/types.js +1 -0
  114. package/dist/flows/run/index.d.ts +1 -1
  115. package/dist/flows/run/prepare-flow.d.ts +14 -0
  116. package/dist/flows/run/prepare-flow.js +44 -0
  117. package/dist/flows/run/project.d.ts +33 -2
  118. package/dist/flows/run/project.js +139 -57
  119. package/dist/flows/run/run.d.ts +3 -4
  120. package/dist/flows/run/run.js +23 -193
  121. package/dist/flows/run/walker.d.ts +56 -0
  122. package/dist/flows/run/walker.js +225 -0
  123. package/dist/flows/steps/catalog.d.ts +1 -1
  124. package/dist/flows/steps/handler.d.ts +1 -1
  125. package/dist/flows/steps/set.d.ts +1 -1
  126. package/dist/index.js +1 -1
  127. package/dist/locator/actionability.d.ts +33 -0
  128. package/dist/locator/actionability.js +104 -0
  129. package/dist/locator/chain-resolver.d.ts +20 -0
  130. package/dist/locator/chain-resolver.js +102 -0
  131. package/dist/locator/document-swap.d.ts +1 -0
  132. package/dist/locator/document-swap.js +11 -0
  133. package/dist/locator/element-state.d.ts +3 -1
  134. package/dist/locator/element-state.js +11 -3
  135. package/dist/locator/file-actions.d.ts +35 -0
  136. package/dist/locator/file-actions.js +55 -0
  137. package/dist/{script/helpers.js → locator/helper-realm.js} +2 -2
  138. package/dist/locator/input-actions.d.ts +18 -0
  139. package/dist/locator/input-actions.js +80 -0
  140. package/dist/locator/label-locator.d.ts +1 -1
  141. package/dist/locator/label-locator.js +5 -12
  142. package/dist/locator/locator.d.ts +14 -37
  143. package/dist/locator/locator.js +68 -424
  144. package/dist/locator/page-scripts.d.ts +16 -0
  145. package/dist/locator/page-scripts.js +54 -0
  146. package/dist/locator/page-search.d.ts +10 -0
  147. package/dist/locator/page-search.js +19 -0
  148. package/dist/locator/selector-errors.d.ts +0 -4
  149. package/dist/locator/selector-errors.js +0 -7
  150. package/dist/locator/selector.d.ts +14 -13
  151. package/dist/locator/selector.js +10 -16
  152. package/dist/locator/text-locator.d.ts +1 -1
  153. package/dist/locator/text-locator.js +5 -9
  154. package/dist/logger/index.d.ts +6 -0
  155. package/dist/logger/index.js +14 -7
  156. package/dist/network/bidi-wire.d.ts +10 -0
  157. package/dist/network/bidi-wire.js +38 -0
  158. package/dist/network/intercept.d.ts +30 -0
  159. package/dist/network/intercept.js +130 -0
  160. package/dist/network/network-tracker.d.ts +10 -47
  161. package/dist/network/network-tracker.js +34 -68
  162. package/dist/network/network-types.d.ts +33 -0
  163. package/dist/network/network-types.js +1 -0
  164. package/dist/network/network-wait.d.ts +7 -2
  165. package/dist/network/network-wait.js +14 -4
  166. package/dist/network/response.d.ts +1 -1
  167. package/dist/network/response.js +1 -1
  168. package/dist/network/route.d.ts +92 -0
  169. package/dist/network/route.js +142 -0
  170. package/dist/network/router.d.ts +6 -91
  171. package/dist/network/router.js +25 -258
  172. package/dist/recorder/expectation.d.ts +5 -0
  173. package/dist/recorder/expectation.js +20 -0
  174. package/dist/recorder/files-recorder.d.ts +30 -0
  175. package/dist/recorder/files-recorder.js +88 -0
  176. package/dist/recorder/normalise.d.ts +17 -6
  177. package/dist/recorder/normalise.js +96 -91
  178. package/dist/recorder/options.d.ts +56 -0
  179. package/dist/recorder/options.js +1 -0
  180. package/dist/recorder/page-messages.d.ts +35 -0
  181. package/dist/recorder/page-messages.js +26 -0
  182. package/dist/recorder/ranking.js +1 -0
  183. package/dist/recorder/realms.d.ts +6 -0
  184. package/dist/recorder/realms.js +13 -0
  185. package/dist/recorder/recorder.d.ts +10 -74
  186. package/dist/recorder/recorder.js +39 -199
  187. package/dist/recorder/session-browser.d.ts +22 -0
  188. package/dist/recorder/session-browser.js +21 -0
  189. package/dist/recorder/session-secrets.d.ts +8 -0
  190. package/dist/recorder/session-secrets.js +45 -0
  191. package/dist/recorder/session.d.ts +5 -14
  192. package/dist/recorder/session.js +22 -120
  193. package/dist/recorder/spec-source.d.ts +39 -0
  194. package/dist/recorder/spec-source.js +84 -0
  195. package/dist/recorder/toolbar/action-guard.d.ts +21 -0
  196. package/dist/recorder/toolbar/action-guard.js +43 -0
  197. package/dist/recorder/toolbar/actions/assert.d.ts +4 -0
  198. package/dist/recorder/toolbar/actions/assert.js +52 -0
  199. package/dist/recorder/toolbar/actions/insert.d.ts +6 -0
  200. package/dist/recorder/toolbar/actions/insert.js +93 -0
  201. package/dist/recorder/toolbar/actions/pick.d.ts +8 -0
  202. package/dist/recorder/toolbar/actions/pick.js +26 -0
  203. package/dist/recorder/toolbar/actions/reply.d.ts +7 -0
  204. package/dist/recorder/toolbar/actions/reply.js +10 -0
  205. package/dist/recorder/toolbar/controller-fixture.d.ts +47 -0
  206. package/dist/recorder/toolbar/controller-fixture.js +129 -0
  207. package/dist/recorder/toolbar/controller.d.ts +12 -78
  208. package/dist/recorder/toolbar/controller.js +35 -247
  209. package/dist/recorder/toolbar/inspect.d.ts +1 -3
  210. package/dist/recorder/toolbar/inspect.js +2 -2
  211. package/dist/recorder/toolbar/page-scope.d.ts +19 -0
  212. package/dist/recorder/toolbar/page-scope.js +43 -0
  213. package/dist/recorder/toolbar/protocol.d.ts +2 -1
  214. package/dist/recorder/toolbar/source/attach.d.ts +2 -0
  215. package/dist/recorder/toolbar/source/attach.js +41 -0
  216. package/dist/recorder/toolbar/source/dom.d.ts +2 -0
  217. package/dist/recorder/toolbar/source/dom.js +47 -0
  218. package/dist/recorder/toolbar/source/drag.d.ts +2 -0
  219. package/dist/recorder/toolbar/source/drag.js +32 -0
  220. package/dist/recorder/toolbar/source/events-page.d.ts +2 -0
  221. package/dist/recorder/toolbar/source/events-page.js +67 -0
  222. package/dist/recorder/toolbar/source/events-toolbar.d.ts +2 -0
  223. package/dist/recorder/toolbar/source/events-toolbar.js +35 -0
  224. package/dist/recorder/toolbar/source/fields.d.ts +2 -0
  225. package/dist/recorder/toolbar/source/fields.js +44 -0
  226. package/dist/recorder/toolbar/source/handlers.d.ts +2 -0
  227. package/dist/recorder/toolbar/source/handlers.js +134 -0
  228. package/dist/recorder/toolbar/source/names.d.ts +2 -0
  229. package/dist/recorder/toolbar/source/names.js +12 -0
  230. package/dist/recorder/toolbar/source/picking.d.ts +2 -0
  231. package/dist/recorder/toolbar/source/picking.js +72 -0
  232. package/dist/recorder/toolbar/source/placement.d.ts +2 -0
  233. package/dist/recorder/toolbar/source/placement.js +45 -0
  234. package/dist/recorder/toolbar/source/recorder-side.d.ts +2 -0
  235. package/dist/recorder/toolbar/source/recorder-side.js +54 -0
  236. package/dist/recorder/toolbar/source/setup.d.ts +2 -0
  237. package/dist/recorder/toolbar/source/setup.js +50 -0
  238. package/dist/recorder/toolbar/source/views-panels.d.ts +2 -0
  239. package/dist/recorder/toolbar/source/views-panels.js +150 -0
  240. package/dist/recorder/toolbar/source/views-rail.d.ts +2 -0
  241. package/dist/recorder/toolbar/source/views-rail.js +40 -0
  242. package/dist/recorder/toolbar/source/views-render.d.ts +2 -0
  243. package/dist/recorder/toolbar/source/views-render.js +48 -0
  244. package/dist/recorder/toolbar/source.js +30 -856
  245. package/dist/recorder/toolbar/types.d.ts +70 -0
  246. package/dist/recorder/toolbar/types.js +1 -0
  247. package/dist/recorder/toolbar-bridge.d.ts +43 -0
  248. package/dist/recorder/toolbar-bridge.js +106 -0
  249. package/dist/runner/browsers.d.ts +0 -12
  250. package/dist/runner/browsers.js +1 -33
  251. package/dist/runner/options.d.ts +31 -0
  252. package/dist/runner/options.js +1 -0
  253. package/dist/runner/pooled-run.d.ts +43 -0
  254. package/dist/runner/pooled-run.js +75 -0
  255. package/dist/runner/ports.d.ts +2 -0
  256. package/dist/runner/ports.js +2 -0
  257. package/dist/runner/registry.d.ts +1 -1
  258. package/dist/runner/registry.js +1 -1
  259. package/dist/runner/reporter.d.ts +31 -10
  260. package/dist/runner/reporter.js +39 -6
  261. package/dist/runner/run-test.d.ts +16 -0
  262. package/dist/runner/run-test.js +178 -0
  263. package/dist/runner/run.d.ts +5 -3
  264. package/dist/runner/run.js +28 -29
  265. package/dist/runner/select.d.ts +24 -0
  266. package/dist/runner/select.js +77 -0
  267. package/dist/runner/test-dir.d.ts +13 -0
  268. package/dist/runner/test-dir.js +27 -0
  269. package/dist/runner/test-runner.d.ts +25 -56
  270. package/dist/runner/test-runner.js +81 -270
  271. package/dist/runner/worker-pool.d.ts +67 -0
  272. package/dist/runner/worker-pool.js +275 -0
  273. package/dist/runner/worker-process.d.ts +6 -0
  274. package/dist/runner/worker-process.js +54 -0
  275. package/dist/runner/worker-protocol.d.ts +67 -0
  276. package/dist/runner/worker-protocol.js +1 -0
  277. package/dist/runner/worker-session.d.ts +7 -0
  278. package/dist/runner/worker-session.js +60 -0
  279. package/dist/runner/worker.d.ts +26 -0
  280. package/dist/runner/worker.js +121 -0
  281. package/dist/runner/workers-option.d.ts +15 -0
  282. package/dist/runner/workers-option.js +22 -0
  283. package/dist/script/call-function.d.ts +1 -9
  284. package/dist/script/call-function.js +2 -19
  285. package/dist/script/element-handle.d.ts +1 -2
  286. package/dist/script/element-handle.js +1 -2
  287. package/dist/script/serialize.d.ts +3 -12
  288. package/dist/script/serialize.js +53 -62
  289. package/dist/steps/parse/call.d.ts +3 -0
  290. package/dist/steps/parse/call.js +81 -0
  291. package/dist/steps/parse/expectation.d.ts +5 -0
  292. package/dist/steps/parse/expectation.js +71 -0
  293. package/dist/steps/parse/file-entry.d.ts +4 -0
  294. package/dist/steps/parse/file-entry.js +74 -0
  295. package/dist/steps/parse/literals.d.ts +4 -0
  296. package/dist/steps/parse/literals.js +28 -0
  297. package/dist/steps/parse/step.d.ts +4 -0
  298. package/dist/steps/parse/step.js +81 -0
  299. package/dist/steps/parse.js +2 -323
  300. package/dist/transport/bidi-connection.d.ts +4 -15
  301. package/dist/transport/bidi-connection.js +9 -63
  302. package/dist/transport/log-redaction.d.ts +13 -0
  303. package/dist/transport/log-redaction.js +51 -0
  304. package/dist/transport/subscriptions.js +18 -13
  305. package/dist/transport/unsupported.d.ts +7 -0
  306. package/dist/transport/unsupported.js +13 -0
  307. package/dist/types/bidi-modules/browser.d.ts +1 -12
  308. package/dist/types/bidi-modules/browsing-context.d.ts +8 -18
  309. package/dist/types/bidi-modules/emulation.d.ts +29 -21
  310. package/dist/types/bidi-modules/input.d.ts +1 -9
  311. package/dist/types/bidi-modules/network.d.ts +29 -24
  312. package/dist/types/bidi-modules/script.d.ts +0 -9
  313. package/dist/types/bidi-modules/session.d.ts +0 -8
  314. package/dist/types/bidi-modules/storage.d.ts +0 -5
  315. package/dist/types/bidi.d.ts +7 -37
  316. package/dist/types/config.d.ts +12 -0
  317. package/dist/types/test.d.ts +8 -3
  318. package/dist/wait/wait-until.d.ts +1 -5
  319. package/dist/wait/wait-until.js +7 -16
  320. package/package.json +3 -2
  321. package/dist/config/default.d.ts +0 -2
  322. package/dist/config/default.js +0 -3
  323. package/dist/types/bidi-modules/web-extensions.d.ts +0 -9
  324. package/dist/types/bidi-protocols/command.d.ts +0 -7
  325. package/dist/types/bidi-protocols/event.d.ts +0 -17
  326. /package/dist/{script/helpers.d.ts → locator/helper-realm.d.ts} +0 -0
@@ -1,82 +1,9 @@
1
1
  import type { ContextTree } from "../browser/context-tree.js";
2
2
  import type { BiDiConnector } from "../transport/bidi-connection.js";
3
- import { type NetworkRequest } from "./network-tracker.js";
3
+ import { type RouteHandler } from "./route.js";
4
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
- * Whether a replaced body needs a `content-length` of its own. Firefox keeps the old one and cuts the new body
40
- * off; Chrome sets the right one itself and hangs the request when it is given one.
41
- */
42
- sendsContentLength: boolean;
43
- }
44
- /**
45
- * A request the browser holds until it is answered. Answer it exactly once with `fulfill`, `continue`,
46
- * `abort` or `fallback`; a second call throws.
47
- */
48
- export declare class Route {
49
- private readonly info;
50
- /** @internal */
51
- readonly entry: RouteEntry;
52
- private readonly deps;
53
- private answered;
54
- /** @internal */
55
- timer: ReturnType<typeof setTimeout> | undefined;
56
- constructor(info: NetworkRequest,
57
- /** @internal */
58
- entry: RouteEntry, deps: RouteDeps);
59
- request(): NetworkRequest;
60
- /** @internal */
61
- get isAnswered(): boolean;
62
- /** Answers the request without sending it: status 200 and no body unless told otherwise */
63
- fulfill(options?: FulfillOptions): Promise<void>;
64
- /**
65
- * Lets the request go on, optionally changed. `headers` replaces all request headers. A new `postData` brings its
66
- * own `content-length` (the request's old one would cut the new body off or leave the server waiting).
67
- */
68
- continue(options?: ContinueOptions): Promise<void>;
69
- /** Makes the request fail (the browser reports a network error to the page) */
70
- abort(): Promise<void>;
71
- /** Hands the request to the next route that matches it, or lets it continue when there is none */
72
- fallback(): Promise<void>;
73
- /** @internal Lets the request continue unchanged; failures are logged, not thrown */
74
- release(): Promise<void>;
75
- /** @internal Fails the request; failures are logged, not thrown */
76
- failQuietly(): Promise<void>;
77
- private claim;
78
- private answer;
79
- }
5
+ export type { RouteHandler } from "./route.js";
6
+ export { Route } from "./route.js";
80
7
  /**
81
8
  * Lets a page mock, block and change requests. One `network.addIntercept` serves all routes; matching
82
9
  * happens here. Every request the browser holds is answered exactly once, so nothing hangs.
@@ -88,11 +15,7 @@ export declare class Router {
88
15
  private routes;
89
16
  private pending;
90
17
  private failures;
91
- private queue;
92
- private interceptIds;
93
- private currentKey;
94
- private currentId;
95
- private subscription;
18
+ private intercept;
96
19
  private disposed;
97
20
  private routeTimeout;
98
21
  constructor(connector: BiDiConnector, tree: ContextTree, contextId: string, options?: {
@@ -108,18 +31,10 @@ export declare class Router {
108
31
  /** Lets held requests continue, removes the intercept and stops listening. Idempotent */
109
32
  dispose(): Promise<void>;
110
33
  private remove;
111
- private enqueue;
112
- /** Makes the browser-side intercept match the routes: none, one for everything, or one for exact URLs */
113
- private sync;
114
- /** The exact URLs to block, or undefined to block everything and match here */
115
- private urlPatterns;
116
- private addIntercept;
117
- private removeIntercept;
118
- private listen;
119
- private stop;
34
+ /** Makes the browser-side intercept match the routes */
35
+ private syncIntercept;
120
36
  private onBeforeRequestSent;
121
37
  /** Runs the `index`-th matching route; past the last one the request continues unchanged */
122
38
  private dispatch;
123
39
  private record;
124
40
  }
125
- export {};
@@ -1,138 +1,13 @@
1
- import { STATUS_CODES } from "node:http";
2
1
  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";
2
+ import { toRequest } from "./bidi-wire.js";
3
+ import { Intercept } from "./intercept.js";
4
+ import { PendingRoute, } from "./route.js";
5
+ import { matchUrl, samePattern } from "./url-match.js";
6
+ export { Route } from "./route.js";
6
7
  /** Time (ms) a handler may take to answer a request before the router lets it continue */
7
8
  const DEFAULT_ROUTE_TIMEOUT = 30000;
8
9
  /** How many handler errors the router keeps */
9
10
  const MAX_ERRORS = 100;
10
- function toHeaders(headers) {
11
- return Object.entries(headers).map(([name, value]) => ({
12
- name,
13
- value: { type: "string", value },
14
- }));
15
- }
16
- function toBytes(data) {
17
- return typeof data === "string"
18
- ? { type: "string", value: data }
19
- : { type: "base64", value: data.toString("base64") };
20
- }
21
- const hasHeader = (headers, name) => Object.keys(headers).some((key) => key.toLowerCase() === name);
22
- /**
23
- * A request the browser holds until it is answered. Answer it exactly once with `fulfill`, `continue`,
24
- * `abort` or `fallback`; a second call throws.
25
- */
26
- export class Route {
27
- constructor(info,
28
- /** @internal */
29
- entry, deps) {
30
- this.info = info;
31
- this.entry = entry;
32
- this.deps = deps;
33
- this.answered = false;
34
- }
35
- request() {
36
- return this.info;
37
- }
38
- /** @internal */
39
- get isAnswered() {
40
- return this.answered;
41
- }
42
- /** Answers the request without sending it: status 200 and no body unless told otherwise */
43
- async fulfill(options = {}) {
44
- if (options.json !== undefined && options.body !== undefined) {
45
- throw new TypeError("route.fulfill(): use either json or body, not both");
46
- }
47
- const text = options.json !== undefined ? JSON.stringify(options.json) : undefined;
48
- const body = options.body ?? text;
49
- const headers = { ...options.headers };
50
- const contentType = options.contentType ??
51
- (text !== undefined ? "application/json" : undefined);
52
- if (contentType && !hasHeader(headers, "content-type"))
53
- headers["content-type"] = contentType;
54
- if (!hasHeader(headers, "content-length")) {
55
- headers["content-length"] = String(body === undefined ? 0 : Buffer.byteLength(body));
56
- }
57
- const status = options.status ?? 200;
58
- await this.answer("network.provideResponse", {
59
- request: this.info.id,
60
- statusCode: status,
61
- reasonPhrase: STATUS_CODES[status] ?? "",
62
- headers: toHeaders(headers),
63
- ...(body !== undefined && { body: toBytes(body) }),
64
- });
65
- }
66
- /**
67
- * Lets the request go on, optionally changed. `headers` replaces all request headers. A new `postData` brings its
68
- * own `content-length` (the request's old one would cut the new body off or leave the server waiting).
69
- */
70
- async continue(options = {}) {
71
- let headers = options.headers;
72
- if (options.postData !== undefined) {
73
- const withoutLength = Object.fromEntries(Object.entries(headers ?? this.info.headers).filter(([name]) => name.toLowerCase() !== "content-length"));
74
- headers = this.deps.sendsContentLength
75
- ? {
76
- ...withoutLength,
77
- "content-length": String(Buffer.byteLength(options.postData)),
78
- }
79
- : withoutLength;
80
- }
81
- await this.answer("network.continueRequest", {
82
- request: this.info.id,
83
- ...(options.url !== undefined && { url: options.url }),
84
- ...(options.method !== undefined && { method: options.method }),
85
- ...(headers !== undefined && { headers: toHeaders(headers) }),
86
- ...(options.postData !== undefined && {
87
- body: toBytes(options.postData),
88
- }),
89
- });
90
- }
91
- /** Makes the request fail (the browser reports a network error to the page) */
92
- async abort() {
93
- await this.answer("network.failRequest", { request: this.info.id });
94
- }
95
- /** Hands the request to the next route that matches it, or lets it continue when there is none */
96
- async fallback() {
97
- this.claim();
98
- await this.deps.next();
99
- }
100
- /** @internal Lets the request continue unchanged; failures are logged, not thrown */
101
- async release() {
102
- if (this.answered)
103
- return;
104
- try {
105
- await this.continue();
106
- }
107
- catch (err) {
108
- logger.debug("Could not let %s continue", this.info.url, { err });
109
- }
110
- }
111
- /** @internal Fails the request; failures are logged, not thrown */
112
- async failQuietly() {
113
- if (this.answered)
114
- return;
115
- try {
116
- await this.abort();
117
- }
118
- catch (err) {
119
- logger.debug("Could not fail %s", this.info.url, { err });
120
- }
121
- }
122
- claim() {
123
- if (this.answered)
124
- throw new Error("route already handled");
125
- this.answered = true;
126
- clearTimeout(this.timer);
127
- this.deps.settled(this);
128
- }
129
- async answer(method, params) {
130
- this.claim();
131
- await this.deps.send(method, params);
132
- }
133
- }
134
- /** Whether a browser accepts `urlPatterns` in `network.addIntercept`; asked once per connection */
135
- const patternsUnsupported = new WeakSet();
136
11
  /**
137
12
  * Lets a page mock, block and change requests. One `network.addIntercept` serves all routes; matching
138
13
  * happens here. Every request the browser holds is answered exactly once, so nothing hangs.
@@ -145,8 +20,6 @@ export class Router {
145
20
  this.routes = [];
146
21
  this.pending = new Set();
147
22
  this.failures = [];
148
- this.queue = Promise.resolve();
149
- this.interceptIds = new Set();
150
23
  this.disposed = false;
151
24
  this.onBeforeRequestSent = (params) => {
152
25
  if (!params.isBlocked || params.context === null)
@@ -154,7 +27,7 @@ export class Router {
154
27
  if (!this.tree.isWithin(params.context, this.contextId))
155
28
  return;
156
29
  // Held by an intercept of ours, not someone else's
157
- if (!(params.intercepts ?? []).some((id) => this.interceptIds.has(id)))
30
+ if (!this.intercept.holds(params.intercepts))
158
31
  return;
159
32
  const info = toRequest(params.request, params.navigation, params.isBlocked, null);
160
33
  const matching = this.routes
@@ -163,6 +36,7 @@ export class Router {
163
36
  this.dispatch(info, matching, 0).catch((err) => logger.error("Handling the request to %s failed", info.url, { err }));
164
37
  };
165
38
  this.routeTimeout = options.routeTimeout ?? DEFAULT_ROUTE_TIMEOUT;
39
+ this.intercept = new Intercept(connector, contextId, this.onBeforeRequestSent);
166
40
  }
167
41
  /** Errors thrown by route handlers, oldest first */
168
42
  get errors() {
@@ -175,7 +49,7 @@ export class Router {
175
49
  const entry = { pattern, handler };
176
50
  this.routes.push(entry);
177
51
  try {
178
- await this.enqueue(() => this.sync());
52
+ await this.syncIntercept();
179
53
  }
180
54
  catch (err) {
181
55
  this.routes = this.routes.filter((route) => route !== entry);
@@ -204,146 +78,47 @@ export class Router {
204
78
  this.routes = this.routes.filter((route) => !removed.includes(route));
205
79
  // Requests a removed route still holds would wait for nobody
206
80
  await Promise.all([...this.pending]
207
- .filter((route) => removed.includes(route.entry))
81
+ .filter((route) => route.entry && removed.includes(route.entry))
208
82
  .map((route) => route.release()));
209
- await this.enqueue(() => this.sync());
83
+ await this.syncIntercept();
210
84
  }
211
- enqueue(work) {
212
- const run = this.queue.then(work);
213
- this.queue = run.catch(() => undefined);
214
- return run;
215
- }
216
- /** Makes the browser-side intercept match the routes: none, one for everything, or one for exact URLs */
217
- async sync() {
218
- if (this.routes.length === 0)
219
- return this.stop();
220
- await this.listen();
221
- const patterns = this.urlPatterns();
222
- const key = patterns ? JSON.stringify(patterns) : "all";
223
- if (key === this.currentKey)
224
- return;
225
- const id = await this.addIntercept(patterns);
226
- const previous = this.currentId;
227
- this.currentId = id;
228
- this.currentKey =
229
- patterns && !patternsUnsupported.has(this.connector) ? key : "all";
230
- this.interceptIds.add(id);
231
- // The new intercept is in place before the old one goes: nothing slips through in between
232
- if (previous !== undefined)
233
- await this.removeIntercept(previous);
234
- }
235
- /** The exact URLs to block, or undefined to block everything and match here */
236
- urlPatterns() {
237
- if (patternsUnsupported.has(this.connector))
238
- return undefined;
239
- const patterns = [];
240
- for (const { pattern } of this.routes) {
241
- // The browser reads a URL string in normal form; anything else could mean something else to it
242
- if (!isExactString(pattern) || !isNormalUrl(pattern))
243
- return undefined;
244
- if (!patterns.some((existing) => existing.type === "string" && existing.pattern === pattern)) {
245
- patterns.push({ type: "string", pattern });
246
- }
247
- }
248
- return patterns;
249
- }
250
- async addIntercept(patterns) {
251
- const params = {
252
- phases: ["beforeRequestSent"],
253
- contexts: [this.contextId],
254
- };
255
- try {
256
- const { intercept } = await this.connector.send("network.addIntercept", {
257
- ...params,
258
- ...(patterns && { urlPatterns: patterns }),
259
- });
260
- return intercept;
261
- }
262
- catch (err) {
263
- if (patterns &&
264
- err instanceof BiDiError &&
265
- err.code === "invalid argument") {
266
- patternsUnsupported.add(this.connector);
267
- return this.addIntercept(undefined);
268
- }
269
- throw err;
270
- }
271
- }
272
- async removeIntercept(id) {
273
- try {
274
- await this.connector.send("network.removeIntercept", { intercept: id });
275
- }
276
- catch (err) {
277
- if (!(err instanceof BiDiError && err.code === "no such intercept"))
278
- throw err;
279
- }
280
- finally {
281
- this.interceptIds.delete(id);
282
- }
283
- }
284
- async listen() {
285
- if (this.subscription)
286
- return;
287
- this.connector.onEvent("network.beforeRequestSent", this.onBeforeRequestSent);
288
- try {
289
- this.subscription = await this.connector.subscribe([
290
- "network.beforeRequestSent",
291
- ]);
292
- }
293
- catch (err) {
294
- this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
295
- throw err;
296
- }
297
- }
298
- async stop() {
299
- const id = this.currentId;
300
- this.currentId = undefined;
301
- this.currentKey = undefined;
302
- if (id !== undefined)
303
- await this.removeIntercept(id);
304
- if (this.subscription) {
305
- this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
306
- const subscription = this.subscription;
307
- this.subscription = undefined;
308
- await subscription.unsubscribe();
309
- }
85
+ /** Makes the browser-side intercept match the routes */
86
+ syncIntercept() {
87
+ return this.intercept.sync(this.routes.map((route) => route.pattern));
310
88
  }
311
89
  /** Runs the `index`-th matching route; past the last one the request continues unchanged */
312
90
  async dispatch(info, matching, index) {
313
91
  const entry = matching[index];
314
- const deps = {
92
+ const control = {
315
93
  send: (method, params) => this.connector.send(method, params),
316
94
  next: () => this.dispatch(info, matching, index + 1),
317
- settled: (settled) => {
95
+ onSettled: (settled) => {
318
96
  this.pending.delete(settled);
319
97
  },
320
98
  sendsContentLength: this.connector.browserName !== "chrome",
321
99
  };
322
100
  if (!entry) {
323
101
  // Nobody (left) to ask: an unmatched request, or every route fell back
324
- await new Route(info, { pattern: "", handler: () => { } }, deps).release();
102
+ await new PendingRoute(info, control).release();
325
103
  return;
326
104
  }
327
- const route = new Route(info, entry, deps);
328
- this.pending.add(route);
329
- route.timer = setTimeout(() => {
330
- if (route.isAnswered)
331
- return;
105
+ const pending = new PendingRoute(info, control, entry);
106
+ this.pending.add(pending);
107
+ pending.startTimer(this.routeTimeout, () => {
332
108
  logger.error("A route for %s did not answer within %dms; letting the request continue", info.url, this.routeTimeout);
333
- void route.release();
334
- }, this.routeTimeout);
335
- route.timer.unref?.();
109
+ void pending.release();
110
+ });
336
111
  try {
337
- await entry.handler(route);
112
+ await entry.handler(pending.route);
338
113
  }
339
114
  catch (err) {
340
115
  this.record(info, err);
341
- await route.failQuietly();
116
+ await pending.failQuietly();
342
117
  return;
343
118
  }
344
- if (!route.isAnswered) {
119
+ if (!pending.isAnswered) {
345
120
  logger.warn("The route for %s did not answer the request; letting it continue", info.url);
346
- await route.release();
121
+ await pending.release();
347
122
  }
348
123
  }
349
124
  record(info, err) {
@@ -355,11 +130,3 @@ export class Router {
355
130
  logger.error("A route handler threw for %s", info.url, { err });
356
131
  }
357
132
  }
358
- function isNormalUrl(value) {
359
- try {
360
- return new URL(value).href === value;
361
- }
362
- catch {
363
- return false;
364
- }
365
- }
@@ -0,0 +1,5 @@
1
+ import type { Expectation } from "../steps/model.js";
2
+ /** The name a secret gets in `secrets.<NAME>`: the field's name or id, upper snake case */
3
+ export declare function secretNameFor(hint: string | undefined): string;
4
+ /** The assertion an Alt+click or a pick starts with: the value of a field, else the text (cut when long), else visible */
5
+ export declare function expectationFor(text: string, value: string | undefined): Expectation;
@@ -0,0 +1,20 @@
1
+ const TEXT_LIMIT = 100;
2
+ /** The name a secret gets in `secrets.<NAME>`: the field's name or id, upper snake case */
3
+ export function secretNameFor(hint) {
4
+ const name = (hint ?? "")
5
+ .replace(/([a-z\d])([A-Z])/g, "$1_$2")
6
+ .replace(/[^A-Za-z0-9]+/g, "_")
7
+ .replace(/^_+|_+$/g, "")
8
+ .toUpperCase();
9
+ return name === "" || /^\d/.test(name) ? "PASSWORD" : name;
10
+ }
11
+ /** The assertion an Alt+click or a pick starts with: the value of a field, else the text (cut when long), else visible */
12
+ export function expectationFor(text, value) {
13
+ if (value !== undefined)
14
+ return { matcher: "toHaveValue", expected: value };
15
+ if (text === "")
16
+ return { matcher: "toBeVisible" };
17
+ return text.length <= TEXT_LIMIT
18
+ ? { matcher: "toHaveText", expected: text }
19
+ : { matcher: "toContainText", expected: text.slice(0, TEXT_LIMIT).trim() };
20
+ }
@@ -0,0 +1,30 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ import type { FileRecording, MissingFile } from "./options.js";
3
+ /** The warning for a file that isn't saved, as the toolbar and the CLI show it */
4
+ export declare function missingFileText(file: MissingFile, folder: string): string;
5
+ /** What `FilesRecorder` tells the toolbar, when there is one */
6
+ export interface FilesNotices {
7
+ warn(text: string): void;
8
+ /** A file is being saved; `undefined` when done */
9
+ saving(name: string | undefined): void;
10
+ }
11
+ /** Saves the files a person picked or dropped into the project's files folder and keeps count of what was and was not saved */
12
+ export declare class FilesRecorder {
13
+ private readonly connector;
14
+ private readonly context;
15
+ private readonly recording;
16
+ private readonly notices;
17
+ private readonly stored;
18
+ private readonly missing;
19
+ constructor(connector: BiDiConnector, context: string, recording: FileRecording, notices: FilesNotices);
20
+ /** Files stored or reused in the files folder by this recording, project-relative */
21
+ get storedFiles(): string[];
22
+ /** Files whose steps point at a path that wasn't written */
23
+ get missingFiles(): MissingFile[];
24
+ /** Saves one kept file to the files folder; returns the path its step gets */
25
+ store(file: {
26
+ id: number;
27
+ name: string;
28
+ size: number;
29
+ }): Promise<string>;
30
+ }
@@ -0,0 +1,88 @@
1
+ import { mkdtemp, open, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import logger from "../logger/index.js";
5
+ import { cleanFileName, filesFolder, RECORD_LIMIT, storeFile, } from "../project/index.js";
6
+ import { humanSize } from "../steps/files.js";
7
+ import { readPageFile, releasePageFile } from "./files-reader.js";
8
+ /** The warning for a file that isn't saved, as the toolbar and the CLI show it */
9
+ export function missingFileText(file, folder) {
10
+ const where = folder === "" ? "the project folder" : `${folder}/`;
11
+ return file.reason === "too-big"
12
+ ? `${file.name} is ${humanSize(file.size)}. Files over 50 MB aren't saved: add it to ${where} yourself, or use a generated file.`
13
+ : `${file.name} couldn't be read, so it isn't saved: add it to ${where} yourself, or use a generated file.`;
14
+ }
15
+ /** Saves the files a person picked or dropped into the project's files folder and keeps count of what was and was not saved */
16
+ export class FilesRecorder {
17
+ constructor(connector, context, recording, notices) {
18
+ this.connector = connector;
19
+ this.context = context;
20
+ this.recording = recording;
21
+ this.notices = notices;
22
+ this.stored = new Set();
23
+ this.missing = [];
24
+ }
25
+ /** Files stored or reused in the files folder by this recording, project-relative */
26
+ get storedFiles() {
27
+ return [...this.stored];
28
+ }
29
+ /** Files whose steps point at a path that wasn't written */
30
+ get missingFiles() {
31
+ return [...this.missing];
32
+ }
33
+ /** Saves one kept file to the files folder; returns the path its step gets */
34
+ async store(file) {
35
+ const recording = this.recording;
36
+ const folder = filesFolder(recording.projectDir, recording.filesDir);
37
+ const notSaved = (reason) => {
38
+ const clean = cleanFileName(file.name);
39
+ const missing = {
40
+ path: folder.relative ? `${folder.relative}/${clean}` : clean,
41
+ name: file.name,
42
+ size: file.size,
43
+ reason,
44
+ };
45
+ this.missing.push(missing);
46
+ recording.onMissing?.(missing);
47
+ this.notices.warn(missingFileText(missing, folder.relative));
48
+ return missing.path;
49
+ };
50
+ if (file.size > RECORD_LIMIT) {
51
+ await releasePageFile(this.connector, this.context, file.id).catch(() => undefined);
52
+ return notSaved("too-big");
53
+ }
54
+ this.notices.saving(file.name);
55
+ let temp;
56
+ try {
57
+ temp = await mkdtemp(path.join(tmpdir(), "samurai-upload-"));
58
+ const content = path.join(temp, "content");
59
+ const out = await open(content, "w");
60
+ try {
61
+ await readPageFile(this.connector, this.context, file.id, file.size, (bytes) => out.write(bytes));
62
+ }
63
+ finally {
64
+ await out.close();
65
+ }
66
+ const stored = await storeFile({
67
+ projectDir: recording.projectDir,
68
+ ...(recording.filesDir !== undefined && {
69
+ filesDir: recording.filesDir,
70
+ }),
71
+ name: file.name,
72
+ content,
73
+ });
74
+ this.stored.add(stored.path);
75
+ return stored.path;
76
+ }
77
+ catch (err) {
78
+ logger.warn("Recorder could not save %s", file.name, { err });
79
+ return notSaved("unreadable");
80
+ }
81
+ finally {
82
+ this.notices.saving(undefined);
83
+ if (temp)
84
+ await rm(temp, { recursive: true, force: true }).catch(() => undefined);
85
+ await releasePageFile(this.connector, this.context, file.id).catch(() => undefined);
86
+ }
87
+ }
88
+ }
@@ -76,7 +76,7 @@ export type Observation = {
76
76
  type: "goto";
77
77
  url: string;
78
78
  };
79
- export interface NormaliserOptions {
79
+ interface NormaliserOptions {
80
80
  /** Index the first recorded step gets */
81
81
  at: number;
82
82
  /** Relative URLs are written against this */
@@ -84,11 +84,6 @@ export interface NormaliserOptions {
84
84
  /** A navigation within this many ms of the last action is a consequence of it. @default 2000 */
85
85
  navigationWindow?: number;
86
86
  }
87
- /** A picker that opens within this many ms of a click was opened by it */
88
- export declare const PICKER_WINDOW = 1000;
89
- /** The name a secret gets in `secrets.<NAME>`: the field's name or id, upper snake case */
90
- export declare function secretNameFor(hint: string | undefined): string;
91
- export declare function expectationFor(text: string, value: string | undefined): Expectation;
92
87
  /**
93
88
  * Turns what the tester did into steps:
94
89
  * - typing into a field becomes one `fill`, rewritten as the text grows (a password becomes a secret reference);
@@ -117,8 +112,24 @@ export declare class Normaliser {
117
112
  /** Steps the toolbar asks for directly; undefined for page observations */
118
113
  private toolbarStep;
119
114
  observe(observation: Observation, at: number): RecorderEvent[];
115
+ /**
116
+ * The sandbox reports a picker for any click on a file input, including ones a page script fires: it is only a
117
+ * hint, and only the very last step, recorded just before, may be replaced or removed (a page's own
118
+ * input.click() also counts as the opening click, bounded to the last step within PICKER_WINDOW)
119
+ */
120
+ private pickerOpened;
121
+ /** The picker closed without a choice: the click that opened it is taken out again */
122
+ private pickerCancelled;
123
+ private filesSet;
124
+ private clicked;
125
+ private pressed;
126
+ /** An Alt+click */
127
+ private asserted;
128
+ /** Typing: one `fill` per field, rewritten as the text grows */
129
+ private typed;
120
130
  /** A top-level navigation started */
121
131
  navigated(url: string, at: number): RecorderEvent[];
122
132
  /** The page the recording starts on, written as a `goto` */
123
133
  initialGoto(url: string): RecorderEvent[];
124
134
  }
135
+ export {};