diff --git a/.changeset/curly-windows-burn.md b/.changeset/curly-windows-burn.md new file mode 100644 index 0000000000..94574203d0 --- /dev/null +++ b/.changeset/curly-windows-burn.md @@ -0,0 +1,8 @@ +--- +'@xstate/graph': major +--- + +pr: #3036 +@author: @davidkpiano + +Renamed `getAdjacencyMap` to `getValueAdjacencyMap`. diff --git a/.changeset/curly-windows-learn.md b/.changeset/curly-windows-learn.md new file mode 100644 index 0000000000..ad7dd7e3ca --- /dev/null +++ b/.changeset/curly-windows-learn.md @@ -0,0 +1,10 @@ +--- +'@xstate/graph': major +--- + +pr: #3036 +@author: @davidkpiano + +Changed `getSimplePaths` to `getSimplePlans`, and `getShortestPaths` to `getShortestPlans`. Both of these functions can be passed a machine, and return `StatePlan[]`. + +Added functions `traverseSimplePlans`, `traverseShortestPlans`,`traverseShortestPlansFromTo`, `traverseSimplePlansTo` and `traverseSimplePlansFromTo`, which can be passed a `Behavior` and return `StatePlan[]`. diff --git a/.changeset/great-lions-buy.md b/.changeset/great-lions-buy.md new file mode 100644 index 0000000000..450cc32ebb --- /dev/null +++ b/.changeset/great-lions-buy.md @@ -0,0 +1,12 @@ +--- +'@xstate/test': major +--- + +pr: #3036 + +@author: @mattpocock +@author: @davidkpiano + +Substantially simplified how paths and plans work in `TestModel`. Changed `getShortestPlans` and `getSimplePlans` to `getShortestPaths` and `getSimplePaths`. These functions now return an array of paths, instead of an array of plans which contain paths. + +Also added `getPaths`, which defaults to `getShortestPaths`. This can be passed a `pathGenerator` to customize how paths are generated. diff --git a/.changeset/lazy-turtles-brand.md b/.changeset/lazy-turtles-brand.md new file mode 100644 index 0000000000..92aeac2e30 --- /dev/null +++ b/.changeset/lazy-turtles-brand.md @@ -0,0 +1,37 @@ +--- +'@xstate/test': major +--- + +pr: #3036 +@author: @mattpocock + +Moved event cases out of `events`, and into their own attribute called `eventCases`: + +```ts +const model = createTestModel(machine, { + eventCases: { + CHOOSE_CURRENCY: [ + { + currency: 'GBP' + }, + { + currency: 'USD' + } + ] + } +}); + +model.getPaths().forEach((path) => { + it(path.description, async () => { + await path.test({ + events: { + CHOOSE_CURRENCY: ({ event }) => { + console.log(event.currency); + } + } + }); + }); +}); +``` + +`eventCases` will also now always produce a new path, instead of only creating a path for the first case which matches. diff --git a/.changeset/lazy-turtles-bread.md b/.changeset/lazy-turtles-bread.md new file mode 100644 index 0000000000..d8b7f86588 --- /dev/null +++ b/.changeset/lazy-turtles-bread.md @@ -0,0 +1,8 @@ +--- +'@xstate/test': major +--- + +pr: #3036 +@author: @davidkpiano + +Removed `.testCoverage()`, and instead made `getPlans`, `getShortestPlans` and `getSimplePlans` cover all states and transitions enabled by event cases by default. diff --git a/.changeset/lazy-turtles-grand.md b/.changeset/lazy-turtles-grand.md new file mode 100644 index 0000000000..2890e97ab5 --- /dev/null +++ b/.changeset/lazy-turtles-grand.md @@ -0,0 +1,10 @@ +--- +'@xstate/test': major +--- + +pr: #3036 +@author: @davidkpiano + +Added validation on `createTestModel` to ensure that you don't include invalid machine configuration in your test machine. Invalid machine configs include `invoke`, `after`, and any actions with a `delay`. + +Added `createTestMachine`, which provides a slimmed-down API for creating machines which removes these types from the config type signature. diff --git a/.changeset/lazy-turtles-grate.md b/.changeset/lazy-turtles-grate.md new file mode 100644 index 0000000000..a6c172df56 --- /dev/null +++ b/.changeset/lazy-turtles-grate.md @@ -0,0 +1,47 @@ +--- +'@xstate/test': major +--- + +pr: #3036 +@author: @davidkpiano + +`getShortestPaths()` and `getPaths()` will now traverse all _transitions_ by default, not just all events. + +Take this machine: + +```ts +const machine = createTestMachine({ + initial: 'toggledOn', + states: { + toggledOn: { + on: { + TOGGLE: 'toggledOff' + } + }, + toggledOff: { + on: { + TOGGLE: 'toggledOn' + } + } + } +}); +``` + +In `@xstate/test` version 0.x, this would run this path by default: + +```txt +toggledOn -> TOGGLE -> toggledOff +``` + +This is because it satisfies two conditions: + +1. Covers all states +2. Covers all events + +But this a complete test - it doesn't test if going from `toggledOff` to `toggledOn` works. + +Now, we seek to cover all transitions by default. So the path would be: + +```txt +toggledOn -> TOGGLE -> toggledOff -> TOGGLE -> toggledOn +``` diff --git a/.changeset/lazy-turtles-great.md b/.changeset/lazy-turtles-great.md new file mode 100644 index 0000000000..0d715f2542 --- /dev/null +++ b/.changeset/lazy-turtles-great.md @@ -0,0 +1,8 @@ +--- +'@xstate/test': minor +--- + +pr: #3036 +@author: @mattpocock @davidkpiano + +Added `path.testSync(...)` to allow for testing paths in sync-only environments, such as Cypress. diff --git a/.changeset/lazy-turtles-mate.md b/.changeset/lazy-turtles-mate.md new file mode 100644 index 0000000000..2f3e816355 --- /dev/null +++ b/.changeset/lazy-turtles-mate.md @@ -0,0 +1,28 @@ +--- +'@xstate/test': major +--- + +pr: #3036 +@author: @mattpocock @davidkpiano + +Moved `events` from `createTestModel` to `path.test`. + +Old: + +```ts +const model = createTestModel(machine, { + events: {} +}); +``` + +New: + +```ts +const paths = model.getPaths().forEach((path) => { + path.test({ + events: {} + }); +}); +``` + +This allows for easier usage of per-test mocks and per-test context. diff --git a/.changeset/lazy-turtles-trade.md b/.changeset/lazy-turtles-trade.md new file mode 100644 index 0000000000..d0ca4744bf --- /dev/null +++ b/.changeset/lazy-turtles-trade.md @@ -0,0 +1,21 @@ +--- +'@xstate/test': major +--- + +pr: #3036 +@author: @mattpocock @davidkpiano + +Added `states` to `path.test()`: + +```ts +const paths = model.getPaths().forEach((path) => { + path.test({ + states: { + myState: () => {}, + 'myState.deep': () => {} + } + }); +}); +``` + +This allows you to define your tests outside of your machine, keeping the machine itself easy to read. diff --git a/packages/core/src/interpreter.ts b/packages/core/src/interpreter.ts index e35b0e309b..794d81abf6 100644 --- a/packages/core/src/interpreter.ts +++ b/packages/core/src/interpreter.ts @@ -1094,7 +1094,7 @@ export class Interpreter< }) .start(); - return actor as any; + return actor as ActorRef>; } private spawnBehavior( behavior: Behavior, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 51986d862d..5b00a12efd 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1504,6 +1504,9 @@ export interface StateConfig { */ activities?: ActivityMap; meta?: any; + /** + * @deprecated + */ events?: TEvent[]; configuration: Array>; transitions: Array>; diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 1c0a49e13e..2522135718 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -1,23 +1,27 @@ -import { - StateNode, +import type { State, - DefaultContext, Event, EventObject, - StateMachine, - AnyEventObject, - AnyStateMachine + AnyStateMachine, + AnyState, + StateFrom, + EventFrom } from 'xstate'; -import { - StatePathsMap, - StatePaths, - AdjacencyMap, - Segments, - ValueAdjMapOptions, +import type { + SerializedEvent, + SerializedState, + SimpleBehavior, + StatePath, + StatePlan, + StatePlanMap, + ValueAdjacencyMap, + Steps, + ValueAdjacencyMapOptions, DirectedGraphEdge, DirectedGraphNode, - AnyStateNode, - StatePath + TraversalOptions, + VisitedContext, + AnyStateNode } from './types'; function flatten(array: Array): T[] { @@ -34,8 +38,6 @@ export function toEventObject( return event; } -const EMPTY_MAP = {}; - /** * Returns all state nodes of the given `node`. * @param stateNode State node to recursively get child state nodes from @@ -67,59 +69,51 @@ export function getChildren(stateNode: AnyStateNode): AnyStateNode[] { return children; } -export function serializeState(state: State): string { +export function serializeMachineState(state: AnyState): SerializedState { const { value, context } = state; - return context === undefined - ? JSON.stringify(value) - : JSON.stringify(value) + ' | ' + JSON.stringify(context); + return JSON.stringify({ value, context }) as SerializedState; } export function serializeEvent( event: TEvent -): string { - return JSON.stringify(event); +): SerializedEvent { + return JSON.stringify(event) as SerializedEvent; } -export function deserializeEventString( - eventString: string -): TEvent { - return JSON.parse(eventString) as TEvent; -} - -const defaultValueAdjMapOptions: Required> = { +const defaultValueAdjacencyMapOptions: Required< + ValueAdjacencyMapOptions +> = { events: {}, filter: () => true, - stateSerializer: serializeState, + stateSerializer: serializeMachineState, eventSerializer: serializeEvent }; -function getValueAdjMapOptions( - options?: ValueAdjMapOptions -): Required> { +function getValueAdjacencyMapOptions( + options?: ValueAdjacencyMapOptions +): Required> { return { - ...(defaultValueAdjMapOptions as Required< - ValueAdjMapOptions + ...(defaultValueAdjacencyMapOptions as Required< + ValueAdjacencyMapOptions >), ...options }; } -export function getAdjacencyMap< - TContext = DefaultContext, - TEvent extends EventObject = AnyEventObject ->( - node: - | StateNode - | StateMachine, - options?: ValueAdjMapOptions -): AdjacencyMap { - const optionsWithDefaults = getValueAdjMapOptions(options); +export function getValueAdjacencyMap( + machine: TMachine, + options?: ValueAdjacencyMapOptions, EventFrom> +): ValueAdjacencyMap, EventFrom> { + type TState = StateFrom; + type TEvent = EventFrom; + + const optionsWithDefaults = getValueAdjacencyMapOptions(options); const { filter, stateSerializer, eventSerializer } = optionsWithDefaults; const { events } = optionsWithDefaults; - const adjacency: AdjacencyMap = {}; + const adjacency: ValueAdjacencyMap = {}; - function findAdjacencies(state: State) { + function findAdjacencies(state: TState) { const { nextEvents } = state; const stateHash = stateSerializer(state); @@ -146,9 +140,9 @@ export function getAdjacencyMap< ).map((event) => toEventObject(event)); for (const event of potentialEvents) { - let nextState: State; + let nextState: TState; try { - nextState = node.transition(state, event); + nextState = machine.transition(state, event) as TState; } catch (e) { throw new Error( `Unable to transition from state ${stateSerializer( @@ -171,90 +165,122 @@ export function getAdjacencyMap< } } - findAdjacencies(node.initialState); + findAdjacencies(machine.initialState as TState); return adjacency; } -export function getShortestPaths< - TContext = DefaultContext, - TEvent extends EventObject = EventObject ->( - machine: StateMachine, - options?: ValueAdjMapOptions -): StatePathsMap { - if (!machine.states) { - return EMPTY_MAP; +const defaultMachineStateOptions: TraversalOptions, any> = { + serializeState: serializeMachineState, + serializeEvent, + eventCases: {}, + getEvents: (state) => { + return state.nextEvents.map((type) => ({ type })); } - const optionsWithDefaults = getValueAdjMapOptions(options); +}; - const adjacency = getAdjacencyMap( - machine, - optionsWithDefaults +export function getShortestPlans( + machine: TMachine, + options?: Partial, EventFrom>> +): Array, EventFrom>> { + const resolvedOptions = resolveTraversalOptions( + options, + defaultMachineStateOptions ); + return traverseShortestPlans( + { + transition: (state, event) => machine.transition(state, event), + initialState: machine.initialState + }, + resolvedOptions + ) as Array, EventFrom>>; +} + +export function traverseShortestPlans( + behavior: SimpleBehavior, + options?: Partial> +): Array> { + const optionsWithDefaults = resolveTraversalOptions(options); + const serializeState = optionsWithDefaults.serializeState as ( + ...args: Parameters + ) => SerializedState; + + const adjacency = performDepthFirstTraversal(behavior, optionsWithDefaults); // weight, state, event const weightMap = new Map< - string, - [number, string | undefined, string | undefined] + SerializedState, + [ + weight: number, + state: SerializedState | undefined, + event: TEvent | undefined + ] >(); - const stateMap = new Map>(); - const initialVertex = optionsWithDefaults.stateSerializer( - machine.initialState - ); - stateMap.set(initialVertex, machine.initialState); + const stateMap = new Map(); + const initialSerializedState = serializeState(behavior.initialState, null); + stateMap.set(initialSerializedState, behavior.initialState); - weightMap.set(initialVertex, [0, undefined, undefined]); - const unvisited = new Set(); - const visited = new Set(); + weightMap.set(initialSerializedState, [0, undefined, undefined]); + const unvisited = new Set(); + const visited = new Set(); - unvisited.add(initialVertex); + unvisited.add(initialSerializedState); while (unvisited.size > 0) { - for (const vertex of unvisited) { - const [weight] = weightMap.get(vertex)!; - for (const event of Object.keys(adjacency[vertex])) { - const nextSegment = adjacency[vertex][event]; - const nextVertex = optionsWithDefaults.stateSerializer( - nextSegment.state - ); - stateMap.set(nextVertex, nextSegment.state); - if (!weightMap.has(nextVertex)) { - weightMap.set(nextVertex, [weight + 1, vertex, event]); + for (const serializedState of unvisited) { + const [weight] = weightMap.get(serializedState)!; + for (const event of Object.keys( + adjacency[serializedState].transitions + ) as SerializedEvent[]) { + const { state: nextState, event: eventObject } = adjacency[ + serializedState + ].transitions[event]; + const nextSerializedState = serializeState(nextState, eventObject); + stateMap.set(nextSerializedState, nextState); + if (!weightMap.has(nextSerializedState)) { + weightMap.set(nextSerializedState, [ + weight + 1, + serializedState, + eventObject + ]); } else { - const [nextWeight] = weightMap.get(nextVertex)!; + const [nextWeight] = weightMap.get(nextSerializedState)!; if (nextWeight > weight + 1) { - weightMap.set(nextVertex, [weight + 1, vertex, event]); + weightMap.set(nextSerializedState, [ + weight + 1, + serializedState, + eventObject + ]); } } - if (!visited.has(nextVertex)) { - unvisited.add(nextVertex); + if (!visited.has(nextSerializedState)) { + unvisited.add(nextSerializedState); } } - visited.add(vertex); - unvisited.delete(vertex); + visited.add(serializedState); + unvisited.delete(serializedState); } } - const statePathMap: StatePathsMap = {}; + const statePlanMap: StatePlanMap = {}; weightMap.forEach(([weight, fromState, fromEvent], stateSerial) => { const state = stateMap.get(stateSerial)!; - statePathMap[stateSerial] = { + statePlanMap[stateSerial] = { state, paths: !fromState ? [ { state, - segments: [], + steps: [], weight } ] : [ { state, - segments: statePathMap[fromState].paths[0].segments.concat({ + steps: statePlanMap[fromState].paths[0].steps.concat({ state: stateMap.get(fromState)!, - event: deserializeEventString(fromEvent!) as TEvent + event: fromEvent! }), weight } @@ -262,91 +288,22 @@ export function getShortestPaths< }; }); - return statePathMap; + return Object.values(statePlanMap); } -export function getSimplePaths< - TContext = DefaultContext, - TEvent extends EventObject = EventObject ->( - machine: StateMachine, - options?: ValueAdjMapOptions -): StatePathsMap { - const optionsWithDefaults = getValueAdjMapOptions(options); - - const { stateSerializer } = optionsWithDefaults; - - if (!machine.states) { - return EMPTY_MAP; - } - - // @ts-ignore - excessively deep - const adjacency = getAdjacencyMap(machine, optionsWithDefaults); - const stateMap = new Map>(); - const visited = new Set(); - const path: Segments = []; - const paths: StatePathsMap = {}; - - function util(fromState: State, toStateSerial: string) { - const fromStateSerial = stateSerializer(fromState); - visited.add(fromStateSerial); - - if (fromStateSerial === toStateSerial) { - if (!paths[toStateSerial]) { - paths[toStateSerial] = { - state: stateMap.get(toStateSerial)!, - paths: [] - }; - } - paths[toStateSerial].paths.push({ - state: fromState, - weight: path.length, - segments: [...path] - }); - } else { - for (const subEvent of Object.keys(adjacency[fromStateSerial])) { - const nextSegment = adjacency[fromStateSerial][subEvent]; - - if (!nextSegment) { - continue; - } - - const nextStateSerial = stateSerializer(nextSegment.state); - stateMap.set(nextStateSerial, nextSegment.state); - - if (!visited.has(nextStateSerial)) { - path.push({ - state: stateMap.get(fromStateSerial)!, - event: deserializeEventString(subEvent) - }); - util(nextSegment.state, toStateSerial); - } - } - } - - path.pop(); - visited.delete(fromStateSerial); - } - - const initialStateSerial = stateSerializer(machine.initialState); - stateMap.set(initialStateSerial, machine.initialState); - - for (const nextStateSerial of Object.keys(adjacency)) { - util(machine.initialState, nextStateSerial); - } - - return paths; -} +export function getSimplePlans( + machine: TMachine, + options?: Partial, EventFrom>> +): Array, EventFrom>> { + const resolvedOptions = resolveTraversalOptions( + options, + defaultMachineStateOptions + ); -export function getSimplePathsAsArray< - TContext = DefaultContext, - TEvent extends EventObject = EventObject ->( - machine: StateMachine, - options?: ValueAdjMapOptions -): Array> { - const result = getSimplePaths(machine, options); - return Object.keys(result).map((key) => result[key]); + return traverseSimplePlans( + machine as SimpleBehavior, + resolvedOptions + ); } export function toDirectedGraph( @@ -360,7 +317,7 @@ export function toDirectedGraph( const edge: DirectedGraphEdge = { id: `${stateNode.id}:${transitionIndex}:${targetIndex}`, source: stateNode as AnyStateNode, - target, + target: target as AnyStateNode, transition: t, label: { text: t.eventType, @@ -393,64 +350,297 @@ export function toDirectedGraph( } export function getPathFromEvents< - TContext = DefaultContext, + TState, TEvent extends EventObject = EventObject >( - machine: StateMachine, - events: Array -): StatePath { - const optionsWithDefaults = getValueAdjMapOptions({ - events: events.reduce((events, event) => { - events[event.type] ??= []; - events[event.type].push(event); - return events; - }, {}) - }); + behavior: SimpleBehavior, + events: TEvent[] +): StatePath { + const optionsWithDefaults = resolveTraversalOptions( + { + getEvents: () => { + return events; + } + }, + defaultMachineStateOptions as any + ); - const { stateSerializer, eventSerializer } = optionsWithDefaults; + const { serializeState, serializeEvent } = optionsWithDefaults; - if (!machine.states) { - return { - state: machine.initialState, - segments: [], - weight: 0 - }; - } + const adjacency = performDepthFirstTraversal(behavior, optionsWithDefaults); - const adjacency = getAdjacencyMap(machine, optionsWithDefaults); - const stateMap = new Map>(); - const path: Segments = []; + const stateMap = new Map(); + const path: Steps = []; - const initialStateSerial = stateSerializer(machine.initialState); - stateMap.set(initialStateSerial, machine.initialState); + const initialStateSerial = serializeState(behavior.initialState, null); + stateMap.set(initialStateSerial, behavior.initialState); let stateSerial = initialStateSerial; - let state = machine.initialState; + let state = behavior.initialState; for (const event of events) { path.push({ state: stateMap.get(stateSerial)!, event }); - const eventSerial = eventSerializer(event); - const nextSegment = adjacency[stateSerial][eventSerial]; + const eventSerial = serializeEvent(event); + const { state: nextState, event: _nextEvent } = adjacency[ + stateSerial + ].transitions[eventSerial]; - if (!nextSegment) { + if (!nextState) { throw new Error( `Invalid transition from ${stateSerial} with ${eventSerial}` ); } - const nextStateSerial = stateSerializer(nextSegment.state); - stateMap.set(nextStateSerial, nextSegment.state); + const nextStateSerial = serializeState(nextState, event); + stateMap.set(nextStateSerial, nextState); stateSerial = nextStateSerial; - state = nextSegment.state; + state = nextState; } return { state, - segments: path, + steps: path, weight: path.length }; } + +interface AdjacencyMap { + [key: SerializedState]: { + state: TState; + transitions: { + [key: SerializedEvent]: { + event: TEvent; + state: TState; + }; + }; + }; +} + +export function performDepthFirstTraversal( + behavior: SimpleBehavior, + options: TraversalOptions +): AdjacencyMap { + const { transition, initialState } = behavior; + const { + serializeEvent, + serializeState, + getEvents, + eventCases, + traversalLimit: limit + } = resolveTraversalOptions(options); + const adj: AdjacencyMap = {}; + + let iterations = 0; + const queue: Array<[TState, TEvent | null]> = [[initialState, null]]; + + while (queue.length) { + const [state, event] = queue.shift()!; + + if (iterations++ > limit) { + throw new Error('Traversal limit exceeded'); + } + + const serializedState = serializeState(state, event); + if (adj[serializedState]) { + continue; + } + + adj[serializedState] = { + state, + transitions: {} + }; + + const events = getEvents(state, eventCases); + + for (const subEvent of events) { + const nextState = transition(state, subEvent); + + if (!options.filter || options.filter(nextState, subEvent)) { + adj[serializedState].transitions[ + serializeEvent(subEvent) as SerializedEvent + ] = { + event: subEvent, + state: nextState + }; + queue.push([nextState, subEvent]); + } + } + } + + return adj; +} + +function resolveTraversalOptions( + traversalOptions?: Partial>, + defaultOptions?: TraversalOptions +): Required> { + const serializeState = + traversalOptions?.serializeState ?? + defaultOptions?.serializeState ?? + ((state) => JSON.stringify(state)); + return { + serializeState, + serializeEvent, + filter: () => true, + eventCases: {}, + getEvents: () => [], + traversalLimit: Infinity, + ...defaultOptions, + ...traversalOptions + }; +} + +export function traverseSimplePlans( + behavior: SimpleBehavior, + options: Partial> +): Array> { + const { initialState } = behavior; + const resolvedOptions = resolveTraversalOptions(options); + const serializeState = resolvedOptions.serializeState as ( + ...args: Parameters + ) => SerializedState; + const adjacency = performDepthFirstTraversal(behavior, resolvedOptions); + const stateMap = new Map(); + const visitCtx: VisitedContext = { + vertices: new Set(), + edges: new Set() + }; + const path: any[] = []; + const pathMap: Record< + SerializedState, + { state: TState; paths: Array> } + > = {}; + + function util( + fromState: TState, + toStateSerial: SerializedState, + event: TEvent | null + ) { + const fromStateSerial = serializeState(fromState, event); + visitCtx.vertices.add(fromStateSerial); + + if (fromStateSerial === toStateSerial) { + if (!pathMap[toStateSerial]) { + pathMap[toStateSerial] = { + state: stateMap.get(toStateSerial)!, + paths: [] + }; + } + + const toStatePlan = pathMap[toStateSerial]; + + const path2: StatePath = { + state: fromState, + weight: path.length, + steps: [...path] + }; + + toStatePlan.paths.push(path2); + } else { + for (const serializedEvent of Object.keys( + adjacency[fromStateSerial].transitions + ) as SerializedEvent[]) { + const { state: nextState, event: subEvent } = adjacency[ + fromStateSerial + ].transitions[serializedEvent]; + + if (!(serializedEvent in adjacency[fromStateSerial].transitions)) { + continue; + } + + const nextStateSerial = serializeState(nextState, subEvent); + stateMap.set(nextStateSerial, nextState); + + if (!visitCtx.vertices.has(serializeState(nextState, subEvent))) { + visitCtx.edges.add(serializedEvent); + path.push({ + state: stateMap.get(fromStateSerial)!, + event: subEvent + }); + util(nextState, toStateSerial, subEvent); + } + } + } + + path.pop(); + visitCtx.vertices.delete(fromStateSerial); + } + + const initialStateSerial = serializeState(initialState, null); + stateMap.set(initialStateSerial, initialState); + + for (const nextStateSerial of Object.keys(adjacency) as SerializedState[]) { + util(initialState, nextStateSerial, null); + } + + return Object.values(pathMap); +} + +function filterPlans( + plans: Array>, + predicate: (state: TState, plan: StatePlan) => boolean +): Array> { + const filteredPlans = plans.filter((plan) => predicate(plan.state, plan)); + + return filteredPlans; +} + +export function traverseSimplePathsTo( + behavior: SimpleBehavior, + predicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const simplePlansMap = traverseSimplePlans(behavior, resolvedOptions); + + return filterPlans(simplePlansMap, predicate); +} + +export function traverseSimplePathsFromTo( + behavior: SimpleBehavior, + fromPredicate: (state: TState) => boolean, + toPredicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const simplePlansMap = traverseSimplePlans(behavior, resolvedOptions); + + // Return all plans that contain a "from" state and target a "to" state + return filterPlans(simplePlansMap, (state, plan) => { + return ( + toPredicate(state) && plan.paths.some((path) => fromPredicate(path.state)) + ); + }); +} + +export function traverseShortestPathsTo( + behavior: SimpleBehavior, + predicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const simplePlansMap = traverseShortestPlans(behavior, resolvedOptions); + + return filterPlans(simplePlansMap, predicate); +} + +export function traverseShortestPathsFromTo( + behavior: SimpleBehavior, + fromPredicate: (state: TState) => boolean, + toPredicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const shortesPlansMap = traverseShortestPlans(behavior, resolvedOptions); + + // Return all plans that contain a "from" state and target a "to" state + return filterPlans(shortesPlansMap, (state, plan) => { + return ( + toPredicate(state) && plan.paths.some((path) => fromPredicate(path.state)) + ); + }); +} diff --git a/packages/xstate-graph/src/index.ts b/packages/xstate-graph/src/index.ts index 78e99e57a7..95e0060c22 100644 --- a/packages/xstate-graph/src/index.ts +++ b/packages/xstate-graph/src/index.ts @@ -1,21 +1,15 @@ -import { - getStateNodes, - getPathFromEvents, - getSimplePaths, - getShortestPaths, - serializeEvent, - serializeState, - toDirectedGraph -} from './graph'; - export { getStateNodes, getPathFromEvents, - getSimplePaths, - getShortestPaths, + getSimplePlans, + getShortestPlans, serializeEvent, - serializeState, - toDirectedGraph -}; + serializeMachineState as serializeState, + toDirectedGraph, + performDepthFirstTraversal, + traverseShortestPlans, + traverseSimplePlans, + traverseSimplePathsTo +} from './graph'; export * from './types'; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index f4b19e1c57..a3eac716e2 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -1,5 +1,4 @@ import { - State, EventObject, StateValue, StateNode, @@ -57,58 +56,59 @@ export type DirectedGraphNode = JSONSerializable< } >; -export interface AdjacencyMap { - [stateId: string]: Record< - string, +export interface ValueAdjacencyMap { + [stateId: SerializedState]: Record< + SerializedState, { - state: State; + state: TState; event: TEvent; } >; } -export interface StatePaths { +export interface StatePlan { /** * The target state. */ - state: State; + state: TState; /** * The paths that reach the target state. */ - paths: Array>; + paths: Array>; } -export interface StatePath { +export interface StatePath { /** * The ending state of the path. */ - state: State; + state: TState; /** - * The ordered array of state-event pairs (segments) which reach the ending `state`. + * The ordered array of state-event pairs (steps) which reach the ending `state`. */ - segments: Segments; + steps: Steps; /** - * The combined weight of all segments in the path. + * The combined weight of all steps in the path. */ weight: number; } -export interface StatePathsMap { - [key: string]: StatePaths; +export interface StatePlanMap { + [key: string]: StatePlan; } -export interface Segment { + +export interface Step { /** * The current state before taking the event. */ - state: State; + state: TState; /** * The event to be taken from the specified state. */ event: TEvent; } -export type Segments = Array< - Segment +export type Steps = Array< + Step >; export type ExtractEvent< @@ -116,13 +116,72 @@ export type ExtractEvent< TType extends TEvent['type'] > = TEvent extends { type: TType } ? TEvent : never; -export interface ValueAdjMapOptions { +export interface ValueAdjacencyMapOptions { events?: { [K in TEvent['type']]?: | Array> - | ((state: State) => Array>); + | ((state: TState) => Array>); }; - filter?: (state: State) => boolean; - stateSerializer?: (state: State) => string; + filter?: (state: TState) => boolean; + stateSerializer?: (state: TState) => string; eventSerializer?: (event: TEvent) => string; } + +export interface VisitedContext { + vertices: Set; + edges: Set; + a?: TState | TEvent; // TODO: remove +} + +export interface SerializationOptions { + eventCases: EventCaseMap; + serializeState: (state: TState, event: TEvent | null) => string; + serializeEvent: (event: TEvent) => string; +} + +/** + * A sample event object payload (_without_ the `type` property). + * + * @example + * + * ```js + * { + * value: 'testValue', + * other: 'something', + * id: 42 + * } + * ``` + */ +type EventCase = Omit; + +export interface TraversalOptions + extends SerializationOptions { + filter?: (state: TState, event: TEvent) => boolean; + getEvents?: ( + state: TState, + cases: EventCaseMap + ) => ReadonlyArray; + /** + * The maximum number of traversals to perform when calculating + * the state transition adjacency map. + * + * @default `Infinity` + */ + traversalLimit?: number; +} + +export type EventCaseMap = { + [E in TEvent as E['type']]?: + | ((state: TState) => Array>) + | Array>; +}; + +type Brand = T & { __tag: Tag }; + +export type SerializedState = Brand; +export type SerializedEvent = Brand; + +export interface SimpleBehavior { + transition: (state: TState, event: TEvent) => TState; + initialState: TState; +} diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index ad13a435de..20a6043832 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -2,9743 +2,489 @@ exports[`@xstate/graph getPathFromEvents() should return a path to the last entered state by the event sequence: path from events 1`] = ` Object { - "segments": Array [ + "state": Object { + "red": "flashing", + }, + "steps": Array [ Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, + "eventType": "TIMER", + "state": "green", }, Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, + "eventType": "TIMER", + "state": "yellow", }, Object { - "event": Object { - "type": "TIMER", - }, + "eventType": "TIMER", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, + "red": "walk", }, }, Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, + "eventType": "POWER_OUTAGE", + "state": "green", }, ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", +} +`; + +exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states (parallel): shortest paths parallel 1`] = ` +Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", + "steps": Array [], + }, + Object { + "state": Object { + "a": "a2", + "b": "b2", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", + "steps": Array [ + Object { + "eventType": "2", + "state": Object { + "a": "a1", + "b": "b1", }, - "name": "xstate.init", - "type": "external", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", + ], + }, + Object { + "state": Object { + "a": "a3", + "b": "b3", }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, + "steps": Array [ + Object { + "eventType": "3", + "state": Object { + "a": "a1", + "b": "b1", }, - "yellow": undefined, }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, + ], }, - "weight": 4, -} +] `; -exports[`@xstate/graph getShortestPaths() should represent conditional paths based on context: shortest paths conditional 1`] = ` -Object { - "\\"bar\\" | {\\"id\\":\\"foo\\"}": Object { - "paths": Array [ +exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states: shortest paths 1`] = ` +Array [ + Object { + "state": "green", + "steps": Array [], + }, + Object { + "state": "yellow", + "steps": Array [ Object { - "segments": Array [ - Object { - "event": Object { - "id": "whatever", - "type": "EVENT", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "id": "whatever", - "type": "EVENT", - }, - "name": "EVENT", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "id": "whatever", - "type": "EVENT", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", - }, - "historyValue": Object { - "current": "bar", - "states": Object { - "bar": undefined, - "foo": undefined, - "pending": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "bar", - }, - "weight": 1, + "eventType": "TIMER", + "state": "green", }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "id": "whatever", - "type": "EVENT", - }, - "name": "EVENT", - "type": "external", + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "POWER_OUTAGE", + "state": "green", + }, + ], + }, + Object { + "state": Object { + "red": "walk", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "id": "foo", + Object { + "eventType": "TIMER", + "state": "yellow", }, - "done": false, - "event": Object { - "id": "whatever", - "type": "EVENT", + ], + }, + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", + Object { + "eventType": "TIMER", + "state": "yellow", }, - "historyValue": Object { - "current": "bar", - "states": Object { - "bar": undefined, - "foo": undefined, - "pending": undefined, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "bar", - }, + ], }, - "\\"foo\\" | {\\"id\\":\\"foo\\"}": Object { - "paths": Array [ + Object { + "state": Object { + "red": "stop", + }, + "steps": Array [ Object { - "segments": Array [ - Object { - "event": Object { - "type": "STATE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", - }, - }, - ], + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "STATE", - }, - "name": "STATE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "STATE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", - }, - "historyValue": Object { - "current": "foo", - "states": Object { - "bar": undefined, - "foo": undefined, - "pending": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "foo", + "red": "walk", }, - "weight": 1, }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "STATE", + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", }, - "name": "STATE", - "type": "external", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "id": "foo", + ], + }, +] +`; + +exports[`@xstate/graph getSimplePaths() should return a mapping of arrays of simple paths to all states 2`] = ` +Array [ + Object { + "state": "green", + "steps": Array [], + }, + Object { + "state": "yellow", + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "done": false, - "event": Object { - "type": "STATE", + Object { + "eventType": "TIMER", + "state": "yellow", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "xstate.init", + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", }, - "historyValue": Object { - "current": "foo", - "states": Object { - "bar": undefined, - "foo": undefined, - "pending": undefined, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", }, }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "foo", - }, - }, - "\\"pending\\" | {\\"id\\":\\"foo\\"}": Object { - "paths": Array [ Object { - "segments": Array [], + "eventType": "POWER_OUTAGE", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", + "red": "stop", }, - "weight": 0, }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", + Object { + "eventType": "TIMER", + "state": "yellow", }, - "done": false, - "event": Object { - "type": "xstate.init", + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", + }, }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", - }, - }, -} -`; - -exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states (parallel): shortest paths parallel 1`] = ` -Object { - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}": Object { - "paths": Array [ Object { - "segments": Array [], + "eventType": "POWER_OUTAGE", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "1", - }, - "name": "1", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "1", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a1", - "b": "b1", - }, - "states": Object { - "a": Object { - "current": "a1", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b1", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, + "red": "wait", }, - "weight": 0, }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "1", - }, - "name": "1", - "type": "external", + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "1", + Object { + "eventType": "TIMER", + "state": "yellow", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a1", - "b": "b1", - }, - "states": Object { - "a": Object { - "current": "a1", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b1", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - }, - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}": Object { - "paths": Array [ Object { - "segments": Array [ - Object { - "event": Object { - "type": "2", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "1", - }, - "name": "1", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "1", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a1", - "b": "b1", - }, - "states": Object { - "a": Object { - "current": "a1", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b1", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - }, - ], + "eventType": "POWER_OUTAGE", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, + "red": "walk", }, - "weight": 1, }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", + Object { + "eventType": "POWER_OUTAGE", + "state": "yellow", }, - }, + ], }, - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}": Object { - "paths": Array [ + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ Object { - "segments": Array [ - Object { - "event": Object { - "type": "3", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "1", - }, - "name": "1", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "1", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a1", - "b": "b1", - }, - "states": Object { - "a": Object { - "current": "a1", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b1", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "3", - }, - "name": "3", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "3", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a3", - "b": "b3", - }, - "states": Object { - "a": Object { - "current": "a3", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b3", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a3", - "b": "b3", - }, - }, - "weight": 1, + "eventType": "POWER_OUTAGE", + "state": "green", }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "3", - }, - "name": "3", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "3", + "red": "walk", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, + Object { + "eventType": "TIMER", + "state": "yellow", }, - "historyValue": Object { - "current": Object { - "a": "a3", - "b": "b3", - }, - "states": Object { - "a": Object { - "current": "a3", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b3", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, + ], + }, + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a3", - "b": "b3", + Object { + "eventType": "TIMER", + "state": "yellow", }, - }, - }, -} -`; - -exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states: shortest paths 1`] = ` -Object { - "\\"green\\"": Object { - "paths": Array [ Object { - "segments": Array [], + "eventType": "PED_COUNTDOWN", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", + "red": "walk", }, - "weight": 0, }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", + "red": "stop", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, + Object { + "eventType": "TIMER", + "state": "yellow", }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - "\\"yellow\\"": Object { - "paths": Array [ Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - ], + "eventType": "PED_COUNTDOWN", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", + "red": "wait", }, - "weight": 1, }, ], + }, +] +`; + +exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths to all states (parallel): simple paths parallel 1`] = ` +Array [ + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", + "a": "a1", + "b": "b1", }, + "steps": Array [], }, - "{\\"red\\":\\"flashing\\"}": Object { - "paths": Array [ + Object { + "state": Object { + "a": "a2", + "b": "b2", + }, + "steps": Array [ Object { - "segments": Array [ - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - ], + "eventType": "2", "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, + "a": "a1", + "b": "b1", }, - "weight": 1, }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, + "a": "a3", + "b": "b3", + }, + "steps": Array [ + Object { + "eventType": "2", + "state": Object { + "a": "a1", + "b": "b1", }, }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - }, - "{\\"red\\":\\"stop\\"}": Object { - "paths": Array [ Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "weight": 4, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - }, - "{\\"red\\":\\"wait\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - "weight": 3, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - "{\\"red\\":\\"walk\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "weight": 2, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, -} -`; - -exports[`@xstate/graph getSimplePaths() should return a mapping of arrays of simple paths to all states: simple paths 1`] = ` -Object { - "\\"green\\"": Object { - "paths": Array [ - Object { - "segments": Array [], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - "weight": 0, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - "\\"yellow\\"": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - "{\\"red\\":\\"flashing\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 5, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 4, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 3, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 2, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - }, - "{\\"red\\":\\"stop\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "weight": 4, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - }, - "{\\"red\\":\\"wait\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - "weight": 3, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - "{\\"red\\":\\"walk\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "weight": 2, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, -} -`; - -exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths to all states (parallel): simple paths parallel 1`] = ` -Object { - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}": Object { - "paths": Array [ - Object { - "segments": Array [], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - "weight": 0, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - }, - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "2", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - }, - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "2", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - }, - Object { - "event": Object { - "type": "3", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "3", - }, - "name": "3", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "3", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a3", - "b": "b3", - }, - "states": Object { - "a": Object { - "current": "a3", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b3", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a3", - "b": "b3", - }, - }, - "weight": 2, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "3", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "1", - }, - "name": "1", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "1", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a1", - "b": "b1", - }, - "states": Object { - "a": Object { - "current": "a1", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b1", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "3", - }, - "name": "3", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "3", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a1", - "b": "b1", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a3", - "b": "b3", - }, - "states": Object { - "a": Object { - "current": "a3", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b3", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a3", - "b": "b3", - }, - }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "3", - }, - "name": "3", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "3", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "2", - }, - "name": "2", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "2", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "a": "a2", - "b": "b2", - }, - "states": Object { - "a": Object { - "current": "a2", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b2", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a2", - "b": "b2", - }, - }, - "historyValue": Object { - "current": Object { - "a": "a3", - "b": "b3", - }, - "states": Object { - "a": Object { - "current": "a3", - "states": Object { - "a1": undefined, - "a2": undefined, - "a3": undefined, - }, - }, - "b": Object { - "current": "b3", - "states": Object { - "b1": undefined, - "b2": undefined, - "b3": undefined, - }, - }, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "a": "a3", - "b": "b3", - }, - }, - }, -} -`; - -exports[`@xstate/graph getSimplePaths() should return multiple paths for equivalent transitions: simple paths equal transitions 1`] = ` -Object { - "\\"a\\"": Object { - "paths": Array [ - Object { - "segments": Array [], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "a", - }, - "weight": 0, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "a", - }, - }, - "\\"b\\"": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "FOO", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "a", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "FOO", - }, - "name": "FOO", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "FOO", - }, - "events": Array [], - "historyValue": Object { - "current": "b", - "states": Object { - "a": undefined, - "b": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "b", - }, - "weight": 1, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "BAR", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "a", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "BAR", - }, - "name": "BAR", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "BAR", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "a", - }, - "historyValue": Object { - "current": "b", - "states": Object { - "a": undefined, - "b": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "b", - }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "FOO", - }, - "name": "FOO", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "FOO", - }, - "events": Array [], - "historyValue": Object { - "current": "b", - "states": Object { - "a": undefined, - "b": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "b", - }, - }, -} -`; - -exports[`@xstate/graph getSimplePaths() should return value-based paths: simple paths context 1`] = ` -Object { - "\\"finish\\" | {\\"count\\":3}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "INC", - "value": 1, - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "count": 0, - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - Object { - "event": Object { - "type": "INC", - "value": 1, - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 1, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - Object { - "event": Object { - "type": "INC", - "value": 1, - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 2, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 3, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 2, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - "historyValue": Object { - "current": "finish", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "finish", - }, - "weight": 3, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 3, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 2, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - "historyValue": Object { - "current": "finish", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "finish", - }, - }, - "\\"start\\" | {\\"count\\":0}": Object { - "paths": Array [ - Object { - "segments": Array [], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "count": 0, - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - "weight": 0, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "count": 0, - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - "\\"start\\" | {\\"count\\":1}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "INC", - "value": 1, - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "count": 0, - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 1, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 1, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - "\\"start\\" | {\\"count\\":2}": Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "INC", - "value": 1, - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "count": 0, - }, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - Object { - "event": Object { - "type": "INC", - "value": 1, - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 1, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 2, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - "weight": 2, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "INC", - "value": 1, - }, - "name": "INC", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "count": 2, - }, - "done": false, - "event": Object { - "type": "INC", - "value": 1, - }, - "events": Array [], - "historyValue": Object { - "current": "start", - "states": Object { - "finish": undefined, - "start": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", - }, - }, -} -`; - -exports[`@xstate/graph getSimplePathsAsArray() should return an array of shortest paths to all states: simple paths array 1`] = ` -Array [ - Object { - "paths": Array [ - Object { - "segments": Array [], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - "weight": 0, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "weight": 2, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - "weight": 3, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - "weight": 4, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - }, - Object { - "paths": Array [ - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "stop", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "stop", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "stop", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 5, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "PED_COUNTDOWN", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "wait", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 4, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 3, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "yellow", - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 2, - }, - Object { - "segments": Array [ - Object { - "event": Object { - "type": "POWER_OUTAGE", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "TIMER", - }, - "name": "TIMER", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "TIMER", - }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "walk", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", - }, - "name": "POWER_OUTAGE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", - }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", - }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, + "eventType": "3", + "state": Object { + "a": "a2", + "b": "b2", }, - "weight": 1, }, ], + }, + Object { "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", + "a": "a3", + "b": "b3", + }, + "steps": Array [ + Object { + "eventType": "3", + "state": Object { + "a": "a1", + "b": "b1", }, - "name": "POWER_OUTAGE", - "type": "external", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "POWER_OUTAGE", + ], + }, +] +`; + +exports[`@xstate/graph getSimplePaths() should return multiple paths for equivalent transitions: simple paths equal transitions 1`] = ` +Array [ + Object { + "state": "a", + "steps": Array [], + }, + Object { + "state": "b", + "steps": Array [ + Object { + "eventType": "FOO", + "state": "a", + }, + ], + }, + Object { + "state": "b", + "steps": Array [ + Object { + "eventType": "BAR", + "state": "a", }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "flashing", - }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, - }, - "yellow": undefined, - }, + ], + }, +] +`; + +exports[`@xstate/graph getSimplePaths() should return value-based paths: simple paths context 1`] = ` +Array [ + Object { + "state": "start", + "steps": Array [], + }, + Object { + "state": "start", + "steps": Array [ + Object { + "eventType": "INC", + "state": "start", + }, + ], + }, + Object { + "state": "start", + "steps": Array [ + Object { + "eventType": "INC", + "state": "start", }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", + Object { + "eventType": "INC", + "state": "start", }, - }, + ], + }, + Object { + "state": "finish", + "steps": Array [ + Object { + "eventType": "INC", + "state": "start", + }, + Object { + "eventType": "INC", + "state": "start", + }, + Object { + "eventType": "INC", + "state": "start", + }, + ], }, ] `; @@ -9835,3 +581,369 @@ Object { "id": "light", } `; + +exports[`shortest paths for reducers 1`] = ` +Array [ + Object { + "state": 0, + "steps": Array [], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "reset", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + Object { + "eventType": "reset", + "state": 2, + }, + Object { + "eventType": "b", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "reset", + "state": 1, + }, + Object { + "eventType": "b", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + Object { + "eventType": "b", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + Object { + "eventType": "reset", + "state": 2, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "reset", + "state": 1, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + Object { + "eventType": "reset", + "state": 2, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "reset", + "state": 1, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "reset", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "reset", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + Object { + "eventType": "b", + "state": 0, + }, + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + ], + }, +] +`; + +exports[`simple paths for reducers 1`] = ` +Array [ + Object { + "state": 0, + "steps": Array [], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + ], + }, +] +`; diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 63725661ea..5f1179e924 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -1,13 +1,52 @@ -import { Machine, StateNode, createMachine } from 'xstate'; +import { + Machine, + StateNode, + createMachine, + State, + EventObject, + StateValue +} from 'xstate'; import { getStateNodes, getPathFromEvents, - getSimplePaths, - getShortestPaths, - toDirectedGraph + getSimplePlans, + getShortestPlans, + toDirectedGraph, + StatePath, + StatePlan } from '../src/index'; -import { getSimplePathsAsArray, getAdjacencyMap } from '../src/graph'; +import { + getValueAdjacencyMap, + traverseShortestPlans, + traverseSimplePlans +} from '../src/graph'; import { assign } from 'xstate'; +import { flatten } from 'xstate/lib/utils'; + +function getPathsMapSnapshot( + plans: Array> +): Array> { + return flatten( + plans.map((plan) => { + return plan.paths.map(getPathSnapshot); + }) + ); +} + +function getPathSnapshot( + path: StatePath +): { + state: StateValue; + steps: Array<{ state: StateValue; eventType: string }>; +} { + return { + state: path.state instanceof State ? path.state.value : path.state, + steps: path.steps.map((step) => ({ + state: step.state instanceof State ? step.state.value : step.state, + eventType: step.event.type + })) + }; +} describe('@xstate/graph', () => { const pedestrianStates = { @@ -67,7 +106,7 @@ describe('@xstate/graph', () => { } type CondMachineEvents = { type: 'EVENT'; id: string } | { type: 'STATE' }; - const condMachine = Machine({ + const condMachine = createMachine({ key: 'cond', initial: 'pending', states: { @@ -94,7 +133,7 @@ describe('@xstate/graph', () => { } }); - const parallelMachine = Machine({ + const parallelMachine = createMachine({ type: 'parallel', key: 'p', states: { @@ -125,7 +164,7 @@ describe('@xstate/graph', () => { } }); - describe('getNodes()', () => { + describe('getStateNodes()', () => { it('should return an array of all nodes', () => { const nodes = getStateNodes(lightMachine); expect(nodes.every((node) => node instanceof StateNode)).toBe(true); @@ -158,63 +197,109 @@ describe('@xstate/graph', () => { describe('getShortestPaths()', () => { it('should return a mapping of shortest paths to all states', () => { - const paths = getShortestPaths(lightMachine) as any; + const paths = getShortestPlans(lightMachine); - expect(paths).toMatchSnapshot('shortest paths'); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot('shortest paths'); }); it('should return a mapping of shortest paths to all states (parallel)', () => { - const paths = getShortestPaths(parallelMachine) as any; - expect(paths).toMatchSnapshot('shortest paths parallel'); + const paths = getShortestPlans(parallelMachine); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'shortest paths parallel' + ); }); it('the initial state should have a zero-length path', () => { + const shortestPaths = getShortestPlans(lightMachine); + expect( - getShortestPaths(lightMachine)[ - JSON.stringify(lightMachine.initialState.value) - ].paths[0].segments + shortestPaths.find((plan) => + plan.state.matches(lightMachine.initialState.value) + )!.paths[0].steps ).toHaveLength(0); }); xit('should not throw when a condition is present', () => { - expect(() => getShortestPaths(condMachine)).not.toThrow(); + expect(() => getShortestPlans(condMachine)).not.toThrow(); }); - it('should represent conditional paths based on context', () => { - // explicit type arguments could be removed once davidkpiano/xstate#652 gets resolved - const paths = getShortestPaths( - condMachine.withContext({ + it.skip('should represent conditional paths based on context', () => { + const machine = createMachine({ + key: 'cond', + initial: 'pending', + context: { id: 'foo' - }), - { - events: { - EVENT: [ - { - type: 'EVENT', - id: 'whatever' - } - ], - STATE: [ - { - type: 'STATE' - } - ] - } + }, + states: { + pending: { + on: { + EVENT: [ + { + target: 'foo', + cond: (_, e) => e.id === 'foo' + }, + { target: 'bar' } + ], + STATE: [ + { + target: 'foo', + cond: (s) => s.id === 'foo' + }, + { target: 'bar' } + ] + } + }, + foo: {}, + bar: {} } - ); + }); + + const paths = getShortestPlans(machine, { + getEvents: () => + [ + { + type: 'EVENT', + id: 'whatever' + }, + { + type: 'STATE' + } + ] as const + }); - expect(paths).toMatchSnapshot('shortest paths conditional'); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'shortest paths conditional' + ); }); }); describe('getSimplePaths()', () => { it('should return a mapping of arrays of simple paths to all states', () => { - const paths = getSimplePaths(lightMachine) as any; + const paths = getSimplePlans(lightMachine); + + expect(paths.map((path) => path.state.value)).toMatchInlineSnapshot(` + Array [ + "green", + "yellow", + Object { + "red": "flashing", + }, + Object { + "red": "walk", + }, + Object { + "red": "wait", + }, + Object { + "red": "stop", + }, + ] + `); - expect(paths).toMatchSnapshot('simple paths'); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot(); }); - const equivMachine = Machine({ + const equivMachine = createMachine({ initial: 'a', states: { a: { on: { FOO: 'b', BAR: 'b' } }, @@ -223,23 +308,63 @@ describe('@xstate/graph', () => { }); it('should return a mapping of simple paths to all states (parallel)', () => { - const paths = getSimplePaths(parallelMachine); - expect(paths).toMatchSnapshot('simple paths parallel'); + const paths = getSimplePlans(parallelMachine); + + expect(paths.map((p) => p.state.value)).toMatchInlineSnapshot(` + Array [ + Object { + "a": "a1", + "b": "b1", + }, + Object { + "a": "a2", + "b": "b2", + }, + Object { + "a": "a3", + "b": "b3", + }, + ] + `); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'simple paths parallel' + ); }); it('should return multiple paths for equivalent transitions', () => { - const paths = getSimplePaths(equivMachine); - expect(paths).toMatchSnapshot('simple paths equal transitions'); + const paths = getSimplePlans(equivMachine); + + expect(paths.map((p) => p.state.value)).toMatchInlineSnapshot(` + Array [ + "a", + "b", + ] + `); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'simple paths equal transitions' + ); }); it('should return a single empty path for the initial state', () => { - expect(getSimplePaths(lightMachine)['"green"'].paths).toHaveLength(1); expect( - getSimplePaths(lightMachine)['"green"'].paths[0].segments + getSimplePlans(lightMachine).find((p) => + p.state.matches(lightMachine.initialState.value) + )!.paths + ).toHaveLength(1); + expect( + getSimplePlans(lightMachine).find((p) => + p.state.matches(lightMachine.initialState.value) + )!.paths[0].steps ).toHaveLength(0); - expect(getSimplePaths(equivMachine)['"a"'].paths).toHaveLength(1); expect( - getSimplePaths(equivMachine)['"a"'].paths[0].segments + getSimplePlans(equivMachine).find((p) => + p.state.matches(equivMachine.initialState.value) + )!.paths + ).toHaveLength(1); + expect( + getSimplePlans(equivMachine).find((p) => + p.state.matches(equivMachine.initialState.value) + )!.paths[0].steps ).toHaveLength(0); }); @@ -251,7 +376,7 @@ describe('@xstate/graph', () => { type: 'INC'; value: number; } - const countMachine = Machine({ + const countMachine = createMachine({ id: 'count', initial: 'start', context: { @@ -275,22 +400,21 @@ describe('@xstate/graph', () => { } }); - const paths = getSimplePaths(countMachine, { - events: { - INC: [{ type: 'INC', value: 1 }] - } + const paths = getSimplePlans(countMachine, { + getEvents: () => [{ type: 'INC', value: 1 }] as const }); - expect(paths).toMatchSnapshot('simple paths context'); - }); - }); - - describe('getSimplePathsAsArray()', () => { - it('should return an array of shortest paths to all states', () => { - const pathsArray = getSimplePathsAsArray(lightMachine); - - expect(Array.isArray(pathsArray)).toBeTruthy(); - expect(pathsArray).toMatchSnapshot('simple paths array'); + expect(paths.map((p) => p.state.value)).toMatchInlineSnapshot(` + Array [ + "start", + "start", + "start", + "finish", + ] + `); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'simple paths context' + ); }); }); @@ -303,10 +427,10 @@ describe('@xstate/graph', () => { { type: 'POWER_OUTAGE' } ]); - expect(path).toMatchSnapshot('path from events'); + expect(getPathSnapshot(path)).toMatchSnapshot('path from events'); }); - it('should throw when an invalid event sequence is provided', () => { + it.skip('should throw when an invalid event sequence is provided', () => { expect(() => getPathFromEvents(lightMachine, [ { type: 'TIMER' }, @@ -316,7 +440,7 @@ describe('@xstate/graph', () => { }); }); - describe('getAdjacencyMap', () => { + describe('getValueAdjacencyMap', () => { it('should map adjacencies', () => { interface Ctx { count: number; @@ -324,7 +448,7 @@ describe('@xstate/graph', () => { } type Events = { type: 'INC'; value: number } | { type: 'DEC' }; - const counterMachine = Machine({ + const counterMachine = createMachine({ id: 'counter', initial: 'empty', context: { @@ -354,8 +478,7 @@ describe('@xstate/graph', () => { } }); - // explicit type arguments could be removed once davidkpiano/xstate#652 gets resolved - const adj = getAdjacencyMap(counterMachine, { + const adj = getValueAdjacencyMap(counterMachine, { filter: (state) => state.context.count >= 0 && state.context.count <= 5, stateSerializer: (state) => { const ctx = { @@ -395,13 +518,26 @@ describe('@xstate/graph', () => { } }); - const adj = getAdjacencyMap(machine, { + const adj = getValueAdjacencyMap(machine, { events: { - EVENT: (state) => [{ type: 'EVENT', value: state.context.count + 10 }] + EVENT: (state) => [ + { type: 'EVENT' as const, value: state.context.count + 10 } + ] } }); - expect(adj).toHaveProperty('"second" | {"count":10}'); + const states = flatten( + Object.values(adj).map((map) => Object.values(map)) + ); + + expect(states).toContainEqual( + expect.objectContaining({ + state: expect.objectContaining({ + value: 'second', + context: { count: 10 } + }) + }) + ); }); }); @@ -432,3 +568,100 @@ describe('@xstate/graph', () => { }); }); }); + +it('simple paths for reducers', () => { + const a = traverseShortestPlans( + { + transition: (s, e) => { + if (e.type === 'a') { + return 1; + } + if (e.type === 'b' && s === 1) { + return 2; + } + if (e.type === 'reset') { + return 0; + } + return s; + }, + initialState: 0 + }, + { + getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], + serializeState: (v, e) => JSON.stringify(v) + ' | ' + JSON.stringify(e) + } + ); + + expect(getPathsMapSnapshot(a)).toMatchSnapshot(); +}); + +it('shortest paths for reducers', () => { + const a = traverseSimplePlans( + { + transition: (s, e) => { + if (e.type === 'a') { + return 1; + } + if (e.type === 'b' && s === 1) { + return 2; + } + if (e.type === 'reset') { + return 0; + } + return s; + }, + initialState: 0 as number + }, + { + getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], + serializeState: (v, e) => JSON.stringify(v) + ' | ' + JSON.stringify(e) + } + ); + + expect(getPathsMapSnapshot(a)).toMatchSnapshot(); +}); + +describe('filtering', () => { + it('should not traverse past filtered states', () => { + const machine = createMachine<{ count: number }>({ + initial: 'counting', + context: { count: 0 }, + states: { + counting: { + on: { + INC: { + actions: assign({ + count: (ctx) => ctx.count + 1 + }) + } + } + } + } + }); + + const sp = getShortestPlans(machine, { + getEvents: () => [{ type: 'INC' }], + filter: (s) => s.context.count < 5 + }); + + expect(sp.map((p) => p.state.context)).toMatchInlineSnapshot(` + Array [ + Object { + "count": 0, + }, + Object { + "count": 1, + }, + Object { + "count": 2, + }, + Object { + "count": 3, + }, + Object { + "count": 4, + }, + ] + `); + }); +}); diff --git a/packages/xstate-graph/test/types.test.ts b/packages/xstate-graph/test/types.test.ts new file mode 100644 index 0000000000..eebd1e7ca5 --- /dev/null +++ b/packages/xstate-graph/test/types.test.ts @@ -0,0 +1,113 @@ +import { createMachine } from 'xstate'; +import { getShortestPlans } from '../src'; + +describe('types', () => { + it('`getEvents` should be allowed to return a mutable array', () => { + const machine = createMachine( + {} + ); + + getShortestPlans(machine, { + getEvents: () => [ + { + type: 'FOO' + } as const + ] + }); + }); + + it('`getEvents` should be allowed to return a readonly array', () => { + const machine = createMachine( + {} + ); + + getShortestPlans(machine, { + getEvents: () => + [ + { + type: 'FOO' + } + ] as const + }); + }); + + it('`eventCases` should allow known event', () => { + const machine = createMachine({}); + + getShortestPlans(machine, { + eventCases: { + FOO: [ + { + value: 100 + } + ] + } + }); + }); + + it('`eventCases` should not require all event types', () => { + const machine = createMachine< + unknown, + { type: 'FOO'; value: number } | { type: 'BAR'; value: number } + >({}); + + getShortestPlans(machine, { + eventCases: { + FOO: [ + { + value: 100 + } + ] + } + }); + }); + + it('`eventCases` should not allow unknown events', () => { + const machine = createMachine({}); + + getShortestPlans(machine, { + eventCases: { + // @ts-expect-error + UNKNOWN: [ + { + value: 100 + } + ] + } + }); + }); + + it('`eventCases` should only allow props of a specific event', () => { + const machine = createMachine< + unknown, + { type: 'FOO'; value: number } | { type: 'BAR'; other: string } + >({}); + + getShortestPlans(machine, { + eventCases: { + FOO: [ + { + // @ts-expect-error + other: 'nana nana nananana' + } + ] + } + }); + }); + + it('`serializeEvent` should be allowed to return plain string', () => { + const machine = createMachine({}); + + getShortestPlans(machine, { + serializeEvent: () => '' + }); + }); + + it('`serializeState` should be allowed to return plain string', () => { + const machine = createMachine({}); + + getShortestPlans(machine, { + serializeState: () => '' + }); + }); +}); diff --git a/packages/xstate-react/test/useSpawn.test.tsx b/packages/xstate-react/test/useSpawn.test.tsx index 6a057f6350..99d2b56680 100644 --- a/packages/xstate-react/test/useSpawn.test.tsx +++ b/packages/xstate-react/test/useSpawn.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, screen } from '@testing-library/react'; import * as React from 'react'; -import { fromReducer } from 'xstate/src/behaviors'; +import { fromReducer } from 'xstate/lib/behaviors'; import { useActor, useSpawn } from '../src'; import { describeEachReactMode } from './utils'; diff --git a/packages/xstate-test/README.md b/packages/xstate-test/README.md index f09f21d4ca..bb791980bd 100644 --- a/packages/xstate-test/README.md +++ b/packages/xstate-test/README.md @@ -1,9 +1,10 @@ # @xstate/test -This package contains utilities for facilitating [model-based testing](https://en.wikipedia.org/wiki/Model-based_testing) for any software. +Built self-documenting, easy-to-maintain tests which integrate with _any test framework_ using Statecharts. -- [Read the full documentation in the XState docs](https://xstate.js.org/docs/packages/xstate-test/). -- [Read our contribution guidelines](https://github.com/statelyai/xstate/blob/main/CONTRIBUTING.md). +Create a visual model, tell it how to interact with your app, and execute the model to **test your app automatically**. + +Read our intro to model-based testing [in our beta docs](https://graph-docs.vercel.app/model-based-testing/intro) ## Talk @@ -14,104 +15,87 @@ This package contains utilities for facilitating [model-based testing](https://e 1. Install `xstate` and `@xstate/test`: ```bash -npm install xstate @xstate/test +yarn add xstate @xstate/test ``` -2. Create the machine that will be used to model the system under test (SUT): +2. Create your test machine using `createTestMachine`: -```js -import { createMachine } from 'xstate'; +```ts +import { createTestMachine } from '@xstate/test'; -const toggleMachine = createMachine({ - id: 'toggle', - initial: 'inactive', +const machine = createTestMachine({ + initial: 'onHomePage', states: { - inactive: { + onHomePage: { on: { - TOGGLE: 'active' + SEARCH_FOR_MODEL_BASED_TESTING: 'searchResultsVisible' } }, - active: { + searchResultsVisible: { on: { - TOGGLE: 'inactive' - } - } - } -}); -``` - -3. Add assertions for each state in the machine (in this example, using [Puppeteer](https://github.com/GoogleChrome/puppeteer)): - -```js -// ... - -const toggleMachine = createMachine({ - id: 'toggle', - initial: 'inactive', - states: { - inactive: { - on: { - /* ... */ - }, - meta: { - test: async (page) => { - await page.waitFor('input:checked'); - } + CLICK_MODEL_BASED_TESTING_RESULT: 'onModelBasedTestingPage', + PRESS_ESCAPE: 'searchBoxClosed' } }, - active: { - on: { - /* ... */ - }, - meta: { - test: async (page) => { - await page.waitFor('input:not(:checked)'); - } - } - } + searchBoxClosed: {}, + onModelBasedTestingPage: {} } }); ``` -4. Create the model: - -```js -import { createMachine } from 'xstate'; -import { createModel } from '@xstate/test'; +3. Turn the machine into a test model using `createTestModel` -const toggleMachine = createMachine(/* ... */); +```ts +import { createTestModel } from '@xstate/test'; -const toggleModel = createModel(toggleMachine).withEvents({ - TOGGLE: { - exec: async (page) => { - await page.click('input'); - } - } -}); +const model = createTestModel(machine); ``` -5. Create test plans and run the tests with coverage: - -```js -// ... - -describe('toggle', () => { - const testPlans = toggleModel.getShortestPathPlans(); - - testPlans.forEach((plan) => { - describe(plan.description, () => { - plan.paths.forEach((path) => { - it(path.description, async () => { - // do any setup, then... - - await path.test(page); - }); +4. Run the model in a test, passing it the instructions on what to do on different `states` and `events`. For example, using [Cypress](https://www.cypress.io/). + +```ts +describe('Toggle component', () => { + /** + * For each path generated by XState, + * run a new test via `it` + */ + model.getPaths().forEach((path) => { + it(path.description, () => { + // Run any setup before each test here + + /** + * In environments, like Cypress, + * that don’t support async, run plan.testSync(); + * + * Otherwise, you can run await plan.test(); + */ + path.testSync({ + states: { + onHomePage: () => { + cy.visit('/'); + }, + searchResultsVisible: () => { + cy.findByText('Model-based testing').should('be.visible'); + }, + searchBoxClosed: () => { + cy.findByText('Model-based testing').should('not.be.visible'); + }, + onModelBasedTestingPage: () => { + cy.url().should('include', '/model-based-testing/intro'); + } + }, + events: { + CLICK_MODEL_BASED_TESTING_RESULT: () => { + cy.findByText('Model-based testing').click(); + }, + SEARCH_FOR_MODEL_BASED_TESTING: () => { + cy.findByPlaceholderText('Search').type('Model-based testing'); + } + } }); - }); - }); - it('should have full coverage', () => { - return toggleModel.testCoverage(); + // Run any cleanup after each test here + }); }); }); ``` diff --git a/packages/xstate-test/package.json b/packages/xstate-test/package.json index a62b5c155d..6af5f053b0 100644 --- a/packages/xstate-test/package.json +++ b/packages/xstate-test/package.json @@ -19,10 +19,6 @@ "main": "lib/index.js", "module": "es/index.js", "types": "lib/index.d.ts", - "browser": { - "./lib/slimChalk.js": "./lib/slimChalk.browser.js", - "./es/slimChalk.js": "./es/slimChalk.browser.js" - }, "sideEffects": false, "files": [ "lib/**/*.js", @@ -55,7 +51,6 @@ "xstate": "*" }, "dependencies": { - "@xstate/graph": "^1.4.2", - "chalk": "^2.4.2" + "@xstate/graph": "^1.4.2" } } diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts new file mode 100644 index 0000000000..a1f6b5ed6c --- /dev/null +++ b/packages/xstate-test/src/TestModel.ts @@ -0,0 +1,420 @@ +import { + getPathFromEvents, + performDepthFirstTraversal, + SerializedEvent, + SerializedState, + SimpleBehavior, + StatePath, + Step, + TraversalOptions, + traverseSimplePathsTo +} from '@xstate/graph'; +import { EventObject } from 'xstate'; +import { isStateLike } from 'xstate/lib/utils'; +import { pathGeneratorWithDedup } from './dedupPaths'; +import { getShortestPaths, getSimplePaths } from './pathGenerators'; +import type { + EventExecutor, + GetPathsOptions, + PathGenerator, + StatePredicate, + TestModelOptions, + TestParam, + TestPath, + TestPathResult, + TestStepResult +} from './types'; +import { + formatPathTestResult, + getDescription, + mapPlansToPaths, + simpleStringify +} from './utils'; + +export interface TestModelDefaults { + pathGenerator: PathGenerator; +} + +export const testModelDefaults: TestModelDefaults = { + pathGenerator: getShortestPaths +}; + +/** + * Creates a test model that represents an abstract model of a + * system under test (SUT). + * + * The test model is used to generate test paths, which are used to + * verify that states in the model are reachable in the SUT. + */ +export class TestModel { + public options: TestModelOptions; + public defaultTraversalOptions?: TraversalOptions; + public getDefaultOptions(): TestModelOptions { + return { + serializeState: (state) => simpleStringify(state) as SerializedState, + serializeEvent: (event) => simpleStringify(event) as SerializedEvent, + // For non-state-machine test models, we cannot identify + // separate transitions, so just use event type + serializeTransition: (state, event) => + `${simpleStringify(state)}|${event?.type ?? ''}`, + getEvents: () => [], + stateMatcher: (_, stateKey) => stateKey === '*', + eventCases: {}, + execute: () => void 0, + logger: { + log: console.log.bind(console), + error: console.error.bind(console) + } + }; + } + public static defaults: TestModelDefaults = testModelDefaults; + + constructor( + public behavior: SimpleBehavior, + options?: Partial> + ) { + this.options = { + ...this.getDefaultOptions(), + ...options + }; + } + + public getShortestPaths( + options?: Partial> + ): Array> { + return this.getPaths({ ...options, pathGenerator: getShortestPaths }); + } + + public getPaths( + options?: Partial> + ): Array> { + const pathGenerator = pathGeneratorWithDedup( + options?.pathGenerator || TestModel.defaults.pathGenerator + ); + const paths = pathGenerator(this.behavior, this.resolveOptions(options)); + + return paths.map(this.toTestPath); + } + + public getShortestPathsTo( + statePredicate: StatePredicate + ): Array> { + let minWeight = Infinity; + let shortestPaths: Array> = []; + + const paths = this.filterPathsTo(statePredicate, this.getShortestPaths()); + + for (const path of paths) { + const currWeight = path.weight; + if (currWeight < minWeight) { + minWeight = currWeight; + shortestPaths = [path]; + } else if (currWeight === minWeight) { + shortestPaths.push(path); + } + } + + return shortestPaths; + } + + public getSimplePaths( + options?: Partial> + ): Array> { + return this.getPaths({ + ...options, + pathGenerator: getSimplePaths + }); + } + + public getSimplePathsTo( + predicate: StatePredicate + ): Array> { + return mapPlansToPaths( + traverseSimplePathsTo(this.behavior, predicate, this.options) + ).map(this.toTestPath); + } + + private filterPathsTo( + statePredicate: StatePredicate, + testPaths: Array> + ): Array> { + return testPaths.filter((testPath) => { + return statePredicate(testPath.state); + }); + } + + private toTestPath = ( + statePath: StatePath + ): TestPath => { + function formatEvent(event: EventObject): string { + const { type, ...other } = event; + + const propertyString = Object.keys(other).length + ? ` (${JSON.stringify(other)})` + : ''; + + return `${type}${propertyString}`; + } + + const eventsString = statePath.steps + .map((s) => formatEvent(s.event)) + .join(' → '); + return { + ...statePath, + test: (params: TestParam) => + this.testPath(statePath, params), + testSync: (params: TestParam) => + this.testPathSync(statePath, params), + description: isStateLike(statePath.state) + ? `Reaches ${getDescription( + statePath.state as any + ).trim()}: ${eventsString}` + : JSON.stringify(statePath.state) + }; + }; + + public getPathFromEvents( + events: TEvent[], + statePredicate: StatePredicate + ): TestPath { + const path = getPathFromEvents(this.behavior, events); + + if (!statePredicate(path.state)) { + throw new Error( + `The last state ${JSON.stringify( + (path.state as any).value + )} does not match the target}` + ); + } + + return this.toTestPath(path); + } + + public getAllStates(): TState[] { + const adj = performDepthFirstTraversal(this.behavior, this.options); + return Object.values(adj).map((x) => x.state); + } + + public testPathSync( + path: StatePath, + params: TestParam, + options?: Partial> + ): TestPathResult { + const testPathResult: TestPathResult = { + steps: [], + state: { + error: null + } + }; + + try { + for (const step of path.steps) { + const testStepResult: TestStepResult = { + step, + state: { error: null }, + event: { error: null } + }; + + testPathResult.steps.push(testStepResult); + + try { + this.testStateSync(params, step.state, options); + } catch (err) { + testStepResult.state.error = err; + + throw err; + } + + try { + this.testTransitionSync(params, step); + } catch (err) { + testStepResult.event.error = err; + + throw err; + } + } + + try { + this.testStateSync(params, path.state, options); + } catch (err) { + testPathResult.state.error = err.message; + throw err; + } + } catch (err) { + // TODO: make option + err.message += formatPathTestResult(path, testPathResult, this.options); + throw err; + } + + return testPathResult; + } + + public async testPath( + path: StatePath, + params: TestParam, + options?: Partial> + ): Promise { + const testPathResult: TestPathResult = { + steps: [], + state: { + error: null + } + }; + + try { + for (const step of path.steps) { + const testStepResult: TestStepResult = { + step, + state: { error: null }, + event: { error: null } + }; + + testPathResult.steps.push(testStepResult); + + try { + await this.testState(params, step.state, options); + } catch (err) { + testStepResult.state.error = err; + + throw err; + } + + try { + await this.testTransition(params, step); + } catch (err) { + testStepResult.event.error = err; + + throw err; + } + } + + try { + await this.testState(params, path.state, options); + } catch (err) { + testPathResult.state.error = err.message; + throw err; + } + } catch (err) { + // TODO: make option + err.message += formatPathTestResult(path, testPathResult, this.options); + throw err; + } + + return testPathResult; + } + + public async testState( + params: TestParam, + state: TState, + options?: Partial> + ): Promise { + const resolvedOptions = this.resolveOptions(options); + + const stateTestKeys = this.getStateTestKeys(params, state, resolvedOptions); + + for (const stateTestKey of stateTestKeys) { + await params.states?.[stateTestKey](state); + } + + this.afterTestState(state, resolvedOptions); + } + + private getStateTestKeys( + params: TestParam, + state: TState, + resolvedOptions: TestModelOptions + ) { + const states = params.states || {}; + const stateTestKeys = Object.keys(states).filter((stateKey) => { + return resolvedOptions.stateMatcher(state, stateKey); + }); + + // Fallthrough state tests + if (!stateTestKeys.length && '*' in states) { + stateTestKeys.push('*'); + } + + return stateTestKeys; + } + + private afterTestState( + state: TState, + resolvedOptions: TestModelOptions + ) { + resolvedOptions.execute(state); + } + + public testStateSync( + params: TestParam, + state: TState, + options?: Partial> + ): void { + const resolvedOptions = this.resolveOptions(options); + + const stateTestKeys = this.getStateTestKeys(params, state, resolvedOptions); + + for (const stateTestKey of stateTestKeys) { + errorIfPromise( + params.states?.[stateTestKey](state), + `The test for '${stateTestKey}' returned a promise - did you mean to use the sync method?` + ); + } + + this.afterTestState(state, resolvedOptions); + } + + private getEventExec( + params: TestParam, + step: Step + ) { + const eventExec = + params.events?.[(step.event as any).type as TEvent['type']]; + + return eventExec; + } + + public async testTransition( + params: TestParam, + step: Step + ): Promise { + const eventExec = this.getEventExec(params, step); + await (eventExec as EventExecutor)?.(step); + } + + public testTransitionSync( + params: TestParam, + step: Step + ): void { + const eventExec = this.getEventExec(params, step); + + errorIfPromise( + (eventExec as EventExecutor)?.(step), + `The event '${step.event.type}' returned a promise - did you mean to use the sync method?` + ); + } + + public resolveOptions( + options?: Partial> + ): TestModelOptions { + return { ...this.defaultTraversalOptions, ...this.options, ...options }; + } +} + +/** + * Specifies default configuration for `TestModel` instances for path generation options + * + * @param testModelConfiguration The partial configuration for all subsequent `TestModel` instances + */ +export function configure( + testModelConfiguration: Partial< + TestModelDefaults + > = testModelDefaults +): void { + TestModel.defaults = { ...testModelDefaults, ...testModelConfiguration }; +} + +const errorIfPromise = (result: unknown, err: string) => { + if (typeof result === 'object' && result && 'then' in result) { + throw new Error(err); + } +}; diff --git a/packages/xstate-test/src/dedupPaths.ts b/packages/xstate-test/src/dedupPaths.ts new file mode 100644 index 0000000000..7efbc96dce --- /dev/null +++ b/packages/xstate-test/src/dedupPaths.ts @@ -0,0 +1,75 @@ +import { StatePath } from '@xstate/graph'; +import { EventObject } from 'xstate'; +import { PathGenerator } from './types'; + +/** + * Deduplicates your path plans so that A -> B + * is not executed separately to A -> B -> C + */ +export const pathGeneratorWithDedup = ( + pathGenerator: PathGenerator +): PathGenerator => (behavior, options) => { + const paths = pathGenerator(behavior, options); + + /** + * Put all plans on the same level so we can dedup them + */ + const allPathsWithEventSequence: Array<{ + path: StatePath; + eventSequence: string[]; + }> = []; + + paths.forEach((path) => { + allPathsWithEventSequence.push({ + path, + eventSequence: path.steps.map((step) => + options.serializeEvent(step.event) + ) + }); + }); + + // Sort by path length, descending + allPathsWithEventSequence.sort( + (a, z) => z.path.steps.length - a.path.steps.length + ); + + const superpathsWithEventSequence: typeof allPathsWithEventSequence = []; + + /** + * Filter out the paths that are subpaths of superpaths + */ + pathLoop: for (const pathWithEventSequence of allPathsWithEventSequence) { + // Check each existing superpath to see if the path is a subpath of it + superpathLoop: for (const superpathWithEventSequence of superpathsWithEventSequence) { + for (const i in pathWithEventSequence.eventSequence) { + // Check event sequence to determine if path is subpath, e.g.: + // + // This will short-circuit the check + // ['a', 'b', 'c', 'd'] (superpath) + // ['a', 'b', 'x'] (path) + // + // This will not short-circuit; path is subpath + // ['a', 'b', 'c', 'd'] (superpath) + // ['a', 'b', 'c'] (path) + if ( + pathWithEventSequence.eventSequence[i] !== + superpathWithEventSequence.eventSequence[i] + ) { + // If the path is different from the superpath, + // continue to the next superpath + continue superpathLoop; + } + } + + // If we reached here, path is subpath of superpath + // Continue & do not add path to superpaths + continue pathLoop; + } + + // If we reached here, path is not a subpath of any existing superpaths + // So add it to the superpaths + superpathsWithEventSequence.push(pathWithEventSequence); + } + + return superpathsWithEventSequence.map((path) => path.path); +}; diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 96003a9f23..7f653c2735 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,463 +1,4 @@ -import { - getPathFromEvents, - getShortestPaths, - getSimplePaths, - getStateNodes, - StatePathsMap, - ValueAdjMapOptions -} from '@xstate/graph'; -import { StateMachine, EventObject, State, StateValue } from 'xstate'; -import slimChalk from './slimChalk'; -import { - TestModelCoverage, - TestModelOptions, - TestPlan, - StatePredicate, - TestPathResult, - TestSegmentResult, - TestMeta, - EventExecutor, - CoverageOptions -} from './types'; - -/** - * Creates a test model that represents an abstract model of a - * system under test (SUT). - * - * The test model is used to generate test plans, which are used to - * verify that states in the `machine` are reachable in the SUT. - * - * @example - * - * ```js - * const toggleModel = createModel(toggleMachine).withEvents({ - * TOGGLE: { - * exec: async page => { - * await page.click('input'); - * } - * } - * }); - * ``` - * - */ -export class TestModel { - public coverage: TestModelCoverage = { - stateNodes: new Map(), - transitions: new Map() - }; - public options: TestModelOptions; - public static defaultOptions: TestModelOptions = { - events: {} - }; - - constructor( - public machine: StateMachine, - options?: Partial> - ) { - this.options = { - ...TestModel.defaultOptions, - ...options - }; - } - - public getShortestPathPlans( - options?: Partial> - ): Array> { - const shortestPaths = getShortestPaths(this.machine, { - ...options, - events: getEventSamples(this.options.events) - }) as StatePathsMap; - - return this.getTestPlans(shortestPaths); - } - - public getShortestPathPlansTo( - stateValue: StateValue | StatePredicate - ): Array> { - let minWeight = Infinity; - let shortestPlans: Array> = []; - - const plans = this.filterPathsTo(stateValue, this.getShortestPathPlans()); - - for (const plan of plans) { - const currWeight = plan.paths[0].weight; - if (currWeight < minWeight) { - minWeight = currWeight; - shortestPlans = [plan]; - } else if (currWeight === minWeight) { - shortestPlans.push(plan); - } - } - - return shortestPlans; - } - - private filterPathsTo( - stateValue: StateValue | StatePredicate, - testPlans: Array> - ): Array> { - const predicate = - typeof stateValue === 'function' - ? (plan) => stateValue(plan.state) - : (plan) => plan.state.matches(stateValue); - return testPlans.filter(predicate); - } - - public getPlanFromEvents( - events: Array, - { target }: { target: StateValue } - ): TestPlan { - const path = getPathFromEvents(this.machine, events); - - if (!path.state.matches(target)) { - throw new Error( - `The last state ${JSON.stringify( - (path.state as any).value - )} does not match the target: ${JSON.stringify(target)}` - ); - } - - const plans = this.getTestPlans({ - [JSON.stringify(path.state.value)]: { - state: path.state, - paths: [path] - } - }); - - return plans[0]; - } - - public getSimplePathPlans( - options?: Partial> - ): Array> { - const simplePaths = getSimplePaths(this.machine, { - ...options, - events: getEventSamples(this.options.events) - }) as StatePathsMap; - - return this.getTestPlans(simplePaths); - } - - public getSimplePathPlansTo( - stateValue: StateValue | StatePredicate - ): Array> { - return this.filterPathsTo(stateValue, this.getSimplePathPlans()); - } - - public getTestPlans( - statePathsMap: StatePathsMap - ): Array> { - return Object.keys(statePathsMap).map((key) => { - const testPlan = statePathsMap[key]; - const paths = testPlan.paths.map((path) => { - const segments = path.segments.map((segment) => { - return { - ...segment, - description: getDescription(segment.state), - test: (testContext) => this.testState(segment.state, testContext), - exec: (testContext) => this.executeEvent(segment.event, testContext) - }; - }); - - function formatEvent(event: EventObject): string { - const { type, ...other } = event; - - const propertyString = Object.keys(other).length - ? ` (${JSON.stringify(other)})` - : ''; - - return `${type}${propertyString}`; - } - - const eventsString = path.segments - .map((s) => formatEvent(s.event)) - .join(' → '); - - return { - ...path, - segments, - description: `via ${eventsString}`, - test: async (testContext) => { - const testPathResult: TestPathResult = { - segments: [], - state: { - error: null - } - }; - - try { - for (const segment of segments) { - const testSegmentResult: TestSegmentResult = { - segment, - state: { error: null }, - event: { error: null } - }; - - testPathResult.segments.push(testSegmentResult); - - try { - await segment.test(testContext); - } catch (err) { - testSegmentResult.state.error = err; - - throw err; - } - - try { - await segment.exec(testContext); - } catch (err) { - testSegmentResult.event.error = err; - - throw err; - } - } - - try { - await this.testState(testPlan.state, testContext); - } catch (err) { - testPathResult.state.error = err; - throw err; - } - } catch (err) { - const targetStateString = `${JSON.stringify(path.state.value)} ${ - path.state.context === undefined - ? '' - : JSON.stringify(path.state.context) - }`; - - let hasFailed = false; - err.message += - '\nPath:\n' + - testPathResult.segments - .map((s) => { - const stateString = `${JSON.stringify( - s.segment.state.value - )} ${ - s.segment.state.context === undefined - ? '' - : JSON.stringify(s.segment.state.context) - }`; - const eventString = `${JSON.stringify(s.segment.event)}`; - - const stateResult = `\tState: ${ - hasFailed - ? slimChalk('gray', stateString) - : s.state.error - ? ((hasFailed = true), - slimChalk('redBright', stateString)) - : slimChalk('greenBright', stateString) - }`; - const eventResult = `\tEvent: ${ - hasFailed - ? slimChalk('gray', eventString) - : s.event.error - ? ((hasFailed = true), slimChalk('red', eventString)) - : slimChalk('green', eventString) - }`; - - return [stateResult, eventResult].join('\n'); - }) - .concat( - `\tState: ${ - hasFailed - ? slimChalk('gray', targetStateString) - : testPathResult.state.error - ? slimChalk('red', targetStateString) - : slimChalk('green', targetStateString) - }` - ) - .join('\n\n'); - - throw err; - } - - return testPathResult; - } - }; - }); - - return { - ...testPlan, - test: async (testContext) => { - for (const path of paths) { - await path.test(testContext); - } - }, - description: `reaches ${getDescription(testPlan.state)}`, - paths - } as TestPlan; - }); - } - - public async testState( - state: State, - testContext: TTestContext - ) { - for (const id of Object.keys(state.meta)) { - const stateNodeMeta = state.meta[id] as TestMeta; - if (typeof stateNodeMeta.test === 'function' && !stateNodeMeta.skip) { - this.coverage.stateNodes.set( - id, - (this.coverage.stateNodes.get(id) || 0) + 1 - ); - - await stateNodeMeta.test(testContext, state); - } - } - } - - public getEventExecutor( - event: EventObject - ): EventExecutor | undefined { - const testEvent = this.options.events[event.type]; - - if (!testEvent) { - // tslint:disable-next-line:no-console - console.warn(`Missing config for event "${event.type}".`); - return undefined; - } - - if (typeof testEvent === 'function') { - return testEvent; - } - - return testEvent.exec; - } - - public async executeEvent(event: EventObject, testContext: TTestContext) { - const executor = this.getEventExecutor(event); - - if (executor) { - await executor(testContext, event); - } - } - - public getCoverage( - options?: CoverageOptions - ): { stateNodes: Record } { - const filter = options ? options.filter : undefined; - const stateNodes = getStateNodes(this.machine); - const filteredStateNodes = filter ? stateNodes.filter(filter) : stateNodes; - const coverage = { - stateNodes: filteredStateNodes.reduce((acc, stateNode) => { - acc[stateNode.id] = 0; - return acc; - }, {}) - }; - - for (const key of this.coverage.stateNodes.keys()) { - coverage.stateNodes[key] = this.coverage.stateNodes.get(key); - } - - return coverage; - } - - public testCoverage(options?: CoverageOptions): void { - const coverage = this.getCoverage(options); - const missingStateNodes = Object.keys(coverage.stateNodes).filter((id) => { - return !coverage.stateNodes[id]; - }); - - if (missingStateNodes.length) { - throw new Error( - 'Missing coverage for state nodes:\n' + - missingStateNodes.map((id) => `\t${id}`).join('\n') - ); - } - } - - public withEvents( - eventMap: TestModelOptions['events'] - ): TestModel { - return new TestModel(this.machine, { - events: eventMap - }); - } -} - -function getDescription(state: State): string { - const contextString = - state.context === undefined ? '' : `(${JSON.stringify(state.context)})`; - - const stateStrings = state.configuration - .filter((sn) => sn.type === 'atomic' || sn.type === 'final') - .map(({ id }) => { - const meta = state.meta[id] as TestMeta; - if (!meta) { - return `"#${id}"`; - } - - const { description } = meta; - - if (typeof description === 'function') { - return description(state); - } - - return description ? `"${description}"` : JSON.stringify(state.value); - }); - - return ( - `state${stateStrings.length === 1 ? '' : 's'}: ` + - stateStrings.join(', ') + - ` ${contextString}` - ); -} - -function getEventSamples(eventsOptions: TestModelOptions['events']) { - const result = {}; - - Object.keys(eventsOptions).forEach((key) => { - const eventConfig = eventsOptions[key]; - if (typeof eventConfig === 'function') { - return [ - { - type: key - } - ]; - } - - result[key] = eventConfig.cases - ? eventConfig.cases.map((sample) => ({ - type: key, - ...sample - })) - : [ - { - type: key - } - ]; - }); - - return result; -} - -/** - * Creates a test model that represents an abstract model of a - * system under test (SUT). - * - * The test model is used to generate test plans, which are used to - * verify that states in the `machine` are reachable in the SUT. - * - * @example - * - * ```js - * const toggleModel = createModel(toggleMachine).withEvents({ - * TOGGLE: { - * exec: async page => { - * await page.click('input'); - * } - * } - * }); - * ``` - * - * @param machine The state machine used to represent the abstract model. - * @param options Options for the created test model: - * - `events`: an object mapping string event types (e.g., `SUBMIT`) - * to an event test config (e.g., `{exec: () => {...}, cases: [...]}`) - */ -export function createModel( - machine: StateMachine, - options?: TestModelOptions -): TestModel { - return new TestModel(machine, options); -} +export { createTestModel, createTestMachine } from './machine'; +export { TestModel, configure, TestModelDefaults } from './TestModel'; +export * from './types'; +export * from './pathGenerators'; diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts new file mode 100644 index 0000000000..d3f14861ba --- /dev/null +++ b/packages/xstate-test/src/machine.ts @@ -0,0 +1,146 @@ +import { SerializedState, serializeState, SimpleBehavior } from '@xstate/graph'; +import { + ActionObject, + AnyEventObject, + AnyState, + AnyStateMachine, + createMachine, + EventFrom, + EventObject, + StateFrom, + TypegenConstraint, + TypegenDisabled +} from 'xstate'; +import { TestModel } from './TestModel'; +import { + TestMachineConfig, + TestMachineOptions, + TestModelOptions +} from './types'; +import { flatten, simpleStringify } from './utils'; +import { validateMachine } from './validateMachine'; + +export async function testStateFromMeta(state: AnyState) { + for (const id of Object.keys(state.meta)) { + const stateNodeMeta = state.meta[id]; + if (typeof stateNodeMeta.test === 'function' && !stateNodeMeta.skip) { + await stateNodeMeta.test(state); + } + } +} + +export function createTestMachine< + TContext, + TEvent extends EventObject = AnyEventObject, + TTypesMeta extends TypegenConstraint = TypegenDisabled +>( + config: TestMachineConfig, + options?: TestMachineOptions +) { + return createMachine(config, options as any); +} + +export function executeAction( + actionObject: ActionObject, + state: AnyState +): void { + if (typeof actionObject.exec === 'function') { + actionObject.exec(state.context, state.event, { + _event: state._event, + action: actionObject, + state + }); + } +} + +function serializeMachineTransition( + state: AnyState, + _event: AnyEventObject | null, + { serializeEvent }: { serializeEvent: (event: AnyEventObject) => string } +): string { + // Only consider the transition via the serialized event if there actually + // was a defined transition for the event + if (!state.event || state.transitions.length === 0) { + return ''; + } + + return ` via ${serializeEvent(state.event)}`; +} + +/** + * Creates a test model that represents an abstract model of a + * system under test (SUT). + * + * The test model is used to generate test plans, which are used to + * verify that states in the `machine` are reachable in the SUT. + * + * @example + * + * ```js + * const toggleModel = createModel(toggleMachine).withEvents({ + * TOGGLE: { + * exec: async page => { + * await page.click('input'); + * } + * } + * }); + * ``` + * + * @param machine The state machine used to represent the abstract model. + * @param options Options for the created test model: + * - `events`: an object mapping string event types (e.g., `SUBMIT`) + * to an event test config (e.g., `{exec: () => {...}, cases: [...]}`) + */ +export function createTestModel( + machine: TMachine, + options?: Partial, EventFrom>> +): TestModel, EventFrom> { + validateMachine(machine); + + const serializeEvent = options?.serializeEvent ?? simpleStringify; + const serializeTransition = + options?.serializeTransition ?? serializeMachineTransition; + + const testModel = new TestModel, EventFrom>( + machine as SimpleBehavior, + { + serializeState: (state, event) => { + return `${serializeState(state)}${serializeTransition(state, event, { + serializeEvent + })}` as SerializedState; + }, + stateMatcher: (state, key) => { + return key.startsWith('#') + ? state.configuration.includes(machine.getStateNodeById(key)) + : state.matches(key); + }, + execute: (state) => { + state.actions.forEach((action) => { + executeAction(action, state); + }); + }, + getEvents: (state, eventCases) => + flatten( + state.nextEvents.map((eventType) => { + const eventCaseGenerator = eventCases?.[eventType]; + + const cases = eventCaseGenerator + ? Array.isArray(eventCaseGenerator) + ? eventCaseGenerator + : eventCaseGenerator(state) + : [{ type: eventType }]; + + return ( + // Use generated events or a plain event without payload + cases.map((e) => { + return { type: eventType, ...(e as any) }; + }) + ); + }) + ), + ...options + } + ); + + return testModel; +} diff --git a/packages/xstate-test/src/pathGenerators.ts b/packages/xstate-test/src/pathGenerators.ts new file mode 100644 index 0000000000..ab2e7d1819 --- /dev/null +++ b/packages/xstate-test/src/pathGenerators.ts @@ -0,0 +1,27 @@ +import { + SimpleBehavior, + StatePath, + TraversalOptions, + traverseShortestPlans, + traverseSimplePlans +} from '@xstate/graph'; +import { EventObject } from 'xstate'; +import { mapPlansToPaths } from './utils'; + +export const getShortestPaths = ( + behavior: SimpleBehavior, + options: TraversalOptions +): Array> => { + const plans = traverseShortestPlans(behavior, options); + + return mapPlansToPaths(plans); +}; + +export const getSimplePaths = ( + behavior: SimpleBehavior, + options: TraversalOptions +): Array> => { + const plans = traverseSimplePlans(behavior, options); + + return mapPlansToPaths(plans); +}; diff --git a/packages/xstate-test/src/slimChalk.browser.ts b/packages/xstate-test/src/slimChalk.browser.ts deleted file mode 100644 index 645dc8dfc4..0000000000 --- a/packages/xstate-test/src/slimChalk.browser.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function slimChalk(_color: string, str: string) { - return str; -} diff --git a/packages/xstate-test/src/slimChalk.ts b/packages/xstate-test/src/slimChalk.ts deleted file mode 100644 index a45fb1b9f5..0000000000 --- a/packages/xstate-test/src/slimChalk.ts +++ /dev/null @@ -1,5 +0,0 @@ -import chalk from 'chalk'; - -export default function slimChalk(color: string, str: string) { - return chalk[color](str); -} diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 79d8851582..299972f15a 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -1,144 +1,154 @@ -import { AnyState, EventObject, State, StateNode } from 'xstate'; +import { + SimpleBehavior, + StatePath, + Step, + TraversalOptions +} from '@xstate/graph'; +import { + BaseActionObject, + EventObject, + MachineConfig, + MachineOptions, + MachineSchema, + ServiceMap, + State, + StateNodeConfig, + StateSchema, + TransitionConfig, + TypegenConstraint, + TypegenDisabled, + ExtractEvent +} from 'xstate'; + +export type GetPathsOptions = Partial< + TraversalOptions & { + pathGenerator?: PathGenerator; + } +>; + +export interface TestMachineConfig< + TContext, + TEvent extends EventObject, + TTypesMeta = TypegenDisabled +> extends TestStateNodeConfig { + context?: MachineConfig['context']; + schema?: MachineSchema; + tsTypes?: TTypesMeta; +} + +export interface TestStateNodeConfig + extends Pick< + StateNodeConfig, + | 'type' + | 'history' + | 'on' + | 'onDone' + | 'entry' + | 'exit' + | 'meta' + | 'always' + | 'data' + | 'id' + | 'tags' + | 'description' + > { + initial?: string; + states?: Record>; +} + +export type TestMachineOptions< + TContext, + TEvent extends EventObject, + TTypesMeta extends TypegenConstraint = TypegenDisabled +> = Pick< + MachineOptions, + 'actions' | 'guards' +>; + export interface TestMeta { test?: (testContext: T, state: State) => Promise | void; description?: string | ((state: State) => string); skip?: boolean; } -interface TestSegment { - state: AnyState; - event: EventObject; - description: string; - test: (testContext: T) => Promise; - exec: (testContext: T) => Promise; -} interface TestStateResult { error: null | Error; } -export interface TestSegmentResult { - segment: TestSegment; +export interface TestStepResult { + step: Step; state: TestStateResult; event: { error: null | Error; }; } -export interface TestPath { - weight: number; - segments: Array>; + +export interface TestParam { + states?: { + [key: string]: (state: TState) => void | Promise; + }; + events?: { + [TEventType in TEvent['type']]?: EventExecutor; + }; +} + +export interface TestPath + extends StatePath { description: string; /** - * Tests and executes each segment in `segments` sequentially, and then + * Tests and executes each step in `steps` sequentially, and then * tests the postcondition that the `state` is reached. */ - test: (testContext: T) => Promise; + test: (params: TestParam) => Promise; + testSync: (params: TestParam) => TestPathResult; } export interface TestPathResult { - segments: TestSegmentResult[]; + steps: TestStepResult[]; state: TestStateResult; } -/** - * A collection of `paths` used to verify that the SUT reaches - * the target `state`. - */ -export interface TestPlan { - /** - * The target state. - */ - state: State; - /** - * The paths that reach the target `state`. - */ - paths: Array>; - /** - * The description of the target `state` to be reached. - */ - description: string; - /** - * Tests the postcondition that the `state` is reached. - * - * This should be tested after navigating any path in `paths`. - */ - test: ( - /** - * The test context used for verifying the SUT. - */ - testContext: TTestContext - ) => Promise | void; -} - -/** - * A sample event object payload (_without_ the `type` property). - * - * @example - * - * ```js - * { - * value: 'testValue', - * other: 'something', - * id: 42 - * } - * ``` - */ -interface EventCase { - type?: never; - [prop: string]: any; -} - -export type StatePredicate = (state: State) => boolean; +export type StatePredicate = (state: TState) => boolean; /** * Executes an effect using the `testContext` and `event` * that triggers the represented `event`. */ -export type EventExecutor = ( - /** - * The testing context used to execute the effect - */ - testContext: T, - /** - * The represented event that will be triggered when executed - */ - event: EventObject +export type EventExecutor = ( + step: Step ) => Promise | void; -export interface TestEventConfig { - /** - * Executes an effect that triggers the represented event. - * - * @example - * - * ```js - * exec: async (page, event) => { - * await page.type('.foo', event.value); - * } - * ``` - */ - exec?: EventExecutor; +export interface TestModelOptions + extends TraversalOptions { /** - * Sample event object payloads _without_ the `type` property. - * - * @example - * - * ```js - * cases: [ - * { value: 'foo' }, - * { value: '' } - * ] - * ``` + * Executes actions based on the `state` after the state is tested. */ - cases?: EventCase[]; + execute: (state: TState) => void; + stateMatcher: (state: TState, stateKey: string) => boolean; + logger: { + log: (msg: string) => void; + error: (msg: string) => void; + }; + serializeTransition: (state: TState, event: TEvent | null) => string; } -export interface TestEventsConfig { - [eventType: string]: EventExecutor | TestEventConfig; -} -export interface TestModelOptions { - events: TestEventsConfig; -} -export interface TestModelCoverage { - stateNodes: Map; - transitions: Map>; +export interface TestTransitionConfig< + TContext, + TEvent extends EventObject, + TTestContext +> extends TransitionConfig { + test?: (state: State, testContext: TTestContext) => void; } -export interface CoverageOptions { - filter?: (stateNode: StateNode) => boolean; -} +export type TestTransitionsConfigMap< + TContext, + TEvent extends EventObject, + TTestContext +> = { + [K in TEvent['type'] | '' | '*']?: K extends '' | '*' + ? TestTransitionConfig | string + : + | TestTransitionConfig, TTestContext> + | string; +}; + +export type PathGenerator = ( + behavior: SimpleBehavior, + options: TraversalOptions +) => Array>; diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts new file mode 100644 index 0000000000..5468245e16 --- /dev/null +++ b/packages/xstate-test/src/utils.ts @@ -0,0 +1,116 @@ +import { + SerializationOptions, + SerializedEvent, + SerializedState, + StatePath, + StatePlan +} from '@xstate/graph'; +import { AnyState, EventObject } from 'xstate'; +import { TestMeta, TestPathResult } from './types'; + +interface TestResultStringOptions extends SerializationOptions { + formatColor: (color: string, string: string) => string; +} + +export function simpleStringify(value: any): string { + return JSON.stringify(value); +} + +export function formatPathTestResult( + path: StatePath, + testPathResult: TestPathResult, + options?: Partial +): string { + const resolvedOptions: TestResultStringOptions = { + formatColor: (_color, string) => string, + serializeState: (state, _event) => + simpleStringify(state) as SerializedState, + serializeEvent: (event) => simpleStringify(event) as SerializedEvent, + eventCases: {}, + ...options + }; + + const { formatColor, serializeState, serializeEvent } = resolvedOptions; + + const { state } = path; + const targetStateString = serializeState(state, null); + + let errMessage = ''; + let hasFailed = false; + errMessage += + '\nPath:\n' + + testPathResult.steps + .map((s) => { + const stateString = serializeState(s.step.state, s.step.event); + const eventString = serializeEvent(s.step.event); + + const stateResult = `\tState: ${ + hasFailed + ? formatColor('gray', stateString) + : s.state.error + ? ((hasFailed = true), formatColor('redBright', stateString)) + : formatColor('greenBright', stateString) + }`; + const eventResult = `\tEvent: ${ + hasFailed + ? formatColor('gray', eventString) + : s.event.error + ? ((hasFailed = true), formatColor('red', eventString)) + : formatColor('green', eventString) + }`; + + return [stateResult, eventResult].join('\n'); + }) + .concat( + `\tState: ${ + hasFailed + ? formatColor('gray', targetStateString) + : testPathResult.state.error + ? formatColor('red', targetStateString) + : formatColor('green', targetStateString) + }` + ) + .join('\n\n'); + + return errMessage; +} + +export function getDescription(state: AnyState): string { + const contextString = + state.context === undefined ? '' : `(${JSON.stringify(state.context)})`; + + const stateStrings = state.configuration + .filter((sn) => sn.type === 'atomic' || sn.type === 'final') + .map(({ id }) => { + const meta = state.meta[id] as TestMeta; + if (!meta) { + return `"#${id}"`; + } + + const { description } = meta; + + if (typeof description === 'function') { + return description(state); + } + + return description ? `"${description}"` : JSON.stringify(state.value); + }); + + return ( + `state${stateStrings.length === 1 ? '' : 's'} ` + + stateStrings.join(', ') + + ` ${contextString}`.trim() + ); +} + +export function flatten(array: Array): T[] { + return ([] as T[]).concat(...array); +} + +export const mapPlansToPaths = ( + plans: StatePlan[] +): Array> => { + return plans.reduce((acc, plan) => { + return acc.concat(plan.paths); + }, [] as Array>); +}; diff --git a/packages/xstate-test/src/validateMachine.ts b/packages/xstate-test/src/validateMachine.ts new file mode 100644 index 0000000000..19b9d595aa --- /dev/null +++ b/packages/xstate-test/src/validateMachine.ts @@ -0,0 +1,30 @@ +import { AnyStateMachine } from 'xstate'; + +export const validateMachine = (machine: AnyStateMachine) => { + const states = machine.stateIds.map((stateId) => + machine.getStateNodeById(stateId) + ); + + states.forEach((state) => { + if (state.invoke.length > 0) { + throw new Error('Invocations on test machines are not supported'); + } + if (state.after.length > 0) { + throw new Error('After events on test machines are not supported'); + } + const actions = [...state.onEntry, ...state.onExit]; + + state.transitions.forEach((transition) => { + actions.push(...transition.actions); + }); + + actions.forEach((action) => { + if ( + action.type.startsWith('xstate.') && + typeof action.delay === 'number' + ) { + throw new Error('Delayed actions on test machines are not supported'); + } + }); + }); +}; diff --git a/packages/xstate-test/test/__snapshots__/index.test.ts.snap b/packages/xstate-test/test/__snapshots__/index.test.ts.snap deleted file mode 100644 index 982426f72f..0000000000 --- a/packages/xstate-test/test/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`error path trace should return trace for failed state should show an error path trace: error path trace 1`] = ` -"test error -Path: - State: \\"first\\" - Event: {\\"type\\":\\"NEXT\\"} - - State: \\"second\\" - Event: {\\"type\\":\\"NEXT\\"} - - State: \\"third\\" " -`; diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts new file mode 100644 index 0000000000..810d88701b --- /dev/null +++ b/packages/xstate-test/test/dieHard.test.ts @@ -0,0 +1,285 @@ +import { assign, createMachine } from 'xstate'; +import { createTestModel } from '../src'; +import { createTestMachine } from '../src/machine'; +import { getDescription } from '../src/utils'; + +describe('die hard example', () => { + interface DieHardContext { + three: number; + five: number; + } + + const pour3to5 = assign((ctx) => { + const poured = Math.min(5 - ctx.five, ctx.three); + + return { + three: ctx.three - poured, + five: ctx.five + poured + }; + }); + const pour5to3 = assign((ctx) => { + const poured = Math.min(3 - ctx.three, ctx.five); + + const res = { + three: ctx.three + poured, + five: ctx.five - poured + }; + + return res; + }); + const fill3 = assign({ three: 3 }); + const fill5 = assign({ five: 5 }); + const empty3 = assign({ three: 0 }); + const empty5 = assign({ five: 0 }); + + class Jugs { + public version = 0; + public three = 0; + public five = 0; + + public fillThree() { + this.three = 3; + } + public fillFive() { + this.five = 5; + } + public emptyThree() { + this.three = 0; + } + public emptyFive() { + this.five = 0; + } + public transferThree() { + const poured = Math.min(5 - this.five, this.three); + + this.three = this.three - poured; + this.five = this.five + poured; + } + public transferFive() { + const poured = Math.min(3 - this.three, this.five); + + this.three = this.three + poured; + this.five = this.five - poured; + } + } + let jugs: Jugs; + + const createDieHardModel = () => { + const dieHardMachine = createMachine( + { + id: 'dieHard', + initial: 'pending', + context: { three: 0, five: 0 }, + states: { + pending: { + always: { + target: 'success', + cond: 'weHave4Gallons' + }, + on: { + POUR_3_TO_5: { + actions: pour3to5 + }, + POUR_5_TO_3: { + actions: pour5to3 + }, + FILL_3: { + actions: fill3 + }, + FILL_5: { + actions: fill5 + }, + EMPTY_3: { + actions: empty3 + }, + EMPTY_5: { + actions: empty5 + } + } + }, + success: { + type: 'final' + } + } + }, + { + guards: { + weHave4Gallons: (ctx) => ctx.five === 4 + } + } + ); + + const options = { + states: { + pending: (state) => { + expect(jugs.five).not.toEqual(4); + expect(jugs.three).toEqual(state.context.three); + expect(jugs.five).toEqual(state.context.five); + }, + success: () => { + expect(jugs.five).toEqual(4); + } + }, + events: { + POUR_3_TO_5: async () => { + await jugs.transferThree(); + }, + POUR_5_TO_3: async () => { + await jugs.transferFive(); + }, + EMPTY_3: async () => { + await jugs.emptyThree(); + }, + EMPTY_5: async () => { + await jugs.emptyFive(); + }, + FILL_3: async () => { + await jugs.fillThree(); + }, + FILL_5: async () => { + await jugs.fillFive(); + } + } + }; + + return { + model: createTestModel(dieHardMachine), + options + }; + }; + + beforeEach(() => { + jugs = new Jugs(); + jugs.version = Math.random(); + }); + + describe('testing a model (shortestPathsTo)', () => { + const dieHardModel = createDieHardModel(); + + dieHardModel.model + .getShortestPathsTo((state) => state.matches('success')) + .forEach((path) => { + describe(`path ${getDescription(path.state)}`, () => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.model.testPath(path, dieHardModel.options); + }); + }); + }); + }); + + describe('testing a model (simplePathsTo)', () => { + const dieHardModel = createDieHardModel(); + dieHardModel.model + .getSimplePathsTo((state) => state.matches('success')) + .forEach((path) => { + describe(`reaches state ${JSON.stringify( + path.state.value + )} (${JSON.stringify(path.state.context)})`, () => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.model.testPath(path, dieHardModel.options); + }); + }); + }); + }); + + describe('testing a model (getPathFromEvents)', () => { + const dieHardModel = createDieHardModel(); + + const path = dieHardModel.model.getPathFromEvents( + [ + { type: 'FILL_5' }, + { type: 'POUR_5_TO_3' }, + { type: 'EMPTY_3' }, + { type: 'POUR_5_TO_3' }, + { type: 'FILL_5' }, + { type: 'POUR_5_TO_3' } + ], + (state) => state.matches('success') + ); + + describe(`reaches state ${JSON.stringify( + path.state.value + )} (${JSON.stringify(path.state.context)})`, () => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.model.testPath(path, dieHardModel.options); + }); + }); + + it('should throw if the target does not match the last entered state', () => { + expect(() => { + dieHardModel.model.getPathFromEvents([{ type: 'FILL_5' }], (state) => + state.matches('success') + ); + }).toThrow(); + }); + }); + + describe('.testPath(path)', () => { + const dieHardModel = createDieHardModel(); + const paths = dieHardModel.model.getSimplePathsTo((state) => { + return state.matches('success') && state.context.three === 0; + }); + + paths.forEach((path) => { + describe(`reaches state ${JSON.stringify( + path.state.value + )} (${JSON.stringify(path.state.context)})`, () => { + describe(`path ${getDescription(path.state)}`, () => { + it(`reaches the target state`, async () => { + await dieHardModel.model.testPath(path, dieHardModel.options); + }); + }); + }); + }); + }); +}); +describe('error path trace', () => { + describe('should return trace for failed state', () => { + const machine = createTestMachine({ + initial: 'first', + states: { + first: { + on: { NEXT: 'second' } + }, + second: { + on: { NEXT: 'third' } + }, + third: {} + } + }); + + const testModel = createTestModel(machine); + + testModel + .getShortestPathsTo((state) => state.matches('third')) + .forEach((path) => { + it('should show an error path trace', async () => { + try { + await testModel.testPath(path, { + states: { + third: () => { + throw new Error('test error'); + } + } + }); + } catch (err) { + expect(err.message).toEqual(expect.stringContaining('test error')); + expect(err.message).toMatchInlineSnapshot(` + "test error + Path: + State: {\\"value\\":\\"first\\"} + Event: {\\"type\\":\\"NEXT\\"} + + State: {\\"value\\":\\"second\\"} via {\\"type\\":\\"NEXT\\"} + Event: {\\"type\\":\\"NEXT\\"} + + State: {\\"value\\":\\"third\\"} via {\\"type\\":\\"NEXT\\"}" + `); + return; + } + + throw new Error('Should have failed'); + }); + }); + }); +}); diff --git a/packages/xstate-test/test/events.test.ts b/packages/xstate-test/test/events.test.ts new file mode 100644 index 0000000000..da79906034 --- /dev/null +++ b/packages/xstate-test/test/events.test.ts @@ -0,0 +1,61 @@ +import { createTestModel } from '../src'; +import { createTestMachine } from '../src/machine'; +import { testUtils } from './testUtils'; + +describe('events', () => { + it('should execute events (`exec` property)', async () => { + let executed = false; + + const testModel = createTestModel( + createTestMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } + }) + ); + + await testUtils.testModel(testModel, { + events: { + EVENT: () => { + executed = true; + } + } + }); + + expect(executed).toBe(true); + }); + + it('should execute events (function)', async () => { + let executed = false; + + const testModel = createTestModel( + createTestMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } + }) + ); + + await testUtils.testModel(testModel, { + events: { + EVENT: () => { + executed = true; + } + } + }); + + expect(executed).toBe(true); + }); +}); diff --git a/packages/xstate-test/test/forbiddenAttributes.test.ts b/packages/xstate-test/test/forbiddenAttributes.test.ts new file mode 100644 index 0000000000..bb02463c43 --- /dev/null +++ b/packages/xstate-test/test/forbiddenAttributes.test.ts @@ -0,0 +1,49 @@ +import { createMachine, send } from 'xstate'; +import { createTestModel } from '../src'; + +describe('Forbidden attributes', () => { + it('Should not let you declare invocations on your test machine', () => { + const machine = createMachine({ + invoke: { + src: 'myInvoke' + } + }); + + expect(() => { + createTestModel(machine); + }).toThrowError('Invocations on test machines are not supported'); + }); + + it('Should not let you declare after on your test machine', () => { + const machine = createMachine({ + after: { + 5000: { + actions: () => {} + } + } + }); + + expect(() => { + createTestModel(machine); + }).toThrowError('After events on test machines are not supported'); + }); + + it('Should not let you delayed actions on your machine', () => { + const machine = createMachine({ + entry: [ + send( + { + type: 'EVENT' + }, + { + delay: 1000 + } + ) + ] + }); + + expect(() => { + createTestModel(machine); + }).toThrowError('Delayed actions on test machines are not supported'); + }); +}); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 2d6e27f4e7..7218d4f038 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,426 +1,7 @@ -// nothing yet -import { createModel } from '../src'; -import { Machine, assign } from 'xstate'; -import stripAnsi from 'strip-ansi'; - -interface DieHardContext { - three: number; - five: number; -} - -const pour3to5 = assign((ctx) => { - const poured = Math.min(5 - ctx.five, ctx.three); - - return { - three: ctx.three - poured, - five: ctx.five + poured - }; -}); -const pour5to3 = assign((ctx) => { - const poured = Math.min(3 - ctx.three, ctx.five); - - const res = { - three: ctx.three + poured, - five: ctx.five - poured - }; - - return res; -}); -const fill3 = assign({ three: 3 }); -const fill5 = assign({ five: 5 }); -const empty3 = assign({ three: 0 }); -const empty5 = assign({ five: 0 }); - -const dieHardMachine = Machine( - { - id: 'dieHard', - initial: 'pending', - context: { three: 0, five: 0 }, - states: { - pending: { - on: { - '': { - target: 'success', - cond: 'weHave4Gallons' - }, - POUR_3_TO_5: { - actions: pour3to5 - }, - POUR_5_TO_3: { - actions: pour5to3 - }, - FILL_3: { - actions: fill3 - }, - FILL_5: { - actions: fill5 - }, - EMPTY_3: { - actions: empty3 - }, - EMPTY_5: { - actions: empty5 - } - }, - meta: { - description: (state) => { - return `pending with (${state.context.three}, ${state.context.five})`; - }, - test: async ({ jugs }, state) => { - expect(jugs.five).not.toEqual(4); - expect(jugs.three).toEqual(state.context.three); - expect(jugs.five).toEqual(state.context.five); - } - } - }, - success: { - type: 'final', - meta: { - description: '4 gallons', - test: async ({ jugs }) => { - expect(jugs.five).toEqual(4); - } - } - } - } - }, - { - guards: { - weHave4Gallons: (ctx) => ctx.five === 4 - } - } -); - -class Jugs { - public three = 0; - public five = 0; - - public fillThree() { - this.three = 3; - } - public fillFive() { - this.five = 5; - } - public emptyThree() { - this.three = 0; - } - public emptyFive() { - this.five = 0; - } - public transferThree() { - const poured = Math.min(5 - this.five, this.three); - - this.three = this.three - poured; - this.five = this.five + poured; - } - public transferFive() { - const poured = Math.min(3 - this.three, this.five); - - this.three = this.three + poured; - this.five = this.five - poured; - } -} - -const dieHardModel = createModel<{ jugs: Jugs }>(dieHardMachine).withEvents({ - POUR_3_TO_5: { - exec: async ({ jugs }) => { - await jugs.transferThree(); - } - }, - POUR_5_TO_3: { - exec: async ({ jugs }) => { - await jugs.transferFive(); - } - }, - EMPTY_3: { - exec: async ({ jugs }) => { - await jugs.emptyThree(); - } - }, - EMPTY_5: { - exec: async ({ jugs }) => { - await jugs.emptyFive(); - } - }, - FILL_3: { - exec: async ({ jugs }) => { - await jugs.fillThree(); - } - }, - FILL_5: { - exec: async ({ jugs }) => { - await jugs.fillFive(); - } - } -}); - -describe('testing a model (shortestPathsTo)', () => { - dieHardModel - .getShortestPathPlansTo('success') // ... - .forEach((plan) => { - describe(plan.description, () => { - plan.paths.forEach((path) => { - it(path.description, () => { - const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); - }); - }); - }); - }); -}); - -describe('testing a model (simplePathsTo)', () => { - dieHardModel - .getSimplePathPlansTo('success') // ... - .forEach((plan) => { - describe(`reaches state ${JSON.stringify( - plan.state.value - )} (${JSON.stringify(plan.state.context)})`, () => { - plan.paths.forEach((path) => { - it(path.description, () => { - const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); - }); - }); - }); - }); -}); - -describe('testing a model (getPlanFromEvents)', () => { - const plan = dieHardModel.getPlanFromEvents( - [ - { type: 'FILL_5' }, - { type: 'POUR_5_TO_3' }, - { type: 'EMPTY_3' }, - { type: 'POUR_5_TO_3' }, - { type: 'FILL_5' }, - { type: 'POUR_5_TO_3' } - ], - { - target: 'success' - } - ); - - describe(`reaches state ${JSON.stringify(plan.state.value)} (${JSON.stringify( - plan.state.context - )})`, () => { - plan.paths.forEach((path) => { - it(path.description, () => { - const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); - }); - }); - }); - - it('should throw if the target does not match the last entered state', () => { - expect(() => { - dieHardModel.getPlanFromEvents([{ type: 'FILL_5' }], { - target: 'success' - }); - }).toThrow(); - }); -}); - -describe('path.test()', () => { - const plans = dieHardModel.getSimplePathPlansTo((state) => { - return state.matches('success') && state.context.three === 0; - }); - - plans.forEach((plan) => { - describe(`reaches state ${JSON.stringify( - plan.state.value - )} (${JSON.stringify(plan.state.context)})`, () => { - plan.paths.forEach((path) => { - describe(path.description, () => { - it(`reaches the target state`, () => { - const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); - }); - }); - }); - }); - }); -}); - -describe('error path trace', () => { - describe('should return trace for failed state', () => { - const machine = Machine({ - initial: 'first', - states: { - first: { - on: { NEXT: 'second' } - }, - second: { - on: { NEXT: 'third' } - }, - third: { - meta: { - test: () => { - throw new Error('test error'); - } - } - } - } - }); - - const testModel = createModel(machine).withEvents({ - NEXT: () => { - /* noop */ - } - }); - - testModel.getShortestPathPlansTo('third').forEach((plan) => { - plan.paths.forEach((path) => { - it('should show an error path trace', async () => { - try { - await path.test(undefined); - } catch (err) { - expect(err.message).toEqual(expect.stringContaining('test error')); - expect(stripAnsi(err.message)).toMatchSnapshot('error path trace'); - return; - } - - throw new Error('Should have failed'); - }); - }); - }); - }); -}); - -describe('coverage', () => { - it('reports state node coverage', () => { - const coverage = dieHardModel.getCoverage(); - - expect(coverage.stateNodes['dieHard.pending']).toBeGreaterThan(0); - expect(coverage.stateNodes['dieHard.success']).toBeGreaterThan(0); - }); - - it('tests missing state node coverage', async () => { - const machine = Machine({ - id: 'missing', - initial: 'first', - states: { - first: { - on: { NEXT: 'third' }, - meta: { - test: () => true - } - }, - second: { - meta: { - test: () => true - } - }, - third: { - initial: 'one', - states: { - one: { - meta: { - test: () => true - } - }, - two: { - meta: { - test: () => true - } - }, - three: { - meta: { - test: () => true - } - } - }, - meta: { - test: () => true - } - } - } - }); - - const testModel = createModel(machine).withEvents({ - NEXT: () => { - /* ... */ - } - }); - const plans = testModel.getShortestPathPlans(); - - for (const plan of plans) { - for (const path of plan.paths) { - await path.test(undefined); - } - } - - try { - testModel.testCoverage(); - } catch (err) { - expect(err.message).toEqual(expect.stringContaining('missing.second')); - expect(err.message).toEqual(expect.stringContaining('missing.third.two')); - expect(err.message).toEqual( - expect.stringContaining('missing.third.three') - ); - } - }); - - it('skips filtered states (filter option)', async () => { - const TestBug = Machine({ - id: 'testbug', - initial: 'idle', - context: { - retries: 0 - }, - states: { - idle: { - on: { - START: 'passthrough' - }, - meta: { - test: () => { - /* ... */ - } - } - }, - passthrough: { - on: { - '': 'end' - } - }, - end: { - type: 'final', - meta: { - test: () => { - /* ... */ - } - } - } - } - }); - - const testModel = createModel(TestBug).withEvents({ - START: () => { - /* ... */ - } - }); - - const testPlans = testModel.getShortestPathPlans(); - - const promises: any[] = []; - testPlans.forEach((plan) => { - plan.paths.forEach(() => { - promises.push(plan.test(undefined)); - }); - }); - - await Promise.all(promises); - - expect(() => { - testModel.testCoverage({ - filter: (stateNode) => { - return !!stateNode.meta; - } - }); - }).not.toThrow(); - }); -}); +import { assign, createMachine } from 'xstate'; +import { createTestModel } from '../src'; +import { createTestMachine } from '../src/machine'; +import { testUtils } from './testUtils'; describe('events', () => { it('should allow for representing many cases', async () => { @@ -430,8 +11,11 @@ describe('events', () => { | { type: 'CLOSE' } | { type: 'ESC' } | { type: 'SUBMIT'; value: string }; - const feedbackMachine = Machine({ + const feedbackMachine = createTestMachine({ id: 'feedback', + schema: { + events: {} as Events + }, initial: 'question', states: { question: { @@ -440,11 +24,6 @@ describe('events', () => { CLICK_BAD: 'form', CLOSE: 'closed', ESC: 'closed' - }, - meta: { - test: () => { - // ... - } } }, form: { @@ -461,77 +40,35 @@ describe('events', () => { CLOSE: 'closed', ESC: 'closed' }, - meta: { - test: () => { - // ... - } - }, initial: 'valid', states: { - valid: { - meta: { - test: () => { - // noop - } - } - }, - invalid: { - meta: { - test: () => { - // noop - } - } - } + valid: {}, + invalid: {} } }, thanks: { on: { CLOSE: 'closed', ESC: 'closed' - }, - meta: { - test: () => { - // ... - } } }, closed: { - type: 'final', - meta: { - test: () => { - // ... - } - } + type: 'final' } } }); - const testModel = createModel(feedbackMachine).withEvents({ - CLICK_BAD: () => { - /* ... */ - }, - CLICK_GOOD: () => { - /* ... */ - }, - CLOSE: () => { - /* ... */ - }, - SUBMIT: { - cases: [{ value: 'something' }, { value: '' }] + const testModel = createTestModel(feedbackMachine, { + eventCases: { + SUBMIT: [{ value: 'something' }, { value: '' }] } }); - const testPlans = testModel.getShortestPathPlans(); - - for (const plan of testPlans) { - await plan.test(undefined); - } - - return testModel.testCoverage(); + await testUtils.testModel(testModel, {}); }); it('should not throw an error for unimplemented events', () => { - const testMachine = Machine({ + const testMachine = createTestMachine({ initial: 'idle', states: { idle: { @@ -541,21 +78,81 @@ describe('events', () => { } }); - const testModel = createModel(testMachine); - - const testPlans = testModel.getShortestPathPlans(); + const testModel = createTestModel(testMachine); expect(async () => { - for (const plan of testPlans) { - await plan.test(undefined); - } + await testUtils.testModel(testModel, {}); }).not.toThrow(); }); + + it('should allow for dynamic generation of cases based on state', async () => { + const values = [1, 2, 3]; + const testMachine = createMachine< + { values: number[] }, + { type: 'EVENT'; value: number } + >({ + initial: 'a', + context: { + values // to be read by generator + }, + states: { + a: { + on: { + EVENT: [ + { cond: (_, e) => e.value === 1, target: 'b' }, + { cond: (_, e) => e.value === 2, target: 'c' }, + { cond: (_, e) => e.value === 3, target: 'd' } + ] + } + }, + b: {}, + c: {}, + d: {} + } + }); + + const testedEvents: any[] = []; + + const testModel = createTestModel(testMachine, { + eventCases: { + EVENT: (state) => state.context.values.map((value) => ({ value })) + } + }); + + const paths = testModel.getShortestPaths(); + + expect(paths.length).toBe(3); + + await testUtils.testPaths(paths, { + events: { + EVENT: ({ event }) => { + testedEvents.push(event); + } + } + }); + + expect(testedEvents).toMatchInlineSnapshot(` + Array [ + Object { + "type": "EVENT", + "value": 1, + }, + Object { + "type": "EVENT", + "value": 2, + }, + Object { + "type": "EVENT", + "value": 3, + }, + ] + `); + }); }); describe('state limiting', () => { it('should limit states with filter option', () => { - const machine = Machine<{ count: number }>({ + const machine = createMachine<{ count: number }>({ initial: 'counting', context: { count: 0 }, states: { @@ -571,81 +168,276 @@ describe('state limiting', () => { } }); - const testModel = createModel(machine); - const testPlans = testModel.getShortestPathPlans({ + const testModel = createTestModel(machine); + + const testPaths = testModel.getShortestPaths({ filter: (state) => { return state.context.count < 5; } }); - expect(testPlans).toHaveLength(5); + expect(testPaths).toHaveLength(1); }); }); -describe('plan description', () => { - const machine = Machine({ - id: 'test', - initial: 'atomic', - context: { count: 0 }, +// https://github.com/statelyai/xstate/issues/1935 +it('prevents infinite recursion based on a provided limit', () => { + const machine = createMachine<{ count: number }>({ + id: 'machine', + context: { + count: 0 + }, + on: { + TOGGLE: { + actions: assign({ count: (ctx) => ctx.count + 1 }) + } + } + }); + + const model = createTestModel(machine); + + expect(() => { + model.getShortestPaths({ traversalLimit: 100 }); + }).toThrowErrorMatchingInlineSnapshot(`"Traversal limit exceeded"`); +}); + +// TODO: have this as an opt-in +it('executes actions', async () => { + let executedActive = false; + let executedDone = false; + const machine = createTestMachine({ + initial: 'idle', states: { - atomic: { - on: { NEXT: 'compound', DONE: 'final' } + idle: { + on: { + TOGGLE: { target: 'active', actions: 'boom' } + } }, - final: { - type: 'final' + active: { + entry: () => { + executedActive = true; + }, + on: { TOGGLE: 'done' } }, - compound: { - initial: 'child', + done: { + entry: () => { + executedDone = true; + } + } + } + }); + + const model = createTestModel(machine); + + await testUtils.testModel(model, {}); + + expect(executedActive).toBe(true); + expect(executedDone).toBe(true); +}); + +describe('test model options', () => { + it('options.testState(...) should test state', async () => { + const testedStates: any[] = []; + + const model = createTestModel( + createTestMachine({ + initial: 'inactive', states: { - child: { + inactive: { on: { - NEXT: 'childWithMeta' + NEXT: 'active' } }, - childWithMeta: { - meta: { - description: 'child with meta' - } - } - }, - on: { - NEXT: 'parallel' + active: {} } - }, - parallel: { - type: 'parallel', - states: { - one: {}, - two: { - meta: { - description: 'two description' - } - } - }, - on: { - NEXT: 'noMetaDescription' + }) + ); + + await testUtils.testModel(model, { + states: { + '*': (state) => { + testedStates.push(state.value); } + } + }); + + expect(testedStates).toEqual(['inactive', 'active']); + }); +}); + +// https://github.com/statelyai/xstate/issues/1538 +it('tests transitions', async () => { + expect.assertions(2); + const machine = createTestMachine({ + initial: 'first', + states: { + first: { + on: { NEXT: 'second' } }, - noMetaDescription: { - meta: {} + second: {} + } + }); + + const model = createTestModel(machine); + + const paths = model.getShortestPathsTo((state) => state.matches('second')); + + await paths[0].test({ + events: { + NEXT: (step) => { + expect(step).toHaveProperty('event'); + expect(step).toHaveProperty('state'); } } }); +}); + +// https://github.com/statelyai/xstate/issues/982 +it('Event in event executor should contain payload from case', async () => { + const machine = createTestMachine({ + initial: 'first', + states: { + first: { + on: { NEXT: 'second' } + }, + second: {} + } + }); + + const obj = {}; + + const nonSerializableData = () => 42; + + const model = createTestModel(machine, { + eventCases: { + NEXT: [{ payload: 10, fn: nonSerializableData }] + } + }); + + const paths = model.getShortestPathsTo((state) => state.matches('second')); + + await model.testPath( + paths[0], + { + events: { + NEXT: (step) => { + expect(step.event).toEqual({ + type: 'NEXT', + payload: 10, + fn: nonSerializableData + }); + } + } + }, + obj + ); +}); - const testModel = createModel(machine); - const testPlans = testModel.getShortestPathPlans(); +describe('state tests', () => { + it('should test states', async () => { + // a (1) + // a -> b (2) + expect.assertions(2); - it('should give a description for every plan', () => { - const planDescriptions = testPlans.map((plan) => plan.description); + const machine = createTestMachine({ + initial: 'a', + states: { + a: { + on: { NEXT: 'b' } + }, + b: {} + } + }); - expect(planDescriptions).toMatchInlineSnapshot(` + const model = createTestModel(machine); + + await testUtils.testModel(model, { + states: { + a: (state) => { + expect(state.value).toEqual('a'); + }, + b: (state) => { + expect(state.value).toEqual('b'); + } + } + }); + }); + + it('should test wildcard state for non-matching states', async () => { + // a (1) + // a -> b (2) + // a -> c (2) + expect.assertions(4); + + const machine = createTestMachine({ + initial: 'a', + states: { + a: { + on: { NEXT: 'b', OTHER: 'c' } + }, + b: {}, + c: {} + } + }); + + const model = createTestModel(machine); + + await testUtils.testModel(model, { + states: { + a: (state) => { + expect(state.value).toEqual('a'); + }, + b: (state) => { + expect(state.value).toEqual('b'); + }, + '*': (state) => { + expect(state.value).toEqual('c'); + } + } + }); + }); + + it('should test nested states', async () => { + const testedStateValues: any[] = []; + + const machine = createTestMachine({ + initial: 'a', + states: { + a: { + on: { NEXT: 'b' } + }, + b: { + initial: 'b1', + states: { + b1: {} + } + } + } + }); + + const model = createTestModel(machine); + + await testUtils.testModel(model, { + states: { + a: (state) => { + testedStateValues.push('a'); + expect(state.value).toEqual('a'); + }, + b: (state) => { + testedStateValues.push('b'); + expect(state.matches('b')).toBe(true); + }, + 'b.b1': (state) => { + testedStateValues.push('b.b1'); + expect(state.value).toEqual({ b: 'b1' }); + } + } + }); + expect(testedStateValues).toMatchInlineSnapshot(` Array [ - "reaches state: \\"#test.atomic\\" ({\\"count\\":0})", - "reaches state: \\"#test.compound.child\\" ({\\"count\\":0})", - "reaches state: \\"#test.final\\" ({\\"count\\":0})", - "reaches state: \\"child with meta\\" ({\\"count\\":0})", - "reaches states: \\"#test.parallel.one\\", \\"two description\\" ({\\"count\\":0})", - "reaches state: \\"noMetaDescription\\" ({\\"count\\":0})", + "a", + "b", + "b.b1", ] `); }); diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts new file mode 100644 index 0000000000..b440653075 --- /dev/null +++ b/packages/xstate-test/test/paths.test.ts @@ -0,0 +1,191 @@ +import { createTestModel } from '../src'; +import { createTestMachine } from '../src/machine'; +import { testUtils } from './testUtils'; + +const multiPathMachine = createTestMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: { + on: { + EVENT: 'c' + } + }, + c: { + on: { + EVENT: 'd', + EVENT_2: 'e' + } + }, + d: {}, + e: {} + } +}); + +describe('testModel.testPaths(...)', () => { + it('custom path generators can be provided', async () => { + const testModel = createTestModel( + createTestMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } + }) + ); + + const paths = testModel.getPaths({ + pathGenerator: (behavior, options) => { + const events = options.getEvents?.(behavior.initialState, {}) ?? []; + + const nextState = behavior.transition(behavior.initialState, events[0]); + return [ + { + state: nextState, + steps: [ + { + state: behavior.initialState, + event: events[0] + } + ], + weight: 1 + } + ]; + } + }); + + await testUtils.testPaths(paths, {}); + }); + + describe('When the machine only has one path', () => { + it('Should only follow that path', () => { + const machine = createTestMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: { + on: { + EVENT: 'c' + } + }, + c: {} + } + }); + + const model = createTestModel(machine); + + const paths = model.getPaths(); + + expect(paths).toHaveLength(1); + }); + }); + + describe('getSimplePaths', () => { + it('Should dedup simple path paths', () => { + const model = createTestModel(multiPathMachine); + + const paths = model.getSimplePaths(); + + expect(paths).toHaveLength(2); + }); + }); +}); + +describe('path.description', () => { + it('Should write a readable description including the target state and the path', () => { + const model = createTestModel(multiPathMachine); + + const paths = model.getPaths(); + + expect(paths.map((path) => path.description)).toEqual([ + 'Reaches state "#(machine).d": EVENT → EVENT → EVENT', + 'Reaches state "#(machine).e": EVENT → EVENT → EVENT_2' + ]); + }); +}); + +describe('transition coverage', () => { + it('path generation should cover all transitions by default', () => { + const machine = createTestMachine({ + initial: 'a', + states: { + a: { + on: { + NEXT: 'b', + END: 'b' + } + }, + b: { + on: { + PREV: 'a', + RESTART: 'a' + } + } + } + }); + + const model = createTestModel(machine); + + const paths = model.getPaths(); + + expect(paths.map((path) => path.description)).toMatchInlineSnapshot(` + Array [ + "Reaches state \\"#(machine).a\\": NEXT → PREV", + "Reaches state \\"#(machine).a\\": NEXT → RESTART", + "Reaches state \\"#(machine).b\\": END", + ] + `); + }); + + it('transition coverage should consider guarded transitions', () => { + const machine = createTestMachine( + { + initial: 'a', + states: { + a: { + on: { + NEXT: [{ cond: 'valid', target: 'b' }, { target: 'b' }] + } + }, + b: {} + } + }, + { + guards: { + valid: (_, event) => { + return event.value > 10; + } + } + } + ); + + const model = createTestModel(machine); + + const paths = model.getPaths({ + eventCases: { + NEXT: [{ value: 0 }, { value: 100 }, { value: 1000 }] + } + }); + + // { value: 1000 } already covered by first guarded transition + expect(paths.map((path) => path.description)).toMatchInlineSnapshot(` + Array [ + "Reaches state \\"#(machine).b\\": NEXT ({\\"value\\":0})", + "Reaches state \\"#(machine).b\\": NEXT ({\\"value\\":100})", + "Reaches state \\"#(machine).b\\": NEXT ({\\"value\\":1000})", + ] + `); + }); +}); diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts new file mode 100644 index 0000000000..c17cb41f45 --- /dev/null +++ b/packages/xstate-test/test/states.test.ts @@ -0,0 +1,128 @@ +import { StateValue } from 'xstate'; +import { createTestModel } from '../src'; +import { createTestMachine } from '../src/machine'; +import { testUtils } from './testUtils'; + +describe('states', () => { + it('should test states by key', async () => { + const testedStateValues: StateValue[] = []; + const testModel = createTestModel( + createTestMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: { + initial: 'b1', + states: { + b1: { on: { NEXT: 'b2' } }, + b2: {} + } + } + } + }) + ); + + await testUtils.testModel(testModel, { + states: { + a: (state) => { + testedStateValues.push(state.value); + }, + b: (state) => { + testedStateValues.push(state.value); + }, + 'b.b1': (state) => { + testedStateValues.push(state.value); + }, + 'b.b2': (state) => { + testedStateValues.push(state.value); + } + } + }); + + expect(testedStateValues).toMatchInlineSnapshot(` + Array [ + "a", + Object { + "b": "b1", + }, + Object { + "b": "b1", + }, + Object { + "b": "b2", + }, + Object { + "b": "b2", + }, + ] + `); + }); + it('should test states by ID', async () => { + const testedStateValues: StateValue[] = []; + const testModel = createTestModel( + createTestMachine({ + initial: 'a', + states: { + a: { + id: 'state_a', + on: { + EVENT: 'b' + } + }, + b: { + id: 'state_b', + initial: 'b1', + states: { + b1: { + id: 'state_b1', + on: { NEXT: 'b2' } + }, + b2: { + id: 'state_b2' + } + } + } + } + }) + ); + + await testUtils.testModel(testModel, { + states: { + '#state_a': (state) => { + testedStateValues.push(state.value); + }, + '#state_b': (state) => { + testedStateValues.push(state.value); + }, + '#state_b1': (state) => { + testedStateValues.push(state.value); + }, + '#state_b2': (state) => { + testedStateValues.push(state.value); + } + } + }); + + expect(testedStateValues).toMatchInlineSnapshot(` + Array [ + "a", + Object { + "b": "b1", + }, + Object { + "b": "b1", + }, + Object { + "b": "b2", + }, + Object { + "b": "b2", + }, + ] + `); + }); +}); diff --git a/packages/xstate-test/test/sync.test.ts b/packages/xstate-test/test/sync.test.ts new file mode 100644 index 0000000000..cfe2559fa7 --- /dev/null +++ b/packages/xstate-test/test/sync.test.ts @@ -0,0 +1,74 @@ +import { createMachine } from 'xstate'; +import { createTestModel } from '../src'; + +const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } +}); + +const promiseStateModel = createTestModel(machine); + +const promiseEventModel = createTestModel(machine); + +const syncModel = createTestModel(machine); + +describe('.testPathSync', () => { + it('Should error if it encounters a promise in a state', () => { + expect(() => + promiseStateModel.getPaths().forEach((path) => + promiseStateModel.testPathSync(path, { + states: { + a: async () => {}, + b: () => {} + }, + events: { + EVENT: () => {} + } + }) + ) + ).toThrowError( + `The test for 'a' returned a promise - did you mean to use the sync method?` + ); + }); + + it('Should error if it encounters a promise in an event', () => { + expect(() => + promiseEventModel.getPaths().forEach((path) => + promiseEventModel.testPathSync(path, { + states: { + a: () => {}, + b: () => {} + }, + events: { + EVENT: async () => {} + } + }) + ) + ).toThrowError( + `The event 'EVENT' returned a promise - did you mean to use the sync method?` + ); + }); + + it('Should succeed if it encounters no promises', () => { + expect(() => + syncModel.getPaths().forEach((path) => + syncModel.testPathSync(path, { + states: { + a: () => {}, + b: () => {} + }, + events: { + EVENT: () => {} + } + }) + ) + ).not.toThrow(); + }); +}); diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts new file mode 100644 index 0000000000..e141f51fc3 --- /dev/null +++ b/packages/xstate-test/test/testModel.test.ts @@ -0,0 +1,83 @@ +import { TestModel } from '../src'; +import { testUtils } from './testUtils'; + +describe('custom test models', () => { + it('tests any behavior', async () => { + const model = new TestModel( + { + initialState: 15, + transition: (value, event) => { + if (event.type === 'even') { + return value / 2; + } else { + return value * 3 + 1; + } + } + }, + { + getEvents: (state) => { + if (state % 2 === 0) { + return [{ type: 'even' }]; + } + return [{ type: 'odd' }]; + } + } + ); + + const paths = model.getShortestPathsTo((state) => state === 1); + + expect(paths.length).toBeGreaterThan(0); + }); + + it('tests states for any behavior', async () => { + const testedStateKeys: string[] = []; + + const model = new TestModel( + { + initialState: 15, + transition: (value, event) => { + if (event.type === 'even') { + return value / 2; + } else { + return value * 3 + 1; + } + } + }, + { + getEvents: (state) => { + if (state % 2 === 0) { + return [{ type: 'even' }]; + } + return [{ type: 'odd' }]; + }, + stateMatcher: (state, key) => { + if (key === 'even') { + return state % 2 === 0; + } + if (key === 'odd') { + return state % 2 === 1; + } + return false; + } + } + ); + + const paths = model.getShortestPathsTo((state) => state === 1); + + await testUtils.testPaths(paths, { + states: { + even: (state) => { + testedStateKeys.push('even'); + expect(state % 2).toBe(0); + }, + odd: (state) => { + testedStateKeys.push('odd'); + expect(state % 2).toBe(1); + } + } + }); + + expect(testedStateKeys).toContain('even'); + expect(testedStateKeys).toContain('odd'); + }); +}); diff --git a/packages/xstate-test/test/testUtils.ts b/packages/xstate-test/test/testUtils.ts new file mode 100644 index 0000000000..15e52aaa5f --- /dev/null +++ b/packages/xstate-test/test/testUtils.ts @@ -0,0 +1,25 @@ +import { TestModel } from '../src/TestModel'; +import { TestParam, TestPath } from '../src/types'; + +const testModel = async ( + model: TestModel, + params: TestParam +) => { + for (const path of model.getPaths()) { + await path.test(params); + } +}; + +const testPaths = async ( + paths: TestPath[], + params: TestParam +) => { + for (const path of paths) { + await path.test(params); + } +}; + +export const testUtils = { + testPaths, + testModel +};