Skip to main content

Installation

npm install --save-dev @rezi-ui/testkit

Overview

@rezi-ui/testkit provides testing utilities for Rezi applications:
  • Snapshot testing: Capture and compare rendered output
  • Golden file testing: Binary comparison with hexdump
  • Random number generation: Deterministic RNG for reproducible tests
  • Fixture loading: Read test fixtures from disk
  • Node.js test harness: Minimal test runner utilities

API Reference

Snapshot Testing

matchesSnapshot
function
Compare rendered output against stored snapshot.
import { matchesSnapshot } from "@rezi-ui/testkit";
import { createTestRenderer, ui } from "@rezi-ui/core";
import { describe, test } from "node:test";

describe("MyWidget", () => {
  test("renders correctly", () => {
    const renderer = createTestRenderer({ width: 80, height: 24 });
    const tree = ui.column({ p: 1 }, [
      ui.text("Hello"),
      ui.button({ id: "btn", label: "Click" }),
    ]);

    const result = renderer.render(tree);

    matchesSnapshot(result.text, {
      snapshotPath: "__snapshots__/my-widget.txt",
      update: process.env.UPDATE_SNAPSHOTS === "1",
    });
  });
});

Options

options.snapshotPath
string
required
Path to snapshot file.
options.update
boolean
Update snapshot if mismatch (default: false).
options.normalize
(text: string) => string
Normalize output before comparison.

Golden File Testing

assertBytesEqual
function
Compare binary data with golden file, showing hexdump on mismatch.
hexdump
function
Generate hexdump string from binary data.
import { assertBytesEqual, hexdump } from "@rezi-ui/testkit";
import { createDrawlistBuilderV3 } from "@rezi-ui/core";
import { readFileSync } from "node:fs";
import { describe, test } from "node:test";

describe("Drawlist generation", () => {
  test("matches golden file", () => {
    const builder = createDrawlistBuilderV3({ width: 80, height: 24 });
    builder.text(0, 0, "Hello");
    const result = builder.build();

    const golden = readFileSync("golden/hello.zrdl");
    assertBytesEqual(result.bytes, golden, "Drawlist mismatch");
  });

  test("inspect bytes", () => {
    const data = new Uint8Array([0x5a, 0x52, 0x44, 0x4c]);
    console.log(hexdump(data));
    // 00000000  5a 52 44 4c                                       |ZRDL|
  });
});

Fixture Loading

readFixture
function
Load test fixture file from fixtures/ directory.
import { readFixture } from "@rezi-ui/testkit";
import { parseReproBundleBytes } from "@rezi-ui/core";
import { describe, test } from "node:test";

describe("Repro bundle", () => {
  test("parses fixture", () => {
    const bytes = readFixture("repro-v1-basic.bin");
    const result = parseReproBundleBytes(bytes);

    assert(result.ok === true);
    assert(result.bundle.schema === 1);
  });
});
Fixture directory: packages/testkit/fixtures/

Random Number Generation

createRng
function
Create deterministic random number generator for reproducible tests.
import { createRng } from "@rezi-ui/testkit";
import { describe, test, assert } from "node:test";

describe("RNG", () => {
  test("generates reproducible random numbers", () => {
    const rng1 = createRng(12345);
    const rng2 = createRng(12345);

    // Same seed produces same sequence
    assert.strictEqual(rng1.next(), rng2.next());
    assert.strictEqual(rng1.next(), rng2.next());
  });

  test("generates random integers in range", () => {
    const rng = createRng(54321);
    const value = rng.nextInt(1, 10); // 1 <= value < 10

    assert(value >= 1 && value < 10);
  });

  test("generates random floats", () => {
    const rng = createRng(99999);
    const value = rng.nextFloat(); // 0 <= value < 1

    assert(value >= 0 && value < 1);
  });
});

RNG Methods

next
() => number
Generate next 32-bit unsigned integer.
nextInt
(min: number, max: number) => number
Generate random integer in [min, max).
nextFloat
() => number
Generate random float in [0, 1).
nextBool
() => boolean
Generate random boolean.
shuffle
<T>(array: T[]) => T[]
Shuffle array in-place using Fisher-Yates algorithm.

Test Harness Utilities

describe
function
Test suite grouping (re-export from node:test).
test
function
Individual test case (re-export from node:test).
assert
function
Assertion utilities (re-export from node:assert).
import { describe, test, assert } from "@rezi-ui/testkit";

describe("Math utilities", () => {
  test("addition works", () => {
    assert.strictEqual(1 + 1, 2);
  });

  test("async test", async () => {
    const result = await fetchData();
    assert(result.ok);
  });
});

Testing Patterns

Unit Test Example

