1use std::collections::HashMap;
2use std::fmt;
3use std::ops::Deref;
4use std::sync::Arc;
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::Error;
9use crate::provider::Provider;
10use crate::role::Role;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ElementData {
19 pub role: Role,
21
22 pub name: Option<String>,
30
31 pub value: Option<String>,
37
38 pub description: Option<String>,
45
46 pub bounds: Option<Rect>,
58
59 pub actions: Vec<String>,
66
67 pub states: StateSet,
69
70 pub numeric_value: Option<f64>,
72
73 pub min_value: Option<f64>,
75
76 pub max_value: Option<f64>,
78
79 pub stable_id: Option<String>,
86
87 pub pid: Option<u32>,
89
90 pub raw: RawPlatformData,
92
93 #[serde(skip, default)]
96 pub handle: u64,
97}
98
99#[derive(Clone)]
107pub struct Element {
108 data: ElementData,
109 provider: Arc<dyn Provider>,
110}
111
112impl Deref for Element {
113 type Target = ElementData;
114
115 fn deref(&self) -> &ElementData {
116 &self.data
117 }
118}
119
120impl fmt::Debug for Element {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 fmt::Debug::fmt(&self.data, f)
123 }
124}
125
126impl fmt::Display for Element {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 let name_part = self
129 .data
130 .name
131 .as_ref()
132 .map(|n| format!(" \"{}\"", n))
133 .unwrap_or_default();
134 let value_part = self
135 .data
136 .value
137 .as_ref()
138 .map(|v| format!(" value=\"{}\"", v))
139 .unwrap_or_default();
140 write!(
141 f,
142 "{}{}{}",
143 self.data.role.to_snake_case(),
144 name_part,
145 value_part,
146 )
147 }
148}
149
150impl Serialize for Element {
151 fn serialize<S: serde::Serializer>(
152 &self,
153 serializer: S,
154 ) -> std::result::Result<S::Ok, S::Error> {
155 self.data.serialize(serializer)
156 }
157}
158
159impl Element {
160 pub fn new(data: ElementData, provider: Arc<dyn Provider>) -> Self {
162 Self { data, provider }
163 }
164
165 pub fn data(&self) -> &ElementData {
167 &self.data
168 }
169
170 pub fn provider(&self) -> &Arc<dyn Provider> {
172 &self.provider
173 }
174
175 pub fn children(&self) -> crate::error::Result<Vec<Element>> {
179 let children = self.provider.get_children(Some(&self.data))?;
180 Ok(children
181 .into_iter()
182 .map(|d| Element::new(d, Arc::clone(&self.provider)))
183 .collect())
184 }
185
186 pub fn parent(&self) -> crate::error::Result<Option<Element>> {
190 let parent = self.provider.get_parent(&self.data)?;
191 Ok(parent.map(|d| Element::new(d, Arc::clone(&self.provider))))
192 }
193
194 pub fn pid(&self) -> Option<u32> {
196 self.data.pid
197 }
198
199 pub fn tree(&self, max_depth: Option<usize>) -> crate::error::Result<TreeNode> {
204 build_tree_node(self, max_depth, 0)
205 }
206
207 pub fn dump(&self, max_depth: Option<usize>) -> crate::error::Result<String> {
212 let node = self.tree(max_depth)?;
213 let mut out = String::new();
214 write_tree_node(&node, 0, &mut out);
215 Ok(out)
216 }
217
218 pub fn press(&self) -> crate::error::Result<()> {
228 self.provider.press(&self.data)
229 }
230
231 pub fn focus(&self) -> crate::error::Result<()> {
233 self.provider.focus(&self.data)
234 }
235
236 pub fn blur(&self) -> crate::error::Result<()> {
238 self.provider.blur(&self.data)
239 }
240
241 pub fn toggle(&self) -> crate::error::Result<()> {
243 self.provider.toggle(&self.data)
244 }
245
246 pub fn select(&self) -> crate::error::Result<()> {
248 self.provider.select(&self.data)
249 }
250
251 pub fn expand(&self) -> crate::error::Result<()> {
253 self.provider.expand(&self.data)
254 }
255
256 pub fn collapse(&self) -> crate::error::Result<()> {
258 self.provider.collapse(&self.data)
259 }
260
261 pub fn show_menu(&self) -> crate::error::Result<()> {
263 self.provider.show_menu(&self.data)
264 }
265
266 pub fn increment(&self) -> crate::error::Result<()> {
268 self.provider.increment(&self.data)
269 }
270
271 pub fn decrement(&self) -> crate::error::Result<()> {
273 self.provider.decrement(&self.data)
274 }
275
276 pub fn scroll_into_view(&self) -> crate::error::Result<()> {
280 self.provider.scroll_into_view(&self.data)
281 }
282
283 pub fn set_value(&self, value: &str) -> crate::error::Result<()> {
286 self.provider.set_value(&self.data, value)
287 }
288
289 pub fn set_numeric_value(&self, value: f64) -> crate::error::Result<()> {
293 if !value.is_finite() {
294 return Err(Error::InvalidActionData {
295 message: format!("set_numeric_value requires a finite value, got {}", value),
296 });
297 }
298 self.provider.set_numeric_value(&self.data, value)
299 }
300
301 pub fn type_text(&self, text: &str) -> crate::error::Result<()> {
305 self.provider.type_text(&self.data, text)
306 }
307
308 pub fn select_text(&self, start: u32, end: u32) -> crate::error::Result<()> {
312 if start > end {
313 return Err(Error::InvalidActionData {
314 message: format!("select_text start ({}) must be <= end ({})", start, end),
315 });
316 }
317 self.provider.set_text_selection(&self.data, start, end)
318 }
319
320 pub fn perform_action(&self, action: &str) -> crate::error::Result<()> {
326 self.provider.perform_action(&self.data, action)
327 }
328}
329
330fn build_tree_node(
331 element: &Element,
332 max_depth: Option<usize>,
333 depth: usize,
334) -> crate::error::Result<TreeNode> {
335 let children = if max_depth.is_none_or(|d| depth < d) {
336 element
337 .children()?
338 .into_iter()
339 .map(|child| build_tree_node(&child, max_depth, depth + 1))
340 .collect::<crate::error::Result<Vec<_>>>()?
341 } else {
342 vec![]
343 };
344 Ok(TreeNode {
345 role: element.data.role.to_snake_case().to_string(),
346 name: element.data.name.clone(),
347 value: element.data.value.clone(),
348 children,
349 })
350}
351
352fn write_tree_node(node: &TreeNode, depth: usize, out: &mut String) {
353 use fmt::Write as _;
354 let indent = " ".repeat(depth);
355 write!(out, "{}{}", indent, node.role).unwrap();
356 if let Some(ref n) = node.name {
357 write!(out, " \"{}\"", n).unwrap();
358 }
359 if let Some(ref v) = node.value {
360 write!(out, " value=\"{}\"", v).unwrap();
361 }
362 out.push('\n');
363 for child in &node.children {
364 write_tree_node(child, depth + 1, out);
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
384#[non_exhaustive]
385pub struct StateSet {
386 pub enabled: bool,
387 pub visible: bool,
388 pub focused: bool,
389 #[serde(default)]
396 pub active: bool,
397 pub checked: Option<Toggled>,
399 pub selected: bool,
400 pub expanded: Option<bool>,
402 pub editable: bool,
403 pub focusable: bool,
405 pub modal: bool,
407 pub required: bool,
409 pub busy: bool,
411}
412
413impl Default for StateSet {
414 fn default() -> Self {
415 Self {
416 enabled: true,
417 visible: true,
418 focused: false,
419 active: false,
420 checked: None,
421 selected: false,
422 expanded: None,
423 editable: false,
424 focusable: false,
425 modal: false,
426 required: false,
427 busy: false,
428 }
429 }
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
434pub enum Toggled {
435 Off,
436 On,
437 Mixed,
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
445pub struct Rect {
446 pub x: i32,
447 pub y: i32,
448 pub width: u32,
449 pub height: u32,
450}
451
452impl Rect {
453 #[must_use]
467 pub fn to_physical(self, scale: f64) -> Rect {
468 let s = sane_scale(scale);
469 Rect {
470 x: scale_i32(self.x, s),
471 y: scale_i32(self.y, s),
472 width: scale_u32(self.width, s),
473 height: scale_u32(self.height, s),
474 }
475 }
476
477 #[must_use]
482 pub fn to_logical(self, scale: f64) -> Rect {
483 self.to_physical(1.0 / sane_scale(scale))
484 }
485}
486
487pub(crate) fn sane_scale(scale: f64) -> f64 {
491 if scale.is_finite() && scale > 0.0 {
492 scale
493 } else {
494 1.0
495 }
496}
497
498fn scale_i32(v: i32, scale: f64) -> i32 {
499 (f64::from(v) * scale).round() as i32
500}
501
502fn scale_u32(v: u32, scale: f64) -> u32 {
503 let scaled = (f64::from(v) * scale).round();
504 if scaled < 0.0 {
505 0
506 } else {
507 scaled as u32
508 }
509}
510
511#[cfg(test)]
512mod rect_scale_tests {
513 use super::Rect;
514
515 const R: Rect = Rect {
516 x: 100,
517 y: 200,
518 width: 300,
519 height: 40,
520 };
521
522 #[test]
523 fn scale_one_is_identity() {
524 assert_eq!(R.to_physical(1.0), R);
525 assert_eq!(R.to_logical(1.0), R);
526 }
527
528 #[test]
529 fn to_physical_multiplies_all_fields() {
530 assert_eq!(
531 R.to_physical(2.0),
532 Rect {
533 x: 200,
534 y: 400,
535 width: 600,
536 height: 80
537 }
538 );
539 }
540
541 #[test]
542 fn to_logical_divides_all_fields() {
543 let physical = Rect {
545 x: 150,
546 y: 300,
547 width: 450,
548 height: 60,
549 };
550 assert_eq!(
551 physical.to_logical(1.5),
552 Rect {
553 x: 100,
554 y: 200,
555 width: 300,
556 height: 40
557 }
558 );
559 }
560
561 #[test]
562 fn round_trip_preserves_within_one_px() {
563 for &scale in &[1.25_f64, 1.5, 1.75, 2.0] {
564 let back = R.to_physical(scale).to_logical(scale);
565 assert!((back.x - R.x).abs() <= 1, "x drift at {scale}");
566 assert!((back.y - R.y).abs() <= 1, "y drift at {scale}");
567 assert!(
568 (back.width as i64 - R.width as i64).abs() <= 1,
569 "w drift at {scale}"
570 );
571 assert!(
572 (back.height as i64 - R.height as i64).abs() <= 1,
573 "h drift at {scale}"
574 );
575 }
576 }
577
578 #[test]
579 fn negative_origin_scales_correctly() {
580 let r = Rect {
582 x: -1920,
583 y: -100,
584 width: 200,
585 height: 100,
586 };
587 assert_eq!(
588 r.to_physical(2.0),
589 Rect {
590 x: -3840,
591 y: -200,
592 width: 400,
593 height: 200
594 }
595 );
596 }
597
598 #[test]
599 fn fractional_scale_rounds_to_nearest() {
600 let r = Rect {
601 x: 3,
602 y: 3,
603 width: 5,
604 height: 5,
605 };
606 assert_eq!(
609 r.to_physical(1.5),
610 Rect {
611 x: 5,
612 y: 5,
613 width: 8,
614 height: 8
615 }
616 );
617 }
618
619 #[test]
620 fn bad_scale_degrades_to_identity() {
621 assert_eq!(R.to_physical(0.0), R);
622 assert_eq!(R.to_physical(-2.0), R);
623 assert_eq!(R.to_physical(f64::NAN), R);
624 assert_eq!(R.to_physical(f64::INFINITY), R);
625 assert_eq!(R.to_logical(0.0), R);
626 }
627}
628
629pub type RawPlatformData = HashMap<String, serde_json::Value>;
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct TreeNode {
643 pub role: String,
644 pub name: Option<String>,
645 pub value: Option<String>,
646 pub children: Vec<TreeNode>,
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
656 use crate::mock::{build_provider, MockProvider};
657 use crate::selector::Selector;
658
659 fn find_element(provider: &Arc<MockProvider>, selector: &str) -> Element {
663 let parsed = Selector::parse(selector).expect("selector must parse");
664 let provider_dyn: Arc<dyn Provider> = provider.clone();
665 let root = provider_dyn
666 .list_apps()
667 .expect("list_apps must succeed")
668 .into_iter()
669 .next()
670 .expect("mock provider must expose an application root");
671 let mut matches = provider_dyn
672 .find_elements(&root, &parsed, Some(1), None)
673 .expect("find_elements must succeed");
674 let data = matches.pop().expect("selector matched no elements");
675 Element::new(data, provider_dyn)
676 }
677
678 fn last_action(provider: &Arc<MockProvider>) -> (u64, String, Option<String>) {
679 provider
680 .actions()
681 .last()
682 .cloned()
683 .expect("expected at least one recorded action")
684 }
685
686 #[test]
687 fn nullary_actions_record_correct_name() {
688 let provider = build_provider();
689 let cases = [
690 (r#"button[name="Back"]"#, "press" as &str),
691 (r#"button[name="Back"]"#, "focus"),
692 (r#"button[name="Back"]"#, "blur"),
693 (r#"check_box[name="Agree"]"#, "toggle"),
694 (r#"list_item[name="Item 1"]"#, "select"),
695 (r#"list[name="Items"]"#, "expand"),
696 (r#"list[name="Items"]"#, "collapse"),
697 (r#"button[name="Back"]"#, "show_menu"),
698 (r#"slider[name="Volume"]"#, "increment"),
699 (r#"slider[name="Volume"]"#, "decrement"),
700 (r#"button[name="Back"]"#, "scroll_into_view"),
701 ];
702 for (selector, action) in cases {
703 provider.clear_actions();
704 let el = find_element(&provider, selector);
705 match action {
706 "press" => el.press().unwrap(),
707 "focus" => el.focus().unwrap(),
708 "blur" => el.blur().unwrap(),
709 "toggle" => el.toggle().unwrap(),
710 "select" => el.select().unwrap(),
711 "expand" => el.expand().unwrap(),
712 "collapse" => el.collapse().unwrap(),
713 "show_menu" => el.show_menu().unwrap(),
714 "increment" => el.increment().unwrap(),
715 "decrement" => el.decrement().unwrap(),
716 "scroll_into_view" => el.scroll_into_view().unwrap(),
717 _ => unreachable!(),
718 }
719 let (handle, name, data) = last_action(&provider);
720 assert_eq!(
721 name, action,
722 "wrong action recorded for selector {selector}"
723 );
724 assert_eq!(data, None, "nullary action should not carry data");
725 assert_eq!(handle, el.data.handle);
726 }
727 }
728
729 #[test]
730 fn set_value_records_text_payload() {
731 let provider = build_provider();
732 let el = find_element(&provider, r#"text_field[name="Search"]"#);
733 el.set_value("world").unwrap();
734 let (handle, name, data) = last_action(&provider);
735 assert_eq!(handle, el.data.handle);
736 assert_eq!(name, "set_value");
737 assert_eq!(data.as_deref(), Some("world"));
738 }
739
740 #[test]
741 fn set_numeric_value_records_payload() {
742 let provider = build_provider();
743 let el = find_element(&provider, r#"slider[name="Volume"]"#);
744 el.set_numeric_value(42.0).unwrap();
745 let (_, name, data) = last_action(&provider);
746 assert_eq!(name, "set_numeric_value");
747 assert_eq!(data.as_deref(), Some("42"));
748 }
749
750 #[test]
751 fn set_numeric_value_rejects_non_finite() {
752 let provider = build_provider();
753 let el = find_element(&provider, r#"slider[name="Volume"]"#);
754 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
755 assert!(matches!(
756 el.set_numeric_value(bad),
757 Err(Error::InvalidActionData { .. })
758 ));
759 }
760 assert!(provider.actions().is_empty());
762 }
763
764 #[test]
765 fn type_text_records_payload() {
766 let provider = build_provider();
767 let el = find_element(&provider, r#"text_field[name="Search"]"#);
768 el.type_text("abc").unwrap();
769 let (_, name, data) = last_action(&provider);
770 assert_eq!(name, "type_text");
771 assert_eq!(data.as_deref(), Some("abc"));
772 }
773
774 #[test]
775 fn select_text_records_range() {
776 let provider = build_provider();
777 let el = find_element(&provider, r#"text_field[name="Search"]"#);
778 el.select_text(1, 4).unwrap();
779 let (_, name, data) = last_action(&provider);
780 assert_eq!(name, "set_text_selection");
781 assert_eq!(data.as_deref(), Some("1..4"));
782 }
783
784 #[test]
785 fn select_text_rejects_inverted_range() {
786 let provider = build_provider();
787 let el = find_element(&provider, r#"text_field[name="Search"]"#);
788 assert!(matches!(
789 el.select_text(5, 2),
790 Err(Error::InvalidActionData { .. })
791 ));
792 assert!(provider.actions().is_empty());
793 }
794
795 #[test]
796 fn perform_action_records_arbitrary_name() {
797 let provider = build_provider();
798 let el = find_element(&provider, r#"button[name="Back"]"#);
799 el.perform_action("raise").unwrap();
800 let (_, name, _) = last_action(&provider);
801 assert_eq!(name, "raise");
802 }
803
804 #[test]
805 fn locator_actions_desugar_to_element_actions() {
806 let provider = build_provider();
811 let provider_dyn: Arc<dyn Provider> = provider.clone();
812 let locator = crate::locator::Locator::new(provider_dyn, None, r#"button[name="Back"]"#);
813 locator.press().unwrap();
814 let (_, name, data) = last_action(&provider);
815 assert_eq!(name, "press");
816 assert_eq!(data, None);
817 }
818
819 #[test]
820 fn locator_validation_runs_before_auto_wait() {
821 let provider = build_provider();
826 let provider_dyn: Arc<dyn Provider> = provider.clone();
827 let locator =
828 crate::locator::Locator::new(provider_dyn, None, r#"button[name="never-matches"]"#);
829 let started = std::time::Instant::now();
830 let err = locator.set_numeric_value(f64::NAN).unwrap_err();
831 assert!(matches!(err, Error::InvalidActionData { .. }));
832 assert!(
833 started.elapsed() < std::time::Duration::from_secs(1),
834 "validation must short-circuit auto-wait",
835 );
836 }
837}