Part of #143 — the Guide expansion. Group Going further.
Purpose & audience
Teach testing an app without a terminal. The Introduction claims "assert on what is on screen in an
ordinary test run" and never shows how — this cashes that promise. Cheap page, high credibility.
The gap today
The headless-mount pattern is used all over the repo's own tests but is nowhere in user-facing docs.
A reader who wants to test their TUI in CI has no guidance.
Outline
- Why it's possible: the engine is host-agnostic — no real TTY needed; you drive input and read the
composed frame.
- Mounting headlessly: build the app with a headless/viewport host and no native tty.
- Reading the screen:
app.loop.renderRoot.buffer().rows() gives the composed frame as strings —
assert on what's actually painted.
- Driving input: dispatching keys/commands (
app.loop.dispatch(...)) and asserting the result.
- Async + modal testing: awaiting an
execView dialog headlessly, resolving it, asserting after.
createRenderRoot({ schedule }) for a bare view (no app), and the trap: that counter conflates
repaint with relayout — assert a reflow via an inline literal on view.host, not the schedule
counter.
Code examples to write
- Mount an app, dispatch a key, assert a row of
buffer().rows().
- Drive a dialog: open via
execView, dispatch Enter, await, assert the resolved value.
- A bare-view render-root test with
createRenderRoot.
Live examples
- Reuse: none — a test-authoring page; a live xterm can't show a vitest run.
- Build new: none. (The runnable artifact here is a copy-pasteable
*.test.ts, not a Play example.)
Source pointers
packages/ui/src/view/ — createRenderRoot / RenderRoot (buffer().rows()).
- Memories
ui-relayout-test-seam (no ViewHost double in ui tests; assert reflow on public
view.host, never the createRenderRoot({schedule}) counter — it conflates repaint with relayout)
and jsvision-ui-modal-dialog-patterns (the headless async/nested-confirm test harness).
- The docs-site's own
test/paint-smoke.spec.test.ts mounts every example headlessly — a working
reference for the mount pattern.
Constraints
- No
.layout = (snippet-drift). Test files have their own conventions, but this page is a teaching
artifact and is held to the guard.
Acceptance criteria
Part of #143 — the Guide expansion. Group Going further.
Purpose & audience
Teach testing an app without a terminal. The Introduction claims "assert on what is on screen in an
ordinary test run" and never shows how — this cashes that promise. Cheap page, high credibility.
The gap today
The headless-mount pattern is used all over the repo's own tests but is nowhere in user-facing docs.
A reader who wants to test their TUI in CI has no guidance.
Outline
composed frame.
app.loop.renderRoot.buffer().rows()gives the composed frame as strings —assert on what's actually painted.
app.loop.dispatch(...)) and asserting the result.execViewdialog headlessly, resolving it, asserting after.createRenderRoot({ schedule })for a bare view (no app), and the trap: that counter conflatesrepaint with relayout — assert a reflow via an inline literal on
view.host, not the schedulecounter.
Code examples to write
buffer().rows().execView, dispatch Enter, await, assert the resolved value.createRenderRoot.Live examples
*.test.ts, not a Play example.)Source pointers
packages/ui/src/view/—createRenderRoot/RenderRoot(buffer().rows()).ui-relayout-test-seam(noViewHostdouble in ui tests; assert reflow on publicview.host, never thecreateRenderRoot({schedule})counter — it conflates repaint with relayout)and
jsvision-ui-modal-dialog-patterns(the headless async/nested-confirm test harness).test/paint-smoke.spec.test.tsmounts every example headlessly — a workingreference for the mount pattern.
Constraints
.layout =(snippet-drift). Test files have their own conventions, but this page is a teachingartifact and is held to the guard.
Acceptance criteria
buffer().rows()assertion shown; input driving shown.