Skip to main content

xa11y_core/
element.rs

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/// Declare a reader/writer struct pair from one field list.
13///
14/// `#[non_exhaustive]` gives *readers* a stability guarantee: adding a field
15/// does not break a consumer that only reads one. *Writers* need the opposite
16/// — a new field should stop their build until they have decided what it
17/// means. Two types satisfy both, but only if their field sets cannot drift,
18/// which is what this macro enforces: there is one list, so a field cannot be
19/// added to one type and defaulted in the other.
20///
21/// The reader gets the doc comments, serde attributes, and `#[non_exhaustive]`.
22/// The writer gets the bare fields and stays exhaustive, so a struct literal
23/// in another crate fails to compile the moment the list grows.
24macro_rules! reader_writer_pair {
25    (
26        $(#[$reader_meta:meta])*
27        pub struct $reader:ident;
28
29        $(#[$writer_meta:meta])*
30        pub struct $writer:ident;
31
32        fields {
33            $(
34                $(#[$field_meta:meta])*
35                pub $field:ident : $ty:ty,
36            )*
37        }
38    ) => {
39        $(#[$reader_meta])*
40        #[non_exhaustive]
41        pub struct $reader {
42            $(
43                $(#[$field_meta])*
44                pub $field: $ty,
45            )*
46        }
47
48        $(#[$writer_meta])*
49        #[doc(hidden)]
50        pub struct $writer {
51            $(pub $field: $ty,)*
52        }
53
54        impl From<$writer> for $reader {
55            fn from(parts: $writer) -> Self {
56                // Destructured, not field-by-field: both halves come from the
57                // macro's single field list, so neither can gain a field the
58                // other silently defaults.
59                let $writer { $($field,)* } = parts;
60                Self { $($field,)* }
61            }
62        }
63    };
64}
65
66// Importable by path from sibling modules (`Event` uses it too).
67pub(crate) use reader_writer_pair;
68
69reader_writer_pair! {
70    /// The raw data for a single element in an accessibility tree.
71    ///
72    /// This is the underlying data struct. Most consumers should use
73    /// [`Element`], which wraps `ElementData` with a provider reference for
74    /// lazy navigation. `ElementData` is used directly by provider
75    /// implementors.
76    ///
77    /// `#[non_exhaustive]`: this is the type that grows every time the
78    /// normalized element model learns a new property, so adding a field must
79    /// not break the consumers that only ever *read* one. Providers, which
80    /// *write* one, get the opposite guarantee from [`ElementParts`].
81    ///
82    /// Build a partial element (an event target, a test fixture) with
83    /// [`ElementData::for_role`] and assign what you have:
84    ///
85    /// ```
86    /// # use xa11y_core::{ElementData, Role};
87    /// let mut data = ElementData::for_role(Role::Button);
88    /// data.name = Some("Submit".to_string());
89    /// ```
90    #[derive(Debug, Clone, Serialize, Deserialize)]
91    pub struct ElementData;
92
93    /// Every field a provider must decide on when it builds a *complete*
94    /// element from a platform node.
95    ///
96    /// Deliberately exhaustive: a struct literal in `xa11y-linux`,
97    /// `xa11y-macos`, or `xa11y-windows` stops compiling the moment a field
98    /// is added, which is the only thing that forces a per-platform decision
99    /// instead of a silent `None` on every backend.
100    ///
101    /// Paths that are partial by nature — an event target with no bounds, a
102    /// test fixture — should use [`ElementData::for_role`] instead and accept
103    /// that a new field arrives there as its default.
104    ///
105    /// Not public API (`#[doc(hidden)]`). Because a new field here is a
106    /// compile error in the sibling provider crates, they pin `xa11y-core`
107    /// with `=` rather than a caret requirement — see the workspace
108    /// `Cargo.toml`.
109    #[allow(
110        clippy::exhaustive_structs,
111        reason = "This type IS the completeness guard. Literal construction \
112                  from the provider crates is exactly what makes a new \
113                  ElementData field fail their build until each platform maps \
114                  it; #[non_exhaustive] here would delete the property it \
115                  exists for."
116    )]
117    #[derive(Debug, Clone)]
118    pub struct ElementParts;
119
120    fields {
121        /// Element role
122        pub role: Role,
123
124        /// Human-readable name (title, label).
125        ///
126        /// Stripped of Unicode bidi format controls (LRM, RLM, embeddings,
127        /// overrides, isolates) so equality assertions match the logical text.
128        /// The unstripped platform string is preserved in [`Self::raw`] under the
129        /// platform-native key (e.g. `AXTitle` on macOS, `atspi_name` on Linux,
130        /// `uia_name` on Windows). See [`crate::text::strip_bidi`].
131        pub name: Option<String>,
132
133        /// Current value (text content, slider position, etc.).
134        ///
135        /// Stripped of Unicode bidi format controls. The unstripped platform
136        /// string is preserved in [`Self::raw`] (`AXValue` on macOS, `atspi_value`
137        /// on Linux, `uia_value` on Windows). See [`crate::text::strip_bidi`].
138        pub value: Option<String>,
139
140        /// Supplementary description (tooltip, help text).
141        ///
142        /// Stripped of Unicode bidi format controls. The unstripped platform
143        /// string is preserved in [`Self::raw`] (`AXDescription`/`AXHelp` on
144        /// macOS, `atspi_description` on Linux, `uia_help_text` on Windows).
145        /// See [`crate::text::strip_bidi`].
146        pub description: Option<String>,
147
148        /// Bounding rectangle in **logical** screen coordinates
149        /// (device-independent points), origin at the top-left of the primary
150        /// display. This is the same coordinate space accepted by
151        /// [`crate::ScreenshotProvider::capture_region`] and by the input layer's
152        /// [`crate::input::Point`], so bounds can be fed directly to
153        /// `screenshot_element` / `click` without conversion.
154        ///
155        /// To map to physical device pixels (e.g. to index into a captured image),
156        /// multiply by the [`crate::Screenshot::scale`] reported for that display:
157        /// `physical = logical × scale`. See [`Rect::to_physical`] /
158        /// [`Rect::to_logical`].
159        pub bounds: Option<Rect>,
160
161        /// Available actions reported by the platform.
162        ///
163        /// Names are `snake_case` strings — well-known actions use their standard
164        /// names (`"press"`, `"toggle"`, `"expand"`, etc.) and platform-specific
165        /// actions use their converted names (e.g. macOS `AXCustomThing` →
166        /// `"custom_thing"`).
167        pub actions: Vec<String>,
168
169        /// Current state flags
170        pub states: StateSet,
171
172        /// Numeric value for range controls (sliders, progress bars, spinners).
173        pub numeric_value: Option<f64>,
174
175        /// Minimum value for range controls.
176        pub min_value: Option<f64>,
177
178        /// Maximum value for range controls.
179        pub max_value: Option<f64>,
180
181        /// Platform-assigned stable identifier for cross-snapshot correlation.
182        /// - macOS: `AXIdentifier`
183        /// - Windows: `AutomationId`
184        /// - Linux: D-Bus `object_path`
185        ///
186        /// Not all elements have one.
187        pub stable_id: Option<String>,
188
189        /// Process ID of the application that owns this element.
190        pub pid: Option<u32>,
191
192        /// Platform-specific raw data
193        pub raw: RawPlatformData,
194
195        /// Opaque handle for the provider to look up the platform object.
196        /// Not serialized — only valid within the provider that created it.
197        #[serde(skip, default)]
198        pub handle: u64,
199    }
200}
201
202impl ElementData {
203    /// An element with the given role and every other field empty.
204    ///
205    /// `states` starts at [`StateSet::default`] (enabled and visible, nothing
206    /// else), and `handle` at `0` — providers assign their own.
207    ///
208    /// This is the *partial* construction path. A provider translating a real
209    /// platform node should use [`ElementParts`] instead, so that a new field
210    /// fails its build rather than arriving as a default.
211    ///
212    /// Named `for_role` rather than `new` because `ElementData` is flattened
213    /// onto `Element` for the bindings-parity check, where a member called
214    /// `new` would collide with the existing [`Element::new`].
215    pub fn for_role(role: Role) -> Self {
216        // Struct literal, not a builder: this lives in the defining crate, so
217        // the compiler still checks it for completeness when a field is added.
218        Self {
219            role,
220            name: None,
221            value: None,
222            description: None,
223            bounds: None,
224            actions: Vec::new(),
225            states: StateSet::default(),
226            numeric_value: None,
227            min_value: None,
228            max_value: None,
229            stable_id: None,
230            pid: None,
231            raw: RawPlatformData::new(),
232            handle: 0,
233        }
234    }
235}
236
237impl Default for ElementData {
238    /// A [`Role::Unknown`] element with no properties.
239    fn default() -> Self {
240        Self::for_role(Role::Unknown)
241    }
242}
243
244/// A live element with lazy navigation via a provider reference.
245///
246/// `Element` dereferences to [`ElementData`], so all properties (`role`, `name`,
247/// `value`, `states`, etc.) are accessible via field access. Navigation
248/// methods (`parent()`, `children()`) call the provider on demand.
249///
250/// Elements are cheap to clone (they share the provider via `Arc`).
251#[derive(Clone)]
252pub struct Element {
253    data: ElementData,
254    provider: Arc<dyn Provider>,
255}
256
257impl Deref for Element {
258    type Target = ElementData;
259
260    fn deref(&self) -> &ElementData {
261        &self.data
262    }
263}
264
265impl fmt::Debug for Element {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        fmt::Debug::fmt(&self.data, f)
268    }
269}
270
271impl fmt::Display for Element {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        let name_part = self
274            .data
275            .name
276            .as_ref()
277            .map(|n| format!(" \"{}\"", n))
278            .unwrap_or_default();
279        let value_part = self
280            .data
281            .value
282            .as_ref()
283            .map(|v| format!(" value=\"{}\"", v))
284            .unwrap_or_default();
285        write!(
286            f,
287            "{}{}{}",
288            self.data.role.to_snake_case(),
289            name_part,
290            value_part,
291        )
292    }
293}
294
295impl Serialize for Element {
296    fn serialize<S: serde::Serializer>(
297        &self,
298        serializer: S,
299    ) -> std::result::Result<S::Ok, S::Error> {
300        self.data.serialize(serializer)
301    }
302}
303
304impl Element {
305    /// Create an Element from raw data and a provider reference.
306    pub fn new(data: ElementData, provider: Arc<dyn Provider>) -> Self {
307        Self { data, provider }
308    }
309
310    /// Get the underlying ElementData.
311    pub fn data(&self) -> &ElementData {
312        &self.data
313    }
314
315    /// Get the provider reference.
316    pub fn provider(&self) -> &Arc<dyn Provider> {
317        &self.provider
318    }
319
320    /// Get direct children of this element.
321    ///
322    /// Each call queries the provider — results are not cached.
323    pub fn children(&self) -> crate::error::Result<Vec<Element>> {
324        let children = self.provider.get_children(Some(&self.data))?;
325        Ok(children
326            .into_iter()
327            .map(|d| Element::new(d, Arc::clone(&self.provider)))
328            .collect())
329    }
330
331    /// Get the parent element, if any (root-level elements have no parent).
332    ///
333    /// Each call queries the provider — results are not cached.
334    pub fn parent(&self) -> crate::error::Result<Option<Element>> {
335        let parent = self.provider.get_parent(&self.data)?;
336        Ok(parent.map(|d| Element::new(d, Arc::clone(&self.provider))))
337    }
338
339    /// Get the process ID from the element data.
340    pub fn pid(&self) -> Option<u32> {
341        self.data.pid
342    }
343
344    /// Capture the subtree rooted at this element as a recursive snapshot.
345    ///
346    /// `max_depth` limits traversal depth: `0` = only this node (no children),
347    /// `1` = node + direct children, and so on. `None` traverses the full subtree.
348    pub fn tree(&self, max_depth: Option<usize>) -> crate::error::Result<TreeNode> {
349        build_tree_node(self, max_depth, 0)
350    }
351
352    /// Render the subtree rooted at this element as an indented string.
353    ///
354    /// Each line is `{indent}{role} "{name}" [value="{value}"]`. Returns the
355    /// string without printing it. Same depth semantics as [`Element::tree`].
356    pub fn dump(&self, max_depth: Option<usize>) -> crate::error::Result<String> {
357        let node = self.tree(max_depth)?;
358        let mut out = String::new();
359        write_tree_node(&node, 0, &mut out);
360        Ok(out)
361    }
362
363    // ── Actions ─────────────────────────────────────────────────────
364    //
365    // Element actions invoke the platform via the captured provider handle —
366    // they do **not** re-resolve the selector. If the underlying element has
367    // been destroyed since this snapshot was taken, the provider returns a
368    // platform-specific "gone" error. For resilient retry-on-change semantics,
369    // use the equivalent method on [`crate::Locator`] instead.
370
371    /// Click / invoke this element via the accessibility action layer.
372    pub fn press(&self) -> crate::error::Result<()> {
373        self.provider.press(&self.data)
374    }
375
376    /// Set keyboard focus to this element.
377    pub fn focus(&self) -> crate::error::Result<()> {
378        self.provider.focus(&self.data)
379    }
380
381    /// Remove keyboard focus from this element.
382    pub fn blur(&self) -> crate::error::Result<()> {
383        self.provider.blur(&self.data)
384    }
385
386    /// Toggle a two- or three-state control (checkbox, switch).
387    pub fn toggle(&self) -> crate::error::Result<()> {
388        self.provider.toggle(&self.data)
389    }
390
391    /// Select this element (list item, tab, row).
392    pub fn select(&self) -> crate::error::Result<()> {
393        self.provider.select(&self.data)
394    }
395
396    /// Expand a disclosure, menu, combo box, or tree item.
397    pub fn expand(&self) -> crate::error::Result<()> {
398        self.provider.expand(&self.data)
399    }
400
401    /// Collapse an expanded element.
402    pub fn collapse(&self) -> crate::error::Result<()> {
403        self.provider.collapse(&self.data)
404    }
405
406    /// Open this element's context menu or dropdown.
407    pub fn show_menu(&self) -> crate::error::Result<()> {
408        self.provider.show_menu(&self.data)
409    }
410
411    /// Increment a numeric control (slider, spinner) by its platform step.
412    pub fn increment(&self) -> crate::error::Result<()> {
413        self.provider.increment(&self.data)
414    }
415
416    /// Decrement a numeric control (slider, spinner) by its platform step.
417    pub fn decrement(&self) -> crate::error::Result<()> {
418        self.provider.decrement(&self.data)
419    }
420
421    /// Scroll this element into the visible area.
422    ///
423    /// No-op on macOS — the macOS accessibility API has no equivalent.
424    pub fn scroll_into_view(&self) -> crate::error::Result<()> {
425        self.provider.scroll_into_view(&self.data)
426    }
427
428    /// Set the text value of this element. Replaces the entire value rather
429    /// than inserting at the caret — use [`Element::type_text`] for insertion.
430    pub fn set_value(&self, value: &str) -> crate::error::Result<()> {
431        self.provider.set_value(&self.data, value)
432    }
433
434    /// Set the numeric value of this element (slider, spinner).
435    ///
436    /// Returns [`Error::InvalidActionData`] if `value` is NaN or infinite.
437    pub fn set_numeric_value(&self, value: f64) -> crate::error::Result<()> {
438        if !value.is_finite() {
439            return Err(Error::InvalidActionData {
440                message: format!("set_numeric_value requires a finite value, got {}", value),
441            });
442        }
443        self.provider.set_numeric_value(&self.data, value)
444    }
445
446    /// Insert text at the current cursor position.
447    ///
448    /// Uses the platform accessibility API — never simulates keyboard events.
449    pub fn type_text(&self, text: &str) -> crate::error::Result<()> {
450        self.provider.type_text(&self.data, text)
451    }
452
453    /// Select the text range from `start` to `end` (0-based character offsets).
454    ///
455    /// Returns [`Error::InvalidActionData`] if `start > end`.
456    pub fn select_text(&self, start: u32, end: u32) -> crate::error::Result<()> {
457        if start > end {
458            return Err(Error::InvalidActionData {
459                message: format!("select_text start ({}) must be <= end ({})", start, end),
460            });
461        }
462        self.provider.set_text_selection(&self.data, start, end)
463    }
464
465    /// Perform an action by its `snake_case` name.
466    ///
467    /// Use this for actions the element advertises in its [`actions`](ElementData::actions)
468    /// list that don't have a dedicated method. Well-known names (`"press"`,
469    /// `"focus"`, etc.) also work — providers delegate to the named methods.
470    pub fn perform_action(&self, action: &str) -> crate::error::Result<()> {
471        self.provider.perform_action(&self.data, action)
472    }
473}
474
475fn build_tree_node(
476    element: &Element,
477    max_depth: Option<usize>,
478    depth: usize,
479) -> crate::error::Result<TreeNode> {
480    let children = if max_depth.is_none_or(|d| depth < d) {
481        element
482            .children()?
483            .into_iter()
484            .map(|child| build_tree_node(&child, max_depth, depth + 1))
485            .collect::<crate::error::Result<Vec<_>>>()?
486    } else {
487        vec![]
488    };
489    Ok(TreeNode {
490        role: element.data.role.to_snake_case().to_string(),
491        name: element.data.name.clone(),
492        value: element.data.value.clone(),
493        children,
494    })
495}
496
497fn write_tree_node(node: &TreeNode, depth: usize, out: &mut String) {
498    use fmt::Write as _;
499    let indent = "  ".repeat(depth);
500    write!(out, "{}{}", indent, node.role).unwrap();
501    if let Some(ref n) = node.name {
502        write!(out, " \"{}\"", n).unwrap();
503    }
504    if let Some(ref v) = node.value {
505        write!(out, " value=\"{}\"", v).unwrap();
506    }
507    out.push('\n');
508    for child in &node.children {
509        write_tree_node(child, depth + 1, out);
510    }
511}
512
513reader_writer_pair! {
514    /// Boolean state flags for an element.
515    ///
516    /// **Semantics for non-applicable states:** When a state doesn't apply to
517    /// an element's role, the backend uses the platform's reported value or
518    /// defaults:
519    /// - `enabled`: `true` (elements are enabled unless explicitly disabled)
520    /// - `visible`: `true` (elements are visible unless explicitly hidden/offscreen)
521    /// - `focused`, `active`, `focusable`, `modal`, `selected`, `editable`, `required`, `busy`: `false`
522    ///
523    /// States that are inherently inapplicable use `Option`: `checked` is
524    /// `None` for non-checkable elements, `expanded` is `None` for
525    /// non-expandable elements.
526    ///
527    /// `#[non_exhaustive]`: more states arrive in compatible releases and must
528    /// not break readers. Providers building a complete state set use
529    /// [`StateParts`], which is exhaustive — the documented defaults above are
530    /// what a *partial* construction falls back to, not a licence for a
531    /// backend to skip deciding.
532    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
533    pub struct StateSet;
534
535    /// Every state a provider must decide on when it translates a platform
536    /// node's state bits.
537    ///
538    /// Deliberately exhaustive, for the same reason as [`ElementParts`]: a new
539    /// state must fail each backend's build rather than silently inherit
540    /// [`StateSet::default`]. That matters more here than the defaults
541    /// suggest — the parity check requires every state to surface as a binding
542    /// getter, so a silently-defaulted state ships as a documented API that no
543    /// platform populates.
544    ///
545    /// Not public API (`#[doc(hidden)]`).
546    #[allow(
547        clippy::exhaustive_structs,
548        reason = "This type IS the completeness guard for element state. See \
549                  ElementParts; the same reasoning applies."
550    )]
551    #[derive(Debug, Clone)]
552    pub struct StateParts;
553
554    fields {
555        pub enabled: bool,
556        pub visible: bool,
557        pub focused: bool,
558        /// Whether this element is the active (foreground) window — the window that
559        /// currently receives the user's input. Only meaningful for window-like
560        /// elements (windows, dialogs); `false` elsewhere. Distinct from `focused`,
561        /// which is element-level keyboard focus. Platform mappings: the AT-SPI
562        /// `ACTIVE` state (Linux), `AXMain` (macOS), and the foreground `HWND`
563        /// (Windows).
564        #[serde(default)]
565        pub active: bool,
566        /// None = not checkable
567        pub checked: Option<Toggled>,
568        pub selected: bool,
569        /// None = not expandable
570        pub expanded: Option<bool>,
571        pub editable: bool,
572        /// Whether the element can receive keyboard focus
573        pub focusable: bool,
574        /// Whether the element is a modal dialog
575        pub modal: bool,
576        /// Form field required
577        pub required: bool,
578        /// Async operation in progress
579        pub busy: bool,
580    }
581}
582
583impl Default for StateSet {
584    fn default() -> Self {
585        Self {
586            enabled: true,
587            visible: true,
588            focused: false,
589            active: false,
590            checked: None,
591            selected: false,
592            expanded: None,
593            editable: false,
594            focusable: false,
595            modal: false,
596            required: false,
597            busy: false,
598        }
599    }
600}
601
602/// Tri-state toggle value.
603#[allow(
604    clippy::exhaustive_enums,
605    reason = "Closed domain: a toggle is off, on, or indeterminate. Every \
606              platform's tri-state checkbox is exactly these three values, \
607              and a fourth would not be a toggle."
608)]
609#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
610pub enum Toggled {
611    Off,
612    On,
613    /// Indeterminate / tri-state
614    Mixed,
615}
616
617/// Screen-pixel bounding rectangle (origin + size).
618/// `x`/`y` are signed to support negative multi-monitor coordinates.
619/// `width`/`height` are unsigned (always non-negative).
620#[allow(
621    clippy::exhaustive_structs,
622    reason = "Closed domain: an axis-aligned rectangle is fully described by \
623              an origin and a size. Literal construction is the point of the \
624              type, and it will not gain a fifth field."
625)]
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
627pub struct Rect {
628    pub x: i32,
629    pub y: i32,
630    pub width: u32,
631    pub height: u32,
632}
633
634impl Rect {
635    /// Convert a **logical** rectangle to **physical** device pixels by
636    /// multiplying every component by `scale` (the physical-to-logical ratio,
637    /// e.g. `1.5` at 150% or `2.0` on a typical Retina display).
638    ///
639    /// This is the inverse of [`Rect::to_logical`]. Each field is rounded to
640    /// the nearest integer independently; for a single rectangle the position
641    /// and size therefore round separately, which can differ by 1px from
642    /// scaling the far edge — acceptable for capture/hit-test use where a 1px
643    /// slack is expected on fractional scales.
644    ///
645    /// A non-finite or non-positive `scale` is treated as `1.0` (identity):
646    /// callers on platforms without a known scale factor pass `1.0`, and a
647    /// bogus value must never produce garbage coordinates.
648    #[must_use]
649    pub fn to_physical(self, scale: f64) -> Rect {
650        let s = sane_scale(scale);
651        Rect {
652            x: scale_i32(self.x, s),
653            y: scale_i32(self.y, s),
654            width: scale_u32(self.width, s),
655            height: scale_u32(self.height, s),
656        }
657    }
658
659    /// Convert a **physical** rectangle (device pixels) to **logical**
660    /// coordinates by dividing every component by `scale`. Inverse of
661    /// [`Rect::to_physical`]. See that method for rounding and `scale`
662    /// validity semantics.
663    #[must_use]
664    pub fn to_logical(self, scale: f64) -> Rect {
665        self.to_physical(1.0 / sane_scale(scale))
666    }
667}
668
669/// Clamp a scale factor to a usable positive, finite value. Non-finite or
670/// non-positive inputs collapse to `1.0` so a bad platform reading degrades
671/// to identity rather than producing nonsense coordinates.
672pub(crate) fn sane_scale(scale: f64) -> f64 {
673    if scale.is_finite() && scale > 0.0 {
674        scale
675    } else {
676        1.0
677    }
678}
679
680fn scale_i32(v: i32, scale: f64) -> i32 {
681    (f64::from(v) * scale).round() as i32
682}
683
684fn scale_u32(v: u32, scale: f64) -> u32 {
685    let scaled = (f64::from(v) * scale).round();
686    if scaled < 0.0 {
687        0
688    } else {
689        scaled as u32
690    }
691}
692
693#[cfg(test)]
694mod rect_scale_tests {
695    use super::Rect;
696
697    const R: Rect = Rect {
698        x: 100,
699        y: 200,
700        width: 300,
701        height: 40,
702    };
703
704    #[test]
705    fn scale_one_is_identity() {
706        assert_eq!(R.to_physical(1.0), R);
707        assert_eq!(R.to_logical(1.0), R);
708    }
709
710    #[test]
711    fn to_physical_multiplies_all_fields() {
712        assert_eq!(
713            R.to_physical(2.0),
714            Rect {
715                x: 200,
716                y: 400,
717                width: 600,
718                height: 80
719            }
720        );
721    }
722
723    #[test]
724    fn to_logical_divides_all_fields() {
725        // Physical bounds on a 150% display -> logical points.
726        let physical = Rect {
727            x: 150,
728            y: 300,
729            width: 450,
730            height: 60,
731        };
732        assert_eq!(
733            physical.to_logical(1.5),
734            Rect {
735                x: 100,
736                y: 200,
737                width: 300,
738                height: 40
739            }
740        );
741    }
742
743    #[test]
744    fn round_trip_preserves_within_one_px() {
745        for &scale in &[1.25_f64, 1.5, 1.75, 2.0] {
746            let back = R.to_physical(scale).to_logical(scale);
747            assert!((back.x - R.x).abs() <= 1, "x drift at {scale}");
748            assert!((back.y - R.y).abs() <= 1, "y drift at {scale}");
749            assert!(
750                (back.width as i64 - R.width as i64).abs() <= 1,
751                "w drift at {scale}"
752            );
753            assert!(
754                (back.height as i64 - R.height as i64).abs() <= 1,
755                "h drift at {scale}"
756            );
757        }
758    }
759
760    #[test]
761    fn negative_origin_scales_correctly() {
762        // Multi-monitor: a window on a display left of the primary.
763        let r = Rect {
764            x: -1920,
765            y: -100,
766            width: 200,
767            height: 100,
768        };
769        assert_eq!(
770            r.to_physical(2.0),
771            Rect {
772                x: -3840,
773                y: -200,
774                width: 400,
775                height: 200
776            }
777        );
778    }
779
780    #[test]
781    fn fractional_scale_rounds_to_nearest() {
782        let r = Rect {
783            x: 3,
784            y: 3,
785            width: 5,
786            height: 5,
787        };
788        // 3 * 1.5 = 4.5 -> 5 (round half away from zero via f64::round);
789        // 5 * 1.5 = 7.5 -> 8.
790        assert_eq!(
791            r.to_physical(1.5),
792            Rect {
793                x: 5,
794                y: 5,
795                width: 8,
796                height: 8
797            }
798        );
799    }
800
801    #[test]
802    fn bad_scale_degrades_to_identity() {
803        assert_eq!(R.to_physical(0.0), R);
804        assert_eq!(R.to_physical(-2.0), R);
805        assert_eq!(R.to_physical(f64::NAN), R);
806        assert_eq!(R.to_physical(f64::INFINITY), R);
807        assert_eq!(R.to_logical(0.0), R);
808    }
809}
810
811/// Platform-specific raw data attached to every element.
812///
813/// An untyped key-value map containing the original platform-specific data
814/// exactly as the platform reported it. Keys use `snake_case` naming. This is
815/// the escape hatch for consumers who need full platform fidelity.
816pub type RawPlatformData = HashMap<String, serde_json::Value>;
817
818/// A node in a recursive snapshot of the accessibility subtree.
819///
820/// Returned by [`Element::tree`] and [`Locator::tree`]. Each node carries the
821/// role, display name, and value of one element, plus its children recursively.
822/// `children` is empty when `max_depth` was reached or the element is a leaf.
823///
824/// `#[non_exhaustive]`: a dump node grows alongside [`ElementData`] — bounds
825/// and stable ids are both plausible additions. Build one with
826/// [`TreeNode::new`].
827#[derive(Debug, Clone, Serialize, Deserialize)]
828#[non_exhaustive]
829pub struct TreeNode {
830    pub role: String,
831    pub name: Option<String>,
832    pub value: Option<String>,
833    pub children: Vec<TreeNode>,
834}
835
836impl TreeNode {
837    /// A leaf node with the given role and no name, value, or children.
838    pub fn new(role: impl Into<String>) -> Self {
839        Self {
840            role: role.into(),
841            name: None,
842            value: None,
843            children: Vec::new(),
844        }
845    }
846}
847
848#[cfg(test)]
849mod tests {
850    //! Unit tests for `Element` action methods. Verifies each action records the
851    //! expected entry in the mock provider's action log and that validation
852    //! errors fire before the provider is ever called.
853
854    use super::*;
855    use crate::mock::{build_provider, MockProvider};
856    use crate::selector::Selector;
857
858    /// Resolve `selector` against the mock tree and return the first match
859    /// wrapped in an `Element`. Panics on no match — these are unit tests, not
860    /// production paths.
861    fn find_element(provider: &Arc<MockProvider>, selector: &str) -> Element {
862        let parsed = Selector::parse(selector).expect("selector must parse");
863        let provider_dyn: Arc<dyn Provider> = provider.clone();
864        let root = provider_dyn
865            .list_apps()
866            .expect("list_apps must succeed")
867            .into_iter()
868            .next()
869            .expect("mock provider must expose an application root");
870        let mut matches = provider_dyn
871            .find_elements(&root, &parsed, Some(1), None)
872            .expect("find_elements must succeed");
873        let data = matches.pop().expect("selector matched no elements");
874        Element::new(data, provider_dyn)
875    }
876
877    fn last_action(provider: &Arc<MockProvider>) -> (u64, String, Option<String>) {
878        provider
879            .actions()
880            .last()
881            .cloned()
882            .expect("expected at least one recorded action")
883    }
884
885    #[test]
886    fn nullary_actions_record_correct_name() {
887        let provider = build_provider();
888        let cases = [
889            (r#"button[name="Back"]"#, "press" as &str),
890            (r#"button[name="Back"]"#, "focus"),
891            (r#"button[name="Back"]"#, "blur"),
892            (r#"check_box[name="Agree"]"#, "toggle"),
893            (r#"list_item[name="Item 1"]"#, "select"),
894            (r#"list[name="Items"]"#, "expand"),
895            (r#"list[name="Items"]"#, "collapse"),
896            (r#"button[name="Back"]"#, "show_menu"),
897            (r#"slider[name="Volume"]"#, "increment"),
898            (r#"slider[name="Volume"]"#, "decrement"),
899            (r#"button[name="Back"]"#, "scroll_into_view"),
900        ];
901        for (selector, action) in cases {
902            provider.clear_actions();
903            let el = find_element(&provider, selector);
904            match action {
905                "press" => el.press().unwrap(),
906                "focus" => el.focus().unwrap(),
907                "blur" => el.blur().unwrap(),
908                "toggle" => el.toggle().unwrap(),
909                "select" => el.select().unwrap(),
910                "expand" => el.expand().unwrap(),
911                "collapse" => el.collapse().unwrap(),
912                "show_menu" => el.show_menu().unwrap(),
913                "increment" => el.increment().unwrap(),
914                "decrement" => el.decrement().unwrap(),
915                "scroll_into_view" => el.scroll_into_view().unwrap(),
916                _ => unreachable!(),
917            }
918            let (handle, name, data) = last_action(&provider);
919            assert_eq!(
920                name, action,
921                "wrong action recorded for selector {selector}"
922            );
923            assert_eq!(data, None, "nullary action should not carry data");
924            assert_eq!(handle, el.data.handle);
925        }
926    }
927
928    #[test]
929    fn set_value_records_text_payload() {
930        let provider = build_provider();
931        let el = find_element(&provider, r#"text_field[name="Search"]"#);
932        el.set_value("world").unwrap();
933        let (handle, name, data) = last_action(&provider);
934        assert_eq!(handle, el.data.handle);
935        assert_eq!(name, "set_value");
936        assert_eq!(data.as_deref(), Some("world"));
937    }
938
939    #[test]
940    fn set_numeric_value_records_payload() {
941        let provider = build_provider();
942        let el = find_element(&provider, r#"slider[name="Volume"]"#);
943        el.set_numeric_value(42.0).unwrap();
944        let (_, name, data) = last_action(&provider);
945        assert_eq!(name, "set_numeric_value");
946        assert_eq!(data.as_deref(), Some("42"));
947    }
948
949    #[test]
950    fn set_numeric_value_rejects_non_finite() {
951        let provider = build_provider();
952        let el = find_element(&provider, r#"slider[name="Volume"]"#);
953        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
954            assert!(matches!(
955                el.set_numeric_value(bad),
956                Err(Error::InvalidActionData { .. })
957            ));
958        }
959        // None of the validation failures should have reached the provider.
960        assert!(provider.actions().is_empty());
961    }
962
963    #[test]
964    fn type_text_records_payload() {
965        let provider = build_provider();
966        let el = find_element(&provider, r#"text_field[name="Search"]"#);
967        el.type_text("abc").unwrap();
968        let (_, name, data) = last_action(&provider);
969        assert_eq!(name, "type_text");
970        assert_eq!(data.as_deref(), Some("abc"));
971    }
972
973    #[test]
974    fn select_text_records_range() {
975        let provider = build_provider();
976        let el = find_element(&provider, r#"text_field[name="Search"]"#);
977        el.select_text(1, 4).unwrap();
978        let (_, name, data) = last_action(&provider);
979        assert_eq!(name, "set_text_selection");
980        assert_eq!(data.as_deref(), Some("1..4"));
981    }
982
983    #[test]
984    fn select_text_rejects_inverted_range() {
985        let provider = build_provider();
986        let el = find_element(&provider, r#"text_field[name="Search"]"#);
987        assert!(matches!(
988            el.select_text(5, 2),
989            Err(Error::InvalidActionData { .. })
990        ));
991        assert!(provider.actions().is_empty());
992    }
993
994    #[test]
995    fn perform_action_records_arbitrary_name() {
996        let provider = build_provider();
997        let el = find_element(&provider, r#"button[name="Back"]"#);
998        el.perform_action("raise").unwrap();
999        let (_, name, _) = last_action(&provider);
1000        assert_eq!(name, "raise");
1001    }
1002
1003    #[test]
1004    fn locator_actions_desugar_to_element_actions() {
1005        // Locator's auto-wait wraps the resolved data in an Element and calls
1006        // its action — no duplication at the provider call site. This test
1007        // pins that behavior: pressing via the Locator should record exactly
1008        // the same entry as pressing via the Element it resolves to.
1009        let provider = build_provider();
1010        let provider_dyn: Arc<dyn Provider> = provider.clone();
1011        let locator = crate::locator::Locator::new(provider_dyn, None, r#"button[name="Back"]"#);
1012        locator.press().unwrap();
1013        let (_, name, data) = last_action(&provider);
1014        assert_eq!(name, "press");
1015        assert_eq!(data, None);
1016    }
1017
1018    #[test]
1019    fn locator_validation_runs_before_auto_wait() {
1020        // Locator validates payloads before entering its 5s auto-wait poll.
1021        // We verify by passing invalid input against a never-matching selector:
1022        // if validation fired first we get InvalidActionData immediately, not
1023        // a Timeout 5 seconds later.
1024        let provider = build_provider();
1025        let provider_dyn: Arc<dyn Provider> = provider.clone();
1026        let locator =
1027            crate::locator::Locator::new(provider_dyn, None, r#"button[name="never-matches"]"#);
1028        let started = std::time::Instant::now();
1029        let err = locator.set_numeric_value(f64::NAN).unwrap_err();
1030        assert!(matches!(err, Error::InvalidActionData { .. }));
1031        assert!(
1032            started.elapsed() < std::time::Duration::from_secs(1),
1033            "validation must short-circuit auto-wait",
1034        );
1035    }
1036}