Skip to content
Foldcase

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.

Storyfoldkit/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.

ts
story(
  update,
  given(initialModel),
  message(ClickedIncrement()),
  Command.expectNone(),
  model((m) => assert.equal(m.count, 1)),
)

Scenefoldkit/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.

ts
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:

ts
{ 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.

drivesanswerscannot see
Storyupdate + Commands + OutMessagesis the state machine rightthe markup
Sceneupdate + view, virtually rendereddoes the markup reach the machinethe Model — opaque by design
Showcasewhatever play holds — usually a Storywhich 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