Expert guidance on testing XState v5 Actors using xstate-audition library for comprehensive state machine and actor testing
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
references/core-functions.mdreferences/options-types.mdreferences/testing-patterns.mddescription: Expert guidance on testing XState v5 Actors using xstate-audition library for comprehensive state machine and actor testing triggers:
You are an expert in testing XState v5 Actors using xstate-audition, a dependency-free library for testing XState actor behavior. Use this knowledge to guide test implementation with best practices for state machine and actor testing.
Use xstate-audition when:
Do NOT use xstate-audition for:
All xstate-audition functions follow this pattern:
Actor using createActor(logic)Promise<T> using a condition function (e.g., runUntilDone(actor))await the Promise<T>Timport { createActor } from 'xstate';
import { runUntilDone } from 'xstate-audition';
const actor = createActor(myMachine);
const result = await runUntilDone(actor); // starts and runs to completion
assert.equal(result, expectedOutput);
runUntil*() vs waitFor*()runUntil*(): Starts the actor, waits for condition, then stops the actorwaitFor*(): Starts the actor, waits for condition, but keeps the actor runningUse runUntil*() for isolated tests, waitFor*() when you need to continue testing.
runUntilDone() - Wait for final staterunUntilEmitted() - Wait for event emissionsrunUntilTransition() - Wait for specific state changesrunUntilSnapshot() - Wait for snapshot predicaterunUntilSpawn() - Wait for child actor spawningrunUntilEventReceived(), runUntilEventSent() - Inter-actor communicationimport { createActor, fromPromise } from 'xstate';
import { runUntilDone } from 'xstate-audition';
const promiseLogic = fromPromise<string, string>(async ({ input }) => {
return `hello ${input}`;
});
it('should complete with expected output', async () => {
const actor = createActor(promiseLogic, { input: 'world' });
const result = await runUntilDone(actor);
assert.equal(result, 'hello world');
});
import { runUntilTransition } from 'xstate-audition';
it('should transition from idle to loading', async () => {
const actor = createActor(fetchMachine);
// Curried form for reusability
const waitFromIdle = runUntilTransition(actor, 'fetchMachine.idle');
actor.send({ type: 'FETCH' });
await waitFromIdle('fetchMachine.loading');
});
import { emit, setup } from 'xstate';
import { runUntilEmitted } from 'xstate-audition';
const emitterMachine = setup({
types: {
emitted: {} as { type: 'READY'; value: string },
},
}).createMachine({
entry: emit({ type: 'READY', value: 'initialized' }),
});
it('should emit READY event on entry', async () => {
const actor = createActor(emitterMachine);
const [event] = await runUntilEmitted(actor, ['READY']);
assert.deepEqual(event, { type: 'READY', value: 'initialized' });
});
When actors need events to satisfy conditions:
import { waitForSpawn } from 'xstate-audition';
it('should spawn child when event received', async () => {
const actor = createActor(spawnerMachine);
// Setup the promise BEFORE sending the event
const promise = waitForSpawn(actor, 'childId');
// Now send the event that triggers spawning
actor.send({ type: 'SPAWN' });
// Finally await the result
const childRef = await promise;
assert.equal(childRef.id, 'childId');
});
import { runUntilSnapshot } from 'xstate-audition';
it('should reach error state with error in context', async () => {
const actor = createActor(fetchMachine);
actor.send({ type: 'FETCH', url: 'invalid' });
const snapshot = await runUntilSnapshot(
actor,
(snapshot) => snapshot.matches('error') && snapshot.context.error !== null,
);
assert.ok(snapshot.context.error);
assert.equal(snapshot.value, 'error');
});
All functions ending in With() accept AuditionOptions as the second parameter:
import { runUntilDoneWith } from 'xstate-audition';
it('should timeout if takes too long', async () => {
const actor = createActor(slowMachine);
await assert.rejects(
runUntilDoneWith(actor, { timeout: 100 }), // 100ms timeout
(err: Error) => {
assert.match(err.message, /did not complete in 100ms/);
return true;
},
);
});
timeout (default: 1000ms): Maximum wait time. Set to 0, negative, or Infinity to disable.logger (default: no-op): Custom logger function for debugging.inspector: Custom inspector callback or observer for actor events.All functions are curried for reusability:
import { runUntilTransition } from 'xstate-audition';
describe('stateMachine', () => {
let actor: Actor<typeof machine>;
let runFromIdle: CurryTransitionP2<typeof actor>;
beforeEach(() => {
actor = createActor(machine);
// Curry with actor and fromState
runFromIdle = runUntilTransition(actor, 'machine.idle');
});
it('should transition to loading', async () => {
actor.send({ type: 'FETCH' });
await runFromIdle('machine.loading');
});
it('should transition to success', async () => {
actor.send({ type: 'FETCH' });
actor.send({ type: 'SUCCESS' });
await runFromIdle('machine.success');
});
});
waitFor*() for multi-stage tests - Keep actor alive for sequential assertionsrunUntilSpawn() to get correct typesrunUntilTransition() for clear intentrunUntilSnapshot() for complex conditions - When multiple conditions must be metWhen testing parent/child actor systems:
import { runUntilSpawn, waitForSnapshot } from 'xstate-audition';
it('should spawn child and communicate', async () => {
const parent = createActor(parentMachine);
// Wait for child to spawn
const child = await waitForSpawn<typeof childLogic>(parent, 'childActor');
// Parent still running, send event to child
child.send({ type: 'CHILD_EVENT' });
// Wait for parent to react to child's output
await waitForSnapshot(parent, (snapshot) =>
snapshot.matches('parentReacted'),
);
});
import { describe, it, beforeEach } from 'node:test';
import { strict as assert } from 'node:assert';
describe('myMachine', () => {
let actor: Actor<typeof myMachine>;
beforeEach(() => {
actor = createActor(myMachine);
});
it('should complete successfully', async () => {
const result = await runUntilDone(actor);
assert.equal(result, 'expected');
});
});
import { describe, it, beforeEach, expect } from 'vitest';
describe('myMachine', () => {
it('should reach error state', async () => {
const actor = createActor(myMachine);
actor.send({ type: 'ERROR' });
const snapshot = await runUntilSnapshot(actor, (s) => s.matches('error'));
expect(snapshot.context.error).toBeDefined();
});
});
When tests fail or timeout:
{ logger: console.log } to see actor activity{ inspector: (event) => console.log(event) } for detailed eventsimport { runUntilSnapshotWith } from 'xstate-audition';
const snapshot = await runUntilSnapshotWith(
actor,
{
logger: console.log,
timeout: 5000,
inspector: (event) => console.log('Inspector:', event),
},
(snapshot) => snapshot.matches('targetState'),
);
For detailed API documentation, see the references directory:
Awaiting before sending required event:
// ❌ WRONG - promise created but never satisfied
const promise = waitForSnapshot(actor, (s) => s.matches('done'));
await promise; // hangs forever, no event sent!
// ✅ CORRECT
const promise = waitForSnapshot(actor, (s) => s.matches('done'));
actor.send({ type: 'COMPLETE' }); // send before await
await promise;
Wrong state ID format:
// ❌ WRONG - missing machine ID prefix
await runUntilTransition(actor, 'idle', 'loading');
// ✅ CORRECT - include machine ID
await runUntilTransition(actor, 'myMachine.idle', 'myMachine.loading');
Not providing type parameters for spawn:
// ❌ WRONG - type is AnyActorRef (not useful)
const child = await runUntilSpawn(actor, 'childId');
// ✅ CORRECT - explicit type
const child = await runUntilSpawn<typeof childLogic>(actor, 'childId');
When implementing tests with xstate-audition:
runUntilDone() tests for basic actor behaviorrunUntilTransition, runUntilEmitted) for targeted testsRemember: xstate-audition excels at testing actor behavior and interactions. It complements (not replaces) unit testing of individual guards, actions, and services.