@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,28 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { Writable } from "node:stream";
4
- import { transports } from "winston";
5
- import { MASK, registerSecret, resetSecrets } from "../config/mask.js";
6
- import logger from "./index.js";
7
-
8
- test("the real logger masks registered secrets in messages and metadata", () => {
9
- let out = "";
10
- const transport = new transports.Stream({
11
- stream: new Writable({
12
- write(chunk, _enc, done) {
13
- out += String(chunk);
14
- done();
15
- },
16
- }),
17
- });
18
- logger.add(transport);
19
- registerSecret("TEST_CARD", "4242424242424242");
20
- try {
21
- logger.info("paying with 4242424242424242", { card: "4242424242424242" });
22
- } finally {
23
- logger.remove(transport);
24
- resetSecrets();
25
- }
26
- assert.ok(out.includes(MASK), out);
27
- assert.ok(!out.includes("4242424242424242"), out);
28
- });
@@ -1,40 +0,0 @@
1
- import { createLogger, format, transports } from "winston";
2
- import { maskFormat } from "../config/mask.js";
3
-
4
- const logger = createLogger({
5
- level: "debug",
6
- format: format.combine(
7
- format.timestamp({
8
- format: "DD-MM-YYYY HH:mm:ss",
9
- }),
10
- format.errors({ stack: true }),
11
- format.splat(),
12
- format.metadata({ key: "meta" }),
13
- maskFormat(),
14
- format.json({ space: 2 }),
15
- ),
16
- defaultMeta: { service: "samurai-framework" },
17
- transports: [
18
- //
19
- // - Write to all logs with level `info` and below to `quick-start-combined.log`.
20
- // - Write all logs error (and below) to `quick-start-error.log`.
21
- //
22
- new transports.Console({
23
- level: "debug",
24
- format: format.combine(
25
- format.timestamp(),
26
- format.errors({ stack: true }),
27
- format.printf(({ level, message, meta }) => {
28
- return `${level.toUpperCase()}: ${message}${meta ? `\n${JSON.stringify(meta)}` : null}`;
29
- }),
30
- ),
31
- }),
32
- new transports.File({
33
- filename: `logs/error.log`,
34
- level: "error",
35
- }),
36
- new transports.File({ filename: `logs/combined.log` }),
37
- ],
38
- });
39
-
40
- export default logger;
@@ -1,111 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
4
- import { BiDiConnector } from "../transport/bidi-connection.js";
5
- import { DataCollector, ResponseBodyUnavailableError } from "./data-collector.js";
6
-
7
- const stops: Array<() => void> = [];
8
- afterEach(() => stops.splice(0).forEach((stop) => stop()));
9
-
10
- const failing = (code: string, message = "nope") => Object.assign(new Error(message), { code });
11
-
12
- function setup(handlers: Record<string, (params: any) => object | Error> = {}) {
13
- const ws = new FakeWebSocket();
14
- const connector = new BiDiConnector(ws as unknown as WebSocket);
15
- stops.push(autoReply(ws, { "network.addDataCollector": () => ({ collector: "c1" }), ...handlers }));
16
- const commands = (method: string) => ws.sent.filter((m) => m.method === method).map((m) => m.params as any);
17
- return { ws, connector, commands };
18
- }
19
-
20
- describe("DataCollector.start", () => {
21
- it("collects response bodies of this page, up to maxBodySize", async () => {
22
- const { connector, commands } = await setup();
23
- await DataCollector.start(connector, "top", { maxBodySize: 5000 });
24
- assert.deepEqual(commands("network.addDataCollector"), [
25
- { dataTypes: ["response"], maxEncodedDataSize: 5000, contexts: ["top"] },
26
- ]);
27
- });
28
-
29
- it("defaults to 10 MiB", async () => {
30
- const { connector, commands } = await setup();
31
- await DataCollector.start(connector, "top");
32
- assert.equal(commands("network.addDataCollector")[0].maxEncodedDataSize, 10 * 1024 * 1024);
33
- });
34
- });
35
-
36
- describe("DataCollector.read", () => {
37
- it("returns a text body as its UTF-8 bytes", async () => {
38
- const { connector, commands } = await setup({
39
- "network.getData": () => ({ bytes: { type: "string", value: "héllo" } }),
40
- });
41
- const collector = await DataCollector.start(connector, "top");
42
- const body = await collector.read("r1", "https://e.test/x");
43
- assert.deepEqual([...body], [...Buffer.from("héllo", "utf8")]);
44
- assert.deepEqual(commands("network.getData"), [{ dataType: "response", request: "r1", collector: "c1" }]);
45
- });
46
-
47
- it("decodes a base64 body byte for byte", async () => {
48
- const bytes = Buffer.from([0, 1, 2, 250, 255]);
49
- const { connector } = await setup({
50
- "network.getData": () => ({ bytes: { type: "base64", value: bytes.toString("base64") } }),
51
- });
52
- const collector = await DataCollector.start(connector, "top");
53
- assert.deepEqual([...(await collector.read("r1", "u"))], [...bytes]);
54
- });
55
-
56
- it("explains missing data: collected too late, or larger than the limit", async () => {
57
- const { connector } = await setup({ "network.getData": () => failing("no such network data") });
58
- const collector = await DataCollector.start(connector, "top");
59
- await assert.rejects(collector.read("r1", "https://e.test/x"), (err: unknown) => {
60
- assert.ok(err instanceof ResponseBodyUnavailableError);
61
- assert.equal(err.url, "https://e.test/x");
62
- assert.match(err.reason, /no body was collected/);
63
- assert.match(err.reason, /started after the request/);
64
- assert.match(err.reason, /larger than maxBodySize/);
65
- assert.match(err.message, /^Response body of https:\/\/e\.test\/x is unavailable: /);
66
- return true;
67
- });
68
- });
69
-
70
- it("says when the data is not there yet", async () => {
71
- const { connector } = await setup({ "network.getData": () => failing("unavailable network data") });
72
- const collector = await DataCollector.start(connector, "top");
73
- await assert.rejects(collector.read("r1", "u"), /not available yet/);
74
- });
75
-
76
- it("other browser errors are rethrown as they are", async () => {
77
- const { connector } = await setup({ "network.getData": () => failing("unknown error") });
78
- const collector = await DataCollector.start(connector, "top");
79
- await assert.rejects(collector.read("r1", "u"), { name: "BiDiError", code: "unknown error" });
80
- });
81
- });
82
-
83
- describe("DataCollector.release and dispose", () => {
84
- it("release frees the browser's copy", async () => {
85
- const { connector, commands } = await setup();
86
- const collector = await DataCollector.start(connector, "top");
87
- await collector.release("r1");
88
- assert.deepEqual(commands("network.disownData"), [{ dataType: "response", collector: "c1", request: "r1" }]);
89
- });
90
-
91
- it("release never throws: the copy may be gone already", async () => {
92
- const { connector } = await setup({ "network.disownData": () => failing("no such network data") });
93
- const collector = await DataCollector.start(connector, "top");
94
- await assert.doesNotReject(collector.release("r1"));
95
- });
96
-
97
- it("dispose removes the collector once", async () => {
98
- const { connector, commands } = await setup();
99
- const collector = await DataCollector.start(connector, "top");
100
- await collector.dispose();
101
- await collector.dispose();
102
- await tick(5);
103
- assert.deepEqual(commands("network.removeDataCollector"), [{ collector: "c1" }]);
104
- });
105
-
106
- it("dispose ignores a collector the browser no longer knows", async () => {
107
- const { connector } = await setup({ "network.removeDataCollector": () => failing("no such network collector") });
108
- const collector = await DataCollector.start(connector, "top");
109
- await assert.doesNotReject(collector.dispose());
110
- });
111
- });
@@ -1,92 +0,0 @@
1
- import type { BiDiConnector } from "../transport/bidi-connection.js";
2
- import { BiDiError } from "../transport/bidi-error.js";
3
- import type { Collector } from "../types/bidi-modules/network.js";
4
-
5
- /** How large a response body may be to be kept: 10 MiB */
6
- const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
7
-
8
- /** The body of a response cannot be read */
9
- export class ResponseBodyUnavailableError extends Error {
10
- public readonly url: string;
11
- public readonly reason: string;
12
-
13
- constructor(url: string, reason: string) {
14
- super(`Response body of ${url} is unavailable: ${reason}`);
15
- this.name = "ResponseBodyUnavailableError";
16
- this.url = url;
17
- this.reason = reason;
18
- }
19
- }
20
-
21
- /**
22
- * A browser only keeps response bodies for a request when a data collector was registered before the request
23
- * started. This registers one for a page and reads bodies through it.
24
- */
25
- export class DataCollector {
26
- private disposed = false;
27
-
28
- private constructor(
29
- private connector: BiDiConnector,
30
- private id: Collector,
31
- ) {}
32
-
33
- public static async start(
34
- connector: BiDiConnector,
35
- contextId: string,
36
- options: { maxBodySize?: number } = {},
37
- ): Promise<DataCollector> {
38
- const { collector } = await connector.send("network.addDataCollector", {
39
- dataTypes: ["response"],
40
- maxEncodedDataSize: options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE,
41
- contexts: [contextId],
42
- });
43
- return new DataCollector(connector, collector);
44
- }
45
-
46
- /** The body of the response to request `requestId`; `url` only names it in errors */
47
- public async read(requestId: string, url: string): Promise<Buffer> {
48
- try {
49
- const { bytes } = await this.connector.send("network.getData", {
50
- dataType: "response",
51
- request: requestId,
52
- collector: this.id,
53
- });
54
- return bytes.type === "base64" ? Buffer.from(bytes.value, "base64") : Buffer.from(bytes.value, "utf8");
55
- } catch (err) {
56
- if (err instanceof BiDiError && err.code === "no such network data") {
57
- throw new ResponseBodyUnavailableError(
58
- url,
59
- "no body was collected: the collector started after the request, or the response is larger than maxBodySize",
60
- );
61
- }
62
- if (err instanceof BiDiError && err.code === "unavailable network data") {
63
- throw new ResponseBodyUnavailableError(url, "the body is not available yet");
64
- }
65
- throw err;
66
- }
67
- }
68
-
69
- /** Lets the browser drop its copy of a body that has been read. Never throws */
70
- public async release(requestId: string): Promise<void> {
71
- try {
72
- await this.connector.send("network.disownData", {
73
- dataType: "response",
74
- collector: this.id,
75
- request: requestId,
76
- });
77
- } catch {
78
- // Already gone: nothing left to free
79
- }
80
- }
81
-
82
- /** Idempotent */
83
- public async dispose(): Promise<void> {
84
- if (this.disposed) return;
85
- this.disposed = true;
86
- try {
87
- await this.connector.send("network.removeDataCollector", { collector: this.id });
88
- } catch (err) {
89
- if (!(err instanceof BiDiError && err.code === "no such network collector")) throw err;
90
- }
91
- }
92
- }
@@ -1,192 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createServer, type Server } from "node:http";
3
- import type { AddressInfo, Socket } from "node:net";
4
- import { after, before, beforeEach, test } from "node:test";
5
- import { Browser } from "../browser/browser.js";
6
- import Page from "../browser/page.js";
7
- import { callFunction } from "../script/call-function.js";
8
- import { setContent } from "../testing/browser-fixture.js";
9
- import type { BiDiConnector } from "../transport/bidi-connection.js";
10
- import { ResponseBodyUnavailableError } from "./data-collector.js";
11
- import { ResponseTimeoutError } from "./network-wait.js";
12
-
13
- let browser: Browser;
14
- let page: Page;
15
- let server: Server;
16
- let base: string;
17
- const sockets = new Set<Socket>();
18
- const BINARY = Buffer.from([0, 1, 2, 3, 128, 200, 254, 255]);
19
- let cachedHits = 0;
20
-
21
- before(async () => {
22
- server = createServer((req, res) => {
23
- const path = (req.url ?? "").split("?")[0];
24
- switch (path) {
25
- case "/real":
26
- res.setHeader("content-type", "text/html; charset=utf-8").end("<!doctype html><body id='b'>héllo document</body>");
27
- break;
28
- case "/json":
29
- res.setHeader("content-type", "application/json").end(JSON.stringify({ users: [{ id: 1, name: "Zoë" }] }));
30
- break;
31
- case "/text":
32
- res.setHeader("content-type", "text/plain; charset=utf-8").end("plain ✓ text");
33
- break;
34
- case "/binary":
35
- res.setHeader("content-type", "application/octet-stream").end(BINARY);
36
- break;
37
- case "/big":
38
- res.setHeader("content-type", "text/plain").end("x".repeat(5000));
39
- break;
40
- case "/redirect":
41
- res.writeHead(302, { location: "/json" }).end();
42
- break;
43
- case "/cached":
44
- cachedHits++;
45
- res.setHeader("content-type", "text/plain").setHeader("cache-control", "max-age=600").end(`hit ${cachedHits}`);
46
- break;
47
- case "/empty":
48
- res.writeHead(204).end();
49
- break;
50
- default:
51
- res.setHeader("content-type", "text/plain").end("ok");
52
- }
53
- });
54
- server.on("connection", (socket) => {
55
- sockets.add(socket);
56
- socket.on("close", () => sockets.delete(socket));
57
- });
58
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
59
- base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
60
-
61
- ({ browser, page } = await Browser.launch("firefox", { port: 9243, headless: true }));
62
- await page.navigateTo(`${base}/real`);
63
- });
64
-
65
- after(async () => {
66
- await page.dispose();
67
- await browser?.close();
68
- sockets.forEach((socket) => socket.destroy());
69
- await new Promise((resolve) => server.close(resolve));
70
- });
71
-
72
- beforeEach(async () => {
73
- await page.setCacheDisabled(false);
74
- });
75
-
76
- const OPTIONS = { timeout: 30000 };
77
- const connector = () => (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
78
- const run = <T>(source: string) => callFunction<T>(connector(), page.contextId, source);
79
- const fetchInPage = (path: string) => run(`() => { fetch(${JSON.stringify(base + path)}).catch(() => {}); }`);
80
-
81
- test("json() reads a JSON body", OPTIONS, async () => {
82
- const response = await page.waitForResponse("**/json", { trigger: () => fetchInPage("/json") });
83
- assert.equal(response.status, 200);
84
- assert.equal(response.headers["content-type"], "application/json");
85
- assert.deepEqual(await response.json(), { users: [{ id: 1, name: "Zoë" }] });
86
- });
87
-
88
- test("text() decodes UTF-8", OPTIONS, async () => {
89
- const response = await page.waitForResponse("**/text", { trigger: () => fetchInPage("/text") });
90
- assert.equal(await response.text(), "plain ✓ text");
91
- });
92
-
93
- test("body() returns binary content byte for byte", OPTIONS, async () => {
94
- const response = await page.waitForResponse("**/binary", { trigger: () => fetchInPage("/binary") });
95
- assert.deepEqual([...(await response.body())], [...BINARY]);
96
- });
97
-
98
- test("the body can be read again and again", OPTIONS, async () => {
99
- const response = await page.waitForResponse("**/json", { trigger: () => fetchInPage("/json") });
100
- const first = await response.text();
101
- assert.equal(await response.text(), first);
102
- assert.deepEqual(await response.json(), JSON.parse(first));
103
- });
104
-
105
- test("an empty response has an empty body", OPTIONS, async () => {
106
- const response = await page.waitForResponse("**/empty", { trigger: () => fetchInPage("/empty") });
107
- assert.equal(response.status, 204);
108
- assert.equal((await response.body()).length, 0);
109
- });
110
-
111
- test("the document of a navigation has a body too", OPTIONS, async () => {
112
- const document = await page.waitForResponse((r) => r.request.resourceType === "document" && r.url.endsWith("/real?doc"), {
113
- trigger: async () => {
114
- await page.navigateTo(`${base}/real?doc`);
115
- },
116
- });
117
- assert.match(await document.text(), /héllo document/);
118
- });
119
-
120
- test("a redirect response has no body, the target has", OPTIONS, async () => {
121
- const seen: string[] = [];
122
- const onResponse = (response: { status: number; url: string }) => {
123
- if (/\/(redirect|json)$/.test(response.url)) seen.push(`${response.status} ${new URL(response.url).pathname}`);
124
- };
125
- page.on("response", onResponse);
126
- const first = await page.waitForResponse("**/redirect", { trigger: () => fetchInPage("/redirect") });
127
- await page.waitForNetworkIdle({ idleTime: 100, timeout: 5000 });
128
- page.off("response", onResponse);
129
- assert.equal(first.status, 302);
130
- await assert.rejects(first.body(), (err: unknown) => {
131
- assert.ok(err instanceof ResponseBodyUnavailableError);
132
- assert.equal(err.reason, "redirect response");
133
- return true;
134
- });
135
- assert.deepEqual(seen, ["302 /redirect", "200 /json"]);
136
- });
137
-
138
- test("waitForResponse with a click as its trigger", OPTIONS, async () => {
139
- await setContent(
140
- page,
141
- `<button id="load">Load</button>`,
142
- `document.getElementById("load").addEventListener("click", () => fetch(${JSON.stringify(base + "/json?click")}));`,
143
- );
144
- const response = await page.waitForResponse((r) => r.url.endsWith("/json?click"), {
145
- trigger: () => page.locator("button[@id='load']").click({ timeout: 5000 }),
146
- });
147
- assert.equal((await response.json<{ users: unknown[] }>()).users.length, 1);
148
- });
149
-
150
- test("waitForRequest sees the request before its response", OPTIONS, async () => {
151
- const request = await page.waitForRequest("**/text?req", { trigger: () => fetchInPage("/text?req") });
152
- assert.equal(request.method, "GET");
153
- assert.equal(request.url, `${base}/text?req`);
154
- });
155
-
156
- test("a body over maxBodySize is reported as unavailable, with the reason", OPTIONS, async () => {
157
- // A second view of the same page, with a small limit
158
- const small = new Page(connector(), page.contextId, { network: { maxBodySize: 100 } });
159
- try {
160
- const response = await small.waitForResponse("**/big", { trigger: () => fetchInPage("/big") });
161
- await assert.rejects(response.text(), (err: unknown) => {
162
- assert.ok(err instanceof ResponseBodyUnavailableError);
163
- assert.match(err.reason, /larger than maxBodySize/);
164
- return true;
165
- });
166
- const fits = await small.waitForResponse("**/text", { trigger: () => fetchInPage("/text") });
167
- assert.equal(await fits.text(), "plain ✓ text");
168
- } finally {
169
- await small.dispose();
170
- }
171
- });
172
-
173
- test("waitForResponse times out naming what it waited for", OPTIONS, async () => {
174
- await assert.rejects(page.waitForResponse("**/never", { timeout: 300 }), (err: unknown) => {
175
- assert.ok(err instanceof ResponseTimeoutError);
176
- assert.equal(err.message, 'waitForResponse(): no response matching "**/never" within 300ms');
177
- return true;
178
- });
179
- });
180
-
181
- test("setCacheDisabled makes every request reach the server", OPTIONS, async () => {
182
- const twice = async (path: string) => {
183
- cachedHits = 0;
184
- await run(`async () => { await (await fetch(${JSON.stringify(base + path)})).text(); await (await fetch(${JSON.stringify(base + path)})).text(); }`);
185
- return cachedHits;
186
- };
187
- assert.equal(await twice("/cached?default"), 1, "the second fetch came from the cache");
188
- await page.setCacheDisabled(true);
189
- assert.equal(await twice("/cached?bypass"), 2, "with the cache off both fetches hit the server");
190
- await page.setCacheDisabled(false);
191
- assert.equal(await twice("/cached?again"), 1, "back on");
192
- });
@@ -1,151 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createServer, type Server } from "node:http";
3
- import type { AddressInfo, Socket } from "node:net";
4
- import { after, before, test } from "node:test";
5
- import { Browser } from "../browser/browser.js";
6
- import type Page from "../browser/page.js";
7
- import type { BiDiConnector } from "../transport/bidi-connection.js";
8
- import { callFunction } from "../script/call-function.js";
9
- import { NetworkIdleTimeoutError } from "./network-tracker.js";
10
- import type { FailedRequest, NetworkRequest, NetworkResponse } from "./network-tracker.js";
11
-
12
- let browser: Browser;
13
- let page: Page;
14
- let server: Server;
15
- let base: string;
16
- const sockets = new Set<Socket>();
17
- const hits: string[] = [];
18
-
19
- before(async () => {
20
- server = createServer((req, res) => {
21
- hits.push(req.url ?? "");
22
- switch (req.url?.split("?")[0]) {
23
- case "/":
24
- res.setHeader("content-type", "text/html").end("<!doctype html><title>t</title><body>home</body>");
25
- break;
26
- case "/slow":
27
- setTimeout(() => res.setHeader("content-type", "text/plain").end("slow"), 300);
28
- break;
29
- case "/redirect":
30
- res.writeHead(302, { location: "/slow" }).end();
31
- break;
32
- case "/fail":
33
- req.socket.destroy();
34
- break;
35
- case "/hang":
36
- break; // never answers
37
- case "/frame":
38
- res.setHeader("content-type", "text/html").end('<body>frame<script>fetch("/slow")</script></body>');
39
- break;
40
- default:
41
- res.setHeader("content-type", "text/plain").end("ok");
42
- }
43
- });
44
- server.on("connection", (socket) => {
45
- sockets.add(socket);
46
- socket.on("close", () => sockets.delete(socket));
47
- });
48
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
49
- base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
50
-
51
- ({ browser, page } = await Browser.launch("firefox", { port: 9237, headless: true }));
52
- await page.navigateTo(`${base}/`, "complete");
53
- });
54
-
55
- after(async () => {
56
- await page.dispose();
57
- await browser?.close();
58
- sockets.forEach((socket) => socket.destroy());
59
- await new Promise((resolve) => server.close(resolve));
60
- });
61
-
62
- const OPTIONS = { timeout: 30000 };
63
- const connector = () => (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
64
- const run = (source: string) => callFunction(connector(), page.contextId, source);
65
- const IDLE = { idleTime: 200, timeout: 10000 };
66
-
67
- test("idle waits for the last of several timed requests, then for idleTime", OPTIONS, async () => {
68
- await run(`() => {
69
- fetch("/slow?a");
70
- setTimeout(() => fetch("/slow?b"), 200);
71
- setTimeout(() => fetch("/slow?c"), 400);
72
- }`);
73
- const started = Date.now();
74
- await page.waitForNetworkIdle(IDLE);
75
- const took = Date.now() - started;
76
- // last request starts at ~400ms, takes 300ms, then 200ms of quiet
77
- assert.ok(took >= 850, `resolved too early: ${took}ms`);
78
- assert.ok(hits.includes("/slow?c"));
79
- });
80
-
81
- test("an already quiet page is idle after idleTime", OPTIONS, async () => {
82
- await page.waitForNetworkIdle({ idleTime: 100, timeout: 5000 });
83
- const started = Date.now();
84
- await page.waitForNetworkIdle({ idleTime: 150, timeout: 5000 });
85
- const took = Date.now() - started;
86
- assert.ok(took >= 140 && took < 2000, `took ${took}ms`);
87
- });
88
-
89
- test("a redirect chain is followed to its end and linked", OPTIONS, async () => {
90
- const requests: NetworkRequest[] = [];
91
- const responses: NetworkResponse[] = [];
92
- const onRequest = (request: NetworkRequest) => requests.push(request);
93
- const onResponse = (response: NetworkResponse) => responses.push(response);
94
- page.on("request", onRequest);
95
- page.on("response", onResponse);
96
- await run(`() => { fetch("/redirect"); }`);
97
- await page.waitForNetworkIdle(IDLE);
98
- page.off("request", onRequest);
99
- page.off("response", onResponse);
100
-
101
- const hops = requests.filter((request) => request.url.includes("/redirect") || request.url.endsWith("/slow"));
102
- assert.equal(hops.length, 2);
103
- assert.equal(hops[0]!.url, `${base}/redirect`);
104
- assert.equal(hops[1]!.url, `${base}/slow`);
105
- assert.equal(hops[1]!.redirectedFrom, hops[0]);
106
- assert.deepEqual(responses.map((response) => response.status), [302, 200]);
107
- assert.equal(responses[1]!.headers["content-type"], "text/plain");
108
- });
109
-
110
- test("a failed request is reported and does not keep the page busy", OPTIONS, async () => {
111
- const failed: FailedRequest[] = [];
112
- const onFailed = (request: FailedRequest) => failed.push(request);
113
- page.on("requestfailed", onFailed);
114
- await run(`() => { fetch("/fail").catch(() => {}); }`);
115
- await page.waitForNetworkIdle(IDLE);
116
- page.off("requestfailed", onFailed);
117
- assert.equal(failed.length, 1);
118
- assert.equal(failed[0]!.url, `${base}/fail`);
119
- assert.ok(failed[0]!.errorText.length > 0);
120
- });
121
-
122
- test("requests of an iframe count towards the page", OPTIONS, async () => {
123
- await run(`() => {
124
- const frame = document.createElement("iframe");
125
- frame.src = "/frame";
126
- document.body.appendChild(frame);
127
- }`);
128
- const started = Date.now();
129
- await page.waitForNetworkIdle(IDLE);
130
- // frame document, then the frame's own slow fetch (300ms)
131
- assert.ok(Date.now() - started >= 450, `resolved before the frame's request finished: ${Date.now() - started}ms`);
132
- });
133
-
134
- test("a request that never ends is named when the wait times out", OPTIONS, async () => {
135
- await run(`() => { fetch("/hang").catch(() => {}); }`);
136
- await assert.rejects(page.waitForNetworkIdle({ idleTime: 100, timeout: 1000 }), (err) => {
137
- assert.ok(err instanceof NetworkIdleTimeoutError);
138
- assert.deepEqual(err.last, [`${base}/hang`]);
139
- return true;
140
- });
141
- });
142
-
143
- test("navigation requests are tracked from the start of the browser", OPTIONS, async () => {
144
- const requests: string[] = [];
145
- const onRequest = (request: NetworkRequest) => requests.push(`${request.resourceType} ${request.url}`);
146
- page.on("request", onRequest);
147
- await page.navigateTo(`${base}/?nav`, "complete");
148
- await page.waitForNetworkIdle(IDLE);
149
- page.off("request", onRequest);
150
- assert.ok(requests.includes(`document ${base}/?nav`), requests.join("\n"));
151
- });