@itmetsam/samurai-framework 0.10.0 → 0.12.1

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 (326) hide show
  1. package/README.md +3 -4
  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 +2 -1
  19. package/dist/browser/dialog-handling.d.ts +32 -0
  20. package/dist/browser/dialog-handling.js +85 -0
  21. package/dist/browser/document-watcher.d.ts +44 -0
  22. package/dist/browser/document-watcher.js +160 -0
  23. package/dist/browser/download-folders.d.ts +0 -1
  24. package/dist/browser/download-folders.js +7 -15
  25. package/dist/browser/download.js +5 -10
  26. package/dist/browser/downloads-dir.js +2 -2
  27. package/dist/browser/emit.d.ts +7 -0
  28. package/dist/browser/emit.js +16 -0
  29. package/dist/browser/emulation.d.ts +1 -1
  30. package/dist/browser/emulation.js +54 -18
  31. package/dist/browser/file-chooser.js +4 -11
  32. package/dist/browser/history.d.ts +6 -0
  33. package/dist/browser/history.js +89 -0
  34. package/dist/browser/index.d.ts +2 -0
  35. package/dist/browser/index.js +2 -0
  36. package/dist/browser/init-script.d.ts +2 -0
  37. package/dist/browser/init-script.js +16 -0
  38. package/dist/browser/launch-firefox.d.ts +20 -0
  39. package/dist/browser/launch-firefox.js +66 -0
  40. package/dist/browser/launch-flags.d.ts +18 -0
  41. package/dist/browser/launch-flags.js +100 -0
  42. package/dist/browser/lazy.d.ts +21 -0
  43. package/dist/browser/lazy.js +44 -0
  44. package/dist/browser/page-events.d.ts +44 -0
  45. package/dist/browser/page-events.js +58 -0
  46. package/dist/browser/page-logs.js +2 -9
  47. package/dist/browser/page-navigation.d.ts +58 -0
  48. package/dist/browser/page-navigation.js +108 -0
  49. package/dist/browser/page-network.d.ts +69 -0
  50. package/dist/browser/page-network.js +163 -0
  51. package/dist/browser/page-settings.d.ts +35 -0
  52. package/dist/browser/page-settings.js +51 -0
  53. package/dist/browser/page-transfers.d.ts +36 -0
  54. package/dist/browser/page-transfers.js +50 -0
  55. package/dist/browser/page.d.ts +39 -154
  56. package/dist/browser/page.js +134 -758
  57. package/dist/browser/pdf.d.ts +1 -1
  58. package/dist/browser/session.d.ts +16 -0
  59. package/dist/browser/session.js +32 -0
  60. package/dist/browser/viewport.d.ts +19 -0
  61. package/dist/browser/viewport.js +72 -0
  62. package/dist/browser/warm-up.d.ts +1 -1
  63. package/dist/browser/warm-up.js +2 -1
  64. package/dist/browsers/index.d.ts +1 -1
  65. package/dist/browsers/index.js +1 -1
  66. package/dist/browsers/pins.d.ts +0 -2
  67. package/dist/browsers/pins.js +0 -2
  68. package/dist/browsers/store.d.ts +0 -2
  69. package/dist/browsers/store.js +1 -2
  70. package/dist/cli/args.d.ts +13 -6
  71. package/dist/cli/args.js +13 -10
  72. package/dist/cli/browsers.d.ts +1 -2
  73. package/dist/cli/browsers.js +2 -2
  74. package/dist/cli/console-reporter.js +3 -2
  75. package/dist/cli/flow-reporter.js +2 -1
  76. package/dist/cli/flow.d.ts +1 -2
  77. package/dist/cli/flow.js +25 -22
  78. package/dist/cli/init.js +2 -7
  79. package/dist/cli/io.d.ts +10 -0
  80. package/dist/cli/io.js +1 -0
  81. package/dist/cli/main.d.ts +3 -10
  82. package/dist/cli/main.js +10 -143
  83. package/dist/cli/paint.d.ts +2 -0
  84. package/dist/cli/paint.js +4 -0
  85. package/dist/cli/record.d.ts +4 -0
  86. package/dist/cli/record.js +93 -0
  87. package/dist/cli/usage.d.ts +1 -0
  88. package/dist/cli/usage.js +58 -0
  89. package/dist/cli/version.d.ts +2 -0
  90. package/dist/cli/version.js +7 -0
  91. package/dist/config/browsers.d.ts +11 -0
  92. package/dist/config/browsers.js +32 -0
  93. package/dist/config/config.js +4 -1
  94. package/dist/config/mask.d.ts +1 -1
  95. package/dist/config/mask.js +7 -2
  96. package/dist/config/run-settings.d.ts +1 -2
  97. package/dist/config/run-settings.js +1 -6
  98. package/dist/config/timeouts.d.ts +2 -0
  99. package/dist/config/timeouts.js +13 -0
  100. package/dist/config/workers.d.ts +14 -0
  101. package/dist/config/workers.js +29 -0
  102. package/dist/flows/run/branch-context.d.ts +14 -0
  103. package/dist/flows/run/branch-context.js +35 -0
  104. package/dist/flows/run/handlers/condition.d.ts +1 -1
  105. package/dist/flows/run/handlers/end.d.ts +1 -1
  106. package/dist/flows/run/handlers/group.d.ts +1 -1
  107. package/dist/flows/run/handlers/index.d.ts +2 -2
  108. package/dist/flows/run/handlers/parallel.d.ts +1 -1
  109. package/dist/flows/run/handlers/start.d.ts +1 -1
  110. package/dist/flows/run/handlers/support.d.ts +3 -27
  111. package/dist/flows/run/handlers/test.d.ts +1 -1
  112. package/dist/flows/run/handlers/types.d.ts +27 -0
  113. package/dist/flows/run/handlers/types.js +1 -0
  114. package/dist/flows/run/index.d.ts +1 -1
  115. package/dist/flows/run/prepare-flow.d.ts +14 -0
  116. package/dist/flows/run/prepare-flow.js +44 -0
  117. package/dist/flows/run/project.d.ts +33 -2
  118. package/dist/flows/run/project.js +139 -57
  119. package/dist/flows/run/run.d.ts +3 -4
  120. package/dist/flows/run/run.js +23 -193
  121. package/dist/flows/run/walker.d.ts +56 -0
  122. package/dist/flows/run/walker.js +225 -0
  123. package/dist/flows/steps/catalog.d.ts +1 -1
  124. package/dist/flows/steps/handler.d.ts +1 -1
  125. package/dist/flows/steps/set.d.ts +1 -1
  126. package/dist/index.js +1 -1
  127. package/dist/locator/actionability.d.ts +33 -0
  128. package/dist/locator/actionability.js +104 -0
  129. package/dist/locator/chain-resolver.d.ts +20 -0
  130. package/dist/locator/chain-resolver.js +102 -0
  131. package/dist/locator/document-swap.d.ts +1 -0
  132. package/dist/locator/document-swap.js +11 -0
  133. package/dist/locator/element-state.d.ts +3 -1
  134. package/dist/locator/element-state.js +11 -3
  135. package/dist/locator/file-actions.d.ts +35 -0
  136. package/dist/locator/file-actions.js +55 -0
  137. package/dist/{script/helpers.js → locator/helper-realm.js} +2 -2
  138. package/dist/locator/input-actions.d.ts +18 -0
  139. package/dist/locator/input-actions.js +80 -0
  140. package/dist/locator/label-locator.d.ts +1 -1
  141. package/dist/locator/label-locator.js +5 -12
  142. package/dist/locator/locator.d.ts +14 -37
  143. package/dist/locator/locator.js +68 -424
  144. package/dist/locator/page-scripts.d.ts +16 -0
  145. package/dist/locator/page-scripts.js +54 -0
  146. package/dist/locator/page-search.d.ts +10 -0
  147. package/dist/locator/page-search.js +19 -0
  148. package/dist/locator/selector-errors.d.ts +0 -4
  149. package/dist/locator/selector-errors.js +0 -7
  150. package/dist/locator/selector.d.ts +14 -13
  151. package/dist/locator/selector.js +10 -16
  152. package/dist/locator/text-locator.d.ts +1 -1
  153. package/dist/locator/text-locator.js +5 -9
  154. package/dist/logger/index.d.ts +6 -0
  155. package/dist/logger/index.js +14 -7
  156. package/dist/network/bidi-wire.d.ts +10 -0
  157. package/dist/network/bidi-wire.js +38 -0
  158. package/dist/network/intercept.d.ts +30 -0
  159. package/dist/network/intercept.js +130 -0
  160. package/dist/network/network-tracker.d.ts +10 -47
  161. package/dist/network/network-tracker.js +34 -68
  162. package/dist/network/network-types.d.ts +33 -0
  163. package/dist/network/network-types.js +1 -0
  164. package/dist/network/network-wait.d.ts +7 -2
  165. package/dist/network/network-wait.js +14 -4
  166. package/dist/network/response.d.ts +1 -1
  167. package/dist/network/response.js +1 -1
  168. package/dist/network/route.d.ts +92 -0
  169. package/dist/network/route.js +142 -0
  170. package/dist/network/router.d.ts +6 -91
  171. package/dist/network/router.js +25 -258
  172. package/dist/recorder/expectation.d.ts +5 -0
  173. package/dist/recorder/expectation.js +20 -0
  174. package/dist/recorder/files-recorder.d.ts +30 -0
  175. package/dist/recorder/files-recorder.js +88 -0
  176. package/dist/recorder/normalise.d.ts +17 -6
  177. package/dist/recorder/normalise.js +96 -91
  178. package/dist/recorder/options.d.ts +56 -0
  179. package/dist/recorder/options.js +1 -0
  180. package/dist/recorder/page-messages.d.ts +35 -0
  181. package/dist/recorder/page-messages.js +26 -0
  182. package/dist/recorder/ranking.js +1 -0
  183. package/dist/recorder/realms.d.ts +6 -0
  184. package/dist/recorder/realms.js +13 -0
  185. package/dist/recorder/recorder.d.ts +10 -74
  186. package/dist/recorder/recorder.js +39 -199
  187. package/dist/recorder/session-browser.d.ts +22 -0
  188. package/dist/recorder/session-browser.js +21 -0
  189. package/dist/recorder/session-secrets.d.ts +8 -0
  190. package/dist/recorder/session-secrets.js +45 -0
  191. package/dist/recorder/session.d.ts +5 -14
  192. package/dist/recorder/session.js +22 -120
  193. package/dist/recorder/spec-source.d.ts +39 -0
  194. package/dist/recorder/spec-source.js +84 -0
  195. package/dist/recorder/toolbar/action-guard.d.ts +21 -0
  196. package/dist/recorder/toolbar/action-guard.js +43 -0
  197. package/dist/recorder/toolbar/actions/assert.d.ts +4 -0
  198. package/dist/recorder/toolbar/actions/assert.js +52 -0
  199. package/dist/recorder/toolbar/actions/insert.d.ts +6 -0
  200. package/dist/recorder/toolbar/actions/insert.js +93 -0
  201. package/dist/recorder/toolbar/actions/pick.d.ts +8 -0
  202. package/dist/recorder/toolbar/actions/pick.js +26 -0
  203. package/dist/recorder/toolbar/actions/reply.d.ts +7 -0
  204. package/dist/recorder/toolbar/actions/reply.js +10 -0
  205. package/dist/recorder/toolbar/controller-fixture.d.ts +47 -0
  206. package/dist/recorder/toolbar/controller-fixture.js +129 -0
  207. package/dist/recorder/toolbar/controller.d.ts +12 -78
  208. package/dist/recorder/toolbar/controller.js +35 -247
  209. package/dist/recorder/toolbar/inspect.d.ts +1 -3
  210. package/dist/recorder/toolbar/inspect.js +2 -2
  211. package/dist/recorder/toolbar/page-scope.d.ts +19 -0
  212. package/dist/recorder/toolbar/page-scope.js +43 -0
  213. package/dist/recorder/toolbar/protocol.d.ts +2 -1
  214. package/dist/recorder/toolbar/source/attach.d.ts +2 -0
  215. package/dist/recorder/toolbar/source/attach.js +41 -0
  216. package/dist/recorder/toolbar/source/dom.d.ts +2 -0
  217. package/dist/recorder/toolbar/source/dom.js +47 -0
  218. package/dist/recorder/toolbar/source/drag.d.ts +2 -0
  219. package/dist/recorder/toolbar/source/drag.js +32 -0
  220. package/dist/recorder/toolbar/source/events-page.d.ts +2 -0
  221. package/dist/recorder/toolbar/source/events-page.js +67 -0
  222. package/dist/recorder/toolbar/source/events-toolbar.d.ts +2 -0
  223. package/dist/recorder/toolbar/source/events-toolbar.js +35 -0
  224. package/dist/recorder/toolbar/source/fields.d.ts +2 -0
  225. package/dist/recorder/toolbar/source/fields.js +44 -0
  226. package/dist/recorder/toolbar/source/handlers.d.ts +2 -0
  227. package/dist/recorder/toolbar/source/handlers.js +134 -0
  228. package/dist/recorder/toolbar/source/names.d.ts +2 -0
  229. package/dist/recorder/toolbar/source/names.js +12 -0
  230. package/dist/recorder/toolbar/source/picking.d.ts +2 -0
  231. package/dist/recorder/toolbar/source/picking.js +72 -0
  232. package/dist/recorder/toolbar/source/placement.d.ts +2 -0
  233. package/dist/recorder/toolbar/source/placement.js +45 -0
  234. package/dist/recorder/toolbar/source/recorder-side.d.ts +2 -0
  235. package/dist/recorder/toolbar/source/recorder-side.js +54 -0
  236. package/dist/recorder/toolbar/source/setup.d.ts +2 -0
  237. package/dist/recorder/toolbar/source/setup.js +50 -0
  238. package/dist/recorder/toolbar/source/views-panels.d.ts +2 -0
  239. package/dist/recorder/toolbar/source/views-panels.js +150 -0
  240. package/dist/recorder/toolbar/source/views-rail.d.ts +2 -0
  241. package/dist/recorder/toolbar/source/views-rail.js +40 -0
  242. package/dist/recorder/toolbar/source/views-render.d.ts +2 -0
  243. package/dist/recorder/toolbar/source/views-render.js +48 -0
  244. package/dist/recorder/toolbar/source.js +30 -856
  245. package/dist/recorder/toolbar/types.d.ts +70 -0
  246. package/dist/recorder/toolbar/types.js +1 -0
  247. package/dist/recorder/toolbar-bridge.d.ts +43 -0
  248. package/dist/recorder/toolbar-bridge.js +106 -0
  249. package/dist/runner/browsers.d.ts +0 -12
  250. package/dist/runner/browsers.js +1 -33
  251. package/dist/runner/options.d.ts +31 -0
  252. package/dist/runner/options.js +1 -0
  253. package/dist/runner/pooled-run.d.ts +43 -0
  254. package/dist/runner/pooled-run.js +75 -0
  255. package/dist/runner/ports.d.ts +2 -0
  256. package/dist/runner/ports.js +2 -0
  257. package/dist/runner/registry.d.ts +1 -1
  258. package/dist/runner/registry.js +1 -1
  259. package/dist/runner/reporter.d.ts +31 -10
  260. package/dist/runner/reporter.js +39 -6
  261. package/dist/runner/run-test.d.ts +16 -0
  262. package/dist/runner/run-test.js +178 -0
  263. package/dist/runner/run.d.ts +5 -3
  264. package/dist/runner/run.js +28 -29
  265. package/dist/runner/select.d.ts +24 -0
  266. package/dist/runner/select.js +77 -0
  267. package/dist/runner/test-dir.d.ts +13 -0
  268. package/dist/runner/test-dir.js +27 -0
  269. package/dist/runner/test-runner.d.ts +25 -56
  270. package/dist/runner/test-runner.js +81 -270
  271. package/dist/runner/worker-pool.d.ts +67 -0
  272. package/dist/runner/worker-pool.js +275 -0
  273. package/dist/runner/worker-process.d.ts +6 -0
  274. package/dist/runner/worker-process.js +54 -0
  275. package/dist/runner/worker-protocol.d.ts +67 -0
  276. package/dist/runner/worker-protocol.js +1 -0
  277. package/dist/runner/worker-session.d.ts +7 -0
  278. package/dist/runner/worker-session.js +60 -0
  279. package/dist/runner/worker.d.ts +26 -0
  280. package/dist/runner/worker.js +121 -0
  281. package/dist/runner/workers-option.d.ts +15 -0
  282. package/dist/runner/workers-option.js +22 -0
  283. package/dist/script/call-function.d.ts +1 -9
  284. package/dist/script/call-function.js +2 -19
  285. package/dist/script/element-handle.d.ts +1 -2
  286. package/dist/script/element-handle.js +1 -2
  287. package/dist/script/serialize.d.ts +3 -12
  288. package/dist/script/serialize.js +53 -62
  289. package/dist/steps/parse/call.d.ts +3 -0
  290. package/dist/steps/parse/call.js +81 -0
  291. package/dist/steps/parse/expectation.d.ts +5 -0
  292. package/dist/steps/parse/expectation.js +71 -0
  293. package/dist/steps/parse/file-entry.d.ts +4 -0
  294. package/dist/steps/parse/file-entry.js +74 -0
  295. package/dist/steps/parse/literals.d.ts +4 -0
  296. package/dist/steps/parse/literals.js +28 -0
  297. package/dist/steps/parse/step.d.ts +4 -0
  298. package/dist/steps/parse/step.js +81 -0
  299. package/dist/steps/parse.js +2 -323
  300. package/dist/transport/bidi-connection.d.ts +4 -15
  301. package/dist/transport/bidi-connection.js +9 -63
  302. package/dist/transport/log-redaction.d.ts +13 -0
  303. package/dist/transport/log-redaction.js +51 -0
  304. package/dist/transport/subscriptions.js +18 -13
  305. package/dist/transport/unsupported.d.ts +7 -0
  306. package/dist/transport/unsupported.js +13 -0
  307. package/dist/types/bidi-modules/browser.d.ts +1 -12
  308. package/dist/types/bidi-modules/browsing-context.d.ts +8 -18
  309. package/dist/types/bidi-modules/emulation.d.ts +29 -21
  310. package/dist/types/bidi-modules/input.d.ts +1 -9
  311. package/dist/types/bidi-modules/network.d.ts +29 -24
  312. package/dist/types/bidi-modules/script.d.ts +0 -9
  313. package/dist/types/bidi-modules/session.d.ts +0 -8
  314. package/dist/types/bidi-modules/storage.d.ts +0 -5
  315. package/dist/types/bidi.d.ts +7 -37
  316. package/dist/types/config.d.ts +12 -0
  317. package/dist/types/test.d.ts +8 -3
  318. package/dist/wait/wait-until.d.ts +1 -5
  319. package/dist/wait/wait-until.js +7 -16
  320. package/package.json +3 -2
  321. package/dist/config/default.d.ts +0 -2
  322. package/dist/config/default.js +0 -3
  323. package/dist/types/bidi-modules/web-extensions.d.ts +0 -9
  324. package/dist/types/bidi-protocols/command.d.ts +0 -7
  325. package/dist/types/bidi-protocols/event.d.ts +0 -17
  326. /package/dist/{script/helpers.d.ts → locator/helper-realm.d.ts} +0 -0
