Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "xstate",
|
||||||
|
"description": "XState v5 backend state management with comprehensive state machine patterns, actor testing, and server-side orchestration",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "boneskull"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# xstate
|
||||||
|
|
||||||
|
XState v5 backend state management with comprehensive state machine patterns, actor testing, and server-side orchestration
|
||||||
323
commands/audition.md
Normal file
323
commands/audition.md
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
---
|
||||||
|
description: Get expert guidance on testing XState v5 actors with xstate-audition
|
||||||
|
argument-hint: '[what-to-test]'
|
||||||
|
---
|
||||||
|
|
||||||
|
# XState Audition Testing Guidance
|
||||||
|
|
||||||
|
Provide expert guidance on testing XState v5 actors using the xstate-audition library for comprehensive state machine and actor testing.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/audition [optional: what you need to test]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `what-to-test` (optional): Describe what you're trying to test or paste test code you need help with
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
When this command is invoked:
|
||||||
|
|
||||||
|
1. **Load the xstate-audition skill** from `skills/xstate-audition/SKILL.md`
|
||||||
|
|
||||||
|
2. **If the user provided context**, analyze their needs and:
|
||||||
|
- Identify which xstate-audition functions apply to their testing scenario
|
||||||
|
- Provide specific test examples using xstate-audition APIs
|
||||||
|
- Explain the testing pattern and timing considerations
|
||||||
|
- Show best practices for the specific test case
|
||||||
|
- Reference the appropriate documentation from `references/`:
|
||||||
|
- Core Functions for API documentation
|
||||||
|
- Options & Types for configuration details
|
||||||
|
- Testing Patterns for advanced examples
|
||||||
|
|
||||||
|
3. **If no context was provided**, offer to help by:
|
||||||
|
- Asking what they're trying to test or what actor behavior needs verification
|
||||||
|
- Listing common test scenarios:
|
||||||
|
- Testing state transitions
|
||||||
|
- Testing promise actors and async operations
|
||||||
|
- Testing event emissions and actor communication
|
||||||
|
- Testing hierarchical actors and spawning
|
||||||
|
- Testing with timeouts and error handling
|
||||||
|
- Integration testing of actor systems
|
||||||
|
- Offering to show examples of specific patterns
|
||||||
|
|
||||||
|
4. **Always emphasize xstate-audition best practices**:
|
||||||
|
- Use currying for repeated patterns to reduce boilerplate
|
||||||
|
- Set external input BEFORE awaiting (avoid race conditions)
|
||||||
|
- Use `waitFor*()` for multi-stage tests (keeps actor alive)
|
||||||
|
- Provide explicit type arguments (especially for `runUntilSpawn`)
|
||||||
|
- Use appropriate timeouts (less than test framework timeout)
|
||||||
|
- Test state transitions explicitly with `runUntilTransition()`
|
||||||
|
- Use `runUntilSnapshot()` for complex conditions
|
||||||
|
|
||||||
|
5. **Explain the core testing pattern**:
|
||||||
|
1. Create an Actor using `createActor(logic)`
|
||||||
|
2. Create a Promise using a condition function (e.g., `runUntilDone(actor)`)
|
||||||
|
3. If the actor needs external input, perform that operation BEFORE awaiting
|
||||||
|
4. Await the Promise
|
||||||
|
5. Make assertions about the result
|
||||||
|
|
||||||
|
6. **Clarify `runUntil*()` vs `waitFor*()`**:
|
||||||
|
- `runUntil*()` - Starts actor, waits for condition, then STOPS the actor
|
||||||
|
- `waitFor*()` - Starts actor, waits for condition, but KEEPS the actor running
|
||||||
|
|
||||||
|
## Example Interactions
|
||||||
|
|
||||||
|
**With context - testing a promise actor:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /audition How do I test this promise actor?
|
||||||
|
const fetchLogic = fromPromise(async ({ input }) => {
|
||||||
|
const res = await fetch(input.url);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
Agent: Here's how to test your promise actor with xstate-audition:
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
import { createActor, fromPromise } from 'xstate';
|
||||||
|
import { runUntilDone } from 'xstate-audition';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
|
||||||
|
const fetchLogic = fromPromise<any, { url: string }>(
|
||||||
|
async ({ input }) => {
|
||||||
|
const res = await fetch(input.url);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('fetchLogic', () => {
|
||||||
|
it('should complete with fetched data', async () => {
|
||||||
|
const actor = createActor(fetchLogic, {
|
||||||
|
input: { url: 'https://api.example.com/data' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await runUntilDone(actor);
|
||||||
|
|
||||||
|
assert.ok(result);
|
||||||
|
assert.equal(typeof result, 'object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
const actor = createActor(fetchLogic, {
|
||||||
|
input: { url: 'https://invalid.url' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
runUntilDone(actor),
|
||||||
|
(err: Error) => {
|
||||||
|
assert.ok(err.message.includes('fetch'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Key patterns:
|
||||||
|
- `runUntilDone()` waits for the promise actor to complete
|
||||||
|
- Pass input via actor options
|
||||||
|
- Use `assert.rejects()` to test error cases
|
||||||
|
- The actor automatically starts when you await the promise
|
||||||
|
```
|
||||||
|
|
||||||
|
**With context - testing state transitions:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /audition I need to test that my machine transitions from idle to loading when FETCH is sent
|
||||||
|
|
||||||
|
Agent: Use `runUntilTransition()` to test explicit state changes:
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
import { createActor } from 'xstate';
|
||||||
|
import { runUntilTransition } from 'xstate-audition';
|
||||||
|
import { describe, it, beforeEach } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
|
||||||
|
describe('fetchMachine transitions', () => {
|
||||||
|
let actor: Actor<typeof fetchMachine>;
|
||||||
|
let runFromIdle: CurryTransitionP2<typeof actor>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actor = createActor(fetchMachine);
|
||||||
|
// Curry with actor and fromState for reusability
|
||||||
|
runFromIdle = runUntilTransition(actor, 'fetchMachine.idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition from idle to loading', async () => {
|
||||||
|
// Setup the promise FIRST
|
||||||
|
const promise = runFromIdle('fetchMachine.loading');
|
||||||
|
|
||||||
|
// THEN send the event that triggers the transition
|
||||||
|
actor.send({ type: 'FETCH' });
|
||||||
|
|
||||||
|
// Finally await the result
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transition through loading to success', async () => {
|
||||||
|
actor.send({ type: 'FETCH' });
|
||||||
|
await runFromIdle('fetchMachine.success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Important timing note:
|
||||||
|
1. Create the promise BEFORE sending the event
|
||||||
|
2. Send the event that triggers the transition
|
||||||
|
3. Then await the promise
|
||||||
|
|
||||||
|
This pattern avoids race conditions. The curried form (`runFromIdle`) reduces boilerplate!
|
||||||
|
```
|
||||||
|
|
||||||
|
**With context - testing event emissions:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /audition How do I test that my machine emits events?
|
||||||
|
|
||||||
|
Agent: Use `runUntilEmitted()` to test event emissions:
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
import { createActor, setup, emit } from 'xstate';
|
||||||
|
import { runUntilEmitted } from 'xstate-audition';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
|
||||||
|
const notifierMachine = setup({
|
||||||
|
types: {
|
||||||
|
emitted: {} as
|
||||||
|
| { type: 'READY'; message: string }
|
||||||
|
| { type: 'ERROR'; error: Error },
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
initial: 'initializing',
|
||||||
|
states: {
|
||||||
|
initializing: {
|
||||||
|
entry: emit({ type: 'READY', message: 'System initialized' }),
|
||||||
|
on: {
|
||||||
|
START: 'running',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
running: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('notifierMachine emissions', () => {
|
||||||
|
it('should emit READY on initialization', async () => {
|
||||||
|
const actor = createActor(notifierMachine);
|
||||||
|
|
||||||
|
// Wait for READY event
|
||||||
|
const [readyEvent] = await runUntilEmitted(actor, ['READY']);
|
||||||
|
|
||||||
|
assert.equal(readyEvent.type, 'READY');
|
||||||
|
assert.equal(readyEvent.message, 'System initialized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit multiple events', async () => {
|
||||||
|
const actor = createActor(notifierMachine);
|
||||||
|
|
||||||
|
// Wait for any of these events
|
||||||
|
const events = await runUntilEmitted(actor, ['READY', 'ERROR']);
|
||||||
|
|
||||||
|
assert.ok(events.length > 0);
|
||||||
|
assert.equal(events[0].type, 'READY');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
The function returns an array of emitted events. Test the ones you care about!
|
||||||
|
```
|
||||||
|
|
||||||
|
**With context - testing hierarchical actors:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /audition How do I test parent/child actor communication?
|
||||||
|
|
||||||
|
Agent: Use `waitForSpawn()` to test hierarchical actors:
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
import { createActor } from 'xstate';
|
||||||
|
import { waitForSpawn, waitForSnapshot } from 'xstate-audition';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
|
||||||
|
describe('parent-child communication', () => {
|
||||||
|
it('should spawn child and communicate', async () => {
|
||||||
|
const parent = createActor(parentMachine);
|
||||||
|
|
||||||
|
// Wait for child to spawn (parent stays running)
|
||||||
|
const child = await waitForSpawn<typeof childLogic>(
|
||||||
|
parent,
|
||||||
|
'childActor'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(child);
|
||||||
|
assert.equal(child.id, 'childActor');
|
||||||
|
|
||||||
|
// Send event to child
|
||||||
|
child.send({ type: 'CHILD_EVENT' });
|
||||||
|
|
||||||
|
// Wait for parent to react to child's output
|
||||||
|
const parentSnapshot = await waitForSnapshot(
|
||||||
|
parent,
|
||||||
|
(snapshot) => snapshot.matches('parentReacted')
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(parentSnapshot.matches('parentReacted'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- Use `waitForSpawn()` not `runUntilSpawn()` to keep parent running
|
||||||
|
- Provide type argument `<typeof childLogic>` for proper typing
|
||||||
|
- Parent stays alive so you can continue testing interactions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Without context:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /audition
|
||||||
|
|
||||||
|
Agent: I can help you test XState v5 actors with xstate-audition! What do you need to test?
|
||||||
|
|
||||||
|
Common testing scenarios:
|
||||||
|
- **State transitions** - Test explicit state changes with `runUntilTransition()`
|
||||||
|
- **Promise actors** - Test async operations with `runUntilDone()`
|
||||||
|
- **Event emissions** - Test emitted events with `runUntilEmitted()`
|
||||||
|
- **Actor spawning** - Test child actors with `runUntilSpawn()` / `waitForSpawn()`
|
||||||
|
- **Complex conditions** - Test multiple conditions with `runUntilSnapshot()`
|
||||||
|
- **Event communication** - Test inter-actor events with `runUntilEventReceived()`
|
||||||
|
- **Timeout handling** - Test with custom timeouts using `*With()` functions
|
||||||
|
- **Integration tests** - Test complete actor systems with `waitFor*()` functions
|
||||||
|
|
||||||
|
You can also:
|
||||||
|
- Paste your existing test code for review
|
||||||
|
- Ask about specific xstate-audition functions
|
||||||
|
- Request help debugging timeout issues
|
||||||
|
- Learn about currying patterns for cleaner tests
|
||||||
|
|
||||||
|
Core testing pattern:
|
||||||
|
1. Create actor with `createActor(logic)`
|
||||||
|
2. Create promise with condition (e.g., `runUntilDone(actor)`)
|
||||||
|
3. If actor needs input, send events BEFORE awaiting
|
||||||
|
4. Await the promise
|
||||||
|
5. Make assertions
|
||||||
|
|
||||||
|
Key distinction:
|
||||||
|
- `runUntil*()` - stops actor after condition
|
||||||
|
- `waitFor*()` - keeps actor running
|
||||||
|
|
||||||
|
What would you like to test?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- [XState Audition Skill](../skills/xstate-audition/SKILL.md)
|
||||||
|
- [Core Functions Reference](../skills/xstate-audition/references/core-functions.md)
|
||||||
|
- [Options & Types Reference](../skills/xstate-audition/references/options-types.md)
|
||||||
|
- [Testing Patterns Reference](../skills/xstate-audition/references/testing-patterns.md)
|
||||||
336
commands/xstate.md
Normal file
336
commands/xstate.md
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
---
|
||||||
|
description: Get expert guidance on XState v5 state machines and actor model implementation
|
||||||
|
argument-hint: '[what-you-need]'
|
||||||
|
---
|
||||||
|
|
||||||
|
# XState v5 Expert Guidance
|
||||||
|
|
||||||
|
Provide expert guidance on implementing XState v5 state machines, actors, and statecharts with TypeScript best practices for backend applications.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/xstate [optional: what you need help with]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `what-you-need` (optional): Describe what you're trying to implement or paste code you need help with
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
When this command is invoked:
|
||||||
|
|
||||||
|
1. **Load the xstate-v5 skill** from `skills/xstate-v5/SKILL.md`
|
||||||
|
|
||||||
|
2. **If the user provided context**, analyze their needs and:
|
||||||
|
- Identify which XState patterns or APIs apply to their situation
|
||||||
|
- Provide specific code examples using XState v5 APIs
|
||||||
|
- Show TypeScript integration when relevant
|
||||||
|
- Explain best practices and common pitfalls
|
||||||
|
- Reference the appropriate documentation from `references/`:
|
||||||
|
- Core API Reference for function documentation
|
||||||
|
- Actors Reference for actor model guidance
|
||||||
|
- Common Patterns for production-ready examples
|
||||||
|
- TypeScript Integration for typing help
|
||||||
|
- Testing Strategies for test guidance
|
||||||
|
|
||||||
|
3. **If no context was provided**, offer to help by:
|
||||||
|
- Asking what they're trying to implement or what problem they're solving
|
||||||
|
- Listing common scenarios:
|
||||||
|
- Creating state machines with `createMachine` and `setup`
|
||||||
|
- Managing actor lifecycle and communication
|
||||||
|
- Implementing loading states, authentication flows, or wizards
|
||||||
|
- Adding TypeScript types to machines
|
||||||
|
- Testing with xstate-audition
|
||||||
|
- Performance optimization
|
||||||
|
- Offering to show examples of specific patterns
|
||||||
|
|
||||||
|
4. **Always emphasize XState v5 best practices**:
|
||||||
|
- Always use `setup()` for better type inference and reusable logic
|
||||||
|
- Name all actions and guards (avoid inline functions)
|
||||||
|
- Use context for data, states for behavior
|
||||||
|
- Keep machines focused - one machine per logical unit
|
||||||
|
- Leverage TypeScript for compile-time safety
|
||||||
|
- Don't mutate context directly - always use `assign`
|
||||||
|
- Don't use side effects in guards - guards should be pure
|
||||||
|
- Use Promise actors for asynchronous operations
|
||||||
|
|
||||||
|
5. **Focus on backend use cases**:
|
||||||
|
- This plugin is tailored for Node.js/Deno/Bun backend applications
|
||||||
|
- Emphasize server-side patterns: database transactions, API orchestration, rate limiting
|
||||||
|
- Reference backend-specific patterns from the Common Patterns reference
|
||||||
|
|
||||||
|
## Example Interactions
|
||||||
|
|
||||||
|
**With context - creating a machine:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /xstate I need to implement a loading state pattern with retry logic
|
||||||
|
|
||||||
|
Agent: Here's an XState v5 loading pattern with retry logic:
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
import { setup, assign, fromPromise } from 'xstate';
|
||||||
|
|
||||||
|
const fetchMachine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as {
|
||||||
|
data: string | null;
|
||||||
|
error: Error | null;
|
||||||
|
retryCount: number;
|
||||||
|
},
|
||||||
|
events: {} as
|
||||||
|
| { type: 'FETCH' }
|
||||||
|
| { type: 'RETRY' }
|
||||||
|
| { type: 'RESET' },
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
fetchData: fromPromise<string, void>(async () => {
|
||||||
|
const response = await fetch('https://api.example.com/data');
|
||||||
|
return response.text();
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
assignData: assign({
|
||||||
|
data: ({ event }) => (event as any).output,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
assignError: assign({
|
||||||
|
error: ({ event }) => (event as any).error,
|
||||||
|
}),
|
||||||
|
incrementRetry: assign({
|
||||||
|
retryCount: ({ context }) => context.retryCount + 1,
|
||||||
|
}),
|
||||||
|
resetRetry: assign({
|
||||||
|
retryCount: 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
canRetry: ({ context }) => context.retryCount < 3,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
id: 'fetch',
|
||||||
|
initial: 'idle',
|
||||||
|
context: {
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
retryCount: 0,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
FETCH: 'loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
invoke: {
|
||||||
|
src: 'fetchData',
|
||||||
|
onDone: {
|
||||||
|
target: 'success',
|
||||||
|
actions: ['assignData', 'resetRetry'],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'failure',
|
||||||
|
actions: 'assignError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
on: {
|
||||||
|
RESET: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
on: {
|
||||||
|
RETRY: {
|
||||||
|
target: 'loading',
|
||||||
|
guard: 'canRetry',
|
||||||
|
actions: 'incrementRetry',
|
||||||
|
},
|
||||||
|
RESET: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Key XState v5 patterns used:
|
||||||
|
- `setup()` for type-safe configuration and reusable logic
|
||||||
|
- Named actions and guards for clarity
|
||||||
|
- Promise actors with `fromPromise` for async operations
|
||||||
|
- Context for data management (data, error, retryCount)
|
||||||
|
- Guards for conditional transitions (retry limit)
|
||||||
|
- `assign` for immutable context updates
|
||||||
|
|
||||||
|
This pattern is common for backend API calls. See [references/patterns.md] for more examples.
|
||||||
|
```
|
||||||
|
|
||||||
|
**With context - TypeScript help:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /xstate How do I add strong typing to my machine?
|
||||||
|
|
||||||
|
Agent: Use the `setup()` pattern for strong typing in XState v5:
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
import { setup, assign } from 'xstate';
|
||||||
|
|
||||||
|
// Define your types
|
||||||
|
interface UserContext {
|
||||||
|
userId: string | null;
|
||||||
|
userData: User | null;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEvents =
|
||||||
|
| { type: 'LOGIN'; userId: string }
|
||||||
|
| { type: 'LOGOUT' }
|
||||||
|
| { type: 'FETCH_USER' };
|
||||||
|
|
||||||
|
// Use setup for type inference
|
||||||
|
const userMachine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as UserContext,
|
||||||
|
events: {} as UserEvents,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setUserId: assign({
|
||||||
|
userId: ({ event }) => {
|
||||||
|
// TypeScript knows event can be LOGIN
|
||||||
|
if (event.type === 'LOGIN') {
|
||||||
|
return event.userId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
clearUser: assign({
|
||||||
|
userId: null,
|
||||||
|
userData: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
id: 'user',
|
||||||
|
initial: 'loggedOut',
|
||||||
|
context: {
|
||||||
|
userId: null,
|
||||||
|
userData: null,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
loggedOut: {
|
||||||
|
on: {
|
||||||
|
LOGIN: {
|
||||||
|
target: 'loggedIn',
|
||||||
|
actions: 'setUserId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loggedIn: {
|
||||||
|
on: {
|
||||||
|
LOGOUT: {
|
||||||
|
target: 'loggedOut',
|
||||||
|
actions: 'clearUser',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Key TypeScript patterns:
|
||||||
|
- Define context and event types explicitly
|
||||||
|
- Use `{} as Type` syntax in `types` config
|
||||||
|
- TypeScript infers types throughout the machine
|
||||||
|
- Event types are narrowed in action implementations
|
||||||
|
- No `any` types needed
|
||||||
|
|
||||||
|
See [references/typescript.md] for advanced patterns including generic machine factories.
|
||||||
|
```
|
||||||
|
|
||||||
|
**With context - debugging:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /xstate Why is my context not updating?
|
||||||
|
const machine = createMachine({
|
||||||
|
context: { count: 0 },
|
||||||
|
on: {
|
||||||
|
INC: { actions: (ctx) => ctx.context.count++ }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Agent: The issue is that you're mutating context directly, which XState doesn't detect. Always use `assign` for context updates:
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
// ❌ WRONG - mutating context directly
|
||||||
|
on: {
|
||||||
|
INC: { actions: (ctx) => ctx.context.count++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ CORRECT - use assign for immutable updates
|
||||||
|
import { assign } from 'xstate';
|
||||||
|
|
||||||
|
on: {
|
||||||
|
INC: {
|
||||||
|
actions: assign({
|
||||||
|
count: ({ context }) => context.count + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
This is a common mistake! XState requires immutable context updates to:
|
||||||
|
1. Maintain predictable state transitions
|
||||||
|
2. Enable time-travel debugging
|
||||||
|
3. Support snapshot serialization
|
||||||
|
4. Ensure proper actor communication
|
||||||
|
|
||||||
|
Other common mistakes to avoid:
|
||||||
|
- Don't use side effects in guards (guards should be pure)
|
||||||
|
- Don't overuse nested states (flatten when possible)
|
||||||
|
- Don't use `setInterval` in machines (use delays instead)
|
||||||
|
|
||||||
|
Want to see more best practices?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Without context:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
User: /xstate
|
||||||
|
|
||||||
|
Agent: I can help you with XState v5! What are you trying to implement?
|
||||||
|
|
||||||
|
Common use cases:
|
||||||
|
- **State machines** - Model application logic with explicit states and transitions
|
||||||
|
- **Actor systems** - Manage concurrent actors and communication
|
||||||
|
- **Loading patterns** - Implement fetch/retry/timeout logic
|
||||||
|
- **Authentication flows** - Handle login/logout/session management
|
||||||
|
- **Wizards & forms** - Multi-step forms with validation
|
||||||
|
- **Backend workflows** - Database transactions, message queue processing, rate limiting
|
||||||
|
- **TypeScript integration** - Add strong typing to your machines
|
||||||
|
- **Testing** - Test actors with xstate-audition
|
||||||
|
|
||||||
|
You can also:
|
||||||
|
- Paste your existing state machine code for review
|
||||||
|
- Ask about specific XState v5 APIs
|
||||||
|
- Request examples of common patterns
|
||||||
|
- Get help debugging issues
|
||||||
|
|
||||||
|
I have access to comprehensive documentation:
|
||||||
|
- Core API reference (setup, createMachine, actors, actions, guards)
|
||||||
|
- Actor model guide (all actor types, spawning, communication)
|
||||||
|
- Common patterns (loading, auth, forms, pagination, etc.)
|
||||||
|
- TypeScript integration patterns
|
||||||
|
- Testing strategies with xstate-audition
|
||||||
|
|
||||||
|
What would you like to work on?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- [XState v5 Skill](../skills/xstate-v5/SKILL.md)
|
||||||
|
- [Core API Reference](../skills/xstate-v5/references/core-api.md)
|
||||||
|
- [Actors Reference](../skills/xstate-v5/references/actors.md)
|
||||||
|
- [Common Patterns](../skills/xstate-v5/references/patterns.md)
|
||||||
|
- [TypeScript Integration](../skills/xstate-v5/references/typescript.md)
|
||||||
|
- [Testing Strategies](../skills/xstate-v5/references/testing.md)
|
||||||
97
plugin.lock.json
Normal file
97
plugin.lock.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:boneskull/claude-plugins:plugins/xstate",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "38eebe391fde61ecad7f2d265e01556b647939eb",
|
||||||
|
"treeHash": "190878d13623d86661a4481d35a273fb785bad2e53628b764eba12ee4cd6fdb3",
|
||||||
|
"generatedAt": "2025-11-28T10:14:19.807920Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "xstate",
|
||||||
|
"description": "XState v5 backend state management with comprehensive state machine patterns, actor testing, and server-side orchestration",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "c5d3fe738299bbe739a823a148364c467d12b844407624051750feb3f9371b84"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "3b382a67d0c42c83decab0f234f69617a707eae756605550582f0d75e23efa31"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/audition.md",
|
||||||
|
"sha256": "e5d52fdfa1a8a49193dc1a483de1c553f5f763077184bf2ff9c0900e7318018b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/xstate.md",
|
||||||
|
"sha256": "62a9c54296a2cec07a1b8b1462d2669c64699d6489f8b6d79d2f860423f1fc81"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill-rules.json",
|
||||||
|
"sha256": "372cdc641c4ce4ec23758abc8c7c420bd991e3eaf611e1d2db0c2f785927f41e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-v5/SKILL.md",
|
||||||
|
"sha256": "62fc0808a1757f05d20be8aab9d5977882686bfa351cdb392cd72aa541c0a9e0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-v5/references/testing.md",
|
||||||
|
"sha256": "44372c1904ddf529a9ae781f7595d22c12e74222748d3b80ac5b5f7f95a0abd2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-v5/references/actors.md",
|
||||||
|
"sha256": "363a4f816a1c04271bdc09ee2b029352df5a379c479fe5d6d344eb38799a2d11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-v5/references/README.md",
|
||||||
|
"sha256": "982fc4397a4dcfe5f315d1a5c93b5ad8563d6331812e7a1a4107756f7a0985bf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-v5/references/patterns.md",
|
||||||
|
"sha256": "0b8da5a14e4c6dc70fa1f2130ab8219faba71a0f2d37bf50eb4834b295b22a95"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-v5/references/core-api.md",
|
||||||
|
"sha256": "2a0c2129a88d1497d98a096c5f171ec411f098507e58801f892a71fe4d1fb62c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-v5/references/typescript.md",
|
||||||
|
"sha256": "7e7ffb889f310da111ad511916d952cbbec4d896713a59fa6693d67b0b59d587"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-audition/SKILL.md",
|
||||||
|
"sha256": "1f80f9c3cbdc12c0dfa291088c847d64af9e4d165b9e02c759b8f25ccbf5ccd0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-audition/references/core-functions.md",
|
||||||
|
"sha256": "1d6782018c9c215c363c4635fc1a4ce955c38ffe5fc28095178d7203e7108af5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-audition/references/options-types.md",
|
||||||
|
"sha256": "a82d864aab3e5f6b1b73c8af1487af8d63bb3b4fd8a62d4d648af60e45aa118e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/xstate-audition/references/testing-patterns.md",
|
||||||
|
"sha256": "d4146a354c978750a80165480fc30d246620fc2f0fb654aa2d2cdd03f83785c1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "190878d13623d86661a4481d35a273fb785bad2e53628b764eba12ee4cd6fdb3"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
97
skills/skill-rules.json
Normal file
97
skills/skill-rules.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"description": "Skill activation rules for xstate plugin",
|
||||||
|
"skills": {
|
||||||
|
"xstate@boneskull-plugins:xstate-v5": {
|
||||||
|
"type": "domain",
|
||||||
|
"enforcement": "suggest",
|
||||||
|
"priority": "high",
|
||||||
|
"description": "Expert guidance for implementing and analyzing XState v5 state machines with TypeScript support and best practices",
|
||||||
|
"promptTriggers": {
|
||||||
|
"keywords": [
|
||||||
|
"xstate",
|
||||||
|
"state machine",
|
||||||
|
"statechart",
|
||||||
|
"createMachine",
|
||||||
|
"createActor",
|
||||||
|
"fsm",
|
||||||
|
"finite state machine",
|
||||||
|
"actor model",
|
||||||
|
"state management",
|
||||||
|
"xstate v5"
|
||||||
|
],
|
||||||
|
"intentPatterns": [
|
||||||
|
"(create|write|define|add|implement).*state machine",
|
||||||
|
"(create|write|define|add|implement).*statechart",
|
||||||
|
"xstate.*(machine|actor|state)",
|
||||||
|
"(how to|how do I).*xstate",
|
||||||
|
"(setup|configure).*xstate",
|
||||||
|
"(model|implement).*state.*transition"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fileTriggers": {
|
||||||
|
"pathPatterns": ["**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx"],
|
||||||
|
"contentPatterns": [
|
||||||
|
"from 'xstate'",
|
||||||
|
"from \"xstate\"",
|
||||||
|
"createMachine\\(",
|
||||||
|
"createActor\\(",
|
||||||
|
"setup\\(\\{",
|
||||||
|
"\\.send\\(",
|
||||||
|
"\\.start\\(",
|
||||||
|
"assign\\(",
|
||||||
|
"fromPromise\\(",
|
||||||
|
"fromCallback\\("
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xstate@boneskull-plugins:xstate-audition": {
|
||||||
|
"type": "domain",
|
||||||
|
"enforcement": "suggest",
|
||||||
|
"priority": "high",
|
||||||
|
"description": "Expert guidance on testing XState v5 Actors using xstate-audition library for comprehensive state machine and actor testing",
|
||||||
|
"promptTriggers": {
|
||||||
|
"keywords": [
|
||||||
|
"xstate-audition",
|
||||||
|
"xstate audition",
|
||||||
|
"test state machine",
|
||||||
|
"test actor",
|
||||||
|
"actor testing",
|
||||||
|
"state machine testing",
|
||||||
|
"xstate test",
|
||||||
|
"runUntilDone",
|
||||||
|
"waitForSnapshot",
|
||||||
|
"waitForEmission"
|
||||||
|
],
|
||||||
|
"intentPatterns": [
|
||||||
|
"(test|testing).*state machine",
|
||||||
|
"(test|testing).*actor",
|
||||||
|
"(test|testing).*xstate",
|
||||||
|
"(how to|how do I).*test.*xstate",
|
||||||
|
"(write|create|add).*test.*machine",
|
||||||
|
"xstate-audition.*test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fileTriggers": {
|
||||||
|
"pathPatterns": [
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/test/**/*.ts",
|
||||||
|
"**/test/**/*.js"
|
||||||
|
],
|
||||||
|
"contentPatterns": [
|
||||||
|
"from 'xstate-audition'",
|
||||||
|
"from \"xstate-audition\"",
|
||||||
|
"runUntilDone\\(",
|
||||||
|
"runUntilEmission\\(",
|
||||||
|
"runUntilTransition\\(",
|
||||||
|
"waitForSnapshot\\(",
|
||||||
|
"waitForEmission\\(",
|
||||||
|
"waitForTransition\\("
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
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
|
||||||
160
skills/xstate-v5/SKILL.md
Normal file
160
skills/xstate-v5/SKILL.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
description: Expert guidance for implementing and analyzing XState v5 state machines with TypeScript support and best practices
|
||||||
|
triggers:
|
||||||
|
- xstate
|
||||||
|
- state machine
|
||||||
|
- statechart
|
||||||
|
- createMachine
|
||||||
|
- createActor
|
||||||
|
- fsm implementation
|
||||||
|
- actor model
|
||||||
|
- state management xstate
|
||||||
|
---
|
||||||
|
|
||||||
|
# XState v5 Expert Skill
|
||||||
|
|
||||||
|
You are an expert in XState v5, a JavaScript/TypeScript library for creating, interpreting, and executing finite state machines and statecharts using the actor model. Use this knowledge to help implement and analyze XState v5 code with precision and adherence to best practices.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### State Machines & Statecharts
|
||||||
|
|
||||||
|
XState implements event-driven programming through state machines and statecharts, providing predictable and robust logic handling. Always:
|
||||||
|
|
||||||
|
- Model application logic as explicit states and transitions
|
||||||
|
- Use statecharts for complex hierarchical and parallel state management
|
||||||
|
- Ensure every state transition is intentional and documented
|
||||||
|
|
||||||
|
### Actor Model
|
||||||
|
|
||||||
|
XState uses the actor model for distributed, concurrent computation:
|
||||||
|
|
||||||
|
- **State machine actors**: Primary actors created from state machines
|
||||||
|
- **Promise actors**: Handle asynchronous operations
|
||||||
|
- **Transition actors**: Manage pure transitions
|
||||||
|
- **Callback actors**: Custom imperative logic
|
||||||
|
- **Observable actors**: Stream-based actors
|
||||||
|
|
||||||
|
## Quick Start Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setup, createActor, assign } from 'xstate';
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as { count: number },
|
||||||
|
events: {} as { type: 'INCREMENT' } | { type: 'DECREMENT' },
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
increment: assign({ count: ({ context }) => context.count + 1 }),
|
||||||
|
decrement: assign({ count: ({ context }) => context.count - 1 }),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
isPositive: ({ context }) => context.count > 0,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
id: 'counter',
|
||||||
|
initial: 'active',
|
||||||
|
context: { count: 0 },
|
||||||
|
states: {
|
||||||
|
active: {
|
||||||
|
on: {
|
||||||
|
INCREMENT: { actions: 'increment' },
|
||||||
|
DECREMENT: {
|
||||||
|
actions: 'decrement',
|
||||||
|
guard: 'isPositive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actor = createActor(machine);
|
||||||
|
actor.subscribe((snapshot) => console.log(snapshot.context));
|
||||||
|
actor.start();
|
||||||
|
actor.send({ type: 'INCREMENT' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Reference Documentation
|
||||||
|
|
||||||
|
For detailed implementation guidance, consult the comprehensive reference documentation:
|
||||||
|
|
||||||
|
### [Core API Reference](./references/core-api.md)
|
||||||
|
|
||||||
|
Complete API documentation including:
|
||||||
|
|
||||||
|
- Machine creation (`createMachine`, `setup`)
|
||||||
|
- Actor management and lifecycle
|
||||||
|
- Actions, guards, and services
|
||||||
|
- Utility functions and type helpers
|
||||||
|
|
||||||
|
### [Actors Reference](./references/actors.md)
|
||||||
|
|
||||||
|
Deep dive into the actor model:
|
||||||
|
|
||||||
|
- All actor types (state machine, promise, callback, transition, observable)
|
||||||
|
- Actor communication and orchestration
|
||||||
|
- Spawning vs invoking actors
|
||||||
|
- Error handling and persistence
|
||||||
|
|
||||||
|
### [Common Patterns](./references/patterns.md)
|
||||||
|
|
||||||
|
Production-ready patterns and solutions:
|
||||||
|
|
||||||
|
- Loading states with retry logic
|
||||||
|
- Form validation and submission
|
||||||
|
- Authentication flows
|
||||||
|
- Pagination, wizards, modals
|
||||||
|
- Debouncing and queue processing
|
||||||
|
|
||||||
|
### [TypeScript Integration](./references/typescript.md)
|
||||||
|
|
||||||
|
Complete TypeScript usage guide:
|
||||||
|
|
||||||
|
- Setup pattern with strong typing
|
||||||
|
- Type inference and helpers
|
||||||
|
- Generic machine factories
|
||||||
|
- Backend service types
|
||||||
|
|
||||||
|
### [Testing Strategies](./references/testing.md)
|
||||||
|
|
||||||
|
Backend testing approaches:
|
||||||
|
|
||||||
|
- Unit testing state machines
|
||||||
|
- Using xstate-audition for actor testing
|
||||||
|
- Mocking external services
|
||||||
|
- Testing async backend operations
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always use setup()** for better type inference and reusable logic
|
||||||
|
2. **Name all actions and guards** for clarity and reusability
|
||||||
|
3. **Use context for data, states for behavior**
|
||||||
|
4. **Keep machines focused** - one machine per logical unit
|
||||||
|
5. **Leverage TypeScript** for compile-time safety
|
||||||
|
6. **Avoid inline functions** in machine definitions. Used named guards and actions.
|
||||||
|
7. **Test with `xstate-audition`** for comprehensive coverage
|
||||||
|
8. **Use Promise actors** for asynchronous operations
|
||||||
|
|
||||||
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
|
1. **Don't mutate context directly** - always use `assign`
|
||||||
|
2. **Don't use side effects in guards** - guards should be pure
|
||||||
|
3. **Don't overuse nested states** - flatten when possible
|
||||||
|
4. **Don't ignore TypeScript errors** - they prevent runtime issues
|
||||||
|
5. **Don't mix concerns** - separate UI from business logic
|
||||||
|
6. **Don't use string events** when objects provide better typing
|
||||||
|
7. **Don't forget error handling** in async operations
|
||||||
|
8. **Don't use `setInterval`** in machine definitions. Use delays instead.
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. Use `enqueueActions()` for conditional actions instead of multiple transitions
|
||||||
|
2. Minimize context updates
|
||||||
|
3. Use lazy evaluation with function updaters
|
||||||
|
4. Leverage memoization for expensive computations
|
||||||
|
5. Split large machines into smaller actors
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Remember: XState excels at making complex logic predictable and maintainable. Always prioritize clarity and correctness over brevity.
|
||||||
93
skills/xstate-v5/references/README.md
Normal file
93
skills/xstate-v5/references/README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# XState v5 Reference Documentation
|
||||||
|
|
||||||
|
Comprehensive reference materials for XState v5 implementation and analysis in **backend applications**.
|
||||||
|
|
||||||
|
## Available References
|
||||||
|
|
||||||
|
### [Core API](./core-api.md)
|
||||||
|
|
||||||
|
Complete API reference for XState v5 including:
|
||||||
|
|
||||||
|
- Machine creation (`createMachine`, `setup`)
|
||||||
|
- Actor management (`createActor`, lifecycle methods)
|
||||||
|
- State configuration and transitions
|
||||||
|
- Actions, guards, and services
|
||||||
|
- Utility functions and type helpers
|
||||||
|
|
||||||
|
### [Actors](./actors.md)
|
||||||
|
|
||||||
|
Deep dive into the actor model:
|
||||||
|
|
||||||
|
- Actor types (state machine, promise, callback, transition, observable)
|
||||||
|
- Actor lifecycle and communication
|
||||||
|
- Spawning vs invoking actors
|
||||||
|
- Actor systems and orchestration
|
||||||
|
- Error handling and persistence
|
||||||
|
|
||||||
|
### [Common Patterns](./patterns.md)
|
||||||
|
|
||||||
|
Production-ready patterns and solutions:
|
||||||
|
|
||||||
|
- Loading states with retry logic
|
||||||
|
- Form validation and submission
|
||||||
|
- Authentication flows
|
||||||
|
- Pagination and data fetching
|
||||||
|
- Wizard/stepper interfaces
|
||||||
|
- Parallel states and history
|
||||||
|
- Debouncing and queue processing
|
||||||
|
- Modal/dialog management
|
||||||
|
- Connection handling with reconnection
|
||||||
|
|
||||||
|
### [TypeScript Integration](./typescript.md)
|
||||||
|
|
||||||
|
Complete TypeScript usage guide:
|
||||||
|
|
||||||
|
- Setup pattern with strong typing
|
||||||
|
- Type inference and helpers
|
||||||
|
- Generic machine factories
|
||||||
|
- Event assertions and discriminated unions
|
||||||
|
- Backend service types
|
||||||
|
- Migration from v4 typegen
|
||||||
|
|
||||||
|
### [Testing Strategies](./testing.md)
|
||||||
|
|
||||||
|
Backend testing approaches:
|
||||||
|
|
||||||
|
- Unit testing state machines
|
||||||
|
- Testing with xstate-audition
|
||||||
|
- Mocking external services and databases
|
||||||
|
- Testing async backend operations
|
||||||
|
- Performance testing for server environments
|
||||||
|
- Best practices for backend testing
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
For new XState v5 implementations, start with:
|
||||||
|
|
||||||
|
1. **[TypeScript Integration](./typescript.md)** - Set up proper typing
|
||||||
|
2. **[Core API](./core-api.md)** - Learn the fundamental APIs
|
||||||
|
3. **[Common Patterns](./patterns.md)** - Apply proven solutions
|
||||||
|
4. **[Testing Strategies](./testing.md)** - Ensure reliability
|
||||||
|
|
||||||
|
## External Resources
|
||||||
|
|
||||||
|
- [Official XState Documentation](https://stately.ai/docs/xstate)
|
||||||
|
- [XState Visualizer](https://stately.ai/viz)
|
||||||
|
- [XState Catalog](https://xstate-catalogue.com/)
|
||||||
|
- [xstate-audition](https://boneskull.github.io/xstate-audition/) - Model-based testing
|
||||||
|
|
||||||
|
## Version Requirements
|
||||||
|
|
||||||
|
- **XState**: v5.x
|
||||||
|
- **TypeScript**: 5.0 or greater
|
||||||
|
- **Node.js**: 18+ recommended
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To add or update reference documentation:
|
||||||
|
|
||||||
|
1. Follow the existing markdown structure
|
||||||
|
2. Include practical code examples
|
||||||
|
3. Document both best practices and anti-patterns
|
||||||
|
4. Keep examples TypeScript-first
|
||||||
|
5. Test all code examples for accuracy
|
||||||
548
skills/xstate-v5/references/actors.md
Normal file
548
skills/xstate-v5/references/actors.md
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
# XState v5 Actors Reference
|
||||||
|
|
||||||
|
## Actor Model Fundamentals
|
||||||
|
|
||||||
|
Actors are independent entities that:
|
||||||
|
|
||||||
|
- Maintain private, encapsulated state
|
||||||
|
- Communicate via asynchronous message passing (events)
|
||||||
|
- Process messages sequentially from an internal mailbox
|
||||||
|
- Can create new child actors
|
||||||
|
- Can only expose internal state through events or snapshots
|
||||||
|
|
||||||
|
## Actor Types Overview
|
||||||
|
|
||||||
|
| Actor Type | Receives Events | Sends Events | Spawns Actors | Has Output | Use Case |
|
||||||
|
| ------------- | --------------- | ------------ | ------------- | ---------- | ------------------- |
|
||||||
|
| State Machine | Yes | Yes | Yes | Yes | Complex state logic |
|
||||||
|
| Promise | No | Yes | No | Yes | Async operations |
|
||||||
|
| Transition | Yes | Yes | No | No | Pure reducers |
|
||||||
|
| Callback | Yes | Yes | No | No | Imperative logic |
|
||||||
|
| Observable | No | Yes | No | No | Streams |
|
||||||
|
|
||||||
|
## Creating and Managing Actors
|
||||||
|
|
||||||
|
### Basic Actor Creation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createActor } from 'xstate';
|
||||||
|
|
||||||
|
const actor = createActor(someLogic, {
|
||||||
|
id: 'myActor',
|
||||||
|
input: { initialData: 'value' },
|
||||||
|
snapshot: previousSnapshot, // For restoration
|
||||||
|
systemId: 'mySystem',
|
||||||
|
logger: console.log,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
actor.start(); // Must call to begin processing
|
||||||
|
actor.stop(); // Stops actor and all children
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor Subscription
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Simple observer function
|
||||||
|
const subscription = actor.subscribe((snapshot) => {
|
||||||
|
console.log('State:', snapshot.value);
|
||||||
|
console.log('Context:', snapshot.context);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Full observer object
|
||||||
|
actor.subscribe({
|
||||||
|
next(snapshot) {
|
||||||
|
console.log('New snapshot:', snapshot);
|
||||||
|
},
|
||||||
|
error(err) {
|
||||||
|
console.error('Actor error:', err);
|
||||||
|
},
|
||||||
|
complete() {
|
||||||
|
console.log('Actor completed');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
subscription.unsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Snapshot Access
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Synchronous snapshot access
|
||||||
|
const currentSnapshot = actor.getSnapshot();
|
||||||
|
|
||||||
|
// Persistable snapshot (serializable)
|
||||||
|
const persistedSnapshot = actor.getPersistedSnapshot();
|
||||||
|
localStorage.setItem('actor-state', JSON.stringify(persistedSnapshot));
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Machine Actors
|
||||||
|
|
||||||
|
The most powerful actor type, implementing full statechart semantics.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMachine, createActor } from 'xstate';
|
||||||
|
|
||||||
|
const toggleMachine = createMachine({
|
||||||
|
id: 'toggle',
|
||||||
|
initial: 'inactive',
|
||||||
|
context: { count: 0 },
|
||||||
|
states: {
|
||||||
|
inactive: {
|
||||||
|
on: {
|
||||||
|
TOGGLE: {
|
||||||
|
target: 'active',
|
||||||
|
actions: assign({ count: ({ context }) => context.count + 1 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
on: { TOGGLE: 'inactive' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actor = createActor(toggleMachine);
|
||||||
|
actor.start();
|
||||||
|
actor.send({ type: 'TOGGLE' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Promise Actors
|
||||||
|
|
||||||
|
Handle single async operations that resolve or reject.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fromPromise, createActor } from 'xstate';
|
||||||
|
|
||||||
|
// Basic promise actor
|
||||||
|
const fetchLogic = fromPromise(async () => {
|
||||||
|
const response = await fetch('/api/data');
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
// With input parameter
|
||||||
|
const fetchWithInput = fromPromise(async ({ input }) => {
|
||||||
|
const response = await fetch(`/api/data/${input.id}`);
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
// With abort signal
|
||||||
|
const cancellableFetch = fromPromise(async ({ input, signal }) => {
|
||||||
|
const response = await fetch('/api/data', { signal });
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const actor = createActor(fetchWithInput, {
|
||||||
|
input: { id: '123' },
|
||||||
|
});
|
||||||
|
|
||||||
|
actor.subscribe({
|
||||||
|
next: (snapshot) => {
|
||||||
|
if (snapshot.status === 'done') {
|
||||||
|
console.log('Result:', snapshot.output);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => console.error('Failed:', err),
|
||||||
|
});
|
||||||
|
|
||||||
|
actor.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transition Actors
|
||||||
|
|
||||||
|
Pure reducers that transform state based on events.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fromTransition, createActor } from 'xstate';
|
||||||
|
|
||||||
|
const counterLogic = fromTransition(
|
||||||
|
(state, event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'INC':
|
||||||
|
return { ...state, count: state.count + 1 };
|
||||||
|
case 'DEC':
|
||||||
|
return { ...state, count: state.count - 1 };
|
||||||
|
case 'RESET':
|
||||||
|
return { count: 0 };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ count: 0 }, // Initial state
|
||||||
|
);
|
||||||
|
|
||||||
|
const actor = createActor(counterLogic);
|
||||||
|
actor.start();
|
||||||
|
actor.send({ type: 'INC' });
|
||||||
|
console.log(actor.getSnapshot().context.count); // 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Callback Actors
|
||||||
|
|
||||||
|
Imperative actors for complex side effects and cleanup.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fromCallback, createActor } from 'xstate';
|
||||||
|
|
||||||
|
const intervalLogic = fromCallback(({ sendBack, receive, input }) => {
|
||||||
|
// Setup
|
||||||
|
console.log('Starting with input:', input);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
sendBack({ type: 'TICK', timestamp: Date.now() });
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Event handler
|
||||||
|
receive((event) => {
|
||||||
|
if (event.type === 'STOP_TICKING') {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup function (called on stop)
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
console.log('Cleaned up');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const actor = createActor(intervalLogic, {
|
||||||
|
input: { message: 'Timer started' },
|
||||||
|
});
|
||||||
|
|
||||||
|
actor.subscribe((snapshot) => {
|
||||||
|
console.log('Snapshot:', snapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
actor.start();
|
||||||
|
// Later: actor.send({ type: 'STOP_TICKING' });
|
||||||
|
// Or: actor.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observable Actors
|
||||||
|
|
||||||
|
Integration with reactive streams (RxJS, etc.).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fromObservable, createActor } from 'xstate';
|
||||||
|
import { interval } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
const timerLogic = fromObservable(() =>
|
||||||
|
interval(1000).pipe(map((i) => ({ type: 'TICK', count: i }))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const actor = createActor(timerLogic);
|
||||||
|
actor.subscribe((snapshot) => {
|
||||||
|
console.log('Event from observable:', snapshot.context);
|
||||||
|
});
|
||||||
|
actor.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invoking vs Spawning Actors
|
||||||
|
|
||||||
|
### Invoked Actors (State-based)
|
||||||
|
|
||||||
|
Tied to state lifecycle - start on entry, stop on exit.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const machine = createMachine({
|
||||||
|
states: {
|
||||||
|
loading: {
|
||||||
|
invoke: {
|
||||||
|
id: 'fetcher',
|
||||||
|
src: fromPromise(async () => {
|
||||||
|
const res = await fetch('/api');
|
||||||
|
return res.json();
|
||||||
|
}),
|
||||||
|
input: ({ context }) => ({ url: context.apiUrl }),
|
||||||
|
onDone: {
|
||||||
|
target: 'success',
|
||||||
|
actions: assign({
|
||||||
|
data: ({ event }) => event.output,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'failure',
|
||||||
|
actions: assign({
|
||||||
|
error: ({ event }) => event.error,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onSnapshot: {
|
||||||
|
actions: ({ event }) => {
|
||||||
|
console.log('Invoked actor snapshot:', event.snapshot);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: {},
|
||||||
|
failure: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spawned Actors (Action-based)
|
||||||
|
|
||||||
|
Created dynamically, managed manually.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { spawn, stopChild } from 'xstate';
|
||||||
|
|
||||||
|
const parentMachine = createMachine({
|
||||||
|
context: {
|
||||||
|
todos: [],
|
||||||
|
todoActors: {},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
ADD_TODO: {
|
||||||
|
actions: assign({
|
||||||
|
todoActors: ({ context, spawn }) => {
|
||||||
|
const id = Math.random().toString();
|
||||||
|
const todoActor = spawn(todoMachine, {
|
||||||
|
id: `todo-${id}`,
|
||||||
|
input: { text: event.text },
|
||||||
|
syncSnapshot: true, // Sync snapshots with parent
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...context.todoActors,
|
||||||
|
[id]: todoActor,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
REMOVE_TODO: {
|
||||||
|
actions: [
|
||||||
|
stopChild(({ context, event }) => `todo-${event.id}`),
|
||||||
|
assign({
|
||||||
|
todoActors: ({ context, event }) => {
|
||||||
|
const { [event.id]: removed, ...rest } = context.todoActors;
|
||||||
|
return rest;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actor Communication
|
||||||
|
|
||||||
|
### Sending Between Actors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { sendTo, sendParent } from 'xstate';
|
||||||
|
|
||||||
|
const childMachine = createMachine({
|
||||||
|
entry: [
|
||||||
|
// Send to parent
|
||||||
|
sendParent({ type: 'CHILD_READY' }),
|
||||||
|
// Or use emit (preferred in v5)
|
||||||
|
emit({ type: 'CHILD_READY' }),
|
||||||
|
],
|
||||||
|
on: {
|
||||||
|
PING: {
|
||||||
|
actions: emit({ type: 'PONG' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentMachine = createMachine({
|
||||||
|
invoke: {
|
||||||
|
id: 'child',
|
||||||
|
src: childMachine,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
SEND_TO_CHILD: {
|
||||||
|
actions: sendTo('child', { type: 'PING' }),
|
||||||
|
},
|
||||||
|
PONG: {
|
||||||
|
actions: log('Received pong from child'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor System
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Access system from any actor
|
||||||
|
const actor = createActor(machine);
|
||||||
|
actor.start();
|
||||||
|
|
||||||
|
// Get actor from system
|
||||||
|
const childActor = actor.system.get('childId');
|
||||||
|
|
||||||
|
// Inspect all actors in system
|
||||||
|
actor.system._set.forEach((actor, id) => {
|
||||||
|
console.log(`Actor ${id}:`, actor.getSnapshot());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Waiting for Actors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { waitFor } from 'xstate';
|
||||||
|
|
||||||
|
// Wait for condition
|
||||||
|
const doneSnapshot = await waitFor(
|
||||||
|
actor,
|
||||||
|
(snapshot) => snapshot.status === 'done',
|
||||||
|
{ timeout: 5000 }, // Optional timeout
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to promise
|
||||||
|
import { toPromise } from 'xstate';
|
||||||
|
|
||||||
|
const result = await toPromise(actor);
|
||||||
|
// Resolves with output when done
|
||||||
|
// Rejects with error when failed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Promise Actor Errors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const errorProneLogic = fromPromise(async () => {
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
});
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
invoke: {
|
||||||
|
src: errorProneLogic,
|
||||||
|
onError: {
|
||||||
|
target: 'errorState',
|
||||||
|
actions: ({ event }) => {
|
||||||
|
console.error('Error:', event.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Callback Actor Error Reporting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const callbackWithErrors = fromCallback(({ sendBack }) => {
|
||||||
|
try {
|
||||||
|
// risky operation
|
||||||
|
} catch (error) {
|
||||||
|
sendBack({ type: 'error.platform', error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actor Persistence
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Save actor state
|
||||||
|
const actor = createActor(machine);
|
||||||
|
actor.subscribe(() => {
|
||||||
|
const snapshot = actor.getPersistedSnapshot();
|
||||||
|
localStorage.setItem('actor-state', JSON.stringify(snapshot));
|
||||||
|
});
|
||||||
|
actor.start();
|
||||||
|
|
||||||
|
// Restore actor state
|
||||||
|
const savedState = localStorage.getItem('actor-state');
|
||||||
|
if (savedState) {
|
||||||
|
const restoredActor = createActor(machine, {
|
||||||
|
snapshot: JSON.parse(savedState),
|
||||||
|
});
|
||||||
|
restoredActor.start();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Choose the Right Actor Type
|
||||||
|
|
||||||
|
- **State machines**: Complex logic with multiple states
|
||||||
|
- **Promises**: Single async operations
|
||||||
|
- **Transitions**: Pure state reducers
|
||||||
|
- **Callbacks**: Imperative code with cleanup
|
||||||
|
- **Observables**: Stream integrations
|
||||||
|
|
||||||
|
### 2. Actor Lifecycle Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Always subscribe before starting
|
||||||
|
const subscription = actor.subscribe(observer);
|
||||||
|
actor.start();
|
||||||
|
|
||||||
|
// Clean up properly
|
||||||
|
subscription.unsubscribe();
|
||||||
|
actor.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Error Boundaries
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Always handle errors in parent
|
||||||
|
const parentMachine = createMachine({
|
||||||
|
invoke: {
|
||||||
|
src: childLogic,
|
||||||
|
onError: {
|
||||||
|
actions: [
|
||||||
|
log('Child failed'),
|
||||||
|
assign({ error: ({ event }) => event.error }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Input Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const actorWithInput = fromPromise(async ({ input }) => {
|
||||||
|
// Validate input
|
||||||
|
if (!input?.id) {
|
||||||
|
throw new Error('ID required');
|
||||||
|
}
|
||||||
|
return fetchData(input.id);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Sync vs Async Snapshots
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Spawned actors can sync snapshots with parent
|
||||||
|
spawn(childMachine, {
|
||||||
|
id: 'child',
|
||||||
|
syncSnapshot: true, // Parent re-renders on child changes
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Actors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createActor } from 'xstate';
|
||||||
|
import { expect, test } from 'vitest';
|
||||||
|
|
||||||
|
test('actor processes events correctly', async () => {
|
||||||
|
const actor = createActor(machine);
|
||||||
|
|
||||||
|
const states = [];
|
||||||
|
actor.subscribe((snapshot) => {
|
||||||
|
states.push(snapshot.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
actor.start();
|
||||||
|
actor.send({ type: 'EVENT1' });
|
||||||
|
actor.send({ type: 'EVENT2' });
|
||||||
|
|
||||||
|
expect(states).toEqual(['initial', 'state1', 'state2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('promise actor resolves', async () => {
|
||||||
|
const actor = createActor(promiseLogic);
|
||||||
|
actor.start();
|
||||||
|
|
||||||
|
const result = await toPromise(actor);
|
||||||
|
expect(result).toEqual({ data: 'expected' });
|
||||||
|
});
|
||||||
|
```
|
||||||
495
skills/xstate-v5/references/core-api.md
Normal file
495
skills/xstate-v5/references/core-api.md
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
# XState v5 Core API Reference
|
||||||
|
|
||||||
|
## Machine Creation
|
||||||
|
|
||||||
|
### `createMachine(config)`
|
||||||
|
|
||||||
|
Creates a state machine configuration.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMachine } from 'xstate';
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
id: 'machineId', // Unique identifier
|
||||||
|
initial: 'stateName', // Initial state
|
||||||
|
context: {}, // Initial context data
|
||||||
|
types: {}, // TypeScript type definitions
|
||||||
|
states: {}, // State definitions
|
||||||
|
on: {}, // Global transitions
|
||||||
|
entry: [], // Entry actions
|
||||||
|
exit: [], // Exit actions
|
||||||
|
after: {}, // Delayed transitions
|
||||||
|
always: [], // Eventless transitions
|
||||||
|
invoke: {}, // Invoked services
|
||||||
|
tags: [], // State tags
|
||||||
|
description: '', // Machine description
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `setup(config)`
|
||||||
|
|
||||||
|
Configures reusable machine logic with strong typing.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setup } from 'xstate';
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as ContextType,
|
||||||
|
events: {} as EventType,
|
||||||
|
input: {} as InputType,
|
||||||
|
output: {} as OutputType,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Named action implementations
|
||||||
|
actionName: (context, event, meta) => {
|
||||||
|
/* ... */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
// Named guard implementations
|
||||||
|
guardName: (context, event, meta) => boolean,
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
// Actor logic definitions
|
||||||
|
actorName: fromPromise(() => fetch('/api')),
|
||||||
|
},
|
||||||
|
delays: {
|
||||||
|
// Named delay functions
|
||||||
|
delayName: (context, event) => 1000,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
// Machine configuration
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actor Creation and Management
|
||||||
|
|
||||||
|
### `createActor(logic, options?)`
|
||||||
|
|
||||||
|
Creates an actor instance from machine or actor logic.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const actor = createActor(machine, {
|
||||||
|
id: 'actorId', // Actor identifier
|
||||||
|
input: {}, // Initial input
|
||||||
|
snapshot: {}, // Restored snapshot
|
||||||
|
systemId: 'systemId', // Actor system ID
|
||||||
|
logger: console.log, // Custom logger
|
||||||
|
inspect: (event) => {}, // Inspection handler
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor Methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Lifecycle
|
||||||
|
actor.start(); // Start the actor
|
||||||
|
actor.stop(); // Stop the actor
|
||||||
|
actor.getSnapshot(); // Get current snapshot
|
||||||
|
actor.getPersistedSnapshot(); // Get persistable snapshot
|
||||||
|
|
||||||
|
// Communication
|
||||||
|
actor.send(event); // Send an event
|
||||||
|
actor.send({ type: 'EVENT' }); // Send event object
|
||||||
|
|
||||||
|
// Subscription
|
||||||
|
const subscription = actor.subscribe(observer);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
|
||||||
|
// System
|
||||||
|
actor.system; // Access actor system
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Configuration
|
||||||
|
|
||||||
|
### State Node Properties
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
type: 'atomic' | 'compound' | 'parallel' | 'final' | 'history',
|
||||||
|
initial: 'childStateName', // For compound states
|
||||||
|
context: {}, // State-specific context
|
||||||
|
states: {}, // Child states
|
||||||
|
on: {}, // Transitions
|
||||||
|
entry: [], // Entry actions
|
||||||
|
exit: [], // Exit actions
|
||||||
|
always: [], // Eventless transitions
|
||||||
|
after: {}, // Delayed transitions
|
||||||
|
invoke: {}, // Invoked services
|
||||||
|
tags: [], // State tags
|
||||||
|
description: '', // State description
|
||||||
|
meta: {}, // Metadata
|
||||||
|
history: 'shallow' | 'deep', // For history states
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
target: 'stateName' | ['state1', 'state2'], // Target state(s)
|
||||||
|
guard: 'guardName' | guardFunction, // Condition
|
||||||
|
actions: ['action1', 'action2'], // Actions to execute
|
||||||
|
reenter: boolean, // Re-enter state
|
||||||
|
description: 'Transition description', // Documentation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
### `assign(assignment)`
|
||||||
|
|
||||||
|
Updates machine context immutably.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Function updater
|
||||||
|
assign({
|
||||||
|
count: ({ context }) => context.count + 1,
|
||||||
|
user: ({ context, event }) => event.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Object updater
|
||||||
|
assign({ count: 5, user: null });
|
||||||
|
|
||||||
|
// Single property
|
||||||
|
assign(({ context }) => ({
|
||||||
|
...context,
|
||||||
|
modified: true,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### `raise(event)`
|
||||||
|
|
||||||
|
Raises an event internally.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
raise({ type: 'INTERNAL_EVENT' });
|
||||||
|
raise(({ context }) => ({
|
||||||
|
type: 'DYNAMIC_EVENT',
|
||||||
|
data: context.someValue,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### `sendTo(actor, event)`
|
||||||
|
|
||||||
|
Sends event to another actor.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
sendTo('actorId', { type: 'MESSAGE' });
|
||||||
|
sendTo(({ context }) => context.someActor, { type: 'EVENT' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `emit(event)`
|
||||||
|
|
||||||
|
Emits an event to parent actor.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
emit({ type: 'CHILD_EVENT', data: value });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `log(message)`
|
||||||
|
|
||||||
|
Logs a message (for debugging).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
log('State entered');
|
||||||
|
log(({ context }) => `Count: ${context.count}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `stop(actorId)`
|
||||||
|
|
||||||
|
Stops a spawned actor.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
stop('childActorId');
|
||||||
|
```
|
||||||
|
|
||||||
|
### `cancel(sendId)`
|
||||||
|
|
||||||
|
Cancels a delayed send.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
cancel('delayedSendId');
|
||||||
|
```
|
||||||
|
|
||||||
|
### `enqueueActions(callback)`
|
||||||
|
|
||||||
|
Enqueues actions conditionally at runtime. Replaces v4's `pure()` and `choose()`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { enqueueActions } from 'xstate';
|
||||||
|
|
||||||
|
// Basic conditional actions
|
||||||
|
entry: enqueueActions(({ context, event, enqueue, check }) => {
|
||||||
|
// Conditionally enqueue actions
|
||||||
|
if (context.count > 0) {
|
||||||
|
enqueue('logPositive');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use check() for guards
|
||||||
|
if (check({ type: 'hasPermission' })) {
|
||||||
|
enqueue('performSecureAction');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue multiple actions
|
||||||
|
enqueue([
|
||||||
|
{ type: 'log', params: { message: 'Processing' } },
|
||||||
|
'processData',
|
||||||
|
assign({ processing: true }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// With parameters
|
||||||
|
enqueueActions(({ context, enqueue }, params: { threshold: number }) => {
|
||||||
|
if (context.value > params.threshold) {
|
||||||
|
enqueue('handleHighValue');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guards
|
||||||
|
|
||||||
|
### Guard Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Inline guard
|
||||||
|
guard: ({ context, event }) => context.count > 0;
|
||||||
|
|
||||||
|
// Named guard
|
||||||
|
guard: 'isValid';
|
||||||
|
|
||||||
|
// Negated guard
|
||||||
|
guard: not('isInvalid');
|
||||||
|
|
||||||
|
// Combined guards
|
||||||
|
guard: and(['isValid', 'isAuthorized']);
|
||||||
|
guard: or(['isAdmin', 'isOwner']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Guard Helpers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { not, and, or, stateIn } from 'xstate';
|
||||||
|
|
||||||
|
not(guard); // Negates a guard
|
||||||
|
and([guard1, guard2]); // All must be true
|
||||||
|
or([guard1, guard2]); // At least one must be true
|
||||||
|
stateIn('state.path'); // Checks if in state
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invoked Services
|
||||||
|
|
||||||
|
### Service Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
invoke: {
|
||||||
|
id: 'serviceId',
|
||||||
|
src: 'serviceName' | actorLogic,
|
||||||
|
input: ({ context, event }) => ({}),
|
||||||
|
onDone: {
|
||||||
|
target: 'success',
|
||||||
|
actions: assign({ data: ({ event }) => event.output }),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'failure',
|
||||||
|
actions: assign({ error: ({ event }) => event.error }),
|
||||||
|
},
|
||||||
|
onSnapshot: {
|
||||||
|
actions: ({ event }) => console.log(event.snapshot),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Invocations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
invoke: [
|
||||||
|
{ id: 'service1', src: 'api1' },
|
||||||
|
{ id: 'service2', src: 'api2' },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spawning Actors
|
||||||
|
|
||||||
|
### `spawn(logic, options?)`
|
||||||
|
|
||||||
|
Spawns a child actor.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { spawn } from 'xstate';
|
||||||
|
|
||||||
|
// In an action
|
||||||
|
spawn(childMachine, {
|
||||||
|
id: 'childId',
|
||||||
|
systemId: 'childSystem',
|
||||||
|
input: { initial: 'data' },
|
||||||
|
syncSnapshot: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `stopChild(actorId)`
|
||||||
|
|
||||||
|
Stops a spawned child actor.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
stopChild('childId');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delayed Transitions
|
||||||
|
|
||||||
|
### After Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
after: {
|
||||||
|
1000: 'timeout', // Fixed delay
|
||||||
|
DELAY_NAME: 'delayed', // Named delay
|
||||||
|
[({ context }) => context.delay]: 'dynamic', // Dynamic delay
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eventless Transitions
|
||||||
|
|
||||||
|
### Always Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
always: [
|
||||||
|
{ target: 'state1', guard: 'condition1' },
|
||||||
|
{ target: 'state2', guard: 'condition2' },
|
||||||
|
{ target: 'default' }, // Fallback
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Functions
|
||||||
|
|
||||||
|
### `waitFor(actor, predicate, options?)`
|
||||||
|
|
||||||
|
Waits for an actor to reach a specific condition.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const snapshot = await waitFor(actor, (snapshot) => snapshot.matches('done'), {
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `toPromise(actor)`
|
||||||
|
|
||||||
|
Converts an actor to a Promise.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await toPromise(actor);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `createEmptyActor()`
|
||||||
|
|
||||||
|
Creates an actor that does nothing.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const emptyActor = createEmptyActor();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Helpers
|
||||||
|
|
||||||
|
### `ActorRefFrom<T>`
|
||||||
|
|
||||||
|
Gets the ActorRef type from logic.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MyActorRef = ActorRefFrom<typeof machine>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `SnapshotFrom<T>`
|
||||||
|
|
||||||
|
Gets the Snapshot type from logic or ActorRef.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MySnapshot = SnapshotFrom<typeof machine>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `EventFromLogic<T>`
|
||||||
|
|
||||||
|
Gets the event union type from logic.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MyEvents = EventFromLogic<typeof machine>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `StateValueFrom<T>`
|
||||||
|
|
||||||
|
Gets the state value type from logic.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MyStateValue = StateValueFrom<typeof machine>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ContextFrom<T>`
|
||||||
|
|
||||||
|
Gets the context type from logic.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MyContext = ContextFrom<typeof machine>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inspection and Debugging
|
||||||
|
|
||||||
|
### Inspector API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const actor = createActor(machine, {
|
||||||
|
inspect: (inspectionEvent) => {
|
||||||
|
if (inspectionEvent.type === '@xstate.snapshot') {
|
||||||
|
console.log('Snapshot:', inspectionEvent.snapshot);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inspection Event Types
|
||||||
|
|
||||||
|
- `@xstate.actor` - Actor created/stopped
|
||||||
|
- `@xstate.snapshot` - Snapshot updated
|
||||||
|
- `@xstate.event` - Event processed
|
||||||
|
- `@xstate.microstep` - Microstep taken
|
||||||
|
|
||||||
|
## State Methods
|
||||||
|
|
||||||
|
### `state.matches(stateValue)`
|
||||||
|
|
||||||
|
Checks if in a specific state.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (state.matches('loading')) {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
if (state.matches({ form: 'valid' })) {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `state.hasTag(tag)`
|
||||||
|
|
||||||
|
Checks if state has a tag.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (state.hasTag('loading')) {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `state.can(event)`
|
||||||
|
|
||||||
|
Checks if an event can cause a transition.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (state.can({ type: 'SUBMIT' })) {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `state.nextEvents`
|
||||||
|
|
||||||
|
Gets available events from current state.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const events = state.nextEvents; // ['SUBMIT', 'CANCEL']
|
||||||
|
```
|
||||||
929
skills/xstate-v5/references/patterns.md
Normal file
929
skills/xstate-v5/references/patterns.md
Normal file
@@ -0,0 +1,929 @@
|
|||||||
|
# XState v5 Common Patterns
|
||||||
|
|
||||||
|
## Conditional Actions Pattern
|
||||||
|
|
||||||
|
### Using enqueueActions (replaces v4's pure/choose)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const machine = createMachine({
|
||||||
|
context: {
|
||||||
|
count: 0,
|
||||||
|
user: null,
|
||||||
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
PROCESS: {
|
||||||
|
actions: enqueueActions(({ context, event, enqueue, check }) => {
|
||||||
|
// Conditional logic at runtime
|
||||||
|
if (context.count > 10) {
|
||||||
|
enqueue('notifyHighCount');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check guards
|
||||||
|
if (check('isAuthenticated')) {
|
||||||
|
enqueue('processAuthenticatedUser');
|
||||||
|
|
||||||
|
if (context.isAdmin) {
|
||||||
|
enqueue('grantAdminPrivileges');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enqueue('redirectToLogin');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic action selection
|
||||||
|
const action = context.count % 2 === 0 ? 'handleEven' : 'handleOdd';
|
||||||
|
enqueue(action);
|
||||||
|
|
||||||
|
// Always executed
|
||||||
|
enqueue(assign({ lastProcessed: Date.now() }));
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loading States Pattern
|
||||||
|
|
||||||
|
### Basic Loading Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fetchMachine = createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: {
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
FETCH: 'loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
entry: assign({ error: null }), // Clear previous errors
|
||||||
|
invoke: {
|
||||||
|
src: 'fetchData',
|
||||||
|
onDone: {
|
||||||
|
target: 'success',
|
||||||
|
actions: assign({
|
||||||
|
data: ({ event }) => event.output,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'failure',
|
||||||
|
actions: assign({
|
||||||
|
error: ({ event }) => event.error,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
on: {
|
||||||
|
REFETCH: 'loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
on: {
|
||||||
|
RETRY: 'loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Retry Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const retryMachine = createMachine({
|
||||||
|
context: {
|
||||||
|
retries: 0,
|
||||||
|
maxRetries: 3,
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
loading: {
|
||||||
|
invoke: {
|
||||||
|
src: 'fetchData',
|
||||||
|
onDone: {
|
||||||
|
target: 'success',
|
||||||
|
actions: assign({
|
||||||
|
data: ({ event }) => event.output,
|
||||||
|
retries: 0, // Reset on success
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'retrying',
|
||||||
|
guard: ({ context }) => context.retries < context.maxRetries,
|
||||||
|
actions: assign({
|
||||||
|
retries: ({ context }) => context.retries + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'failure',
|
||||||
|
actions: assign({
|
||||||
|
error: ({ event }) => event.error,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
retrying: {
|
||||||
|
after: {
|
||||||
|
1000: 'loading', // Retry after 1 second
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: {},
|
||||||
|
failure: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Validation Pattern
|
||||||
|
|
||||||
|
### Multi-field Form Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const formMachine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as {
|
||||||
|
fields: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
errors: {
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
};
|
||||||
|
touched: {
|
||||||
|
email: boolean;
|
||||||
|
password: boolean;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
isEmailValid: ({ context }) =>
|
||||||
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(context.fields.email),
|
||||||
|
isPasswordValid: ({ context }) => context.fields.password.length >= 8,
|
||||||
|
isFormValid: ({ context }) =>
|
||||||
|
!context.errors.email && !context.errors.password,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
initial: 'editing',
|
||||||
|
context: {
|
||||||
|
fields: { email: '', password: '' },
|
||||||
|
errors: {},
|
||||||
|
touched: { email: false, password: false },
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
UPDATE_EMAIL: {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
fields: ({ context, event }) => ({
|
||||||
|
...context.fields,
|
||||||
|
email: event.value,
|
||||||
|
}),
|
||||||
|
touched: ({ context }) => ({
|
||||||
|
...context.touched,
|
||||||
|
email: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'validateEmail',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
UPDATE_PASSWORD: {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
fields: ({ context, event }) => ({
|
||||||
|
...context.fields,
|
||||||
|
password: event.value,
|
||||||
|
}),
|
||||||
|
touched: ({ context }) => ({
|
||||||
|
...context.touched,
|
||||||
|
password: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'validatePassword',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
SUBMIT: {
|
||||||
|
target: 'validating',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validating: {
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
target: 'submitting',
|
||||||
|
guard: 'isFormValid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'editing',
|
||||||
|
actions: assign({
|
||||||
|
touched: { email: true, password: true },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
submitting: {
|
||||||
|
invoke: {
|
||||||
|
src: 'submitForm',
|
||||||
|
input: ({ context }) => context.fields,
|
||||||
|
onDone: {
|
||||||
|
target: 'success',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'editing',
|
||||||
|
actions: assign({
|
||||||
|
errors: ({ event }) => event.error.fieldErrors || {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: 'final',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Flow Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const authMachine = createMachine({
|
||||||
|
initial: 'checkingAuth',
|
||||||
|
context: {
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
checkingAuth: {
|
||||||
|
invoke: {
|
||||||
|
src: 'checkStoredAuth',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'authenticated',
|
||||||
|
guard: ({ event }) => !!event.output.token,
|
||||||
|
actions: assign({
|
||||||
|
user: ({ event }) => event.output.user,
|
||||||
|
token: ({ event }) => event.output.token,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'unauthenticated',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unauthenticated: {
|
||||||
|
on: {
|
||||||
|
LOGIN: 'authenticating',
|
||||||
|
REGISTER: 'registering',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authenticating: {
|
||||||
|
invoke: {
|
||||||
|
src: 'authenticate',
|
||||||
|
input: ({ event }) => ({
|
||||||
|
email: event.email,
|
||||||
|
password: event.password,
|
||||||
|
}),
|
||||||
|
onDone: {
|
||||||
|
target: 'authenticated',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
user: ({ event }) => event.output.user,
|
||||||
|
token: ({ event }) => event.output.token,
|
||||||
|
}),
|
||||||
|
'storeAuth',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'unauthenticated',
|
||||||
|
actions: 'showError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registering: {
|
||||||
|
// Similar to authenticating
|
||||||
|
},
|
||||||
|
authenticated: {
|
||||||
|
on: {
|
||||||
|
LOGOUT: {
|
||||||
|
target: 'unauthenticated',
|
||||||
|
actions: [assign({ user: null, token: null }), 'clearStoredAuth'],
|
||||||
|
},
|
||||||
|
TOKEN_EXPIRED: 'refreshing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refreshing: {
|
||||||
|
invoke: {
|
||||||
|
src: 'refreshToken',
|
||||||
|
onDone: {
|
||||||
|
target: 'authenticated',
|
||||||
|
actions: assign({
|
||||||
|
token: ({ event }) => event.output.token,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'unauthenticated',
|
||||||
|
actions: ['clearStoredAuth'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const paginationMachine = createMachine({
|
||||||
|
context: {
|
||||||
|
items: [],
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
totalItems: 0,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
LOAD_PAGE: {
|
||||||
|
target: 'loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
entry: assign({ isLoading: true }),
|
||||||
|
exit: assign({ isLoading: false }),
|
||||||
|
invoke: {
|
||||||
|
src: 'fetchPage',
|
||||||
|
input: ({ context, event }) => ({
|
||||||
|
page: event.page || context.currentPage,
|
||||||
|
pageSize: context.pageSize,
|
||||||
|
}),
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: assign({
|
||||||
|
items: ({ event }) => event.output.items,
|
||||||
|
currentPage: ({ event }) => event.output.page,
|
||||||
|
totalPages: ({ event }) => event.output.totalPages,
|
||||||
|
totalItems: ({ event }) => event.output.totalItems,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
on: {
|
||||||
|
RETRY: 'loading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
NEXT_PAGE: {
|
||||||
|
target: '.loading',
|
||||||
|
guard: ({ context }) => context.currentPage < context.totalPages,
|
||||||
|
actions: assign({
|
||||||
|
currentPage: ({ context }) => context.currentPage + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
PREV_PAGE: {
|
||||||
|
target: '.loading',
|
||||||
|
guard: ({ context }) => context.currentPage > 1,
|
||||||
|
actions: assign({
|
||||||
|
currentPage: ({ context }) => context.currentPage - 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
GO_TO_PAGE: {
|
||||||
|
target: '.loading',
|
||||||
|
guard: ({ context, event }) =>
|
||||||
|
event.page > 0 && event.page <= context.totalPages,
|
||||||
|
actions: assign({
|
||||||
|
currentPage: ({ event }) => event.page,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wizard/Stepper Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wizardMachine = createMachine({
|
||||||
|
initial: 'step1',
|
||||||
|
context: {
|
||||||
|
step1Data: null,
|
||||||
|
step2Data: null,
|
||||||
|
step3Data: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
step1: {
|
||||||
|
initial: 'editing',
|
||||||
|
states: {
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
SAVE: {
|
||||||
|
target: 'validated',
|
||||||
|
actions: assign({
|
||||||
|
step1Data: ({ event }) => event.data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validated: {
|
||||||
|
type: 'final',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: 'step2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
step2: {
|
||||||
|
initial: 'editing',
|
||||||
|
states: {
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
SAVE: {
|
||||||
|
target: 'validated',
|
||||||
|
actions: assign({
|
||||||
|
step2Data: ({ event }) => event.data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validated: {
|
||||||
|
type: 'final',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
BACK: 'step1',
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: 'step3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
step3: {
|
||||||
|
initial: 'editing',
|
||||||
|
states: {
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
SAVE: {
|
||||||
|
target: 'validated',
|
||||||
|
actions: assign({
|
||||||
|
step3Data: ({ event }) => event.data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validated: {
|
||||||
|
type: 'final',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
BACK: 'step2',
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: 'review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
on: {
|
||||||
|
EDIT_STEP1: 'step1',
|
||||||
|
EDIT_STEP2: 'step2',
|
||||||
|
EDIT_STEP3: 'step3',
|
||||||
|
SUBMIT: 'submitting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submitting: {
|
||||||
|
invoke: {
|
||||||
|
src: 'submitWizard',
|
||||||
|
input: ({ context }) => ({
|
||||||
|
step1: context.step1Data,
|
||||||
|
step2: context.step2Data,
|
||||||
|
step3: context.step3Data,
|
||||||
|
}),
|
||||||
|
onDone: {
|
||||||
|
target: 'complete',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'review',
|
||||||
|
actions: 'showError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
complete: {
|
||||||
|
type: 'final',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel States Pattern
|
||||||
|
|
||||||
|
### Upload/Download Manager
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fileManagerMachine = createMachine({
|
||||||
|
type: 'parallel',
|
||||||
|
context: {
|
||||||
|
uploads: [],
|
||||||
|
downloads: [],
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
upload: {
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
START_UPLOAD: 'uploading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uploading: {
|
||||||
|
invoke: {
|
||||||
|
src: 'uploadFiles',
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: 'addUploadedFiles',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'uploadError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uploadError: {
|
||||||
|
on: {
|
||||||
|
RETRY_UPLOAD: 'uploading',
|
||||||
|
CANCEL_UPLOAD: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
START_DOWNLOAD: 'downloading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
downloading: {
|
||||||
|
invoke: {
|
||||||
|
src: 'downloadFiles',
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: 'addDownloadedFiles',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'downloadError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
downloadError: {
|
||||||
|
on: {
|
||||||
|
RETRY_DOWNLOAD: 'downloading',
|
||||||
|
CANCEL_DOWNLOAD: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## History States Pattern
|
||||||
|
|
||||||
|
### Editor with History
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const editorMachine = createMachine({
|
||||||
|
initial: 'editing',
|
||||||
|
context: {
|
||||||
|
content: '',
|
||||||
|
mode: 'text',
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
editing: {
|
||||||
|
initial: 'text',
|
||||||
|
states: {
|
||||||
|
text: {
|
||||||
|
on: {
|
||||||
|
SWITCH_TO_VISUAL: 'visual',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visual: {
|
||||||
|
on: {
|
||||||
|
SWITCH_TO_TEXT: 'text',
|
||||||
|
OPEN_SETTINGS: '#editor.settings',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
type: 'history',
|
||||||
|
history: 'shallow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
SAVE: 'saving',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
on: {
|
||||||
|
CLOSE: '#editor.editing.history', // Return to previous state
|
||||||
|
APPLY: {
|
||||||
|
target: '#editor.editing.history',
|
||||||
|
actions: 'applySettings',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
saving: {
|
||||||
|
invoke: {
|
||||||
|
src: 'saveContent',
|
||||||
|
onDone: {
|
||||||
|
target: 'editing',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'editing',
|
||||||
|
actions: 'showSaveError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debouncing Pattern
|
||||||
|
|
||||||
|
### Search with Debounce
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const searchMachine = createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: {
|
||||||
|
query: '',
|
||||||
|
results: [],
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
SEARCH: {
|
||||||
|
target: 'debouncing',
|
||||||
|
actions: assign({
|
||||||
|
query: ({ event }) => event.query,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
debouncing: {
|
||||||
|
after: {
|
||||||
|
300: 'searching', // 300ms debounce
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
SEARCH: {
|
||||||
|
target: 'debouncing',
|
||||||
|
actions: assign({
|
||||||
|
query: ({ event }) => event.query,
|
||||||
|
}),
|
||||||
|
reenter: true, // Reset the timer
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searching: {
|
||||||
|
invoke: {
|
||||||
|
src: 'performSearch',
|
||||||
|
input: ({ context }) => ({ query: context.query }),
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: assign({
|
||||||
|
results: ({ event }) => event.output,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: 'logError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Queue Processing Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const queueMachine = createMachine({
|
||||||
|
context: {
|
||||||
|
queue: [],
|
||||||
|
currentItem: null,
|
||||||
|
processed: [],
|
||||||
|
failed: [],
|
||||||
|
},
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
target: 'processing',
|
||||||
|
guard: ({ context }) => context.queue.length > 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
on: {
|
||||||
|
ADD_TO_QUEUE: {
|
||||||
|
actions: assign({
|
||||||
|
queue: ({ context, event }) => [...context.queue, event.item],
|
||||||
|
}),
|
||||||
|
target: 'processing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
processing: {
|
||||||
|
entry: assign({
|
||||||
|
currentItem: ({ context }) => context.queue[0],
|
||||||
|
queue: ({ context }) => context.queue.slice(1),
|
||||||
|
}),
|
||||||
|
invoke: {
|
||||||
|
src: 'processItem',
|
||||||
|
input: ({ context }) => context.currentItem,
|
||||||
|
onDone: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: assign({
|
||||||
|
processed: ({ context, event }) => [
|
||||||
|
...context.processed,
|
||||||
|
{ item: context.currentItem, result: event.output },
|
||||||
|
],
|
||||||
|
currentItem: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: assign({
|
||||||
|
failed: ({ context, event }) => [
|
||||||
|
...context.failed,
|
||||||
|
{ item: context.currentItem, error: event.error },
|
||||||
|
],
|
||||||
|
currentItem: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
CLEAR_QUEUE: {
|
||||||
|
actions: assign({
|
||||||
|
queue: [],
|
||||||
|
processed: [],
|
||||||
|
failed: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modal/Dialog Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const modalMachine = createMachine({
|
||||||
|
initial: 'closed',
|
||||||
|
context: {
|
||||||
|
data: null,
|
||||||
|
result: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
closed: {
|
||||||
|
on: {
|
||||||
|
OPEN: {
|
||||||
|
target: 'open',
|
||||||
|
actions: assign({
|
||||||
|
data: ({ event }) => event.data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
CONFIRM: {
|
||||||
|
target: 'confirming',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirming: {
|
||||||
|
invoke: {
|
||||||
|
src: 'handleConfirm',
|
||||||
|
input: ({ context }) => context.data,
|
||||||
|
onDone: {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
result: ({ event }) => event.output,
|
||||||
|
}),
|
||||||
|
emit({ type: 'MODAL_CONFIRMED' }),
|
||||||
|
],
|
||||||
|
target: '#modal.closed',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: 'showError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
CANCEL: {
|
||||||
|
target: 'closed',
|
||||||
|
actions: [
|
||||||
|
assign({ data: null, result: null }),
|
||||||
|
emit({ type: 'MODAL_CANCELLED' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CLOSE: {
|
||||||
|
target: 'closed',
|
||||||
|
actions: assign({ data: null, result: null }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection Management Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const connectionMachine = createMachine({
|
||||||
|
initial: 'disconnected',
|
||||||
|
context: {
|
||||||
|
retries: 0,
|
||||||
|
maxRetries: 5,
|
||||||
|
socket: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
disconnected: {
|
||||||
|
on: {
|
||||||
|
CONNECT: 'connecting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connecting: {
|
||||||
|
invoke: {
|
||||||
|
src: 'createConnection',
|
||||||
|
onDone: {
|
||||||
|
target: 'connected',
|
||||||
|
actions: assign({
|
||||||
|
socket: ({ event }) => event.output,
|
||||||
|
retries: 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'reconnecting',
|
||||||
|
guard: ({ context }) => context.retries < context.maxRetries,
|
||||||
|
actions: assign({
|
||||||
|
retries: ({ context }) => context.retries + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'failed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connected: {
|
||||||
|
invoke: {
|
||||||
|
src: 'monitorConnection',
|
||||||
|
onError: {
|
||||||
|
target: 'reconnecting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
DISCONNECT: {
|
||||||
|
target: 'disconnected',
|
||||||
|
actions: 'closeConnection',
|
||||||
|
},
|
||||||
|
CONNECTION_LOST: 'reconnecting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reconnecting: {
|
||||||
|
after: {
|
||||||
|
// Exponential backoff
|
||||||
|
[({ context }) => Math.min(1000 * Math.pow(2, context.retries), 30000)]:
|
||||||
|
'connecting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
on: {
|
||||||
|
RETRY: {
|
||||||
|
target: 'connecting',
|
||||||
|
actions: assign({ retries: 0 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
491
skills/xstate-v5/references/testing.md
Normal file
491
skills/xstate-v5/references/testing.md
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
# XState v5 Testing Reference (Backend Focus)
|
||||||
|
|
||||||
|
This reference covers testing XState v5 actors and state machines for **backend applications**. For comprehensive actor testing patterns, see the [xstate-audition skill](../../xstate-audition/SKILL.md).
|
||||||
|
|
||||||
|
## Testing Strategy Overview
|
||||||
|
|
||||||
|
XState v5 testing for backend applications focuses on:
|
||||||
|
|
||||||
|
- Actor lifecycle and behavior testing
|
||||||
|
- State transition verification
|
||||||
|
- Event handling and inter-actor communication
|
||||||
|
- Asynchronous operation testing
|
||||||
|
- Error handling and recovery
|
||||||
|
|
||||||
|
## Recommended Testing Library: xstate-audition
|
||||||
|
|
||||||
|
For comprehensive actor testing, use **[xstate-audition](https://boneskull.github.io/xstate-audition/)**, which provides:
|
||||||
|
|
||||||
|
- Dependency-free XState v5 actor testing
|
||||||
|
- Async-first testing utilities
|
||||||
|
- Comprehensive actor lifecycle testing
|
||||||
|
- Built-in timeout handling
|
||||||
|
- Curried functions for test reusability
|
||||||
|
|
||||||
|
See the [xstate-audition skill documentation](../../xstate-audition/SKILL.md) for detailed usage patterns.
|
||||||
|
|
||||||
|
## Basic Unit Testing
|
||||||
|
|
||||||
|
While xstate-audition excels at actor testing, you may need to unit test individual pieces:
|
||||||
|
|
||||||
|
### Testing Guards in Isolation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
|
||||||
|
// Guard function
|
||||||
|
const isValidThreshold = ({ context }: { context: { value: number } }) =>
|
||||||
|
context.value > 10;
|
||||||
|
|
||||||
|
describe('guards', () => {
|
||||||
|
test('isValidThreshold returns true for values > 10', () => {
|
||||||
|
expect(isValidThreshold({ context: { value: 15 } })).toBe(true);
|
||||||
|
expect(isValidThreshold({ context: { value: 5 } })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Action Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { vi, describe, test, expect } from 'vitest';
|
||||||
|
|
||||||
|
// Action function
|
||||||
|
const logMessage = ({ context, event }: any) => {
|
||||||
|
console.log(`Event: ${event.type}, Count: ${context.count}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('logMessage action logs correctly', () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'log');
|
||||||
|
|
||||||
|
logMessage({
|
||||||
|
context: { count: 5 },
|
||||||
|
event: { type: 'TEST_EVENT' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('Event: TEST_EVENT, Count: 5');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend-Specific Testing Patterns
|
||||||
|
|
||||||
|
### Testing Database Transaction States
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createActor } from 'xstate';
|
||||||
|
import { runUntilDone, waitForSnapshot } from 'xstate-audition';
|
||||||
|
|
||||||
|
const dbTransactionMachine = createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: { BEGIN: 'transaction' },
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
initial: 'processing',
|
||||||
|
states: {
|
||||||
|
processing: {
|
||||||
|
invoke: {
|
||||||
|
src: 'executeQuery',
|
||||||
|
onDone: 'committing',
|
||||||
|
onError: 'rollingBack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
committing: {
|
||||||
|
invoke: {
|
||||||
|
src: 'commitTransaction',
|
||||||
|
onDone: '#db.success',
|
||||||
|
onError: '.rollingBack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollingBack: {
|
||||||
|
invoke: {
|
||||||
|
src: 'rollbackTransaction',
|
||||||
|
onDone: '#db.failed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: { type: 'final' },
|
||||||
|
failed: { type: 'final' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successful database transaction', async () => {
|
||||||
|
const actor = createActor(dbTransactionMachine);
|
||||||
|
const promise = waitForSnapshot(actor, (s) => s.matches('success'));
|
||||||
|
|
||||||
|
actor.send({ type: 'BEGIN' });
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(actor.getSnapshot().value).toBe('success');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Message Queue Processing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const queueProcessorMachine = createMachine({
|
||||||
|
context: {
|
||||||
|
messages: [],
|
||||||
|
processed: 0,
|
||||||
|
failed: 0,
|
||||||
|
},
|
||||||
|
initial: 'polling',
|
||||||
|
states: {
|
||||||
|
polling: {
|
||||||
|
invoke: {
|
||||||
|
src: 'pollQueue',
|
||||||
|
onDone: {
|
||||||
|
target: 'processing',
|
||||||
|
actions: assign({
|
||||||
|
messages: ({ event }) => event.output,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
processing: {
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
target: 'idle',
|
||||||
|
guard: ({ context }) => context.messages.length === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'processingMessage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
processingMessage: {
|
||||||
|
invoke: {
|
||||||
|
src: 'processMessage',
|
||||||
|
input: ({ context }) => context.messages[0],
|
||||||
|
onDone: {
|
||||||
|
target: 'processing',
|
||||||
|
actions: assign({
|
||||||
|
messages: ({ context }) => context.messages.slice(1),
|
||||||
|
processed: ({ context }) => context.processed + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'processing',
|
||||||
|
actions: assign({
|
||||||
|
messages: ({ context }) => context.messages.slice(1),
|
||||||
|
failed: ({ context }) => context.failed + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
idle: {
|
||||||
|
after: {
|
||||||
|
5000: 'polling',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processes queue messages', async () => {
|
||||||
|
const actor = createActor(queueProcessorMachine);
|
||||||
|
|
||||||
|
await waitForSnapshot(actor, (snapshot) => snapshot.context.processed >= 3);
|
||||||
|
|
||||||
|
expect(actor.getSnapshot().context.failed).toBe(0);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing API Rate Limiting States
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const rateLimiterMachine = createMachine({
|
||||||
|
context: {
|
||||||
|
requestCount: 0,
|
||||||
|
resetTime: null,
|
||||||
|
},
|
||||||
|
initial: 'ready',
|
||||||
|
states: {
|
||||||
|
ready: {
|
||||||
|
on: {
|
||||||
|
REQUEST: [
|
||||||
|
{
|
||||||
|
target: 'throttled',
|
||||||
|
guard: ({ context }) => context.requestCount >= 100,
|
||||||
|
actions: assign({
|
||||||
|
resetTime: () => Date.now() + 60000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'processing',
|
||||||
|
actions: assign({
|
||||||
|
requestCount: ({ context }) => context.requestCount + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
processing: {
|
||||||
|
invoke: {
|
||||||
|
src: 'handleRequest',
|
||||||
|
onDone: 'ready',
|
||||||
|
onError: 'ready',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
throttled: {
|
||||||
|
after: {
|
||||||
|
60000: {
|
||||||
|
target: 'ready',
|
||||||
|
actions: assign({
|
||||||
|
requestCount: 0,
|
||||||
|
resetTime: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enforces rate limits', async () => {
|
||||||
|
const actor = createActor(rateLimiterMachine);
|
||||||
|
|
||||||
|
// Send 100 requests
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
actor.send({ type: 'REQUEST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 101st request should be throttled
|
||||||
|
actor.send({ type: 'REQUEST' });
|
||||||
|
|
||||||
|
await waitForSnapshot(actor, (s) => s.matches('throttled'));
|
||||||
|
expect(actor.getSnapshot().context.resetTime).toBeDefined();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Mock Services
|
||||||
|
|
||||||
|
### Mocking External Services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import { fromPromise } from 'xstate';
|
||||||
|
|
||||||
|
test('handles service failures gracefully', async () => {
|
||||||
|
const mockService = vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(new Error('Service unavailable'));
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
actors: {
|
||||||
|
externalService: fromPromise(mockService),
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
initial: 'calling',
|
||||||
|
states: {
|
||||||
|
calling: {
|
||||||
|
invoke: {
|
||||||
|
src: 'externalService',
|
||||||
|
onError: {
|
||||||
|
target: 'fallback',
|
||||||
|
actions: assign({
|
||||||
|
error: ({ event }) => event.error.message,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
type: 'final',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actor = createActor(machine);
|
||||||
|
const result = await runUntilDone(actor);
|
||||||
|
|
||||||
|
expect(actor.getSnapshot().context.error).toBe('Service unavailable');
|
||||||
|
expect(mockService).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Async Behavior
|
||||||
|
|
||||||
|
### Using waitFor from xstate
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { waitFor } from 'xstate';
|
||||||
|
|
||||||
|
test('waits for async completion', async () => {
|
||||||
|
const actor = createActor(asyncMachine);
|
||||||
|
actor.start();
|
||||||
|
|
||||||
|
actor.send({ type: 'START_ASYNC' });
|
||||||
|
|
||||||
|
const snapshot = await waitFor(
|
||||||
|
actor,
|
||||||
|
(snapshot) => snapshot.context.isComplete === true,
|
||||||
|
{ timeout: 5000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.context.result).toBeDefined();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing with Fake Timers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
describe('delayed transitions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transitions after delay', () => {
|
||||||
|
const actor = createActor(delayedMachine);
|
||||||
|
actor.start();
|
||||||
|
|
||||||
|
expect(actor.getSnapshot().value).toBe('waiting');
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
|
||||||
|
expect(actor.getSnapshot().value).toBe('ready');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Actor Communication
|
||||||
|
|
||||||
|
### Testing Parent-Child Actors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { spawn, sendTo } from 'xstate';
|
||||||
|
import { runUntilSpawn, waitForSnapshot } from 'xstate-audition';
|
||||||
|
|
||||||
|
test('parent spawns and communicates with child', async () => {
|
||||||
|
const parentMachine = createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: {
|
||||||
|
childResponse: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
START: {
|
||||||
|
actions: spawn(childMachine, { id: 'child' }),
|
||||||
|
target: 'waiting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
waiting: {
|
||||||
|
on: {
|
||||||
|
CHILD_RESPONSE: {
|
||||||
|
actions: assign({
|
||||||
|
childResponse: ({ event }) => event.data,
|
||||||
|
}),
|
||||||
|
target: 'done',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
entry: sendTo('child', { type: 'PROCESS' }),
|
||||||
|
},
|
||||||
|
done: { type: 'final' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actor = createActor(parentMachine);
|
||||||
|
const childRef = await runUntilSpawn(actor, 'child');
|
||||||
|
|
||||||
|
await waitForSnapshot(actor, (s) => s.matches('done'));
|
||||||
|
|
||||||
|
expect(actor.getSnapshot().context.childResponse).toBeDefined();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('handles high throughput', async () => {
|
||||||
|
const start = performance.now();
|
||||||
|
const actor = createActor(highThroughputMachine);
|
||||||
|
|
||||||
|
actor.start();
|
||||||
|
|
||||||
|
// Send many events rapidly
|
||||||
|
for (let i = 0; i < 10000; i++) {
|
||||||
|
actor.send({ type: 'PROCESS', data: i });
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitFor(actor, (s) => s.context.processedCount === 10000);
|
||||||
|
|
||||||
|
const duration = performance.now() - start;
|
||||||
|
expect(duration).toBeLessThan(5000); // Should process within 5 seconds
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('recovers from errors with retry', async () => {
|
||||||
|
let attempts = 0;
|
||||||
|
const mockService = vi.fn().mockImplementation(() => {
|
||||||
|
attempts++;
|
||||||
|
if (attempts < 3) {
|
||||||
|
throw new Error('Temporary failure');
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryMachine = createMachine({
|
||||||
|
context: { retries: 0, maxRetries: 3 },
|
||||||
|
initial: 'attempting',
|
||||||
|
states: {
|
||||||
|
attempting: {
|
||||||
|
invoke: {
|
||||||
|
src: fromPromise(mockService),
|
||||||
|
onDone: 'success',
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'attempting',
|
||||||
|
guard: ({ context }) => context.retries < context.maxRetries,
|
||||||
|
actions: assign({
|
||||||
|
retries: ({ context }) => context.retries + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'failed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: { type: 'final' },
|
||||||
|
failed: { type: 'final' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const actor = createActor(retryMachine);
|
||||||
|
await runUntilDone(actor);
|
||||||
|
|
||||||
|
expect(actor.getSnapshot().value).toBe('success');
|
||||||
|
expect(mockService).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Organization Best Practices
|
||||||
|
|
||||||
|
1. **Separate unit tests from integration tests** - Test guards and actions separately from full actor behavior
|
||||||
|
2. **Use xstate-audition for actor tests** - Leverage its powerful async utilities
|
||||||
|
3. **Mock external dependencies** - Database, APIs, message queues
|
||||||
|
4. **Test error paths explicitly** - Ensure graceful degradation
|
||||||
|
5. **Use descriptive test names** - Clearly indicate what behavior is being tested
|
||||||
|
6. **Keep tests focused** - One behavior per test
|
||||||
|
7. **Use fake timers for time-dependent logic** - Control time progression explicitly
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
For backend XState v5 testing:
|
||||||
|
|
||||||
|
- Use **[xstate-audition](../../xstate-audition/SKILL.md)** for comprehensive actor testing
|
||||||
|
- Focus on actor behavior, not UI interactions
|
||||||
|
- Test database transactions, message processing, and API interactions
|
||||||
|
- Mock external services appropriately
|
||||||
|
- Handle async behavior with proper utilities
|
||||||
|
- Ensure error recovery and retry logic works correctly
|
||||||
626
skills/xstate-v5/references/typescript.md
Normal file
626
skills/xstate-v5/references/typescript.md
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
# XState v5 TypeScript Reference
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **TypeScript 5.0 or greater** is required for XState v5
|
||||||
|
- Enable `strictNullChecks: true` in tsconfig.json (strongly recommended)
|
||||||
|
- Set `skipLibCheck: true` for better performance
|
||||||
|
|
||||||
|
## Setup Pattern (Recommended)
|
||||||
|
|
||||||
|
The `setup()` function is the primary way to achieve strong typing in XState v5.
|
||||||
|
|
||||||
|
### Basic Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setup } from 'xstate';
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as {
|
||||||
|
count: number;
|
||||||
|
user: User | null;
|
||||||
|
},
|
||||||
|
events: {} as
|
||||||
|
| { type: 'INCREMENT' }
|
||||||
|
| { type: 'DECREMENT' }
|
||||||
|
| { type: 'SET_USER'; user: User },
|
||||||
|
input: {} as {
|
||||||
|
initialCount?: number;
|
||||||
|
},
|
||||||
|
output: {} as {
|
||||||
|
finalCount: number;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
incrementCount: assign({
|
||||||
|
count: ({ context }) => context.count + 1,
|
||||||
|
}),
|
||||||
|
setUser: assign({
|
||||||
|
user: ({ event }) => {
|
||||||
|
// TypeScript knows event has 'user' property here
|
||||||
|
assertEvent(event, 'SET_USER');
|
||||||
|
return event.user;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
isPositive: ({ context }) => context.count > 0,
|
||||||
|
hasUser: ({ context }) => context.user !== null,
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
fetchUser: fromPromise(async ({ input }: { input: { id: string } }) => {
|
||||||
|
const response = await fetch(`/api/users/${input.id}`);
|
||||||
|
return response.json() as Promise<User>;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
delays: {
|
||||||
|
RETRY_DELAY: ({ context }) => context.count * 1000,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
// Machine configuration with full type inference
|
||||||
|
id: 'typedMachine',
|
||||||
|
initial: 'idle',
|
||||||
|
context: ({ input }) => ({
|
||||||
|
count: input?.initialCount ?? 0,
|
||||||
|
user: null,
|
||||||
|
}),
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
INCREMENT: {
|
||||||
|
actions: 'incrementCount',
|
||||||
|
},
|
||||||
|
SET_USER: {
|
||||||
|
actions: 'setUser',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Inference
|
||||||
|
|
||||||
|
### Context Type Inference
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Context is fully typed in actions, guards, etc.
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as {
|
||||||
|
items: string[];
|
||||||
|
selectedIndex: number;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
context: {
|
||||||
|
items: [],
|
||||||
|
selectedIndex: -1,
|
||||||
|
},
|
||||||
|
// TypeScript knows context shape everywhere
|
||||||
|
entry: ({ context }) => {
|
||||||
|
console.log(context.items); // string[]
|
||||||
|
console.log(context.selectedIndex); // number
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Type Inference
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
events: {} as
|
||||||
|
| { type: 'ADD_ITEM'; item: string }
|
||||||
|
| { type: 'REMOVE_ITEM'; index: number }
|
||||||
|
| { type: 'CLEAR_ALL' },
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
on: {
|
||||||
|
ADD_ITEM: {
|
||||||
|
actions: ({ event }) => {
|
||||||
|
// TypeScript knows event.item exists and is string
|
||||||
|
console.log(event.item.toUpperCase());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
REMOVE_ITEM: {
|
||||||
|
actions: ({ event }) => {
|
||||||
|
// TypeScript knows event.index exists and is number
|
||||||
|
console.log(event.index + 1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Parameters
|
||||||
|
|
||||||
|
### Typed Action Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as { message: string },
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
log: (
|
||||||
|
{ context },
|
||||||
|
params: { prefix: string; level?: 'info' | 'warn' | 'error' },
|
||||||
|
) => {
|
||||||
|
const level = params.level || 'info';
|
||||||
|
console[level](`${params.prefix}: ${context.message}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
entry: {
|
||||||
|
type: 'log',
|
||||||
|
params: { prefix: 'Starting', level: 'info' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Guard Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as { value: number },
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
isGreaterThan: ({ context }, params: { threshold: number }) =>
|
||||||
|
context.value > params.threshold,
|
||||||
|
isInRange: ({ context }, params: { min: number; max: number }) =>
|
||||||
|
context.value >= params.min && context.value <= params.max,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
on: {
|
||||||
|
CHECK: [
|
||||||
|
{
|
||||||
|
target: 'high',
|
||||||
|
guard: {
|
||||||
|
type: 'isGreaterThan',
|
||||||
|
params: { threshold: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'medium',
|
||||||
|
guard: {
|
||||||
|
type: 'isInRange',
|
||||||
|
params: { min: 50, max: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Assertion
|
||||||
|
|
||||||
|
Use `assertEvent()` to narrow event types:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { assertEvent } from 'xstate';
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
events: {} as
|
||||||
|
| { type: 'SUBMIT'; data: FormData }
|
||||||
|
| { type: 'CANCEL' }
|
||||||
|
| { type: 'RESET'; fields?: string[] },
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
on: {
|
||||||
|
'*': {
|
||||||
|
actions: ({ event }) => {
|
||||||
|
// Assert single event type
|
||||||
|
assertEvent(event, 'SUBMIT');
|
||||||
|
console.log(event.data); // TypeScript knows data exists
|
||||||
|
|
||||||
|
// Assert multiple event types
|
||||||
|
assertEvent(event, ['SUBMIT', 'RESET']);
|
||||||
|
if (event.type === 'SUBMIT') {
|
||||||
|
console.log(event.data);
|
||||||
|
} else {
|
||||||
|
console.log(event.fields);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Helper Utilities
|
||||||
|
|
||||||
|
### ActorRefFrom
|
||||||
|
|
||||||
|
Extract actor reference type from machine or logic:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ActorRefFrom } from 'xstate';
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
/* ... */
|
||||||
|
});
|
||||||
|
|
||||||
|
type MachineActorRef = ActorRefFrom<typeof machine>;
|
||||||
|
|
||||||
|
function handleActor(actor: MachineActorRef) {
|
||||||
|
actor.send({ type: 'SOME_EVENT' });
|
||||||
|
const snapshot = actor.getSnapshot();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SnapshotFrom
|
||||||
|
|
||||||
|
Extract snapshot type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SnapshotFrom } from 'xstate';
|
||||||
|
|
||||||
|
type MachineSnapshot = SnapshotFrom<typeof machine>;
|
||||||
|
|
||||||
|
function processSnapshot(snapshot: MachineSnapshot) {
|
||||||
|
if (snapshot.matches('loading')) {
|
||||||
|
// Handle loading state
|
||||||
|
}
|
||||||
|
console.log(snapshot.context);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EventFromLogic
|
||||||
|
|
||||||
|
Extract event union type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EventFromLogic } from 'xstate';
|
||||||
|
|
||||||
|
type MachineEvents = EventFromLogic<typeof machine>;
|
||||||
|
|
||||||
|
function createEvent(): MachineEvents {
|
||||||
|
return { type: 'INCREMENT' };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### StateValueFrom
|
||||||
|
|
||||||
|
Extract state value type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { StateValueFrom } from 'xstate';
|
||||||
|
|
||||||
|
type MachineStateValue = StateValueFrom<typeof machine>;
|
||||||
|
|
||||||
|
function isInState(value: MachineStateValue) {
|
||||||
|
// Type-safe state value checking
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ContextFrom
|
||||||
|
|
||||||
|
Extract context type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ContextFrom } from 'xstate';
|
||||||
|
|
||||||
|
type MachineContext = ContextFrom<typeof machine>;
|
||||||
|
|
||||||
|
function processContext(context: MachineContext) {
|
||||||
|
// Access typed context properties
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actor Types
|
||||||
|
|
||||||
|
### Typed Promise Actors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUserLogic = fromPromise(
|
||||||
|
async ({ input }: { input: { userId: string } }): Promise<UserData> => {
|
||||||
|
const response = await fetch(`/api/users/${input.userId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch user');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use in machine
|
||||||
|
const machine = setup({
|
||||||
|
actors: {
|
||||||
|
fetchUser: fetchUserLogic,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
invoke: {
|
||||||
|
src: 'fetchUser',
|
||||||
|
input: ({ context }) => ({ userId: context.userId }),
|
||||||
|
onDone: {
|
||||||
|
actions: assign({
|
||||||
|
user: ({ event }) => event.output, // Typed as UserData
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Callback Actors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface WebSocketMessage {
|
||||||
|
type: 'message' | 'error' | 'close';
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const websocketLogic = fromCallback<WebSocketMessage, { url: string }>(
|
||||||
|
({ sendBack, receive, input }) => {
|
||||||
|
const ws = new WebSocket(input.url);
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
sendBack({ type: 'message', data: event.data });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
sendBack({ type: 'error', data: error });
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
sendBack({ type: 'close', data: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
receive((event) => {
|
||||||
|
if (event.type === 'SEND') {
|
||||||
|
ws.send(event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => ws.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic Machines
|
||||||
|
|
||||||
|
### Creating Generic Machine Factories
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createCrudMachine<T extends { id: string }>() {
|
||||||
|
return setup({
|
||||||
|
types: {
|
||||||
|
context: {} as {
|
||||||
|
items: T[];
|
||||||
|
selectedItem: T | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
},
|
||||||
|
events: {} as
|
||||||
|
| { type: 'FETCH' }
|
||||||
|
| { type: 'CREATE'; item: Omit<T, 'id'> }
|
||||||
|
| { type: 'UPDATE'; id: string; updates: Partial<T> }
|
||||||
|
| { type: 'DELETE'; id: string }
|
||||||
|
| { type: 'SELECT'; item: T },
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: {
|
||||||
|
items: [],
|
||||||
|
selectedItem: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
FETCH: 'fetching',
|
||||||
|
CREATE: 'creating',
|
||||||
|
SELECT: {
|
||||||
|
actions: assign({
|
||||||
|
selectedItem: ({ event }) => event.item,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fetching: {
|
||||||
|
// Implementation
|
||||||
|
},
|
||||||
|
creating: {
|
||||||
|
// Implementation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use with specific type
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMachine = createCrudMachine<User>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discriminated Unions
|
||||||
|
|
||||||
|
### State-based Discrimination
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MachineState =
|
||||||
|
| {
|
||||||
|
value: 'idle';
|
||||||
|
context: { data: null; error: null };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: 'loading';
|
||||||
|
context: { data: null; error: null };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: 'success';
|
||||||
|
context: { data: string; error: null };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: 'failure';
|
||||||
|
context: { data: null; error: Error };
|
||||||
|
};
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as MachineState['context'],
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {},
|
||||||
|
loading: {},
|
||||||
|
success: {},
|
||||||
|
failure: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type-safe state checking
|
||||||
|
function handleState(state: SnapshotFrom<typeof machine>) {
|
||||||
|
if (state.matches('success')) {
|
||||||
|
// TypeScript should know data is string here
|
||||||
|
console.log(state.context.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common TypeScript Patterns
|
||||||
|
|
||||||
|
### Exhaustive Event Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
events: {} as { type: 'A' } | { type: 'B' } | { type: 'C' },
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
on: {
|
||||||
|
'*': {
|
||||||
|
actions: ({ event }) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'A':
|
||||||
|
// Handle A
|
||||||
|
break;
|
||||||
|
case 'B':
|
||||||
|
// Handle B
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
// Handle C
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// TypeScript ensures this is never reached
|
||||||
|
const exhaustive: never = event;
|
||||||
|
throw new Error(`Unhandled event: ${exhaustive}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branded Types for IDs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type UserId = string & { __brand: 'UserId' };
|
||||||
|
type PostId = string & { __brand: 'PostId' };
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as {
|
||||||
|
userId: UserId | null;
|
||||||
|
postId: PostId | null;
|
||||||
|
},
|
||||||
|
events: {} as
|
||||||
|
| { type: 'SET_USER'; id: UserId }
|
||||||
|
| { type: 'SET_POST'; id: PostId },
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
on: {
|
||||||
|
SET_USER: {
|
||||||
|
actions: assign({
|
||||||
|
userId: ({ event }) => event.id, // Type-safe: only UserId accepted
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
SET_POST: {
|
||||||
|
actions: assign({
|
||||||
|
postId: ({ event }) => event.id, // Type-safe: only PostId accepted
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Const Assertions for Events
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define events with const assertion for literal types
|
||||||
|
const EVENTS = {
|
||||||
|
INCREMENT: { type: 'INCREMENT' } as const,
|
||||||
|
DECREMENT: { type: 'DECREMENT' } as const,
|
||||||
|
RESET: { type: 'RESET' } as const,
|
||||||
|
SET_VALUE: (value: number) => ({ type: 'SET_VALUE', value }) as const,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type MachineEvent = ReturnType<(typeof EVENTS)[keyof typeof EVENTS]>;
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
events: {} as MachineEvent,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
// Machine configuration
|
||||||
|
});
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
actor.send(EVENTS.INCREMENT);
|
||||||
|
actor.send(EVENTS.SET_VALUE(42));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from v4 Types
|
||||||
|
|
||||||
|
### Replacing Typegen
|
||||||
|
|
||||||
|
v4 used typegen for type safety. In v5, use `setup()` instead:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// v4 with typegen
|
||||||
|
import { createMachine } from 'xstate';
|
||||||
|
import { typegen } from './machine.typegen';
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
tsTypes: typegen,
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// v5 with setup
|
||||||
|
import { setup } from 'xstate';
|
||||||
|
|
||||||
|
const machine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as ContextType,
|
||||||
|
events: {} as EventsUnion,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always use setup()** for new machines
|
||||||
|
2. **Define types first** in the setup configuration
|
||||||
|
3. **Use const assertions** for literal types
|
||||||
|
4. **Enable strict mode** in tsconfig.json
|
||||||
|
5. **Use branded types** for domain-specific IDs
|
||||||
|
6. **Leverage type helpers** (ActorRefFrom, SnapshotFrom, etc.)
|
||||||
|
7. **Assert events** when needed with assertEvent()
|
||||||
|
8. **Create generic factories** for reusable patterns
|
||||||
|
9. **Type external actors** properly with input/output types
|
||||||
|
10. **Use discriminated unions** for complex state variations
|
||||||
Reference in New Issue
Block a user