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

> Learn to test Astryx components effectively using Vitest and @testing-library/react. Get started quickly with pre-configured jsdom, Jest-DOM, and path aliases.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-05

---

**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`](https://github.com/facebook/astryx/blob/main/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)](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`](https://github.com/facebook/astryx/blob/main/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

```typescript
// 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)](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

```typescript
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:

```tsx
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`](https://github.com/facebook/astryx/blob/main/packages/core/src/Button/Button.test.tsx#L19-L23)

### User Interactions with user-event

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

```tsx
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`](https://github.com/facebook/astryx/blob/main/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:

```tsx
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`](https://github.com/facebook/astryx/blob/main/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:

```tsx
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`](https://github.com/facebook/astryx/blob/main/packages/core/src/Button/Button.test.tsx#L54-L66)

## Running Tests in Astryx

The [`package.json`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/package.json#L5-L14)

## Key Configuration Files

- **[`vitest.config.ts`](https://github.com/facebook/astryx/blob/main/vitest.config.ts)** – Central configuration with project definitions, jsdom environment, and path aliases
- **[`internal/test-utils/src/setup.ts`](https://github.com/facebook/astryx/blob/main/internal/test-utils/src/setup.ts)** – Global setup with Jest-DOM matchers and Testing Library configuration
- **[`packages/core/src/Button/Button.test.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Button/Button.test.tsx)** – Reference implementation covering rendering, interactions, loading states, and slots
- **[`internal/test-utils/src/README.md`](https://github.com/facebook/astryx/blob/main/internal/test-utils/src/README.md)** – Documentation for the shared test utilities package

## 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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/Button.test.tsx) demonstrates this pattern with `aria-busy` and disabled state verification.