Writing Tests

Every test follows the same shape: render a component, query the DOM, interact, then assert.

Render

render mounts a component into a container on document.body and returns queries plus helpers.

JSX
import { render } from "@opentf/web-test";
import Greeting from "./Greeting.jsx";

const { container, getByRole, unmount } = render(Greeting);

Passing props

The second argument is the component's props:

JSX
const { getByRole } = render(Greeting, { name: "Ada" });
expect(getByRole("heading").textContent).toBe("Hello, Ada");

Assert on reactive updates

Because OTF Web updates the DOM surgically, you read the node again after an interaction — no flush, no act():

JSX
import { test, expect } from "bun:test";
import { render, userEvent } from "@opentf/web-test";
import Counter from "./Counter.jsx";

test("increments", async () => {
  const { getByTestId } = render(Counter);
  const user = userEvent.setup();
  const btn = getByTestId("btn");

  expect(btn.textContent).toBe("Count: 0");
  await user.click(btn);
  expect(btn.textContent).toBe("Count: 1");
});
Use data-testid sparingly

Prefer role- and text-based queries (they mirror how users find things). Reach for data-testid only when there's no accessible handle. See Queries.

Cleanup

With the recommended setup, each test is isolated automatically. To tear down within a test, call unmount:

JSX
const { unmount } = render(Modal, { open: true });
// …assertions…
unmount(); // removes the component and runs onCleanup

Organizing

Group related cases with describe, and keep one behavior per test:

JSX
import { describe, test, expect } from "bun:test";
import { render } from "@opentf/web-test";
import LoginForm from "./LoginForm.jsx";

describe("LoginForm", () => {
  test("shows a validation error for a bad email", async () => {
    /* … */
  });

  test("submits valid credentials", async () => {
    /* … */
  });
});