@monoes/monobrowse 1.0.12 → 1.0.14

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 (271) hide show
  1. package/dist/src/__tests__/bridge-instruments.test.d.ts +2 -0
  2. package/dist/src/__tests__/bridge-instruments.test.d.ts.map +1 -0
  3. package/dist/src/__tests__/bridge-instruments.test.js +132 -0
  4. package/dist/src/__tests__/bridge-instruments.test.js.map +1 -0
  5. package/dist/src/__tests__/bridge-transport.test.d.ts +2 -0
  6. package/dist/src/__tests__/bridge-transport.test.d.ts.map +1 -0
  7. package/dist/src/__tests__/bridge-transport.test.js +287 -0
  8. package/dist/src/__tests__/bridge-transport.test.js.map +1 -0
  9. package/dist/src/__tests__/cdp-transport.test.d.ts +2 -0
  10. package/dist/src/__tests__/cdp-transport.test.d.ts.map +1 -0
  11. package/dist/src/__tests__/cdp-transport.test.js +167 -0
  12. package/dist/src/__tests__/cdp-transport.test.js.map +1 -0
  13. package/dist/src/__tests__/profiler-heap.test.d.ts +2 -0
  14. package/dist/src/__tests__/profiler-heap.test.d.ts.map +1 -0
  15. package/dist/src/__tests__/profiler-heap.test.js +154 -0
  16. package/dist/src/__tests__/profiler-heap.test.js.map +1 -0
  17. package/dist/src/__tests__/report-a11y.test.d.ts +2 -0
  18. package/dist/src/__tests__/report-a11y.test.d.ts.map +1 -0
  19. package/dist/src/__tests__/report-a11y.test.js +147 -0
  20. package/dist/src/__tests__/report-a11y.test.js.map +1 -0
  21. package/dist/src/__tests__/report-analyze.test.d.ts +2 -0
  22. package/dist/src/__tests__/report-analyze.test.d.ts.map +1 -0
  23. package/dist/src/__tests__/report-analyze.test.js +157 -0
  24. package/dist/src/__tests__/report-analyze.test.js.map +1 -0
  25. package/dist/src/__tests__/report-budget.test.d.ts +2 -0
  26. package/dist/src/__tests__/report-budget.test.d.ts.map +1 -0
  27. package/dist/src/__tests__/report-budget.test.js +80 -0
  28. package/dist/src/__tests__/report-budget.test.js.map +1 -0
  29. package/dist/src/__tests__/report-diff.test.d.ts +2 -0
  30. package/dist/src/__tests__/report-diff.test.d.ts.map +1 -0
  31. package/dist/src/__tests__/report-diff.test.js +231 -0
  32. package/dist/src/__tests__/report-diff.test.js.map +1 -0
  33. package/dist/src/__tests__/report-evidence.test.d.ts +2 -0
  34. package/dist/src/__tests__/report-evidence.test.d.ts.map +1 -0
  35. package/dist/src/__tests__/report-evidence.test.js +214 -0
  36. package/dist/src/__tests__/report-evidence.test.js.map +1 -0
  37. package/dist/src/__tests__/report-flake.test.d.ts +2 -0
  38. package/dist/src/__tests__/report-flake.test.d.ts.map +1 -0
  39. package/dist/src/__tests__/report-flake.test.js +182 -0
  40. package/dist/src/__tests__/report-flake.test.js.map +1 -0
  41. package/dist/src/__tests__/report-history.test.d.ts +2 -0
  42. package/dist/src/__tests__/report-history.test.d.ts.map +1 -0
  43. package/dist/src/__tests__/report-history.test.js +229 -0
  44. package/dist/src/__tests__/report-history.test.js.map +1 -0
  45. package/dist/src/__tests__/report-output.test.d.ts +2 -0
  46. package/dist/src/__tests__/report-output.test.d.ts.map +1 -0
  47. package/dist/src/__tests__/report-output.test.js +169 -0
  48. package/dist/src/__tests__/report-output.test.js.map +1 -0
  49. package/dist/src/__tests__/report-png.test.d.ts +2 -0
  50. package/dist/src/__tests__/report-png.test.d.ts.map +1 -0
  51. package/dist/src/__tests__/report-png.test.js +84 -0
  52. package/dist/src/__tests__/report-png.test.js.map +1 -0
  53. package/dist/src/__tests__/report-render-extras.test.d.ts +2 -0
  54. package/dist/src/__tests__/report-render-extras.test.d.ts.map +1 -0
  55. package/dist/src/__tests__/report-render-extras.test.js +285 -0
  56. package/dist/src/__tests__/report-render-extras.test.js.map +1 -0
  57. package/dist/src/__tests__/report-render.test.d.ts +2 -0
  58. package/dist/src/__tests__/report-render.test.d.ts.map +1 -0
  59. package/dist/src/__tests__/report-render.test.js +143 -0
  60. package/dist/src/__tests__/report-render.test.js.map +1 -0
  61. package/dist/src/__tests__/report-run.test.d.ts +2 -0
  62. package/dist/src/__tests__/report-run.test.d.ts.map +1 -0
  63. package/dist/src/__tests__/report-run.test.js +234 -0
  64. package/dist/src/__tests__/report-run.test.js.map +1 -0
  65. package/dist/src/browser/bridge.d.ts +119 -0
  66. package/dist/src/browser/bridge.d.ts.map +1 -0
  67. package/dist/src/browser/bridge.js +264 -0
  68. package/dist/src/browser/bridge.js.map +1 -0
  69. package/dist/src/browser/cdp.d.ts +11 -1
  70. package/dist/src/browser/cdp.d.ts.map +1 -1
  71. package/dist/src/browser/cdp.js +60 -56
  72. package/dist/src/browser/cdp.js.map +1 -1
  73. package/dist/src/browser/index.d.ts +3 -0
  74. package/dist/src/browser/index.d.ts.map +1 -1
  75. package/dist/src/browser/index.js +3 -0
  76. package/dist/src/browser/index.js.map +1 -1
  77. package/dist/src/browser/profiler.d.ts.map +1 -1
  78. package/dist/src/browser/profiler.js +19 -22
  79. package/dist/src/browser/profiler.js.map +1 -1
  80. package/dist/src/browser/transport.d.ts +62 -0
  81. package/dist/src/browser/transport.d.ts.map +1 -0
  82. package/dist/src/browser/transport.js +65 -0
  83. package/dist/src/browser/transport.js.map +1 -0
  84. package/dist/src/cli/commander-adapter.d.ts +16 -0
  85. package/dist/src/cli/commander-adapter.d.ts.map +1 -0
  86. package/dist/src/cli/commander-adapter.js +68 -0
  87. package/dist/src/cli/commander-adapter.js.map +1 -0
  88. package/dist/src/cli/commands-context.d.ts +14 -0
  89. package/dist/src/cli/commands-context.d.ts.map +1 -0
  90. package/dist/src/cli/commands-context.js +351 -0
  91. package/dist/src/cli/commands-context.js.map +1 -0
  92. package/dist/src/cli/commands-data.d.ts +12 -0
  93. package/dist/src/cli/commands-data.d.ts.map +1 -0
  94. package/dist/src/cli/commands-data.js +246 -0
  95. package/dist/src/cli/commands-data.js.map +1 -0
  96. package/dist/src/cli/commands-element.d.ts +15 -0
  97. package/dist/src/cli/commands-element.d.ts.map +1 -0
  98. package/dist/src/cli/commands-element.js +262 -0
  99. package/dist/src/cli/commands-element.js.map +1 -0
  100. package/dist/src/cli/commands-files.d.ts +9 -0
  101. package/dist/src/cli/commands-files.d.ts.map +1 -0
  102. package/dist/src/cli/commands-files.js +207 -0
  103. package/dist/src/cli/commands-files.js.map +1 -0
  104. package/dist/src/cli/commands-input.d.ts +26 -0
  105. package/dist/src/cli/commands-input.d.ts.map +1 -0
  106. package/dist/src/cli/commands-input.js +399 -0
  107. package/dist/src/cli/commands-input.js.map +1 -0
  108. package/dist/src/cli/commands-navigate.d.ts +12 -0
  109. package/dist/src/cli/commands-navigate.d.ts.map +1 -0
  110. package/dist/src/cli/commands-navigate.js +389 -0
  111. package/dist/src/cli/commands-navigate.js.map +1 -0
  112. package/dist/src/cli/commands-page.d.ts +25 -0
  113. package/dist/src/cli/commands-page.d.ts.map +1 -0
  114. package/dist/src/cli/commands-page.js +377 -0
  115. package/dist/src/cli/commands-page.js.map +1 -0
  116. package/dist/src/cli/commands-report.d.ts +8 -0
  117. package/dist/src/cli/commands-report.d.ts.map +1 -0
  118. package/dist/src/cli/commands-report.js +256 -0
  119. package/dist/src/cli/commands-report.js.map +1 -0
  120. package/dist/src/cli/commands-script.d.ts +43 -0
  121. package/dist/src/cli/commands-script.d.ts.map +1 -0
  122. package/dist/src/cli/commands-script.js +168 -0
  123. package/dist/src/cli/commands-script.js.map +1 -0
  124. package/dist/src/cli/commands-session.d.ts +15 -0
  125. package/dist/src/cli/commands-session.d.ts.map +1 -0
  126. package/dist/src/cli/commands-session.js +357 -0
  127. package/dist/src/cli/commands-session.js.map +1 -0
  128. package/dist/src/cli/commands-trace.d.ts +12 -0
  129. package/dist/src/cli/commands-trace.d.ts.map +1 -0
  130. package/dist/src/cli/commands-trace.js +347 -0
  131. package/dist/src/cli/commands-trace.js.map +1 -0
  132. package/dist/src/cli/commands.d.ts +22 -42
  133. package/dist/src/cli/commands.d.ts.map +1 -1
  134. package/dist/src/cli/commands.js +48 -3400
  135. package/dist/src/cli/commands.js.map +1 -1
  136. package/dist/src/cli/session.d.ts +47 -0
  137. package/dist/src/cli/session.d.ts.map +1 -0
  138. package/dist/src/cli/session.js +343 -0
  139. package/dist/src/cli/session.js.map +1 -0
  140. package/dist/src/report/a11y.d.ts +52 -0
  141. package/dist/src/report/a11y.d.ts.map +1 -0
  142. package/dist/src/report/a11y.js +231 -0
  143. package/dist/src/report/a11y.js.map +1 -0
  144. package/dist/src/report/analyze.d.ts +20 -0
  145. package/dist/src/report/analyze.d.ts.map +1 -0
  146. package/dist/src/report/analyze.js +136 -0
  147. package/dist/src/report/analyze.js.map +1 -0
  148. package/dist/src/report/budget.d.ts +27 -0
  149. package/dist/src/report/budget.d.ts.map +1 -0
  150. package/dist/src/report/budget.js +143 -0
  151. package/dist/src/report/budget.js.map +1 -0
  152. package/dist/src/report/collect-a11y.d.ts +25 -0
  153. package/dist/src/report/collect-a11y.d.ts.map +1 -0
  154. package/dist/src/report/collect-a11y.js +154 -0
  155. package/dist/src/report/collect-a11y.js.map +1 -0
  156. package/dist/src/report/collect.d.ts +45 -0
  157. package/dist/src/report/collect.d.ts.map +1 -0
  158. package/dist/src/report/collect.js +275 -0
  159. package/dist/src/report/collect.js.map +1 -0
  160. package/dist/src/report/evidence.d.ts +80 -0
  161. package/dist/src/report/evidence.d.ts.map +1 -0
  162. package/dist/src/report/evidence.js +279 -0
  163. package/dist/src/report/evidence.js.map +1 -0
  164. package/dist/src/report/flake.d.ts +88 -0
  165. package/dist/src/report/flake.d.ts.map +1 -0
  166. package/dist/src/report/flake.js +232 -0
  167. package/dist/src/report/flake.js.map +1 -0
  168. package/dist/src/report/history.d.ts +85 -0
  169. package/dist/src/report/history.d.ts.map +1 -0
  170. package/dist/src/report/history.js +176 -0
  171. package/dist/src/report/history.js.map +1 -0
  172. package/dist/src/report/index.d.ts +102 -0
  173. package/dist/src/report/index.d.ts.map +1 -0
  174. package/dist/src/report/index.js +216 -0
  175. package/dist/src/report/index.js.map +1 -0
  176. package/dist/src/report/pixel-diff.d.ts +30 -0
  177. package/dist/src/report/pixel-diff.d.ts.map +1 -0
  178. package/dist/src/report/pixel-diff.js +175 -0
  179. package/dist/src/report/pixel-diff.js.map +1 -0
  180. package/dist/src/report/png.d.ts +28 -0
  181. package/dist/src/report/png.d.ts.map +1 -0
  182. package/dist/src/report/png.js +259 -0
  183. package/dist/src/report/png.js.map +1 -0
  184. package/dist/src/report/render-evidence.d.ts +16 -0
  185. package/dist/src/report/render-evidence.d.ts.map +1 -0
  186. package/dist/src/report/render-evidence.js +174 -0
  187. package/dist/src/report/render-evidence.js.map +1 -0
  188. package/dist/src/report/render-trend.d.ts +16 -0
  189. package/dist/src/report/render-trend.d.ts.map +1 -0
  190. package/dist/src/report/render-trend.js +194 -0
  191. package/dist/src/report/render-trend.js.map +1 -0
  192. package/dist/src/report/render.d.ts +21 -0
  193. package/dist/src/report/render.d.ts.map +1 -0
  194. package/dist/src/report/render.js +286 -0
  195. package/dist/src/report/render.js.map +1 -0
  196. package/dist/src/report/run-diff.d.ts +18 -0
  197. package/dist/src/report/run-diff.d.ts.map +1 -0
  198. package/dist/src/report/run-diff.js +51 -0
  199. package/dist/src/report/run-diff.js.map +1 -0
  200. package/dist/src/report/structure.d.ts +41 -0
  201. package/dist/src/report/structure.d.ts.map +1 -0
  202. package/dist/src/report/structure.js +210 -0
  203. package/dist/src/report/structure.js.map +1 -0
  204. package/dist/src/report/trend.d.ts +26 -0
  205. package/dist/src/report/trend.d.ts.map +1 -0
  206. package/dist/src/report/trend.js +164 -0
  207. package/dist/src/report/trend.js.map +1 -0
  208. package/dist/src/report/types.d.ts +320 -0
  209. package/dist/src/report/types.d.ts.map +1 -0
  210. package/dist/src/report/types.js +11 -0
  211. package/dist/src/report/types.js.map +1 -0
  212. package/dist/src/report/util.d.ts +17 -0
  213. package/dist/src/report/util.d.ts.map +1 -0
  214. package/dist/src/report/util.js +45 -0
  215. package/dist/src/report/util.js.map +1 -0
  216. package/dist/tsconfig.tsbuildinfo +1 -1
  217. package/package.json +1 -1
  218. package/src/__tests__/bridge-instruments.test.ts +180 -0
  219. package/src/__tests__/bridge-transport.test.ts +365 -0
  220. package/src/__tests__/cdp-transport.test.ts +198 -0
  221. package/src/__tests__/profiler-heap.test.ts +191 -0
  222. package/src/__tests__/report-a11y.test.ts +180 -0
  223. package/src/__tests__/report-analyze.test.ts +202 -0
  224. package/src/__tests__/report-budget.test.ts +92 -0
  225. package/src/__tests__/report-diff.test.ts +272 -0
  226. package/src/__tests__/report-evidence.test.ts +276 -0
  227. package/src/__tests__/report-flake.test.ts +214 -0
  228. package/src/__tests__/report-history.test.ts +293 -0
  229. package/src/__tests__/report-output.test.ts +210 -0
  230. package/src/__tests__/report-png.test.ts +95 -0
  231. package/src/__tests__/report-render-extras.test.ts +315 -0
  232. package/src/__tests__/report-render.test.ts +175 -0
  233. package/src/__tests__/report-run.test.ts +264 -0
  234. package/src/browser/bridge.ts +354 -0
  235. package/src/browser/cdp.ts +57 -52
  236. package/src/browser/index.ts +3 -0
  237. package/src/browser/profiler.ts +28 -21
  238. package/src/browser/transport.ts +111 -0
  239. package/src/cli/commander-adapter.ts +65 -0
  240. package/src/cli/commands-context.ts +367 -0
  241. package/src/cli/commands-data.ts +248 -0
  242. package/src/cli/commands-element.ts +305 -0
  243. package/src/cli/commands-files.ts +248 -0
  244. package/src/cli/commands-input.ts +434 -0
  245. package/src/cli/commands-navigate.ts +442 -0
  246. package/src/cli/commands-page.ts +420 -0
  247. package/src/cli/commands-report.ts +275 -0
  248. package/src/cli/commands-script.ts +183 -0
  249. package/src/cli/commands-session.ts +376 -0
  250. package/src/cli/commands-trace.ts +349 -0
  251. package/src/cli/commands.ts +101 -3677
  252. package/src/cli/session.ts +412 -0
  253. package/src/report/a11y.ts +242 -0
  254. package/src/report/analyze.ts +181 -0
  255. package/src/report/budget.ts +155 -0
  256. package/src/report/collect-a11y.ts +200 -0
  257. package/src/report/collect.ts +408 -0
  258. package/src/report/evidence.ts +379 -0
  259. package/src/report/flake.ts +303 -0
  260. package/src/report/history.ts +219 -0
  261. package/src/report/index.ts +307 -0
  262. package/src/report/pixel-diff.ts +197 -0
  263. package/src/report/png.ts +290 -0
  264. package/src/report/render-evidence.ts +194 -0
  265. package/src/report/render-trend.ts +225 -0
  266. package/src/report/render.ts +314 -0
  267. package/src/report/run-diff.ts +63 -0
  268. package/src/report/structure.ts +212 -0
  269. package/src/report/trend.ts +189 -0
  270. package/src/report/types.ts +356 -0
  271. package/src/report/util.ts +48 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Asking about elements — locating them by semantic query, and reading back
