@itmetsam/samurai-framework 0.9.0 → 0.12.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 (331) hide show
  1. package/README.md +4 -5
  2. package/dist/assert/assertion-error.d.ts +1 -1
  3. package/dist/assert/expect.d.ts +5 -50
  4. package/dist/assert/expect.js +5 -182
  5. package/dist/assert/locator-assertions.d.ts +22 -0
  6. package/dist/assert/locator-assertions.js +61 -0
  7. package/dist/assert/pending-assertions.d.ts +7 -0
  8. package/dist/assert/pending-assertions.js +40 -0
  9. package/dist/assert/text-match.d.ts +4 -0
  10. package/dist/assert/text-match.js +11 -0
  11. package/dist/assert/value-assertions.d.ts +23 -0
  12. package/dist/assert/value-assertions.js +82 -0
  13. package/dist/browser/browser-context.d.ts +0 -1
  14. package/dist/browser/browser-context.js +13 -5
  15. package/dist/browser/browser-finder.js +5 -9
  16. package/dist/browser/browser.d.ts +3 -31
  17. package/dist/browser/browser.js +36 -219
  18. package/dist/browser/chrome-launch.d.ts +14 -1
  19. package/dist/browser/chrome-launch.js +44 -1
  20. package/dist/browser/dialog-handling.d.ts +32 -0
  21. package/dist/browser/dialog-handling.js +85 -0
  22. package/dist/browser/document-watcher.d.ts +44 -0
  23. package/dist/browser/document-watcher.js +160 -0
  24. package/dist/browser/download-folders.d.ts +0 -1
  25. package/dist/browser/download-folders.js +7 -15
  26. package/dist/browser/download.js +5 -10
  27. package/dist/browser/downloads-dir.js +2 -2
  28. package/dist/browser/emit.d.ts +7 -0
  29. package/dist/browser/emit.js +16 -0
  30. package/dist/browser/emulation.d.ts +1 -1
  31. package/dist/browser/emulation.js +54 -18
  32. package/dist/browser/file-chooser.js +4 -11
  33. package/dist/browser/history.d.ts +6 -0
  34. package/dist/browser/history.js +89 -0
  35. package/dist/browser/index.d.ts +2 -0
  36. package/dist/browser/index.js +2 -0
  37. package/dist/browser/init-script.d.ts +2 -0
  38. package/dist/browser/init-script.js +16 -0
  39. package/dist/browser/launch-firefox.d.ts +20 -0
  40. package/dist/browser/launch-firefox.js +66 -0
  41. package/dist/browser/launch-flags.d.ts +18 -0
  42. package/dist/browser/launch-flags.js +100 -0
  43. package/dist/browser/lazy.d.ts +21 -0
  44. package/dist/browser/lazy.js +44 -0
  45. package/dist/browser/page-events.d.ts +44 -0
  46. package/dist/browser/page-events.js +58 -0
  47. package/dist/browser/page-logs.js +2 -9
  48. package/dist/browser/page-navigation.d.ts +58 -0
  49. package/dist/browser/page-navigation.js +108 -0
  50. package/dist/browser/page-network.d.ts +69 -0
  51. package/dist/browser/page-network.js +163 -0
  52. package/dist/browser/page-settings.d.ts +35 -0
  53. package/dist/browser/page-settings.js +51 -0
  54. package/dist/browser/page-transfers.d.ts +36 -0
  55. package/dist/browser/page-transfers.js +50 -0
  56. package/dist/browser/page.d.ts +39 -154
  57. package/dist/browser/page.js +134 -758
  58. package/dist/browser/pdf.d.ts +1 -1
  59. package/dist/browser/session.d.ts +16 -0
  60. package/dist/browser/session.js +32 -0
  61. package/dist/browser/viewport.d.ts +19 -0
  62. package/dist/browser/viewport.js +72 -0
  63. package/dist/browser/warm-up.d.ts +1 -1
  64. package/dist/browser/warm-up.js +2 -1
  65. package/dist/browsers/index.d.ts +1 -1
  66. package/dist/browsers/index.js +1 -1
  67. package/dist/browsers/pins.d.ts +0 -2
  68. package/dist/browsers/pins.js +0 -2
  69. package/dist/browsers/store.d.ts +0 -2
  70. package/dist/browsers/store.js +1 -2
  71. package/dist/cli/args.d.ts +19 -7
  72. package/dist/cli/args.js +18 -9
  73. package/dist/cli/browsers.d.ts +1 -2
  74. package/dist/cli/browsers.js +2 -2
  75. package/dist/cli/console-reporter.d.ts +4 -0
  76. package/dist/cli/console-reporter.js +15 -7
  77. package/dist/cli/flow-reporter.js +2 -1
  78. package/dist/cli/flow.d.ts +1 -2
  79. package/dist/cli/flow.js +25 -22
  80. package/dist/cli/init.js +3 -8
  81. package/dist/cli/io.d.ts +10 -0
  82. package/dist/cli/io.js +1 -0
  83. package/dist/cli/main.d.ts +3 -10
  84. package/dist/cli/main.js +11 -137
  85. package/dist/cli/paint.d.ts +2 -0
  86. package/dist/cli/paint.js +4 -0
  87. package/dist/cli/record.d.ts +4 -0
  88. package/dist/cli/record.js +93 -0
  89. package/dist/cli/usage.d.ts +1 -0
  90. package/dist/cli/usage.js +58 -0
  91. package/dist/cli/version.d.ts +2 -0
  92. package/dist/cli/version.js +7 -0
  93. package/dist/config/browsers.d.ts +11 -0
  94. package/dist/config/browsers.js +32 -0
  95. package/dist/config/config.js +8 -1
  96. package/dist/config/mask.d.ts +1 -1
  97. package/dist/config/mask.js +7 -2
  98. package/dist/config/run-settings.d.ts +1 -2
  99. package/dist/config/run-settings.js +1 -6
  100. package/dist/config/timeouts.d.ts +2 -0
  101. package/dist/config/timeouts.js +13 -0
  102. package/dist/config/workers.d.ts +14 -0
  103. package/dist/config/workers.js +29 -0
  104. package/dist/flows/run/branch-context.d.ts +14 -0
  105. package/dist/flows/run/branch-context.js +35 -0
  106. package/dist/flows/run/handlers/condition.d.ts +1 -1
  107. package/dist/flows/run/handlers/end.d.ts +1 -1
  108. package/dist/flows/run/handlers/group.d.ts +1 -1
  109. package/dist/flows/run/handlers/index.d.ts +2 -2
  110. package/dist/flows/run/handlers/parallel.d.ts +1 -1
  111. package/dist/flows/run/handlers/start.d.ts +1 -1
  112. package/dist/flows/run/handlers/support.d.ts +3 -24
  113. package/dist/flows/run/handlers/support.js +1 -1
  114. package/dist/flows/run/handlers/test.d.ts +1 -1
  115. package/dist/flows/run/handlers/types.d.ts +27 -0
  116. package/dist/flows/run/handlers/types.js +1 -0
  117. package/dist/flows/run/index.d.ts +1 -1
  118. package/dist/flows/run/prepare-flow.d.ts +14 -0
  119. package/dist/flows/run/prepare-flow.js +44 -0
  120. package/dist/flows/run/project.d.ts +33 -2
  121. package/dist/flows/run/project.js +139 -49
  122. package/dist/flows/run/run.d.ts +8 -4
  123. package/dist/flows/run/run.js +30 -194
  124. package/dist/flows/run/walker.d.ts +56 -0
  125. package/dist/flows/run/walker.js +225 -0
  126. package/dist/flows/steps/catalog.d.ts +1 -1
  127. package/dist/flows/steps/handler.d.ts +1 -1
  128. package/dist/flows/steps/set.d.ts +1 -1
  129. package/dist/index.js +1 -1
  130. package/dist/locator/actionability.d.ts +33 -0
  131. package/dist/locator/actionability.js +104 -0
  132. package/dist/locator/chain-resolver.d.ts +20 -0
  133. package/dist/locator/chain-resolver.js +102 -0
  134. package/dist/locator/document-swap.d.ts +1 -0
  135. package/dist/locator/document-swap.js +11 -0
  136. package/dist/locator/element-state.d.ts +3 -1
  137. package/dist/locator/element-state.js +11 -3
  138. package/dist/locator/file-actions.d.ts +35 -0
  139. package/dist/locator/file-actions.js +55 -0
  140. package/dist/{script/helpers.js → locator/helper-realm.js} +2 -2
  141. package/dist/locator/input-actions.d.ts +18 -0
  142. package/dist/locator/input-actions.js +80 -0
  143. package/dist/locator/label-locator.d.ts +1 -1
  144. package/dist/locator/label-locator.js +5 -12
  145. package/dist/locator/locator.d.ts +14 -37
  146. package/dist/locator/locator.js +68 -424
  147. package/dist/locator/page-scripts.d.ts +16 -0
  148. package/dist/locator/page-scripts.js +54 -0
  149. package/dist/locator/page-search.d.ts +10 -0
  150. package/dist/locator/page-search.js +19 -0
  151. package/dist/locator/selector-errors.d.ts +0 -4
  152. package/dist/locator/selector-errors.js +0 -7
  153. package/dist/locator/selector.d.ts +14 -13
  154. package/dist/locator/selector.js +10 -16
  155. package/dist/locator/text-locator.d.ts +1 -1
  156. package/dist/locator/text-locator.js +5 -9
  157. package/dist/logger/index.d.ts +6 -0
  158. package/dist/logger/index.js +14 -7
  159. package/dist/network/bidi-wire.d.ts +10 -0
  160. package/dist/network/bidi-wire.js +38 -0
  161. package/dist/network/intercept.d.ts +30 -0
  162. package/dist/network/intercept.js +130 -0
  163. package/dist/network/network-tracker.d.ts +10 -47
  164. package/dist/network/network-tracker.js +34 -68
  165. package/dist/network/network-types.d.ts +33 -0
  166. package/dist/network/network-types.js +1 -0
  167. package/dist/network/network-wait.d.ts +7 -2
  168. package/dist/network/network-wait.js +14 -4
  169. package/dist/network/response.d.ts +1 -1
  170. package/dist/network/response.js +1 -1
  171. package/dist/network/route.d.ts +92 -0
  172. package/dist/network/route.js +142 -0
  173. package/dist/network/router.d.ts +6 -91
  174. package/dist/network/router.js +25 -258
  175. package/dist/recorder/expectation.d.ts +5 -0
  176. package/dist/recorder/expectation.js +20 -0
  177. package/dist/recorder/files-recorder.d.ts +30 -0
  178. package/dist/recorder/files-recorder.js +88 -0
  179. package/dist/recorder/normalise.d.ts +17 -6
  180. package/dist/recorder/normalise.js +96 -91
  181. package/dist/recorder/options.d.ts +56 -0
  182. package/dist/recorder/options.js +1 -0
  183. package/dist/recorder/page-messages.d.ts +35 -0
  184. package/dist/recorder/page-messages.js +26 -0
  185. package/dist/recorder/ranking.js +1 -0
  186. package/dist/recorder/realms.d.ts +6 -0
  187. package/dist/recorder/realms.js +13 -0
  188. package/dist/recorder/recorder.d.ts +10 -74
  189. package/dist/recorder/recorder.js +39 -199
  190. package/dist/recorder/session-browser.d.ts +22 -0
  191. package/dist/recorder/session-browser.js +21 -0
  192. package/dist/recorder/session-secrets.d.ts +8 -0
  193. package/dist/recorder/session-secrets.js +45 -0
  194. package/dist/recorder/session.d.ts +7 -13
  195. package/dist/recorder/session.js +22 -117
  196. package/dist/recorder/spec-source.d.ts +39 -0
  197. package/dist/recorder/spec-source.js +84 -0
  198. package/dist/recorder/toolbar/action-guard.d.ts +21 -0
  199. package/dist/recorder/toolbar/action-guard.js +43 -0
  200. package/dist/recorder/toolbar/actions/assert.d.ts +4 -0
  201. package/dist/recorder/toolbar/actions/assert.js +52 -0
  202. package/dist/recorder/toolbar/actions/insert.d.ts +6 -0
  203. package/dist/recorder/toolbar/actions/insert.js +93 -0
  204. package/dist/recorder/toolbar/actions/pick.d.ts +8 -0
  205. package/dist/recorder/toolbar/actions/pick.js +26 -0
  206. package/dist/recorder/toolbar/actions/reply.d.ts +7 -0
  207. package/dist/recorder/toolbar/actions/reply.js +10 -0
  208. package/dist/recorder/toolbar/controller-fixture.d.ts +47 -0
  209. package/dist/recorder/toolbar/controller-fixture.js +129 -0
  210. package/dist/recorder/toolbar/controller.d.ts +12 -78
  211. package/dist/recorder/toolbar/controller.js +35 -247
  212. package/dist/recorder/toolbar/inspect.d.ts +1 -3
  213. package/dist/recorder/toolbar/inspect.js +2 -2
  214. package/dist/recorder/toolbar/page-scope.d.ts +19 -0
  215. package/dist/recorder/toolbar/page-scope.js +43 -0
  216. package/dist/recorder/toolbar/protocol.d.ts +2 -1
  217. package/dist/recorder/toolbar/source/attach.d.ts +2 -0
  218. package/dist/recorder/toolbar/source/attach.js +41 -0
  219. package/dist/recorder/toolbar/source/dom.d.ts +2 -0
  220. package/dist/recorder/toolbar/source/dom.js +47 -0
  221. package/dist/recorder/toolbar/source/drag.d.ts +2 -0
  222. package/dist/recorder/toolbar/source/drag.js +32 -0
  223. package/dist/recorder/toolbar/source/events-page.d.ts +2 -0
  224. package/dist/recorder/toolbar/source/events-page.js +67 -0
  225. package/dist/recorder/toolbar/source/events-toolbar.d.ts +2 -0
  226. package/dist/recorder/toolbar/source/events-toolbar.js +35 -0
  227. package/dist/recorder/toolbar/source/fields.d.ts +2 -0
  228. package/dist/recorder/toolbar/source/fields.js +44 -0
  229. package/dist/recorder/toolbar/source/handlers.d.ts +2 -0
  230. package/dist/recorder/toolbar/source/handlers.js +134 -0
  231. package/dist/recorder/toolbar/source/names.d.ts +2 -0
  232. package/dist/recorder/toolbar/source/names.js +12 -0
  233. package/dist/recorder/toolbar/source/picking.d.ts +2 -0
  234. package/dist/recorder/toolbar/source/picking.js +72 -0
  235. package/dist/recorder/toolbar/source/placement.d.ts +2 -0
  236. package/dist/recorder/toolbar/source/placement.js +45 -0
  237. package/dist/recorder/toolbar/source/recorder-side.d.ts +2 -0
  238. package/dist/recorder/toolbar/source/recorder-side.js +54 -0
  239. package/dist/recorder/toolbar/source/setup.d.ts +2 -0
  240. package/dist/recorder/toolbar/source/setup.js +50 -0
  241. package/dist/recorder/toolbar/source/views-panels.d.ts +2 -0
  242. package/dist/recorder/toolbar/source/views-panels.js +150 -0
  243. package/dist/recorder/toolbar/source/views-rail.d.ts +2 -0
  244. package/dist/recorder/toolbar/source/views-rail.js +40 -0
  245. package/dist/recorder/toolbar/source/views-render.d.ts +2 -0
  246. package/dist/recorder/toolbar/source/views-render.js +48 -0
  247. package/dist/recorder/toolbar/source.js +30 -856
  248. package/dist/recorder/toolbar/types.d.ts +70 -0
  249. package/dist/recorder/toolbar/types.js +1 -0
  250. package/dist/recorder/toolbar-bridge.d.ts +43 -0
  251. package/dist/recorder/toolbar-bridge.js +106 -0
  252. package/dist/runner/browsers.d.ts +10 -0
  253. package/dist/runner/browsers.js +12 -0
  254. package/dist/runner/groups.d.ts +5 -1
  255. package/dist/runner/groups.js +9 -2
  256. package/dist/runner/options.d.ts +31 -0
  257. package/dist/runner/options.js +1 -0
  258. package/dist/runner/pooled-run.d.ts +43 -0
  259. package/dist/runner/pooled-run.js +75 -0
  260. package/dist/runner/ports.d.ts +2 -0
  261. package/dist/runner/ports.js +2 -0
  262. package/dist/runner/registry.d.ts +1 -1
  263. package/dist/runner/registry.js +1 -1
  264. package/dist/runner/reporter.d.ts +40 -11
  265. package/dist/runner/reporter.js +69 -16
  266. package/dist/runner/run-test.d.ts +16 -0
  267. package/dist/runner/run-test.js +178 -0
  268. package/dist/runner/run.d.ts +5 -2
  269. package/dist/runner/run.js +28 -28
  270. package/dist/runner/select.d.ts +24 -0
  271. package/dist/runner/select.js +77 -0
  272. package/dist/runner/test-dir.d.ts +13 -0
  273. package/dist/runner/test-dir.js +27 -0
  274. package/dist/runner/test-runner.d.ts +27 -50
  275. package/dist/runner/test-runner.js +102 -264
  276. package/dist/runner/worker-pool.d.ts +67 -0
  277. package/dist/runner/worker-pool.js +275 -0
  278. package/dist/runner/worker-process.d.ts +6 -0
  279. package/dist/runner/worker-process.js +54 -0
  280. package/dist/runner/worker-protocol.d.ts +67 -0
  281. package/dist/runner/worker-protocol.js +1 -0
  282. package/dist/runner/worker-session.d.ts +7 -0
  283. package/dist/runner/worker-session.js +60 -0
  284. package/dist/runner/worker.d.ts +26 -0
  285. package/dist/runner/worker.js +121 -0
  286. package/dist/runner/workers-option.d.ts +15 -0
  287. package/dist/runner/workers-option.js +22 -0
  288. package/dist/script/call-function.d.ts +1 -9
  289. package/dist/script/call-function.js +2 -19
  290. package/dist/script/element-handle.d.ts +1 -2
  291. package/dist/script/element-handle.js +1 -2
  292. package/dist/script/serialize.d.ts +3 -12
  293. package/dist/script/serialize.js +53 -62
  294. package/dist/steps/parse/call.d.ts +3 -0
  295. package/dist/steps/parse/call.js +81 -0
  296. package/dist/steps/parse/expectation.d.ts +5 -0
  297. package/dist/steps/parse/expectation.js +71 -0
  298. package/dist/steps/parse/file-entry.d.ts +4 -0
  299. package/dist/steps/parse/file-entry.js +74 -0
  300. package/dist/steps/parse/literals.d.ts +4 -0
  301. package/dist/steps/parse/literals.js +28 -0
  302. package/dist/steps/parse/step.d.ts +4 -0
  303. package/dist/steps/parse/step.js +81 -0
  304. package/dist/steps/parse.js +2 -323
  305. package/dist/transport/bidi-connection.d.ts +4 -2
  306. package/dist/transport/bidi-connection.js +9 -43
  307. package/dist/transport/log-redaction.d.ts +13 -0
  308. package/dist/transport/log-redaction.js +51 -0
  309. package/dist/transport/subscriptions.js +18 -13
  310. package/dist/transport/unsupported.d.ts +7 -0
  311. package/dist/transport/unsupported.js +13 -0
  312. package/dist/types/bidi-modules/browser.d.ts +1 -12
  313. package/dist/types/bidi-modules/browsing-context.d.ts +8 -18
  314. package/dist/types/bidi-modules/emulation.d.ts +29 -21
  315. package/dist/types/bidi-modules/input.d.ts +1 -9
  316. package/dist/types/bidi-modules/network.d.ts +29 -24
  317. package/dist/types/bidi-modules/script.d.ts +0 -9
  318. package/dist/types/bidi-modules/session.d.ts +0 -8
  319. package/dist/types/bidi-modules/storage.d.ts +0 -5
  320. package/dist/types/bidi.d.ts +7 -37
  321. package/dist/types/config.d.ts +17 -3
  322. package/dist/types/test.d.ts +15 -2
  323. package/dist/wait/wait-until.d.ts +1 -5
  324. package/dist/wait/wait-until.js +7 -16
  325. package/package.json +3 -2
  326. package/dist/config/default.d.ts +0 -2
  327. package/dist/config/default.js +0 -3
  328. package/dist/types/bidi-modules/web-extensions.d.ts +0 -9
  329. package/dist/types/bidi-protocols/command.d.ts +0 -7
  330. package/dist/types/bidi-protocols/event.d.ts +0 -17
  331. /package/dist/{script/helpers.d.ts → locator/helper-realm.d.ts} +0 -0
