@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,156 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseCommandLine, parseRunnerFlags, parseRunOverrides } from "./args.js";
3
+ import { recordSpec } from "../recorder/index.js";
4
+ import { listTests, runTests } from "../runner/run.js";
5
+ import { stepToSource } from "../steps/index.js";
6
+ import { ConsoleReporter } from "./console-reporter.js";
7
+ import { initProject } from "./init.js";
8
+ export const USAGE = `samurai: browser tests over WebDriver BiDi
9
+
10
+ Usage:
11
+ samurai run [options] Run the tests (the default command)
12
+ samurai list [options] List the tests without running them
13
+ samurai record <spec> Open a browser and record what you do into a spec file
14
+ samurai init [folder] Scaffold a project: config, an example test, .env.example
15
+
16
+ Options for run and list:
17
+ --env <name> Environment to run against (or SAMURAI_ENV)
18
+ --file <spec> Only this spec file; repeatable
19
+ --grep <text> Only tests whose full name contains the text
20
+ --json Machine-readable output: JSON lines for run, one array for list
21
+
22
+ Options for run:
23
+ --timeout <ms> Time one test may take
24
+ --expect-timeout <ms> Time actions and assertions retry
25
+ --headless Run the browser without a window
26
+ --port <n> Browser debugging port (default 9223)
27
+
28
+ Options for record (also --env, --port, --json):
29
+ --new <title> Add a test with this title (creates the spec file if needed)
30
+ --test <name|index> Record into this test; not needed when the file has one
31
+ --at <n> Insert from step n (default: after the last step)
32
+ --url <url> Page to start on (default: the environment's baseURL)
33
+ Stop with Ctrl+C or by closing the browser window.
34
+
35
+ -h, --help Show this help
36
+ -v, --version Show the version
37
+
38
+ Exit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.
39
+ Run it in the project folder, the one holding samurai.config.ts.
40
+ `;
41
+ function version() {
42
+ const manifest = new URL("../../package.json", import.meta.url);
43
+ return JSON.parse(readFileSync(manifest, "utf8"))
44
+ .version;
45
+ }
46
+ /** The command line as `samurai` runs it. Returns the exit code */
47
+ export async function main(argv, io) {
48
+ try {
49
+ const first = argv[0];
50
+ const command = first !== undefined && !first.startsWith("-") ? first : "run";
51
+ const rest = command === first ? argv.slice(1) : argv;
52
+ const { values, positionals } = parseCommandLine(rest);
53
+ if (values.help || command === "help") {
54
+ io.out(USAGE);
55
+ return 0;
56
+ }
57
+ if (values.version) {
58
+ io.out(`${version()}\n`);
59
+ return 0;
60
+ }
61
+ if (command === "init") {
62
+ const { written, skipped } = initProject(positionals[0] ?? ".");
63
+ for (const file of written)
64
+ io.out(`created ${file}\n`);
65
+ for (const file of skipped)
66
+ io.out(`kept ${file} (already there)\n`);
67
+ io.out("\nNext: install the dependencies, then `samurai run`.\n");
68
+ return 0;
69
+ }
70
+ if (command === "record") {
71
+ const file = positionals[0];
72
+ if (!file)
73
+ throw new Error("Which spec? Usage: samurai record <spec> [--new <title>]");
74
+ const at = values.at === undefined ? undefined : Number(values.at);
75
+ if (at !== undefined && (!Number.isInteger(at) || at < 0))
76
+ throw new Error(`--at must be a step number, got "${values.at}"`);
77
+ const flags = parseRunnerFlags(rest);
78
+ const test = values.test === undefined
79
+ ? undefined
80
+ : /^\d+$/.test(values.test)
81
+ ? Number(values.test)
82
+ : values.test;
83
+ let count = 0;
84
+ const result = await recordSpec({
85
+ ...parseRunOverrides(rest, io.env),
86
+ file,
87
+ ...(test !== undefined && { test }),
88
+ ...(values.new !== undefined && { create: values.new }),
89
+ ...(at !== undefined && { at }),
90
+ ...(values.url !== undefined && { url: values.url }),
91
+ ...(flags.port !== undefined && { port: flags.port }),
92
+ ...(io.signal && { signal: io.signal }),
93
+ onStarted: () => {
94
+ if (!values.json)
95
+ io.out("Recording. Alt+click an element to assert on it. Press Ctrl+C or close the window to stop.\n");
96
+ },
97
+ onEvent: (event) => {
98
+ if (values.json)
99
+ return void io.out(`${JSON.stringify(event)}\n`);
100
+ if (event.op === "insert")
101
+ io.out(` + ${stepToSource(event.step)}\n`);
102
+ else
103
+ io.out(` ~ ${stepToSource(event.step)}\n`);
104
+ if (event.op === "insert")
105
+ count++;
106
+ },
107
+ });
108
+ if (!values.json) {
109
+ io.out(`\nRecorded ${count} ${count === 1 ? "step" : "steps"} into "${result.test}" in ${result.file}\n`);
110
+ if (result.secrets.length > 0) {
111
+ io.out(`Set ${result.secrets.map((name) => `SAMURAI_SECRET_${name}`).join(", ")} (environment variable or .env.<environment>) before running it.\n`);
112
+ }
113
+ }
114
+ return 0;
115
+ }
116
+ if (command !== "run" && command !== "list") {
117
+ io.err(`Unknown command "${command}"\n\n${USAGE}`);
118
+ return 2;
119
+ }
120
+ const flags = parseRunnerFlags(rest);
121
+ const options = {
122
+ ...parseRunOverrides(rest, io.env),
123
+ ...(flags.headless !== undefined && { headless: flags.headless }),
124
+ ...(flags.port !== undefined && { port: flags.port }),
125
+ ...(flags.grep !== undefined && { grep: flags.grep }),
126
+ ...(flags.files && { files: flags.files }),
127
+ };
128
+ if (command === "list") {
129
+ const tests = await listTests(options);
130
+ if (values.json)
131
+ io.out(`${JSON.stringify(tests, null, 2)}\n`);
132
+ else
133
+ for (const found of tests)
134
+ io.out(`${found.name}\n ${found.file}\n`);
135
+ return 0;
136
+ }
137
+ const human = values.json
138
+ ? undefined
139
+ : new ConsoleReporter({ write: io.out, color: io.color });
140
+ const summary = await runTests({
141
+ ...options,
142
+ ...(io.signal && { signal: io.signal }),
143
+ onEvent: (event) => {
144
+ if (values.json)
145
+ io.out(`${JSON.stringify(event)}\n`);
146
+ else
147
+ human?.handle(event);
148
+ },
149
+ });
150
+ return summary.status === "success" ? 0 : 1;
151
+ }
152
+ catch (err) {
153
+ io.err(`${err instanceof Error ? err.message : String(err)}\n`);
154
+ return 2;
155
+ }
156
+ }
@@ -0,0 +1,24 @@
1
+ import type { SamuraiTestConfig } from "../types/config.js";
2
+ /** The project a run belongs to; without one, everything is relative to the working directory */
3
+ interface ActiveProject {
4
+ dir: string;
5
+ /** Used instead of reading `samurai.config.ts` */
6
+ config?: SamuraiTestConfig | undefined;
7
+ /** Where the framework keeps browser profiles; see `browsersDir` */
8
+ dataDir?: string | undefined;
9
+ }
10
+ /** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
11
+ export declare function setActiveProject(project: ActiveProject | undefined): void;
12
+ /** The folder of the project being run: the active project's, else the working directory */
13
+ export declare function projectDir(): string;
14
+ /**
15
+ * Where the framework keeps what it generates for browsers (profile preferences, Firefox app data):
16
+ * `SAMURAI_DATA_DIR`, else the active project's `dataDir`, else `browsers/` in the project folder.
17
+ * An embedding app points it at a folder it may write to.
18
+ */
19
+ export declare function browsersDir(): string;
20
+ /** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
21
+ export declare function loadConfig(): Promise<SamuraiTestConfig>;
22
+ export declare function readConfig<K extends keyof SamuraiTestConfig>(key: K): Promise<SamuraiTestConfig[K]>;
23
+ export declare function defineConfig(config: SamuraiTestConfig): SamuraiTestConfig;
24
+ export {};
@@ -0,0 +1,42 @@
1
+ import path from "path";
2
+ import logger from "../logger/index.js";
3
+ import { pathToFileURL } from "url";
4
+ let activeProject;
5
+ /** Makes `project` the one config, secrets, reports and downloads are resolved for; `undefined` ends it */
6
+ export function setActiveProject(project) {
7
+ activeProject = project && { ...project, dir: path.resolve(project.dir) };
8
+ }
9
+ /** The folder of the project being run: the active project's, else the working directory */
10
+ export function projectDir() {
11
+ return activeProject?.dir ?? process.cwd();
12
+ }
13
+ /**
14
+ * Where the framework keeps what it generates for browsers (profile preferences, Firefox app data):
15
+ * `SAMURAI_DATA_DIR`, else the active project's `dataDir`, else `browsers/` in the project folder.
16
+ * An embedding app points it at a folder it may write to.
17
+ */
18
+ export function browsersDir() {
19
+ return path.resolve(process.env.SAMURAI_DATA_DIR ??
20
+ activeProject?.dataDir ??
21
+ path.join(projectDir(), "browsers"));
22
+ }
23
+ /** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
24
+ export async function loadConfig() {
25
+ if (activeProject?.config)
26
+ return activeProject.config;
27
+ const configUrl = path.join(projectDir(), "samurai.config.ts");
28
+ logger.debug("Reading config from: %s", configUrl);
29
+ const config = (await import(pathToFileURL(configUrl).href));
30
+ if (!config.default) {
31
+ throw new Error("samurai.config.ts does not export defineConfig as default!");
32
+ }
33
+ return config.default;
34
+ }
35
+ export async function readConfig(key) {
36
+ const value = (await loadConfig())[key];
37
+ logger.debug("Resolved config value: ", { value });
38
+ return value;
39
+ }
40
+ export function defineConfig(config) {
41
+ return config;
42
+ }
@@ -0,0 +1,2 @@
1
+ import type { SamuraiTestConfig } from "../types/config.js";
2
+ export declare const config: SamuraiTestConfig;
@@ -0,0 +1,3 @@
1
+ export const config = {
2
+ srcDir: "./src",
3
+ };
@@ -0,0 +1,11 @@
1
+ export declare const MASK = "\u2022\u2022\u2022\u2022";
2
+ /** Registers a secret value; every later maskText/maskDeep/log line hides it */
3
+ export declare function registerSecret(name: string, value: string): void;
4
+ /** Forgets every registered secret (tests only) */
5
+ export declare function resetSecrets(): void;
6
+ export declare function isShortSecret(value: string): boolean;
7
+ export declare function maskText(text: string): string;
8
+ /** A copy of `value` with every string in it masked, following JSON.stringify semantics for objects */
9
+ export declare function maskDeep<T>(value: T): T;
10
+ /** Winston format that masks every string field of a log entry (message and metadata) */
11
+ export declare const maskFormat: import("logform").FormatWrap;
@@ -0,0 +1,85 @@
1
+ import { inspect } from "node:util";
2
+ import { format } from "winston";
3
+ export const MASK = "••••";
4
+ /** Values shorter than this only match as a whole token, so "1" does not hide every digit */
5
+ const SHORT_LENGTH = 4;
6
+ const secrets = new Map();
7
+ let patterns = [];
8
+ /** Registers a secret value; every later maskText/maskDeep/log line hides it */
9
+ export function registerSecret(name, value) {
10
+ if (value === "")
11
+ return;
12
+ secrets.set(name, value);
13
+ rebuild();
14
+ }
15
+ /** Forgets every registered secret (tests only) */
16
+ export function resetSecrets() {
17
+ secrets.clear();
18
+ patterns = [];
19
+ }
20
+ export function isShortSecret(value) {
21
+ return value.length > 0 && value.length < SHORT_LENGTH;
22
+ }
23
+ function escapeRegExp(value) {
24
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ }
26
+ /** A value plus its JSON-escaped, inspect-escaped and percent-encoded spellings */
27
+ function variants(value) {
28
+ const forms = [value, JSON.stringify(value).slice(1, -1), inspect(value).slice(1, -1), encodeURIComponent(value)];
29
+ return forms.filter((form) => form !== "");
30
+ }
31
+ function rebuild() {
32
+ // Longest first, so a secret that contains another one is hidden whole
33
+ const forms = [...new Set([...secrets.values()].flatMap(variants))].sort((a, b) => b.length - a.length);
34
+ patterns = forms.map((value) => isShortSecret(value)
35
+ ? new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(value)}(?![\\p{L}\\p{N}])`, "gu")
36
+ : new RegExp(escapeRegExp(value), "gu"));
37
+ }
38
+ export function maskText(text) {
39
+ let masked = text;
40
+ for (const pattern of patterns)
41
+ masked = masked.replace(pattern, MASK);
42
+ return masked;
43
+ }
44
+ /** A copy of `value` with every string in it masked, following JSON.stringify semantics for objects */
45
+ export function maskDeep(value) {
46
+ if (patterns.length === 0)
47
+ return value;
48
+ return walk(value, new WeakSet());
49
+ }
50
+ function walk(value, path) {
51
+ if (typeof value === "string")
52
+ return maskText(value);
53
+ if (value === null || typeof value !== "object")
54
+ return value;
55
+ if (path.has(value))
56
+ return "[Circular]";
57
+ path.add(value);
58
+ try {
59
+ if (Array.isArray(value))
60
+ return value.map((item) => walk(item, path));
61
+ const toJSON = value.toJSON;
62
+ if (typeof toJSON === "function")
63
+ return walk(toJSON.call(value), path);
64
+ const entries = Object.entries(value);
65
+ if (value instanceof Error) {
66
+ const own = new Set(entries.map(([key]) => key));
67
+ for (const key of ["name", "message", "stack"]) {
68
+ if (!own.has(key))
69
+ entries.unshift([key, value[key]]);
70
+ }
71
+ }
72
+ return Object.fromEntries(entries.map(([key, item]) => [key, walk(item, path)]));
73
+ }
74
+ finally {
75
+ path.delete(value);
76
+ }
77
+ }
78
+ /** Winston format that masks every string field of a log entry (message and metadata) */
79
+ export const maskFormat = format((info) => {
80
+ if (patterns.length === 0)
81
+ return info;
82
+ for (const key of Object.keys(info))
83
+ info[key] = walk(info[key], new WeakSet());
84
+ return info;
85
+ });
@@ -0,0 +1,28 @@
1
+ import type { SamuraiTestConfig } from "../types/config.js";
2
+ export type Variables = Record<string, string | number | boolean>;
3
+ /** Values given for one run (CLI flags, SAMURAI_ENV); they beat the config */
4
+ export interface RunOverrides {
5
+ environment?: string;
6
+ timeout?: number;
7
+ expectTimeout?: number;
8
+ }
9
+ /** Everything about a run that depends on the chosen environment */
10
+ export interface RunSettings {
11
+ environment: string;
12
+ baseURL?: string;
13
+ /** Time (ms) one test may take */
14
+ timeout: number;
15
+ /** Time (ms) actions and assertions retry */
16
+ expectTimeout: number;
17
+ variables: Variables;
18
+ }
19
+ export declare class UnknownEnvironmentError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ /** Picks the environment and applies precedence: run override > environment > project > built-in default */
23
+ export declare function resolveRunSettings(config: SamuraiTestConfig, overrides?: RunOverrides): RunSettings;
24
+ /** Makes `settings` the run's settings; `undefined` ends the run (tests use this to reset) */
25
+ export declare function setRunSettings(settings: RunSettings | undefined): void;
26
+ /** The active run's settings, or undefined outside a run (unit and browser tests) */
27
+ export declare function peekRunSettings(): RunSettings | undefined;
28
+ export declare function getRunSettings(): RunSettings;
@@ -0,0 +1,61 @@
1
+ const DEFAULT_TIMEOUT = 30000;
2
+ const DEFAULT_EXPECT_TIMEOUT = 5000;
3
+ /** The environment of a config without `environments` */
4
+ const IMPLICIT_ENVIRONMENT = "default";
5
+ export class UnknownEnvironmentError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "UnknownEnvironmentError";
9
+ }
10
+ }
11
+ /** Picks the environment and applies precedence: run override > environment > project > built-in default */
12
+ export function resolveRunSettings(config, overrides = {}) {
13
+ const environments = config.environments ?? {};
14
+ const names = Object.keys(environments);
15
+ const available = `Available: ${names.join(", ")}`;
16
+ let name = overrides.environment ?? config.defaultEnvironment;
17
+ if (name === undefined) {
18
+ if (names.length === 0)
19
+ name = IMPLICIT_ENVIRONMENT;
20
+ else if (names.length === 1)
21
+ name = names[0];
22
+ else
23
+ throw new UnknownEnvironmentError(`No environment chosen. ${available} (use --env or set defaultEnvironment)`);
24
+ }
25
+ const environment = Object.prototype.hasOwnProperty.call(environments, name)
26
+ ? environments[name]
27
+ : undefined;
28
+ if (!environment && !(names.length === 0 && name === IMPLICIT_ENVIRONMENT)) {
29
+ throw new UnknownEnvironmentError(names.length === 0
30
+ ? `Unknown environment "${name}". No environments are defined in samurai.config.ts`
31
+ : `Unknown environment "${name}". ${available}`);
32
+ }
33
+ const baseURL = environment?.baseURL ?? config.baseURL;
34
+ return {
35
+ environment: name,
36
+ ...(baseURL !== undefined && { baseURL }),
37
+ timeout: overrides.timeout ??
38
+ environment?.timeout ??
39
+ config.timeout ??
40
+ DEFAULT_TIMEOUT,
41
+ expectTimeout: overrides.expectTimeout ??
42
+ environment?.expect?.timeout ??
43
+ config.expect?.timeout ??
44
+ DEFAULT_EXPECT_TIMEOUT,
45
+ variables: { ...(environment?.variables ?? {}) },
46
+ };
47
+ }
48
+ let active;
49
+ /** Makes `settings` the run's settings; `undefined` ends the run (tests use this to reset) */
50
+ export function setRunSettings(settings) {
51
+ active = settings;
52
+ }
53
+ /** The active run's settings, or undefined outside a run (unit and browser tests) */
54
+ export function peekRunSettings() {
55
+ return active;
56
+ }
57
+ export function getRunSettings() {
58
+ if (!active)
59
+ throw new Error("No test run is active; run settings are set when the runner starts");
60
+ return active;
61
+ }
@@ -0,0 +1,10 @@
1
+ export declare class MissingSecretError extends Error {
2
+ constructor(name: string, environment: string);
3
+ }
4
+ /** Secrets by name: `.env.<environment>` in `cwd` first, then env vars (which win) */
5
+ export declare function loadSecrets(environment: string, options?: {
6
+ cwd?: string;
7
+ env?: NodeJS.ProcessEnv;
8
+ }): Map<string, string>;
9
+ /** The test's `secrets`: values by name, read-only; JSON and inspect show only masks */
10
+ export declare function createSecretsFixture(environment: string, values: ReadonlyMap<string, string>): Readonly<Record<string, string>>;
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { inspect, parseEnv } from "node:util";
4
+ import { projectDir } from "./config.js";
5
+ import logger from "../logger/index.js";
6
+ import { MASK } from "./mask.js";
7
+ import { isPassthroughKey } from "./variables.js";
8
+ const PREFIX = "SAMURAI_SECRET_";
9
+ const NAME = /^[A-Z][A-Z0-9_]*$/;
10
+ export class MissingSecretError extends Error {
11
+ constructor(name, environment) {
12
+ super(`Secret "${name}" is not set (expected env var ${PREFIX}${name} or .env.${environment})`);
13
+ this.name = "MissingSecretError";
14
+ }
15
+ }
16
+ /** Secrets by name: `.env.<environment>` in `cwd` first, then env vars (which win) */
17
+ export function loadSecrets(environment, options = {}) {
18
+ const file = path.join(options.cwd ?? projectDir(), `.env.${environment}`);
19
+ const sources = [];
20
+ if (existsSync(file))
21
+ sources.push(parseEnv(readFileSync(file, "utf8")));
22
+ sources.push(options.env ?? process.env);
23
+ const secrets = new Map();
24
+ for (const source of sources) {
25
+ for (const [key, value] of Object.entries(source)) {
26
+ if (!key.startsWith(PREFIX) || value === undefined || value === "")
27
+ continue;
28
+ const name = key.slice(PREFIX.length);
29
+ if (!NAME.test(name)) {
30
+ logger.warn(`Ignoring ${key}: secret names must be UPPER_SNAKE_CASE`);
31
+ continue;
32
+ }
33
+ secrets.set(name, value);
34
+ }
35
+ }
36
+ return secrets;
37
+ }
38
+ /** The test's `secrets`: values by name, read-only; JSON and inspect show only masks */
39
+ export function createSecretsFixture(environment, values) {
40
+ const masked = () => Object.fromEntries([...values.keys()].map((name) => [name, MASK]));
41
+ // Not frozen: the get trap returns the real value, which a frozen target's invariants forbid
42
+ const target = masked();
43
+ Object.defineProperty(target, "toJSON", { value: masked, enumerable: false });
44
+ Object.defineProperty(target, inspect.custom, {
45
+ value: masked,
46
+ enumerable: false,
47
+ });
48
+ const readOnly = () => {
49
+ throw new TypeError("secrets are read-only");
50
+ };
51
+ return new Proxy(target, {
52
+ get(target, key, receiver) {
53
+ if (isPassthroughKey(target, key))
54
+ return Reflect.get(target, key, receiver);
55
+ const name = String(key);
56
+ if (!NAME.test(name))
57
+ throw new Error(`Invalid secret name "${name}" (use UPPER_SNAKE_CASE)`);
58
+ const value = values.get(name);
59
+ if (value === undefined)
60
+ throw new MissingSecretError(name, environment);
61
+ return value;
62
+ },
63
+ set: readOnly,
64
+ defineProperty: readOnly,
65
+ deleteProperty: readOnly,
66
+ });
67
+ }
@@ -0,0 +1,11 @@
1
+ import type { Variables } from "./run-settings.js";
2
+ export declare class UnknownVariableError extends Error {
3
+ constructor(name: string, environment: string);
4
+ }
5
+ /**
6
+ * Keys that every object answers and that `await`, JSON.stringify and util.inspect probe:
7
+ * symbols, `then`, `toJSON` and Object.prototype members. Reading them never throws.
8
+ */
9
+ export declare function isPassthroughKey(target: object, key: string | symbol): boolean;
10
+ /** The test's `env`: the environment's variables, read-only; an unknown name throws */
11
+ export declare function createEnvFixture(environment: string, variables: Variables): Readonly<Variables>;
@@ -0,0 +1,26 @@
1
+ export class UnknownVariableError extends Error {
2
+ constructor(name, environment) {
3
+ super(`Variable "${name}" is not defined in environment "${environment}"`);
4
+ this.name = "UnknownVariableError";
5
+ }
6
+ }
7
+ /**
8
+ * Keys that every object answers and that `await`, JSON.stringify and util.inspect probe:
9
+ * symbols, `then`, `toJSON` and Object.prototype members. Reading them never throws.
10
+ */
11
+ export function isPassthroughKey(target, key) {
12
+ return (typeof key === "symbol" ||
13
+ key === "then" ||
14
+ key === "toJSON" ||
15
+ (!Object.prototype.hasOwnProperty.call(target, key) && key in target));
16
+ }
17
+ /** The test's `env`: the environment's variables, read-only; an unknown name throws */
18
+ export function createEnvFixture(environment, variables) {
19
+ return new Proxy(Object.freeze({ ...variables }), {
20
+ get(target, key, receiver) {
21
+ if (isPassthroughKey(target, key) || Object.prototype.hasOwnProperty.call(target, key))
22
+ return Reflect.get(target, key, receiver);
23
+ throw new UnknownVariableError(String(key), environment);
24
+ },
25
+ });
26
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { main } from "./cli/main.js";
2
+ // Development entry (`bun run dev`): the same as `samurai run`, straight from the sources
3
+ const interrupt = new AbortController();
4
+ process.on("SIGINT", () => {
5
+ if (interrupt.signal.aborted)
6
+ process.exit(130);
7
+ interrupt.abort();
8
+ });
9
+ process.exit(await main(process.argv.slice(2), {
10
+ out: (text) => process.stdout.write(text),
11
+ err: (text) => process.stderr.write(text),
12
+ color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
13
+ env: process.env,
14
+ signal: interrupt.signal,
15
+ }));
@@ -0,0 +1,25 @@
1
+ import type { CheckResults, WaitForState } from "./element-state.js";
2
+ export type ActionTimeoutReason = "unreadable" | "not-attached" | "not-actionable" | "wrong-state";
3
+ export interface ActionTimeoutDetails {
4
+ /** Locator method that timed out, e.g. "click" */
5
+ action: string;
6
+ selector: string;
7
+ timeout: number;
8
+ reason: ActionTimeoutReason;
9
+ /** Check results from the last probe, in check order */
10
+ checks?: CheckResults | undefined;
11
+ /** Description of the element covering the target, when the hit-target check failed */
12
+ coveredBy?: string | undefined;
13
+ /** Target state, for waitFor */
14
+ state?: WaitForState | undefined;
15
+ }
16
+ export declare class ActionTimeoutError extends Error {
17
+ readonly action: string;
18
+ readonly selector: string;
19
+ readonly timeout: number;
20
+ readonly reason: ActionTimeoutReason;
21
+ readonly checks: CheckResults | undefined;
22
+ readonly coveredBy: string | undefined;
23
+ readonly state: WaitForState | undefined;
24
+ constructor(details: ActionTimeoutDetails);
25
+ }
@@ -0,0 +1,35 @@
1
+ const SYMBOLS = { pass: "✓", fail: "✗", pending: "—" };
2
+ function formatMessage({ action, selector, timeout, reason, checks, coveredBy, state, }) {
3
+ switch (reason) {
4
+ case "unreadable":
5
+ return `${action}(): could not read ${selector} within ${timeout}ms`;
6
+ case "not-attached":
7
+ return `${action}(): ${selector} was not attached within ${timeout}ms`;
8
+ case "wrong-state":
9
+ return `${action}(): ${selector} did not become ${state} within ${timeout}ms`;
10
+ case "not-actionable": {
11
+ const summary = Object.entries(checks ?? {})
12
+ .map(([name, result]) => {
13
+ const covered = name === "hit target" && result === "fail" && coveredBy
14
+ ? ` (covered by ${coveredBy})`
15
+ : "";
16
+ return `${name} ${SYMBOLS[result]}${covered}`;
17
+ })
18
+ .join(" ");
19
+ return `${action}(): ${selector} was not actionable within ${timeout}ms\n ${summary}`;
20
+ }
21
+ }
22
+ }
23
+ export class ActionTimeoutError extends Error {
24
+ constructor(details) {
25
+ super(formatMessage(details));
26
+ this.name = "ActionTimeoutError";
27
+ this.action = details.action;
28
+ this.selector = details.selector;
29
+ this.timeout = details.timeout;
30
+ this.reason = details.reason;
31
+ this.checks = details.checks;
32
+ this.coveredBy = details.coveredBy;
33
+ this.state = details.state;
34
+ }
35
+ }