Initial commit
This commit is contained in:
386
skills/xstate-audition/SKILL.md
Normal file
386
skills/xstate-audition/SKILL.md
Normal file
@@ -0,0 +1,386 @@
|
||||
---
|
||||
description: Expert guidance on testing XState v5 Actors using xstate-audition library for comprehensive state machine and actor testing
|
||||
triggers:
|
||||
- xstate-audition
|
||||
- testing state machines
|
||||
- testing actors
|
||||
- actor testing
|
||||
- state machine testing
|
||||
- xstate test
|
||||
- runUntilDone
|
||||
- waitForSnapshot
|
||||
---
|
||||
|
||||
# XState Audition Testing Skill
|
||||
|
||||
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.
|
||||
|
||||
## When to Use xstate-audition
|
||||
|
||||
Use **xstate-audition** when:
|
||||
|
||||
- ✅ Testing XState v5 Actors (state machines, promise actors, callback actors, etc.)
|
||||
- ✅ Verifying state transitions and actor lifecycle behavior
|
||||
- ✅ Testing event handling, emissions, and inter-actor communication
|
||||
- ✅ Validating actor spawning and hierarchical actor systems
|
||||
- ✅ Testing asynchronous actor behavior with timeouts
|
||||
- ✅ Comprehensive actor integration testing
|
||||
|
||||
**Do NOT use xstate-audition for:**
|
||||
|
||||
- ❌ Unit testing pure functions (guards, actions in isolation)
|
||||
- ❌ Testing UI components directly (use framework-specific testing tools)
|
||||
- ❌ XState v4 actors (this library is v5-only)
|
||||
|
||||
## Core Testing Pattern
|
||||
|
||||
All xstate-audition functions follow this pattern:
|
||||
|
||||
1. Create an `Actor` using `createActor(logic)`
|
||||
2. Create a `Promise<T>` using a condition function (e.g., `runUntilDone(actor)`)
|
||||
3. **If** the actor needs external input, perform that operation _before_ awaiting
|
||||
4. `await` the `Promise<T>`
|
||||
5. Make assertions about `T`
|
||||
|
||||
```typescript
|
||||
import { 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);
|
||||
```
|
||||
|
||||
## Key Functions Overview
|
||||
|
||||
### `runUntil*()` vs `waitFor*()`
|
||||
|
||||
- **`runUntil*()`**: Starts the actor, waits for condition, then **stops** the actor
|
||||
- **`waitFor*()`**: Starts the actor, waits for condition, but **keeps** the actor running
|
||||
|
||||
Use `runUntil*()` for isolated tests, `waitFor*()` when you need to continue testing.
|
||||
|
||||
### Function Categories
|
||||
|
||||
1. **Completion Testing**: `runUntilDone()` - Wait for final state
|
||||
2. **Emission Testing**: `runUntilEmitted()` - Wait for event emissions
|
||||
3. **Transition Testing**: `runUntilTransition()` - Wait for specific state changes
|
||||
4. **Snapshot Testing**: `runUntilSnapshot()` - Wait for snapshot predicate
|
||||
5. **Spawn Testing**: `runUntilSpawn()` - Wait for child actor spawning
|
||||
6. **Event Testing**: `runUntilEventReceived()`, `runUntilEventSent()` - Inter-actor communication
|
||||
|
||||
## Common Testing Patterns
|
||||
|
||||
### Pattern 1: Testing Promise Actors
|
||||
|
||||
```typescript
|
||||
import { 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');
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Testing State Transitions
|
||||
|
||||
```typescript
|
||||
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');
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: Testing Event Emissions
|
||||
|
||||
```typescript
|
||||
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' });
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 4: Testing with External Input
|
||||
|
||||
When actors need events to satisfy conditions:
|
||||
|
||||
```typescript
|
||||
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');
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 5: Testing Snapshot Predicates
|
||||
|
||||
```typescript
|
||||
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');
|
||||
});
|
||||
```
|
||||
|
||||
## Using AuditionOptions
|
||||
|
||||
All functions ending in `With()` accept `AuditionOptions` as the second parameter:
|
||||
|
||||
```typescript
|
||||
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;
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### AuditionOptions Properties
|
||||
|
||||
- **`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.
|
||||
|
||||
## Currying Pattern
|
||||
|
||||
All functions are curried for reusability:
|
||||
|
||||
```typescript
|
||||
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');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
1. **Use currying for repeated patterns** - Reduce boilerplate in test suites
|
||||
2. **Set external input before awaiting** - Avoid race conditions
|
||||
3. **Use `waitFor*()` for multi-stage tests** - Keep actor alive for sequential assertions
|
||||
4. **Provide explicit type arguments** - Especially for `runUntilSpawn()` to get correct types
|
||||
5. **Use appropriate timeouts** - Set timeout less than test framework timeout
|
||||
6. **Test state transitions explicitly** - Use `runUntilTransition()` for clear intent
|
||||
7. **Use `runUntilSnapshot()` for complex conditions** - When multiple conditions must be met
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
1. **Don't await immediately for event-driven actors** - Setup promise, send event, then await
|
||||
2. **Don't use xstate-audition for unit testing** - Test guards and actions separately
|
||||
3. **Don't rely on default timeout** - Be explicit when tests are expected to be slow
|
||||
4. **Don't mix v4 and v5** - This library is v5-only
|
||||
5. **Don't forget type parameters** - Especially for spawned actors
|
||||
6. **Don't test UI directly** - Use framework-specific testing tools
|
||||
7. **Don't use string event types** - Use typed event objects for better type safety
|
||||
|
||||
## Testing Hierarchical Actors
|
||||
|
||||
When testing parent/child actor systems:
|
||||
|
||||
```typescript
|
||||
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'),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Integration with Test Frameworks
|
||||
|
||||
### Node.js test (built-in)
|
||||
|
||||
```typescript
|
||||
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');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Jest/Vitest
|
||||
|
||||
```typescript
|
||||
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();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
When tests fail or timeout:
|
||||
|
||||
1. **Use custom logger**: Pass `{ logger: console.log }` to see actor activity
|
||||
2. **Use inspector**: Pass `{ inspector: (event) => console.log(event) }` for detailed events
|
||||
3. **Increase timeout**: Start with longer timeout to identify if it's a timing issue
|
||||
4. **Check state IDs**: Ensure state IDs match (use machine ID prefix)
|
||||
5. **Verify event types**: Ensure event type strings match exactly
|
||||
6. **Test incrementally**: Break complex tests into smaller steps
|
||||
|
||||
```typescript
|
||||
import { runUntilSnapshotWith } from 'xstate-audition';
|
||||
|
||||
const snapshot = await runUntilSnapshotWith(
|
||||
actor,
|
||||
{
|
||||
logger: console.log,
|
||||
timeout: 5000,
|
||||
inspector: (event) => console.log('Inspector:', event),
|
||||
},
|
||||
(snapshot) => snapshot.matches('targetState'),
|
||||
);
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
For detailed API documentation, see the [references](./references/) directory:
|
||||
|
||||
- [Core Functions](./references/core-functions.md) - Detailed API for all main functions
|
||||
- [Options & Types](./references/options-types.md) - AuditionOptions and TypeScript types
|
||||
- [Testing Patterns](./references/testing-patterns.md) - Advanced patterns and examples
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Awaiting before sending required event**:
|
||||
|
||||
```typescript
|
||||
// ❌ 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;
|
||||
```
|
||||
|
||||
2. **Wrong state ID format**:
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - missing machine ID prefix
|
||||
await runUntilTransition(actor, 'idle', 'loading');
|
||||
|
||||
// ✅ CORRECT - include machine ID
|
||||
await runUntilTransition(actor, 'myMachine.idle', 'myMachine.loading');
|
||||
```
|
||||
|
||||
3. **Not providing type parameters for spawn**:
|
||||
|
||||
```typescript
|
||||
// ❌ 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:
|
||||
|
||||
- Start with simple `runUntilDone()` tests for basic actor behavior
|
||||
- Use specific condition functions (`runUntilTransition`, `runUntilEmitted`) for targeted tests
|
||||
- Leverage currying to reduce test boilerplate
|
||||
- Always consider timing - setup promises before triggering conditions
|
||||
- Use TypeScript types for better test reliability
|
||||
|
||||
Remember: xstate-audition excels at testing actor behavior and interactions. It complements (not replaces) unit testing of individual guards, actions, and services.
|
||||
766
skills/xstate-audition/references/core-functions.md
Normal file
766
skills/xstate-audition/references/core-functions.md
Normal file
@@ -0,0 +1,766 @@
|
||||
# xstate-audition Core Functions API Reference
|
||||
|
||||
Complete API reference for all xstate-audition testing functions.
|
||||
|
||||
## Function Naming Conventions
|
||||
|
||||
All functions follow consistent naming patterns:
|
||||
|
||||
- **`runUntil*()`**: Starts actor, waits for condition, **stops** actor
|
||||
- **`waitFor*()`**: Starts actor, waits for condition, **keeps** actor running
|
||||
- **`*With()`**: Accepts `AuditionOptions` as second parameter
|
||||
- **Without `With()`**: Uses default options (no `AuditionOptions` parameter)
|
||||
|
||||
## runUntilDone / waitForDone
|
||||
|
||||
Run a Promise Actor or State Machine Actor to completion.
|
||||
|
||||
### Signatures
|
||||
|
||||
```typescript
|
||||
function runUntilDone<TLogic extends AnyActorLogic>(
|
||||
actor: Actor<TLogic>,
|
||||
): Promise<OutputFrom<TLogic>>;
|
||||
|
||||
function runUntilDoneWith<TLogic extends AnyActorLogic>(
|
||||
actor: Actor<TLogic>,
|
||||
options: AuditionOptions,
|
||||
): Promise<OutputFrom<TLogic>>;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: Actor to run (Promise Actor or State Machine Actor)
|
||||
- **`options`** (With variant only): `AuditionOptions` object
|
||||
|
||||
### Returns
|
||||
|
||||
`Promise<Output>` - Resolves with actor output when reaching final state
|
||||
|
||||
### Behavior
|
||||
|
||||
- Starts the actor if not already started
|
||||
- Waits for actor to reach final state (`type: 'final'`)
|
||||
- Immediately stops the actor upon completion
|
||||
- Rejects if timeout expires before completion
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const promiseLogic = fromPromise<string>(async () => 'result');
|
||||
const actor = createActor(promiseLogic);
|
||||
|
||||
const output = await runUntilDone(actor);
|
||||
// output === 'result'
|
||||
|
||||
// With timeout
|
||||
await runUntilDoneWith(actor, { timeout: 100 });
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Similar to XState's `toPromise()` but with options support
|
||||
- No `waitForDone()` variant (would be identical to `runUntilDone()`)
|
||||
|
||||
---
|
||||
|
||||
## runUntilEmitted / waitForEmitted
|
||||
|
||||
Run a State Machine until it emits specified events.
|
||||
|
||||
### Signatures
|
||||
|
||||
```typescript
|
||||
function runUntilEmitted<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
emittedTypes: Array<EmittedFrom<TActor>['type']>,
|
||||
): Promise<Array<EmittedFrom<TActor>>>;
|
||||
|
||||
function runUntilEmittedWith<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions,
|
||||
emittedTypes: Array<EmittedFrom<TActor>['type']>,
|
||||
): Promise<Array<EmittedFrom<TActor>>>;
|
||||
|
||||
function waitForEmitted<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
emittedTypes: Array<EmittedFrom<TActor>['type']>,
|
||||
): Promise<Array<EmittedFrom<TActor>>>;
|
||||
|
||||
function waitForEmittedWith<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions,
|
||||
emittedTypes: Array<EmittedFrom<TActor>['type']>,
|
||||
): Promise<Array<EmittedFrom<TActor>>>;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: State Machine Actor
|
||||
- **`options`** (With variant only): `AuditionOptions` object
|
||||
- **`emittedTypes`**: Array of event type strings to wait for
|
||||
|
||||
### Returns
|
||||
|
||||
`Promise<Event[]>` - Resolves with array of emitted events (in order emitted)
|
||||
|
||||
### Behavior
|
||||
|
||||
- Starts the actor if not already started
|
||||
- Waits for all specified event types to be emitted (via `emit()` action)
|
||||
- `runUntil*` stops actor after all events emitted
|
||||
- `waitFor*` keeps actor running after events emitted
|
||||
- Order of events in array matches emission order
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const machine = setup({
|
||||
types: {
|
||||
emitted: {} as { type: 'READY' } | { type: 'DONE'; value: string },
|
||||
},
|
||||
}).createMachine({
|
||||
entry: [emit({ type: 'READY' }), emit({ type: 'DONE', value: 'success' })],
|
||||
});
|
||||
|
||||
const actor = createActor(machine);
|
||||
const [readyEvent, doneEvent] = await runUntilEmitted(actor, ['READY', 'DONE']);
|
||||
// readyEvent.type === 'READY'
|
||||
// doneEvent.type === 'DONE', doneEvent.value === 'success'
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Only applies to events emitted via XState's event emitter API (`emit()`)
|
||||
- Does NOT track events sent between actors (use `runUntilEventSent/Received`)
|
||||
- Events must be emitted in any order, not necessarily the specified order
|
||||
|
||||
### Currying
|
||||
|
||||
```typescript
|
||||
// Curry with actor
|
||||
const runUntilEmit = runUntilEmitted(actor);
|
||||
const events = await runUntilEmit(['READY', 'DONE']);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## runUntilTransition / waitForTransition
|
||||
|
||||
Run a State Machine until it transitions from one state to another.
|
||||
|
||||
### Signatures
|
||||
|
||||
```typescript
|
||||
function runUntilTransition<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
fromStateId: string,
|
||||
toStateId: string,
|
||||
): Promise<void>;
|
||||
|
||||
function runUntilTransitionWith<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions,
|
||||
fromStateId: string,
|
||||
toStateId: string,
|
||||
): Promise<void>;
|
||||
|
||||
function waitForTransition<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
fromStateId: string,
|
||||
toStateId: string,
|
||||
): Promise<void>;
|
||||
|
||||
function waitForTransitionWith<TActor extends AnyStateMachineActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions,
|
||||
fromStateId: string,
|
||||
toStateId: string,
|
||||
): Promise<void>;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: State Machine Actor
|
||||
- **`options`** (With variant only): `AuditionOptions` object
|
||||
- **`fromStateId`**: Full state ID (including machine ID prefix)
|
||||
- **`toStateId`**: Full state ID (including machine ID prefix)
|
||||
|
||||
### Returns
|
||||
|
||||
`Promise<void>` - Resolves when transition occurs
|
||||
|
||||
### Behavior
|
||||
|
||||
- Starts the actor if not already started
|
||||
- Waits for actor to be in state `fromStateId`
|
||||
- Then waits for transition to state `toStateId`
|
||||
- `runUntil*` stops actor after transition
|
||||
- `waitFor*` keeps actor running after transition
|
||||
- Rejects if transition doesn't occur within timeout
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const machine = createMachine({
|
||||
id: 'fetch',
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
on: { FETCH: 'loading' },
|
||||
},
|
||||
loading: {
|
||||
on: { SUCCESS: 'success' },
|
||||
},
|
||||
success: {},
|
||||
},
|
||||
});
|
||||
|
||||
const actor = createActor(machine);
|
||||
|
||||
// Setup promise
|
||||
const promise = runUntilTransition(actor, 'fetch.idle', 'fetch.loading');
|
||||
|
||||
// Send event
|
||||
actor.send({ type: 'FETCH' });
|
||||
|
||||
// Wait for transition
|
||||
await promise;
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **State IDs must include machine ID prefix**: `'machineId.stateId'`
|
||||
- If no machine ID provided in `createMachine()`, default is `'(machine)'`
|
||||
- Transition must be direct (from → to), not via intermediate states
|
||||
- Useful for testing explicit transition paths
|
||||
|
||||
### Currying
|
||||
|
||||
```typescript
|
||||
// Curry with actor and fromState
|
||||
const runFromIdle = runUntilTransition(actor, 'fetch.idle');
|
||||
await runFromIdle('fetch.loading');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## runUntilSnapshot / waitForSnapshot
|
||||
|
||||
Run an Actor until its snapshot satisfies a predicate.
|
||||
|
||||
### Signatures
|
||||
|
||||
```typescript
|
||||
function runUntilSnapshot<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
predicate: (snapshot: SnapshotFrom<TActor>) => boolean,
|
||||
): Promise<SnapshotFrom<TActor>>;
|
||||
|
||||
function runUntilSnapshotWith<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions,
|
||||
predicate: (snapshot: SnapshotFrom<TActor>) => boolean,
|
||||
): Promise<SnapshotFrom<TActor>>;
|
||||
|
||||
function waitForSnapshot<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
predicate: (snapshot: SnapshotFrom<TActor>) => boolean,
|
||||
): Promise<SnapshotFrom<TActor>>;
|
||||
|
||||
function waitForSnapshotWith<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions,
|
||||
predicate: (snapshot: SnapshotFrom<TActor>) => boolean,
|
||||
): Promise<SnapshotFrom<TActor>>;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: Any Actor type
|
||||
- **`options`** (With variant only): `AuditionOptions` object
|
||||
- **`predicate`**: Function that tests snapshot conditions
|
||||
|
||||
### Returns
|
||||
|
||||
`Promise<Snapshot>` - Resolves with snapshot that satisfied predicate
|
||||
|
||||
### Behavior
|
||||
|
||||
- Starts the actor if not already started
|
||||
- Evaluates predicate on each snapshot change
|
||||
- Resolves when predicate returns `true`
|
||||
- `runUntil*` stops actor after predicate satisfied
|
||||
- `waitFor*` keeps actor running after predicate satisfied
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const machine = setup({
|
||||
types: {
|
||||
context: {} as { count: number; status: string },
|
||||
},
|
||||
}).createMachine({
|
||||
context: { count: 0, status: 'idle' },
|
||||
on: {
|
||||
INCREMENT: {
|
||||
actions: assign({ count: ({ context }) => context.count + 1 }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const actor = createActor(machine);
|
||||
|
||||
const promise = runUntilSnapshot(
|
||||
actor,
|
||||
(snapshot) => snapshot.context.count >= 3,
|
||||
);
|
||||
|
||||
actor.send({ type: 'INCREMENT' });
|
||||
actor.send({ type: 'INCREMENT' });
|
||||
actor.send({ type: 'INCREMENT' });
|
||||
|
||||
const snapshot = await promise;
|
||||
// snapshot.context.count === 3
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Testing complex context conditions
|
||||
- Waiting for computed values
|
||||
- Combining multiple state/context checks
|
||||
- Testing conditions not tied to specific states
|
||||
|
||||
### Notes
|
||||
|
||||
- Similar to XState's `waitFor()` but with stop behavior
|
||||
- Predicate called on every snapshot change
|
||||
- Keep predicates performant (called frequently)
|
||||
|
||||
---
|
||||
|
||||
## runUntilSpawn / waitForSpawn
|
||||
|
||||
Run a State Machine until it spawns a child actor.
|
||||
|
||||
### Signatures
|
||||
|
||||
```typescript
|
||||
function runUntilSpawn<TLogic extends AnyActorLogic = AnyActorLogic>(
|
||||
actor: AnyStateMachineActor,
|
||||
childId: string | RegExp,
|
||||
): Promise<ActorRefFrom<TLogic>>;
|
||||
|
||||
function runUntilSpawnWith<TLogic extends AnyActorLogic = AnyActorLogic>(
|
||||
actor: AnyStateMachineActor,
|
||||
options: AuditionOptions,
|
||||
childId: string | RegExp,
|
||||
): Promise<ActorRefFrom<TLogic>>;
|
||||
|
||||
function waitForSpawn<TLogic extends AnyActorLogic = AnyActorLogic>(
|
||||
actor: AnyStateMachineActor,
|
||||
childId: string | RegExp,
|
||||
): Promise<ActorRefFrom<TLogic>>;
|
||||
|
||||
function waitForSpawnWith<TLogic extends AnyActorLogic = AnyActorLogic>(
|
||||
actor: AnyStateMachineActor,
|
||||
options: AuditionOptions,
|
||||
childId: string | RegExp,
|
||||
): Promise<ActorRefFrom<TLogic>>;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: State Machine Actor (parent)
|
||||
- **`options`** (With variant only): `AuditionOptions` object
|
||||
- **`childId`**: String or RegExp to match spawned child ID
|
||||
|
||||
### Type Parameters
|
||||
|
||||
- **`TLogic`**: Type of the spawned child's logic (for type safety)
|
||||
|
||||
### Returns
|
||||
|
||||
`Promise<ActorRef>` - Resolves with reference to spawned child actor
|
||||
|
||||
### Behavior
|
||||
|
||||
- Starts the actor if not already started
|
||||
- Waits for any actor in the system to spawn child with matching ID
|
||||
- `runUntil*` stops parent actor after spawn detected
|
||||
- `waitFor*` keeps parent actor running after spawn detected
|
||||
- Can match child spawned by parent or any descendant
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const childLogic = fromPromise<string>(async () => 'child result');
|
||||
|
||||
const parentMachine = setup({
|
||||
actors: { child: childLogic },
|
||||
}).createMachine({
|
||||
on: {
|
||||
SPAWN: {
|
||||
actions: spawnChild('child', { id: 'myChild' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const actor = createActor(parentMachine);
|
||||
|
||||
// Provide explicit type for child
|
||||
const promise = waitForSpawn<typeof childLogic>(actor, 'myChild');
|
||||
|
||||
actor.send({ type: 'SPAWN' });
|
||||
|
||||
const childRef = await promise;
|
||||
// childRef.id === 'myChild'
|
||||
// childRef has correct typing based on childLogic
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Always provide type parameter** for proper TypeScript typing
|
||||
- Without type parameter, returns `AnyActorRef` (not useful)
|
||||
- Cannot specify which parent should spawn the child
|
||||
- Useful for testing actor hierarchies and dynamic spawning
|
||||
|
||||
### Currying
|
||||
|
||||
```typescript
|
||||
const waitForChild = waitForSpawn<typeof childLogic>(actor);
|
||||
const childRef = await waitForChild('myChild');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## runUntilEventReceived / waitForEventReceived
|
||||
|
||||
Run an Actor until it receives specified events.
|
||||
|
||||
### Signatures
|
||||
|
||||
```typescript
|
||||
function runUntilEventReceived<TActor extends EventReceivingActor>(
|
||||
actor: TActor,
|
||||
eventTypes: Array<EventReceivedFrom<TActor>['type']>,
|
||||
): Promise<Array<EventReceivedFrom<TActor>>>;
|
||||
|
||||
function runUntilEventReceivedWith<TActor extends EventReceivingActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions & { otherActorId?: string | RegExp },
|
||||
eventTypes: Array<EventReceivedFrom<TActor>['type']>,
|
||||
): Promise<Array<EventReceivedFrom<TActor>>>;
|
||||
|
||||
function waitForEventReceived<TActor extends EventReceivingActor>(
|
||||
actor: TActor,
|
||||
eventTypes: Array<EventReceivedFrom<TActor>['type']>,
|
||||
): Promise<Array<EventReceivedFrom<TActor>>>;
|
||||
|
||||
function waitForEventReceivedWith<TActor extends EventReceivingActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions & { otherActorId?: string | RegExp },
|
||||
eventTypes: Array<EventReceivedFrom<TActor>['type']>,
|
||||
): Promise<Array<EventReceivedFrom<TActor>>>;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: State Machine, Callback, or Transition Actor
|
||||
- **`options`** (With variant only): `AuditionOptions` with optional `otherActorId`
|
||||
- **`eventTypes`**: Array of event type strings to wait for
|
||||
|
||||
### Options Extension
|
||||
|
||||
- **`otherActorId`**: Filter events to only those received FROM actor with this ID
|
||||
|
||||
### Returns
|
||||
|
||||
`Promise<Event[]>` - Resolves with array of received events
|
||||
|
||||
### Behavior
|
||||
|
||||
- Starts the actor if not already started
|
||||
- Waits for all specified event types to be received
|
||||
- `runUntil*` stops actor after all events received
|
||||
- `waitFor*` keeps actor running after events received
|
||||
- If `otherActorId` provided, only counts events from that sender
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const machine = setup({
|
||||
types: {
|
||||
events: {} as { type: 'PING' } | { type: 'PONG' },
|
||||
},
|
||||
}).createMachine({
|
||||
on: {
|
||||
PING: { actions: /* respond */ },
|
||||
PONG: { actions: /* respond */ },
|
||||
},
|
||||
});
|
||||
|
||||
const actor = createActor(machine);
|
||||
const promise = runUntilEventReceived(actor, ['PING', 'PONG']);
|
||||
|
||||
actor.send({ type: 'PING' });
|
||||
actor.send({ type: 'PONG' });
|
||||
|
||||
const [pingEvent, pongEvent] = await promise;
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Testing event handling
|
||||
- Verifying inter-actor communication
|
||||
- Testing event sources and filters
|
||||
|
||||
---
|
||||
|
||||
## runUntilEventSent / waitForEventSent
|
||||
|
||||
Run an Actor until it sends specified events to other actors.
|
||||
|
||||
### Signatures
|
||||
|
||||
```typescript
|
||||
function runUntilEventSent<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
eventTypes: string[],
|
||||
): Promise<Array<EventObject>>;
|
||||
|
||||
function runUntilEventSentWith<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions & { otherActorId?: string | RegExp },
|
||||
eventTypes: string[],
|
||||
): Promise<Array<EventObject>>;
|
||||
|
||||
function waitForEventSent<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
eventTypes: string[],
|
||||
): Promise<Array<EventObject>>;
|
||||
|
||||
function waitForEventSentWith<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
options: AuditionOptions & { otherActorId?: string | RegExp },
|
||||
eventTypes: string[],
|
||||
): Promise<Array<EventObject>>;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: Any Actor type
|
||||
- **`options`** (With variant only): `AuditionOptions` with optional `otherActorId`
|
||||
- **`eventTypes`**: Array of event type strings to wait for
|
||||
|
||||
### Options Extension
|
||||
|
||||
- **`otherActorId`**: Filter events to only those sent TO actor with this ID
|
||||
|
||||
### Returns
|
||||
|
||||
`Promise<Event[]>` - Resolves with array of sent events
|
||||
|
||||
### Behavior
|
||||
|
||||
- Starts the actor if not already started
|
||||
- Waits for actor to send all specified event types
|
||||
- `runUntil*` stops actor after all events sent
|
||||
- `waitFor*` keeps actor running after events sent
|
||||
- If `otherActorId` provided, only counts events to that recipient
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const childLogic = fromPromise<string>(async () => 'done');
|
||||
|
||||
const parentMachine = setup({
|
||||
actors: { child: childLogic },
|
||||
}).createMachine({
|
||||
invoke: {
|
||||
id: 'child',
|
||||
src: 'child',
|
||||
onDone: {
|
||||
actions: sendTo('child', { type: 'COMPLETE' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const actor = createActor(parentMachine);
|
||||
const [sentEvent] = await runUntilEventSent(actor, ['COMPLETE']);
|
||||
// sentEvent.type === 'COMPLETE'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## createActorFromLogic
|
||||
|
||||
Curried function to create actors from logic.
|
||||
|
||||
### Signature
|
||||
|
||||
```typescript
|
||||
function createActorFromLogic<TLogic extends AnyActorLogic>(
|
||||
logic: TLogic,
|
||||
options?: ActorOptions<TLogic>,
|
||||
): Actor<TLogic>;
|
||||
|
||||
// Curried
|
||||
const createActor = createActorFromLogic(logic);
|
||||
const actor = createActor(options);
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`logic`**: Actor logic (machine, promise logic, etc.)
|
||||
- **`options`**: XState actor options (input, inspect, logger, etc.)
|
||||
|
||||
### Returns
|
||||
|
||||
Curried function or Actor
|
||||
|
||||
### Use Case
|
||||
|
||||
Reduce boilerplate when testing same logic with different inputs:
|
||||
|
||||
```typescript
|
||||
const createActor = createActorFromLogic(myLogic);
|
||||
|
||||
it('should work with input A', async () => {
|
||||
const actor = createActor({ input: 'A' });
|
||||
await runUntilDone(actor);
|
||||
});
|
||||
|
||||
it('should work with input B', async () => {
|
||||
const actor = createActor({ input: 'B' });
|
||||
await runUntilDone(actor);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## createActorWith
|
||||
|
||||
Curried function to create actors with options.
|
||||
|
||||
### Signature
|
||||
|
||||
```typescript
|
||||
function createActorWith<TLogic extends AnyActorLogic>(
|
||||
options: ActorOptions<TLogic>,
|
||||
logic: TLogic,
|
||||
): Actor<TLogic>;
|
||||
|
||||
// Curried
|
||||
const createActor = createActorWith(options);
|
||||
const actor = createActor(logic);
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`options`**: XState actor options (input, inspect, logger, etc.)
|
||||
- **`logic`**: Actor logic (machine, promise logic, etc.)
|
||||
|
||||
### Returns
|
||||
|
||||
Curried function or Actor
|
||||
|
||||
### Use Case
|
||||
|
||||
Reduce boilerplate when testing different logic with same input:
|
||||
|
||||
```typescript
|
||||
const createActor = createActorWith({ input: 'testInput' });
|
||||
|
||||
it('should work with machineA', async () => {
|
||||
const actor = createActor(machineA);
|
||||
await runUntilDone(actor);
|
||||
});
|
||||
|
||||
it('should work with machineB', async () => {
|
||||
const actor = createActor(machineB);
|
||||
await runUntilDone(actor);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## patchActor
|
||||
|
||||
Modify an Actor for use with xstate-audition.
|
||||
|
||||
### Signature
|
||||
|
||||
```typescript
|
||||
function patchActor<TActor extends AnyActor>(
|
||||
actor: TActor,
|
||||
options?: AuditionOptions,
|
||||
): TActor;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: Actor to modify
|
||||
- **`options`**: Optional `AuditionOptions`
|
||||
|
||||
### Returns
|
||||
|
||||
Modified actor (same reference)
|
||||
|
||||
### Notes
|
||||
|
||||
- Used internally by all other functions
|
||||
- Mutates the actor (adds internal tracking)
|
||||
- Generally not needed in user code
|
||||
- All other xstate-audition functions call this automatically
|
||||
|
||||
---
|
||||
|
||||
## unpatchActor
|
||||
|
||||
Revert modifications made by xstate-audition.
|
||||
|
||||
### Signature
|
||||
|
||||
```typescript
|
||||
function unpatchActor<TActor extends AnyActor>(actor: TActor): TActor;
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- **`actor`**: Actor to revert
|
||||
|
||||
### Returns
|
||||
|
||||
Reverted actor (same reference)
|
||||
|
||||
### Notes
|
||||
|
||||
- **Experimental** - may be removed in future
|
||||
- Undoes internal mutations from `patchActor()`
|
||||
- No-op if actor was never patched
|
||||
- Rarely needed in practice
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Function | Condition | Stops Actor | Keeps Running | Options Support |
|
||||
| ----------------------- | ------------------ | ----------- | ------------- | --------------- |
|
||||
| `runUntilDone` | Final state | Yes | No | With variant |
|
||||
| `runUntilEmitted` | Events emitted | Yes | No | With variant |
|
||||
| `waitForEmitted` | Events emitted | No | Yes | With variant |
|
||||
| `runUntilTransition` | State transition | Yes | No | With variant |
|
||||
| `waitForTransition` | State transition | No | Yes | With variant |
|
||||
| `runUntilSnapshot` | Snapshot predicate | Yes | No | With variant |
|
||||
| `waitForSnapshot` | Snapshot predicate | No | Yes | With variant |
|
||||
| `runUntilSpawn` | Child spawned | Yes | No | With variant |
|
||||
| `waitForSpawn` | Child spawned | No | Yes | With variant |
|
||||
| `runUntilEventReceived` | Events received | Yes | No | With variant |
|
||||
| `waitForEventReceived` | Events received | No | Yes | With variant |
|
||||
| `runUntilEventSent` | Events sent | Yes | No | With variant |
|
||||
| `waitForEventSent` | Events sent | No | Yes | With variant |
|
||||
| `createActorFromLogic` | N/A (utility) | N/A | N/A | No |
|
||||
| `createActorWith` | N/A (utility) | N/A | N/A | No |
|
||||
| `patchActor` | N/A (internal) | N/A | N/A | Yes |
|
||||
| `unpatchActor` | N/A (internal) | N/A | N/A | No |
|
||||
545
skills/xstate-audition/references/options-types.md
Normal file
545
skills/xstate-audition/references/options-types.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# xstate-audition Options & Types Reference
|
||||
|
||||
Complete reference for AuditionOptions and TypeScript types used throughout xstate-audition.
|
||||
|
||||
## AuditionOptions
|
||||
|
||||
Configuration object accepted by all functions ending in `With()`.
|
||||
|
||||
### Type Definition
|
||||
|
||||
```typescript
|
||||
interface AuditionOptions {
|
||||
/**
|
||||
* Inspector callback or observer to attach to actor Will not overwrite
|
||||
* existing inspectors
|
||||
*/
|
||||
inspector?: ((event: InspectionEvent) => void) | Observer<InspectionEvent>;
|
||||
|
||||
/**
|
||||
* Custom logger function Default: no-op (no logging) XState default:
|
||||
* console.log
|
||||
*/
|
||||
logger?: (...args: any[]) => void;
|
||||
|
||||
/**
|
||||
* Maximum milliseconds to wait for condition Default: 1000 Set to 0,
|
||||
* negative, or Infinity to disable
|
||||
*/
|
||||
timeout?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### `inspector`
|
||||
|
||||
**Type**: `((event: InspectionEvent) => void) | Observer<InspectionEvent>`
|
||||
**Default**: `undefined`
|
||||
**Optional**: Yes
|
||||
|
||||
Attaches a custom inspector to the actor for debugging and monitoring.
|
||||
|
||||
**Callback signature**:
|
||||
|
||||
```typescript
|
||||
(event: InspectionEvent) => void;
|
||||
```
|
||||
|
||||
**Observer signature**:
|
||||
|
||||
```typescript
|
||||
interface Observer<T> {
|
||||
next: (value: T) => void;
|
||||
error?: (error: any) => void;
|
||||
complete?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
|
||||
- Merges with any existing inspectors (doesn't replace)
|
||||
- Called for every inspection event from the actor
|
||||
- Useful for debugging failed tests
|
||||
- Similar to `inspect` option in `createActor()`
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
await runUntilDoneWith(actor, {
|
||||
inspector: (event) => {
|
||||
console.log('Inspection event:', event.type);
|
||||
},
|
||||
});
|
||||
|
||||
// Or with observer
|
||||
await runUntilDoneWith(actor, {
|
||||
inspector: {
|
||||
next: (event) => console.log('Event:', event),
|
||||
error: (err) => console.error('Error:', err),
|
||||
complete: () => console.log('Complete'),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**InspectionEvent types**:
|
||||
|
||||
```typescript
|
||||
type InspectionEvent =
|
||||
| { type: 'actor.snapshot'; actorRef: AnyActorRef; snapshot: Snapshot }
|
||||
| { type: 'actor.event'; actorRef: AnyActorRef; event: EventObject }
|
||||
| { type: 'actor.state'; actorRef: AnyActorRef; state: StateValue };
|
||||
// ... and more
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `logger`
|
||||
|
||||
**Type**: `(...args: any[]) => void`
|
||||
**Default**: `() => {}` (no-op)
|
||||
**Optional**: Yes
|
||||
|
||||
Sets a custom logger for the actor and all its children.
|
||||
|
||||
**Behavior**:
|
||||
|
||||
- Replaces actor's default logger
|
||||
- Cascades to all child actors
|
||||
- XState's default logger is `console.log`
|
||||
- xstate-audition's default is no-op (silent)
|
||||
- Useful for debugging test failures
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
await runUntilDoneWith(actor, {
|
||||
logger: console.log, // Use console.log
|
||||
});
|
||||
|
||||
await runUntilDoneWith(actor, {
|
||||
logger: (...args) => {
|
||||
// Custom formatting
|
||||
console.log('[XState]', ...args);
|
||||
},
|
||||
});
|
||||
|
||||
// With test framework logger
|
||||
await runUntilDoneWith(actor, {
|
||||
logger: vi.fn(), // Vitest mock
|
||||
});
|
||||
```
|
||||
|
||||
**What gets logged**:
|
||||
|
||||
- State transitions
|
||||
- Action executions
|
||||
- Event handling
|
||||
- Context updates
|
||||
- Guard evaluations
|
||||
|
||||
---
|
||||
|
||||
#### `timeout`
|
||||
|
||||
**Type**: `number`
|
||||
**Default**: `1000` (milliseconds)
|
||||
**Optional**: Yes
|
||||
|
||||
Maximum time to wait for condition before rejecting the promise.
|
||||
|
||||
**Behavior**:
|
||||
|
||||
- Measured in milliseconds
|
||||
- Default is 1000ms (1 second)
|
||||
- Set to `0`, negative number, or `Infinity` to disable timeout
|
||||
- Should be **less than your test framework's timeout**
|
||||
- Timeout error message includes elapsed time
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// 5 second timeout
|
||||
await runUntilDoneWith(actor, { timeout: 5000 });
|
||||
|
||||
// No timeout
|
||||
await runUntilDoneWith(actor, { timeout: 0 });
|
||||
await runUntilDoneWith(actor, { timeout: Infinity });
|
||||
|
||||
// Will reject after 100ms
|
||||
await assert.rejects(
|
||||
runUntilDoneWith(slowActor, { timeout: 100 }),
|
||||
(err: Error) => {
|
||||
assert.match(err.message, /did not complete in 100ms/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
**Timeout error format**:
|
||||
|
||||
```typescript
|
||||
// Error message pattern
|
||||
`Actor did not complete in ${timeout}ms`;
|
||||
`Actor did not emit [${types}] in ${timeout}ms`;
|
||||
`Actor did not transition from ${from} to ${to} in ${timeout}ms`;
|
||||
// etc.
|
||||
```
|
||||
|
||||
**Best practices**:
|
||||
|
||||
1. Set explicit timeouts for slow operations
|
||||
2. Keep timeout < test framework timeout
|
||||
3. Use longer timeouts in CI environments
|
||||
4. Disable timeout for debugging (set to `Infinity`)
|
||||
|
||||
---
|
||||
|
||||
### Extended Options
|
||||
|
||||
Some functions accept additional options beyond `AuditionOptions`.
|
||||
|
||||
#### Event Filtering Options
|
||||
|
||||
Used by `runUntilEventReceivedWith` and `runUntilEventSentWith`:
|
||||
|
||||
```typescript
|
||||
interface EventFilterOptions extends AuditionOptions {
|
||||
/**
|
||||
* Filter events by source/target actor ID String: exact match RegExp: pattern
|
||||
* match
|
||||
*/
|
||||
otherActorId?: string | RegExp;
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// Only events received FROM actor with ID 'child'
|
||||
await runUntilEventReceivedWith(actor, { otherActorId: 'child' }, ['PING']);
|
||||
|
||||
// Events from any actor matching pattern
|
||||
await runUntilEventReceivedWith(actor, { otherActorId: /^child-\d+$/ }, [
|
||||
'PING',
|
||||
]);
|
||||
|
||||
// Only events sent TO actor with ID 'parent'
|
||||
await runUntilEventSentWith(actor, { otherActorId: 'parent' }, ['RESPONSE']);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
### Curried Function Types
|
||||
|
||||
xstate-audition exports types for curried function variants.
|
||||
|
||||
#### Example: `CurryEmittedP1`
|
||||
|
||||
Type for partially applied `runUntilEmitted()`:
|
||||
|
||||
```typescript
|
||||
type CurryEmittedP1<TActor extends AnyStateMachineActor> = (
|
||||
emittedTypes: Array<EmittedFrom<TActor>['type']>,
|
||||
) => Promise<Array<EmittedFrom<TActor>>>;
|
||||
|
||||
// Usage
|
||||
let actor: Actor<typeof machine>;
|
||||
let runUntilEmit: CurryEmittedP1<typeof actor>;
|
||||
|
||||
runUntilEmit = runUntilEmitted(actor);
|
||||
const events = await runUntilEmit(['EVENT1', 'EVENT2']);
|
||||
```
|
||||
|
||||
#### Example: `CurryTransitionP2`
|
||||
|
||||
Type for partially applied `runUntilTransition()`:
|
||||
|
||||
```typescript
|
||||
type CurryTransitionP2<TActor extends AnyStateMachineActor> = (
|
||||
toStateId: string,
|
||||
) => Promise<void>;
|
||||
|
||||
// Usage
|
||||
let actor: Actor<typeof machine>;
|
||||
let runFromIdle: CurryTransitionP2<typeof actor>;
|
||||
|
||||
runFromIdle = runUntilTransition(actor, 'machine.idle');
|
||||
await runFromIdle('machine.loading');
|
||||
```
|
||||
|
||||
### Actor Type Constraints
|
||||
|
||||
Types that constrain which actors can be used with specific functions.
|
||||
|
||||
#### `AnyStateMachineActor`
|
||||
|
||||
```typescript
|
||||
type AnyStateMachineActor = Actor<
|
||||
StateMachine<any, any, any, any, any, any, any, any, any>
|
||||
>;
|
||||
```
|
||||
|
||||
Used by: `runUntilEmitted`, `runUntilTransition`, `runUntilSpawn`
|
||||
|
||||
#### `EventReceivingActor`
|
||||
|
||||
```typescript
|
||||
type EventReceivingActor =
|
||||
| AnyStateMachineActor
|
||||
| CallbackActor
|
||||
| TransitionActor;
|
||||
```
|
||||
|
||||
Used by: `runUntilEventReceived`, `waitForEventReceived`
|
||||
|
||||
#### `AnyActor`
|
||||
|
||||
```typescript
|
||||
type AnyActor = Actor<AnyActorLogic>;
|
||||
```
|
||||
|
||||
Used by: `runUntilSnapshot`, `runUntilEventSent`, and most other functions
|
||||
|
||||
### Utility Types from XState
|
||||
|
||||
xstate-audition leverages XState's utility types:
|
||||
|
||||
#### `SnapshotFrom<TActor>`
|
||||
|
||||
Extracts snapshot type from actor:
|
||||
|
||||
```typescript
|
||||
import { SnapshotFrom } from 'xstate';
|
||||
|
||||
const machine = createMachine({...});
|
||||
type MachineSnapshot = SnapshotFrom<typeof machine>;
|
||||
```
|
||||
|
||||
#### `OutputFrom<TLogic>`
|
||||
|
||||
Extracts output type from logic:
|
||||
|
||||
```typescript
|
||||
import { OutputFrom } from 'xstate';
|
||||
|
||||
const promiseLogic = fromPromise<string>(async () => 'result');
|
||||
type Output = OutputFrom<typeof promiseLogic>; // string
|
||||
```
|
||||
|
||||
#### `EmittedFrom<TActor>`
|
||||
|
||||
Extracts emitted event types from actor:
|
||||
|
||||
```typescript
|
||||
import { EmittedFrom } from 'xstate';
|
||||
|
||||
const machine = setup({
|
||||
types: {
|
||||
emitted: {} as { type: 'READY' } | { type: 'DONE' },
|
||||
},
|
||||
}).createMachine({...});
|
||||
|
||||
type Emitted = EmittedFrom<typeof machine>;
|
||||
// { type: 'READY' } | { type: 'DONE' }
|
||||
```
|
||||
|
||||
#### `ActorRefFrom<TLogic>`
|
||||
|
||||
Extracts actor reference type from logic:
|
||||
|
||||
```typescript
|
||||
import { ActorRefFrom } from 'xstate';
|
||||
|
||||
const childLogic = fromPromise<string>(async () => 'done');
|
||||
type ChildRef = ActorRefFrom<typeof childLogic>;
|
||||
|
||||
const child = await runUntilSpawn<typeof childLogic>(parent, 'childId');
|
||||
// child has type ActorRefFrom<typeof childLogic>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Parameter Patterns
|
||||
|
||||
### Providing Explicit Type Parameters
|
||||
|
||||
Some functions benefit from explicit type parameters for better type inference.
|
||||
|
||||
#### `runUntilSpawn` with Type Parameter
|
||||
|
||||
**Why needed**: Child actor type cannot be inferred from ID alone
|
||||
|
||||
```typescript
|
||||
// ❌ Without type parameter - returns AnyActorRef
|
||||
const child = await runUntilSpawn(parent, 'childId');
|
||||
// child: AnyActorRef (not useful)
|
||||
|
||||
// ✅ With type parameter - returns properly typed ActorRef
|
||||
const childLogic = fromPromise<string>(async () => 'result');
|
||||
const child = await runUntilSpawn<typeof childLogic>(parent, 'childId');
|
||||
// child: ActorRefFrom<typeof childLogic>
|
||||
// child.getSnapshot() returns Promise<string>
|
||||
```
|
||||
|
||||
#### Generic Actor Testing
|
||||
|
||||
```typescript
|
||||
function testActor<TLogic extends AnyActorLogic>(
|
||||
logic: TLogic,
|
||||
input: InputFrom<TLogic>,
|
||||
) {
|
||||
return async () => {
|
||||
const actor = createActor(logic, { input });
|
||||
const output = await runUntilDone(actor);
|
||||
return output;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Safety Best Practices
|
||||
|
||||
### 1. Use `setup()` for Type Inference
|
||||
|
||||
```typescript
|
||||
// ✅ Good - full type inference
|
||||
const machine = setup({
|
||||
types: {
|
||||
context: {} as { count: number },
|
||||
events: {} as { type: 'INC' } | { type: 'DEC' },
|
||||
emitted: {} as { type: 'CHANGED'; count: number },
|
||||
},
|
||||
}).createMachine({...});
|
||||
|
||||
// Now all xstate-audition functions have proper types
|
||||
const [event] = await runUntilEmitted(machine, ['CHANGED']);
|
||||
// event.type is 'CHANGED'
|
||||
// event.count is number
|
||||
```
|
||||
|
||||
### 2. Explicitly Type Actors in Tests
|
||||
|
||||
```typescript
|
||||
// ✅ Good - explicit typing
|
||||
let actor: Actor<typeof machine>;
|
||||
|
||||
beforeEach(() => {
|
||||
actor = createActor(machine);
|
||||
});
|
||||
|
||||
// Now all uses of actor have correct type
|
||||
```
|
||||
|
||||
### 3. Use Curried Types
|
||||
|
||||
```typescript
|
||||
// ✅ Good - type curried functions
|
||||
let runFromIdle: CurryTransitionP2<typeof actor>;
|
||||
|
||||
beforeEach(() => {
|
||||
actor = createActor(machine);
|
||||
runFromIdle = runUntilTransition(actor, 'machine.idle');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Type Errors and Solutions
|
||||
|
||||
### Error: "Type 'string' is not assignable to type 'never'"
|
||||
|
||||
**Cause**: Event or emitted types not properly defined
|
||||
|
||||
**Solution**:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - no types defined
|
||||
const machine = createMachine({...});
|
||||
|
||||
// ✅ Fixed - define types with setup()
|
||||
const machine = setup({
|
||||
types: {
|
||||
events: {} as { type: 'FETCH' } | { type: 'SUCCESS' },
|
||||
},
|
||||
}).createMachine({...});
|
||||
```
|
||||
|
||||
### Error: "Property 'X' does not exist on type 'AnyActorRef'"
|
||||
|
||||
**Cause**: Missing type parameter on `runUntilSpawn`
|
||||
|
||||
**Solution**:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - no type parameter
|
||||
const child = await runUntilSpawn(parent, 'childId');
|
||||
|
||||
// ✅ Fixed - provide type parameter
|
||||
const child = await runUntilSpawn<typeof childLogic>(parent, 'childId');
|
||||
```
|
||||
|
||||
### Error: "Argument of type 'Promise<...>' is not assignable"
|
||||
|
||||
**Cause**: Forgetting to `await` the promise
|
||||
|
||||
**Solution**:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - not awaiting
|
||||
const result = runUntilDone(actor);
|
||||
|
||||
// ✅ Fixed - await the promise
|
||||
const result = await runUntilDone(actor);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Defaults
|
||||
|
||||
Quick reference for all default values:
|
||||
|
||||
| Option | Default Value | Type |
|
||||
| -------------- | ----------------------- | ---------------------- |
|
||||
| `timeout` | `1000` (ms) | `number` |
|
||||
| `logger` | `() => {}` (no-op) | `Function` |
|
||||
| `inspector` | `undefined` (none) | `Function \| Observer` |
|
||||
| `otherActorId` | `undefined` (any actor) | `string \| RegExp` |
|
||||
|
||||
---
|
||||
|
||||
## Creating Custom Wrappers
|
||||
|
||||
You can create your own wrappers with preset options:
|
||||
|
||||
```typescript
|
||||
import { runUntilDoneWith, AuditionOptions } from 'xstate-audition';
|
||||
|
||||
// Custom wrapper with defaults
|
||||
function runWithDefaults<TLogic extends AnyActorLogic>(
|
||||
actor: Actor<TLogic>,
|
||||
overrides?: Partial<AuditionOptions>,
|
||||
) {
|
||||
const options: AuditionOptions = {
|
||||
timeout: 5000, // 5 seconds default
|
||||
logger: console.log, // Always log
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return runUntilDoneWith(actor, options);
|
||||
}
|
||||
|
||||
// Usage
|
||||
await runWithDefaults(actor); // Uses 5s timeout + console.log
|
||||
await runWithDefaults(actor, { timeout: 10000 }); // Override timeout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Core Functions Reference](./core-functions.md) - Detailed API for all functions
|
||||
- [Testing Patterns](./testing-patterns.md) - Advanced usage patterns and examples
|
||||
826
skills/xstate-audition/references/testing-patterns.md
Normal file
826
skills/xstate-audition/references/testing-patterns.md
Normal file
@@ -0,0 +1,826 @@
|
||||
# xstate-audition Testing Patterns
|
||||
|
||||
Advanced patterns, best practices, and real-world examples for testing XState actors with xstate-audition.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Test Organization Patterns](#test-organization-patterns)
|
||||
- [Currying Patterns](#currying-patterns)
|
||||
- [Hierarchical Actor Testing](#hierarchical-actor-testing)
|
||||
- [Event Communication Patterns](#event-communication-patterns)
|
||||
- [Timeout and Error Testing](#timeout-and-error-testing)
|
||||
- [Integration Testing Patterns](#integration-testing-patterns)
|
||||
- [Debugging Failed Tests](#debugging-failed-tests)
|
||||
- [Performance Testing](#performance-testing)
|
||||
- [Advanced TypeScript Patterns](#advanced-typescript-patterns)
|
||||
|
||||
---
|
||||
|
||||
## Test Organization Patterns
|
||||
|
||||
### Pattern: Describe Block with Shared Setup
|
||||
|
||||
Organize tests with shared actor creation and curried functions.
|
||||
|
||||
```typescript
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { createActor } from 'xstate';
|
||||
import {
|
||||
runUntilDone,
|
||||
runUntilTransition,
|
||||
type CurryTransitionP2,
|
||||
} from 'xstate-audition';
|
||||
|
||||
describe('FetchMachine', () => {
|
||||
let actor: Actor<typeof fetchMachine>;
|
||||
let runFromIdle: CurryTransitionP2<typeof actor>;
|
||||
|
||||
beforeEach(() => {
|
||||
actor = createActor(fetchMachine);
|
||||
runFromIdle = runUntilTransition(actor, 'fetch.idle');
|
||||
});
|
||||
|
||||
describe('successful fetch', () => {
|
||||
it('should transition from idle to loading', async () => {
|
||||
actor.send({ type: 'FETCH' });
|
||||
await runFromIdle('fetch.loading');
|
||||
});
|
||||
|
||||
it('should complete with data', async () => {
|
||||
actor.send({ type: 'FETCH' });
|
||||
actor.send({ type: 'SUCCESS', data: 'test' });
|
||||
|
||||
const snapshot = await runUntilSnapshot(actor, (s) =>
|
||||
s.matches('success'),
|
||||
);
|
||||
|
||||
assert.equal(snapshot.context.data, 'test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed fetch', () => {
|
||||
it('should transition to error state', async () => {
|
||||
actor.send({ type: 'FETCH' });
|
||||
actor.send({ type: 'ERROR', error: new Error('Network error') });
|
||||
|
||||
await runFromIdle('fetch.error');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Factory Functions for Test Actors
|
||||
|
||||
Create reusable factory functions for common test scenarios.
|
||||
|
||||
```typescript
|
||||
import { createActorFromLogic } from 'xstate-audition';
|
||||
|
||||
// Factory with preset configuration
|
||||
function createTestActor(input: string) {
|
||||
return createActor(myMachine, {
|
||||
input,
|
||||
logger: process.env.DEBUG ? console.log : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// Factory with currying
|
||||
const createActorWithInput = createActorFromLogic(myMachine);
|
||||
|
||||
describe('myMachine', () => {
|
||||
it('should work with input A', async () => {
|
||||
const actor = createActorWithInput({ input: 'A' });
|
||||
const output = await runUntilDone(actor);
|
||||
assert.equal(output, 'processed-A');
|
||||
});
|
||||
|
||||
it('should work with input B', async () => {
|
||||
const actor = createActorWithInput({ input: 'B' });
|
||||
const output = await runUntilDone(actor);
|
||||
assert.equal(output, 'processed-B');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Parameterized Tests
|
||||
|
||||
Test multiple scenarios with the same logic.
|
||||
|
||||
```typescript
|
||||
const testCases = [
|
||||
{ input: 'hello', expected: 'HELLO' },
|
||||
{ input: 'world', expected: 'WORLD' },
|
||||
{ input: '', expected: '' },
|
||||
];
|
||||
|
||||
describe('StringTransformMachine', () => {
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
it(`should transform "${input}" to "${expected}"`, async () => {
|
||||
const actor = createActor(transformMachine, { input });
|
||||
const output = await runUntilDone(actor);
|
||||
assert.equal(output, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Currying Patterns
|
||||
|
||||
### Pattern: Multi-Level Currying
|
||||
|
||||
Create increasingly specific test helpers through currying.
|
||||
|
||||
```typescript
|
||||
describe('TransitionMachine', () => {
|
||||
let actor: Actor<typeof machine>;
|
||||
let runWithActor: ReturnType<typeof runUntilTransition>;
|
||||
let runFromIdle: CurryTransitionP2<typeof actor>;
|
||||
|
||||
beforeEach(() => {
|
||||
actor = createActor(machine);
|
||||
|
||||
// Level 1: Curry with actor
|
||||
runWithActor = runUntilTransition(actor);
|
||||
|
||||
// Level 2: Curry with actor + fromState
|
||||
runFromIdle = runWithActor('machine.idle');
|
||||
});
|
||||
|
||||
it('should transition to loading', async () => {
|
||||
actor.send({ type: 'LOAD' });
|
||||
// Level 3: Only provide toState
|
||||
await runFromIdle('machine.loading');
|
||||
});
|
||||
|
||||
it('should transition to success', async () => {
|
||||
actor.send({ type: 'LOAD' });
|
||||
actor.send({ type: 'SUCCESS' });
|
||||
await runFromIdle('machine.success');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Shared Options with Currying
|
||||
|
||||
Create curried functions with preset options.
|
||||
|
||||
```typescript
|
||||
import { runUntilSnapshotWith } from 'xstate-audition';
|
||||
|
||||
describe('SlowMachine', () => {
|
||||
const options = {
|
||||
timeout: 5000,
|
||||
logger: console.log,
|
||||
};
|
||||
|
||||
it('should eventually reach done state', async () => {
|
||||
const actor = createActor(slowMachine);
|
||||
|
||||
const snapshot = await runUntilSnapshotWith(actor, options, (s) =>
|
||||
s.matches('done'),
|
||||
);
|
||||
|
||||
assert.ok(snapshot.matches('done'));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hierarchical Actor Testing
|
||||
|
||||
### Pattern: Parent-Child Communication
|
||||
|
||||
Test parent-child actor interactions.
|
||||
|
||||
```typescript
|
||||
import { waitForSpawn, waitForSnapshot } from 'xstate-audition';
|
||||
|
||||
const childLogic = fromPromise<string, string>(async ({ input }) => {
|
||||
return `processed: ${input}`;
|
||||
});
|
||||
|
||||
const parentMachine = setup({
|
||||
actors: { child: childLogic },
|
||||
types: {
|
||||
context: {} as { result?: string },
|
||||
events: {} as { type: 'START'; value: string },
|
||||
},
|
||||
}).createMachine({
|
||||
initial: 'idle',
|
||||
context: { result: undefined },
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
START: 'processing',
|
||||
},
|
||||
},
|
||||
processing: {
|
||||
invoke: {
|
||||
id: 'worker',
|
||||
src: 'child',
|
||||
input: ({ event }) => event.value,
|
||||
onDone: {
|
||||
target: 'done',
|
||||
actions: assign({
|
||||
result: ({ event }) => event.output,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
done: {
|
||||
type: 'final',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('Parent-Child Communication', () => {
|
||||
it('should spawn child and receive result', async () => {
|
||||
const parent = createActor(parentMachine);
|
||||
|
||||
// Setup spawn detection
|
||||
const childPromise = waitForSpawn<typeof childLogic>(parent, 'worker');
|
||||
|
||||
// Trigger spawning
|
||||
parent.send({ type: 'START', value: 'test' });
|
||||
|
||||
// Wait for child to spawn
|
||||
const child = await childPromise;
|
||||
assert.equal(child.id, 'worker');
|
||||
|
||||
// Wait for parent to complete
|
||||
const snapshot = await waitForSnapshot(parent, (s) => s.matches('done'));
|
||||
assert.equal(snapshot.context.result, 'processed: test');
|
||||
|
||||
// Cleanup
|
||||
parent.stop();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Testing Actor Hierarchies
|
||||
|
||||
Test complex multi-level actor systems.
|
||||
|
||||
```typescript
|
||||
describe('Three-Level Hierarchy', () => {
|
||||
it('should coordinate grandparent-parent-child', async () => {
|
||||
const grandparent = createActor(grandparentMachine);
|
||||
|
||||
// Wait for parent to spawn
|
||||
const parent = await waitForSpawn<typeof parentLogic>(
|
||||
grandparent,
|
||||
'parentActor',
|
||||
);
|
||||
|
||||
// Wait for child to spawn (spawned by parent)
|
||||
const child = await waitForSpawn<typeof childLogic>(
|
||||
grandparent, // Detectable from root
|
||||
'childActor',
|
||||
);
|
||||
|
||||
// Send event to child
|
||||
child.send({ type: 'WORK', value: 42 });
|
||||
|
||||
// Wait for result to propagate to grandparent
|
||||
const snapshot = await waitForSnapshot(
|
||||
grandparent,
|
||||
(s) => s.context.finalResult !== undefined,
|
||||
);
|
||||
|
||||
assert.equal(snapshot.context.finalResult, 42);
|
||||
|
||||
grandparent.stop();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Communication Patterns
|
||||
|
||||
### Pattern: Inter-Actor Event Flow
|
||||
|
||||
Test event communication between actors.
|
||||
|
||||
```typescript
|
||||
describe('Ping-Pong Actors', () => {
|
||||
it('should exchange events', async () => {
|
||||
const ping = createActor(pingMachine);
|
||||
const pong = createActor(pongMachine);
|
||||
|
||||
// Start both actors
|
||||
ping.start();
|
||||
pong.start();
|
||||
|
||||
try {
|
||||
// Setup detection for pong receiving PING
|
||||
const pongPromise = waitForEventReceived(pong, ['PING']);
|
||||
|
||||
// Send PING from ping to pong
|
||||
ping.send({ type: 'START_PING', target: pong });
|
||||
|
||||
// Wait for pong to receive
|
||||
const [receivedEvent] = await pongPromise;
|
||||
assert.equal(receivedEvent.type, 'PING');
|
||||
|
||||
// Setup detection for ping receiving PONG
|
||||
const pingPromise = waitForEventReceived(ping, ['PONG']);
|
||||
|
||||
// Pong responds
|
||||
pong.send({ type: 'RESPOND', target: ping });
|
||||
|
||||
// Wait for ping to receive
|
||||
const [pongEvent] = await pingPromise;
|
||||
assert.equal(pongEvent.type, 'PONG');
|
||||
} finally {
|
||||
ping.stop();
|
||||
pong.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Event Source Filtering
|
||||
|
||||
Test events from specific actor sources.
|
||||
|
||||
```typescript
|
||||
describe('Multi-Child Event Filtering', () => {
|
||||
it('should filter events by source actor', async () => {
|
||||
const parent = createActor(parentMachine);
|
||||
|
||||
// Parent spawns multiple children: child-1, child-2, child-3
|
||||
parent.send({ type: 'SPAWN_CHILDREN', count: 3 });
|
||||
|
||||
// Wait for events only from child-2
|
||||
const events = await runUntilEventReceivedWith(
|
||||
parent,
|
||||
{ otherActorId: 'child-2', timeout: 2000 },
|
||||
['READY', 'COMPLETE'],
|
||||
);
|
||||
|
||||
// Should only have events from child-2
|
||||
assert.equal(events.length, 2);
|
||||
assert.equal(events[0].type, 'READY');
|
||||
assert.equal(events[1].type, 'COMPLETE');
|
||||
});
|
||||
|
||||
it('should filter events by pattern', async () => {
|
||||
const parent = createActor(parentMachine);
|
||||
|
||||
// Filter events from any child matching pattern
|
||||
const events = await runUntilEventReceivedWith(
|
||||
parent,
|
||||
{ otherActorId: /^child-\d+$/ },
|
||||
['STATUS'],
|
||||
);
|
||||
|
||||
assert.equal(events[0].type, 'STATUS');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timeout and Error Testing
|
||||
|
||||
### Pattern: Testing Timeout Scenarios
|
||||
|
||||
Verify actors handle timeouts correctly.
|
||||
|
||||
```typescript
|
||||
describe('Timeout Handling', () => {
|
||||
it('should timeout if actor takes too long', async () => {
|
||||
const actor = createActor(slowMachine);
|
||||
|
||||
await assert.rejects(
|
||||
runUntilDoneWith(actor, { timeout: 100 }),
|
||||
(err: Error) => {
|
||||
assert.match(err.message, /did not complete in 100ms/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should complete within timeout', async () => {
|
||||
const actor = createActor(fastMachine);
|
||||
|
||||
// Should not timeout
|
||||
const output = await runUntilDoneWith(actor, { timeout: 1000 });
|
||||
assert.ok(output);
|
||||
});
|
||||
|
||||
it('should work with infinite timeout', async () => {
|
||||
const actor = createActor(unpredictableMachine);
|
||||
|
||||
// No timeout
|
||||
const output = await runUntilDoneWith(actor, { timeout: Infinity });
|
||||
assert.ok(output);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Error State Testing
|
||||
|
||||
Test error handling and recovery.
|
||||
|
||||
```typescript
|
||||
describe('Error Handling', () => {
|
||||
it('should transition to error state on failure', async () => {
|
||||
const actor = createActor(fetchMachine);
|
||||
|
||||
actor.send({ type: 'FETCH' });
|
||||
actor.send({ type: 'ERROR', error: new Error('Network failed') });
|
||||
|
||||
const snapshot = await runUntilSnapshot(actor, (s) => s.matches('error'));
|
||||
|
||||
assert.ok(snapshot.context.error);
|
||||
assert.match(snapshot.context.error.message, /Network failed/);
|
||||
});
|
||||
|
||||
it('should retry after error', async () => {
|
||||
const actor = createActor(retryMachine);
|
||||
|
||||
actor.send({ type: 'FETCH' });
|
||||
actor.send({ type: 'ERROR' });
|
||||
|
||||
// Should transition to error
|
||||
await waitForSnapshot(actor, (s) => s.matches('error'));
|
||||
|
||||
// Send retry
|
||||
actor.send({ type: 'RETRY' });
|
||||
|
||||
// Should transition back to loading
|
||||
const snapshot = await waitForSnapshot(actor, (s) => s.matches('loading'));
|
||||
|
||||
assert.ok(snapshot.matches('loading'));
|
||||
actor.stop();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Testing Patterns
|
||||
|
||||
### Pattern: End-to-End Flow Testing
|
||||
|
||||
Test complete user flows through state machines.
|
||||
|
||||
```typescript
|
||||
describe('User Registration Flow', () => {
|
||||
it('should complete full registration', async () => {
|
||||
const actor = createActor(registrationMachine);
|
||||
|
||||
// Start registration
|
||||
const idleToForm = waitForTransition(
|
||||
actor,
|
||||
'registration.idle',
|
||||
'registration.form',
|
||||
);
|
||||
actor.send({ type: 'START_REGISTRATION' });
|
||||
await idleToForm;
|
||||
|
||||
// Fill form
|
||||
actor.send({
|
||||
type: 'SUBMIT_FORM',
|
||||
data: { email: 'user@example.com', password: 'secure123' },
|
||||
});
|
||||
|
||||
// Wait for validation
|
||||
await waitForSnapshot(actor, (s) => s.matches('validating'));
|
||||
|
||||
// Simulate validation success
|
||||
actor.send({ type: 'VALIDATION_SUCCESS' });
|
||||
|
||||
// Wait for API call
|
||||
await waitForSnapshot(actor, (s) => s.matches('submitting'));
|
||||
|
||||
// Simulate API success
|
||||
actor.send({
|
||||
type: 'API_SUCCESS',
|
||||
userId: '123',
|
||||
});
|
||||
|
||||
// Should reach success state
|
||||
const snapshot = await waitForSnapshot(actor, (s) => s.matches('success'));
|
||||
|
||||
assert.equal(snapshot.context.userId, '123');
|
||||
actor.stop();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Testing with External Services
|
||||
|
||||
Test actors that interact with external services.
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('API Integration', () => {
|
||||
it('should call API and handle response', async () => {
|
||||
// Mock API
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
json: async () => ({ id: '123', name: 'Test' }),
|
||||
});
|
||||
|
||||
const machine = setup({
|
||||
actors: {
|
||||
fetchUser: fromPromise(async ({ input }) => {
|
||||
const response = await mockFetch(`/api/users/${input.id}`);
|
||||
return response.json();
|
||||
}),
|
||||
},
|
||||
}).createMachine({...});
|
||||
|
||||
const actor = createActor(machine);
|
||||
|
||||
actor.send({ type: 'FETCH_USER', id: '123' });
|
||||
|
||||
const snapshot = await runUntilSnapshot(
|
||||
actor,
|
||||
(s) => s.context.userData !== null
|
||||
);
|
||||
|
||||
assert.ok(mockFetch.calledOnce);
|
||||
assert.equal(snapshot.context.userData.id, '123');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
### Pattern: Adding Debug Logging
|
||||
|
||||
Use logger and inspector for debugging.
|
||||
|
||||
```typescript
|
||||
describe('Debugging Failed Test', () => {
|
||||
it('should debug with logger', async () => {
|
||||
const actor = createActor(problemMachine);
|
||||
|
||||
const snapshot = await runUntilSnapshotWith(
|
||||
actor,
|
||||
{
|
||||
logger: (...args) => console.log('[DEBUG]', ...args),
|
||||
timeout: 5000,
|
||||
},
|
||||
(s) => s.matches('targetState'),
|
||||
);
|
||||
|
||||
// Logs will show all state transitions and actions
|
||||
});
|
||||
|
||||
it('should debug with inspector', async () => {
|
||||
const actor = createActor(problemMachine);
|
||||
const events: any[] = [];
|
||||
|
||||
const snapshot = await runUntilSnapshotWith(
|
||||
actor,
|
||||
{
|
||||
inspector: (event) => {
|
||||
events.push(event);
|
||||
console.log('Inspection:', event.type);
|
||||
},
|
||||
timeout: 5000,
|
||||
},
|
||||
(s) => s.matches('targetState'),
|
||||
);
|
||||
|
||||
// Inspect collected events
|
||||
console.log('All events:', events);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Incremental Testing
|
||||
|
||||
Break complex tests into smaller steps.
|
||||
|
||||
```typescript
|
||||
describe('Complex Flow - Incremental', () => {
|
||||
it('Step 1: should start correctly', async () => {
|
||||
const actor = createActor(complexMachine);
|
||||
actor.send({ type: 'START' });
|
||||
|
||||
await waitForSnapshot(actor, (s) => s.matches('started'));
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it('Step 2: should process data', async () => {
|
||||
const actor = createActor(complexMachine);
|
||||
actor.send({ type: 'START' });
|
||||
await waitForSnapshot(actor, (s) => s.matches('started'));
|
||||
|
||||
actor.send({ type: 'PROCESS', data: 'test' });
|
||||
await waitForSnapshot(actor, (s) => s.matches('processing'));
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it('Step 3: should complete', async () => {
|
||||
const actor = createActor(complexMachine);
|
||||
actor.send({ type: 'START' });
|
||||
await waitForSnapshot(actor, (s) => s.matches('started'));
|
||||
|
||||
actor.send({ type: 'PROCESS', data: 'test' });
|
||||
await waitForSnapshot(actor, (s) => s.matches('processing'));
|
||||
|
||||
actor.send({ type: 'COMPLETE' });
|
||||
const snapshot = await runUntilSnapshot(actor, (s) => s.matches('done'));
|
||||
|
||||
assert.ok(snapshot.matches('done'));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Pattern: Testing Timing Constraints
|
||||
|
||||
Verify actors meet performance requirements.
|
||||
|
||||
```typescript
|
||||
describe('Performance', () => {
|
||||
it('should complete within 100ms', async () => {
|
||||
const start = Date.now();
|
||||
const actor = createActor(fastMachine);
|
||||
|
||||
await runUntilDoneWith(actor, { timeout: 100 });
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
assert.ok(elapsed < 100, `Took ${elapsed}ms, expected < 100ms`);
|
||||
});
|
||||
|
||||
it('should handle burst of events', async () => {
|
||||
const actor = createActor(eventHandlerMachine);
|
||||
|
||||
// Send many events rapidly
|
||||
const promise = waitForSnapshot(
|
||||
actor,
|
||||
(s) => s.context.processedCount === 1000,
|
||||
);
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
actor.send({ type: 'PROCESS', id: i });
|
||||
}
|
||||
|
||||
const snapshot = await promise;
|
||||
assert.equal(snapshot.context.processedCount, 1000);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Memory Leak Detection
|
||||
|
||||
Test for memory leaks in long-running actors.
|
||||
|
||||
```typescript
|
||||
describe('Memory Leaks', () => {
|
||||
it('should not leak memory with many iterations', async () => {
|
||||
const iterations = 1000;
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const actor = createActor(myMachine);
|
||||
await runUntilDone(actor);
|
||||
// Actor is stopped by runUntilDone
|
||||
}
|
||||
|
||||
// If test completes, no obvious leaks
|
||||
// Use memory profiling tools for deeper analysis
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced TypeScript Patterns
|
||||
|
||||
### Pattern: Generic Test Helpers
|
||||
|
||||
Create reusable, type-safe test utilities.
|
||||
|
||||
```typescript
|
||||
async function testMachineOutput<TLogic extends AnyActorLogic>(
|
||||
logic: TLogic,
|
||||
input: InputFrom<TLogic>,
|
||||
expectedOutput: OutputFrom<TLogic>,
|
||||
) {
|
||||
const actor = createActor(logic, { input });
|
||||
const output = await runUntilDone(actor);
|
||||
|
||||
assert.deepEqual(output, expectedOutput);
|
||||
}
|
||||
|
||||
// Usage
|
||||
await testMachineOutput(promiseMachine, 'input', 'expected-output');
|
||||
```
|
||||
|
||||
### Pattern: Type-Safe Event Testing
|
||||
|
||||
Create helpers with full type inference.
|
||||
|
||||
```typescript
|
||||
function createEmittedTester<TMachine extends AnyStateMachineActor>(
|
||||
machine: TMachine,
|
||||
) {
|
||||
return async (
|
||||
emittedTypes: Array<EmittedFrom<TMachine>['type']>,
|
||||
assertions: (events: Array<EmittedFrom<TMachine>>) => void,
|
||||
) => {
|
||||
const actor = createActor(machine);
|
||||
const events = await runUntilEmitted(actor, emittedTypes);
|
||||
assertions(events);
|
||||
};
|
||||
}
|
||||
|
||||
// Usage with full type safety
|
||||
const testEmitted = createEmittedTester(myMachine);
|
||||
|
||||
await testEmitted(['EVENT1', 'EVENT2'], (events) => {
|
||||
// events is fully typed as Array<EmittedFrom<typeof myMachine>>
|
||||
assert.equal(events[0].type, 'EVENT1');
|
||||
assert.equal(events[1].type, 'EVENT2');
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Conditional Type Guards
|
||||
|
||||
Use type guards for snapshot testing.
|
||||
|
||||
```typescript
|
||||
function isErrorState(
|
||||
snapshot: SnapshotFrom<typeof machine>,
|
||||
): snapshot is SnapshotFrom<typeof machine> & { context: { error: Error } } {
|
||||
return snapshot.matches('error') && snapshot.context.error !== null;
|
||||
}
|
||||
|
||||
it('should reach error state with error', async () => {
|
||||
const actor = createActor(machine);
|
||||
|
||||
actor.send({ type: 'TRIGGER_ERROR' });
|
||||
|
||||
const snapshot = await runUntilSnapshot(actor, isErrorState);
|
||||
|
||||
// TypeScript knows snapshot.context.error is Error
|
||||
assert.equal(snapshot.context.error.message, 'Expected error');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practice Checklist
|
||||
|
||||
### Test Organization
|
||||
|
||||
- ✅ Use `describe` blocks for logical grouping
|
||||
- ✅ Extract shared setup to `beforeEach`
|
||||
- ✅ Clean up actors in `afterEach` if using `waitFor*`
|
||||
- ✅ Use factory functions for actor creation
|
||||
- ✅ Keep tests focused on single behavior
|
||||
|
||||
### Currying
|
||||
|
||||
- ✅ Curry repeated patterns in `beforeEach`
|
||||
- ✅ Type curried functions explicitly
|
||||
- ✅ Use multi-level currying for complex scenarios
|
||||
- ✅ Create reusable helpers with preset options
|
||||
|
||||
### Assertions
|
||||
|
||||
- ✅ Assert on specific values, not just truthiness
|
||||
- ✅ Use snapshot matchers for complex state
|
||||
- ✅ Test both success and error paths
|
||||
- ✅ Verify context changes, not just state values
|
||||
|
||||
### Performance
|
||||
|
||||
- ✅ Set explicit timeouts for slow operations
|
||||
- ✅ Keep timeouts < test framework timeout
|
||||
- ✅ Test timing constraints when critical
|
||||
- ✅ Profile for memory leaks in long-running tests
|
||||
|
||||
### Debugging
|
||||
|
||||
- ✅ Add logger for failing tests
|
||||
- ✅ Use inspector to see all events
|
||||
- ✅ Break complex tests into steps
|
||||
- ✅ Increase timeout to isolate timing issues
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Core Functions Reference](./core-functions.md) - Detailed API documentation
|
||||
- [Options & Types](./options-types.md) - AuditionOptions and TypeScript types
|
||||
Reference in New Issue
Block a user