Skip to content

Testing

Jace edited this page Sep 2, 2026 · 3 revisions

Testing

Everything here drives a real browser against the dev server, so start that first.

bun run play          # serves localhost:5175, leave it running
bun run check:stress  # 46 cases through one element
bun run check:flow    # the words around a value
bun run check:churn   # interruption: what a second update must not break
bun run check:versus  # parity against the upstream this forked from
bun run check:docs    # the behavioural claims the documentation makes
bun run check:size    # the README's size table against the built packages

STRESS_URL and its siblings point the harnesses at another build, a deployed one for instance. The check: commands assert. check:size is the one that needs no browser: it builds the packages, gzips what ships and fails if the README's table has drifted from it.

The dev pages /versus, /suite and /look are served by bun run play but are not built into the deployed site. The checks drive them against the dev server.

bun run check:stress

Forty-six cases through one element. Grouped numbers and currency, signs, emoji, CJK, RTL, zero-width joiners, values long enough to wrap, values sitting inside an inline wrapper, values with a neighbour hard against either side.

It asserts what breaks quietly. No glyph may overlap a neighbour by more than half a pixel, a kept glyph has to land where the settled layout puts it, and the box has to end up the size of the text it holds.

bun run check:flow

The wrapping side. A word that keeps its line has to slide along it, a word that changes line has to hand off between ghosts, the leaving copies drawn for it, rather than fly, and the block has to hold its height while a value inside it resizes. The homepage's live sentence gets its own case, because a counter ticking mid-paragraph is the most likely thing to shuffle a line the reader is not watching.

A settled host, the element the value is rendered in, wears nothing a transition put on it. The harness hammers a value across widths inside a paragraph and asserts as much afterwards, with display, margin, shrink-clip and inline width all back where they started, and that the paragraph never gains a line box while the hammering runs. The width transition survives across updates, so its teardown is only ever reached by the animation finishing, and that is the thing worth guarding.

bun run check:churn

Interruption. Fifteen transitions redraw only what changed. Eighteen kept-run position checks. A rapid 0.230.40 still has the 2 rolling when the 4 arrives. That case exists to stop overlapping rolls being "fixed" by cancelling them. Twelve overlapping WordsNumbersWords cycles keep all eight glyphs of Creative visible.

bun run check:docs

Guards the behavioural claims the documentation makes, which are the ones that go quietly false. Ten of them: that scrittochange fires twice on a real change and not at all on an unchanged value, that the value setter does not animate while update() does, that the box animations live on the host rather than in the shadow tree, that a formatted number keeps the glyphs that did not change, that an offscreen element updates without animating, that edgeFade leaves roomy text unfaded on auto, forces the band on always and refuses it on never, that a value breaks between its words and never inside one, that the options apply as set, and that prefers-reduced-motion updates the value without motion. The last one runs in its own emulated context.

A signature check would catch none of this. Every identifier the docs name exists and is spelled correctly, and the documentation was still wrong about what happens when a value is unchanged. Names going stale is not the failure mode. Behavioural claims going stale is.

bun run check:versus

Holds this fork against the upstream it forked. /versus already runs both engines on one timer with the same constants pushed into both, so this drives that page at three cadences, 90ms and 40ms and 20ms against a 590ms roll, and compares what a reader sees rather than what the code intends.

It records with the frame limiter off, at around 100fps, then measures three things per cadence: ink per half straight off the recorded frames, how wide that ink spreads, and how long a ghost lives from the moment its animation is created to the moment it leaves the DOM. Ink and spread are guarded both ways, since carrying less than upstream is a divergence too, at 1.03x and 1.04x, and a glyph's life within 90ms.

Ink is summed brightness over the background, not a count of pixels past a cutoff. A cutoff reads 1.05x off a difference in how the brightness is distributed while the ink itself is level, and nothing in these frames ever reaches a lift of 150 out of 255, so a cutoff sits in the tail.

Measured, stable across runs: ink 0.998x at every cadence, spread 1.000x, a glyph living 590ms here against 591ms upstream.

/versus

Upstream numeric-text and this fork in one figure, on one timer, with every constant written into both engines' CONFIG so the only thing differing is the code. Upstream is vendored unmodified at apps/website/src/vendor/numeric-text and registers <numeric-text>, so the two tags coexist; nothing else on the site imports it.

Freeze pauses every animation in both shadow roots, catching each where it stands rather than rewinding to a shared zero. At these cadences a dozen rolls are in flight at different ages, and that spread is the thing worth looking at. Scrub then walks the whole scene forward together, and the readout counts ink still on screen grouped by the slot it sits in.

Forward only, because backward cannot be accurate. A glyph that has finished is released, and rewinding cannot bring it back, so the two sides would show different pasts of the same value.

This page is the case against hurry. Measured at 90ms and 40ms cadences the two engines match to within a frame, with lifetimes 607 against 607ms, ghosts on screen 248 against 236ms, entrances 317 against 301ms and nothing cut short on either side, and both stack ghosts in the same slot rather than swapping or deleting them. Every apparent difference between them traces back to that option running the fork's ghosts at up to 6×.

One engine difference remains: direction. Upstream reads a grouped value with parseFloat, so 5,229 → 5,236 is 5 → 5 and rolls downward; the fork parses the whole number and rises.

Case sweep

bun research/run-cases.ts runs the documented value changes and writes research/cases.json: what is kept, how far it travels, how the box resizes. It is the source for Measured cases. Re-run it after any change to the matcher.

Clone this wiki locally