3
+ * whether one is visible, enabled or checked.
4
+ *
5
+ * These answer questions; they never change the page (`highlight` aside, which
6
+ * draws a temporary overlay).
7
+ */
8
+
9
+ import type { ElementRef, FindAction } from '../index.js';
10
+ import { output } from './output.js';
11
+ import { ensureConnected, getBrowser, print, resolveElementObjectId, session } from './session.js';
12
+ import type { Command, CommandContext, CommandResult } from './types.js';
13
+
14
+ export const isvisibleCommand: Command = {
15
+ name: 'isvisible',
16
+ description: 'Check if element is visible. Usage: monomind browse isvisible @e1|"selector"',
17
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
18
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
19
+ const { client, sessionId } = await ensureConnected(session.port);
20
+ const arg = ctx.args[0] as string;
21
+ if (!arg) throw new Error('Usage: monomind browse isvisible @e1|".selector"');
22
+ const objectId = await resolveElementObjectId(client, sessionId, session.refs, arg);
23
+ const r = await client.send<{ result: { value?: boolean } }>(
24
+ 'Runtime.callFunctionOn',
25
+ {
26
+ functionDeclaration: `function(){var r=this.getBoundingClientRect(),s=window.getComputedStyle(this);return r.width>0&&r.height>0&&s.visibility!=='hidden'&&s.display!=='none'&&parseFloat(s.opacity)>0;}`,
27
+ objectId,
28
+ returnByValue: true,
29
+ },
30
+ sessionId,
31
+ );
32
+ const visible = r.result?.value ?? false;
33
+ if (ctx.flags.json) {
34
+ print(JSON.stringify({ visible }));
35
+ } else {
36
+ output.printSuccess(`isvisible: ${visible}`);
37
+ }
38
+ return { success: true, data: { visible } };
39
+ },
40
+ };
41
+
42
+ export const isenabledCommand: Command = {
43
+ name: 'isenabled',
44
+ description:
45
+ 'Check if element is enabled (not disabled). Usage: monomind browse isenabled @e1|"selector"',
46
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
47
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
48
+ const { client, sessionId } = await ensureConnected(session.port);
49
+ const arg = ctx.args[0] as string;
50
+ if (!arg) throw new Error('Usage: monomind browse isenabled @e1|".selector"');
51
+ const objectId = await resolveElementObjectId(client, sessionId, session.refs, arg);
52
+ const r = await client.send<{ result: { value?: boolean } }>(
53
+ 'Runtime.callFunctionOn',
54
+ {
55
+ functionDeclaration: `function(){return !this.disabled;}`,
56
+ objectId,
57
+ returnByValue: true,
58
+ },
59
+ sessionId,
60
+ );
61
+ const enabled = r.result?.value ?? true;
62
+ if (ctx.flags.json) {
63
+ print(JSON.stringify({ enabled }));
64
+ } else {
65
+ output.printSuccess(`isenabled: ${enabled}`);
66
+ }
67
+ return { success: true, data: { enabled } };
68
+ },
69
+ };
70
+
71
+ export const ischeckedCommand: Command = {
72
+ name: 'ischecked',
73
+ description:
74
+ 'Check if checkbox/radio is checked. Usage: monomind browse ischecked @e1|"selector"',
75
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
76
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
77
+ const { client, sessionId } = await ensureConnected(session.port);
78
+ const arg = ctx.args[0] as string;
79
+ if (!arg) throw new Error('Usage: monomind browse ischecked @e1|".selector"');
80
+ const objectId = await resolveElementObjectId(client, sessionId, session.refs, arg);
81
+ const r = await client.send<{ result: { value?: boolean } }>(
82
+ 'Runtime.callFunctionOn',
83
+ {
84
+ functionDeclaration: `function(){var el=this,tag=el.tagName&&el.tagName.toUpperCase();if(tag==='INPUT'&&(el.type==='checkbox'||el.type==='radio'))return el.checked;var role=el.getAttribute&&el.getAttribute('role');if(role&&['checkbox','radio','switch','menuitemcheckbox','menuitemradio','option','treeitem'].indexOf(role)!==-1)return el.getAttribute('aria-checked')==='true';var label=tag!=='LABEL'?el.closest&&el.closest('label'):el;if(label&&label.control&&(label.control.type==='checkbox'||label.control.type==='radio'))return label.control.checked;var inp=el.querySelector&&el.querySelector('input[type="checkbox"],input[type="radio"]');return inp?inp.checked:false;}`,
85
+ objectId,
86
+ returnByValue: true,
87
+ },
88
+ sessionId,
89
+ );
90
+ const checked = r.result?.value ?? false;
91
+ if (ctx.flags.json) {
92
+ print(JSON.stringify({ checked }));
93
+ } else {
94
+ output.printSuccess(`ischecked: ${checked}`);
95
+ }
96
+ return { success: true, data: { checked } };
97
+ },
98
+ };
99
+
100
+ export const isCommand: Command = {
101
+ name: 'is',
102
+ description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
103
+ options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
104
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
105
+ const { client, sessionId } = await ensureConnected(session.port);
106
+ const browser = await getBrowser();
107
+ const check = ctx.args[0] as string;
108
+ const refArg = ctx.args[1] as string;
109
+ if (!check || !refArg) throw new Error('Usage: monomind browse is visible|enabled|checked @e1');
110
+
111
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
112
+ const ref = await browser.resolveRef(client, sessionId, session.refs, refKey);
113
+
114
+ let result: boolean;
115
+ switch (check) {
116
+ case 'visible':
117
+ result = await browser.isVisible(client, sessionId, ref);
118
+ break;
119
+ case 'enabled':
120
+ result = await browser.isEnabled(client, sessionId, ref);
121
+ break;
122
+ case 'checked':
123
+ result = await browser.isChecked(client, sessionId, ref);
124
+ break;
125
+ default:
126
+ throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
127
+ }
128
+
129
+ if (ctx.flags.json) {
130
+ print(JSON.stringify({ data: { [check]: result } }));
131
+ } else {
132
+ print(result ? 'true' : 'false');
133
+ }
134
+ return { success: true, data: { [check]: result } };
135
+ },
136
+ };
137
+
138
+ export const findCommand: Command = {
139
+ name: 'find',
140
+ description:
141
+ 'Find elements by semantic locators. Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]',
142
+ options: [
143
+ { name: 'name', type: 'string', description: 'Filter by accessible name' },
144
+ { name: 'exact', type: 'boolean', description: 'Require exact match', default: false },
145
+ { name: 'nth', type: 'number', description: 'Find nth match' },
146
+ { name: 'last', type: 'boolean', description: 'Find last match', default: false },
147
+ ],
148
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
149
+ const { client, sessionId } = await ensureConnected(session.port);
150
+ const browser = await getBrowser();
151
+ const locator = ctx.args[0] as string;
152
+ const value = ctx.args[1] as string;
153
+ const action = ctx.args[2] as FindAction | undefined;
154
+
155
+ if (!locator || !value)
156
+ throw new Error(
157
+ 'Usage: monomind browse find role|text|label|placeholder|testid|alttext|title|selector <value> [action]',
158
+ );
159
+
160
+ const opts = {
161
+ name: ctx.flags.name as string,
162
+ exact: ctx.flags.exact as boolean,
163
+ nth: ctx.flags.nth as number,
164
+ last: ctx.flags.last as boolean,
165
+ };
166
+
167
+ // alttext: find element by alt attribute (images, icons)
168
+ if (locator === 'alttext') {
169
+ // I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
170
+ const valJson = JSON.stringify(value);
171
+ const found = (await browser.evaluateJs(
172
+ client,
173
+ sessionId,
174
+ `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`,
175
+ )) as boolean;
176
+ if (!found) {
177
+ output.printWarning(`alttext not found: ${value}`);
178
+ return { success: false };
179
+ }
180
+ output.printSuccess(`Found element with alt="${value}"`);
181
+ return { success: true, data: { alttext: value } };
182
+ }
183
+
184
+ // title: find element by title attribute
185
+ if (locator === 'title') {
186
+ // I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
187
+ const valJson = JSON.stringify(value);
188
+ const found = (await browser.evaluateJs(
189
+ client,
190
+ sessionId,
191
+ `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`,
192
+ )) as boolean;
193
+ if (!found) {
194
+ output.printWarning(`title not found: ${value}`);
195
+ return { success: false };
196
+ }
197
+ output.printSuccess(`Found element with title="${value}"`);
198
+ return { success: true, data: { title: value } };
199
+ }
200
+
201
+ let ref: ElementRef | null = null;
202
+ switch (locator) {
203
+ case 'role':
204
+ ref = await browser.findByRole(client, sessionId, session.refs, value, opts);
205
+ break;
206
+ case 'text':
207
+ ref = await browser.findByText(client, sessionId, session.refs, value, opts);
208
+ break;
209
+ case 'label':
210
+ ref = await browser.findByLabel(client, sessionId, session.refs, value, opts);
211
+ break;
212
+ case 'placeholder':
213
+ ref = await browser.findByPlaceholder(client, sessionId, session.refs, value, opts);
214
+ break;
215
+ case 'selector':
216
+ ref = await browser.findBySelector(client, sessionId, session.refs, value, opts);
217
+ break;
218
+ case 'testid': {
219
+ const sel = await browser.findByTestId(client, sessionId, value);
220
+ if (!sel) {
221
+ output.printWarning(`testid not found: ${value}`);
222
+ return { success: false };
223
+ }
224
+ output.printSuccess(`Found testid selector: ${sel}`);
225
+ return { success: true, data: { selector: sel } };
226
+ }
227
+ default:
228
+ throw new Error(
229
+ `Unknown locator: ${locator}. Use: role|text|label|placeholder|testid|alttext|title|selector`,
230
+ );
231
+ }
232
+
233
+ if (!ref) {
234
+ output.printWarning(`No element found: ${locator}="${value}"`);
235
+ return { success: false };
236
+ }
237
+
238
+ output.printSuccess(`Found: ${ref.role} "${ref.name}" [@${ref.ref}]`);
239
+
240
+ if (action) {
241
+ switch (action) {
242
+ case 'click':
243
+ await browser.clickElement(client, sessionId, ref);
244
+ break;
245
+ case 'fill': {
246
+ const fillValue = ctx.args[3] as string;
247
+ await browser.fillElement(client, sessionId, ref, fillValue ?? '');
248
+ break;
249
+ }
250
+ case 'type': {
251
+ const typeValue = ctx.args[3] as string;
252
+ await browser.typeIntoElement(client, sessionId, ref, typeValue ?? '');
253
+ break;
254
+ }
255
+ case 'hover':
256
+ await browser.hoverElement(client, sessionId, ref);
257
+ break;
258
+ case 'focus':
259
+ await browser.focusElement(client, sessionId, ref);
260
+ break;
261
+ case 'check':
262
+ await browser.checkElement(client, sessionId, ref, true);
263
+ break;
264
+ case 'uncheck':
265
+ await browser.checkElement(client, sessionId, ref, false);
266
+ break;
267
+ case 'text': {
268
+ const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
269
+ if (objectId) {
270
+ const r = await client.send<{ result: { value?: string } }>(
271
+ 'Runtime.callFunctionOn',
272
+ {
273
+ functionDeclaration:
274
+ 'function() { return this.innerText || this.textContent || ""; }',
275
+ objectId,
276
+ returnByValue: true,
277
+ },
278
+ sessionId,
279
+ );
280
+ print(r.result?.value ?? '');
281
+ }
282
+ break;
283
+ }
284
+ }
285
+ }
286
+
287
+ return { success: true, data: { ref } };
288
+ },
289
+ };
290
+
291
+ export const highlightCommand: Command = {
292
+ name: 'highlight',
293
+ description: 'Highlight an element for 2 seconds. Usage: monomind browse highlight @e1',
294
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
295
+ const { client, sessionId } = await ensureConnected(session.port);
296
+ const browser = await getBrowser();
297
+ const refArg = ctx.args[0] as string;
298
+ if (!refArg) throw new Error('Usage: monomind browse highlight @e1');
299
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
300
+ const ref = await browser.resolveRef(client, sessionId, session.refs, refKey);
301
+ await browser.highlightElement(client, sessionId, ref);
302
+ output.printSuccess(`Highlighted: ${ref.role} "${ref.name}"`);
303
+ return { success: true };
304
+ },
305
+ };
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Files in and out of the page — screenshots, PDFs, uploads and downloads.
3
+ */
4
+
5
+ import { output } from './output.js';
6
+ import {
7
+ ensureConnected,
8
+ getBrowser,
9
+ imageFormat,
10
+ print,
11
+ resolveElementObjectId,
12
+ session,
13
+ } from './session.js';
14
+ import type { Command, CommandContext, CommandResult } from './types.js';
15
+
16
+ export const screenshotCommand: Command = {
17
+ name: 'screenshot',
18
+ description:
19
+ 'Capture a screenshot. Usage: monomind browse screenshot [path] [--annotate] [--hide-scrollbars]',
20
+ options: [
21
+ { name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
22
+ { name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
23
+ { name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
24
+ {
25
+ name: 'annotate',
26
+ type: 'boolean',
27
+ description:
28
+ 'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)',
29
+ default: false,
30
+ },
31
+ {
32
+ name: 'hide-scrollbars',
33
+ type: 'boolean',
34
+ description: 'Hide native scrollbars via CSS injection before capture',
35
+ default: false,
36
+ },
37
+ { name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
38
+ ],
39
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
40
+ const { client, sessionId } = await ensureConnected(session.port);
41
+ const browser = await getBrowser();
42
+
43
+ const hideScrollbars = ctx.flags['hide-scrollbars'] as boolean;
44
+ if (hideScrollbars) {
45
+ await client
46
+ .send(
47
+ 'Runtime.evaluate',
48
+ {
49
+ expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)return;var el=document.createElement('style');el.id='__mm_noscroll__';el.textContent='*::-webkit-scrollbar{display:none!important}*{scrollbar-width:none!important;-ms-overflow-style:none!important}';document.head.appendChild(el);})()`,
50
+ returnByValue: false,
51
+ },
52
+ sessionId,
53
+ )
54
+ .catch(() => {});
55
+ }
56
+
57
+ const annotate = ctx.flags.annotate as boolean;
58
+ // eslint-disable-next-line prefer-const
59
+ let result!: { path: string; dataUrl: string };
60
+ try {
61
+ result = await browser.captureScreenshot(client, sessionId, {
62
+ path: ctx.args[0] as string,
63
+ fullPage: ctx.flags.full as boolean,
64
+ format: imageFormat(ctx.flags.format, ['png', 'jpeg', 'webp'] as const, 'png'),
65
+ quality: ctx.flags.quality as number,
66
+ annotate,
67
+ refs: annotate ? session.refs : undefined,
68
+ });
69
+ } finally {
70
+ if (hideScrollbars) {
71
+ await client
72
+ .send(
73
+ 'Runtime.evaluate',
74
+ {
75
+ expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)s.remove();})()`,
76
+ returnByValue: false,
77
+ },
78
+ sessionId,
79
+ )
80
+ .catch(() => {});
81
+ }
82
+ }
83
+
84
+ if (ctx.flags.json) {
85
+ print(JSON.stringify({ data: { path: result.path } }));
86
+ } else {
87
+ output.printSuccess(`Screenshot saved: ${result.path}`);
88
+ }
89
+
90
+ return { success: true, data: result };
91
+ },
92
+ };
93
+
94
+ export const uploadCommand: Command = {
95
+ name: 'upload',
96
+ description: 'Upload files to a file input. Usage: monomind browse upload @e1 ./file.pdf',
97
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
98
+ const { client, sessionId } = await ensureConnected(session.port);
99
+ const browser = await getBrowser();
100
+ const refArg = ctx.args[0] as string;
101
+ const files = ctx.args.slice(1) as string[];
102
+ if (!refArg || files.length === 0)
103
+ throw new Error('Usage: monomind browse upload @e1 <file1> [file2...]');
104
+ const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
105
+ const ref = await browser.resolveRef(client, sessionId, session.refs, refKey);
106
+ await browser.uploadFile(client, sessionId, ref, files);
107
+ output.printSuccess(`Uploaded ${files.length} file(s) to @${refKey}`);
108
+ return { success: true };
109
+ },
110
+ };
111
+
112
+ export const downloadCommand: Command = {
113
+ name: 'download',
114
+ description:
115
+ 'Click an element and capture the triggered file download. Usage: monomind browse download @e1 ./output.pdf',
116
+ options: [
117
+ {
118
+ name: 'timeout',
119
+ short: 't',
120
+ type: 'number',
121
+ description: 'Max wait for download in ms',
122
+ default: 30000,
123
+ },
124
+ { name: 'json', type: 'boolean', description: 'Output result as JSON', default: false },
125
+ ],
126
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
127
+ const { client, sessionId } = await ensureConnected(session.port);
128
+ const _browser = await getBrowser();
129
+ const refOrSel = ctx.args[0] as string;
130
+ const savePath = ctx.args[1] as string;
131
+ if (!refOrSel || !savePath)
132
+ throw new Error('Usage: monomind browse download @e1|selector <save-path>');
133
+
134
+ const { mkdir } = await import('node:fs/promises');
135
+ const { dirname } = await import('node:path');
136
+ const { tmpdir } = await import('node:os');
137
+ const { join } = await import('node:path');
138
+ const downloadDir = join(tmpdir(), `mm-download-${Date.now()}`);
139
+ await mkdir(downloadDir, { recursive: true });
140
+
141
+ // Enable Page.downloadWillBegin / Page.downloadProgress events
142
+ await client
143
+ .send(
144
+ 'Browser.setDownloadBehavior',
145
+ {
146
+ behavior: 'allow',
147
+ downloadPath: downloadDir,
148
+ eventsEnabled: true,
149
+ },
150
+ undefined,
151
+ )
152
+ .catch(() => {
153
+ // Fallback: older Chrome API (session-scoped)
154
+ return client
155
+ .send(
156
+ 'Page.setDownloadBehavior',
157
+ {
158
+ behavior: 'allow',
159
+ downloadPath: downloadDir,
160
+ },
161
+ sessionId,
162
+ )
163
+ .catch(() => {});
164
+ });
165
+
166
+ // Track when download completes
167
+ const downloadPromise = new Promise<string>((resolve, reject) => {
168
+ let guid = '';
169
+ // C2: capture off() functions to avoid listener leaks in batch mode
170
+ const offBegin = client.on('Browser.downloadWillBegin', (params: Record<string, unknown>) => {
171
+ guid = params.guid as string;
172
+ });
173
+ let offProgress: (() => void) | undefined;
174
+ // cleanup defined before setTimeout so the timeout callback can call it
175
+ let timeout: ReturnType<typeof setTimeout>;
176
+ const cleanup = () => {
177
+ clearTimeout(timeout);
178
+ offBegin?.();
179
+ offProgress?.();
180
+ };
181
+ timeout = setTimeout(() => {
182
+ cleanup();
183
+ reject(new Error('Download timed out'));
184
+ }, ctx.flags.timeout as number);
185
+ offProgress = client.on(
186
+ 'Browser.downloadProgress',
187
+ async (params: Record<string, unknown>) => {
188
+ if (params.guid === guid && params.state === 'completed') {
189
+ cleanup();
190
+ // Find the downloaded file in downloadDir
191
+ const { readdir, rename, rmdir } = await import('node:fs/promises');
192
+ const files = await readdir(downloadDir);
193
+ if (files.length > 0) {
194
+ const src = join(downloadDir, files[0]);
195
+ await mkdir(dirname(savePath), { recursive: true });
196
+ await rename(src, savePath);
197
+ await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
198
+ resolve(savePath);
199
+ } else {
200
+ await rmdir(downloadDir).catch(() => {}); // I1: cleanup temp dir
201
+ reject(new Error('Download completed but no file found'));
202
+ }
203
+ } else if (params.guid === guid && params.state === 'canceled') {
204
+ cleanup();
205
+ reject(new Error('Download was canceled'));
206
+ }
207
+ },
208
+ );
209
+ });
210
+
211
+ // Click the element to trigger download
212
+ const objectId = await resolveElementObjectId(client, sessionId, session.refs, refOrSel);
213
+ await client.send<{ result: unknown }>(
214
+ 'Runtime.callFunctionOn',
215
+ {
216
+ functionDeclaration: 'function(){ this.click(); }',
217
+ objectId,
218
+ returnByValue: true,
219
+ },
220
+ sessionId,
221
+ );
222
+
223
+ const finalPath = await downloadPromise;
224
+ if (ctx.flags.json) print(JSON.stringify({ data: { path: finalPath } }));
225
+ else output.printSuccess(`Downloaded: ${finalPath}`);
226
+ return { success: true, data: { path: finalPath } };
227
+ },
228
+ };
229
+
230
+ export const pdfCommand: Command = {
231
+ name: 'pdf',
232
+ description: 'Save page as PDF. Usage: monomind browse pdf [path]',
233
+ options: [
234
+ { name: 'landscape', type: 'boolean', description: 'Landscape orientation', default: false },
235
+ { name: 'background', type: 'boolean', description: 'Print background', default: true },
236
+ ],
237
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
238
+ const { client, sessionId } = await ensureConnected(session.port);
239
+ const browser = await getBrowser();
240
+ const path = await browser.capturePdf(client, sessionId, {
241
+ path: ctx.args[0] as string,
242
+ landscape: ctx.flags.landscape as boolean,
243
+ printBackground: ctx.flags.background as boolean,
244
+ });
245
+ output.printSuccess(`PDF saved: ${path}`);
246
+ return { success: true, data: { path } };
247
+ },
248
+ };