@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
@@ -1,55 +1,21 @@
1
- import { inFillOrder, meetOf, nodeDef, } from "../core/index.js";
1
+ import {} from "../core/index.js";
2
2
  import logger from "../../logger/index.js";
3
3
  import { STEP_NODES } from "../steps/catalog.js";
4
4
  import { HANDLERS } from "./handlers/index.js";
5
- const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
6
- if (signal.aborted)
7
- return reject(new Error("aborted"));
8
- const onAbort = () => {
9
- clearTimeout(timer);
10
- reject(new Error("aborted"));
11
- };
12
- const timer = setTimeout(() => {
13
- signal.removeEventListener("abort", onAbort);
14
- resolve();
15
- }, ms);
16
- signal.addEventListener("abort", onAbort, { once: true });
17
- });
18
- class ShapeError extends Error {
19
- }
20
- const dynamicBranches = (def) => def.shape === "branches" && !!def.branches && !Array.isArray(def.branches);
5
+ import { assertRunnable, defaultSleep, definitionOf, ShapeError, shapeError, } from "./prepare-flow.js";
6
+ import { FlowWalker } from "./walker.js";
21
7
  /**
22
- * Walks the flow from Start by the shape of each node's kind: a step runs and
23
- * follows its one way out; a node with a fixed list of branches follows the one its
24
- * handler names and skips the others; a node with a dynamic list walks every branch in
25
- * fill order; an end stops. Branches meet again at `meetOf`.
8
+ * Runs a flow: sets up what its nodes share (expression context, handler access to the tests), walks it from
9
+ * Start (see `FlowWalker`) and reports how it ended. A flow whose shape can't run ends with an `error`; a flow
10
+ * with a kind that has no handler throws before it starts, unless `unsupported` is `"skip"`.
26
11
  */
