@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,72 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ /** Paper sizes in centimetres, as the browser wants them */
3
+ declare const PAPER: {
4
+ readonly Letter: {
5
+ readonly width: 21.59;
6
+ readonly height: 27.94;
7
+ };
8
+ readonly Legal: {
9
+ readonly width: 21.59;
10
+ readonly height: 35.56;
11
+ };
12
+ readonly Tabloid: {
13
+ readonly width: 27.94;
14
+ readonly height: 43.18;
15
+ };
16
+ readonly Ledger: {
17
+ readonly width: 43.18;
18
+ readonly height: 27.94;
19
+ };
20
+ readonly A0: {
21
+ readonly width: 84.1;
22
+ readonly height: 118.9;
23
+ };
24
+ readonly A1: {
25
+ readonly width: 59.4;
26
+ readonly height: 84.1;
27
+ };
28
+ readonly A2: {
29
+ readonly width: 42;
30
+ readonly height: 59.4;
31
+ };
32
+ readonly A3: {
33
+ readonly width: 29.7;
34
+ readonly height: 42;
35
+ };
36
+ readonly A4: {
37
+ readonly width: 21;
38
+ readonly height: 29.7;
39
+ };
40
+ readonly A5: {
41
+ readonly width: 14.8;
42
+ readonly height: 21;
43
+ };
44
+ readonly A6: {
45
+ readonly width: 10.5;
46
+ readonly height: 14.8;
47
+ };
48
+ };
49
+ export type PaperFormat = keyof typeof PAPER;
50
+ export interface PdfOptions {
51
+ /** Also write the PDF here (folders are created) */
52
+ path?: string;
53
+ landscape?: boolean;
54
+ /** 0.1 to 2 */
55
+ scale?: number;
56
+ /** Pages to print, e.g. `["1-3", 5]` */
57
+ pageRanges?: (string | number)[];
58
+ /** Page margins in centimetres */
59
+ margin?: {
60
+ top?: number;
61
+ bottom?: number;
62
+ left?: number;
63
+ right?: number;
64
+ };
65
+ /** Print background colours and images */
66
+ printBackground?: boolean;
67
+ /** Paper size. Without one the browser's default is used */
68
+ format?: PaperFormat;
69
+ }
70
+ /** Renders the page as a PDF and returns it (also written to `options.path`) */
71
+ export declare function printPdf(connector: BiDiConnector, context: string, options?: PdfOptions): Promise<Buffer>;
72
+ export {};
@@ -0,0 +1,37 @@
1
+ import { writeOutput } from "./screenshot.js";
2
+ /** Paper sizes in centimetres, as the browser wants them */
3
+ const PAPER = {
4
+ Letter: { width: 21.59, height: 27.94 },
5
+ Legal: { width: 21.59, height: 35.56 },
6
+ Tabloid: { width: 27.94, height: 43.18 },
7
+ Ledger: { width: 43.18, height: 27.94 },
8
+ A0: { width: 84.1, height: 118.9 },
9
+ A1: { width: 59.4, height: 84.1 },
10
+ A2: { width: 42, height: 59.4 },
11
+ A3: { width: 29.7, height: 42 },
12
+ A4: { width: 21, height: 29.7 },
13
+ A5: { width: 14.8, height: 21 },
14
+ A6: { width: 10.5, height: 14.8 },
15
+ };
16
+ /** Renders the page as a PDF and returns it (also written to `options.path`) */
17
+ export async function printPdf(connector, context, options = {}) {
18
+ const { scale, format } = options;
19
+ if (scale !== undefined && !(scale >= 0.1 && scale <= 2)) {
20
+ throw new RangeError("pdf(): scale must be between 0.1 and 2");
21
+ }
22
+ if (format !== undefined && !(format in PAPER)) {
23
+ throw new RangeError(`pdf(): unknown paper format "${format}"; use one of ${Object.keys(PAPER).join(", ")}`);
24
+ }
25
+ const { data } = await connector.send("browsingContext.print", {
26
+ context,
27
+ ...(options.landscape && { orientation: "landscape" }),
28
+ ...(scale !== undefined && { scale }),
29
+ ...(options.pageRanges && { pageRanges: options.pageRanges }),
30
+ ...(options.margin && { margin: options.margin }),
31
+ ...(options.printBackground && { background: true }),
32
+ ...(format && { page: { ...PAPER[format] } }),
33
+ });
34
+ const pdf = Buffer.from(data, "base64");
35
+ await writeOutput(options.path, pdf);
36
+ return pdf;
37
+ }
@@ -0,0 +1,29 @@
1
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
2
+ export interface ScreenshotOptions {
3
+ /** Also write the image here (folders are created) */
4
+ path?: string;
5
+ /** The whole document instead of the viewport */
6
+ fullPage?: boolean;
7
+ /** Only this part: in viewport coordinates, or document coordinates with `fullPage` */
8
+ clip?: {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ };
14
+ /** @default "png" */
15
+ type?: "png" | "jpeg";
16
+ /** 0 to 100, jpeg only */
17
+ quality?: number;
18
+ }
19
+ /** The options a single element's screenshot takes: the image format and where to write it */
20
+ export type ElementScreenshotOptions = Pick<ScreenshotOptions, "path" | "type" | "quality">;
21
+ /**
22
+ * Captures the page (or, with `element`, that element) and returns the image. Writes it to `options.path` too.
23
+ * The browser answers Base64.
24
+ */
25
+ export declare function takeScreenshot(connector: BiDiConnector, context: string, options?: ScreenshotOptions, element?: {
26
+ sharedId: string;
27
+ }): Promise<Buffer>;
28
+ /** Writes `data` to `file`, creating the folders; nothing happens without a path */
29
+ export declare function writeOutput(file: string | undefined, data: Buffer): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const MIME = { png: "image/png", jpeg: "image/jpeg" };
4
+ /** The `format` of a capture command; validates the type/quality combination */
5
+ function imageFormat({ type, quality }) {
6
+ if (quality !== undefined) {
7
+ if (type !== "jpeg")
8
+ throw new RangeError("screenshot(): quality only applies to jpeg");
9
+ if (!Number.isFinite(quality) || quality < 0 || quality > 100) {
10
+ throw new RangeError("screenshot(): quality must be between 0 and 100");
11
+ }
12
+ }
13
+ if (type === undefined)
14
+ return undefined;
15
+ return { type: MIME[type], ...(quality !== undefined && { quality: quality / 100 }) };
16
+ }
17
+ /**
18
+ * Captures the page (or, with `element`, that element) and returns the image. Writes it to `options.path` too.
19
+ * The browser answers Base64.
20
+ */
21
+ export async function takeScreenshot(connector, context, options = {}, element) {
22
+ const format = imageFormat(options);
23
+ const clip = element
24
+ ? { type: "element", element: { sharedId: element.sharedId } }
25
+ : options.clip && { type: "box", ...options.clip };
26
+ const { data } = await connector.send("browsingContext.captureScreenshot", {
27
+ context,
28
+ ...(options.fullPage && { origin: "document" }),
29
+ ...(format && { format }),
30
+ ...(clip && { clip }),
31
+ });
32
+ const image = Buffer.from(data, "base64");
33
+ await writeOutput(options.path, image);
34
+ return image;
35
+ }
36
+ /** Writes `data` to `file`, creating the folders; nothing happens without a path */
37
+ export async function writeOutput(file, data) {
38
+ if (!file)
39
+ return;
40
+ await mkdir(path.dirname(file), { recursive: true });
41
+ await writeFile(file, data);
42
+ }
@@ -0,0 +1,35 @@
1
+ import type { ProjectOptions } from "../runner/run.js";
2
+ /** The values a command line can override for one run */
3
+ type RunOverrides = Pick<ProjectOptions, "environment" | "timeout" | "expectTimeout">;
4
+ /** Flags and positional arguments of a command line; an unknown flag is an error */
5
+ export declare function parseCommandLine(argv: string[]): {
6
+ values: {
7
+ env?: string | undefined;
8
+ timeout?: string | undefined;
9
+ "expect-timeout"?: string | undefined;
10
+ headless?: boolean | undefined;
11
+ port?: string | undefined;
12
+ grep?: string | undefined;
13
+ file?: string[] | undefined;
14
+ json?: boolean | undefined;
15
+ test?: string | undefined;
16
+ at?: string | undefined;
17
+ new?: string | undefined;
18
+ url?: string | undefined;
19
+ help?: boolean | undefined;
20
+ version?: boolean | undefined;
21
+ };
22
+ positionals: string[];
23
+ };
24
+ /** How the command line says tests are selected and the browser starts */
25
+ export interface RunnerFlags {
26
+ headless?: boolean;
27
+ port?: number;
28
+ grep?: string;
29
+ files?: string[];
30
+ }
31
+ /** Reads `--headless`, `--port <n>`, `--grep <text>` and `--file <spec>` (repeatable) from `argv` */
32
+ export declare function parseRunnerFlags(argv: string[]): RunnerFlags;
33
+ /** Reads `--env`, `--timeout` and `--expect-timeout` from `argv`; the environment falls back to SAMURAI_ENV */
34
+ export declare function parseRunOverrides(argv: string[], env?: Partial<NodeJS.ProcessEnv>): RunOverrides;
35
+ export {};
@@ -0,0 +1,69 @@
1
+ import { parseArgs } from "node:util";
2
+ const FLAGS = {
3
+ env: { type: "string" },
4
+ timeout: { type: "string" },
5
+ "expect-timeout": { type: "string" },
6
+ headless: { type: "boolean" },
7
+ port: { type: "string" },
8
+ grep: { type: "string" },
9
+ file: { type: "string", multiple: true },
10
+ json: { type: "boolean" },
11
+ test: { type: "string" },
12
+ at: { type: "string" },
13
+ new: { type: "string" },
14
+ url: { type: "string" },
15
+ help: { type: "boolean", short: "h" },
16
+ version: { type: "boolean", short: "v" },
17
+ };
18
+ /** Flags and positional arguments of a command line; an unknown flag is an error */
19
+ export function parseCommandLine(argv) {
20
+ return parseArgs({
21
+ args: argv,
22
+ options: FLAGS,
23
+ strict: true,
24
+ allowPositionals: true,
25
+ });
26
+ }
27
+ function parseFlags(argv) {
28
+ return parseCommandLine(argv).values;
29
+ }
30
+ /** Reads `--headless`, `--port <n>`, `--grep <text>` and `--file <spec>` (repeatable) from `argv` */
31
+ export function parseRunnerFlags(argv) {
32
+ const values = parseFlags(argv);
33
+ const flags = {};
34
+ if (values.headless)
35
+ flags.headless = true;
36
+ if (values.port !== undefined) {
37
+ const port = Number(values.port);
38
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
39
+ throw new Error(`--port must be a port number between 1 and 65535, got "${values.port}"`);
40
+ }
41
+ flags.port = port;
42
+ }
43
+ if (values.grep !== undefined)
44
+ flags.grep = values.grep;
45
+ if (values.file?.length)
46
+ flags.files = values.file;
47
+ return flags;
48
+ }
49
+ /** Reads `--env`, `--timeout` and `--expect-timeout` from `argv`; the environment falls back to SAMURAI_ENV */
50
+ export function parseRunOverrides(argv, env = {}) {
51
+ const values = parseFlags(argv);
52
+ const overrides = {};
53
+ const environment = values.env ?? env.SAMURAI_ENV;
54
+ if (environment)
55
+ overrides.environment = environment;
56
+ if (values.timeout !== undefined)
57
+ overrides.timeout = milliseconds("--timeout", values.timeout);
58
+ if (values["expect-timeout"] !== undefined) {
59
+ overrides.expectTimeout = milliseconds("--expect-timeout", values["expect-timeout"]);
60
+ }
61
+ return overrides;
62
+ }
63
+ function milliseconds(flag, raw) {
64
+ const value = Number(raw);
65
+ if (raw.trim() === "" || !Number.isInteger(value) || value < 0) {
66
+ throw new Error(`${flag} must be a whole number of milliseconds, got "${raw}"`);
67
+ }
68
+ return value;
69
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { ConsoleReporter } from "./console-reporter.js";
7
+ import { initProject, scaffold } from "./init.js";
8
+ import { main, USAGE } from "./main.js";
9
+ function io() {
10
+ const out = [];
11
+ const err = [];
12
+ const target = {
13
+ out: (t) => out.push(t),
14
+ err: (t) => err.push(t),
15
+ color: false,
16
+ env: {},
17
+ };
18
+ return { target, out: () => out.join(""), err: () => err.join("") };
19
+ }
20
+ describe("main", () => {
21
+ it("prints the usage for --help, -h and help", async () => {
22
+ for (const argv of [["--help"], ["-h"], ["help"], ["run", "--help"]]) {
23
+ const { target, out } = io();
24
+ assert.equal(await main(argv, target), 0);
25
+ assert.equal(out(), USAGE);
26
+ }
27
+ });
28
+ it("prints the version", async () => {
29
+ const { target, out } = io();
30
+ assert.equal(await main(["--version"], target), 0);
31
+ assert.match(out(), /^\d+\.\d+\.\d+/);
32
+ });
33
+ it("exits 2 for an unknown command, with the usage", async () => {
34
+ const { target, err } = io();
35
+ assert.equal(await main(["bogus"], target), 2);
36
+ assert.match(err(), /Unknown command "bogus"/);
37
+ assert.match(err(), /Usage:/);
38
+ });
39
+ it("exits 2 for an unknown flag or a bad value, with a message", async () => {
40
+ const flag = io();
41
+ assert.equal(await main(["run", "--nope"], flag.target), 2);
42
+ assert.match(flag.err(), /nope/);
43
+ const port = io();
44
+ assert.equal(await main(["run", "--port", "abc"], port.target), 2);
45
+ assert.match(port.err(), /--port/);
46
+ });
47
+ it("exits 2 when the project has no config", async () => {
48
+ const dir = mkdtempSync(path.join(tmpdir(), "samurai-cli-"));
49
+ const previous = process.cwd();
50
+ process.chdir(dir);
51
+ try {
52
+ const { target, err } = io();
53
+ assert.equal(await main(["list"], target), 2);
54
+ assert.ok(err().length > 0);
55
+ }
56
+ finally {
57
+ process.chdir(previous);
58
+ }
59
+ });
60
+ it("record needs a spec and a sensible --at", async () => {
61
+ const none = io();
62
+ assert.equal(await main(["record"], none.target), 2);
63
+ assert.match(none.err(), /Which spec/);
64
+ const bad = io();
65
+ assert.equal(await main(["record", "a.spec.ts", "--at", "x"], bad.target), 2);
66
+ assert.match(bad.err(), /--at/);
67
+ });
68
+ it("record refuses a spec that does not exist unless --new is given", async () => {
69
+ const dir = mkdtempSync(path.join(tmpdir(), "samurai-cli-"));
70
+ writeFileSync(path.join(dir, "samurai.config.ts"), "export default {};");
71
+ const previous = process.cwd();
72
+ process.chdir(dir);
73
+ try {
74
+ const { target, err } = io();
75
+ assert.equal(await main(["record", "tests/none.spec.ts"], target), 2);
76
+ assert.match(err(), /does not exist/);
77
+ }
78
+ finally {
79
+ process.chdir(previous);
80
+ }
81
+ });
82
+ it("init scaffolds a project and never overwrites", async () => {
83
+ const dir = mkdtempSync(path.join(tmpdir(), "samurai-init-"));
84
+ writeFileSync(path.join(dir, "package.json"), '{"name":"mine"}');
85
+ const { target, out } = io();
86
+ assert.equal(await main(["init", dir], target), 0);
87
+ assert.match(out(), /kept\s+package\.json/);
88
+ assert.match(out(), /created samurai\.config\.ts/);
89
+ assert.equal(readFileSync(path.join(dir, "package.json"), "utf8"), '{"name":"mine"}');
90
+ for (const file of [
91
+ "samurai.config.ts",
92
+ "tests/example.spec.ts",
93
+ ".env.example",
94
+ ".gitignore",
95
+ ]) {
96
+ assert.ok(existsSync(path.join(dir, file)), file);
97
+ }
98
+ });
99
+ });
100
+ describe("scaffold", () => {
101
+ it("is an ES module project with a test script", () => {
102
+ const manifest = JSON.parse(scaffold("shop")["package.json"]);
103
+ assert.equal(manifest.type, "module");
104
+ assert.equal(manifest.scripts.test, "samurai run");
105
+ assert.equal(manifest.name, "shop");
106
+ });
107
+ it("names the package after the folder, safely", () => {
108
+ const dir = mkdtempSync(path.join(tmpdir(), "My Shop!-"));
109
+ initProject(dir);
110
+ const manifest = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
111
+ assert.match(manifest.name, /^[a-z0-9-]+$/);
112
+ });
113
+ });
114
+ describe("ConsoleReporter", () => {
115
+ const summary = {
116
+ status: "failed",
117
+ duration: 2300,
118
+ startTime: 0,
119
+ environment: "staging",
120
+ tests: [
121
+ {
122
+ name: "a",
123
+ file: "/x/a.spec.ts",
124
+ startTime: 0,
125
+ status: "success",
126
+ duration: 1000,
127
+ },
128
+ {
129
+ name: "b > c",
130
+ file: "/x/a.spec.ts",
131
+ startTime: 0,
132
+ status: "failed",
133
+ duration: 1300,
134
+ type: "error",
135
+ message: "boom\nsecond line",
136
+ },
137
+ ],
138
+ };
139
+ it("prints a line per test, the failures in full and a count", () => {
140
+ let text = "";
141
+ const reporter = new ConsoleReporter({
142
+ write: (t) => (text += t),
143
+ color: false,
144
+ });
145
+ reporter.handle({ type: "run-start", environment: "staging", total: 2 });
146
+ reporter.handle({ type: "test-start", name: "a", file: "/x/a.spec.ts" });
147
+ for (const result of summary.tests)
148
+ reporter.handle({
149
+ type: "test-end",
150
+ name: "name" in result ? result.name : "",
151
+ file: "/x/a.spec.ts",
152
+ result,
153
+ });
154
+ reporter.handle({ type: "run-end", summary });
155
+ assert.match(text, /Running 2 tests against staging/);
156
+ assert.match(text, /✔ a \(1\.0s\)/);
157
+ assert.match(text, /✖ b > c \(1\.3s\)/);
158
+ assert.match(text, /\n {4}boom\n {4}second line\n/);
159
+ assert.match(text, /1 failed, 1 passed \(2\.3s\)/);
160
+ assert.ok(!text.includes("\u001b"), "no colour codes when colour is off");
161
+ });
162
+ it("colours when asked to", () => {
163
+ let text = "";
164
+ new ConsoleReporter({ write: (t) => (text += t), color: true }).handle({
165
+ type: "run-end",
166
+ summary,
167
+ });
168
+ assert.ok(text.includes("\u001b[31m"));
169
+ });
170
+ });
@@ -0,0 +1,15 @@
1
+ import type { RunEvent } from "../runner/run.js";
2
+ export interface ConsoleReporterOptions {
3
+ write: (text: string) => void;
4
+ /** Colour with ANSI codes */
5
+ color: boolean;
6
+ }
7
+ /** Prints a run for a person: one line per test as it ends, then every failure in full and a count */
8
+ export declare class ConsoleReporter {
9
+ private readonly write;
10
+ private readonly paint;
11
+ constructor({ write, color }: ConsoleReporterOptions);
12
+ handle(event: RunEvent): void;
13
+ private line;
14
+ private summary;
15
+ }
@@ -0,0 +1,52 @@
1
+ const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
2
+ /** Prints a run for a person: one line per test as it ends, then every failure in full and a count */
3
+ export class ConsoleReporter {
4
+ constructor({ write, color }) {
5
+ this.write = write;
6
+ this.paint = (code, text) => color ? `\u001b[${code}m${text}\u001b[0m` : text;
7
+ }
8
+ handle(event) {
9
+ switch (event.type) {
10
+ case "run-start":
11
+ this.write(`Running ${event.total} ${event.total === 1 ? "test" : "tests"} against ${event.environment}\n`);
12
+ return;
13
+ case "test-end":
14
+ this.write(` ${this.line(event.name, event.result)}\n`);
15
+ return;
16
+ case "run-end":
17
+ this.summary(event.summary);
18
+ return;
19
+ case "test-start":
20
+ return;
21
+ }
22
+ }
23
+ line(name, result) {
24
+ const took = "duration" in result
25
+ ? ` ${this.paint(90, `(${seconds(result.duration)})`)}`
26
+ : "";
27
+ return result.status === "success"
28
+ ? `${this.paint(32, "✔")} ${name}${took}`
29
+ : `${this.paint(31, "✖")} ${name}${took}`;
30
+ }
31
+ summary(summary) {
32
+ const failed = summary.tests.filter((test) => test.status !== "success");
33
+ for (const test of failed) {
34
+ if (!("name" in test))
35
+ continue;
36
+ const message = "message" in test ? test.message : "did not finish";
37
+ this.write(`\n${this.paint(31, `${test.name}`)}\n`);
38
+ this.write(`${message
39
+ .split("\n")
40
+ .map((line) => ` ${line}`)
41
+ .join("\n")}\n`);
42
+ if ("file" in test)
43
+ this.write(` ${this.paint(90, test.file)}\n`);
44
+ }
45
+ const passed = summary.tests.length - failed.length;
46
+ const parts = [
47
+ failed.length > 0 && this.paint(31, `${failed.length} failed`),
48
+ this.paint(32, `${passed} passed`),
49
+ ].filter(Boolean);
50
+ this.write(`\n${parts.join(", ")} ${this.paint(90, `(${seconds(summary.duration)})`)}\n`);
51
+ }
52
+ }
@@ -0,0 +1,7 @@
1
+ /** The files `samurai init` writes, by path relative to the project folder */
2
+ export declare function scaffold(name: string): Record<string, string>;
3
+ /** Writes the scaffold into `dir`, never overwriting a file. Returns what was written and what was left alone */
4
+ export declare function initProject(dir: string): {
5
+ written: string[];
6
+ skipped: string[];
7
+ };
@@ -0,0 +1,61 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ const PACKAGE = "@itmetsam/samurai-framework";
4
+ /** The files `samurai init` writes, by path relative to the project folder */
5
+ export function scaffold(name) {
6
+ return {
7
+ "package.json": JSON.stringify({
8
+ name,
9
+ version: "0.1.0",
10
+ private: true,
11
+ type: "module",
12
+ scripts: { test: "samurai run" },
13
+ dependencies: { [PACKAGE]: "^0.1.0" },
14
+ devDependencies: { tsx: "^4.21.0" },
15
+ }, null, 2) + "\n",
16
+ "samurai.config.ts": `import { defineConfig } from "${PACKAGE}";
17
+
18
+ export default defineConfig({
19
+ srcDir: "./tests",
20
+ browser: "firefox",
21
+ timeout: 30000,
22
+ environments: {
23
+ local: { baseURL: "http://localhost:3000" },
24
+ },
25
+ defaultEnvironment: "local",
26
+ });
27
+ `,
28
+ "tests/example.spec.ts": `import { expect, test } from "${PACKAGE}";
29
+
30
+ test("example.com has its heading", async ({ page }) => {
31
+ await page.goto("https://example.com");
32
+ await expect(page.getByRole("heading", { name: "Example Domain" })).toBeVisible();
33
+ });
34
+ `,
35
+ ".env.example": `# Copy to .env.<environment>, e.g. .env.local. Never commit real values.
36
+ # Tests read these as secrets.TEST_PASSWORD; real environment variables win over this file.
37
+ SAMURAI_SECRET_TEST_PASSWORD=
38
+ `,
39
+ ".gitignore": "node_modules\nresult\nlogs\nbrowsers\n.env\n.env.*\n!.env.example\n",
40
+ };
41
+ }
42
+ /** Writes the scaffold into `dir`, never overwriting a file. Returns what was written and what was left alone */
43
+ export function initProject(dir) {
44
+ const root = path.resolve(dir);
45
+ const written = [];
46
+ const skipped = [];
47
+ for (const [file, content] of Object.entries(scaffold(path
48
+ .basename(root)
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9-]+/g, "-") || "samurai-tests"))) {
51
+ const target = path.join(root, file);
52
+ if (existsSync(target)) {
53
+ skipped.push(file);
54
+ continue;
55
+ }
56
+ mkdirSync(path.dirname(target), { recursive: true });
57
+ writeFileSync(target, content);
58
+ written.push(file);
59
+ }
60
+ return { written, skipped };
61
+ }
@@ -0,0 +1,12 @@
1
+ export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless Run the browser without a window\n --port <n> Browser debugging port (default 9223)\n\nOptions for record (also --env, --port, --json):\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\nRun it in the project folder, the one holding samurai.config.ts.\n";
2
+ export interface Io {
3
+ out: (text: string) => void;
4
+ err: (text: string) => void;
5
+ /** Whether output goes to a terminal that shows colours */
6
+ color: boolean;
7
+ env: Partial<NodeJS.ProcessEnv>;
8
+ /** Aborts a recording (Ctrl+C) */
9
+ signal?: AbortSignal;
10
+ }
11
+ /** The command line as `samurai` runs it. Returns the exit code */
12
+ export declare function main(argv: string[], io: Io): Promise<number>;