Testing DSL
Ignite ships a scenario-style testing helper built on the headless runtime. It is exported as test from both ignite-element and ignite-element/xstate (and the other adapter entrypoints); import it under any name you like.
import { test as igniteTest } from 'ignite-element/xstate';import { igniteCore } from 'ignite-element/xstate';
const counter = igniteCore({ source: counterMachine, view: ({ matches }) => ({ isOn: matches('on'), }), commands: ({ actor }) => ({ toggle: () => actor.send({ type: 'TOGGLE' }), }), events: (event) => ({ toggled: event<{ isOn: boolean }>(), }), effects: ({ emit, select }) => { const isOn = select((snapshot) => snapshot.matches('on')); if (!isOn.changed) return; emit('toggled', { isOn: isOn.current }); },});Scenarios
Section titled “Scenarios”Call igniteTest(component) with a runtime to get a chainable scenario. given(...) asserts the starting state, when(...) executes a command (it is async — await it), and the expect* methods assert the result of the most recent when(...).
(await igniteTest(counter).given('off').when('toggle')) .expectState('on') .expectEvent('toggled', { isOn: true });Scenario methods — every given/expect* returns the scenario so calls chain; when(...) returns a Promise of the scenario:
given(expected)— assert the current state before acting. Accepts a state value, a deep-partial match, or a predicate.when(commandName, payload?)— execute a command; returns aPromiseof the scenario.expectState(expected)— assert the post-command state.expectView(expected)— assert the projected view (mirrorsgetView()). Accepts a deep-partial object or a predicate.expectEvent(type, payload?)— assert an event was emitted, optionally matching its payload.expectEvents(expected)— assert several events by{ type, payload? }.expectNoEvents()— assert the command emitted nothing.getResult()— return the raw{ state, events }of the lastwhen(...).
State, view, and payload expectations accept a deep-partial object, a predicate function, or — for state — the bare state value.
Stories
Section titled “Stories”A story records command, state, view, and event activity as one ordered behavior trace. Start one from the runtime’s record(name):
const story = counter.record('toggles on');
await story.execute('toggle');await story.until((view) => view.isOn, async () => { await story.execute('toggle');});
const trace = story.trace();const lifecycle = story.lifecycle();const summary = story.summary();
story.stop();Story methods:
execute(commandName, payload?)— run a command and record it; resolves to the same{ state, events }as the runtime.until(viewPredicate, action, options?)— repeatedly runactionuntil the projected view satisfiesviewPredicate.options.maxStepsbounds the loop. Returns aPromise<View>(the satisfying view).trace()— returnsIgniteStoryTraceEntry[]: the ordered behavior trace ofcommand,state,view, andevententries withkind,sequence, andstep.lifecycle()— returnsIgniteStoryLifecycleEntry[]: DOM lifecycle evidence (registration, connection, render, disconnection, cleanup) when a custom element or accessibility bridge participates; otherwise an empty array.summary()— returns{ name, finalState, finalView, events, commandCount, traceCount, lifecycleCount }.stop()— releases the story’s runtime subscriptions; returns nothing.
story.trace() holds only behavior evidence; DOM evidence stays on the separate story.lifecycle() channel, so logic stays inspectable without a DOM.
Trace and snapshot helpers
Section titled “Trace and snapshot helpers”The test export also carries helpers as properties:
serializeTrace(trace)— convertstory.trace()into a deterministic JSON-safe snapshot (the same ordered shape, deep-cloned).snapshotStory(story)— produce{ name, trace, lifecycle, summary }for inline snapshot tests.expectTrace(trace, expected, options?)— assert ordered checkpoints against a trace. By default it matches a subsequence; pass{ exact: true }to require a one-to-one match. Each expectation is a deep-partial entry or a predicate. Returns nothing; throws on mismatch.
const story = counter.record('toggles on');await story.execute('toggle');
const snapshot = igniteTest.serializeTrace(story.trace());
igniteTest.expectTrace(story.trace(), [ { kind: 'command', command: 'toggle' }, { kind: 'event', event: 'toggled', payload: { isOn: true } },]);
const full = igniteTest.snapshotStory(story);DOM accessibility bridge
Section titled “DOM accessibility bridge”For projection proof, mount the same runtime behind a test-only DOM bridge and assert rendered controls by role and accessible name. The bridge renders against the same runtime state the story mutates, so behavior assertions stay headless while DOM assertions stay focused.
import { test as igniteTest } from 'ignite-element/xstate';
const story = counter.record('reaches limit');const bridge = igniteTest.accessibilityBridge( counter, ({ count, limit, isLimited, increment, setLimit }) => ( <section> <output role="status" aria-label="Counter status"> {count} / {limit} </output> <button type="button" onClick={() => increment()}> Increment </button> {isLimited && <p role="status">Limit reached</p>} </section> ), { elementName: 'counter-accessibility-bridge' },);
await story.execute('increment');
igniteTest.expectControls(bridge, [ { role: 'status', name: 'Counter status' }, { role: 'button', name: 'Increment' },]);
bridge.stop();story.stop();Bridge surface:
accessibilityBridge(component, renderer, options?)— render the runtime into a shadow root and return a bridge.bridge.getByRole(role, options?)/bridge.queryByRole(role, options?)— find a control by role, optionally narrowing by accessiblename,text, orvalue(string,RegExp, or predicate).getByRolereturns the matching control (throws if none/ambiguous);queryByRolereturns the control ornull.bridge.expectControls(expected)(origniteTest.expectControls(bridge, expected)) — assert a set of controls by role, name, text, or value. Returns nothing; throws on mismatch.bridge.host/bridge.root— the host element and its shadow root.bridge.stop()— tear the bridge down; returns nothing.
Related
Section titled “Related”- Testing guide — project setup, machine tests, and DOM tests.
- Build for agents — the behavior-first workflow.
- Headless runtime — the runtime these helpers wrap.