@elizaos/computeruse 0.24.20
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/Cargo.toml +34 -0
- package/build.rs +10 -0
- package/computeruse.darwin-arm64.node +0 -0
- package/index.d.ts +0 -0
- package/index.js +327 -0
- package/package.json +74 -0
- package/scripts/sync-version.js +60 -0
- package/src/desktop.rs +2763 -0
- package/src/element.rs +1341 -0
- package/src/exceptions.rs +65 -0
- package/src/lib.rs +26 -0
- package/src/locator.rs +172 -0
- package/src/selector.rs +158 -0
- package/src/types.rs +963 -0
- package/src/window_manager.rs +342 -0
- package/tests/comprehensive-ui-elements.test.js +524 -0
- package/tests/cross-app-verification.test.js +243 -0
- package/tests/desktop-verify.test.js +169 -0
- package/tests/element-chaining.test.js +158 -0
- package/tests/element-range.test.js +207 -0
- package/tests/element-scroll-into-view.test.js +256 -0
- package/tests/element-value.test.js +264 -0
- package/tests/execute-browser-script-wrapper.test.js +135 -0
- package/tests/fixtures/sample-browser-script.js +7 -0
- package/tests/fixtures/script-with-env.js +16 -0
- package/tests/locator-validate.test.js +260 -0
- package/tests/locator-waitfor.test.js +286 -0
- package/wrapper.d.ts +84 -0
- package/wrapper.js +344 -0
- package/wrapper.ts +394 -0
package/src/element.rs
ADDED
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
use napi::bindgen_prelude::FromNapiValue;
|
|
2
|
+
use napi::{self};
|
|
3
|
+
use napi_derive::napi;
|
|
4
|
+
use computeruse::{
|
|
5
|
+
UIElement as ComputerUseUIElement, UIElementAttributes as ComputerUseUIElementAttributes,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
use crate::{
|
|
9
|
+
map_error, ActionResult, Bounds, ClickResult, ClickType, FontStyle, HighlightHandle, Locator,
|
|
10
|
+
ScreenshotResult, TextPosition, UIElementAttributes,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
use crate::Selector;
|
|
14
|
+
use napi::bindgen_prelude::Either;
|
|
15
|
+
|
|
16
|
+
/// Normalize key format to ensure curly brace syntax for special keys.
|
|
17
|
+
/// If key already contains `{`, assume it's correctly formatted.
|
|
18
|
+
/// Otherwise, wrap the entire key in `{}` to ensure it's treated as a special key press.
|
|
19
|
+
fn normalize_key(key: &str) -> String {
|
|
20
|
+
if key.contains('{') {
|
|
21
|
+
key.to_string()
|
|
22
|
+
} else {
|
|
23
|
+
format!("{{{}}}", key)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Click position within element bounds as percentages (0-100)
|
|
28
|
+
#[napi(object)]
|
|
29
|
+
#[derive(Default, Clone)]
|
|
30
|
+
pub struct ClickPosition {
|
|
31
|
+
/// X position as percentage from left edge (0-100). 50 = center.
|
|
32
|
+
pub x_percentage: u8,
|
|
33
|
+
/// Y position as percentage from top edge (0-100). 50 = center.
|
|
34
|
+
pub y_percentage: u8,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Options for action methods (click, pressKey, scroll, etc.)
|
|
38
|
+
#[napi(object)]
|
|
39
|
+
#[derive(Default)]
|
|
40
|
+
pub struct ActionOptions {
|
|
41
|
+
/// Whether to highlight the element before performing the action. Defaults to false.
|
|
42
|
+
pub highlight_before_action: Option<bool>,
|
|
43
|
+
/// Whether to capture window screenshot after action. Defaults to true.
|
|
44
|
+
pub include_window_screenshot: Option<bool>,
|
|
45
|
+
/// Whether to capture monitor screenshots after action. Defaults to false.
|
|
46
|
+
pub include_monitor_screenshots: Option<bool>,
|
|
47
|
+
/// Whether to try focusing the element before the action. Defaults to true.
|
|
48
|
+
pub try_focus_before: Option<bool>,
|
|
49
|
+
/// Whether to try clicking the element if focus fails. Defaults to true.
|
|
50
|
+
pub try_click_before: Option<bool>,
|
|
51
|
+
/// Whether to capture UI tree before/after action and compute diff. Defaults to false.
|
|
52
|
+
pub ui_diff_before_after: Option<bool>,
|
|
53
|
+
/// Max depth for tree capture when doing UI diff.
|
|
54
|
+
pub ui_diff_max_depth: Option<u32>,
|
|
55
|
+
/// Click position within element bounds. If not specified, clicks at center.
|
|
56
|
+
pub click_position: Option<ClickPosition>,
|
|
57
|
+
/// Type of click: 'Left', 'Double', or 'Right'. Defaults to 'Left'.
|
|
58
|
+
pub click_type: Option<ClickType>,
|
|
59
|
+
/// Whether to restore cursor to original position after click. Defaults to false.
|
|
60
|
+
pub restore_cursor: Option<bool>,
|
|
61
|
+
/// Whether to restore the original focus and caret position after the action. Defaults to false.
|
|
62
|
+
/// When true, saves the currently focused element and caret position before the action, then restores them after.
|
|
63
|
+
pub restore_focus: Option<bool>,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Options for typeText method
|
|
67
|
+
#[napi(object)]
|
|
68
|
+
#[derive(Default)]
|
|
69
|
+
pub struct TypeTextOptions {
|
|
70
|
+
/// REQUIRED: Whether to clear existing text before typing.
|
|
71
|
+
/// Set to true to clear the field first, false to append.
|
|
72
|
+
pub clear_before_typing: bool,
|
|
73
|
+
/// Whether to use clipboard for pasting. Defaults to false.
|
|
74
|
+
pub use_clipboard: Option<bool>,
|
|
75
|
+
/// Whether to highlight the element before typing. Defaults to false.
|
|
76
|
+
pub highlight_before_action: Option<bool>,
|
|
77
|
+
/// Whether to capture window screenshot after action. Defaults to true.
|
|
78
|
+
pub include_window_screenshot: Option<bool>,
|
|
79
|
+
/// Whether to capture monitor screenshots after action. Defaults to false.
|
|
80
|
+
pub include_monitor_screenshots: Option<bool>,
|
|
81
|
+
/// Whether to try focusing the element before typing. Defaults to true.
|
|
82
|
+
pub try_focus_before: Option<bool>,
|
|
83
|
+
/// Whether to try clicking the element if focus fails. Defaults to true.
|
|
84
|
+
pub try_click_before: Option<bool>,
|
|
85
|
+
/// Whether to restore the original focus and caret position after typing. Defaults to false.
|
|
86
|
+
/// When true, saves the currently focused element and caret position before typing, then restores them after.
|
|
87
|
+
pub restore_focus: Option<bool>,
|
|
88
|
+
/// Whether to capture UI tree before/after action and compute diff. Defaults to false.
|
|
89
|
+
pub ui_diff_before_after: Option<bool>,
|
|
90
|
+
/// Max depth for tree capture when doing UI diff.
|
|
91
|
+
pub ui_diff_max_depth: Option<u32>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Result of screenshot capture for Element methods
|
|
95
|
+
#[derive(Default)]
|
|
96
|
+
struct ElementScreenshotPaths {
|
|
97
|
+
window_path: Option<String>,
|
|
98
|
+
monitor_paths: Option<Vec<String>>,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Helper to capture and save screenshots for Element methods
|
|
102
|
+
fn capture_element_screenshots(
|
|
103
|
+
element: &ComputerUseUIElement,
|
|
104
|
+
include_window: bool,
|
|
105
|
+
include_monitors: bool,
|
|
106
|
+
operation: &str,
|
|
107
|
+
) -> ElementScreenshotPaths {
|
|
108
|
+
let mut result = ElementScreenshotPaths::default();
|
|
109
|
+
|
|
110
|
+
if !include_window && !include_monitors {
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
computeruse::screenshot_logger::init();
|
|
115
|
+
let prefix = computeruse::screenshot_logger::generate_prefix(None, operation);
|
|
116
|
+
|
|
117
|
+
if include_window {
|
|
118
|
+
// Capture via element's application
|
|
119
|
+
if let Ok(Some(app)) = element.application() {
|
|
120
|
+
if let Ok(screenshot) = app.capture() {
|
|
121
|
+
if let Some(saved) = computeruse::screenshot_logger::save_window_screenshot(
|
|
122
|
+
&screenshot,
|
|
123
|
+
&prefix,
|
|
124
|
+
None,
|
|
125
|
+
) {
|
|
126
|
+
result.window_path = Some(saved.path.to_string_lossy().to_string());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if include_monitors {
|
|
133
|
+
// Create temporary desktop for monitor capture
|
|
134
|
+
if let Ok(temp_desktop) = computeruse::Desktop::new(false, false) {
|
|
135
|
+
if let Ok(monitors) = futures::executor::block_on(temp_desktop.capture_all_monitors()) {
|
|
136
|
+
let saved = computeruse::screenshot_logger::save_monitor_screenshots(
|
|
137
|
+
&monitors, &prefix, None,
|
|
138
|
+
);
|
|
139
|
+
if !saved.is_empty() {
|
|
140
|
+
result.monitor_paths = Some(
|
|
141
|
+
saved
|
|
142
|
+
.into_iter()
|
|
143
|
+
.map(|s| s.path.to_string_lossy().to_string())
|
|
144
|
+
.collect(),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
result
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// A UI element in the accessibility tree.
|
|
155
|
+
#[napi(js_name = "Element")]
|
|
156
|
+
pub struct Element {
|
|
157
|
+
pub(crate) inner: ComputerUseUIElement,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
impl From<ComputerUseUIElement> for Element {
|
|
161
|
+
fn from(e: ComputerUseUIElement) -> Self {
|
|
162
|
+
Element { inner: e }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
impl FromNapiValue for Element {
|
|
167
|
+
unsafe fn from_napi_value(
|
|
168
|
+
env: napi::sys::napi_env,
|
|
169
|
+
napi_val: napi::sys::napi_value,
|
|
170
|
+
) -> napi::Result<Self> {
|
|
171
|
+
let mut result = std::ptr::null_mut();
|
|
172
|
+
let status = napi::sys::napi_get_value_external(env, napi_val, &mut result);
|
|
173
|
+
if status != napi::sys::Status::napi_ok {
|
|
174
|
+
return Err(napi::Error::new(
|
|
175
|
+
napi::Status::InvalidArg,
|
|
176
|
+
"Failed to get external value",
|
|
177
|
+
));
|
|
178
|
+
}
|
|
179
|
+
Ok(std::ptr::read(result as *const Element))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[napi]
|
|
184
|
+
impl Element {
|
|
185
|
+
/// Get the element's ID.
|
|
186
|
+
///
|
|
187
|
+
/// @returns {string | null} The element's ID, if available.
|
|
188
|
+
#[napi]
|
|
189
|
+
pub fn id(&self) -> Option<String> {
|
|
190
|
+
self.inner.id()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Get the element's role.
|
|
194
|
+
///
|
|
195
|
+
/// @returns {string} The element's role (e.g., "button", "textfield").
|
|
196
|
+
#[napi]
|
|
197
|
+
pub fn role(&self) -> napi::Result<String> {
|
|
198
|
+
Ok(self.inner.role())
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Get all attributes of the element.
|
|
202
|
+
///
|
|
203
|
+
/// @returns {UIElementAttributes} The element's attributes.
|
|
204
|
+
#[napi]
|
|
205
|
+
pub fn attributes(&self) -> UIElementAttributes {
|
|
206
|
+
let attrs: ComputerUseUIElementAttributes = self.inner.attributes();
|
|
207
|
+
UIElementAttributes {
|
|
208
|
+
role: attrs.role,
|
|
209
|
+
name: attrs.name,
|
|
210
|
+
label: attrs.label,
|
|
211
|
+
value: attrs.value,
|
|
212
|
+
description: attrs.description,
|
|
213
|
+
properties: attrs
|
|
214
|
+
.properties
|
|
215
|
+
.into_iter()
|
|
216
|
+
.map(|(k, v)| (k, v.map(|v| v.to_string())))
|
|
217
|
+
.collect(),
|
|
218
|
+
is_keyboard_focusable: attrs.is_keyboard_focusable,
|
|
219
|
+
bounds: attrs.bounds.map(|(x, y, width, height)| Bounds {
|
|
220
|
+
x,
|
|
221
|
+
y,
|
|
222
|
+
width,
|
|
223
|
+
height,
|
|
224
|
+
}),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Get the element's name.
|
|
229
|
+
///
|
|
230
|
+
/// @returns {string | null} The element's name, if available.
|
|
231
|
+
#[napi]
|
|
232
|
+
pub fn name(&self) -> napi::Result<Option<String>> {
|
|
233
|
+
Ok(self.inner.name())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// Get children of this element.
|
|
237
|
+
///
|
|
238
|
+
/// @returns {Array<Element>} List of child elements.
|
|
239
|
+
#[napi]
|
|
240
|
+
pub fn children(&self) -> napi::Result<Vec<Element>> {
|
|
241
|
+
self.inner
|
|
242
|
+
.children()
|
|
243
|
+
.map(|kids| kids.into_iter().map(Element::from).collect())
|
|
244
|
+
.map_err(map_error)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Get the parent element.
|
|
248
|
+
///
|
|
249
|
+
/// @returns {Element | null} The parent element, if available.
|
|
250
|
+
#[napi]
|
|
251
|
+
pub fn parent(&self) -> napi::Result<Option<Element>> {
|
|
252
|
+
self.inner
|
|
253
|
+
.parent()
|
|
254
|
+
.map(|opt| opt.map(Element::from))
|
|
255
|
+
.map_err(map_error)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Get element bounds.
|
|
259
|
+
///
|
|
260
|
+
/// @returns {Bounds} The element's bounds (x, y, width, height).
|
|
261
|
+
#[napi]
|
|
262
|
+
pub fn bounds(&self) -> napi::Result<Bounds> {
|
|
263
|
+
self.inner.bounds().map(Bounds::from).map_err(map_error)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Click on this element.
|
|
267
|
+
///
|
|
268
|
+
/// @param {ActionOptions} [options] - Options for the click action.
|
|
269
|
+
/// @returns {Promise<ClickResult>} Result of the click operation.
|
|
270
|
+
#[napi]
|
|
271
|
+
pub async fn click(&self, options: Option<ActionOptions>) -> napi::Result<ClickResult> {
|
|
272
|
+
let opts = options.unwrap_or_default();
|
|
273
|
+
// Default false: clicking means user wants focus on clicked element
|
|
274
|
+
let _restore_focus = opts.restore_focus.unwrap_or(false);
|
|
275
|
+
|
|
276
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
277
|
+
#[cfg(target_os = "windows")]
|
|
278
|
+
let saved_focus = if _restore_focus {
|
|
279
|
+
tracing::debug!("[TS SDK] click: saving focus state BEFORE activate_window");
|
|
280
|
+
computeruse::platforms::windows::save_focus_state()
|
|
281
|
+
} else {
|
|
282
|
+
None
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
286
|
+
let _ = self.inner.highlight_before_action("click");
|
|
287
|
+
}
|
|
288
|
+
let _ = self.inner.activate_window();
|
|
289
|
+
|
|
290
|
+
// Determine click type
|
|
291
|
+
let click_type: computeruse::ClickType = opts
|
|
292
|
+
.click_type
|
|
293
|
+
.map(|ct| ct.into())
|
|
294
|
+
.unwrap_or(computeruse::ClickType::Left);
|
|
295
|
+
|
|
296
|
+
// Check if custom position is specified
|
|
297
|
+
let use_position = opts.click_position.is_some();
|
|
298
|
+
let (x_pct, y_pct) = opts
|
|
299
|
+
.click_position
|
|
300
|
+
.map(|p| (p.x_percentage, p.y_percentage))
|
|
301
|
+
.unwrap_or((50, 50));
|
|
302
|
+
|
|
303
|
+
let mut result = if opts.ui_diff_before_after.unwrap_or(false) {
|
|
304
|
+
// Use backend's execute_on_element_with_ui_diff for UI diff capture
|
|
305
|
+
let diff_options = computeruse::UiDiffOptions {
|
|
306
|
+
max_depth: opts.ui_diff_max_depth.map(|d| d as usize),
|
|
307
|
+
settle_delay_ms: Some(1500),
|
|
308
|
+
include_detailed_attributes: Some(true),
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Get desktop to call execute_on_element_with_ui_diff
|
|
312
|
+
let desktop = computeruse::Desktop::new_default().map_err(map_error)?;
|
|
313
|
+
let element_clone = self.inner.clone();
|
|
314
|
+
|
|
315
|
+
let click_result_with_diff = if use_position {
|
|
316
|
+
desktop
|
|
317
|
+
.execute_on_element_with_ui_diff(
|
|
318
|
+
element_clone,
|
|
319
|
+
|el| async move { el.click_at_position(x_pct, y_pct, click_type) },
|
|
320
|
+
Some(diff_options),
|
|
321
|
+
)
|
|
322
|
+
.await
|
|
323
|
+
} else {
|
|
324
|
+
desktop
|
|
325
|
+
.execute_on_element_with_ui_diff(
|
|
326
|
+
element_clone,
|
|
327
|
+
|el| async move { el.click() },
|
|
328
|
+
Some(diff_options),
|
|
329
|
+
)
|
|
330
|
+
.await
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
match click_result_with_diff {
|
|
334
|
+
Ok((click_result, _element, ui_diff)) => {
|
|
335
|
+
let ui_diff_converted = ui_diff.map(|d| crate::types::UiDiffResult {
|
|
336
|
+
diff: d.diff,
|
|
337
|
+
has_changes: d.has_changes,
|
|
338
|
+
});
|
|
339
|
+
ClickResult {
|
|
340
|
+
method: click_result.method,
|
|
341
|
+
coordinates: click_result
|
|
342
|
+
.coordinates
|
|
343
|
+
.map(|c| crate::Coordinates { x: c.0, y: c.1 }),
|
|
344
|
+
details: click_result.details,
|
|
345
|
+
window_screenshot_path: None,
|
|
346
|
+
monitor_screenshot_paths: None,
|
|
347
|
+
ui_diff: ui_diff_converted,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
Err(e) => {
|
|
351
|
+
return Err(map_error(e));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
// Standard click without UI diff
|
|
356
|
+
let click_res = if use_position {
|
|
357
|
+
self.inner
|
|
358
|
+
.click_at_position(x_pct, y_pct, click_type)
|
|
359
|
+
.map_err(map_error)?
|
|
360
|
+
} else {
|
|
361
|
+
self.inner.click().map_err(map_error)?
|
|
362
|
+
};
|
|
363
|
+
ClickResult {
|
|
364
|
+
method: click_res.method,
|
|
365
|
+
coordinates: click_res
|
|
366
|
+
.coordinates
|
|
367
|
+
.map(|c| crate::Coordinates { x: c.0, y: c.1 }),
|
|
368
|
+
details: click_res.details,
|
|
369
|
+
window_screenshot_path: None,
|
|
370
|
+
monitor_screenshot_paths: None,
|
|
371
|
+
ui_diff: None,
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Capture screenshots if requested
|
|
376
|
+
let screenshots = capture_element_screenshots(
|
|
377
|
+
&self.inner,
|
|
378
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
379
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
380
|
+
"click",
|
|
381
|
+
);
|
|
382
|
+
result.window_screenshot_path = screenshots.window_path;
|
|
383
|
+
result.monitor_screenshot_paths = screenshots.monitor_paths;
|
|
384
|
+
|
|
385
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
386
|
+
#[cfg(target_os = "windows")]
|
|
387
|
+
if let Some(state) = saved_focus {
|
|
388
|
+
tracing::debug!("[TS SDK] click: restoring focus state after action");
|
|
389
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
Ok(result)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Double click on this element.
|
|
396
|
+
///
|
|
397
|
+
/// @param {ActionOptions} [options] - Options for the double click action.
|
|
398
|
+
/// @returns {ClickResult} Result of the click operation.
|
|
399
|
+
#[napi]
|
|
400
|
+
pub fn double_click(&self, options: Option<ActionOptions>) -> napi::Result<ClickResult> {
|
|
401
|
+
let opts = options.unwrap_or_default();
|
|
402
|
+
// Default false: clicking means user wants focus on clicked element
|
|
403
|
+
let _restore_focus = opts.restore_focus.unwrap_or(false);
|
|
404
|
+
|
|
405
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
406
|
+
#[cfg(target_os = "windows")]
|
|
407
|
+
let saved_focus = if _restore_focus {
|
|
408
|
+
tracing::debug!("[TS SDK] double_click: saving focus state BEFORE activate_window");
|
|
409
|
+
computeruse::platforms::windows::save_focus_state()
|
|
410
|
+
} else {
|
|
411
|
+
None
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
415
|
+
let _ = self.inner.highlight_before_action("double_click");
|
|
416
|
+
}
|
|
417
|
+
let _ = self.inner.activate_window();
|
|
418
|
+
let mut result: ClickResult = self
|
|
419
|
+
.inner
|
|
420
|
+
.double_click()
|
|
421
|
+
.map(ClickResult::from)
|
|
422
|
+
.map_err(map_error)?;
|
|
423
|
+
|
|
424
|
+
// Capture screenshots if requested
|
|
425
|
+
let screenshots = capture_element_screenshots(
|
|
426
|
+
&self.inner,
|
|
427
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
428
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
429
|
+
"doubleClick",
|
|
430
|
+
);
|
|
431
|
+
result.window_screenshot_path = screenshots.window_path;
|
|
432
|
+
result.monitor_screenshot_paths = screenshots.monitor_paths;
|
|
433
|
+
|
|
434
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
435
|
+
#[cfg(target_os = "windows")]
|
|
436
|
+
if let Some(state) = saved_focus {
|
|
437
|
+
tracing::debug!("[TS SDK] double_click: restoring focus state after action");
|
|
438
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
Ok(result)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// Right click on this element.
|
|
445
|
+
///
|
|
446
|
+
/// @param {ActionOptions} [options] - Options for the right click action.
|
|
447
|
+
#[napi]
|
|
448
|
+
pub fn right_click(&self, options: Option<ActionOptions>) -> napi::Result<()> {
|
|
449
|
+
let opts = options.unwrap_or_default();
|
|
450
|
+
// Default false: clicking means user wants focus on clicked element
|
|
451
|
+
let _restore_focus = opts.restore_focus.unwrap_or(false);
|
|
452
|
+
|
|
453
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
454
|
+
#[cfg(target_os = "windows")]
|
|
455
|
+
let saved_focus = if _restore_focus {
|
|
456
|
+
tracing::debug!("[TS SDK] right_click: saving focus state BEFORE activate_window");
|
|
457
|
+
computeruse::platforms::windows::save_focus_state()
|
|
458
|
+
} else {
|
|
459
|
+
None
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
463
|
+
let _ = self.inner.highlight_before_action("right_click");
|
|
464
|
+
}
|
|
465
|
+
let _ = self.inner.activate_window();
|
|
466
|
+
let result = self.inner.right_click().map_err(map_error);
|
|
467
|
+
|
|
468
|
+
// Capture screenshots if requested
|
|
469
|
+
let _screenshots = capture_element_screenshots(
|
|
470
|
+
&self.inner,
|
|
471
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
472
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
473
|
+
"rightClick",
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
477
|
+
#[cfg(target_os = "windows")]
|
|
478
|
+
if let Some(state) = saved_focus {
|
|
479
|
+
tracing::debug!("[TS SDK] right_click: restoring focus state after action");
|
|
480
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
result
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// Hover over this element.
|
|
487
|
+
///
|
|
488
|
+
/// @param {ActionOptions} [options] - Optional action options.
|
|
489
|
+
#[napi]
|
|
490
|
+
pub fn hover(&self, options: Option<ActionOptions>) -> napi::Result<()> {
|
|
491
|
+
let opts = options.unwrap_or_default();
|
|
492
|
+
// Default false: hover shouldn't steal focus
|
|
493
|
+
let _restore_focus = opts.restore_focus.unwrap_or(false);
|
|
494
|
+
|
|
495
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
496
|
+
#[cfg(target_os = "windows")]
|
|
497
|
+
let saved_focus = if _restore_focus {
|
|
498
|
+
tracing::debug!("[TS SDK] hover: saving focus state BEFORE activate_window");
|
|
499
|
+
computeruse::platforms::windows::save_focus_state()
|
|
500
|
+
} else {
|
|
501
|
+
None
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
505
|
+
let _ = self.inner.highlight_before_action("hover");
|
|
506
|
+
}
|
|
507
|
+
let _ = self.inner.activate_window();
|
|
508
|
+
let result = self.inner.hover().map_err(map_error);
|
|
509
|
+
|
|
510
|
+
// Capture screenshots if requested
|
|
511
|
+
let _screenshots = capture_element_screenshots(
|
|
512
|
+
&self.inner,
|
|
513
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
514
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
515
|
+
"hover",
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
519
|
+
#[cfg(target_os = "windows")]
|
|
520
|
+
if let Some(state) = saved_focus {
|
|
521
|
+
tracing::debug!("[TS SDK] hover: restoring focus state after action");
|
|
522
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
result
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/// Check if element is visible.
|
|
529
|
+
///
|
|
530
|
+
/// @returns {boolean} True if the element is visible.
|
|
531
|
+
#[napi]
|
|
532
|
+
pub fn is_visible(&self) -> napi::Result<bool> {
|
|
533
|
+
self.inner.is_visible().map_err(map_error)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/// Check if element is enabled.
|
|
537
|
+
///
|
|
538
|
+
/// @returns {boolean} True if the element is enabled.
|
|
539
|
+
#[napi]
|
|
540
|
+
pub fn is_enabled(&self) -> napi::Result<bool> {
|
|
541
|
+
self.inner.is_enabled().map_err(map_error)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/// Focus this element.
|
|
545
|
+
#[napi]
|
|
546
|
+
pub fn focus(&self) -> napi::Result<()> {
|
|
547
|
+
self.inner.focus().map_err(map_error)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/// Get text content of this element.
|
|
551
|
+
///
|
|
552
|
+
/// @param {number} [maxDepth] - Maximum depth to search for text.
|
|
553
|
+
/// @returns {string} The element's text content.
|
|
554
|
+
#[napi]
|
|
555
|
+
pub fn text(&self, max_depth: Option<u32>) -> napi::Result<String> {
|
|
556
|
+
self.inner
|
|
557
|
+
.text(max_depth.unwrap_or(1) as usize)
|
|
558
|
+
.map_err(map_error)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/// Type text into this element.
|
|
562
|
+
///
|
|
563
|
+
/// @param {string} text - The text to type.
|
|
564
|
+
/// @param {TypeTextOptions} [options] - Options for typing.
|
|
565
|
+
/// @returns {ActionResult} Result of the type operation.
|
|
566
|
+
#[napi]
|
|
567
|
+
pub fn type_text(
|
|
568
|
+
&self,
|
|
569
|
+
text: String,
|
|
570
|
+
options: Option<TypeTextOptions>,
|
|
571
|
+
) -> napi::Result<ActionResult> {
|
|
572
|
+
let opts = options.unwrap_or_default();
|
|
573
|
+
let _restore_focus = opts.restore_focus.unwrap_or(true);
|
|
574
|
+
|
|
575
|
+
// CRITICAL: Save focus state BEFORE activate_window() if restore is requested
|
|
576
|
+
// activate_window() steals focus, so we must save first
|
|
577
|
+
#[cfg(target_os = "windows")]
|
|
578
|
+
let saved_focus = if _restore_focus {
|
|
579
|
+
tracing::debug!("[TS SDK] type_text: saving focus state BEFORE activate_window");
|
|
580
|
+
computeruse::platforms::windows::save_focus_state()
|
|
581
|
+
} else {
|
|
582
|
+
None
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
586
|
+
let _ = self.inner.highlight_before_action("type");
|
|
587
|
+
}
|
|
588
|
+
let _ = self.inner.activate_window();
|
|
589
|
+
|
|
590
|
+
// Clear existing text if requested (matches MCP's clear_before_typing behavior)
|
|
591
|
+
if opts.clear_before_typing {
|
|
592
|
+
let _ = self.inner.set_value("");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
let try_focus_before = opts.try_focus_before.unwrap_or(true);
|
|
596
|
+
let try_click_before = opts.try_click_before.unwrap_or(true);
|
|
597
|
+
// Pass restore_focus=false to platform layer since we handle it ourselves
|
|
598
|
+
self.inner
|
|
599
|
+
.type_text_with_state_and_focus_restore(
|
|
600
|
+
&text,
|
|
601
|
+
opts.use_clipboard.unwrap_or(false),
|
|
602
|
+
try_focus_before,
|
|
603
|
+
try_click_before,
|
|
604
|
+
false, // We handle focus restore ourselves since we saved BEFORE activate_window
|
|
605
|
+
)
|
|
606
|
+
.map_err(map_error)?;
|
|
607
|
+
|
|
608
|
+
// Restore focus state if we saved it
|
|
609
|
+
#[cfg(target_os = "windows")]
|
|
610
|
+
if let Some(state) = saved_focus {
|
|
611
|
+
tracing::debug!("[TS SDK] type_text: restoring focus state after typing");
|
|
612
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Capture screenshots if requested
|
|
616
|
+
let screenshots = capture_element_screenshots(
|
|
617
|
+
&self.inner,
|
|
618
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
619
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
620
|
+
"typeText",
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
Ok(ActionResult {
|
|
624
|
+
success: true,
|
|
625
|
+
window_screenshot_path: screenshots.window_path,
|
|
626
|
+
monitor_screenshot_paths: screenshots.monitor_paths,
|
|
627
|
+
ui_diff: None,
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/// Press a key while this element is focused.
|
|
632
|
+
///
|
|
633
|
+
/// @param {string} key - The key to press.
|
|
634
|
+
/// @param {ActionOptions} [options] - Options for the key press action.
|
|
635
|
+
/// @returns {ActionResult} Result of the key press operation.
|
|
636
|
+
#[napi]
|
|
637
|
+
pub fn press_key(
|
|
638
|
+
&self,
|
|
639
|
+
key: String,
|
|
640
|
+
options: Option<ActionOptions>,
|
|
641
|
+
) -> napi::Result<ActionResult> {
|
|
642
|
+
let opts = options.unwrap_or_default();
|
|
643
|
+
let _restore_focus = opts.restore_focus.unwrap_or(true);
|
|
644
|
+
|
|
645
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
646
|
+
#[cfg(target_os = "windows")]
|
|
647
|
+
let saved_focus = if _restore_focus {
|
|
648
|
+
tracing::debug!("[TS SDK] press_key: saving focus state BEFORE activate_window");
|
|
649
|
+
computeruse::platforms::windows::save_focus_state()
|
|
650
|
+
} else {
|
|
651
|
+
None
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
655
|
+
let _ = self.inner.highlight_before_action("key");
|
|
656
|
+
}
|
|
657
|
+
let _ = self.inner.activate_window();
|
|
658
|
+
let try_focus_before = opts.try_focus_before.unwrap_or(true);
|
|
659
|
+
let try_click_before = opts.try_click_before.unwrap_or(true);
|
|
660
|
+
// Normalize key to ensure curly brace format (e.g., "Enter" -> "{Enter}")
|
|
661
|
+
let normalized_key = normalize_key(&key);
|
|
662
|
+
tracing::debug!(
|
|
663
|
+
"[TS SDK] press_key: normalized key: {} -> {}",
|
|
664
|
+
key,
|
|
665
|
+
normalized_key
|
|
666
|
+
);
|
|
667
|
+
self.inner
|
|
668
|
+
.press_key_with_state_and_focus(&normalized_key, try_focus_before, try_click_before)
|
|
669
|
+
.map_err(map_error)?;
|
|
670
|
+
|
|
671
|
+
// Capture screenshots if requested
|
|
672
|
+
let screenshots = capture_element_screenshots(
|
|
673
|
+
&self.inner,
|
|
674
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
675
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
676
|
+
"pressKey",
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
680
|
+
#[cfg(target_os = "windows")]
|
|
681
|
+
if let Some(state) = saved_focus {
|
|
682
|
+
tracing::debug!("[TS SDK] press_key: restoring focus state after action");
|
|
683
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
Ok(ActionResult {
|
|
687
|
+
success: true,
|
|
688
|
+
window_screenshot_path: screenshots.window_path,
|
|
689
|
+
monitor_screenshot_paths: screenshots.monitor_paths,
|
|
690
|
+
ui_diff: None,
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/// Set value of this element.
|
|
695
|
+
///
|
|
696
|
+
/// @param {string} value - The value to set.
|
|
697
|
+
/// @param {ActionOptions} [options] - Options for the set value action.
|
|
698
|
+
/// @returns {ActionResult} Result of the set value operation.
|
|
699
|
+
#[napi]
|
|
700
|
+
pub fn set_value(
|
|
701
|
+
&self,
|
|
702
|
+
value: String,
|
|
703
|
+
options: Option<ActionOptions>,
|
|
704
|
+
) -> napi::Result<ActionResult> {
|
|
705
|
+
let opts = options.unwrap_or_default();
|
|
706
|
+
let _restore_focus = opts.restore_focus.unwrap_or(true);
|
|
707
|
+
|
|
708
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
709
|
+
#[cfg(target_os = "windows")]
|
|
710
|
+
let saved_focus = if _restore_focus {
|
|
711
|
+
tracing::debug!("[TS SDK] set_value: saving focus state BEFORE action");
|
|
712
|
+
computeruse::platforms::windows::save_focus_state()
|
|
713
|
+
} else {
|
|
714
|
+
None
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
718
|
+
let _ = self.inner.highlight_before_action("set_value");
|
|
719
|
+
}
|
|
720
|
+
self.inner.set_value(&value).map_err(map_error)?;
|
|
721
|
+
|
|
722
|
+
// Capture screenshots if requested
|
|
723
|
+
let screenshots = capture_element_screenshots(
|
|
724
|
+
&self.inner,
|
|
725
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
726
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
727
|
+
"setValue",
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
731
|
+
#[cfg(target_os = "windows")]
|
|
732
|
+
if let Some(state) = saved_focus {
|
|
733
|
+
tracing::debug!("[TS SDK] set_value: restoring focus state after action");
|
|
734
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
Ok(ActionResult {
|
|
738
|
+
success: true,
|
|
739
|
+
window_screenshot_path: screenshots.window_path,
|
|
740
|
+
monitor_screenshot_paths: screenshots.monitor_paths,
|
|
741
|
+
ui_diff: None,
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/// Perform a named action on this element.
|
|
746
|
+
///
|
|
747
|
+
/// @param {string} action - The action to perform.
|
|
748
|
+
#[napi]
|
|
749
|
+
pub fn perform_action(&self, action: String) -> napi::Result<()> {
|
|
750
|
+
self.inner.perform_action(&action).map_err(map_error)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/// Invoke this element (triggers the default action).
|
|
754
|
+
/// This is often more reliable than clicking for controls like radio buttons or menu items.
|
|
755
|
+
///
|
|
756
|
+
/// @param {ActionOptions} [options] - Options for the invoke action.
|
|
757
|
+
/// @returns {ActionResult} Result of the invoke operation.
|
|
758
|
+
#[napi]
|
|
759
|
+
pub fn invoke(&self, options: Option<ActionOptions>) -> napi::Result<ActionResult> {
|
|
760
|
+
let opts = options.unwrap_or_default();
|
|
761
|
+
// Default false: invoking is like clicking
|
|
762
|
+
let _restore_focus = opts.restore_focus.unwrap_or(false);
|
|
763
|
+
|
|
764
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
765
|
+
#[cfg(target_os = "windows")]
|
|
766
|
+
let saved_focus = if _restore_focus {
|
|
767
|
+
tracing::debug!("[TS SDK] invoke: saving focus state BEFORE action");
|
|
768
|
+
computeruse::platforms::windows::save_focus_state()
|
|
769
|
+
} else {
|
|
770
|
+
None
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
774
|
+
let _ = self.inner.highlight_before_action("invoke");
|
|
775
|
+
}
|
|
776
|
+
self.inner.invoke().map_err(map_error)?;
|
|
777
|
+
|
|
778
|
+
// Capture screenshots if requested
|
|
779
|
+
let screenshots = capture_element_screenshots(
|
|
780
|
+
&self.inner,
|
|
781
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
782
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
783
|
+
"invoke",
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
787
|
+
#[cfg(target_os = "windows")]
|
|
788
|
+
if let Some(state) = saved_focus {
|
|
789
|
+
tracing::debug!("[TS SDK] invoke: restoring focus state after action");
|
|
790
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
Ok(ActionResult {
|
|
794
|
+
success: true,
|
|
795
|
+
window_screenshot_path: screenshots.window_path,
|
|
796
|
+
monitor_screenshot_paths: screenshots.monitor_paths,
|
|
797
|
+
ui_diff: None,
|
|
798
|
+
})
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/// Scroll the element in a given direction.
|
|
802
|
+
///
|
|
803
|
+
/// @param {string} direction - The direction to scroll.
|
|
804
|
+
/// @param {number} amount - The amount to scroll.
|
|
805
|
+
/// @param {ActionOptions} [options] - Options for the scroll action.
|
|
806
|
+
/// @returns {ActionResult} Result of the scroll operation.
|
|
807
|
+
#[napi]
|
|
808
|
+
pub fn scroll(
|
|
809
|
+
&self,
|
|
810
|
+
direction: String,
|
|
811
|
+
amount: f64,
|
|
812
|
+
options: Option<ActionOptions>,
|
|
813
|
+
) -> napi::Result<ActionResult> {
|
|
814
|
+
let opts = options.unwrap_or_default();
|
|
815
|
+
let _restore_focus = opts.restore_focus.unwrap_or(true);
|
|
816
|
+
|
|
817
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
818
|
+
#[cfg(target_os = "windows")]
|
|
819
|
+
let saved_focus = if _restore_focus {
|
|
820
|
+
tracing::debug!("[TS SDK] scroll: saving focus state BEFORE action");
|
|
821
|
+
computeruse::platforms::windows::save_focus_state()
|
|
822
|
+
} else {
|
|
823
|
+
None
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
827
|
+
let _ = self.inner.highlight_before_action("scroll");
|
|
828
|
+
}
|
|
829
|
+
self.inner.scroll(&direction, amount).map_err(map_error)?;
|
|
830
|
+
|
|
831
|
+
// Capture screenshots if requested
|
|
832
|
+
let screenshots = capture_element_screenshots(
|
|
833
|
+
&self.inner,
|
|
834
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
835
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
836
|
+
"scroll",
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
840
|
+
#[cfg(target_os = "windows")]
|
|
841
|
+
if let Some(state) = saved_focus {
|
|
842
|
+
tracing::debug!("[TS SDK] scroll: restoring focus state after action");
|
|
843
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
Ok(ActionResult {
|
|
847
|
+
success: true,
|
|
848
|
+
window_screenshot_path: screenshots.window_path,
|
|
849
|
+
monitor_screenshot_paths: screenshots.monitor_paths,
|
|
850
|
+
ui_diff: None,
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/// Activate the window containing this element.
|
|
855
|
+
#[napi]
|
|
856
|
+
pub fn activate_window(&self) -> napi::Result<()> {
|
|
857
|
+
self.inner.activate_window().map_err(map_error)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/// Minimize the window containing this element.
|
|
861
|
+
#[napi]
|
|
862
|
+
pub fn minimize_window(&self) -> napi::Result<()> {
|
|
863
|
+
self.inner.minimize_window().map_err(map_error)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/// Maximize the window containing this element.
|
|
867
|
+
#[napi]
|
|
868
|
+
pub fn maximize_window(&self) -> napi::Result<()> {
|
|
869
|
+
self.inner.maximize_window().map_err(map_error)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/// Check if element is focused.
|
|
873
|
+
///
|
|
874
|
+
/// @returns {boolean} True if the element is focused.
|
|
875
|
+
#[napi]
|
|
876
|
+
pub fn is_focused(&self) -> napi::Result<bool> {
|
|
877
|
+
self.inner.is_focused().map_err(map_error)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/// Check if element is keyboard focusable.
|
|
881
|
+
///
|
|
882
|
+
/// @returns {boolean} True if the element can receive keyboard focus.
|
|
883
|
+
#[napi]
|
|
884
|
+
pub fn is_keyboard_focusable(&self) -> napi::Result<bool> {
|
|
885
|
+
self.inner.is_keyboard_focusable().map_err(map_error)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/// Drag mouse from start to end coordinates.
|
|
889
|
+
///
|
|
890
|
+
/// @param {number} startX - Starting X coordinate.
|
|
891
|
+
/// @param {number} startY - Starting Y coordinate.
|
|
892
|
+
/// @param {number} endX - Ending X coordinate.
|
|
893
|
+
/// @param {number} endY - Ending Y coordinate.
|
|
894
|
+
/// @param {ActionOptions} [options] - Optional action options.
|
|
895
|
+
#[napi]
|
|
896
|
+
pub fn mouse_drag(
|
|
897
|
+
&self,
|
|
898
|
+
start_x: f64,
|
|
899
|
+
start_y: f64,
|
|
900
|
+
end_x: f64,
|
|
901
|
+
end_y: f64,
|
|
902
|
+
options: Option<ActionOptions>,
|
|
903
|
+
) -> napi::Result<()> {
|
|
904
|
+
let opts = options.unwrap_or_default();
|
|
905
|
+
|
|
906
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
907
|
+
let _ = self.inner.highlight_before_action("mouse_drag");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
let result = self
|
|
911
|
+
.inner
|
|
912
|
+
.mouse_drag(start_x, start_y, end_x, end_y)
|
|
913
|
+
.map_err(map_error);
|
|
914
|
+
|
|
915
|
+
// Capture screenshots if requested
|
|
916
|
+
let _screenshots = capture_element_screenshots(
|
|
917
|
+
&self.inner,
|
|
918
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
919
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
920
|
+
"mouseDrag",
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
result
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/// Press and hold mouse at coordinates.
|
|
927
|
+
///
|
|
928
|
+
/// @param {number} x - X coordinate.
|
|
929
|
+
/// @param {number} y - Y coordinate.
|
|
930
|
+
#[napi]
|
|
931
|
+
pub fn mouse_click_and_hold(&self, x: f64, y: f64) -> napi::Result<()> {
|
|
932
|
+
self.inner.mouse_click_and_hold(x, y).map_err(map_error)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/// Move mouse to coordinates.
|
|
936
|
+
///
|
|
937
|
+
/// @param {number} x - X coordinate.
|
|
938
|
+
/// @param {number} y - Y coordinate.
|
|
939
|
+
#[napi]
|
|
940
|
+
pub fn mouse_move(&self, x: f64, y: f64) -> napi::Result<()> {
|
|
941
|
+
self.inner.mouse_move(x, y).map_err(map_error)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/// Release mouse button.
|
|
945
|
+
///
|
|
946
|
+
/// @param {ActionOptions} [options] - Optional action options.
|
|
947
|
+
#[napi]
|
|
948
|
+
pub fn mouse_release(&self, options: Option<ActionOptions>) -> napi::Result<()> {
|
|
949
|
+
let opts = options.unwrap_or_default();
|
|
950
|
+
|
|
951
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
952
|
+
let _ = self.inner.highlight_before_action("mouse_release");
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
let result = self.inner.mouse_release().map_err(map_error);
|
|
956
|
+
|
|
957
|
+
// Capture screenshots if requested
|
|
958
|
+
let _screenshots = capture_element_screenshots(
|
|
959
|
+
&self.inner,
|
|
960
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
961
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
962
|
+
"mouseRelease",
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
result
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/// Create a locator from this element.
|
|
969
|
+
/// Accepts either a selector string or a Selector object.
|
|
970
|
+
///
|
|
971
|
+
/// @param {string | Selector} selector - The selector.
|
|
972
|
+
/// @returns {Locator} A new locator for finding elements.
|
|
973
|
+
#[napi]
|
|
974
|
+
pub fn locator(
|
|
975
|
+
&self,
|
|
976
|
+
#[napi(ts_arg_type = "string | Selector")] selector: Either<String, &Selector>,
|
|
977
|
+
) -> napi::Result<Locator> {
|
|
978
|
+
use napi::bindgen_prelude::Either::*;
|
|
979
|
+
let sel_rust: computeruse::selector::Selector = match selector {
|
|
980
|
+
A(sel_str) => sel_str.as_str().into(),
|
|
981
|
+
B(sel_obj) => sel_obj.inner.clone(),
|
|
982
|
+
};
|
|
983
|
+
let loc = self.inner.locator(sel_rust).map_err(map_error)?;
|
|
984
|
+
Ok(Locator::from(loc))
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/// Get the containing application element.
|
|
988
|
+
///
|
|
989
|
+
/// @returns {Element | null} The containing application element, if available.
|
|
990
|
+
#[napi]
|
|
991
|
+
pub fn application(&self) -> napi::Result<Option<Element>> {
|
|
992
|
+
self.inner
|
|
993
|
+
.application()
|
|
994
|
+
.map(|opt| opt.map(Element::from))
|
|
995
|
+
.map_err(map_error)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/// Get the containing window element.
|
|
999
|
+
///
|
|
1000
|
+
/// @returns {Element | null} The containing window element, if available.
|
|
1001
|
+
#[napi]
|
|
1002
|
+
pub fn window(&self) -> napi::Result<Option<Element>> {
|
|
1003
|
+
self.inner
|
|
1004
|
+
.window()
|
|
1005
|
+
.map(|opt| opt.map(Element::from))
|
|
1006
|
+
.map_err(map_error)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/// Highlights the element with a colored border and optional text overlay.
|
|
1010
|
+
///
|
|
1011
|
+
/// @param {number} [color] - Optional BGR color code (32-bit integer). Default: 0x0000FF (red)
|
|
1012
|
+
/// @param {number} [durationMs] - Optional duration in milliseconds.
|
|
1013
|
+
/// @param {string} [text] - Optional text to display. Text will be truncated to 10 characters.
|
|
1014
|
+
/// @param {TextPosition} [textPosition] - Optional position for the text overlay (default: Top)
|
|
1015
|
+
/// @param {FontStyle} [fontStyle] - Optional font styling for the text
|
|
1016
|
+
/// @returns {HighlightHandle} Handle that can be used to close the highlight early
|
|
1017
|
+
#[napi]
|
|
1018
|
+
pub fn highlight(
|
|
1019
|
+
&self,
|
|
1020
|
+
color: Option<u32>,
|
|
1021
|
+
duration_ms: Option<f64>,
|
|
1022
|
+
text: Option<String>,
|
|
1023
|
+
text_position: Option<TextPosition>,
|
|
1024
|
+
font_style: Option<FontStyle>,
|
|
1025
|
+
) -> napi::Result<HighlightHandle> {
|
|
1026
|
+
let duration = duration_ms.map(|ms| std::time::Duration::from_millis(ms as u64));
|
|
1027
|
+
|
|
1028
|
+
#[cfg(target_os = "windows")]
|
|
1029
|
+
{
|
|
1030
|
+
let rust_text_position = text_position.map(|pos| pos.into());
|
|
1031
|
+
let rust_font_style = font_style.map(|style| style.into());
|
|
1032
|
+
|
|
1033
|
+
let handle = self
|
|
1034
|
+
.inner
|
|
1035
|
+
.highlight(
|
|
1036
|
+
color,
|
|
1037
|
+
duration,
|
|
1038
|
+
text.as_deref(),
|
|
1039
|
+
rust_text_position,
|
|
1040
|
+
rust_font_style,
|
|
1041
|
+
)
|
|
1042
|
+
.map_err(map_error)?;
|
|
1043
|
+
|
|
1044
|
+
Ok(HighlightHandle::new(handle))
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
#[cfg(not(target_os = "windows"))]
|
|
1048
|
+
{
|
|
1049
|
+
let _ = (color, duration, text, text_position, font_style);
|
|
1050
|
+
Ok(HighlightHandle::new_dummy())
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/// Capture a screenshot of this element.
|
|
1055
|
+
///
|
|
1056
|
+
/// @returns {ScreenshotResult} The screenshot data containing image data and dimensions.
|
|
1057
|
+
#[napi]
|
|
1058
|
+
pub fn capture(&self) -> napi::Result<ScreenshotResult> {
|
|
1059
|
+
self.inner
|
|
1060
|
+
.capture()
|
|
1061
|
+
.map(|result| ScreenshotResult {
|
|
1062
|
+
image_data: result.image_data,
|
|
1063
|
+
width: result.width,
|
|
1064
|
+
height: result.height,
|
|
1065
|
+
monitor: result.monitor.map(crate::types::Monitor::from),
|
|
1066
|
+
})
|
|
1067
|
+
.map_err(map_error)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/// Get the process ID of the application containing this element.
|
|
1071
|
+
///
|
|
1072
|
+
/// @returns {number} The process ID.
|
|
1073
|
+
#[napi]
|
|
1074
|
+
pub fn process_id(&self) -> napi::Result<u32> {
|
|
1075
|
+
self.inner.process_id().map_err(map_error)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/// Get the process name of the application containing this element.
|
|
1079
|
+
///
|
|
1080
|
+
/// @returns {string} The process name (e.g., "chrome", "notepad").
|
|
1081
|
+
#[napi]
|
|
1082
|
+
pub fn process_name(&self) -> napi::Result<String> {
|
|
1083
|
+
self.inner.process_name().map_err(map_error)
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
#[napi]
|
|
1087
|
+
pub fn to_string(&self) -> napi::Result<String> {
|
|
1088
|
+
let id_part = self.inner.id().map_or("null".to_string(), |id| id);
|
|
1089
|
+
|
|
1090
|
+
let attrs = self.inner.attributes();
|
|
1091
|
+
let json =
|
|
1092
|
+
serde_json::to_string(&attrs).map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
|
1093
|
+
|
|
1094
|
+
Ok(format!("Element<{id_part}, {json}>"))
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/// Sets the transparency of the window.
|
|
1098
|
+
///
|
|
1099
|
+
/// @param {number} percentage - The transparency percentage from 0 (completely transparent) to 100 (completely opaque).
|
|
1100
|
+
/// @returns {void}
|
|
1101
|
+
#[napi]
|
|
1102
|
+
pub fn set_transparency(&self, percentage: u8) -> napi::Result<()> {
|
|
1103
|
+
self.inner.set_transparency(percentage).map_err(map_error)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/// Close the element if it's closable (like windows, applications).
|
|
1107
|
+
/// Does nothing for non-closable elements (like buttons, text, etc.).
|
|
1108
|
+
///
|
|
1109
|
+
/// @returns {void}
|
|
1110
|
+
#[napi]
|
|
1111
|
+
pub fn close(&self) -> napi::Result<()> {
|
|
1112
|
+
self.inner.close().map_err(map_error)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/// Get the monitor containing this element.
|
|
1116
|
+
///
|
|
1117
|
+
/// @returns {Monitor} The monitor information for the display containing this element.
|
|
1118
|
+
#[napi]
|
|
1119
|
+
pub fn monitor(&self) -> napi::Result<crate::types::Monitor> {
|
|
1120
|
+
self.inner
|
|
1121
|
+
.monitor()
|
|
1122
|
+
.map(crate::types::Monitor::from)
|
|
1123
|
+
.map_err(map_error)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/// Scrolls the element into view within its window viewport.
|
|
1127
|
+
/// If the element is already visible, returns immediately.
|
|
1128
|
+
///
|
|
1129
|
+
/// @returns {void}
|
|
1130
|
+
#[napi]
|
|
1131
|
+
pub fn scroll_into_view(&self) -> napi::Result<()> {
|
|
1132
|
+
self.inner.scroll_into_view().map_err(map_error)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/// Selects an option in a dropdown or combobox by its visible text.
|
|
1136
|
+
///
|
|
1137
|
+
/// @param {string} optionName - The visible text of the option to select.
|
|
1138
|
+
/// @param {ActionOptions} [options] - Optional action options.
|
|
1139
|
+
/// @returns {void}
|
|
1140
|
+
#[napi]
|
|
1141
|
+
pub fn select_option(
|
|
1142
|
+
&self,
|
|
1143
|
+
option_name: String,
|
|
1144
|
+
options: Option<ActionOptions>,
|
|
1145
|
+
) -> napi::Result<()> {
|
|
1146
|
+
let opts = options.unwrap_or_default();
|
|
1147
|
+
let _restore_focus = opts.restore_focus.unwrap_or(true);
|
|
1148
|
+
|
|
1149
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
1150
|
+
#[cfg(target_os = "windows")]
|
|
1151
|
+
let saved_focus = if _restore_focus {
|
|
1152
|
+
tracing::debug!("[TS SDK] select_option: saving focus state BEFORE action");
|
|
1153
|
+
computeruse::platforms::windows::save_focus_state()
|
|
1154
|
+
} else {
|
|
1155
|
+
None
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
1159
|
+
let _ = self.inner.highlight_before_action("select_option");
|
|
1160
|
+
}
|
|
1161
|
+
let result = self.inner.select_option(&option_name).map_err(map_error);
|
|
1162
|
+
|
|
1163
|
+
// Capture screenshots if requested
|
|
1164
|
+
let _screenshots = capture_element_screenshots(
|
|
1165
|
+
&self.inner,
|
|
1166
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
1167
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
1168
|
+
"selectOption",
|
|
1169
|
+
);
|
|
1170
|
+
|
|
1171
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
1172
|
+
#[cfg(target_os = "windows")]
|
|
1173
|
+
if let Some(state) = saved_focus {
|
|
1174
|
+
tracing::debug!("[TS SDK] select_option: restoring focus state after action");
|
|
1175
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
result
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/// Lists all available option strings from a dropdown or list box.
|
|
1182
|
+
///
|
|
1183
|
+
/// @returns {Array<string>} List of available option strings.
|
|
1184
|
+
#[napi]
|
|
1185
|
+
pub fn list_options(&self) -> napi::Result<Vec<String>> {
|
|
1186
|
+
self.inner.list_options().map_err(map_error)
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/// Checks if a control (like a checkbox or toggle switch) is currently toggled on.
|
|
1190
|
+
///
|
|
1191
|
+
/// @returns {boolean} True if the control is toggled on.
|
|
1192
|
+
#[napi]
|
|
1193
|
+
pub fn is_toggled(&self) -> napi::Result<bool> {
|
|
1194
|
+
self.inner.is_toggled().map_err(map_error)
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/// Sets the state of a toggleable control.
|
|
1198
|
+
/// It only performs an action if the control is not already in the desired state.
|
|
1199
|
+
///
|
|
1200
|
+
/// @param {boolean} state - The desired toggle state.
|
|
1201
|
+
/// @param {ActionOptions} [options] - Optional action options.
|
|
1202
|
+
/// @returns {void}
|
|
1203
|
+
#[napi]
|
|
1204
|
+
pub fn set_toggled(&self, state: bool, options: Option<ActionOptions>) -> napi::Result<()> {
|
|
1205
|
+
let opts = options.unwrap_or_default();
|
|
1206
|
+
|
|
1207
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
1208
|
+
let _ = self.inner.highlight_before_action("set_toggled");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
let result = self.inner.set_toggled(state).map_err(map_error);
|
|
1212
|
+
|
|
1213
|
+
// Capture screenshots if requested
|
|
1214
|
+
let _screenshots = capture_element_screenshots(
|
|
1215
|
+
&self.inner,
|
|
1216
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
1217
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
1218
|
+
"setToggled",
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
result
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/// Checks if an element is selected (e.g., list item, tree node, tab).
|
|
1225
|
+
///
|
|
1226
|
+
/// @returns {boolean} True if the element is selected, false otherwise.
|
|
1227
|
+
#[napi]
|
|
1228
|
+
pub fn is_selected(&self) -> napi::Result<bool> {
|
|
1229
|
+
self.inner.is_selected().map_err(map_error)
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/// Sets the selection state of a selectable item.
|
|
1233
|
+
/// Only performs an action if the element is not already in the desired state.
|
|
1234
|
+
///
|
|
1235
|
+
/// @param {boolean} state - The desired selection state.
|
|
1236
|
+
/// @param {ActionOptions} [options] - Optional action options.
|
|
1237
|
+
/// @returns {void}
|
|
1238
|
+
#[napi]
|
|
1239
|
+
pub fn set_selected(&self, state: bool, options: Option<ActionOptions>) -> napi::Result<()> {
|
|
1240
|
+
let opts = options.unwrap_or_default();
|
|
1241
|
+
let _restore_focus = opts.restore_focus.unwrap_or(true);
|
|
1242
|
+
|
|
1243
|
+
// FOCUS RESTORATION: Save focus state BEFORE any window operations
|
|
1244
|
+
#[cfg(target_os = "windows")]
|
|
1245
|
+
let saved_focus = if _restore_focus {
|
|
1246
|
+
tracing::debug!("[TS SDK] set_selected: saving focus state BEFORE action");
|
|
1247
|
+
computeruse::platforms::windows::save_focus_state()
|
|
1248
|
+
} else {
|
|
1249
|
+
None
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
1253
|
+
let _ = self.inner.highlight_before_action("set_selected");
|
|
1254
|
+
}
|
|
1255
|
+
let result = self.inner.set_selected(state).map_err(map_error);
|
|
1256
|
+
|
|
1257
|
+
// Capture screenshots if requested
|
|
1258
|
+
let _screenshots = capture_element_screenshots(
|
|
1259
|
+
&self.inner,
|
|
1260
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
1261
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
1262
|
+
"setSelected",
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
// FOCUS RESTORATION: Restore focus state after action if we saved it
|
|
1266
|
+
#[cfg(target_os = "windows")]
|
|
1267
|
+
if let Some(state) = saved_focus {
|
|
1268
|
+
tracing::debug!("[TS SDK] set_selected: restoring focus state after action");
|
|
1269
|
+
computeruse::platforms::windows::restore_focus_state(state);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
result
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/// Gets the current value from a range-based control like a slider or progress bar.
|
|
1276
|
+
///
|
|
1277
|
+
/// @returns {number} The current value of the range control.
|
|
1278
|
+
#[napi]
|
|
1279
|
+
pub fn get_range_value(&self) -> napi::Result<f64> {
|
|
1280
|
+
self.inner.get_range_value().map_err(map_error)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/// Sets the value of a range-based control like a slider.
|
|
1284
|
+
///
|
|
1285
|
+
/// @param {number} value - The value to set.
|
|
1286
|
+
/// @param {ActionOptions} [options] - Optional action options.
|
|
1287
|
+
/// @returns {void}
|
|
1288
|
+
#[napi]
|
|
1289
|
+
pub fn set_range_value(&self, value: f64, options: Option<ActionOptions>) -> napi::Result<()> {
|
|
1290
|
+
let opts = options.unwrap_or_default();
|
|
1291
|
+
|
|
1292
|
+
if opts.highlight_before_action.unwrap_or(false) {
|
|
1293
|
+
let _ = self.inner.highlight_before_action("set_range_value");
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
let result = self.inner.set_range_value(value).map_err(map_error);
|
|
1297
|
+
|
|
1298
|
+
// Capture screenshots if requested
|
|
1299
|
+
let _screenshots = capture_element_screenshots(
|
|
1300
|
+
&self.inner,
|
|
1301
|
+
opts.include_window_screenshot.unwrap_or(true),
|
|
1302
|
+
opts.include_monitor_screenshots.unwrap_or(false),
|
|
1303
|
+
"setRangeValue",
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
result
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/// Gets the value attribute of an element (text inputs, combo boxes, etc.).
|
|
1310
|
+
///
|
|
1311
|
+
/// @returns {string | null} The value attribute, or null if not available.
|
|
1312
|
+
#[napi]
|
|
1313
|
+
pub fn get_value(&self) -> napi::Result<Option<String>> {
|
|
1314
|
+
self.inner.get_value().map_err(map_error)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/// Execute JavaScript in web browser using dev tools console.
|
|
1318
|
+
/// Returns the result of the script execution as a string.
|
|
1319
|
+
///
|
|
1320
|
+
/// @param {string} script - The JavaScript code to execute.
|
|
1321
|
+
/// @returns {Promise<string>} The result of script execution.
|
|
1322
|
+
#[napi]
|
|
1323
|
+
pub async fn execute_browser_script(&self, script: String) -> napi::Result<String> {
|
|
1324
|
+
self.inner
|
|
1325
|
+
.execute_browser_script(&script)
|
|
1326
|
+
.await
|
|
1327
|
+
.map_err(map_error)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/// Get the UI tree starting from this element.
|
|
1331
|
+
/// Returns a tree structure containing this element and all its descendants.
|
|
1332
|
+
///
|
|
1333
|
+
/// @param {number} [maxDepth=100] - Maximum depth to traverse (default: 100).
|
|
1334
|
+
/// @returns {UINode} Tree structure with recursive children.
|
|
1335
|
+
#[napi]
|
|
1336
|
+
pub fn get_tree(&self, max_depth: Option<i32>) -> napi::Result<crate::UINode> {
|
|
1337
|
+
let depth = max_depth.unwrap_or(100).max(0) as usize;
|
|
1338
|
+
let serializable_tree = self.inner.to_serializable_tree(depth);
|
|
1339
|
+
Ok(crate::types::serializable_to_ui_node(&serializable_tree))
|
|
1340
|
+
}
|
|
1341
|
+
}
|