@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,129 @@
1
+ import assert from "node:assert/strict";
2
+ import { ElementHandle } from "../../script/element-handle.js";
3
+ import { ToolbarController } from "./controller.js";
4
+ export const SECRET = "hunter2-very-secret";
5
+ export const NEW_SECRET = "n3w-s3cr3t-value";
6
+ const OPTIONS = {
7
+ environment: "dev",
8
+ canAddVariables: true,
9
+ variables: { testUser: "dev@example.com", retries: 3 },
10
+ secrets: { ADMIN_PASSWORD: SECRET, API_TOKEN: undefined },
11
+ secretsIgnored: true,
12
+ screenshots: 0,
13
+ };
14
+ export const TARGET = new ElementHandle("el-1");
15
+ export const CANDIDATES = [
16
+ { method: "getByRole", role: "textbox", name: "Email" },
17
+ ];
18
+ export const LOCATOR = { chain: [CANDIDATES[0]], fallbacks: [] };
19
+ export const COUNT_CALL = {
20
+ method: "getByRole",
21
+ role: "listitem",
22
+ };
23
+ export const INFO = {
24
+ ok: true,
25
+ label: `textbox "Email"`,
26
+ text: "",
27
+ value: "a@b.c",
28
+ attributes: [{ name: "id", value: "email" }],
29
+ first: { matcher: "toHaveValue", expected: "a@b.c" },
30
+ counts: [{ label: "listitem", count: 3 }],
31
+ };
32
+ export const lastOf = (items) => items[items.length - 1];
33
+ export const msg = (message) => ({ type: "toolbar", ...message });
34
+ export const insert = (id, kind, name) => msg({
35
+ action: "insert",
36
+ id,
37
+ target: TARGET,
38
+ candidates: CANDIDATES,
39
+ kind,
40
+ name,
41
+ });
42
+ export const add = (id, kind, name, value) => msg({
43
+ action: "add-insertable",
44
+ id,
45
+ target: TARGET,
46
+ candidates: CANDIDATES,
47
+ kind,
48
+ name,
49
+ value,
50
+ });
51
+ export const pick = (id) => msg({ action: "pick", id, target: TARGET, candidates: CANDIDATES });
52
+ /** A controller over fakes that record everything */
53
+ export function setup(options = {}, overrides = {}) {
54
+ const pushes = [];
55
+ const replies = [];
56
+ const observed = [];
57
+ const statuses = [];
58
+ const calls = [];
59
+ const page = { url: "http://localhost/a" };
60
+ let controller;
61
+ const deps = {
62
+ push: (state) => pushes.push(structuredClone(state)),
63
+ reply: (id, result) => replies.push({ id, result: structuredClone(result) }),
64
+ currentUrl: async () => page.url,
65
+ rank: async () => LOCATOR,
66
+ inspect: async () => ({
67
+ info: INFO,
68
+ counts: [COUNT_CALL],
69
+ }),
70
+ type: async (_locator, value) => {
71
+ calls.push(`type ${value}`);
72
+ },
73
+ mute: async () => {
74
+ calls.push("mute");
75
+ },
76
+ unmute: async () => {
77
+ calls.push("unmute");
78
+ },
79
+ addVariable: async (name, value) => {
80
+ calls.push(`addVariable ${name}=${value}`);
81
+ },
82
+ setSecret: async (name, value) => {
83
+ calls.push(`setSecret ${name}=${value}`);
84
+ },
85
+ secretAdded: (name) => {
86
+ calls.push(`secretAdded ${name}`);
87
+ },
88
+ // As the recorder does: an observation becomes a step, which is counted
89
+ observe: (observation) => {
90
+ observed.push(observation);
91
+ controller.stepAdded(observation.type === "screenshot"
92
+ ? {
93
+ kind: "screenshot",
94
+ name: observation.name,
95
+ fullPage: observation.fullPage,
96
+ }
97
+ : { kind: "waitForNetworkIdle" });
98
+ },
99
+ stop: () => {
100
+ calls.push("stop");
101
+ },
102
+ onStatus: (status) => statuses.push(status),
103
+ ...overrides,
104
+ };
105
+ controller = new ToolbarController(deps, { ...OPTIONS, ...options });
106
+ const last = () => lastOf(pushes);
107
+ const noSecretLeaks = () => {
108
+ const sent = JSON.stringify(pushes) + JSON.stringify(replies);
109
+ assert.ok(!sent.includes(SECRET), "a secret value reached the page");
110
+ assert.ok(!sent.includes(NEW_SECRET), "a new secret value reached the page");
111
+ };
112
+ return {
113
+ controller,
114
+ pushes,
115
+ replies,
116
+ observed,
117
+ statuses,
118
+ calls,
119
+ page,
120
+ last,
121
+ noSecretLeaks,
122
+ };
123
+ }
124
+ /** Deps whose `type` fails, as a field that can't be typed into does */
125
+ export const failingType = {
126
+ type: async () => {
127
+ throw new Error("not editable");
128
+ },
129
+ };
@@ -1,51 +1,10 @@
1
- import type { ElementHandle } from "../../script/element-handle.js";
2
- import type { LocatorCall, LocatorSpec, Step } 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
- type Message = ToolbarMessage & {
47
- target?: ElementHandle;
48
- };
1
+ import type { Step } from "../../steps/model.js";
2
+ import type { ControllerDeps, ControllerOptions, Message } from "./types.js";
3
+ /**
4
+ * The recorder's side of the toolbar: owns its state (pause, the rail, the environment's variables and
5
+ * secrets) and turns its messages into observations. Secret values stay here; the page only ever gets
6
+ * their names and whether they are set.
7
+ */
49
8
  export declare class ToolbarController {
50
9
  private readonly deps;
51
10
  private readonly options;
@@ -64,24 +23,17 @@ export declare class ToolbarController {
64
23
  private savingName;
65
24
  private warnings;
66
25
  private warningId;
67
- /** The picked element an `assert` with the same id refers to */
68
26
  private pick;
69
- /** Ids of `assert`, `insert` and `add-insertable` being processed */
70
- private readonly inFlight;
71
- /** Ids `accept` let through whose `handle` hasn't started yet */
72
- private readonly claimed;
73
- /** Ids answered `{ ok: true }`: their step exists, a repeat is a double click */
74
- private readonly done;
75
- /** Bumped on every page load: work started for an older page is not answered */
76
- private page;
77
- /** Wake-ups for the work that is waiting on the current page: called, and forgotten, when the page changes */
78
- private pageGone;
27
+ private readonly guard;
28
+ private readonly scope;
79
29
  private toolbar;
80
30
  /** A `ready` arrived since the last load: it counts for the next one */
81
31
  private readySinceLoad;
82
32
  private watchdog;
83
33
  private disposed;
84
34
  constructor(deps: ControllerDeps, options: ControllerOptions);
35
+ /** What the action handlers (assert, insert, add) use of this controller */
36
+ private readonly actions;
85
37
  get paused(): boolean;
86
38
  private state;
87
39
  private push;
@@ -101,10 +53,6 @@ export declare class ToolbarController {
101
53
  * and doesn't count for the coming load
102
54
  */
103
55
  navigating(): void;
104
- /** Lets what waits on the current document stop waiting: its commands may never be answered once it is gone */
105
- private leavePage;
106
- /** Runs `work`, but stops waiting for it when the page is left; resolves `gone` then */
107
- private whileOnPage;
108
56
  /** A step was emitted (any source): count it */
109
57
  stepAdded(step: Step): void;
110
58
  /** A recorded step was taken out again (a cancelled file picker's click) */
@@ -120,7 +68,6 @@ export declare class ToolbarController {
120
68
  private pause;
121
69
  private resume;
122
70
  private answer;
123
- /** Runs an action that expects a reply; a second one with the same id while the first runs is dropped */
124
71
  /**
125
72
  * Whether to process `message`; call it as the message arrives, before queueing it. An `assert`, `insert` or
126
73
  * `add-insertable` whose id is in flight or was already answered `{ ok: true }` is a double click and is
@@ -128,19 +75,6 @@ export declare class ToolbarController {
128
75
  */
129
76
  accept(message: Message): boolean;
130
77
  /** Runs an action that expects a reply, unless it is a repeat (see `accept`) */
131
- private once;
78
+ private replying;
132
79
  private picked;
133
- /**
134
- * A field may hold an inserted secret: its value (and any text or attribute equal to a secret) is left out,
135
- * and the first matcher follows the Alt+click rule without a value, so neither the page nor the spec gets it.
136
- */
137
- private withoutSecrets;
138
- private assert;
139
- private insert;
140
- private addInsertable;
141
- /** Undefined when `work` succeeded, else the reply for its error */
142
- private write;
143
- private locate;
144
- private typeIn;
145
80
  }
146
- export {};
@@ -1,29 +1,20 @@
1
1
  import { cleanName } from "../../browser/evidence.js";
2
- import { ConfigEditError } from "../../project/config-edit.js";
3
- import { SECRET_NAME } from "../../project/secrets-file.js";
4
- import { ExistsError, VARIABLE_NAME } from "../../project/variables.js";
5
- import { expectationFor } from "../normalise.js";
2
+ import { ActionGuard } from "./action-guard.js";
3
+ import { assertAction } from "./actions/assert.js";
4
+ import { addInsertableAction, insertAction } from "./actions/insert.js";
5
+ import { withoutSecrets } from "./actions/pick.js";
6
+ import { fail, PAUSED, UNIDENTIFIABLE } from "./actions/reply.js";
7
+ import { LEFT, PageScope } from "./page-scope.js";
6
8
  /** A wait for time is 0.1–60 s */
7
9
  const WAIT_MIN_MS = 100;
8
10
  const WAIT_MAX_MS = 60000;
9
11
  /** How long a loaded page's toolbar has to say `ready` */
10
12
  const READY_WITHIN = 2000;
11
- const UNIDENTIFIABLE = "This element can't be identified uniquely";
12
- const TYPE_FAILED = "Couldn't type into that field";
13
- const NO_ENVIRONMENT = "This recording has no environment";
14
- const PAUSED = "Nothing you do now is recorded.";
15
- const fail = (error, field) => ({
16
- ok: false,
17
- error,
18
- ...(field && { field }),
19
- });
20
13
  /**
21
14
  * The recorder's side of the toolbar: owns its state (pause, the rail, the environment's variables and
22
15
  * secrets) and turns its messages into observations. Secret values stay here; the page only ever gets
23
16
  * their names and whether they are set.
24
17
  */
25
- /** What a wait on the page ends with when the page was left first */
26
- const LEFT = Symbol("left the page");
27
18
  export class ToolbarController {
28
19
  constructor(deps, options) {
29
20
  this.deps = deps;
@@ -35,16 +26,8 @@ export class ToolbarController {
35
26
  this.toastId = 0;
36
27
  this.warnings = [];
37
28
  this.warningId = 0;
38
- /** Ids of `assert`, `insert` and `add-insertable` being processed */
39
- this.inFlight = new Set();
40
- /** Ids `accept` let through whose `handle` hasn't started yet */
41
- this.claimed = new Set();
42
- /** Ids answered `{ ok: true }`: their step exists, a repeat is a double click */
43
- this.done = new Set();
44
- /** Bumped on every page load: work started for an older page is not answered */
45
- this.page = 0;
46
- /** Wake-ups for the work that is waiting on the current page: called, and forgotten, when the page changes */
47
- this.pageGone = new Set();
29
+ this.guard = new ActionGuard();
30
+ this.scope = new PageScope();
48
31
  this.toolbar = true;
49
32
  /** A `ready` arrived since the last load: it counts for the next one */
50
33
  this.readySinceLoad = false;
@@ -55,6 +38,19 @@ export class ToolbarController {
55
38
  String(value),
56
39
  ]));
57
40
  this.secrets = new Map(Object.entries(options.secrets));
41
+ this.actions = {
42
+ deps,
43
+ options,
44
+ variables: this.variables,
45
+ secrets: this.secrets,
46
+ isPaused: () => this.paused,
47
+ pick: () => this.pick,
48
+ setPick: (pick) => {
49
+ this.pick = pick;
50
+ },
51
+ showToast: (text) => this.showToast(text),
52
+ push: () => this.push(),
53
+ };
58
54
  }
59
55
  get paused() {
60
56
  return this.status === "paused";
@@ -140,11 +136,11 @@ export class ToolbarController {
140
136
  case "pick":
141
137
  return this.picked(message);
142
138
  case "assert":
143
- return this.once(message, () => this.assert(message));
139
+ return this.replying(message, () => assertAction(this.actions, message));
144
140
  case "insert":
145
- return this.once(message, () => this.insert(message));
141
+ return this.replying(message, () => insertAction(this.actions, message));
146
142
  case "add-insertable":
147
- return this.once(message, () => this.addInsertable(message));
143
+ return this.replying(message, () => addInsertableAction(this.actions, message));
148
144
  case "wait":
149
145
  if (this.paused)
150
146
  return;
@@ -176,8 +172,7 @@ export class ToolbarController {
176
172
  loaded() {
177
173
  if (this.disposed)
178
174
  return;
179
- this.page++;
180
- this.leavePage();
175
+ this.scope.enter();
181
176
  this.pick = undefined;
182
177
  this.toast = undefined;
183
178
  this.push();
@@ -202,28 +197,7 @@ export class ToolbarController {
202
197
  */
203
198
  navigating() {
204
199
  this.readySinceLoad = false;
205
- this.leavePage();
206
- }
207
- /** Lets what waits on the current document stop waiting: its commands may never be answered once it is gone */
208
- leavePage() {
209
- const waiting = [...this.pageGone];
210
- this.pageGone.clear();
211
- for (const wake of waiting)
212
- wake();
213
- }
214
- /** Runs `work`, but stops waiting for it when the page is left; resolves `gone` then */
215
- async whileOnPage(work, gone) {
216
- let wake;
217
- const left = new Promise((resolve) => {
218
- wake = () => resolve(gone);
219
- this.pageGone.add(wake);
220
- });
221
- try {
222
- return await Promise.race([work(), left]);
223
- }
224
- finally {
225
- this.pageGone.delete(wake);
226
- }
200
+ this.scope.leave();
227
201
  }
228
202
  /** A step was emitted (any source): count it */
229
203
  stepAdded(step) {
@@ -297,229 +271,43 @@ export class ToolbarController {
297
271
  this.deps.observe({ type: "goto", url });
298
272
  this.push();
299
273
  }
300
- /** Runs an action that expects a reply; a second one with the same id while the first runs is dropped */
301
274
  /**
302
275
  * Whether to process `message`; call it as the message arrives, before queueing it. An `assert`, `insert` or
303
276
  * `add-insertable` whose id is in flight or was already answered `{ ok: true }` is a double click and is
304
277
  * dropped (its first copy answers). A retry after `{ ok: false }` is accepted.
305
278
  */
306
279
  accept(message) {
307
- if (message.action !== "assert" &&
308
- message.action !== "insert" &&
309
- message.action !== "add-insertable")
310
- return true;
311
- const { id } = message;
312
- if (this.inFlight.has(id) || this.done.has(id))
313
- return false;
314
- this.inFlight.add(id);
315
- this.claimed.add(id);
316
- return true;
280
+ return this.guard.accept(message);
317
281
  }
318
282
  /** Runs an action that expects a reply, unless it is a repeat (see `accept`) */
319
- async once(message, work) {
320
- const { id } = message;
321
- if (!this.claimed.has(id) && !this.accept(message))
322
- return;
323
- this.claimed.delete(id);
324
- try {
325
- const result = await work();
326
- if (result.ok)
327
- this.done.add(id);
328
- this.reply(id, result);
329
- }
330
- finally {
331
- this.inFlight.delete(id);
332
- }
283
+ replying(message, work) {
284
+ return this.guard.run(message, work, (id, result) => this.reply(id, result));
333
285
  }
334
286
  async picked(message) {
335
- const page = this.page;
287
+ const page = this.scope.current;
336
288
  if (this.paused)
337
289
  return this.reply(message.id, fail(PAUSED));
338
290
  let found;
339
291
  try {
340
292
  // Chrome never answers a command into a document that is being replaced, so a pick of the old page must not
341
293
  // keep the messages behind it waiting
342
- found = await this.whileOnPage(async () => {
294
+ found = await this.scope.whileOn(async () => {
343
295
  const locator = message.target &&
344
296
  (await this.deps.rank(message.target, message.candidates));
345
297
  if (!locator)
346
298
  return undefined;
347
299
  const inspected = await this.deps.inspect(message.target, locator, message.candidates);
348
- if (page === this.page)
300
+ if (page === this.scope.current)
349
301
  this.pick = { id: message.id, locator, counts: inspected.counts };
350
302
  return inspected;
351
- }, LEFT);
303
+ });
352
304
  }
353
305
  catch {
354
306
  found = undefined; // the element went away while it was inspected
355
307
  }
356
308
  // The page that asked is gone: its successor's toolbar isn't waiting for this
357
- if (found === LEFT || page !== this.page)
309
+ if (found === LEFT || page !== this.scope.current)
358
310
  return;
359
- this.reply(message.id, found ? this.withoutSecrets(found.info) : fail(UNIDENTIFIABLE));
360
- }
361
- /**
362
- * A field may hold an inserted secret: its value (and any text or attribute equal to a secret) is left out,
363
- * and the first matcher follows the Alt+click rule without a value, so neither the page nor the spec gets it.
364
- */
365
- withoutSecrets(info) {
366
- const values = [...this.secrets.values()].filter((value) => value !== undefined && value !== "");
367
- const secret = (text) => text !== undefined && values.some((value) => text.includes(value));
368
- if (!secret(info.value) &&
369
- !secret(info.text) &&
370
- !secret(info.first.expected) &&
371
- !info.attributes.some((a) => secret(a.value)))
372
- return info;
373
- const text = secret(info.text) ? "" : info.text;
374
- const first = expectationFor(text, undefined);
375
- const { value: _value, ...rest } = info;
376
- return {
377
- ...rest,
378
- text,
379
- attributes: info.attributes.filter((a) => !secret(a.value)),
380
- first: {
381
- matcher: first.matcher,
382
- ...("expected" in first && { expected: String(first.expected) }),
383
- },
384
- };
385
- }
386
- async assert(message) {
387
- if (this.paused)
388
- return fail(PAUSED);
389
- const pick = this.pick;
390
- if (!pick || pick.id !== message.id)
391
- return fail("Pick the element again");
392
- let locator = pick.locator;
393
- let expectation;
394
- const expected = String(message.expected ?? "");
395
- switch (message.matcher) {
396
- case "toBeVisible":
397
- expectation = { matcher: "toBeVisible" };
398
- break;
399
- case "toHaveText":
400
- case "toContainText":
401
- case "toHaveValue":
402
- expectation = { matcher: message.matcher, expected };
403
- break;
404
- case "toHaveAttribute":
405
- if (!message.attribute)
406
- return fail("Choose an attribute");
407
- expectation = {
408
- matcher: "toHaveAttribute",
409
- name: message.attribute,
410
- expected,
411
- };
412
- break;
413
- case "toHaveCount": {
414
- const call = pick.counts[message.countIndex ?? 0];
415
- const count = Number(expected);
416
- if (!call)
417
- return fail("Choose what to count");
418
- if (expected.trim() === "" || !Number.isInteger(count) || count < 0)
419
- return fail("A count is a whole number");
420
- locator = { chain: [call], fallbacks: [] };
421
- expectation = { matcher: "toHaveCount", expected: count };
422
- break;
423
- }
424
- default:
425
- return fail("Choose a matcher");
426
- }
427
- this.pick = undefined;
428
- this.deps.observe({
429
- type: "expect",
430
- locator,
431
- not: message.not === true,
432
- expectation,
433
- });
434
- return { ok: true };
435
- }
436
- async insert(message) {
437
- if (this.paused)
438
- return fail(PAUSED);
439
- const { kind, name } = message;
440
- const value = kind === "secret" ? this.secrets.get(name) : this.variables.get(name);
441
- if (value === undefined)
442
- return fail(kind === "secret"
443
- ? `Set a value for ${name} first`
444
- : `There is no variable "${name}"`);
445
- const locator = await this.locate(message);
446
- if (!locator)
447
- return fail(UNIDENTIFIABLE);
448
- return this.typeIn(locator, kind, name, value);
449
- }
450
- async addInsertable(message) {
451
- if (this.paused)
452
- return fail(PAUSED);
453
- const { kind, name } = message;
454
- const value = String(message.value ?? "");
455
- if (kind === "env" && !this.options.canAddVariables)
456
- return fail(NO_ENVIRONMENT);
457
- const known = kind === "env"
458
- ? this.variables.has(name)
459
- : this.secrets.get(name) !== undefined;
460
- if (known)
461
- return fail(`"${name}" already exists`, "name");
462
- // Found before anything is written: an element that can't be filled leaves the project as it was
463
- const locator = await this.locate(message);
464
- if (!locator)
465
- return fail(UNIDENTIFIABLE);
466
- const written = kind === "env"
467
- ? await this.write(() => this.deps.addVariable(name, value), (err) => err instanceof ConfigEditError
468
- ? fail(`samurai.config.ts can't be changed automatically. Add ${name} to environments.${this.options.environment}.variables yourself.`)
469
- : err instanceof ExistsError || !VARIABLE_NAME.test(name)
470
- ? fail(errorText(err), "name")
471
- : fail(errorText(err)))
472
- : await this.write(() => this.deps.setSecret(name, value), (err) =>
473
- // An error never echoes the value
474
- SECRET_NAME.test(name)
475
- ? fail(scrub(errorText(err), value))
476
- : fail(scrub(errorText(err), value), "name"));
477
- if (written)
478
- return written;
479
- if (kind === "env")
480
- this.variables.set(name, value);
481
- else {
482
- this.secrets.set(name, value);
483
- this.deps.secretAdded(name);
484
- }
485
- this.push();
486
- return this.typeIn(locator, kind, name, value, true);
487
- }
488
- /** Undefined when `work` succeeded, else the reply for its error */
489
- async write(work, replyFor) {
490
- try {
491
- await work();
492
- return undefined;
493
- }
494
- catch (err) {
495
- return replyFor(err);
496
- }
497
- }
498
- async locate(message) {
499
- if (!message.target)
500
- return undefined;
501
- try {
502
- return await this.deps.rank(message.target, message.candidates);
503
- }
504
- catch {
505
- return undefined; // the element went away
506
- }
507
- }
508
- async typeIn(locator, kind, name, value, saved = false) {
509
- try {
510
- await this.deps.type(locator, value);
511
- }
512
- catch {
513
- // Typing clicks the field, which closes the panel the reply is for: the toast is what the tester sees
514
- this.showToast(saved
515
- ? `Saved ${name}, but couldn't type into that field`
516
- : TYPE_FAILED);
517
- this.push();
518
- return fail(TYPE_FAILED);
519
- }
520
- this.deps.observe({ type: "insert", locator, value: { kind, name } });
521
- return { ok: true };
311
+ this.reply(message.id, found ? withoutSecrets(found.info, this.secrets) : fail(UNIDENTIFIABLE));
522
312
  }
523
313
  }
524
- const errorText = (err) => err instanceof Error ? err.message : String(err);
525
- const scrub = (text, secret) => secret === "" ? text : text.split(secret).join("••••••");
@@ -2,9 +2,7 @@ import type Page from "../../browser/page.js";
2
2
  import type { ElementHandle } from "../../script/element-handle.js";
3
3
  import type { LocatorCall, LocatorSpec } from "../../steps/model.js";
4
4
  import type { BiDiConnector } from "../../transport/bidi-connection.js";
5
- import type { Inspection } from "./controller.js";
6
- /** A locator call in short, as the Assert panel shows it: `heading "Sign in"`, `label "Email"`, `li.item` */
7
- export declare function describeCall(call: LocatorCall): string;
5
+ import type { Inspection } from "./types.js";
8
6
  /**
9
7
  * What the Assert panel needs for a picked element: its short label, text, value, attributes, the first
10
8
  * matcher (today's Alt+click rule) and up to three locators that match more than one element, with counts.
@@ -1,7 +1,7 @@
1
1
  import { callFunction } from "../../script/call-function.js";
2
2
  import { locatorFromSpec } from "../../steps/build.js";
3
3
  import { CANDIDATES_JS } from "../candidates.js";
4
- import { expectationFor } from "../normalise.js";
4
+ import { expectationFor } from "../expectation.js";
5
5
  import { TOOLBAR_SANDBOX } from "./protocol.js";
6
6
  /** Most Count choices the Assert panel offers */
7
7
  const MAX_COUNTS = 3;
@@ -27,7 +27,7 @@ const INSPECT_SOURCE = String.raw `(el) => {
27
27
  };
28
28
  }`;
29
29
  /** A locator call in short, as the Assert panel shows it: `heading "Sign in"`, `label "Email"`, `li.item` */
30
- export function describeCall(call) {
30
+ function describeCall(call) {
31
31
  switch (call.method) {
32
32
  case "getByRole":
33
33
  return call.name === undefined
@@ -0,0 +1,19 @@
1
+ /** What a wait on the page ends with when the page was left first */
2
+ export declare const LEFT: unique symbol;
3
+ /**
4
+ * Tracks which document the toolbar is working for. Work started for an older document must not be answered, and
5
+ * Chrome never answers a command into a document that is being replaced, so such work stops being waited for.
6
+ */
7
+ export declare class PageScope {
8
+ private page;
9
+ /** Wake-ups for the work waiting on the current page: called, and forgotten, when the page changes */
10
+ private readonly waiting;
11
+ /** Bumped on every page load */
12
+ get current(): number;
13
+ /** A new document loaded */
14
+ enter(): void;
15
+ /** Lets what waits on the current document stop waiting */
16
+ leave(): void;
17
+ /** Runs `work`, but stops waiting for it when the page is left; resolves `LEFT` then */
18
+ whileOn<T>(work: () => Promise<T>): Promise<T | typeof LEFT>;
19
+ }