Tests
The harness ships with the framework, and assertions are synchronous:
import { render } from 'lemonadejs/test'; // any DOM: browser or jsdom
const t = render(Counter, { start: 5 });
t.query('button').click();
t.query('p').textContent === '6'; // assert on the NEXT LINE: no act(),
t.unmount(); // no nextTick(), no waitFor()
But a unit harness is half the story. This chapter documents the discipline the project actually practices: jsdom for state and DOM logic, real Chrome for geometry, because the second half is where test suites overstate what they verified.
Why two layers
- jsdom has no layout engine. Every
getBoundingClientRect()returns zeros; nothing has a width, nothing overlaps, nothing overflows, nothing anchors. A dropdown panel rendered 400px away from its input passes every test written for it. - Scheduled updates require flush calls. When the engine schedules DOM updates, assertions must wait for them to settle. The common case is usually handled automatically; flakiness clusters at the edges: timers, transitions, manually scheduled work.
- “Renders without crashing” is not conformance. A published component promises props, events, a bindable value, an api. Almost no test suite mechanically checks the promise.
- Agents make this acute. An agent cannot eyeball a browser. If its feedback loop cannot see layout, it ships geometry bugs with a green test suite, and both real bugs below did exactly that.
The test layers
Layer 1, lemonadejs/test, on any DOM. render() mounts into a
fresh container and returns a handle: query/queryAll (scoped
querySelector), text(), snapshot() (deterministic, attribute-sorted,
diffable text rendering of the DOM tree), inspect() (the live component
tree as plain JSON: { component, contract, states, children }), and
unmount(). Because state updates are synchronous, there
is no flush step: click, then assert.
Two helpers cover what synchronous updates cannot. flush() drains
microtasks and zero-delay timers, the awaiting step for genuinely async
work like a resource() response landing:
await flush() then assert. setRect(el, { left, width, ... })
plants geometry on an element in jsdom, where no layout engine exists
and every rectangle is zero, so a modal’s auto-adjust, a tooltip’s
anchoring or a drag’s hit-testing become logic-testable in jsdom, with
the real-pixel truth still owned by the probe layer below.
verify() is the conformance layer: it reads the component’s
contract and exercises the promise. It mounts with
defaults, every prop as a plain value and as a live state, every
event, bind, and the declared api actually exposed through
props.ref. Any LJS-* dev warning during any check fails that check.
The Studio registry refuses blocks whose verify() fails.
Layer 2, real Chrome, for geometry. The probe pattern, zero dependencies:
.probe/*.tsare in-page assertion scripts: mount the block, drive it (open, scroll, type, drag), measure with realgetBoundingClientRect(), and writePASS/FAILlines into a<pre id="lm-probe">.scripts/chrome-probe.mjs(96 lines, no packages) spawns the installed Chrome headless with a DevTools port, connects over Node’s globalWebSocket, polls the page for the#lm-probeblock, prints it, and exits non-zero on anyFAIL.npm run probebundles every probe page and runs them all against the dev server.
An assertion from the dropdown probe, the kind jsdom structurally cannot make:
api.open();
await frame();
const panelRect = panel.getBoundingClientRect();
log('panel-anchored-under-input',
Math.abs(panelRect.top - (inputRect.bottom + 1)) <= 2 &&
Math.abs(panelRect.left - inputRect.left) <= 2,
{ panelTop: Math.round(panelRect.top), inputBottom: Math.round(inputRect.bottom) });
In practice
The receipts: at the time of writing, the repository runs 1,049 behavior tests (engine + all 43 Studio blocks, jsdom) and 62 real-Chrome probe assertions across eight probe pages (modal, dropdown, grid, menu, calendar, schedule, gantt, router) on every change. Two bugs that suite structure caught, and jsdom alone provably could not:
The Modal anchor bug. Anchored panels read their top/left props
once, at construction (setup runs once by design), so a panel fed live
anchor coordinates opened wherever the coordinates were before the
caller measured them. Every jsdom test stayed green: in jsdom all
rectangles are zero, so “panel at 0,0” is indistinguishable from
correct. The probe assertion above (panel top within 2px of the input’s
bottom) failed in real Chrome; the fix re-reads position props per open.
This bug class later motivated computed().
The dropdown focus race. In a real browser, a mousedown’s default
action focuses the label; opening the panel swaps a branch, the renderer
disposes the focused node, and the browser fires focusout exactly as
if the user clicked away, closing the panel in the same tick it opened.
jsdom never produces that event sequence. The probe drives the genuine
flow (field.focus() then a coordinate-carrying mousedown) and
asserts the panel survives; the fix is the isDisposing() guard
(see Destroy).
And the conformance loop an agent runs after generating a component:
import { verify } from 'lemonadejs/test';
const report = verify(Switch);
report.pass; // true, or:
report.checks; // [{ name: 'prop label (live state)', pass: false, detail: '...' }]
Generate the component and its proof in one breath; a failing check names the broken promise.
Reference
import { render, verify } from 'lemonadejs/test';
const t = render(Component, props?); // TestHandle
t.root // the mounted container
t.query(sel) // querySelector, scoped: HTMLElement | null
t.queryAll(sel) // real array of elements
t.text() // full visible text
t.snapshot() // deterministic indented DOM tree (attrs sorted), diffable
t.inspect() // { component, contract, states, children } as plain JSON
t.unmount() // dispose everything
verify(C) // { component, pass, checks: [{ name, pass, detail? }] }
// requires a contract; any LJS-* warning fails a check
import { flush, setRect } from 'lemonadejs/test';
await flush() // drain microtasks + zero-delay timers (async work landed)
setRect(el, { left: 10, width: 200, height: 24 })
// plant geometry in jsdom (no layout there): modal,
// tooltip and drag logic become testable
Probe commands (library repo):
npm test # vitest: engine + every block, jsdom
npm run dev # serves the playground + probe pages on :3000
npm run probe # bundle .probe/*.ts, run each page in headless Chrome
node scripts/chrome-probe.mjs http://localhost:3000/.probe/probe-grid.html
# CHROME=/path/to/chrome to point at a different binary
The division of labor, memorized: state, events, contract conformance go to jsdom, synchronous, fast. Anything with a coordinate in it goes to a probe, in the real engine, with the assertion in the page.