On this pageOverview
Testing
The Elm Architecture makes testing straightforward. The update function is pure. Given a Model and a Message, it always returns the same result. No DOM, no HTTP calls, no timers. Just a function that takes data and returns data.
Foldkit ships two testing primitives. Story tests the state machine: you send Messages directly through update, resolve Commands inline, and assert on the Model. Scene tests features through the rendered view (clicking buttons, typing into inputs, pressing keys) using accessible locators. Both are pure, deterministic, and fast.
Use Story for update logic, edge cases, and Command wiring. Use Scene for user flows, view rendering, and accessibility. A well-tested Foldkit app uses both.
Story.story simulates the update loop. Each step reads like a sentence: send a Message, resolve a Command, check the Model. See the Story page for the full API.
Story tests are flexible about testing level. Because Story sends Messages directly to update and asserts on the Model, testing a child’s update in isolation is valid: the function signature is the contract, and it works the same whether the parent calls it or the test does.
import { Story } from 'foldkit'
import { expect, test } from 'vitest'
test('delayed reset: count resets after the delay fires', () => {
Story.story(
update,
Story.with({ count: 5 }),
Story.message(ClickedResetAfterDelay()),
Story.Command.expectExact(DelayReset),
Story.Command.resolve(DelayReset, CompletedDelayReset()),
Story.model(model => {
expect(model.count).toBe(0)
}),
)
})Scene.scene exercises the view. Locators find elements the way users do: by role, label, or placeholder. Interactions dispatch Messages through the rendered event handlers. Inline assertions check the HTML between steps. Scene also tracks the Mount lifecycle: the side effects declared by OnMount attributes in the view must be acknowledged via Scene.Mount.resolve, mirroring how Commands are resolved. See the Scene page for the full API.
Scene tests should always run from the root update and view. In a Submodel app, only the root view has the (model) => Html signature that Scene.scene expects. Every level below takes a toParentMessage adapter. Testing a child view in isolation means inventing a code path that never runs in production: the parent’s Command mapping, OutMessage handling, and Model transitions would all be invisible. Test what users see, through the same code path they use.
import { Scene } from 'foldkit'
import { test } from 'vitest'
test('type a zip code, click get weather, see the forecast', () => {
Scene.scene(
{ update, view },
Scene.with(model),
Scene.type(Scene.label('Zip code'), '90210'),
Scene.click(Scene.role('button', { name: 'Get Weather' })),
Scene.expect(Scene.role('button', { name: 'Loading...' })).toExist(),
Scene.Command.expectExact(FetchWeather),
Scene.Command.resolve(
FetchWeather,
SucceededFetchWeather({ weather: beverlyHillsWeather }),
),
Scene.inside(
Scene.role('article'),
Scene.expect(Scene.text('Beverly Hills, California')).toExist(),
Scene.expect(Scene.text('72\u00B0F')).toExist(),
Scene.expect(Scene.text('Clear sky')).toExist(),
),
)
})