@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,307 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import { ContextTree } from "../browser/context-tree.js";
4
- import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
5
- import { BiDiConnector } from "../transport/bidi-connection.js";
6
- import { WaitTimeoutError } from "../wait/wait-until.js";
7
- import { NetworkIdleTimeoutError, NetworkTracker, type NetworkRequest } from "./network-tracker.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
- afterEach(() => stops.splice(0).forEach((stop) => stop()));
16
-
17
- async function setup(options?: { ignore?: (r: NetworkRequest) => boolean }) {
18
- const ws = new FakeWebSocket();
19
- const connector = new BiDiConnector(ws as unknown as WebSocket);
20
- stops.push(autoReply(ws, { "browsingContext.getTree": () => ({ contexts: TREE }) }));
21
- const tree = await ContextTree.create(connector);
22
- const tracker = await NetworkTracker.start(connector, tree, "top", options);
23
- return { ws, tracker };
24
- }
25
-
26
- function requestData(id: string, url = `https://example.test/${id}`, extra: object = {}) {
27
- return {
28
- request: id,
29
- url,
30
- method: "GET",
31
- headers: [{ name: "Accept", value: { type: "string", value: "text/html" } }],
32
- cookies: [],
33
- headersSize: 0,
34
- bodySize: null,
35
- destination: "script",
36
- initiatorType: "script",
37
- timings: {},
38
- ...extra,
39
- };
40
- }
41
- const base = (id: string, context: string | null, extra: { redirectCount?: number; url?: string; isBlocked?: boolean } = {}) => ({
42
- context,
43
- isBlocked: extra.isBlocked ?? false,
44
- navigation: null,
45
- redirectCount: extra.redirectCount ?? 0,
46
- request: requestData(id, extra.url),
47
- timestamp: 0,
48
- });
49
- const response = (status = 200, headers: Array<{ name: string; value: object }> = []) => ({
50
- url: "https://example.test/x",
51
- protocol: "http/1.1",
52
- status,
53
- statusText: "OK",
54
- fromCache: false,
55
- headers,
56
- mimeType: "text/plain",
57
- bytesReceived: 0,
58
- headersSize: 0,
59
- bodySize: 0,
60
- content: { size: 0 },
61
- });
62
- const start = (ws: FakeWebSocket, id: string, context: string | null = "top", extra = {}) =>
63
- ws.emitEvent("network.beforeRequestSent", base(id, context, extra));
64
- const done = (ws: FakeWebSocket, id: string, context: string | null = "top", extra: { redirectCount?: number; status?: number; headers?: Array<{ name: string; value: object }> } = {}) =>
65
- ws.emitEvent("network.responseCompleted", {
66
- ...base(id, context, extra),
67
- response: response(extra.status, extra.headers),
68
- });
69
- const fail = (ws: FakeWebSocket, id: string, context: string | null = "top", extra = {}) =>
70
- ws.emitEvent("network.fetchError", { ...base(id, context, extra), errorText: "NS_ERROR_NET_RESET" });
71
-
72
- describe("NetworkTracker in-flight bookkeeping", () => {
73
- it("counts a request from beforeRequestSent until responseCompleted", async () => {
74
- const { ws, tracker } = await setup();
75
- start(ws, "1");
76
- start(ws, "2");
77
- assert.equal(tracker.inflight, 2);
78
- done(ws, "1");
79
- assert.equal(tracker.inflight, 1);
80
- done(ws, "2");
81
- assert.equal(tracker.inflight, 0);
82
- });
83
-
84
- it("fetchError ends a request too", async () => {
85
- const { ws, tracker } = await setup();
86
- start(ws, "1");
87
- fail(ws, "1");
88
- assert.equal(tracker.inflight, 0);
89
- });
90
-
91
- it("a redirect chain counts as one live request at a time", async () => {
92
- const { ws, tracker } = await setup();
93
- start(ws, "1", "top", { redirectCount: 0 });
94
- done(ws, "1", "top", { redirectCount: 0, status: 302 });
95
- assert.equal(tracker.inflight, 0);
96
- start(ws, "1", "top", { redirectCount: 1 });
97
- assert.equal(tracker.inflight, 1);
98
- done(ws, "1", "top", { redirectCount: 1 });
99
- assert.equal(tracker.inflight, 0);
100
- });
101
-
102
- it("ignores completions of requests it never saw", async () => {
103
- const { ws, tracker } = await setup();
104
- done(ws, "unknown");
105
- fail(ws, "unknown");
106
- assert.equal(tracker.inflight, 0);
107
- });
108
-
109
- it("counts iframe requests and ignores other pages and contextless requests", async () => {
110
- const { ws, tracker } = await setup();
111
- start(ws, "a", "frame");
112
- start(ws, "b", "other");
113
- start(ws, "c", null);
114
- assert.equal(tracker.inflight, 1);
115
- done(ws, "b", "other");
116
- assert.equal(tracker.inflight, 1);
117
- done(ws, "a", "frame");
118
- assert.equal(tracker.inflight, 0);
119
- });
120
-
121
- it("does not count requests the ignore predicate rejects, and not their completion either", async () => {
122
- const { ws, tracker } = await setup({ ignore: (request: NetworkRequest) => request.url.endsWith("/events") });
123
- ws.emitEvent("network.beforeRequestSent", base("s", "top", { url: "https://example.test/events" }));
124
- start(ws, "n");
125
- assert.equal(tracker.inflight, 1);
126
- assert.deepEqual(tracker.inflightUrls, ["https://example.test/n"]);
127
- });
128
-
129
- it("keeps a blocked request in flight until it completes", async () => {
130
- const { ws, tracker } = await setup();
131
- start(ws, "1", "top", { isBlocked: true });
132
- await tick(5);
133
- assert.equal(tracker.inflight, 1);
134
- });
135
- });
136
-
137
- describe("NetworkTracker.waitForIdle", () => {
138
- it("resolves after idleTime on a page that is already quiet", async () => {
139
- const { tracker } = await setup();
140
- const started = Date.now();
141
- await tracker.waitForIdle({ idleTime: 40, timeout: 1000 });
142
- assert.ok(Date.now() - started >= 35);
143
- });
144
-
145
- it("waits for requests in flight, then for idleTime after the last one", async () => {
146
- const { ws, tracker } = await setup();
147
- start(ws, "1");
148
- let resolved = false;
149
- const idle = tracker.waitForIdle({ idleTime: 60, timeout: 2000 }).then(() => (resolved = true));
150
- await tick(80);
151
- assert.equal(resolved, false, "a request is still in flight");
152
- done(ws, "1");
153
- const finished = Date.now();
154
- await tick(30);
155
- assert.equal(resolved, false, "idleTime has not passed yet");
156
- await idle;
157
- assert.ok(Date.now() - finished >= 55);
158
- });
159
-
160
- it("restarts the idle timer when a new request starts", async () => {
161
- const { ws, tracker } = await setup();
162
- let resolved = false;
163
- const idle = tracker.waitForIdle({ idleTime: 60, timeout: 2000 }).then(() => (resolved = true));
164
- await tick(40);
165
- start(ws, "1");
166
- await tick(60);
167
- assert.equal(resolved, false);
168
- done(ws, "1");
169
- await idle;
170
- assert.equal(resolved, true);
171
- });
172
-
173
- it("times out with the URLs still in flight", async () => {
174
- const { ws, tracker } = await setup();
175
- for (let i = 0; i < 7; i++) start(ws, `r${i}`);
176
- await assert.rejects(tracker.waitForIdle({ idleTime: 20, timeout: 60 }), (err: unknown) => {
177
- assert.ok(err instanceof NetworkIdleTimeoutError);
178
- assert.ok(err instanceof WaitTimeoutError);
179
- assert.equal(err.timeout, 60);
180
- assert.equal(err.last?.length, 5, "at most five URLs");
181
- assert.match(err.message, /waitForNetworkIdle\(\): still 7 request\(s\) in flight after 60ms: https:\/\/example\.test\/r0/);
182
- return true;
183
- });
184
- });
185
-
186
- it("stops listening after it resolves or times out", async () => {
187
- const { ws, tracker } = await setup();
188
- await tracker.waitForIdle({ idleTime: 10, timeout: 500 });
189
- start(ws, "1");
190
- done(ws, "1");
191
- assert.equal(tracker.waiting, 0);
192
- });
193
-
194
- it("rejects pending waits with page closed when disposed", async () => {
195
- const { ws, tracker } = await setup();
196
- start(ws, "1");
197
- const idle = tracker.waitForIdle({ idleTime: 20, timeout: 5000 });
198
- const rejection = assert.rejects(idle, /page closed/);
199
- await tracker.dispose();
200
- await rejection;
201
- });
202
- });
203
-
204
- describe("NetworkTracker events", () => {
205
- it("emits request, response and requestfailed with plain wrappers", async () => {
206
- const { ws, tracker } = await setup();
207
- const seen: string[] = [];
208
- let request!: NetworkRequest;
209
- tracker.on("request", (r) => {
210
- request = r;
211
- seen.push(`request ${r.method} ${r.url}`);
212
- });
213
- tracker.on("response", (r) => seen.push(`response ${r.status} ${r.request.url}`));
214
- tracker.on("requestfailed", (r) => seen.push(`failed ${r.errorText}`));
215
- start(ws, "1");
216
- done(ws, "1", "top", { headers: [{ name: "Content-Type", value: { type: "string", value: "text/plain" } }] });
217
- start(ws, "2");
218
- fail(ws, "2");
219
- assert.deepEqual(seen, [
220
- "request GET https://example.test/1",
221
- "response 200 https://example.test/1",
222
- "request GET https://example.test/2",
223
- "failed NS_ERROR_NET_RESET",
224
- ]);
225
- assert.deepEqual(request.headers, { accept: "text/html" });
226
- assert.equal(request.resourceType, "script");
227
- assert.equal(request.redirectedFrom, null);
228
- });
229
-
230
- it("decodes base64 header values and joins repeated headers", async () => {
231
- const { ws, tracker } = await setup();
232
- let headers: Record<string, string> = {};
233
- tracker.on("response", (r) => (headers = r.headers));
234
- start(ws, "1");
235
- done(ws, "1", "top", {
236
- headers: [
237
- { name: "X-B64", value: { type: "base64", value: Buffer.from("héllo").toString("base64") } },
238
- { name: "Set-Cookie", value: { type: "string", value: "a=1" } },
239
- { name: "set-cookie", value: { type: "string", value: "b=2" } },
240
- ],
241
- });
242
- assert.deepEqual(headers, { "x-b64": "héllo", "set-cookie": "a=1, b=2" });
243
- });
244
-
245
- it("links a redirect to the request it came from", async () => {
246
- const { ws, tracker } = await setup();
247
- const requests: NetworkRequest[] = [];
248
- tracker.on("request", (r) => requests.push(r));
249
- start(ws, "1", "top", { redirectCount: 0, url: "https://example.test/old" });
250
- done(ws, "1", "top", { redirectCount: 0, status: 301 });
251
- start(ws, "1", "top", { redirectCount: 1, url: "https://example.test/new" });
252
- assert.equal(requests[1]!.redirectedFrom, requests[0]);
253
- assert.equal(requests[0]!.redirectedFrom, null);
254
- });
255
-
256
- it("events of other pages are not emitted", async () => {
257
- const { ws, tracker } = await setup();
258
- let count = 0;
259
- tracker.on("request", () => count++);
260
- start(ws, "1", "other");
261
- assert.equal(count, 0);
262
- });
263
-
264
- it("a throwing listener does not break tracking or other listeners", async () => {
265
- const { ws, tracker } = await setup();
266
- let second = 0;
267
- tracker.on("request", () => {
268
- throw new Error("listener failed");
269
- });
270
- tracker.on("request", () => second++);
271
- start(ws, "1");
272
- assert.equal(second, 1);
273
- assert.equal(tracker.inflight, 1);
274
- });
275
-
276
- it("off removes a listener", async () => {
277
- const { ws, tracker } = await setup();
278
- let count = 0;
279
- const listener = () => count++;
280
- tracker.on("request", listener);
281
- tracker.off("request", listener);
282
- start(ws, "1");
283
- assert.equal(count, 0);
284
- });
285
- });
286
-
287
- describe("NetworkTracker lifecycle", () => {
288
- it("subscribes to the three network events and releases them on dispose", async () => {
289
- const { ws, tracker } = await setup();
290
- const subscribed = ws.sent.filter((m) => m.method === "session.subscribe").map((m) => (m.params as any).events[0]);
291
- for (const event of ["network.beforeRequestSent", "network.responseCompleted", "network.fetchError"]) {
292
- assert.ok(subscribed.includes(event), event);
293
- }
294
- await tracker.dispose();
295
- await tick(10);
296
- const unsubscribed = ws.sent.filter((m) => m.method === "session.unsubscribe").length;
297
- assert.ok(unsubscribed >= 3);
298
- start(ws, "1");
299
- assert.equal(tracker.inflight, 0, "listeners removed");
300
- });
301
-
302
- it("dispose is idempotent", async () => {
303
- const { tracker } = await setup();
304
- await tracker.dispose();
305
- await assert.doesNotReject(tracker.dispose());
306
- });
307
- });
@@ -1,286 +0,0 @@
1
- import EventEmitter from "node:events";
2
- import type { ContextTree } from "../browser/context-tree.js";
3
- import logger from "../logger/index.js";
4
- import type { BiDiConnector } from "../transport/bidi-connection.js";
5
- import type { Subscription } from "../transport/subscriptions.js";
6
- import type {
7
- BeforeRequestSentParameters,
8
- FetchErrorParameters,
9
- Header,
10
- RequestData,
11
- ResponseCompletedParameters,
12
- } from "../types/bidi-modules/network.js";
13
- import { WaitTimeoutError } from "../wait/wait-until.js";
14
-
15
- /** How many URLs a timeout message names */
16
- const URLS_IN_MESSAGE = 5;
17
-
18
- export interface NetworkRequest {
19
- /** The browser's id; the same for every hop of a redirect chain */
20
- id: string;
21
- url: string;
22
- method: string;
23
- /** Header names lower-cased; repeated headers joined with ", " */
24
- headers: Record<string, string>;
25
- /** "document", "script", "image", …; falls back to the initiator type, then "other" */
26
- resourceType: string;
27
- navigation: string | null;
28
- /** The request this one is the redirect of, or null */
29
- redirectedFrom: NetworkRequest | null;
30
- /** True while a network intercept holds it */
31
- isBlocked: boolean;
32
- }
33
-
34
- export interface NetworkResponse {
35
- url: string;
36
- status: number;
37
- statusText: string;
38
- headers: Record<string, string>;
39
- fromCache: boolean;
40
- request: NetworkRequest;
41
- }
42
-
43
- export type FailedRequest = NetworkRequest & { errorText: string };
44
-
45
- interface EventMap {
46
- request: [NetworkRequest];
47
- response: [NetworkResponse];
48
- requestfailed: [FailedRequest];
49
- }
50
- export type NetworkEventName = keyof EventMap;
51
-
52
- export interface TrackerOptions {
53
- /** Requests it returns true for are neither counted nor emitted. Default: none */
54
- ignore?: (request: NetworkRequest) => boolean;
55
- }
56
-
57
- /** waitForIdle ran out of time with requests still in flight; `last` holds (up to five of) their URLs */
58
- export class NetworkIdleTimeoutError extends WaitTimeoutError<string[]> {
59
- constructor(timeout: number, inflight: number, urls: string[]) {
60
- super(timeout, urls);
61
- this.name = "NetworkIdleTimeoutError";
62
- this.message = `waitForNetworkIdle(): still ${inflight} request(s) in flight after ${timeout}ms: ${urls.join(", ")}`;
63
- }
64
- }
65
-
66
- interface Waiter {
67
- reject: (reason: unknown) => void;
68
- }
69
-
70
- /**
71
- * Follows the network activity of one page (its frames included): which requests are in flight,
72
- * and `request` / `response` / `requestfailed` events. In-flight is keyed by `request id:redirect count`,
73
- * so a redirect chain is one live request at a time.
74
- */
75
- export class NetworkTracker {
76
- private events = new EventEmitter();
77
- private active = new Map<string, string>();
78
- /** Requests that may still be the source of a redirect, by hop key */
79
- private hops = new Map<string, NetworkRequest>();
80
- private waiters = new Set<Waiter>();
81
- private disposed = false;
82
-
83
- private constructor(
84
- private tree: ContextTree,
85
- private contextId: string,
86
- private ignore: (request: NetworkRequest) => boolean,
87
- private connector: BiDiConnector,
88
- ) {}
89
-
90
- public static async start(
91
- connector: BiDiConnector,
92
- tree: ContextTree,
93
- contextId: string,
94
- options: TrackerOptions = {},
95
- ): Promise<NetworkTracker> {
96
- const tracker = new NetworkTracker(tree, contextId, options.ignore ?? (() => false), connector);
97
- // Listen first, then subscribe: nothing can arrive in between
98
- connector.onEvent("network.beforeRequestSent", tracker.onBeforeRequestSent);
99
- connector.onEvent("network.responseCompleted", tracker.onResponseCompleted);
100
- connector.onEvent("network.fetchError", tracker.onFetchError);
101
- try {
102
- tracker.subscription = await connector.subscribe([
103
- "network.beforeRequestSent",
104
- "network.responseCompleted",
105
- "network.fetchError",
106
- ]);
107
- } catch (err) {
108
- tracker.removeListeners();
109
- throw err;
110
- }
111
- return tracker;
112
- }
113
-
114
- private subscription: Subscription | undefined;
115
-
116
- /** Requests currently in flight */
117
- public get inflight(): number {
118
- return this.active.size;
119
- }
120
-
121
- /** URLs of the requests currently in flight, oldest first */
122
- public get inflightUrls(): string[] {
123
- return [...this.active.values()];
124
- }
125
-
126
- /** Waits that have not finished yet */
127
- public get waiting(): number {
128
- return this.waiters.size;
129
- }
130
-
131
- public on<E extends NetworkEventName>(event: E, listener: (...args: EventMap[E]) => void): void {
132
- this.events.on(event, listener as (...args: unknown[]) => void);
133
- }
134
-
135
- public off<E extends NetworkEventName>(event: E, listener: (...args: EventMap[E]) => void): void {
136
- this.events.off(event, listener as (...args: unknown[]) => void);
137
- }
138
-
139
- /**
140
- * Resolves once no request has been in flight for `idleTime` ms. A quiet page resolves after `idleTime`.
141
- * Rejects with NetworkIdleTimeoutError after `timeout` ms, or with "page closed" when disposed.
142
- */
143
- public waitForIdle({ idleTime, timeout }: { idleTime: number; timeout: number }): Promise<void> {
144
- return new Promise<void>((resolve, reject) => {
145
- let idleTimer: ReturnType<typeof setTimeout> | undefined;
146
- const waiter: Waiter = { reject: (reason) => finish(() => reject(reason)) };
147
- const finish = (settle: () => void) => {
148
- clearTimeout(idleTimer);
149
- clearTimeout(deadline);
150
- this.events.off("change", check);
151
- this.waiters.delete(waiter);
152
- settle();
153
- };
154
- const check = () => {
155
- clearTimeout(idleTimer);
156
- if (this.active.size === 0) idleTimer = setTimeout(() => finish(resolve), idleTime);
157
- };
158
- const deadline = setTimeout(
159
- () =>
160
- finish(() =>
161
- reject(
162
- new NetworkIdleTimeoutError(
163
- timeout,
164
- this.active.size,
165
- this.inflightUrls.slice(0, URLS_IN_MESSAGE),
166
- ),
167
- ),
168
- ),
169
- timeout,
170
- );
171
- this.waiters.add(waiter);
172
- this.events.on("change", check);
173
- check();
174
- });
175
- }
176
-
177
- /** Stops tracking, unsubscribes and rejects pending waits. Idempotent */
178
- public async dispose(): Promise<void> {
179
- if (this.disposed) return;
180
- this.disposed = true;
181
- this.removeListeners();
182
- this.active.clear();
183
- this.hops.clear();
184
- [...this.waiters].forEach((waiter) => waiter.reject(new Error("page closed")));
185
- await this.subscription?.unsubscribe();
186
- }
187
-
188
- private removeListeners() {
189
- this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
190
- this.connector.offEvent("network.responseCompleted", this.onResponseCompleted);
191
- this.connector.offEvent("network.fetchError", this.onFetchError);
192
- }
193
-
194
- private owns(context: string | null): boolean {
195
- return context !== null && this.tree.isWithin(context, this.contextId);
196
- }
197
-
198
- private static key(data: RequestData, redirectCount: number | Number): string {
199
- return `${data.request}:${Number(redirectCount)}`;
200
- }
201
-
202
- private onBeforeRequestSent = (params: BeforeRequestSentParameters) => {
203
- if (!this.owns(params.context)) return;
204
- const key = NetworkTracker.key(params.request, params.redirectCount);
205
- const redirectCount = Number(params.redirectCount);
206
- const previousKey = `${params.request.request}:${redirectCount - 1}`;
207
- const redirectedFrom = redirectCount > 0 ? (this.hops.get(previousKey) ?? null) : null;
208
- this.hops.delete(previousKey);
209
-
210
- const request = toRequest(params.request, params.navigation, params.isBlocked, redirectedFrom);
211
- if (this.ignore(request)) return;
212
- this.hops.set(key, request);
213
- this.active.set(key, request.url);
214
- this.emit("request", request);
215
- this.events.emit("change");
216
- };
217
-
218
- private onResponseCompleted = (params: ResponseCompletedParameters) => {
219
- if (!this.owns(params.context)) return;
220
- const key = NetworkTracker.key(params.request, params.redirectCount);
221
- const request = this.hops.get(key);
222
- if (!this.active.delete(key) || !request) return;
223
- const { response } = params;
224
- // A redirect response keeps its request around: the next hop links back to it
225
- if (response.status < 300 || response.status >= 400) this.hops.delete(key);
226
- this.emit("response", {
227
- url: response.url,
228
- status: response.status,
229
- statusText: response.statusText,
230
- headers: toHeaders(response.headers),
231
- fromCache: response.fromCache,
232
- request,
233
- });
234
- this.events.emit("change");
235
- };
236
-
237
- private onFetchError = (params: FetchErrorParameters) => {
238
- if (!this.owns(params.context)) return;
239
- const key = NetworkTracker.key(params.request, params.redirectCount);
240
- const request = this.hops.get(key);
241
- if (!this.active.delete(key) || !request) return;
242
- this.hops.delete(key);
243
- this.emit("requestfailed", Object.assign(request, { errorText: params.errorText }));
244
- this.events.emit("change");
245
- };
246
-
247
- /** A listener that throws must not stop the tracker or the other listeners */
248
- private emit<E extends NetworkEventName>(event: E, ...args: EventMap[E]) {
249
- for (const listener of this.events.listeners(event)) {
250
- try {
251
- listener(...args);
252
- } catch (err) {
253
- logger.error("A network %s listener threw", event, { err });
254
- }
255
- }
256
- }
257
- }
258
-
259
- export function toRequest(
260
- data: RequestData,
261
- navigation: string | null,
262
- isBlocked: boolean,
263
- redirectedFrom: NetworkRequest | null,
264
- ): NetworkRequest {
265
- return {
266
- id: data.request,
267
- url: data.url,
268
- method: data.method,
269
- headers: toHeaders(data.headers),
270
- resourceType: data.destination || data.initiatorType || "other",
271
- navigation,
272
- redirectedFrom,
273
- isBlocked,
274
- };
275
- }
276
-
277
- /** Header list to a lower-cased record; repeated names are joined, base64 values decoded */
278
- export function toHeaders(headers: Header[]): Record<string, string> {
279
- const result: Record<string, string> = {};
280
- for (const { name, value } of headers) {
281
- const text = value.type === "base64" ? Buffer.from(value.value, "base64").toString("utf8") : value.value;
282
- const key = name.toLowerCase();
283
- result[key] = key in result ? `${result[key]}, ${text}` : text;
284
- }
285
- return result;
286
- }
@@ -1,28 +0,0 @@
1
- import { WaitTimeoutError } from "../wait/wait-until.js";
2
- import { matchUrl, type UrlPattern } from "./url-match.js";
3
-
4
- /** What `page.waitForRequest` / `waitForResponse` match: a URL pattern, or a function */
5
- export type NetworkMatch<T> = UrlPattern | ((item: T) => boolean);
6
-
7
- /** No matching request or response arrived in time */
8
- export class NetworkWaitTimeoutError extends WaitTimeoutError<undefined> {
9
- constructor(operation: "waitForRequest" | "waitForResponse", match: NetworkMatch<never>, timeout: number) {
10
- super(timeout, undefined);
11
- this.name = operation === "waitForRequest" ? "RequestTimeoutError" : "ResponseTimeoutError";
12
- const kind = operation === "waitForRequest" ? "request" : "response";
13
- this.message = `${operation}(): no ${kind} matching ${describeMatch(match)} within ${timeout}ms`;
14
- }
15
- }
16
-
17
- export class RequestTimeoutError extends NetworkWaitTimeoutError {}
18
- export class ResponseTimeoutError extends NetworkWaitTimeoutError {}
19
-
20
- function describeMatch(match: NetworkMatch<never>): string {
21
- if (typeof match === "function") return "the given function";
22
- return typeof match === "string" ? JSON.stringify(match) : String(match);
23
- }
24
-
25
- /** The predicate for a match: a function as it is, a URL pattern tested on the item's url */
26
- export function toPredicate<T extends { url: string }>(match: NetworkMatch<T>): (item: T) => boolean {
27
- return typeof match === "function" ? match : (item) => matchUrl(match, item.url);
28
- }