@atomic-testing/component-driver-fluent-v9
v0.104.0
Published
Component driver for Fluent UI v9 ("Fluent 2") React components (@fluentui/react-components)
Readme
@atomic-testing/component-driver-fluent-v9
Component drivers for Fluent UI v9 ("Fluent 2", @fluentui/react-components). Component drivers expose simple APIs for unit tests or end-to-end tests to interact with Fluent-based components—reading state and driving actions—so test engineers focus on test flows instead of the component internals.
The problem
Fluent v9 styles every component with Griffel, an atomic CSS-in-JS engine — the classes it emits are hashed and change across builds, so they are not stable test anchors. Fluent also ships a strong accessibility program (it is the Microsoft 365/Office design system), so the stable anchors, in priority order, are:
role+ ARIA state — Fluent renders correct roles/aria-*per component (e.g.aria-pressedonToggleButton,aria-disabledonLink).- Fluent's own un-hashed structural classes — every component stamps a plain
fui-<ComponentName>class (andfui-<ComponentName>__<part>for sub-parts, e.g.fui-Field__hint) alongside the hashed Griffel utility classes. These are Fluent-owned and stable across releases; the drivers in this package use them where role/ARIA isn't enough (e.g.FieldDriver's hint/validation-message reads). - Never the hashed Griffel utility classes.
Several core controls (Input, Textarea, Checkbox, Switch, Radio, Select) render as real native form elements at their root — data-testid (or any locator) placed on the component lands directly on the native <input>/<textarea>/<select>, not a styled wrapper — so this package reuses @atomic-testing/component-driver-html's drivers wholesale wherever that holds.
The solution
The drivers in this package locate Fluent parts by those stable anchors and expose high-level interactions. Combined with the React adapter (@atomic-testing/react-19 or another React major), the same scene definitions run across DOM (jsdom) and end-to-end (Playwright) tests.
Target package & version pin
This driver targets Fluent UI v9 and is declared as a peer dependency pinned to ^9.0.0: consumers bring their own @fluentui/react-components. Fluent v8 (@fluentui/react) is a materially different DOM/styling contract (mergeStyles, no Griffel) and is out of scope for this package.
Installation
npm install @atomic-testing/core @atomic-testing/react-19 \
@atomic-testing/component-driver-html @atomic-testing/component-driver-fluent-v9 \
@fluentui/react-components --save-devRefer to the documentation for usage patterns and examples.
Portal & overlay recipe (Wave 2)
Dialog, Popover, Drawer (OverlayDrawer), Menu, Toast/Toaster, and TeachingPopover all portal by default — verified against rendered DOM (@fluentui/[email protected]): each mounts into a cloned FluentProvider on document.body (Fluent's own mountNode default, confirmed against @fluentui/react-portal's types), a sibling of the render root rather than a descendant of the trigger. InlineDrawer is the one exception — it renders in-tree, no portal at all. Two distinct re-root techniques cover this wave, chosen per component by whether the scene's own locator can land directly on the portalled surface:
- Static re-root + class compounding (
Dialog,Popover,OverlayDrawer,TeachingPopover,Toaster) — the driver overrides theoverriddenParentLocator()/overrideLocatorRelativePosition()static hooks (seepackages/core/src/drivers/ComponentDriver.ts) to re-root at the un-hashed Fluent structural class of that surface (e.g..fui-DialogSurface,.fui-PopoverSurface), and the scene's own locator (forwarded onto the surface component, e.g.<DialogSurface data-testid="...">) compounds onto that SAME element. Two simultaneously open instances disambiguate correctly because each surface only matches its own forwarded test id — verified with two open dialogs/popovers side by side. Anchored on the class rather thanrolewhere the role is shared:role="dialog"alone is worn byDialog,OverlayDrawer, andTeachingPopover;role="group"is far too generic forPopoveralone. - Trigger-anchored +
byLinkedElement(Menu) — the driver is constructed from the TRIGGER locator and resolves the portalledMenuListby following the trigger'sidto the list'saria-labelledby, re-read fresh on every call (byLinkedElement, the same techniquecomponent-driver-radix-v1uses for its ownaria-controls/aria-describedbylinks). Necessary becauserole="menu"/role="presentation"are identical across every simultaneously open menu — a static class/role re-root cannot tell "this menu" from a sibling one, but the trigger↔list id link can. Verified with two open menus side by side. - Trigger-anchored +
byLinkedElementonaria-controls(Combobox,Dropdown,TagPicker, Wave 3) — the samebyLinkedElementidiom asMenu, but following the trigger/input'saria-controls(notaria-labelledby) to the portalled listbox, and the attribute is present ONLY while open (absent entirely, not merely empty, once closed) — every listbox-reading method guards the resolution in try/catch, andisOpen()reads the trigger's ownaria-expandedrather than listbox existence, since Fluent keeps the listbox mounted briefly after a legitimate close.
Tooltip is trigger-anchored too, but for a different reason: its content carries no reliable per-instance link in the default relationship="label" mode (only relationship="description" sets aria-describedby), so isOpen() falls back to a best-effort shared-portal check in that mode — see the Known gaps section below and TooltipDriver's TSDoc.
Escape dismisses the topmost stacked overlay, not a specific targeted one — verified against real Chromium: with two overlays open, Escape always closes the most-recently-opened one, regardless of which overlay's locator the key event is dispatched on (Fluent's dismiss handling is a global, stack-ordered listener). Drive closeByEscape() on the LAST-opened instance in a stacked scenario.
Drivers
Drivers land in waves (see the umbrella issue #1098); all 6 waves (core form primitives; overlays & portals; selection & specialized inputs; navigation & disclosure; data display & feedback; complex/composite) ship in full below, plus a Wave 6 follow-up (issue #1138) adding FlatTree/FlatTreeItem — this is the complete driver catalog.
| Driver | Fluent component | Notes |
| ---------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ButtonDriver | Button | Native <button>; delegates wholesale to HTMLButtonDriver. |
| CompoundButtonDriver | CompoundButton | Same native <button> root as Button; getSecondaryContent() reads the fui-CompoundButton__secondaryContent part (see JSDoc for the known primary/secondary text-splitting limitation). |
| ToggleButtonDriver | ToggleButton | Native <button>; pressed state read/written via aria-pressed (no native "pressed" concept exists for <button>). |
| InputDriver | Input | The root IS a native <input> — full HTMLTextInputDriver surface, incl. isError via aria-invalid. |
| TextareaDriver | Textarea | The root IS a native <textarea> — full HTMLTextAreaDriver surface. |
| CheckboxDriver | Checkbox | Extends HTMLCheckboxDriver (the root IS a real native <input type="checkbox">); label prop renders a sibling <label for>, resolved via the for↔id link. isIndeterminate() reads the live .indeterminate property via the :indeterminate CSS pseudo-class. |
| SwitchDriver | Switch | Same shape as Checkbox, but no value concept (pure on/off) — does not implement IFormFieldDriver. |
| RadioDriver | Radio | The root IS a real native <input type="radio">; setSelected(false) is rejected (native radio semantics). |
| RadioGroupDriver | RadioGroup | Delegates to HTMLRadioButtonGroupDriver — point its ScenePart locator at the radio inputs (e.g. the group container appended with an input[type="radio"] descendant selector), not at the [role="radiogroup"] wrapper. |
| SelectDriver | Select | The root IS a native <select> — full HTMLSelectDriver surface. |
| LabelDriver | Label | Plain native <label>; getFor() reads the linked control's id. |
| FieldDriver | Field | Container wrapper; getLabel/getHint/getValidationMessage read descendant parts anchored on Fluent's fui-Field__* structural classes. |
| LinkDriver | Link | Native <a>; overrides isDisabled to read aria-disabled (an anchor has no native disabled property). |
| DividerDriver | Divider | [role="separator"]; getOrientation() reads aria-orientation. |
| ImageDriver | Image | Native <img>; getSrc/getAlt read attributes directly. |
| TextDriver | Text | Plain content wrapper; all behavior is inherited (getText). |
Wave 2 — overlays & portals (see the portal & overlay recipe above):
| Driver | Fluent component | Notes |
| ------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| DialogDriver | Dialog (+ DialogSurface/Body/Title/Content/Actions) | Portalled; re-roots on .fui-DialogSurface. isModal() reads aria-modal; non-modal dialogs auto-render a close X. No portable closeByBackdropClick (the backdrop is an un-linkable document.body sibling, not a descendant) — only closeByEscape. |
| PopoverDriver | Popover (+ PopoverTrigger/Surface) | Portalled; re-roots on .fui-PopoverSurface (role is the too-generic "group"). Shares that class with TeachingPopoverSurface — use TeachingPopoverDriver for that component specifically. |
| OverlayDrawerDriver | OverlayDrawer | Portalled; re-roots on .fui-OverlayDrawer (role="dialog", shared with Dialog/TeachingPopover). defaultOpen is deprecated/non-functional — drive it via the controlled open prop. |
| InlineDrawerDriver | InlineDrawer | Renders in-tree — no portal, no re-root, unlike every other driver in this wave. |
| DrawerDriverBase | shared base | getHeaderTitle/getBodyText + open/close lifecycle common to both drawer variants. |
| MenuDriver | Menu (+ MenuTrigger/Popover/List) | Constructed from the TRIGGER locator; resolves the portalled MenuList via the trigger id ↔ list aria-labelledby link (byLinkedElement) — correctly disambiguates two simultaneously open menus. |
| MenuItemDriver | MenuItem | role="menuitem"; getLabel/isDisabled. |
| MenuItemCheckboxDriver | MenuItemCheckbox | role="menuitemcheckbox"; adds isChecked() via aria-checked. Selecting one persists the open menu. |
| MenuItemRadioDriver | MenuItemRadio | role="menuitemradio"; adds isChecked() via aria-checked. Selecting one closes the menu (unlike checkbox items) — re-open to observe the persisted choice. |
| MenuButtonDriver | MenuButton | Native <button> (delegates to HTMLButtonDriver); getMenu() returns the MenuDriver it opens. |
| SplitButtonDriver | SplitButton | Wrapper <div> around a primary action button and a menu-invoking button; clickPrimary()/getMenu() expose each half separately (the base click() on the wrapper is not meaningful). |
| TooltipDriver | Tooltip | Trigger-anchored. getContent() reads aria-label (default relationship="label") or follows aria-describedby (relationship="description") — available regardless of open state, since Fluent mounts the content unconditionally. isOpen()'s fallback for "label"-relationship tooltips is best-effort when multiple tooltips are mounted (see Known gaps). Opens via .focus(), not hover (hover does not reveal Fluent's tooltip under jsdom). |
| ToasterDriver | Toaster | Portalled; re-roots on .fui-Toaster. Deliberately has no caller-supplied interior — toasts arrive dynamically via dispatchToast, read positionally/by-title (getToastByIndex/getToastByTitle) rather than as a declared scene. |
| ToastDriver | Toast (+ Title/Body) | getTitle/getBodyText; no built-in dismiss button (use a declared content part for consumer-supplied actions, like component-driver-mui-v9's SnackbarDriver). |
| TeachingPopoverDriver | TeachingPopover (+ Header/Title/Body/Footer) | Portalled; re-roots on the more-specific .fui-TeachingPopoverSurface (not the shared .fui-PopoverSurface). Has a built-in dismiss button, unlike plain Dialog/Popover. |
Wave 3 — selection & specialized inputs:
| Driver | Fluent component | Notes |
| ------------------------ | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ComboboxDriver | Combobox (+ Option/OptionGroup) | Root IS a native <input role="combobox"> — extends HTMLTextInputDriver wholesale. Listbox is trigger-anchored via aria-controls byLinkedElement (same technique as MenuDriver). No selectByValue — Fluent never reflects Option's value to the DOM, only the label. Single-select only. |
| ComboboxOptionDriver | Option | role="option"; getLabel/isDisabled/isSelected — a standalone class, not sharing MenuItemDriver's inheritance. |
| DropdownDriver | Dropdown (@fluentui/react-select) | NOT the native-<select>-backed Select despite the shared naming — a fully custom combobox widget. Trigger is a real <button role="combobox">; isOpen() reads its aria-expanded (the listbox stays mounted post-close). Listbox resolved via aria-controls byLinkedElement. Single-select only. |
| DropdownOptionDriver | Option (single-select Dropdown) | role="option"; getLabel/isSelected/isDisabled. |
| SliderDriver | Slider | Root IS a native <input type="range"> — extends HTMLRangeInputDriver wholesale. Adds getMin/getMax/getStep/isRequired/getLabel. Single-thumb only — Fluent v9 ships no multi-thumb variant. |
| SpinButtonDriver | SpinButton | Root IS the native <input role="spinbutton">; stepper <button>s reached via the general-sibling combinator (~), same escape hatch as component-driver-astryx's NumberInputDriver. setValue types then blurs to commit; increment/decrement click the steppers; moveToMin/moveToMax/incrementByPage/decrementByPage drive Home/End/PageUp/PageDown. |
| SwatchPickerDriver | SwatchPicker | Does not portal. Items matched on the un-hashed .fui-ColorSwatch class (role flips radio/gridcell with layout). No getValue/selectByValue — color-based equivalents instead (getSwatchColors/selectByColor/getSelectedColor). |
| SwatchPickerItemDriver | ColorSwatch | Real native <button>; isSelected() reads aria-checked falling back to aria-selected. setSelected(false) rejected (no self-deselect). getColor() reads the --fui-SwatchPicker--color inline CSS var — no getValue(). |
| RatingDriver | Rating | Root IS role="radiogroup" with visually-hidden native radio items; getValue/setValue drive the :checked radio via Interactor.activate. No native disabled/readOnly — isDisabled() reads a :disabled descendant radio (consumer <fieldset disabled> cascade). |
| RatingDisplayDriver | RatingDisplay | Read-only sibling of Rating; root is role="img", never radiogroup. Does not implement IInputDriver. getValue/getCount read fui-RatingDisplay__valueText/__countText. |
| TagDriver | Tag | Static, non-dismissible tag; root IS <span class="fui-Tag">. getLabel() reads the fui-Tag__primaryText part. Does not implement IDisableableDriver — disabled has zero DOM reflection. |
| InteractionTagDriver | InteractionTag (+ Primary/Secondary) | Dismissible tag; wrapper <div> around two real native <button>s, delegated wholesale to HTMLButtonDriver. isDisabled() reads the Primary button's disabled. clickPrimary() selects; dismiss() removes. |
| TagGroupDriver | TagGroup | List container; never portals. isDisabled() reads the group's own always-present aria-disabled. getTagCount/getTagLabels/getTagByIndex enumerate mixed Tag/InteractionTag children positionally. |
| TagPickerDriver | TagPicker (+ Control/Group/Input/List/Option) | Constructed from TagPickerControl's locator (<TagPicker> itself renders no DOM). Portalled TagPickerList resolved via aria-controls byLinkedElement; isOpen reads the input's aria-expanded. getSelectedLabels/removeSelected read the in-tree TagPickerGroup; getOptionCount/selectByLabel auto-open the portalled list. |
| TagPickerOptionDriver | TagPickerOption / a selected Tag | Shared getLabel/isDisabled surface for both an open-list option and an already-selected tag (both render role="option"). No portable isSelected for either. |
| SearchBoxDriver | SearchBox | Same native <input type="search"> root as Input — full HTMLTextInputDriver surface, plus clear()/hasClearButton(). The dismiss button renders unconditionally by default and is a sibling with no per-instance link, resolved via the ancestor-:has() re-root (same idiom as component-driver-mui-v9's CheckboxDriver). |
| ColorPickerDriver | ColorPicker (+ ColorArea/ColorSlider) | Composite; renders its own <div class="fui-ColorPicker"> root, unlike a bare context provider. area/hueSlider parts locate children by un-hashed structural classes. Strictly a controlled component (color/onColorChange only). |
| ColorAreaDriver | ColorArea | The 2-D saturation/value picker; root wraps two real native <input type="range">s (inputX/inputY), driven via setRangeValue/getInputValue. No disabled/required (not valid attributes on a plain <div>). |
| ColorSliderDriver | ColorSlider | The 1-D hue slider; locator lands straight on a real native <input type="range"> — same shape as the standalone SliderDriver, so this driver extends HTMLRangeInputDriver wholesale. |
Wave 4 — navigation & disclosure: none of this wave's components portal by default except NavDrawer (which reuses the Wave 2 OverlayDrawer recipe). A unifying design rule shapes the whole wave: a driver models each independently INTERACTIVE/addressable unit; purely structural wrapper elements (a decorative divider, a header/panel pairing that's always 1:1 with its item, a non-interactive group <div>) fold into their interactive sibling's driver rather than getting their own class — the same way AccordionHeader/AccordionPanel fold into AccordionItemDriver, and BreadcrumbDivider isn't modeled at all (see its row below).
| Driver | Fluent component | Notes |
| -------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| TabListDriver | TabList | A ListComponentDriver over role="tab" children (templated off component-driver-mui-v9's TabsDriver), since TabList has no built-in TabPanel — panel wiring is left to the consumer. Tab's value prop genuinely reflects onto the native value attribute (unlike Option.value elsewhere in this package), so getSelectedValue/selectByValue are reliable, un-hashed reads. |
| TabDriver | Tab | Real <button role="tab">; aria-selected is absent entirely (not "false") when disabled. Overrides getText(): when reserveSelectedTabSpace (the default) and the tab is unselected, Fluent renders an invisible SECOND copy of the label in a fui-Tab__content--reserved-space span purely to reserve layout width — the inherited whole-button text read would return the label doubled (verified: "ProfileProfile"), so this driver reads .fui-Tab__content specifically instead. |
| BreadcrumbDriver | Breadcrumb | Items live inside a child <ol role="list">, interleaved with decorative <li> dividers — both share the <li> tag, so item enumeration uses childListHelper's :nth-child + class-selector filter (same mixed-sibling shape TagGroupDriver handles) rather than ListComponentDriver. BreadcrumbDivider has no driver — purely decorative (aria-hidden), see the wave-level rule above. |
| BreadcrumbItemDriver | BreadcrumbItem | The <li> wrapper itself carries no interactive state; folds in BreadcrumbButton's behavior via getButton() and overrides click() to target the button, not the wrapper. |
| BreadcrumbButtonDriver | BreadcrumbButton | Renders <a> (with href) or <button> — isDisabled() combines a native-disabled check with an aria-disabled read (mirrors LinkDriver's reasoning) since either element shape is possible. isCurrent() reads aria-current="page", present only when the current prop is set. |
| AccordionDriver | Accordion | A ListComponentDriver over fui-AccordionItem children (homogeneous <div> siblings, safe for :nth-of-type). multiple/collapsible mode has zero DOM reflection (grepped the compiled package) — this driver exposes no mode getter; drive/observe expansion per item instead. |
| AccordionItemDriver | AccordionItem (+ AccordionHeader/AccordionPanel) | Folds header/panel state in directly (templated off component-driver-radix-v1's single-item AccordionDriver). AccordionPanel fully UNMOUNTS while collapsed (Fluent's hard-coded unmountOnExit) — getPanelText() returns null whenever absent. collapse() no-ops on the parent's OWN collapsible prop (Fluent refuses to reach zero open items otherwise) — see Known Gaps. click() no-ops on a disabled header, same portability contract as RadioDriver.setSelected. |
| NavDriver | Nav | Never portals. Item enumeration (NavItem/NavCategory/NavCategoryItem/NavSubItem, arbitrarily nested) walks via childListHelper's groupSelector: '*' recursion, flattening the tree. Shares its item-query surface with NavDrawerDriver via the internal NavDriverBase. |
| NavDrawerDriver | NavDrawer | Portal-backed by default (wraps Drawer → OverlayDrawer, same as Wave 2) — re-roots on the un-hashed fui-NavDrawer class. Corrects the umbrella issue's hypothesis: "Nav's flyouts are overlay-backed" is true only of NavDrawer's own outer surface, NOT of NavCategory expansion inside it (see NavCategoryItemDriver). Targets the default (portal) variant only — type="inline" isn't covered. |
| NavItemDriver | NavItem / NavSubItem | Renders <a href> or <button> — the component's OWN root IS the interactive element (no wrapper to look past, unlike BreadcrumbItemDriver). aria-current is always a literal "page"/"false" string, never merely absent. |
| NavCategoryItemDriver | NavCategoryItem | Extends NavItemDriver, adding expand/collapse. Always a real <button>, never <a>. No portal — grepped the entire compiled @fluentui/react-nav package for Popover/Menu/role="menu": zero matches; the sub-item group is a same-tree animated accordion, reached via the general-sibling CSS combinator (same escape hatch SpinButtonDriver uses for its steppers). |
| ToolbarDriver | Toolbar | aria-orientation is present only when vertical — getOrientation() defaults the horizontal case rather than passing through null. Button enumeration (getButtonByLabel) descends one level into .fui-ToolbarGroup wrappers via childListHelper's groupSelector (ToolbarRadioGroup shares this same class, no separate fui-ToolbarRadioGroup class is exported), since ToolbarButton/ToolbarToggleButton/ToolbarRadioButton all share the identical fui-Button class. |
| ToolbarButtonDriver | ToolbarButton | Plain native <button class="fui-Button"> — no fui-ToolbarButton class of its own; delegates wholesale, like ButtonDriver. |
| ToolbarDividerDriver | ToolbarDivider | Delegates to DividerDriver wholesale (identical DOM, no separate class). Orientation is INVERTED relative to the toolbar — verified against source (vertical: !toolbarContext.vertical): a horizontal toolbar's divider itself reports 'vertical'. |
| ToolbarRadioGroupDriver | ToolbarRadioGroup | ToolbarRadioGroup IS ToolbarGroup with role="radiogroup" forced on, sharing its class — no native :checked/[value=] to delegate to HTMLRadioButtonGroupDriver, so this is a ListComponentDriver over role="radio" buttons instead (mirrors TabListDriver's shape). |
| ToolbarRadioButtonDriver | ToolbarRadioButton | Real <button role="radio" aria-checked> — Fluent explicitly strips the aria-pressed its shared toggle-button primitive would otherwise carry. setSelected(false) is rejected, same contract as RadioDriver. |
| OverflowDriver | Overflow | Overflow/OverflowItem render NO wrapper of their own — they clone ref/class/data-* attributes onto the consumer's OWN child, so the scene locator must target that element directly, assuming the idiomatic flat-row usage (every item a direct child of the wrapped container). getOverflowMenu() returns a plain Wave 2 MenuDriver resolved via [data-overflow-menu] — no new portal logic needed, since useOverflowMenu() wires a trigger but builds no Menu of its own. |
| OverflowItemDriver | OverflowItem | isOverflowing() reads the data-overflowing attribute directly (portable/jsdom-safe) rather than relying on computed CSS visibility, since overflowed items stay mounted (display: none via a stylesheet rule) rather than being removed. |
Wave 5 — data display & feedback: presentational/read-state drivers — the fastest wave per-component (few new interactions, no portals except InfoButton's own inline-by-default popover). A unifying design rule shapes several of this wave's composite components: CardDriver folds CardHeader/CardFooter/CardPreview reads directly onto itself (mirroring DialogDriver/TeachingPopoverDriver's "one composite driver" shape from Wave 2), rather than minting a driver class per sub-component — none of the three are independently addressable/interactive the way a repeated list item (AvatarGroupItem, ListItem) is.
| Driver | Fluent component | Notes |
| ----------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AvatarDriver | Avatar | role="img" aria-label="{name}"; getName() reads that aria-label (present regardless of whether a badge is also set). getInitials() reads the fui-Avatar__initials part, undefined when an image renders instead. getPresenceBadge() returns a nested PresenceBadgeDriver for the fui-Avatar__badge part. |
| AvatarGroupDriver | AvatarGroup | A ListComponentDriver over AvatarGroupItem wrappers (NOT the nested .fui-Avatar elements directly — each is the sole avatar under its own wrapper, so :nth-of-type addressing on the avatar itself would miscount; see AvatarGroupItemDriver). Overflow (AvatarGroupPopover, the +N trigger) is out of scope — only inline items are enumerated. |
| AvatarGroupItemDriver | AvatarGroupItem | Wrapper with no state of its own; getAvatar() returns the nested AvatarDriver. |
| BadgeDriver | Badge | Plain <div class="fui-Badge">; all state is its own text content (inherited getText()) — appearance/color/shape/size have no un-hashed DOM reflection. |
| CounterBadgeDriver | CounterBadge | Extends BadgeDriver wholesale (identical DOM shape plus a marker class). getDisplayedCount() reads the already-overflowCount-clamped text (e.g. count={150} with the default overflowCount={99} renders "99+"). |
| PresenceBadgeDriver | PresenceBadge | role="img" aria-label="{status}" — for a plain status the label IS the status string; getStatusLabel() reads it raw (the outOfOffice composite label text is unverified — see Known gaps). |
| CardDriver | Card (+ CardHeader/CardFooter/CardPreview) | Folds header/footer/preview reads directly (see the wave-level rule above). A selectable card (selected/onSelectionChange supplied) renders a REAL native <input type="checkbox" class="fui-Card__checkbox">, driven directly via Interactor.isChecked/click. isDisabled() reads the root's aria-disabled (present regardless of selectable). |
| PersonaDriver | Persona | getPrimaryText/getSecondaryText/getTertiaryText read their own fui-Persona__* structural classes (the whole-root getText() would double-count the avatar's own initials). getAvatar()/getPresenceBadge() reach the avatar or (in presenceOnly mode) the standalone presence slot. |
| ListDriver | List | A ListComponentDriver over .fui-ListItem children — homogeneous siblings regardless of root tag (<ul>/<ol>/<div>) or role (role="list" plain, role="listbox" when selectionMode is set). getSelectedValues() enumerates selected items' values. |
| ListItemDriver | ListItem | Its value prop reflects directly onto a plain value attribute (a rare un-hashed prop reflection in this package). isSelected() reads aria-selected, present only when the parent's selectionMode is set. |
| SkeletonDriver | Skeleton (+ SkeletonItem) | role="progressbar" aria-busy="true"; individual items carry zero distinguishable state, so this driver exposes only getItemCount() rather than a per-item class (same "no interactive unit" call Wave 4 makes for BreadcrumbDivider). |
| SpinnerDriver | Spinner | role="progressbar"; getLabel() reads the fui-Spinner__label part, undefined when rendered without one. |
| ProgressBarDriver | ProgressBar | role="progressbar" with aria-valuemin/aria-valuemax/aria-valuenow; aria-valuenow is entirely ABSENT (not "0") in the indeterminate (no value prop) state — isIndeterminate() reads that absence directly. |
| InfoLabelDriver | InfoLabel | The scene locator lands on the inner <label> (InfoLabel's PRIMARY slot), not the wrapping <span> — extends LabelDriver wholesale. getInfoButton() reaches the sibling InfoButton via an ancestor :has() re-root (same idiom as SearchBoxDriver's wrapper). |
| InfoButtonDriver | InfoButton | Unlike every other Popover-backed overlay in this package, InfoButton's inline prop defaults to true — its popover surface renders as a plain DOM SIBLING (adjacent-sibling-combinator addressed), no portal re-root needed. Targets only that default case; inline={false} (portal) is out of scope. |
| MessageBarDriver | MessageBar (+ MessageBarBody/Title/Actions) | getTitle() reads fui-MessageBarTitle exactly; getBodyText() reads the WHOLE fui-MessageBarBody, which includes the title's text too since Title nests INSIDE Body (known gap, same class as CompoundButtonDriver's). No built-in dismiss — declare the consumer-supplied action as its own scene part, same as ToastDriver. |
| AlertDriver | Alert | Deprecated by Fluent itself (AlertProps's own TSDoc: "use the Toast or MessageBar component") and importable ONLY from @fluentui/react-components/unstable — NOT the stable package surface. getText() includes an action button's text when present (no isolated message read; known gap). |
Wave 6 — complex/composite (the highest-engineering-cost wave, saved for last by design): Table/DataGrid — the single largest driver family in this catalog, split the same way component-driver-mui-v9's Table family is (a top-level driver over rows, rows over cells, plus a distinct header-row driver) — Tree, and Carousel.
| Driver | Fluent component | Notes |
| -------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TableDriver | Table | A ListComponentDriver over data (body) rows (.fui-TableBody .fui-TableRow), templated off component-driver-mui-v9's TableDriver. Header row is a separate TableHeaderRowDriver rather than folded in — a real DOM difference from MUI (Fluent's TableCell/TableHeaderCell are two structurally distinct components, <td>/<th>, each with its own structural class), not a stylistic choice. sortByColumn/getSortDirection are generic/best-effort (read aria-sort, click the header cell) since plain Table ships no built-in sort orchestration. |
| TableRowDriver | TableRow (body) | A ListComponentDriver over .fui-TableCell children. |
| TableHeaderRowDriver | TableRow (header) | A ListComponentDriver over .fui-TableHeaderCell children; adds best-effort getSortDirection/sortByColumn. |
| TableRowDriverBase | shared base | ListComponentDriver-based cell iteration (getCellCount/getCell/getCellTexts) shared by both row kinds above. |
| TableCellDriver | TableCell | Plain native <td>, no role; relies on inherited getText(). getActionButtons()/isActionsVisible() read a nested TableCellActions (shared logic with DataGridCellDriver) — see Wave 6 scope decisions. |
| TableHeaderCellDriver | TableHeaderCell | Plain native <th>; isSortable()/getSortDirection() read aria-sort (absent entirely, not "none", when not sortable). |
| DataGridDriver | DataGrid | A ListComponentDriver over data rows (.fui-DataGridBody [role="row"]) with built-in sort, row selection (single/multiselect), and column resize — the richer columns/items-driven sibling of Table. Columns are addressed by zero-based index, not field — columnId has zero DOM reflection (verified: getNativeElementProps's per-tag attribute allowlist strips it from every rendered slot). |
| DataGridRowDriver | DataGridRow (data) | A childListHelper-based row over .fui-DataGridCell children (not ListComponentDriver — DataGridRow conditionally renders a leading DataGridSelectionCell only when selectionMode is set, which breaks :nth-of-type addressing). Implements IToggleDriver; setSelected clicks the row's real native checkbox/radio. |
| DataGridHeaderRowDriver | DataGridRow (header) | Same childListHelper shape over [role="columnheader"] children; adds sort, "select all" (multiselect only), and per-column resize delegation.
