alepha@docs:~/framework/docs/guides/testing$
cat 2-react-tests.md | pretty
4 min read
Last commit:

#React Tests

Alepha provides testing utilities for React components through alepha/testing/react. Browser tests run in jsdom via Vitest and use @testing-library/react under the hood.

#Setup

Vitest ships with alepha, but jsdom and Testing Library do not. Install them in the project:

npm install -D jsdom @testing-library/react @testing-library/dom

#File Naming Convention

Browser test files use the .browser.spec.ts or .browser.spec.tsx extension. Route them to jsdom with jsdomProject from alepha/testing/vitest, in the test block of your vite.config.ts, beside a node project that excludes them:

typescript
 1import { jsdomProject } from "alepha/testing/vitest"; 2import { defineConfig } from "vitest/config"; 3  4export default defineConfig({ 5  test: { 6    root: ".", 7    globals: true, 8    projects: [ 9      {10        extends: true,11        test: {12          name: "node",13          environment: "node",14          exclude: ["**/node_modules/**", "**/*.browser.spec.{ts,tsx}"],15        },16      },17      jsdomProject(["src/**/*.browser.spec.{ts,tsx}"]),18    ],19  },20});

jsdomProject carries every setting a jsdom project needs to agree on: the environment, the browser resolve conditions (so a package's browser entry is picked), a Node flag that lets jsdom install its own localStorage, and a setup file polyfilling matchMedia. A config contributes only its include.

txt
src/
  web/
    components/
      LoginForm.tsx
      LoginForm.browser.spec.tsx   # Runs in jsdom

#Utilities

The alepha/testing/react module exports:

Function Purpose
renderWithAlepha(element, options?) Async render with Alepha context (auto-starts the instance)
renderWithAlephaSync(element, options?) Sync render (autoStart defaults to false)
fillForm(screen, formId, values) Fill form fields by test ID ({formId}-{fieldName})
fillField(screen, testId, value) Fill a single field by its test ID
submitForm(screen, options?) Click the submit button (default text: "Submit")
resetForm(screen, buttonText?) Click the reset button (default text: "Reset")
waitForFormSubmit(alepha, formId) Wait for the form:submit:end event
toggleSwitch(screen, role?, name?) Toggle a switch or checkbox element
setupJsdomMocks() Mock matchMedia, ResizeObserver, IntersectionObserver, scrollTo, getComputedStyle

#Basic Rendering

tsx
 1import { renderWithAlepha, setupJsdomMocks } from "alepha/testing/react"; 2  3beforeAll(() => setupJsdomMocks()); 4  5describe("Greeting", () => { 6  it("should render greeting", async () => { 7    const { getByText, alepha } = await renderWithAlepha( 8      <Greeting name="Alice" />, 9    );10 11    expect(getByText("Hello, Alice")).toBeDefined();12  });13});

renderWithAlepha creates an Alepha instance (Alepha.create().with(AlephaLogger)), starts it, and wraps the component in AlephaContext.Provider. It returns the standard @testing-library/react render result plus the alepha instance. Its options are the @testing-library/react render options plus alepha (a pre-configured instance), autoStart (default true; false for renderWithAlephaSync) and wrapper.

#Custom Alepha Configuration

Pass a pre-configured Alepha instance to swap services or providers:

tsx
 1import { Alepha } from "alepha"; 2import { renderWithAlepha } from "alepha/testing/react"; 3  4describe("UserProfile", () => { 5  it("should render with a substituted service", async () => { 6    const alepha = Alepha.create().with({ 7      provide: UserService, 8      use: FakeUserService, 9    });10 11    const { getByText } = await renderWithAlepha(<UserProfile />, { alepha });12 13    expect(getByText("Fake User")).toBeDefined();14  });15});

#Wrapper Components

Use the wrapper option to add UI framework providers (theme, i18n):

tsx
 1import { renderWithAlepha } from "alepha/testing/react"; 2import { ThemeProvider } from "my-ui-library"; 3  4describe("Button", () => { 5  it("should render themed component", async () => { 6    const { getByRole } = await renderWithAlepha(<Button>Click me</Button>, { 7      wrapper: ThemeProvider, 8    }); 9 10    expect(getByRole("button")).toBeDefined();11  });12});

#Form Testing

Form helpers find inputs by test IDs constructed as {formId}-{fieldName}. useForm sets both the id and the data-testid of each field to that value, so pass the form an explicit id (useForm({ id: "login-form", ... })); without one the prefix is a generated useId() value. See Forms for useForm itself.

  • fillForm and fillField set <input>, <textarea> and <select> values. fillForm accepts a fourth { timeout } argument (default 1000 ms) for fields that appear late.
  • submitForm only clicks the button whose text is submitButtonText. It does not wait for the submission: pair it with waitForFormSubmit(alepha, formId), which resolves on the form's form:submit:end event and takes a timeout as its third argument (default 5000 ms). Start waiting before the click, so a fast submission cannot end before anyone listens.
tsx
 1import { screen } from "@testing-library/react"; 2import { 3  renderWithAlepha, 4  fillForm, 5  submitForm, 6  waitForFormSubmit, 7  setupJsdomMocks, 8} from "alepha/testing/react"; 9 10beforeAll(() => setupJsdomMocks());11 12describe("LoginForm", () => {13  it("should submit login form", async () => {14    const { alepha } = await renderWithAlepha(<LoginForm />);15 16    await fillForm(screen, "login-form", {17      email: "alice@example.com",18      password: "secret123",19    });20 21    const submitted = waitForFormSubmit(alepha, "login-form");22    await submitForm(screen, { submitButtonText: "Login" });23    await submitted;24  });25});

#Nested Fields

For nested form fields, use dot notation in the field name:

typescript
1await fillForm(screen, "address-form", {2  "address.city": "New York",3  "address.zip": "10001",4});

#Checkboxes and Switches

fillForm automatically detects checkbox and switch inputs. Pass true or false as the value:

typescript
1await fillForm(screen, "settings-form", {2  notifications: true,3  darkMode: false,4});

You can also toggle switches directly:

typescript
1import { toggleSwitch } from "alepha/testing/react";2 3await toggleSwitch(screen, "switch", "Enable notifications");

#jsdom Mocks

Call setupJsdomMocks() in beforeAll to mock browser APIs that jsdom does not implement. This is required for components that use responsive styles, scroll behavior, or intersection observers. Each mock is installed only when the global is missing, so it is safe beside jsdomProject, whose setup file already provides matchMedia.

typescript
1import { setupJsdomMocks } from "alepha/testing/react";2 3beforeAll(() => {4  setupJsdomMocks();5});

Mocked APIs:

  • window.matchMedia
  • window.ResizeObserver
  • window.IntersectionObserver
  • window.scrollTo
  • window.getComputedStyle

#Server-side rendering

ReactServerProvider.render() is a supported test API. It renders a registered page through the production streaming path and collects the HTML for assertions. It does not require a browser. The default result is page markup; { html: true } includes the document wrapper, and { hydration: false } omits hydration data.

typescript
 1import { Alepha } from "alepha"; 2import { $page, ReactServerProvider } from "alepha/react/router"; 3import { describe, it } from "vitest"; 4  5class TestPages { 6  home = $page({ path: "/", component: () => "Hello from SSR" }); 7} 8  9describe("the home page", () => {10  it("renders on the server", async ({ expect }) => {11    const alepha = Alepha.create({ env: { SERVER_PORT: 0 } }).with(TestPages);12    await alepha.start();13    const result = await alepha.inject(ReactServerProvider).render("home");14    expect(result.html).toContain("Hello from SSR");15  });16});