Skip to main content

Radix / shadcn driver coverage matrix

@atomic-testing/component-driver-radix-v1 provides component drivers for Radix UI Primitives (the unified radix-ui@^1.0.0 package, not the per-primitive @radix-ui/react-* packages — they render identical DOM, so the drivers work against either).

@atomic-testing/component-driver-shadcn-v1 is a pure re-export of the same drivers under the name your codebase speaks if you consume shadcn/ui: shadcn components are styled Radix primitives, so their DOM — and therefore their drivers — are identical. Re-exporting (rather than subclassing or copying) keeps a single class identity per driver, so instanceof agrees across both package names. The shadcn package declares no drivers of its own and tracks component-driver-radix-v1's major version in lockstep; add new drivers only to component-driver-radix-v1.

Radix primitives are unstyled, so — like Astryx's StyleX classes — Tailwind/shadcn utility class names are never stable anchors. Every driver locates by, in priority order: role + accessible name, data-slot (the shadcn/ui convention naming each part), then Radix's own data-state/data-orientation/ data-disabled attributes. Radix's generated ids (radix-_r_0_) are render-order-dependent and are never hardcoded; id links (aria-controls, aria-labelledby, label[for]) are resolved with byLinkedElement instead.

How to read coverage​

Each driver is exercised by one shared suite that runs in two worlds, same as every other package in this repo:

  • DOM (jsdom) — fast, structural. Faithful for markup, attributes (data-*, aria-*), text, and open/closed state, because Radix overlays are plain createPortal + popper positioning with no native Popover API involved (unlike Astryx — see the portal note below).
  • E2E (Playwright) — Chromium, Firefox, WebKit. Required for geometry (Slider drag, ScrollArea positional effects), pointer-driven interaction, and timer-driven behavior (Toast auto-dismiss).

A behavior tagged E2E-only below is real but only observable in a browser; the jsdom suite asserts the structural/ARIA facts that hold in both worlds, and the driver's JSDoc names the gap.

Portals: how Radix differs from MUI and Astryx​

Radix overlays (Dialog, DropdownMenu, Popover, Tooltip, ContextMenu, Select, …) portal their content as direct children of document.body — closer to MUI's behavior than Astryx's, but not identical:

  • MUI wraps portalled content in a shared role="presentation" element.
  • Radix mounts with no shared wrapper at all — the overlay/content elements are themselves direct <body> children (sometimes as siblings, e.g. Dialog.Overlay and Dialog.Content land as two separate body children).
  • Astryx renders most overlays in-tree using the native HTML Popover API (showPopover()/:popover-open), not a portal.

The portal re-root recipe still applies verbatim — copy the shape from the Material UI drivers, exactly as the guide says — but per-primitive re-root anchors differ enough from MUI's own DOM that they're worth cataloguing:

PrimitivePortalled node under <body>Re-root anchor
Dialog / AlertDialogoverlay <div data-state> and content <div role="dialog"|"alertdialog"> are each direct body childrenbyRole('dialog'|'alertdialog', 'Root')
DropdownMenu / ContextMenudiv[data-radix-popper-content-wrapper] wrapping content <div role="menu">byRole('menu', 'Root')
Selectcontent <div role="listbox"> (direct body child)byRole('listbox', 'Root')
Popoverdiv[data-radix-popper-content-wrapper] wrapping content <div role="dialog"> — collides with an open modal Dialog's roletrigger's aria-controls → byLinkedElement (not a static role re-root)
Tooltip / HoverCarddiv[data-radix-popper-content-wrapper] wrapping a role-less positioned <div>trigger's aria-describedby/forwarded data-testid (no shared role to anchor on)
Menubar (per menu)one data-radix-popper-content-wrapper per open menu, several same-role menus under one bartrigger's aria-controls → byLinkedElement (role re-root would collide)

NavigationMenu, Toast, and Toolbar render in-tree, no portal at all.

Coverage — Wave 0 (foundation) and Wave 1 (pain-first flagship)​

ComponentDriverE2E-only behavior
SeparatorSeparatorDriver—
SelectSelectDriver— (jsdom needs hasPointerCapture/scrollIntoView polyfills the harness ships; value identity is label-based, not data-value, since Radix renders none on Select.Item)
DialogDialogDriveronly closeByEscape is exposed — Dialog.Overlay carries no ARIA distinguishing it from page content, so there is no portable closeByBackdropClick
DropdownMenuDropdownMenuDrivera ContainerDriver, so scenes can declare custom content parts for CheckboxItem/RadioGroup/submenu content
PopoverPopoverDrivertrigger-anchored via aria-controls → byLinkedElement (its content shares role="dialog" with modal Dialog, so a static re-root would collide)

Coverage — Wave 2 (foundation controls)​

All in-tree — no portals.

