@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,82 +0,0 @@
1
- import type { LogRecord, PageError } from "../browser/page-logs.js";
2
- import type { SamuraiTestConfig } from "../types/config.js";
3
- import type { TestError } from "../types/test.js";
4
- import { toTestError } from "./test-error.js";
5
-
6
- /** Config `logs` */
7
- export type LogsConfig = NonNullable<SamuraiTestConfig["logs"]>;
8
-
9
- /** The page threw exceptions nobody caught, and config `logs.failOnPageError` says that fails the test */
10
- export class PageErrorsError extends Error {
11
- public readonly errors: readonly PageError[];
12
-
13
- constructor(errors: readonly PageError[]) {
14
- super(`${errors.length} uncaught page error(s): ${errors.map((error) => error.message).join("\n")}`);
15
- this.name = "PageErrorsError";
16
- this.errors = errors;
17
- }
18
- }
19
-
20
- /** A route handler threw: the test's mocks did not do what the test meant */
21
- export class RouteErrorsError extends Error {
22
- public readonly errors: readonly Error[];
23
-
24
- constructor(errors: readonly Error[]) {
25
- super(`${errors.length} route handler error(s): ${errors.map((error) => error.message).join("\n")}`);
26
- this.name = "RouteErrorsError";
27
- this.errors = errors;
28
- }
29
- }
30
-
31
- export interface PageLogsInput {
32
- config: LogsConfig | undefined;
33
- entries: readonly LogRecord[];
34
- errors: readonly PageError[];
35
- /** How many entries the page's buffer dropped */
36
- dropped: number;
37
- /** The test body already failed (or timed out) */
38
- testFailed: boolean;
39
- /** The test called `page.allowPageErrors()` */
40
- allowPageErrors: boolean;
41
- /** Errors thrown by `page.route` handlers; they fail a passing test */
42
- routeErrors?: readonly Error[];
43
- }
44
-
45
- export interface PageLogsVerdict {
46
- /** Set when a test that passed has to fail because of page errors */
47
- error?: TestError;
48
- /** The entries to put in the report, per `logs.capture` */
49
- logs?: LogRecord[];
50
- logsDropped?: number;
51
- }
52
-
53
- function isIgnored(error: PageError, ignore: readonly (string | RegExp)[]): boolean {
54
- return ignore.some((pattern) =>
55
- typeof pattern === "string" ? error.message.includes(pattern) : pattern.test(error.message),
56
- );
57
- }
58
-
59
- /**
60
- * Decides, once a test body is done, whether page errors fail it and which log entries go in the report:
61
- * `capture` "failures" (default) attaches them to failed tests, "all" always, "off" never.
62
- */
63
- export function judgePageLogs(input: PageLogsInput): PageLogsVerdict {
64
- const { config, entries, errors, dropped, testFailed, allowPageErrors, routeErrors = [] } = input;
65
- const capture = config?.capture ?? "failures";
66
-
67
- let error: TestError | undefined;
68
- if (routeErrors.length > 0 && !testFailed) {
69
- error = toTestError(new RouteErrorsError(routeErrors));
70
- } else if (config?.failOnPageError && !testFailed && !allowPageErrors) {
71
- const counted = errors.filter((pageError) => !isIgnored(pageError, config.ignoreErrors ?? []));
72
- if (counted.length > 0) error = toTestError(new PageErrorsError(counted));
73
- }
74
-
75
- const failed = testFailed || error !== undefined;
76
- const attach = capture === "all" || (capture === "failures" && failed);
77
- return {
78
- ...(error && { error }),
79
- ...(attach && { logs: [...entries] }),
80
- ...(attach && dropped > 0 && { logsDropped: dropped }),
81
- };
82
- }
@@ -1,33 +0,0 @@
1
- import { after, afterEach, test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import path from "node:path";
6
- import { prepareRun } from "./prepare-run.js";
7
- import { peekRunSettings, setRunSettings } from "../config/run-settings.js";
8
- import { MASK, maskText, resetSecrets } from "../config/mask.js";
9
-
10
- const dir = mkdtempSync(path.join(tmpdir(), "samurai-run-"));
11
- writeFileSync(path.join(dir, ".env.staging"), "SAMURAI_SECRET_TEST_CARD=4242424242424242\n");
12
- after(() => rmSync(dir, { recursive: true, force: true }));
13
- afterEach(() => {
14
- setRunSettings(undefined);
15
- resetSecrets();
16
- });
17
-
18
- test("resolves settings, loads secrets for that environment and activates the run", () => {
19
- const run = prepareRun(
20
- { environments: { dev: {}, staging: { baseURL: "https://staging.harbor.shop" } } },
21
- { environment: "staging" },
22
- { cwd: dir, env: { SAMURAI_SECRET_PIN: "12" } as unknown as NodeJS.ProcessEnv },
23
- );
24
- assert.equal(run.settings.environment, "staging");
25
- assert.equal(peekRunSettings(), run.settings);
26
- assert.deepEqual(Object.fromEntries(run.secrets), { TEST_CARD: "4242424242424242", PIN: "12" });
27
- assert.equal(maskText("card 4242424242424242 pin 12"), `card ${MASK} pin ${MASK}`);
28
- });
29
-
30
- test("an unknown environment fails before anything is activated", () => {
31
- assert.throws(() => prepareRun({ environments: { dev: {} } }, { environment: "stg" }, { cwd: dir, env: {} as unknown as NodeJS.ProcessEnv }));
32
- assert.equal(peekRunSettings(), undefined);
33
- });
@@ -1,36 +0,0 @@
1
- import { isShortSecret, registerSecret } from "../config/mask.js";
2
- import {
3
- resolveRunSettings,
4
- setRunSettings,
5
- type RunOverrides,
6
- type RunSettings,
7
- } from "../config/run-settings.js";
8
- import { loadSecrets } from "../config/secrets.js";
9
- import logger from "../logger/index.js";
10
- import type { SamuraiTestConfig } from "../types/config.js";
11
-
12
- export interface PreparedRun {
13
- settings: RunSettings;
14
- secrets: ReadonlyMap<string, string>;
15
- }
16
-
17
- /**
18
- * Starts a run: picks the environment, loads its secrets and registers every value for masking
19
- * (all of them, not only the ones a test reads), then makes the settings active.
20
- */
21
- export function prepareRun(
22
- config: SamuraiTestConfig,
23
- overrides: RunOverrides,
24
- options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
25
- ): PreparedRun {
26
- const settings = resolveRunSettings(config, overrides);
27
- const secrets = loadSecrets(settings.environment, options);
28
- for (const [name, value] of secrets) {
29
- registerSecret(name, value);
30
- if (isShortSecret(value)) {
31
- logger.warn(`Secret "${name}" is short; masking it may hide unrelated text in logs`);
32
- }
33
- }
34
- setRunSettings(settings);
35
- return { settings, secrets };
36
- }
@@ -1,59 +0,0 @@
1
- import { afterEach, test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { fileURLToPath } from "node:url";
4
- import {
5
- clearRegistry,
6
- describe as samuraiDescribe,
7
- registeredTests,
8
- test as samuraiTest,
9
- } from "./registry.js";
10
-
11
- afterEach(() => clearRegistry());
12
-
13
- const noop = async () => {};
14
-
15
- test("a test outside describe keeps its title", () => {
16
- samuraiTest("logs in", noop);
17
- const [registered] = registeredTests();
18
- assert.equal(registered?.name, "logs in");
19
- assert.deepEqual(registered?.titlePath, ["logs in"]);
20
- assert.equal(registered?.function, noop);
21
- });
22
-
23
- test("nested describe titles are joined with >", () => {
24
- samuraiDescribe("Checkout", () => {
25
- samuraiDescribe("Guest", () => {
26
- samuraiTest("applies coupon", noop);
27
- });
28
- samuraiTest("pays with saved card", noop);
29
- });
30
- samuraiTest("after", noop);
31
- assert.deepEqual(
32
- registeredTests().map((t) => t.name),
33
- ["Checkout > Guest > applies coupon", "Checkout > pays with saved card", "after"],
34
- );
35
- });
36
-
37
- test("the title stack is restored when a describe callback throws", () => {
38
- assert.throws(() =>
39
- samuraiDescribe("Broken", () => {
40
- throw new Error("boom");
41
- }),
42
- );
43
- samuraiTest("after", noop);
44
- assert.equal(registeredTests()[0]?.name, "after");
45
- });
46
-
47
- test("an async describe callback is rejected", () => {
48
- assert.throws(
49
- () => samuraiDescribe("Async", async () => {}),
50
- { message: "describe() callback must be synchronous; it only registers tests" },
51
- );
52
- samuraiTest("after", noop);
53
- assert.equal(registeredTests()[0]?.name, "after");
54
- });
55
-
56
- test("records the calling spec file", () => {
57
- samuraiTest("where", noop);
58
- assert.equal(registeredTests()[0]?.file, fileURLToPath(import.meta.url));
59
- });
@@ -1,46 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import logger from "../logger/index.js";
3
- import type { RegisteredTestCase, TestCaseBase } from "../types/test.js";
4
-
5
- const registered: RegisteredTestCase[] = [];
6
- /** Titles of the describe blocks being registered, outermost first */
7
- const titleStack: string[] = [];
8
-
9
- /** Groups the tests registered inside `fn` under `title`. `fn` runs immediately and must be synchronous. */
10
- export function describe(title: string, fn: () => void): void {
11
- titleStack.push(title);
12
- try {
13
- const result: unknown = fn();
14
- if (result instanceof Promise) {
15
- result.catch(() => {});
16
- throw new Error("describe() callback must be synchronous; it only registers tests");
17
- }
18
- } finally {
19
- titleStack.pop();
20
- }
21
- }
22
-
23
- export function test(title: string, fn: TestCaseBase["function"]): void {
24
- const titlePath = [...titleStack, title];
25
- const name = titlePath.join(" > ");
26
- registered.push({ name, titlePath, function: fn, file: callerFile() });
27
- logger.debug("Registered test %s", name);
28
- }
29
-
30
- export function registeredTests(): readonly RegisteredTestCase[] {
31
- return registered;
32
- }
33
-
34
- /** Forgets every registered test (tests only) */
35
- export function clearRegistry(): void {
36
- registered.length = 0;
37
- titleStack.length = 0;
38
- }
39
-
40
- /** File of the code that called test(): stack lines are Error, callerFile, test, caller */
41
- function callerFile(): string {
42
- const caller = new Error().stack?.split("\n")[3] ?? "";
43
- const match = caller.match(/\((.+?):\d+:\d+\)$/) ?? caller.match(/at (.+?):\d+:\d+$/);
44
- const location = match?.[1] ?? "";
45
- return location.startsWith("file://") ? fileURLToPath(location) : location;
46
- }
@@ -1,73 +0,0 @@
1
- import { after, afterEach, test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import path from "node:path";
6
- import TestReporter from "./reporter.js";
7
- import { MASK, registerSecret, resetSecrets } from "../config/mask.js";
8
- import type { RegisteredTestCase } from "../types/test.js";
9
-
10
- const dir = mkdtempSync(path.join(tmpdir(), "samurai-reporter-"));
11
- after(() => rmSync(dir, { recursive: true, force: true }));
12
- afterEach(() => resetSecrets());
13
-
14
- function testCase(file: string, name: string): RegisteredTestCase {
15
- return { file, name, titlePath: name.split(" > "), function: async () => {} };
16
- }
17
-
18
- async function report(run: (reporter: TestReporter) => void) {
19
- const output = path.join(dir, `${Math.random()}.json`);
20
- const reporter = new TestReporter({ environment: "staging", output });
21
- reporter.onStart();
22
- run(reporter);
23
- await reporter.onEnd();
24
- return JSON.parse(readFileSync(output, "utf8"));
25
- }
26
-
27
- test("records the environment", async () => {
28
- const summary = await report(() => {});
29
- assert.equal(summary.environment, "staging");
30
- });
31
-
32
- test("tests with the same title in different files are kept apart", async () => {
33
- const a = testCase("a.spec.ts", "Checkout > pays");
34
- const b = testCase("b.spec.ts", "Checkout > pays");
35
- const summary = await report((reporter) => {
36
- reporter.onTestStart(a);
37
- reporter.onTestStart(b);
38
- reporter.onTestEnd(a);
39
- reporter.onTestEnd(b, { message: "boom", type: "error" });
40
- });
41
- assert.deepEqual(
42
- summary.tests.map((t: { file: string; name: string; status: string }) => [t.file, t.name, t.status]),
43
- [
44
- ["a.spec.ts", "Checkout > pays", "success"],
45
- ["b.spec.ts", "Checkout > pays", "failed"],
46
- ],
47
- );
48
- });
49
-
50
- test("masks secrets in errors and logs", async () => {
51
- registerSecret("TEST_CARD", "4242424242424242");
52
- const t = testCase("a.spec.ts", "pays");
53
- const summary = await report((reporter) => {
54
- reporter.onTestStart(t);
55
- reporter.onTestEnd(
56
- t,
57
- {
58
- message: 'expected "4242424242424242"',
59
- type: "assertion",
60
- stack: "AssertionError: expected 4242424242424242\n at x",
61
- expected: "4242424242424242",
62
- actual: "4242",
63
- },
64
- { logs: [{ level: "info", type: "console", text: "card 4242424242424242", timestamp: 1 }] },
65
- );
66
- });
67
- const [result] = summary.tests;
68
- assert.equal(result.message, `expected "${MASK}"`);
69
- assert.equal(result.expected, MASK);
70
- assert.equal(result.actual, "4242");
71
- assert.doesNotMatch(result.stack, /4242424242424242/);
72
- assert.equal(result.logs[0].text, `card ${MASK}`);
73
- });
@@ -1,109 +0,0 @@
1
- import { mkdir, writeFile } from "fs/promises";
2
- import path from "path";
3
- import { maskDeep } from "../config/mask.js";
4
- import type {
5
- PartialTestResult,
6
- RegisteredTestCase,
7
- TestError,
8
- TestLogEntry,
9
- TestResult,
10
- TestSummary,
11
- } from "../types/test.js";
12
-
13
- export interface ReporterOptions {
14
- /** The environment the run uses, recorded in the report */
15
- environment: string;
16
- /** Where the report is written. @default <cwd>/result/report.json */
17
- output?: string;
18
- }
19
-
20
- /** Results are keyed by file and full title, so equal titles in different files or describe blocks stay apart */
21
- function keyOf(test: RegisteredTestCase): string {
22
- return `${test.file}::${test.name}`;
23
- }
24
-
25
- export default class TestReporter {
26
- private testResults: Map<string, TestResult> = new Map();
27
- private summary: TestSummary | undefined;
28
- private environment: string;
29
- private output: string;
30
-
31
- constructor(options: ReporterOptions) {
32
- this.environment = options.environment;
33
- this.output = options.output ?? path.join(process.cwd(), "result/report.json");
34
- }
35
-
36
- public onStart() {
37
- const t = performance.mark(`group-start`);
38
- this.summary = {
39
- duration: 0,
40
- status: "failed",
41
- startTime: t.startTime,
42
- environment: this.environment,
43
- tests: [],
44
- };
45
- }
46
-
47
- public async onEnd() {
48
- performance.mark(`group-finish`);
49
- const duration = performance.measure(
50
- "test-duration",
51
- `group-start`,
52
- `group-finish`,
53
- ).duration;
54
-
55
- this.summary = {
56
- ...this.summary,
57
- duration,
58
- environment: this.environment,
59
- tests: Array.from(this.testResults.values()),
60
- status: Array.from(this.testResults.values()).every(
61
- (r: TestResult) => r.status === "success",
62
- )
63
- ? "success"
64
- : "failed",
65
- } as TestSummary;
66
-
67
- await mkdir(path.dirname(this.output), { recursive: true });
68
- await writeFile(this.output, JSON.stringify(this.summary, null, 2), {
69
- encoding: "utf8",
70
- });
71
- }
72
-
73
- public onTestStart(test: RegisteredTestCase) {
74
- const key = keyOf(test);
75
- const t = performance.mark(`${key}-start`);
76
- this.testResults.set(key, {
77
- name: test.name,
78
- file: test.file,
79
- status: "started",
80
- startTime: t.startTime,
81
- });
82
- }
83
-
84
- public onTestEnd(
85
- test: RegisteredTestCase,
86
- error?: TestError,
87
- logs?: { logs?: TestLogEntry[]; logsDropped?: number },
88
- ) {
89
- const key = keyOf(test);
90
- performance.mark(`${key}-finish`);
91
- const duration = performance.measure(
92
- "test-duration",
93
- `${key}-start`,
94
- `${key}-finish`,
95
- ).duration;
96
-
97
- this.testResults.set(
98
- key,
99
- maskDeep({
100
- ...(this.testResults.get(key) as PartialTestResult),
101
- duration,
102
- status: error ? "failed" : "success",
103
- ...(error ? error : undefined),
104
- ...(logs?.logs && { logs: logs.logs }),
105
- ...(logs?.logsDropped && { logsDropped: logs.logsDropped }),
106
- } as TestResult),
107
- );
108
- }
109
- }
@@ -1,39 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { toTestError } from "./test-error.js";
4
- import { AssertionError } from "../assert/assertion-error.js";
5
-
6
- test("AssertionError becomes an assertion error with expected/actual", () => {
7
- const err = new AssertionError({
8
- matcher: "toHaveText",
9
- expected: /contact/i,
10
- actual: "Home",
11
- locator: "//h1",
12
- });
13
- const result = toTestError(err);
14
- assert.equal(result.type, "assertion");
15
- assert.equal(result.message, err.message);
16
- assert.equal(result.stack, err.stack);
17
- assert.equal(result.expected, "/contact/i");
18
- assert.equal(result.actual, "Home");
19
- });
20
-
21
- test("a BigInt AssertionError can be written to the report", () => {
22
- const result = toTestError(new AssertionError({ matcher: "toBe", expected: 4n, actual: 3n }));
23
- assert.doesNotThrow(() => JSON.stringify(result));
24
- assert.equal(result.expected, "4n");
25
- assert.equal(result.actual, "3n");
26
- });
27
-
28
- test("other errors stay type error", () => {
29
- const err = new Error("boom");
30
- assert.deepEqual(toTestError(err), { message: "boom", stack: err.stack, type: "error" });
31
- });
32
-
33
- test("thrown strings stay type error", () => {
34
- assert.deepEqual(toTestError("PORT 9223 is already in use!"), {
35
- message: "PORT 9223 is already in use!",
36
- stack: undefined,
37
- type: "error",
38
- });
39
- });
@@ -1,20 +0,0 @@
1
- import { AssertionError, toReportValue } from "../assert/assertion-error.js";
2
- import type { TestError } from "../types/test.js";
3
-
4
- /** Maps anything a test threw to the error stored in the report */
5
- export function toTestError(err: unknown): TestError {
6
- if (err instanceof AssertionError) {
7
- return {
8
- message: err.message,
9
- stack: err.stack,
10
- type: "assertion",
11
- expected: toReportValue(err.expected),
12
- actual: toReportValue(err.actual),
13
- };
14
- }
15
- return {
16
- message: err instanceof Error ? err.message : String(err),
17
- stack: err instanceof Error ? err.stack : undefined,
18
- type: "error",
19
- };
20
- }
@@ -1,184 +0,0 @@
1
- import assert from "assert";
2
- import { glob } from "node:fs/promises";
3
- import path from "path";
4
- import { pathToFileURL } from "url";
5
- import { Browser } from "../browser/browser.js";
6
- import { readConfig } from "../config/config.js";
7
- import logger from "../logger/index.js";
8
- import type { SupportedBrowser } from "../types/browser.js";
9
- import type { RegisteredTestCase, TestFixtures } from "../types/test.js";
10
- import { registeredTests } from "./registry.js";
11
- import type { PreparedRun } from "./prepare-run.js";
12
- import { createEnvFixture } from "../config/variables.js";
13
- import { createSecretsFixture } from "../config/secrets.js";
14
- import TestReporter from "./reporter.js";
15
- import type { SamuraiGroup } from "../types/config.js";
16
- import Page from "../browser/page.js";
17
- import { takePendingAssertions } from "../assert/expect.js";
18
- import { toTestError } from "./test-error.js";
19
- import { judgePageLogs } from "./page-logs-report.js";
20
- import type { LogsConfig } from "./page-logs-report.js";
21
-
22
- export default class TestRunner {
23
- private testFiles: string[];
24
- private run: PreparedRun;
25
- private group?: SamuraiGroup;
26
-
27
- constructor(testFiles: string[], run: PreparedRun, group?: SamuraiGroup) {
28
- this.testFiles = testFiles;
29
- this.run = run;
30
- this.group = group;
31
- }
32
-
33
- static async init(run: PreparedRun, group?: SamuraiGroup) {
34
- logger.verbose("Initializing test runner");
35
- let srcDir = "";
36
-
37
- if (group && group.src) {
38
- srcDir = group.src;
39
- } else {
40
- srcDir = (await readConfig("srcDir")) ?? "./src";
41
- }
42
-
43
- assert(srcDir, "No srcDir declared!");
44
-
45
- const cwd = path.resolve(srcDir);
46
- logger.verbose("Reading test files from: %s", cwd);
47
- const testFiles: string[] = [];
48
- for await (const entry of glob("**/*.spec.ts", {
49
- cwd,
50
- })) {
51
- testFiles.push(entry);
52
- }
53
- logger.debug("Found %d tests in srcDir", testFiles.length);
54
- return new TestRunner(testFiles, run, group);
55
- }
56
-
57
- public async start() {
58
- const reporter = new TestReporter({ environment: this.run.settings.environment });
59
- for (let i = 0; i < this.testFiles.length; i++) {
60
- const file = this.testFiles[i] as string;
61
- logger.verbose("Trying to register file: %s", file);
62
- const targetPath = path.resolve((await readConfig("srcDir")) ?? "./src", file);
63
- await import(pathToFileURL(targetPath).href);
64
- }
65
-
66
- logger.verbose("Running test files");
67
-
68
- // TODO: Implement test grouping
69
- reporter.onStart();
70
- for (const test of registeredTests()) {
71
- await this.executeTestCase(test, reporter);
72
- }
73
- await reporter.onEnd();
74
- }
75
-
76
- private async executeTestCase(
77
- test: RegisteredTestCase,
78
- reporter: TestReporter,
79
- ) {
80
- logger.verbose("Starting test: %s", test.name);
81
- reporter.onTestStart(test);
82
- const selectedBrowser: SupportedBrowser = (await readConfig("browser")) ?? "firefox";
83
- const { settings, secrets } = this.run;
84
- const timeout = settings.timeout;
85
-
86
- let browser: Browser | undefined;
87
- let page: Page | undefined;
88
- let timeoutTimer: NodeJS.Timeout | undefined;
89
- const timeoutController = new AbortController();
90
-
91
- await Promise.race([
92
- new Promise<void>((resolve) => {
93
- timeoutTimer = setTimeout(
94
- () => {
95
- reporter.onTestEnd(test, {
96
- message: `Test timed out after ${timeout / 1000} seconds`,
97
- type: "timeout",
98
- });
99
- timeoutController.abort(new Error("Test timed out"));
100
- resolve();
101
- },
102
- timeout,
103
- );
104
- }),
105
- new Promise<void>(async (resolve, reject) => {
106
- try {
107
- const launched = await Browser.launch(
108
- selectedBrowser,
109
- {
110
- port: 9223,
111
- headless: false,
112
- },
113
- timeoutController.signal,
114
- );
115
- browser = launched.browser;
116
- page = launched.page;
117
- const fixtures: TestFixtures = {
118
- page: launched.page,
119
- browser: launched.browser,
120
- env: createEnvFixture(settings.environment, settings.variables),
121
- secrets: createSecretsFixture(settings.environment, secrets),
122
- };
123
- await test.function(fixtures);
124
- // Already reported as timed out
125
- if (timeoutController.signal.aborted) return resolve();
126
- const unawaited = takePendingAssertions();
127
- if (unawaited.length > 0) {
128
- reporter.onTestEnd(
129
- test,
130
- {
131
- message: unawaited
132
- .map((matcher) => `expect(locator).${matcher}() was not awaited`)
133
- .join("\n"),
134
- type: "assertion",
135
- },
136
- await judgeLogs(page, true),
137
- );
138
- return resolve();
139
- }
140
- const verdict = await judgeLogs(page, false);
141
- reporter.onTestEnd(test, verdict.error, verdict);
142
- resolve();
143
- } catch (err) {
144
- if (timeoutController.signal.aborted) return resolve();
145
- reporter.onTestEnd(test, toTestError(err), await judgeLogs(page, true));
146
- resolve();
147
- }
148
- }),
149
- ]);
150
-
151
- clearTimeout(timeoutTimer);
152
- takePendingAssertions();
153
- logger.verbose("Closing browser for test: %s", test.name);
154
- await browser?.close();
155
- }
156
- }
157
-
158
- /** Reads the page's log once the test body is done and applies config `logs` (see judgePageLogs) */
159
- async function judgeLogs(page: Page | undefined, testFailed: boolean) {
160
- if (!page) return {};
161
- try {
162
- // Events and command replies share one ordered connection: after this, everything logged so far is in
163
- await page.syncLogs();
164
- } catch (err) {
165
- logger.debug("Could not sync the page's logs", { err });
166
- }
167
- let config: LogsConfig | undefined;
168
- try {
169
- config = await readConfig("logs");
170
- } catch (err) {
171
- logger.debug("Could not read logs config, using defaults", { err });
172
- }
173
- return judgePageLogs({
174
- config,
175
- entries: page.getLogs(),
176
- errors: page.pageErrors(),
177
- dropped: page.logsDropped,
178
- testFailed,
179
- allowPageErrors: page.pageErrorsAllowed,
180
- routeErrors: page.routeErrors(),
181
- });
182
- }
183
-
184
- export { describe, test } from "./registry.js";