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(©));
1785 }
1786 for s in all_states() {
1787 let copy = s;
1788 assert_eq!(hash_of(&s), hash_of(©));
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}