@@ -0,0 +1,35 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ export function forkContext(context) {
3
+ return {
4
+ context: {
5
+ ...context,
6
+ vars: { ...context.vars },
7
+ nodes: { ...context.nodes },
8
+ },
9
+ base: { ...context.vars },
10
+ };
11
+ }
12
+ /**
13
+ * Takes what the branches wrote into `into`: every node output, and every variable a branch changed. A variable
14
+ * two branches changed to different values is a clash and is not taken; the first clash is returned.
15
+ */
16
+ export function mergeForks(into, forks) {
17
+ const changed = new Map();
18
+ const clashes = new Set();
19
+ for (const { context, base } of forks) {
20
+ Object.assign(into.nodes, context.nodes);
21
+ for (const [name, value] of Object.entries(context.vars)) {
22
+ if (name in base && isDeepStrictEqual(base[name], value))
23
+ continue;
24
+ if (changed.has(name) && !isDeepStrictEqual(changed.get(name), value))
25
+ clashes.add(name);
26
+ else
27
+ changed.set(name, value);
28
+ }
29
+ }
30
+ for (const [name, value] of changed)
31
+ if (!clashes.has(name))
32
+ into.vars[name] = value;
33
+ const [clash] = clashes;
34
+ return clash === undefined ? {} : { clash };
35
+ }
@@ -1,2 +1,2 @@
1
- import { type Handler } from "./support.js";
1
+ import type { Handler } from "./types.js";
2
2
  export declare const condition: Handler;
