@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,93 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { ResponseBodyUnavailableError } from "./data-collector.js";
4
- import type { NetworkRequest, NetworkResponse } from "./network-tracker.js";
5
- import { Response } from "./response.js";
6
-
7
- const request: NetworkRequest = {
8
- id: "r1",
9
- url: "https://e.test/api",
10
- method: "GET",
11
- headers: {},
12
- resourceType: "fetch",
13
- navigation: null,
14
- redirectedFrom: null,
15
- isBlocked: false,
16
- };
17
- const data = (extra: Partial<NetworkResponse> = {}): NetworkResponse => ({
18
- url: "https://e.test/api",
19
- status: 200,
20
- statusText: "OK",
21
- headers: { "content-type": "application/json" },
22
- fromCache: false,
23
- request,
24
- ...extra,
25
- });
26
-
27
- function response(body: Buffer | Error, extra: Partial<NetworkResponse> = {}) {
28
- let loads = 0;
29
- const wrapped = new Response(data(extra), async () => {
30
- loads++;
31
- if (body instanceof Error) throw body;
32
- return body;
33
- });
34
- return { wrapped, loads: () => loads };
35
- }
36
-
37
- describe("Response", () => {
38
- it("carries what the network tracker knew", () => {
39
- const { wrapped } = response(Buffer.alloc(0));
40
- assert.equal(wrapped.id, "r1");
41
- assert.equal(wrapped.url, "https://e.test/api");
42
- assert.equal(wrapped.status, 200);
43
- assert.equal(wrapped.statusText, "OK");
44
- assert.deepEqual(wrapped.headers, { "content-type": "application/json" });
45
- assert.equal(wrapped.fromCache, false);
46
- assert.equal(wrapped.request, request);
47
- });
48
-
49
- it("body, text and json read the same bytes", async () => {
50
- const { wrapped } = response(Buffer.from('{"a":"héllo"}'));
51
- assert.deepEqual([...(await wrapped.body())], [...Buffer.from('{"a":"héllo"}')]);
52
- assert.equal(await wrapped.text(), '{"a":"héllo"}');
53
- assert.deepEqual(await wrapped.json(), { a: "héllo" });
54
- });
55
-
56
- it("loads the body once, however often or concurrently it is read", async () => {
57
- const { wrapped, loads } = response(Buffer.from("1"));
58
- await Promise.all([wrapped.body(), wrapped.text(), wrapped.json()]);
59
- await wrapped.body();
60
- assert.equal(loads(), 1);
61
- });
62
-
63
- it("json names the url when the body is not JSON", async () => {
64
- const { wrapped } = response(Buffer.from("<html>"));
65
- await assert.rejects(wrapped.json(), /Response body of https:\/\/e\.test\/api is not JSON: /);
66
- });
67
-
68
- it("a failed load can be tried again", async () => {
69
- let attempts = 0;
70
- const wrapped = new Response(data(), async () => {
71
- if (++attempts === 1) throw new Error("first fails");
72
- return Buffer.from("ok");
73
- });
74
- await assert.rejects(wrapped.body(), /first fails/);
75
- assert.equal(await wrapped.text(), "ok");
76
- });
77
-
78
- it("a redirect has no body, and the browser is not asked", async () => {
79
- const { wrapped, loads } = response(Buffer.from("x"), { status: 302, headers: { location: "/next" } });
80
- await assert.rejects(wrapped.body(), (err: unknown) => {
81
- assert.ok(err instanceof ResponseBodyUnavailableError);
82
- assert.equal(err.reason, "redirect response");
83
- return true;
84
- });
85
- assert.equal(loads(), 0);
86
- });
87
-
88
- it("a 3xx without Location (304 Not Modified) is asked for", async () => {
89
- const { wrapped, loads } = response(Buffer.from("cached"), { status: 304 });
90
- assert.equal(await wrapped.text(), "cached");
91
- assert.equal(loads(), 1);
92
- });
93
- });
@@ -1,58 +0,0 @@
1
- import { ResponseBodyUnavailableError } from "./data-collector.js";
2
- import type { NetworkRequest, NetworkResponse } from "./network-tracker.js";
3
-
4
- /** A response of the page, with access to its body */
5
- export class Response {
6
- public readonly url: string;
7
- public readonly status: number;
8
- public readonly statusText: string;
9
- /** Header names lower-cased */
10
- public readonly headers: Record<string, string>;
11
- public readonly fromCache: boolean;
12
- public readonly request: NetworkRequest;
13
- private body_: Promise<Buffer> | undefined;
14
-
15
- constructor(
16
- data: NetworkResponse,
17
- private loadBody: (response: Response) => Promise<Buffer>,
18
- ) {
19
- this.url = data.url;
20
- this.status = data.status;
21
- this.statusText = data.statusText;
22
- this.headers = data.headers;
23
- this.fromCache = data.fromCache;
24
- this.request = data.request;
25
- }
26
-
27
- /** The browser's id of the request; the same for every hop of a redirect chain */
28
- public get id(): string {
29
- return this.request.id;
30
- }
31
-
32
- /** The body as bytes. Read once from the browser, then kept */
33
- public body(): Promise<Buffer> {
34
- if (this.status >= 300 && this.status < 400 && "location" in this.headers) {
35
- return Promise.reject(new ResponseBodyUnavailableError(this.url, "redirect response"));
36
- }
37
- this.body_ ??= this.loadBody(this).catch((err) => {
38
- this.body_ = undefined; // let a later call try again
39
- throw err;
40
- });
41
- return this.body_;
42
- }
43
-
44
- /** The body as UTF-8 text */
45
- public async text(): Promise<string> {
46
- return (await this.body()).toString("utf8");
47
- }
48
-
49
- /** The body parsed as JSON */
50
- public async json<T = unknown>(): Promise<T> {
51
- const text = await this.text();
52
- try {
53
- return JSON.parse(text) as T;
54
- } catch (err) {
55
- throw new Error(`Response body of ${this.url} is not JSON: ${(err as Error).message}`);
56
- }
57
- }
58
- }
@@ -1,260 +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 type Page from "../browser/page.js";
7
- import { callFunction } from "../script/call-function.js";
8
- import type { BiDiConnector } from "../transport/bidi-connection.js";
9
-
10
- let browser: Browser;
11
- let page: Page;
12
- let server: Server;
13
- let base: string;
14
- const sockets = new Set<Socket>();
15
- /** What reached the server: method, url and request headers */
16
- let hits: Array<{ method: string; url: string; headers: Record<string, string | string[] | undefined>; body: string }> = [];
17
-
18
- const GIF = Buffer.from("R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", "base64");
19
-
20
- before(async () => {
21
- server = createServer((req, res) => {
22
- const chunks: Buffer[] = [];
23
- req.on("data", (chunk) => chunks.push(chunk));
24
- req.on("end", () => {
25
- const path = (req.url ?? "").split("?")[0];
26
- // The browser asks for a favicon after a navigation; that is not what the tests look at
27
- if (path !== "/favicon.ico") {
28
- hits.push({ method: req.method ?? "", url: req.url ?? "", headers: req.headers, body: Buffer.concat(chunks).toString() });
29
- }
30
- switch (path) {
31
- case "/api/users":
32
- res.setHeader("content-type", "application/json").end(JSON.stringify([{ id: 99, from: "server" }]));
33
- break;
34
- case "/echo":
35
- res.setHeader("content-type", "application/json").end(JSON.stringify({ method: req.method, headers: req.headers, body: Buffer.concat(chunks).toString() }));
36
- break;
37
- case "/img.gif":
38
- res.setHeader("content-type", "image/gif").end(GIF);
39
- break;
40
- case "/redirect":
41
- res.writeHead(302, { location: "/api/users" }).end();
42
- break;
43
- case "/frame":
44
- res.setHeader("content-type", "text/html").end('<body>frame<script>fetch("/api/users").then((r) => r.json()).then((d) => parent.postMessage(JSON.stringify(d), "*"))</script></body>');
45
- break;
46
- case "/real":
47
- res.setHeader("content-type", "text/html").end("<!doctype html><body>the real page</body>");
48
- break;
49
- default:
50
- res.setHeader("content-type", "text/plain").end("ok");
51
- }
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: 9241, 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.unrouteAll();
74
- hits = [];
75
- });
76
-
77
- const OPTIONS = { timeout: 30000 };
78
- const connector = () => (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
79
- const run = <T>(source: string) => callFunction<T>(connector(), page.contextId, source);
80
- const fetchJson = (path: string, init = "{}") =>
81
- run<{ status: number; type: string | null; body: unknown }>(`async () => {
82
- const response = await fetch(${JSON.stringify(base + path)}, ${init});
83
- return { status: response.status, type: response.headers.get("content-type"), body: await response.json() };
84
- }`);
85
-
86
- test("fulfill answers a request with JSON, and the server never sees it", OPTIONS, async () => {
87
- await page.route(`${base}/api/users`, (route) => route.fulfill({ json: [{ id: 1, from: "mock" }] }));
88
- const result = await fetchJson("/api/users");
89
- assert.deepEqual(result, { status: 200, type: "application/json", body: [{ id: 1, from: "mock" }] });
90
- assert.deepEqual(hits, []);
91
- });
92
-
93
- test("fulfill sets the status, headers and text of the answer", OPTIONS, async () => {
94
- await page.route("**/teapot", (route) =>
95
- route.fulfill({ status: 418, contentType: "text/plain", headers: { "x-mock": "yes" }, body: "short and stout" }),
96
- );
97
- const result = await run<{ status: number; text: string; mock: string | null; length: string | null }>(`async () => {
98
- const response = await fetch(${JSON.stringify(base + "/teapot")});
99
- return { status: response.status, text: await response.text(), mock: response.headers.get("x-mock"), length: response.headers.get("content-length") };
100
- }`);
101
- assert.deepEqual(result, { status: 418, text: "short and stout", mock: "yes", length: "15" });
102
- });
103
-
104
- test("a Buffer body arrives byte for byte", OPTIONS, async () => {
105
- const bytes = Buffer.from([0, 1, 2, 3, 250, 251, 252, 255]);
106
- await page.route("**/binary", (route) => route.fulfill({ contentType: "application/octet-stream", body: bytes }));
107
- const received = await run<number[]>(`async () => {
108
- const response = await fetch(${JSON.stringify(base + "/binary")});
109
- return Array.from(new Uint8Array(await response.arrayBuffer()));
110
- }`);
111
- assert.deepEqual(received, [...bytes]);
112
- });
113
-
114
- test("routes that do not match leave requests alone", OPTIONS, async () => {
115
- await page.route(`${base}/api/users`, (route) => route.fulfill({ json: [] }));
116
- const other = await fetchJson("/echo");
117
- assert.equal((other.body as { method: string }).method, "GET");
118
- assert.deepEqual(hits.map((hit) => hit.url), ["/echo"]);
119
- });
120
-
121
- test("globs and regular expressions match", OPTIONS, async () => {
122
- await page.route("**/glob/*", (route) => route.fulfill({ json: "glob" }));
123
- await page.route(/\/regex\/\d+$/, (route) => route.fulfill({ json: "regex" }));
124
- assert.equal((await fetchJson("/glob/anything")).body, "glob");
125
- assert.equal((await fetchJson("/regex/42")).body, "regex");
126
- assert.deepEqual(hits, []);
127
- });
128
-
129
- test("abort makes an image fail to load", OPTIONS, async () => {
130
- await page.route(/\.gif\?abort$/, (route) => route.abort());
131
- const outcome = await run<string>(`() => new Promise((resolve) => {
132
- const image = new Image();
133
- image.onload = () => resolve("loaded");
134
- image.onerror = () => resolve("error");
135
- image.src = ${JSON.stringify(base + "/img.gif?abort")};
136
- })`);
137
- assert.equal(outcome, "error");
138
- assert.deepEqual(hits, []);
139
- await page.unrouteAll();
140
- const after = await run<string>(`() => new Promise((resolve) => {
141
- const image = new Image();
142
- image.onload = () => resolve("loaded");
143
- image.onerror = () => resolve("error");
144
- image.src = ${JSON.stringify(base + "/img.gif?ok")};
145
- })`);
146
- assert.equal(after, "loaded", "without the route the image loads");
147
- });
148
-
149
- test("abort makes fetch reject", OPTIONS, async () => {
150
- await page.route("**/api/**", (route) => route.abort());
151
- const outcome = await run<string>(`async () => { try { await fetch(${JSON.stringify(base + "/api/users")}); return "resolved"; } catch { return "rejected"; } }`);
152
- assert.equal(outcome, "rejected");
153
- });
154
-
155
- test("continue adds a header the server sees", OPTIONS, async () => {
156
- await page.route("**/echo", (route) => route.continue({ headers: { ...route.request().headers, "x-test": "1" } }));
157
- const result = await fetchJson("/echo");
158
- assert.equal((result.body as { headers: Record<string, string> }).headers["x-test"], "1");
159
- });
160
-
161
- test("continue can send a navigation to another url", OPTIONS, async () => {
162
- await page.route(`${base}/navigate-me`, (route) => route.continue({ url: `${base}/real` }));
163
- await page.navigateTo(`${base}/navigate-me`);
164
- assert.equal(await page.locator("body").textContent(), "the real page");
165
- assert.deepEqual(hits.map((hit) => hit.url), ["/real"]);
166
- });
167
-
168
- test("continue can change the method", OPTIONS, async () => {
169
- await page.route("**/echo", (route) => route.continue({ method: "POST" }));
170
- const result = await fetchJson("/echo");
171
- assert.equal((result.body as { method: string }).method, "POST");
172
- assert.deepEqual(hits.map((hit) => `${hit.method} ${hit.url}`), ["POST /echo"]);
173
- });
174
-
175
- test("continue can replace the body of a request, also with a longer one", OPTIONS, async () => {
176
- await page.route("**/echo", (route) => route.continue({ postData: "a much longer replacement body" }));
177
- const result = await fetchJson("/echo", `{ method: "POST", body: "short" }`);
178
- assert.equal(result.status, 200);
179
- assert.equal((result.body as { body: string }).body, "a much longer replacement body");
180
- assert.equal(hits[0]!.headers["content-length"], String("a much longer replacement body".length));
181
- });
182
-
183
- test("continue can replace the body with a shorter one too", OPTIONS, async () => {
184
- await page.route("**/echo", (route) => route.continue({ postData: "tiny" }));
185
- const result = await fetchJson("/echo", `{ method: "POST", body: "a rather long original body" }`);
186
- assert.equal((result.body as { body: string }).body, "tiny");
187
- });
188
-
189
- test("a redirect hop is routed on its own", OPTIONS, async () => {
190
- await page.route(`${base}/api/users`, (route) => route.fulfill({ json: "mocked after redirect" }));
191
- const result = await fetchJson("/redirect");
192
- assert.equal(result.body, "mocked after redirect");
193
- assert.deepEqual(hits.map((hit) => hit.url), ["/redirect"], "only the first hop reached the server");
194
- });
195
-
196
- test("a page can be mocked: navigating to a routed url shows the mocked document", OPTIONS, async () => {
197
- await page.route(`${base}/mocked-page`, (route) =>
198
- route.fulfill({ contentType: "text/html", body: "<!doctype html><body id='b'>mocked document</body>" }),
199
- );
200
- await page.navigateTo(`${base}/mocked-page`);
201
- assert.equal(await page.locator("body[@id='b']").textContent(), "mocked document");
202
- assert.deepEqual(hits, []);
203
- await page.navigateTo(`${base}/real`);
204
- });
205
-
206
- test("requests of an iframe are routed too", OPTIONS, async () => {
207
- await page.route(`${base}/api/users`, (route) => route.fulfill({ json: [{ from: "mock in frame" }] }));
208
- const message = await run<string>(`() => new Promise((resolve) => {
209
- window.addEventListener("message", (event) => resolve(event.data), { once: true });
210
- const frame = document.createElement("iframe");
211
- frame.src = ${JSON.stringify(base + "/frame")};
212
- document.body.appendChild(frame);
213
- })`);
214
- assert.deepEqual(JSON.parse(message), [{ from: "mock in frame" }]);
215
- assert.ok(!hits.some((hit) => hit.url === "/api/users"), "the frame's fetch was mocked");
216
- });
217
-
218
- test("a handler that does not answer lets the request through instead of hanging", OPTIONS, async () => {
219
- await page.route("**/echo", () => {});
220
- const result = await fetchJson("/echo");
221
- assert.equal((result.body as { method: string }).method, "GET");
222
- assert.equal(hits.length, 1);
223
- });
224
-
225
- test("fallback passes to an older route; the newest is asked first", OPTIONS, async () => {
226
- await page.route("**/api/users", (route) => route.fulfill({ json: "older" }));
227
- await page.route("**/api/*", (route) => route.fallback());
228
- assert.equal((await fetchJson("/api/users")).body, "older");
229
- await page.route("**/api/users", (route) => route.fulfill({ json: "newest" }));
230
- assert.equal((await fetchJson("/api/users")).body, "newest");
231
- });
232
-
233
- test("unroute stops the mocking", OPTIONS, async () => {
234
- const handler = (route: { fulfill(o: { json: unknown }): Promise<void> }) => route.fulfill({ json: "mocked" });
235
- await page.route(`${base}/api/users`, handler);
236
- assert.equal((await fetchJson("/api/users")).body, "mocked");
237
- await page.unroute(`${base}/api/users`, handler);
238
- assert.deepEqual((await fetchJson("/api/users")).body, [{ id: 99, from: "server" }]);
239
- });
240
-
241
- test("a request held by a handler keeps the network busy until it is answered", OPTIONS, async () => {
242
- await page.route("**/slow-mock", async (route) => {
243
- await new Promise((resolve) => setTimeout(resolve, 400));
244
- await route.fulfill({ json: "late" });
245
- });
246
- await run(`() => { fetch(${JSON.stringify(base + "/slow-mock")}); }`);
247
- const started = Date.now();
248
- await page.waitForNetworkIdle({ idleTime: 100, timeout: 10000 });
249
- assert.ok(Date.now() - started >= 350, `idle came while the request was held: ${Date.now() - started}ms`);
250
- });
251
-
252
- test("a handler that throws fails the request and is reported", OPTIONS, async () => {
253
- await page.route("**/echo", () => {
254
- throw new Error("handler bug");
255
- });
256
- const outcome = await run<string>(`async () => { try { await fetch(${JSON.stringify(base + "/echo")}); return "resolved"; } catch { return "rejected"; } }`);
257
- assert.equal(outcome, "rejected");
258
- assert.equal(page.routeErrors().length, 1);
259
- assert.match(page.routeErrors()[0]!.message, /handler bug/);
260
- });