Skip to main content

azul_core/
a11y.rs

1//! Accessibility types for screen reader support.
2//!
3//! Key types:
4//! - [`AccessibilityInfo`] — full accessibility metadata for a UI element
5//! - [`SmallAriaInfo`] — lightweight alternative for common cases (label + role + description)
6//! - [`AccessibilityRole`] — element purpose (button, link, checkbox, etc.)
7//! - [`AccessibilityState`] — dynamic state (focused, checked, expanded, etc.)
8//! - [`AccessibilityAction`] — actions performable on an element (click, scroll, etc.)
9//!
10//! These types are consumed by `layout/src/managers/a11y.rs` and mapped to
11//! platform accessibility backends in `dll/src/desktop/shell2/`.
12
13use alloc::vec::Vec;
14use azul_css::{
15    AzString, OptionF32, OptionString,
16    props::basic::length::FloatValue,
17};
18use crate::{
19    dom::OptionDomNodeId,
20    geom::LogicalPosition,
21    window::OptionVirtualKeyCodeCombo,
22};
23
24/// Holds information about a UI element for accessibility purposes (e.g., screen readers).
25/// This is a wrapper for platform-specific accessibility APIs like MSAA.
26#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
27#[repr(C)]
28pub struct AccessibilityInfo {
29    /// Get the "name" of the `IAccessible`, for example the
30    /// name of a button, checkbox or menu item. Try to use unique names
31    /// for each item in a dialog so that voice dictation software doesn't
32    /// have to deal with extra ambiguity.
33    pub accessibility_name: OptionString,
34    /// Get the "value" of the `IAccessible`, for example a number in a slider,
35    /// a URL for a link, the text a user entered in a field.
36    pub accessibility_value: OptionString,
37    /// Optional text description providing additional context about the element.
38    /// Maps to `aria-description` / accesskit's `set_description()`.
39    pub description: OptionString,
40    /// Optional keyboard accelerator.
41    pub accelerator: OptionVirtualKeyCodeCombo,
42    /// Optional "default action" description. Only used when there is at least
43    /// one `ComponentEventFilter::DefaultAction` callback present on this node.
44    pub default_action: OptionString,
45    /// Possible on/off states, such as focused, focusable, selected, selectable,
46    /// visible, protected (for passwords), checked, etc.
47    pub states: AccessibilityStateVec,
48    /// A list of actions the user can perform on this element.
49    /// Maps to accesskit's Action enum.
50    pub supported_actions: AccessibilityActionVec,
51    /// ID of another node that labels this one (for `aria-labelledby`).
52    pub labelled_by: OptionDomNodeId,
53    /// ID of another node that describes this one (for `aria-describedby`).
54    pub described_by: OptionDomNodeId,
55    /// Get an enumerated value representing what this `IAccessible` is used for,
56    /// for example is it a link, static text, editable text, a checkbox, or a table cell, etc.
57    pub role: AccessibilityRole,
58    /// For live regions that update automatically (e.g., chat messages, timers).
59    /// Maps to accesskit's `Live` property.
60    pub is_live_region: bool,
61}
62
63/// Actions that can be performed on an accessible element.
64/// This is a simplified version of `accesskit::Action` to avoid direct dependency in core.
65#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66#[repr(C, u8)]
67pub enum AccessibilityAction {
68    /// The default action for the element (usually a click).
69    Default,
70    /// Set focus to this element.
71    Focus,
72    /// Remove focus from this element.
73    Blur,
74    /// Collapse an expandable element (e.g., tree node, accordion).
75    Collapse,
76    /// Expand a collapsible element (e.g., tree node, accordion).
77    Expand,
78    /// Scroll this element into view.
79    ScrollIntoView,
80    /// Increment a numeric value (e.g., slider, spinner).
81    Increment,
82    /// Decrement a numeric value (e.g., slider, spinner).
83    Decrement,
84    /// Show a context menu.
85    ShowContextMenu,
86    /// Hide a tooltip.
87    HideTooltip,
88    /// Show a tooltip.
89    ShowTooltip,
90    /// Scroll up.
91    ScrollUp,
92    /// Scroll down.
93    ScrollDown,
94    /// Scroll left.
95    ScrollLeft,
96    /// Scroll right.
97    ScrollRight,
98    /// Replace selected text with new text.
99    ReplaceSelectedText(AzString),
100    /// Scroll to a specific point.
101    ScrollToPoint(LogicalPosition),
102    /// Set scroll offset.
103    SetScrollOffset(LogicalPosition),
104    /// Set text selection.
105    SetTextSelection(TextSelectionStartEnd),
106    /// Set sequential focus navigation starting point.
107    SetSequentialFocusNavigationStartingPoint,
108    /// Set the value of a control.
109    SetValue(AzString),
110    /// Set numeric value of a control.
111    SetNumericValue(FloatValue),
112    /// Custom action with ID.
113    CustomAction(i32),
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
117#[repr(C)]
118pub struct TextSelectionStartEnd {
119    pub selection_start: usize,
120    pub selection_end: usize,
121}
122
123impl_vec!(AccessibilityAction, AccessibilityActionVec, AccessibilityActionVecDestructor, AccessibilityActionVecDestructorType, AccessibilityActionVecSlice, OptionAccessibilityAction);
124impl_vec_debug!(AccessibilityAction, AccessibilityActionVec);
125impl_vec_clone!(
126    AccessibilityAction,
127    AccessibilityActionVec,
128    AccessibilityActionVecDestructor
129);
130impl_vec_partialeq!(AccessibilityAction, AccessibilityActionVec);
131impl_vec_eq!(AccessibilityAction, AccessibilityActionVec);
132impl_vec_partialord!(AccessibilityAction, AccessibilityActionVec);
133impl_vec_ord!(AccessibilityAction, AccessibilityActionVec);
134impl_vec_hash!(AccessibilityAction, AccessibilityActionVec);
135
136impl_option![
137    AccessibilityAction,
138    OptionAccessibilityAction,
139    copy = false,
140    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
141];
142
143impl_option!(
144    AccessibilityInfo,
145    OptionAccessibilityInfo,
146    copy = false,
147    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
148);
149
150/// Defines the element's purpose for accessibility APIs, informing assistive technologies
151/// like screen readers about the function of a UI element.
152///
153/// Each variant corresponds to a
154/// standard control type or UI structure.
155///
156/// For more details, see the [MSDN Role Constants page](https://docs.microsoft.com/en-us/windows/winauto/object-roles).
157#[repr(C)]
158#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
159pub enum AccessibilityRole {
160    /// Represents the title or caption bar of a window.
161    /// - **Purpose**: To identify the title bar containing the window title and system commands.
162    /// - **When to use**: This role is typically inserted by the operating system for standard
163    ///   windows.
164    /// - **Example**: The bar at the top of an application window displaying its name and the
165    ///   minimize, maximize, and close buttons.
166    TitleBar,
167
168    /// Represents a menu bar at the top of a window.
169    /// - **Purpose**: To contain a set of top-level menus for an application.
170    /// - **When to use**: For the main menu bar of an application, such as one containing "File,"
171    ///   "Edit," and "View."
172    /// - **Example**: The "File", "Edit", "View" menu bar at the top of a text editor.
173    MenuBar,
174
175    /// Represents a vertical or horizontal scroll bar.
176    /// - **Purpose**: To enable scrolling through content that is larger than the visible area.
177    /// - **When to use**: For any scrollable region of content.
178    /// - **Example**: The bar on the side of a web page that allows the user to scroll up and
179    ///   down.
180    ScrollBar,
181
182    /// Represents a handle or grip used for moving or resizing.
183    /// - **Purpose**: To provide a user interface element for manipulating another element's size
184    ///   or position.
185    /// - **When to use**: For handles that allow resizing of windows, panes, or other objects.
186    /// - **Example**: The small textured area in the bottom-right corner of a window that can be
187    ///   dragged to resize it.
188    Grip,
189
190    /// Represents a system sound indicating an event.
191    /// - **Purpose**: To associate a sound with a UI event, providing an auditory cue.
192    /// - **When to use**: When a sound is the primary representation of an event.
193    /// - **Example**: A system notification sound that plays when a new message arrives.
194    Sound,
195
196    /// Represents the system's mouse pointer or other pointing device.
197    /// - **Purpose**: To indicate the screen position of the user's pointing device.
198    /// - **When to use**: This role is managed by the operating system.
199    /// - **Example**: The arrow that moves on the screen as you move the mouse.
200    Cursor,
201
202    /// Represents the text insertion point indicator.
203    /// - **Purpose**: To show the current text entry or editing position.
204    /// - **When to use**: This role is typically managed by the operating system for text input
205    ///   fields.
206    /// - **Example**: The blinking vertical line in a text box that shows where the next character
207    ///   will be typed.
208    Caret,
209
210    /// Represents an alert or notification.
211    /// - **Purpose**: To convey an important, non-modal message to the user.
212    /// - **When to use**: For non-intrusive notifications that do not require immediate user
213    ///   interaction.
214    /// - **Example**: A small, temporary "toast" notification that appears to confirm an action,
215    ///   like "Email sent."
216    Alert,
217
218    /// Represents a window frame.
219    /// - **Purpose**: To serve as the container for other objects like a title bar and client
220    ///   area.
221    /// - **When to use**: This is a fundamental role, typically managed by the windowing system.
222    /// - **Example**: The main window of any application, which contains all other UI elements.
223    Window,
224
225    /// Represents a window's client area, where the main content is displayed.
226    /// - **Purpose**: To define the primary content area of a window.
227    /// - **When to use**: For the main content region of a window. It's often the default role for
228    ///   a custom control container.
229    /// - **Example**: The area of a web browser where the web page content is rendered.
230    Client,
231
232    /// Represents a pop-up menu.
233    /// - **Purpose**: To display a list of `MenuItem` objects that appears when a user performs an
234    ///   action.
235    /// - **When to use**: For context menus (right-click menus) or drop-down menus.
236    /// - **Example**: The menu that appears when you right-click on a file in a file explorer.
237    MenuPopup,
238
239    /// Represents an individual item within a menu.
240    /// - **Purpose**: To represent a single command, option, or separator within a menu.
241    /// - **When to use**: For individual options inside a `MenuBar` or `MenuPopup`.
242    /// - **Example**: The "Save" option within the "File" menu.
243    MenuItem,
244
245    /// Represents a small pop-up window that provides information.
246    /// - **Purpose**: To offer brief, contextual help or information about a UI element.
247    /// - **When to use**: For informational pop-ups that appear on mouse hover.
248    /// - **Example**: The small box of text that appears when you hover over a button in a
249    ///   toolbar.
250    Tooltip,
251
252    /// Represents the main window of an application.
253    /// - **Purpose**: To identify the top-level window of an application.
254    /// - **When to use**: For the primary window that represents the application itself.
255    /// - **Example**: The main window of a calculator or notepad application.
256    Application,
257
258    /// Represents a document window within an application.
259    /// - **Purpose**: To represent a contained document, typically in a Multiple Document
260    ///   Interface (MDI) application.
261    /// - **When to use**: For individual document windows inside a larger application shell.
262    /// - **Example**: In a photo editor that allows multiple images to be open in separate
263    ///   windows, each image window would be a `Document`.
264    Document,
265
266    /// Represents a pane or a distinct section of a window.
267    /// - **Purpose**: To divide a window into visually and functionally distinct areas.
268    /// - **When to use**: For sub-regions of a window, like a navigation pane, preview pane, or
269    ///   sidebar.
270    /// - **Example**: The preview pane in an email client that shows the content of the selected
271    ///   email.
272    Pane,
273
274    /// Represents a graphical chart or graph.
275    /// - **Purpose**: To display data visually in a chart format.
276    /// - **When to use**: For any type of chart, such as a bar chart, line chart, or pie chart.
277    /// - **Example**: A bar chart displaying monthly sales figures.
278    Chart,
279
280    /// Represents a dialog box or message box.
281    /// - **Purpose**: To create a secondary window that requires user interaction before returning
282    ///   to the main application.
283    /// - **When to use**: For modal or non-modal windows that prompt the user for information or a
284    ///   response.
285    /// - **Example**: The "Open File" or "Print" dialog in most applications.
286    Dialog,
287
288    /// Represents a window's border.
289    /// - **Purpose**: To identify the border of a window, which is often used for resizing.
290    /// - **When to use**: This role is typically managed by the windowing system.
291    /// - **Example**: The decorative and functional frame around a window.
292    Border,
293
294    /// Represents a group of related controls.
295    /// - **Purpose**: To logically group other objects that share a common purpose.
296    /// - **When to use**: For grouping controls like a set of radio buttons or a fieldset with a
297    ///   legend.
298    /// - **Example**: A "Settings" group box in a dialog that contains several related checkboxes.
299    Grouping,
300
301    /// Represents a visual separator.
302    /// - **Purpose**: To visually divide a space or a group of controls.
303    /// - **When to use**: For visual separators in menus, toolbars, or between panes.
304    /// - **Example**: The horizontal line in a menu that separates groups of related menu items.
305    Separator,
306
307    /// Represents a toolbar containing a group of controls.
308    /// - **Purpose**: To group controls, typically buttons, for quick access to frequently used
309    ///   functions.
310    /// - **When to use**: For a bar of buttons or other controls, usually at the top of a window
311    ///   or pane.
312    /// - **Example**: The toolbar at the top of a word processor with buttons for "Bold,"
313    ///   "Italic," and "Underline."
314    Toolbar,
315
316    /// Represents a status bar for displaying information.
317    /// - **Purpose**: To display status information about the current state of the application.
318    /// - **When to use**: For a bar, typically at the bottom of a window, that displays messages.
319    /// - **Example**: The bar at the bottom of a web browser that shows the loading status of a
320    ///   page.
321    StatusBar,
322
323    /// Represents a data table.
324    /// - **Purpose**: To present data in a two-dimensional grid of rows and columns.
325    /// - **When to use**: For grid-like data presentation.
326    /// - **Example**: A spreadsheet or a table of data in a database application.
327    Table,
328
329    /// Represents a column header in a table.
330    /// - **Purpose**: To provide a label for a column of data.
331    /// - **When to use**: For the headers of columns in a `Table`.
332    /// - **Example**: The header row in a spreadsheet with labels like "Name," "Date," and
333    ///   "Amount."
334    ColumnHeader,
335
336    /// Represents a row header in a table.
337    /// - **Purpose**: To provide a label for a row of data.
338    /// - **When to use**: For the headers of rows in a `Table`.
339    /// - **Example**: The numbered rows on the left side of a spreadsheet.
340    RowHeader,
341
342    /// Represents a full column of cells in a table.
343    /// - **Purpose**: To represent an entire column as a single accessible object.
344    /// - **When to use**: When it is useful to interact with a column as a whole.
345    /// - **Example**: The "Amount" column in a financial data table.
346    Column,
347
348    /// Represents a full row of cells in a table.
349    /// - **Purpose**: To represent an entire row as a single accessible object.
350    /// - **When to use**: When it is useful to interact with a row as a whole.
351    /// - **Example**: A row representing a single customer's information in a customer list.
352    Row,
353
354    /// Represents a single cell within a table.
355    /// - **Purpose**: To represent a single data point or control within a `Table`.
356    /// - **When to use**: For individual cells in a grid or table.
357    /// - **Example**: A single cell in a spreadsheet containing a specific value.
358    Cell,
359
360    /// Represents a hyperlink to a resource.
361    /// - **Purpose**: To provide a navigational link to another document or location.
362    /// - **When to use**: For text or images that, when clicked, navigate to another resource.
363    /// - **Example**: A clickable link on a web page.
364    Link,
365
366    /// Represents a help balloon or pop-up.
367    /// - **Purpose**: To provide more detailed help information than a standard tooltip.
368    /// - **When to use**: For a pop-up that offers extended help text, often initiated by a help
369    ///   button.
370    /// - **Example**: A pop-up balloon with a paragraph of help text that appears when a user
371    ///   clicks a help icon.
372    HelpBalloon,
373
374    /// Represents an animated, character-like graphic object.
375    /// - **Purpose**: To provide an animated agent for user assistance or entertainment.
376    /// - **When to use**: For animated characters or avatars that provide help or guidance.
377    /// - **Example**: An animated paperclip that offers tips in a word processor (e.g.,
378    ///   Microsoft's Clippy).
379    Character,
380
381    /// Represents a list of items.
382    /// - **Purpose**: To contain a set of `ListItem` objects.
383    /// - **When to use**: For list boxes or similar controls that present a list of selectable
384    ///   items.
385    /// - **Example**: The list of files in a file selection dialog.
386    List,
387
388    /// Represents an individual item within a list.
389    /// - **Purpose**: To represent a single, selectable item within a `List`.
390    /// - **When to use**: For each individual item in a list box or combo box.
391    /// - **Example**: A single file name in a list of files.
392    ListItem,
393
394    /// Represents an outline or tree structure.
395    /// - **Purpose**: To display a hierarchical view of data.
396    /// - **When to use**: For tree-view controls that show nested items.
397    /// - **Example**: A file explorer's folder tree view.
398    Outline,
399
400    /// Represents an individual item within an outline or tree.
401    /// - **Purpose**: To represent a single node (which can be a leaf or a branch) in an
402    ///   `Outline`.
403    /// - **When to use**: For each node in a tree view.
404    /// - **Example**: A single folder in a file explorer's tree view.
405    OutlineItem,
406
407    /// Represents a single tab in a tabbed interface.
408    /// - **Purpose**: To provide a control for switching between different `PropertyPage` views.
409    /// - **When to use**: For the individual tabs that the user can click to switch pages.
410    /// - **Example**: The "General" and "Security" tabs in a file properties dialog.
411    PageTab,
412
413    /// Represents the content of a page in a property sheet.
414    /// - **Purpose**: To serve as a container for the controls displayed when a `PageTab` is
415    ///   selected.
416    /// - **When to use**: For the content area associated with a specific tab.
417    /// - **Example**: The set of options displayed when the "Security" tab is active.
418    PropertyPage,
419
420    /// Represents a visual indicator, like a slider thumb.
421    /// - **Purpose**: To visually indicate the current value or position of another control.
422    /// - **When to use**: For a sub-element that indicates status, like the thumb of a scrollbar.
423    /// - **Example**: The draggable thumb of a scrollbar that indicates the current scroll
424    ///   position.
425    Indicator,
426
427    /// Represents a picture or graphical image.
428    /// - **Purpose**: To display a non-interactive image.
429    /// - **When to use**: For images and icons that are purely decorative or informational.
430    /// - **Example**: A company logo displayed in an application's "About" dialog.
431    Graphic,
432
433    /// Represents read-only text.
434    /// - **Purpose**: To provide a non-editable text label for another control or for displaying
435    ///   information.
436    /// - **When to use**: For text that the user cannot edit.
437    /// - **Example**: The label "Username:" next to a text input field.
438    StaticText,
439
440    /// Represents editable text or a text area.
441    /// - **Purpose**: To allow for user text input or selection.
442    /// - **When to use**: For text input fields where the user can type.
443    /// - **Example**: A text box for entering a username or password.
444    Text,
445
446    /// Represents a standard push button.
447    /// - **Purpose**: To initiate an immediate action.
448    /// - **When to use**: For standard buttons that perform an action when clicked.
449    /// - **Example**: An "OK" or "Cancel" button in a dialog.
450    PushButton,
451
452    /// Represents a check box control.
453    /// - **Purpose**: To allow the user to make a binary choice (checked or unchecked).
454    /// - **When to use**: For options that can be toggled on or off independently.
455    /// - **Example**: A "Remember me" checkbox on a login form.
456    CheckButton,
457
458    /// Represents a radio button.
459    /// - **Purpose**: To allow the user to select one option from a mutually exclusive group.
460    /// - **When to use**: For a choice where only one option from a `Grouping` can be selected.
461    /// - **Example**: "Male" and "Female" radio buttons for selecting gender.
462    RadioButton,
463
464    /// Represents a combination of a text field and a drop-down list.
465    /// - **Purpose**: To allow the user to either type a value or select one from a list.
466    /// - **When to use**: For controls that offer a list of suggestions but also allow custom
467    ///   input.
468    /// - **Example**: A font selector that allows you to type a font name or choose one from a
469    ///   list.
470    ComboBox,
471
472    /// Represents a drop-down list box.
473    /// - **Purpose**: To allow the user to select an item from a non-editable list that drops
474    ///   down.
475    /// - **When to use**: For selecting a single item from a predefined list of options.
476    /// - **Example**: A country selection drop-down menu.
477    DropList,
478
479    /// Represents a progress bar.
480    /// - **Purpose**: To indicate the progress of a lengthy operation.
481    /// - **When to use**: To provide feedback for tasks like file downloads or installations.
482    /// - **Example**: The bar that fills up to show the progress of a file copy operation.
483    ProgressBar,
484
485    /// Represents a dial or knob.
486    /// - **Purpose**: To allow selecting a value from a continuous or discrete range, often
487    ///   circularly.
488    /// - **When to use**: For controls that resemble real-world dials, like a volume knob.
489    /// - **Example**: A volume control knob in a media player application.
490    Dial,
491
492    /// Represents a control for entering a keyboard shortcut.
493    /// - **Purpose**: To capture a key combination from the user.
494    /// - **When to use**: In settings where users can define their own keyboard shortcuts.
495    /// - **Example**: A text field in a settings dialog where a user can press a key combination
496    ///   to assign it to a command.
497    HotkeyField,
498
499    /// Represents a slider for selecting a value within a range.
500    /// - **Purpose**: To allow the user to adjust a setting along a continuous or discrete range.
501    /// - **When to use**: For adjusting values like volume, brightness, or zoom level.
502    /// - **Example**: A slider to control the volume of a video.
503    Slider,
504
505    /// Represents a spin button (up/down arrows) for incrementing or decrementing a value.
506    /// - **Purpose**: To provide fine-tuned adjustment of a value, typically numeric.
507    /// - **When to use**: For controls that allow stepping through a range of values.
508    /// - **Example**: The up and down arrows next to a number input for setting the font size.
509    SpinButton,
510
511    /// Represents a diagram or flowchart.
512    /// - **Purpose**: To represent data or relationships in a schematic form.
513    /// - **When to use**: For visual representations of structures that are not charts, like a
514    ///   database schema diagram.
515    /// - **Example**: A flowchart illustrating a business process.
516    Diagram,
517
518    /// Represents an animation control.
519    /// - **Purpose**: To display a sequence of images or indicate an ongoing process.
520    /// - **When to use**: For animations that show that an operation is in progress.
521    /// - **Example**: The animation that plays while files are being copied.
522    Animation,
523
524    /// Represents a mathematical equation.
525    /// - **Purpose**: To display a mathematical formula in the correct format.
526    /// - **When to use**: For displaying mathematical equations.
527    /// - **Example**: A rendered mathematical equation in a scientific document editor.
528    Equation,
529
530    /// Represents a button that drops down a list of items.
531    /// - **Purpose**: To combine a default action button with a list of alternative actions.
532    /// - **When to use**: For buttons that have a primary action and a secondary list of options.
533    /// - **Example**: A "Send" button with a dropdown arrow that reveals "Send and Archive."
534    ButtonDropdown,
535
536    /// Represents a button that drops down a full menu.
537    /// - **Purpose**: To provide a button that opens a menu of choices rather than performing a
538    ///   single action.
539    /// - **When to use**: When a button's primary purpose is to reveal a menu.
540    /// - **Example**: A "Tools" button that opens a menu with various tool options.
541    ButtonMenu,
542
543    /// Represents a button that drops down a grid for selection.
544    /// - **Purpose**: To allow selection from a two-dimensional grid of options.
545    /// - **When to use**: For buttons that open a grid-based selection UI.
546    /// - **Example**: A color picker button that opens a grid of color swatches.
547    ButtonDropdownGrid,
548
549    /// Represents blank space between other objects.
550    /// - **Purpose**: To represent significant empty areas in a UI that are part of the layout.
551    /// - **When to use**: Sparingly, to signify that a large area is intentionally blank.
552    /// - **Example**: A large empty panel in a complex layout might use this role.
553    Whitespace,
554
555    /// Represents the container for a set of tabs.
556    /// - **Purpose**: To group a set of `PageTab` elements.
557    /// - **When to use**: To act as the parent container for a row or column of tabs.
558    /// - **Example**: The entire row of tabs at the top of a properties dialog.
559    PageTabList,
560
561    /// Represents a clock control.
562    /// - **Purpose**: To display the current time.
563    /// - **When to use**: For any UI element that displays time.
564    /// - **Example**: The clock in the system tray of the operating system.
565    Clock,
566
567    /// Represents a button with two parts: a default action and a dropdown.
568    /// - **Purpose**: To combine a frequently used action with a set of related, less-used
569    ///   actions.
570    /// - **When to use**: When a button has a default action and other related actions available
571    ///   in a dropdown.
572    /// - **Example**: A "Save" split button where the primary part saves, and the dropdown offers
573    ///   "Save As."
574    SplitButton,
575
576    /// Represents a control for entering an IP address.
577    /// - **Purpose**: To provide a specialized input field for IP addresses, often with formatting
578    ///   and validation.
579    /// - **When to use**: For dedicated IP address input fields.
580    /// - **Example**: A network configuration dialog with a field for entering a static IP
581    ///   address.
582    IpAddress,
583
584    /// Represents an element with no specific role.
585    /// - **Purpose**: To indicate an element that has no semantic meaning for accessibility.
586    /// - **When to use**: Should be used sparingly for purely decorative elements that should be
587    ///   ignored by assistive technologies.
588    /// - **Example**: A decorative graphical flourish that has no function or information to
589    ///   convey.
590    Nothing,
591
592    /// Unknown or unspecified role.
593    /// - **Purpose**: Default fallback when no specific role is assigned.
594    /// - **When to use**: As a default value or when role information is unavailable.
595    Unknown,
596}
597
598impl_option!(
599    AccessibilityRole,
600    OptionAccessibilityRole,
601    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
602);
603
604/// Defines the current state of an element for accessibility APIs (e.g., focused, checked).
605/// These states provide dynamic information to assistive technologies about the element's
606/// condition.
607///
608/// See the [MSDN State Constants page](https://docs.microsoft.com/en-us/windows/win32/winauto/object-state-constants) for more details.
609#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
610#[repr(C)]
611pub enum AccessibilityState {
612    /// The element is unavailable and cannot be interacted with.
613    /// - **Purpose**: To indicate that a control is disabled or grayed out.
614    /// - **When to use**: For disabled buttons, non-interactive menu items, or any control that is
615    ///   temporarily non-functional.
616    /// - **Example**: A "Save" button that is disabled until the user makes changes to a document.
617    Unavailable,
618
619    /// The element is selected.
620    /// - **Purpose**: To indicate that an item is currently chosen or highlighted. This is
621    ///   distinct from having focus.
622    /// - **When to use**: For selected items in a list, highlighted text, or the currently active
623    ///   tab in a tab list.
624    /// - **Example**: A file highlighted in a file explorer, or multiple selected emails in an
625    ///   inbox.
626    Selected,
627
628    /// The element has the keyboard focus.
629    /// - **Purpose**: To identify the single element that will receive keyboard input.
630    /// - **When to use**: For the control that is currently active and ready to be manipulated by
631    ///   the keyboard.
632    /// - **Example**: A text box with a blinking cursor, or a button with a dotted outline around
633    ///   it.
634    Focused,
635
636    /// The element is checked, toggled, or in an "on" state.
637    /// - **Purpose**: To represent checked checkboxes, selected radio buttons, and active toggles.
638    /// - **Example**: A checked "I agree" checkbox, a selected "Yes" radio button.
639    CheckedTrue,
640    /// The element is unchecked, untoggled, or in an "off" state.
641    /// - **Purpose**: To explicitly represent an unchecked checkbox or unselected radio button.
642    /// - **Example**: An unchecked checkbox that the user has not yet ticked.
643    CheckedFalse,
644
645    /// The element's content cannot be edited by the user.
646    /// - **Purpose**: To indicate that the element's value can be viewed and copied, but not
647    ///   modified.
648    /// - **When to use**: For display-only text fields or documents.
649    /// - **Example**: A text box displaying a license agreement that the user can scroll through
650    ///   but cannot edit.
651    Readonly,
652
653    /// The element is the default action in a dialog or form.
654    /// - **Purpose**: To identify the button that will be activated if the user presses the Enter
655    ///   key.
656    /// - **When to use**: For the primary confirmation button in a dialog.
657    /// - **Example**: The "OK" button in a dialog box, which often has a thicker or colored
658    ///   border.
659    Default,
660
661    /// The element is expanded, showing its child items.
662    /// - **Purpose**: To indicate that a collapsible element is currently open and its contents
663    ///   are visible.
664    /// - **When to use**: For tree view nodes, combo boxes with their lists open, or expanded
665    ///   accordion panels.
666    /// - **Example**: A folder in a file explorer's tree view that has been clicked to show its
667    ///   subfolders.
668    Expanded,
669
670    /// The element is collapsed, hiding its child items.
671    /// - **Purpose**: To indicate that a collapsible element is closed and its contents are
672    ///   hidden.
673    /// - **When to use**: The counterpart to `Expanded` for any collapsible UI element.
674    /// - **Example**: A closed folder in a file explorer's tree view, hiding its contents.
675    Collapsed,
676
677    /// The element is busy and cannot respond to user interaction.
678    /// - **Purpose**: To indicate that the element or application is performing an operation and
679    ///   is temporarily unresponsive.
680    /// - **When to use**: When an application is loading, processing data, or otherwise occupied.
681    /// - **Example**: A window that is grayed out and shows a spinning cursor while saving a large
682    ///   file.
683    Busy,
684
685    /// The element is not currently visible on the screen.
686    /// - **Purpose**: To indicate that an element exists but is currently scrolled out of the
687    ///   visible area.
688    /// - **When to use**: For items in a long list or a large document that are not within the
689    ///   current viewport.
690    /// - **Example**: A list item in a long dropdown that you would have to scroll down to see.
691    Offscreen,
692
693    /// The element can accept keyboard focus.
694    /// - **Purpose**: To indicate that the user can navigate to this element using the keyboard
695    ///   (e.g., with the Tab key).
696    /// - **When to use**: On all interactive elements like buttons, links, and input fields,
697    ///   whether they currently have focus or not.
698    /// - **Example**: A button that can receive focus, even if it is not the currently focused
699    ///   element.
700    Focusable,
701
702    /// The element is a container whose children can be selected.
703    /// - **Purpose**: To indicate that the element contains items that can be chosen.
704    /// - **When to use**: On container controls like list boxes, tree views, or text spans where
705    ///   text can be highlighted.
706    /// - **Example**: A list box control is `Selectable`, while its individual list items have the
707    ///   `Selected` state when chosen.
708    Selectable,
709
710    /// The element is a hyperlink.
711    /// - **Purpose**: To identify an object that navigates to another resource or location when
712    ///   activated.
713    /// - **When to use**: On any object that functions as a hyperlink.
714    /// - **Example**: Text or an image that, when clicked, opens a web page.
715    Linked,
716
717    /// The element is a hyperlink that has been visited.
718    /// - **Purpose**: To indicate that a hyperlink has already been followed by the user.
719    /// - **When to use**: On a `Linked` object that the user has previously activated.
720    /// - **Example**: A hyperlink on a web page that has changed color to show it has been
721    ///   visited.
722    Traversed,
723
724    /// The element allows multiple of its children to be selected at once.
725    /// - **Purpose**: To indicate that a container control supports multi-selection.
726    /// - **When to use**: On container controls like list boxes or file explorers that support
727    ///   multiple selections (e.g., with Ctrl-click).
728    /// - **Example**: A file list that allows the user to select several files at once for a copy
729    ///   operation.
730    Multiselectable,
731
732    /// The element contains protected content that should not be read aloud.
733    /// - **Purpose**: To prevent assistive technologies from speaking the content of a sensitive
734    ///   field.
735    /// - **When to use**: Primarily for password input fields.
736    /// - **Example**: A password text box where typed characters are masked with asterisks or
737    ///   dots.
738    Protected,
739}
740
741impl_option!(
742    AccessibilityState,
743    OptionAccessibilityState,
744    [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
745);
746
747impl_vec!(AccessibilityState, AccessibilityStateVec, AccessibilityStateVecDestructor, AccessibilityStateVecDestructorType, AccessibilityStateVecSlice, OptionAccessibilityState);
748impl_vec_clone!(
749    AccessibilityState,
750    AccessibilityStateVec,
751    AccessibilityStateVecDestructor
752);
753impl_vec_debug!(AccessibilityState, AccessibilityStateVec);
754impl_vec_partialeq!(AccessibilityState, AccessibilityStateVec);
755impl_vec_partialord!(AccessibilityState, AccessibilityStateVec);
756impl_vec_eq!(AccessibilityState, AccessibilityStateVec);
757impl_vec_ord!(AccessibilityState, AccessibilityStateVec);
758impl_vec_hash!(AccessibilityState, AccessibilityStateVec);
759
760/// Compact accessibility information for common use cases.
761///
762/// This is a lighter-weight alternative to `AccessibilityInfo` for cases where
763/// only basic accessibility properties are needed. Developers must explicitly
764/// pass `None` if they choose not to provide accessibility information.
765#[derive(Debug, Clone, PartialEq, Eq, Hash)]
766#[repr(C)]
767pub struct SmallAriaInfo {
768    /// Accessible label/name
769    pub label: OptionString,
770    /// Element's role (button, link, etc.)
771    pub role: OptionAccessibilityRole,
772    /// Additional description
773    pub description: OptionString,
774}
775
776impl_option!(
777    SmallAriaInfo,
778    OptionSmallAriaInfo,
779    copy = false,
780    [Debug, Clone, PartialEq, Eq, Hash]
781);
782
783impl SmallAriaInfo {
784    pub fn label<S: Into<AzString>>(text: S) -> Self {
785        Self {
786            label: OptionString::Some(text.into()),
787            role: OptionAccessibilityRole::None,
788            description: OptionString::None,
789        }
790    }
791
792    #[must_use] pub const fn with_role(mut self, role: AccessibilityRole) -> Self {
793        self.role = OptionAccessibilityRole::Some(role);
794        self
795    }
796
797    #[must_use] pub fn with_description<S: Into<AzString>>(mut self, desc: S) -> Self {
798        self.description = OptionString::Some(desc.into());
799        self
800    }
801
802    /// Convert to full `AccessibilityInfo`
803    #[must_use] pub fn to_full_info(&self) -> AccessibilityInfo {
804        AccessibilityInfo {
805            accessibility_name: self.label.clone(),
806            accessibility_value: OptionString::None,
807            description: self.description.clone(),
808            role: match self.role {
809                OptionAccessibilityRole::Some(r) => r,
810                OptionAccessibilityRole::None => AccessibilityRole::Unknown,
811            },
812            states: Vec::new().into(),
813            accelerator: OptionVirtualKeyCodeCombo::None,
814            default_action: OptionString::None,
815            supported_actions: Vec::new().into(),
816            is_live_region: false,
817            labelled_by: OptionDomNodeId::None,
818            described_by: OptionDomNodeId::None,
819        }
820    }
821}
822
823/// Accessibility information for a `<progress>` indicator.
824///
825/// Mirrors HTML's `<progress value max>` plus an `indeterminate` flag for
826/// progress bars whose end is unknown. Maps to `AccessibilityRole::ProgressBar`.
827#[derive(Debug, Clone, PartialEq, Eq)]
828#[repr(C)]
829pub struct ProgressAriaInfo {
830    /// Accessible label describing the task being measured.
831    pub label: OptionString,
832    /// Current progress value. `None` for indeterminate progress.
833    pub current_value: OptionF32,
834    /// Maximum value the progress bar can reach. `None` falls back to `1.0`.
835    pub max: OptionF32,
836    /// `true` for spinners / progress with no known endpoint. Overrides `current_value`.
837    pub indeterminate: bool,
838    /// Optional extended description (`aria-describedby` equivalent).
839    pub description: OptionString,
840}
841
842impl_option!(
843    ProgressAriaInfo,
844    OptionProgressAriaInfo,
845    copy = false,
846    [Debug, Clone, PartialEq, Eq]
847);
848
849impl ProgressAriaInfo {
850    /// Creates a `ProgressAriaInfo` with only an accessible label.
851    #[must_use] pub const fn create(label: AzString) -> Self {
852        Self {
853            label: OptionString::Some(label),
854            current_value: OptionF32::None,
855            max: OptionF32::None,
856            indeterminate: false,
857            description: OptionString::None,
858        }
859    }
860
861    /// Returns a copy with the given current value.
862    #[must_use] pub const fn with_current_value(mut self, value: f32) -> Self {
863        self.current_value = OptionF32::Some(value);
864        self
865    }
866
867    /// Returns a copy with the given maximum value.
868    #[must_use] pub const fn with_max(mut self, max: f32) -> Self {
869        self.max = OptionF32::Some(max);
870        self
871    }
872
873    /// Returns a copy with the indeterminate flag set.
874    #[must_use] pub const fn with_indeterminate(mut self, indeterminate: bool) -> Self {
875        self.indeterminate = indeterminate;
876        self
877    }
878
879    /// Returns a copy with the given description.
880    #[must_use] pub fn with_description(mut self, desc: AzString) -> Self {
881        self.description = OptionString::Some(desc);
882        self
883    }
884
885    /// Convert to full `AccessibilityInfo` so the value can be installed on a node.
886    #[must_use] pub fn to_full_info(&self) -> AccessibilityInfo {
887        let value_string = if self.indeterminate {
888            OptionString::None
889        } else {
890            match self.current_value {
891                OptionF32::Some(v) => OptionString::Some(format!("{v}").into()),
892                OptionF32::None => OptionString::None,
893            }
894        };
895        AccessibilityInfo {
896            accessibility_name: self.label.clone(),
897            accessibility_value: value_string,
898            description: self.description.clone(),
899            role: AccessibilityRole::ProgressBar,
900            states: Vec::new().into(),
901            accelerator: OptionVirtualKeyCodeCombo::None,
902            default_action: OptionString::None,
903            supported_actions: Vec::new().into(),
904            is_live_region: false,
905            labelled_by: OptionDomNodeId::None,
906            described_by: OptionDomNodeId::None,
907        }
908    }
909}
910
911/// Accessibility information for a `<meter>` gauge.
912///
913/// Unlike `<progress>`, `<meter>` always carries a known `value`/`min`/`max`
914/// triple, so those fields are required at construction time. Maps to
915/// `AccessibilityRole::Indicator`.
916#[derive(Debug, Clone, PartialEq)]
917#[repr(C)]
918pub struct MeterAriaInfo {
919    /// Accessible label describing what the meter measures.
920    pub label: OptionString,
921    /// Current value of the meter (within `[min, max]`).
922    pub current_value: f32,
923    /// Lower bound of the measurement range.
924    pub min: f32,
925    /// Upper bound of the measurement range.
926    pub max: f32,
927    /// Optional "low" threshold (values below this are considered low).
928    pub low: OptionF32,
929    /// Optional "high" threshold (values above this are considered high).
930    pub high: OptionF32,
931    /// Optional optimum value within the range.
932    pub optimum: OptionF32,
933    /// Optional extended description.
934    pub description: OptionString,
935}
936
937impl_option!(
938    MeterAriaInfo,
939    OptionMeterAriaInfo,
940    copy = false,
941    [Debug, Clone, PartialEq]
942);
943
944impl MeterAriaInfo {
945    /// Creates a `MeterAriaInfo` with the required label and value/range triple.
946    #[must_use] pub const fn create(label: AzString, current_value: f32, min: f32, max: f32) -> Self {
947        Self {
948            label: OptionString::Some(label),
949            current_value,
950            min,
951            max,
952            low: OptionF32::None,
953            high: OptionF32::None,
954            optimum: OptionF32::None,
955            description: OptionString::None,
956        }
957    }
958
959    /// Returns a copy with the given low threshold.
960    #[must_use] pub const fn with_low(mut self, low: f32) -> Self {
961        self.low = OptionF32::Some(low);
962        self
963    }
964
965    /// Returns a copy with the given high threshold.
966    #[must_use] pub const fn with_high(mut self, high: f32) -> Self {
967        self.high = OptionF32::Some(high);
968        self
969    }
970
971    /// Returns a copy with the given optimum value.
972    #[must_use] pub const fn with_optimum(mut self, optimum: f32) -> Self {
973        self.optimum = OptionF32::Some(optimum);
974        self
975    }
976
977    /// Returns a copy with the given description.
978    #[must_use] pub fn with_description(mut self, desc: AzString) -> Self {
979        self.description = OptionString::Some(desc);
980        self
981    }
982
983    /// Convert to full `AccessibilityInfo` so the value can be installed on a node.
984    #[must_use] pub fn to_full_info(&self) -> AccessibilityInfo {
985        AccessibilityInfo {
986            accessibility_name: self.label.clone(),
987            accessibility_value: OptionString::Some(format!("{}", self.current_value).into()),
988            description: self.description.clone(),
989            role: AccessibilityRole::Indicator,
990            states: Vec::new().into(),
991            accelerator: OptionVirtualKeyCodeCombo::None,
992            default_action: OptionString::None,
993            supported_actions: Vec::new().into(),
994            is_live_region: false,
995            labelled_by: OptionDomNodeId::None,
996            described_by: OptionDomNodeId::None,
997        }
998    }
999}
1000
1001/// Accessibility information for a `<dialog>` element.
1002///
1003/// Captures the modal/non-modal distinction and a reference to a separate
1004/// node that describes the dialog (`aria-describedby`). The `role` defaults
1005/// to `AccessibilityRole::Dialog` but can be overridden (e.g., for alert
1006/// dialogs).
1007#[derive(Debug, Clone, PartialEq, Eq)]
1008#[repr(C)]
1009pub struct DialogAriaInfo {
1010    /// Accessible label / title for the dialog.
1011    pub label: OptionString,
1012    /// Optional ID of another node that describes the dialog content.
1013    pub described_by: OptionString,
1014    /// Optional inline description.
1015    pub description: OptionString,
1016    /// Role for the dialog. Defaults to `Dialog`; use `Alert` for urgent dialogs.
1017    pub role: AccessibilityRole,
1018    /// `true` if the dialog is modal (focus trapped, background inert).
1019    pub modal: bool,
1020}
1021
1022impl_option!(
1023    DialogAriaInfo,
1024    OptionDialogAriaInfo,
1025    copy = false,
1026    [Debug, Clone, PartialEq, Eq]
1027);
1028
1029impl DialogAriaInfo {
1030    /// Creates a `DialogAriaInfo` with the given accessible label. Defaults
1031    /// to a non-modal dialog with role `Dialog`.
1032    #[must_use] pub const fn create(label: AzString) -> Self {
1033        Self {
1034            label: OptionString::Some(label),
1035            modal: false,
1036            described_by: OptionString::None,
1037            role: AccessibilityRole::Dialog,
1038            description: OptionString::None,
1039        }
1040    }
1041
1042    /// Returns a copy with the given modality flag.
1043    #[must_use] pub const fn with_modal(mut self, modal: bool) -> Self {
1044        self.modal = modal;
1045        self
1046    }
1047
1048    /// Returns a copy with `aria-describedby` pointing at the given node ID.
1049    #[must_use] pub fn with_described_by(mut self, described_by: AzString) -> Self {
1050        self.described_by = OptionString::Some(described_by);
1051        self
1052    }
1053
1054    /// Returns a copy with the given role (defaults to `Dialog`).
1055    #[must_use] pub const fn with_role(mut self, role: AccessibilityRole) -> Self {
1056        self.role = role;
1057        self
1058    }
1059
1060    /// Returns a copy with the given inline description.
1061    #[must_use] pub fn with_description(mut self, desc: AzString) -> Self {
1062        self.description = OptionString::Some(desc);
1063        self
1064    }
1065
1066    /// Convert to full `AccessibilityInfo` so the value can be installed on a node.
1067    #[must_use] pub fn to_full_info(&self) -> AccessibilityInfo {
1068        AccessibilityInfo {
1069            accessibility_name: self.label.clone(),
1070            accessibility_value: OptionString::None,
1071            description: self.description.clone(),
1072            role: self.role,
1073            states: Vec::new().into(),
1074            accelerator: OptionVirtualKeyCodeCombo::None,
1075            default_action: OptionString::None,
1076            supported_actions: Vec::new().into(),
1077            is_live_region: false,
1078            labelled_by: OptionDomNodeId::None,
1079            described_by: OptionDomNodeId::None,
1080        }
1081    }
1082}
1083
1084#[cfg(test)]
1085mod autotest_generated {
1086    use super::*;
1087    use alloc::string::String;
1088
1089    // ---- small helpers to reach into the FFI-style option wrappers ----
1090
1091    fn name_str(o: &OptionString) -> Option<&str> {
1092        o.as_ref().map(|s| s.as_str())
1093    }
1094
1095    fn f32_of(o: &OptionF32) -> Option<f32> {
1096        o.as_ref().copied()
1097    }
1098
1099    /// A battery of adversarial strings: empty, embedded NUL, control chars,
1100    /// combining unicode, emoji, RTL, and a very large allocation.
1101    fn adversarial_strings() -> Vec<String> {
1102        vec![
1103            String::new(),
1104            String::from(" "),
1105            String::from("\0"),
1106            String::from("a\0b\0c"),
1107            String::from("\t\r\n\x1b[0m"),
1108            String::from("日本語のテキスト"),
1109            String::from("🎉👨‍👩‍👧‍👦🇺🇳"),
1110            String::from("e\u{0301}\u{0301}\u{0301}"), // combining accents
1111            String::from("\u{202e}reversed\u{202d}"),  // RTL override
1112            String::from("\u{FFFD}\u{10FFFF}"),        // replacement + max scalar
1113            "x".repeat(100_000),                        // huge
1114        ]
1115    }
1116
1117    /// Numeric edge values for f32 fields.
1118    fn adversarial_f32() -> Vec<f32> {
1119        vec![
1120            0.0,
1121            -0.0,
1122            1.0,
1123            -1.0,
1124            f32::MIN,
1125            f32::MAX,
1126            f32::MIN_POSITIVE,
1127            -f32::MIN_POSITIVE,
1128            f32::EPSILON,
1129            f32::NAN,
1130            f32::INFINITY,
1131            f32::NEG_INFINITY,
1132        ]
1133    }
1134
1135    // =====================================================================
1136    // 1. SmallAriaInfo::label — no_panic_smoke
1137    // =====================================================================
1138
1139    #[test]
1140    fn small_label_no_panic_smoke() {
1141        for s in adversarial_strings() {
1142            let expected = s.clone();
1143            let info = SmallAriaInfo::label(s);
1144            // The label must round-trip verbatim and the other fields default to None.
1145            assert_eq!(name_str(&info.label), Some(expected.as_str()));
1146            assert!(info.role.is_none());
1147            assert!(info.description.is_none());
1148            // to_full_info must not panic even for pathological labels.
1149            let full = info.to_full_info();
1150            assert_eq!(name_str(&full.accessibility_name), Some(expected.as_str()));
1151        }
1152        // `&str` input path as well.
1153        let info = SmallAriaInfo::label("hello");
1154        assert_eq!(name_str(&info.label), Some("hello"));
1155    }
1156
1157    // =====================================================================
1158    // 2. SmallAriaInfo::with_role — no_panic + invariants
1159    // =====================================================================
1160
1161    fn representative_roles() -> Vec<AccessibilityRole> {
1162        vec![
1163            AccessibilityRole::TitleBar, // first variant
1164            AccessibilityRole::PushButton,
1165            AccessibilityRole::CheckButton,
1166            AccessibilityRole::Slider,
1167            AccessibilityRole::Link,
1168            AccessibilityRole::Nothing,
1169            AccessibilityRole::Unknown, // last variant
1170        ]
1171    }
1172
1173    #[test]
1174    fn small_with_role_invariants() {
1175        for role in representative_roles() {
1176            let info = SmallAriaInfo::label("base").with_role(role);
1177            // Only the role field changes; label preserved, description untouched.
1178            assert_eq!(info.role, OptionAccessibilityRole::Some(role));
1179            assert_eq!(name_str(&info.label), Some("base"));
1180            assert!(info.description.is_none());
1181        }
1182        // Last-write-wins when applied twice.
1183        let info = SmallAriaInfo::label("x")
1184            .with_role(AccessibilityRole::Link)
1185            .with_role(AccessibilityRole::Slider);
1186        assert_eq!(info.role, OptionAccessibilityRole::Some(AccessibilityRole::Slider));
1187    }
1188
1189    // =====================================================================
1190    // 3. SmallAriaInfo::with_description — no_panic + invariants
1191    // =====================================================================
1192
1193    #[test]
1194    fn small_with_description_invariants() {
1195        for s in adversarial_strings() {
1196            let expected = s.clone();
1197            let info = SmallAriaInfo::label("base").with_description(s);
1198            assert_eq!(name_str(&info.description), Some(expected.as_str()));
1199            // label untouched, role still None.
1200            assert_eq!(name_str(&info.label), Some("base"));
1201            assert!(info.role.is_none());
1202        }
1203    }
1204
1205    // =====================================================================
1206    // 4. SmallAriaInfo::to_full_info — basic + edge
1207    // =====================================================================
1208
1209    #[test]
1210    fn small_to_full_info_basic() {
1211        let info = SmallAriaInfo::label("Submit")
1212            .with_role(AccessibilityRole::PushButton)
1213            .with_description("primary action")
1214            .to_full_info();
1215        assert_eq!(name_str(&info.accessibility_name), Some("Submit"));
1216        assert_eq!(info.role, AccessibilityRole::PushButton);
1217        assert_eq!(name_str(&info.description), Some("primary action"));
1218        assert!(info.accessibility_value.is_none());
1219        assert_eq!(info.states.len(), 0);
1220        assert_eq!(info.supported_actions.len(), 0);
1221        assert!(!info.is_live_region);
1222        assert!(info.labelled_by.is_none());
1223        assert!(info.described_by.is_none());
1224    }
1225
1226    #[test]
1227    fn small_to_full_info_edge_missing_role_maps_to_unknown() {
1228        // No role set => full info must fall back to `Unknown`, never panic.
1229        let info = SmallAriaInfo::label("").to_full_info();
1230        assert_eq!(info.role, AccessibilityRole::Unknown);
1231        assert_eq!(name_str(&info.accessibility_name), Some(""));
1232        assert!(info.description.is_none());
1233    }
1234
1235    // =====================================================================
1236    // 5. ProgressAriaInfo::create — no_panic_smoke
1237    // =====================================================================
1238
1239    #[test]
1240    fn progress_create_no_panic_smoke() {
1241        for s in adversarial_strings() {
1242            let expected = s.clone();
1243            let p = ProgressAriaInfo::create(s.into());
1244            assert_eq!(name_str(&p.label), Some(expected.as_str()));
1245            // Documented defaults.
1246            assert!(p.current_value.is_none());
1247            assert!(p.max.is_none());
1248            assert!(!p.indeterminate);
1249            assert!(p.description.is_none());
1250        }
1251    }
1252
1253    // =====================================================================
1254    // 6. ProgressAriaInfo::with_current_value — no_panic + invariants (numeric)
1255    // =====================================================================
1256
1257    #[test]
1258    fn progress_with_current_value_numeric() {
1259        for v in adversarial_f32() {
1260            let p = ProgressAriaInfo::create("p".into()).with_current_value(v);
1261            match f32_of(&p.current_value) {
1262                Some(got) if v.is_nan() => assert!(got.is_nan()),
1263                Some(got) => assert_eq!(got, v),
1264                None => panic!("current_value should be Some after with_current_value"),
1265            }
1266            // to_full_info must not panic for any float, and (since determinate)
1267            // must emit a value string.
1268            let full = p.to_full_info();
1269            assert!(full.accessibility_value.is_some());
1270        }
1271    }
1272
1273    // =====================================================================
1274    // 7. ProgressAriaInfo::with_max — no_panic + invariants (numeric)
1275    // =====================================================================
1276
1277    #[test]
1278    fn progress_with_max_numeric() {
1279        for v in adversarial_f32() {
1280            let p = ProgressAriaInfo::create("p".into()).with_max(v);
1281            match f32_of(&p.max) {
1282                Some(got) if v.is_nan() => assert!(got.is_nan()),
1283                Some(got) => assert_eq!(got, v),
1284                None => panic!("max should be Some after with_max"),
1285            }
1286            // max does not influence the value string; current_value stays None.
1287            assert!(p.current_value.is_none());
1288        }
1289    }
1290
1291    // =====================================================================
1292    // 8. ProgressAriaInfo::with_indeterminate — no_panic + invariants
1293    // =====================================================================
1294
1295    #[test]
1296    fn progress_with_indeterminate_invariants() {
1297        for flag in [true, false] {
1298            let p = ProgressAriaInfo::create("p".into()).with_indeterminate(flag);
1299            assert_eq!(p.indeterminate, flag);
1300        }
1301        // indeterminate must override a present current_value in to_full_info.
1302        let p = ProgressAriaInfo::create("p".into())
1303            .with_current_value(0.5)
1304            .with_indeterminate(true);
1305        assert!(p.to_full_info().accessibility_value.is_none());
1306    }
1307
1308    // =====================================================================
1309    // 9. ProgressAriaInfo::with_description — no_panic + invariants
1310    // =====================================================================
1311
1312    #[test]
1313    fn progress_with_description_invariants() {
1314        for s in adversarial_strings() {
1315            let expected = s.clone();
1316            let p = ProgressAriaInfo::create("p".into()).with_description(s.into());
1317            assert_eq!(name_str(&p.description), Some(expected.as_str()));
1318            assert_eq!(name_str(&p.label), Some("p"));
1319        }
1320    }
1321
1322    // =====================================================================
1323    // 10. ProgressAriaInfo::to_full_info — basic + edge
1324    // =====================================================================
1325
1326    #[test]
1327    fn progress_to_full_info_basic() {
1328        let info = ProgressAriaInfo::create("Loading".into())
1329            .with_current_value(0.5)
1330            .to_full_info();
1331        assert_eq!(name_str(&info.accessibility_name), Some("Loading"));
1332        assert_eq!(info.role, AccessibilityRole::ProgressBar);
1333        assert_eq!(name_str(&info.accessibility_value), Some("0.5"));
1334        assert_eq!(info.states.len(), 0);
1335        assert_eq!(info.supported_actions.len(), 0);
1336    }
1337
1338    #[test]
1339    fn progress_to_full_info_edge() {
1340        // No current value => value string is None.
1341        let info = ProgressAriaInfo::create("x".into()).to_full_info();
1342        assert!(info.accessibility_value.is_none());
1343        assert_eq!(info.role, AccessibilityRole::ProgressBar);
1344
1345        // NaN / inf current values format to defined strings, no panic.
1346        assert_eq!(
1347            name_str(
1348                &ProgressAriaInfo::create("x".into())
1349                    .with_current_value(f32::NAN)
1350                    .to_full_info()
1351                    .accessibility_value
1352            ),
1353            Some("NaN")
1354        );
1355        assert_eq!(
1356            name_str(
1357                &ProgressAriaInfo::create("x".into())
1358                    .with_current_value(f32::INFINITY)
1359                    .to_full_info()
1360                    .accessibility_value
1361            ),
1362            Some("inf")
1363        );
1364        assert_eq!(
1365            name_str(
1366                &ProgressAriaInfo::create("x".into())
1367                    .with_current_value(f32::NEG_INFINITY)
1368                    .to_full_info()
1369                    .accessibility_value
1370            ),
1371            Some("-inf")
1372        );
1373    }
1374
1375    // =====================================================================
1376    // 11. MeterAriaInfo::create — numeric (zero / min_max / negative / nan_inf)
1377    // =====================================================================
1378
1379    #[test]
1380    fn meter_create_zero() {
1381        let m = MeterAriaInfo::create("z".into(), 0.0, 0.0, 0.0);
1382        assert_eq!(m.current_value, 0.0);
1383        assert_eq!(m.min, 0.0);
1384        assert_eq!(m.max, 0.0);
1385        assert_eq!(name_str(&m.to_full_info().accessibility_value), Some("0"));
1386    }
1387
1388    #[test]
1389    fn meter_create_min_max() {
1390        let m = MeterAriaInfo::create("mm".into(), f32::MAX, f32::MIN, f32::MAX);
1391        assert_eq!(m.current_value, f32::MAX);
1392        assert_eq!(m.min, f32::MIN);
1393        assert_eq!(m.max, f32::MAX);
1394        // Formatting an extreme (but finite) float must not panic.
1395        assert!(m.to_full_info().accessibility_value.is_some());
1396    }
1397
1398    #[test]
1399    fn meter_create_negative() {
1400        let m = MeterAriaInfo::create("neg".into(), -5.0, -10.0, -1.0);
1401        assert_eq!(m.current_value, -5.0);
1402        assert_eq!(m.min, -10.0);
1403        assert_eq!(m.max, -1.0);
1404        assert_eq!(name_str(&m.to_full_info().accessibility_value), Some("-5"));
1405        // Inverted range (min > max) is accepted verbatim; no panic, no clamping.
1406        let inv = MeterAriaInfo::create("inv".into(), 5.0, 100.0, 0.0);
1407        assert_eq!(inv.min, 100.0);
1408        assert_eq!(inv.max, 0.0);
1409        assert!(inv.to_full_info().accessibility_value.is_some());
1410    }
1411
1412    #[test]
1413    fn meter_create_overflow_saturates_to_inf() {
1414        // f32 arithmetic saturates rather than panicking; feed the saturated
1415        // result straight in and confirm formatting stays defined.
1416        let over = f32::MAX * 2.0; // == +inf
1417        assert!(over.is_infinite());
1418        let m = MeterAriaInfo::create("o".into(), over, -over, over);
1419        assert_eq!(name_str(&m.to_full_info().accessibility_value), Some("inf"));
1420    }
1421
1422    #[test]
1423    fn meter_create_nan_inf() {
1424        // NaN preserved as NaN, no panic constructing or formatting.
1425        let m = MeterAriaInfo::create("n".into(), f32::NAN, 0.0, 1.0);
1426        assert!(m.current_value.is_nan());
1427        assert_eq!(name_str(&m.to_full_info().accessibility_value), Some("NaN"));
1428
1429        let pos = MeterAriaInfo::create("n".into(), f32::INFINITY, 0.0, 1.0);
1430        assert_eq!(name_str(&pos.to_full_info().accessibility_value), Some("inf"));
1431
1432        let neg = MeterAriaInfo::create("n".into(), f32::NEG_INFINITY, 0.0, 1.0);
1433        assert_eq!(name_str(&neg.to_full_info().accessibility_value), Some("-inf"));
1434
1435        // Non-finite bounds must not panic either.
1436        let bounds = MeterAriaInfo::create("n".into(), 0.5, f32::NEG_INFINITY, f32::INFINITY);
1437        assert!(bounds.min.is_infinite());
1438        assert!(bounds.max.is_infinite());
1439        assert!(bounds.to_full_info().accessibility_value.is_some());
1440    }
1441
1442    // =====================================================================
1443    // 12-14. MeterAriaInfo::with_low / with_high / with_optimum — numeric invariants
1444    // =====================================================================
1445
1446    #[test]
1447    fn meter_with_low_high_optimum_numeric() {
1448        for v in adversarial_f32() {
1449            let m = MeterAriaInfo::create("m".into(), 0.5, 0.0, 1.0)
1450                .with_low(v)
1451                .with_high(v)
1452                .with_optimum(v);
1453            for opt in [&m.low, &m.high, &m.optimum] {
1454                match f32_of(opt) {
1455                    Some(got) if v.is_nan() => assert!(got.is_nan()),
1456                    Some(got) => assert_eq!(got, v),
1457                    None => panic!("threshold should be Some after builder"),
1458                }
1459            }
1460            // Core value/range untouched by the threshold builders.
1461            assert_eq!(m.current_value, 0.5);
1462            assert_eq!(m.min, 0.0);
1463            assert_eq!(m.max, 1.0);
1464        }
1465    }
1466
1467    // =====================================================================
1468    // 15. MeterAriaInfo::with_description — no_panic + invariants
1469    // =====================================================================
1470
1471    #[test]
1472    fn meter_with_description_invariants() {
1473        for s in adversarial_strings() {
1474            let expected = s.clone();
1475            let m = MeterAriaInfo::create("m".into(), 1.0, 0.0, 2.0).with_description(s.into());
1476            assert_eq!(name_str(&m.description), Some(expected.as_str()));
1477            assert_eq!(m.current_value, 1.0);
1478        }
1479    }
1480
1481    // =====================================================================
1482    // 16. MeterAriaInfo::to_full_info — basic + edge
1483    // =====================================================================
1484
1485    #[test]
1486    fn meter_to_full_info_basic() {
1487        let info = MeterAriaInfo::create("Disk".into(), 42.0, 0.0, 100.0)
1488            .with_description("usage".into())
1489            .to_full_info();
1490        assert_eq!(name_str(&info.accessibility_name), Some("Disk"));
1491        assert_eq!(info.role, AccessibilityRole::Indicator);
1492        assert_eq!(name_str(&info.accessibility_value), Some("42"));
1493        assert_eq!(name_str(&info.description), Some("usage"));
1494        assert_eq!(info.states.len(), 0);
1495        assert_eq!(info.supported_actions.len(), 0);
1496    }
1497
1498    #[test]
1499    fn meter_to_full_info_edge() {
1500        // Meter always emits a value string (unlike progress). Even for an
1501        // extreme/empty-label instance it must not panic.
1502        let info = MeterAriaInfo::create("".into(), f32::MIN, f32::MIN, f32::MAX).to_full_info();
1503        assert!(info.accessibility_value.is_some());
1504        assert_eq!(info.role, AccessibilityRole::Indicator);
1505        assert_eq!(name_str(&info.accessibility_name), Some(""));
1506    }
1507
1508    // =====================================================================
1509    // 17. DialogAriaInfo::create — no_panic_smoke
1510    // =====================================================================
1511
1512    #[test]
1513    fn dialog_create_no_panic_smoke() {
1514        for s in adversarial_strings() {
1515            let expected = s.clone();
1516            let d = DialogAriaInfo::create(s.into());
1517            assert_eq!(name_str(&d.label), Some(expected.as_str()));
1518            // Documented defaults: non-modal, role Dialog, no describers.
1519            assert!(!d.modal);
1520            assert_eq!(d.role, AccessibilityRole::Dialog);
1521            assert!(d.described_by.is_none());
1522            assert!(d.description.is_none());
1523        }
1524    }
1525
1526    // =====================================================================
1527    // 18. DialogAriaInfo::with_modal — no_panic + invariants
1528    // =====================================================================
1529
1530    #[test]
1531    fn dialog_with_modal_invariants() {
1532        for flag in [true, false] {
1533            let d = DialogAriaInfo::create("t".into()).with_modal(flag);
1534            assert_eq!(d.modal, flag);
1535            // Unrelated fields keep their defaults.
1536            assert_eq!(d.role, AccessibilityRole::Dialog);
1537            assert_eq!(name_str(&d.label), Some("t"));
1538        }
1539    }
1540
1541    // =====================================================================
1542    // 19. DialogAriaInfo::with_described_by — no_panic + invariants
1543    // =====================================================================
1544
1545    #[test]
1546    fn dialog_with_described_by_invariants() {
1547        for s in adversarial_strings() {
1548            let expected = s.clone();
1549            let d = DialogAriaInfo::create("t".into()).with_described_by(s.into());
1550            assert_eq!(name_str(&d.described_by), Some(expected.as_str()));
1551            assert_eq!(name_str(&d.label), Some("t"));
1552        }
1553    }
1554
1555    // =====================================================================
1556    // 20. DialogAriaInfo::with_role — no_panic + invariants
1557    // =====================================================================
1558
1559    #[test]
1560    fn dialog_with_role_invariants() {
1561        for role in representative_roles() {
1562            let d = DialogAriaInfo::create("t".into()).with_role(role);
1563            assert_eq!(d.role, role);
1564            assert!(!d.modal);
1565        }
1566        // to_full_info propagates the overridden role verbatim.
1567        let info = DialogAriaInfo::create("t".into())
1568            .with_role(AccessibilityRole::Alert)
1569            .to_full_info();
1570        assert_eq!(info.role, AccessibilityRole::Alert);
1571    }
1572
1573    // =====================================================================
1574    // 21. DialogAriaInfo::with_description — no_panic + invariants
1575    // =====================================================================
1576
1577    #[test]
1578    fn dialog_with_description_invariants() {
1579        for s in adversarial_strings() {
1580            let expected = s.clone();
1581            let d = DialogAriaInfo::create("t".into()).with_description(s.into());
1582            assert_eq!(name_str(&d.description), Some(expected.as_str()));
1583            assert_eq!(name_str(&d.label), Some("t"));
1584        }
1585    }
1586
1587    // =====================================================================
1588    // 22. DialogAriaInfo::to_full_info — basic + edge
1589    // =====================================================================
1590
1591    #[test]
1592    fn dialog_to_full_info_basic() {
1593        let info = DialogAriaInfo::create("Confirm".into())
1594            .with_modal(true)
1595            .with_role(AccessibilityRole::Alert)
1596            .with_described_by("body-node".into())
1597            .with_description("Are you sure?".into())
1598            .to_full_info();
1599        assert_eq!(name_str(&info.accessibility_name), Some("Confirm"));
1600        assert_eq!(info.role, AccessibilityRole::Alert);
1601        assert_eq!(name_str(&info.description), Some("Are you sure?"));
1602        // The string `described_by` node-ref is NOT propagated into the DomNodeId field.
1603        assert!(info.described_by.is_none());
1604        assert!(info.labelled_by.is_none());
1605        assert!(info.accessibility_value.is_none());
1606        assert_eq!(info.states.len(), 0);
1607        assert_eq!(info.supported_actions.len(), 0);
1608    }
1609
1610    #[test]
1611    fn dialog_to_full_info_edge() {
1612        // Default (non-modal, empty) instance must convert without panic.
1613        let info = DialogAriaInfo::create("".into()).to_full_info();
1614        assert_eq!(info.role, AccessibilityRole::Dialog);
1615        assert_eq!(name_str(&info.accessibility_name), Some(""));
1616        assert!(info.description.is_none());
1617    }
1618
1619    // #####################################################################
1620    // Appended: round-trip, total-order and FFI-vec coverage.
1621    //
1622    // The block above exercises the 22 listed builder/getter fns. What it
1623    // does NOT cover is the machinery those fns feed into: the FFI vec/option
1624    // wrappers, and the `Eq`/`Ord`/`Hash` impls that `AccessibilityInfo`
1625    // derives *through* f32-carrying payloads (`LogicalPosition`,
1626    // `FloatValue`). Those derives are where a total-order contract can
1627    // silently break, so they get the adversarial treatment here.
1628    // #####################################################################
1629
1630    use core::hash::{Hash, Hasher};
1631
1632    use crate::{
1633        dom::{DomId, DomNodeId},
1634        styled_dom::NodeHierarchyItemId,
1635    };
1636
1637    /// FNV-1a. Hand-rolled rather than `DefaultHasher` so these tests still
1638    /// build when azul-core is compiled `--no-default-features` (i.e. `no_std`,
1639    /// where `std::collections::hash_map` does not exist).
1640    struct Fnv(u64);
1641
1642    impl Default for Fnv {
1643        fn default() -> Self {
1644            Self(0xcbf2_9ce4_8422_2325) // offset basis
1645        }
1646    }
1647
1648    impl Hasher for Fnv {
1649        fn finish(&self) -> u64 {
1650            self.0
1651        }
1652        fn write(&mut self, bytes: &[u8]) {
1653            for b in bytes {
1654                self.0 ^= u64::from(*b);
1655                self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3); // FNV prime
1656            }
1657        }
1658    }
1659
1660    fn hash_of<T: Hash>(t: &T) -> u64 {
1661        let mut h = Fnv::default();
1662        t.hash(&mut h);
1663        h.finish()
1664    }
1665
1666    /// Every `AccessibilityRole`, in declaration order.
1667    ///
1668    /// `Ord` is derived, so declaration order *is* the sort order — the tests
1669    /// below pin that. Kept in sync with the enum by `role_exhaustiveness_canary`.
1670    fn all_roles() -> Vec<AccessibilityRole> {
1671        use AccessibilityRole::*;
1672        vec![
1673            TitleBar, MenuBar, ScrollBar, Grip, Sound, Cursor, Caret, Alert, Window, Client,
1674            MenuPopup, MenuItem, Tooltip, Application, Document, Pane, Chart, Dialog, Border,
1675            Grouping, Separator, Toolbar, StatusBar, Table, ColumnHeader, RowHeader, Column, Row,
1676            Cell, Link, HelpBalloon, Character, List, ListItem, Outline, OutlineItem, PageTab,
1677            PropertyPage, Indicator, Graphic, StaticText, Text, PushButton, CheckButton,
1678            RadioButton, ComboBox, DropList, ProgressBar, Dial, HotkeyField, Slider, SpinButton,
1679            Diagram, Animation, Equation, ButtonDropdown, ButtonMenu, ButtonDropdownGrid,
1680            Whitespace, PageTabList, Clock, SplitButton, IpAddress, Nothing, Unknown,
1681        ]
1682    }
1683
1684    /// Every `AccessibilityState`, in declaration order.
1685    fn all_states() -> Vec<AccessibilityState> {
1686        use AccessibilityState::*;
1687        vec![
1688            Unavailable, Selected, Focused, CheckedTrue, CheckedFalse, Readonly, Default, Expanded,
1689            Collapsed, Busy, Offscreen, Focusable, Selectable, Linked, Traversed, Multiselectable,
1690            Protected,
1691        ]
1692    }
1693
1694    /// Exhaustive `match`es: if a variant is added upstream without being added
1695    /// to `all_roles()` / `all_states()`, this stops compiling. That is the
1696    /// point — it keeps the ordering tests below honest instead of letting them
1697    /// silently degrade into partial coverage.
1698    #[test]
1699    fn role_exhaustiveness_canary() {
1700        use AccessibilityRole::*;
1701        for r in all_roles() {
1702            let known = match r {
1703                TitleBar | MenuBar | ScrollBar | Grip | Sound | Cursor | Caret | Alert | Window
1704                | Client | MenuPopup | MenuItem | Tooltip | Application | Document | Pane | Chart
1705                | Dialog | Border | Grouping | Separator | Toolbar | StatusBar | Table
1706                | ColumnHeader | RowHeader | Column | Row | Cell | Link | HelpBalloon | Character
1707                | List | ListItem | Outline | OutlineItem | PageTab | PropertyPage | Indicator
1708                | Graphic | StaticText | Text | PushButton | CheckButton | RadioButton | ComboBox
1709                | DropList | ProgressBar | Dial | HotkeyField | Slider | SpinButton | Diagram
1710                | Animation | Equation | ButtonDropdown | ButtonMenu | ButtonDropdownGrid
1711                | Whitespace | PageTabList | Clock | SplitButton | IpAddress | Nothing | Unknown => true,
1712            };
1713            assert!(known);
1714        }
1715
1716        use AccessibilityState::*;
1717        for s in all_states() {
1718            let known = match s {
1719                Unavailable | Selected | Focused | CheckedTrue | CheckedFalse | Readonly
1720                | Default | Expanded | Collapsed | Busy | Offscreen | Focusable | Selectable
1721                | Linked | Traversed | Multiselectable | Protected => true,
1722            };
1723            assert!(known);
1724        }
1725    }
1726
1727    // =====================================================================
1728    // Total-order / Eq / Hash contracts on the plain C-like enums
1729    // =====================================================================
1730
1731    #[test]
1732    fn role_ord_is_strict_declaration_order() {
1733        let roles = all_roles();
1734        // Strictly increasing => derived Ord follows declaration order AND the
1735        // list has no duplicates.
1736        for pair in roles.windows(2) {
1737            assert!(
1738                pair[0] < pair[1],
1739                "roles must sort in declaration order: {:?} !< {:?}",
1740                pair[0],
1741                pair[1]
1742            );
1743        }
1744        // Trichotomy: for every ordered pair exactly one of <, ==, > holds.
1745        for a in &roles {
1746            for b in &roles {
1747                let lt = a < b;
1748                let eq = a == b;
1749                let gt = a > b;
1750                assert_eq!(
1751                    u8::from(lt) + u8::from(eq) + u8::from(gt),
1752                    1,
1753                    "trichotomy violated for {a:?} vs {b:?}"
1754                );
1755            }
1756        }
1757        // Reflexivity + the documented endpoints.
1758        assert_eq!(roles[0], AccessibilityRole::TitleBar);
1759        assert_eq!(*roles.last().unwrap(), AccessibilityRole::Unknown);
1760        assert!(AccessibilityRole::TitleBar < AccessibilityRole::Unknown);
1761    }
1762
1763    #[test]
1764    fn state_ord_is_strict_declaration_order() {
1765        let states = all_states();
1766        for pair in states.windows(2) {
1767            assert!(pair[0] < pair[1], "{:?} !< {:?}", pair[0], pair[1]);
1768        }
1769        // CheckedTrue / CheckedFalse are adjacent but must never compare equal —
1770        // aliasing them would make a checked and unchecked box indistinguishable.
1771        assert_ne!(AccessibilityState::CheckedTrue, AccessibilityState::CheckedFalse);
1772        assert_ne!(
1773            hash_of(&AccessibilityState::CheckedTrue),
1774            hash_of(&AccessibilityState::CheckedFalse)
1775        );
1776    }
1777
1778    #[test]
1779    fn role_and_state_hash_agrees_with_eq() {
1780        // Eq => equal hashes (the direction the Hash contract actually requires),
1781        // and Hash is deterministic across calls.
1782        for r in all_roles() {
1783            let copy = r;
1784            assert_eq!(hash_of(&r), hash_of(&copy));
1785        }
1786        for s in all_states() {
1787            let copy = s;
1788            assert_eq!(hash_of(&s), hash_of(&copy));
1789        }
1790
1791        // Stronger: no two distinct variants may collide. A collision here would
1792        // let two different roles/states alias as the same HashMap key. The
1793        // derive hashes the (necessarily distinct) discriminant, and FNV-1a's
1794        // multiply step is invertible mod 2^64, so distinctness is guaranteed —
1795        // this pins that no variant is ever given a duplicate discriminant.
1796        let role_hashes: Vec<u64> = all_roles().iter().map(hash_of).collect();
1797        for (i, a) in role_hashes.iter().enumerate() {
1798            for (j, b) in role_hashes.iter().enumerate() {
1799                assert_eq!(i == j, a == b, "role hash collision at {i}/{j}");
1800            }
1801        }
1802
1803        let state_hashes: Vec<u64> = all_states().iter().map(hash_of).collect();
1804        for (i, a) in state_hashes.iter().enumerate() {
1805            for (j, b) in state_hashes.iter().enumerate() {
1806                assert_eq!(i == j, a == b, "state hash collision at {i}/{j}");
1807            }
1808        }
1809    }
1810
1811    // =====================================================================
1812    // AccessibilityStateVec — FFI vec round-trip
1813    // =====================================================================
1814
1815    #[test]
1816    fn state_vec_round_trips_through_ffi_wrapper() {
1817        let cases: Vec<Vec<AccessibilityState>> = vec![
1818            Vec::new(),
1819            vec![AccessibilityState::Focused],
1820            all_states(),
1821            // duplicates must survive verbatim (this is a Vec, not a Set)
1822            vec![
1823                AccessibilityState::Busy,
1824                AccessibilityState::Busy,
1825                AccessibilityState::Busy,
1826            ],
1827            // large allocation: the FFI wrapper owns the buffer, so this is the
1828            // shape most likely to trip a bad len/cap or double-free.
1829            core::iter::repeat(AccessibilityState::Selected).take(10_000).collect(),
1830        ];
1831
1832        for original in cases {
1833            let wrapped: AccessibilityStateVec = original.clone().into();
1834
1835            // len / is_empty stay consistent with the source Vec.
1836            assert_eq!(wrapped.len(), original.len());
1837            assert_eq!(wrapped.is_empty(), original.is_empty());
1838            assert_eq!(wrapped.as_slice(), original.as_slice());
1839            assert_eq!(wrapped.iter().count(), original.len());
1840
1841            // Clone must deep-copy: equal content, and dropping the clone must
1842            // not invalidate the original (both are dropped at end of scope).
1843            let cloned = wrapped.clone();
1844            assert_eq!(cloned.as_slice(), original.as_slice());
1845            assert_eq!(cloned, wrapped);
1846            assert_eq!(hash_of(&cloned), hash_of(&wrapped));
1847            drop(cloned);
1848            assert_eq!(wrapped.as_slice(), original.as_slice());
1849
1850            // Round-trip back out: decode(encode(x)) == x.
1851            let back = wrapped.into_library_owned_vec();
1852            assert_eq!(back, original);
1853        }
1854    }
1855
1856    #[test]
1857    fn state_vec_indexing_is_bounds_safe() {
1858        let v: AccessibilityStateVec = all_states().into();
1859        let len = v.len();
1860
1861        for (i, expected) in all_states().into_iter().enumerate() {
1862            assert_eq!(v.get(i), Some(&expected));
1863        }
1864        // One-past-the-end and the pathological index must return None, not panic.
1865        assert_eq!(v.get(len), None);
1866        assert_eq!(v.get(len + 1), None);
1867        assert_eq!(v.get(usize::MAX), None);
1868        assert!(v.c_get(usize::MAX).is_none());
1869        assert!(v.c_get(len).is_none());
1870        assert!(v.c_get(0).is_some());
1871
1872        // The empty vec has no valid index at all.
1873        let empty = AccessibilityStateVec::new();
1874        assert!(empty.is_empty());
1875        assert_eq!(empty.get(0), None);
1876        assert_eq!(empty.get(usize::MAX), None);
1877        assert!(empty.c_get(0).is_none());
1878    }
1879
1880    #[test]
1881    fn state_vec_from_vec_preserves_order_len_and_lookup() {
1882        // The C-ABI vec is built from a Rust Vec and is then read-only — it has
1883        // no push/pop. Assert the round-trip is lossless and lookups agree.
1884        let empty = AccessibilityStateVec::new();
1885        assert_eq!(empty.len(), 0);
1886        assert!(empty.is_empty());
1887        assert_eq!(empty.get(0), None);
1888
1889        let states = all_states();
1890        let v = AccessibilityStateVec::from_vec(states.clone());
1891        assert_eq!(v.len(), states.len());
1892        assert!(!v.is_empty());
1893        assert!(v.capacity() >= v.len(), "capacity must never trail len");
1894
1895        // Order is preserved and every index is reachable.
1896        assert_eq!(v.as_slice(), states.as_slice());
1897        for (i, s) in states.iter().enumerate() {
1898            assert_eq!(v.get(i), Some(s));
1899        }
1900        assert_eq!(
1901            v.get(states.len()),
1902            None,
1903            "out-of-bounds must be None, not a panic"
1904        );
1905        assert!(v.iter().eq(states.iter()));
1906    }
1907
1908    // =====================================================================
1909    // AccessibilityAction — payload-carrying variants
1910    // =====================================================================
1911
1912    /// One instance of every `AccessibilityAction` variant, in declaration order.
1913    fn all_actions() -> Vec<AccessibilityAction> {
1914        use AccessibilityAction::*;
1915        vec![
1916            Default,
1917            Focus,
1918            Blur,
1919            Collapse,
1920            Expand,
1921            ScrollIntoView,
1922            Increment,
1923            Decrement,
1924            ShowContextMenu,
1925            HideTooltip,
1926            ShowTooltip,
1927            ScrollUp,
1928            ScrollDown,
1929            ScrollLeft,
1930            ScrollRight,
1931            ReplaceSelectedText("replacement".into()),
1932            ScrollToPoint(LogicalPosition::new(1.0, 2.0)),
1933            SetScrollOffset(LogicalPosition::new(-3.0, 4.0)),
1934            SetTextSelection(TextSelectionStartEnd {
1935                selection_start: 0,
1936                selection_end: 5,
1937            }),
1938            SetSequentialFocusNavigationStartingPoint,
1939            SetValue("value".into()),
1940            SetNumericValue(FloatValue::new(1.5)),
1941            CustomAction(42),
1942        ]
1943    }
1944
1945    #[test]
1946    fn action_vec_round_trips_with_payloads() {
1947        let original = all_actions();
1948        let wrapped: AccessibilityActionVec = original.clone().into();
1949
1950        assert_eq!(wrapped.len(), original.len());
1951        assert_eq!(wrapped.as_slice(), original.as_slice());
1952
1953        // The payload variants own heap data (AzString). Cloning must deep-copy;
1954        // dropping the clone must leave the original intact (no double-free).
1955        let cloned = wrapped.clone();
1956        assert_eq!(cloned, wrapped);
1957        assert_eq!(hash_of(&cloned), hash_of(&wrapped));
1958        drop(cloned);
1959        assert_eq!(wrapped.as_slice(), original.as_slice());
1960
1961        let back = wrapped.into_library_owned_vec();
1962        assert_eq!(back, original);
1963
1964        // Variant order dominates payload in the derived Ord.
1965        for pair in original.windows(2) {
1966            assert!(pair[0] < pair[1], "{:?} !< {:?}", pair[0], pair[1]);
1967        }
1968    }
1969
1970    #[test]
1971    fn action_string_payloads_survive_adversarial_strings() {
1972        for s in adversarial_strings() {
1973            let expected = s.clone();
1974
1975            let replace = AccessibilityAction::ReplaceSelectedText(s.clone().into());
1976            let set = AccessibilityAction::SetValue(s.into());
1977
1978            // Payload preserved verbatim — including interior NUL and lone
1979            // combining marks, which a C-string round-trip would truncate.
1980            match &replace {
1981                AccessibilityAction::ReplaceSelectedText(got) => {
1982                    assert_eq!(got.as_str(), expected.as_str());
1983                    assert_eq!(got.as_str().len(), expected.len());
1984                }
1985                other => panic!("wrong variant: {other:?}"),
1986            }
1987            match &set {
1988                AccessibilityAction::SetValue(got) => assert_eq!(got.as_str(), expected.as_str()),
1989                other => panic!("wrong variant: {other:?}"),
1990            }
1991
1992            // Clone/Eq/Hash agree even for the pathological payloads.
1993            assert_eq!(replace.clone(), replace);
1994            assert_eq!(hash_of(&replace.clone()), hash_of(&replace));
1995            // Different variants with the *same* payload must never alias.
1996            assert_ne!(replace, set);
1997        }
1998    }
1999
2000    #[test]
2001    fn action_custom_action_i32_limits() {
2002        let min = AccessibilityAction::CustomAction(i32::MIN);
2003        let zero = AccessibilityAction::CustomAction(0);
2004        let max = AccessibilityAction::CustomAction(i32::MAX);
2005
2006        // Signed ordering, not a bit-pattern/unsigned ordering.
2007        assert!(min < zero, "i32::MIN must sort below 0");
2008        assert!(zero < max);
2009        assert!(min < max);
2010
2011        assert_eq!(min, AccessibilityAction::CustomAction(i32::MIN));
2012        assert_ne!(min, max);
2013        assert_eq!(hash_of(&min), hash_of(&AccessibilityAction::CustomAction(i32::MIN)));
2014
2015        // -1 must not alias u32::MAX-style onto anything.
2016        assert_ne!(
2017            AccessibilityAction::CustomAction(-1),
2018            AccessibilityAction::CustomAction(i32::MAX)
2019        );
2020    }
2021
2022    #[test]
2023    fn text_selection_start_end_limits() {
2024        // usize::MAX bounds: constructing and comparing must not overflow.
2025        let huge = TextSelectionStartEnd {
2026            selection_start: usize::MAX,
2027            selection_end: usize::MAX,
2028        };
2029        assert_eq!(huge.selection_start, usize::MAX);
2030        assert_eq!(huge.selection_end, usize::MAX);
2031        assert_eq!(huge, huge);
2032
2033        // Inverted range (start > end) is accepted verbatim — the type does not
2034        // normalise or clamp, so downstream consumers must not assume start<=end.
2035        let inverted = TextSelectionStartEnd {
2036            selection_start: 10,
2037            selection_end: 0,
2038        };
2039        assert_eq!(inverted.selection_start, 10);
2040        assert_eq!(inverted.selection_end, 0);
2041        assert_ne!(
2042            inverted,
2043            TextSelectionStartEnd {
2044                selection_start: 0,
2045                selection_end: 10,
2046            }
2047        );
2048
2049        // Collapsed (zero-length) selection is distinct from an empty-at-zero one.
2050        let collapsed = TextSelectionStartEnd {
2051            selection_start: 7,
2052            selection_end: 7,
2053        };
2054        assert_ne!(
2055            collapsed,
2056            TextSelectionStartEnd {
2057                selection_start: 0,
2058                selection_end: 0,
2059            }
2060        );
2061
2062        // Ord is lexicographic (start, then end).
2063        let a = TextSelectionStartEnd {
2064            selection_start: 1,
2065            selection_end: 99,
2066        };
2067        let b = TextSelectionStartEnd {
2068            selection_start: 2,
2069            selection_end: 0,
2070        };
2071        assert!(a < b, "selection_start must dominate the ordering");
2072
2073        // Wrapped in the action, the same invariants hold.
2074        let action = AccessibilityAction::SetTextSelection(huge);
2075        assert_eq!(action.clone(), action);
2076        assert_eq!(hash_of(&action.clone()), hash_of(&action));
2077    }
2078
2079    // =====================================================================
2080    // f32-carrying payloads: the Eq/Ord/Hash total-order contract
2081    //
2082    // `AccessibilityAction` *derives* Eq + Ord + Hash while carrying
2083    // `LogicalPosition` (two f32s) and `FloatValue`. f32 is not Eq/Ord, so
2084    // those inner types must supply total impls. These tests pin the actual
2085    // behaviour at NaN / inf / overflow, where a naive impl breaks the
2086    // reflexivity (a == a) that HashMap and BTreeMap rely on.
2087    // =====================================================================
2088
2089    #[test]
2090    fn scroll_to_point_nan_is_reflexive_and_totally_ordered() {
2091        let nan = AccessibilityAction::ScrollToPoint(LogicalPosition::new(f32::NAN, f32::NAN));
2092        let origin = AccessibilityAction::ScrollToPoint(LogicalPosition::new(0.0, 0.0));
2093
2094        // Reflexivity: `Eq` promises a == a. Raw f32 PartialEq would return
2095        // false here and quietly corrupt any HashMap keyed on this action.
2096        assert_eq!(nan, nan.clone());
2097        assert_eq!(hash_of(&nan), hash_of(&nan.clone()));
2098        assert_eq!(nan.cmp(&nan.clone()), core::cmp::Ordering::Equal);
2099
2100        // NaN must NOT alias onto the origin (LogicalPosition::quantize maps NaN
2101        // to a dedicated i64::MIN sentinel precisely to avoid that collision).
2102        assert_ne!(nan, origin);
2103        assert_ne!(hash_of(&nan), hash_of(&origin));
2104        assert!(nan < origin, "NaN sorts below every real coordinate");
2105
2106        // Ord is total: every pair of these is comparable and antisymmetric.
2107        let neg = AccessibilityAction::ScrollToPoint(LogicalPosition::new(-1.0, -1.0));
2108        let pos = AccessibilityAction::ScrollToPoint(LogicalPosition::new(1.0, 1.0));
2109        let mut sorted = vec![pos.clone(), origin.clone(), nan.clone(), neg.clone()];
2110        sorted.sort();
2111        assert_eq!(sorted, vec![nan, neg, origin, pos]);
2112    }
2113
2114    #[test]
2115    fn scroll_to_point_infinite_coords_saturate_without_panic() {
2116        let inf = AccessibilityAction::SetScrollOffset(LogicalPosition::new(
2117            f32::INFINITY,
2118            f32::NEG_INFINITY,
2119        ));
2120        let finite = AccessibilityAction::SetScrollOffset(LogicalPosition::new(1.0, 1.0));
2121
2122        // Defined, reflexive, no panic on the fixed-point conversion.
2123        assert_eq!(inf, inf.clone());
2124        assert_eq!(hash_of(&inf), hash_of(&inf.clone()));
2125        assert!(inf > finite, "+inf x-coordinate must sort above a finite one");
2126
2127        // Documented saturation: the fixed-point quantisation clamps, so
2128        // f32::MAX and +inf land in the same bucket. Asserted so a future
2129        // change to the quantiser has to consciously break this.
2130        let max = AccessibilityAction::SetScrollOffset(LogicalPosition::new(f32::MAX, f32::MAX));
2131        let plus_inf =
2132            AccessibilityAction::SetScrollOffset(LogicalPosition::new(f32::INFINITY, f32::INFINITY));
2133        assert_eq!(
2134            max, plus_inf,
2135            "f32::MAX and +inf both saturate to the same quantised coordinate"
2136        );
2137    }
2138
2139    #[test]
2140    fn set_numeric_value_float_edges_are_defined() {
2141        // Representable-under-quantisation values round-trip exactly
2142        // (FloatValue is fixed-point with a 1/1000 quantum).
2143        for v in [0.0_f32, 1.5, -1.5, 2.25, -3.75, 1000.0] {
2144            let f = FloatValue::new(v);
2145            assert_eq!(f.get(), v, "FloatValue must round-trip {v}");
2146            let action = AccessibilityAction::SetNumericValue(f);
2147            assert_eq!(action.clone(), action);
2148        }
2149
2150        // Non-finite input must not panic. `as isize` saturates, so:
2151        assert_eq!(FloatValue::new(f32::INFINITY).number(), isize::MAX);
2152        assert_eq!(FloatValue::new(f32::NEG_INFINITY).number(), isize::MIN);
2153
2154        // NOTE (reported, not a weakened assertion): FloatValue::new maps NaN to
2155        // 0 via a raw `as isize` cast, so a NaN numeric value is INDISTINGUISHABLE
2156        // from 0.0. LogicalPosition::quantize explicitly fixed this same aliasing
2157        // (NaN -> i64::MIN sentinel); FloatValue still has it. Pinning the current
2158        // behaviour so the aliasing is visible and a fix has to update this test.
2159        assert_eq!(FloatValue::new(f32::NAN).number(), 0);
2160        assert_eq!(
2161            AccessibilityAction::SetNumericValue(FloatValue::new(f32::NAN)),
2162            AccessibilityAction::SetNumericValue(FloatValue::new(0.0)),
2163            "KNOWN ALIASING: NaN numeric value collides with 0.0"
2164        );
2165
2166        // Reflexivity still holds for the NaN case (it is Eq-safe, just aliased).
2167        let nan_action = AccessibilityAction::SetNumericValue(FloatValue::new(f32::NAN));
2168        assert_eq!(hash_of(&nan_action), hash_of(&nan_action.clone()));
2169
2170        // f32::MAX overflows the fixed-point scale and saturates rather than wrapping.
2171        assert_eq!(FloatValue::new(f32::MAX).number(), isize::MAX);
2172        assert_eq!(FloatValue::new(f32::MIN).number(), isize::MIN);
2173    }
2174
2175    // =====================================================================
2176    // Float -> value-string encoding: format/parse round-trip
2177    // =====================================================================
2178
2179    #[test]
2180    fn progress_value_string_round_trips_through_parse() {
2181        for v in adversarial_f32() {
2182            let full = ProgressAriaInfo::create("p".into())
2183                .with_current_value(v)
2184                .to_full_info();
2185            let s = name_str(&full.accessibility_value).expect("determinate => Some");
2186
2187            if v.is_nan() {
2188                assert_eq!(s, "NaN");
2189                assert!(s.parse::<f32>().unwrap().is_nan());
2190            } else if v.is_infinite() {
2191                assert_eq!(s, if v > 0.0 { "inf" } else { "-inf" });
2192            } else {
2193                // Display for f32 is shortest-round-trip: decode(encode(v)) == v.
2194                let parsed: f32 = s.parse().expect("emitted value string must re-parse");
2195                assert_eq!(parsed, v, "round-trip failed for {v} via {s:?}");
2196                if v != 0.0 {
2197                    // Bit-exact for everything except +0.0/-0.0, which compare
2198                    // equal under `==` by definition.
2199                    assert_eq!(parsed.to_bits(), v.to_bits(), "lossy round-trip for {v}");
2200                }
2201            }
2202        }
2203    }
2204
2205    #[test]
2206    fn meter_value_string_round_trips_through_parse() {
2207        for v in adversarial_f32() {
2208            let full = MeterAriaInfo::create("m".into(), v, 0.0, 1.0).to_full_info();
2209            // Meter ALWAYS emits a value string (unlike progress).
2210            let s = name_str(&full.accessibility_value).expect("meter always emits a value");
2211
2212            if v.is_nan() {
2213                assert_eq!(s, "NaN");
2214            } else if v.is_infinite() {
2215                assert_eq!(s, if v > 0.0 { "inf" } else { "-inf" });
2216            } else {
2217                let parsed: f32 = s.parse().expect("emitted value string must re-parse");
2218                assert_eq!(parsed, v);
2219                if v != 0.0 {
2220                    assert_eq!(parsed.to_bits(), v.to_bits());
2221                }
2222            }
2223        }
2224    }
2225
2226    // =====================================================================
2227    // Builder algebra: purity, idempotence, last-write-wins, order-independence
2228    // =====================================================================
2229
2230    #[test]
2231    fn to_full_info_is_pure_and_idempotent() {
2232        let small = SmallAriaInfo::label("s")
2233            .with_role(AccessibilityRole::Slider)
2234            .with_description("d");
2235        let progress = ProgressAriaInfo::create("p".into())
2236            .with_current_value(0.25)
2237            .with_max(10.0);
2238        let meter = MeterAriaInfo::create("m".into(), 1.0, 0.0, 2.0).with_low(0.5);
2239        let dialog = DialogAriaInfo::create("d".into()).with_modal(true);
2240
2241        // &self getters must not mutate the receiver, and must be deterministic:
2242        // f(x) == f(x) for repeated calls.
2243        let (s0, p0, m0, d0) = (small.clone(), progress.clone(), meter.clone(), dialog.clone());
2244
2245        assert_eq!(small.to_full_info(), small.to_full_info());
2246        assert_eq!(progress.to_full_info(), progress.to_full_info());
2247        assert_eq!(meter.to_full_info(), meter.to_full_info());
2248        assert_eq!(dialog.to_full_info(), dialog.to_full_info());
2249
2250        assert_eq!(small, s0, "to_full_info must not mutate SmallAriaInfo");
2251        assert_eq!(progress, p0, "to_full_info must not mutate ProgressAriaInfo");
2252        assert_eq!(meter, m0, "to_full_info must not mutate MeterAriaInfo");
2253        assert_eq!(dialog, d0, "to_full_info must not mutate DialogAriaInfo");
2254
2255        // Idempotent even when the value is NaN — the AccessibilityInfo carries a
2256        // *string* ("NaN"), which is Eq-comparable, so this holds where a raw f32
2257        // comparison would not.
2258        let nan_meter = MeterAriaInfo::create("m".into(), f32::NAN, 0.0, 1.0);
2259        assert_eq!(nan_meter.to_full_info(), nan_meter.to_full_info());
2260    }
2261
2262    #[test]
2263    fn progress_max_is_never_surfaced_in_full_info() {
2264        // `max` has no representation in AccessibilityInfo, so setting it to
2265        // anything at all — including inf/NaN — must not perturb the conversion.
2266        let baseline = ProgressAriaInfo::create("p".into())
2267            .with_current_value(0.5)
2268            .to_full_info();
2269
2270        for v in adversarial_f32() {
2271            let with_max = ProgressAriaInfo::create("p".into())
2272                .with_current_value(0.5)
2273                .with_max(v)
2274                .to_full_info();
2275            assert_eq!(with_max, baseline, "with_max({v}) leaked into to_full_info");
2276        }
2277    }
2278
2279    #[test]
2280    fn meter_threshold_builders_are_order_independent_and_last_write_wins() {
2281        // Order-independence: the three threshold setters touch disjoint fields.
2282        let a = MeterAriaInfo::create("m".into(), 0.5, 0.0, 1.0)
2283            .with_low(0.1)
2284            .with_high(0.9)
2285            .with_optimum(0.7);
2286        let b = MeterAriaInfo::create("m".into(), 0.5, 0.0, 1.0)
2287            .with_optimum(0.7)
2288            .with_high(0.9)
2289            .with_low(0.1);
2290        assert_eq!(a, b);
2291
2292        // Last-write-wins, including when the second write is a non-finite value.
2293        let m = MeterAriaInfo::create("m".into(), 0.5, 0.0, 1.0)
2294            .with_low(0.1)
2295            .with_low(f32::INFINITY);
2296        assert_eq!(f32_of(&m.low), Some(f32::INFINITY));
2297
2298        // Nonsensical-but-accepted config: low > high, optimum outside [min,max].
2299        // The type performs no validation; assert it stores them verbatim rather
2300        // than silently clamping (downstream code must do its own validation).
2301        let weird = MeterAriaInfo::create("m".into(), 5.0, 0.0, 1.0)
2302            .with_low(100.0)
2303            .with_high(-100.0)
2304            .with_optimum(-1.0);
2305        assert_eq!(f32_of(&weird.low), Some(100.0));
2306        assert_eq!(f32_of(&weird.high), Some(-100.0));
2307        assert_eq!(f32_of(&weird.optimum), Some(-1.0));
2308        assert_eq!(weird.current_value, 5.0); // out of [min,max], not clamped
2309        assert!(weird.to_full_info().accessibility_value.is_some());
2310    }
2311
2312    #[test]
2313    fn progress_and_dialog_builders_last_write_wins() {
2314        let p = ProgressAriaInfo::create("p".into())
2315            .with_current_value(1.0)
2316            .with_current_value(2.0)
2317            .with_indeterminate(true)
2318            .with_indeterminate(false)
2319            .with_description("a".into())
2320            .with_description("b".into());
2321        assert_eq!(f32_of(&p.current_value), Some(2.0));
2322        assert!(!p.indeterminate);
2323        assert_eq!(name_str(&p.description), Some("b"));
2324        // Not indeterminate => the (last) current value is surfaced.
2325        assert_eq!(name_str(&p.to_full_info().accessibility_value), Some("2"));
2326
2327        let d = DialogAriaInfo::create("d".into())
2328            .with_modal(true)
2329            .with_modal(false)
2330            .with_role(AccessibilityRole::Alert)
2331            .with_role(AccessibilityRole::Dialog)
2332            .with_described_by("x".into())
2333            .with_described_by("y".into());
2334        assert!(!d.modal);
2335        assert_eq!(d.role, AccessibilityRole::Dialog);
2336        assert_eq!(name_str(&d.described_by), Some("y"));
2337    }
2338
2339    // =====================================================================
2340    // AccessibilityInfo — the fully-populated aggregate
2341    // =====================================================================
2342
2343    fn full_info_fixture() -> AccessibilityInfo {
2344        AccessibilityInfo {
2345            accessibility_name: OptionString::Some("name".into()),
2346            accessibility_value: OptionString::Some("value".into()),
2347            description: OptionString::Some("desc".into()),
2348            accelerator: OptionVirtualKeyCodeCombo::None,
2349            default_action: OptionString::Some("activate".into()),
2350            states: all_states().into(),
2351            supported_actions: all_actions().into(),
2352            labelled_by: OptionDomNodeId::Some(DomNodeId::ROOT),
2353            described_by: OptionDomNodeId::Some(DomNodeId {
2354                dom: DomId { inner: 3 },
2355                node: NodeHierarchyItemId::from_raw(7),
2356            }),
2357            role: AccessibilityRole::PushButton,
2358            is_live_region: true,
2359        }
2360    }
2361
2362    #[test]
2363    fn full_info_clone_eq_hash_ord_are_consistent() {
2364        let a = full_info_fixture();
2365        let b = a.clone();
2366
2367        // Deep clone: equal, equally hashed, mutually Equal under Ord.
2368        assert_eq!(a, b);
2369        assert_eq!(hash_of(&a), hash_of(&b));
2370        assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
2371
2372        // The clone owns its own heap buffers — dropping it must leave `a` intact.
2373        drop(b);
2374        assert_eq!(a.states.len(), all_states().len());
2375        assert_eq!(a.supported_actions.len(), all_actions().len());
2376        assert_eq!(name_str(&a.accessibility_name), Some("name"));
2377
2378        // Perturbing any single field must break equality (no field is ignored
2379        // by the derived PartialEq — a field silently dropped from the derive
2380        // would let two different a11y nodes compare equal).
2381        let mut differs = a.clone();
2382        differs.is_live_region = false;
2383        assert_ne!(a, differs);
2384
2385        let mut differs = a.clone();
2386        differs.role = AccessibilityRole::Unknown;
2387        assert_ne!(a, differs);
2388
2389        let mut differs = a.clone();
2390        differs.labelled_by = OptionDomNodeId::None;
2391        assert_ne!(a, differs);
2392
2393        let mut differs = a.clone();
2394        differs.states = Vec::new().into();
2395        assert_ne!(a, differs);
2396
2397        let mut differs = a.clone();
2398        differs.supported_actions = Vec::new().into();
2399        assert_ne!(a, differs);
2400
2401        let mut differs = a.clone();
2402        differs.default_action = OptionString::None;
2403        assert_ne!(a, differs);
2404    }
2405
2406    // =====================================================================
2407    // Option<T> FFI wrappers — Some/None round-trip
2408    // =====================================================================
2409
2410    #[test]
2411    fn option_wrappers_round_trip() {
2412        // Copy payloads.
2413        for r in all_roles() {
2414            let opt = OptionAccessibilityRole::Some(r);
2415            assert!(opt.is_some());
2416            assert!(!opt.is_none());
2417            assert_eq!(opt.as_ref(), Some(&r));
2418            assert_eq!(opt.into_option(), Some(r));
2419        }
2420        assert!(OptionAccessibilityRole::None.is_none());
2421        assert_eq!(OptionAccessibilityRole::None.into_option(), None);
2422
2423        for s in all_states() {
2424            assert_eq!(OptionAccessibilityState::Some(s).into_option(), Some(s));
2425        }
2426        assert_eq!(OptionAccessibilityState::None.into_option(), None);
2427
2428        // Non-Copy payloads (heap-owning) must round-trip without a double-free.
2429        for a in all_actions() {
2430            let opt = OptionAccessibilityAction::Some(a.clone());
2431            assert!(opt.is_some());
2432            assert_eq!(opt.into_option(), Some(a));
2433        }
2434        assert!(OptionAccessibilityAction::None.is_none());
2435
2436        let small = SmallAriaInfo::label("s").with_role(AccessibilityRole::Link);
2437        assert_eq!(
2438            OptionSmallAriaInfo::Some(small.clone()).into_option(),
2439            Some(small)
2440        );
2441        assert!(OptionSmallAriaInfo::None.is_none());
2442
2443        let progress = ProgressAriaInfo::create("p".into()).with_current_value(0.5);
2444        assert_eq!(
2445            OptionProgressAriaInfo::Some(progress.clone()).into_option(),
2446            Some(progress)
2447        );
2448
2449        let meter = MeterAriaInfo::create("m".into(), 1.0, 0.0, 2.0);
2450        assert_eq!(
2451            OptionMeterAriaInfo::Some(meter.clone()).into_option(),
2452            Some(meter)
2453        );
2454
2455        let dialog = DialogAriaInfo::create("d".into()).with_modal(true);
2456        assert_eq!(
2457            OptionDialogAriaInfo::Some(dialog.clone()).into_option(),
2458            Some(dialog)
2459        );
2460
2461        // The big aggregate, which owns two FFI vecs.
2462        let info = full_info_fixture();
2463        assert_eq!(
2464            OptionAccessibilityInfo::Some(info.clone()).into_option(),
2465            Some(info)
2466        );
2467        assert!(OptionAccessibilityInfo::None.is_none());
2468    }
2469}