Story, Scene, Showcase
Foldkit already ships two ways to test a component. Foldcase adds a third that is not a way of testing at all.
Foldkit already ships two ways to test a component. Foldcase adds a third that is not a way of testing at all.
Story — foldkit/story — drives update and everything it returns: send Messages,
resolve the Commands they produce (Command.resolveAll cascades through a whole async
flow), assert on the Model and on OutMessages. It accounts for every Command the reducer
returned, so a story cannot pass on one you forgot to think about.
story(
update,
given(initialModel),
message(ClickedIncrement()),
Command.expectNone(),
model((m) => assert.equal(m.count, 1)),
)Scene — foldkit/scene — mounts { update, view } and reads the rendered markup
the way a user does: getByRole, getByLabel, click, type, dropFiles. It renders
to Foldkit's virtual tree and queries that, so it needs no browser and no DOM — the
vitest-plus-happy-dom setup in Foldkit's examples is convention, not a requirement;
examples/counter runs two Scene Showcases under both bins with no
DOM package installed. A failed assertion names the locator — Expected element matching
button "Restart" to exist — so a miss reads as "the control you asked for is not in the
markup". What a Scene deliberately cannot see is the Model: it asserts through the view.
scene(
{ update, view },
given(homeModel),
expect(role("link", { name: "Calendar" })).toExist(),
)The two answer different questions, and neither catches the other's bugs — and some bugs
escape both. Building the Foldkit component gallery showed the ceiling: a Story proved
DragAndDrop moves a card between columns and lands it in the right place, and it was
right — but a real browser showed that the card's element is rebuilt when it changes
column, so focus drops to <body> halfway through a keyboard drag. Element identity
across a re-render is invisible to a Model assertion and to a virtual render alike; only a
browser sees it. Test the machine with a Story, the markup with a Scene, and keep a
browser in the loop for what only it can show.
A Showcase is neither. It is a record, not a function:
{ id, play, message?, model?, dispatches? }play usually holds a Story — the cheap, deterministic half — but the runner never looks
inside it. What Foldcase reads is everything around it: the id names one component in
one state, and message/model are the Schemas that state is built from. A Story and a
Scene are functions a runner calls; they run, they pass, and they are gone. A Showcase is
a description that stays readable, so one declaration feeds the CI run, the Markdown
tables, the coverage attribution and the agent's catalog at once.
| drives | answers | cannot see | |
|---|---|---|---|
| Story | update + Commands + OutMessages | is the state machine right | the markup |
| Scene | update + view, virtually rendered | does the markup reach the machine | the Model — opaque by design |
| Showcase | whatever play holds — usually a Story | which components exist, in which states, built from which Schemas | — |
Foldcase does not depend on Foldkit, and play is an opaque thunk: it can hold a Story, a
Scene, another assertion library, or plain code, and stays headless either way.
Last updated Aug 4, 2026