@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
@@ -0,0 +1,113 @@
1
+ function nonEmpty(value, what) {
2
+ if (value.trim() === "")
3
+ throw new TypeError(`${what} must not be empty`);
4
+ return value;
5
+ }
6
+ export function xpathSelector(value) {
7
+ return { kind: "xpath", value: nonEmpty(value, "an xpath") };
8
+ }
9
+ export function cssSelector(value) {
10
+ return { kind: "css", value: nonEmpty(value, "a css selector") };
11
+ }
12
+ export function textSelector(value, { match = "full", ignoreCase = false } = {}) {
13
+ return {
14
+ kind: "text",
15
+ value: nonEmpty(value, "the text"),
16
+ match,
17
+ ignoreCase,
18
+ };
19
+ }
20
+ export function labelSelector(value, { match = "full", ignoreCase = false } = {}) {
21
+ return {
22
+ kind: "label",
23
+ value: nonEmpty(value, "the label"),
24
+ match,
25
+ ignoreCase,
26
+ };
27
+ }
28
+ export function testIdSelector(value) {
29
+ return { kind: "testid", value: nonEmpty(value, "the test id") };
30
+ }
31
+ export function roleSelector(role, { name } = {}) {
32
+ nonEmpty(role, "the role");
33
+ return name === undefined
34
+ ? { kind: "role", role }
35
+ : { kind: "role", role, name };
36
+ }
37
+ /**
38
+ * Normalises an xpath. Relative xpaths get a leading `//` (`.//` when `scoped`, i.e. searching inside
39
+ * a parent match). Absolute (`/…`), grouped (`(…)`) and context (`./…`) paths are kept; in a scoped
40
+ * position `//…` is also made relative, so `parent.locator("//x")` means "x inside parent".
41
+ */
42
+ export function normalizeXpath(xpath, scoped) {
43
+ if (scoped) {
44
+ if (xpath.startsWith("//"))
45
+ return "." + xpath;
46
+ return /^[/(.]/.test(xpath) ? xpath : ".//" + xpath;
47
+ }
48
+ return /^[/(.]/.test(xpath) ? xpath : "//" + xpath;
49
+ }
50
+ /** The BiDi locator for a step. `scoped` is true for every step after the first */
51
+ export function toBiDiLocator(selector, scoped) {
52
+ switch (selector.kind) {
53
+ case "xpath":
54
+ return { type: "xpath", value: normalizeXpath(selector.value, scoped) };
55
+ case "css":
56
+ return { type: "css", value: selector.value };
57
+ case "text":
58
+ return {
59
+ type: "innerText",
60
+ value: selector.value,
61
+ matchType: selector.match,
62
+ ignoreCase: selector.ignoreCase,
63
+ };
64
+ case "role":
65
+ return {
66
+ type: "accessibility",
67
+ value: {
68
+ ...(selector.role !== undefined && { role: selector.role }),
69
+ ...(selector.name !== undefined && { name: selector.name }),
70
+ },
71
+ };
72
+ case "testid":
73
+ return {
74
+ type: "css",
75
+ value: `[data-testid="${selector.value.replace(/["\\]/g, "\\$&")}"]`,
76
+ };
77
+ case "label":
78
+ throw new Error("label is resolved by the framework, not by the browser");
79
+ case "nth":
80
+ throw new Error("nth is resolved by the framework, not by the browser");
81
+ }
82
+ }
83
+ /** A short name for error messages */
84
+ export function describeSelector(selector, scoped) {
85
+ switch (selector.kind) {
86
+ case "xpath":
87
+ return normalizeXpath(selector.value, scoped);
88
+ case "css":
89
+ return `css=${selector.value}`;
90
+ case "label":
91
+ case "text": {
92
+ const flags = [
93
+ selector.match === "partial" && "partial",
94
+ selector.ignoreCase && "ignoring case",
95
+ ].filter(Boolean);
96
+ return `${selector.kind}=${JSON.stringify(selector.value)}${flags.length ? ` (${flags.join(", ")})` : ""}`;
97
+ }
98
+ case "testid":
99
+ return `testid=${JSON.stringify(selector.value)}`;
100
+ case "role":
101
+ return (`role=${selector.role ?? ""}` +
102
+ (selector.name === undefined
103
+ ? ""
104
+ : `[name=${JSON.stringify(selector.name)}]`));
105
+ case "nth":
106
+ return `nth=${selector.index}`;
107
+ }
108
+ }
109
+ export function describeChain(chain) {
110
+ return chain
111
+ .map((selector, index) => describeSelector(selector, index > 0))
112
+ .join(" >> ");
113
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Function declaration run in the page as `(starts, value, match, ignoreCase)`; returns the elements
3
+ * whose text matches. Used when the browser does not implement BiDi's `innerText` locator
4
+ * (Firefox does not): same idea, done by the framework.
5
+ *
6
+ * - Searches the descendants of every start node (the whole document when there are none).
7
+ * - Text is `innerText`, whitespace collapsed and trimmed; `full` needs equality, `partial` a substring.
8
+ * - Only the innermost matches count, so a wrapper `<div>` around a matching `<button>` is not returned.
9
+ * - Results are unique and in document order.
10
+ *
11
+ * A string, so the transpiler cannot inject helpers that do not exist in the page.
12
+ */
13
+ export declare const TEXT_LOCATE = "(starts, value, match, ignoreCase) => {\n const skipped = new Set([\"SCRIPT\", \"STYLE\", \"NOSCRIPT\", \"TEMPLATE\", \"HEAD\", \"TITLE\", \"META\", \"LINK\"]);\n const normalise = (text) => {\n const collapsed = String(text).replace(/\\s+/g, \" \").trim();\n return ignoreCase ? collapsed.toLowerCase() : collapsed;\n };\n const wanted = normalise(value);\n const roots = starts.length > 0 ? starts : [document];\n const seen = new Set();\n const matches = [];\n for (const root of roots) {\n for (const el of root.querySelectorAll(\"*\")) {\n if (seen.has(el) || skipped.has(el.tagName.toUpperCase())) continue;\n seen.add(el);\n const text = normalise(el.innerText ?? el.textContent ?? \"\");\n if (match === \"full\" ? text === wanted : text.includes(wanted)) matches.push(el);\n }\n }\n const hasMatchingDescendant = new Set();\n for (const el of matches) {\n for (let parent = el.parentElement; parent; parent = parent.parentElement) hasMatchingDescendant.add(parent);\n }\n return matches\n .filter((el) => !hasMatchingDescendant.has(el))\n .sort((a, b) => (a.compareDocumentPosition(b) & 4 ? -1 : 1));\n}";
@@ -0,0 +1,2 @@
1
+ declare const logger: import("winston").Logger;
2
+ export default logger;
@@ -0,0 +1,30 @@
1
+ import { createLogger, format, transports } from "winston";
2
+ import { maskFormat } from "../config/mask.js";
3
+ const logger = createLogger({
4
+ level: "debug",
5
+ format: format.combine(format.timestamp({
6
+ format: "DD-MM-YYYY HH:mm:ss",
7
+ }), format.errors({ stack: true }), format.splat(), format.metadata({ key: "meta" }), maskFormat(), format.json({ space: 2 })),
8
+ defaultMeta: { service: "samurai-framework" },
9
+ transports: [
10
+ //
11
+ // - Write to all logs with level `info` and below to `quick-start-combined.log`.
12
+ // - Write all logs error (and below) to `quick-start-error.log`.
13
+ //
14
+ new transports.Console({
15
+ // The `samurai` command quiets this to "warn"; the log files keep everything
16
+ level: process.env.SAMURAI_LOG_LEVEL ?? "debug",
17
+ // stdout stays free for a command's own output (`samurai run --json`)
18
+ stderrLevels: ["error", "warn", "info", "verbose", "debug"],
19
+ format: format.combine(format.timestamp(), format.errors({ stack: true }), format.printf(({ level, message, meta }) => {
20
+ return `${level.toUpperCase()}: ${message}${meta ? `\n${JSON.stringify(meta)}` : null}`;
21
+ })),
22
+ }),
23
+ new transports.File({
24
+ filename: `logs/error.log`,
25
+ level: "error",
26
+ }),
27
+ new transports.File({ filename: `logs/combined.log` }),
28
+ ],
29
+ });
30
+ export default logger;
@@ -0,0 +1,26 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ /** The body of a response cannot be read */
3
+ export declare class ResponseBodyUnavailableError extends Error {
4
+ readonly url: string;
5
+ readonly reason: string;
6
+ constructor(url: string, reason: string);
7
+ }
8
+ /**
9
+ * A browser only keeps response bodies for a request when a data collector was registered before the request
10
+ * started. This registers one for a page and reads bodies through it.
11
+ */
12
+ export declare class DataCollector {
13
+ private connector;
14
+ private id;
15
+ private disposed;
16
+ private constructor();
17
+ static start(connector: BiDiConnector, contextId: string, options?: {
18
+ maxBodySize?: number;
19
+ }): Promise<DataCollector>;
20
+ /** The body of the response to request `requestId`; `url` only names it in errors */
21
+ read(requestId: string, url: string): Promise<Buffer>;
22
+ /** Lets the browser drop its copy of a body that has been read. Never throws */
23
+ release(requestId: string): Promise<void>;
24
+ /** Idempotent */
25
+ dispose(): Promise<void>;
26
+ }
@@ -0,0 +1,77 @@
1
+ import { BiDiError } from "../transport/bidi-error.js";
2
+ /** How large a response body may be to be kept: 10 MiB */
3
+ const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
4
+ /** The body of a response cannot be read */
5
+ export class ResponseBodyUnavailableError extends Error {
6
+ constructor(url, reason) {
7
+ super(`Response body of ${url} is unavailable: ${reason}`);
8
+ this.name = "ResponseBodyUnavailableError";
9
+ this.url = url;
10
+ this.reason = reason;
11
+ }
12
+ }
13
+ /**
14
+ * A browser only keeps response bodies for a request when a data collector was registered before the request
15
+ * started. This registers one for a page and reads bodies through it.
16
+ */
17
+ export class DataCollector {
18
+ constructor(connector, id) {
19
+ this.connector = connector;
20
+ this.id = id;
21
+ this.disposed = false;
22
+ }
23
+ static async start(connector, contextId, options = {}) {
24
+ const { collector } = await connector.send("network.addDataCollector", {
25
+ dataTypes: ["response"],
26
+ maxEncodedDataSize: options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE,
27
+ contexts: [contextId],
28
+ });
29
+ return new DataCollector(connector, collector);
30
+ }
31
+ /** The body of the response to request `requestId`; `url` only names it in errors */
32
+ async read(requestId, url) {
33
+ try {
34
+ const { bytes } = await this.connector.send("network.getData", {
35
+ dataType: "response",
36
+ request: requestId,
37
+ collector: this.id,
38
+ });
39
+ return bytes.type === "base64" ? Buffer.from(bytes.value, "base64") : Buffer.from(bytes.value, "utf8");
40
+ }
41
+ catch (err) {
42
+ if (err instanceof BiDiError && err.code === "no such network data") {
43
+ throw new ResponseBodyUnavailableError(url, "no body was collected: the collector started after the request, or the response is larger than maxBodySize");
44
+ }
45
+ if (err instanceof BiDiError && err.code === "unavailable network data") {
46
+ throw new ResponseBodyUnavailableError(url, "the body is not available yet");
47
+ }
48
+ throw err;
49
+ }
50
+ }
51
+ /** Lets the browser drop its copy of a body that has been read. Never throws */
52
+ async release(requestId) {
53
+ try {
54
+ await this.connector.send("network.disownData", {
55
+ dataType: "response",
56
+ collector: this.id,
57
+ request: requestId,
58
+ });
59
+ }
60
+ catch {
61
+ // Already gone: nothing left to free
62
+ }
63
+ }
64
+ /** Idempotent */
65
+ async dispose() {
66
+ if (this.disposed)
67
+ return;
68
+ this.disposed = true;
69
+ try {
70
+ await this.connector.send("network.removeDataCollector", { collector: this.id });
71
+ }
72
+ catch (err) {
73
+ if (!(err instanceof BiDiError && err.code === "no such network collector"))
74
+ throw err;
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,94 @@
1
+ import type { ContextTree } from "../browser/context-tree.js";
2
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
3
+ import type { Header, RequestData } from "../types/bidi-modules/network.js";
4
+ import { WaitTimeoutError } from "../wait/wait-until.js";
5
+ export interface NetworkRequest {
6
+ /** The browser's id; the same for every hop of a redirect chain */
7
+ id: string;
8
+ url: string;
9
+ method: string;
10
+ /** Header names lower-cased; repeated headers joined with ", " */
11
+ headers: Record<string, string>;
12
+ /** "document", "script", "image", …; falls back to the initiator type, then "other" */
13
+ resourceType: string;
14
+ navigation: string | null;
15
+ /** The request this one is the redirect of, or null */
16
+ redirectedFrom: NetworkRequest | null;
17
+ /** True while a network intercept holds it */
18
+ isBlocked: boolean;
19
+ }
20
+ export interface NetworkResponse {
21
+ url: string;
22
+ status: number;
23
+ statusText: string;
24
+ headers: Record<string, string>;
25
+ fromCache: boolean;
26
+ request: NetworkRequest;
27
+ }
28
+ export type FailedRequest = NetworkRequest & {
29
+ errorText: string;
30
+ };
31
+ interface EventMap {
32
+ request: [NetworkRequest];
33
+ response: [NetworkResponse];
34
+ requestfailed: [FailedRequest];
35
+ }
36
+ export type NetworkEventName = keyof EventMap;
37
+ export interface TrackerOptions {
38
+ /** Requests it returns true for are neither counted nor emitted. Default: none */
39
+ ignore?: (request: NetworkRequest) => boolean;
40
+ }
41
+ /** waitForIdle ran out of time with requests still in flight; `last` holds (up to five of) their URLs */
42
+ export declare class NetworkIdleTimeoutError extends WaitTimeoutError<string[]> {
43
+ constructor(timeout: number, inflight: number, urls: string[]);
44
+ }
45
+ /**
46
+ * Follows the network activity of one page (its frames included): which requests are in flight,
47
+ * and `request` / `response` / `requestfailed` events. In-flight is keyed by `request id:redirect count`,
48
+ * so a redirect chain is one live request at a time.
49
+ */
50
+ export declare class NetworkTracker {
51
+ private tree;
52
+ private contextId;
53
+ private ignore;
54
+ private connector;
55
+ private events;
56
+ private active;
57
+ /** Requests that may still be the source of a redirect, by hop key */
58
+ private hops;
59
+ private waiters;
60
+ private disposed;
61
+ private constructor();
62
+ static start(connector: BiDiConnector, tree: ContextTree, contextId: string, options?: TrackerOptions): Promise<NetworkTracker>;
63
+ private subscription;
64
+ /** Requests currently in flight */
65
+ get inflight(): number;
66
+ /** URLs of the requests currently in flight, oldest first */
67
+ get inflightUrls(): string[];
68
+ /** Waits that have not finished yet */
69
+ get waiting(): number;
70
+ on<E extends NetworkEventName>(event: E, listener: (...args: EventMap[E]) => void): void;
71
+ off<E extends NetworkEventName>(event: E, listener: (...args: EventMap[E]) => void): void;
72
+ /**
73
+ * Resolves once no request has been in flight for `idleTime` ms. A quiet page resolves after `idleTime`.
74
+ * Rejects with NetworkIdleTimeoutError after `timeout` ms, or with "page closed" when disposed.
75
+ */
76
+ waitForIdle({ idleTime, timeout }: {
77
+ idleTime: number;
78
+ timeout: number;
79
+ }): Promise<void>;
80
+ /** Stops tracking, unsubscribes and rejects pending waits. Idempotent */
81
+ dispose(): Promise<void>;
82
+ private removeListeners;
83
+ private owns;
84
+ private static key;
85
+ private onBeforeRequestSent;
86
+ private onResponseCompleted;
87
+ private onFetchError;
88
+ /** A listener that throws must not stop the tracker or the other listeners */
89
+ private emit;
90
+ }
91
+ export declare function toRequest(data: RequestData, navigation: string | null, isBlocked: boolean, redirectedFrom: NetworkRequest | null): NetworkRequest;
92
+ /** Header list to a lower-cased record; repeated names are joined, base64 values decoded */
93
+ export declare function toHeaders(headers: Header[]): Record<string, string>;
94
+ export {};
@@ -0,0 +1,198 @@
1
+ import EventEmitter from "node:events";
2
+ import logger from "../logger/index.js";
3
+ import { WaitTimeoutError } from "../wait/wait-until.js";
4
+ /** How many URLs a timeout message names */
5
+ const URLS_IN_MESSAGE = 5;
6
+ /** waitForIdle ran out of time with requests still in flight; `last` holds (up to five of) their URLs */
7
+ export class NetworkIdleTimeoutError extends WaitTimeoutError {
8
+ constructor(timeout, inflight, urls) {
9
+ super(timeout, urls);
10
+ this.name = "NetworkIdleTimeoutError";
11
+ this.message = `waitForNetworkIdle(): still ${inflight} request(s) in flight after ${timeout}ms: ${urls.join(", ")}`;
12
+ }
13
+ }
14
+ /**
15
+ * Follows the network activity of one page (its frames included): which requests are in flight,
16
+ * and `request` / `response` / `requestfailed` events. In-flight is keyed by `request id:redirect count`,
17
+ * so a redirect chain is one live request at a time.
18
+ */
19
+ export class NetworkTracker {
20
+ constructor(tree, contextId, ignore, connector) {
21
+ this.tree = tree;
22
+ this.contextId = contextId;
23
+ this.ignore = ignore;
24
+ this.connector = connector;
25
+ this.events = new EventEmitter();
26
+ this.active = new Map();
27
+ /** Requests that may still be the source of a redirect, by hop key */
28
+ this.hops = new Map();
29
+ this.waiters = new Set();
30
+ this.disposed = false;
31
+ this.onBeforeRequestSent = (params) => {
32
+ if (!this.owns(params.context))
33
+ return;
34
+ const key = NetworkTracker.key(params.request, params.redirectCount);
35
+ const redirectCount = Number(params.redirectCount);
36
+ const previousKey = `${params.request.request}:${redirectCount - 1}`;
37
+ const redirectedFrom = redirectCount > 0 ? (this.hops.get(previousKey) ?? null) : null;
38
+ this.hops.delete(previousKey);
39
+ const request = toRequest(params.request, params.navigation, params.isBlocked, redirectedFrom);
40
+ if (this.ignore(request))
41
+ return;
42
+ this.hops.set(key, request);
43
+ this.active.set(key, request.url);
44
+ this.emit("request", request);
45
+ this.events.emit("change");
46
+ };
47
+ this.onResponseCompleted = (params) => {
48
+ if (!this.owns(params.context))
49
+ return;
50
+ const key = NetworkTracker.key(params.request, params.redirectCount);
51
+ const request = this.hops.get(key);
52
+ if (!this.active.delete(key) || !request)
53
+ return;
54
+ const { response } = params;
55
+ // A redirect response keeps its request around: the next hop links back to it
56
+ if (response.status < 300 || response.status >= 400)
57
+ this.hops.delete(key);
58
+ this.emit("response", {
59
+ url: response.url,
60
+ status: response.status,
61
+ statusText: response.statusText,
62
+ headers: toHeaders(response.headers),
63
+ fromCache: response.fromCache,
64
+ request,
65
+ });
66
+ this.events.emit("change");
67
+ };
68
+ this.onFetchError = (params) => {
69
+ if (!this.owns(params.context))
70
+ return;
71
+ const key = NetworkTracker.key(params.request, params.redirectCount);
72
+ const request = this.hops.get(key);
73
+ if (!this.active.delete(key) || !request)
74
+ return;
75
+ this.hops.delete(key);
76
+ this.emit("requestfailed", Object.assign(request, { errorText: params.errorText }));
77
+ this.events.emit("change");
78
+ };
79
+ }
80
+ static async start(connector, tree, contextId, options = {}) {
81
+ const tracker = new NetworkTracker(tree, contextId, options.ignore ?? (() => false), connector);
82
+ // Listen first, then subscribe: nothing can arrive in between
83
+ connector.onEvent("network.beforeRequestSent", tracker.onBeforeRequestSent);
84
+ connector.onEvent("network.responseCompleted", tracker.onResponseCompleted);
85
+ connector.onEvent("network.fetchError", tracker.onFetchError);
86
+ try {
87
+ tracker.subscription = await connector.subscribe([
88
+ "network.beforeRequestSent",
89
+ "network.responseCompleted",
90
+ "network.fetchError",
91
+ ]);
92
+ }
93
+ catch (err) {
94
+ tracker.removeListeners();
95
+ throw err;
96
+ }
97
+ return tracker;
98
+ }
99
+ /** Requests currently in flight */
100
+ get inflight() {
101
+ return this.active.size;
102
+ }
103
+ /** URLs of the requests currently in flight, oldest first */
104
+ get inflightUrls() {
105
+ return [...this.active.values()];
106
+ }
107
+ /** Waits that have not finished yet */
108
+ get waiting() {
109
+ return this.waiters.size;
110
+ }
111
+ on(event, listener) {
112
+ this.events.on(event, listener);
113
+ }
114
+ off(event, listener) {
115
+ this.events.off(event, listener);
116
+ }
117
+ /**
118
+ * Resolves once no request has been in flight for `idleTime` ms. A quiet page resolves after `idleTime`.
119
+ * Rejects with NetworkIdleTimeoutError after `timeout` ms, or with "page closed" when disposed.
120
+ */
121
+ waitForIdle({ idleTime, timeout }) {
122
+ return new Promise((resolve, reject) => {
123
+ let idleTimer;
124
+ const waiter = { reject: (reason) => finish(() => reject(reason)) };
125
+ const finish = (settle) => {
126
+ clearTimeout(idleTimer);
127
+ clearTimeout(deadline);
128
+ this.events.off("change", check);
129
+ this.waiters.delete(waiter);
130
+ settle();
131
+ };
132
+ const check = () => {
133
+ clearTimeout(idleTimer);
134
+ if (this.active.size === 0)
135
+ idleTimer = setTimeout(() => finish(resolve), idleTime);
136
+ };
137
+ const deadline = setTimeout(() => finish(() => reject(new NetworkIdleTimeoutError(timeout, this.active.size, this.inflightUrls.slice(0, URLS_IN_MESSAGE)))), timeout);
138
+ this.waiters.add(waiter);
139
+ this.events.on("change", check);
140
+ check();
141
+ });
142
+ }
143
+ /** Stops tracking, unsubscribes and rejects pending waits. Idempotent */
144
+ async dispose() {
145
+ if (this.disposed)
146
+ return;
147
+ this.disposed = true;
148
+ this.removeListeners();
149
+ this.active.clear();
150
+ this.hops.clear();
151
+ [...this.waiters].forEach((waiter) => waiter.reject(new Error("page closed")));
152
+ await this.subscription?.unsubscribe();
153
+ }
154
+ removeListeners() {
155
+ this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
156
+ this.connector.offEvent("network.responseCompleted", this.onResponseCompleted);
157
+ this.connector.offEvent("network.fetchError", this.onFetchError);
158
+ }
159
+ owns(context) {
160
+ return context !== null && this.tree.isWithin(context, this.contextId);
161
+ }
162
+ static key(data, redirectCount) {
163
+ return `${data.request}:${Number(redirectCount)}`;
164
+ }
165
+ /** A listener that throws must not stop the tracker or the other listeners */
166
+ emit(event, ...args) {
167
+ for (const listener of this.events.listeners(event)) {
168
+ try {
169
+ listener(...args);
170
+ }
171
+ catch (err) {
172
+ logger.error("A network %s listener threw", event, { err });
173
+ }
174
+ }
175
+ }
176
+ }
177
+ export function toRequest(data, navigation, isBlocked, redirectedFrom) {
178
+ return {
179
+ id: data.request,
180
+ url: data.url,
181
+ method: data.method,
182
+ headers: toHeaders(data.headers),
183
+ resourceType: data.destination || data.initiatorType || "other",
184
+ navigation,
185
+ redirectedFrom,
186
+ isBlocked,
187
+ };
188
+ }
189
+ /** Header list to a lower-cased record; repeated names are joined, base64 values decoded */
190
+ export function toHeaders(headers) {
191
+ const result = {};
192
+ for (const { name, value } of headers) {
193
+ const text = value.type === "base64" ? Buffer.from(value.value, "base64").toString("utf8") : value.value;
194
+ const key = name.toLowerCase();
195
+ result[key] = key in result ? `${result[key]}, ${text}` : text;
196
+ }
197
+ return result;
198
+ }
@@ -0,0 +1,16 @@
1
+ import { WaitTimeoutError } from "../wait/wait-until.js";
2
+ import { type UrlPattern } from "./url-match.js";
3
+ /** What `page.waitForRequest` / `waitForResponse` match: a URL pattern, or a function */
4
+ export type NetworkMatch<T> = UrlPattern | ((item: T) => boolean);
5
+ /** No matching request or response arrived in time */
6
+ export declare class NetworkWaitTimeoutError extends WaitTimeoutError<undefined> {
7
+ constructor(operation: "waitForRequest" | "waitForResponse", match: NetworkMatch<never>, timeout: number);
8
+ }
9
+ export declare class RequestTimeoutError extends NetworkWaitTimeoutError {
10
+ }
11
+ export declare class ResponseTimeoutError extends NetworkWaitTimeoutError {
12
+ }
13
+ /** The predicate for a match: a function as it is, a URL pattern tested on the item's url */
14
+ export declare function toPredicate<T extends {
15
+ url: string;
16
+ }>(match: NetworkMatch<T>): (item: T) => boolean;
@@ -0,0 +1,24 @@
1
+ import { WaitTimeoutError } from "../wait/wait-until.js";
2
+ import { matchUrl } from "./url-match.js";
3
+ /** No matching request or response arrived in time */
4
+ export class NetworkWaitTimeoutError extends WaitTimeoutError {
5
+ constructor(operation, match, timeout) {
6
+ super(timeout, undefined);
7
+ this.name = operation === "waitForRequest" ? "RequestTimeoutError" : "ResponseTimeoutError";
8
+ const kind = operation === "waitForRequest" ? "request" : "response";
9
+ this.message = `${operation}(): no ${kind} matching ${describeMatch(match)} within ${timeout}ms`;
10
+ }
11
+ }
12
+ export class RequestTimeoutError extends NetworkWaitTimeoutError {
13
+ }
14
+ export class ResponseTimeoutError extends NetworkWaitTimeoutError {
15
+ }
16
+ function describeMatch(match) {
17
+ if (typeof match === "function")
18
+ return "the given function";
19
+ return typeof match === "string" ? JSON.stringify(match) : String(match);
20
+ }
21
+ /** The predicate for a match: a function as it is, a URL pattern tested on the item's url */
22
+ export function toPredicate(match) {
23
+ return typeof match === "function" ? match : (item) => matchUrl(match, item.url);
24
+ }
@@ -0,0 +1,22 @@
1
+ import type { NetworkRequest, NetworkResponse } from "./network-tracker.js";
2
+ /** A response of the page, with access to its body */
3
+ export declare class Response {
4
+ private loadBody;
5
+ readonly url: string;
6
+ readonly status: number;
7
+ readonly statusText: string;
8
+ /** Header names lower-cased */
9
+ readonly headers: Record<string, string>;
10
+ readonly fromCache: boolean;
11
+ readonly request: NetworkRequest;
12
+ private body_;
13
+ constructor(data: NetworkResponse, loadBody: (response: Response) => Promise<Buffer>);
14
+ /** The browser's id of the request; the same for every hop of a redirect chain */
15
+ get id(): string;
16
+ /** The body as bytes. Read once from the browser, then kept */
17
+ body(): Promise<Buffer>;
18
+ /** The body as UTF-8 text */
19
+ text(): Promise<string>;
20
+ /** The body parsed as JSON */
21
+ json<T = unknown>(): Promise<T>;
22
+ }
@@ -0,0 +1,42 @@
1
+ import { ResponseBodyUnavailableError } from "./data-collector.js";
2
+ /** A response of the page, with access to its body */
3
+ export class Response {
4
+ constructor(data, loadBody) {
5
+ this.loadBody = loadBody;
6
+ this.url = data.url;
7
+ this.status = data.status;
8
+ this.statusText = data.statusText;
9
+ this.headers = data.headers;
10
+ this.fromCache = data.fromCache;
11
+ this.request = data.request;
12
+ }
13
+ /** The browser's id of the request; the same for every hop of a redirect chain */
14
+ get id() {
15
+ return this.request.id;
16
+ }
17
+ /** The body as bytes. Read once from the browser, then kept */
18
+ body() {
19
+ if (this.status >= 300 && this.status < 400 && "location" in this.headers) {
20
+ return Promise.reject(new ResponseBodyUnavailableError(this.url, "redirect response"));
21
+ }
22
+ this.body_ ??= this.loadBody(this).catch((err) => {
23
+ this.body_ = undefined; // let a later call try again
24
+ throw err;
25
+ });
26
+ return this.body_;
27
+ }
28
+ /** The body as UTF-8 text */
29
+ async text() {
30
+ return (await this.body()).toString("utf8");
31
+ }
32
+ /** The body parsed as JSON */
33
+ async json() {
34
+ const text = await this.text();
35
+ try {
36
+ return JSON.parse(text);
37
+ }
38
+ catch (err) {
39
+ throw new Error(`Response body of ${this.url} is not JSON: ${err.message}`);
40
+ }
41
+ }
42
+ }