@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,6 +1,18 @@
1
- import { CANDIDATES_JS } from "../candidates.js";
2
- import { TOOLBAR_KEY } from "./protocol.js";
3
- import { TOOLBAR_CSS } from "./styles.js";
1
+ import { SETUP_JS } from "./source/setup.js";
2
+ import { DOM_JS } from "./source/dom.js";
3
+ import { NAMES_JS } from "./source/names.js";
4
+ import { PLACEMENT_JS } from "./source/placement.js";
5
+ import { VIEWS_RAIL_JS } from "./source/views-rail.js";
6
+ import { VIEWS_PANELS_JS } from "./source/views-panels.js";
7
+ import { VIEWS_RENDER_JS } from "./source/views-render.js";
8
+ import { PICKING_JS } from "./source/picking.js";
9
+ import { HANDLERS_JS } from "./source/handlers.js";
10
+ import { FIELDS_JS } from "./source/fields.js";
11
+ import { DRAG_JS } from "./source/drag.js";
12
+ import { EVENTS_TOOLBAR_JS } from "./source/events-toolbar.js";
13
+ import { EVENTS_PAGE_JS } from "./source/events-page.js";
14
+ import { RECORDER_SIDE_JS } from "./source/recorder-side.js";
15
+ import { ATTACH_JS } from "./source/attach.js";
4
16
  /**
5
17
  * The recorder toolbar: a function declaration `(send) => { … }` installed as a preload script in the
6
18
  * `samurai-toolbar` sandbox realm (the caller names the sandbox), top-level documents only. `send` is the
@@ -33,857 +45,19 @@ import { TOOLBAR_CSS } from "./styles.js";
33
45
  * A string, so no transpiler helper can end up in it.
34
46
  */
