@itmetsam/samurai-framework 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/README.md +129 -0
  2. package/bin/samurai.mjs +31 -0
  3. package/dist/api.js +3 -0
  4. package/dist/assert/assertion-error.d.ts +23 -0
  5. package/dist/assert/assertion-error.js +45 -0
  6. package/dist/assert/expect.d.ts +55 -0
  7. package/dist/assert/expect.js +188 -0
  8. package/dist/browser/browser-context.d.ts +91 -0
  9. package/dist/browser/browser-context.js +146 -0
  10. package/dist/browser/browser-finder.d.ts +2 -0
  11. package/dist/browser/browser-finder.js +95 -0
  12. package/dist/browser/browser.d.ts +95 -0
  13. package/dist/browser/browser.js +372 -0
  14. package/dist/browser/context-tree.d.ts +23 -0
  15. package/dist/browser/context-tree.js +60 -0
  16. package/dist/browser/dialog.d.ts +37 -0
  17. package/dist/browser/dialog.js +61 -0
  18. package/dist/browser/download.d.ts +71 -0
  19. package/dist/browser/download.js +137 -0
  20. package/dist/browser/downloads-dir.d.ts +8 -0
  21. package/dist/browser/downloads-dir.js +38 -0
  22. package/dist/browser/emulation.d.ts +61 -0
  23. package/dist/browser/emulation.js +144 -0
  24. package/dist/browser/file-chooser.d.ts +39 -0
  25. package/dist/browser/file-chooser.js +86 -0
  26. package/dist/browser/file-paths.d.ts +4 -0
  27. package/dist/browser/file-paths.js +18 -0
  28. package/dist/browser/index.d.ts +4 -0
  29. package/dist/browser/index.js +4 -0
  30. package/dist/browser/navigation-error.d.ts +13 -0
  31. package/dist/browser/navigation-error.js +19 -0
  32. package/dist/browser/page-logs.d.ts +86 -0
  33. package/dist/browser/page-logs.js +149 -0
  34. package/dist/browser/page-wait.d.ts +9 -0
  35. package/dist/browser/page-wait.js +17 -0
  36. package/dist/browser/page.d.ts +370 -0
  37. package/dist/browser/page.js +1038 -0
  38. package/dist/browser/pdf.d.ts +72 -0
  39. package/dist/browser/pdf.js +37 -0
  40. package/dist/browser/screenshot.d.ts +29 -0
  41. package/dist/browser/screenshot.js +42 -0
  42. package/dist/cli/args.d.ts +35 -0
  43. package/dist/cli/args.js +69 -0
  44. package/dist/cli/cli.cli-test.d.ts +1 -0
  45. package/dist/cli/cli.cli-test.js +170 -0
  46. package/dist/cli/console-reporter.d.ts +15 -0
  47. package/dist/cli/console-reporter.js +52 -0
  48. package/dist/cli/init.d.ts +7 -0
  49. package/dist/cli/init.js +61 -0
  50. package/dist/cli/main.d.ts +12 -0
  51. package/dist/cli/main.js +156 -0
  52. package/dist/config/config.d.ts +24 -0
  53. package/dist/config/config.js +42 -0
  54. package/dist/config/default.d.ts +2 -0
  55. package/dist/config/default.js +3 -0
  56. package/dist/config/mask.d.ts +11 -0
  57. package/dist/config/mask.js +85 -0
  58. package/dist/config/run-settings.d.ts +28 -0
  59. package/dist/config/run-settings.js +61 -0
  60. package/dist/config/secrets.d.ts +10 -0
  61. package/dist/config/secrets.js +67 -0
  62. package/dist/config/variables.d.ts +11 -0
  63. package/dist/config/variables.js +26 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +15 -0
  66. package/dist/locator/action-timeout-error.d.ts +25 -0
  67. package/dist/locator/action-timeout-error.js +35 -0
  68. package/dist/locator/element-state.d.ts +44 -0
  69. package/dist/locator/element-state.js +109 -0
  70. package/dist/locator/label-locator.d.ts +13 -0
  71. package/dist/locator/label-locator.js +49 -0
  72. package/dist/locator/locator.d.ts +168 -0
  73. package/dist/locator/locator.js +591 -0
  74. package/dist/locator/selector-errors.d.ts +9 -0
  75. package/dist/locator/selector-errors.js +15 -0
  76. package/dist/locator/selector.d.ts +60 -0
  77. package/dist/locator/selector.js +113 -0
  78. package/dist/locator/text-locator.d.ts +13 -0
  79. package/dist/logger/index.d.ts +2 -0
  80. package/dist/logger/index.js +30 -0
  81. package/dist/network/data-collector.d.ts +26 -0
  82. package/dist/network/data-collector.js +77 -0
  83. package/dist/network/network-tracker.d.ts +94 -0
  84. package/dist/network/network-tracker.js +198 -0
  85. package/dist/network/network-wait.d.ts +16 -0
  86. package/dist/network/network-wait.js +24 -0
  87. package/dist/network/response.d.ts +22 -0
  88. package/dist/network/response.js +42 -0
  89. package/dist/network/router.d.ts +120 -0
  90. package/dist/network/router.js +343 -0
  91. package/dist/network/url-match.d.ts +8 -0
  92. package/dist/network/url-match.js +38 -0
  93. package/dist/recorder/apply.d.ts +3 -0
  94. package/dist/recorder/apply.js +10 -0
  95. package/dist/recorder/capture.d.ts +18 -0
  96. package/dist/recorder/capture.js +198 -0
  97. package/dist/recorder/index.d.ts +3 -0
  98. package/dist/recorder/index.js +3 -0
  99. package/dist/recorder/normalise.d.ts +68 -0
  100. package/dist/recorder/normalise.js +104 -0
  101. package/dist/recorder/ranking.d.ts +9 -0
  102. package/dist/recorder/ranking.js +21 -0
  103. package/dist/recorder/recorder.d.ts +52 -0
  104. package/dist/recorder/recorder.js +143 -0
  105. package/dist/recorder/session.d.ts +49 -0
  106. package/dist/recorder/session.js +113 -0
  107. package/dist/runner/module-type.d.ts +6 -0
  108. package/dist/runner/module-type.js +29 -0
  109. package/dist/runner/page-logs-report.d.ts +40 -0
  110. package/dist/runner/page-logs-report.js +44 -0
  111. package/dist/runner/prepare-run.d.ts +14 -0
  112. package/dist/runner/prepare-run.js +20 -0
  113. package/dist/runner/project.d.ts +17 -0
  114. package/dist/runner/project.js +33 -0
  115. package/dist/runner/registry.d.ts +7 -0
  116. package/dist/runner/registry.js +40 -0
  117. package/dist/runner/reporter.d.ts +43 -0
  118. package/dist/runner/reporter.js +83 -0
  119. package/dist/runner/run.d.ts +27 -0
  120. package/dist/runner/run.js +34 -0
  121. package/dist/runner/test-error.d.ts +3 -0
  122. package/dist/runner/test-error.js +18 -0
  123. package/dist/runner/test-runner.d.ts +44 -0
  124. package/dist/runner/test-runner.js +205 -0
  125. package/dist/script/call-function.d.ts +34 -0
  126. package/dist/script/call-function.js +71 -0
  127. package/dist/script/element-handle.d.ts +10 -0
  128. package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
  129. package/dist/script/helpers.d.ts +28 -0
  130. package/dist/script/helpers.js +50 -0
  131. package/dist/script/preload.d.ts +31 -0
  132. package/dist/script/preload.js +71 -0
  133. package/dist/script/serialize.d.ts +29 -0
  134. package/dist/script/serialize.js +207 -0
  135. package/dist/steps/build.d.ts +5 -0
  136. package/dist/steps/build.js +38 -0
  137. package/dist/steps/edit.d.ts +30 -0
  138. package/dist/steps/edit.js +80 -0
  139. package/dist/steps/emit.d.ts +6 -0
  140. package/dist/steps/emit.js +73 -0
  141. package/dist/steps/index.d.ts +5 -0
  142. package/dist/steps/index.js +4 -0
  143. package/dist/steps/model.d.ts +103 -0
  144. package/dist/steps/model.js +1 -0
  145. package/dist/steps/parse.d.ts +3 -0
  146. package/dist/steps/parse.js +272 -0
  147. package/dist/transport/bidi-connection.d.ts +46 -0
  148. package/dist/transport/bidi-connection.js +176 -0
  149. package/dist/transport/bidi-error.d.ts +10 -0
  150. package/dist/transport/bidi-error.js +10 -0
  151. package/dist/transport/subscriptions.d.ts +36 -0
  152. package/dist/transport/subscriptions.js +81 -0
  153. package/{src → dist}/types/test.d.ts +10 -4
  154. package/dist/wait/wait-until.d.ts +20 -0
  155. package/dist/wait/wait-until.js +63 -0
  156. package/package.json +44 -7
  157. package/.claude/settings.local.json +0 -7
  158. package/.env.example +0 -3
  159. package/.github/actions/setup/action.yml +0 -16
  160. package/.github/workflows/ci.yml +0 -71
  161. package/.trunk/configs/.markdownlint.yaml +0 -2
  162. package/.trunk/configs/.yamllint.yaml +0 -7
  163. package/.trunk/configs/svgo.config.mjs +0 -14
  164. package/.trunk/trunk.yaml +0 -34
  165. package/docs/superpowers/bidi-roadmap.md +0 -60
  166. package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
  167. package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
  168. package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
  169. package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
  170. package/docs/superpowers/recorder-roadmap.md +0 -52
  171. package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
  172. package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
  173. package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
  174. package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
  175. package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
  176. package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
  177. package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
  178. package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
  179. package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
  180. package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
  181. package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
  182. package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
  183. package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
  184. package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
  185. package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
  186. package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
  187. package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
  188. package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
  189. package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
  190. package/public/logo-dark.svg +0 -236
  191. package/public/logo-light.svg +0 -262
  192. package/samurai.config.ts +0 -7
  193. package/src/assert/assertion-error.test.ts +0 -55
  194. package/src/assert/assertion-error.ts +0 -62
  195. package/src/assert/expect-locator.test.ts +0 -217
  196. package/src/assert/expect.test.ts +0 -73
  197. package/src/assert/expect.ts +0 -299
  198. package/src/browser/browser-context.browser-test.ts +0 -201
  199. package/src/browser/browser-context.test.ts +0 -407
  200. package/src/browser/browser-context.ts +0 -208
  201. package/src/browser/browser-finder.ts +0 -110
  202. package/src/browser/browser.test.ts +0 -88
  203. package/src/browser/browser.ts +0 -515
  204. package/src/browser/context-tree.test.ts +0 -86
  205. package/src/browser/context-tree.ts +0 -70
  206. package/src/browser/dialog.browser-test.ts +0 -144
  207. package/src/browser/dialog.test.ts +0 -322
  208. package/src/browser/dialog.ts +0 -83
  209. package/src/browser/download.test.ts +0 -161
  210. package/src/browser/download.ts +0 -172
  211. package/src/browser/downloads-dir.test.ts +0 -75
  212. package/src/browser/downloads-dir.ts +0 -39
  213. package/src/browser/emulation.browser-test.ts +0 -186
  214. package/src/browser/emulation.test.ts +0 -200
  215. package/src/browser/emulation.ts +0 -204
  216. package/src/browser/file-chooser.test.ts +0 -142
  217. package/src/browser/file-chooser.ts +0 -99
  218. package/src/browser/file-paths.ts +0 -18
  219. package/src/browser/goto.browser-test.ts +0 -44
  220. package/src/browser/navigation-error.ts +0 -26
  221. package/src/browser/navigation.browser-test.ts +0 -172
  222. package/src/browser/page-api.browser-test.ts +0 -228
  223. package/src/browser/page-logs.browser-test.ts +0 -119
  224. package/src/browser/page-logs.test.ts +0 -212
  225. package/src/browser/page-logs.ts +0 -208
  226. package/src/browser/page-wait.ts +0 -19
  227. package/src/browser/page.test.ts +0 -1567
  228. package/src/browser/page.ts +0 -1176
  229. package/src/browser/pdf.ts +0 -62
  230. package/src/browser/screenshot.ts +0 -65
  231. package/src/browser/transfers.browser-test.ts +0 -180
  232. package/src/config/config.ts +0 -33
  233. package/src/config/default.ts +0 -5
  234. package/src/config/fixtures.test.ts +0 -137
  235. package/src/config/mask.test.ts +0 -143
  236. package/src/config/mask.ts +0 -87
  237. package/src/config/run-settings.test.ts +0 -163
  238. package/src/config/run-settings.ts +0 -114
  239. package/src/config/secrets.ts +0 -70
  240. package/src/config/variables.ts +0 -31
  241. package/src/index.ts +0 -10
  242. package/src/locator/action-timeout-error.test.ts +0 -83
  243. package/src/locator/action-timeout-error.ts +0 -72
  244. package/src/locator/element-state.test.ts +0 -181
  245. package/src/locator/element-state.ts +0 -159
  246. package/src/locator/locate-nodes.browser-test.ts +0 -169
  247. package/src/locator/locator-actions.test.ts +0 -355
  248. package/src/locator/locator-chain.test.ts +0 -292
  249. package/src/locator/locator.browser-test.ts +0 -137
  250. package/src/locator/locator.test.ts +0 -179
  251. package/src/locator/locator.ts +0 -656
  252. package/src/locator/selector-errors.ts +0 -18
  253. package/src/locator/selector.test.ts +0 -124
  254. package/src/locator/selector.ts +0 -107
  255. package/src/locator/set-files.browser-test.ts +0 -141
  256. package/src/locator/set-files.test.ts +0 -191
  257. package/src/locator/text-locator.test.ts +0 -118
  258. package/src/logger/index.test.ts +0 -28
  259. package/src/logger/index.ts +0 -40
  260. package/src/network/data-collector.test.ts +0 -111
  261. package/src/network/data-collector.ts +0 -92
  262. package/src/network/network-data.browser-test.ts +0 -192
  263. package/src/network/network-tracker.browser-test.ts +0 -151
  264. package/src/network/network-tracker.test.ts +0 -307
  265. package/src/network/network-tracker.ts +0 -286
  266. package/src/network/network-wait.ts +0 -28
  267. package/src/network/response.test.ts +0 -93
  268. package/src/network/response.ts +0 -58
  269. package/src/network/router.browser-test.ts +0 -260
  270. package/src/network/router.test.ts +0 -528
  271. package/src/network/router.ts +0 -420
  272. package/src/network/url-match.test.ts +0 -69
  273. package/src/network/url-match.ts +0 -41
  274. package/src/runner/page-logs-report.test.ts +0 -144
  275. package/src/runner/page-logs-report.ts +0 -82
  276. package/src/runner/prepare-run.test.ts +0 -33
  277. package/src/runner/prepare-run.ts +0 -36
  278. package/src/runner/registry.test.ts +0 -59
  279. package/src/runner/registry.ts +0 -46
  280. package/src/runner/reporter.test.ts +0 -73
  281. package/src/runner/reporter.ts +0 -109
  282. package/src/runner/test-error.test.ts +0 -39
  283. package/src/runner/test-error.ts +0 -20
  284. package/src/runner/test-runner.ts +0 -184
  285. package/src/script/call-function.browser-test.ts +0 -112
  286. package/src/script/call-function.test.ts +0 -174
  287. package/src/script/call-function.ts +0 -113
  288. package/src/script/helpers.test.ts +0 -116
  289. package/src/script/helpers.ts +0 -68
  290. package/src/script/preload.browser-test.ts +0 -144
  291. package/src/script/preload.test.ts +0 -180
  292. package/src/script/preload.ts +0 -102
  293. package/src/script/serialize.test.ts +0 -182
  294. package/src/script/serialize.ts +0 -203
  295. package/src/testing/browser-fixture.ts +0 -18
  296. package/src/testing/fake-websocket.ts +0 -72
  297. package/src/testing/stub-connector.ts +0 -154
  298. package/src/tests/index.spec.ts +0 -30
  299. package/src/transport/bidi-connection.test.ts +0 -155
  300. package/src/transport/bidi-connection.ts +0 -256
  301. package/src/transport/bidi-error.ts +0 -23
  302. package/src/transport/foundation.browser-test.ts +0 -73
  303. package/src/transport/subscriptions.test.ts +0 -140
  304. package/src/transport/subscriptions.ts +0 -110
  305. package/src/types/bidi-commands.test-d.ts +0 -55
  306. package/src/wait/wait-until.test.ts +0 -117
  307. package/src/wait/wait-until.ts +0 -86
  308. package/tsconfig.json +0 -44
  309. /package/{src/api.ts → dist/api.d.ts} +0 -0
  310. /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
  311. /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
  312. /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
  313. /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
  314. /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
  315. /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
  316. /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
  317. /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
  318. /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
  319. /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
  320. /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
  321. /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
  322. /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
  323. /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
  324. /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
  325. /package/{src → dist}/types/bidi.d.ts +0 -0
  326. /package/{src → dist}/types/browser.d.ts +0 -0
  327. /package/{src → dist}/types/config.d.ts +0 -0
  328. /package/{src → dist}/types/env.d.ts +0 -0