import { describe, test, assert } from "@rezi-ui/testkit";
import { computeSelection, toggleSort } from "@rezi-ui/core";

describe("Table utilities", () => {
  test("computeSelection returns correct indices", () => {
    const result = computeSelection(
      { type: "range", start: 2, end: 5 },
      10,
      "multi"
    );

    assert.deepStrictEqual(result.selectedIndices, [2, 3, 4, 5]);
  });

  test("toggleSort cycles through states", () => {
    const col1 = toggleSort(null, "name");
    assert.strictEqual(col1.column, "name");
    assert.strictEqual(col1.direction, "asc");

    const col2 = toggleSort(col1, "name");
    assert.strictEqual(col2.direction, "desc");

    const col3 = toggleSort(col2, "name");
    assert.strictEqual(col3, null);
  });
});

Snapshot Test Example

import { matchesSnapshot } from "@rezi-ui/testkit";
import { createTestRenderer, ui } from "@rezi-ui/core";
import { describe, test } from "node:test";

describe("Dashboard screen", () => {
  test("renders with empty state", () => {
    const renderer = createTestRenderer({ width: 80, height: 24 });
    const state = { items: [], loading: false };

    const tree = ui.page({ p: 1 }, [
      ui.panel("Dashboard", [
        ui.text(state.items.length === 0 ? "No items" : `${state.items.length} items`),
      ]),
    ]);

    const result = renderer.render(tree);
    matchesSnapshot(result.text, {
      snapshotPath: "__snapshots__/dashboard-empty.txt",
    });
  });

  test("renders with data", () => {
    const renderer = createTestRenderer({ width: 80, height: 24 });
    const state = { items: ["A", "B", "C"], loading: false };

    const tree = ui.page({ p: 1 }, [
      ui.panel("Dashboard", [
        ui.text(`${state.items.length} items`),
        ...state.items.map((item) => ui.text(item)),
      ]),
    ]);

    const result = renderer.render(tree);
    matchesSnapshot(result.text, {
      snapshotPath: "__snapshots__/dashboard-data.txt",
    });
  });
});

Integration Test Example

import { createRng, matchesSnapshot } from "@rezi-ui/testkit";
import { createTestRenderer, ui, defineWidget } from "@rezi-ui/core";
import { describe, test } from "node:test";

const Counter = defineWidget<{ initial: number }>((props, ctx) => {
  const [count, setCount] = ctx.useState(props.initial);

  return ui.row([
    ui.text(`Count: ${count}`),
    ui.button({ id: ctx.id("inc"), label: "+" }),
  ]);
});

describe("Counter widget", () => {
  test("renders with initial value", () => {
    const renderer = createTestRenderer({ width: 80, height: 24 });
    const tree = Counter({ initial: 42 });

    const result = renderer.render(tree);
    matchesSnapshot(result.text, {
      snapshotPath: "__snapshots__/counter-initial.txt",
    });
  });
});

Running Tests

Node.js Test Runner

# Run all tests
node --test

# Run specific test file
node --test src/__tests__/myWidget.test.ts

# With coverage
node --test --experimental-test-coverage

npm scripts

package.json
{
  "scripts": {
    "test": "node --test",
    "test:watch": "node --test --watch",
    "test:coverage": "node --test --experimental-test-coverage",
    "test:update-snapshots": "UPDATE_SNAPSHOTS=1 node --test"
  }
}

With tsx (TypeScript)

npm install --save-dev tsx

# Run TypeScript tests directly
tsx --test src/__tests__/*.test.ts

Snapshot Management

Update Snapshots

UPDATE_SNAPSHOTS=1 node --test

Review Changes

git diff __snapshots__/

Commit Snapshots

Commit snapshot files to version control:
git add __snapshots__/
git commit -m "test: update snapshots"

Best Practices

Always seed createRng() with a fixed value to ensure reproducible test failures.
const rng = createRng(12345); // Fixed seed
Strip timestamps, random IDs, and other non-deterministic data from snapshots.
matchesSnapshot(output, {
  snapshotPath: "snapshot.txt",
  normalize: (text) => text.replace(/\d{4}-\d{2}-\d{2}/g, "YYYY-MM-DD"),
});
Unit test pure utility functions before testing stateful widgets.
// Easy to test
test("formatDuration", () => {
  assert.strictEqual(formatDuration(90), "1m 30s");
});
Prefer @rezi-ui/testkit utilities over generic test libraries for better integration.

TypeScript Types

import type {
  Rng,
  SnapshotMatchOptions,
} from "@rezi-ui/testkit";

Testing Utilities

Core testing exports

Test Renderer

Using createTestRenderer

Node.js Tests

Node.js test runner docs

Repro System

Reproducible bug reports

Build docs developers (and LLMs) love