35
47
  export const TOOLBAR_SOURCE = String.raw `(send) => {
36
- if (window !== window.top) return;
37
- const KEY = Symbol.for("${TOOLBAR_KEY}");
38
- if (globalThis[KEY]) {
39
- globalThis[KEY].rebind(send);
40
- return;
41
- }
42
-
43
- ${CANDIDATES_JS}
44
-
45
- const CSS = ${JSON.stringify(TOOLBAR_CSS)};
46
- const DEFAULTS = {
47
- status: "recording", collapsed: false, position: null, steps: 0, environment: "", canAddVariables: false,
48
- variables: [], secrets: [], secretsIgnored: true, resumeQuestion: false, nextScreenshot: "screenshot-1",
49
- warnings: [],
50
- };
51
- const MATCHERS = [
52
- ["toBeVisible", "Visible"], ["toHaveText", "Has text"], ["toContainText", "Contains text"],
53
- ["toHaveValue", "Has value"], ["toHaveAttribute", "Has attribute"], ["toHaveCount", "Count"],
54
- ];
55
-
56
- let channel = send;
57
- let state = Object.assign({}, DEFAULTS);
58
- let nextId = Date.now();
59
- let railBox = { x: 0, y: 0, w: 0, h: 0 };
60
- let toastTimer = 0;
61
- // Local UI state; everything about steps lives in the recorder
62
- const ui = {
63
- panel: null, picking: false, busy: false, closeOnUpdate: false, pending: 0, focus: null,
64
- field: null, assert: null, insert: null, form: null,
65
- wait: { kind: "idle", seconds: "2" }, shot: { name: "", fullPage: false },
66
- drag: null, dropped: null, toastId: null,
67
- };
68
-
69
- const post = (message) => {
70
- try { channel(Object.assign({ type: "toolbar" }, message)); } catch (err) { /* the channel is gone */ }
71
- };
72
- // Listeners on the page's window and document, so dispose() can take them all off again
73
- const listening = [];
74
- const listen = (target, type, handler, capture) => {
75
- target.addEventListener(type, handler, capture === true);
76
- listening.push([target, type, handler, capture === true]);
77
- };
78
- const paused = () => state.status === "paused";
79
- // Whole pixels, so a rail at the far edge does not overhang it by a rounding error
80
- const clamp = (value, max) => Math.max(0, Math.min(value, Math.floor(max)));
81
-
82
- // ---- DOM ----
83
- const h = (tag, props, ...kids) => {
84
- const node = document.createElement(tag);
85
- for (const key in props || {}) {
86
- const value = props[key];
87
- if (value === undefined || value === null || value === false) continue;
88
- if (key === "class") node.className = value;
89
- else if (key === "value" || key === "checked" || key === "disabled" || key === "selected") node[key] = value;
90
- else node.setAttribute(key, value === true ? "" : String(value));
91
- }
92
- for (const kid of kids.flat(Infinity)) {
93
- if (kid === null || kid === undefined || kid === false || kid === "") continue;
94
- node.append(typeof kid === "string" ? document.createTextNode(kid) : kid);
95
- }
96
- return node;
97
- };
98
- const ico = (text) => h("span", { class: "ico" }, text);
99
-
100
- // Styles go through the CSSOM (property by property, and a constructed sheet), which a page's CSP does not
101
- // block, unlike a style attribute or a <style> element without 'unsafe-inline'
102
- const host = document.createElement("samurai-recorder-toolbar");
103
- const HOST_STYLE = [
104
- ["all", "initial"], ["display", "block"], ["position", "fixed"], ["top", "0"], ["left", "0"], ["width", "0"],
105
- ["height", "0"], ["overflow", "visible"], ["z-index", "2147483647"],
106
- ];
107
- for (const [name, value] of HOST_STYLE) host.style.setProperty(name, value, "important");
108
- const root = host.attachShadow({ mode: "closed" });
109
- const frame = h("div", { class: "frame" });
110
- const container = h("div", { class: "ui" });
111
- const outline = h("div", { class: "outline", hidden: true });
112
- const toast = h("div", { class: "toast ui", hidden: true });
113
- const notices = h("div", { class: "notices ui" });
114
- let adopted = false;
115
- try {
116
- const sheet = new CSSStyleSheet();
117
- sheet.replaceSync(CSS);
118
- root.adoptedStyleSheets = [sheet];
119
- adopted = root.adoptedStyleSheets.length === 1;
120
- } catch (err) {
121
- adopted = false;
122
- }
123
- // Without constructed sheets: a <style> element, which a strict CSP blocks
124
- root.append(...(adopted ? [] : [h("style", null, CSS)]), frame, outline, container, toast, notices);
125
-
126
- const viewport = () => ({ w: frame.clientWidth || innerWidth, h: frame.clientHeight || innerHeight });
127
-
128
- // ---- Names and labels ----
129
- const words = (text) => String(text).replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
130
- const camel = (text) => words(text).map((w, i) => (i ? w[0].toUpperCase() + w.slice(1).toLowerCase() : w.toLowerCase())).join("");
131
- const snake = (text) => words(text).map((w) => w.toUpperCase()).join("_");
132
- const variableName = (text) => (/^[A-Za-z_$][\w$]*$/.test(text.trim()) ? text.trim() : camel(text));
133
- const fieldName = (el) => labelTexts(el)[0] || el.getAttribute("name") || el.id || "";
134
- const fieldLabel = (el) => {
135
- const text = labelTexts(el)[0] || el.getAttribute("placeholder") || el.getAttribute("name") || el.id || "field";
136
- return text.length > 40 ? text.slice(0, 39) + "…" : text;
137
- };
138
-
139
- // ---- Placement ----
140
- const place = (node, want) => {
141
- const rect = node.getBoundingClientRect();
142
- const v = viewport();
143
- const x = clamp(want ? want.x : v.w - rect.width - 12, v.w - rect.width);
144
- const y = clamp(want ? want.y : 12, v.h - rect.height);
145
- node.style.left = x + "px";
146
- node.style.top = y + "px";
147
- return { x, y, w: rect.width, h: rect.height };
148
- };
149
- const wantedSpot = () => (ui.drag ? ui.drag.pos : ui.dropped || state.position);
150
- const placePanel = (panel) => {
151
- const rect = panel.getBoundingClientRect();
152
- const v = viewport();
153
- let x;
154
- let y;
155
- if (ui.panel === "assert" && ui.assert) {
156
- const t = ui.assert.rect;
157
- // Beside the element; below it (or above) when it is too wide for either side
158
- if (t.right + 8 + rect.width <= v.w) [x, y] = [t.right + 8, t.top];
159
- else if (t.left - 8 - rect.width >= 0) [x, y] = [t.left - 8 - rect.width, t.top];
160
- else [x, y] = [t.left, t.bottom + 8 + rect.height <= v.h ? t.bottom + 8 : t.top - 8 - rect.height];
161
- } else {
162
- x = railBox.x + railBox.w / 2 > v.w / 2 ? railBox.x - 8 - rect.width : railBox.x + railBox.w + 8;
163
- y = railBox.y;
164
- }
165
- panel.style.left = clamp(x, v.w - rect.width) + "px";
166
- panel.style.top = clamp(y, v.h - rect.height) + "px";
167
- };
168
- const placeAll = () => {
169
- const [rail, panel] = container.children;
170
- if (rail) railBox = place(rail, wantedSpot());
171
- if (panel) placePanel(panel);
172
- };
173
- const showOutline = (el) => {
174
- const r = el.getBoundingClientRect();
175
- outline.style.left = r.left + "px";
176
- outline.style.top = r.top + "px";
177
- outline.style.width = r.width + "px";
178
- outline.style.height = r.height + "px";
179
- outline.hidden = false;
180
- };
181
- const hideOutline = () => { outline.hidden = true; };
182
-
183
- // ---- Views ----
184
- const toolOn = (tool) =>
185
- tool === "assert" ? ui.picking || ui.panel === "assert" : tool === "insert" ? ui.panel === "insert" || ui.panel === "new" : ui.panel === tool;
186
-
187
- const chipView = () =>
188
- h("div", { class: "chip" },
189
- h("span", { "data-grip": true, title: "Drag to move" }, "⋮⋮"),
190
- h("span", { class: paused() ? "dot pause" : "dot" }),
191
- (paused() ? "Paused" : "Rec") + " · " + state.steps + (state.steps === 1 ? " step" : " steps"),
192
- h("button", { class: "chev", "data-action": "expand", title: "Expand" }, "⌄"));
193
-
194
- const railView = () => {
195
- const tool = (action, icon, label) =>
196
- h("button", { class: toolOn(action) ? "b on" : "b", "data-action": action, disabled: paused() }, ico(icon), label);
197
- return h("div", { class: paused() || state.resumeQuestion ? "rail wide" : "rail" },
198
- h("div", { class: "top" },
199
- h("span", { "data-grip": true, title: "Drag to move" }, "⋮⋮⋮"),
200
- h("button", { class: "chev", "data-action": "collapse", title: "Collapse" }, "⌃")),
201
- h("div", { class: "st" }, h("span", { class: paused() ? "dot pause" : "dot" }), paused() ? "Paused" : "Recording"),
202
- paused() && h("div", { class: "note" }, "Nothing you do now is recorded."),
203
- state.resumeQuestion && [
204
- h("div", { class: "note" }, "Add a step that opens this page?"),
205
- h("div", { class: "ask" },
206
- h("button", { class: "yes", "data-action": "resume-yes" }, "Yes"),
207
- h("button", { "data-action": "resume-no" }, "No")),
208
- ],
209
- h("div", { class: "sep" }),
210
- paused()
211
- ? h("button", { class: "b on", "data-action": "resume" }, ico("▶"), "Resume")
212
- : h("button", { class: "b", "data-action": "pause" }, ico("❚❚"), "Pause"),
213
- tool("assert", "✓", "Assert"),
214
- ui.picking && h("div", { class: "note" }, "Click an element · Esc cancels"),
215
- tool("insert", "{x}", "Insert"),
216
- tool("wait", "⏱", "Wait"),
217
- tool("screenshot", "◉", "Screenshot"),
218
- h("div", { class: "sep" }),
219
- h("button", { class: "b stop", "data-action": "stop" }, ico("■"), "Stop"));
220
- };
221
-
222
- const unavailable = (info, matcher) =>
223
- (matcher === "toHaveValue" && info.value === undefined) ||
224
- (matcher === "toHaveAttribute" && info.attributes.length === 0) ||
225
- (matcher === "toHaveCount" && info.counts.length === 0);
226
- const prefill = (a, matcher) => {
227
- const info = a.info;
228
- if (matcher === "toHaveAttribute") {
229
- const found = info.attributes.find((at) => at.name === a.attribute) || info.attributes[0];
230
- return found ? found.value : "";
231
- }
232
- if (matcher === "toHaveCount") {
233
- const found = info.counts[a.countIndex];
234
- return found ? String(found.count) : "";
235
- }
236
- if (matcher === info.first.matcher && info.first.expected !== undefined) return info.first.expected;
237
- if (matcher === "toHaveText" || matcher === "toContainText") return info.text;
238
- if (matcher === "toHaveValue") return info.value || "";
239
- return "";
240
- };
241
- const assertValue = (a) => {
242
- const info = a.info;
243
- const expected = (type) => h("input", { type, class: "in", "data-field": "expected", value: a.expected, "data-enter": "assert-add", autocomplete: "off" });
244
- if (a.matcher === "toHaveText" || a.matcher === "toContainText" || a.matcher === "toHaveValue") return h("div", { class: "val" }, expected("text"));
245
- if (a.matcher === "toHaveAttribute") {
246
- return h("div", { class: "val" },
247
- h("select", { class: "in", "data-field": "attribute" }, info.attributes.map((at) => {
248
- const shown = at.name + '="' + at.value + '"';
249
- return h("option", { value: at.name, selected: at.name === a.attribute }, shown.length > 44 ? shown.slice(0, 43) + "…" : shown);
250
- })),
251
- expected("text"));
252
- }
253
- if (a.matcher === "toHaveCount") {
254
- return h("div", { class: "val" },
255
- h("select", { class: "in", "data-field": "countIndex" }, info.counts.map((c, i) =>
256
- h("option", { value: String(i), selected: i === a.countIndex }, c.label + " (" + c.count + ")"))),
257
- expected("number"));
258
- }
259
- return null;
260
- };
261
- const assertPanel = () => {
262
- const a = ui.assert;
263
- const info = a.info;
264
- const head = h("div", { class: "h" }, "Assert", info && info.ok && h("code", { title: info.label }, info.label));
265
- if (!info) return [head, h("div", { class: "msg" }, "…")];
266
- if (!info.ok) return [head, h("div", { class: "err" }, info.error)];
267
- return [
268
- head,
269
- h("div", { class: "opts" }, MATCHERS.map(([matcher, label]) =>
270
- h("button", { class: a.matcher === matcher ? "o sel" : "o", "data-action": "matcher", "data-matcher": matcher, disabled: unavailable(info, matcher) }, label))),
271
- assertValue(a),
272
- a.error && h("div", { class: "err" }, a.error),
273
- h("div", { class: "row" },
274
- h("label", { class: "neg" }, h("input", { type: "checkbox", "data-action": "not", checked: a.not }), "Not"),
275
- h("button", { class: "add", "data-action": "assert-add", disabled: ui.busy }, "Add")),
276
- ];
277
- };
278
-
279
- const matches = () => {
280
- const filter = ui.insert.filter.trim().toLowerCase();
281
- const hit = (item) => !filter || item.name.toLowerCase().indexOf(filter) >= 0;
282
- return {
283
- filter,
284
- variables: state.canAddVariables ? state.variables.filter(hit) : [],
285
- secrets: state.secrets.filter(hit),
286
- };
287
- };
288
- const insertPanel = () => {
289
- const ins = ui.insert;
290
- if (!ins.field) return [h("div", { class: "h" }, "Insert"), h("div", { class: "msg" }, "Click into a field first")];
291
- const found = matches();
292
- const none = found.filter && found.variables.length === 0 && found.secrets.length === 0;
293
- const item = (kind, name, value) =>
294
- h("button", { class: "it", "data-action": "insert-item", "data-kind": kind, "data-name": name, disabled: ui.busy }, h("code", null, name), value);
295
- return [
296
- h("div", { class: "h stack" }, 'Insert into "' + fieldLabel(ins.field) + '"', h("small", null, "Environment: " + state.environment)),
297
- h("input", { type: "text", class: "in search", "data-field": "filter", placeholder: "Filter…", value: ins.filter, "data-enter": "insert-first", autocomplete: "off", spellcheck: "false" }),
298
- state.canAddVariables && [
299
- h("div", { class: "grp" }, "Variables"),
300
- found.variables.length
301
- ? found.variables.map((v) => item("env", v.name, h("span", { class: "v", title: v.value }, v.value)))
302
- : h("div", { class: "empty" }, found.filter ? "No variable matches." : "No variables yet."),
303
- ],
304
- h("div", { class: "grp" }, "Secrets"),
305
- found.secrets.length
306
- ? found.secrets.map((s) => item("secret", s.name, s.set ? h("span", { class: "v" }, "••••••") : h("span", { class: "warn" }, "not set")))
307
- : h("div", { class: "empty" }, found.filter ? "No secret matches." : "No secrets yet."),
308
- ins.error && h("div", { class: "err" }, ins.error),
309
- h("button", { class: "act", "data-action": "insert-new", "data-kind": "env" }, "+ Add variable" + (none ? ' "' + variableName(ins.filter) + '"' : "")),
310
- h("button", { class: "act", "data-action": "insert-new", "data-kind": "secret" }, "+ Add secret" + (none ? ' "' + snake(ins.filter) + '"' : "")),
311
- ];
312
- };
313
-
314
- const newHint = (secret, env, name) => {
315
- if (!secret) return ["Saved to ", h("code", null, "environments." + env + ".variables"), " in samurai.config.ts. Other environments don't get it — add it there before running against them."];
316
- if (!state.secretsIgnored) return h("span", { class: "warn" }, "Add .env." + env + " to .gitignore before committing.");
317
- return ["Saved as ", h("code", null, "SAMURAI_SECRET_" + (name.trim() || "<NAME>")), " in ", h("code", null, ".env." + env), " (git-ignored)."];
318
- };
319
- const newPanel = () => {
320
- const f = ui.form;
321
- const secret = f.kind === "secret";
322
- const blocked = !secret && !state.canAddVariables;
323
- const env = state.environment;
324
- return [
325
- h("div", { class: "h stack" }, f.setValue ? "Set value" : secret ? "New secret" : "New variable", h("small", null, 'For "' + fieldLabel(ui.insert.field) + '"')),
326
- h("div", { class: "seg" },
327
- h("button", { class: secret ? "" : "sel", "data-action": "new-kind", "data-kind": "env" }, "Variable"),
328
- h("button", { class: secret ? "sel" : "", "data-action": "new-kind", "data-kind": "secret" }, "Secret")),
329
- blocked
330
- ? h("div", { class: "msg" }, "This recording has no environment")
331
- : [
332
- h("div", { class: "lbl" }, "Name"),
333
- h("div", { class: "f" }, h("input", { type: "text", class: "in code", "data-field": "name", value: f.name, "data-enter": "new-save", autocomplete: "off", spellcheck: "false" })),
334
- f.nameError && h("div", { class: "field-error" }, f.nameError),
335
- h("div", { class: "lbl" }, "Value in " + env),
336
- h("div", { class: "f" }, h("input", { type: secret ? "password" : "text", class: "in", "data-field": "value", value: f.value, "data-enter": "new-save", autocomplete: secret ? "new-password" : "off" })),
337
- h("div", { class: "hint" }, newHint(secret, env, f.name)),
338
- f.error && h("div", { class: "err" }, f.error),
339
- ],
340
- h("div", { class: "foot plain" },
341
- h("button", { class: "cancel", "data-action": "new-cancel" }, "Cancel"),
342
- !blocked && h("button", { class: "add", "data-action": "new-save", disabled: ui.busy || !f.name.trim() }, "Save and insert")),
343
- ];
344
- };
345
-
346
- const secondsValid = () => {
347
- const seconds = Number(ui.wait.seconds);
348
- return ui.wait.seconds.trim() !== "" && isFinite(seconds) && seconds >= 0.1 && seconds <= 60;
349
- };
350
- const radio = (field, value, checked, ...label) =>
351
- h("label", { class: "radio" }, h("input", { type: "radio", name: field, "data-field": field, value, checked }), label);
352
- const waitPanel = () => [
353
- h("div", { class: "h" }, "Wait"),
354
- radio("waitKind", "idle", ui.wait.kind === "idle", "Until the page settles"),
355
- h("div", { class: "sub" }, "No requests for 500 ms"),
356
- radio("waitKind", "time", ui.wait.kind === "time", "For ",
357
- h("input", { type: "number", class: "num", "data-field": "seconds", min: "0.1", max: "60", step: "0.1", value: ui.wait.seconds, "data-enter": "wait-add" }),
358
- " seconds"),
359
- h("div", { class: "foot" }, h("button", { class: "add", "data-action": "wait-add", disabled: ui.busy || (ui.wait.kind === "time" && !secondsValid()) }, "Add")),
360
- ];
361
- const screenshotPanel = () => [
362
- h("div", { class: "h" }, "Screenshot"),
363
- h("div", { class: "grp" }, "Name"),
364
- h("div", { class: "f" }, h("input", { type: "text", class: "in", "data-field": "shotName", value: ui.shot.name, "data-enter": "screenshot-add", autocomplete: "off", spellcheck: "false" })),
365
- radio("fullPage", "false", !ui.shot.fullPage, "Visible part"),
366
- radio("fullPage", "true", ui.shot.fullPage, "Whole page"),
367
- h("div", { class: "foot" }, h("button", { class: "add", "data-action": "screenshot-add", disabled: ui.busy }, "Add")),
368
- ];
369
- const PANELS = { assert: assertPanel, insert: insertPanel, new: newPanel, wait: waitPanel, screenshot: screenshotPanel };
370
-
371
- // Notice nodes live as long as their warning: rebuilding them would make screen readers announce each alert again
372
- const noticeNodes = new Map();
373
- let savingNode = null;
374
- let savingText = null;
375
- const renderNotices = () => {
376
- const ids = new Set(state.warnings.map((w) => w.id));
377
- for (const [id, node] of noticeNodes) {
378
- if (ids.has(id)) continue;
379
- node.remove();
380
- noticeNodes.delete(id);
381
- }
382
- for (const w of state.warnings) {
383
- if (noticeNodes.has(w.id)) continue;
384
- const node = h("div", { class: "notice warning", "data-warning": String(w.id), role: "alert" },
385
- h("span", null, w.text),
386
- h("button", { class: "dismiss", "data-action": "dismiss", "data-id": String(w.id) }, "Dismiss"));
387
- noticeNodes.set(w.id, node);
388
- // Before the saving line, which stays last
389
- notices.insertBefore(node, savingNode);
390
- }
391
- const saving = state.saving || null;
392
- if (saving !== savingText) {
393
- savingText = saving;
394
- if (savingNode) savingNode.remove();
395
- savingNode = saving ? h("div", { class: "notice saving" }, "Saving " + saving + "…") : null;
396
- if (savingNode) notices.append(savingNode);
397
- }
398
- };
399
- const render = () => {
400
- const active = root.activeElement;
401
- const kept = active && active.type !== "radio" ? active.getAttribute("data-field") : null;
402
- let selection = null;
403
- try { selection = kept ? [active.selectionStart, active.selectionEnd] : null; } catch (err) { selection = null; }
404
- const panel = ui.panel && !state.collapsed ? h("div", { class: ui.panel === "new" || ui.panel === "insert" ? "panel wide" : "panel" }, PANELS[ui.panel]()) : null;
405
- container.replaceChildren(state.collapsed ? chipView() : railView(), ...(panel ? [panel] : []));
406
- renderNotices();
407
- placeAll();
408
- const focus = ui.focus || kept;
409
- ui.focus = null;
410
- const target = focus && root.querySelector('[data-field="' + focus + '"]');
411
- if (!target) return;
412
- target.focus({ preventScroll: true });
413
- if (focus === kept && selection && selection[0] !== null) {
414
- try { target.setSelectionRange(selection[0], selection[1]); } catch (err) { /* not a text input */ }
415
- }
416
- };
417
-
418
- // ---- Panels and picking ----
419
- const closePanel = () => {
420
- ui.panel = null;
421
- ui.assert = null;
422
- ui.busy = false;
423
- ui.closeOnUpdate = false;
424
- ui.pending = 0;
425
- hideOutline();
426
- };
427
- const stopPicking = () => {
428
- ui.picking = false;
429
- if (ui.panel !== "assert") hideOutline();
430
- };
431
- const usable = () => !paused() && !state.collapsed;
432
- const openTool = (tool) => {
433
- if (!usable()) return;
434
- const wasOpen = toolOn(tool);
435
- stopPicking();
436
- closePanel();
437
- if (!wasOpen) {
438
- ui.panel = tool;
439
- if (tool === "insert") {
440
- // The field focused now, else the last one (a toolbar button may have taken the focus)
441
- const active = document.activeElement;
442
- const focused = active && active !== host && active.nodeType === 1 && isTextEntry(active) ? active : null;
443
- const field = focused || (ui.field && ui.field.isConnected ? ui.field : null);
444
- ui.field = field;
445
- ui.insert = { field, filter: "", error: null };
446
- if (field) ui.focus = "filter";
447
- } else if (tool === "wait") {
448
- ui.wait = { kind: "idle", seconds: "2" };
449
- } else if (tool === "screenshot") {
450
- ui.shot = { name: state.nextScreenshot, fullPage: false };
451
- ui.focus = "shotName";
452
- }
453
- }
454
- render();
455
- };
456
- const pick = (target) => {
457
- if (!ui.picking || !target || target.nodeType !== 1) return;
458
- ui.picking = false;
459
- closePanel();
460
- const id = nextId++;
461
- const candidates = candidatesOf(target);
462
- const r = target.getBoundingClientRect();
463
- ui.assert = {
464
- id, target, candidates, info: null, matcher: "toBeVisible", expected: "", attribute: null, countIndex: 0, not: false, error: null,
465
- rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom },
466
- };
467
- ui.panel = "assert";
468
- ui.pending = id;
469
- showOutline(target);
470
- post({ action: "pick", id, target, candidates });
471
- render();
472
- };
473
- const openForm = (kind, name, setValue) => {
474
- ui.form = { kind, name, value: "", nameError: null, error: null, setValue };
475
- ui.panel = "new";
476
- ui.focus = name ? "value" : "name";
477
- render();
478
- };
479
- const sendInsert = (kind, name) => {
480
- const field = ui.insert.field;
481
- const id = nextId++;
482
- ui.pending = id;
483
- ui.busy = true;
484
- ui.insert.error = null;
485
- post({ action: "insert", id, target: field, candidates: candidatesOf(field), kind, name });
486
- render();
487
- };
488
-
489
- // Every action is what a click on its control does; press() runs the same functions
490
- const handlers = {
491
- dismiss: (d) => post({ action: "dismiss", id: Number(d.id) }),
492
- pause: () => { if (!paused()) post({ action: "pause" }); },
493
- resume: () => { if (paused()) post({ action: "resume" }); },
494
- stop: () => {
495
- stopPicking();
496
- closePanel();
497
- post({ action: "stop" });
498
- render();
499
- },
500
- collapse: () => {
501
- stopPicking();
502
- closePanel();
503
- post({ action: "collapse", collapsed: true });
504
- render();
505
- },
506
- expand: () => post({ action: "collapse", collapsed: false }),
507
- "resume-yes": () => { if (state.resumeQuestion) post({ action: "resume-goto", add: true }); },
508
- "resume-no": () => { if (state.resumeQuestion) post({ action: "resume-goto", add: false }); },
509
- assert: () => {
510
- if (!usable()) return;
511
- const wasOn = toolOn("assert");
512
- stopPicking();
513
- closePanel();
514
- ui.picking = !wasOn;
515
- render();
516
- },
517
- insert: () => openTool("insert"),
518
- wait: () => openTool("wait"),
519
- screenshot: () => openTool("screenshot"),
520
- pick: (detail) => pick(detail.target),
521
- matcher: (detail) => {
522
- const a = ui.panel === "assert" && ui.assert;
523
- if (!a || !a.info || !a.info.ok || unavailable(a.info, detail.matcher)) return;
524
- a.matcher = detail.matcher;
525
- a.expected = prefill(a, detail.matcher);
526
- render();
527
- },
528
- not: () => {
529
- if (ui.panel !== "assert" || !ui.assert || !ui.assert.info) return;
530
- ui.assert.not = !ui.assert.not;
531
- render();
532
- },
533
- "assert-add": () => {
534
- const a = ui.panel === "assert" && ui.assert;
535
- if (!a || ui.busy || !a.info || !a.info.ok) return;
536
- const message = { action: "assert", id: a.id, target: a.target, candidates: a.candidates, matcher: a.matcher, not: a.not };
537
- if (a.matcher !== "toBeVisible") message.expected = a.expected;
538
- if (a.matcher === "toHaveAttribute") message.attribute = a.attribute;
539
- if (a.matcher === "toHaveCount") message.countIndex = a.countIndex;
540
- ui.busy = true;
541
- a.error = null;
542
- post(message);
543
- render();
544
- },
545
- "insert-item": (detail) => {
546
- if (ui.panel !== "insert" || ui.busy || !ui.insert.field) return;
547
- if (detail.kind === "secret") {
548
- const secret = state.secrets.find((s) => s.name === detail.name);
549
- if (!secret) return;
550
- if (!secret.set) return openForm("secret", secret.name, true);
551
- } else if (!state.canAddVariables || !state.variables.some((v) => v.name === detail.name)) {
552
- return;
553
- }
554
- sendInsert(detail.kind, detail.name);
555
- },
556
- "insert-first": () => {
557
- if (ui.panel !== "insert" || !ui.insert.field) return;
558
- const found = matches();
559
- const first = found.variables[0] ? { kind: "env", name: found.variables[0].name } : found.secrets[0] && { kind: "secret", name: found.secrets[0].name };
560
- if (first) handlers["insert-item"](first);
561
- },
562
- "insert-new": (detail) => {
563
- if (ui.panel !== "insert" || !ui.insert.field) return;
564
- const filter = ui.insert.filter.trim();
565
- const base = filter || fieldName(ui.insert.field);
566
- const name = detail.kind === "secret" ? snake(base) : filter ? variableName(filter) : camel(base);
567
- openForm(detail.kind === "secret" ? "secret" : "env", name, false);
568
- },
569
- "new-kind": (detail) => {
570
- const f = ui.panel === "new" && ui.form;
571
- if (!f || ui.busy || f.kind === detail.kind) return;
572
- f.kind = detail.kind === "secret" ? "secret" : "env";
573
- f.name = f.kind === "secret" ? snake(f.name) : camel(f.name);
574
- f.nameError = null;
575
- f.error = null;
576
- render();
577
- },
578
- "new-save": () => {
579
- const f = ui.panel === "new" && ui.form;
580
- if (!f || ui.busy || !f.name.trim() || (f.kind === "env" && !state.canAddVariables)) return;
581
- const field = ui.insert.field;
582
- const id = nextId++;
583
- ui.pending = id;
584
- ui.busy = true;
585
- f.nameError = null;
586
- f.error = null;
587
- post({ action: "add-insertable", id, target: field, candidates: candidatesOf(field), kind: f.kind, name: f.name.trim(), value: f.value });
588
- render();
589
- },
590
- "new-cancel": () => {
591
- if (ui.panel !== "new") return;
592
- ui.panel = "insert";
593
- ui.form = null;
594
- ui.busy = false;
595
- ui.pending = 0;
596
- ui.focus = "filter";
597
- render();
598
- },
599
- "wait-add": () => {
600
- if (ui.panel !== "wait" || ui.busy) return;
601
- if (ui.wait.kind === "time" && !secondsValid()) return;
602
- ui.busy = true;
603
- ui.closeOnUpdate = true;
604
- post(ui.wait.kind === "time" ? { action: "wait", kind: "time", ms: Math.round(Number(ui.wait.seconds) * 1000) } : { action: "wait", kind: "idle" });
605
- render();
606
- },
607
- "screenshot-add": () => {
608
- if (ui.panel !== "screenshot" || ui.busy) return;
609
- ui.busy = true;
610
- ui.closeOnUpdate = true;
611
- post({ action: "screenshot", name: ui.shot.name.trim(), fullPage: ui.shot.fullPage });
612
- render();
613
- },
614
- escape: () => {
615
- if (!ui.picking && !ui.panel) return false;
616
- stopPicking();
617
- closePanel();
618
- render();
619
- return true;
620
- },
621
- };
622
- const press = (action, detail) => {
623
- const handler = handlers[action];
624
- if (handler) handler(detail || {});
625
- };
626
-
627
- const onField = (event, target) => {
628
- const t = target || event.target;
629
- const field = t && t.getAttribute && t.getAttribute("data-field");
630
- if (!field) return;
631
- const value = t.value;
632
- if (field === "expected" && ui.assert) ui.assert.expected = value;
633
- else if (field === "attribute" && ui.assert) {
634
- ui.assert.attribute = value;
635
- ui.assert.expected = prefill(ui.assert, "toHaveAttribute");
636
- render();
637
- } else if (field === "countIndex" && ui.assert) {
638
- ui.assert.countIndex = Number(value);
639
- ui.assert.expected = prefill(ui.assert, "toHaveCount");
640
- render();
641
- } else if (field === "filter" && ui.insert) {
642
- if (ui.insert.filter === value) return;
643
- ui.insert.filter = value;
644
- render();
645
- } else if (field === "name" && ui.form) {
646
- if (ui.form.name === value) return;
647
- ui.form.name = value;
648
- ui.form.nameError = null;
649
- render();
650
- } else if (field === "value" && ui.form) ui.form.value = value;
651
- else if (field === "waitKind") {
652
- ui.wait.kind = value === "time" ? "time" : "idle";
653
- render();
654
- } else if (field === "seconds") {
655
- ui.wait.seconds = value;
656
- ui.wait.kind = "time";
657
- render();
658
- } else if (field === "shotName") ui.shot.name = value;
659
- else if (field === "fullPage") {
660
- ui.shot.fullPage = value === "true";
661
- render();
662
- }
663
- };
664
-
665
- // ---- Drag ----
666
- const onDragMove = (event) => {
667
- if (!ui.drag) return;
668
- ui.drag.pos = { x: event.clientX - ui.drag.dx, y: event.clientY - ui.drag.dy };
669
- ui.drag.moved = true;
670
- placeAll();
671
- };
672
- const endDrag = () => {
673
- if (!ui.drag) return;
674
- const { moved, pointer } = ui.drag;
675
- ui.drag = null;
676
- window.removeEventListener("pointermove", onDragMove, true);
677
- window.removeEventListener("pointerup", endDrag, true);
678
- window.removeEventListener("pointercancel", endDrag, true);
679
- try { host.releasePointerCapture(pointer); } catch (err) { /* already released */ }
680
- if (!moved) return;
681
- ui.dropped = { x: Math.round(railBox.x), y: Math.round(railBox.y) };
682
- post({ action: "move", x: ui.dropped.x, y: ui.dropped.y });
683
- placeAll();
684
- };
685
- const startDrag = (event) => {
686
- if (event.button !== 0) return;
687
- event.preventDefault();
688
- ui.drag = { dx: event.clientX - railBox.x, dy: event.clientY - railBox.y, pos: { x: railBox.x, y: railBox.y }, moved: false, pointer: event.pointerId };
689
- // Captured on the host, which re-renders never replace
690
- try { host.setPointerCapture(event.pointerId); } catch (err) { /* moves still arrive while over the window */ }
691
- window.addEventListener("pointermove", onDragMove, true);
692
- window.addEventListener("pointerup", endDrag, true);
693
- window.addEventListener("pointercancel", endDrag, true);
694
- };
695
-
696
- // ---- Events inside the toolbar; none of them reach the page's bubbling listeners ----
697
- root.addEventListener("click", (event) => {
698
- event.stopPropagation();
699
- const control = event.target.closest && event.target.closest("[data-action]");
700
- if (!control || control.disabled) return;
701
- press(control.getAttribute("data-action"), {
702
- kind: control.getAttribute("data-kind"),
703
- name: control.getAttribute("data-name"),
704
- matcher: control.getAttribute("data-matcher"),
705
- id: control.getAttribute("data-id"),
706
- });
707
- });
708
- root.addEventListener("pointerdown", (event) => {
709
- event.stopPropagation();
710
- if (event.target.closest && event.target.closest("[data-grip]")) startDrag(event);
711
- });
712
- const onKeydown = (event, target) => {
713
- const enter = event.key === "Enter" && target && target.getAttribute && target.getAttribute("data-enter");
714
- if (!enter || event.isComposing) return;
715
- event.preventDefault();
716
- if (target.getAttribute("data-field")) onField(event, target);
717
- press(enter);
718
- };
719
- // Typing reaches these only when it does not cross the shadow boundary (the window listeners below take the rest)
720
- root.addEventListener("input", (event) => { event.stopPropagation(); onField(event); });
721
- root.addEventListener("change", (event) => { event.stopPropagation(); onField(event); });
722
- root.addEventListener("keydown", (event) => {
723
- event.stopPropagation();
724
- onKeydown(event, event.target);
725
- });
726
- for (const type of ["keyup", "keypress", "mousedown", "mouseup", "pointerup", "dblclick", "focusin", "focusout"]) {
727
- root.addEventListener(type, (event) => event.stopPropagation());
728
- }
729
-
730
- // ---- Events on the page (window capture runs before the page's and the capture script's listeners) ----
731
- const swallow = (event) => {
732
- event.preventDefault();
733
- event.stopImmediatePropagation();
734
- };
735
- // Typing in the toolbar stays in the toolbar: these run first on the window (registered at document start,
736
- // before any page script), so no page listener sees a key, typed text or the clipboard of a toolbar input
737
- // (the New secret value among them). The browser's default action, the text going into the input, still
738
- // happens. Outside the shadow root the event's target is the host, so the input is the focused element.
739
- const TYPING = ["keydown", "keyup", "keypress", "beforeinput", "input", "compositionstart", "compositionupdate", "compositionend", "paste", "cut", "copy"];
740
- for (const type of TYPING) {
741
- listen(window, type, (event) => {
742
- // An untrusted event was dispatched by the page itself: it neither carries the tester's typing nor acts
743
- if (!event.isTrusted || !inToolbar(event)) return;
744
- event.stopImmediatePropagation();
745
- const target = root.activeElement;
746
- if (type === "keydown") {
747
- if (event.key === "Escape" && (ui.picking || ui.panel)) {
748
- event.preventDefault();
749
- handlers.escape();
750
- } else onKeydown(event, target);
751
- } else if (type === "input" && target && target.type !== "radio" && target.type !== "checkbox") {
752
- // A radio or checkbox the pointer changed need not have the focus; its change event (below) updates it
753
- onField(event, target);
754
- }
755
- }, true);
756
- }
757
- listen(window, "keydown", (event) => {
758
- if (event.isTrusted && event.key === "Escape" && (ui.picking || ui.panel)) {
759
- swallow(event);
760
- handlers.escape();
761
- }
762
- }, true);
763
- listen(window, "pointerover", (event) => {
764
- if (!ui.picking) return;
765
- if (inToolbar(event) || !event.target || event.target.nodeType !== 1) hideOutline();
766
- else showOutline(event.target);
767
- }, true);
768
- for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "dblclick", "auxclick", "contextmenu"]) {
769
- listen(window, type, (event) => {
770
- if (inToolbar(event)) return;
771
- if (ui.picking) return swallow(event);
772
- if (type === "pointerdown" && ui.panel) {
773
- closePanel();
774
- render();
775
- }
776
- }, true);
777
- }
778
- listen(window, "click", (event) => {
779
- if (!ui.picking || inToolbar(event)) return;
780
- swallow(event);
781
- pick(event.target);
782
- }, true);
783
- listen(document, "focusin", (event) => {
784
- if (inToolbar(event) || !event.target || event.target.nodeType !== 1) return;
785
- ui.field = isTextEntry(event.target) ? event.target : null;
786
- if (ui.panel === "insert" && !ui.insert.field && ui.field) {
787
- ui.insert.field = ui.field;
788
- render();
789
- }
790
- }, true);
791
- listen(window, "resize", () => render());
792
- listen(window, "scroll", () => {
793
- if (ui.panel === "assert" && ui.assert && !outline.hidden) showOutline(ui.assert.target);
794
- }, true);
795
-
796
- // ---- The recorder's side ----
797
- const showToast = (text) => {
798
- toast.textContent = text;
799
- toast.hidden = false;
800
- toast.classList.remove("gone");
801
- clearTimeout(toastTimer);
802
- toastTimer = setTimeout(() => {
803
- toast.classList.add("gone");
804
- toastTimer = setTimeout(() => { toast.hidden = true; }, 300);
805
- }, 2500);
806
- };
807
- const update = (next) => {
808
- state = Object.assign({}, DEFAULTS, next);
809
- const dropped = ui.dropped;
810
- if (dropped && state.position && Math.round(state.position.x) === dropped.x && Math.round(state.position.y) === dropped.y) ui.dropped = null;
811
- if (ui.closeOnUpdate) closePanel();
812
- if (!usable()) {
813
- stopPicking();
814
- closePanel();
815
- }
816
- if (state.toast && state.toast.id !== ui.toastId) {
817
- ui.toastId = state.toast.id;
818
- showToast(state.toast.text);
819
- }
820
- attach();
821
- render();
822
- };
823
- const reply = (id, result) => {
824
- if (!ui.pending || id !== ui.pending || !result) return;
825
- ui.busy = false;
826
- const ok = result.ok === true;
827
- if (ui.panel === "assert" && ui.assert && !ui.assert.info) {
828
- const a = ui.assert;
829
- a.info = result;
830
- if (ok) {
831
- a.matcher = result.first.matcher;
832
- a.attribute = result.attributes[0] ? result.attributes[0].name : null;
833
- a.countIndex = 0;
834
- a.expected = prefill(a, a.matcher);
835
- }
836
- } else if (ok) {
837
- closePanel();
838
- } else if (ui.panel === "assert" && ui.assert) {
839
- ui.assert.error = result.error;
840
- } else if (ui.panel === "insert") {
841
- ui.insert.error = result.error;
842
- } else if (ui.panel === "new") {
843
- if (result.field === "name") ui.form.nameError = result.error;
844
- else ui.form.error = result.error;
845
- }
846
- render();
847
- };
848
-
849
- // ---- Attach ----
850
- let announced = false;
851
- const attach = () => {
852
- const html = document.documentElement;
853
- if (!html) return false;
854
- if (host.parentNode !== html) html.appendChild(host);
855
- if (!announced) {
856
- announced = true;
857
- render();
858
- post({ action: "ready", url: location.href });
859
- }
860
- return true;
861
- };
862
- const rebind = (fresh) => {
863
- channel = fresh;
864
- announced = false;
865
- attach();
866
- };
867
- let watcher = null;
868
- // The recording ended while this document stays open: take every listener and the host away
869
- const dispose = () => {
870
- endDrag();
871
- for (const [target, type, handler, capture] of listening.splice(0)) target.removeEventListener(type, handler, capture);
872
- if (watcher) watcher.disconnect();
873
- clearTimeout(toastTimer);
874
- channel = () => {};
875
- host.remove();
876
- if (globalThis[KEY] && globalThis[KEY].root === root) delete globalThis[KEY];
877
- };
878
- globalThis[KEY] = { root, update, reply, press, rebind, dispose, state: () => state, picking: () => ui.picking };
879
-
880
- const focused = document.activeElement;
881
- if (focused && focused.nodeType === 1 && isTextEntry(focused)) ui.field = focused;
882
- if (!attach()) {
883
- watcher = new MutationObserver(() => { if (attach()) watcher.disconnect(); });
884
- watcher.observe(document, { childList: true });
885
- }
886
- // The page may replace the root element while it loads
887
- listen(document, "DOMContentLoaded", attach);
888
- listen(window, "load", attach);
48
+ ${SETUP_JS}
49
+ ${DOM_JS}
50
+ ${NAMES_JS}
51
+ ${PLACEMENT_JS}
52
+ ${VIEWS_RAIL_JS}
53
+ ${VIEWS_PANELS_JS}
54
+ ${VIEWS_RENDER_JS}
55
+ ${PICKING_JS}
56
+ ${HANDLERS_JS}
57
+ ${FIELDS_JS}
58
+ ${DRAG_JS}
59
+ ${EVENTS_TOOLBAR_JS}
60
+ ${EVENTS_PAGE_JS}
61
+ ${RECORDER_SIDE_JS}
62
+ ${ATTACH_JS}
889
63
  }`;