Harnesses for testing XState v5 Actors. Actor test...audition...get it??
xstate-audition is a dependency-free library for testing the behavior of XState Actors.
API Documentation | GitHub | npm
- Usage & Examples
runUntilDone(actor)
runUntilEmitted(actor, emittedTypes)
runUntilTransition(actor, fromStateId, toStateId)
runUntilSnapshot(actor, predicate)
runUntilSpawn(actor, childId)
runUntilEventReceived(actor, eventTypes)
runUntilEventSent()
createActorFromLogic(logic, options)
createActorWith(options, logic)
patchActor(actor, options)
unpatchActor(actor)
AuditionOptions
- Requirements
- Installation
- API Notes
- License
- Disclaimer
TL;DR:
- Create an
Actor
usingxstate.createActor(logic)
. - Create a
Promise<T>
using one of the functions below (e.g.,runUntilDone(actor: Actor) => Promise<T>
whereT
is the Actor output). If the actor hadn't yet been started, it will be started now. - If your actor needs external input to resolve the condition (e.g., it must receive an event), perform that operation before you
await
thePromise<T>
(examples below). - Now, you can
await
thePromise<T>
from step 2. - Finally, make an assertion about
T
.
Run a Promise Actor or State Machine to Completion
runUntilDone(actor)
runUntilDoneWith(actor, options)
waitForDone(actor)
waitForDoneWith(actor, options)
runUntilDone(actor)
/ runUntilDoneWith(actor, options)
are curried functions that will start a Promise Actor or State Machine Actor and run it until it reaches a final state. Once the Actor
reaches a final state, it will immediately be stopped. The Promise
will be resolved with the output of the Actor
.
Note
runUntilDone()
is not significantly different than XState'stoPromise()
.runUntilDoneWith()
may be used to overwrite the internal logger and/or add an inspector callback (orObserver
) to anActor
.- There is no such
waitForDone(...)
/waitForDoneWith(...)
variant, since that would be silly.
import {strict as assert} from 'node:assert';
import {beforeEach, describe, it} from 'node:test';
import {type Actor, createActor, fromPromise} from 'xstate';
import {runUntilDone, runUntilDoneWith} from 'xstate-audition';
const promiseLogic = fromPromise<string, string>(
// this signal is aborted via call to Actor.stop()
async ({input, signal}) => {
let listener!: () => void;
try {
return await new Promise((resolve, reject) => {
listener = () => {
clearTimeout(timeout);
// this rejection is eaten by xstate-audition
// in lieu of its own timeout error (seen below)
reject(signal.reason);
};
const timeout = setTimeout(() => {
resolve(`hello ${input}`);
}, 500);
signal.addEventListener('abort', listener);
});
} finally {
signal.removeEventListener('abort', listener);
}
},
);
describe('logic', () => {
let actor: Actor<typeof promiseLogic>;
beforeEach(() => {
actor = createActor(promiseLogic, {input: 'world'});
});
it('should output with the expected value', async () => {
const result = await runUntilDone(actor);
assert.equal(result, 'hello world');
});
it('should abort when provided a too-short timeout', async () => {
await assert.rejects(
runUntilDoneWith(actor, {timeout: 100}),
(err: Error) => {
assert.equal(err.message, 'Actor did not complete in 100ms');
return true;
},
);
});
});
Run a State Machine Until It Emits Events
runUntilEmitted(actor, emittedTypes)
runUntilEmittedWith(actor, options, emittedTypes)
waitForEmitted(actor, emittedTypes)
waitForEmittedWith(actor, options, emittedTypes)
runUntilEmitted(actor, eventTypes)
/ runUntilEmittedWith(actor, options, eventTypes)
are curried function that will start an Actor
and run it until emits one or more events of the specified type
. Once the events have been emitted, the actor will immediately be stopped.
waitForEmitted(actor, eventTypes)
/ waitForEmittedWith(actor, options, eventTypes)
are similar, but do not stop the actor.
Note
This function only applies to events emitted via the event emitter API.
import {strict as assert} from 'node:assert';
import {beforeEach, describe, it} from 'node:test';
import {type Actor, createActor, emit, setup} from 'xstate';
import {type CurryEmittedP1, runUntilEmitted} from 'xstate-audition';
type Emit1 = {type: 'EMIT1'; value: string};
type Emit2 = {type: 'EMIT2'; value: number};
type EmitterEmitted = Emit1 | Emit2;
const emitterMachine = setup({
types: {
emitted: {} as EmitterEmitted,
},
}).createMachine({
entry: [
emit({type: 'EMIT1', value: 'value'}),
emit({type: 'EMIT2', value: 42}),
],
});
describe('emitterMachine', () => {
let actor: Actor<typeof emitterMachine>;
let runUntilEmit: CurryEmittedP1<typeof actor>;
beforeEach(() => {
actor = createActor(emitterMachine);
// runUntilEmitted is curried, so could be called with [actor, ['EMIT1', 'EMIT2']]
// instead
runUntilEmit = runUntilEmitted(actor);
});
it('should emit two events', async () => {
const [emit1Event, emit2Event] = await runUntilEmit(['EMIT1', 'EMIT2']);
assert.deepEqual(emit1Event, {type: 'EMIT1', value: 'value'});
assert.deepEqual(emit2Event, {type: 'EMIT2', value: 42});
});
});
Run a State Machine Until It Transitions from One State to Another
runUntilTransition(actor, fromStateId, toStateId)
runUntilTransitionWith(actor, options, fromStateId, toStateId)
waitForTransition(actor, fromStateId, toStateId)
waitForTransitionWith(actor, options, fromStateId, toStateId)
runUntilTransition(actor, fromStateId, toStateId)
/ runUntilTransitionWith(actor, options, fromStateId, toStateId)
are curried functions that will start an Actor
and run it until it transitions from state with ID fromStateId
to state with ID toStateId
. Once the Actor
transitions to the specified state, it will immediately be stopped.
waitForTransition(actor, fromStateId, toStateId)
/ waitForStateWith(actor, options, fromStateId, toStateId)
are similar, but do not stop the Actor
.
import {strict as assert} from 'node:assert';
import {beforeEach, describe, it} from 'node:test';
import {type Actor, createActor, createMachine} from 'xstate';
import {type CurryTransitionP2, runUntilTransition} from '../src/index.js';
const transitionMachine = createMachine({
// if you do not supply a default ID, then the ID will be `(machine)`
id: 'transitionMachine',
initial: 'first',
states: {
first: {
after: {
100: 'second',
},
},
second: {
after: {
100: 'third',
},
},
third: {
type: 'final',
},
},
});
describe('transitionMachine', () => {
let actor: Actor<typeof transitionMachine>;
let runWithFirst: CurryTransitionP2<typeof actor>;
beforeEach(() => {
actor = createActor(transitionMachine);
// curried
runWithFirst = runUntilTransition(actor, 'transitionMachine.first');
});
it('should transition from "first" to "second"', async () => {
await runWithFirst('transitionMachine.second');
});
it('should not transition from "first" to "third"', async () => {
await assert.rejects(runWithFirst('transitionMachine.third'));
});
});
Run a Actor Until It Satisfies a Snapshot Predicate
runUntilSnapshot(actor, predicate)
runUntilSnapshotWith(actor, options, predicate)
waitForSnapshot(actor, predicate)
waitForSnapshotWith(actor, options, predicate)
runUntilSnapshot(actor, predicate)
/ runUntilSnapshotWith(actor, options, predicate)
are curried functions that will start an Actor
and run it until the actor's Snapshot satisfies predicate
(which is the same type as the predicate
parameter of xstate.waitFor()
). Once the snapshot matches the predicate, the actor will immediately be stopped.
Note
- Like
runUntilDone()
,runUntilSnapshot()
is not significantly different than XState'swaitFor()
. runUntilSnapshotWith()
may be used to overwrite the internal logger and/or add an inspector callback (orObserver
) to an Actor.
import {strict as assert} from 'node:assert';
import {describe, it} from 'node:test';
import {assign, createActor, setup} from 'xstate';
import {runUntilSnapshot} from 'xstate-audition';
const snapshotLogic = setup({
types: {
context: {} as {word?: string},
},
}).createMachine({
initial: 'first',
states: {
done: {
type: 'final',
},
first: {
after: {
50: 'second',
},
entry: assign({
word: 'foo',
}),
},
second: {
after: {
50: 'third',
},
entry: assign({
word: 'bar',
}),
},
third: {
after: {
50: 'done',
},
entry: assign({
word: 'baz',
}),
},
},
});
describe('snapshotLogic', () => {
it('should contain word "bar" in state "second"', async () => {
const actor = createActor(snapshotLogic);
const snapshot = await runUntilSnapshot(actor, (snapshot) =>
snapshot.matches('second'),
);
assert.deepEqual(snapshot.context, {word: 'bar'});
});
it('should be in state "second" when word is "bar"', async () => {
const actor = createActor(snapshotLogic);
const snapshot = await runUntilSnapshot(
actor,
(snapshot) => snapshot.context.word === 'bar',
);
assert.equal(snapshot.value, 'second');
});
});
Run a State Machine Actor Until Its System Spawns a Child Actor
runUntilSpawn(actor, childId)
runUntilSpawnWith(actor, options, childId)
waitForSpawn(actor, childId)
waitForSpawnWith(actor, options, childId)
runUntilSpawn(actor, childId)
/ runUntilSpawnWith(actor, options, childId)
are curried functions that will start an Actor
and run it until it spawns a child ActorRef
with id
matching childId
(which may be a RegExp
). Once the child ActorRef
is spawned, the Actor
will immediately be stopped. The Promise
will be resolved with a reference to the spawned ActorRef
(an AnyActorRef
by default).
waitForSpawn(actor, childId)
/ waitForSpawnWith(actor, options, childId)
are similar, but do not stop the actor.
The root State Machine actor itself needn't spawn the child with the matching id
, but any ActorRef
within the root actor's system may spawn the child.
Note
- The type of the spawned
ActorRef
cannot be inferred by ID alone. For this reason, it's recommended to provide an explicit type argument torunUntilSpawn
(and variants) declaring the type of the spawnedActorRef
'sActorLogic
, as seen in the below example. - As of this writing, there is no way to specify the parent of the spawned
ActorRef
.
import {strict as assert} from 'node:assert';
import {describe, it} from 'node:test';
import {createActor, fromPromise, setup, spawnChild} from 'xstate';
import {waitForSpawn} from 'xstate-audition';
const noopPromiseLogic = fromPromise<void, void>(async () => {});
const spawnerMachine = setup({
actors: {noop: noopPromiseLogic},
types: {events: {} as {type: 'SPAWN'}},
}).createMachine({
on: {
SPAWN: {
actions: spawnChild('noop', {id: 'noopPromise'}),
},
},
});
describe('spawnerMachine', () => {
it('should spawn a child with ID "noopPromise" when "SPAWN" event received', async () => {
const actor = createActor(spawnerMachine);
try {
// spawnerMachine needs an event to spawn the actor. but at this point,
// the actor hasn't started, so we cannot send the event because nothing
// will be listening for it.
//
// but if we start the actor ourselves & send the event, spawning could
// happen before waitForSpawn can detect it! so instead of immediately
// awaiting, let's just set it up first.
const promise = waitForSpawn<typeof noopPromiseLogic>(
actor,
'noopPromise',
);
// the detection is now setup and the actor is active; the code running in
// the Promise is waiting for the spawn to occur. so let's oblige it:
actor.send({type: 'SPAWN'});
// ...then we can finally await the promise.
const actorRef = await promise;
assert.equal(actorRef.id, 'noopPromise');
} finally {
// you can shutdown manually! for fun!
actor.stop();
}
});
});
Run an Actor Until It Receives an Event
runUntilEventReceived(actor, eventTypes)
runUntilEventReceivedWith(actor, options, eventTypes)
waitForEventReceived(actor, eventTypes)
waitForEventReceivedWith(actor, options, eventTypes)
runUntilEventReceived(actor, eventTypes)
/ runUntilEventReceivedWith(actor, options, eventTypes)
are curried functions that will start a State Machine Actor, Callback Actor, or Transition Actor and run it until it receives event(s) of the specified type
. Once the event(s) are received, the actor will immediately be stopped. The Promise
will be resolved with the received event(s).
runUntilEventReceived()
's options
parameter accepts an otherActorId
(string
or RegExp
) property. If set, this will ensure the event was received from the actor with ID matching otherActorId
.
withForEventReceived(actor, eventTypes)
/ waitForEventReceivedWith(actor, options, eventTypes)
are similar, but do not stop the actor.
Usage is similar to runUntilEmitted()
—with the exception of the otherActorId
property as described above.
Run an Actor Until It Sends an Event
runUntilEventSent(actor, eventTypes)
runUntilEventSentWith(actor, options, eventTypes)
waitForEventSent(actor, eventTypes)
waitForEventSentWith(actor, options, eventTypes)
runUntilEventSent(actor, eventTypes)
/ runUntilEventSentWith(actor, options, eventTypes)
are curried functions that will start an Actor
and run it until it sends event(s) of the specified type
. Once the event(s) are sent, the actor will immediately be stopped. The Promise
will be resolved with the sent event(s).
runUntilEventSentWith()
's options
parameter accepts an otherActorId
(string
or RegExp
) property. If set, this will ensure the event was sent to the actor with ID matching otherActorId
.
waitForEventSent(actor, eventTypes)
/ waitForEventSentWith(actor, options, eventTypes)
are similar, but do not stop the actor.
Usage is similar to runUntilEmitted()
—with the exception of the otherActorId
property as described above.
Curried Function to Create an Actor from Logic
A convenience function for when you find yourself repeatedly creating the same actor with different input.
See also createActorWith()
.
const createActor = createActorFromLogic(myLogic);
it('should do x with input y', () => {
const actor = createActor({input: 'y'});
// ...
});
it('should do x2 with input z', () => {
const actor = createActor({input: 'z'});
// ...
});
Curried Function to Create an Actor with Options
A function for when you find yourself repeatedly creating different actors with the same input.
See also createActorFromLogic()
.
const createYActor = createActorWith({input: 'y'}});
it('should do x with FooMachine', () => {
const actor = createYActor(fooMachine);
// ...
});
it('should do x2 with BarMachine', () => {
const actor = createYActor(barMachine);
// ...
});
Modify an Actor for Use with xstate-audition
This is used internally by all of the other curried functions to violate mutate existing actors. You shouldn't need to use it, but it's there if you want to.
Revert Modifications Made to an Actor by xstate-audition
Warning
This function is experimental and may be removed in a future release.
unpatchActor(actor)
will "undo" what xstate-inspector did in patchActor.
If xstate-audition has never mutated the Actor
, this function is a no-op.
Options for many xstate-audition Functions
If you want to attach your own inspector, use a different logger, or set a different timeout, you can use AuditionOptions
.
It bears repeating: all functions ending in With()
accept an AuditionOptions
object as the second argument. If the function name doesn't end with With()
, it does not accept an AuditionOptions
object.
The AuditionOptions
object may contain the following properties:
-
inspector
-((event: xstate.InspectionEvent) => void) | xstate.Observer<xstate.InspectionEvent>
: An inspector callback or observer to attach to the actor. This will not overwrite any existing inspector, but may be "merged" with any inspector used internally by xstate-audition.The behavior is similar to setting the
inspect
option when callingxstate.createActor()
. -
logger
-(args: ...any[]) => void
: Default: no-op (no logging; XState defaults toconsole.log
). Set the logger of the Actor.The behavior is similar to setting the
logger
option when callingxstate.createActor()
; however, this loggerwillshould cascade to all child actors. -
timeout
-number
: Default: 1000ms. The maximum number of milliseconds to wait for the actor to satisfy the condition. If the actor does not satisfy the condition within this time, thePromise
will be rejected.A
timeout
of0
, a negative number, orInfinity
will disable the timeout.The value of
timeout
should be less than the test timeout!
- Node.js v20.0.0+ or modern browser
xstate
v5+
Caution
Haven't tested the browser yet, but there are no dependencies on Node.js builtins.
xstate v5+ is a peer dependency of xstate-audition.
npm install xstate-audition -D
- All functions exposed by xstate-audition's are curried. The final return type of each function is
Promise<T>
. - All functions ending in
With()
accept anAuditionOptions
object as the second argument. If the function name doesn't end withWith()
, it does not accept anAuditionOptions
object (exceptingcreateActorFromLogic
). - Any inspectors already attached to an
Actor
provided to xstate-audition will be preserved. - At this time, xstate-audition offers no mechanism to set global defaults for
AuditionOptions
.
©️ 2024 Christopher "boneskull" Hiller. Licensed Apache-2.0.
This project is not affiliated with nor endorsed by Stately.ai.