@@ -1,87 +0,0 @@
1
- import { inspect } from "node:util";
2
- import { format } from "winston";
3
-
4
- export const MASK = "••••";
5
- /** Values shorter than this only match as a whole token, so "1" does not hide every digit */
6
- const SHORT_LENGTH = 4;
7
-
8
- const secrets = new Map<string, string>();
9
- let patterns: RegExp[] = [];
10
-
11
- /** Registers a secret value; every later maskText/maskDeep/log line hides it */
12
- export function registerSecret(name: string, value: string): void {
13
- if (value === "") return;
14
- secrets.set(name, value);
15
- rebuild();
16
- }
17
-
18
- /** Forgets every registered secret (tests only) */
19
- export function resetSecrets(): void {
20
- secrets.clear();
21
- patterns = [];
22
- }
23
-
24
- export function isShortSecret(value: string): boolean {
25
- return value.length > 0 && value.length < SHORT_LENGTH;
26
- }
27
-
28
- function escapeRegExp(value: string): string {
29
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
- }
31
-
32
- /** A value plus its JSON-escaped, inspect-escaped and percent-encoded spellings */
33
- function variants(value: string): string[] {
34
- const forms = [value, JSON.stringify(value).slice(1, -1), inspect(value).slice(1, -1), encodeURIComponent(value)];
35
- return forms.filter((form) => form !== "");
36
- }
37
-
38
- function rebuild(): void {
39
- // Longest first, so a secret that contains another one is hidden whole
40
- const forms = [...new Set([...secrets.values()].flatMap(variants))].sort((a, b) => b.length - a.length);
41
- patterns = forms.map((value) =>
42
- isShortSecret(value)
43
- ? new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(value)}(?![\\p{L}\\p{N}])`, "gu")
44
- : new RegExp(escapeRegExp(value), "gu"),
45
- );
46
- }
47
-
48
- export function maskText(text: string): string {
49
- let masked = text;
50
- for (const pattern of patterns) masked = masked.replace(pattern, MASK);
51
- return masked;
52
- }
53
-
54
- /** A copy of `value` with every string in it masked, following JSON.stringify semantics for objects */
55
- export function maskDeep<T>(value: T): T {
56
- if (patterns.length === 0) return value;
57
- return walk(value, new WeakSet()) as T;
58
- }
59
-
60
- function walk(value: unknown, path: WeakSet<object>): unknown {
61
- if (typeof value === "string") return maskText(value);
62
- if (value === null || typeof value !== "object") return value;
63
- if (path.has(value)) return "[Circular]";
64
- path.add(value);
65
- try {
66
- if (Array.isArray(value)) return value.map((item) => walk(item, path));
67
- const toJSON = (value as { toJSON?: unknown }).toJSON;
68
- if (typeof toJSON === "function") return walk(toJSON.call(value), path);
69
- const entries: Array<[string, unknown]> = Object.entries(value);
70
- if (value instanceof Error) {
71
- const own = new Set(entries.map(([key]) => key));
72
- for (const key of ["name", "message", "stack"] as const) {
73
- if (!own.has(key)) entries.unshift([key, value[key]]);
74
- }
75
- }
76
- return Object.fromEntries(entries.map(([key, item]) => [key, walk(item, path)]));
77
- } finally {
78
- path.delete(value);
79
- }
80
- }
81
-
82
- /** Winston format that masks every string field of a log entry (message and metadata) */
83
- export const maskFormat = format((info) => {
84
- if (patterns.length === 0) return info;
85
- for (const key of Object.keys(info)) info[key] = walk(info[key], new WeakSet());
86
- return info;
87
- });
@@ -1,163 +0,0 @@
1
- import { afterEach, describe, test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import type { SamuraiTestConfig } from "../types/config.js";
4
- import {
5
- getRunSettings,
6
- parseRunOverrides,
7
- peekRunSettings,
8
- resolveRunSettings,
9
- setRunSettings,
10
- UnknownEnvironmentError,
11
- } from "./run-settings.js";
12
-
13
- const twoEnvironments: SamuraiTestConfig = {
14
- baseURL: "https://harbor.shop",
15
- timeout: 20000,
16
- expect: { timeout: 3000 },
17
- environments: {
18
- dev: { baseURL: "http://localhost:3000", variables: { customerEmail: "dev@example.test" } },
19
- staging: { timeout: 45000, expect: { timeout: 8000 }, variables: { customerEmail: "sam@example.test" } },
20
- },
21
- };
22
-
23
- describe("resolveRunSettings: choosing the environment", () => {
24
- test("an override wins over defaultEnvironment", () => {
25
- const settings = resolveRunSettings({ ...twoEnvironments, defaultEnvironment: "staging" }, { environment: "dev" });
26
- assert.equal(settings.environment, "dev");
27
- });
28
-
29
- test("defaultEnvironment is used without an override", () => {
30
- assert.equal(resolveRunSettings({ ...twoEnvironments, defaultEnvironment: "staging" }).environment, "staging");
31
- });
32
-
33
- test("the only environment is used when nothing is chosen", () => {
34
- assert.equal(resolveRunSettings({ environments: { qa: {} } }).environment, "qa");
35
- });
36
-
37
- test("no environments gives the implicit default environment with top-level values", () => {
38
- assert.deepEqual(resolveRunSettings({ baseURL: "https://harbor.shop", timeout: 1000 }), {
39
- environment: "default",
40
- baseURL: "https://harbor.shop",
41
- timeout: 1000,
42
- expectTimeout: 5000,
43
- variables: {},
44
- });
45
- });
46
-
47
- test("several environments and no choice throws, listing them", () => {
48
- assert.throws(
49
- () => resolveRunSettings(twoEnvironments),
50
- (err) =>
51
- err instanceof UnknownEnvironmentError &&
52
- err.message === "No environment chosen. Available: dev, staging (use --env or set defaultEnvironment)",
53
- );
54
- });
55
-
56
- test("an unknown name throws, listing the available ones", () => {
57
- assert.throws(
58
- () => resolveRunSettings(twoEnvironments, { environment: "stg" }),
59
- (err) => err instanceof UnknownEnvironmentError && err.message === 'Unknown environment "stg". Available: dev, staging',
60
- );
61
- });
62
-
63
- test("a named environment when none are defined throws", () => {
64
- assert.throws(
65
- () => resolveRunSettings({}, { environment: "staging" }),
66
- (err) =>
67
- err instanceof UnknownEnvironmentError &&
68
- err.message === 'Unknown environment "staging". No environments are defined in samurai.config.ts',
69
- );
70
- });
71
- });
72
-
73
- describe("resolveRunSettings: precedence", () => {
74
- test("environment values beat project values", () => {
75
- const settings = resolveRunSettings(twoEnvironments, { environment: "staging" });
76
- assert.equal(settings.timeout, 45000);
77
- assert.equal(settings.expectTimeout, 8000);
78
- assert.equal(settings.baseURL, "https://harbor.shop");
79
- assert.deepEqual(settings.variables, { customerEmail: "sam@example.test" });
80
- });
81
-
82
- test("project values fill what the environment leaves out", () => {
83
- const settings = resolveRunSettings(twoEnvironments, { environment: "dev" });
84
- assert.equal(settings.timeout, 20000);
85
- assert.equal(settings.expectTimeout, 3000);
86
- assert.equal(settings.baseURL, "http://localhost:3000");
87
- });
88
-
89
- test("run overrides beat everything", () => {
90
- const settings = resolveRunSettings(twoEnvironments, { environment: "staging", timeout: 1, expectTimeout: 2 });
91
- assert.equal(settings.timeout, 1);
92
- assert.equal(settings.expectTimeout, 2);
93
- });
94
-
95
- test("built-in defaults apply last", () => {
96
- const settings = resolveRunSettings({ environments: { qa: {} } });
97
- assert.equal(settings.timeout, 30000);
98
- assert.equal(settings.expectTimeout, 5000);
99
- assert.equal("baseURL" in settings, false);
100
- });
101
-
102
- test("variables are a copy", () => {
103
- const config: SamuraiTestConfig = { environments: { qa: { variables: { a: 1 } } } };
104
- resolveRunSettings(config).variables.a = 2;
105
- assert.equal(config.environments?.qa?.variables?.a, 1);
106
- });
107
- });
108
-
109
- describe("parseRunOverrides", () => {
110
- test("reads --env, --timeout and --expect-timeout", () => {
111
- assert.deepEqual(parseRunOverrides(["--env", "dev", "--timeout", "1000", "--expect-timeout=200"]), {
112
- environment: "dev",
113
- timeout: 1000,
114
- expectTimeout: 200,
115
- });
116
- });
117
-
118
- test("falls back to SAMURAI_ENV", () => {
119
- assert.deepEqual(parseRunOverrides([], { SAMURAI_ENV: "staging" }), { environment: "staging" });
120
- assert.deepEqual(parseRunOverrides(["--env", "dev"], { SAMURAI_ENV: "staging" }), { environment: "dev" });
121
- });
122
-
123
- test("nothing given is no overrides", () => {
124
- assert.deepEqual(parseRunOverrides([], {}), {});
125
- });
126
-
127
- test("rejects bad overrides", () => {
128
- assert.throws(() => parseRunOverrides(["--env"]), /--env/);
129
- assert.throws(() => parseRunOverrides(["--timeout", "abc"]), {
130
- message: '--timeout must be a whole number of milliseconds, got "abc"',
131
- });
132
- // A value starting with "-" must use "=" (parseArgs treats "--timeout -5" as a missing value)
133
- assert.throws(() => parseRunOverrides(["--timeout=-5"]), /--timeout must be a whole number/);
134
- assert.throws(() => parseRunOverrides(["--expect-timeout", ""]), /--expect-timeout must be a whole number/);
135
- });
136
- });
137
-
138
- describe("active run settings", () => {
139
- afterEach(() => setRunSettings(undefined));
140
-
141
- test("nothing is active until set", () => {
142
- assert.equal(peekRunSettings(), undefined);
143
- assert.throws(() => getRunSettings(), /No test run is active/);
144
- });
145
-
146
- test("set settings are returned", () => {
147
- const settings = resolveRunSettings({});
148
- setRunSettings(settings);
149
- assert.equal(peekRunSettings(), settings);
150
- assert.equal(getRunSettings(), settings);
151
- });
152
- });
153
-
154
- test("prototype keys are not environments", () => {
155
- for (const name of ["constructor", "toString", "__proto__"]) {
156
- assert.throws(
157
- () => resolveRunSettings(twoEnvironments, { environment: name }),
158
- (err) =>
159
- err instanceof UnknownEnvironmentError &&
160
- err.message === `Unknown environment "${name}". Available: dev, staging`,
161
- );
162
- }
163
- });
@@ -1,114 +0,0 @@
1
- import { parseArgs } from "node:util";
2
- import type { SamuraiTestConfig } from "../types/config.js";
3
-
4
- export type Variables = Record<string, string | number | boolean>;
5
-
6
- /** Values given for one run (CLI flags, SAMURAI_ENV); they beat the config */
7
- export interface RunOverrides {
8
- environment?: string;
9
- timeout?: number;
10
- expectTimeout?: number;
11
- }
12
-
13
- /** Everything about a run that depends on the chosen environment */
14
- export interface RunSettings {
15
- environment: string;
16
- baseURL?: string;
17
- /** Time (ms) one test may take */
18
- timeout: number;
19
- /** Time (ms) actions and assertions retry */
20
- expectTimeout: number;
21
- variables: Variables;
22
- }
23
-
24
- const DEFAULT_TIMEOUT = 30000;
25
- const DEFAULT_EXPECT_TIMEOUT = 5000;
26
- /** The environment of a config without `environments` */
27
- const IMPLICIT_ENVIRONMENT = "default";
28
-
29
- export class UnknownEnvironmentError extends Error {
30
- constructor(message: string) {
31
- super(message);
32
- this.name = "UnknownEnvironmentError";
33
- }
34
- }
35
-
36
- /** Picks the environment and applies precedence: run override > environment > project > built-in default */
37
- export function resolveRunSettings(config: SamuraiTestConfig, overrides: RunOverrides = {}): RunSettings {
38
- const environments = config.environments ?? {};
39
- const names = Object.keys(environments);
40
- const available = `Available: ${names.join(", ")}`;
41
-
42
- let name = overrides.environment ?? config.defaultEnvironment;
43
- if (name === undefined) {
44
- if (names.length === 0) name = IMPLICIT_ENVIRONMENT;
45
- else if (names.length === 1) name = names[0]!;
46
- else throw new UnknownEnvironmentError(`No environment chosen. ${available} (use --env or set defaultEnvironment)`);
47
- }
48
-
49
- const environment = Object.prototype.hasOwnProperty.call(environments, name) ? environments[name] : undefined;
50
- if (!environment && !(names.length === 0 && name === IMPLICIT_ENVIRONMENT)) {
51
- throw new UnknownEnvironmentError(
52
- names.length === 0
53
- ? `Unknown environment "${name}". No environments are defined in samurai.config.ts`
54
- : `Unknown environment "${name}". ${available}`,
55
- );
56
- }
57
-
58
- const baseURL = environment?.baseURL ?? config.baseURL;
59
- return {
60
- environment: name,
61
- ...(baseURL !== undefined && { baseURL }),
62
- timeout: overrides.timeout ?? environment?.timeout ?? config.timeout ?? DEFAULT_TIMEOUT,
63
- expectTimeout:
64
- overrides.expectTimeout ?? environment?.expect?.timeout ?? config.expect?.timeout ?? DEFAULT_EXPECT_TIMEOUT,
65
- variables: { ...(environment?.variables ?? {}) },
66
- };
67
- }
68
-
69
- /** Reads `--env`, `--timeout` and `--expect-timeout` from `argv`; the environment falls back to SAMURAI_ENV */
70
- export function parseRunOverrides(argv: string[], env: Partial<NodeJS.ProcessEnv> = {}): RunOverrides {
71
- const { values } = parseArgs({
72
- args: argv,
73
- options: {
74
- env: { type: "string" },
75
- timeout: { type: "string" },
76
- "expect-timeout": { type: "string" },
77
- },
78
- strict: true,
79
- allowPositionals: true,
80
- });
81
- const overrides: RunOverrides = {};
82
- const environment = values.env ?? env.SAMURAI_ENV;
83
- if (environment) overrides.environment = environment;
84
- if (values.timeout !== undefined) overrides.timeout = milliseconds("--timeout", values.timeout);
85
- if (values["expect-timeout"] !== undefined) {
86
- overrides.expectTimeout = milliseconds("--expect-timeout", values["expect-timeout"]);
87
- }
88
- return overrides;
89
- }
90
-
91
- function milliseconds(flag: string, raw: string): number {
92
- const value = Number(raw);
93
- if (raw.trim() === "" || !Number.isInteger(value) || value < 0) {
94
- throw new Error(`${flag} must be a whole number of milliseconds, got "${raw}"`);
95
- }
96
- return value;
97
- }
98
-
99
- let active: RunSettings | undefined;
100
-
101
- /** Makes `settings` the run's settings; `undefined` ends the run (tests use this to reset) */
102
- export function setRunSettings(settings: RunSettings | undefined): void {
103
- active = settings;
104
- }
105
-
106
- /** The active run's settings, or undefined outside a run (unit and browser tests) */
107
- export function peekRunSettings(): RunSettings | undefined {
108
- return active;
109
- }
110
-
111
- export function getRunSettings(): RunSettings {
112
- if (!active) throw new Error("No test run is active; run settings are set when the runner starts");
113
- return active;
114
- }
@@ -1,70 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import path from "node:path";
3
- import { inspect, parseEnv } from "node:util";
4
- import logger from "../logger/index.js";
5
- import { MASK } from "./mask.js";
6
- import { isPassthroughKey } from "./variables.js";
7
-
8
- const PREFIX = "SAMURAI_SECRET_";
9
- const NAME = /^[A-Z][A-Z0-9_]*$/;
10
-
11
- export class MissingSecretError extends Error {
12
- constructor(name: string, environment: string) {
13
- super(`Secret "${name}" is not set (expected env var ${PREFIX}${name} or .env.${environment})`);
14
- this.name = "MissingSecretError";
15
- }
16
- }
17
-
18
- /** Secrets by name: `.env.<environment>` in `cwd` first, then env vars (which win) */
19
- export function loadSecrets(
20
- environment: string,
21
- options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
22
- ): Map<string, string> {
23
- const file = path.join(options.cwd ?? process.cwd(), `.env.${environment}`);
24
- const sources: Array<Record<string, string | undefined>> = [];
25
- if (existsSync(file)) sources.push(parseEnv(readFileSync(file, "utf8")) as Record<string, string>);
26
- sources.push(options.env ?? process.env);
27
-
28
- const secrets = new Map<string, string>();
29
- for (const source of sources) {
30
- for (const [key, value] of Object.entries(source)) {
31
- if (!key.startsWith(PREFIX) || value === undefined || value === "") continue;
32
- const name = key.slice(PREFIX.length);
33
- if (!NAME.test(name)) {
34
- logger.warn(`Ignoring ${key}: secret names must be UPPER_SNAKE_CASE`);
35
- continue;
36
- }
37
- secrets.set(name, value);
38
- }
39
- }
40
- return secrets;
41
- }
42
-
43
- /** The test's `secrets`: values by name, read-only; JSON and inspect show only masks */
44
- export function createSecretsFixture(
45
- environment: string,
46
- values: ReadonlyMap<string, string>,
47
- ): Readonly<Record<string, string>> {
48
- const masked = () => Object.fromEntries([...values.keys()].map((name) => [name, MASK]));
49
- // Not frozen: the get trap returns the real value, which a frozen target's invariants forbid
50
- const target: Record<string, string> = masked();
51
- Object.defineProperty(target, "toJSON", { value: masked, enumerable: false });
52
- Object.defineProperty(target, inspect.custom, { value: masked, enumerable: false });
53
- const readOnly = (): never => {
54
- throw new TypeError("secrets are read-only");
55
- };
56
-
57
- return new Proxy(target, {
58
- get(target, key, receiver) {
59
- if (isPassthroughKey(target, key)) return Reflect.get(target, key, receiver);
60
- const name = String(key);
61
- if (!NAME.test(name)) throw new Error(`Invalid secret name "${name}" (use UPPER_SNAKE_CASE)`);
62
- const value = values.get(name);
63
- if (value === undefined) throw new MissingSecretError(name, environment);
64
- return value;
65
- },
66
- set: readOnly,
67
- defineProperty: readOnly,
68
- deleteProperty: readOnly,
69
- });
70
- }
@@ -1,31 +0,0 @@
1
- import type { Variables } from "./run-settings.js";
2
-
3
- export class UnknownVariableError extends Error {
4
- constructor(name: string, environment: string) {
5
- super(`Variable "${name}" is not defined in environment "${environment}"`);
6
- this.name = "UnknownVariableError";
7
- }
8
- }
9
-
10
- /**
11
- * Keys that every object answers and that `await`, JSON.stringify and util.inspect probe:
12
- * symbols, `then`, `toJSON` and Object.prototype members. Reading them never throws.
13
- */
14
- export function isPassthroughKey(target: object, key: string | symbol): boolean {
15
- return (
16
- typeof key === "symbol" ||
17
- key === "then" ||
18
- key === "toJSON" ||
19
- (!Object.prototype.hasOwnProperty.call(target, key) && key in target)
20
- );
21
- }
22
-
23
- /** The test's `env`: the environment's variables, read-only; an unknown name throws */
24
- export function createEnvFixture(environment: string, variables: Variables): Readonly<Variables> {
25
- return new Proxy(Object.freeze({ ...variables }), {
26
- get(target, key, receiver) {
27
- if (isPassthroughKey(target, key) || Object.prototype.hasOwnProperty.call(target, key)) return Reflect.get(target, key, receiver);
28
- throw new UnknownVariableError(String(key), environment);
29
- },
30
- });
31
- }
package/src/index.ts DELETED
@@ -1,10 +0,0 @@
1
- import { loadConfig } from "./config/config.js";
2
- import { parseRunOverrides } from "./config/run-settings.js";
3
- import { prepareRun } from "./runner/prepare-run.js";
4
- import TestRunner from "./runner/test-runner.js";
5
-
6
- const run = prepareRun(await loadConfig(), parseRunOverrides(process.argv.slice(2), process.env));
7
- const runner = await TestRunner.init(run);
8
- await runner.start();
9
-
10
- process.exit();
@@ -1,83 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { ActionTimeoutError } from "./action-timeout-error.js";
4
-
5
- const base = { action: "click", selector: '//button[@type="submit"]', timeout: 5000 } as const;
6
-
7
- test("unreadable: no probe completed", () => {
8
- const err = new ActionTimeoutError({ ...base, reason: "unreadable" });
9
- assert.equal(err.message, 'click(): could not read //button[@type="submit"] within 5000ms');
10
- });
11
-
12
- test("not attached", () => {
13
- const err = new ActionTimeoutError({
14
- ...base,
15
- reason: "not-attached",
16
- checks: { attached: "fail", visible: "pending" },
17
- });
18
- assert.equal(err.message, 'click(): //button[@type="submit"] was not attached within 5000ms');
19
- });
20
-
21
- test("wrong state for waitFor", () => {
22
- const err = new ActionTimeoutError({
23
- action: "waitFor",
24
- selector: '//div[@class="spinner"]',
25
- timeout: 300,
26
- reason: "wrong-state",
27
- state: "hidden",
28
- });
29
- assert.equal(err.message, 'waitFor(): //div[@class="spinner"] did not become hidden within 300ms');
30
- });
31
-
32
- test("not actionable lists every check in order", () => {
33
- const err = new ActionTimeoutError({
34
- ...base,
35
- reason: "not-actionable",
36
- checks: {
37
- attached: "pass",
38
- visible: "pass",
39
- stable: "pass",
40
- enabled: "fail",
41
- "hit target": "pending",
42
- },
43
- });
44
- assert.equal(
45
- err.message,
46
- 'click(): //button[@type="submit"] was not actionable within 5000ms\n' +
47
- " attached ✓ visible ✓ stable ✓ enabled ✗ hit target —",
48
- );
49
- });
50
-
51
- test("a failed hit target names the covering element", () => {
52
- const err = new ActionTimeoutError({
53
- ...base,
54
- reason: "not-actionable",
55
- checks: {
56
- attached: "pass",
57
- visible: "pass",
58
- stable: "pass",
59
- enabled: "pass",
60
- "hit target": "fail",
61
- },
62
- coveredBy: "div#cookie-banner.overlay",
63
- });
64
- assert.equal(
65
- err.message,
66
- 'click(): //button[@type="submit"] was not actionable within 5000ms\n' +
67
- " attached ✓ visible ✓ stable ✓ enabled ✓ hit target ✗ (covered by div#cookie-banner.overlay)",
68
- );
69
- });
70
-
71
- test("exposes its details", () => {
72
- const checks = { attached: "fail" } as const;
73
- const err = new ActionTimeoutError({ ...base, reason: "not-attached", checks });
74
- assert.ok(err instanceof Error);
75
- assert.equal(err.name, "ActionTimeoutError");
76
- assert.equal(err.action, "click");
77
- assert.equal(err.selector, '//button[@type="submit"]');
78
- assert.equal(err.timeout, 5000);
79
- assert.equal(err.reason, "not-attached");
80
- assert.deepEqual(err.checks, checks);
81
- assert.equal(err.coveredBy, undefined);
82
- assert.equal(err.state, undefined);
83
- });
@@ -1,72 +0,0 @@
1
- import type { CheckName, CheckResult, CheckResults, WaitForState } from "./element-state.js";
2
-
3
- export type ActionTimeoutReason = "unreadable" | "not-attached" | "not-actionable" | "wrong-state";
4
-
5
- export interface ActionTimeoutDetails {
6
- /** Locator method that timed out, e.g. "click" */
7
- action: string;
8
- selector: string;
9
- timeout: number;
10
- reason: ActionTimeoutReason;
11
- /** Check results from the last probe, in check order */
12
- checks?: CheckResults | undefined;
13
- /** Description of the element covering the target, when the hit-target check failed */
14
- coveredBy?: string | undefined;
15
- /** Target state, for waitFor */
16
- state?: WaitForState | undefined;
17
- }
18
-
19
- const SYMBOLS: Record<CheckResult, string> = { pass: "✓", fail: "✗", pending: "—" };
20
-
21
- function formatMessage({
22
- action,
23
- selector,
24
- timeout,
25
- reason,
26
- checks,
27
- coveredBy,
28
- state,
29
- }: ActionTimeoutDetails) {
30
- switch (reason) {
31
- case "unreadable":
32
- return `${action}(): could not read ${selector} within ${timeout}ms`;
33
- case "not-attached":
34
- return `${action}(): ${selector} was not attached within ${timeout}ms`;
35
- case "wrong-state":
36
- return `${action}(): ${selector} did not become ${state} within ${timeout}ms`;
37
- case "not-actionable": {
38
- const summary = (Object.entries(checks ?? {}) as Array<[CheckName, CheckResult]>)
39
- .map(([name, result]) => {
40
- const covered =
41
- name === "hit target" && result === "fail" && coveredBy
42
- ? ` (covered by ${coveredBy})`
43
- : "";
44
- return `${name} ${SYMBOLS[result]}${covered}`;
45
- })
46
- .join(" ");
47
- return `${action}(): ${selector} was not actionable within ${timeout}ms\n ${summary}`;
48
- }
49
- }
50
- }
51
-
52
- export class ActionTimeoutError extends Error {
53
- public readonly action: string;
54
- public readonly selector: string;
55
- public readonly timeout: number;
56
- public readonly reason: ActionTimeoutReason;
57
- public readonly checks: CheckResults | undefined;
58
- public readonly coveredBy: string | undefined;
59
- public readonly state: WaitForState | undefined;
60
-
61
- constructor(details: ActionTimeoutDetails) {
62
- super(formatMessage(details));
63
- this.name = "ActionTimeoutError";
64
- this.action = details.action;
65
- this.selector = details.selector;
66
- this.timeout = details.timeout;
67
- this.reason = details.reason;
68
- this.checks = details.checks;
69
- this.coveredBy = details.coveredBy;
70
- this.state = details.state;
71
- }
72
- }