How to Test Astryx Components with Vitest and @testing-library/react

Astryx provides a complete testing stack using Vitest as the test runner and @testing-library/react for component testing, with automatic jsdom environment setup, Jest-DOM matchers, and path aliases configured out of the box.

Testing React components in the Astryx design system follows a streamlined workflow defined by the repository's centralized configuration. The facebook/astryx repository orchestrates multiple UI packages through a single Vitest configuration that handles DOM simulation, StyleX compilation, and testing utilities automatically. This guide walks through the configuration structure, global setup, and practical patterns for writing robust component tests.

Vitest Configuration in Astryx

Astryx uses a project-based Vitest configuration to separate UI and Node.js testing concerns. The vitest.config.ts file in the repository root defines two distinct projects:

  • ui – Runs all DOM-dependent tests for packages/core, packages/lab, and packages/charts in a jsdom environment with StyleX Babel transformation
  • node – Runs CLI scripts and build tools without DOM simulation

The UI project matches files with the pattern **/*.test.{ts,tsx,mjs} and applies global setup files automatically. Path aliases ensure imports resolve to source files rather than built distributions, eliminating the need to rebuild packages during test development.

See the complete configuration: [vitest.config.ts](https://github.com/facebook/astryx/blob/main/vitest.config.ts)

Global Test Setup and Matchers

Astryx extends Vitest's capabilities through internal/test-utils/src/setup.ts, which loads automatically via the setupFiles configuration option. This setup file performs three critical functions:

  1. Jest-DOM matchers – Extends expect with DOM-specific assertions like toBeInTheDocument(), toHaveAttribute(), and toHaveClass()
  2. Testing Library configuration – Configures @testing-library/react to ignore script, style, and live-region nodes that cause flaky text queries
  3. Browser API polyfills – Provides matchMedia and Popover API implementations for jsdom compatibility
// Simplified excerpt from the global setup
import '@testing-library/jest-dom';
import { configure } from '@testing-library/react';

configure({
  // Ignore style/script/live-region nodes for text queries
  testIdAttribute: 'data-testid',
});

// Polyfills for jsdom
window.matchMedia = window.matchMedia || function() {
  return { matches: false, addEventListener: () => {}, removeEventListener: () => {} };
};

Full source: [internal/test-utils/src/setup.ts](https://github.com/facebook/astryx/blob/main/internal/test-utils/src/setup.ts)

Writing Astryx Component Tests

A typical Astryx component test imports from three sources: Vitest's test primitives, Testing Library's rendering utilities, and the component under test.

Basic Import Pattern

import { describe, it, expect, vi } from 'vitest';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

Rendering and Accessibility Checks

Test visible output and ARIA semantics using screen queries:

describe('Button – basic rendering', () => {
  it('shows the label as visible text', () => {
    render(<Button label="Click me" />);
    
    expect(screen.getByRole('button', { name: 'Click me' }))
      .toBeInTheDocument();
  });
});

Source: packages/core/src/Button/Button.test.tsx#L19-L23

User Interactions with user-event

Asynchronous interactions require userEvent.setup() and proper awaiting:

it('fires onClick when the button is pressed', async () => {
  const handleClick = vi.fn();
  const user = userEvent.setup();

  render(<Button label="Press" onClick={handleClick} />);
  await user.click(screen.getByRole('button'));

  expect(handleClick).toHaveBeenCalledTimes(1);
});

Source: packages/core/src/Button/Button.test.tsx#L21-L28

Testing Async States and ARIA Attributes

Loading states and disabled states require careful handling of promise resolution:

it('sets aria-busy while async clickAction is pending', async () => {
  const user = userEvent.setup();
  let resolve: (() => void) | undefined;

  const clickAction = vi.fn(
    async () => new Promise<void>(r => { resolve = r; })
  );

  render(<Button label="Save" clickAction={clickAction} />);
  await user.click(screen.getByRole('button'));

  const btn = screen.getByRole('button');
  expect(btn).toHaveAttribute('aria-busy', 'true');
  expect(btn).toBeDisabled();

  await act(async () => {
    resolve?.();
    await Promise.resolve();
  });

  expect(btn).not.toHaveAttribute('aria-busy');
  expect(btn).not.toBeDisabled();
});

Source: packages/core/src/Button/Button.test.tsx#L75-L99

Testing Slots and Child Content

The endContent prop and other slots render as children—test their presence and order:

it('renders endContent after label', () => {
  render(
    <Button
      label="Click me"
      endContent={<span data-testid="end">3</span>}
    />,
  );

  const btn = screen.getByRole('button');
  expect(btn).toHaveTextContent('Click me');
  expect(screen.getByTestId('end')).toBeInTheDocument();
  expect(screen.getByTestId('end')).toHaveTextContent('3');
});

Source: packages/core/src/Button/Button.test.tsx#L54-L66

Running Tests in Astryx

The package.json scripts provide three execution modes:

Command Behavior
pnpm test Single run with vitest run
pnpm test:watch Interactive watch mode
pnpm test:coverage V8 coverage report generation

All UI tests execute under the ui project by default, applying the jsdom environment and StyleX transformation automatically.

Scripts source: package.json#L5-L14

Key Configuration Files

Summary

  • Astryx uses project-based Vitest configuration to separate UI (jsdom) and Node.js test environments
  • Global setup automatically loads Jest-DOM matchers and configures @testing-library/react for reliable queries
  • userEvent.setup() with async/await handles all user interactions properly
  • Path aliases in Vitest config let you import from source without rebuilding packages
  • npm/pnpm scripts provide run, watch, and coverage modes via vitest.config.ts project selection

Frequently Asked Questions

What test runner does Astryx use for component testing?

Astryx uses Vitest as the primary test runner, configured with a jsdom environment for DOM-dependent component tests. The setup is defined in vitest.config.ts at the repository root, which declares separate "ui" and "node" projects to handle different runtime requirements.

How does Astryx add Jest-DOM matchers to Vitest?

The internal/test-utils/src/setup.ts file imports @testing-library/jest-dom and extends Vitest's expect automatically. This file loads via the setupFiles array in vitest.config.ts, making matchers like toBeInTheDocument() and toHaveAttribute() available in every test without explicit imports.

Why does Astryx use userEvent.setup() instead of fireEvent?

userEvent.setup() from @testing-library/user-event simulates complete user interactions—including pointer events, keyboard navigation, and focus management—rather than firing isolated DOM events. This produces more realistic behavior and automatically handles the asynchronous timing required by modern React's concurrent features.

How do I test async loading states in Astryx components?

Capture the promise resolver in your mock function, trigger the async action with userEvent, assert the loading state immediately, then use act() from @testing-library/react to flush the resolved promise before asserting the final state. The Button loading test in Button.test.tsx demonstrates this pattern with aria-busy and disabled state verification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →