Skip to content
brix.v2 stable

Search documentation

70 guides

Documentation menu
Docs / Project tooling / TestingRead Markdown ↗

Testing components and HTTP consumers

Use managed Chromium component tests for Preact components, islands, stores, and browser hooks. Use contract-aware MSW handlers when the subject calls HTTP; do not substitute them for route-harness tests.

Render a browser component

Name the file *.client.test.tsx and import browser helpers through Brix:

import { describe, expect, it } from "brix/test";
import { render, userEvent } from "brix/test/browser";
import { BookingButton } from "./BookingButton.tsx";

describe("BookingButton", () => {
  it("submits a booking", async () => {
    const screen = render(<BookingButton />);
    await userEvent.click(screen.getByRole("button", { name: "Book" }));
    await expect.element(screen.getByText("Booked")).toBeVisible();
  });
});

brix/test/browser exports render, renderWithTranslations, page, and userEvent. Locators match text exactly and case-sensitively by default; use toMatchTextContent for partial text or regular expressions. Await interactions and browser assertions.

Mount the production translation provider when the component uses translation hooks:

renderWithTranslations(<CheckoutButton />, {
  locale: "sv",
  messages: { "checkout.submit": "Fortsätt" },
  isProduction: false,
});

The renderer is Preact-only. Managed React applications test their components in the React application's browser toolchain.

Test stores and hooks through a consumer

Render a small component that calls the hook and exposes meaningful state through accessible UI. Wrap it in the real generated provider when testing a store:

function CartSummary() {
  const cart = useCartStore();
  return <button onClick={() => cart.add("sku-123")}>Items {cart.items.value.length}</button>;
}

render(
  <CartStoreProvider initialItems={[]}>
    <CartSummary />
  </CartStoreProvider>,
);

await userEvent.click(page.getByRole("button", { name: "Items 0" }));
await expect.element(page.getByRole("button", { name: "Items 1" })).toBeVisible();

This verifies provider setup, signals, rerendering, and user-visible behavior together. For a hook without visible output, use a tiny local test component to expose its result, but prefer behavior over internal implementation state. Use vi.fn or vi.spyOn for callback boundaries; do not replace modules.

Mock HTTP consumers with MSW

mockContract and mockContractError produce typed MSW handlers for executeApi, useApi, browser components, and other HTTP-consuming code. Initialize the version-matched browser worker once:

brix msw init public --save
import { afterAll, afterEach, beforeAll, expect, it } from "brix/test";
import { mockContract, mockContractError } from "brix/test";
import { setupWorker } from "brix/msw/browser";
import { page, render, userEvent } from "brix/test/browser";
import { getItemsApiContract } from "./contract.ts";
import { ItemList } from "./ItemList.tsx";

const worker = setupWorker();

beforeAll(() => worker.start({ onUnhandledRequest: "error" }));
afterEach(() => worker.resetHandlers());
afterAll(() => worker.stop());

it("loads items", async () => {
  worker.use(
    mockContract(getItemsApiContract, {
      data: [{ id: "item-1", name: "Backpack" }],
    }),
  );

  render(<ItemList />);
  await userEvent.click(page.getByRole("button", { name: "Load items" }));
  await expect.element(page.getByText("Backpack")).toBeVisible();
});

it("shows an API error", async () => {
  worker.use(mockContractError(getItemsApiContract, 503));
  render(<ItemList />);
  await userEvent.click(page.getByRole("button", { name: "Load items" }));
  await expect.element(page.getByRole("alert")).toBeVisible();
});

For an HTTP consumer running in the Node project, use the same contract mocks with setupServer from brix/msw/node, then listen in beforeAll, resetHandlers in afterEach, and close in afterAll.

Mock success data is required when the contract declares a response and omitted for an empty response such as { status: 204 }. Fixtures use response-schema input, are JSON-normalized and validated, and produce parsed output in the client. A request-aware sync or async resolver receives typed params, parsed body and query, plus the original request:

mockContract(updateItemApiContract, {
  status: 201,
  data: ({ params, body, request }) => ({
    id: params.itemId,
    name: body.name,
    requestId: request.headers.get("x-request-id"),
  }),
});

Use baseUrl to pin a handler to an external origin; the default matches any origin. mockContractError(contract, status, { data?, baseUrl? }) preserves explicit falsy JSON error bodies and otherwise returns { error: "Mock <status> error" }.

Do not use these handlers to test the corresponding Brix route. Use the route harness, which exercises middleware, environment, auth, rendering, and server error handling.

Debug browser tests

Run one component file or the browser project:

brix test src/features/checkout/BookingButton.client.test.tsx
brix test --project browser
brix test src/features/checkout/BookingButton.client.test.tsx --browser.traceView --reporter=html

Generated traces, reports, screenshots, and attachments go under .vitest/. Commit reference screenshots under __screenshots__/, not generated .vitest/ output. Use brix test --repeats 20 <test-file> to reproduce order-independent flakes.