Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

0.3.6

The test engine runs Playwright-shaped end-to-end tests against a real superui UI, headlessly, via cargo superui test.

What it does

cargo superui test reads superui.test.toml from the directory you run it in, loads the UI project it points at, and discovers the *.spec.ts files in the spec directory. For each spec file it builds a fresh app, mounts the UI, and runs that file’s tests in order. Locators, actions, and matchers auto-wait — they retry against the live DOM until they succeed or time out, the way Playwright does — so you assert on the UI’s eventual state rather than sleeping.

Results print as a [PASS]/[FAIL] summary and an HTML report. The process exits 0 when every test passed, 1 when a test failed, and 2 on a config or project error.

One DOM per spec file. Every test in a spec file shares the same mounted app — isolation is per file, not per test. A fresh mount starts on the UI’s default state, so order within a file matters: a test that navigates away leaves that state for the next test.

Setup

The config file

superui.test.toml lives in the directory you run cargo superui test from.

keytyperequireddefault
projectpathyes—
specDirpathyes—
widthintegerno1280
heightintegerno720
maxDiffRatiofloatno0.01

Relative paths resolve against the config file’s directory. A worked example:

project = "assets/ui/game_menu"
specDir = "tests"
width = 1280
height = 720
maxDiffRatio = 0.02

The UI project

project points at a normal superui UI directory — the same project you mount in your game. The test engine renders it for real. It needs:

  • index.html (required).
  • An entry script, resolved in order app.tsx → app.js → .superui/build/app.js.
  • A stylesheet, style.css or theme.css.
  • Any image or font assets the UI references.

The spec directory

specDir holds the test files: *.spec.ts, discovered non-recursively (top level only). Each spec imports its API from "superui/test":

import { test, expect } from "superui/test";

JSON fixtures

A spec can import a .json file directly, the same as a UI’s own .tsx (see Importing JSON): import fixture from "./fixture.json" inlines the parsed value as const fixture. Because the UI project under test resolves its own .json imports the same way, you can assert and screenshot data-driven UI straight from the file the app reads — no Bevy bridge needed for static data. A missing or invalid .json import in a spec file is fatal: that spec fails to transpile, unlike the app’s warn-and-skip handling.

Editor types

cargo superui install projects the superui/test ambient declarations into your project so test, page, and expect autocomplete and type-check in your editor. See Set up editor support in Getting Started.

Writing a spec

A spec is a .ts file that imports its API from "superui/test" and registers tests:

import { test, expect } from "superui/test";

test("main menu renders", async ({ page }) => {
  await expect(page.locator(".screen.main")).toBeVisible();
});

Tests

test(name, fn) registers a test. fn receives { page } and may be async; await every action and assertion so the test waits for each step.

Locators

page.locator(sel, opts?) builds a lazy, chainable handle to matching elements — it resolves when you act on or assert against it, not when you create it. Chain .locator(sel, opts?) to narrow to descendants:

const toggle = page.locator(".cfg-row", { hasText: "Camera follow" }).locator(".toggle");
  • opts.hasText keeps only elements whose text contains the string. It is a plain substring, not a regular expression.
  • .nth(i) picks the match at 0-based index i; .first() is .nth(0).

The selector engine supports tag, class, and id selectors plus descendant combinators only — see Limitations.

Actions

Actions are async and auto-wait for the element to be ready:

  • .click() — click the element.
  • .fill(text) — replace an input’s value with text.
  • .press(key) — dispatch a key press (for example "Enter").
  • .hover() — hover the element.

Assertions

expect(target) begins an assertion; target is a locator or page. Each matcher auto-waits, retrying against the live DOM until it passes or times out.

matcherasserts
toBeVisible()attached and not inline display:none
toHaveText(text)exact text equals text
toHaveCount(n)locator resolves to n elements
toHaveClass(re)class list contains the pattern (substring match)
toHaveAttribute(name, value?)attribute present, optionally equal to value
toHaveScreenshot(name)pixels match the stored baseline

Driving the game bridge

page.emit(name, value?) stands in for the game in the game→UI direction: it delivers value to every bevy.on(name, …) subscriber so you can assert the UI’s reaction. An omitted value is null. The payload is hand-authored and must match the shape your registered event serializes.

await page.emit("frame", { player_hp: 7, player_max_hp: 10 });
await expect(page.locator("#hp")).toHaveText("7 / 10");

See The Bevy Bridge for the bridge this mirrors.

A worked example

Clicking a tab, then asserting the panel it reveals and the active tab’s class:

import { test, expect } from "superui/test";

test("tab bar navigates to settings", async ({ page }) => {
  await page.locator(".tabs .tab", { hasText: "SETTINGS" }).click();
  await expect(page.locator(".settings-card")).toBeVisible();
  await expect(page.locator(".tabs .tab.active")).toHaveText("SETTINGS");
});

test("toggling a switch turns it on", async ({ page }) => {
  await page.locator(".tabs .tab", { hasText: "SETTINGS" }).click();
  const cam = page.locator(".cfg-row", { hasText: "Camera follow" }).locator(".toggle");
  await cam.click();
  await expect(cam).toHaveClass(/on/);
});

Running

Run the suite from the directory holding superui.test.toml:

cargo superui test

A positional filter runs only the spec files whose path contains the given substring:

cargo superui test settings

Flags:

  • --update — write or overwrite screenshot baselines instead of diffing against them.
  • --ui — open the interactive runner (see UI mode).

The process exits 0 when every test passed, 1 when a test failed, and 2 on a config or project error. Each run also writes an HTML report to <specDir>/report.html with per-test status and a DOM snapshot after each step.

Screenshots

toHaveScreenshot(name) compares the rendered frame against a stored baseline at:

<specDir>/__snapshots__/<spec file>/<name>-<os>.png

The first run — or any run with --update — writes the baseline and passes. Later runs diff against it; on a mismatch the engine writes <name>-<os>.actual.png and <name>-<os>.diff.png next to the baseline and fails. maxDiffRatio sets the allowed fraction of changed pixels before a diff counts as a failure.

Baselines are platform-specific — the -<os> suffix keeps a separate image per operating system, so commit the baseline for each OS you run tests on.

UI mode

cargo superui test --ui

This opens a windowed runner instead of printing to the terminal. The left panel lists the specs as a tree, each with a Run button; running a spec nests its tests beneath it. A spec’s header shows a spinner while it runs and a pass/fail mark when it finishes, and each nested test carries its own pass/fail mark. Selecting a test shows its rendered frame alongside a time-travel slider over the recorded steps — moving the slider steps the DOM snapshot and the rendered image together. It is the fastest way to see why a step failed.

Limitations

The current engine is deliberately a subset of Playwright. What to expect today:

  • Selectors are tag, class, and id plus descendant combinators only — no [attr=value] selectors.
  • toHaveClass is a substring match against the class list, not a full regular-expression engine.
  • toBeVisible checks that the element is attached and not inline display:none — it does not inspect computed layout.
  • press(key) dispatches a keydown event; it does not type text into the element.
  • There is no drag primitive, no .not, no .last(), and no .value matcher.
  • toHaveScreenshot is evaluated once, not retried like the other matchers.

Next

PROJECTSUPERUI · BEVY UI
DRAWN BYSTROWK
SCALENOT TO SCALE
REVR1 · 0.1 EARLY BUILD
LICENCEMIT / APACHE-2.0