ComponentDriverE2E-only behavior
CheckboxCheckboxDriver— (renders <button role="checkbox">, not a native input — reads aria-checked/data-state)
RadioGroupRadioGroupDriver / RadioGroupItemDriver— (same button-not-input shape as Checkbox/Switch)
SwitchSwitchDriver—
ToggleToggleDriver—
ToggleGroupToggleGroupDriver— (group value has no single DOM attribute; the driver derives it by scanning per-item data-state)
TabsTabDriver / TabsDriver—
LabelLabelDriver—
ProgressProgressDriver—
AspectRatioAspectRatioDriver—
AvatarAvatarDriverloaded-image path — Avatar.Image's onLoadingStatusChange never fires under jsdom; structure/fallback only there
CollapsibleCollapsibleDriver—
AccordionAccordionDriver—

Coverage — Wave 3 (remaining overlays & menus)​

ComponentDriverE2E-only behavior
AlertDialogAlertDialogDriver— (DialogDriver's shape, re-rooted at role="alertdialog"; Cancel/Action are consumer content parts)
TooltipTooltipDriverdelay-driven open transitions and pointer-leave close (jsdom fires no pointerleave); hover-open itself works under jsdom
HoverCardHoverCardDriversame hover-close split as Tooltip. The one Radix overlay with no a11y anchor at all (sighted-users-only by design) — re-roots at [data-radix-popper-content-wrapper] instead of a role/link
ContextMenuContextMenuDriver— (opens via the right-click primitive; menu surface is document-rooted, safe because Radix menus are modal)
MenubarMenubarDriver / MenubarMenuDriver— (bar is in-tree role="menubar"; each menu follows its own trigger's aria-controls link)
NavigationMenuNavigationMenuDriver / NavigationMenuItemDriver— (entirely in-tree — content mounts into NavigationMenu.Viewport, no portal)
ToastToastDrivertimer-driven auto-dismissal is untested by the suite (real-time behavior); click paths (Action/Close) are covered
ToolbarToolbarDriver—

Coverage — Wave 4 (interaction-heavy)​

ComponentDriverE2E-only behavior
SliderSliderDriverpointer-driven dragBy; keyboard setValue (arrows/Home/End) works in both worlds. No native <input type="range"> exists in the DOM — single-thumb scope, mirroring the Astryx driver
ScrollAreaScrollAreaDriverpositional scroll effects (scrollBy/scrollIntoView results); getScrollbarState's data-state read is jsdom-safe
PasswordToggleFieldPasswordToggleFieldDriver— (PasswordToggleField.Root renders no DOM node of its own — a pure context provider — so the scene must supply an explicit wrapping element for the driver's root locator)
OneTimePasswordFieldOneTimePasswordFieldDriver— (setValue types one character per box rather than using Radix's paste-into-first-box autofill path — no portable Interactor primitive for ClipboardEvent)

Coverage — Wave 5 (best-effort v1: Combobox)​

Radix ships no Combobox primitive. ComboboxDriver targets the canonical shadcn/ui composition — a Radix Popover hosting a cmdk Command palette — and is this package's one deliberate two-library driver, shipped per the same "ship a documented, honest v1 scope and name the blocking dependency" discipline Astryx's hard-set drivers use (see facebook/astryx#3240 for that precedent). The ship-vs-descope investigation and evidence are recorded on #1007.

ComponentDriverv1 scope & blocking dependency
ComboboxComboboxDriver / ComboboxOptionDriverThe Radix half (trigger + portalled panel) is inherited from PopoverDriver unchanged; the cmdk half anchors on cmdk's public styling attributes ([cmdk-input], [cmdk-list], [cmdk-item], [cmdk-empty]). Selected value is read as the trigger's visible text — the composition keeps selection in consumer React state, per shadcn's own recipe, so there is no DOM attribute carrying it. Command.Group headings are not modeled — grouped items are still enumerated (in document order) via childListHelper's wrapper descent. cmdk-inside-Dialog (the command-palette usage) is out of scope — sustained demand for that pairing would motivate a standalone component-driver-cmdk package rather than folding it into this driver. cmdk is declared an optional peer dependency of component-driver-radix-v1 (the driver never imports it — the coupling is CSS-attribute-only), so only Combobox consumers need it installed.

Unlike SelectDriver (label-based, since Radix renders no data-value on Select.Item), ComboboxDriver is value-based (selectByValue, getHighlightedValue, getOptionValues) because cmdk does render data-value on every Command.Item — selectByLabel is also provided for text-first scenes. Filtering is cmdk-internal and unmounts non-matching items rather than hiding them, so getOptionCount/getOptionValues reflect the current filter by construction and isEmpty() is a presence check against Command.Empty.

Out of scope (no driver)​

  • shadcn's Command (standalone), Calendar, Drawer, Sonner, and Carousel blocks are not Radix primitives — they wrap cmdk, react-day-picker, vaul, and embla-carousel respectively, and are out of scope for this package (Combobox is the one exception, since it composes Radix's own Popover).
  • Radix's Slot, VisuallyHidden, and AccessibleIcon are utility/layout primitives with no testable widget surface — no driver, the same treatment Astryx gives Stack/Card/Skeleton/Icon.

Framework roadmap​

React ships first. Vue support (against Reka UI, the unofficial Vue port of Radix) is planned as its own follow-up effort once the React driver set is stable — not bundled into this package.