@@ -1,2 +1,2 @@
1
- import type { Handler } from "./support.js";
1
+ import type { Handler } from "./types.js";
2
2
  export declare const end: Handler;
@@ -1,2 +1,2 @@
1
- import { type Handler } from "./support.js";
1
+ import type { Handler } from "./types.js";
2
2
  export declare const group: Handler;
@@ -1,4 +1,4 @@
1
- import type { Handler } from "./support.js";
2
- export type { Handler, HandlerRun, Outcome } from "./support.js";
1
+ import type { Handler } from "./types.js";
2
+ export type { Handler, HandlerRun, Outcome } from "./types.js";
3
3
  /** The kinds a run can execute, by kind: the closed ones and every built-in step */
4
4
  export declare const HANDLERS: Readonly<Record<string, Handler>>;
@@ -1,3 +1,3 @@
1
- import type { Handler } from "./support.js";
1
+ import type { Handler } from "./types.js";
2
2
  /** Its branches are walked by the walker; the node itself just opens them */
3
3
  export declare const parallel: Handler;
@@ -1,2 +1,2 @@
1
- import type { Handler } from "./support.js";
1
+ import type { Handler } from "./types.js";
2
2
  export declare const start: Handler;
@@ -1,27 +1,6 @@
1
- import { type AnyNodeDefinition, type ExpressionContext, type FlowFile, type FlowNodeModel } from "../../core/index.js";
2
- import type { RunEvent } from "../../../runner/reporter.js";
3
- import type { NodeResult, NodeTest } from "../events.js";
4
- /** What a handler can use of the run */
5
- export interface HandlerRun {
6
- flow: FlowFile;
7
- environment: {
8
- name: string;
9
- variables: Record<string, unknown>;
10
- };
11
- context: ExpressionContext;
12
- nodeTests: Record<string, NodeTest[]>;
13
- signal: AbortSignal;
14
- runTests: (tests: NodeTest[], onEvent: (e: RunEvent) => void) => Promise<"passed" | "failed" | "cancelled">;
15
- sleep: (ms: number, signal: AbortSignal) => Promise<void>;
16
- onTestEvent: (nodeId: string, e: RunEvent) => void;
17
- /** Definitions of kinds core doesn't have (steps), for expression scopes */
18
- nodes: readonly AnyNodeDefinition[];
19
- log: (nodeId: string, level: "info" | "warn" | "error", text: string) => void;
20
- }
21
- /** What a handler decides; the walker adds timing */
22
- export type Outcome = Omit<NodeResult, "durationMs"> | "cancelled";
23
- /** Runs one kind of node. A `branches` node names the branch it takes in `branch` */
24
- export type Handler = (node: FlowNodeModel, run: HandlerRun) => Promise<Outcome>;
1
+ import { type FlowNodeModel } from "../../core/index.js";
2
+ import type { NodeTest } from "../events.js";
3
+ import type { HandlerRun, Outcome } from "./types.js";
25
4
  export declare const fail: (e: unknown) => Outcome;
