@fr0/testing
v0.1.0
Published
Snapshot / visual regression / determinism testing utilities for fr0.
Downloads
145
Readme
@fr0/testing
Snapshot and diff utilities for Timeline IR regression testing.
Status
✅ Phase α-3 — done (JSON snapshot / diff / sampling) ⏳ Visual regression (pixel-level) — planned alongside the Phase β+ canvas renderer / encoder
Purpose
Catch unintended changes in what @fr0/core's resolve() produces, without standing up a browser for every test.
Two complementary entry points:
snapshotState/snapshotTimeline— canonical, deterministic JSON snapshots ofResolvedState, ready forvitest'stoMatchSnapshot/toMatchInlineSnapshot.diffResolved/formatDiff— human-readable diff of two resolved states with dotted paths and stable (alphabetically sorted) output, for debugging failed assertions.
Pure TypeScript, no DOM, no jsdom.
Quick start
import { resolve } from '@fr0/core';
import { snapshotTimeline, snapshotState, diffResolved, formatDiff } from '@fr0/testing';
import { describe, expect, it } from 'vitest';
import { titleCard } from '@fr0/templates';
describe('titleCard', () => {
it('matches the approved snapshot', () => {
expect(snapshotTimeline(titleCard.timeline as never)).toMatchSnapshot();
});
it('frame 0 opacity is 0 (fadeIn start)', () => {
const state = resolve(titleCard.timeline as never, 0);
expect(snapshotState(state).title.transform.opacity).toBe(0);
});
});
// On failure:
const actual = resolve(timeline, 10);
const baseline = resolve(timeline, 10); // or reference state
console.log(formatDiff(diffResolved(baseline, actual)));
// → 'title.transform.opacity: 0.333333 -> 0.5'Public API
| Export | Purpose |
|---|---|
| canonicalize(value, options?) | Recursively rewrite a JSON-like value into canonical form: keys sorted alphabetically at every level, finite numbers rounded to precision (default 6), -0 normalized to 0, undefined fields dropped, arrays preserve order. |
| sampleFrames(timeline, count) | Pick count evenly-spaced integer frame indices across [0, totalFrames). Always includes frame 0 and totalFrames - 1. |
| snapshotState(state, options?) | Canonicalize a single ResolvedState for snapshotting. |
| snapshotTimeline(timeline, options?) | Resolve the timeline at multiple frames (explicit list or sample count) and wrap the results with width / height / fps / totalFrames metadata. |
| diffResolved(a, b, options?) | Recursive diff between two ResolvedState values. Returns a stable, flat list of DiffEntry. Numeric tolerance controlled by epsilon (default 1e-6). |
| formatDiff(entries) | Pretty-print a DiffEntry[] as one line per entry (path: a -> b). Returns (identical) on empty input. |
Design notes
- Deterministic output: object keys are sorted before serialization and finite numbers are rounded, so two runs of the same code produce byte-identical snapshots even when easing math produces sub-precision jitter.
- JSON-safe:
undefinedfields are dropped from canonicalized output so the result round-trips throughJSON.stringifywithout losing information. - No tree-circularity handling: Timeline IR and
ResolvedStateare tree-shaped. Cycles would indicate a bug elsewhere and should surface loudly via a stack overflow rather than being silently papered over. - Pure Node: no jsdom, no DOM, no canvas dependencies. Runs instantly in any vitest environment.
Testing
pnpm --filter @fr0/testing test31 tests across 4 files (canonicalize, sample-frames, snapshot, diff).
What this package is NOT
- Not a test runner (use vitest / jest / etc.)
- Not a pixel-level visual regression tool yet (Phase β+, once the canvas renderer / encoder lands server-side)
- Not tied to a specific framework — returns plain JS values, so any assertion library works