@@ -0,0 +1,70 @@
1
+ import type { ElementHandle } from "../../script/element-handle.js";
2
+ import type { LocatorCall, LocatorSpec } from "../../steps/model.js";
3
+ import type { Observation } from "../normalise.js";
4
+ import type { ActionReply, PickInfo, ToolbarMessage, ToolbarState } from "./protocol.js";
5
+ /** What `inspect` found for a picked element: the reply, and the locator behind each Count choice */
6
+ export interface Inspection {
7
+ info: PickInfo & {
8
+ ok: true;
9
+ };
10
+ /** `counts[i]` is the locator `info.counts[i]` describes */
11
+ counts: LocatorCall[];
12
+ }
13
+ /** Everything the controller does to the browser and the project; the recorder implements these */
14
+ export interface ControllerDeps {
15
+ /** Sends the state to every loaded top-level document's toolbar */
16
+ push(state: ToolbarState): void;
17
+ reply(id: number, result: PickInfo | ActionReply): void;
18
+ currentUrl(): Promise<string>;
19
+ rank(target: ElementHandle, candidates: LocatorCall[]): Promise<LocatorSpec | undefined>;
20
+ inspect(target: ElementHandle, locator: LocatorSpec, candidates: LocatorCall[]): Promise<Inspection>;
21
+ /** Types into the field with capture muted */
22
+ type(locator: LocatorSpec, value: string): Promise<void>;
23
+ mute(): Promise<void>;
24
+ unmute(): Promise<void>;
25
+ addVariable(name: string, value: string): Promise<void>;
26
+ setSecret(name: string, value: string): Promise<void>;
27
+ /** A secret was written to the project (called before its value is typed) */
28
+ secretAdded(name: string): void;
29
+ observe(observation: Observation): void;
30
+ stop(): void;
31
+ onStatus(status: {
32
+ state: "recording" | "paused";
33
+ toolbar: boolean;
34
+ }): void;
35
+ }
36
+ export interface ControllerOptions {
37
+ environment: string;
38
+ canAddVariables: boolean;
39
+ variables: Record<string, string | number | boolean>;
40
+ /** Name → value; undefined when not set */
41
+ secrets: Record<string, string | undefined>;
42
+ secretsIgnored: boolean;
43
+ /** Screenshot steps already in the test, for the default name */
44
+ screenshots: number;
45
+ }
46
+ export type Message = ToolbarMessage & {
47
+ target?: ElementHandle;
48
+ };
49
+ export type Of<A extends Message["action"]> = Extract<Message, {
50
+ action: A;
51
+ }>;
52
+ /** The element picked for an Assert, and the locator behind each of its Count choices */
53
+ export interface PickState {
54
+ id: number;
55
+ locator: LocatorSpec;
56
+ counts: LocatorCall[];
57
+ }
58
+ /** What the action handlers (assert, insert, add a variable or secret) use of the controller */
59
+ export interface ActionContext {
60
+ readonly deps: ControllerDeps;
61
+ readonly options: ControllerOptions;
62
+ readonly variables: Map<string, string>;
63
+ readonly secrets: Map<string, string | undefined>;
64
+ isPaused(): boolean;
65
+ /** The picked element an `assert` with the same id refers to */
66
+ pick(): PickState | undefined;
67
+ setPick(pick: PickState | undefined): void;
68
+ showToast(text: string): void;
69
+ push(): void;
70
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import type Page from "../browser/page.js";
2
+ import type { BiDiConnector } from "../transport/bidi-connection.js";
3
+ import type { Observation } from "./normalise.js";
4
+ import type { ToolbarOptions } from "./options.js";
5
+ import { ToolbarController } from "./toolbar/controller.js";
6
+ /** The toolbar's hook in its sandbox */
7
+ export declare const TOOLBAR_HOOK: string;
8
+ /** What the bridge needs of the recording it belongs to */
9
+ export interface BridgeHost {
10
+ page: Page;
11
+ connector: BiDiConnector;
12
+ context: string;
13
+ mute(): Promise<void>;
14
+ unmute(): Promise<void>;
15
+ /** A step the toolbar made (wait, screenshot, insert, assertion) */
16
+ observe(observation: Observation): void;
17
+ /** The toolbar's Stop, when the caller did not ask to be told */
18
+ stop(): void;
19
+ /** Runs `work` in order with the recording's other events */
20
+ enqueue(work: () => Promise<void>): void;
21
+ }
22
+ /**
23
+ * The recorder's side of the toolbar: builds its controller, sends it state and replies, and takes its messages
24
+ * only from the toolbar's own sandbox realm (the page cannot reach that sandbox).
25
+ */
26
+ export declare class ToolbarBridge {
27
+ private readonly host;
28
+ readonly controller: ToolbarController;
29
+ /** The context's `samurai-toolbar` sandbox realms, as last listed */
30
+ private realms;
31
+ /** Messages waiting for a realm lookup, and those that arrived behind them (kept in order) */
32
+ private backlog;
33
+ private waiting;
34
+ constructor(host: BridgeHost, toolbar: ToolbarOptions);
35
+ /** A message that came on the toolbar's channel from `realm` */
36
+ receive(realm: string, data: unknown): void;
37
+ private listRealms;
38
+ /** A message from the toolbar's own sandbox */
39
+ private fromToolbar;
40
+ private createController;
41
+ /** Calls the toolbar hook's `method` in the loaded document; a document between loads has no toolbar yet */
42
+ private call;
43
+ }
@@ -0,0 +1,106 @@
1
+ import logger from "../logger/index.js";
2
+ import { callFunction } from "../script/call-function.js";
3
+ import { fromRemoteValue } from "../script/serialize.js";
4
+ import { locatorFromSpec } from "../steps/build.js";
5
+ import { addVariable, setSecret } from "../project/index.js";
6
+ import { listSandboxRealms } from "./realms.js";
7
+ import { rankLocator } from "./ranking.js";
8
+ import { ToolbarController } from "./toolbar/controller.js";
9
+ import { inspectElement } from "./toolbar/inspect.js";
10
+ import { TOOLBAR_KEY, TOOLBAR_SANDBOX, } from "./toolbar/protocol.js";
11
+ /** The toolbar's hook in its sandbox */
12
+ export const TOOLBAR_HOOK = `globalThis[Symbol.for(${JSON.stringify(TOOLBAR_KEY)})]`;
13
+ /**
14
+ * The recorder's side of the toolbar: builds its controller, sends it state and replies, and takes its messages
15
+ * only from the toolbar's own sandbox realm (the page cannot reach that sandbox).
16
+ */
17
+ export class ToolbarBridge {
18
+ constructor(host, toolbar) {
19
+ this.host = host;
20
+ /** The context's `samurai-toolbar` sandbox realms, as last listed */
21
+ this.realms = new Set();
22
+ /** Messages waiting for a realm lookup, and those that arrived behind them (kept in order) */
23
+ this.backlog = Promise.resolve();
24
+ this.waiting = 0;
25
+ this.controller = this.createController(toolbar);
26
+ }
27
+ /** A message that came on the toolbar's channel from `realm` */
28
+ receive(realm, data) {
29
+ if (this.waiting === 0 && this.realms.has(realm)) {
30
+ this.fromToolbar(data);
31
+ return;
32
+ }
33
+ // A sandbox not seen yet (a new document's toolbar): look it up, and keep later messages behind it
34
+ this.waiting++;
35
+ this.backlog = this.backlog
36
+ .then(async () => {
37
+ if (!this.realms.has(realm))
38
+ await this.listRealms();
39
+ if (this.realms.has(realm))
40
+ this.fromToolbar(data);
41
+ else
42
+ logger.warn("Recorder ignored a toolbar message from outside the toolbar's sandbox");
43
+ })
44
+ .catch((err) => {
45
+ logger.debug("Could not check a toolbar message's realm", { err });
46
+ })
47
+ .finally(() => this.waiting--);
48
+ }
49
+ async listRealms() {
50
+ this.realms = await listSandboxRealms(this.host.connector, this.host.context, TOOLBAR_SANDBOX);
51
+ }
52
+ /** A message from the toolbar's own sandbox */
53
+ fromToolbar(data) {
54
+ const message = fromRemoteValue(data);
55
+ const controller = this.controller;
56
+ if (message.type !== "toolbar" || !("action" in message))
57
+ return;
58
+ if (message.action === "ready") {
59
+ // Not behind a slow insert: the ready watchdog is waiting for it
60
+ void controller.handle(message);
61
+ }
62
+ else if (controller.accept(message)) {
63
+ // Decided as it arrives: a double click's second copy would only run after the first had finished
64
+ this.host.enqueue(async () => controller.handle(message));
65
+ }
66
+ }
67
+ createController(toolbar) {
68
+ const { page, connector, context } = this.host;
69
+ const { projectDir, environment, onStatus, onStop, onSecretAdded } = toolbar;
70
+ return new ToolbarController({
71
+ push: (state) => this.call("update", [state]),
72
+ reply: (id, result) => this.call("reply", [id, result]),
73
+ currentUrl: () => page.url(),
74
+ rank: (target, candidates) => rankLocator(page, target, candidates),
75
+ inspect: (target, locator, candidates) => inspectElement(page, connector, context, target, locator, candidates),
76
+ type: async (locator, value) => {
77
+ await this.host.mute();
78
+ try {
79
+ await locatorFromSpec(page, locator).fill(value, {
80
+ timeout: 5000,
81
+ });
82
+ }
83
+ finally {
84
+ await this.host.unmute();
85
+ }
86
+ },
87
+ mute: () => this.host.mute(),
88
+ unmute: () => this.host.unmute(),
89
+ addVariable: (name, value) => addVariable(projectDir, environment, name, value),
90
+ setSecret: (name, value) => setSecret(projectDir, environment, name, value),
91
+ secretAdded: (name) => onSecretAdded?.(name),
92
+ observe: (observation) => this.host.observe(observation),
93
+ stop: () => {
94
+ if (onStop)
95
+ onStop();
96
+ else
97
+ this.host.stop();
98
+ },
99
+ onStatus: (status) => onStatus?.(status),
100
+ }, toolbar);
101
+ }
102
+ /** Calls the toolbar hook's `method` in the loaded document; a document between loads has no toolbar yet */
103
+ call(method, args) {
104
+ callFunction(this.host.connector, this.host.context, `(...args) => { ${TOOLBAR_HOOK}?.${method}(...args); }`, args, { sandbox: TOOLBAR_SANDBOX, awaitPromise: false }).catch((err) => logger.debug("Could not reach the recorder toolbar", { err }));
105
+ }
106
+ }
@@ -1,16 +1,4 @@
1
1
  import type { SupportedBrowser } from "../types/browser.js";
2
- import type { SamuraiTestConfig } from "../types/config.js";
3
- /** Every browser a config may name, in the order the messages list them */
4
- export declare const BROWSER_NAMES: readonly SupportedBrowser[];
5
- /** The browsers of a run that names none */
6
- export declare const DEFAULT_BROWSERS: readonly SupportedBrowser[];
7
- /** Throws when `list` is not a non-empty list of distinct browser names; `where` names the key in the message */
8
- export declare function checkBrowserList(list: unknown, where: string): void;
9
- /**
10
- * Checks the browser keys of a config: the removed `browser`, and `browsers` on the project and on each group.
11
- * Throws an Error that says what to write instead.
12
- */
13
- export declare function validateBrowserConfig(config: SamuraiTestConfig): void;
14
2
  /**
15
3
  * The browsers a run uses, the first that is set winning: the command line, the group being run, the project, then
16
4
  * Firefox. An empty list counts as not given.
@@ -1,37 +1,5 @@
1
- /** Every browser a config may name, in the order the messages list them */
2
- export const BROWSER_NAMES = ["firefox", "chrome"];
3
1
  /** The browsers of a run that names none */
4
- export const DEFAULT_BROWSERS = ["firefox"];
5
- const isBrowser = (value) => typeof value === "string" &&
6
- BROWSER_NAMES.includes(value);
7
- /** Throws when `list` is not a non-empty list of distinct browser names; `where` names the key in the message */
8
- export function checkBrowserList(list, where) {
9
- if (!Array.isArray(list) || list.length === 0)
10
- throw new Error(`${where} must list at least one browser.`);
11
- const seen = new Set();
12
- for (const name of list) {
13
- if (!isBrowser(name))
14
- throw new Error(`Unknown browser "${String(name)}" in ${where}. Choose ${BROWSER_NAMES.join(" or ")}.`);
15
- if (seen.has(name))
16
- throw new Error(`Browser "${name}" is listed twice in ${where}.`);
17
- seen.add(name);
18
- }
19
- }
20
- /**
21
- * Checks the browser keys of a config: the removed `browser`, and `browsers` on the project and on each group.
22
- * Throws an Error that says what to write instead.
23
- */
24
- export function validateBrowserConfig(config) {
25
- const legacy = config.browser;
26
- if (legacy !== undefined)
27
- throw new Error(`"browser" was replaced by "browsers": use browsers: [${JSON.stringify(legacy)}]`);
28
- if (config.browsers !== undefined)
29
- checkBrowserList(config.browsers, "browsers");
30
- config.groups?.forEach((group, index) => {
31
- if (group.browsers !== undefined)
32
- checkBrowserList(group.browsers, `groups[${index}].browsers`);
33
- });
34
- }
2
+ const DEFAULT_BROWSERS = ["firefox"];
35
3
  /**
36
4
  * The browsers a run uses, the first that is set winning: the command line, the group being run, the project, then
37
5
  * Firefox. An empty list counts as not given.
@@ -0,0 +1,31 @@
1
+ import type { SupportedBrowser } from "../types/browser.js";
2
+ import type { RunEvent } from "./reporter.js";
3
+ /** What decides which tests of the found files run, and how the browser starts */
4
+ export interface RunnerOptions {
5
+ /** Spec files to run, absolute or relative to the project folder. Default: every `*.spec.ts` under `srcDir` */
6
+ files?: string[];
7
+ /** Only tests whose full name matches (a string must be contained in it) */
8
+ grep?: string | RegExp;
9
+ /** Only tests with exactly one of these full names (`describe` titles joined with " > ") */
10
+ testNames?: string[];
11
+ /** Only the tests of this group of the config's `groups` (unknown name: an error). Combines with the other filters */
12
+ group?: string;
13
+ /** The browsers to run in, instead of the group's or the project's `browsers` (the command line's `--browser`) */
14
+ browsers?: SupportedBrowser[];
15
+ /**
16
+ * How many tests run at the same time, each in a worker process: a whole number from 1 or a share of the CPUs
17
+ * like `"50%"` (the command line's `--workers`). Beats `SAMURAI_WORKERS` and the config's `workers`.
18
+ * @default the config's `workers`, else half the CPUs
19
+ */
20
+ workers?: number | string;
21
+ /** Run the browser without a window. @default false */
22
+ headless?: boolean;
23
+ /** Remote debugging port of the browser. @default DEFAULT_PORT (9223) */
24
+ port?: number;
25
+ /** Where the report is written; `false` writes none. @default `<project>/result/report.json` */
26
+ reportPath?: string | false;
27
+ /** Receives start and end of the run and of every test, as they happen */
28
+ onEvent?: (event: RunEvent) => void;
29
+ /** Aborting stops the running test (its browser is closed), skips the rest and reports the run as failed */
30
+ signal?: AbortSignal;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import type { SupportedBrowser } from "../types/browser.js";
2
+ import type { SamuraiGroup } from "../types/config.js";
3
+ import type { RegisteredTestCase, TestResult } from "../types/test.js";
4
+ import type { ResolvedGroup } from "./groups.js";
5
+ import type TestReporter from "./reporter.js";
6
+ import type { RunHooks } from "./worker-pool.js";
7
+ import type { WorkItem } from "./worker-protocol.js";
8
+ /** What a run needs from a worker pool: `WorkerPool` itself, or a fake in tests */
9
+ export interface PooledRunner {
10
+ run(items: WorkItem[], hooks?: RunHooks): Promise<TestResult[]>;
11
+ }
12
+ /** A test in a browser, as the run lists it: the work item at the same position says the same in a form a worker reads */
13
+ export interface PlannedTest {
14
+ test: RegisteredTestCase;
15
+ browser: SupportedBrowser;
16
+ }
17
+ /**
18
+ * Lists the run: every selected test in the first browser, then every selected test in the next one. The
19
+ * position in the list is the item's `index`. `serialKey` says which group makes a test run alone.
20
+ */
21
+ export declare function planItems(selected: RegisteredTestCase[], browsers: SupportedBrowser[], serialKey: (test: RegisteredTestCase) => string | undefined): {
22
+ items: WorkItem[];
23
+ planned: PlannedTest[];
24
+ };
25
+ /**
26
+ * Which key makes a test run alone: the first of the config's groups with `workers: 1` that holds it. `groups`
27
+ * and `configGroups` are the same list, resolved and as written.
28
+ */
29
+ export declare function serialKeyOf(groups: ResolvedGroup[], configGroups: SamuraiGroup[] | undefined, srcDir: string): (test: RegisteredTestCase) => string | undefined;
30
+ /**
31
+ * How a run goes: through the pool, in this process, or in this process because the workers could not read the
32
+ * config (it was given as an object; a worker reads `samurai.config.ts` from the project folder)
33
+ */
34
+ export declare function poolMode(input: {
35
+ workers: number;
36
+ items: number;
37
+ inlineConfig: boolean;
38
+ }): "pool" | "in-process" | "inline-config";
39
+ /**
40
+ * Runs the planned items on the pool and reports them as they happen. An item the pool answers without events
41
+ * (a queued one cut short by an abort, or one whose worker died before it began) is reported here at the end.
42
+ */
43
+ export declare function runPooled(pool: PooledRunner, planned: PlannedTest[], items: WorkItem[], reporter: TestReporter, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,75 @@
1
+ import { idOf } from "./select.js";
2
+ /**
3
+ * Lists the run: every selected test in the first browser, then every selected test in the next one. The
4
+ * position in the list is the item's `index`. `serialKey` says which group makes a test run alone.
5
+ */
6
+ export function planItems(selected, browsers, serialKey) {
7
+ const planned = browsers.flatMap((browser) => selected.map((test) => ({ test, browser })));
8
+ const items = planned.map(({ test, browser }, index) => {
9
+ const key = serialKey(test);
10
+ return {
11
+ index,
12
+ file: test.file,
13
+ testName: test.name,
14
+ browser,
15
+ ...(key !== undefined && { serialKey: key }),
16
+ };
17
+ });
18
+ return { items, planned };
19
+ }
20
+ /**
21
+ * Which key makes a test run alone: the first of the config's groups with `workers: 1` that holds it. `groups`
22
+ * and `configGroups` are the same list, resolved and as written.
23
+ */
24
+ export function serialKeyOf(groups, configGroups, srcDir) {
25
+ const keys = new Map();
26
+ groups.forEach((group, index) => {
27
+ if (configGroups?.[index]?.workers !== 1)
28
+ return;
29
+ for (const id of group.testIds)
30
+ if (!keys.has(id))
31
+ keys.set(id, `group:${group.name}`);
32
+ });
33
+ return (test) => keys.get(idOf(test, srcDir));
34
+ }
35
+ /**
36
+ * How a run goes: through the pool, in this process, or in this process because the workers could not read the
37
+ * config (it was given as an object; a worker reads `samurai.config.ts` from the project folder)
38
+ */
39
+ export function poolMode(input) {
40
+ if (input.workers <= 1 || input.items <= 1)
41
+ return "in-process";
42
+ return input.inlineConfig ? "inline-config" : "pool";
43
+ }
44
+ /**
45
+ * Runs the planned items on the pool and reports them as they happen. An item the pool answers without events
46
+ * (a queued one cut short by an abort, or one whose worker died before it began) is reported here at the end.
47
+ */
48
+ export async function runPooled(pool, planned, items, reporter, signal) {
49
+ reporter.setOrder(planned);
50
+ const started = new Set();
51
+ const ended = new Set();
52
+ const begin = (index) => {
53
+ if (started.has(index))
54
+ return;
55
+ started.add(index);
56
+ const { test, browser } = planned[index];
57
+ reporter.onTestStart(test, browser);
58
+ };
59
+ const end = (index, result) => {
60
+ begin(index);
61
+ ended.add(index);
62
+ const { test, browser } = planned[index];
63
+ reporter.onRemoteEnd(test, browser, result);
64
+ };
65
+ const results = await pool.run(items, {
66
+ ...(signal && { signal }),
67
+ onEvent: (event) => event.type === "test-start"
68
+ ? begin(event.item.index)
69
+ : end(event.item.index, event.result),
70
+ });
71
+ results.forEach((result, index) => {
72
+ if (!ended.has(index))
73
+ end(index, result);
74
+ });
75
+ }
@@ -0,0 +1,2 @@
1
+ /** The browser's debugging port when a run or a recording is given none */
2
+ export declare const DEFAULT_PORT = 9223;
@@ -0,0 +1,2 @@
1
+ /** The browser's debugging port when a run or a recording is given none */
2
+ export const DEFAULT_PORT = 9223;
@@ -3,5 +3,5 @@ import type { RegisteredTestCase, TestCaseBase } from "../types/test.js";
3
3
  export declare function describe(title: string, fn: () => void): void;
4
4
  export declare function test(title: string, fn: TestCaseBase["function"]): void;
5
5
  export declare function registeredTests(): readonly RegisteredTestCase[];
6
- /** Forgets every registered test (tests only) */
6
+ /** Forgets every registered test: a run registers its specs afresh */
7
7
  export declare function clearRegistry(): void;
@@ -26,7 +26,7 @@ export function test(title, fn) {
26
26
  export function registeredTests() {
27
27
  return registered;
28
28
  }
29
- /** Forgets every registered test (tests only) */
29
+ /** Forgets every registered test: a run registers its specs afresh */
30
30
  export function clearRegistry() {
31
31
  registered.length = 0;
32
32
  titleStack.length = 0;
@@ -7,6 +7,8 @@ export type RunEvent = {
7
7
  /** Every selected test in every browser */
8
8
  total: number;
9
9
  browsers: SupportedBrowser[];
10
+ /** How many tests run at the same time; absent when they run one after the other */
11
+ workers?: number;
10
12
  } | {
11
13
  type: "test-start";
12
14
  name: string;
@@ -32,24 +34,43 @@ export interface ReporterOptions {
32
34
  /** Called with every event, in order. A throwing listener is ignored */
33
35
  onEvent?: (event: RunEvent) => void;
34
36
  }
35
- export default class TestReporter {
37
+ /** What `runOneTest` reports a test to: the reporter of a run, or a worker that relays it */
38
+ export interface TestSink {
39
+ onTestStart(test: RegisteredTestCase, browser: SupportedBrowser): void;
40
+ onTestEnd(test: RegisteredTestCase, browser: SupportedBrowser, error?: TestError, extras?: TestEndExtras): void;
41
+ }
42
+ /** What a test's report entry lists besides its status */
43
+ export interface TestEndExtras {
44
+ logs?: TestLogEntry[];
45
+ logsDropped?: number;
46
+ screenshots?: {
47
+ name: string;
48
+ path: string;
49
+ }[];
50
+ }
51
+ export default class TestReporter implements TestSink {
36
52
  private testResults;
37
53
  private summary;
38
54
  private environment;
39
55
  private browsers;
40
56
  private output;
41
57
  private listener;
58
+ /** Keys of the run's tests in the order the report lists them; unset: the order they started in */
59
+ private order;
42
60
  constructor(options: ReporterOptions);
43
61
  private emit;
44
- onStart(total?: number): void;
62
+ /** Lists the tests of the run in the order the report shows them, whichever order they end in */
63
+ setOrder(tests: {
64
+ test: RegisteredTestCase;
65
+ browser: SupportedBrowser;
66
+ }[]): void;
67
+ onStart(total: number, workers?: number): void;
45
68
  onEnd(): Promise<TestSummary>;
46
69
  onTestStart(test: RegisteredTestCase, browser: SupportedBrowser): void;
47
- onTestEnd(test: RegisteredTestCase, browser: SupportedBrowser, error?: TestError, extras?: {
48
- logs?: TestLogEntry[];
49
- logsDropped?: number;
50
- screenshots?: {
51
- name: string;
52
- path: string;
53
- }[];
54
- }): void;
70
+ /**
71
+ * Ends a test that ran in a worker: the worker built the result (masked, with its own duration), this keeps the
72
+ * start time from this process's clock, as the worker's clock has another origin
73
+ */
74
+ onRemoteEnd(test: RegisteredTestCase, browser: SupportedBrowser, result: TestResult): void;
75
+ onTestEnd(test: RegisteredTestCase, browser: SupportedBrowser, error?: TestError, extras?: TestEndExtras): void;
55
76
  }
@@ -23,8 +23,12 @@ export default class TestReporter {
23
23
  // A listener must not break the run
24
24
  }
25
25
  }
26
- onStart(total = 0) {
27
- const t = performance.mark(`group-start`);
26
+ /** Lists the tests of the run in the order the report shows them, whichever order they end in */
27
+ setOrder(tests) {
28
+ this.order = tests.map(({ test, browser }) => keyOf(test, browser));
29
+ }
30
+ onStart(total, workers = 1) {
31
+ const t = performance.mark("run-start");
28
32
  this.summary = {
29
33
  duration: 0,
30
34
  status: "failed",
@@ -38,18 +42,27 @@ export default class TestReporter {
38
42
  environment: this.environment,
39
43
  total,
40
44
  browsers: this.browsers,
45
+ ...(workers > 1 && { workers }),
41
46
  });
42
47
  }
43
48
  async onEnd() {
44
- performance.mark(`group-finish`);
45
- const duration = performance.measure("test-duration", `group-start`, `group-finish`).duration;
49
+ performance.mark("run-end");
50
+ const duration = performance.measure("test-duration", "run-start", "run-end").duration;
51
+ const tests = this.order
52
+ ? [
53
+ ...this.order.flatMap((key) => this.testResults.get(key) ?? []),
54
+ ...[...this.testResults]
55
+ .filter(([key]) => !this.order.includes(key))
56
+ .map(([, result]) => result),
57
+ ]
58
+ : [...this.testResults.values()];
46
59
  this.summary = {
47
60
  ...this.summary,
48
61
  duration,
49
62
  environment: this.environment,
50
63
  browsers: this.browsers,
51
- tests: Array.from(this.testResults.values()),
52
- status: Array.from(this.testResults.values()).every((r) => r.status === "success")
64
+ tests,
65
+ status: tests.every((r) => r.status === "success")
53
66
  ? "success"
54
67
  : "failed",
55
68
  };
@@ -79,6 +92,26 @@ export default class TestReporter {
79
92
  browser,
80
93
  });
81
94
  }
95
+ /**
96
+ * Ends a test that ran in a worker: the worker built the result (masked, with its own duration), this keeps the
97
+ * start time from this process's clock, as the worker's clock has another origin
98
+ */
99
+ onRemoteEnd(test, browser, result) {
100
+ const key = keyOf(test, browser);
101
+ const started = this.testResults.get(key);
102
+ const ended = {
103
+ ...result,
104
+ ...(started && { startTime: started.startTime }),
105
+ };
106
+ this.testResults.set(key, ended);
107
+ this.emit({
108
+ type: "test-end",
109
+ name: test.name,
110
+ file: test.file,
111
+ browser,
112
+ result: ended,
113
+ });
114
+ }
82
115
  onTestEnd(test, browser, error, extras) {
83
116
  const key = keyOf(test, browser);
84
117
  performance.mark(`${key}-finish`);
@@ -0,0 +1,16 @@
1
+ import type { SupportedBrowser } from "../types/browser.js";
2
+ import type { RegisteredTestCase } from "../types/test.js";
3
+ import type { RunnerOptions } from "./options.js";
4
+ import type { PreparedRun } from "./prepare-run.js";
5
+ import type { TestSink } from "./reporter.js";
6
+ /** What a worker gives its tests that the main process does not: its own port and profile, and its number */
7
+ export interface WorkerResources {
8
+ /** The browser's debugging port. @default `options.port`, else DEFAULT_PORT */
9
+ port?: number;
10
+ /** The browser's profile folder. @default the browser's own */
11
+ profileDir?: string;
12
+ /** The `workerIndex` fixture. @default 0 */
13
+ workerIndex?: number;
14
+ }
15
+ /** Runs one test in one browser and reports it: launch, run the body, judge the page's logs, close the browser */
16
+ export declare function runOneTest(test: RegisteredTestCase, browserName: SupportedBrowser, run: PreparedRun, options: RunnerOptions, srcDir: string, reporter: TestSink, resources?: WorkerResources): Promise<void>;