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.
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
npm testThis 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/):
npm run test:packagesRun only script tests (.test.mjs files under scripts/__tests__/):
npm run test:scriptsIndividual Test Files
Run a single test file directly with Node's test runner:
node --test packages/core/dist/runtime/__tests__/reconcile.keyed.test.jsNote that tests run against compiled output in dist/, so you must build first:
npm run build && node --test packages/core/dist/path/to/test.jsDrawlist codegen guardrail
When changing drawlist command layout for v1, regenerate and verify writers:
npm run codegen
npm run codegen:checkcodegen: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
npm run test:e2e
# Reduced profile (fewer scenarios, faster)
npm run test:e2e:reducedLive 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:
node scripts/run-tests.mjs --filter "codeEditor.syntax"Manual HSR + GIF Workflow
Use the built-in demos under scripts/hsr/:
npm run hsr:demo:widget
npm run hsr:demo:routerPreferred 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:
npm run hsr:record:widget
npm run hsr:record:routerhsr: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:
<test-directory>/__snapshots__/my-widget-default.txtSet 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: 2488Audited 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:packages/core/src/layout/__tests__/myModule.test.ts -
Use
node:testAPIs. Importdescribe,it, andassertfromnode:testandnode:assert:import { describe, it } from "node:test"; import assert from "node:assert/strict"; describe("myModule", () => { it("should compute the correct value", () => { assert.strictEqual(myFunction(1, 2), 3); }); }); -
Build. Run
npm run buildso the TypeScript test file is compiled todist/. -
Run. Execute the full suite or just your new file:
npm test # or node --test packages/core/dist/layout/__tests__/myModule.test.js
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.
Related
Build
This page covers how to build Rezi from source, including the TypeScript packages, native addon, and documentation site.
Live PTY UI Testing and Frame Audit Runbook
This runbook documents how to validate Rezi UI behavior autonomously in a real terminal (PTY), capture end-to-end frame telemetry, and pinpoint regressions across core/node/native layers.