@zenalexa/unicli 0.217.0 → 0.217.3

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 (341) hide show
  1. package/AGENTS.md +7 -6
  2. package/README.md +59 -19
  3. package/README.zh-CN.md +36 -15
  4. package/crates/unicli-atspi/Cargo.toml +47 -0
  5. package/crates/unicli-atspi/README.md +6 -0
  6. package/crates/unicli-atspi/src/errors.rs +213 -0
  7. package/crates/unicli-atspi/src/input.rs +1004 -0
  8. package/crates/unicli-atspi/src/invoke.rs +1132 -0
  9. package/crates/unicli-atspi/src/main.rs +130 -0
  10. package/crates/unicli-atspi/src/refs.rs +24 -0
  11. package/crates/unicli-atspi/src/screenshot.rs +756 -0
  12. package/crates/unicli-atspi/src/tree.rs +2319 -0
  13. package/crates/unicli-shared/Cargo.toml +13 -0
  14. package/crates/unicli-shared/src/lib.rs +77 -0
  15. package/crates/unicli-uia/Cargo.toml +29 -0
  16. package/crates/unicli-uia/README.md +6 -0
  17. package/crates/unicli-uia/src/errors.rs +179 -0
  18. package/crates/unicli-uia/src/input.rs +790 -0
  19. package/crates/unicli-uia/src/invoke.rs +977 -0
  20. package/crates/unicli-uia/src/main.rs +130 -0
  21. package/crates/unicli-uia/src/refs.rs +24 -0
  22. package/crates/unicli-uia/src/screenshot.rs +685 -0
  23. package/crates/unicli-uia/src/tree.rs +2135 -0
  24. package/dist/adapters/macos/actions.d.ts +9 -0
  25. package/dist/adapters/macos/actions.d.ts.map +1 -0
  26. package/dist/adapters/macos/actions.js +55 -0
  27. package/dist/adapters/macos/actions.js.map +1 -0
  28. package/dist/browser/bridge.d.ts +2 -0
  29. package/dist/browser/bridge.d.ts.map +1 -1
  30. package/dist/browser/bridge.js +39 -0
  31. package/dist/browser/bridge.js.map +1 -1
  32. package/dist/browser/cdp-client.d.ts +2 -0
  33. package/dist/browser/cdp-client.d.ts.map +1 -1
  34. package/dist/browser/cdp-client.js +7 -0
  35. package/dist/browser/cdp-client.js.map +1 -1
  36. package/dist/browser/page.d.ts +2 -0
  37. package/dist/browser/page.d.ts.map +1 -1
  38. package/dist/browser/page.js +35 -0
  39. package/dist/browser/page.js.map +1 -1
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +13 -1
  42. package/dist/cli.js.map +1 -1
  43. package/dist/commands/approvals.d.ts +3 -0
  44. package/dist/commands/approvals.d.ts.map +1 -0
  45. package/dist/commands/approvals.js +123 -0
  46. package/dist/commands/approvals.js.map +1 -0
  47. package/dist/commands/browser-operator-runtime.d.ts.map +1 -1
  48. package/dist/commands/browser-operator-runtime.js +1 -0
  49. package/dist/commands/browser-operator-runtime.js.map +1 -1
  50. package/dist/commands/browser-operator.d.ts.map +1 -1
  51. package/dist/commands/browser-operator.js +84 -12
  52. package/dist/commands/browser-operator.js.map +1 -1
  53. package/dist/commands/compute.d.ts +3 -0
  54. package/dist/commands/compute.d.ts.map +1 -0
  55. package/dist/commands/compute.js +324 -0
  56. package/dist/commands/compute.js.map +1 -0
  57. package/dist/commands/dispatch.d.ts.map +1 -1
  58. package/dist/commands/dispatch.js +10 -4
  59. package/dist/commands/dispatch.js.map +1 -1
  60. package/dist/commands/doctor-compute.d.ts +38 -0
  61. package/dist/commands/doctor-compute.d.ts.map +1 -0
  62. package/dist/commands/doctor-compute.js +376 -0
  63. package/dist/commands/doctor-compute.js.map +1 -0
  64. package/dist/commands/lint.d.ts.map +1 -1
  65. package/dist/commands/lint.js +69 -1
  66. package/dist/commands/lint.js.map +1 -1
  67. package/dist/commands/mcp.d.ts.map +1 -1
  68. package/dist/commands/mcp.js +4 -0
  69. package/dist/commands/mcp.js.map +1 -1
  70. package/dist/commands/runs.d.ts +3 -0
  71. package/dist/commands/runs.d.ts.map +1 -0
  72. package/dist/commands/runs.js +367 -0
  73. package/dist/commands/runs.js.map +1 -0
  74. package/dist/core/envelope.d.ts +8 -0
  75. package/dist/core/envelope.d.ts.map +1 -1
  76. package/dist/core/envelope.js +1 -0
  77. package/dist/core/envelope.js.map +1 -1
  78. package/dist/discovery/aliases.d.ts.map +1 -1
  79. package/dist/discovery/aliases.js +15 -0
  80. package/dist/discovery/aliases.js.map +1 -1
  81. package/dist/discovery/loader.d.ts.map +1 -1
  82. package/dist/discovery/loader.js +9 -0
  83. package/dist/discovery/loader.js.map +1 -1
  84. package/dist/discovery/macos-dynamic.d.ts +58 -0
  85. package/dist/discovery/macos-dynamic.d.ts.map +1 -0
  86. package/dist/discovery/macos-dynamic.js +429 -0
  87. package/dist/discovery/macos-dynamic.js.map +1 -0
  88. package/dist/discovery/search.d.ts.map +1 -1
  89. package/dist/discovery/search.js +152 -3
  90. package/dist/discovery/search.js.map +1 -1
  91. package/dist/electron-apps.d.ts +1 -0
  92. package/dist/electron-apps.d.ts.map +1 -1
  93. package/dist/electron-apps.js +1 -0
  94. package/dist/electron-apps.js.map +1 -1
  95. package/dist/engine/approval-store.d.ts +43 -0
  96. package/dist/engine/approval-store.d.ts.map +1 -0
  97. package/dist/engine/approval-store.js +193 -0
  98. package/dist/engine/approval-store.js.map +1 -0
  99. package/dist/engine/browser/action-evidence.d.ts +2 -0
  100. package/dist/engine/browser/action-evidence.d.ts.map +1 -1
  101. package/dist/engine/browser/action-evidence.js +35 -1
  102. package/dist/engine/browser/action-evidence.js.map +1 -1
  103. package/dist/engine/browser/evidence.d.ts +22 -0
  104. package/dist/engine/browser/evidence.d.ts.map +1 -1
  105. package/dist/engine/browser/evidence.js +72 -0
  106. package/dist/engine/browser/evidence.js.map +1 -1
  107. package/dist/engine/browser/session-lease.d.ts +53 -0
  108. package/dist/engine/browser/session-lease.d.ts.map +1 -0
  109. package/dist/engine/browser/session-lease.js +100 -0
  110. package/dist/engine/browser/session-lease.js.map +1 -0
  111. package/dist/engine/browser/session-lock.d.ts +17 -0
  112. package/dist/engine/browser/session-lock.d.ts.map +1 -0
  113. package/dist/engine/browser/session-lock.js +114 -0
  114. package/dist/engine/browser/session-lock.js.map +1 -0
  115. package/dist/engine/browser/session-runtime.d.ts +10 -0
  116. package/dist/engine/browser/session-runtime.d.ts.map +1 -0
  117. package/dist/engine/browser/session-runtime.js +87 -0
  118. package/dist/engine/browser/session-runtime.js.map +1 -0
  119. package/dist/engine/capability-policy.d.ts +13 -2
  120. package/dist/engine/capability-policy.d.ts.map +1 -1
  121. package/dist/engine/capability-policy.js +113 -3
  122. package/dist/engine/capability-policy.js.map +1 -1
  123. package/dist/engine/executor.d.ts +8 -3
  124. package/dist/engine/executor.d.ts.map +1 -1
  125. package/dist/engine/executor.js +9 -2
  126. package/dist/engine/executor.js.map +1 -1
  127. package/dist/engine/kernel/execute.d.ts +1 -0
  128. package/dist/engine/kernel/execute.d.ts.map +1 -1
  129. package/dist/engine/kernel/execute.js +125 -3
  130. package/dist/engine/kernel/execute.js.map +1 -1
  131. package/dist/engine/kernel/types.d.ts +13 -0
  132. package/dist/engine/kernel/types.d.ts.map +1 -1
  133. package/dist/engine/operation-policy.d.ts +9 -1
  134. package/dist/engine/operation-policy.d.ts.map +1 -1
  135. package/dist/engine/operation-policy.js +6 -2
  136. package/dist/engine/operation-policy.js.map +1 -1
  137. package/dist/engine/permission-rules.d.ts +43 -0
  138. package/dist/engine/permission-rules.d.ts.map +1 -0
  139. package/dist/engine/permission-rules.js +401 -0
  140. package/dist/engine/permission-rules.js.map +1 -0
  141. package/dist/engine/permission-runtime.d.ts +11 -0
  142. package/dist/engine/permission-runtime.d.ts.map +1 -0
  143. package/dist/engine/permission-runtime.js +21 -0
  144. package/dist/engine/permission-runtime.js.map +1 -0
  145. package/dist/engine/repair/remedies.d.ts +4 -0
  146. package/dist/engine/repair/remedies.d.ts.map +1 -0
  147. package/dist/engine/repair/remedies.js +169 -0
  148. package/dist/engine/repair/remedies.js.map +1 -0
  149. package/dist/engine/runtime-resource-guard.d.ts +23 -0
  150. package/dist/engine/runtime-resource-guard.d.ts.map +1 -0
  151. package/dist/engine/runtime-resource-guard.js +85 -0
  152. package/dist/engine/runtime-resource-guard.js.map +1 -0
  153. package/dist/engine/session/args.d.ts +3 -0
  154. package/dist/engine/session/args.d.ts.map +1 -0
  155. package/dist/engine/session/args.js +17 -0
  156. package/dist/engine/session/args.js.map +1 -0
  157. package/dist/engine/session/compare.d.ts +92 -0
  158. package/dist/engine/session/compare.d.ts.map +1 -0
  159. package/dist/engine/session/compare.js +324 -0
  160. package/dist/engine/session/compare.js.map +1 -0
  161. package/dist/engine/session/environment.d.ts +4 -0
  162. package/dist/engine/session/environment.d.ts.map +1 -0
  163. package/dist/engine/session/environment.js +25 -0
  164. package/dist/engine/session/environment.js.map +1 -0
  165. package/dist/engine/session/events.d.ts +2 -0
  166. package/dist/engine/session/events.d.ts.map +1 -1
  167. package/dist/engine/session/events.js +12 -0
  168. package/dist/engine/session/events.js.map +1 -1
  169. package/dist/engine/session/query.d.ts +47 -0
  170. package/dist/engine/session/query.d.ts.map +1 -0
  171. package/dist/engine/session/query.js +299 -0
  172. package/dist/engine/session/query.js.map +1 -0
  173. package/dist/engine/session/replay.d.ts +35 -0
  174. package/dist/engine/session/replay.d.ts.map +1 -0
  175. package/dist/engine/session/replay.js +144 -0
  176. package/dist/engine/session/replay.js.map +1 -0
  177. package/dist/engine/session/run-loop.d.ts.map +1 -1
  178. package/dist/engine/session/run-loop.js +62 -23
  179. package/dist/engine/session/run-loop.js.map +1 -1
  180. package/dist/engine/session/store.d.ts +7 -0
  181. package/dist/engine/session/store.d.ts.map +1 -1
  182. package/dist/engine/session/store.js +131 -1
  183. package/dist/engine/session/store.js.map +1 -1
  184. package/dist/engine/session/types.d.ts +3 -1
  185. package/dist/engine/session/types.d.ts.map +1 -1
  186. package/dist/engine/steps/compute.d.ts +41 -0
  187. package/dist/engine/steps/compute.d.ts.map +1 -0
  188. package/dist/engine/steps/compute.js +55 -0
  189. package/dist/engine/steps/compute.js.map +1 -0
  190. package/dist/engine/steps/desktop-ax.d.ts +8 -0
  191. package/dist/engine/steps/desktop-ax.d.ts.map +1 -1
  192. package/dist/engine/steps/desktop-ax.js +16 -0
  193. package/dist/engine/steps/desktop-ax.js.map +1 -1
  194. package/dist/engine/steps/desktop-sidecar.d.ts +49 -0
  195. package/dist/engine/steps/desktop-sidecar.d.ts.map +1 -0
  196. package/dist/engine/steps/desktop-sidecar.js +50 -0
  197. package/dist/engine/steps/desktop-sidecar.js.map +1 -0
  198. package/dist/engine/steps/download.d.ts +1 -1
  199. package/dist/engine/steps/download.d.ts.map +1 -1
  200. package/dist/engine/steps/download.js +24 -2
  201. package/dist/engine/steps/download.js.map +1 -1
  202. package/dist/engine/steps/exec.d.ts +1 -1
  203. package/dist/engine/steps/exec.d.ts.map +1 -1
  204. package/dist/engine/steps/exec.js +23 -7
  205. package/dist/engine/steps/exec.js.map +1 -1
  206. package/dist/engine/steps/fetch-text.d.ts +1 -1
  207. package/dist/engine/steps/fetch-text.d.ts.map +1 -1
  208. package/dist/engine/steps/fetch-text.js +12 -4
  209. package/dist/engine/steps/fetch-text.js.map +1 -1
  210. package/dist/engine/steps/fetch.d.ts +2 -1
  211. package/dist/engine/steps/fetch.d.ts.map +1 -1
  212. package/dist/engine/steps/fetch.js +29 -6
  213. package/dist/engine/steps/fetch.js.map +1 -1
  214. package/dist/engine/steps/index.d.ts +2 -0
  215. package/dist/engine/steps/index.d.ts.map +1 -1
  216. package/dist/engine/steps/index.js +2 -0
  217. package/dist/engine/steps/index.js.map +1 -1
  218. package/dist/engine/steps/navigate.d.ts +1 -1
  219. package/dist/engine/steps/navigate.d.ts.map +1 -1
  220. package/dist/engine/steps/navigate.js +29 -2
  221. package/dist/engine/steps/navigate.js.map +1 -1
  222. package/dist/fast-path.d.ts.map +1 -1
  223. package/dist/fast-path.js +96 -12
  224. package/dist/fast-path.js.map +1 -1
  225. package/dist/manifest-compact.txt +2 -2
  226. package/dist/manifest-search.json +1 -1
  227. package/dist/manifest.json +1024 -1
  228. package/dist/mcp/handler.d.ts +2 -2
  229. package/dist/mcp/handler.d.ts.map +1 -1
  230. package/dist/mcp/handler.js +59 -5
  231. package/dist/mcp/handler.js.map +1 -1
  232. package/dist/mcp/profiles/computer-use.d.ts +4 -0
  233. package/dist/mcp/profiles/computer-use.d.ts.map +1 -0
  234. package/dist/mcp/profiles/computer-use.js +305 -0
  235. package/dist/mcp/profiles/computer-use.js.map +1 -0
  236. package/dist/mcp/server.d.ts.map +1 -1
  237. package/dist/mcp/server.js +30 -6
  238. package/dist/mcp/server.js.map +1 -1
  239. package/dist/mcp/tools.d.ts +9 -0
  240. package/dist/mcp/tools.d.ts.map +1 -1
  241. package/dist/mcp/tools.js +20 -0
  242. package/dist/mcp/tools.js.map +1 -1
  243. package/dist/output/envelope.d.ts +6 -0
  244. package/dist/output/envelope.d.ts.map +1 -1
  245. package/dist/output/envelope.js.map +1 -1
  246. package/dist/output/error-map.d.ts.map +1 -1
  247. package/dist/output/error-map.js +4 -0
  248. package/dist/output/error-map.js.map +1 -1
  249. package/dist/registry.d.ts +1 -0
  250. package/dist/registry.d.ts.map +1 -1
  251. package/dist/registry.js +5 -0
  252. package/dist/registry.js.map +1 -1
  253. package/dist/transport/adapters/cdp-browser.d.ts +38 -2
  254. package/dist/transport/adapters/cdp-browser.d.ts.map +1 -1
  255. package/dist/transport/adapters/cdp-browser.js +349 -22
  256. package/dist/transport/adapters/cdp-browser.js.map +1 -1
  257. package/dist/transport/adapters/desktop-atspi.d.ts +23 -17
  258. package/dist/transport/adapters/desktop-atspi.d.ts.map +1 -1
  259. package/dist/transport/adapters/desktop-atspi.js +143 -32
  260. package/dist/transport/adapters/desktop-atspi.js.map +1 -1
  261. package/dist/transport/adapters/desktop-ax-helpers.d.ts +24 -0
  262. package/dist/transport/adapters/desktop-ax-helpers.d.ts.map +1 -0
  263. package/dist/transport/adapters/desktop-ax-helpers.js +190 -0
  264. package/dist/transport/adapters/desktop-ax-helpers.js.map +1 -0
  265. package/dist/transport/adapters/desktop-ax-swift.d.ts +13 -0
  266. package/dist/transport/adapters/desktop-ax-swift.d.ts.map +1 -1
  267. package/dist/transport/adapters/desktop-ax-swift.js +176 -2
  268. package/dist/transport/adapters/desktop-ax-swift.js.map +1 -1
  269. package/dist/transport/adapters/desktop-ax.d.ts +11 -2
  270. package/dist/transport/adapters/desktop-ax.d.ts.map +1 -1
  271. package/dist/transport/adapters/desktop-ax.js +131 -16
  272. package/dist/transport/adapters/desktop-ax.js.map +1 -1
  273. package/dist/transport/adapters/desktop-sidecar-errors.d.ts +3 -0
  274. package/dist/transport/adapters/desktop-sidecar-errors.d.ts.map +1 -0
  275. package/dist/transport/adapters/desktop-sidecar-errors.js +34 -0
  276. package/dist/transport/adapters/desktop-sidecar-errors.js.map +1 -0
  277. package/dist/transport/adapters/desktop-sidecar-snapshot.d.ts +10 -0
  278. package/dist/transport/adapters/desktop-sidecar-snapshot.d.ts.map +1 -0
  279. package/dist/transport/adapters/desktop-sidecar-snapshot.js +89 -0
  280. package/dist/transport/adapters/desktop-sidecar-snapshot.js.map +1 -0
  281. package/dist/transport/adapters/desktop-uia.d.ts +23 -17
  282. package/dist/transport/adapters/desktop-uia.d.ts.map +1 -1
  283. package/dist/transport/adapters/desktop-uia.js +142 -32
  284. package/dist/transport/adapters/desktop-uia.js.map +1 -1
  285. package/dist/transport/adapters/subprocess.d.ts +7 -0
  286. package/dist/transport/adapters/subprocess.d.ts.map +1 -1
  287. package/dist/transport/adapters/subprocess.js +64 -0
  288. package/dist/transport/adapters/subprocess.js.map +1 -1
  289. package/dist/transport/bus.d.ts +2 -0
  290. package/dist/transport/bus.d.ts.map +1 -1
  291. package/dist/transport/bus.js +7 -11
  292. package/dist/transport/bus.js.map +1 -1
  293. package/dist/transport/capability.d.ts.map +1 -1
  294. package/dist/transport/capability.js +123 -98
  295. package/dist/transport/capability.js.map +1 -1
  296. package/dist/transport/cascade.d.ts +5 -0
  297. package/dist/transport/cascade.d.ts.map +1 -0
  298. package/dist/transport/cascade.js +550 -0
  299. package/dist/transport/cascade.js.map +1 -0
  300. package/dist/transport/cdp-session.d.ts +11 -0
  301. package/dist/transport/cdp-session.d.ts.map +1 -0
  302. package/dist/transport/cdp-session.js +52 -0
  303. package/dist/transport/cdp-session.js.map +1 -0
  304. package/dist/transport/refs.d.ts +51 -0
  305. package/dist/transport/refs.d.ts.map +1 -0
  306. package/dist/transport/refs.js +135 -0
  307. package/dist/transport/refs.js.map +1 -0
  308. package/dist/transport/sidecar-binary.d.ts +18 -0
  309. package/dist/transport/sidecar-binary.d.ts.map +1 -0
  310. package/dist/transport/sidecar-binary.js +55 -0
  311. package/dist/transport/sidecar-binary.js.map +1 -0
  312. package/dist/transport/sidecar.d.ts +35 -0
  313. package/dist/transport/sidecar.d.ts.map +1 -0
  314. package/dist/transport/sidecar.js +134 -0
  315. package/dist/transport/sidecar.js.map +1 -0
  316. package/dist/transport/snapshot-encoder.d.ts +34 -0
  317. package/dist/transport/snapshot-encoder.d.ts.map +1 -0
  318. package/dist/transport/snapshot-encoder.js +139 -0
  319. package/dist/transport/snapshot-encoder.js.map +1 -0
  320. package/dist/transport/types.d.ts +6 -1
  321. package/dist/transport/types.d.ts.map +1 -1
  322. package/dist/types.d.ts +2 -0
  323. package/dist/types.d.ts.map +1 -1
  324. package/dist/types.js.map +1 -1
  325. package/docs/mcp/clients/claude-code.md +29 -0
  326. package/docs/mcp/clients/claude-desktop.md +47 -0
  327. package/docs/mcp/clients/codex.md +29 -0
  328. package/docs/mcp/clients/cursor.md +38 -0
  329. package/docs/mcp/clients/gemini-cli.md +38 -0
  330. package/docs/operate/compute.md +172 -0
  331. package/docs/operate/electron.md +87 -0
  332. package/docs/operate/focus-behavior.md +40 -0
  333. package/docs/operate/troubleshooting.md +379 -0
  334. package/package.json +29 -4
  335. package/src/adapters/juejin/hot.test.ts +25 -0
  336. package/src/adapters/juejin/hot.yaml +52 -0
  337. package/src/adapters/juejin/search.test.ts +27 -0
  338. package/src/adapters/juejin/search.yaml +58 -0
  339. package/src/adapters/leetcode/discuss-search.test.ts +29 -0
  340. package/src/adapters/leetcode/discuss-search.yaml +56 -0
  341. package/src/adapters/macos/actions.ts +63 -0
