@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,528 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, describe, it, mock } from "node:test";
3
- import { ContextTree } from "../browser/context-tree.js";
4
- import logger from "../logger/index.js";
5
- import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
6
- import { BiDiConnector } from "../transport/bidi-connection.js";
7
- import { Router, type Route } from "./router.js";
8
-
9
- const TREE = [
10
- { context: "top", parent: null, children: [{ context: "frame", parent: "top", children: [] }] },
11
- { context: "other", parent: null, children: [] },
12
- ];
13
-
14
- const stops: Array<() => void> = [];
15
- let warn: ReturnType<typeof mock.method>;
16
- let error: ReturnType<typeof mock.method>;
17
- beforeEach(() => {
18
- warn = mock.method(logger, "warn", () => logger);
19
- error = mock.method(logger, "error", () => logger);
20
- });
21
- afterEach(() => {
22
- mock.restoreAll();
23
- stops.splice(0).forEach((stop) => stop());
24
- });
25
-
26
- interface Options {
27
- routeTimeout?: number;
28
- handlers?: Record<string, (params: any) => object | Error>;
29
- }
30
-
31
- async function setup({ routeTimeout = 5000, handlers = {} }: Options = {}) {
32
- const ws = new FakeWebSocket();
33
- const connector = new BiDiConnector(ws as unknown as WebSocket);
34
- let intercepts = 0;
35
- stops.push(
36
- autoReply(ws, {
37
- "browsingContext.getTree": () => ({ contexts: TREE }),
38
- "network.addIntercept": () => ({ intercept: `i${++intercepts}` }),
39
- ...handlers,
40
- }),
41
- );
42
- const tree = await ContextTree.create(connector);
43
- const router = new Router(connector, tree, "top", { routeTimeout });
44
- const commands = (method: string) => ws.sent.filter((m) => m.method === method).map((m) => m.params as any);
45
- const blocked = (id: string, url: string, extra: { context?: string; intercepts?: string[]; isBlocked?: boolean; method?: string; headers?: Array<{ name: string; value: object }> } = {}) =>
46
- ws.emitEvent("network.beforeRequestSent", {
47
- context: extra.context ?? "top",
48
- isBlocked: extra.isBlocked ?? true,
49
- intercepts: extra.intercepts ?? ["i1"],
50
- navigation: null,
51
- redirectCount: 0,
52
- timestamp: 0,
53
- request: {
54
- request: id,
55
- url,
56
- method: extra.method ?? "GET",
57
- headers: extra.headers ?? [{ name: "Accept", value: { type: "string", value: "*/*" } }],
58
- cookies: [],
59
- headersSize: 0,
60
- bodySize: null,
61
- destination: "script",
62
- initiatorType: "fetch",
63
- timings: {},
64
- },
65
- });
66
- return { ws, router, commands, blocked };
67
- }
68
-
69
- const header = (name: string, value: string) => ({ name, value: { type: "string", value } });
70
-
71
- describe("registering routes", () => {
72
- it("sends one intercept, for this page, however many routes", async () => {
73
- const { router, commands } = await setup();
74
- await router.route("**/api/**", () => {});
75
- await router.route(/\.png$/, () => {});
76
- assert.deepEqual(commands("network.addIntercept"), [{ phases: ["beforeRequestSent"], contexts: ["top"] }]);
77
- });
78
-
79
- it("concurrent route() calls share one intercept", async () => {
80
- const { router, commands } = await setup();
81
- await Promise.all([router.route("**/a", () => {}), router.route("**/b", () => {}), router.route(/c/, () => {})]);
82
- assert.equal(commands("network.addIntercept").length, 1);
83
- });
84
-
85
- it("with only exact URLs the browser is asked to block just those", async () => {
86
- const { router, commands } = await setup();
87
- await router.route("https://example.test/api/users", () => {});
88
- await router.route("https://example.test/api/items?page=1", () => {});
89
- assert.deepEqual(commands("network.addIntercept").map((c) => c.urlPatterns), [
90
- [{ type: "string", pattern: "https://example.test/api/users" }],
91
- [
92
- { type: "string", pattern: "https://example.test/api/users" },
93
- { type: "string", pattern: "https://example.test/api/items?page=1" },
94
- ],
95
- ]);
96
- });
97
-
98
- it("adding a glob widens the intercept: the new one is added before the old one is removed", async () => {
99
- const { ws, router } = await setup();
100
- await router.route("https://example.test/api/users", () => {});
101
- await router.route("**/images/*", () => {});
102
- const order = ws.sent.filter((m) => m.method === "network.addIntercept" || m.method === "network.removeIntercept").map((m) => m.method);
103
- assert.deepEqual(order, ["network.addIntercept", "network.addIntercept", "network.removeIntercept"]);
104
- assert.equal((ws.sent.filter((m) => m.method === "network.addIntercept")[1]!.params as any).urlPatterns, undefined);
105
- assert.deepEqual(ws.sent.find((m) => m.method === "network.removeIntercept")!.params, { intercept: "i1" });
106
- });
107
-
108
- it("an exact string that is not in URL-normal form is not handed to the browser as a pattern", async () => {
109
- const { router, commands } = await setup();
110
- await router.route("https://example.test", () => {}); // the browser would read it as https://example.test/
111
- assert.equal(commands("network.addIntercept")[0].urlPatterns, undefined);
112
- });
113
-
114
- it("falls back to blocking everything when the browser rejects url patterns, and remembers", async () => {
115
- const rejected = Object.assign(new Error("urlPatterns not supported"), { code: "invalid argument" });
116
- let attempts = 0;
117
- const { router, commands } = await setup({
118
- handlers: {
119
- "network.addIntercept": (params) => {
120
- attempts++;
121
- return params.urlPatterns ? rejected : { intercept: `i${attempts}` };
122
- },
123
- },
124
- });
125
- await router.route("https://example.test/a", () => {});
126
- await router.unrouteAll();
127
- await router.route("https://example.test/b", () => {});
128
- const withPatterns = commands("network.addIntercept").filter((c) => c.urlPatterns);
129
- assert.equal(withPatterns.length, 1, "asked once, then remembered");
130
- });
131
- });
132
-
133
- describe("what a blocked request does", () => {
134
- it("goes to the matching handler, with the request described", async () => {
135
- const { router, blocked } = await setup();
136
- let seen!: Route;
137
- await router.route("**/api/**", (route) => {
138
- seen = route;
139
- return route.continue();
140
- });
141
- blocked("r1", "https://example.test/api/users", { method: "POST", headers: [header("Content-Type", "application/json")] });
142
- await tick(20);
143
- const request = seen.request();
144
- assert.equal(request.url, "https://example.test/api/users");
145
- assert.equal(request.method, "POST");
146
- assert.deepEqual(request.headers, { "content-type": "application/json" });
147
- assert.equal(request.id, "r1");
148
- });
149
-
150
- it("continues unchanged when no route matches", async () => {
151
- const { router, blocked, commands } = await setup();
152
- await router.route("**/api/**", () => {});
153
- blocked("r1", "https://example.test/other.css");
154
- await tick(20);
155
- assert.deepEqual(commands("network.continueRequest"), [{ request: "r1" }]);
156
- assert.equal(commands("network.failRequest").length + commands("network.provideResponse").length, 0);
157
- });
158
-
159
- it("the newest matching route answers first; fallback passes to the next", async () => {
160
- const { router, blocked, commands } = await setup();
161
- const order: string[] = [];
162
- await router.route("**/api/**", (route) => {
163
- order.push("old");
164
- return route.fulfill({ status: 201 });
165
- });
166
- await router.route("**/api/users", async (route) => {
167
- order.push("new");
168
- await route.fallback();
169
- });
170
- blocked("r1", "https://example.test/api/users");
171
- await tick(30);
172
- assert.deepEqual(order, ["new", "old"]);
173
- assert.equal(commands("network.provideResponse")[0].statusCode, 201);
174
- assert.equal(commands("network.continueRequest").length, 0, "answered once");
175
- });
176
-
177
- it("fallback on the last matching route continues the request unchanged", async () => {
178
- const { router, blocked, commands } = await setup();
179
- await router.route("**/api/**", (route) => route.fallback());
180
- blocked("r1", "https://example.test/api/users");
181
- await tick(30);
182
- assert.deepEqual(commands("network.continueRequest"), [{ request: "r1" }]);
183
- });
184
-
185
- it("ignores requests that are not blocked, of other pages, or blocked by someone else's intercept", async () => {
186
- const { router, blocked, ws } = await setup();
187
- let calls = 0;
188
- await router.route("**", () => {
189
- calls++;
190
- });
191
- const before = ws.sent.length;
192
- blocked("r1", "https://e.test/a", { isBlocked: false });
193
- blocked("r2", "https://e.test/b", { context: "other" });
194
- blocked("r3", "https://e.test/c", { intercepts: ["someone-elses"] });
195
- await tick(30);
196
- assert.equal(calls, 0);
197
- assert.equal(ws.sent.length, before);
198
- });
199
-
200
- it("handles requests of an iframe of the page", async () => {
201
- const { router, blocked, commands } = await setup();
202
- await router.route("**", (route) => route.abort());
203
- blocked("r1", "https://e.test/frame.js", { context: "frame" });
204
- await tick(20);
205
- assert.deepEqual(commands("network.failRequest"), [{ request: "r1" }]);
206
- });
207
- });
208
-
209
- describe("Route.fulfill", () => {
210
- async function fulfilled(options: Parameters<Route["fulfill"]>[0]) {
211
- const { router, blocked, commands } = await setup();
212
- await router.route("**", (route) => route.fulfill(options));
213
- blocked("r1", "https://example.test/x");
214
- await tick(20);
215
- return commands("network.provideResponse")[0];
216
- }
217
-
218
- it("answers 200 OK with no body by default", async () => {
219
- const sent = await fulfilled({});
220
- assert.deepEqual(sent, {
221
- request: "r1",
222
- statusCode: 200,
223
- reasonPhrase: "OK",
224
- headers: [header("content-length", "0")],
225
- });
226
- });
227
-
228
- it("json becomes the body, with its content type and length", async () => {
229
- const sent = await fulfilled({ json: [{ id: 1 }] });
230
- assert.deepEqual(sent.body, { type: "string", value: '[{"id":1}]' });
231
- assert.deepEqual(sent.headers, [header("content-type", "application/json"), header("content-length", "10")]);
232
- });
233
-
234
- it("a string body is sent as text, its length counted in bytes", async () => {
235
- const sent = await fulfilled({ status: 404, body: "héllo", contentType: "text/plain" });
236
- assert.equal(sent.statusCode, 404);
237
- assert.equal(sent.reasonPhrase, "Not Found");
238
- assert.deepEqual(sent.body, { type: "string", value: "héllo" });
239
- assert.deepEqual(sent.headers, [header("content-type", "text/plain"), header("content-length", "6")]);
240
- });
241
-
242
- it("a Buffer body is sent as base64", async () => {
243
- const sent = await fulfilled({ body: Buffer.from([0, 1, 2, 255]) });
244
- assert.deepEqual(sent.body, { type: "base64", value: Buffer.from([0, 1, 2, 255]).toString("base64") });
245
- assert.deepEqual(sent.headers.find((h: any) => h.name === "content-length"), header("content-length", "4"));
246
- });
247
-
248
- it("your headers win: content-type and content-length are only added when missing, whatever their case", async () => {
249
- const sent = await fulfilled({
250
- json: { a: 1 },
251
- headers: { "Content-Type": "application/vnd.api+json", "CONTENT-LENGTH": "99", "x-mock": "1" },
252
- });
253
- assert.deepEqual(sent.headers, [
254
- header("Content-Type", "application/vnd.api+json"),
255
- header("CONTENT-LENGTH", "99"),
256
- header("x-mock", "1"),
257
- ]);
258
- });
259
-
260
- it("json and body together are refused", async () => {
261
- const { router, blocked } = await setup();
262
- let failure: unknown;
263
- await router.route("**", async (route) => {
264
- failure = await route.fulfill({ json: 1, body: "x" }).catch((err: Error) => err);
265
- await route.abort();
266
- });
267
- blocked("r1", "https://example.test/x");
268
- await tick(30);
269
- assert.ok(failure instanceof TypeError);
270
- });
271
- });
272
-
273
- describe("Route.continue and Route.abort", () => {
274
- it("continue() sends only the request", async () => {
275
- const { router, blocked, commands } = await setup();
276
- await router.route("**", (route) => route.continue());
277
- blocked("r1", "https://example.test/x");
278
- await tick(20);
279
- assert.deepEqual(commands("network.continueRequest"), [{ request: "r1" }]);
280
- });
281
-
282
- it("continue() overrides url, method and headers", async () => {
283
- const { router, blocked, commands } = await setup();
284
- await router.route("**", (route) =>
285
- route.continue({
286
- url: "https://example.test/other",
287
- method: "PUT",
288
- headers: { ...route.request().headers, "x-test": "1" },
289
- }),
290
- );
291
- blocked("r1", "https://example.test/x");
292
- await tick(20);
293
- assert.deepEqual(commands("network.continueRequest"), [
294
- {
295
- request: "r1",
296
- url: "https://example.test/other",
297
- method: "PUT",
298
- headers: [header("accept", "*/*"), header("x-test", "1")],
299
- },
300
- ]);
301
- });
302
-
303
- it("a new body brings its own content-length: the browser would keep the old one and cut the body off", async () => {
304
- const { router, blocked, commands } = await setup();
305
- await router.route("**", (route) => route.continue({ postData: "héllo body" }));
306
- blocked("r1", "https://example.test/x", {
307
- method: "POST",
308
- headers: [header("Accept", "*/*"), header("Content-Length", "4")],
309
- });
310
- await tick(20);
311
- assert.deepEqual(commands("network.continueRequest"), [
312
- {
313
- request: "r1",
314
- headers: [header("accept", "*/*"), header("content-length", "11")],
315
- body: { type: "string", value: "héllo body" },
316
- },
317
- ]);
318
- });
319
-
320
- it("the content-length of a new body replaces one in the headers you pass, whatever its case", async () => {
321
- const { router, blocked, commands } = await setup();
322
- await router.route("**", (route) =>
323
- route.continue({ postData: "12345", headers: { ...route.request().headers, "Content-Length": "999", "x-a": "1" } }),
324
- );
325
- blocked("r1", "https://example.test/x", { method: "POST", headers: [header("Content-Length", "4")] });
326
- await tick(20);
327
- assert.deepEqual(commands("network.continueRequest")[0].headers, [header("x-a", "1"), header("content-length", "5")]);
328
- });
329
-
330
- it("continue() sends a Buffer as base64, counted in bytes", async () => {
331
- const { router, blocked, commands } = await setup();
332
- await router.route("**", (route) => route.continue({ postData: Buffer.from([1, 2, 3, 255]) }));
333
- blocked("r1", "https://example.test/x", { method: "POST" });
334
- await tick(20);
335
- const sent = commands("network.continueRequest")[0];
336
- assert.deepEqual(sent.body, { type: "base64", value: Buffer.from([1, 2, 3, 255]).toString("base64") });
337
- assert.deepEqual(sent.headers.find((h: any) => h.name === "content-length"), header("content-length", "4"));
338
- });
339
-
340
- it("abort() fails the request", async () => {
341
- const { router, blocked, commands } = await setup();
342
- await router.route(/\.png$/, (route) => route.abort());
343
- blocked("r1", "https://example.test/a.png");
344
- await tick(20);
345
- assert.deepEqual(commands("network.failRequest"), [{ request: "r1" }]);
346
- });
347
- });
348
-
349
- describe("every blocked request is resolved exactly once", () => {
350
- it("a second resolution throws", async () => {
351
- const { router, blocked, commands } = await setup();
352
- const outcomes: string[] = [];
353
- await router.route("**", async (route) => {
354
- await route.fulfill({});
355
- for (const second of [() => route.continue(), () => route.abort(), () => route.fulfill({}), () => route.fallback()]) {
356
- outcomes.push(await second().then(() => "ok", (err: Error) => err.message));
357
- }
358
- });
359
- blocked("r1", "https://example.test/x");
360
- await tick(30);
361
- assert.deepEqual(outcomes, Array(4).fill("route already handled"));
362
- assert.equal(commands("network.provideResponse").length, 1);
363
- });
364
-
365
- it("a handler that throws fails the request and the error is kept", async () => {
366
- const { router, blocked, commands } = await setup();
367
- await router.route("**", () => {
368
- throw new Error("handler bug");
369
- });
370
- blocked("r1", "https://example.test/x");
371
- await tick(30);
372
- assert.deepEqual(commands("network.failRequest"), [{ request: "r1" }]);
373
- assert.equal(router.errors.length, 1);
374
- assert.match(router.errors[0]!.message, /route handler for https:\/\/example\.test\/x threw: handler bug/);
375
- assert.ok(error.mock.callCount() >= 1);
376
- });
377
-
378
- it("a handler that throws after answering keeps the answer and reports the error", async () => {
379
- const { router, blocked, commands } = await setup();
380
- await router.route("**", async (route) => {
381
- await route.fulfill({});
382
- throw new Error("late bug");
383
- });
384
- blocked("r1", "https://example.test/x");
385
- await tick(30);
386
- assert.equal(commands("network.failRequest").length, 0);
387
- assert.equal(router.errors.length, 1);
388
- });
389
-
390
- it("a handler that returns without answering lets the request continue, with a warning", async () => {
391
- const { router, blocked, commands } = await setup();
392
- await router.route("**", () => {});
393
- blocked("r1", "https://example.test/x");
394
- await tick(30);
395
- assert.deepEqual(commands("network.continueRequest"), [{ request: "r1" }]);
396
- assert.equal(warn.mock.callCount(), 1);
397
- assert.match(String((warn.mock.calls[0]!.arguments as unknown[])[0]), /did not answer/);
398
- });
399
-
400
- it("an async handler is awaited: answering later is not overtaken by the default", async () => {
401
- const { router, blocked, commands } = await setup();
402
- await router.route("**", async (route) => {
403
- await tick(40);
404
- await route.fulfill({ status: 202 });
405
- });
406
- blocked("r1", "https://example.test/x");
407
- await tick(15);
408
- assert.equal(commands("network.continueRequest").length, 0, "still waiting for the handler");
409
- await tick(60);
410
- assert.equal(commands("network.provideResponse")[0].statusCode, 202);
411
- assert.equal(commands("network.continueRequest").length, 0);
412
- assert.equal(warn.mock.callCount(), 0);
413
- });
414
-
415
- it("a route pending longer than routeTimeout is continued, and the error is logged", async () => {
416
- const { router, blocked, commands } = await setup({ routeTimeout: 50 });
417
- await router.route("**", async () => {
418
- await tick(300); // never answers within the timeout
419
- });
420
- blocked("r1", "https://example.test/x");
421
- await tick(120);
422
- assert.deepEqual(commands("network.continueRequest"), [{ request: "r1" }]);
423
- assert.ok(error.mock.callCount() >= 1);
424
- const [template, , timeout] = error.mock.calls[0]!.arguments as unknown[];
425
- assert.match(String(template), /did not answer within %dms/);
426
- assert.equal(timeout, 50);
427
- await tick(300); // the handler finishing later must not send anything more
428
- assert.equal(commands("network.continueRequest").length, 1);
429
- });
430
-
431
- it("the browser refusing an answer reaches the handler as a BiDiError", async () => {
432
- const gone = Object.assign(new Error("request is gone"), { code: "no such request" });
433
- const { router, blocked } = await setup({ handlers: { "network.provideResponse": () => gone } });
434
- let caught: unknown;
435
- await router.route("**", async (route) => {
436
- caught = await route.fulfill({}).catch((err: Error) => err);
437
- });
438
- blocked("r1", "https://example.test/x");
439
- await tick(30);
440
- assert.equal((caught as Error).name, "BiDiError");
441
- assert.equal((caught as { code: string }).code, "no such request");
442
- });
443
- });
444
-
445
- describe("removing routes", () => {
446
- it("unroute removes the route; the last one takes the intercept and the listener with it", async () => {
447
- const { ws, router, commands, blocked } = await setup();
448
- await router.route("**/a", () => {});
449
- await router.unroute("**/a");
450
- assert.deepEqual(commands("network.removeIntercept"), [{ intercept: "i1" }]);
451
- await tick(10);
452
- assert.ok(ws.sent.some((m) => m.method === "session.unsubscribe"));
453
- const before = ws.sent.length;
454
- blocked("r1", "https://e.test/a");
455
- await tick(20);
456
- assert.equal(ws.sent.length, before);
457
- });
458
-
459
- it("unroute by pattern and handler removes only that pair", async () => {
460
- const { router, blocked, commands } = await setup();
461
- const first = () => {};
462
- const second = (route: Route) => route.abort();
463
- await router.route("**/a", first);
464
- await router.route("**/a", second);
465
- await router.unroute("**/a", second);
466
- blocked("r1", "https://e.test/a");
467
- await tick(30);
468
- assert.equal(commands("network.failRequest").length, 0);
469
- assert.equal(commands("network.removeIntercept").length, 0, "one route is left");
470
- });
471
-
472
- it("unroute without a handler removes every route for that pattern", async () => {
473
- const { router, blocked, commands } = await setup();
474
- await router.route(/a/, () => {});
475
- await router.route(/a/, () => {});
476
- await router.route("**/b", () => {});
477
- await router.unroute(/a/);
478
- blocked("r1", "https://e.test/b");
479
- await tick(30);
480
- assert.equal(commands("network.continueRequest").length, 1, "the b route is still here (it did not answer, so the request continues)");
481
- assert.equal(commands("network.removeIntercept").length, 0);
482
- });
483
-
484
- it("removing a route continues the requests it still holds", async () => {
485
- const { router, blocked, commands } = await setup();
486
- await router.route("**/slow", async () => {
487
- await tick(500);
488
- });
489
- blocked("r1", "https://e.test/slow");
490
- await tick(20);
491
- await router.unroute("**/slow");
492
- assert.deepEqual(commands("network.continueRequest"), [{ request: "r1" }]);
493
- });
494
-
495
- it("unrouteAll removes everything, and a new route starts again", async () => {
496
- const { router, commands } = await setup();
497
- await router.route("**/a", () => {});
498
- await router.route(/b/, () => {});
499
- await router.unrouteAll();
500
- assert.equal(commands("network.removeIntercept").length, 1);
501
- await router.route("**/c", () => {});
502
- assert.equal(commands("network.addIntercept").length, 2);
503
- });
504
-
505
- it("dispose continues pending requests, removes the intercept and stops listening; it is idempotent", async () => {
506
- const { ws, router, blocked, commands } = await setup();
507
- await router.route("**", async () => {
508
- await tick(500);
509
- });
510
- blocked("r1", "https://e.test/x");
511
- await tick(20);
512
- await router.dispose();
513
- assert.deepEqual(commands("network.continueRequest"), [{ request: "r1" }]);
514
- assert.equal(commands("network.removeIntercept").length, 1);
515
- const before = ws.sent.length;
516
- blocked("r2", "https://e.test/y");
517
- await tick(20);
518
- assert.equal(ws.sent.length, before);
519
- await assert.doesNotReject(router.dispose());
520
- assert.equal(commands("network.removeIntercept").length, 1);
521
- });
522
-
523
- it("route() after dispose is refused", async () => {
524
- const { router } = await setup();
525
- await router.dispose();
526
- await assert.rejects(router.route("**", () => {}), /disposed/);
527
- });
528
- });