#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:
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.
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
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:
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):
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.
fillFormandfillFieldset<input>,<textarea>and<select>values.fillFormaccepts a fourth{ timeout }argument (default 1000 ms) for fields that appear late.submitFormonly clicks the button whose text issubmitButtonText. It does not wait for the submission: pair it withwaitForFormSubmit(alepha, formId), which resolves on the form'sform:submit:endevent 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.
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:
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:
1await fillForm(screen, "settings-form", {2 notifications: true,3 darkMode: false,4});
You can also toggle switches directly:
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.
1import { setupJsdomMocks } from "alepha/testing/react";2 3beforeAll(() => {4 setupJsdomMocks();5});
Mocked APIs:
window.matchMediawindow.ResizeObserverwindow.IntersectionObserverwindow.scrollTowindow.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.
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});