Skip to main content

fret_core/
semantics.rs

1use std::collections::HashSet;
2
3use crate::{AppWindowId, NodeId, Rect};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum SemanticsRole {
8    Generic,
9    Window,
10    Panel,
11    Group,
12    /// A landmark region.
13    ///
14    /// This is primarily used to model ARIA `role="region"` outcomes in ports of DOM-first
15    /// component libraries (e.g. Radix Accordion content panels).
16    Region,
17    Toolbar,
18    Heading,
19    Dialog,
20    AlertDialog,
21    Alert,
22    /// A non-interactive advisory live region (ARIA `role="status"`).
23    Status,
24    /// A chronological stream of advisory updates (ARIA `role="log"`).
25    Log,
26    Button,
27    Link,
28    Image,
29    Checkbox,
30    Switch,
31    Slider,
32    SpinButton,
33    ProgressBar,
34    Meter,
35    ScrollBar,
36    Splitter,
37    ComboBox,
38    RadioGroup,
39    RadioButton,
40    TabList,
41    Tab,
42    TabPanel,
43    MenuBar,
44    Menu,
45    MenuItem,
46    MenuItemCheckbox,
47    MenuItemRadio,
48    Tooltip,
49    Text,
50    TextField,
51    List,
52    ListItem,
53    Separator,
54    ListBox,
55    ListBoxOption,
56    TreeItem,
57    Viewport,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum SemanticsOrientation {
63    Horizontal,
64    Vertical,
65}
66
67#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
68pub struct SemanticsActions {
69    pub focus: bool,
70    pub invoke: bool,
71    pub set_value: bool,
72    /// Decrement a numeric value by one step.
73    pub decrement: bool,
74    /// Increment a numeric value by one step.
75    pub increment: bool,
76    pub scroll_by: bool,
77    pub set_text_selection: bool,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum SemanticsCheckedState {
83    False,
84    True,
85    Mixed,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum SemanticsPressedState {
91    False,
92    True,
93    Mixed,
94}
95
96/// Indicates if a form control has invalid input (ARIA `aria-invalid` class).
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum SemanticsInvalid {
100    True,
101    Grammar,
102    Spelling,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum SemanticsLive {
108    Off,
109    Polite,
110    Assertive,
111}
112
113#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
114pub struct SemanticsFlags {
115    pub focused: bool,
116    pub captured: bool,
117    pub disabled: bool,
118    pub read_only: bool,
119    /// Exclude this node (and its subtree) from the accessibility tree presented to assistive
120    /// technologies.
121    ///
122    /// This is a portable approximation of ARIA `aria-hidden`.
123    pub hidden: bool,
124    /// Indicates that a link has been visited.
125    ///
126    /// This is a portable approximation of the "visited link" concept in HTML.
127    pub visited: bool,
128    /// Indicates that this collection supports selecting multiple items.
129    ///
130    /// This is a portable approximation of ARIA `aria-multiselectable`.
131    pub multiselectable: bool,
132    /// When set, indicates that this node is a live region (ARIA `aria-live`).
133    ///
134    /// `None` means no live region semantics are requested.
135    pub live: Option<SemanticsLive>,
136    /// When true, indicates that updates to this live region should be presented atomically
137    /// (ARIA `aria-atomic`).
138    pub live_atomic: bool,
139    pub selected: bool,
140    pub expanded: bool,
141    /// Legacy binary checked state.
142    ///
143    /// Prefer `checked_state` for tri-state widgets.
144    pub checked: Option<bool>,
145    /// Tri-state checked state (None = not checkable / unknown).
146    pub checked_state: Option<SemanticsCheckedState>,
147    /// Tri-state pressed state for toggle-button-like widgets (None = not a toggle / unknown).
148    pub pressed_state: Option<SemanticsPressedState>,
149    /// Indicates that a form field is required to be filled in.
150    pub required: bool,
151    /// Indicates that a form control has invalid input.
152    pub invalid: Option<SemanticsInvalid>,
153    /// Indicates that this node (and typically its subtree) is currently busy (e.g. loading).
154    ///
155    /// This is a portable approximation of ARIA `aria-busy`.
156    pub busy: bool,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct SemanticsInlineSpan {
161    /// UTF-8 byte range `(start, end)` into `SemanticsNode::value`.
162    pub range_utf8: (u32, u32),
163    pub role: SemanticsRole,
164    /// Opaque, component-defined tag (e.g. a URL for markdown links).
165    pub tag: Option<String>,
166}
167
168#[derive(Debug, Default, Clone, Copy, PartialEq)]
169pub struct SemanticsNumeric {
170    pub value: Option<f64>,
171    pub min: Option<f64>,
172    pub max: Option<f64>,
173    pub step: Option<f64>,
174    pub jump: Option<f64>,
175}
176
177#[derive(Debug, Default, Clone, Copy, PartialEq)]
178pub struct SemanticsScroll {
179    pub x: Option<f64>,
180    pub x_min: Option<f64>,
181    pub x_max: Option<f64>,
182    pub y: Option<f64>,
183    pub y_min: Option<f64>,
184    pub y_max: Option<f64>,
185}
186
187#[derive(Debug, Default, Clone, PartialEq)]
188pub struct SemanticsNodeExtra {
189    pub placeholder: Option<String>,
190    pub url: Option<String>,
191    /// Optional role description override (ARIA `aria-roledescription`-like outcome).
192    pub role_description: Option<String>,
193    /// Optional hierarchy level for outline/tree semantics (1-based).
194    pub level: Option<u32>,
195    pub orientation: Option<SemanticsOrientation>,
196    pub numeric: SemanticsNumeric,
197    pub scroll: SemanticsScroll,
198}
199
200#[derive(Debug, Clone)]
201pub struct SemanticsNode {
202    pub id: NodeId,
203    pub parent: Option<NodeId>,
204    pub role: SemanticsRole,
205    pub bounds: Rect,
206    pub flags: SemanticsFlags,
207    /// Debug/test-only identifier for deterministic automation.
208    ///
209    /// This MUST NOT be mapped into platform accessibility name/label fields by default.
210    pub test_id: Option<String>,
211    /// When this node retains actual keyboard focus but another descendant is the current
212    /// "active item" (e.g. composite widgets using `aria-activedescendant`), this points to that
213    /// active descendant node.
214    pub active_descendant: Option<NodeId>,
215    /// 1-based position of this node within a logical collection (e.g. listbox/menu items).
216    ///
217    /// This is used to support accessible large/virtualized collections where only a window of
218    /// items is present in the semantics snapshot.
219    pub pos_in_set: Option<u32>,
220    /// Total number of items in the logical collection that this node belongs to.
221    ///
222    /// This is used to support accessible large/virtualized collections where only a window of
223    /// items is present in the semantics snapshot.
224    pub set_size: Option<u32>,
225    /// Human-readable name/label for assistive technologies.
226    pub label: Option<String>,
227    /// Value text, typically for text fields and sliders.
228    pub value: Option<String>,
229    pub extra: SemanticsNodeExtra,
230    /// Text selection in UTF-8 byte offsets within `value` (ADR 0071).
231    ///
232    /// This is `(anchor, focus)` to preserve selection direction for assistive technologies.
233    pub text_selection: Option<(u32, u32)>,
234    /// IME composition range in UTF-8 byte offsets within `value` (ADR 0071).
235    ///
236    /// This is a best-effort signal for accessibility and may be omitted by implementations that
237    /// cannot represent composition distinctly.
238    pub text_composition: Option<(u32, u32)>,
239    /// Supported actions for assistive technologies and automation.
240    pub actions: SemanticsActions,
241    /// Nodes which provide this node's accessible name.
242    ///
243    /// This is a portable approximation of relations such as `aria-labelledby`.
244    pub labelled_by: Vec<NodeId>,
245    /// Nodes which provide this node's accessible description.
246    ///
247    /// This is a portable approximation of relations such as `aria-describedby`.
248    pub described_by: Vec<NodeId>,
249    /// Nodes which this node controls.
250    ///
251    /// This is a portable approximation of relations such as `aria-controls`.
252    pub controls: Vec<NodeId>,
253    /// Inline semantics spans within this node's `value` (v1 metadata-only surface).
254    pub inline_spans: Vec<SemanticsInlineSpan>,
255}
256
257#[derive(Debug, Clone)]
258pub struct SemanticsRoot {
259    pub root: NodeId,
260    pub visible: bool,
261    pub blocks_underlay_input: bool,
262    pub hit_testable: bool,
263    /// Paint order index within the window (0 = back/bottom).
264    pub z_index: u32,
265}
266
267#[derive(Debug, Default, Clone)]
268pub struct SemanticsSnapshot {
269    pub window: AppWindowId,
270    pub roots: Vec<SemanticsRoot>,
271    /// The root of the topmost modal layer (if any), matching ADR 0011/0033 semantics gating.
272    pub barrier_root: Option<NodeId>,
273    /// The root of the topmost focus-blocking layer (if any).
274    ///
275    /// This is intentionally decoupled from `barrier_root`: some overlay close transitions keep a
276    /// pointer barrier active while releasing focus containment.
277    pub focus_barrier_root: Option<NodeId>,
278    pub focus: Option<NodeId>,
279    pub captured: Option<NodeId>,
280    pub nodes: Vec<SemanticsNode>,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum SemanticsValidationField {
285    TextSelection,
286    TextComposition,
287    InlineSpan,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum SemanticsNumericField {
292    Value,
293    Min,
294    Max,
295    Step,
296    Jump,
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum SemanticsScrollField {
301    X,
302    XMin,
303    XMax,
304    Y,
305    YMin,
306    YMax,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum SemanticsScrollAxis {
311    X,
312    Y,
313}
314
315#[derive(Debug, Clone)]
316pub enum SemanticsValidationErrorKind {
317    MissingValueForTextRange {
318        field: SemanticsValidationField,
319    },
320    RangeOutOfBounds {
321        field: SemanticsValidationField,
322        start: u32,
323        end: u32,
324        len: u32,
325    },
326    RangeNotCharBoundary {
327        field: SemanticsValidationField,
328        offset: u32,
329    },
330    InvalidRangeOrder {
331        field: SemanticsValidationField,
332        start: u32,
333        end: u32,
334    },
335    DuplicateNodeId {
336        id: NodeId,
337    },
338    MissingReferencedNode {
339        field: SemanticsReferenceField,
340        referenced: NodeId,
341    },
342    InvalidCollectionMetadata {
343        pos_in_set: Option<u32>,
344        set_size: Option<u32>,
345    },
346    InvalidHierarchyLevel {
347        level: u32,
348    },
349    NonFiniteNumeric {
350        field: SemanticsNumericField,
351        value: f64,
352    },
353    InvalidNumericBounds {
354        min: f64,
355        max: f64,
356    },
357    NumericValueOutOfBounds {
358        value: f64,
359        min: f64,
360        max: f64,
361    },
362    InvalidNumericStep {
363        step: f64,
364    },
365    InvalidNumericJump {
366        jump: f64,
367    },
368    NonFiniteScroll {
369        field: SemanticsScrollField,
370        value: f64,
371    },
372    InvalidScrollBounds {
373        axis: SemanticsScrollAxis,
374        min: f64,
375        max: f64,
376    },
377    ScrollValueOutOfBounds {
378        axis: SemanticsScrollAxis,
379        value: f64,
380        min: f64,
381        max: f64,
382    },
383}
384
385impl PartialEq for SemanticsValidationErrorKind {
386    fn eq(&self, other: &Self) -> bool {
387        use SemanticsValidationErrorKind::*;
388        match (self, other) {
389            (MissingValueForTextRange { field: a }, MissingValueForTextRange { field: b }) => {
390                a == b
391            }
392            (
393                RangeOutOfBounds {
394                    field: a_field,
395                    start: a_start,
396                    end: a_end,
397                    len: a_len,
398                },
399                RangeOutOfBounds {
400                    field: b_field,
401                    start: b_start,
402                    end: b_end,
403                    len: b_len,
404                },
405            ) => a_field == b_field && a_start == b_start && a_end == b_end && a_len == b_len,
406            (
407                RangeNotCharBoundary {
408                    field: a_field,
409                    offset: a_offset,
410                },
411                RangeNotCharBoundary {
412                    field: b_field,
413                    offset: b_offset,
414                },
415            ) => a_field == b_field && a_offset == b_offset,
416            (
417                InvalidRangeOrder {
418                    field: a_field,
419                    start: a_start,
420                    end: a_end,
421                },
422                InvalidRangeOrder {
423                    field: b_field,
424                    start: b_start,
425                    end: b_end,
426                },
427            ) => a_field == b_field && a_start == b_start && a_end == b_end,
428            (DuplicateNodeId { id: a }, DuplicateNodeId { id: b }) => a == b,
429            (
430                MissingReferencedNode {
431                    field: a_field,
432                    referenced: a_referenced,
433                },
434                MissingReferencedNode {
435                    field: b_field,
436                    referenced: b_referenced,
437                },
438            ) => a_field == b_field && a_referenced == b_referenced,
439            (
440                InvalidCollectionMetadata {
441                    pos_in_set: a_pos_in_set,
442                    set_size: a_set_size,
443                },
444                InvalidCollectionMetadata {
445                    pos_in_set: b_pos_in_set,
446                    set_size: b_set_size,
447                },
448            ) => a_pos_in_set == b_pos_in_set && a_set_size == b_set_size,
449            (InvalidHierarchyLevel { level: a }, InvalidHierarchyLevel { level: b }) => a == b,
450            (
451                NonFiniteNumeric {
452                    field: a_field,
453                    value: a_value,
454                },
455                NonFiniteNumeric {
456                    field: b_field,
457                    value: b_value,
458                },
459            ) => a_field == b_field && a_value.to_bits() == b_value.to_bits(),
460            (
461                InvalidNumericBounds {
462                    min: a_min,
463                    max: a_max,
464                },
465                InvalidNumericBounds {
466                    min: b_min,
467                    max: b_max,
468                },
469            ) => a_min.to_bits() == b_min.to_bits() && a_max.to_bits() == b_max.to_bits(),
470            (
471                NumericValueOutOfBounds {
472                    value: a_value,
473                    min: a_min,
474                    max: a_max,
475                },
476                NumericValueOutOfBounds {
477                    value: b_value,
478                    min: b_min,
479                    max: b_max,
480                },
481            ) => {
482                a_value.to_bits() == b_value.to_bits()
483                    && a_min.to_bits() == b_min.to_bits()
484                    && a_max.to_bits() == b_max.to_bits()
485            }
486            (InvalidNumericStep { step: a }, InvalidNumericStep { step: b }) => {
487                a.to_bits() == b.to_bits()
488            }
489            (InvalidNumericJump { jump: a }, InvalidNumericJump { jump: b }) => {
490                a.to_bits() == b.to_bits()
491            }
492            (
493                NonFiniteScroll {
494                    field: a_field,
495                    value: a_value,
496                },
497                NonFiniteScroll {
498                    field: b_field,
499                    value: b_value,
500                },
501            ) => a_field == b_field && a_value.to_bits() == b_value.to_bits(),
502            (
503                InvalidScrollBounds {
504                    axis: a_axis,
505                    min: a_min,
506                    max: a_max,
507                },
508                InvalidScrollBounds {
509                    axis: b_axis,
510                    min: b_min,
511                    max: b_max,
512                },
513            ) => {
514                a_axis == b_axis
515                    && a_min.to_bits() == b_min.to_bits()
516                    && a_max.to_bits() == b_max.to_bits()
517            }
518            (
519                ScrollValueOutOfBounds {
520                    axis: a_axis,
521                    value: a_value,
522                    min: a_min,
523                    max: a_max,
524                },
525                ScrollValueOutOfBounds {
526                    axis: b_axis,
527                    value: b_value,
528                    min: b_min,
529                    max: b_max,
530                },
531            ) => {
532                a_axis == b_axis
533                    && a_value.to_bits() == b_value.to_bits()
534                    && a_min.to_bits() == b_min.to_bits()
535                    && a_max.to_bits() == b_max.to_bits()
536            }
537            _ => false,
538        }
539    }
540}
541
542impl Eq for SemanticsValidationErrorKind {}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545pub enum SemanticsReferenceField {
546    Root,
547    BarrierRoot,
548    FocusBarrierRoot,
549    Focus,
550    Captured,
551    Parent,
552    ActiveDescendant,
553    LabelledBy,
554    DescribedBy,
555    Controls,
556}
557
558#[derive(Debug, Clone, PartialEq, Eq)]
559pub struct SemanticsValidationError {
560    pub node: NodeId,
561    pub kind: SemanticsValidationErrorKind,
562}
563
564impl SemanticsNode {
565    pub fn validate(&self) -> Result<(), SemanticsValidationError> {
566        validate_text_ranges(
567            self.id,
568            self.value.as_deref(),
569            self.text_selection,
570            self.text_composition,
571        )?;
572        validate_inline_spans(self.id, self.value.as_deref(), &self.inline_spans)?;
573        validate_extra(self.id, &self.extra)?;
574        Ok(())
575    }
576}
577
578impl SemanticsSnapshot {
579    pub fn validate(&self) -> Result<(), SemanticsValidationError> {
580        let mut ids = HashSet::with_capacity(self.nodes.len());
581        for node in &self.nodes {
582            if !ids.insert(node.id) {
583                return Err(SemanticsValidationError {
584                    node: node.id,
585                    kind: SemanticsValidationErrorKind::DuplicateNodeId { id: node.id },
586                });
587            }
588        }
589
590        let check_ref = |node: NodeId,
591                         field: SemanticsReferenceField,
592                         referenced: NodeId,
593                         ids: &HashSet<NodeId>|
594         -> Result<(), SemanticsValidationError> {
595            if ids.contains(&referenced) {
596                return Ok(());
597            }
598            Err(SemanticsValidationError {
599                node,
600                kind: SemanticsValidationErrorKind::MissingReferencedNode { field, referenced },
601            })
602        };
603
604        for root in &self.roots {
605            check_ref(root.root, SemanticsReferenceField::Root, root.root, &ids)?;
606        }
607        if let Some(barrier_root) = self.barrier_root {
608            check_ref(
609                barrier_root,
610                SemanticsReferenceField::BarrierRoot,
611                barrier_root,
612                &ids,
613            )?;
614        }
615        if let Some(focus_barrier_root) = self.focus_barrier_root {
616            check_ref(
617                focus_barrier_root,
618                SemanticsReferenceField::FocusBarrierRoot,
619                focus_barrier_root,
620                &ids,
621            )?;
622        }
623        if let Some(focus) = self.focus {
624            check_ref(focus, SemanticsReferenceField::Focus, focus, &ids)?;
625        }
626        if let Some(captured) = self.captured {
627            check_ref(captured, SemanticsReferenceField::Captured, captured, &ids)?;
628        }
629
630        for node in &self.nodes {
631            node.validate()?;
632
633            if node.pos_in_set.is_some() ^ node.set_size.is_some() {
634                return Err(SemanticsValidationError {
635                    node: node.id,
636                    kind: SemanticsValidationErrorKind::InvalidCollectionMetadata {
637                        pos_in_set: node.pos_in_set,
638                        set_size: node.set_size,
639                    },
640                });
641            }
642            if let (Some(pos_in_set), Some(set_size)) = (node.pos_in_set, node.set_size)
643                && (pos_in_set == 0 || set_size == 0 || pos_in_set > set_size)
644            {
645                return Err(SemanticsValidationError {
646                    node: node.id,
647                    kind: SemanticsValidationErrorKind::InvalidCollectionMetadata {
648                        pos_in_set: Some(pos_in_set),
649                        set_size: Some(set_size),
650                    },
651                });
652            }
653
654            if let Some(parent) = node.parent {
655                check_ref(node.id, SemanticsReferenceField::Parent, parent, &ids)?;
656            }
657            if let Some(active) = node.active_descendant {
658                check_ref(
659                    node.id,
660                    SemanticsReferenceField::ActiveDescendant,
661                    active,
662                    &ids,
663                )?;
664            }
665            for id in &node.labelled_by {
666                check_ref(node.id, SemanticsReferenceField::LabelledBy, *id, &ids)?;
667            }
668            for id in &node.described_by {
669                check_ref(node.id, SemanticsReferenceField::DescribedBy, *id, &ids)?;
670            }
671            for id in &node.controls {
672                check_ref(node.id, SemanticsReferenceField::Controls, *id, &ids)?;
673            }
674        }
675        Ok(())
676    }
677}
678
679fn validate_extra(
680    node: NodeId,
681    extra: &SemanticsNodeExtra,
682) -> Result<(), SemanticsValidationError> {
683    if let Some(level) = extra.level
684        && level == 0
685    {
686        return Err(SemanticsValidationError {
687            node,
688            kind: SemanticsValidationErrorKind::InvalidHierarchyLevel { level },
689        });
690    }
691
692    validate_numeric(node, extra.numeric)?;
693    validate_scroll(node, extra.scroll)?;
694    Ok(())
695}
696
697fn validate_numeric(
698    node: NodeId,
699    numeric: SemanticsNumeric,
700) -> Result<(), SemanticsValidationError> {
701    let check_finite =
702        |field: SemanticsNumericField, value: f64| -> Result<(), SemanticsValidationError> {
703            if value.is_finite() {
704                Ok(())
705            } else {
706                Err(SemanticsValidationError {
707                    node,
708                    kind: SemanticsValidationErrorKind::NonFiniteNumeric { field, value },
709                })
710            }
711        };
712
713    if let Some(value) = numeric.value {
714        check_finite(SemanticsNumericField::Value, value)?;
715    }
716    if let Some(min) = numeric.min {
717        check_finite(SemanticsNumericField::Min, min)?;
718    }
719    if let Some(max) = numeric.max {
720        check_finite(SemanticsNumericField::Max, max)?;
721    }
722    if let Some(step) = numeric.step {
723        check_finite(SemanticsNumericField::Step, step)?;
724    }
725    if let Some(jump) = numeric.jump {
726        check_finite(SemanticsNumericField::Jump, jump)?;
727    }
728
729    if let (Some(min), Some(max)) = (numeric.min, numeric.max)
730        && min > max
731    {
732        return Err(SemanticsValidationError {
733            node,
734            kind: SemanticsValidationErrorKind::InvalidNumericBounds { min, max },
735        });
736    }
737
738    if let Some(step) = numeric.step
739        && step <= 0.0
740    {
741        return Err(SemanticsValidationError {
742            node,
743            kind: SemanticsValidationErrorKind::InvalidNumericStep { step },
744        });
745    }
746    if let Some(jump) = numeric.jump
747        && jump <= 0.0
748    {
749        return Err(SemanticsValidationError {
750            node,
751            kind: SemanticsValidationErrorKind::InvalidNumericJump { jump },
752        });
753    }
754
755    if let (Some(value), Some(min), Some(max)) = (numeric.value, numeric.min, numeric.max)
756        && (value < min || value > max)
757    {
758        return Err(SemanticsValidationError {
759            node,
760            kind: SemanticsValidationErrorKind::NumericValueOutOfBounds { value, min, max },
761        });
762    }
763
764    Ok(())
765}
766
767fn validate_scroll(node: NodeId, scroll: SemanticsScroll) -> Result<(), SemanticsValidationError> {
768    const EPS: f64 = 1e-9;
769
770    let check_finite =
771        |field: SemanticsScrollField, value: f64| -> Result<(), SemanticsValidationError> {
772            if value.is_finite() {
773                Ok(())
774            } else {
775                Err(SemanticsValidationError {
776                    node,
777                    kind: SemanticsValidationErrorKind::NonFiniteScroll { field, value },
778                })
779            }
780        };
781
782    if let Some(x) = scroll.x {
783        check_finite(SemanticsScrollField::X, x)?;
784    }
785    if let Some(x_min) = scroll.x_min {
786        check_finite(SemanticsScrollField::XMin, x_min)?;
787    }
788    if let Some(x_max) = scroll.x_max {
789        check_finite(SemanticsScrollField::XMax, x_max)?;
790    }
791    if let Some(y) = scroll.y {
792        check_finite(SemanticsScrollField::Y, y)?;
793    }
794    if let Some(y_min) = scroll.y_min {
795        check_finite(SemanticsScrollField::YMin, y_min)?;
796    }
797    if let Some(y_max) = scroll.y_max {
798        check_finite(SemanticsScrollField::YMax, y_max)?;
799    }
800
801    if let (Some(min), Some(max)) = (scroll.x_min, scroll.x_max)
802        && min > max
803    {
804        return Err(SemanticsValidationError {
805            node,
806            kind: SemanticsValidationErrorKind::InvalidScrollBounds {
807                axis: SemanticsScrollAxis::X,
808                min,
809                max,
810            },
811        });
812    }
813    if let (Some(value), Some(min), Some(max)) = (scroll.x, scroll.x_min, scroll.x_max)
814        && (value < min - EPS || value > max + EPS)
815    {
816        return Err(SemanticsValidationError {
817            node,
818            kind: SemanticsValidationErrorKind::ScrollValueOutOfBounds {
819                axis: SemanticsScrollAxis::X,
820                value,
821                min,
822                max,
823            },
824        });
825    }
826
827    if let (Some(min), Some(max)) = (scroll.y_min, scroll.y_max)
828        && min > max
829    {
830        return Err(SemanticsValidationError {
831            node,
832            kind: SemanticsValidationErrorKind::InvalidScrollBounds {
833                axis: SemanticsScrollAxis::Y,
834                min,
835                max,
836            },
837        });
838    }
839    if let (Some(value), Some(min), Some(max)) = (scroll.y, scroll.y_min, scroll.y_max)
840        && (value < min - EPS || value > max + EPS)
841    {
842        return Err(SemanticsValidationError {
843            node,
844            kind: SemanticsValidationErrorKind::ScrollValueOutOfBounds {
845                axis: SemanticsScrollAxis::Y,
846                value,
847                min,
848                max,
849            },
850        });
851    }
852
853    Ok(())
854}
855
856fn validate_text_ranges(
857    node: NodeId,
858    value: Option<&str>,
859    text_selection: Option<(u32, u32)>,
860    text_composition: Option<(u32, u32)>,
861) -> Result<(), SemanticsValidationError> {
862    if text_selection.is_none() && text_composition.is_none() {
863        return Ok(());
864    }
865
866    let Some(value) = value else {
867        return Err(SemanticsValidationError {
868            node,
869            kind: SemanticsValidationErrorKind::MissingValueForTextRange {
870                field: if text_selection.is_some() {
871                    SemanticsValidationField::TextSelection
872                } else {
873                    SemanticsValidationField::TextComposition
874                },
875            },
876        });
877    };
878
879    let len_u32 = u32::try_from(value.len()).unwrap_or(u32::MAX);
880
881    let check_range = |field: SemanticsValidationField,
882                       start: u32,
883                       end: u32|
884     -> Result<(), SemanticsValidationError> {
885        if start > end {
886            return Err(SemanticsValidationError {
887                node,
888                kind: SemanticsValidationErrorKind::InvalidRangeOrder { field, start, end },
889            });
890        }
891        if start > len_u32 || end > len_u32 {
892            return Err(SemanticsValidationError {
893                node,
894                kind: SemanticsValidationErrorKind::RangeOutOfBounds {
895                    field,
896                    start,
897                    end,
898                    len: len_u32,
899                },
900            });
901        }
902
903        let start_usize = start as usize;
904        let end_usize = end as usize;
905        if !value.is_char_boundary(start_usize) {
906            return Err(SemanticsValidationError {
907                node,
908                kind: SemanticsValidationErrorKind::RangeNotCharBoundary {
909                    field,
910                    offset: start,
911                },
912            });
913        }
914        if !value.is_char_boundary(end_usize) {
915            return Err(SemanticsValidationError {
916                node,
917                kind: SemanticsValidationErrorKind::RangeNotCharBoundary { field, offset: end },
918            });
919        }
920        Ok(())
921    };
922
923    if let Some((anchor, focus)) = text_selection {
924        let (start, end) = if anchor <= focus {
925            (anchor, focus)
926        } else {
927            (focus, anchor)
928        };
929        check_range(SemanticsValidationField::TextSelection, start, end)?;
930    }
931
932    if let Some((start, end)) = text_composition {
933        check_range(SemanticsValidationField::TextComposition, start, end)?;
934    }
935
936    Ok(())
937}
938
939fn validate_inline_spans(
940    node: NodeId,
941    value: Option<&str>,
942    spans: &[SemanticsInlineSpan],
943) -> Result<(), SemanticsValidationError> {
944    if spans.is_empty() {
945        return Ok(());
946    }
947
948    let Some(value) = value else {
949        return Err(SemanticsValidationError {
950            node,
951            kind: SemanticsValidationErrorKind::MissingValueForTextRange {
952                field: SemanticsValidationField::InlineSpan,
953            },
954        });
955    };
956
957    let len_u32 = u32::try_from(value.len()).unwrap_or(u32::MAX);
958    for span in spans {
959        let (start, end) = span.range_utf8;
960        if start > end {
961            return Err(SemanticsValidationError {
962                node,
963                kind: SemanticsValidationErrorKind::InvalidRangeOrder {
964                    field: SemanticsValidationField::InlineSpan,
965                    start,
966                    end,
967                },
968            });
969        }
970        if start > len_u32 || end > len_u32 {
971            return Err(SemanticsValidationError {
972                node,
973                kind: SemanticsValidationErrorKind::RangeOutOfBounds {
974                    field: SemanticsValidationField::InlineSpan,
975                    start,
976                    end,
977                    len: len_u32,
978                },
979            });
980        }
981
982        let start_usize = start as usize;
983        let end_usize = end as usize;
984        if !value.is_char_boundary(start_usize) {
985            return Err(SemanticsValidationError {
986                node,
987                kind: SemanticsValidationErrorKind::RangeNotCharBoundary {
988                    field: SemanticsValidationField::InlineSpan,
989                    offset: start,
990                },
991            });
992        }
993        if !value.is_char_boundary(end_usize) {
994            return Err(SemanticsValidationError {
995                node,
996                kind: SemanticsValidationErrorKind::RangeNotCharBoundary {
997                    field: SemanticsValidationField::InlineSpan,
998                    offset: end,
999                },
1000            });
1001        }
1002    }
1003
1004    Ok(())
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010    use slotmap::KeyData;
1011
1012    fn node(id: u64) -> NodeId {
1013        NodeId::from(KeyData::from_ffi(id))
1014    }
1015
1016    fn snapshot_with_nodes(nodes: Vec<SemanticsNode>) -> SemanticsSnapshot {
1017        SemanticsSnapshot {
1018            window: AppWindowId::default(),
1019            roots: vec![SemanticsRoot {
1020                root: nodes.first().expect("at least one node").id,
1021                visible: true,
1022                blocks_underlay_input: false,
1023                hit_testable: true,
1024                z_index: 0,
1025            }],
1026            barrier_root: None,
1027            focus_barrier_root: None,
1028            focus: None,
1029            captured: None,
1030            nodes,
1031        }
1032    }
1033
1034    fn base_node(extra: SemanticsNodeExtra) -> SemanticsNode {
1035        SemanticsNode {
1036            id: node(1),
1037            parent: None,
1038            role: SemanticsRole::Slider,
1039            bounds: Rect::default(),
1040            flags: SemanticsFlags::default(),
1041            test_id: None,
1042            active_descendant: None,
1043            pos_in_set: None,
1044            set_size: None,
1045            label: None,
1046            value: None,
1047            extra,
1048            text_selection: None,
1049            text_composition: None,
1050            actions: SemanticsActions::default(),
1051            labelled_by: Vec::new(),
1052            described_by: Vec::new(),
1053            controls: Vec::new(),
1054            inline_spans: Vec::new(),
1055        }
1056    }
1057
1058    #[test]
1059    fn validates_utf8_char_boundaries_for_text_ranges() {
1060        let n = SemanticsNode {
1061            id: node(1),
1062            parent: None,
1063            role: SemanticsRole::TextField,
1064            bounds: Rect::default(),
1065            flags: SemanticsFlags::default(),
1066            test_id: None,
1067            active_descendant: None,
1068            pos_in_set: None,
1069            set_size: None,
1070            label: None,
1071            value: Some("😀".to_string()), // 4 bytes
1072            extra: SemanticsNodeExtra::default(),
1073            text_selection: Some((0, 4)),
1074            text_composition: Some((0, 4)),
1075            actions: SemanticsActions::default(),
1076            labelled_by: Vec::new(),
1077            described_by: Vec::new(),
1078            controls: Vec::new(),
1079            inline_spans: Vec::new(),
1080        };
1081        n.validate().expect("valid ranges should pass");
1082
1083        let bad = SemanticsNode {
1084            text_selection: Some((0, 2)),
1085            ..n
1086        };
1087        let err = bad.validate().expect_err("non-boundary should fail");
1088        assert_eq!(err.node, node(1));
1089        assert!(matches!(
1090            err.kind,
1091            SemanticsValidationErrorKind::RangeNotCharBoundary {
1092                field: SemanticsValidationField::TextSelection,
1093                offset: 2
1094            }
1095        ));
1096    }
1097
1098    #[test]
1099    fn rejects_ranges_without_value() {
1100        let n = SemanticsNode {
1101            id: node(1),
1102            parent: None,
1103            role: SemanticsRole::TextField,
1104            bounds: Rect::default(),
1105            flags: SemanticsFlags::default(),
1106            test_id: None,
1107            active_descendant: None,
1108            pos_in_set: None,
1109            set_size: None,
1110            label: None,
1111            value: None,
1112            extra: SemanticsNodeExtra::default(),
1113            text_selection: Some((0, 0)),
1114            text_composition: None,
1115            actions: SemanticsActions::default(),
1116            labelled_by: Vec::new(),
1117            described_by: Vec::new(),
1118            controls: Vec::new(),
1119            inline_spans: Vec::new(),
1120        };
1121        let err = n.validate().expect_err("range without value should fail");
1122        assert!(matches!(
1123            err.kind,
1124            SemanticsValidationErrorKind::MissingValueForTextRange { .. }
1125        ));
1126    }
1127
1128    #[test]
1129    fn rejects_inline_spans_without_value() {
1130        let n = SemanticsNode {
1131            id: node(1),
1132            parent: None,
1133            role: SemanticsRole::Text,
1134            bounds: Rect::default(),
1135            flags: SemanticsFlags::default(),
1136            test_id: None,
1137            active_descendant: None,
1138            pos_in_set: None,
1139            set_size: None,
1140            label: None,
1141            value: None,
1142            extra: SemanticsNodeExtra::default(),
1143            text_selection: None,
1144            text_composition: None,
1145            actions: SemanticsActions::default(),
1146            labelled_by: Vec::new(),
1147            described_by: Vec::new(),
1148            controls: Vec::new(),
1149            inline_spans: vec![SemanticsInlineSpan {
1150                range_utf8: (0, 0),
1151                role: SemanticsRole::Link,
1152                tag: None,
1153            }],
1154        };
1155        let err = n
1156            .validate()
1157            .expect_err("inline spans without value should fail");
1158        assert!(matches!(
1159            err.kind,
1160            SemanticsValidationErrorKind::MissingValueForTextRange {
1161                field: SemanticsValidationField::InlineSpan
1162            }
1163        ));
1164    }
1165
1166    #[test]
1167    fn validates_extra_numeric_and_scroll_metadata() {
1168        let n = base_node(SemanticsNodeExtra {
1169            level: Some(1),
1170            numeric: SemanticsNumeric {
1171                value: Some(5.0),
1172                min: Some(0.0),
1173                max: Some(10.0),
1174                step: Some(1.0),
1175                jump: Some(5.0),
1176            },
1177            scroll: SemanticsScroll {
1178                x: Some(0.0),
1179                x_min: Some(0.0),
1180                x_max: Some(0.0),
1181                y: Some(10.0),
1182                y_min: Some(0.0),
1183                y_max: Some(10.0),
1184            },
1185            ..SemanticsNodeExtra::default()
1186        });
1187        n.validate().expect("valid extra metadata should pass");
1188    }
1189
1190    #[test]
1191    fn rejects_invalid_hierarchy_level() {
1192        let n = base_node(SemanticsNodeExtra {
1193            level: Some(0),
1194            ..SemanticsNodeExtra::default()
1195        });
1196        let err = n.validate().expect_err("level=0 should fail");
1197        assert!(matches!(
1198            err.kind,
1199            SemanticsValidationErrorKind::InvalidHierarchyLevel { level: 0 }
1200        ));
1201    }
1202
1203    #[test]
1204    fn rejects_non_finite_numeric_metadata() {
1205        let n = base_node(SemanticsNodeExtra {
1206            numeric: SemanticsNumeric {
1207                value: Some(f64::NAN),
1208                ..SemanticsNumeric::default()
1209            },
1210            ..SemanticsNodeExtra::default()
1211        });
1212        let err = n.validate().expect_err("NaN should fail");
1213        assert!(matches!(
1214            err.kind,
1215            SemanticsValidationErrorKind::NonFiniteNumeric {
1216                field: SemanticsNumericField::Value,
1217                ..
1218            }
1219        ));
1220    }
1221
1222    #[test]
1223    fn rejects_invalid_numeric_bounds() {
1224        let n = base_node(SemanticsNodeExtra {
1225            numeric: SemanticsNumeric {
1226                min: Some(10.0),
1227                max: Some(5.0),
1228                ..SemanticsNumeric::default()
1229            },
1230            ..SemanticsNodeExtra::default()
1231        });
1232        let err = n.validate().expect_err("min > max should fail");
1233        assert!(matches!(
1234            err.kind,
1235            SemanticsValidationErrorKind::InvalidNumericBounds { .. }
1236        ));
1237    }
1238
1239    #[test]
1240    fn rejects_numeric_value_out_of_bounds() {
1241        let n = base_node(SemanticsNodeExtra {
1242            numeric: SemanticsNumeric {
1243                value: Some(11.0),
1244                min: Some(0.0),
1245                max: Some(10.0),
1246                ..SemanticsNumeric::default()
1247            },
1248            ..SemanticsNodeExtra::default()
1249        });
1250        let err = n.validate().expect_err("out-of-bounds should fail");
1251        assert!(matches!(
1252            err.kind,
1253            SemanticsValidationErrorKind::NumericValueOutOfBounds { .. }
1254        ));
1255    }
1256
1257    #[test]
1258    fn rejects_non_positive_numeric_step_and_jump() {
1259        let step = base_node(SemanticsNodeExtra {
1260            numeric: SemanticsNumeric {
1261                step: Some(0.0),
1262                ..SemanticsNumeric::default()
1263            },
1264            ..SemanticsNodeExtra::default()
1265        });
1266        let err = step.validate().expect_err("step <= 0 should fail");
1267        assert!(matches!(
1268            err.kind,
1269            SemanticsValidationErrorKind::InvalidNumericStep { .. }
1270        ));
1271
1272        let jump = base_node(SemanticsNodeExtra {
1273            numeric: SemanticsNumeric {
1274                jump: Some(-1.0),
1275                ..SemanticsNumeric::default()
1276            },
1277            ..SemanticsNodeExtra::default()
1278        });
1279        let err = jump.validate().expect_err("jump <= 0 should fail");
1280        assert!(matches!(
1281            err.kind,
1282            SemanticsValidationErrorKind::InvalidNumericJump { .. }
1283        ));
1284    }
1285
1286    #[test]
1287    fn rejects_non_finite_scroll_metadata() {
1288        let n = base_node(SemanticsNodeExtra {
1289            scroll: SemanticsScroll {
1290                y: Some(f64::INFINITY),
1291                ..SemanticsScroll::default()
1292            },
1293            ..SemanticsNodeExtra::default()
1294        });
1295        let err = n.validate().expect_err("Infinity should fail");
1296        assert!(matches!(
1297            err.kind,
1298            SemanticsValidationErrorKind::NonFiniteScroll {
1299                field: SemanticsScrollField::Y,
1300                ..
1301            }
1302        ));
1303    }
1304
1305    #[test]
1306    fn rejects_invalid_scroll_bounds_and_value_out_of_bounds() {
1307        let bounds = base_node(SemanticsNodeExtra {
1308            scroll: SemanticsScroll {
1309                x_min: Some(10.0),
1310                x_max: Some(5.0),
1311                ..SemanticsScroll::default()
1312            },
1313            ..SemanticsNodeExtra::default()
1314        });
1315        let err = bounds.validate().expect_err("x_min > x_max should fail");
1316        assert!(matches!(
1317            err.kind,
1318            SemanticsValidationErrorKind::InvalidScrollBounds {
1319                axis: SemanticsScrollAxis::X,
1320                ..
1321            }
1322        ));
1323
1324        let oob = base_node(SemanticsNodeExtra {
1325            scroll: SemanticsScroll {
1326                y: Some(11.0),
1327                y_min: Some(0.0),
1328                y_max: Some(10.0),
1329                ..SemanticsScroll::default()
1330            },
1331            ..SemanticsNodeExtra::default()
1332        });
1333        let err = oob.validate().expect_err("y out-of-bounds should fail");
1334        assert!(matches!(
1335            err.kind,
1336            SemanticsValidationErrorKind::ScrollValueOutOfBounds {
1337                axis: SemanticsScrollAxis::Y,
1338                ..
1339            }
1340        ));
1341    }
1342
1343    #[test]
1344    fn validates_utf8_char_boundaries_for_inline_spans() {
1345        let n = SemanticsNode {
1346            id: node(1),
1347            parent: None,
1348            role: SemanticsRole::Text,
1349            bounds: Rect::default(),
1350            flags: SemanticsFlags::default(),
1351            test_id: None,
1352            active_descendant: None,
1353            pos_in_set: None,
1354            set_size: None,
1355            label: None,
1356            value: Some("😀".to_string()), // 4 bytes
1357            extra: SemanticsNodeExtra::default(),
1358            text_selection: None,
1359            text_composition: None,
1360            actions: SemanticsActions::default(),
1361            labelled_by: Vec::new(),
1362            described_by: Vec::new(),
1363            controls: Vec::new(),
1364            inline_spans: vec![SemanticsInlineSpan {
1365                range_utf8: (0, 4),
1366                role: SemanticsRole::Link,
1367                tag: None,
1368            }],
1369        };
1370        n.validate()
1371            .expect("inline span on a utf-8 boundary should pass");
1372
1373        let bad = SemanticsNode {
1374            inline_spans: vec![SemanticsInlineSpan {
1375                range_utf8: (0, 2),
1376                role: SemanticsRole::Link,
1377                tag: None,
1378            }],
1379            ..n
1380        };
1381        let err = bad.validate().expect_err("non-boundary should fail");
1382        assert!(matches!(
1383            err.kind,
1384            SemanticsValidationErrorKind::RangeNotCharBoundary {
1385                field: SemanticsValidationField::InlineSpan,
1386                offset: 2
1387            }
1388        ));
1389    }
1390
1391    #[test]
1392    fn rejects_out_of_bounds_ranges() {
1393        let n = SemanticsNode {
1394            id: node(1),
1395            parent: None,
1396            role: SemanticsRole::TextField,
1397            bounds: Rect::default(),
1398            flags: SemanticsFlags::default(),
1399            test_id: None,
1400            active_descendant: None,
1401            pos_in_set: None,
1402            set_size: None,
1403            label: None,
1404            value: Some("abc".to_string()),
1405            extra: SemanticsNodeExtra::default(),
1406            text_selection: Some((0, 4)),
1407            text_composition: None,
1408            actions: SemanticsActions::default(),
1409            labelled_by: Vec::new(),
1410            described_by: Vec::new(),
1411            controls: Vec::new(),
1412            inline_spans: Vec::new(),
1413        };
1414        let err = n.validate().expect_err("oob should fail");
1415        assert!(matches!(
1416            err.kind,
1417            SemanticsValidationErrorKind::RangeOutOfBounds { .. }
1418        ));
1419    }
1420
1421    #[test]
1422    fn rejects_invalid_composition_order() {
1423        let n = SemanticsNode {
1424            id: node(1),
1425            parent: None,
1426            role: SemanticsRole::TextField,
1427            bounds: Rect::default(),
1428            flags: SemanticsFlags::default(),
1429            test_id: None,
1430            active_descendant: None,
1431            pos_in_set: None,
1432            set_size: None,
1433            label: None,
1434            value: Some("abc".to_string()),
1435            extra: SemanticsNodeExtra::default(),
1436            text_selection: None,
1437            text_composition: Some((2, 1)),
1438            actions: SemanticsActions::default(),
1439            labelled_by: Vec::new(),
1440            described_by: Vec::new(),
1441            controls: Vec::new(),
1442            inline_spans: Vec::new(),
1443        };
1444        let err = n.validate().expect_err("invalid order should fail");
1445        assert!(matches!(
1446            err.kind,
1447            SemanticsValidationErrorKind::InvalidRangeOrder {
1448                field: SemanticsValidationField::TextComposition,
1449                ..
1450            }
1451        ));
1452    }
1453
1454    #[test]
1455    fn rejects_duplicate_node_ids_in_snapshot() {
1456        let n1 = SemanticsNode {
1457            id: node(1),
1458            parent: None,
1459            role: SemanticsRole::Window,
1460            bounds: Rect::default(),
1461            flags: SemanticsFlags::default(),
1462            test_id: None,
1463            active_descendant: None,
1464            pos_in_set: None,
1465            set_size: None,
1466            label: None,
1467            value: None,
1468            extra: SemanticsNodeExtra::default(),
1469            text_selection: None,
1470            text_composition: None,
1471            actions: SemanticsActions::default(),
1472            labelled_by: Vec::new(),
1473            described_by: Vec::new(),
1474            controls: Vec::new(),
1475            inline_spans: Vec::new(),
1476        };
1477        let snap = snapshot_with_nodes(vec![n1.clone(), n1]);
1478        let err = snap.validate().expect_err("duplicate node ids should fail");
1479        assert!(matches!(
1480            err.kind,
1481            SemanticsValidationErrorKind::DuplicateNodeId { .. }
1482        ));
1483    }
1484
1485    #[test]
1486    fn rejects_missing_references() {
1487        let root = SemanticsNode {
1488            id: node(1),
1489            parent: None,
1490            role: SemanticsRole::Window,
1491            bounds: Rect::default(),
1492            flags: SemanticsFlags::default(),
1493            test_id: None,
1494            active_descendant: None,
1495            pos_in_set: None,
1496            set_size: None,
1497            label: None,
1498            value: None,
1499            extra: SemanticsNodeExtra::default(),
1500            text_selection: None,
1501            text_composition: None,
1502            actions: SemanticsActions::default(),
1503            labelled_by: Vec::new(),
1504            described_by: Vec::new(),
1505            controls: Vec::new(),
1506            inline_spans: Vec::new(),
1507        };
1508        let child = SemanticsNode {
1509            id: node(2),
1510            parent: Some(node(999)),
1511            role: SemanticsRole::Group,
1512            bounds: Rect::default(),
1513            flags: SemanticsFlags::default(),
1514            test_id: None,
1515            active_descendant: Some(node(998)),
1516            pos_in_set: None,
1517            set_size: None,
1518            label: None,
1519            value: None,
1520            extra: SemanticsNodeExtra::default(),
1521            text_selection: None,
1522            text_composition: None,
1523            actions: SemanticsActions::default(),
1524            labelled_by: vec![node(997)],
1525            described_by: vec![node(996)],
1526            controls: vec![node(995)],
1527            inline_spans: Vec::new(),
1528        };
1529
1530        let snap = snapshot_with_nodes(vec![root, child]);
1531        let err = snap.validate().expect_err("missing references should fail");
1532        assert!(matches!(
1533            err.kind,
1534            SemanticsValidationErrorKind::MissingReferencedNode { .. }
1535        ));
1536    }
1537
1538    #[test]
1539    fn rejects_invalid_collection_metadata() {
1540        let root = SemanticsNode {
1541            id: node(1),
1542            parent: None,
1543            role: SemanticsRole::Window,
1544            bounds: Rect::default(),
1545            flags: SemanticsFlags::default(),
1546            test_id: None,
1547            active_descendant: None,
1548            pos_in_set: None,
1549            set_size: None,
1550            label: None,
1551            value: None,
1552            extra: SemanticsNodeExtra::default(),
1553            text_selection: None,
1554            text_composition: None,
1555            actions: SemanticsActions::default(),
1556            labelled_by: Vec::new(),
1557            described_by: Vec::new(),
1558            controls: Vec::new(),
1559            inline_spans: Vec::new(),
1560        };
1561
1562        let bad_missing_peer = SemanticsNode {
1563            id: node(2),
1564            parent: Some(node(1)),
1565            role: SemanticsRole::ListItem,
1566            bounds: Rect::default(),
1567            flags: SemanticsFlags::default(),
1568            test_id: None,
1569            active_descendant: None,
1570            pos_in_set: Some(1),
1571            set_size: None,
1572            label: None,
1573            value: None,
1574            extra: SemanticsNodeExtra::default(),
1575            text_selection: None,
1576            text_composition: None,
1577            actions: SemanticsActions::default(),
1578            labelled_by: Vec::new(),
1579            described_by: Vec::new(),
1580            controls: Vec::new(),
1581            inline_spans: Vec::new(),
1582        };
1583        let snap = snapshot_with_nodes(vec![root.clone(), bad_missing_peer]);
1584        let err = snap.validate().expect_err("missing set_size should fail");
1585        assert!(matches!(
1586            err.kind,
1587            SemanticsValidationErrorKind::InvalidCollectionMetadata { .. }
1588        ));
1589
1590        let bad_bounds = SemanticsNode {
1591            id: node(2),
1592            parent: Some(node(1)),
1593            role: SemanticsRole::ListItem,
1594            bounds: Rect::default(),
1595            flags: SemanticsFlags::default(),
1596            test_id: None,
1597            active_descendant: None,
1598            pos_in_set: Some(2),
1599            set_size: Some(1),
1600            label: None,
1601            value: None,
1602            extra: SemanticsNodeExtra::default(),
1603            text_selection: None,
1604            text_composition: None,
1605            actions: SemanticsActions::default(),
1606            labelled_by: Vec::new(),
1607            described_by: Vec::new(),
1608            controls: Vec::new(),
1609            inline_spans: Vec::new(),
1610        };
1611        let snap = snapshot_with_nodes(vec![root, bad_bounds]);
1612        let err = snap
1613            .validate()
1614            .expect_err("pos_in_set > set_size should fail");
1615        assert!(matches!(
1616            err.kind,
1617            SemanticsValidationErrorKind::InvalidCollectionMetadata {
1618                pos_in_set: Some(2),
1619                set_size: Some(1),
1620            }
1621        ));
1622    }
1623}