@@ -0,0 +1,790 @@
1
+ use unicli_shared::SidecarRequest;
2
+
3
+ use crate::errors::{backend_unavailable, HandlerResult, UiaError};
4
+ use crate::invoke::focus_top_level_window;
5
+ #[cfg(target_os = "windows")]
6
+ use crate::tree::resolve_live_descendant_element;
7
+ use crate::tree::{
8
+ enumerate_top_level_windows, resolve_descendant_element_ref, resolve_top_level_window_ref,
9
+ ElementRecord, State, WindowRecord,
10
+ };
11
+
12
+ #[cfg(target_os = "windows")]
13
+ const INPUT_KEYBOARD: u32 = 1;
14
+ #[cfg(target_os = "windows")]
15
+ const INPUT_MOUSE: u32 = 0;
16
+ const KEYEVENTF_EXTENDEDKEY: u32 = 0x0001;
17
+ const KEYEVENTF_KEYUP: u32 = 0x0002;
18
+ const KEYEVENTF_UNICODE: u32 = 0x0004;
19
+ const KEYEVENTF_SCANCODE: u32 = 0x0008;
20
+ const MOUSEEVENTF_WHEEL: u32 = 0x0800;
21
+ const MOUSEEVENTF_HWHEEL: u32 = 0x1000;
22
+ const EXTENDED_SCANCODE_PREFIX: u16 = 0x0100;
23
+ const WHEEL_DELTA: i32 = 120;
24
+
25
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
26
+ struct KeyEventPlan {
27
+ scan_code: u16,
28
+ key_up: bool,
29
+ }
30
+
31
+ impl KeyEventPlan {
32
+ fn down(scan_code: u16) -> Self {
33
+ Self {
34
+ scan_code,
35
+ key_up: false,
36
+ }
37
+ }
38
+
39
+ fn up(scan_code: u16) -> Self {
40
+ Self {
41
+ scan_code,
42
+ key_up: true,
43
+ }
44
+ }
45
+ }
46
+
47
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48
+ struct KeyboardInputRecord {
49
+ scan_code: u16,
50
+ flags: u32,
51
+ }
52
+
53
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54
+ struct MouseInputRecord {
55
+ mouse_data: i32,
56
+ flags: u32,
57
+ }
58
+
59
+ pub fn handle_press(request: &SidecarRequest) -> HandlerResult {
60
+ if !cfg!(target_os = "windows") {
61
+ return Err(backend_unavailable());
62
+ }
63
+
64
+ let combo = read_combo(&request.params)?;
65
+ let plan = scancode_plan_for_combo(&combo).map_err(UiaError::invalid_input)?;
66
+ dispatch_scancode_plan(&plan)?;
67
+ Ok(serde_json::json!({
68
+ "pressed": true,
69
+ "combo": combo,
70
+ "events": plan.len()
71
+ }))
72
+ }
73
+
74
+ pub fn handle_scroll(_state: &mut State, request: &SidecarRequest) -> HandlerResult {
75
+ if !cfg!(target_os = "windows") {
76
+ return Err(backend_unavailable());
77
+ }
78
+
79
+ let stable = read_stable_ref(&request.params)?;
80
+ let direction = read_scroll_direction(&request.params);
81
+ let amount = read_scroll_amount(&request.params);
82
+ let windows = enumerate_top_level_windows()?;
83
+ if let Some((window, element, path)) = resolve_descendant_element_ref(&windows, &stable) {
84
+ focus_top_level_window(window)?;
85
+ if try_native_scroll_descendant(window, &stable, &direction, amount)? {
86
+ return Ok(scroll_response_for_descendant(
87
+ window,
88
+ element,
89
+ &stable,
90
+ &path,
91
+ &direction,
92
+ amount,
93
+ "uia_scroll_pattern",
94
+ ));
95
+ }
96
+ dispatch_mouse_input_records(&mouse_input_records_for_scroll(&direction, amount)?)?;
97
+ return Ok(scroll_response_for_descendant(
98
+ window,
99
+ element,
100
+ &stable,
101
+ &path,
102
+ &direction,
103
+ amount,
104
+ "descendant_sendinput",
105
+ ));
106
+ }
107
+ let window = resolve_top_level_window_ref(&windows, &stable)
108
+ .ok_or_else(|| UiaError::no_element(stable.clone()))?;
109
+ focus_top_level_window(window)?;
110
+ dispatch_mouse_input_records(&mouse_input_records_for_scroll(&direction, amount)?)?;
111
+ Ok(scroll_response_for_window(
112
+ window, &stable, &direction, amount,
113
+ ))
114
+ }
115
+
116
+ pub(crate) fn send_text_input(text: &str) -> HandlerResult {
117
+ dispatch_keyboard_input_records(&unicode_input_records_for_text(text))
118
+ }
119
+
120
+ fn read_combo(params: &serde_json::Value) -> Result<String, UiaError> {
121
+ params
122
+ .get("combo")
123
+ .and_then(serde_json::Value::as_str)
124
+ .map(str::trim)
125
+ .filter(|combo| !combo.is_empty())
126
+ .map(String::from)
127
+ .ok_or_else(|| UiaError::invalid_input("uia_press requires a non-empty combo"))
128
+ }
129
+
130
+ fn read_stable_ref(params: &serde_json::Value) -> Result<String, UiaError> {
131
+ params
132
+ .get("stable")
133
+ .or_else(|| params.get("ref"))
134
+ .and_then(serde_json::Value::as_str)
135
+ .filter(|value| value.starts_with("desktop-uia:"))
136
+ .map(str::to_string)
137
+ .ok_or_else(|| {
138
+ UiaError::invalid_input("uia_scroll requires a desktop-uia stable window ref")
139
+ })
140
+ }
141
+
142
+ fn read_scroll_direction(params: &serde_json::Value) -> String {
143
+ params
144
+ .get("direction")
145
+ .and_then(serde_json::Value::as_str)
146
+ .unwrap_or("down")
147
+ .to_ascii_lowercase()
148
+ }
149
+
150
+ fn read_scroll_amount(params: &serde_json::Value) -> u32 {
151
+ params
152
+ .get("amount")
153
+ .and_then(serde_json::Value::as_u64)
154
+ .and_then(|amount| u32::try_from(amount).ok())
155
+ .filter(|amount| *amount > 0)
156
+ .unwrap_or(300)
157
+ }
158
+
159
+ fn scroll_response_for_window(
160
+ window: &WindowRecord,
161
+ stable: &str,
162
+ direction: &str,
163
+ amount: u32,
164
+ ) -> serde_json::Value {
165
+ serde_json::json!({
166
+ "scrolled": true,
167
+ "via": "top_level_window_sendinput",
168
+ "stable": stable,
169
+ "hwnd": window.hwnd,
170
+ "pid": window.pid,
171
+ "title": window.title,
172
+ "direction": direction,
173
+ "amount": amount,
174
+ })
175
+ }
176
+
177
+ fn scroll_response_for_descendant(
178
+ window: &WindowRecord,
179
+ element: &ElementRecord,
180
+ stable: &str,
181
+ path: &str,
182
+ direction: &str,
183
+ amount: u32,
184
+ via: &str,
185
+ ) -> serde_json::Value {
186
+ let mut target = descendant_target_node(element, path);
187
+ if let Some(bounds) = &element.bounds {
188
+ target["bounds"] = serde_json::json!({
189
+ "x": bounds.x,
190
+ "y": bounds.y,
191
+ "width": bounds.width,
192
+ "height": bounds.height,
193
+ });
194
+ }
195
+ serde_json::json!({
196
+ "scrolled": true,
197
+ "via": via,
198
+ "stable": stable,
199
+ "hwnd": window.hwnd,
200
+ "pid": window.pid,
201
+ "title": window.title,
202
+ "target": target,
203
+ "direction": direction,
204
+ "amount": amount,
205
+ })
206
+ }
207
+
208
+ fn descendant_target_node(element: &ElementRecord, path: &str) -> serde_json::Value {
209
+ let mut target = serde_json::json!({
210
+ "role": element.role,
211
+ "name": element.name,
212
+ "path": path,
213
+ });
214
+ if let Some(value) = &element.value {
215
+ target["value"] = serde_json::json!(value);
216
+ }
217
+ target
218
+ }
219
+
220
+ fn scancode_plan_for_combo(combo: &str) -> Result<Vec<KeyEventPlan>, String> {
221
+ let mut modifier_codes = Vec::new();
222
+ let mut key_code = None;
223
+
224
+ for raw_part in combo.split('+') {
225
+ let part = raw_part.trim().to_ascii_lowercase();
226
+ let key = part.as_str();
227
+ let code = scancode_for_key(key).ok_or_else(|| format!("unsupported key {key}"))?;
228
+ if is_modifier(key) {
229
+ modifier_codes.push(code);
230
+ } else if key_code.replace(code).is_some() {
231
+ return Err("combo contains multiple non-modifier keys".into());
232
+ }
233
+ }
234
+
235
+ let key_code = key_code.ok_or_else(|| "combo must include a non-modifier key".to_string())?;
236
+ let mut plan = Vec::with_capacity((modifier_codes.len() * 2) + 2);
237
+ plan.extend(modifier_codes.iter().copied().map(KeyEventPlan::down));
238
+ plan.push(KeyEventPlan::down(key_code));
239
+ plan.push(KeyEventPlan::up(key_code));
240
+ plan.extend(modifier_codes.iter().rev().copied().map(KeyEventPlan::up));
241
+ Ok(plan)
242
+ }
243
+
244
+ fn dispatch_scancode_plan(plan: &[KeyEventPlan]) -> HandlerResult {
245
+ dispatch_keyboard_input_records(&send_input_records_for_plan(plan))
246
+ }
247
+
248
+ fn send_input_records_for_plan(plan: &[KeyEventPlan]) -> Vec<KeyboardInputRecord> {
249
+ plan.iter()
250
+ .map(|event| {
251
+ let mut flags = KEYEVENTF_SCANCODE;
252
+ if event.key_up {
253
+ flags |= KEYEVENTF_KEYUP;
254
+ }
255
+ if event.scan_code & EXTENDED_SCANCODE_PREFIX != 0 {
256
+ flags |= KEYEVENTF_EXTENDEDKEY;
257
+ }
258
+ KeyboardInputRecord {
259
+ scan_code: event.scan_code & 0x00ff,
260
+ flags,
261
+ }
262
+ })
263
+ .collect()
264
+ }
265
+
266
+ fn unicode_input_records_for_text(text: &str) -> Vec<KeyboardInputRecord> {
267
+ text.encode_utf16()
268
+ .flat_map(|unit| {
269
+ [
270
+ KeyboardInputRecord {
271
+ scan_code: unit,
272
+ flags: KEYEVENTF_UNICODE,
273
+ },
274
+ KeyboardInputRecord {
275
+ scan_code: unit,
276
+ flags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
277
+ },
278
+ ]
279
+ })
280
+ .collect()
281
+ }
282
+
283
+ fn mouse_input_records_for_scroll(
284
+ direction: &str,
285
+ amount: u32,
286
+ ) -> Result<Vec<MouseInputRecord>, UiaError> {
287
+ let delta = wheel_delta_for_amount(amount);
288
+ let record = match direction {
289
+ "up" => MouseInputRecord {
290
+ mouse_data: delta,
291
+ flags: MOUSEEVENTF_WHEEL,
292
+ },
293
+ "down" => MouseInputRecord {
294
+ mouse_data: -delta,
295
+ flags: MOUSEEVENTF_WHEEL,
296
+ },
297
+ "right" => MouseInputRecord {
298
+ mouse_data: delta,
299
+ flags: MOUSEEVENTF_HWHEEL,
300
+ },
301
+ "left" => MouseInputRecord {
302
+ mouse_data: -delta,
303
+ flags: MOUSEEVENTF_HWHEEL,
304
+ },
305
+ other => {
306
+ return Err(UiaError::invalid_input(format!(
307
+ "unsupported scroll direction {other}; expected up, down, left, or right"
308
+ )));
309
+ }
310
+ };
311
+ Ok(vec![record])
312
+ }
313
+
314
+ #[cfg(any(target_os = "windows", test))]
315
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
316
+ enum NativeScrollAmount {
317
+ LargeDecrement,
318
+ SmallDecrement,
319
+ NoAmount,
320
+ LargeIncrement,
321
+ SmallIncrement,
322
+ }
323
+
324
+ #[cfg(any(target_os = "windows", test))]
325
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
326
+ struct NativeScrollPlan {
327
+ horizontal: NativeScrollAmount,
328
+ vertical: NativeScrollAmount,
329
+ }
330
+
331
+ #[cfg(any(target_os = "windows", test))]
332
+ fn native_scroll_plan(direction: &str, amount: u32) -> Result<NativeScrollPlan, UiaError> {
333
+ let directional_amount = if amount <= WHEEL_DELTA as u32 {
334
+ NativeScrollAmount::SmallIncrement
335
+ } else {
336
+ NativeScrollAmount::LargeIncrement
337
+ };
338
+ let decrement_amount = match directional_amount {
339
+ NativeScrollAmount::SmallIncrement => NativeScrollAmount::SmallDecrement,
340
+ NativeScrollAmount::LargeIncrement => NativeScrollAmount::LargeDecrement,
341
+ _ => unreachable!("directional amount is always increment"),
342
+ };
343
+ let no_amount = NativeScrollAmount::NoAmount;
344
+ match direction {
345
+ "up" => Ok(NativeScrollPlan {
346
+ horizontal: no_amount,
347
+ vertical: decrement_amount,
348
+ }),
349
+ "down" => Ok(NativeScrollPlan {
350
+ horizontal: no_amount,
351
+ vertical: directional_amount,
352
+ }),
353
+ "left" => Ok(NativeScrollPlan {
354
+ horizontal: decrement_amount,
355
+ vertical: no_amount,
356
+ }),
357
+ "right" => Ok(NativeScrollPlan {
358
+ horizontal: directional_amount,
359
+ vertical: no_amount,
360
+ }),
361
+ other => Err(UiaError::invalid_input(format!(
362
+ "unsupported scroll direction {other}; expected up, down, left, or right"
363
+ ))),
364
+ }
365
+ }
366
+
367
+ #[cfg(target_os = "windows")]
368
+ fn try_native_scroll_descendant(
369
+ window: &WindowRecord,
370
+ stable: &str,
371
+ direction: &str,
372
+ amount: u32,
373
+ ) -> Result<bool, UiaError> {
374
+ use windows::Win32::UI::Accessibility::{IUIAutomationScrollPattern, UIA_ScrollPatternId};
375
+
376
+ let plan = native_scroll_plan(direction, amount)?;
377
+ let element = match resolve_live_descendant_element(window, stable) {
378
+ Ok(element) => element,
379
+ Err(_) => return Ok(false),
380
+ };
381
+ let pattern = match unsafe {
382
+ element.GetCurrentPatternAs::<IUIAutomationScrollPattern>(UIA_ScrollPatternId)
383
+ } {
384
+ Ok(pattern) => pattern,
385
+ Err(_) => return Ok(false),
386
+ };
387
+ Ok(unsafe {
388
+ pattern.Scroll(
389
+ windows_scroll_amount(plan.horizontal),
390
+ windows_scroll_amount(plan.vertical),
391
+ )
392
+ }
393
+ .is_ok())
394
+ }
395
+
396
+ #[cfg(not(target_os = "windows"))]
397
+ fn try_native_scroll_descendant(
398
+ _window: &WindowRecord,
399
+ _stable: &str,
400
+ _direction: &str,
401
+ _amount: u32,
402
+ ) -> Result<bool, UiaError> {
403
+ Ok(false)
404
+ }
405
+
406
+ #[cfg(target_os = "windows")]
407
+ fn windows_scroll_amount(
408
+ amount: NativeScrollAmount,
409
+ ) -> windows::Win32::UI::Accessibility::ScrollAmount {
410
+ use windows::Win32::UI::Accessibility::{
411
+ ScrollAmount_LargeDecrement, ScrollAmount_LargeIncrement, ScrollAmount_NoAmount,
412
+ ScrollAmount_SmallDecrement, ScrollAmount_SmallIncrement,
413
+ };
414
+
415
+ match amount {
416
+ NativeScrollAmount::LargeDecrement => ScrollAmount_LargeDecrement,
417
+ NativeScrollAmount::SmallDecrement => ScrollAmount_SmallDecrement,
418
+ NativeScrollAmount::NoAmount => ScrollAmount_NoAmount,
419
+ NativeScrollAmount::LargeIncrement => ScrollAmount_LargeIncrement,
420
+ NativeScrollAmount::SmallIncrement => ScrollAmount_SmallIncrement,
421
+ }
422
+ }
423
+
424
+ fn wheel_delta_for_amount(amount: u32) -> i32 {
425
+ let steps = amount.div_ceil(WHEEL_DELTA as u32).max(1);
426
+ (steps as i32) * WHEEL_DELTA
427
+ }
428
+
429
+ #[cfg(target_os = "windows")]
430
+ fn dispatch_keyboard_input_records(records: &[KeyboardInputRecord]) -> HandlerResult {
431
+ use std::mem::size_of;
432
+
433
+ let inputs: Vec<win32::Input> = records
434
+ .iter()
435
+ .map(|record| win32::Input {
436
+ input_type: INPUT_KEYBOARD,
437
+ input: win32::InputUnion {
438
+ keyboard: win32::KeyboardInput {
439
+ virtual_key: 0,
440
+ scan_code: record.scan_code,
441
+ flags: record.flags,
442
+ time: 0,
443
+ extra_info: 0,
444
+ },
445
+ },
446
+ })
447
+ .collect();
448
+
449
+ let inserted = unsafe {
450
+ win32::SendInput(
451
+ inputs.len() as u32,
452
+ inputs.as_ptr(),
453
+ size_of::<win32::Input>() as i32,
454
+ )
455
+ };
456
+ if inserted == inputs.len() as u32 {
457
+ return Ok(serde_json::json!({
458
+ "events": inserted
459
+ }));
460
+ }
461
+
462
+ Err(UiaError::permission(format!(
463
+ "Windows SendInput inserted {inserted} of {} keyboard events: {}",
464
+ inputs.len(),
465
+ std::io::Error::last_os_error()
466
+ )))
467
+ }
468
+
469
+ #[cfg(target_os = "windows")]
470
+ fn dispatch_mouse_input_records(records: &[MouseInputRecord]) -> HandlerResult {
471
+ use std::mem::size_of;
472
+
473
+ let inputs: Vec<win32::Input> = records
474
+ .iter()
475
+ .map(|record| win32::Input {
476
+ input_type: INPUT_MOUSE,
477
+ input: win32::InputUnion {
478
+ mouse: win32::MouseInput {
479
+ dx: 0,
480
+ dy: 0,
481
+ mouse_data: record.mouse_data as u32,
482
+ flags: record.flags,
483
+ time: 0,
484
+ extra_info: 0,
485
+ },
486
+ },
487
+ })
488
+ .collect();
489
+
490
+ let inserted = unsafe {
491
+ win32::SendInput(
492
+ inputs.len() as u32,
493
+ inputs.as_ptr(),
494
+ size_of::<win32::Input>() as i32,
495
+ )
496
+ };
497
+ if inserted == inputs.len() as u32 {
498
+ return Ok(serde_json::json!({
499
+ "events": inserted
500
+ }));
501
+ }
502
+
503
+ Err(UiaError::permission(format!(
504
+ "Windows SendInput inserted {inserted} of {} mouse events: {}",
505
+ inputs.len(),
506
+ std::io::Error::last_os_error()
507
+ )))
508
+ }
509
+
510
+ #[cfg(not(target_os = "windows"))]
511
+ fn dispatch_mouse_input_records(_records: &[MouseInputRecord]) -> HandlerResult {
512
+ Err(backend_unavailable())
513
+ }
514
+
515
+ #[cfg(not(target_os = "windows"))]
516
+ fn dispatch_keyboard_input_records(_records: &[KeyboardInputRecord]) -> HandlerResult {
517
+ Err(backend_unavailable())
518
+ }
519
+
520
+ fn is_modifier(key: &str) -> bool {
521
+ matches!(
522
+ key,
523
+ "ctrl" | "control" | "shift" | "alt" | "option" | "cmd" | "command" | "win" | "windows"
524
+ )
525
+ }
526
+
527
+ fn scancode_for_key(key: &str) -> Option<u16> {
528
+ Some(match key {
529
+ "ctrl" | "control" => 0x01d,
530
+ "shift" => 0x02a,
531
+ "alt" | "option" => 0x038,
532
+ "cmd" | "command" | "win" | "windows" => 0x15b,
533
+ "esc" | "escape" => 0x001,
534
+ "1" => 0x002,
535
+ "2" => 0x003,
536
+ "3" => 0x004,
537
+ "4" => 0x005,
538
+ "5" => 0x006,
539
+ "6" => 0x007,
540
+ "7" => 0x008,
541
+ "8" => 0x009,
542
+ "9" => 0x00a,
543
+ "0" => 0x00b,
544
+ "backspace" => 0x00e,
545
+ "tab" => 0x00f,
546
+ "q" => 0x010,
547
+ "w" => 0x011,
548
+ "e" => 0x012,
549
+ "r" => 0x013,
550
+ "t" => 0x014,
551
+ "y" => 0x015,
552
+ "u" => 0x016,
553
+ "i" => 0x017,
554
+ "o" => 0x018,
555
+ "p" => 0x019,
556
+ "enter" | "return" => 0x01c,
557
+ "a" => 0x01e,
558
+ "s" => 0x01f,
559
+ "d" => 0x020,
560
+ "f" => 0x021,
561
+ "g" => 0x022,
562
+ "h" => 0x023,
563
+ "j" => 0x024,
564
+ "k" => 0x025,
565
+ "l" => 0x026,
566
+ "z" => 0x02c,
567
+ "x" => 0x02d,
568
+ "c" => 0x02e,
569
+ "v" => 0x02f,
570
+ "b" => 0x030,
571
+ "n" => 0x031,
572
+ "m" => 0x032,
573
+ "space" => 0x039,
574
+ "delete" => 0x153,
575
+ _ => return None,
576
+ })
577
+ }
578
+
579
+ #[cfg(target_os = "windows")]
580
+ mod win32 {
581
+ #[repr(C)]
582
+ pub struct Input {
583
+ pub input_type: u32,
584
+ pub input: InputUnion,
585
+ }
586
+
587
+ #[repr(C)]
588
+ pub union InputUnion {
589
+ pub keyboard: KeyboardInput,
590
+ pub mouse: MouseInput,
591
+ }
592
+
593
+ #[derive(Clone, Copy)]
594
+ #[repr(C)]
595
+ pub struct KeyboardInput {
596
+ pub virtual_key: u16,
597
+ pub scan_code: u16,
598
+ pub flags: u32,
599
+ pub time: u32,
600
+ pub extra_info: usize,
601
+ }
602
+
603
+ #[derive(Clone, Copy)]
604
+ #[repr(C)]
605
+ pub struct MouseInput {
606
+ pub dx: i32,
607
+ pub dy: i32,
608
+ pub mouse_data: u32,
609
+ pub flags: u32,
610
+ pub time: u32,
611
+ pub extra_info: usize,
612
+ }
613
+
614
+ #[link(name = "user32")]
615
+ extern "system" {
616
+ pub fn SendInput(input_count: u32, inputs: *const Input, input_size: i32) -> u32;
617
+ }
618
+ }
619
+
620
+ #[cfg(test)]
621
+ mod tests {
622
+ use super::*;
623
+
624
+ #[test]
625
+ fn combo_plan_presses_modifiers_then_key_and_releases_in_reverse() {
626
+ let plan = scancode_plan_for_combo("ctrl+shift+p").expect("combo plan");
627
+
628
+ assert_eq!(
629
+ plan,
630
+ vec![
631
+ KeyEventPlan::down(0x01d),
632
+ KeyEventPlan::down(0x02a),
633
+ KeyEventPlan::down(0x019),
634
+ KeyEventPlan::up(0x019),
635
+ KeyEventPlan::up(0x02a),
636
+ KeyEventPlan::up(0x01d),
637
+ ],
638
+ );
639
+ }
640
+
641
+ #[test]
642
+ fn combo_plan_supports_named_single_keys() {
643
+ let plan = scancode_plan_for_combo("enter").expect("enter plan");
644
+
645
+ assert_eq!(
646
+ plan,
647
+ vec![KeyEventPlan::down(0x01c), KeyEventPlan::up(0x01c)],
648
+ );
649
+ }
650
+
651
+ #[test]
652
+ fn combo_plan_rejects_unknown_keys() {
653
+ let error = scancode_plan_for_combo("ctrl+hyper+p").expect_err("unknown key");
654
+
655
+ assert!(error.contains("hyper"));
656
+ }
657
+
658
+ #[test]
659
+ fn combo_plan_rejects_multiple_non_modifier_keys() {
660
+ let error = scancode_plan_for_combo("ctrl+p+s").expect_err("ambiguous combo");
661
+
662
+ assert!(error.contains("multiple"));
663
+ }
664
+
665
+ #[test]
666
+ fn send_input_records_use_scancode_flags_and_extended_key_prefix() {
667
+ let records =
668
+ send_input_records_for_plan(&[KeyEventPlan::down(0x153), KeyEventPlan::up(0x153)]);
669
+
670
+ assert_eq!(
671
+ records,
672
+ vec![
673
+ KeyboardInputRecord {
674
+ scan_code: 0x053,
675
+ flags: KEYEVENTF_SCANCODE | KEYEVENTF_EXTENDEDKEY,
676
+ },
677
+ KeyboardInputRecord {
678
+ scan_code: 0x053,
679
+ flags: KEYEVENTF_SCANCODE | KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP,
680
+ },
681
+ ],
682
+ );
683
+ }
684
+
685
+ #[test]
686
+ fn unicode_input_records_use_utf16_units_and_keyup_pairs() {
687
+ let records = unicode_input_records_for_text("A\u{1f642}");
688
+
689
+ assert_eq!(
690
+ records,
691
+ vec![
692
+ KeyboardInputRecord {
693
+ scan_code: 0x0041,
694
+ flags: KEYEVENTF_UNICODE,
695
+ },
696
+ KeyboardInputRecord {
697
+ scan_code: 0x0041,
698
+ flags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
699
+ },
700
+ KeyboardInputRecord {
701
+ scan_code: 0xd83d,
702
+ flags: KEYEVENTF_UNICODE,
703
+ },
704
+ KeyboardInputRecord {
705
+ scan_code: 0xd83d,
706
+ flags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
707
+ },
708
+ KeyboardInputRecord {
709
+ scan_code: 0xde42,
710
+ flags: KEYEVENTF_UNICODE,
711
+ },
712
+ KeyboardInputRecord {
713
+ scan_code: 0xde42,
714
+ flags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
715
+ },
716
+ ],
717
+ );
718
+ }
719
+
720
+ #[test]
721
+ fn scroll_input_records_use_wheel_axis_direction_and_amount() {
722
+ let down = mouse_input_records_for_scroll("down", 300).expect("down scroll");
723
+ let left = mouse_input_records_for_scroll("left", 120).expect("left scroll");
724
+
725
+ assert_eq!(
726
+ down,
727
+ vec![MouseInputRecord {
728
+ mouse_data: -360,
729
+ flags: MOUSEEVENTF_WHEEL,
730
+ }],
731
+ );
732
+ assert_eq!(
733
+ left,
734
+ vec![MouseInputRecord {
735
+ mouse_data: -120,
736
+ flags: MOUSEEVENTF_HWHEEL,
737
+ }],
738
+ );
739
+ }
740
+
741
+ #[test]
742
+ fn native_scroll_plan_maps_direction_and_amount_to_uia_scroll_amounts() {
743
+ let down = native_scroll_plan("down", 300).expect("down native scroll");
744
+ let right = native_scroll_plan("right", 120).expect("right native scroll");
745
+
746
+ assert_eq!(
747
+ down,
748
+ NativeScrollPlan {
749
+ horizontal: NativeScrollAmount::NoAmount,
750
+ vertical: NativeScrollAmount::LargeIncrement,
751
+ },
752
+ );
753
+ assert_eq!(
754
+ right,
755
+ NativeScrollPlan {
756
+ horizontal: NativeScrollAmount::SmallIncrement,
757
+ vertical: NativeScrollAmount::NoAmount,
758
+ },
759
+ );
760
+ }
761
+
762
+ #[test]
763
+ fn scroll_response_can_report_native_descendant_scroll_pattern() {
764
+ let response = scroll_response_for_descendant(
765
+ &WindowRecord {
766
+ hwnd: "0x2a".into(),
767
+ pid: 42,
768
+ title: "Settings".into(),
769
+ children: vec![],
770
+ },
771
+ &crate::tree::ElementRecord {
772
+ role: "Pane".into(),
773
+ name: "Advanced".into(),
774
+ value: None,
775
+ bounds: None,
776
+ states: vec!["enabled".into()],
777
+ children: vec![],
778
+ },
779
+ "desktop-uia:pid-42:Window[0]/Pane[0]",
780
+ "Window[0]/Pane[0]",
781
+ "down",
782
+ 300,
783
+ "uia_scroll_pattern",
784
+ );
785
+
786
+ assert_eq!(response["scrolled"], true);
787
+ assert_eq!(response["via"], "uia_scroll_pattern");
788
+ assert_eq!(response["target"]["role"], "Pane");
789
+ }
790
+ }