@atomic-testing/component-driver-astryx
Component drivers for Astryx, Meta's open-source, StyleX-based design system. Component drivers expose simple APIs for unit tests or end-to-end tests to interact with Astryx componentsโreading state and driving actionsโso test engineers focus on test flows instead of the component internals.
Why atomic-testingโ
Testing a UI built on a third-party component library is hard to keep maintainable: a component's markup is an implementation detail, so tests that reach into it break every time the component changes or the library upgrades, and each framework/runner has its own interaction API. Atomic Testing gives you one consistent way to interact with components across every environment โ describe the parts of a scene once, as a ScenePart, and drive them through component drivers that expose semantic actions (click(), setValue(), getText()) instead of DOM manipulation. The same scene and the same test body then run under DOM (jsdom) and end-to-end (Playwright), because only the Interactor underneath changes. See Why Atomic Testing? for the full case.
This package adapts that pattern to Astryx. Astryx styles components with StyleX, whose class names are build-time hashed and therefore not stable test anchors โ one more reason markup-level assertions are a poor fit here specifically. Astryx is also ARIA-role-first: widgets expose a semantic role plus an accessible name (visible text or aria-label). The drivers in this package locate Astryx components by those stable anchors โ data-testid, role, and accessible name, never StyleX classes โ and expose the same high-level interactions across both environments.
Declare the Astryx parts a scene needs, once, as a ScenePart:
import { ButtonDriver, TextInputDriver } from '@atomic-testing/component-driver-astryx';
import { byDataTestId, ScenePart } from '@atomic-testing/core';
const signupScene = {
email: { locator: byDataTestId('email-input'), driver: TextInputDriver },
submit: { locator: byDataTestId('save-button'), driver: ButtonDriver },
} satisfies ScenePart;
Then drive it through engine.parts โ semantic calls, not DOM queries:
await engine.parts.email.setValue('user@example.com');
await engine.parts.submit.click();
expect(await engine.parts.submit.isDisabled()).toBe(false);
The same scene and the same test body run unchanged in a DOM test and an end-to-end test โ only the engine creation differs:
const domEngine = createTestEngine(<SignupForm />, signupScene);
const e2eEngine = createTestEngine(page, signupScene);
For a larger, driver-per-component version of this pattern, see the real suites under package-tests/component-driver-astryx-test/src/examples โ every driver in the tables below has a matching *.suite.ts there, run against both a .dom.test.ts (Jest) and an .e2e.test.ts (Playwright) adapter, so the DOM/E2E parity above is exercised on every driver in this package, not just the example.
Target package & version pinโ
This driver targets the published Astryx package @astryxdesign/core (the components live here; theme packages such as @astryxdesign/theme-neutral are separate). It is declared as a peer dependency pinned to ^0.4.1: consumers bring their own Astryx, and the caret on a 0.x release locks the 0.4 minor (>=0.4.1 <0.5.0)โthe closest analogue to "pin a major" while Astryx is pre-1.0. Astryx peer-requires React โฅ19 and @stylexjs/stylex โฅ0.19.0 (also a peer dependency of @astryxdesign/core).
Astryx forks (-vN) are deferred: a single package tracks one 0.x minor at a time. Astryx 0.2.0, 0.3.0 and 0.4.0 each carried breaking changes, and this package was retargeted in place rather than forked โ Astryx is pre-1.0 with no long-term-support branch, so a fork per breaking minor would multiply maintenance against an API that has not settled. To test an app still on Astryx 0.1.x, pin the last @atomic-testing/component-driver-astryx release that targeted it.
Installationโ
npm install @atomic-testing/core @atomic-testing/react-19 \
@atomic-testing/component-driver-html @atomic-testing/component-driver-astryx \
@astryxdesign/core --save-dev
Refer to the documentation for usage patterns and examples.
Driversโ
Wave 1 โ buttons, inputs, toggles and the structural/feedback primitives around
them. Each driver locates its component by data-testid, role, or accessible
name (never a StyleX class) and exposes high-level reads and interactions. Method
details are in the API docs; anchoring rationale and
any E2E-only behaviour live in each driver's source doc comment.
| Driver | Astryx component | Notes |
|---|
ButtonDriver | Button | getLabel/isDisabled/isLoading; inherited click. |
IconButtonDriver | IconButton | Icon-only Button; getLabel reads the always-present aria-label. |
ToggleButtonDriver | ToggleButton | isSelected/setSelected via aria-pressed. |
ButtonGroupDriver | ButtonGroup | List of buttons; clickButton(name), getButtonCount, getOrientation. |
ToggleButtonGroupDriver | ToggleButtonGroup | select/deselect/isSelected by aria-label; getSelectedLabels. |
LinkDriver | Link | getHref/getTarget/getRel; isButtonFallback (no-href <button>). |
Text inputsโ
| Driver | Astryx component | Notes |
|---|
TextInputDriver | TextInput | Value, clear, getLabel/getStatusMessage (a11y links), isRequired/isInvalid. |
TextAreaDriver | TextArea | Value, getRows, getCharCount. |
NumberInputDriver | NumberInput | Value, getMin/getMax (aria-valuemin/max since Astryx 0.4.0's text-backed spinbutton), getUnits; stepUp/stepDown. No getStep โ step has no DOM representation. |
TimeInputDriver | TimeInput | getValue returns the display string (not ISO); increment/decrement (E2E). |
AstryxFieldInputDriver | โ | Shared base for the field inputs above (linked label/status resolution). |
Selection controlsโ
| Driver | Astryx component | Notes |
|---|
CheckboxInputDriver | CheckboxInput | isChecked/toggle; isIndeterminate (aria-checked="mixed"). |
RadioListDriver | RadioList | getSelectedValue/selectByValue by radio value; isItemChecked. |
CheckboxListDriver | CheckboxList | Label/index addressed (item value is not in the DOM); getCheckedLabels. |
CheckboxListItemDriver | โ | A single CheckboxList row: getLabel/isChecked/toggle. |
SwitchDriver | Switch | isOn/turnOn/turnOff via the role="switch" input; getSize reads the painted track. |
SegmentedControlDriver | SegmentedControl | Single-select radiogroup; value via data-value. |
SelectableCardDriver | SelectableCard | isSelected/toggle via the card's hidden checkbox; clicks the card. |
SliderDriver | Slider | Single-thumb; getValue (aria-valuenow), keyboard setValue (no drag). |
Structure & feedbackโ
| Driver | Astryx component | Notes |
|---|
FieldDriver | Field | getLabel/getDescription/getStatusMessage, isRequired/isOptional. |
InputGroupDriver | InputGroup | getLabel, getAddonTexts. |
FieldStatusDriver | FieldStatus | getStatus/getMessage/isError via stable data-type (role is conditional). |
BannerDriver | Banner | getTitle/getDescription/getStatus, dismiss, toggleExpand. |
PaginationDriver | Pagination | getVariant, getCurrentPage (button or input-variant spinbutton), goToPage/setPage/next/previous, getCountText. |
CollapsibleDriver | Collapsible | isExpanded/expand/collapse via the trigger's aria-expanded; isDisabled. |
Wave 2 โ overlays & menus. Astryx renders these in-tree (no portal): menus and
popovers mount their panel as a sibling of the trigger via the native Popover API,
and dialogs are native <dialog> elements. Each driver anchors on the trigger (or
the <dialog>) and reads open state from aria-expanded or the <dialog> open
attribute; panel visibility and Escape/backdrop dismissal are native behaviours
covered only by the Playwright run (and skipped on WebKit, which cannot drive
native-popover interactions). Anchoring rationale lives in each driver's source doc
comment.
| Driver | Astryx component | Notes |
|---|
NavMenuDriver | NavHeadingMenu | Flat link/action menu; getItemLabels/getItemCount/clickItem/getItemHref. |
DropdownMenuDriver | DropdownMenu | Trigger-anchored; open/close/isOpen, selectByLabel, getTriggerLabel, isItemChecked (for the DropdownMenuCheckboxItem/DropdownMenuRadioItem selectable items). |
MoreMenuDriver | MoreMenu | Icon-only DropdownMenu; getTriggerLabel reads the aria-label. |
TabListDriver | TabList | getItemLabels/getActiveLabel/selectTab/isActive/getTabHref. |
TabDriver | Tab | A single tab: getLabel/isActive (aria-current="page")/getHref. |
ToolbarDriver | Toolbar | getLabel/getOrientation/getSize/getItemCount. |
AstryxMenuDriver | โ | Shared menu base; positional iteration over menuitem/menuitemcheckbox/menuitemradio. |
MenuItemDriver | โ | A single menu item (<a>/<div>): getLabel/isDisabled/getHref/getRole/isChecked/isDestructive/hasSubMenu. description/endContent are unmarked sibling spans upstream, so getLabel returns the row's whole text. |
SubMenuDriver | DropdownMenuSubMenu | A nested flyout; anchor it on the submenu's trigger row. Items read while closed (resolved by the flyout's aria-labelledby back-link). |
Overlays & feedbackโ
| Driver | Astryx component | Notes |
|---|
PopoverDriver | Popover | Trigger-anchored; open/close/isOpen, getLabel/getContent. |
DialogDriver | Dialog | Native <dialog>; isOpen/isModal/getTitle/closeByEscape; interior via within(parts). |
AlertDialogDriver | AlertDialog | role="alertdialog"; getTitle/getDescription, clickAction/clickCancel. |
ToastDriver | Toast | Stable data-type: getType/isError/getRole/getMessage, dismiss. |
Wave 3 โ lists, tables, selectors & dates. The list/table/tree drivers read
structure from native semantics (<li>, <table>, ul[role="tree"]) and per-row
state from ARIA. The combobox family (Selector/MultiSelector/Typeahead/Tokenizer,
plus the CommandPalette dialog and PowerSearch's field suggestions) shares one
option-enumeration mechanism โ options are addressed by their contiguous
${listboxId}-${item|option}-${i} ids โ exposed through AstryxComboboxDriver (its
trigger open/close layer) over an internal IndexedOptionListDriver base. Popup and
calendar-popover visibility are native behaviours covered by the Playwright run;
structure and ARIA render faithfully in jsdom. Anchoring rationale and the
scroll/layout-only caveats (Carousel overflow, Outline scroll-spy) live in each
driver's source doc comment.
Lists & displayโ
| Driver | Astryx component | Notes |
|---|
ListDriver | List | <li>-addressed rows; getItemLabels/getSelectedLabels/clickItem/isOrdered. |
ListItemDriver | โ | A single row: getLabel (full text), isSelected/isDisabled (ARIA), isLink/getHref. |
MetadataListDriver | MetadataList | <dl> pairs; getLabels/getValueByLabel/getEntryCount, showMore/showLess. |
OutlineDriver | Outline | TOC nav; getItemLabels/getActiveLabel (aria-current)/getHref/getLevel/clickItem. |
OutlineItemDriver | โ | A single entry: getLabel/getHref/getLevel (data-level)/isActive. |
CarouselDriver | Carousel | getLabel/getItemCount/hasNavButtons; scrollNext/scrollPrev are E2E-only. |
Tables & treesโ
| Driver | Astryx component | Notes |
|---|
TableDriver | Table | data-column-key/aria-sort/aria-selected; headers, data rows (empty-state row excluded), sort, row selection. TableSortDirection is re-exported. |
TreeListDriver | TreeList | ul[role="tree"] walked depth-first; getVisibleItemLabels/getItemDepth/expandItem/collapseItem/clickItem/hasGuides (the lineGuides/noGuides variant). |
Selectors, typeaheads & searchโ
| Driver | Astryx component | Notes |
|---|
AstryxComboboxDriver | โ | Shared combobox base: trigger open/close/isExpanded over the option-enumeration surface. |
SelectorDriver | Selector | Single-select; getOptionLabels/selectByLabel/getSelectedLabel/isOptionSelected/typeToSelect (the closed-trigger typeahead Astryx 0.4.0 added). |
ComplexSelectorDriver | ComplexSelector | The 0.3.0 shell: trigger text and state, open/close; the popup's interior is scene-owned and reached through within(parts). |
MultiSelectorDriver | MultiSelector | Multi-select; toggleByLabel/getSelectedLabels (excludes select-all)/selectAll/clearAll. |
TypeaheadDriver | Typeahead | Search-as-you-type single-select; type/getResultLabels/selectByLabel/clear/isLoading. |
TokenizerDriver | Tokenizer | Multi-token; getTokenLabels/addByLabel/create/removeToken/clearAll/isLoading. |
CommandPaletteDriver | CommandPalette | Host-controlled <dialog>; search/getOptionLabels/getOptionValue/selectByLabel/getActiveValue. |
PowerSearchDriver | PowerSearch | Best-effort v1: getFilterLabels/removeFilter/clearAll/getFieldSuggestionLabels/getResultCount (in-popover edit is E2E/follow-up). |
| Driver | Astryx component | Notes |
|---|
CalendarDriver | Calendar | Inline; [data-date] cells, getMode/getVisibleMonthLabel/selectDay/selectRange/previousMonth/nextMonth. |
DateInputDriver | DateInput | Input value + calendar popover; open/pickDate (via aria-controls)/isInvalid/clear. |
DateTimeInputDriver | DateTimeInput | Extends DateInputDriver with a paired time field (getTimeValue/setTime). |
DateRangeInputDriver | DateRangeInput | Best-effort v1: popover <dialog> with presets + range pickRange (the end day is re-resolved after the start click). |
Wave 4 โ the remaining display/typography, media/status, and feedback
primitives; a "hard set" of drivers that shipped against interactor
primitives already available or via structural workarounds (each with a
named best-effort v1 limitation); and the nav-chrome and chat-suite
component families. As with the earlier waves, anchoring is data-testid,
role, or accessible name โ never a StyleX class โ and rationale plus any
E2E-only behaviour live in each driver's source doc comment.
Display & typographyโ
| Driver | Astryx component | Notes |
|---|
BadgeDriver | Badge | getVariant via data-variant; label is inherited getText. |
TextDriver | Text | getType/getColor (data-type/data-color); getColor is theme-resolved, not the authored prop. |
HeadingDriver | Heading | getLevel/getAccessibilityLevel; the latter prefers aria-level, falling back to getLevel when absent (i.e. the levels coincide). |
CodeDriver | Code | No own methods; content is inherited getText. |
BlockquoteDriver | Blockquote | getCitation reads the <cite> descendant in isolation; inherited getText returns quote + citation concatenated. |
TimestampDriver | Timestamp | getDateTime reads datetime on the inner <time>; inherited getText gives the display string. data-format lives on the wrapper, read via a sibling part; relative-time tooltip & live updates are E2E-only. |
DividerDriver | Divider | getVariant/getOrientation/getLabel; getLabel reads the middle child, undefined when unlabeled. |
| Driver | Astryx component | Notes |
|---|
StatusDotDriver | StatusDot | getLabel/getVariant/isPresent; hover tooltip is E2E-only. |
CitationDriver | Citation | getTitle/getNumber (parsed from aria-label)/getHref/isLink/getVariant. |
TokenDriver | Token | getLabel/getVariant (data-color)/getHref/isRemovable/remove; disabled state is class-only (not exposed). |
AvatarDriver | Avatar | getAccessibleName/getImageSrc/hasImage/getInitials/getSize/getTooltipText (custom tooltip text only, via aria-describedby); load-failure โ initials fallback is E2E-only. |
AvatarGroupDriver | AvatarGroup | getVisibleCount/getAvatarNames/getOverflowCount, parsed from the overflow chip's aria-label. |
ThumbnailDriver | Thumbnail | getAccessibleName/getImageSrc/isLoading/isPlaceholder/canRemove; hover tooltip & lightbox preview are E2E-only. |
Feedback & miscโ
| Driver | Astryx component | Notes |
|---|
EmptyStateDriver | EmptyState | getTitle/getDescription/getHeadingLevel/isPresent/hasAction; probed across h1โh6 (description is the heading's next-sibling <div>). |
ProgressBarDriver | ProgressBar | getValueNow/getValueMin/getValueMax/getValueText/getLabel/getVariant/isIndeterminate/getMarkCount; both modes share role="progressbar". Mark labels live in a lazy hover Tooltip and are E2E-only. |
SpinnerDriver | Spinner | getAccessibleName/getLabelText/getSize; accessible name falls back to the nested role="status" span when labeled. |
NavIconDriver | NavIcon | No custom methods; presence via inherited exists (the icon has no role/text semantics). |
ItemDriver | Item | getLabel/getDensity/getAlign/getHref; isSelected checks aria-selected (when the role permits it) or aria-current otherwise (Astryx 0.1.9; mutually exclusive). |
MarkdownDriver | Markdown | isInline/getDensity/getHeadingCount/getLinkCount; copy-code is E2E-only (clipboard). |
CodeBlockDriver | CodeBlock | getLanguage/getCode/getLineCount/isCollapsed/toggleCollapse; copy-state flip is E2E-only (clipboard). |
Hard set (best-effort v1)โ
| Driver | Astryx component | Notes |
|---|
FileInputDriver | FileInput | Best-effort v1: getAccept/isMultiple/isInvalid/isDisabled/getLabel/getStatusMessage + uploadFiles (via setInputFiles); file-chip readback and dropzone drag-and-drop are E2E-only. No isRequired โ Astryx 0.2.0 replaced aria-required with a translated visually-hidden string. |
ContextMenuDriver | ContextMenu | Best-effort v1: open (via the contextMenu primitive); items read from the document-rooted role="menu" (no aria-controls link). No isOpen; single-instance per scene. |
AppShellDriver | AppShell | Best-effort v1: getVariant/hasHeader/hasSideNav/hasMain/getMainText/hasSkipLink confirm landmarks and variant, then delegate to child drivers; responsive collapse/mobile drawer are E2E-only. |
ChatComposerInputDriver | ChatComposerInput | Best-effort v1: getValue/appendValue on the contenteditable via textContent (getInputValue returns null; typing is append-only). Suggestions-menu open is E2E-only. |
ChatComposerDriver | ChatComposer | Best-effort v1: submit/canSend/isStopShown/getStatusMessage anchor the send/stop button by verbatim aria-label ("Send"/"Stop") โ no stable class/testid upstream. Enter-to-send is E2E-only. |
HoverCardDriver | HoverCard | Best-effort v1: getContent hovers, then resolves the body-level popover via the trigger's aria-describedby โ layer id โ the layer is lazyMounted since Astryx 0.4.2, so it must be opened first. No role/testid/open attr on the layer, so open state stays E2E-only. |
TooltipDriver | Tooltip | Best-effort v1: same aria-describedby โ layer id anchor as HoverCard; open state is E2E-only. |
Nav chromeโ
| Driver | Astryx component | Notes |
|---|
TopNavDriver | TopNav | Header nav landmark; getLabel/getItemLabels/selectByLabel over startContent links; heading link & menu triggers excluded from the tally. |
TopNavItemDriver | TopNavItem | getLabel/isSelected (aria-current="page")/isDisabled; inherited getHref/click. |
TopNavMenuDriver | TopNavMenu | Sibling popover panel resolved via trigger aria-controls; getLabel/getItemTitles/selectByLabel; isOpen is E2E-only for true (native popover). |
TopNavMegaMenuDriver | TopNavMegaMenu | Sibling panel resolved via the trigger's aria-controls, re-rooted at the document (instance-safe); the panel is role="group", not menu. getLabel/getItemTitles; isExpanded is E2E-only for true. |
BreadcrumbsDriver | Breadcrumbs | <ol> of crumbs; getLabel (aria-label, default "Breadcrumb")/getLabels/getCurrentLabel (aria-current)/getHrefs. |
BreadcrumbItemDriver | BreadcrumbItem | Single crumb; getLabel/getHref/isCurrent; hasMenu/menu() for Astryx 0.1.9's menu-trigger crumbs (โ BreadcrumbMenuDriver). |
BreadcrumbMenuDriver | โ | Returned by BreadcrumbItemDriver.menu(); open/getItemLabels/selectByLabel work while closed; the trigger carries no aria-expanded, so verifying open is E2E-only (no isOpen). |
SideNavDriver | SideNav | getLabel (hardcoded "Side navigation")/hasCollapseButton/getSectionCount (role="group" sections); collapsed (icon-only) state is E2E-only. |
SideNavItemDriver | SideNavItem | Leaf <a> or collapsible <div> + toggle; getLabel/isSelected (aria-current)/getHref/isExpanded; flyout in collapsed mode is E2E-only. |
MobileNavDriver | MobileNav | Native <dialog> drawer; getLabel/getSide (data-side)/hasCloseButton; isOpen is E2E-only for true (showModal is a no-op in jsdom). |
Chat suiteโ
| Driver | Astryx component | Notes |
|---|
ChatMessageDriver | ChatMessage | getSender/getDensity/getBubbleText/getMetadataText; sender name is not exposed (a generated-id aria-labelledby anchor only). |
ChatMessageBubbleDriver | ChatMessageBubble | getText/getSender/getVariant/getDensity. |
ChatMessageListDriver | ChatMessageList | List of ChatMessageDriver rows; getMessageCount/getDensity/getEmptyStateText; auto-scroll is E2E-only. |
ChatSystemMessageDriver | ChatSystemMessage | getText/getVariant. |
ChatToolCallsDriver | ChatToolCalls | isGrouped/getCallCount/isExpanded/toggleGroup. |
ChatLayoutDriver | ChatLayout | getDensity/getEmptyStateText; scroll-to-bottom button is E2E-only. |
ChatSendButtonDriver | ChatSendButton | Icon-only HTMLButtonDriver; anchored on aria-label ("Send"/"Stop"), since Astryx gives it no stable class of its own; isSend/isStop, inherited isDisabled. |
ChatDictationButtonDriver | ChatDictationButton | getAccessibleName/isListening via aria-label ("Start dictation"/"Stop dictation"); live dictation is E2E-only (Web Speech API; mock the dictation prop). |
Learn moreโ
Classesโ
Type Aliasesโ