Testing¶
Rezi uses the built-in node:test runner via a deterministic test discovery
script. The test suite currently contains 873+ tests across multiple
categories.
Running Tests¶
Full Suite¶
This executes node scripts/run-tests.mjs, which deterministically discovers
and runs all test files. Discovery is based on sorted directory walks -- no shell
globs or non-deterministic ordering.
Scoped Runs¶
Run only package tests (compiled .test.js files under packages/*/dist/):
Run only script tests (.test.mjs files under scripts/__tests__/):
Individual Test Files¶
Run a single test file directly with Node's test runner:
Note that tests run against compiled output in dist/, so you must build first:
Drawlist codegen guardrail¶
When changing drawlist command layout for v1, regenerate and verify writers:
codegen:check is enforced in CI and fails if
packages/core/src/drawlist/writers.gen.ts is out of sync with
scripts/drawlist-spec.ts.
End-to-End Tests¶
Live PTY Rendering Validation (for UI regressions)¶
For terminal rendering/theme/layout regressions, run a live PTY session with frame-audit instrumentation in addition to normal tests.
Use the dedicated runbook:
That guide includes deterministic viewport setup, worker-mode run commands, scripted key driving, and cross-layer telemetry analysis.
Test Categories¶
Unit Tests¶
Standard unit tests for pure functions and isolated modules. These make up the majority of the test suite and cover:
- Layout calculations
- Text measurement
- Theme resolution
- Keybinding matching
- State machine transitions
- Update queue ordering
Golden Tests (ZRDL/ZREV Fixtures)¶
Golden tests compare binary output byte-for-byte against committed fixture files. They are used where byte-level stability matters:
- ZRDL fixtures -- Drawlist output. The renderer produces a ZRDL binary
drawlist for a given widget tree, and the test asserts it matches a committed
.zrdlfixture file exactly. - ZREV fixtures -- Event batch parsing. A committed
.zrevbinary is parsed, and the result is compared against expected structured output.
Golden tests catch unintentional changes to the binary protocol. When a legitimate protocol change is made, the fixtures must be regenerated and committed alongside the code change.
Fuzz-Lite Tests (Property-Based)¶
Bounded property-based tests for binary parsers and encoders. These tests generate random inputs within defined bounds and verify invariants:
- Parsers never throw (they return
ParseResult). - Round-trip encoding/decoding produces the original value.
- Out-of-bounds inputs produce structured error results, not crashes.
Fuzz-lite tests are fast (bounded iteration count) and run as part of the normal test suite -- they do not require external fuzzing infrastructure.
Integration Tests¶
End-to-end tests that exercise the full rendering pipeline from state through
view function to drawlist output. These tests use the @rezi-ui/testkit
headless backend to run without a real terminal.
Integration tests cover:
- Full app lifecycle (create, render, update, destroy)
- Widget interaction sequences (focus, input, navigation)
- Routing and navigation
- Resize and reflow behavior
Hot State-Preserving Reload (HSR) Tests¶
When changing app.replaceView(...), app.replaceRoutes(...), createNodeApp({ hotReload }), or createHotStateReload(...), include:
- successful runtime widget-view swap test (
replaceViewwhileRunning) - successful runtime route-table swap test (
replaceRouteswhileRunningin route mode) - preservation test for local widget state/focus/cursor with stable ids/keys
- failure-path test proving previous view/routes remain active after reload import errors
- mode guard tests (raw mode and incompatible API usage)
Code Editor Syntax Tokenizer Tests¶
When changing syntaxLanguage, tokenizer exports, or code-editor token paint behavior, include:
- preset coverage for mainstream language tokenization (
typescript,javascript,json,go,rust,c,cpp/c++,csharp/c#,java,python,bash) - fallback coverage for unsupported languages (
plain) - custom-tokenizer override coverage (
tokenizeLineandtokenizeCodeEditorLineWithCustom(...)) - renderer integration coverage proving tokens and active cursor-cell highlighting render together safely
Quick scoped run:
--filter performs a literal substring match against discovered relative test
file paths. It does not interpret the value as a raw regular expression.
Manual HSR + GIF Workflow¶
Use the built-in demos under scripts/hsr/:
Preferred widget-demo quit keys: F10 / Alt+Q / Ctrl+C / Ctrl+X.
Live-edit these files while each demo is running:
- widget demo:
scripts/hsr/widget-view.mjs - router demo:
scripts/hsr/router-routes.mjs
Widget demo shortcut:
- the in-app
self-edit-codeeditor is focused on startup and shows the TypeScript snippet used for the title banner - edit
SELF_EDIT_BANNER = "..."and save withF6(fallback:Ctrl+O, thenCtrl+S;Enterworks on the save button) to rewritewidget-view.mjsfrom inside the demo - save/reload status appears in a modal overlay to avoid log noise in the capture surface
- the banner text in the header is sourced from
widget-view.mjsSELF_EDIT_BANNER, so successful HSR swaps are visually obvious - if your terminal uses XON/XOFF flow control, prefer
F6/Ctrl+ObecauseCtrl+Smay be swallowed by the terminal
One-command recording:
hsr:record:* uses manual capture by default so you can type/edit freely during recording.
Use scripted mode for deterministic multi-scene showcase captures:
npm run hsr:record:widget:auto
node scripts/record-hsr-gif.mjs --mode widget --scripted
node scripts/record-hsr-gif.mjs --mode widget --scripted --scene-text "My custom headline"
The recorder writes an asciinema cast and attempts GIF conversion via agg (or
asciinema gif when available).
High-level Widget Rendering Tests¶
For widget-level tests, prefer createTestRenderer() from @rezi-ui/core so
tests do not need manual commitVNodeTree() -> layout() -> renderToDrawlist()
setup:
import { createTestRenderer, ui } from "@rezi-ui/core";
const renderer = createTestRenderer({ viewport: { cols: 80, rows: 24 } });
const frame = renderer.render(
ui.column({}, [
ui.text("Hello"),
ui.button({ id: "submit", label: "Submit" }),
]),
);
frame.findText("Hello");
frame.findById("submit");
frame.findAll("button");
frame.toText() returns a deterministic text snapshot of the rendered screen.
Fluent Event Simulation¶
For integration tests that need input batches, use TestEventBuilder instead
of hand-encoding ZREV bytes:
import { TestEventBuilder } from "@rezi-ui/core";
const events = new TestEventBuilder();
events.pressKey("Enter").type("hello@example.com").click(10, 5).resize(120, 40);
backend.pushBatch(events.buildBatch());
This keeps event sequences readable and protocol-safe.
Text Snapshot Assertions¶
Use matchesSnapshot(...) from @rezi-ui/testkit to lock rendered text frames:
import { matchesSnapshot } from "@rezi-ui/testkit";
matchesSnapshot(frame.toText(), "my-widget-default");
Snapshots are read from:
Set UPDATE_SNAPSHOTS=1 when intentionally updating snapshot outputs.
Stress Tests¶
Tests designed to exercise the system under extreme conditions:
- Deep widget trees (hundreds of nesting levels)
- Wide widget trees (thousands of siblings)
- Rapid state update sequences
- Large text content
Stress tests verify that the runtime does not crash, exceed memory bounds, or exhibit non-linear performance degradation.
Animation Regression Suites¶
Animation behavior has dedicated deterministic suites and should be run for any motion-related change:
node --test packages/core/src/widgets/__tests__/composition.animationHooks.test.tsnode --test packages/core/src/app/__tests__/widgetRenderer.transition.test.tsnode --test packages/core/src/runtime/__tests__/commit.fastReuse.regression.test.ts
These suites cover hook retargeting/timer cleanup, ui.box transition property filters (position/size/opacity), and reconciliation fast-reuse correctness with transition props.
Reconciliation Hardening Matrix¶
Reconciliation edge-cases are covered by dedicated runtime suites:
packages/core/src/runtime/__tests__/reconcile.keyed.test.tspackages/core/src/runtime/__tests__/reconcile.unkeyed.test.tspackages/core/src/runtime/__tests__/reconcile.mixed.test.tspackages/core/src/runtime/__tests__/reconcile.composite.test.tspackages/core/src/runtime/__tests__/reconcile.deep.test.ts
These suites lock deterministic behavior for keyed reorder/insert/remove,
unkeyed grow/shrink, mixed keyed+unkeyed slots, defineWidget hook/state
persistence, and deep-tree reconciliation.
Renderer Correctness Audit (Baseline Lock)¶
The renderer correctness audit maintains a baseline lock to track the known-good state of renderer tests:
timestamp_utc: 2026-02-18T11:07:15Z
head: a441bba78ddc99ece4eb76965ce36c0aec9225fe
branch: renderer-correctness-audit
node: v20.19.5
npm: 10.8.2
baseline_test_count: 2488
Audited areas and their dedicated test suites:
| Area | Test file | Tests |
|---|---|---|
| Clip | renderer/__tests__/renderer.clip.test.ts |
18 |
| Border | renderer/__tests__/renderer.border.test.ts |
45 |
| Text | renderer/__tests__/renderer.text.test.ts |
28 |
| Damage | renderer/__tests__/renderer.damage.test.ts |
17 |
| Scrollbar | renderer/__tests__/renderer.scrollbar.test.ts |
24 |
| Total | 132 |
Notable bug fix captured by the audit: overflow: "visible" behavior for
row/column/grid/box containers was fixed to inherit the parent clip
instead of always creating a local content clip.
Test Discovery¶
The scripts/run-tests.mjs script discovers test files deterministically:
- Script tests: Recursively walks
scripts/__tests__/for.test.mjsfiles. - Package tests: For each package in
packages/*/, recursively walksdist/for files matching**/__tests__/**/*.test.js. - All discovered paths are sorted lexicographically.
- The combined list is passed to
node --testas explicit file arguments.
This avoids shell glob non-determinism and ensures consistent test ordering across platforms.
Adding New Tests¶
-
Create the test file. Place it in a
__tests__/directory adjacent to the module being tested: -
Use
node:testAPIs. Importdescribe,it, andassertfromnode:testandnode:assert: -
Build. Run
npm run buildso the TypeScript test file is compiled todist/. -
Run. Execute the full suite or just your new file:
CI Integration¶
Tests run automatically on every push and pull request via the ci.yml GitHub
Actions workflow. The CI pipeline executes:
guardrailsjob:bash scripts/guardrails.sh.- Install dependencies (
npm cion Linux,npm installon non-Linux matrix jobs). npm run lint.npm run typecheck.npm run build.npm run check:core-portability.npm run check:unicode(Linux only).npm run test.- Native/e2e stages (
npm run build:native,npm run test:e2e*,npm run test:native:smoke).
Test failures block pull request merges.