26
5
  /** Runs `list` for the node and turns the result into an outcome */
27
6
  export declare function runNodeTests(node: FlowNodeModel, run: HandlerRun, list: NodeTest[]): Promise<Outcome>;
@@ -7,7 +7,7 @@ export const fail = (e) => ({
7
7
  export async function runNodeTests(node, run, list) {
8
8
  let result;
9
9
  try {
10
- result = await run.runTests(list, (e) => run.onTestEvent(node.id, e));
10
+ result = await run.runTests(list, (e) => run.onTestEvent(node.id, e), run.nodeBrowsers?.[node.id]);
11
11
  }
12
12
  catch (e) {
13
13
  return fail(e);
@@ -1,2 +1,2 @@
1
- import { type Handler } from "./support.js";
1
+ import type { Handler } from "./types.js";
2
2
  export declare const test: Handler;
@@ -0,0 +1,27 @@
1
+ import type { AnyNodeDefinition, ExpressionContext, FlowFile, FlowNodeModel } from "../../core/index.js";
2
+ import type { RunEvent } from "../../../runner/reporter.js";
3
+ import type { SupportedBrowser } from "../../../types/browser.js";
4
+ import type { NodeResult, NodeTest } from "../events.js";
5
+ /** What a handler can use of the run */
6
+ export interface HandlerRun {
7
+ flow: FlowFile;
8
+ environment: {
9
+ name: string;
10
+ variables: Record<string, unknown>;
11
+ };
12
+ context: ExpressionContext;
13
+ nodeTests: Record<string, NodeTest[]>;
14
+ /** The browsers of the nodes that have their own (a Group node's group); the others use the project's */
15
+ nodeBrowsers?: Record<string, SupportedBrowser[]>;
16
+ signal: AbortSignal;
17
+ runTests: (tests: NodeTest[], onEvent: (e: RunEvent) => void, browsers?: SupportedBrowser[]) => Promise<"passed" | "failed" | "cancelled">;
18
+ sleep: (ms: number, signal: AbortSignal) => Promise<void>;
19
+ onTestEvent: (nodeId: string, e: RunEvent) => void;
20
+ /** Definitions of kinds core doesn't have (steps), for expression scopes */
21
+ nodes: readonly AnyNodeDefinition[];
22
+ log: (nodeId: string, level: "info" | "warn" | "error", text: string) => void;
23
+ }
24
+ /** What a handler decides; the walker adds timing */
25
+ export type Outcome = Omit<NodeResult, "durationMs"> | "cancelled";
26
+ /** Runs one kind of node. A `branches` node names the branch it takes in `branch` */
27
+ export type Handler = (node: FlowNodeModel, run: HandlerRun) => Promise<Outcome>;
@@ -0,0 +1 @@
1
+ export {};
@@ -6,4 +6,4 @@ export { runFlow, type RunFlowOptions } from "./run.js";
6
6
  export { FLOW_SUFFIX, flowsDir, listFlows, readFlow, readFlowPath, readFlows, } from "./files.js";
7
7
  export * from "./report.js";
8
8
  export { projectSteps, type StepSet } from "../steps/set.js";
9
- export { FlowCheckError, checkFlowInProject, chooseEnvironment, runFlowInProject, type RunFlowInProjectOptions, } from "./project.js";
9
+ export { FlowCheckError, checkFlowInProject, chooseEnvironment, runFlowInProject, type FlowRunOptions, type RunFlowInProjectOptions, } from "./project.js";
@@ -0,0 +1,14 @@
1
+ import { type AnyNodeDefinition, type FlowFile } from "../core/index.js";
2
+ import type { Handler } from "./handlers/index.js";
3
+ /** The flow's shape can't be walked (no Start, a node reached twice, a branch that never meets): the run ends with this message */
4
+ export declare class ShapeError extends Error {
5
+ }
6
+ export declare const shapeError: (message: string) => ShapeError;
7
+ /** Waits `ms`, or rejects at once when `signal` aborts */
8
+ export declare const defaultSleep: (ms: number, signal: AbortSignal) => Promise<void>;
9
+ /** Whether a node of this kind has a list of branches that is only known at run time (a Parallel node) */
10
+ export declare const dynamicBranches: (def: AnyNodeDefinition) => boolean;
11
+ /** The definition of a kind: the project's own steps first, then the built-in kinds; undefined for a kind nobody knows */
12
+ export declare function definitionOf(nodes: readonly AnyNodeDefinition[], kind: string): AnyNodeDefinition | undefined;
13
+ /** Throws when the flow has a step or branching node whose kind has no handler: the run would not know what to do */
14
+ export declare function assertRunnable(flow: FlowFile, nodes: readonly AnyNodeDefinition[], handlers: Readonly<Record<string, Handler>>): void;
@@ -0,0 +1,44 @@
1
+ import { nodeDef, } from "../core/index.js";
2
+ /** The flow's shape can't be walked (no Start, a node reached twice, a branch that never meets): the run ends with this message */
3
+ export class ShapeError extends Error {
4
+ }
5
+ export const shapeError = (message) => new ShapeError(`This flow's shape can't run: ${message}`);
6
+ /** Waits `ms`, or rejects at once when `signal` aborts */
7
+ export const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
8
+ if (signal.aborted)
9
+ return reject(new Error("aborted"));
10
+ const onAbort = () => {
11
+ clearTimeout(timer);
12
+ reject(new Error("aborted"));
13
+ };
14
+ const timer = setTimeout(() => {
15
+ signal.removeEventListener("abort", onAbort);
16
+ resolve();
17
+ }, ms);
18
+ signal.addEventListener("abort", onAbort, { once: true });
19
+ });
20
+ /** Whether a node of this kind has a list of branches that is only known at run time (a Parallel node) */
21
+ export const dynamicBranches = (def) => def.shape === "branches" && !!def.branches && !Array.isArray(def.branches);
22
+ /** The definition of a kind: the project's own steps first, then the built-in kinds; undefined for a kind nobody knows */
23
+ export function definitionOf(nodes, kind) {
24
+ const own = nodes.find((d) => d.kind === kind);
25
+ if (own)
26
+ return own;
27
+ try {
28
+ return nodeDef(kind);
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
34
+ /** Throws when the flow has a step or branching node whose kind has no handler: the run would not know what to do */
35
+ export function assertRunnable(flow, nodes, handlers) {
36
+ const problems = flow.nodes
37
+ .map((n) => ({ n, def: definitionOf(nodes, n.kind) }))
38
+ .filter(({ n, def }) => def &&
39
+ (def.shape === "step" || def.shape === "branches") &&
40
+ !handlers[n.kind])
41
+ .map(({ def }) => `${def.label} can't run yet.`);
42
+ if (problems.length)
43
+ throw new Error(`This flow can't run: ${[...new Set(problems)].join(" ")}`);
44
+ }
@@ -1,8 +1,11 @@
1
1
  import { type FlowFile, type FlowProblem } from "../core/index.js";
2
+ import { type ResolvedGroup } from "../../runner/groups.js";
2
3
  import { type ProjectOptions } from "../../runner/project.js";
3
4
  import { runTests } from "../../runner/run.js";
5
+ import { WorkerPool, type PoolOptions } from "../../runner/worker-pool.js";
4
6
  import type { SamuraiTestConfig } from "../../types/config.js";
5
- import type { FlowEvent, FlowSummary } from "./events.js";
7
+ import type { FlowEvent, FlowSummary, NodeTest } from "./events.js";
8
+ import type { SupportedBrowser } from "../../types/browser.js";
6
9
  /** The flow has errors; nothing was run */
7
10
  export declare class FlowCheckError extends Error {
8
11
  readonly problems: FlowProblem[];
@@ -20,13 +23,39 @@ export interface RunFlowInProjectOptions extends ProjectOptions {
20
23
  /** `"fail"` (default): a flow with a node that can't run is refused. `"skip"`: those nodes are skipped. */
21
24
  unsupported?: "fail" | "skip";
22
25
  port?: number;
26
+ /**
27
+ * How many worker processes the tests of the whole run share: a number, or a percentage like `"50%"`. Unset
28
+ * takes `SAMURAI_WORKERS` (`workersEnv`), then the config's `workers`, then half the CPUs. 1 runs each Test
29
+ * and Group node through `runTests` in this process; more starts one pool that every node, and every branch
30
+ * of a Parallel node, takes its workers from
31
+ */
32
+ workers?: number | string;
33
+ /** The value of `SAMURAI_WORKERS`, when set */
34
+ workersEnv?: string;
35
+ /** @internal Replaces the worker pool (tests) */
36
+ createPool?: (options: PoolOptions) => Pick<WorkerPool, "run" | "dispose">;
23
37
  /** @internal Replaces the runner of each Test and Group node (tests) */
24
38
  testRunner?: typeof runTests;
25
39
  /** @internal */
26
40
  sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
27
41
  }
42
+ /** What `runFlowInProject` takes when the flow itself is given separately: everything but `flow` and `flowId` */
43
+ export type FlowRunOptions = Omit<RunFlowInProjectOptions, "flow" | "flowId">;
28
44
  /** The environment: the option, else the flow's `envDefault` (when the project has it), else the config's default, else the first, else "default" */
29
45
  export declare function chooseEnvironment(config: SamuraiTestConfig, flow: FlowFile, option?: string): string;
46
+ /** A test of the project as flows see it */
47
+ interface ProjectTest {
48
+ id: string;
49
+ file: string;
50
+ name: string;
51
+ /** The spec file relative to `srcDir`, with `/` separators */
52
+ relative: string;
53
+ }
54
+ /** The tests, and the browsers, of each Test and Group node of a flow; a Test node uses the project's browsers */
55
+ export declare function nodeTestsOf(flow: FlowFile, tests: Pick<ProjectTest, "id" | "file" | "name">[], groups: Pick<ResolvedGroup, "name" | "testIds" | "browsers">[]): {
56
+ nodeTests: Record<string, NodeTest[]>;
57
+ nodeBrowsers: Record<string, SupportedBrowser[]>;
58
+ };
30
59
  /**
31
60
  * Checks a flow against the project without running it: its tests, groups and environment
32
61
  * variables, and which kinds can run. Returns the flow (with keys) and every problem, warnings included.
@@ -38,6 +67,8 @@ export declare function checkFlowInProject(options: Pick<RunFlowInProjectOptions
38
67
  /**
39
68
  * Checks and runs a flow of a project: loads its config, lists its tests and groups, refuses to start
40
69
  * when `checkFlow` finds errors (`FlowCheckError`), then walks the flow; each Test and Group node runs
41
- * its tests through `runTests`, one after the other. One at a time per process.
70
+ * its tests through `runTests`, one after the other, or on one shared worker pool when `workers` is above 1.
71
+ * One at a time per process.
42
72
  */
43
73
  export declare function runFlowInProject(options: RunFlowInProjectOptions): Promise<FlowSummary>;
74
+ export {};
@@ -2,9 +2,15 @@ import path from "node:path";
2
2
  import { projectConfig } from "../../config/config.js";
3
3
  import { checkFlow, withKeys, } from "../core/index.js";
4
4
  import { DEFAULT_SRC_DIR, realPath, resolveGroups, testId, toPosix, } from "../../runner/groups.js";
5
+ import logger from "../../logger/index.js";
6
+ import { resolveWorkers } from "../../runner/workers-option.js";
5
7
  import { withProject } from "../../runner/project.js";
8
+ import { browsersFor } from "../../runner/browsers.js";
9
+ import { DEFAULT_PORT } from "../../runner/ports.js";
6
10
  import { runTests } from "../../runner/run.js";
7
11
  import TestRunner from "../../runner/test-runner.js";
12
+ import { WorkerPool } from "../../runner/worker-pool.js";
13
+ import { processWorkerFactory } from "../../runner/worker-process.js";
8
14
  import { readFlow } from "./files.js";
9
15
  import { projectSteps } from "../steps/set.js";
10
16
  import { HANDLERS } from "./handlers/index.js";
@@ -17,16 +23,66 @@ export class FlowCheckError extends Error {
17
23
  this.name = "FlowCheckError";
18
24
  }
19
25
  }
26
+ /** Whether the project's config defines an environment of this name */
27
+ function hasEnvironment(config, name) {
28
+ return Object.prototype.hasOwnProperty.call(config.environments ?? {}, name);
29
+ }
20
30
  /** The environment: the option, else the flow's `envDefault` (when the project has it), else the config's default, else the first, else "default" */
21
31
  export function chooseEnvironment(config, flow, option) {
22
- const names = Object.keys(config.environments ?? {});
23
32
  if (option)
24
33
  return option;
34
+ const names = Object.keys(config.environments ?? {});
25
35
  if (flow.envDefault && names.includes(flow.envDefault))
26
36
  return flow.envDefault;
27
37
  return config.defaultEnvironment ?? names[0] ?? "default";
28
38
  }
29
- /** What the project says about a flow: its environment, tests and groups */
39
+ /** The work items of a node: each test in each browser, the browsers one after the other */
40
+ function itemsOf(tests, browsers) {
41
+ return browsers
42
+ .flatMap((browser) => tests.map((t) => ({ file: t.file, testName: t.name, browser })))
43
+ .map((item, index) => ({ ...item, index }));
44
+ }
45
+ /** Runs the tests of a node on the pool and reports them as a run would */
46
+ async function runOnPool(pool, items, onEvent, signal) {
47
+ const results = await pool.run(items, {
48
+ signal,
49
+ onEvent: (event) => onEvent({
50
+ type: event.type,
51
+ name: event.item.testName,
52
+ file: event.item.file,
53
+ browser: event.item.browser,
54
+ ...(event.type === "test-end" && { result: event.result }),
55
+ }),
56
+ });
57
+ if (signal.aborted)
58
+ return "cancelled";
59
+ return results.every((r) => r.status === "success") ? "passed" : "failed";
60
+ }
61
+ /** The tests, and the browsers, of each Test and Group node of a flow; a Test node uses the project's browsers */
62
+ export function nodeTestsOf(flow, tests, groups) {
63
+ const byId = new Map(tests.map((t) => [t.id, t]));
64
+ const nodeTests = {};
65
+ const nodeBrowsers = {};
66
+ for (const node of flow.nodes) {
67
+ const group = node.kind === "group"
68
+ ? groups.find((g) => g.name === node.ref?.group)
69
+ : undefined;
70
+ const ids = node.kind === "test"
71
+ ? [node.ref?.testId ?? ""]
72
+ : node.kind === "group"
73
+ ? (group?.testIds ?? [])
74
+ : undefined;
75
+ if (!ids)
76
+ continue;
77
+ if (group)
78
+ nodeBrowsers[node.id] = group.browsers;
79
+ nodeTests[node.id] = ids.flatMap((id) => {
80
+ const t = byId.get(id);
81
+ return t ? [{ file: t.file, name: t.name }] : [];
82
+ });
83
+ }
84
+ return { nodeTests, nodeBrowsers };
85
+ }
30
86
  async function prepare(options) {
31
87
  const projectDir = path.resolve(options.projectDir ?? process.cwd());
32
88
  if (!options.flow && !options.flowId)
@@ -40,7 +96,7 @@ async function prepare(options) {
40
96
  const environment = {
41
97
  name,
42
98
  variables: {
43
- ...(Object.prototype.hasOwnProperty.call(config.environments ?? {}, name)
99
+ ...(hasEnvironment(config, name)
44
100
  ? config.environments[name].variables
45
101
  : {}),
46
102
  },
@@ -99,64 +155,98 @@ export async function checkFlowInProject(options) {
99
155
  /**
100
156
  * Checks and runs a flow of a project: loads its config, lists its tests and groups, refuses to start
101
157
  * when `checkFlow` finds errors (`FlowCheckError`), then walks the flow; each Test and Group node runs
102
- * its tests through `runTests`, one after the other. One at a time per process.
158
+ * its tests through `runTests`, one after the other, or on one shared worker pool when `workers` is above 1.
159
+ * One at a time per process.
103
160
  */
104
161
  export async function runFlowInProject(options) {
105
162
  const { projectDir, flow, unsupported, config, steps, name, environment, tests, groups, problems, } = await prepare(options);
106
163
  const errors = problems.filter((p) => p.level === "error");
107
164
  if (errors.length)
108
165
  throw new FlowCheckError(errors);
109
- // Phase 3: walk it
110
- const byId = new Map(tests.map((t) => [t.id, t]));
111
- const nodeTests = {};
112
- for (const node of flow.nodes) {
113
- const ids = node.kind === "test"
114
- ? [node.ref?.testId ?? ""]
115
- : node.kind === "group"
116
- ? (groups.find((g) => g.name === node.ref?.group)?.testIds ?? [])
117
- : undefined;
118
- if (!ids)
119
- continue;
120
- nodeTests[node.id] = ids.flatMap((id) => {
121
- const t = byId.get(id);
122
- return t ? [{ file: t.file, name: t.name }] : [];
166
+ const { nodeTests, nodeBrowsers } = nodeTestsOf(flow, tests, groups);
167
+ const run = options.testRunner ?? runTests;
168
+ // An injected runner (tests) stays in this process unless the workers are asked for
169
+ const asked = options.workers !== undefined ||
170
+ Boolean(options.workersEnv) ||
171
+ config.workers !== undefined;
172
+ let workers = options.testRunner && options.workers === undefined
173
+ ? 1
174
+ : resolveWorkers({
175
+ flag: options.workers,
176
+ env: options.workersEnv,
177
+ config: config.workers,
123
178
  });
179
+ // A worker reads `samurai.config.ts` from the project folder: a config given as an object stays in this process
180
+ if (options.config !== undefined && !options.createPool && workers > 1) {
181
+ if (asked)
182
+ logger.warn("workers needs samurai.config.ts in the project folder; running on one worker");
183
+ workers = 1;
124
184
  }
125
- const run = options.testRunner ?? runTests;
126
- return runFlow({
127
- flow,
128
- nodeTests,
129
- environment,
130
- ...(options.signal && { signal: options.signal }),
131
- ...(options.onEvent && { onEvent: options.onEvent }),
132
- ...(options.sleep && { sleep: options.sleep }),
133
- unsupported,
134
- handlers: { ...HANDLERS, ...steps.handlers },
135
- nodes: steps.nodes,
136
- runTests: async ({ tests, onEvent, signal }) => {
137
- const summary = await run({
185
+ const files = [
186
+ ...new Set(Object.values(nodeTests).flatMap((list) => list.map((t) => t.file))),
187
+ ];
188
+ // One pool for the whole run: the nodes, and the branches of a Parallel node, share its workers
189
+ const pool = workers > 1
190
+ ? (options.createPool ?? ((o) => new WorkerPool(o)))({
191
+ workers,
192
+ factory: processWorkerFactory(),
193
+ init: {
138
194
  projectDir,
139
- config,
140
195
  ...(options.dataDir !== undefined && { dataDir: options.dataDir }),
141
- // The environment the expressions see, whenever the project defines it
142
- ...(Object.prototype.hasOwnProperty.call(config.environments ?? {}, name)
143
- ? { environment: name }
144
- : {}),
145
- ...(options.headless !== undefined && { headless: options.headless }),
196
+ ...(hasEnvironment(config, name) && { environment: name }),
146
197
  ...(options.timeout !== undefined && { timeout: options.timeout }),
147
198
  ...(options.expectTimeout !== undefined && {
148
199
  expectTimeout: options.expectTimeout,
149
200
  }),
150
- ...(options.port !== undefined && { port: options.port }),
151
- files: [...new Set(tests.map((t) => t.file))],
152
- testNames: tests.map((t) => t.name),
153
- reportPath: false,
154
- signal,
155
- onEvent,
156
- });
157
- if (signal.aborted)
158
- return "cancelled";
159
- return summary.status === "success" ? "passed" : "failed";
160
- },
161
- });
201
+ ...(options.headless !== undefined && {
202
+ headless: options.headless,
203
+ }),
204
+ files,
205
+ basePort: options.port ?? DEFAULT_PORT,
206
+ },
207
+ })
208
+ : undefined;
209
+ try {
210
+ return await runFlow({
211
+ flow,
212
+ nodeTests,
213
+ nodeBrowsers,
214
+ environment,
215
+ ...(options.signal && { signal: options.signal }),
216
+ ...(options.onEvent && { onEvent: options.onEvent }),
217
+ ...(options.sleep && { sleep: options.sleep }),
218
+ unsupported,
219
+ handlers: { ...HANDLERS, ...steps.handlers },
220
+ nodes: steps.nodes,
221
+ runTests: async ({ tests, onEvent, signal, browsers }) => {
222
+ if (pool)
223
+ return runOnPool(pool, itemsOf(tests, browsers ?? browsersFor({ project: config.browsers })), onEvent, signal);
224
+ const summary = await run({
225
+ projectDir,
226
+ config,
227
+ ...(options.dataDir !== undefined && { dataDir: options.dataDir }),
228
+ // The environment the expressions see, whenever the project defines it
229
+ ...(hasEnvironment(config, name) ? { environment: name } : {}),
230
+ ...(options.headless !== undefined && { headless: options.headless }),
231
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
232
+ ...(options.expectTimeout !== undefined && {
233
+ expectTimeout: options.expectTimeout,
234
+ }),
235
+ ...(options.port !== undefined && { port: options.port }),
236
+ ...(browsers && { browsers }),
237
+ files: [...new Set(tests.map((t) => t.file))],
238
+ testNames: tests.map((t) => t.name),
239
+ reportPath: false,
240
+ signal,
241
+ onEvent,
242
+ });
243
+ if (signal.aborted)
244
+ return "cancelled";
245
+ return summary.status === "success" ? "passed" : "failed";
246
+ },
247
+ });
248
+ }
249
+ finally {
250
+ await pool?.dispose();
251
+ }
162
252
  }
@@ -2,10 +2,13 @@ import { type AnyNodeDefinition, type FlowFile } from "../core/index.js";
2
2
  import type { RunEvent } from "../../runner/reporter.js";
3
3
  import type { FlowEvent, FlowSummary, NodeTest } from "./events.js";
4
4
  import { type Handler } from "./handlers/index.js";
5
+ import type { SupportedBrowser } from "../../types/browser.js";
5
6
  export interface RunFlowOptions {
6
7
  flow: FlowFile;
7
8
  /** The tests of each Test and Group node, by node id */
8
9
  nodeTests: Record<string, NodeTest[]>;
10
+ /** The browsers a Group node runs in (its group's); a node not listed uses the project's */
11
+ nodeBrowsers?: Record<string, SupportedBrowser[]>;
9
12
  environment: {
10
13
  name: string;
11
14
  variables: Record<string, string | number | boolean>;
@@ -20,6 +23,8 @@ export interface RunFlowOptions {
20
23
  tests: NodeTest[];
21
24
  onEvent: (e: RunEvent) => void;
22
25
  signal: AbortSignal;
26
+ /** Set for a node that runs in browsers of its own */
27
+ browsers?: SupportedBrowser[];
23
28
  }) => Promise<"passed" | "failed" | "cancelled">;
24
29
  sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
25
30
  /**
@@ -34,9 +39,8 @@ export interface RunFlowOptions {
34
39
  nodes?: readonly AnyNodeDefinition[];
35
40
  }
36
41
  /**
37
- * Walks the flow from Start by the shape of each node's kind: a step runs and
38
- * follows its one way out; a node with a fixed list of branches follows the one its
39
- * handler names and skips the others; a node with a dynamic list walks every branch in
40
- * fill order; an end stops. Branches meet again at `meetOf`.
42
+ * Runs a flow: sets up what its nodes share (expression context, handler access to the tests), walks it from
43
+ * Start (see `FlowWalker`) and reports how it ended. A flow whose shape can't run ends with an `error`; a flow
44
+ * with a kind that has no handler throws before it starts, unless `unsupported` is `"skip"`.
41
45
  */
42
46
  export declare function runFlow(options: RunFlowOptions): Promise<FlowSummary>;