Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:01:45 +08:00
commit 23371433fd
17 changed files with 6735 additions and 0 deletions

View 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 |

View 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

View 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