27
12
  export async function runFlow(options) {
28
13
  const { flow, onEvent = () => { } } = options;
29
14
  const signal = options.signal ?? new AbortController().signal;
30
15
  const handlers = options.handlers ?? HANDLERS;
31
16
  const nodes = options.nodes ?? STEP_NODES;
32
- const defOf = (kind) => nodes.find((d) => d.kind === kind) ??
33
- (() => {
34
- try {
35
- return nodeDef(kind);
36
- }
37
- catch {
38
- return undefined;
39
- }
40
- })();
41
- if ((options.unsupported ?? "fail") === "fail") {
42
- const problems = flow.nodes
43
- .map((n) => ({ n, def: defOf(n.kind) }))
44
- .filter(({ n, def }) => def &&
45
- (def.shape === "step" || def.shape === "branches") &&
46
- !handlers[n.kind])
47
- .map(({ def }) => `${def.label} can't run yet.`);
48
- if (problems.length)
49
- throw new Error(`This flow can't run: ${[...new Set(problems)].join(" ")}`);
50
- }
51
- const byId = new Map(flow.nodes.map((n) => [n.id, n]));
52
- const results = {};
17
+ if ((options.unsupported ?? "fail") === "fail")
18
+ assertRunnable(flow, nodes, handlers);
53
19
  const context = {
54
20
  env: { ...options.environment.variables },
55
21
  vars: {},
@@ -60,181 +26,51 @@ export async function runFlow(options) {
60
26
  startedAt: options.startedAt ?? new Date().toISOString(),
61
27
  },
62
28
  };
63
- const handlerRun = {
29
+ const handlerRunFor = (context, signal) => ({
64
30
  flow,
65
31
  environment: options.environment,
66
32
  context,
67
33
  nodeTests: options.nodeTests,
34
+ ...(options.nodeBrowsers && { nodeBrowsers: options.nodeBrowsers }),
68
35
  signal,
69
36
  sleep: options.sleep ?? defaultSleep,
70
- runTests: (tests, onTestEvent) => options.runTests({ tests, onEvent: onTestEvent, signal }),
37
+ runTests: (tests, onTestEvent, browsers) => options.runTests({
38
+ tests,
39
+ onEvent: onTestEvent,
40
+ signal,
41
+ ...(browsers && { browsers }),
42
+ }),
71
43
  onTestEvent: (nodeId, event) => onEvent({ type: "test", nodeId, event }),
72
44
  nodes,
73
45
  log: (nodeId, level, text) => logger[level](`[${nodeId}] ${text}`),
74
- };
75
- let stopped;
76
- const visited = new Set();
77
- const record = (node, result) => {
78
- results[node.id] = result;
79
- if (node.key)
80
- context.nodes[node.key] = {
81
- ...result.outputs,
82
- status: result.status,
83
- durationMs: result.durationMs,
84
- error: result.error,
85
- };
86
- };
87
- const out = (id) => flow.edges.filter((e) => e.source === id);
88
- /** Marks every node from `from` up to `stop` as skipped (a branch not taken) */
89
- const skip = (from, stop) => {
90
- const seen = new Set();
91
- const visit = (id) => {
92
- if (id === stop || seen.has(id))
93
- return;
94
- seen.add(id);
95
- const node = byId.get(id);
96
- if (node && !results[id]) {
97
- record(node, { status: "skipped" });
98
- onEvent({
99
- type: "node-end",
100
- nodeId: id,
101
- result: { status: "skipped" },
102
- });
103
- }
104
- for (const e of out(id))
105
- visit(e.target);
106
- };
107
- visit(from);
108
- };
109
- const shapeError = (message) => new ShapeError(`This flow's shape can't run: ${message}`);
110
- async function runNode(node, def) {
111
- if (visited.has(node.id))
112
- throw shapeError(`${node.key ?? node.id} is reached twice.`);
113
- visited.add(node.id);
114
- if (signal.aborted) {
115
- stopped = "cancelled";
116
- return undefined;
117
- }
118
- onEvent({ type: "node-start", nodeId: node.id });
119
- const began = Date.now();
120
- const handler = handlers[node.kind];
121
- const outcome = handler
122
- ? await handler(node, handlerRun)
123
- : { status: "skipped", note: "Not supported yet" };
124
- if (outcome === "cancelled" || signal.aborted) {
125
- stopped = "cancelled";
126
- return undefined;
127
- }
128
- const result = { ...outcome, durationMs: Date.now() - began };
129
- record(node, result);
130
- // A node with a dynamic list of branches ends after them: walk() reports it then
131
- if (dynamicBranches(def))
132
- return result;
133
- onEvent({ type: "node-end", nodeId: node.id, result });
134
- if (result.status === "failed" && node.onFailure === "stop")
135
- stopped = "failed";
136
- return result;
137
- }
138
- /** Whether `target` is on the chain from `from` before `stop` */
139
- const reachesBefore = (from, target, stop) => {
140
- const seen = new Set();
141
- const visit = (id) => {
142
- if (id === stop || seen.has(id))
143
- return false;
144
- if (id === target)
145
- return true;
146
- seen.add(id);
147
- return out(id).some((e) => visit(e.target));
148
- };
149
- return visit(from);
150
- };
151
- /** Runs the chain from `from` up to `stop` (undefined: through End) */
152
- async function walk(from, stop) {
153
- let at = from;
154
- while (at !== undefined && at !== stop && !stopped) {
155
- const node = byId.get(at);
156
- if (!node)
157
- throw shapeError("an edge leads to a missing node.");
158
- const def = defOf(node.kind);
159
- if (!def)
160
- throw shapeError(`${node.key ?? node.id} has an unknown kind (${node.kind}).`);
161
- const result = await runNode(node, def);
162
- if (!result || stopped)
163
- return;
164
- if (def.shape === "end")
165
- return;
166
- const edges = out(node.id);
167
- if (def.shape === "branches") {
168
- const meet = meetOf(flow, node.id);
169
- if (!meet)
170
- throw shapeError(`the branches of ${node.key ?? node.id} never meet.`);
171
- if (!dynamicBranches(def)) {
172
- const taken = result.status === "passed"
173
- ? edges.find((e) => e.sourceHandle === result.branch)
174
- : undefined;
175
- for (const e of edges)
176
- if (e !== taken)
177
- skip(e.target, meet);
178
- if (taken)
179
- await walk(taken.target, meet);
180
- }
181
- else {
182
- const opened = Date.now();
183
- for (const e of inFillOrder(edges)) {
184
- if (stopped)
185
- break;
186
- if (e.target !== meet)
187
- await walk(e.target, meet);
188
- }
189
- if (stopped === "cancelled") {
190
- // Stopped inside its branches: like any node cut short, it didn't run to the end
191
- delete results[node.id];
192
- return;
193
- }
194
- // The node fails when anything in its branches failed
195
- const failed = flow.nodes.some((n) => results[n.id]?.status === "failed" &&
196
- edges.some((e) => e.target !== meet && reachesBefore(e.target, n.id, meet)));
197
- const durationMs = (result.durationMs ?? 0) + Date.now() - opened;
198
- const ended = failed
199
- ? {
200
- ...result,
201
- durationMs,
202
- status: "failed",
203
- error: "A branch failed.",
204
- }
205
- : { ...result, durationMs };
206
- record(node, ended);
207
- onEvent({ type: "node-end", nodeId: node.id, result: ended });
208
- if (failed && node.onFailure === "stop")
209
- stopped = "failed";
210
- if (stopped)
211
- return;
212
- }
213
- at = meet;
214
- continue;
215
- }
216
- if (edges.length !== 1)
217
- throw shapeError(`${node.key ?? node.id} has ${edges.length} ways out.`);
218
- at = edges[0].target;
219
- }
220
- }
46
+ });
47
+ const walker = new FlowWalker({
48
+ flow,
49
+ signal,
50
+ handlers,
51
+ nodes,
52
+ handlerRunFor,
53
+ context,
54
+ onEvent,
55
+ });
221
56
  onEvent({ type: "flow-start", environment: options.environment.name });
222
57
  let error;
223
- const start = flow.nodes.find((n) => defOf(n.kind)?.shape === "start");
58
+ const start = flow.nodes.find((n) => definitionOf(nodes, n.kind)?.shape === "start");
224
59
  try {
225
60
  if (!start)
226
61
  throw shapeError("it has no Start.");
227
- await walk(start.id);
62
+ await walker.walk(start.id);
228
63
  }
229
64
  catch (e) {
230
65
  if (!(e instanceof ShapeError))
231
66
  throw e;
232
67
  error = e.message;
233
68
  }
69
+ const results = walker.results;
234
70
  for (const node of flow.nodes)
235
71
  if (!results[node.id])
236
72
  results[node.id] = { status: "not-run" };
237
- const status = stopped === "cancelled"
73
+ const status = walker.stopped === "cancelled"
238
74
  ? "cancelled"
239
75
  : error || Object.values(results).some((r) => r.status === "failed")
240
76
  ? "failed"
@@ -0,0 +1,56 @@
1
+ import { type AnyNodeDefinition, type ExpressionContext, type FlowFile } from "../core/index.js";
2
+ import { type Fork } from "./branch-context.js";
3
+ import type { FlowEvent, NodeResult } from "./events.js";
4
+ import type { Handler, HandlerRun } from "./handlers/index.js";
5
+ /** What the walker needs of the run: the flow, who handles each kind, and where results and events go */
6
+ export interface WalkerInput {
7
+ flow: FlowFile;
8
+ signal: AbortSignal;
9
+ handlers: Readonly<Record<string, Handler>>;
10
+ nodes: readonly AnyNodeDefinition[];
11
+ /** What the handlers get for a context and a signal; the walker also writes each result into `context.nodes` */
12
+ handlerRunFor: (context: ExpressionContext, signal: AbortSignal) => HandlerRun;
13
+ context: ExpressionContext;
14
+ onEvent: (event: FlowEvent) => void;
15
+ }
16
+ /**
17
+ * Walks the flow from Start by the shape of each node's kind: a step runs and follows its one way out; a node
18
+ * with a fixed list of branches follows the one its handler names and skips the others; a node with a dynamic
19
+ * list walks every branch at the same time, each with its own copy of the variables (see `BranchWalk`); an end
20
+ * stops. Branches meet again at `meetOf`.
21
+ */
22
+ export declare class FlowWalker {
23
+ private readonly input;
24
+ /** Why the walk stopped early: a failed node that stops the flow, or an abort */
25
+ stopped: "failed" | "cancelled" | undefined;
26
+ /** The result of every node that ran or was skipped, by node id; shared with the walks of the branches */
27
+ readonly results: Record<string, NodeResult>;
28
+ /** Nodes already entered, so a node reached twice is a shape error; shared with the walks of the branches */
29
+ private readonly visited;
30
+ private readonly byId;
31
+ private readonly handlerRun;
32
+ constructor(input: WalkerInput, shared?: {
33
+ results: Record<string, NodeResult>;
34
+ visited: Set<string>;
35
+ });
36
+ /** The variables and node outputs this walk reads and writes */
37
+ get context(): ExpressionContext;
38
+ private definition;
39
+ private record;
40
+ private out;
41
+ /** Marks every node from `from` up to `stop` as skipped (a branch not taken) */
42
+ private skip;
43
+ private runNode;
44
+ /** Whether `target` is on the chain from `from` before `stop` */
45
+ private reachesBefore;
46
+ /**
47
+ * Walks the branches of a node with a dynamic list at the same time, then reports the node: failed when anything
48
+ * in them failed, or when two branches set a variable differently. A branch that fails and stops the flow cancels
49
+ * the others.
50
+ */
51
+ private walkBranches;
52
+ /** A walker for one branch: the same flow, results and visited nodes, but its own variables and its own signal */
53
+ forBranch(fork: Fork, signal: AbortSignal): FlowWalker;
54
+ /** Runs the chain from `from` up to `stop` (undefined: through End) */
55
+ walk(from: string, stop?: string): Promise<void>;
56
+ }
@@ -0,0 +1,225 @@
1
+ import { inFillOrder, meetOf, } from "../core/index.js";
2
+ import { forkContext, mergeForks } from "./branch-context.js";
3
+ import { definitionOf, dynamicBranches, shapeError } from "./prepare-flow.js";
4
+ /**
5
+ * Walks the flow from Start by the shape of each node's kind: a step runs and follows its one way out; a node
6
+ * with a fixed list of branches follows the one its handler names and skips the others; a node with a dynamic
7
+ * list walks every branch at the same time, each with its own copy of the variables (see `BranchWalk`); an end
8
+ * stops. Branches meet again at `meetOf`.
9
+ */
10
+ export class FlowWalker {
11
+ constructor(input, shared = { results: {}, visited: new Set() }) {
12
+ this.input = input;
13
+ this.byId = new Map(input.flow.nodes.map((n) => [n.id, n]));
14
+ this.results = shared.results;
15
+ this.visited = shared.visited;
16
+ this.handlerRun = input.handlerRunFor(input.context, input.signal);
17
+ }
18
+ /** The variables and node outputs this walk reads and writes */
19
+ get context() {
20
+ return this.input.context;
21
+ }
22
+ definition(kind) {
23
+ return definitionOf(this.input.nodes, kind);
24
+ }
25
+ record(node, result) {
26
+ this.results[node.id] = result;
27
+ if (node.key)
28
+ this.input.context.nodes[node.key] = {
29
+ ...result.outputs,
30
+ status: result.status,
31
+ durationMs: result.durationMs,
32
+ error: result.error,
33
+ };
34
+ }
35
+ out(id) {
36
+ return this.input.flow.edges.filter((e) => e.source === id);
37
+ }
38
+ /** Marks every node from `from` up to `stop` as skipped (a branch not taken) */
39
+ skip(from, stop) {
40
+ const seen = new Set();
41
+ const visit = (id) => {
42
+ if (id === stop || seen.has(id))
43
+ return;
44
+ seen.add(id);
45
+ const node = this.byId.get(id);
46
+ if (node && !this.results[id]) {
47
+ this.record(node, { status: "skipped" });
48
+ this.input.onEvent({
49
+ type: "node-end",
50
+ nodeId: id,
51
+ result: { status: "skipped" },
52
+ });
53
+ }
54
+ for (const e of this.out(id))
55
+ visit(e.target);
56
+ };
57
+ visit(from);
58
+ }
59
+ async runNode(node, def) {
60
+ const { signal, handlers, onEvent } = this.input;
61
+ const { handlerRun } = this;
62
+ if (this.visited.has(node.id))
63
+ throw shapeError(`${node.key ?? node.id} is reached twice.`);
64
+ this.visited.add(node.id);
65
+ if (signal.aborted) {
66
+ this.stopped = "cancelled";
67
+ return undefined;
68
+ }
69
+ onEvent({ type: "node-start", nodeId: node.id });
70
+ const began = Date.now();
71
+ const handler = handlers[node.kind];
72
+ const outcome = handler
73
+ ? await handler(node, handlerRun)
74
+ : { status: "skipped", note: "Not supported yet" };
75
+ if (outcome === "cancelled" || signal.aborted) {
76
+ this.stopped = "cancelled";
77
+ return undefined;
78
+ }
79
+ const result = { ...outcome, durationMs: Date.now() - began };
80
+ this.record(node, result);
81
+ // A node with a dynamic list of branches ends after them: walk() reports it then
82
+ if (dynamicBranches(def))
83
+ return result;
84
+ onEvent({ type: "node-end", nodeId: node.id, result });
85
+ if (result.status === "failed" && node.onFailure === "stop")
86
+ this.stopped = "failed";
87
+ return result;
88
+ }
89
+ /** Whether `target` is on the chain from `from` before `stop` */
90
+ reachesBefore(from, target, stop) {
91
+ const seen = new Set();
92
+ const visit = (id) => {
93
+ if (id === stop || seen.has(id))
94
+ return false;
95
+ if (id === target)
96
+ return true;
97
+ seen.add(id);
98
+ return this.out(id).some((e) => visit(e.target));
99
+ };
100
+ return visit(from);
101
+ }
102
+ /**
103
+ * Walks the branches of a node with a dynamic list at the same time, then reports the node: failed when anything
104
+ * in them failed, or when two branches set a variable differently. A branch that fails and stops the flow cancels
105
+ * the others.
106
+ */
107
+ async walkBranches(node, result, meet) {
108
+ const { flow, onEvent } = this.input;
109
+ const edges = this.out(node.id);
110
+ const opened = Date.now();
111
+ const cancel = new AbortController();
112
+ const onAbort = () => cancel.abort();
113
+ if (this.input.signal.aborted)
114
+ onAbort();
115
+ else
116
+ this.input.signal.addEventListener("abort", onAbort, { once: true });
117
+ const walks = inFillOrder(edges)
118
+ .filter((e) => e.target !== meet)
119
+ .map((e) => new BranchWalk(this, e.target, meet, cancel));
120
+ const settled = await Promise.allSettled(walks.map((w) => w.done));
121
+ this.input.signal.removeEventListener("abort", onAbort);
122
+ const rejected = settled.find((r) => r.status === "rejected");
123
+ if (rejected)
124
+ throw rejected.reason;
125
+ if (walks.some((w) => w.stopped === "failed"))
126
+ this.stopped = "failed";
127
+ if (this.input.signal.aborted)
128
+ this.stopped = "cancelled";
129
+ if (this.stopped === "cancelled") {
130
+ // Stopped inside its branches: like any node cut short, it didn't run to the end
131
+ delete this.results[node.id];
132
+ return;
133
+ }
134
+ const { clash } = mergeForks(this.input.context, walks.map((w) => w.fork));
135
+ // The node fails when anything in its branches failed
136
+ const failed = flow.nodes.some((n) => this.results[n.id]?.status === "failed" &&
137
+ edges.some((e) => e.target !== meet && this.reachesBefore(e.target, n.id, meet)));
138
+ const durationMs = (result.durationMs ?? 0) + Date.now() - opened;
139
+ const ended = clash
140
+ ? {
141
+ ...result,
142
+ durationMs,
143
+ status: "failed",
144
+ error: `branches set ${clash} differently`,
145
+ }
146
+ : failed
147
+ ? {
148
+ ...result,
149
+ durationMs,
150
+ status: "failed",
151
+ error: "A branch failed.",
152
+ }
153
+ : { ...result, durationMs };
154
+ this.record(node, ended);
155
+ onEvent({ type: "node-end", nodeId: node.id, result: ended });
156
+ if (ended.status === "failed" && node.onFailure === "stop")
157
+ this.stopped = "failed";
158
+ }
159
+ /** A walker for one branch: the same flow, results and visited nodes, but its own variables and its own signal */
160
+ forBranch(fork, signal) {
161
+ return new FlowWalker({ ...this.input, context: fork.context, signal }, { results: this.results, visited: this.visited });
162
+ }
163
+ /** Runs the chain from `from` up to `stop` (undefined: through End) */
164
+ async walk(from, stop) {
165
+ const { flow } = this.input;
166
+ let at = from;
167
+ while (at !== undefined && at !== stop && !this.stopped) {
168
+ const node = this.byId.get(at);
169
+ if (!node)
170
+ throw shapeError("an edge leads to a missing node.");
171
+ const def = this.definition(node.kind);
172
+ if (!def)
173
+ throw shapeError(`${node.key ?? node.id} has an unknown kind (${node.kind}).`);
174
+ const result = await this.runNode(node, def);
175
+ if (!result || this.stopped)
176
+ return;
177
+ if (def.shape === "end")
178
+ return;
179
+ const edges = this.out(node.id);
180
+ if (def.shape === "branches") {
181
+ const meet = meetOf(flow, node.id);
182
+ if (!meet)
183
+ throw shapeError(`the branches of ${node.key ?? node.id} never meet.`);
184
+ if (!dynamicBranches(def)) {
185
+ const taken = result.status === "passed"
186
+ ? edges.find((e) => e.sourceHandle === result.branch)
187
+ : undefined;
188
+ for (const e of edges)
189
+ if (e !== taken)
190
+ this.skip(e.target, meet);
191
+ if (taken)
192
+ await this.walk(taken.target, meet);
193
+ }
194
+ else {
195
+ await this.walkBranches(node, result, meet);
196
+ if (this.stopped)
197
+ return;
198
+ }
199
+ at = meet;
200
+ continue;
201
+ }
202
+ if (edges.length !== 1)
203
+ throw shapeError(`${node.key ?? node.id} has ${edges.length} ways out.`);
204
+ at = edges[0].target;
205
+ }
206
+ }
207
+ }
208
+ /** One branch of a Parallel node, walking from its first node up to where the branches meet */
209
+ class BranchWalk {
210
+ constructor(parent, from, meet, cancel) {
211
+ this.fork = forkContext(parent.context);
212
+ this.walker = parent.forBranch(this.fork, cancel.signal);
213
+ this.done = this.walker.walk(from, meet).then(() => {
214
+ // A failure that stops the flow cancels the branches still running
215
+ if (this.walker.stopped === "failed")
216
+ cancel.abort();
217
+ }, (error) => {
218
+ cancel.abort();
219
+ throw error;
220
+ });
221
+ }
222
+ get stopped() {
223
+ return this.walker.stopped;
224
+ }
225
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AnyNodeDefinition, FlowFile, FlowProblem, StepInfo } from "../core/index.js";
2
- import type { Handler } from "../run/handlers/support.js";
2
+ import type { Handler } from "../run/handlers/types.js";
3
3
  import { type StepDefinition } from "./define.js";
4
4
  import type { SamuraiTestConfig } from "../../types/config.js";
5
5
  /** The built-in steps, in library order */
@@ -1,4 +1,4 @@
1
- import { type Handler } from "../run/handlers/support.js";
1
+ import type { Handler } from "../run/handlers/types.js";
2
2
  import { type StepDefinition } from "./define.js";
3
3
  /** The one handler every step runs through */
4
4
  export declare function stepHandler(step: StepDefinition<any>): Handler;
@@ -1,6 +1,6 @@
1
1
  import type { SamuraiTestConfig } from "../../types/config.js";
2
2
  import type { AnyNodeDefinition, StepInfo } from "../core/index.js";
3
- import type { Handler } from "../run/handlers/support.js";
3
+ import type { Handler } from "../run/handlers/types.js";
4
4
  import { type StepDefinition } from "./define.js";
5
5
  type AnyStep = StepDefinition<any>;
6
6
  /** The built-in steps, in library order */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { main } from "./cli/main.js";
2
- // Development entry (`bun run dev`): the same as `samurai run`, straight from the sources
2
+ // Development entry (`npm run dev`, which runs `tsx src/index.ts`): the same as `samurai run`, straight from the sources
3
3
  const interrupt = new AbortController();
4
4
  process.on("SIGINT", () => {
5
5
  if (interrupt.signal.aborted)
@@ -0,0 +1,33 @@
1
+ import type { ElementHandle } from "../script/element-handle.js";
2
+ import { type CheckName, type ElementState, type ProbeOptions, type WaitForState } from "./element-state.js";
3
+ import { type HelperInstaller } from "./helper-realm.js";
4
+ export interface ActionOptions {
5
+ /**
6
+ * Time (ms) to wait for the element to become actionable. Defaults to config `expect.timeout`, then 5000.
7
+ * 0 checks once without waiting; the stability check is skipped because it needs two probes
8
+ */
9
+ timeout?: number;
10
+ /** Skip every actionability check except "attached" */
11
+ force?: boolean;
12
+ }
13
+ /** What the actionability waits need of a locator: finding its element, calling into the page, naming itself */
14
+ export interface ActionabilityContext {
15
+ resolveOne(): Promise<ElementHandle | null>;
16
+ call<T>(fn: string, args: unknown[], sandbox?: string): Promise<T>;
17
+ /** The locator's description for error messages */
18
+ readonly selector: string;
19
+ /** The page's helper realm; without it the probe source is sent on every poll */
20
+ readonly helpers: HelperInstaller | undefined;
21
+ }
22
+ /** Reads the element's actionability state: one search, one probe */
23
+ export declare function probeState(ctx: ActionabilityContext, options: ProbeOptions): Promise<ElementState>;
24
+ /**
25
+ * Probes until every required check passes and returns that state.
26
+ * Throws ActionTimeoutError describing the last probe on timeout.
27
+ */
28
+ export declare function waitForActionable(ctx: ActionabilityContext, action: string, checks: readonly CheckName[], probe: ProbeOptions, options: ActionOptions | undefined): Promise<ElementState>;
29
+ /** Waits until the element reaches `state` (default "visible"); does not scroll */
30
+ export declare function waitForState(ctx: ActionabilityContext, { state, timeout }: {
31
+ state?: WaitForState;
32
+ timeout?: number;
33
+ }): Promise<void>;