@forsion/tangu-computer-use 0.5.8
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.
- package/CHANGELOG.md +223 -0
- package/LICENSE +26 -0
- package/LICENSE.upstream +21 -0
- package/README.md +169 -0
- package/UPSTREAM.md +277 -0
- package/check.mjs +369 -0
- package/icon.png +0 -0
- package/install.sh +112 -0
- package/main.js +541 -0
- package/manifest.json +47 -0
- package/native/linux/bridge-rs/Cargo.lock +1204 -0
- package/native/linux/bridge-rs/Cargo.toml +18 -0
- package/native/linux/bridge-rs/src/atspi.rs +640 -0
- package/native/linux/bridge-rs/src/error.rs +65 -0
- package/native/linux/bridge-rs/src/lib.rs +11 -0
- package/native/linux/bridge-rs/src/main.rs +1074 -0
- package/native/linux/bridge-rs/src/protocol.rs +49 -0
- package/native/linux/bridge-rs/src/state.rs +293 -0
- package/native/linux/bridge-rs/src/wayland.rs +89 -0
- package/native/linux/bridge-rs/src/x11.rs +909 -0
- package/native/linux/bridge-rs/tests/protocol_tests.rs +73 -0
- package/native/macos/agent_cursor.swift +184 -0
- package/native/macos/agent_cursor_motion.swift +252 -0
- package/native/macos/agent_cursor_tests.swift +128 -0
- package/native/macos/agent_highlight.swift +229 -0
- package/native/macos/agent_highlight_tests.swift +99 -0
- package/native/macos/bridge.swift +3852 -0
- package/native/macos/foreground_activity.swift +57 -0
- package/native/macos/foreground_activity_tests.swift +31 -0
- package/native/macos/live_stream.swift +289 -0
- package/native/windows/bridge-rs/Cargo.lock +396 -0
- package/native/windows/bridge-rs/Cargo.toml +30 -0
- package/native/windows/bridge-rs/src/capture.rs +518 -0
- package/native/windows/bridge-rs/src/error.rs +81 -0
- package/native/windows/bridge-rs/src/input.rs +501 -0
- package/native/windows/bridge-rs/src/lib.rs +16 -0
- package/native/windows/bridge-rs/src/main.rs +1683 -0
- package/native/windows/bridge-rs/src/protocol.rs +53 -0
- package/native/windows/bridge-rs/src/refs.rs +237 -0
- package/native/windows/bridge-rs/src/state.rs +55 -0
- package/native/windows/bridge-rs/src/uia.rs +1252 -0
- package/native/windows/bridge-rs/src/window.rs +718 -0
- package/native/windows/bridge-rs/tests/protocol_tests.rs +252 -0
- package/native/windows/bridge-rs/tests/refs_tests.rs +86 -0
- package/native/windows/bridge-rs/tests/state_tests.rs +33 -0
- package/package.json +74 -0
- package/prebuilt/linux/arm64/linux-bridge +0 -0
- package/prebuilt/linux/x64/linux-bridge +0 -0
- package/prebuilt/macos/arm64/bridge +0 -0
- package/prebuilt/macos/arm64/tangu-computer-use.app.json +10 -0
- package/prebuilt/macos/arm64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/macos/x64/bridge +0 -0
- package/prebuilt/macos/x64/tangu-computer-use.app.json +10 -0
- package/prebuilt/macos/x64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/windows/windows-bridge.exe +0 -0
- package/scripts/blind-click.check.mjs +128 -0
- package/scripts/build-native.mjs +302 -0
- package/scripts/build.mjs +84 -0
- package/scripts/calc-fixture.mjs +78 -0
- package/scripts/cli-exit.check.mjs +49 -0
- package/scripts/helper-path.check.mjs +82 -0
- package/scripts/helper-refresh.check.mjs +62 -0
- package/scripts/helper-signal.check.mjs +59 -0
- package/scripts/highlight.check.mjs +32 -0
- package/scripts/keychain-free-install.check.mjs +140 -0
- package/scripts/live-view.check.mjs +124 -0
- package/scripts/macos-bundle.d.mts +1 -0
- package/scripts/macos-bundle.mjs +119 -0
- package/scripts/make-signing-cert.sh +57 -0
- package/scripts/mini-foreground.check.mjs +11 -0
- package/scripts/no-foreground.check.mjs +81 -0
- package/scripts/overlay-visible.check.mjs +221 -0
- package/scripts/package-macos-app.mjs +39 -0
- package/scripts/permissions.check.mjs +199 -0
- package/scripts/platform-contract.check.mjs +30 -0
- package/scripts/setup-helper.mjs +154 -0
- package/scripts/tangu-computer-use.entitlements +10 -0
- package/scripts/verify-macos-bundles.mjs +25 -0
- package/scripts/verify-package.mjs +74 -0
- package/skills/computer-use/SKILL.md +110 -0
- package/src/foregroundNote.ts +43 -0
- package/src/helperState.ts +31 -0
- package/src/index.ts +63 -0
- package/src/onboarding.ts +198 -0
- package/src/pi-compat.ts +49 -0
- package/src/settings.ts +11 -0
- package/src/setup.ts +97 -0
- package/src/tools.ts +289 -0
- package/src/vendor/actions.ts +130 -0
- package/src/vendor/bridge.ts +2405 -0
- package/src/vendor/cdp.ts +658 -0
- package/src/vendor/config.ts +113 -0
- package/src/vendor/contract.ts +104 -0
- package/src/vendor/note.ts +195 -0
- package/src/vendor/outline.ts +651 -0
- package/src/vendor/output.ts +134 -0
- package/src/vendor/permissions.ts +111 -0
- package/src/vendor/platform/architecture.ts +23 -0
- package/src/vendor/platform/coerce.ts +16 -0
- package/src/vendor/platform/index.ts +59 -0
- package/src/vendor/platform/linux/backend.ts +186 -0
- package/src/vendor/platform/linux/helper.ts +238 -0
- package/src/vendor/platform/macos/backend.ts +131 -0
- package/src/vendor/platform/macos/browser.ts +110 -0
- package/src/vendor/platform/macos/helper-path.d.mts +9 -0
- package/src/vendor/platform/macos/helper-path.mjs +34 -0
- package/src/vendor/platform/macos/helper.ts +291 -0
- package/src/vendor/platform/macos/permissions.ts +146 -0
- package/src/vendor/platform/types.ts +222 -0
- package/src/vendor/platform/windows/backend.ts +142 -0
- package/src/vendor/platform/windows/helper.ts +140 -0
- package/src/vendor/root-selection.ts +25 -0
- package/src/vendor/runtime.ts +129 -0
- package/src/vendor/state.ts +146 -0
- package/src/vendor/view.ts +147 -0
- package/tangu-plugins/computer-use/dist/foregroundNote.js +19 -0
- package/tangu-plugins/computer-use/dist/index.js +5672 -0
- package/tangu-plugins/computer-use/tangu-plugin.json +9 -0
- package/tsconfig.json +22 -0
- package/types/tangu-agent.d.ts +142 -0
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
//! Read-only UIA (UI Automation) element extraction for Windows.
|
|
2
|
+
//!
|
|
3
|
+
//! On Windows, uses the [`windows`] crate to walk the UIA accessibility tree
|
|
4
|
+
//! of a given top-level window and extract semantic elements with their
|
|
5
|
+
//! properties. On non-Windows platforms all entry points are stubbed out
|
|
6
|
+
//! and return empty results.
|
|
7
|
+
//!
|
|
8
|
+
//! UIA element extraction for model-visible outlines and element metadata.
|
|
9
|
+
|
|
10
|
+
use serde_json::Value;
|
|
11
|
+
|
|
12
|
+
use crate::refs::RefStore;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Public interface
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/// Extract UIA accessible elements from the window identified by `hwnd`.
|
|
19
|
+
///
|
|
20
|
+
/// Returns a `Vec` of JSON objects, each with shape:
|
|
21
|
+
/// ```json
|
|
22
|
+
/// {
|
|
23
|
+
/// "ref": "@e1",
|
|
24
|
+
/// "role": "edit",
|
|
25
|
+
/// "label": "Address bar",
|
|
26
|
+
/// "automationId": "1001",
|
|
27
|
+
/// "className": "Edit",
|
|
28
|
+
/// "bounds": { "x": 0, "y": 0, "width": 100, "height": 20 },
|
|
29
|
+
/// "capabilities": { "isEnabled": true, "isOffscreen": false }
|
|
30
|
+
/// }
|
|
31
|
+
/// ```
|
|
32
|
+
///
|
|
33
|
+
/// On non-Windows this always returns an empty `Vec`.
|
|
34
|
+
pub fn extract_elements(store: &mut RefStore, hwnd: isize) -> Vec<Value> {
|
|
35
|
+
#[cfg(not(windows))]
|
|
36
|
+
{
|
|
37
|
+
let _ = store;
|
|
38
|
+
let _ = hwnd;
|
|
39
|
+
Vec::new()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[cfg(windows)]
|
|
43
|
+
{
|
|
44
|
+
match uia_extract(store, hwnd) {
|
|
45
|
+
Ok(v) => v,
|
|
46
|
+
Err(e) => {
|
|
47
|
+
eprintln!("[uia] WARN extraction skipped: {e}");
|
|
48
|
+
Vec::new()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Extract the subtree rooted at a previously observed element.
|
|
55
|
+
pub fn extract_elements_from(
|
|
56
|
+
store: &mut RefStore,
|
|
57
|
+
hwnd: isize,
|
|
58
|
+
runtime_id: &[i32],
|
|
59
|
+
automation_id: &str,
|
|
60
|
+
) -> Result<Vec<Value>, String> {
|
|
61
|
+
#[cfg(not(windows))]
|
|
62
|
+
{
|
|
63
|
+
let _ = (store, hwnd, runtime_id, automation_id);
|
|
64
|
+
Ok(Vec::new())
|
|
65
|
+
}
|
|
66
|
+
#[cfg(windows)]
|
|
67
|
+
{
|
|
68
|
+
native::uia_extract_from(store, hwnd, runtime_id, automation_id)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// UIA control type → semantic role mapping
|
|
74
|
+
//
|
|
75
|
+
// These constants and the mapping function are always compiled because
|
|
76
|
+
// they are exercised by cross-platform unit tests, but on non-Windows the
|
|
77
|
+
// compiler flags them as dead code since they are only called from the
|
|
78
|
+
// `#[cfg(windows)] native` module. We suppress the lint for that case.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
82
|
+
const UIA_WINDOW_CONTROL: u32 = 50032;
|
|
83
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
84
|
+
const UIA_PANE_CONTROL: u32 = 50033;
|
|
85
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
86
|
+
const UIA_DOCUMENT_CONTROL: u32 = 50030;
|
|
87
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
88
|
+
const UIA_EDIT_CONTROL: u32 = 50004;
|
|
89
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
90
|
+
const UIA_BUTTON_CONTROL: u32 = 50000;
|
|
91
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
92
|
+
const UIA_CHECKBOX_CONTROL: u32 = 50002;
|
|
93
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
94
|
+
const UIA_RADIOBUTTON_CONTROL: u32 = 50007;
|
|
95
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
96
|
+
const UIA_COMBOBOX_CONTROL: u32 = 50003;
|
|
97
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
98
|
+
const UIA_LIST_CONTROL: u32 = 50008;
|
|
99
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
100
|
+
const UIA_LISTITEM_CONTROL: u32 = 50009;
|
|
101
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
102
|
+
const UIA_TREE_CONTROL: u32 = 50020;
|
|
103
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
104
|
+
const UIA_TREEITEM_CONTROL: u32 = 50021;
|
|
105
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
106
|
+
const UIA_MENUITEM_CONTROL: u32 = 50010;
|
|
107
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
108
|
+
const UIA_TEXT_CONTROL: u32 = 50019;
|
|
109
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
110
|
+
const UIA_HYPERLINK_CONTROL: u32 = 50005;
|
|
111
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
112
|
+
const UIA_TAB_CONTROL: u32 = 50018;
|
|
113
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
114
|
+
const UIA_TABITEM_CONTROL: u32 = 50022;
|
|
115
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
116
|
+
const UIA_HEADER_CONTROL: u32 = 50034;
|
|
117
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
118
|
+
const UIA_HEADERITEM_CONTROL: u32 = 50035;
|
|
119
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
120
|
+
const UIA_TABLE_CONTROL: u32 = 50036;
|
|
121
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
122
|
+
const UIA_IMAGE_CONTROL: u32 = 50031;
|
|
123
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
124
|
+
const UIA_SLIDER_CONTROL: u32 = 50013;
|
|
125
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
126
|
+
const UIA_PROGRESSBAR_CONTROL: u32 = 50006;
|
|
127
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
128
|
+
const UIA_TOOLBAR_CONTROL: u32 = 50016;
|
|
129
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
130
|
+
const UIA_STATUSBAR_CONTROL: u32 = 50014;
|
|
131
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
132
|
+
const UIA_TOOLTIP_CONTROL: u32 = 50015;
|
|
133
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
134
|
+
const UIA_SCROLLBAR_CONTROL: u32 = 50011;
|
|
135
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
136
|
+
const UIA_GROUP_CONTROL: u32 = 50026;
|
|
137
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
138
|
+
const UIA_SEPARATOR_CONTROL: u32 = 50039;
|
|
139
|
+
|
|
140
|
+
/// Map a UIA control type ID to a semantic role string.
|
|
141
|
+
///
|
|
142
|
+
/// Returns `"unknown"` for unrecognised control type IDs.
|
|
143
|
+
#[cfg_attr(not(windows), allow(dead_code))]
|
|
144
|
+
fn control_type_to_role(ctrl_type: u32) -> &'static str {
|
|
145
|
+
match ctrl_type {
|
|
146
|
+
UIA_WINDOW_CONTROL => "window",
|
|
147
|
+
UIA_PANE_CONTROL => "pane",
|
|
148
|
+
UIA_DOCUMENT_CONTROL => "document",
|
|
149
|
+
UIA_EDIT_CONTROL => "edit",
|
|
150
|
+
UIA_BUTTON_CONTROL => "button",
|
|
151
|
+
UIA_CHECKBOX_CONTROL => "checkbox",
|
|
152
|
+
UIA_RADIOBUTTON_CONTROL => "radio",
|
|
153
|
+
UIA_COMBOBOX_CONTROL => "comboBox",
|
|
154
|
+
UIA_LIST_CONTROL => "list",
|
|
155
|
+
UIA_LISTITEM_CONTROL => "listItem",
|
|
156
|
+
UIA_TREE_CONTROL => "tree",
|
|
157
|
+
UIA_TREEITEM_CONTROL => "treeItem",
|
|
158
|
+
UIA_MENUITEM_CONTROL => "menuItem",
|
|
159
|
+
UIA_TEXT_CONTROL => "text",
|
|
160
|
+
UIA_HYPERLINK_CONTROL => "link",
|
|
161
|
+
UIA_TAB_CONTROL => "tab",
|
|
162
|
+
UIA_TABITEM_CONTROL => "tabItem",
|
|
163
|
+
UIA_HEADER_CONTROL => "header",
|
|
164
|
+
UIA_HEADERITEM_CONTROL => "headerItem",
|
|
165
|
+
UIA_TABLE_CONTROL => "table",
|
|
166
|
+
UIA_IMAGE_CONTROL => "image",
|
|
167
|
+
UIA_SLIDER_CONTROL => "slider",
|
|
168
|
+
UIA_PROGRESSBAR_CONTROL => "progressBar",
|
|
169
|
+
UIA_TOOLBAR_CONTROL => "toolBar",
|
|
170
|
+
UIA_STATUSBAR_CONTROL => "statusBar",
|
|
171
|
+
UIA_TOOLTIP_CONTROL => "toolTip",
|
|
172
|
+
UIA_SCROLLBAR_CONTROL => "scrollBar",
|
|
173
|
+
UIA_GROUP_CONTROL => "group",
|
|
174
|
+
UIA_SEPARATOR_CONTROL => "separator",
|
|
175
|
+
_ => "unknown",
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[derive(Clone, Debug, Default)]
|
|
180
|
+
pub struct ElementAnnotationSignals {
|
|
181
|
+
pub invoke: bool,
|
|
182
|
+
pub toggle: bool,
|
|
183
|
+
pub selection_item: bool,
|
|
184
|
+
pub expand_collapse: bool,
|
|
185
|
+
pub legacy_default_action: bool,
|
|
186
|
+
pub value: bool,
|
|
187
|
+
pub text: bool,
|
|
188
|
+
pub value_read_only: Option<bool>,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
pub fn annotation_can_press(signals: &ElementAnnotationSignals) -> bool {
|
|
192
|
+
signals.invoke
|
|
193
|
+
|| signals.toggle
|
|
194
|
+
|| signals.selection_item
|
|
195
|
+
|| signals.expand_collapse
|
|
196
|
+
|| signals.legacy_default_action
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pub fn annotation_can_set_text(signals: &ElementAnnotationSignals) -> bool {
|
|
200
|
+
signals.value && !signals.value_read_only.unwrap_or(false) || signals.text
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Windows implementation (windows crate)
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
#[cfg(windows)]
|
|
208
|
+
mod native {
|
|
209
|
+
use serde_json::{json, Value};
|
|
210
|
+
use std::collections::HashSet;
|
|
211
|
+
|
|
212
|
+
use super::{
|
|
213
|
+
annotation_can_press, annotation_can_set_text, control_type_to_role,
|
|
214
|
+
ElementAnnotationSignals,
|
|
215
|
+
};
|
|
216
|
+
use crate::refs::{NativeHandle, RefStore};
|
|
217
|
+
|
|
218
|
+
use windows::core::{BSTR, VARIANT};
|
|
219
|
+
use windows::Win32::Foundation::*;
|
|
220
|
+
use windows::Win32::System::Com::*;
|
|
221
|
+
use windows::Win32::System::Ole::{
|
|
222
|
+
SafeArrayGetElement, SafeArrayGetLBound, SafeArrayGetUBound,
|
|
223
|
+
};
|
|
224
|
+
use windows::Win32::UI::Accessibility::*;
|
|
225
|
+
|
|
226
|
+
const MAX_ELEMENTS: usize = 200;
|
|
227
|
+
const MAX_TRUNCATION_SCAN: usize = 1_000;
|
|
228
|
+
|
|
229
|
+
/// Entry point called from the public stub on cfg(windows).
|
|
230
|
+
pub fn uia_extract(store: &mut RefStore, hwnd: isize) -> Result<Vec<Value>, String> {
|
|
231
|
+
let _com = ComGuard::new()?;
|
|
232
|
+
|
|
233
|
+
let uia: IUIAutomation = unsafe {
|
|
234
|
+
CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER)
|
|
235
|
+
.map_err(|e| format!("CoCreateInstance IUIAutomation: {e}"))?
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
let root = unsafe {
|
|
239
|
+
uia.ElementFromHandle(HWND(hwnd as *mut _))
|
|
240
|
+
.map_err(|e| format!("ElementFromHandle: {e}"))?
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
extract_from_root(store, &uia, &root)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
pub fn uia_extract_from(
|
|
247
|
+
store: &mut RefStore,
|
|
248
|
+
hwnd: isize,
|
|
249
|
+
runtime_id_target: &[i32],
|
|
250
|
+
automation_id: &str,
|
|
251
|
+
) -> Result<Vec<Value>, String> {
|
|
252
|
+
let (_com, uia, root) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
253
|
+
extract_from_root(store, &uia, &root)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fn extract_from_root(
|
|
257
|
+
store: &mut RefStore,
|
|
258
|
+
uia: &IUIAutomation,
|
|
259
|
+
root: &IUIAutomationElement,
|
|
260
|
+
) -> Result<Vec<Value>, String> {
|
|
261
|
+
let condition = unsafe {
|
|
262
|
+
uia.CreateTrueCondition()
|
|
263
|
+
.map_err(|e| format!("CreateTrueCondition: {e}"))?
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
let found = unsafe {
|
|
267
|
+
root.FindAll(TreeScope_Subtree, &condition)
|
|
268
|
+
.map_err(|e| format!("FindAll: {e}"))?
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
let count = unsafe {
|
|
272
|
+
found
|
|
273
|
+
.Length()
|
|
274
|
+
.map_err(|e| format!("ElementArray.Length: {e}"))?
|
|
275
|
+
} as usize;
|
|
276
|
+
|
|
277
|
+
let limit = count.min(MAX_ELEMENTS);
|
|
278
|
+
let mut elements = Vec::with_capacity(limit);
|
|
279
|
+
let walker = unsafe {
|
|
280
|
+
uia.ControlViewWalker()
|
|
281
|
+
.map_err(|e| format!("ControlViewWalker: {e}"))?
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
for i in 0..limit {
|
|
285
|
+
let element = unsafe {
|
|
286
|
+
found
|
|
287
|
+
.GetElement(i as _)
|
|
288
|
+
.map_err(|e| format!("GetElement({i}): {e}"))?
|
|
289
|
+
};
|
|
290
|
+
let parent_runtime_id = unsafe { walker.GetParentElement(&element).ok() }
|
|
291
|
+
.and_then(|parent| runtime_id(&parent))
|
|
292
|
+
.unwrap_or_default();
|
|
293
|
+
if let Some(json_val) = element_to_json(store, &element, parent_runtime_id) {
|
|
294
|
+
elements.push(json_val);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if count > limit {
|
|
299
|
+
let retained = elements
|
|
300
|
+
.iter()
|
|
301
|
+
.filter_map(|element| element.get("runtimeId").and_then(Value::as_array))
|
|
302
|
+
.map(|runtime_id| {
|
|
303
|
+
runtime_id
|
|
304
|
+
.iter()
|
|
305
|
+
.filter_map(Value::as_i64)
|
|
306
|
+
.map(|value| value.to_string())
|
|
307
|
+
.collect::<Vec<_>>()
|
|
308
|
+
.join(".")
|
|
309
|
+
})
|
|
310
|
+
.collect::<HashSet<_>>();
|
|
311
|
+
let mut truncated = HashSet::new();
|
|
312
|
+
for i in limit..count.min(limit + MAX_TRUNCATION_SCAN) {
|
|
313
|
+
let mut candidate = unsafe { found.GetElement(i as _).ok() };
|
|
314
|
+
for _ in 0..64 {
|
|
315
|
+
let Some(element) = candidate else { break };
|
|
316
|
+
let Some(parent) = (unsafe { walker.GetParentElement(&element).ok() }) else {
|
|
317
|
+
break;
|
|
318
|
+
};
|
|
319
|
+
let key = runtime_id(&parent)
|
|
320
|
+
.unwrap_or_default()
|
|
321
|
+
.iter()
|
|
322
|
+
.map(i32::to_string)
|
|
323
|
+
.collect::<Vec<_>>()
|
|
324
|
+
.join(".");
|
|
325
|
+
if retained.contains(&key) {
|
|
326
|
+
truncated.insert(key);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
candidate = Some(parent);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// If the omitted tail is larger than the bounded ancestry scan,
|
|
333
|
+
// mark the retained extraction root as an honest coarse boundary.
|
|
334
|
+
if count > limit + MAX_TRUNCATION_SCAN {
|
|
335
|
+
let root_key = runtime_id(root)
|
|
336
|
+
.unwrap_or_default()
|
|
337
|
+
.iter()
|
|
338
|
+
.map(i32::to_string)
|
|
339
|
+
.collect::<Vec<_>>()
|
|
340
|
+
.join(".");
|
|
341
|
+
truncated.insert(root_key);
|
|
342
|
+
}
|
|
343
|
+
for element in &mut elements {
|
|
344
|
+
let key = element
|
|
345
|
+
.get("runtimeId")
|
|
346
|
+
.and_then(Value::as_array)
|
|
347
|
+
.map(|runtime_id| {
|
|
348
|
+
runtime_id
|
|
349
|
+
.iter()
|
|
350
|
+
.filter_map(Value::as_i64)
|
|
351
|
+
.map(|value| value.to_string())
|
|
352
|
+
.collect::<Vec<_>>()
|
|
353
|
+
.join(".")
|
|
354
|
+
})
|
|
355
|
+
.unwrap_or_default();
|
|
356
|
+
if truncated.contains(&key) {
|
|
357
|
+
element["truncated"] = json!(true);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
Ok(elements)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fn runtime_id(element: &IUIAutomationElement) -> Option<Vec<i32>> {
|
|
366
|
+
let array = unsafe { element.GetRuntimeId().ok()? };
|
|
367
|
+
if array.is_null() {
|
|
368
|
+
return None;
|
|
369
|
+
}
|
|
370
|
+
let lower = unsafe { SafeArrayGetLBound(array, 1).ok()? };
|
|
371
|
+
let upper = unsafe { SafeArrayGetUBound(array, 1).ok()? };
|
|
372
|
+
let mut values = Vec::with_capacity((upper - lower + 1).max(0) as usize);
|
|
373
|
+
for index in lower..=upper {
|
|
374
|
+
let mut value = 0i32;
|
|
375
|
+
unsafe {
|
|
376
|
+
SafeArrayGetElement(
|
|
377
|
+
array,
|
|
378
|
+
&index,
|
|
379
|
+
&mut value as *mut i32 as *mut core::ffi::c_void,
|
|
380
|
+
)
|
|
381
|
+
.ok()?;
|
|
382
|
+
}
|
|
383
|
+
values.push(value);
|
|
384
|
+
}
|
|
385
|
+
Some(values)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Convert a single UIA element to its JSON representation.
|
|
389
|
+
///
|
|
390
|
+
/// Returns `None` for elements that are offscreen, zero-sized, or
|
|
391
|
+
/// otherwise uninteresting.
|
|
392
|
+
fn element_to_json(
|
|
393
|
+
store: &mut RefStore,
|
|
394
|
+
element: &IUIAutomationElement,
|
|
395
|
+
parent_runtime_id: Vec<i32>,
|
|
396
|
+
) -> Option<Value> {
|
|
397
|
+
let ctrl_type = unsafe { element.CurrentControlType().ok()? };
|
|
398
|
+
let role = control_type_to_role(ctrl_type.0 as u32);
|
|
399
|
+
|
|
400
|
+
let name = unsafe { element.CurrentName().unwrap_or_default().to_string() };
|
|
401
|
+
let automation_id = unsafe {
|
|
402
|
+
element
|
|
403
|
+
.CurrentAutomationId()
|
|
404
|
+
.unwrap_or_default()
|
|
405
|
+
.to_string()
|
|
406
|
+
};
|
|
407
|
+
let runtime_id = runtime_id(element).unwrap_or_default();
|
|
408
|
+
let class_name = unsafe { element.CurrentClassName().unwrap_or_default().to_string() };
|
|
409
|
+
|
|
410
|
+
// Bounding rectangle.
|
|
411
|
+
let rect = unsafe { element.CurrentBoundingRectangle().ok()? };
|
|
412
|
+
|
|
413
|
+
// Skip invisible / offscreen elements.
|
|
414
|
+
let w = rect.right - rect.left;
|
|
415
|
+
let h = rect.bottom - rect.top;
|
|
416
|
+
if w <= 0 || h <= 0 {
|
|
417
|
+
return None;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let is_offscreen = unsafe {
|
|
421
|
+
element
|
|
422
|
+
.CurrentIsOffscreen()
|
|
423
|
+
.map(|value| value.as_bool())
|
|
424
|
+
.unwrap_or(true)
|
|
425
|
+
};
|
|
426
|
+
if is_offscreen {
|
|
427
|
+
return None;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Capabilities.
|
|
431
|
+
let is_enabled = unsafe {
|
|
432
|
+
element
|
|
433
|
+
.CurrentIsEnabled()
|
|
434
|
+
.map(|value| value.as_bool())
|
|
435
|
+
.unwrap_or(true)
|
|
436
|
+
};
|
|
437
|
+
let is_keyboard_focusable = unsafe {
|
|
438
|
+
element
|
|
439
|
+
.CurrentIsKeyboardFocusable()
|
|
440
|
+
.map(|value| value.as_bool())
|
|
441
|
+
.unwrap_or(false)
|
|
442
|
+
};
|
|
443
|
+
let is_password = unsafe {
|
|
444
|
+
element
|
|
445
|
+
.CurrentIsPassword()
|
|
446
|
+
.map(|value| value.as_bool())
|
|
447
|
+
.unwrap_or(false)
|
|
448
|
+
};
|
|
449
|
+
let value_pattern = unsafe {
|
|
450
|
+
element
|
|
451
|
+
.GetCurrentPatternAs::<IUIAutomationValuePattern>(UIA_ValuePatternId)
|
|
452
|
+
.ok()
|
|
453
|
+
};
|
|
454
|
+
let value = value_pattern
|
|
455
|
+
.as_ref()
|
|
456
|
+
.and_then(|pattern| unsafe { pattern.CurrentValue().ok() })
|
|
457
|
+
.map(|value| value.to_string())
|
|
458
|
+
.unwrap_or_default();
|
|
459
|
+
let value_read_only = value_pattern
|
|
460
|
+
.as_ref()
|
|
461
|
+
.and_then(|pattern| unsafe { pattern.CurrentIsReadOnly().ok() })
|
|
462
|
+
.map(|read_only| read_only.as_bool());
|
|
463
|
+
let signals = ElementAnnotationSignals {
|
|
464
|
+
invoke: pattern_available(element, UIA_IsInvokePatternAvailablePropertyId),
|
|
465
|
+
toggle: pattern_available(element, UIA_IsTogglePatternAvailablePropertyId),
|
|
466
|
+
selection_item: pattern_available(
|
|
467
|
+
element,
|
|
468
|
+
UIA_IsSelectionItemPatternAvailablePropertyId,
|
|
469
|
+
),
|
|
470
|
+
expand_collapse: pattern_available(
|
|
471
|
+
element,
|
|
472
|
+
UIA_IsExpandCollapsePatternAvailablePropertyId,
|
|
473
|
+
),
|
|
474
|
+
legacy_default_action: legacy_default_action_available(element),
|
|
475
|
+
value: pattern_available(element, UIA_IsValuePatternAvailablePropertyId),
|
|
476
|
+
text: pattern_available(element, UIA_IsTextPatternAvailablePropertyId),
|
|
477
|
+
value_read_only,
|
|
478
|
+
};
|
|
479
|
+
let can_press = annotation_can_press(&signals);
|
|
480
|
+
let can_set_value = annotation_can_set_text(&signals);
|
|
481
|
+
let can_scroll = pattern_available(element, UIA_IsScrollPatternAvailablePropertyId);
|
|
482
|
+
|
|
483
|
+
let eref = store.insert_element(NativeHandle::new(0));
|
|
484
|
+
|
|
485
|
+
Some(json!({
|
|
486
|
+
"ref": eref.to_string(),
|
|
487
|
+
"role": role,
|
|
488
|
+
"label": name,
|
|
489
|
+
"automationId": automation_id,
|
|
490
|
+
"runtimeId": runtime_id,
|
|
491
|
+
"parentRuntimeId": parent_runtime_id,
|
|
492
|
+
"className": class_name,
|
|
493
|
+
"value": value,
|
|
494
|
+
"isPassword": is_password,
|
|
495
|
+
"bounds": {
|
|
496
|
+
"x": rect.left,
|
|
497
|
+
"y": rect.top,
|
|
498
|
+
"width": w,
|
|
499
|
+
"height": h,
|
|
500
|
+
},
|
|
501
|
+
"capabilities": {
|
|
502
|
+
"isEnabled": is_enabled,
|
|
503
|
+
"isOffscreen": is_offscreen,
|
|
504
|
+
"isKeyboardFocusable": is_keyboard_focusable,
|
|
505
|
+
"canInvoke": signals.invoke,
|
|
506
|
+
"canPress": can_press,
|
|
507
|
+
"canSetValue": can_set_value,
|
|
508
|
+
"canScroll": can_scroll,
|
|
509
|
+
},
|
|
510
|
+
}))
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
pub fn read_live_text(
|
|
514
|
+
hwnd: isize,
|
|
515
|
+
runtime_id_target: &[i32],
|
|
516
|
+
automation_id: &str,
|
|
517
|
+
) -> Result<String, String> {
|
|
518
|
+
let (_com, _uia, element) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
519
|
+
Ok(read_text_from_element(&element))
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
pub fn live_elements(hwnd: isize) -> Result<Vec<Value>, String> {
|
|
523
|
+
let mut store = RefStore::new();
|
|
524
|
+
uia_extract(&mut store, hwnd)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
pub struct ElementSnapshot {
|
|
528
|
+
pub rect: (f64, f64, f64, f64),
|
|
529
|
+
pub runtime_id: Vec<i32>,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
pub enum PressResult {
|
|
533
|
+
Invoked,
|
|
534
|
+
Toggled(bool),
|
|
535
|
+
Selected(bool),
|
|
536
|
+
Expanded,
|
|
537
|
+
LegacyDefaultAction,
|
|
538
|
+
NoPattern,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
pub enum SetTextResult {
|
|
542
|
+
Set { value: String },
|
|
543
|
+
NoPattern,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
pub enum ScrollResult {
|
|
547
|
+
Scrolled,
|
|
548
|
+
NoPattern,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
pub fn snapshot(
|
|
552
|
+
hwnd: isize,
|
|
553
|
+
runtime_id_target: &[i32],
|
|
554
|
+
automation_id: &str,
|
|
555
|
+
) -> Result<ElementSnapshot, String> {
|
|
556
|
+
let (_com, _uia, element) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
557
|
+
let rect = unsafe {
|
|
558
|
+
element
|
|
559
|
+
.CurrentBoundingRectangle()
|
|
560
|
+
.map_err(|e| format!("CurrentBoundingRectangle: {e}"))?
|
|
561
|
+
};
|
|
562
|
+
Ok(ElementSnapshot {
|
|
563
|
+
rect: (
|
|
564
|
+
f64::from(rect.left),
|
|
565
|
+
f64::from(rect.top),
|
|
566
|
+
f64::from(rect.right - rect.left),
|
|
567
|
+
f64::from(rect.bottom - rect.top),
|
|
568
|
+
),
|
|
569
|
+
runtime_id: runtime_id(&element).unwrap_or_default(),
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
pub fn press(
|
|
574
|
+
hwnd: isize,
|
|
575
|
+
runtime_id_target: &[i32],
|
|
576
|
+
automation_id: &str,
|
|
577
|
+
) -> Result<PressResult, String> {
|
|
578
|
+
let (_com, _uia, element) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
579
|
+
if let Ok(pattern) = unsafe {
|
|
580
|
+
element.GetCurrentPatternAs::<IUIAutomationInvokePattern>(UIA_InvokePatternId)
|
|
581
|
+
} {
|
|
582
|
+
unsafe {
|
|
583
|
+
pattern.Invoke().map_err(|e| format!("Invoke: {e}"))?;
|
|
584
|
+
}
|
|
585
|
+
return Ok(PressResult::Invoked);
|
|
586
|
+
}
|
|
587
|
+
if let Ok(pattern) = unsafe {
|
|
588
|
+
element.GetCurrentPatternAs::<IUIAutomationTogglePattern>(UIA_TogglePatternId)
|
|
589
|
+
} {
|
|
590
|
+
unsafe {
|
|
591
|
+
pattern.Toggle().map_err(|e| format!("Toggle: {e}"))?;
|
|
592
|
+
}
|
|
593
|
+
let state = unsafe {
|
|
594
|
+
pattern
|
|
595
|
+
.CurrentToggleState()
|
|
596
|
+
.map(|s| s.0 != 0)
|
|
597
|
+
.unwrap_or(false)
|
|
598
|
+
};
|
|
599
|
+
return Ok(PressResult::Toggled(state));
|
|
600
|
+
}
|
|
601
|
+
if let Ok(pattern) = unsafe {
|
|
602
|
+
element.GetCurrentPatternAs::<IUIAutomationSelectionItemPattern>(
|
|
603
|
+
UIA_SelectionItemPatternId,
|
|
604
|
+
)
|
|
605
|
+
} {
|
|
606
|
+
unsafe {
|
|
607
|
+
pattern.Select().map_err(|e| format!("Select: {e}"))?;
|
|
608
|
+
}
|
|
609
|
+
let selected = unsafe {
|
|
610
|
+
pattern
|
|
611
|
+
.CurrentIsSelected()
|
|
612
|
+
.map(|b| b.as_bool())
|
|
613
|
+
.unwrap_or(false)
|
|
614
|
+
};
|
|
615
|
+
return Ok(PressResult::Selected(selected));
|
|
616
|
+
}
|
|
617
|
+
if let Ok(pattern) = unsafe {
|
|
618
|
+
element.GetCurrentPatternAs::<IUIAutomationExpandCollapsePattern>(
|
|
619
|
+
UIA_ExpandCollapsePatternId,
|
|
620
|
+
)
|
|
621
|
+
} {
|
|
622
|
+
unsafe {
|
|
623
|
+
pattern.Expand().map_err(|e| format!("Expand: {e}"))?;
|
|
624
|
+
}
|
|
625
|
+
return Ok(PressResult::Expanded);
|
|
626
|
+
}
|
|
627
|
+
if let Ok(pattern) = unsafe {
|
|
628
|
+
element.GetCurrentPatternAs::<IUIAutomationLegacyIAccessiblePattern>(
|
|
629
|
+
UIA_LegacyIAccessiblePatternId,
|
|
630
|
+
)
|
|
631
|
+
} {
|
|
632
|
+
let default_action = unsafe {
|
|
633
|
+
pattern
|
|
634
|
+
.CurrentDefaultAction()
|
|
635
|
+
.map(|s| s.to_string())
|
|
636
|
+
.unwrap_or_default()
|
|
637
|
+
};
|
|
638
|
+
if !default_action.trim().is_empty() {
|
|
639
|
+
unsafe {
|
|
640
|
+
pattern
|
|
641
|
+
.DoDefaultAction()
|
|
642
|
+
.map_err(|e| format!("DoDefaultAction: {e}"))?;
|
|
643
|
+
}
|
|
644
|
+
return Ok(PressResult::LegacyDefaultAction);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
Ok(PressResult::NoPattern)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
fn pattern_available(element: &IUIAutomationElement, property_id: UIA_PROPERTY_ID) -> bool {
|
|
651
|
+
unsafe {
|
|
652
|
+
element
|
|
653
|
+
.GetCurrentPropertyValue(property_id)
|
|
654
|
+
.ok()
|
|
655
|
+
.and_then(|value| bool::try_from(&value).ok())
|
|
656
|
+
.unwrap_or(false)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
fn legacy_default_action_available(element: &IUIAutomationElement) -> bool {
|
|
661
|
+
if !pattern_available(element, UIA_IsLegacyIAccessiblePatternAvailablePropertyId) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
unsafe {
|
|
665
|
+
element
|
|
666
|
+
.GetCurrentPatternAs::<IUIAutomationLegacyIAccessiblePattern>(
|
|
667
|
+
UIA_LegacyIAccessiblePatternId,
|
|
668
|
+
)
|
|
669
|
+
.ok()
|
|
670
|
+
.and_then(|pattern| pattern.CurrentDefaultAction().ok())
|
|
671
|
+
.map(|action| !action.to_string().trim().is_empty())
|
|
672
|
+
.unwrap_or(false)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
pub fn set_text(
|
|
677
|
+
hwnd: isize,
|
|
678
|
+
runtime_id_target: &[i32],
|
|
679
|
+
automation_id: &str,
|
|
680
|
+
text: &str,
|
|
681
|
+
) -> Result<SetTextResult, String> {
|
|
682
|
+
let (_com, _uia, element) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
683
|
+
if let Ok(pattern) =
|
|
684
|
+
unsafe { element.GetCurrentPatternAs::<IUIAutomationValuePattern>(UIA_ValuePatternId) }
|
|
685
|
+
{
|
|
686
|
+
let value = BSTR::from(text);
|
|
687
|
+
unsafe {
|
|
688
|
+
pattern
|
|
689
|
+
.SetValue(&value)
|
|
690
|
+
.map_err(|e| format!("SetValue: {e}"))?;
|
|
691
|
+
}
|
|
692
|
+
let value = unsafe {
|
|
693
|
+
pattern
|
|
694
|
+
.CurrentValue()
|
|
695
|
+
.map(|s| s.to_string())
|
|
696
|
+
.unwrap_or_default()
|
|
697
|
+
};
|
|
698
|
+
return Ok(SetTextResult::Set { value });
|
|
699
|
+
}
|
|
700
|
+
Ok(SetTextResult::NoPattern)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
pub fn focus(
|
|
704
|
+
hwnd: isize,
|
|
705
|
+
runtime_id_target: &[i32],
|
|
706
|
+
automation_id: &str,
|
|
707
|
+
) -> Result<(), String> {
|
|
708
|
+
let (_com, _uia, element) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
709
|
+
unsafe { element.SetFocus().map_err(|e| format!("SetFocus: {e}")) }
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
pub fn scroll(
|
|
713
|
+
hwnd: isize,
|
|
714
|
+
runtime_id_target: &[i32],
|
|
715
|
+
automation_id: &str,
|
|
716
|
+
x: f64,
|
|
717
|
+
y: f64,
|
|
718
|
+
) -> Result<ScrollResult, String> {
|
|
719
|
+
let (_com, _uia, element) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
720
|
+
if let Ok(pattern) = unsafe {
|
|
721
|
+
element.GetCurrentPatternAs::<IUIAutomationScrollPattern>(UIA_ScrollPatternId)
|
|
722
|
+
} {
|
|
723
|
+
let horizontal = if x > 0.0 {
|
|
724
|
+
ScrollAmount_SmallIncrement
|
|
725
|
+
} else if x < 0.0 {
|
|
726
|
+
ScrollAmount_SmallDecrement
|
|
727
|
+
} else {
|
|
728
|
+
ScrollAmount_NoAmount
|
|
729
|
+
};
|
|
730
|
+
let vertical = if y > 0.0 {
|
|
731
|
+
ScrollAmount_SmallIncrement
|
|
732
|
+
} else if y < 0.0 {
|
|
733
|
+
ScrollAmount_SmallDecrement
|
|
734
|
+
} else {
|
|
735
|
+
ScrollAmount_NoAmount
|
|
736
|
+
};
|
|
737
|
+
unsafe {
|
|
738
|
+
pattern
|
|
739
|
+
.Scroll(horizontal, vertical)
|
|
740
|
+
.map_err(|e| format!("Scroll: {e}"))?;
|
|
741
|
+
}
|
|
742
|
+
return Ok(ScrollResult::Scrolled);
|
|
743
|
+
}
|
|
744
|
+
Ok(ScrollResult::NoPattern)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
pub fn occlusion_ok(
|
|
748
|
+
hwnd: isize,
|
|
749
|
+
runtime_id_target: &[i32],
|
|
750
|
+
automation_id: &str,
|
|
751
|
+
x: f64,
|
|
752
|
+
y: f64,
|
|
753
|
+
) -> Result<bool, String> {
|
|
754
|
+
let (_com, uia, target) = resolve(hwnd, runtime_id_target, automation_id)?;
|
|
755
|
+
let hit = unsafe {
|
|
756
|
+
uia.ElementFromPoint(POINT {
|
|
757
|
+
x: x.round() as i32,
|
|
758
|
+
y: y.round() as i32,
|
|
759
|
+
})
|
|
760
|
+
.map_err(|e| format!("ElementFromPoint: {e}"))?
|
|
761
|
+
};
|
|
762
|
+
let target_id = runtime_id(&target).unwrap_or_default();
|
|
763
|
+
let hit_id = runtime_id(&hit).unwrap_or_default();
|
|
764
|
+
if !target_id.is_empty() && target_id == hit_id {
|
|
765
|
+
return Ok(true);
|
|
766
|
+
}
|
|
767
|
+
let walker = unsafe {
|
|
768
|
+
uia.ControlViewWalker()
|
|
769
|
+
.map_err(|e| format!("ControlViewWalker: {e}"))?
|
|
770
|
+
};
|
|
771
|
+
Ok(is_ancestor(&walker, &target_id, hit) || is_ancestor(&walker, &hit_id, target))
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
fn is_ancestor(
|
|
775
|
+
walker: &IUIAutomationTreeWalker,
|
|
776
|
+
ancestor_id: &[i32],
|
|
777
|
+
mut element: IUIAutomationElement,
|
|
778
|
+
) -> bool {
|
|
779
|
+
if ancestor_id.is_empty() {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
for _ in 0..64 {
|
|
783
|
+
let Some(parent) = (unsafe { walker.GetParentElement(&element).ok() }) else {
|
|
784
|
+
return false;
|
|
785
|
+
};
|
|
786
|
+
let parent_id = runtime_id(&parent).unwrap_or_default();
|
|
787
|
+
if parent_id == ancestor_id {
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
element = parent;
|
|
791
|
+
}
|
|
792
|
+
false
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
fn resolve(
|
|
796
|
+
hwnd: isize,
|
|
797
|
+
runtime_id_target: &[i32],
|
|
798
|
+
automation_id: &str,
|
|
799
|
+
) -> Result<(ComGuard, IUIAutomation, IUIAutomationElement), String> {
|
|
800
|
+
let com = ComGuard::new()?;
|
|
801
|
+
let uia: IUIAutomation = unsafe {
|
|
802
|
+
CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER)
|
|
803
|
+
.map_err(|e| format!("CoCreateInstance IUIAutomation: {e}"))?
|
|
804
|
+
};
|
|
805
|
+
let root = unsafe {
|
|
806
|
+
uia.ElementFromHandle(HWND(hwnd as *mut _))
|
|
807
|
+
.map_err(|e| format!("ElementFromHandle: {e}"))?
|
|
808
|
+
};
|
|
809
|
+
if runtime_id(&root).as_deref() == Some(runtime_id_target) {
|
|
810
|
+
return Ok((com, uia, root));
|
|
811
|
+
}
|
|
812
|
+
// UIA RuntimeId is exposed as a SAFEARRAY and is not reliably accepted by
|
|
813
|
+
// CreatePropertyCondition across providers/windows-rs VARIANT conversion,
|
|
814
|
+
// so use AutomationId as the fast server-side lookup when available and
|
|
815
|
+
// keep RuntimeId as the authoritative equality check/fallback scan.
|
|
816
|
+
if !automation_id.is_empty() {
|
|
817
|
+
let value = VARIANT::from(automation_id);
|
|
818
|
+
if let Ok(condition) =
|
|
819
|
+
unsafe { uia.CreatePropertyCondition(UIA_AutomationIdPropertyId, &value) }
|
|
820
|
+
{
|
|
821
|
+
if let Ok(element) = unsafe { root.FindFirst(TreeScope_Subtree, &condition) } {
|
|
822
|
+
if runtime_id(&element).as_deref() == Some(runtime_id_target)
|
|
823
|
+
|| runtime_id_target.is_empty()
|
|
824
|
+
{
|
|
825
|
+
return Ok((com, uia, element));
|
|
826
|
+
}
|
|
827
|
+
let candidate_id = unsafe {
|
|
828
|
+
element
|
|
829
|
+
.CurrentAutomationId()
|
|
830
|
+
.unwrap_or_default()
|
|
831
|
+
.to_string()
|
|
832
|
+
};
|
|
833
|
+
if candidate_id == automation_id && runtime_id_target.is_empty() {
|
|
834
|
+
return Ok((com, uia, element));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
let condition = unsafe {
|
|
841
|
+
uia.CreateTrueCondition()
|
|
842
|
+
.map_err(|e| format!("CreateTrueCondition: {e}"))?
|
|
843
|
+
};
|
|
844
|
+
let found = unsafe {
|
|
845
|
+
root.FindAll(TreeScope_Subtree, &condition)
|
|
846
|
+
.map_err(|e| format!("FindAll: {e}"))?
|
|
847
|
+
};
|
|
848
|
+
let count = unsafe {
|
|
849
|
+
found
|
|
850
|
+
.Length()
|
|
851
|
+
.map_err(|e| format!("ElementArray.Length: {e}"))?
|
|
852
|
+
};
|
|
853
|
+
let mut automation_fallback = None;
|
|
854
|
+
for i in 0..count {
|
|
855
|
+
let element = unsafe {
|
|
856
|
+
found
|
|
857
|
+
.GetElement(i)
|
|
858
|
+
.map_err(|e| format!("GetElement({i}): {e}"))?
|
|
859
|
+
};
|
|
860
|
+
if runtime_id(&element).as_deref() == Some(runtime_id_target) {
|
|
861
|
+
return Ok((com, uia, element));
|
|
862
|
+
}
|
|
863
|
+
if !automation_id.is_empty() {
|
|
864
|
+
let candidate_id = unsafe {
|
|
865
|
+
element
|
|
866
|
+
.CurrentAutomationId()
|
|
867
|
+
.unwrap_or_default()
|
|
868
|
+
.to_string()
|
|
869
|
+
};
|
|
870
|
+
if candidate_id == automation_id && automation_fallback.is_none() {
|
|
871
|
+
automation_fallback = Some(element);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if let Some(element) = automation_fallback {
|
|
876
|
+
return Ok((com, uia, element));
|
|
877
|
+
}
|
|
878
|
+
Err("Element reference is stale".to_owned())
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
fn read_text_from_element(element: &IUIAutomationElement) -> String {
|
|
882
|
+
if let Ok(pattern) =
|
|
883
|
+
unsafe { element.GetCurrentPatternAs::<IUIAutomationTextPattern>(UIA_TextPatternId) }
|
|
884
|
+
{
|
|
885
|
+
if let Ok(range) = unsafe { pattern.DocumentRange() } {
|
|
886
|
+
if let Ok(value) = unsafe { range.GetText(-1) } {
|
|
887
|
+
let text = value.to_string();
|
|
888
|
+
if !text.is_empty() {
|
|
889
|
+
return text;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if let Ok(pattern) =
|
|
895
|
+
unsafe { element.GetCurrentPatternAs::<IUIAutomationValuePattern>(UIA_ValuePatternId) }
|
|
896
|
+
{
|
|
897
|
+
if let Ok(value) = unsafe { pattern.CurrentValue() } {
|
|
898
|
+
let text = value.to_string();
|
|
899
|
+
if !text.is_empty() {
|
|
900
|
+
return text;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
unsafe { element.CurrentName().unwrap_or_default().to_string() }
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// -----------------------------------------------------------------------
|
|
908
|
+
// COM lifetime guard
|
|
909
|
+
// -----------------------------------------------------------------------
|
|
910
|
+
|
|
911
|
+
/// Calls `CoInitializeEx` on construction and `CoUninitialize` on drop.
|
|
912
|
+
struct ComGuard;
|
|
913
|
+
|
|
914
|
+
impl ComGuard {
|
|
915
|
+
fn new() -> Result<Self, String> {
|
|
916
|
+
// SAFETY: COM must be initialised for the calling thread before
|
|
917
|
+
// any UIA calls. S_OK (0) and S_FALSE (1) are both success
|
|
918
|
+
// indicators; only a negative HRESULT means failure.
|
|
919
|
+
let hr = unsafe { CoInitializeEx(Some(std::ptr::null()), COINIT_APARTMENTTHREADED) };
|
|
920
|
+
if hr.0 < 0 {
|
|
921
|
+
return Err(format!("CoInitializeEx failed: {:#010x}", hr.0));
|
|
922
|
+
}
|
|
923
|
+
Ok(Self)
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
impl Drop for ComGuard {
|
|
928
|
+
fn drop(&mut self) {
|
|
929
|
+
// SAFETY: each successful CoInitializeEx (including S_FALSE) must
|
|
930
|
+
// be balanced with a CoUninitialize.
|
|
931
|
+
unsafe {
|
|
932
|
+
CoUninitialize();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
#[cfg(windows)]
|
|
939
|
+
pub use native::{
|
|
940
|
+
focus, live_elements, occlusion_ok, press, read_live_text, scroll, set_text, snapshot,
|
|
941
|
+
uia_extract, ElementSnapshot, PressResult, ScrollResult, SetTextResult,
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
#[cfg(not(windows))]
|
|
945
|
+
#[derive(Clone, Debug)]
|
|
946
|
+
pub struct ElementSnapshot {
|
|
947
|
+
pub rect: (f64, f64, f64, f64),
|
|
948
|
+
pub runtime_id: Vec<i32>,
|
|
949
|
+
}
|
|
950
|
+
#[cfg(not(windows))]
|
|
951
|
+
pub enum PressResult {
|
|
952
|
+
Invoked,
|
|
953
|
+
Toggled(bool),
|
|
954
|
+
Selected(bool),
|
|
955
|
+
Expanded,
|
|
956
|
+
LegacyDefaultAction,
|
|
957
|
+
NoPattern,
|
|
958
|
+
}
|
|
959
|
+
#[cfg(not(windows))]
|
|
960
|
+
pub enum SetTextResult {
|
|
961
|
+
Set { value: String },
|
|
962
|
+
NoPattern,
|
|
963
|
+
}
|
|
964
|
+
#[cfg(not(windows))]
|
|
965
|
+
pub enum ScrollResult {
|
|
966
|
+
Scrolled,
|
|
967
|
+
NoPattern,
|
|
968
|
+
}
|
|
969
|
+
#[cfg(not(windows))]
|
|
970
|
+
pub fn read_live_text(
|
|
971
|
+
_hwnd: isize,
|
|
972
|
+
_runtime_id: &[i32],
|
|
973
|
+
_automation_id: &str,
|
|
974
|
+
) -> Result<String, String> {
|
|
975
|
+
Err("UIA is only supported on Windows".to_owned())
|
|
976
|
+
}
|
|
977
|
+
#[cfg(not(windows))]
|
|
978
|
+
pub fn live_elements(_hwnd: isize) -> Result<Vec<Value>, String> {
|
|
979
|
+
Ok(Vec::new())
|
|
980
|
+
}
|
|
981
|
+
#[cfg(not(windows))]
|
|
982
|
+
pub fn snapshot(
|
|
983
|
+
_hwnd: isize,
|
|
984
|
+
_runtime_id: &[i32],
|
|
985
|
+
_automation_id: &str,
|
|
986
|
+
) -> Result<ElementSnapshot, String> {
|
|
987
|
+
Err("UIA is only supported on Windows".to_owned())
|
|
988
|
+
}
|
|
989
|
+
#[cfg(not(windows))]
|
|
990
|
+
pub fn press(
|
|
991
|
+
_hwnd: isize,
|
|
992
|
+
_runtime_id: &[i32],
|
|
993
|
+
_automation_id: &str,
|
|
994
|
+
) -> Result<PressResult, String> {
|
|
995
|
+
Err("UIA is only supported on Windows".to_owned())
|
|
996
|
+
}
|
|
997
|
+
#[cfg(not(windows))]
|
|
998
|
+
pub fn set_text(
|
|
999
|
+
_hwnd: isize,
|
|
1000
|
+
_runtime_id: &[i32],
|
|
1001
|
+
_automation_id: &str,
|
|
1002
|
+
_text: &str,
|
|
1003
|
+
) -> Result<SetTextResult, String> {
|
|
1004
|
+
Err("UIA is only supported on Windows".to_owned())
|
|
1005
|
+
}
|
|
1006
|
+
#[cfg(not(windows))]
|
|
1007
|
+
pub fn focus(_hwnd: isize, _runtime_id: &[i32], _automation_id: &str) -> Result<(), String> {
|
|
1008
|
+
Err("UIA is only supported on Windows".to_owned())
|
|
1009
|
+
}
|
|
1010
|
+
#[cfg(not(windows))]
|
|
1011
|
+
pub fn scroll(
|
|
1012
|
+
_hwnd: isize,
|
|
1013
|
+
_runtime_id: &[i32],
|
|
1014
|
+
_automation_id: &str,
|
|
1015
|
+
_x: f64,
|
|
1016
|
+
_y: f64,
|
|
1017
|
+
) -> Result<ScrollResult, String> {
|
|
1018
|
+
Err("UIA is only supported on Windows".to_owned())
|
|
1019
|
+
}
|
|
1020
|
+
#[cfg(not(windows))]
|
|
1021
|
+
pub fn occlusion_ok(
|
|
1022
|
+
_hwnd: isize,
|
|
1023
|
+
_runtime_id: &[i32],
|
|
1024
|
+
_automation_id: &str,
|
|
1025
|
+
_x: f64,
|
|
1026
|
+
_y: f64,
|
|
1027
|
+
) -> Result<bool, String> {
|
|
1028
|
+
Err("UIA is only supported on Windows".to_owned())
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ---------------------------------------------------------------------------
|
|
1032
|
+
// Tests
|
|
1033
|
+
// ---------------------------------------------------------------------------
|
|
1034
|
+
|
|
1035
|
+
#[cfg(test)]
|
|
1036
|
+
mod unit_tests {
|
|
1037
|
+
use super::*;
|
|
1038
|
+
use crate::refs::NativeHandle;
|
|
1039
|
+
use serde_json::json;
|
|
1040
|
+
|
|
1041
|
+
// -- Platform support check (non-Windows) -------------------------------
|
|
1042
|
+
|
|
1043
|
+
#[test]
|
|
1044
|
+
#[cfg(not(windows))]
|
|
1045
|
+
fn test_extract_elements_empty_on_non_windows() {
|
|
1046
|
+
let mut store = RefStore::new();
|
|
1047
|
+
let result = extract_elements(&mut store, 0);
|
|
1048
|
+
assert!(result.is_empty());
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// -- Role mapping (cross-platform) --------------------------------------
|
|
1052
|
+
|
|
1053
|
+
#[test]
|
|
1054
|
+
fn test_control_type_to_role_edit() {
|
|
1055
|
+
assert_eq!(control_type_to_role(50004), "edit");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
#[test]
|
|
1059
|
+
fn test_control_type_to_role_button() {
|
|
1060
|
+
assert_eq!(control_type_to_role(50000), "button");
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
#[test]
|
|
1064
|
+
fn test_control_type_to_role_checkbox() {
|
|
1065
|
+
assert_eq!(control_type_to_role(50002), "checkbox");
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
#[test]
|
|
1069
|
+
fn test_control_type_to_role_radio() {
|
|
1070
|
+
assert_eq!(control_type_to_role(50007), "radio");
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
#[test]
|
|
1074
|
+
fn test_control_type_to_role_window() {
|
|
1075
|
+
assert_eq!(control_type_to_role(50032), "window");
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
#[test]
|
|
1079
|
+
fn test_control_type_to_role_pane() {
|
|
1080
|
+
assert_eq!(control_type_to_role(50033), "pane");
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
#[test]
|
|
1084
|
+
fn test_control_type_to_role_menu_item() {
|
|
1085
|
+
assert_eq!(control_type_to_role(50010), "menuItem");
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
#[test]
|
|
1089
|
+
fn test_control_type_to_role_list_item() {
|
|
1090
|
+
assert_eq!(control_type_to_role(50009), "listItem");
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
#[test]
|
|
1094
|
+
fn test_control_type_to_role_document() {
|
|
1095
|
+
assert_eq!(control_type_to_role(50030), "document");
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
#[test]
|
|
1099
|
+
fn test_control_type_to_role_unknown() {
|
|
1100
|
+
assert_eq!(control_type_to_role(99999), "unknown");
|
|
1101
|
+
assert_eq!(control_type_to_role(0), "unknown");
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// -- Element JSON shape (cross-platform) --------------------------------
|
|
1105
|
+
|
|
1106
|
+
#[test]
|
|
1107
|
+
fn test_element_json_shape() {
|
|
1108
|
+
let mut store = RefStore::new();
|
|
1109
|
+
let eref = store.insert_element(NativeHandle::new(0));
|
|
1110
|
+
let ref_str = eref.to_string();
|
|
1111
|
+
|
|
1112
|
+
let element = json!({
|
|
1113
|
+
"ref": ref_str,
|
|
1114
|
+
"role": "edit",
|
|
1115
|
+
"label": "Test Label",
|
|
1116
|
+
"automationId": "1001",
|
|
1117
|
+
"runtimeId": [42, 7, 9],
|
|
1118
|
+
"className": "Edit",
|
|
1119
|
+
"bounds": {
|
|
1120
|
+
"x": 10,
|
|
1121
|
+
"y": 20,
|
|
1122
|
+
"width": 100,
|
|
1123
|
+
"height": 30,
|
|
1124
|
+
},
|
|
1125
|
+
"capabilities": {
|
|
1126
|
+
"isEnabled": true,
|
|
1127
|
+
"isOffscreen": false,
|
|
1128
|
+
"isKeyboardFocusable": true,
|
|
1129
|
+
},
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
assert_eq!(element["ref"].as_str(), Some("@e1"));
|
|
1133
|
+
assert_eq!(element["role"].as_str(), Some("edit"));
|
|
1134
|
+
assert_eq!(element["label"].as_str(), Some("Test Label"));
|
|
1135
|
+
assert_eq!(element["automationId"].as_str(), Some("1001"));
|
|
1136
|
+
let runtime_id = element["runtimeId"]
|
|
1137
|
+
.as_array()
|
|
1138
|
+
.unwrap()
|
|
1139
|
+
.iter()
|
|
1140
|
+
.map(|value| value.as_i64().unwrap() as i32)
|
|
1141
|
+
.collect::<Vec<_>>();
|
|
1142
|
+
assert_eq!(runtime_id, vec![42, 7, 9]);
|
|
1143
|
+
assert_eq!(element["className"].as_str(), Some("Edit"));
|
|
1144
|
+
assert!(element["bounds"].is_object());
|
|
1145
|
+
assert_eq!(element["bounds"]["x"], 10);
|
|
1146
|
+
assert_eq!(element["bounds"]["y"], 20);
|
|
1147
|
+
assert_eq!(element["bounds"]["width"], 100);
|
|
1148
|
+
assert_eq!(element["bounds"]["height"], 30);
|
|
1149
|
+
assert!(element["capabilities"].is_object());
|
|
1150
|
+
assert_eq!(element["capabilities"]["isEnabled"], true);
|
|
1151
|
+
assert_eq!(element["capabilities"]["isOffscreen"], false);
|
|
1152
|
+
assert_eq!(element["capabilities"]["isKeyboardFocusable"], true);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#[test]
|
|
1156
|
+
fn pressability_annotation_matrix_matches_grounding_ladder() {
|
|
1157
|
+
let base = ElementAnnotationSignals::default();
|
|
1158
|
+
assert!(!annotation_can_press(&base));
|
|
1159
|
+
|
|
1160
|
+
let cases = [
|
|
1161
|
+
ElementAnnotationSignals {
|
|
1162
|
+
invoke: true,
|
|
1163
|
+
..base.clone()
|
|
1164
|
+
},
|
|
1165
|
+
ElementAnnotationSignals {
|
|
1166
|
+
toggle: true,
|
|
1167
|
+
..base.clone()
|
|
1168
|
+
},
|
|
1169
|
+
ElementAnnotationSignals {
|
|
1170
|
+
selection_item: true,
|
|
1171
|
+
..base.clone()
|
|
1172
|
+
},
|
|
1173
|
+
ElementAnnotationSignals {
|
|
1174
|
+
expand_collapse: true,
|
|
1175
|
+
..base.clone()
|
|
1176
|
+
},
|
|
1177
|
+
ElementAnnotationSignals {
|
|
1178
|
+
legacy_default_action: true,
|
|
1179
|
+
..base.clone()
|
|
1180
|
+
},
|
|
1181
|
+
];
|
|
1182
|
+
for signals in cases {
|
|
1183
|
+
assert!(annotation_can_press(&signals));
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
#[test]
|
|
1188
|
+
fn text_editable_annotation_matrix_matches_set_text_grounding() {
|
|
1189
|
+
assert!(!annotation_can_set_text(
|
|
1190
|
+
&ElementAnnotationSignals::default()
|
|
1191
|
+
));
|
|
1192
|
+
assert!(annotation_can_set_text(&ElementAnnotationSignals {
|
|
1193
|
+
value: true,
|
|
1194
|
+
value_read_only: Some(false),
|
|
1195
|
+
..Default::default()
|
|
1196
|
+
}));
|
|
1197
|
+
assert!(!annotation_can_set_text(&ElementAnnotationSignals {
|
|
1198
|
+
value: true,
|
|
1199
|
+
value_read_only: Some(true),
|
|
1200
|
+
..Default::default()
|
|
1201
|
+
}));
|
|
1202
|
+
assert!(annotation_can_set_text(&ElementAnnotationSignals {
|
|
1203
|
+
text: true,
|
|
1204
|
+
..Default::default()
|
|
1205
|
+
}));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
#[test]
|
|
1209
|
+
fn test_ax_targets_response_shape() {
|
|
1210
|
+
// Simulate the screenshot response with axTargets.
|
|
1211
|
+
let mut store = RefStore::new();
|
|
1212
|
+
let eref = store.insert_element(NativeHandle::new(0));
|
|
1213
|
+
let ref_str = eref.to_string();
|
|
1214
|
+
|
|
1215
|
+
let response = json!({
|
|
1216
|
+
"target": "@w1",
|
|
1217
|
+
"capture": {
|
|
1218
|
+
"stateId": "s-0",
|
|
1219
|
+
"width": 800,
|
|
1220
|
+
"height": 600,
|
|
1221
|
+
"imageFormat": "png",
|
|
1222
|
+
"imageBase64": "dummy",
|
|
1223
|
+
},
|
|
1224
|
+
"axTargets": [
|
|
1225
|
+
{
|
|
1226
|
+
"ref": ref_str,
|
|
1227
|
+
"role": "edit",
|
|
1228
|
+
"label": "Address bar",
|
|
1229
|
+
"automationId": "1001",
|
|
1230
|
+
"className": "Edit",
|
|
1231
|
+
"bounds": { "x": 0, "y": 0, "width": 800, "height": 30 },
|
|
1232
|
+
"capabilities": {
|
|
1233
|
+
"isEnabled": true,
|
|
1234
|
+
"isOffscreen": false,
|
|
1235
|
+
"isKeyboardFocusable": true,
|
|
1236
|
+
},
|
|
1237
|
+
}
|
|
1238
|
+
],
|
|
1239
|
+
"warnings": [],
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
assert_eq!(response["target"].as_str(), Some("@w1"));
|
|
1243
|
+
assert!(response["capture"].is_object());
|
|
1244
|
+
assert!(response["warnings"].is_array());
|
|
1245
|
+
assert!(response["axTargets"].is_array());
|
|
1246
|
+
|
|
1247
|
+
let targets = response["axTargets"].as_array().unwrap();
|
|
1248
|
+
assert_eq!(targets.len(), 1);
|
|
1249
|
+
assert_eq!(targets[0]["ref"].as_str(), Some("@e1"));
|
|
1250
|
+
assert_eq!(targets[0]["role"].as_str(), Some("edit"));
|
|
1251
|
+
}
|
|
1252
|
+
}
|