Skip to main content

@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.

Usageโ€‹

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:

// DOM (Jest + React), via @atomic-testing/react-19
const domEngine = createTestEngine(<SignupForm />, signupScene);

// End-to-end (Playwright), via @atomic-testing/playwright
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.

Buttons & actionsโ€‹

DriverAstryx componentNotes
ButtonDriverButtongetLabel/isDisabled/isLoading; inherited click.
IconButtonDriverIconButtonIcon-only Button; getLabel reads the always-present aria-label.
ToggleButtonDriverToggleButtonisSelected/setSelected via aria-pressed.
ButtonGroupDriverButtonGroupList of buttons; clickButton(name), getButtonCount, getOrientation.
ToggleButtonGroupDriverToggleButtonGroupselect/deselect/isSelected by aria-label; getSelectedLabels.
LinkDriverLinkgetHref/getTarget/getRel; isButtonFallback (no-href <button>).

Text inputsโ€‹

DriverAstryx componentNotes
TextInputDriverTextInputValue, clear, getLabel/getStatusMessage (a11y links), isRequired/isInvalid.
TextAreaDriverTextAreaValue, getRows, getCharCount.
NumberInputDriverNumberInputValue, getMin/getMax (aria-valuemin/max since Astryx 0.4.0's text-backed spinbutton), getUnits; stepUp/stepDown. No getStep โ€” step has no DOM representation.
TimeInputDriverTimeInputgetValue returns the display string (not ISO); increment/decrement (E2E).
AstryxFieldInputDriverโ€”Shared base for the field inputs above (linked label/status resolution).

Selection controlsโ€‹

DriverAstryx componentNotes
CheckboxInputDriverCheckboxInputisChecked/toggle; isIndeterminate (aria-checked="mixed").
RadioListDriverRadioListgetSelectedValue/selectByValue by radio value; isItemChecked.
CheckboxListDriverCheckboxListLabel/index addressed (item value is not in the DOM); getCheckedLabels.
CheckboxListItemDriverโ€”A single CheckboxList row: getLabel/isChecked/toggle.
SwitchDriverSwitchisOn/turnOn/turnOff via the role="switch" input; getSize reads the painted track.
SegmentedControlDriverSegmentedControlSingle-select radiogroup; value via data-value.
SelectableCardDriverSelectableCardisSelected/toggle via the card's hidden checkbox; clicks the card.
SliderDriverSliderSingle-thumb; getValue (aria-valuenow), keyboard setValue (no drag).

Structure & feedbackโ€‹

DriverAstryx componentNotes
FieldDriverFieldgetLabel/getDescription/getStatusMessage, isRequired/isOptional.
InputGroupDriverInputGroupgetLabel, getAddonTexts.
FieldStatusDriverFieldStatusgetStatus/getMessage/isError via stable data-type (role is conditional).
BannerDriverBannergetTitle/getDescription/getStatus, dismiss, toggleExpand.
PaginationDriverPaginationgetVariant, getCurrentPage (button or input-variant spinbutton), goToPage/setPage/next/previous, getCountText.
CollapsibleDriverCollapsibleisExpanded/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.

DriverAstryx componentNotes
NavMenuDriverNavHeadingMenuFlat link/action menu; getItemLabels/getItemCount/clickItem/getItemHref.
DropdownMenuDriverDropdownMenuTrigger-anchored; open/close/isOpen, selectByLabel, getTriggerLabel, isItemChecked (for the DropdownMenuCheckboxItem/DropdownMenuRadioItem selectable items).
MoreMenuDriverMoreMenuIcon-only DropdownMenu; getTriggerLabel reads the aria-label.
TabListDriverTabListgetItemLabels/getActiveLabel/selectTab/isActive/getTabHref.
TabDriverTabA single tab: getLabel/isActive (aria-current="page")/getHref.
ToolbarDriverToolbargetLabel/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.
SubMenuDriverDropdownMenuSubMenuA 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โ€‹

DriverAstryx componentNotes
PopoverDriverPopoverTrigger-anchored; open/close/isOpen, getLabel/getContent.
DialogDriverDialogNative <dialog>; isOpen/isModal/getTitle/closeByEscape; interior via within(parts).
AlertDialogDriverAlertDialogrole="alertdialog"; getTitle/getDescription, clickAction/clickCancel.
ToastDriverToastStable 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โ€‹

DriverAstryx componentNotes
ListDriverList<li>-addressed rows; getItemLabels/getSelectedLabels/clickItem/isOrdered.
ListItemDriverโ€”A single row: getLabel (full text), isSelected/isDisabled (ARIA), isLink/getHref.
MetadataListDriverMetadataList<dl> pairs; getLabels/getValueByLabel/getEntryCount, showMore/showLess.
OutlineDriverOutlineTOC nav; getItemLabels/getActiveLabel (aria-current)/getHref/getLevel/clickItem.
OutlineItemDriverโ€”A single entry: getLabel/getHref/getLevel (data-level)/isActive.
CarouselDriverCarouselgetLabel/getItemCount/hasNavButtons; scrollNext/scrollPrev are E2E-only.

Tables & treesโ€‹

DriverAstryx componentNotes
TableDriverTabledata-column-key/aria-sort/aria-selected; headers, data rows (empty-state row excluded), sort, row selection. TableSortDirection is re-exported.
TreeListDriverTreeListul[role="tree"] walked depth-first; getVisibleItemLabels/getItemDepth/expandItem/collapseItem/clickItem/hasGuides (the lineGuides/noGuides variant).
DriverAstryx componentNotes
AstryxComboboxDriverโ€”Shared combobox base: trigger open/close/isExpanded over the option-enumeration surface.
SelectorDriverSelectorSingle-select; getOptionLabels/selectByLabel/getSelectedLabel/isOptionSelected/typeToSelect (the closed-trigger typeahead Astryx 0.4.0 added).
ComplexSelectorDriverComplexSelectorThe 0.3.0 shell: trigger text and state, open/close; the popup's interior is scene-owned and reached through within(parts).
MultiSelectorDriverMultiSelectorMulti-select; toggleByLabel/getSelectedLabels (excludes select-all)/selectAll/clearAll.
TypeaheadDriverTypeaheadSearch-as-you-type single-select; type/getResultLabels/selectByLabel/clear/isLoading.
TokenizerDriverTokenizerMulti-token; getTokenLabels/addByLabel/create/removeToken/clearAll/isLoading.
CommandPaletteDriverCommandPaletteHost-controlled <dialog>; search/getOptionLabels/getOptionValue/selectByLabel/getActiveValue.
PowerSearchDriverPowerSearchBest-effort v1: getFilterLabels/removeFilter/clearAll/getFieldSuggestionLabels/getResultCount (in-popover edit is E2E/follow-up).

Datesโ€‹

DriverAstryx componentNotes
CalendarDriverCalendarInline; [data-date] cells, getMode/getVisibleMonthLabel/selectDay/selectRange/previousMonth/nextMonth.
DateInputDriverDateInputInput value + calendar popover; open/pickDate (via aria-controls)/isInvalid/clear.
DateTimeInputDriverDateTimeInputExtends DateInputDriver with a paired time field (getTimeValue/setTime).
DateRangeInputDriverDateRangeInputBest-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โ€‹

DriverAstryx componentNotes
BadgeDriverBadgegetVariant via data-variant; label is inherited getText.
TextDriverTextgetType/getColor (data-type/data-color); getColor is theme-resolved, not the authored prop.
HeadingDriverHeadinggetLevel/getAccessibilityLevel; the latter prefers aria-level, falling back to getLevel when absent (i.e. the levels coincide).
CodeDriverCodeNo own methods; content is inherited getText.
BlockquoteDriverBlockquotegetCitation reads the <cite> descendant in isolation; inherited getText returns quote + citation concatenated.
TimestampDriverTimestampgetDateTime 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.
DividerDriverDividergetVariant/getOrientation/getLabel; getLabel reads the middle child, undefined when unlabeled.

Media & statusโ€‹

DriverAstryx componentNotes
StatusDotDriverStatusDotgetLabel/getVariant/isPresent; hover tooltip is E2E-only.
CitationDriverCitationgetTitle/getNumber (parsed from aria-label)/getHref/isLink/getVariant.
TokenDriverTokengetLabel/getVariant (data-color)/getHref/isRemovable/remove; disabled state is class-only (not exposed).
AvatarDriverAvatargetAccessibleName/getImageSrc/hasImage/getInitials/getSize/getTooltipText (custom tooltip text only, via aria-describedby); load-failure โ†’ initials fallback is E2E-only.
AvatarGroupDriverAvatarGroupgetVisibleCount/getAvatarNames/getOverflowCount, parsed from the overflow chip's aria-label.
ThumbnailDriverThumbnailgetAccessibleName/getImageSrc/isLoading/isPlaceholder/canRemove; hover tooltip & lightbox preview are E2E-only.

Feedback & miscโ€‹

DriverAstryx componentNotes
EmptyStateDriverEmptyStategetTitle/getDescription/getHeadingLevel/isPresent/hasAction; probed across h1โ€“h6 (description is the heading's next-sibling <div>).
ProgressBarDriverProgressBargetValueNow/getValueMin/getValueMax/getValueText/getLabel/getVariant/isIndeterminate/getMarkCount; both modes share role="progressbar". Mark labels live in a lazy hover Tooltip and are E2E-only.
SpinnerDriverSpinnergetAccessibleName/getLabelText/getSize; accessible name falls back to the nested role="status" span when labeled.
NavIconDriverNavIconNo custom methods; presence via inherited exists (the icon has no role/text semantics).
ItemDriverItemgetLabel/getDensity/getAlign/getHref; isSelected checks aria-selected (when the role permits it) or aria-current otherwise (Astryx 0.1.9; mutually exclusive).
MarkdownDriverMarkdownisInline/getDensity/getHeadingCount/getLinkCount; copy-code is E2E-only (clipboard).
CodeBlockDriverCodeBlockgetLanguage/getCode/getLineCount/isCollapsed/toggleCollapse; copy-state flip is E2E-only (clipboard).

Hard set (best-effort v1)โ€‹

DriverAstryx componentNotes
FileInputDriverFileInputBest-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.
ContextMenuDriverContextMenuBest-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.
AppShellDriverAppShellBest-effort v1: getVariant/hasHeader/hasSideNav/hasMain/getMainText/hasSkipLink confirm landmarks and variant, then delegate to child drivers; responsive collapse/mobile drawer are E2E-only.
ChatComposerInputDriverChatComposerInputBest-effort v1: getValue/appendValue on the contenteditable via textContent (getInputValue returns null; typing is append-only). Suggestions-menu open is E2E-only.
ChatComposerDriverChatComposerBest-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.
HoverCardDriverHoverCardBest-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.
TooltipDriverTooltipBest-effort v1: same aria-describedby โ†’ layer id anchor as HoverCard; open state is E2E-only.
DriverAstryx componentNotes
TopNavDriverTopNavHeader nav landmark; getLabel/getItemLabels/selectByLabel over startContent links; heading link & menu triggers excluded from the tally.
TopNavItemDriverTopNavItemgetLabel/isSelected (aria-current="page")/isDisabled; inherited getHref/click.
TopNavMenuDriverTopNavMenuSibling popover panel resolved via trigger aria-controls; getLabel/getItemTitles/selectByLabel; isOpen is E2E-only for true (native popover).
TopNavMegaMenuDriverTopNavMegaMenuSibling 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.
BreadcrumbsDriverBreadcrumbs<ol> of crumbs; getLabel (aria-label, default "Breadcrumb")/getLabels/getCurrentLabel (aria-current)/getHrefs.
BreadcrumbItemDriverBreadcrumbItemSingle 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).
SideNavDriverSideNavgetLabel (hardcoded "Side navigation")/hasCollapseButton/getSectionCount (role="group" sections); collapsed (icon-only) state is E2E-only.
SideNavItemDriverSideNavItemLeaf <a> or collapsible <div> + toggle; getLabel/isSelected (aria-current)/getHref/isExpanded; flyout in collapsed mode is E2E-only.
MobileNavDriverMobileNavNative <dialog> drawer; getLabel/getSide (data-side)/hasCloseButton; isOpen is E2E-only for true (showModal is a no-op in jsdom).

Chat suiteโ€‹

DriverAstryx componentNotes
ChatMessageDriverChatMessagegetSender/getDensity/getBubbleText/getMetadataText; sender name is not exposed (a generated-id aria-labelledby anchor only).
ChatMessageBubbleDriverChatMessageBubblegetText/getSender/getVariant/getDensity.
ChatMessageListDriverChatMessageListList of ChatMessageDriver rows; getMessageCount/getDensity/getEmptyStateText; auto-scroll is E2E-only.
ChatSystemMessageDriverChatSystemMessagegetText/getVariant.
ChatToolCallsDriverChatToolCallsisGrouped/getCallCount/isExpanded/toggleGroup.
ChatLayoutDriverChatLayoutgetDensity/getEmptyStateText; scroll-to-bottom button is E2E-only.
ChatSendButtonDriverChatSendButtonIcon-only HTMLButtonDriver; anchored on aria-label ("Send"/"Stop"), since Astryx gives it no stable class of its own; isSend/isStop, inherited isDisabled.
ChatDictationButtonDriverChatDictationButtongetAccessibleName/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โ€‹