From eefc5c75b6def84b4ec7312b8b9b54523dd935b4 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 7 Jan 2020 18:01:59 +0000 Subject: [PATCH 001/394] Suggestion: making example valid in TypeScript The original documented way to increment the simulated clock would not work in TypeScript because `service.clock` is of type `Clock` while only `SimulatedClock` has an `increment` method. At first I thought I'd just use a type assertion like: ``` (service.clock as SimulatedClock).increment(1000); ``` and that may be good enough for the example. I don't like unnecessary type assertions, so this PR proposes to keep the `SimulatedClock` instance in a local variable and increment that instead. As I said, this is just a suggestion. What do you think? --- docs/guides/delays.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/guides/delays.md b/docs/guides/delays.md index 9878b84f09..15bfd1ec91 100644 --- a/docs/guides/delays.md +++ b/docs/guides/delays.md @@ -277,15 +277,16 @@ import { interpret } from 'xstate'; // import { SimulatedClock } from 'xstate/lib/interpreter'; // < 4.6.0 import { SimulatedClock } from 'xstate/lib/SimulatedClock'; // >= 4.6.0 +const simulatedClock = new SimulatedClock(); const service = interpret(lightDelayMachine, { - clock: new SimulatedClock() + clock: simulatedClock }).onTransition(state => console.log(state.value)); service.start(); // => 'green' // move the SimulatedClock forward by 1 second -service.clock.increment(1000); +simulatedClock.increment(1000); // => 'yellow' ``` From b90f0888e011a63d28aa7645403d336de5adee52 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 24 Nov 2021 20:24:53 -0500 Subject: [PATCH 002/394] Update types --- packages/xstate-graph/src/graph.ts | 10 +++---- packages/xstate-graph/src/types.ts | 6 ++-- packages/xstate-test/src/index.ts | 44 +++++++++++++++--------------- packages/xstate-test/src/index2.ts | 12 ++++++++ packages/xstate-test/src/types.ts | 12 ++++---- 5 files changed, 48 insertions(+), 36 deletions(-) create mode 100644 packages/xstate-test/src/index2.ts diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index ef88dd4a27..308f8848fd 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -238,14 +238,14 @@ export function getShortestPaths< ? [ { state, - segments: [], + steps: [], weight } ] : [ { state, - segments: statePathMap[fromState].paths[0].segments.concat({ + steps: statePathMap[fromState].paths[0].steps.concat({ state: stateMap.get(fromState)!, event: deserializeEventString(fromEvent!) as TEvent }), @@ -294,7 +294,7 @@ export function getSimplePaths< paths[toStateSerial].paths.push({ state: fromState, weight: path.length, - segments: [...path] + steps: [...path] }); } else { for (const subEvent of keys(adjacency[fromStateSerial])) { @@ -403,7 +403,7 @@ export function getPathFromEvents< if (!machine.states) { return { state: machine.initialState, - segments: [], + steps: [], weight: 0 }; } @@ -441,7 +441,7 @@ export function getPathFromEvents< return { state, - segments: path, + steps: path, weight: path.length }; } diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index d07cc92e05..19ba17e507 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -82,11 +82,11 @@ export interface StatePath { */ state: State; /** - * 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: Segments; /** - * The combined weight of all segments in the path. + * The combined weight of all steps in the path. */ weight: number; } diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 96003a9f23..935cda5ad6 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -14,7 +14,7 @@ import { TestPlan, StatePredicate, TestPathResult, - TestSegmentResult, + TestStepResult, TestMeta, EventExecutor, CoverageOptions @@ -150,12 +150,12 @@ export class TestModel { return Object.keys(statePathsMap).map((key) => { const testPlan = statePathsMap[key]; const paths = testPlan.paths.map((path) => { - const segments = path.segments.map((segment) => { + const steps = path.steps.map((step) => { return { - ...segment, - description: getDescription(segment.state), - test: (testContext) => this.testState(segment.state, testContext), - exec: (testContext) => this.executeEvent(segment.event, testContext) + ...step, + description: getDescription(step.state), + test: (testContext) => this.testState(step.state, testContext), + exec: (testContext) => this.executeEvent(step.event, testContext) }; }); @@ -169,44 +169,44 @@ export class TestModel { return `${type}${propertyString}`; } - const eventsString = path.segments + const eventsString = path.steps .map((s) => formatEvent(s.event)) .join(' → '); return { ...path, - segments, + steps, description: `via ${eventsString}`, test: async (testContext) => { const testPathResult: TestPathResult = { - segments: [], + steps: [], state: { error: null } }; try { - for (const segment of segments) { - const testSegmentResult: TestSegmentResult = { - segment, + for (const step of steps) { + const testStepResult: TestStepResult = { + step, state: { error: null }, event: { error: null } }; - testPathResult.segments.push(testSegmentResult); + testPathResult.steps.push(testStepResult); try { - await segment.test(testContext); + await step.test(testContext); } catch (err) { - testSegmentResult.state.error = err; + testStepResult.state.error = err; throw err; } try { - await segment.exec(testContext); + await step.exec(testContext); } catch (err) { - testSegmentResult.event.error = err; + testStepResult.event.error = err; throw err; } @@ -228,16 +228,16 @@ export class TestModel { let hasFailed = false; err.message += '\nPath:\n' + - testPathResult.segments + testPathResult.steps .map((s) => { const stateString = `${JSON.stringify( - s.segment.state.value + s.step.state.value )} ${ - s.segment.state.context === undefined + s.step.state.context === undefined ? '' - : JSON.stringify(s.segment.state.context) + : JSON.stringify(s.step.state.context) }`; - const eventString = `${JSON.stringify(s.segment.event)}`; + const eventString = `${JSON.stringify(s.step.event)}`; const stateResult = `\tState: ${ hasFailed diff --git a/packages/xstate-test/src/index2.ts b/packages/xstate-test/src/index2.ts new file mode 100644 index 0000000000..28b86872a3 --- /dev/null +++ b/packages/xstate-test/src/index2.ts @@ -0,0 +1,12 @@ +import type { StateMachine } from 'xstate'; +import { TestPlan } from './types'; + +export function getShortestPathPlans< + TMachine extends StateMachine +>(machine: TMachine): Array> {} + +export function getSimplePathPlans< + TMachine extends StateMachine +>(machine: TMachine): Array> {} + +const plans = generateShortestPaths(machine, { until: transitionsCovered() }); diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index dd630725ed..406b500c1f 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -4,7 +4,7 @@ export interface TestMeta { description?: string | ((state: State) => string); skip?: boolean; } -interface TestSegment { +interface TestStep { state: State; event: EventObject; description: string; @@ -14,8 +14,8 @@ interface TestSegment { interface TestStateResult { error: null | Error; } -export interface TestSegmentResult { - segment: TestSegment; +export interface TestStepResult { + step: TestStep; state: TestStateResult; event: { error: null | Error; @@ -23,16 +23,16 @@ export interface TestSegmentResult { } export interface TestPath { weight: number; - segments: Array>; + steps: Array>; 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; } export interface TestPathResult { - segments: TestSegmentResult[]; + steps: TestStepResult[]; state: TestStateResult; } From 7209528cb1f24f743f580e97f2173d447a757d99 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 29 Nov 2021 20:53:27 -0500 Subject: [PATCH 003/394] WIP DFS --- packages/xstate-graph/src/graph.ts | 30 ++++++++++++++++++++ packages/xstate-graph/test/graph.test.ts | 35 ++++++++++++++++++++---- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 308f8848fd..5af76f7b3a 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -445,3 +445,33 @@ export function getPathFromEvents< weight: path.length }; } + +export function depthFirstTraversal( + reducer: (state: any, event: any) => any, + initialState: any, + events: any[], + serializeState: (state: any) => string +) { + const adj: any = {}; + + function util(state: any) { + const serializedState = serializeState(state); + if (adj[serializedState]) { + return; + } + + adj[serializedState] = {}; + + for (const event of events) { + const nextState = reducer(state, event); + const serializedNextState = serializeState(nextState); + adj[serializedState][JSON.stringify(event)] = serializedNextState; + + util(nextState); + } + } + + util(initialState); + + return adj; +} diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 63725661ea..1693f3bdab 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -6,7 +6,11 @@ import { getShortestPaths, toDirectedGraph } from '../src/index'; -import { getSimplePathsAsArray, getAdjacencyMap } from '../src/graph'; +import { + getSimplePathsAsArray, + getAdjacencyMap, + depthFirstTraversal +} from '../src/graph'; import { assign } from 'xstate'; describe('@xstate/graph', () => { @@ -172,7 +176,7 @@ describe('@xstate/graph', () => { expect( getShortestPaths(lightMachine)[ JSON.stringify(lightMachine.initialState.value) - ].paths[0].segments + ].paths[0].steps ).toHaveLength(0); }); @@ -235,12 +239,12 @@ describe('@xstate/graph', () => { 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 + getSimplePaths(lightMachine)['"green"'].paths[0].steps ).toHaveLength(0); expect(getSimplePaths(equivMachine)['"a"'].paths).toHaveLength(1); - expect( - getSimplePaths(equivMachine)['"a"'].paths[0].segments - ).toHaveLength(0); + expect(getSimplePaths(equivMachine)['"a"'].paths[0].steps).toHaveLength( + 0 + ); }); it('should return value-based paths', () => { @@ -432,3 +436,22 @@ describe('@xstate/graph', () => { }); }); }); + +it.only('hmm', () => { + const a = depthFirstTraversal( + (s, e) => { + if (e === 'a') { + return 1; + } + if (e === 'b' && s === 1) { + return 2; + } + return s; + }, + 0, + ['a', 'b'], + JSON.stringify + ); + + console.log(a); +}); From f0c8611f0c053d9b224f450158b199d82695df81 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 5 Dec 2021 15:19:28 -0500 Subject: [PATCH 004/394] WIP --- packages/xstate-graph/src/graph.ts | 117 +++++++++++++++++++++-- packages/xstate-graph/test/graph.test.ts | 20 ++-- 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 5af76f7b3a..b3a60d8dc5 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -446,13 +446,15 @@ export function getPathFromEvents< }; } -export function depthFirstTraversal( - reducer: (state: any, event: any) => any, - initialState: any, - events: any[], - serializeState: (state: any) => string -) { - const adj: any = {}; +type AdjMap = Record>; + +export function depthFirstTraversal( + reducer: (state: V, event: E) => V, + initialState: V, + events: E[], + serializeState: (state: V) => string +): AdjMap { + const adj: AdjMap = {}; function util(state: any) { const serializedState = serializeState(state); @@ -464,8 +466,7 @@ export function depthFirstTraversal( for (const event of events) { const nextState = reducer(state, event); - const serializedNextState = serializeState(nextState); - adj[serializedState][JSON.stringify(event)] = serializedNextState; + adj[serializedState][JSON.stringify(event)] = nextState; util(nextState); } @@ -475,3 +476,101 @@ export function depthFirstTraversal( return adj; } + +interface VisitedContext { + vertices: Set; + edges: Set; + a?: V | E; // TODO: remove +} + +interface DepthOptions { + serializeVertex?: (vertex: V) => string; + visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; +} + +function getDepthOptions( + depthOptions: DepthOptions +): Required> { + const serializeVertex = + depthOptions.serializeVertex ?? ((v) => JSON.stringify(v)); + return { + serializeVertex, + visitCondition: (v, _e, vctx) => vctx.vertices.has(serializeVertex(v)), + ...depthOptions + }; +} + +export function depthSimplePaths( + reducer: (state: V, event: E) => V, + initialState: V, + events: E[], + options: DepthOptions +) { + const { serializeVertex, visitCondition } = getDepthOptions(options); + const adjacency = depthFirstTraversal( + reducer, + initialState, + events, + serializeVertex + ); + const stateMap = new Map(); + // const visited = new Set(); + const visitCtx: VisitedContext = { + vertices: new Set(), + edges: new Set() + }; + const path: any[] = []; + const paths: Record = {}; + + function util(fromState: V, toStateSerial: string) { + const fromStateSerial = serializeVertex(fromState); + visitCtx.vertices.add(fromStateSerial); + + if (fromStateSerial === toStateSerial) { + if (!paths[toStateSerial]) { + paths[toStateSerial] = { + state: stateMap.get(toStateSerial)!, + paths: [] + }; + } + paths[toStateSerial].paths.push({ + state: fromState, + weight: path.length, + steps: [...path] + }); + } else { + for (const subEvent of keys(adjacency[fromStateSerial])) { + console.log(subEvent); + const nextState = adjacency[fromStateSerial][subEvent]; + + if (!nextState) { + continue; + } + + const nextStateSerial = serializeVertex(nextState); + stateMap.set(nextStateSerial, nextState); + + if (!visitCondition(nextState, JSON.parse(subEvent), visitCtx)) { + visitCtx.edges.add(subEvent); + path.push({ + state: stateMap.get(fromStateSerial)!, + event: deserializeEventString(subEvent) + }); + util(nextState, toStateSerial); + } + } + } + + path.pop(); + visitCtx.vertices.delete(fromStateSerial); + } + + const initialStateSerial = serializeVertex(initialState); + stateMap.set(initialStateSerial, initialState); + + for (const nextStateSerial of keys(adjacency)) { + util(initialState, nextStateSerial); + } + + return paths; +} diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 1693f3bdab..cf59fd9914 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -9,7 +9,7 @@ import { import { getSimplePathsAsArray, getAdjacencyMap, - depthFirstTraversal + depthSimplePaths } from '../src/graph'; import { assign } from 'xstate'; @@ -438,7 +438,7 @@ describe('@xstate/graph', () => { }); it.only('hmm', () => { - const a = depthFirstTraversal( + const a = depthSimplePaths( (s, e) => { if (e === 'a') { return 1; @@ -446,12 +446,20 @@ it.only('hmm', () => { if (e === 'b' && s === 1) { return 2; } + if (e === 'reset') { + return 0; + } return s; }, - 0, - ['a', 'b'], - JSON.stringify + 0 as number, + ['a', 'b', 'reset'], + { + serializeVertex: JSON.stringify, + visitCondition: (_, e, visitCtx) => { + return visitCtx.edges.has(JSON.stringify(e)); + } + } ); - console.log(a); + console.log(JSON.stringify(a, null, 2)); }); From 6b32e75e9c4c40ba3c10fc382db9be95f3dcc347 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 26 Dec 2021 11:46:19 -0500 Subject: [PATCH 005/394] WIP --- packages/xstate-graph/src/graph.ts | 91 +++++++++++++----------- packages/xstate-graph/src/types.ts | 5 ++ packages/xstate-graph/test/graph.test.ts | 5 +- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index b3a60d8dc5..20d531a34c 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -8,7 +8,7 @@ import { AnyEventObject } from 'xstate'; import { flatten, keys } from 'xstate/lib/utils'; -import { StatePath } from '.'; +import { SerializedEvent, SerializedState, StatePath } from '.'; import { StatePathsMap, StatePaths, @@ -446,33 +446,41 @@ export function getPathFromEvents< }; } -type AdjMap = Record>; +interface AdjMap { + [key: SerializedState]: { [key: SerializedEvent]: TState }; +} -export function depthFirstTraversal( - reducer: (state: V, event: E) => V, - initialState: V, - events: E[], - serializeState: (state: V) => string -): AdjMap { - const adj: AdjMap = {}; +interface TraversalOptions { + serializeVertex?: (vertex: V, edge: E | null) => string; + visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; +} - function util(state: any) { - const serializedState = serializeState(state); +export function depthFirstTraversal( + reducer: (state: TState, event: TEvent) => TState, + initialState: TState, + events: TEvent[], + options: TraversalOptions +): AdjMap { + const { serializeVertex } = resolveTraversalOptions(options); + const adj: AdjMap = {}; + + function util(state: any, event: TEvent | null) { + const serializedState = serializeVertex(state, event); if (adj[serializedState]) { return; } adj[serializedState] = {}; - for (const event of events) { - const nextState = reducer(state, event); - adj[serializedState][JSON.stringify(event)] = nextState; + for (const subEvent of events) { + const nextState = reducer(state, subEvent); + adj[serializedState][JSON.stringify(subEvent)] = nextState; - util(nextState); + util(nextState, subEvent); } } - util(initialState); + util(initialState, null); return adj; } @@ -483,35 +491,32 @@ interface VisitedContext { a?: V | E; // TODO: remove } -interface DepthOptions { - serializeVertex?: (vertex: V) => string; - visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; -} - -function getDepthOptions( - depthOptions: DepthOptions -): Required> { +function resolveTraversalOptions( + depthOptions: TraversalOptions +): Required> { const serializeVertex = - depthOptions.serializeVertex ?? ((v) => JSON.stringify(v)); + depthOptions.serializeVertex ?? ((state) => JSON.stringify(state)); return { serializeVertex, - visitCondition: (v, _e, vctx) => vctx.vertices.has(serializeVertex(v)), + visitCondition: (state, event, vctx) => + vctx.vertices.has(serializeVertex(state, event)), ...depthOptions }; } -export function depthSimplePaths( +export function depthSimplePaths( reducer: (state: V, event: E) => V, initialState: V, events: E[], - options: DepthOptions + options: TraversalOptions ) { - const { serializeVertex, visitCondition } = getDepthOptions(options); + const resolvedOptions = resolveTraversalOptions(options); + const { serializeVertex, visitCondition } = resolvedOptions; const adjacency = depthFirstTraversal( reducer, initialState, events, - serializeVertex + resolvedOptions ); const stateMap = new Map(); // const visited = new Set(); @@ -522,8 +527,8 @@ export function depthSimplePaths( const path: any[] = []; const paths: Record = {}; - function util(fromState: V, toStateSerial: string) { - const fromStateSerial = serializeVertex(fromState); + function util(fromState: V, toStateSerial: string, event: E | null) { + const fromStateSerial = serializeVertex(fromState, event); visitCtx.vertices.add(fromStateSerial); if (fromStateSerial === toStateSerial) { @@ -539,24 +544,24 @@ export function depthSimplePaths( steps: [...path] }); } else { - for (const subEvent of keys(adjacency[fromStateSerial])) { - console.log(subEvent); - const nextState = adjacency[fromStateSerial][subEvent]; + for (const serializedEvent of keys(adjacency[fromStateSerial])) { + const subEvent = JSON.parse(serializedEvent); + const nextState = adjacency[fromStateSerial][serializedEvent]; - if (!nextState) { + if (!(serializedEvent in adjacency[fromStateSerial])) { continue; } - const nextStateSerial = serializeVertex(nextState); + const nextStateSerial = serializeVertex(nextState, subEvent); stateMap.set(nextStateSerial, nextState); - if (!visitCondition(nextState, JSON.parse(subEvent), visitCtx)) { - visitCtx.edges.add(subEvent); + if (!visitCondition(nextState, subEvent, visitCtx)) { + visitCtx.edges.add(serializedEvent); path.push({ state: stateMap.get(fromStateSerial)!, - event: deserializeEventString(subEvent) + event: deserializeEventString(serializedEvent) }); - util(nextState, toStateSerial); + util(nextState, toStateSerial, subEvent); } } } @@ -565,11 +570,11 @@ export function depthSimplePaths( visitCtx.vertices.delete(fromStateSerial); } - const initialStateSerial = serializeVertex(initialState); + const initialStateSerial = serializeVertex(initialState, null); stateMap.set(initialStateSerial, initialState); for (const nextStateSerial of keys(adjacency)) { - util(initialState, nextStateSerial); + util(initialState, nextStateSerial, null); } return paths; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 19ba17e507..d0d3eea209 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -124,3 +124,8 @@ export interface ValueAdjMapOptions { stateSerializer?: (state: State) => string; eventSerializer?: (event: TEvent) => string; } + +type Brand = T & { __tag: Tag }; + +export type SerializedState = Brand; +export type SerializedEvent = Brand; diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index cf59fd9914..11ee4121ef 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -454,10 +454,7 @@ it.only('hmm', () => { 0 as number, ['a', 'b', 'reset'], { - serializeVertex: JSON.stringify, - visitCondition: (_, e, visitCtx) => { - return visitCtx.edges.has(JSON.stringify(e)); - } + serializeVertex: (v, e) => JSON.stringify(v) + JSON.stringify(e) } ); From dc0a03b6ff62517620a12063eb7ee7885a7e6230 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 27 Dec 2021 18:01:55 -0500 Subject: [PATCH 006/394] Improve types --- packages/xstate-graph/src/graph.ts | 81 +++++++++++++++++------------- packages/xstate-graph/src/types.ts | 49 ++++++++---------- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 20d531a34c..c3520f97cf 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -13,7 +13,7 @@ import { StatePathsMap, StatePaths, AdjacencyMap, - Segments, + Steps, ValueAdjMapOptions, DirectedGraphEdge, DirectedGraphNode @@ -88,12 +88,12 @@ const defaultValueAdjMapOptions: Required> = { eventSerializer: serializeEvent }; -function getValueAdjMapOptions( - options?: ValueAdjMapOptions -): Required> { +function getValueAdjMapOptions( + options?: ValueAdjMapOptions +): Required> { return { ...(defaultValueAdjMapOptions as Required< - ValueAdjMapOptions + ValueAdjMapOptions >), ...options }; @@ -104,13 +104,13 @@ export function getAdjacencyMap< TEvent extends EventObject = AnyEventObject >( node: StateNode | StateMachine, - options?: ValueAdjMapOptions -): AdjacencyMap { + options?: ValueAdjMapOptions, TEvent> +): AdjacencyMap, TEvent> { const optionsWithDefaults = getValueAdjMapOptions(options); const { filter, stateSerializer, eventSerializer } = optionsWithDefaults; const { events } = optionsWithDefaults; - const adjacency: AdjacencyMap = {}; + const adjacency: AdjacencyMap, TEvent> = {}; function findAdjacencies(state: State) { const { nextEvents } = state; @@ -174,17 +174,14 @@ export function getShortestPaths< TEvent extends EventObject = EventObject >( machine: StateMachine, - options?: ValueAdjMapOptions -): StatePathsMap { + options?: ValueAdjMapOptions, TEvent> +): StatePathsMap, TEvent> { if (!machine.states) { return EMPTY_MAP; } const optionsWithDefaults = getValueAdjMapOptions(options); - const adjacency = getAdjacencyMap( - machine, - optionsWithDefaults - ); + const adjacency = getAdjacencyMap(machine, optionsWithDefaults); // weight, state, event const weightMap = new Map< @@ -228,7 +225,7 @@ export function getShortestPaths< } } - const statePathMap: StatePathsMap = {}; + const statePathMap: StatePathsMap, TEvent> = {}; weightMap.forEach(([weight, fromState, fromEvent], stateSerial) => { const state = stateMap.get(stateSerial)!; @@ -263,8 +260,8 @@ export function getSimplePaths< TEvent extends EventObject = EventObject >( machine: StateMachine, - options?: ValueAdjMapOptions -): StatePathsMap { + options?: ValueAdjMapOptions, TEvent> +): StatePathsMap, TEvent> { const optionsWithDefaults = getValueAdjMapOptions(options); const { stateSerializer } = optionsWithDefaults; @@ -273,12 +270,11 @@ export function getSimplePaths< 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 = {}; + const path: Steps, TEvent> = []; + const paths: StatePathsMap, TEvent> = {}; function util(fromState: State, toStateSerial: string) { const fromStateSerial = stateSerializer(fromState); @@ -336,8 +332,8 @@ export function getSimplePathsAsArray< TEvent extends EventObject = EventObject >( machine: StateNode, - options?: ValueAdjMapOptions -): Array> { + options?: ValueAdjMapOptions, TEvent> +): Array, TEvent>> { const result = getSimplePaths(machine, options); return keys(result).map((key) => result[key]); } @@ -388,9 +384,12 @@ export function getPathFromEvents< TEvent extends EventObject = EventObject >( machine: StateMachine, - events: Array -): StatePath { - const optionsWithDefaults = getValueAdjMapOptions({ + events: TEvent[] +): StatePath, TEvent> { + const optionsWithDefaults = getValueAdjMapOptions< + State, + TEvent + >({ events: events.reduce((events, event) => { events[event.type] ??= []; events[event.type].push(event); @@ -410,7 +409,7 @@ export function getPathFromEvents< const adjacency = getAdjacencyMap(machine, optionsWithDefaults); const stateMap = new Map>(); - const path: Segments = []; + const path: Steps, TEvent> = []; const initialStateSerial = stateSerializer(machine.initialState); stateMap.set(initialStateSerial, machine.initialState); @@ -504,12 +503,18 @@ function resolveTraversalOptions( }; } -export function depthSimplePaths( - reducer: (state: V, event: E) => V, - initialState: V, - events: E[], - options: TraversalOptions -) { +export function depthSimplePaths( + reducer: (state: TState, event: TEvent) => TState, + initialState: TState, + events: TEvent[], + options: TraversalOptions +): Record< + SerializedState, + { + state: TState; + paths: Array<{ state: TState; event: TEvent }>; + } +> { const resolvedOptions = resolveTraversalOptions(options); const { serializeVertex, visitCondition } = resolvedOptions; const adjacency = depthFirstTraversal( @@ -518,16 +523,20 @@ export function depthSimplePaths( events, resolvedOptions ); - const stateMap = new Map(); + const stateMap = new Map(); // const visited = new Set(); - const visitCtx: VisitedContext = { + const visitCtx: VisitedContext = { vertices: new Set(), edges: new Set() }; const path: any[] = []; - const paths: Record = {}; + const paths: Record = {}; - function util(fromState: V, toStateSerial: string, event: E | null) { + function util( + fromState: TState, + toStateSerial: string, + event: TEvent | null + ) { const fromStateSerial = serializeVertex(fromState, event); visitCtx.vertices.add(fromStateSerial); diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index d0d3eea209..ff390da6c2 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -1,14 +1,4 @@ -import { - State, - EventObject, - StateValue, - StateNode, - TransitionDefinition -} from 'xstate'; - -export interface TransitionMap { - state: StateValue | undefined; -} +import { EventObject, StateNode, TransitionDefinition } from 'xstate'; export type JSONSerializable = T & { toJSON: () => U; @@ -55,58 +45,59 @@ export type DirectedGraphNode = JSONSerializable< } >; -export interface AdjacencyMap { +export interface AdjacencyMap { [stateId: string]: Record< string, { - state: State; + state: TState; event: TEvent; } >; } -export interface StatePaths { +export interface StatePaths { /** * 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 (steps) which reach the ending `state`. */ - steps: Segments; + steps: Steps; /** * The combined weight of all steps in the path. */ weight: number; } -export interface StatePathsMap { - [key: string]: StatePaths; +export interface StatePathsMap { + [key: string]: StatePaths; } -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< @@ -114,14 +105,14 @@ export type ExtractEvent< TType extends TEvent['type'] > = TEvent extends { type: TType } ? TEvent : never; -export interface ValueAdjMapOptions { +export interface ValueAdjMapOptions { 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; } From 7974ddfbbc77b2b929d188a620cf34b82b18824c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 27 Dec 2021 19:59:33 -0500 Subject: [PATCH 007/394] WIP, so far so god --- packages/xstate-graph/src/graph.ts | 63 +- .../test/__snapshots__/graph.test.ts.snap | 9590 +++-------------- packages/xstate-graph/test/graph.test.ts | 83 +- 3 files changed, 1757 insertions(+), 7979 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index c3520f97cf..f3261789f7 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -62,11 +62,15 @@ export function getChildren(stateNode: StateNode): StateNode[] { return children; } -export function serializeState(state: State): string { +export function serializeState( + state: State +): SerializedState { const { value, context } = state; - return context === undefined + return (context === undefined ? JSON.stringify(value) - : JSON.stringify(value) + ' | ' + JSON.stringify(context); + : JSON.stringify(value) + + ' | ' + + JSON.stringify(context)) as SerializedState; } export function serializeEvent( @@ -103,7 +107,9 @@ export function getAdjacencyMap< TContext = DefaultContext, TEvent extends EventObject = AnyEventObject >( - node: StateNode | StateMachine, + machine: + | StateNode + | StateMachine, options?: ValueAdjMapOptions, TEvent> ): AdjacencyMap, TEvent> { const optionsWithDefaults = getValueAdjMapOptions(options); @@ -141,7 +147,7 @@ export function getAdjacencyMap< for (const event of potentialEvents) { let nextState: State; try { - nextState = node.transition(state, event); + nextState = machine.transition(state, event); } catch (e) { throw new Error( `Unable to transition from state ${stateSerializer( @@ -164,7 +170,7 @@ export function getAdjacencyMap< } } - findAdjacencies(node.initialState); + findAdjacencies(machine.initialState); return adjacency; } @@ -260,8 +266,17 @@ export function getSimplePaths< TEvent extends EventObject = EventObject >( machine: StateMachine, + _events: TEvent[] = machine.events.map((type) => ({ type })) as TEvent[], options?: ValueAdjMapOptions, TEvent> ): StatePathsMap, TEvent> { + return depthSimplePaths( + (state, event) => machine.transition(state, event), + machine.initialState, + _events, + { + serializeState: serializeState as any + } + ); const optionsWithDefaults = getValueAdjMapOptions(options); const { stateSerializer } = optionsWithDefaults; @@ -334,7 +349,7 @@ export function getSimplePathsAsArray< machine: StateNode, options?: ValueAdjMapOptions, TEvent> ): Array, TEvent>> { - const result = getSimplePaths(machine, options); + const result = getSimplePaths(machine, undefined, options); return keys(result).map((key) => result[key]); } @@ -450,7 +465,7 @@ interface AdjMap { } interface TraversalOptions { - serializeVertex?: (vertex: V, edge: E | null) => string; + serializeState?: (vertex: V, edge: E | null) => SerializedState; visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; } @@ -460,11 +475,11 @@ export function depthFirstTraversal( events: TEvent[], options: TraversalOptions ): AdjMap { - const { serializeVertex } = resolveTraversalOptions(options); + const { serializeState: serializeState } = resolveTraversalOptions(options); const adj: AdjMap = {}; - function util(state: any, event: TEvent | null) { - const serializedState = serializeVertex(state, event); + function util(state: TState, event: TEvent | null) { + const serializedState = serializeState(state, event); if (adj[serializedState]) { return; } @@ -493,30 +508,24 @@ interface VisitedContext { function resolveTraversalOptions( depthOptions: TraversalOptions ): Required> { - const serializeVertex = - depthOptions.serializeVertex ?? ((state) => JSON.stringify(state)); + const serializeState = + depthOptions.serializeState ?? ((state) => JSON.stringify(state) as any); return { - serializeVertex, + serializeState, visitCondition: (state, event, vctx) => - vctx.vertices.has(serializeVertex(state, event)), + vctx.vertices.has(serializeState(state, event)), ...depthOptions }; } -export function depthSimplePaths( +export function depthSimplePaths( reducer: (state: TState, event: TEvent) => TState, initialState: TState, events: TEvent[], options: TraversalOptions -): Record< - SerializedState, - { - state: TState; - paths: Array<{ state: TState; event: TEvent }>; - } -> { +): StatePathsMap { const resolvedOptions = resolveTraversalOptions(options); - const { serializeVertex, visitCondition } = resolvedOptions; + const { serializeState, visitCondition } = resolvedOptions; const adjacency = depthFirstTraversal( reducer, initialState, @@ -537,7 +546,7 @@ export function depthSimplePaths( toStateSerial: string, event: TEvent | null ) { - const fromStateSerial = serializeVertex(fromState, event); + const fromStateSerial = serializeState(fromState, event); visitCtx.vertices.add(fromStateSerial); if (fromStateSerial === toStateSerial) { @@ -561,7 +570,7 @@ export function depthSimplePaths( continue; } - const nextStateSerial = serializeVertex(nextState, subEvent); + const nextStateSerial = serializeState(nextState, subEvent); stateMap.set(nextStateSerial, nextState); if (!visitCondition(nextState, subEvent, visitCtx)) { @@ -579,7 +588,7 @@ export function depthSimplePaths( visitCtx.vertices.delete(fromStateSerial); } - const initialStateSerial = serializeVertex(initialState, null); + const initialStateSerial = serializeState(initialState, null); stateMap.set(initialStateSerial, initialState); for (const nextStateSerial of keys(adjacency)) { diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index ad13a435de..a5663b8769 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -2,7 +2,80 @@ 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 { + "_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", + }, + }, + "steps": Array [ Object { "event": Object { "type": "TIMER", @@ -297,6923 +370,549 @@ 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", + "weight": 4, +} +`; + +exports[`@xstate/graph getShortestPaths() should represent conditional paths based on context: shortest paths conditional 1`] = ` +Object { + "\\"bar\\" | {\\"id\\":\\"foo\\"}": Array [ + Object { + "state": "bar", + "steps": Array [ + Object { + "state": "pending", + "type": "EVENT", + }, + ], }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", + ], + "\\"foo\\" | {\\"id\\":\\"foo\\"}": Array [ + Object { + "state": "foo", + "steps": Array [ + Object { + "state": "pending", + "type": "STATE", }, - "name": "xstate.init", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "xstate.init", + ], + }, + ], + "\\"pending\\" | {\\"id\\":\\"foo\\"}": Array [ + Object { + "state": "pending", + "steps": Array [], + }, + ], +} +`; + +exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states (parallel): shortest paths parallel 1`] = ` +Object { + "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}": Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", + "steps": Array [], }, - "historyValue": Object { - "current": Object { - "red": "flashing", + ], + "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}": Array [ + Object { + "state": Object { + "a": "a2", + "b": "b2", }, - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, + "steps": Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, + "type": "2", }, - "yellow": undefined, - }, + ], }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", + ], + "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}": Array [ + Object { + "state": Object { + "a": "a3", + "b": "b3", + }, + "steps": Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", + }, + "type": "3", + }, + ], }, - }, - "weight": 4, + ], } `; -exports[`@xstate/graph getShortestPaths() should represent conditional paths based on context: shortest paths conditional 1`] = ` +exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states: shortest paths 1`] = ` Object { - "\\"bar\\" | {\\"id\\":\\"foo\\"}": Object { - "paths": 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", + "\\"green\\"": Array [ + Object { + "state": "green", + "steps": Array [], + }, + ], + "\\"yellow\\"": Array [ + Object { + "state": "yellow", + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", }, - "weight": 1, + ], + }, + ], + "{\\"red\\":\\"flashing\\"}": Array [ + Object { + "state": Object { + "red": "flashing", }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "id": "whatever", - "type": "EVENT", + "steps": Array [ + Object { + "state": "green", + "type": "POWER_OUTAGE", }, - "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", + ], + }, + ], + "{\\"red\\":\\"stop\\"}": Array [ + Object { + "state": Object { + "red": "stop", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", + }, + Object { + "state": "yellow", + "type": "TIMER", + }, + Object { + "state": Object { + "red": "walk", }, - "name": "xstate.init", - "type": "external", + "type": "PED_COUNTDOWN", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", + Object { + "state": Object { + "red": "wait", + }, + "type": "PED_COUNTDOWN", }, - "done": false, - "event": Object { - "type": "xstate.init", + ], + }, + ], + "{\\"red\\":\\"wait\\"}": Array [ + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", + Object { + "state": "yellow", + "type": "TIMER", + }, + Object { + "state": Object { + "red": "walk", + }, + "type": "PED_COUNTDOWN", + }, + ], + }, + ], + "{\\"red\\":\\"walk\\"}": Array [ + Object { + "state": Object { + "red": "walk", }, - "historyValue": Object { - "current": "bar", - "states": Object { - "bar": undefined, - "foo": undefined, - "pending": undefined, + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", + }, + Object { + "state": "yellow", + "type": "TIMER", + }, + ], + }, + ], +} +`; + +exports[`@xstate/graph getSimplePaths() should return a mapping of arrays of simple paths to all states 2`] = ` +Object { + "\\"green\\"": Array [ + Object { + "state": "green", + "steps": Array [], + }, + ], + "\\"yellow\\"": Array [ + Object { + "state": "yellow", + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", }, + ], + }, + ], + "{\\"red\\":\\"flashing\\"}": Array [ + Object { + "state": Object { + "red": "flashing", }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "bar", + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", + }, + Object { + "state": "yellow", + "type": "TIMER", + }, + Object { + "state": Object { + "red": "walk", + }, + "type": "POWER_OUTAGE", + }, + ], }, - }, - "\\"foo\\" | {\\"id\\":\\"foo\\"}": Object { - "paths": 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", - }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", + }, + Object { + "state": "yellow", + "type": "TIMER", + }, + Object { + "state": Object { + "red": "walk", }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "STATE", - }, - "name": "STATE", - "type": "external", + "type": "PED_COUNTDOWN", + }, + Object { + "state": Object { + "red": "wait", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "id": "foo", + "type": "POWER_OUTAGE", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", + }, + Object { + "state": "yellow", + "type": "TIMER", + }, + Object { + "state": Object { + "red": "walk", }, - "done": false, - "event": Object { - "type": "STATE", + "type": "PED_COUNTDOWN", + }, + Object { + "state": Object { + "red": "wait", }, - "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, - }, + "type": "PED_COUNTDOWN", + }, + Object { + "state": Object { + "red": "stop", }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "foo", + "type": "POWER_OUTAGE", }, - "weight": 1, + ], + }, + Object { + "state": Object { + "red": "flashing", }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "STATE", + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", }, - "name": "STATE", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": true, - "children": Object {}, - "context": Object { - "id": "foo", + Object { + "state": "yellow", + "type": "POWER_OUTAGE", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", }, - "done": false, - "event": Object { - "type": "STATE", + "steps": Array [ + Object { + "state": "green", + "type": "POWER_OUTAGE", + }, + ], + }, + ], + "{\\"red\\":\\"stop\\"}": Array [ + Object { + "state": Object { + "red": "stop", }, - "events": Array [], - "history": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", + }, + Object { + "state": "yellow", + "type": "TIMER", + }, + Object { + "state": Object { + "red": "walk", }, - "name": "xstate.init", - "type": "external", + "type": "PED_COUNTDOWN", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", + Object { + "state": Object { + "red": "wait", + }, + "type": "PED_COUNTDOWN", }, - "done": false, - "event": Object { - "type": "xstate.init", + ], + }, + ], + "{\\"red\\":\\"wait\\"}": Array [ + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", }, - "events": Array [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", + Object { + "state": "yellow", + "type": "TIMER", + }, + Object { + "state": Object { + "red": "walk", + }, + "type": "PED_COUNTDOWN", + }, + ], + }, + ], + "{\\"red\\":\\"walk\\"}": Array [ + Object { + "state": Object { + "red": "walk", }, - "historyValue": Object { - "current": "foo", - "states": Object { - "bar": undefined, - "foo": undefined, - "pending": undefined, + "steps": Array [ + Object { + "state": "green", + "type": "TIMER", + }, + Object { + "state": "yellow", + "type": "TIMER", }, + ], + }, + ], +} +`; + +exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths to all states (parallel): simple paths parallel 1`] = ` +Object { + "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}": Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "foo", + "steps": Array [], }, - }, - "\\"pending\\" | {\\"id\\":\\"foo\\"}": Object { - "paths": Array [ - Object { - "segments": Array [], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", - }, - "name": "xstate.init", - "type": "external", + ], + "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}": Array [ + Object { + "state": Object { + "a": "a2", + "b": "b2", + }, + "steps": Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": undefined, - "children": Object {}, - "context": Object { - "id": "foo", + "type": "2", + }, + ], + }, + ], + "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}": Array [ + Object { + "state": Object { + "a": "a3", + "b": "b3", + }, + "steps": Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, - "done": false, - "event": Object { - "type": "xstate.init", + "type": "2", + }, + Object { + "state": Object { + "a": "a2", + "b": "b2", }, - "events": Array [], - "history": undefined, - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "pending", + "type": "3", }, - "weight": 0, + ], + }, + Object { + "state": Object { + "a": "a3", + "b": "b3", }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", + "steps": Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", + }, + "type": "3", }, - "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", + ], }, - }, + ], } `; -exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states (parallel): shortest paths parallel 1`] = ` +exports[`@xstate/graph getSimplePaths() should return multiple paths for equivalent transitions: simple paths equal transitions 1`] = ` Object { - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}": Object { - "paths": Array [ - Object { - "segments": Array [], - "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", - }, - }, - "weight": 0, - }, - ], - "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", - }, - }, - }, - "{\\"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", - }, - }, - }, - ], - "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": "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, - }, - ], - "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 getShortestPaths() should return a mapping of shortest paths to all states: shortest paths 1`] = ` -Object { - "\\"green\\"": Object { - "paths": Array [ - Object { - "segments": Array [], - "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", - }, - "weight": 0, - }, - ], - "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", - }, - }, - "\\"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", - }, - }, - ], - "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": "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", - }, - }, - ], - "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": 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": "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", + "\\"a\\"": Array [ + Object { + "state": "a", + "steps": Array [], }, - }, - "\\"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", + ], + "\\"b\\"": Array [ + Object { + "state": "b", + "steps": Array [ + Object { + "state": "a", + "type": "FOO", }, - "weight": 0, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "xstate.init", + ], + }, + Object { + "state": "b", + "steps": Array [ + Object { + "state": "a", + "type": "BAR", }, - "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", + ], +} +`; + +exports[`@xstate/graph getSimplePaths() should return value-based paths: simple paths context 1`] = ` +Object { + "\\"finish\\" | {\\"count\\":3}": Array [ + Object { + "state": "finish", + "steps": Array [ + Object { + "state": "start", + "type": "INC", }, - "weight": 1, - }, - ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { + Object { + "state": "start", "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, + Object { + "state": "start", + "type": "INC", }, - }, - "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 { + ], + "\\"start\\" | {\\"count\\":0}": Array [ + Object { + "state": "start", + "steps": Array [], + }, + ], + "\\"start\\" | {\\"count\\":1}": Array [ + Object { + "state": "start", + "steps": Array [ + Object { + "state": "start", "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, + ], + }, + ], + "\\"start\\" | {\\"count\\":2}": Array [ + Object { + "state": "start", + "steps": Array [ + Object { + "state": "start", + "type": "INC", }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "start", + Object { + "state": "start", + "type": "INC", + }, + ], }, - }, + ], } `; @@ -7222,7 +921,6 @@ Array [ Object { "paths": Array [ Object { - "segments": Array [], "state": Object { "_event": Object { "$$type": "scxml", @@ -7243,7 +941,6 @@ Array [ "type": "xstate.init", }, "events": Array [], - "history": undefined, "historyValue": undefined, "matches": [Function], "meta": Object {}, @@ -7251,6 +948,7 @@ Array [ "toStrings": [Function], "value": "green", }, + "steps": Array [], "weight": 0, }, ], @@ -7274,7 +972,6 @@ Array [ "type": "xstate.init", }, "events": Array [], - "history": undefined, "historyValue": undefined, "matches": [Function], "meta": Object {}, @@ -7286,41 +983,6 @@ Array [ 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", @@ -7363,6 +1025,40 @@ Array [ "toStrings": [Function], "value": "yellow", }, + "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 [], + "historyValue": undefined, + "matches": [Function], + "meta": Object {}, + "tags": Array [], + "toStrings": [Function], + "value": "green", + }, + }, + ], "weight": 1, }, ], @@ -7412,7 +1108,53 @@ Array [ Object { "paths": Array [ Object { - "segments": Array [ + "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", + }, + }, + "steps": Array [ Object { "event": Object { "type": "TIMER", @@ -7421,84 +1163,50 @@ Array [ "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "events": Array [], "history": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "xstate.init", }, - "name": "POWER_OUTAGE", + "name": "xstate.init", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": undefined, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "xstate.init", }, "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, - }, - }, + "historyValue": 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, - }, + "value": "green", }, + "historyValue": undefined, "matches": [Function], "meta": Object {}, "tags": Array [], @@ -7554,52 +1262,6 @@ Array [ }, }, ], - "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, }, ], @@ -7632,28 +1294,74 @@ Array [ "red": Object { "current": "walk", "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, + "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 { + "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, }, }, - "yellow": undefined, + "matches": [Function], + "meta": Object {}, + "tags": Array [], + "toStrings": [Function], + "value": Object { + "red": "flashing", + }, }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "walk", - }, - }, - }, - Object { - "paths": Array [ - Object { - "segments": Array [ + "steps": Array [ Object { "event": Object { "type": "TIMER", @@ -7662,84 +1370,50 @@ Array [ "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "events": Array [], "history": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "xstate.init", }, - "name": "POWER_OUTAGE", + "name": "xstate.init", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": undefined, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "xstate.init", }, "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, - }, - }, + "historyValue": 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, - }, + "value": "green", }, + "historyValue": undefined, "matches": [Function], "meta": Object {}, "tags": Array [], @@ -7796,7 +1470,7 @@ Array [ }, Object { "event": Object { - "type": "PED_COUNTDOWN", + "type": "POWER_OUTAGE", }, "state": Object { "_event": Object { @@ -7846,39 +1520,88 @@ Array [ }, }, ], + "weight": 3, + }, + Object { "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "PED_COUNTDOWN", + "type": "POWER_OUTAGE", }, - "name": "PED_COUNTDOWN", + "name": "POWER_OUTAGE", "type": "external", }, "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], + "actions": Array [], "activities": Object {}, "changed": true, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "PED_COUNTDOWN", + "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": "wait", + "red": "flashing", }, "states": Object { "green": undefined, "red": Object { - "current": "wait", + "current": "flashing", "states": Object { "flashing": undefined, "stop": undefined, @@ -7894,68 +1617,69 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "wait", + "red": "flashing", }, }, - "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, + "steps": Array [ + Object { + "event": Object { + "type": "TIMER", }, - }, - "yellow": undefined, - }, - }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "wait", - }, - }, - }, - Object { - "paths": Array [ - Object { - "segments": Array [ + "state": Object { + "_event": Object { + "$$type": "scxml", + "data": Object { + "type": "PED_COUNTDOWN", + }, + "name": "PED_COUNTDOWN", + "type": "external", + }, + "_sessionid": null, + "actions": Array [], + "activities": Object {}, + "changed": false, + "children": Object {}, + "context": undefined, + "done": false, + "event": Object { + "type": "PED_COUNTDOWN", + }, + "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": undefined, + "matches": [Function], + "meta": Object {}, + "tags": Array [], + "toStrings": [Function], + "value": "green", + }, + }, Object { "event": Object { "type": "TIMER", @@ -7980,13 +1704,60 @@ Array [ "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": "PUSH_BUTTON", + }, + "name": "PUSH_BUTTON", + "type": "external", + }, + "_sessionid": null, + "actions": Array [], + "activities": Object {}, + "changed": false, + "children": Object {}, + "context": undefined, + "done": false, + "event": Object { + "type": "PUSH_BUTTON", + }, + "events": Array [], "history": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "TIMER", }, - "name": "POWER_OUTAGE", + "name": "TIMER", "type": "external", }, "_sessionid": null, @@ -7997,17 +1768,17 @@ Array [ "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "TIMER", }, "events": Array [], "historyValue": Object { "current": Object { - "red": "flashing", + "red": "walk", }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "walk", "states": Object { "flashing": undefined, "stop": undefined, @@ -8023,15 +1794,17 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "flashing", + "red": "walk", }, }, "historyValue": Object { - "current": "green", + "current": Object { + "red": "walk", + }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "walk", "states": Object { "flashing": undefined, "stop": undefined, @@ -8046,39 +1819,48 @@ Array [ "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": "green", + "value": Object { + "red": "walk", + }, }, }, Object { "event": Object { - "type": "TIMER", + "type": "POWER_OUTAGE", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, - "actions": Array [], + "actions": Array [ + Object { + "exec": undefined, + "type": "startCountdown", + }, + ], "activities": Object {}, "changed": true, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "events": Array [], "historyValue": Object { - "current": "yellow", + "current": Object { + "red": "wait", + }, "states": Object { "green": undefined, "red": Object { - "current": "walk", + "current": "wait", "states": Object { "flashing": undefined, "stop": undefined, @@ -8093,97 +1875,197 @@ Array [ "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": "yellow", + "value": Object { + "red": "wait", + }, }, }, - Object { + ], + "weight": 4, + }, + 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 [], + "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": 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", + }, + }, + "steps": Array [ + Object { + "event": Object { + "type": "TIMER", + }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "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, - }, + "history": Object { + "_event": Object { + "$$type": "scxml", + "data": Object { + "type": "xstate.init", }, - "yellow": undefined, + "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": undefined, "matches": [Function], "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": Object { - "red": "walk", - }, + "value": "green", }, }, Object { "event": Object { - "type": "PED_COUNTDOWN", + "type": "TIMER", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "PED_COUNTDOWN", + "type": "TIMER", }, - "name": "PED_COUNTDOWN", + "name": "TIMER", "type": "external", }, "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], + "actions": Array [], "activities": Object {}, "changed": true, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "PED_COUNTDOWN", + "type": "TIMER", }, - "events": Array [], - "historyValue": Object { - "current": Object { - "red": "wait", - }, + "events": Array [], + "historyValue": Object { + "current": "yellow", "states": Object { "green": undefined, "red": Object { - "current": "wait", + "current": "walk", "states": Object { "flashing": undefined, "stop": undefined, @@ -8198,143 +2080,40 @@ Array [ "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, + "value": "yellow", }, }, - "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", + "type": "PED_COUNTDOWN", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, - "name": "TIMER", + "name": "PUSH_BUTTON", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, "events": Array [], "history": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "TIMER", }, - "name": "POWER_OUTAGE", + "name": "TIMER", "type": "external", }, "_sessionid": null, @@ -8345,17 +2124,17 @@ Array [ "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "TIMER", }, "events": Array [], "historyValue": Object { "current": Object { - "red": "flashing", + "red": "walk", }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "walk", "states": Object { "flashing": undefined, "stop": undefined, @@ -8371,15 +2150,17 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "flashing", + "red": "walk", }, }, "historyValue": Object { - "current": "green", + "current": Object { + "red": "walk", + }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "walk", "states": Object { "flashing": undefined, "stop": undefined, @@ -8394,39 +2175,94 @@ Array [ "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": "green", + "value": Object { + "red": "walk", + }, }, }, Object { "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, - "name": "TIMER", + "name": "PUSH_BUTTON", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, "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": "yellow", + "current": Object { + "red": "wait", + }, "states": Object { "green": undefined, "red": Object { - "current": "walk", + "current": "wait", "states": Object { "flashing": undefined, "stop": undefined, @@ -8441,20 +2277,22 @@ Array [ "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": "yellow", + "value": Object { + "red": "wait", + }, }, }, Object { "event": Object { - "type": "PED_COUNTDOWN", + "type": "POWER_OUTAGE", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, @@ -8465,17 +2303,17 @@ Array [ "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "events": Array [], "historyValue": Object { "current": Object { - "red": "walk", + "red": "stop", }, "states": Object { "green": undefined, "red": Object { - "current": "walk", + "current": "stop", "states": Object { "flashing": undefined, "stop": undefined, @@ -8483,21 +2321,114 @@ Array [ "walk": undefined, }, }, - "yellow": undefined, + "yellow": undefined, + }, + }, + "matches": [Function], + "meta": Object {}, + "tags": Array [], + "toStrings": [Function], + "value": Object { + "red": "stop", + }, + }, + }, + ], + "weight": 5, + }, + 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 [], + "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": Object { - "red": "walk", + }, + "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", + }, + }, + "steps": Array [ Object { "event": Object { - "type": "PED_COUNTDOWN", + "type": "TIMER", }, "state": Object { "_event": Object { @@ -8509,14 +2440,9 @@ Array [ "type": "external", }, "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], + "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, @@ -8524,31 +2450,39 @@ Array [ "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, - }, + "history": Object { + "_event": Object { + "$$type": "scxml", + "data": Object { + "type": "xstate.init", }, - "yellow": undefined, + "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": undefined, "matches": [Function], "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": Object { - "red": "wait", - }, + "value": "green", }, }, Object { @@ -8559,9 +2493,9 @@ Array [ "_event": Object { "$$type": "scxml", "data": Object { - "type": "PED_COUNTDOWN", + "type": "TIMER", }, - "name": "PED_COUNTDOWN", + "name": "TIMER", "type": "external", }, "_sessionid": null, @@ -8572,17 +2506,15 @@ Array [ "context": undefined, "done": false, "event": Object { - "type": "PED_COUNTDOWN", + "type": "TIMER", }, "events": Array [], "historyValue": Object { - "current": Object { - "red": "stop", - }, + "current": "yellow", "states": Object { "green": undefined, "red": Object { - "current": "stop", + "current": "walk", "states": Object { "flashing": undefined, "stop": undefined, @@ -8597,12 +2529,13 @@ Array [ "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": Object { - "red": "stop", - }, + "value": "yellow", }, }, ], + "weight": 2, + }, + Object { "state": Object { "_event": Object { "$$type": "scxml", @@ -8623,6 +2556,33 @@ Array [ "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", @@ -8649,13 +2609,10 @@ Array [ "red": "flashing", }, }, - "weight": 5, - }, - Object { - "segments": Array [ + "steps": Array [ Object { "event": Object { - "type": "TIMER", + "type": "POWER_OUTAGE", }, "state": Object { "_event": Object { @@ -8681,9 +2638,9 @@ Array [ "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "PED_COUNTDOWN", }, - "name": "POWER_OUTAGE", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, @@ -8694,17 +2651,17 @@ Array [ "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "PED_COUNTDOWN", }, "events": Array [], "historyValue": Object { "current": Object { - "red": "flashing", + "red": "stop", }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "stop", "states": Object { "flashing": undefined, "stop": undefined, @@ -8720,7 +2677,7 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "flashing", + "red": "stop", }, }, "historyValue": Object { @@ -8728,7 +2685,7 @@ Array [ "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "stop", "states": Object { "flashing": undefined, "stop": undefined, @@ -8739,13 +2696,119 @@ Array [ "yellow": undefined, }, }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", + "matches": [Function], + "meta": Object {}, + "tags": Array [], + "toStrings": [Function], + "value": "green", + }, + }, + ], + "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", + }, + }, + }, + Object { + "paths": Array [ + Object { + "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", + }, + }, + "steps": Array [ Object { "event": Object { "type": "TIMER", @@ -8754,48 +2817,60 @@ Array [ "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "events": Array [], - "historyValue": Object { - "current": "yellow", - "states": Object { - "green": undefined, - "red": Object { - "current": "walk", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, - }, + "history": Object { + "_event": Object { + "$$type": "scxml", + "data": Object { + "type": "xstate.init", }, - "yellow": undefined, + "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": undefined, "matches": [Function], "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": "yellow", + "value": "green", }, }, Object { "event": Object { - "type": "PED_COUNTDOWN", + "type": "TIMER", }, "state": Object { "_event": Object { @@ -8818,9 +2893,7 @@ Array [ }, "events": Array [], "historyValue": Object { - "current": Object { - "red": "walk", - }, + "current": "yellow", "states": Object { "green": undefined, "red": Object { @@ -8839,48 +2912,87 @@ Array [ "meta": Object {}, "tags": Array [], "toStrings": [Function], - "value": Object { - "red": "walk", - }, + "value": "yellow", }, }, Object { "event": Object { - "type": "POWER_OUTAGE", + "type": "PED_COUNTDOWN", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "PED_COUNTDOWN", + "type": "PUSH_BUTTON", }, - "name": "PED_COUNTDOWN", + "name": "PUSH_BUTTON", "type": "external", }, "_sessionid": null, - "actions": Array [ - Object { - "exec": undefined, - "type": "startCountdown", - }, - ], + "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "PED_COUNTDOWN", + "type": "PUSH_BUTTON", }, "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": "wait", + "red": "walk", }, "states": Object { "green": undefined, "red": Object { - "current": "wait", + "current": "walk", "states": Object { "flashing": undefined, "stop": undefined, @@ -8896,90 +3008,97 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "wait", + "red": "walk", }, }, }, ], - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "POWER_OUTAGE", + "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, }, - "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", + "yellow": undefined, + }, + }, + "matches": [Function], + "meta": Object {}, + "tags": Array [], + "toStrings": [Function], + "value": Object { + "red": "wait", + }, + }, + }, + Object { + "paths": Array [ + Object { + "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": "flashing", + "red": "stop", }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "stop", "states": Object { "flashing": undefined, "stop": undefined, @@ -8995,13 +3114,10 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "flashing", + "red": "stop", }, }, - "weight": 4, - }, - Object { - "segments": Array [ + "steps": Array [ Object { "event": Object { "type": "TIMER", @@ -9010,84 +3126,50 @@ Array [ "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "events": Array [], "history": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "xstate.init", }, - "name": "POWER_OUTAGE", + "name": "xstate.init", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": undefined, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "xstate.init", }, "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, - }, - }, + "historyValue": 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, - }, + "value": "green", }, + "historyValue": undefined, "matches": [Function], "meta": Object {}, "tags": Array [], @@ -9144,183 +3226,35 @@ 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 [], - "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", + "type": "PED_COUNTDOWN", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, - "name": "TIMER", + "name": "PUSH_BUTTON", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, "events": Array [], "history": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "TIMER", }, - "name": "POWER_OUTAGE", + "name": "TIMER", "type": "external", }, "_sessionid": null, @@ -9331,84 +3265,39 @@ Array [ "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "TIMER", }, "events": Array [], "historyValue": Object { "current": Object { - "red": "flashing", + "red": "walk", }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "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": "flashing", - }, - }, - "historyValue": Object { - "current": "green", - "states": Object { - "green": undefined, - "red": Object { - "current": "flashing", - "states": Object { - "flashing": undefined, - "stop": undefined, - "wait": undefined, - "walk": undefined, + "flashing": undefined, + "stop": undefined, + "wait": undefined, + "walk": undefined, + }, }, + "yellow": 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", + "matches": [Function], + "meta": Object {}, + "tags": Array [], + "toStrings": [Function], + "value": Object { + "red": "walk", }, - "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", + "current": Object { + "red": "walk", + }, "states": Object { "green": undefined, "red": Object { @@ -9427,154 +3316,68 @@ Array [ "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, - }, + "value": Object { + "red": "walk", }, - "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", + "type": "PED_COUNTDOWN", }, "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, - "name": "TIMER", + "name": "PUSH_BUTTON", "type": "external", }, "_sessionid": null, "actions": Array [], "activities": Object {}, - "changed": true, + "changed": false, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PUSH_BUTTON", }, "events": Array [], "history": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, - "name": "TIMER", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, - "actions": Array [], + "actions": Array [ + Object { + "exec": undefined, + "type": "startCountdown", + }, + ], "activities": Object {}, "changed": true, "children": Object {}, "context": undefined, "done": false, "event": Object { - "type": "TIMER", + "type": "PED_COUNTDOWN", }, "events": Array [], "historyValue": Object { "current": Object { - "red": "walk", + "red": "wait", }, "states": Object { "green": undefined, "red": Object { - "current": "walk", + "current": "wait", "states": Object { "flashing": undefined, "stop": undefined, @@ -9590,15 +3393,17 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "walk", + "red": "wait", }, }, "historyValue": Object { - "current": "green", + "current": Object { + "red": "wait", + }, "states": Object { "green": undefined, "red": Object { - "current": "walk", + "current": "wait", "states": Object { "flashing": undefined, "stop": undefined, @@ -9613,93 +3418,22 @@ Array [ "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, - }, + "value": Object { + "red": "wait", }, - "yellow": undefined, }, }, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": Object { - "red": "flashing", - }, - }, - "weight": 1, + ], + "weight": 4, }, ], "state": Object { "_event": Object { "$$type": "scxml", "data": Object { - "type": "POWER_OUTAGE", + "type": "PED_COUNTDOWN", }, - "name": "POWER_OUTAGE", + "name": "PED_COUNTDOWN", "type": "external", }, "_sessionid": null, @@ -9710,17 +3444,17 @@ Array [ "context": undefined, "done": false, "event": Object { - "type": "POWER_OUTAGE", + "type": "PED_COUNTDOWN", }, "events": Array [], "historyValue": Object { "current": Object { - "red": "flashing", + "red": "stop", }, "states": Object { "green": undefined, "red": Object { - "current": "flashing", + "current": "stop", "states": Object { "flashing": undefined, "stop": undefined, @@ -9736,7 +3470,7 @@ Array [ "tags": Array [], "toStrings": [Function], "value": Object { - "red": "flashing", + "red": "stop", }, }, }, diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 11ee4121ef..447f0c09fe 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -1,10 +1,11 @@ -import { Machine, StateNode, createMachine } from 'xstate'; +import { Machine, StateNode, createMachine, State, EventObject } from 'xstate'; import { getStateNodes, getPathFromEvents, getSimplePaths, getShortestPaths, - toDirectedGraph + toDirectedGraph, + StatePathsMap } from '../src/index'; import { getSimplePathsAsArray, @@ -12,6 +13,21 @@ import { depthSimplePaths } from '../src/graph'; import { assign } from 'xstate'; +import { mapValues } from 'xstate/lib/utils'; + +function getPathsMapSnapshot( + pathsMap: StatePathsMap, EventObject> +): Record { + return mapValues(pathsMap, (plan) => { + return plan.paths.map((path) => ({ + state: path.state.value, + steps: path.steps.map((step) => ({ + state: step.state.value, + type: step.event.type + })) + })); + }); +} describe('@xstate/graph', () => { const pedestrianStates = { @@ -164,12 +180,14 @@ describe('@xstate/graph', () => { it('should return a mapping of shortest paths to all states', () => { const paths = getShortestPaths(lightMachine) as any; - 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'); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'shortest paths parallel' + ); }); it('the initial state should have a zero-length path', () => { @@ -207,18 +225,31 @@ describe('@xstate/graph', () => { } ); - 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; - - expect(paths).toMatchSnapshot('simple paths'); + const paths = getSimplePaths(lightMachine); + + expect(Object.keys(paths)).toMatchInlineSnapshot(` + Array [ + "\\"green\\"", + "\\"yellow\\"", + "{\\"red\\":\\"walk\\"}", + "{\\"red\\":\\"flashing\\"}", + "{\\"red\\":\\"wait\\"}", + "{\\"red\\":\\"stop\\"}", + ] + `); + + expect(getPathsMapSnapshot(paths)).toMatchSnapshot(); }); - const equivMachine = Machine({ + const equivMachine = createMachine({ initial: 'a', states: { a: { on: { FOO: 'b', BAR: 'b' } }, @@ -228,12 +259,16 @@ 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'); + 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'); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'simple paths equal transitions' + ); }); it('should return a single empty path for the initial state', () => { @@ -255,7 +290,7 @@ describe('@xstate/graph', () => { type: 'INC'; value: number; } - const countMachine = Machine({ + const countMachine = createMachine({ id: 'count', initial: 'start', context: { @@ -279,13 +314,13 @@ describe('@xstate/graph', () => { } }); - const paths = getSimplePaths(countMachine, { - events: { - INC: [{ type: 'INC', value: 1 }] - } - }); + const paths = getSimplePaths(countMachine as any, [ + { type: 'INC', value: 1 } + ]); - expect(paths).toMatchSnapshot('simple paths context'); + expect(getPathsMapSnapshot(paths)).toMatchSnapshot( + 'simple paths context' + ); }); }); @@ -437,24 +472,24 @@ describe('@xstate/graph', () => { }); }); -it.only('hmm', () => { +it('hmm', () => { const a = depthSimplePaths( (s, e) => { - if (e === 'a') { + if (e.type === 'a') { return 1; } - if (e === 'b' && s === 1) { + if (e.type === 'b' && s === 1) { return 2; } - if (e === 'reset') { + if (e.type === 'reset') { return 0; } return s; }, 0 as number, - ['a', 'b', 'reset'], + [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], { - serializeVertex: (v, e) => JSON.stringify(v) + JSON.stringify(e) + serializeState: (v, e) => (JSON.stringify(v) + JSON.stringify(e)) as any } ); From ad78c347af427549b8a6d5ea2f733fdd33696a5f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 28 Dec 2021 14:16:07 -0500 Subject: [PATCH 008/394] Generalize functions --- packages/xstate-graph/src/graph.ts | 176 ++-- .../test/__snapshots__/graph.test.ts.snap | 750 +++++++++--------- packages/xstate-graph/test/graph.test.ts | 119 ++- 3 files changed, 538 insertions(+), 507 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index f3261789f7..686a36fe21 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -11,7 +11,6 @@ import { flatten, keys } from 'xstate/lib/utils'; import { SerializedEvent, SerializedState, StatePath } from '.'; import { StatePathsMap, - StatePaths, AdjacencyMap, Steps, ValueAdjMapOptions, @@ -29,8 +28,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 @@ -180,28 +177,46 @@ export function getShortestPaths< TEvent extends EventObject = EventObject >( machine: StateMachine, - options?: ValueAdjMapOptions, TEvent> -): StatePathsMap, TEvent> { - if (!machine.states) { - return EMPTY_MAP; + _events: TEvent[] = machine.events.map((type) => ({ type })) as TEvent[], + options: TraversalOptions, TEvent> = { + serializeState } - const optionsWithDefaults = getValueAdjMapOptions(options); +): StatePathsMap, TEvent> { + return depthShortestPaths( + (state, event) => machine.transition(state, event), + machine.initialState, + _events, + options + ); +} - const adjacency = getAdjacencyMap(machine, optionsWithDefaults); +export function depthShortestPaths( + reducer: (state: TState, event: TEvent) => TState, + initialState: TState, + events: TEvent[], + options?: TraversalOptions +): StatePathsMap { + const optionsWithDefaults = resolveTraversalOptions(options); + const { serializeState } = optionsWithDefaults; + + const adjacency = depthFirstTraversal( + reducer, + initialState, + events, + optionsWithDefaults + ); // weight, state, event const weightMap = new Map< - string, - [number, string | undefined, string | undefined] + SerializedState, + [number, SerializedState | undefined, SerializedEvent | undefined] >(); - const stateMap = new Map>(); - const initialVertex = optionsWithDefaults.stateSerializer( - machine.initialState - ); - stateMap.set(initialVertex, machine.initialState); + const stateMap = new Map(); + const initialVertex = serializeState(initialState, null); + stateMap.set(initialVertex, initialState); weightMap.set(initialVertex, [0, undefined, undefined]); - const unvisited = new Set(); + const unvisited = new Set(); const visited = new Set(); unvisited.add(initialVertex); @@ -209,11 +224,10 @@ export function getShortestPaths< for (const vertex of unvisited) { const [weight] = weightMap.get(vertex)!; for (const event of keys(adjacency[vertex])) { - const nextSegment = adjacency[vertex][event]; - const nextVertex = optionsWithDefaults.stateSerializer( - nextSegment.state - ); - stateMap.set(nextVertex, nextSegment.state); + const eventObject = JSON.parse(event); + const nextState = adjacency[vertex][event]; + const nextVertex = serializeState(nextState, eventObject); + stateMap.set(nextVertex, nextState); if (!weightMap.has(nextVertex)) { weightMap.set(nextVertex, [weight + 1, vertex, event]); } else { @@ -231,7 +245,7 @@ export function getShortestPaths< } } - const statePathMap: StatePathsMap, TEvent> = {}; + const statePathMap: StatePathsMap = {}; weightMap.forEach(([weight, fromState, fromEvent], stateSerial) => { const state = stateMap.get(stateSerial)!; @@ -267,90 +281,16 @@ export function getSimplePaths< >( machine: StateMachine, _events: TEvent[] = machine.events.map((type) => ({ type })) as TEvent[], - options?: ValueAdjMapOptions, TEvent> + options: TraversalOptions, TEvent> = { + serializeState + } ): StatePathsMap, TEvent> { return depthSimplePaths( (state, event) => machine.transition(state, event), machine.initialState, _events, - { - serializeState: serializeState as any - } + options ); - const optionsWithDefaults = getValueAdjMapOptions(options); - - const { stateSerializer } = optionsWithDefaults; - - if (!machine.states) { - return EMPTY_MAP; - } - - const adjacency = getAdjacencyMap(machine, optionsWithDefaults); - const stateMap = new Map>(); - const visited = new Set(); - const path: Steps, TEvent> = []; - const paths: StatePathsMap, TEvent> = {}; - - 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, - steps: [...path] - }); - } else { - for (const subEvent of 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 keys(adjacency)) { - util(machine.initialState, nextStateSerial); - } - - return paths; -} - -export function getSimplePathsAsArray< - TContext = DefaultContext, - TEvent extends EventObject = EventObject ->( - machine: StateNode, - options?: ValueAdjMapOptions, TEvent> -): Array, TEvent>> { - const result = getSimplePaths(machine, undefined, options); - return keys(result).map((key) => result[key]); } export function toDirectedGraph(stateNode: StateNode): DirectedGraphNode { @@ -467,6 +407,7 @@ interface AdjMap { interface TraversalOptions { serializeState?: (vertex: V, edge: E | null) => SerializedState; visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; + shortest?: boolean; } export function depthFirstTraversal( @@ -506,14 +447,17 @@ interface VisitedContext { } function resolveTraversalOptions( - depthOptions: TraversalOptions + depthOptions?: TraversalOptions ): Required> { const serializeState = - depthOptions.serializeState ?? ((state) => JSON.stringify(state) as any); + depthOptions?.serializeState ?? ((state) => JSON.stringify(state) as any); + const shortest = !!depthOptions?.shortest; return { + shortest, serializeState, - visitCondition: (state, event, vctx) => - vctx.vertices.has(serializeState(state, event)), + visitCondition: (state, event, vctx) => { + return shortest ? false : vctx.vertices.has(serializeState(state, event)); + }, ...depthOptions }; } @@ -539,28 +483,36 @@ export function depthSimplePaths( edges: new Set() }; const path: any[] = []; - const paths: Record = {}; + const pathMap: Record< + SerializedState, + { state: TState; paths: Array> } + > = {}; function util( fromState: TState, - toStateSerial: string, + toStateSerial: SerializedState, event: TEvent | null ) { const fromStateSerial = serializeState(fromState, event); visitCtx.vertices.add(fromStateSerial); if (fromStateSerial === toStateSerial) { - if (!paths[toStateSerial]) { - paths[toStateSerial] = { + if (!pathMap[toStateSerial]) { + pathMap[toStateSerial] = { state: stateMap.get(toStateSerial)!, paths: [] }; } - paths[toStateSerial].paths.push({ + + const toStatePlan = pathMap[toStateSerial]; + + const path2: StatePath = { state: fromState, weight: path.length, steps: [...path] - }); + }; + + toStatePlan.paths.push(path2); } else { for (const serializedEvent of keys(adjacency[fromStateSerial])) { const subEvent = JSON.parse(serializedEvent); @@ -595,5 +547,5 @@ export function depthSimplePaths( util(initialState, nextStateSerial, null); } - return paths; + return pathMap; } diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index a5663b8769..feb2d85290 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -3,374 +3,28 @@ exports[`@xstate/graph getPathFromEvents() should return a path to the last entered state by the event sequence: path from events 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 [], - "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", - }, + "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", - }, + "state": "green", + "type": "TIMER", }, 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", - }, + "state": "yellow", + "type": "TIMER", }, 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": "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", }, + "type": "TIMER", }, 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": "green", + "type": "POWER_OUTAGE", }, ], - "weight": 4, } `; @@ -3569,3 +3223,389 @@ Object { "id": "light", } `; + +exports[`shortest paths for reducers 1`] = ` +Object { + "0 | null": Array [ + Object { + "state": 0, + "steps": Array [], + }, + ], + "0 | {\\"type\\":\\"b\\"}": Array [ + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + ], + }, + ], + "0 | {\\"type\\":\\"reset\\"}": Array [ + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "reset", + }, + ], + }, + ], + "1 | {\\"type\\":\\"a\\"}": Array [ + Object { + "state": 1, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + ], + }, + ], + "2 | {\\"type\\":\\"b\\"}": Array [ + Object { + "state": 2, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + ], + }, + ], +} +`; + +exports[`simple paths for reducers 1`] = ` +Object { + "0 | null": Array [ + Object { + "state": 0, + "steps": Array [], + }, + ], + "0 | {\\"type\\":\\"b\\"}": Array [ + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + Object { + "state": 2, + "type": "reset", + }, + Object { + "state": 0, + "type": "b", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "reset", + }, + Object { + "state": 0, + "type": "b", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "reset", + }, + Object { + "state": 0, + "type": "b", + }, + ], + }, + ], + "0 | {\\"type\\":\\"reset\\"}": Array [ + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + Object { + "state": 2, + "type": "reset", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "reset", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + Object { + "state": 2, + "type": "reset", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "reset", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "reset", + }, + ], + }, + Object { + "state": 0, + "steps": Array [ + Object { + "state": 0, + "type": "reset", + }, + ], + }, + ], + "1 | {\\"type\\":\\"a\\"}": Array [ + Object { + "state": 1, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "a", + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "reset", + }, + Object { + "state": 0, + "type": "a", + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "state": 0, + "type": "reset", + }, + Object { + "state": 0, + "type": "a", + }, + ], + }, + Object { + "state": 1, + "steps": Array [ + Object { + "state": 0, + "type": "reset", + }, + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "a", + }, + ], + }, + ], + "2 | {\\"type\\":\\"b\\"}": Array [ + Object { + "state": 2, + "steps": Array [ + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "reset", + }, + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "state": 0, + "type": "reset", + }, + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + ], + }, + Object { + "state": 2, + "steps": Array [ + Object { + "state": 0, + "type": "reset", + }, + Object { + "state": 0, + "type": "b", + }, + Object { + "state": 0, + "type": "a", + }, + Object { + "state": 1, + "type": "b", + }, + ], + }, + ], +} +`; diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 447f0c09fe..b659eb4931 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -5,30 +5,35 @@ import { getSimplePaths, getShortestPaths, toDirectedGraph, - StatePathsMap + StatePathsMap, + StatePath } from '../src/index'; import { - getSimplePathsAsArray, getAdjacencyMap, - depthSimplePaths + depthSimplePaths, + depthShortestPaths } from '../src/graph'; import { assign } from 'xstate'; import { mapValues } from 'xstate/lib/utils'; function getPathsMapSnapshot( - pathsMap: StatePathsMap, EventObject> + pathsMap: StatePathsMap ): Record { return mapValues(pathsMap, (plan) => { - return plan.paths.map((path) => ({ - state: path.state.value, - steps: path.steps.map((step) => ({ - state: step.state.value, - type: step.event.type - })) - })); + return plan.paths.map(getPathSnapshot); }); } +function getPathSnapshot(path: StatePath) { + 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, + type: step.event.type + })) + }; +} + describe('@xstate/graph', () => { const pedestrianStates = { initial: 'walk', @@ -114,7 +119,7 @@ describe('@xstate/graph', () => { } }); - const parallelMachine = Machine({ + const parallelMachine = createMachine({ type: 'parallel', key: 'p', states: { @@ -208,21 +213,15 @@ describe('@xstate/graph', () => { condMachine.withContext({ id: 'foo' }), - { - events: { - EVENT: [ - { - type: 'EVENT', - id: 'whatever' - } - ], - STATE: [ - { - type: 'STATE' - } - ] + [ + { + type: 'EVENT', + id: 'whatever' + }, + { + type: 'STATE' } - } + ] as any[] ); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -259,6 +258,14 @@ describe('@xstate/graph', () => { it('should return a mapping of simple paths to all states (parallel)', () => { const paths = getSimplePaths(parallelMachine); + + expect(Object.keys(paths)).toMatchInlineSnapshot(` + Array [ + "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}", + "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}", + "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}", + ] + `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( 'simple paths parallel' ); @@ -266,6 +273,13 @@ describe('@xstate/graph', () => { it('should return multiple paths for equivalent transitions', () => { const paths = getSimplePaths(equivMachine); + + expect(Object.keys(paths)).toMatchInlineSnapshot(` + Array [ + "\\"a\\"", + "\\"b\\"", + ] + `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( 'simple paths equal transitions' ); @@ -318,21 +332,20 @@ describe('@xstate/graph', () => { { type: 'INC', value: 1 } ]); + expect(Object.keys(paths)).toMatchInlineSnapshot(` + Array [ + "\\"start\\" | {\\"count\\":0}", + "\\"start\\" | {\\"count\\":1}", + "\\"start\\" | {\\"count\\":2}", + "\\"finish\\" | {\\"count\\":3}", + ] + `); expect(getPathsMapSnapshot(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'); - }); - }); - describe('getPathFromEvents()', () => { it('should return a path to the last entered state by the event sequence', () => { const path = getPathFromEvents(lightMachine, [ @@ -342,7 +355,7 @@ 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', () => { @@ -363,7 +376,7 @@ describe('@xstate/graph', () => { } type Events = { type: 'INC'; value: number } | { type: 'DEC' }; - const counterMachine = Machine({ + const counterMachine = createMachine({ id: 'counter', initial: 'empty', context: { @@ -472,7 +485,7 @@ describe('@xstate/graph', () => { }); }); -it('hmm', () => { +it('simple paths for reducers', () => { const a = depthSimplePaths( (s, e) => { if (e.type === 'a') { @@ -489,9 +502,35 @@ it('hmm', () => { 0 as number, [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], { - serializeState: (v, e) => (JSON.stringify(v) + JSON.stringify(e)) as any + serializeState: (v, e) => + (JSON.stringify(v) + ' | ' + JSON.stringify(e)) as any + } + ); + + expect(getPathsMapSnapshot(a)).toMatchSnapshot(); +}); + +it('shortest paths for reducers', () => { + const a = depthShortestPaths( + (s, e) => { + if (e.type === 'a') { + return 1; + } + if (e.type === 'b' && s === 1) { + return 2; + } + if (e.type === 'reset') { + return 0; + } + return s; + }, + 0 as number, + [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], + { + serializeState: (v, e) => + (JSON.stringify(v) + ' | ' + JSON.stringify(e)) as any } ); - console.log(JSON.stringify(a, null, 2)); + expect(getPathsMapSnapshot(a)).toMatchSnapshot(); }); From f4c15c897778f06118ce78bee6d7f047ec69ff71 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 30 Dec 2021 09:01:13 -0500 Subject: [PATCH 009/394] Fix filtering --- packages/xstate-graph/src/graph.ts | 48 ++++++++++------------ packages/xstate-graph/src/types.ts | 12 ++++++ packages/xstate-graph/test/graph.test.ts | 34 ++++++++++++++++ packages/xstate-test/src/index.ts | 52 +++++++++++++++--------- packages/xstate-test/test/index.test.ts | 15 ++++--- 5 files changed, 108 insertions(+), 53 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 686a36fe21..62462b8980 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -15,7 +15,9 @@ import { Steps, ValueAdjMapOptions, DirectedGraphEdge, - DirectedGraphNode + DirectedGraphNode, + TraversalOptions, + VisitedContext } from './types'; export function toEventObject( @@ -172,21 +174,23 @@ export function getAdjacencyMap< return adjacency; } +const defaultMachineStateOptions: TraversalOptions, any> = { + serializeState +}; + export function getShortestPaths< TContext = DefaultContext, TEvent extends EventObject = EventObject >( machine: StateMachine, _events: TEvent[] = machine.events.map((type) => ({ type })) as TEvent[], - options: TraversalOptions, TEvent> = { - serializeState - } + options?: TraversalOptions, TEvent> ): StatePathsMap, TEvent> { return depthShortestPaths( (state, event) => machine.transition(state, event), machine.initialState, _events, - options + { ...defaultMachineStateOptions, ...options } ); } @@ -223,6 +227,9 @@ export function depthShortestPaths( while (unvisited.size > 0) { for (const vertex of unvisited) { const [weight] = weightMap.get(vertex)!; + if (!adjacency[vertex]) { + console.log('not found', vertex); + } for (const event of keys(adjacency[vertex])) { const eventObject = JSON.parse(event); const nextState = adjacency[vertex][event]; @@ -281,15 +288,13 @@ export function getSimplePaths< >( machine: StateMachine, _events: TEvent[] = machine.events.map((type) => ({ type })) as TEvent[], - options: TraversalOptions, TEvent> = { - serializeState - } + options?: TraversalOptions, TEvent> ): StatePathsMap, TEvent> { return depthSimplePaths( (state, event) => machine.transition(state, event), machine.initialState, _events, - options + { ...defaultMachineStateOptions, ...options } ); } @@ -404,19 +409,13 @@ interface AdjMap { [key: SerializedState]: { [key: SerializedEvent]: TState }; } -interface TraversalOptions { - serializeState?: (vertex: V, edge: E | null) => SerializedState; - visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; - shortest?: boolean; -} - export function depthFirstTraversal( reducer: (state: TState, event: TEvent) => TState, initialState: TState, events: TEvent[], options: TraversalOptions ): AdjMap { - const { serializeState: serializeState } = resolveTraversalOptions(options); + const { serializeState } = resolveTraversalOptions(options); const adj: AdjMap = {}; function util(state: TState, event: TEvent | null) { @@ -429,9 +428,11 @@ export function depthFirstTraversal( for (const subEvent of events) { const nextState = reducer(state, subEvent); - adj[serializedState][JSON.stringify(subEvent)] = nextState; - util(nextState, subEvent); + if (!options.filter || options.filter(nextState, subEvent)) { + adj[serializedState][JSON.stringify(subEvent)] = nextState; + util(nextState, subEvent); + } } } @@ -440,23 +441,16 @@ export function depthFirstTraversal( return adj; } -interface VisitedContext { - vertices: Set; - edges: Set; - a?: V | E; // TODO: remove -} - function resolveTraversalOptions( depthOptions?: TraversalOptions ): Required> { const serializeState = depthOptions?.serializeState ?? ((state) => JSON.stringify(state) as any); - const shortest = !!depthOptions?.shortest; return { - shortest, serializeState, + filter: () => true, visitCondition: (state, event, vctx) => { - return shortest ? false : vctx.vertices.has(serializeState(state, event)); + return vctx.vertices.has(serializeState(state, event)); }, ...depthOptions }; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index ff390da6c2..bfed1fca4f 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -116,6 +116,18 @@ export interface ValueAdjMapOptions { eventSerializer?: (event: TEvent) => string; } +export interface VisitedContext { + vertices: Set; + edges: Set; + a?: TState | TEvent; // TODO: remove +} + +export interface TraversalOptions { + serializeState?: (vertex: V, edge: E | null) => SerializedState; + visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; + filter?: (vertex: V, edge: E) => boolean; +} + type Brand = T & { __tag: Tag }; export type SerializedState = Brand; diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index b659eb4931..4016c60416 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -534,3 +534,37 @@ it('shortest paths for reducers', () => { 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 = getShortestPaths(machine, [{ type: 'INC' }], { + filter: (s) => s.context.count < 5 + }); + + expect(Object.keys(sp)).toMatchInlineSnapshot(` + Array [ + "\\"counting\\" | {\\"count\\":0}", + "\\"counting\\" | {\\"count\\":1}", + "\\"counting\\" | {\\"count\\":2}", + "\\"counting\\" | {\\"count\\":3}", + "\\"counting\\" | {\\"count\\":4}", + ] + `); + }); +}); diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 935cda5ad6..6cf425025c 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -3,8 +3,9 @@ import { getShortestPaths, getSimplePaths, getStateNodes, + serializeState, StatePathsMap, - ValueAdjMapOptions + TraversalOptions } from '@xstate/graph'; import { StateMachine, EventObject, State, StateValue } from 'xstate'; import slimChalk from './slimChalk'; @@ -61,12 +62,16 @@ export class TestModel { } public getShortestPathPlans( - options?: Partial> + options?: TraversalOptions, any> ): Array> { - const shortestPaths = getShortestPaths(this.machine, { - ...options, - events: getEventSamples(this.options.events) - }) as StatePathsMap; + const shortestPaths = getShortestPaths( + this.machine, + getEventSamples(this.options.events), + { + serializeState, + ...options + } + ); return this.getTestPlans(shortestPaths); } @@ -128,12 +133,16 @@ export class TestModel { } public getSimplePathPlans( - options?: Partial> + options?: Partial, any>> ): Array> { - const simplePaths = getSimplePaths(this.machine, { - ...options, - events: getEventSamples(this.options.events) - }) as StatePathsMap; + const simplePaths = getSimplePaths( + this.machine, + getEventSamples(this.options.events), + { + serializeState, + ...options + } + ); return this.getTestPlans(simplePaths); } @@ -145,7 +154,7 @@ export class TestModel { } public getTestPlans( - statePathsMap: StatePathsMap + statePathsMap: StatePathsMap, any> ): Array> { return Object.keys(statePathsMap).map((key) => { const testPlan = statePathsMap[key]; @@ -403,20 +412,21 @@ function getDescription(state: State): string { ); } -function getEventSamples(eventsOptions: TestModelOptions['events']) { - const result = {}; +export function getEventSamples( + eventsOptions: TestModelOptions['events'] +) { + const result: T[] = []; Object.keys(eventsOptions).forEach((key) => { const eventConfig = eventsOptions[key]; if (typeof eventConfig === 'function') { - return [ - { - type: key - } - ]; + result.push({ + type: key + } as any); + return; } - result[key] = eventConfig.cases + const events = eventConfig.cases ? eventConfig.cases.map((sample) => ({ type: key, ...sample @@ -426,6 +436,8 @@ function getEventSamples(eventsOptions: TestModelOptions['events']) { type: key } ]; + + result.push(...(events as any[])); }); return result; diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 2d6e27f4e7..1ae5199837 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -380,9 +380,7 @@ describe('coverage', () => { } }, passthrough: { - on: { - '': 'end' - } + always: 'end' }, end: { type: 'final', @@ -430,7 +428,7 @@ describe('events', () => { | { type: 'CLOSE' } | { type: 'ESC' } | { type: 'SUBMIT'; value: string }; - const feedbackMachine = Machine({ + const feedbackMachine = Machine({ id: 'feedback', initial: 'question', states: { @@ -571,7 +569,9 @@ describe('state limiting', () => { } }); - const testModel = createModel(machine); + const testModel = createModel(machine).withEvents({ + INC: () => {} + }); const testPlans = testModel.getShortestPathPlans({ filter: (state) => { return state.context.count < 5; @@ -632,7 +632,10 @@ describe('plan description', () => { } }); - const testModel = createModel(machine); + const testModel = createModel(machine).withEvents({ + NEXT: { exec: () => {} }, + DONE: { exec: () => {} } + }); const testPlans = testModel.getShortestPathPlans(); it('should give a description for every plan', () => { From 2efc2b7f010273f7599f4a73190184c972743e0c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 30 Dec 2021 22:40:28 -0500 Subject: [PATCH 010/394] Reduce arguments --- packages/xstate-graph/src/graph.ts | 46 +- packages/xstate-graph/src/types.ts | 13 +- .../test/__snapshots__/graph.test.ts.snap | 2585 +---------------- packages/xstate-graph/test/graph.test.ts | 36 +- packages/xstate-test/src/index.ts | 16 +- 5 files changed, 74 insertions(+), 2622 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 62462b8980..5f62f4d527 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -175,7 +175,10 @@ export function getAdjacencyMap< } const defaultMachineStateOptions: TraversalOptions, any> = { - serializeState + serializeState, + getEvents: (state) => { + return state.nextEvents.map((type) => ({ type })); + } }; export function getShortestPaths< @@ -183,21 +186,22 @@ export function getShortestPaths< TEvent extends EventObject = EventObject >( machine: StateMachine, - _events: TEvent[] = machine.events.map((type) => ({ type })) as TEvent[], options?: TraversalOptions, TEvent> ): StatePathsMap, TEvent> { + const resolvedOptions = resolveTraversalOptions( + options, + defaultMachineStateOptions + ); return depthShortestPaths( (state, event) => machine.transition(state, event), machine.initialState, - _events, - { ...defaultMachineStateOptions, ...options } + resolvedOptions ); } export function depthShortestPaths( reducer: (state: TState, event: TEvent) => TState, initialState: TState, - events: TEvent[], options?: TraversalOptions ): StatePathsMap { const optionsWithDefaults = resolveTraversalOptions(options); @@ -206,7 +210,6 @@ export function depthShortestPaths( const adjacency = depthFirstTraversal( reducer, initialState, - events, optionsWithDefaults ); @@ -287,14 +290,17 @@ export function getSimplePaths< TEvent extends EventObject = EventObject >( machine: StateMachine, - _events: TEvent[] = machine.events.map((type) => ({ type })) as TEvent[], options?: TraversalOptions, TEvent> ): StatePathsMap, TEvent> { + const resolvedOptions = resolveTraversalOptions( + options, + defaultMachineStateOptions + ); + return depthSimplePaths( (state, event) => machine.transition(state, event), machine.initialState, - _events, - { ...defaultMachineStateOptions, ...options } + resolvedOptions ); } @@ -412,10 +418,9 @@ interface AdjMap { export function depthFirstTraversal( reducer: (state: TState, event: TEvent) => TState, initialState: TState, - events: TEvent[], options: TraversalOptions ): AdjMap { - const { serializeState } = resolveTraversalOptions(options); + const { serializeState, getEvents } = resolveTraversalOptions(options); const adj: AdjMap = {}; function util(state: TState, event: TEvent | null) { @@ -426,6 +431,8 @@ export function depthFirstTraversal( adj[serializedState] = {}; + const events = getEvents(state); + for (const subEvent of events) { const nextState = reducer(state, subEvent); @@ -442,16 +449,21 @@ export function depthFirstTraversal( } function resolveTraversalOptions( - depthOptions?: TraversalOptions + depthOptions?: TraversalOptions, + defaultOptions?: TraversalOptions ): Required> { const serializeState = - depthOptions?.serializeState ?? ((state) => JSON.stringify(state) as any); + depthOptions?.serializeState ?? + defaultOptions?.serializeState ?? + ((state) => JSON.stringify(state) as any); return { serializeState, filter: () => true, visitCondition: (state, event, vctx) => { return vctx.vertices.has(serializeState(state, event)); }, + getEvents: () => [], + ...defaultOptions, ...depthOptions }; } @@ -459,17 +471,11 @@ function resolveTraversalOptions( export function depthSimplePaths( reducer: (state: TState, event: TEvent) => TState, initialState: TState, - events: TEvent[], options: TraversalOptions ): StatePathsMap { const resolvedOptions = resolveTraversalOptions(options); const { serializeState, visitCondition } = resolvedOptions; - const adjacency = depthFirstTraversal( - reducer, - initialState, - events, - resolvedOptions - ); + const adjacency = depthFirstTraversal(reducer, initialState, resolvedOptions); const stateMap = new Map(); // const visited = new Set(); const visitCtx: VisitedContext = { diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index bfed1fca4f..9a6d6ddc74 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -122,10 +122,15 @@ export interface VisitedContext { a?: TState | TEvent; // TODO: remove } -export interface TraversalOptions { - serializeState?: (vertex: V, edge: E | null) => SerializedState; - visitCondition?: (vertex: V, edge: E, vctx: VisitedContext) => boolean; - filter?: (vertex: V, edge: E) => boolean; +export interface TraversalOptions { + serializeState?: (state: TState, event: TEvent | null) => SerializedState; + visitCondition?: ( + state: TState, + event: TEvent, + vctx: VisitedContext + ) => boolean; + filter?: (state: TState, event: TEvent) => boolean; + getEvents?: (state: TState) => TEvent[]; } type Brand = T & { __tag: Tag }; diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index feb2d85290..a8bb66e895 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -250,6 +250,18 @@ Object { "state": Object { "red": "walk", }, + "type": "PED_COUNTDOWN", + }, + Object { + "state": Object { + "red": "wait", + }, + "type": "PED_COUNTDOWN", + }, + Object { + "state": Object { + "red": "stop", + }, "type": "POWER_OUTAGE", }, ], @@ -298,18 +310,6 @@ Object { "state": Object { "red": "walk", }, - "type": "PED_COUNTDOWN", - }, - Object { - "state": Object { - "red": "wait", - }, - "type": "PED_COUNTDOWN", - }, - Object { - "state": Object { - "red": "stop", - }, "type": "POWER_OUTAGE", }, ], @@ -570,2567 +570,6 @@ Object { } `; -exports[`@xstate/graph getSimplePathsAsArray() should return an array of shortest paths to all states: simple paths array 1`] = ` -Array [ - Object { - "paths": Array [ - Object { - "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 [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - "steps": Array [], - "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 [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - Object { - "paths": 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", - }, - "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 [], - "historyValue": undefined, - "matches": [Function], - "meta": Object {}, - "tags": Array [], - "toStrings": [Function], - "value": "green", - }, - }, - ], - "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 { - "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", - }, - }, - "steps": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "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": 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", - }, - }, - ], - "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 { - "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", - }, - }, - "steps": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "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": 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", - }, - }, - }, - ], - "weight": 3, - }, - 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 [], - "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", - }, - }, - "steps": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "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": 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": "PUSH_BUTTON", - }, - "name": "PUSH_BUTTON", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PUSH_BUTTON", - }, - "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": "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", - }, - }, - }, - ], - "weight": 4, - }, - 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 [], - "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": 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", - }, - }, - "steps": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "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": 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": "PUSH_BUTTON", - }, - "name": "PUSH_BUTTON", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PUSH_BUTTON", - }, - "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": "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": "PUSH_BUTTON", - }, - "name": "PUSH_BUTTON", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PUSH_BUTTON", - }, - "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": "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", - }, - }, - }, - ], - "weight": 5, - }, - 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 [], - "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", - }, - }, - "steps": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "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": 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", - }, - }, - ], - "weight": 2, - }, - 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 [], - "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", - }, - }, - "steps": 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", - }, - }, - ], - "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", - }, - }, - }, - Object { - "paths": Array [ - Object { - "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", - }, - }, - "steps": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "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": 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": "PUSH_BUTTON", - }, - "name": "PUSH_BUTTON", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PUSH_BUTTON", - }, - "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": "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": 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 { - "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", - }, - }, - "steps": Array [ - Object { - "event": Object { - "type": "TIMER", - }, - "state": Object { - "_event": Object { - "$$type": "scxml", - "data": Object { - "type": "PED_COUNTDOWN", - }, - "name": "PED_COUNTDOWN", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PED_COUNTDOWN", - }, - "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": 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": "PUSH_BUTTON", - }, - "name": "PUSH_BUTTON", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PUSH_BUTTON", - }, - "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": "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": "PUSH_BUTTON", - }, - "name": "PUSH_BUTTON", - "type": "external", - }, - "_sessionid": null, - "actions": Array [], - "activities": Object {}, - "changed": false, - "children": Object {}, - "context": undefined, - "done": false, - "event": Object { - "type": "PUSH_BUTTON", - }, - "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": "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": 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", - }, - }, - }, -] -`; - exports[`@xstate/graph toDirectedGraph should represent a statechart as a directed graph 1`] = ` Object { "children": Array [ diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 4016c60416..4f3896f1e1 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -213,15 +213,18 @@ describe('@xstate/graph', () => { condMachine.withContext({ id: 'foo' }), - [ - { - type: 'EVENT', - id: 'whatever' - }, - { - type: 'STATE' - } - ] as any[] + { + getEvents: () => + [ + { + type: 'EVENT', + id: 'whatever' + }, + { + type: 'STATE' + } + ] as any[] + } ); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -239,9 +242,9 @@ describe('@xstate/graph', () => { "\\"green\\"", "\\"yellow\\"", "{\\"red\\":\\"walk\\"}", - "{\\"red\\":\\"flashing\\"}", "{\\"red\\":\\"wait\\"}", "{\\"red\\":\\"stop\\"}", + "{\\"red\\":\\"flashing\\"}", ] `); @@ -328,9 +331,9 @@ describe('@xstate/graph', () => { } }); - const paths = getSimplePaths(countMachine as any, [ - { type: 'INC', value: 1 } - ]); + const paths = getSimplePaths(countMachine as any, { + getEvents: () => [{ type: 'INC', value: 1 }] + }); expect(Object.keys(paths)).toMatchInlineSnapshot(` Array [ @@ -500,8 +503,8 @@ it('simple paths for reducers', () => { return s; }, 0 as number, - [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], { + getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], serializeState: (v, e) => (JSON.stringify(v) + ' | ' + JSON.stringify(e)) as any } @@ -525,8 +528,8 @@ it('shortest paths for reducers', () => { return s; }, 0 as number, - [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], { + getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], serializeState: (v, e) => (JSON.stringify(v) + ' | ' + JSON.stringify(e)) as any } @@ -553,7 +556,8 @@ describe('filtering', () => { } }); - const sp = getShortestPaths(machine, [{ type: 'INC' }], { + const sp = getShortestPaths(machine, { + getEvents: () => [{ type: 'INC' }], filter: (s) => s.context.count < 5 }); diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 6cf425025c..f5c9c9c352 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -64,14 +64,11 @@ export class TestModel { public getShortestPathPlans( options?: TraversalOptions, any> ): Array> { - const shortestPaths = getShortestPaths( - this.machine, - getEventSamples(this.options.events), - { - serializeState, - ...options - } - ); + const shortestPaths = getShortestPaths(this.machine, { + serializeState, + getEvents: () => getEventSamples(this.options.events), + ...options + }); return this.getTestPlans(shortestPaths); } @@ -137,9 +134,10 @@ export class TestModel { ): Array> { const simplePaths = getSimplePaths( this.machine, - getEventSamples(this.options.events), + { serializeState, + getEvents: () => getEventSamples(this.options.events), ...options } ); From 89366aead2a68c04dca4c6dfe98be1df749b322f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 31 Dec 2021 10:09:22 -0500 Subject: [PATCH 011/394] Rename paths to plan --- packages/xstate-graph/src/graph.ts | 3 - packages/xstate-graph/src/types.ts | 4 +- packages/xstate-test/src/index.ts | 21 +++- packages/xstate-test/test/index.test.ts | 130 +++++++++++++----------- 4 files changed, 94 insertions(+), 64 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 5f62f4d527..fb1518409a 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -230,9 +230,6 @@ export function depthShortestPaths( while (unvisited.size > 0) { for (const vertex of unvisited) { const [weight] = weightMap.get(vertex)!; - if (!adjacency[vertex]) { - console.log('not found', vertex); - } for (const event of keys(adjacency[vertex])) { const eventObject = JSON.parse(event); const nextState = adjacency[vertex][event]; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 9a6d6ddc74..db8799fc7d 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -55,7 +55,7 @@ export interface AdjacencyMap { >; } -export interface StatePaths { +export interface StatePlan { /** * The target state. */ @@ -82,7 +82,7 @@ export interface StatePath { } export interface StatePathsMap { - [key: string]: StatePaths; + [key: string]: StatePlan; } export interface Step { diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index f5c9c9c352..b0304cb9e3 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -5,6 +5,7 @@ import { getStateNodes, serializeState, StatePathsMap, + StatePlan, TraversalOptions } from '@xstate/graph'; import { StateMachine, EventObject, State, StateValue } from 'xstate'; @@ -151,6 +152,24 @@ export class TestModel { return this.filterPathsTo(stateValue, this.getSimplePathPlans()); } + public async testPlan( + plan: StatePlan, any>, + testContext: TTestContext + ) { + for (const path of plan.paths) { + for (const step of path.steps) { + console.log('testing state', step.state.value); + await this.testState(step.state, testContext); + console.log('executing event', step.event); + + await this.executeEvent(step.event, testContext); + } + console.log('testing state2', path.state.value); + + await this.testState(path.state, testContext); + } + } + public getTestPlans( statePathsMap: StatePathsMap, any> ): Array> { @@ -465,7 +484,7 @@ export function getEventSamples( * - `events`: an object mapping string event types (e.g., `SUBMIT`) * to an event test config (e.g., `{exec: () => {...}, cases: [...]}`) */ -export function createModel( +export function createTestModel( machine: StateMachine, options?: TestModelOptions ): TestModel { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 1ae5199837..3779b74281 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,7 +1,8 @@ // nothing yet -import { createModel } from '../src'; +import { createTestModel } from '../src'; import { Machine, assign } from 'xstate'; import stripAnsi from 'strip-ansi'; +import { getShortestPaths } from '@xstate/graph'; interface DieHardContext { three: number; @@ -38,11 +39,11 @@ const dieHardMachine = Machine( context: { three: 0, five: 0 }, states: { pending: { + always: { + target: 'success', + cond: 'weHave4Gallons' + }, on: { - '': { - target: 'success', - cond: 'weHave4Gallons' - }, POUR_3_TO_5: { actions: pour3to5 }, @@ -121,38 +122,40 @@ class Jugs { } } -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(); +const dieHardModel = createTestModel<{ 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 @@ -263,7 +266,7 @@ describe('error path trace', () => { } }); - const testModel = createModel(machine).withEvents({ + const testModel = createTestModel(machine).withEvents({ NEXT: () => { /* noop */ } @@ -295,7 +298,7 @@ describe('coverage', () => { expect(coverage.stateNodes['dieHard.success']).toBeGreaterThan(0); }); - it('tests missing state node coverage', async () => { + it.only('tests missing state node coverage', async () => { const machine = Machine({ id: 'missing', initial: 'first', @@ -317,6 +320,9 @@ describe('coverage', () => { one: { meta: { test: () => true + }, + on: { + NEXT: 'two' } }, two: { @@ -337,28 +343,36 @@ describe('coverage', () => { } }); - const testModel = createModel(machine).withEvents({ + const plansMap = getShortestPaths(machine); + + const testModel = createTestModel(machine).withEvents({ NEXT: () => { /* ... */ } }); - const plans = testModel.getShortestPathPlans(); - - for (const plan of plans) { - for (const path of plan.paths) { - await path.test(undefined); - } + // TODO: remove + console.log(Object.keys(plansMap)); + for (const plan of Object.values(plansMap)) { + await testModel.testPlan(plan, 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') - ); - } + // 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 () => { @@ -393,7 +407,7 @@ describe('coverage', () => { } }); - const testModel = createModel(TestBug).withEvents({ + const testModel = createTestModel(TestBug).withEvents({ START: () => { /* ... */ } @@ -504,7 +518,7 @@ describe('events', () => { } }); - const testModel = createModel(feedbackMachine).withEvents({ + const testModel = createTestModel(feedbackMachine).withEvents({ CLICK_BAD: () => { /* ... */ }, @@ -539,7 +553,7 @@ describe('events', () => { } }); - const testModel = createModel(testMachine); + const testModel = createTestModel(testMachine); const testPlans = testModel.getShortestPathPlans(); @@ -569,7 +583,7 @@ describe('state limiting', () => { } }); - const testModel = createModel(machine).withEvents({ + const testModel = createTestModel(machine).withEvents({ INC: () => {} }); const testPlans = testModel.getShortestPathPlans({ @@ -632,7 +646,7 @@ describe('plan description', () => { } }); - const testModel = createModel(machine).withEvents({ + const testModel = createTestModel(machine).withEvents({ NEXT: { exec: () => {} }, DONE: { exec: () => {} } }); From a8383d7d3fbb35a2071b228798dcadc404102223 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 16 Jan 2022 11:13:23 -0500 Subject: [PATCH 012/394] WIP --- packages/xstate-graph/src/graph.ts | 201 +++++-- packages/xstate-graph/src/types.ts | 5 + packages/xstate-graph/test/graph.test.ts | 54 +- packages/xstate-test/src/index.ts | 635 ++++++++++++----------- packages/xstate-test/src/types.ts | 82 ++- packages/xstate-test/test/index.test.ts | 431 ++++++++------- 6 files changed, 802 insertions(+), 606 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index fb1518409a..3095f5088b 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -8,7 +8,13 @@ import { AnyEventObject } from 'xstate'; import { flatten, keys } from 'xstate/lib/utils'; -import { SerializedEvent, SerializedState, StatePath } from '.'; +import { + SerializedEvent, + SerializedState, + SimpleBehavior, + StatePath, + StatePlan +} from '.'; import { StatePathsMap, AdjacencyMap, @@ -193,25 +199,22 @@ export function getShortestPaths< defaultMachineStateOptions ); return depthShortestPaths( - (state, event) => machine.transition(state, event), - machine.initialState, + { + transition: (state, event) => machine.transition(state, event), + initialState: machine.initialState + }, resolvedOptions ); } export function depthShortestPaths( - reducer: (state: TState, event: TEvent) => TState, - initialState: TState, + behavior: SimpleBehavior, options?: TraversalOptions ): StatePathsMap { const optionsWithDefaults = resolveTraversalOptions(options); const { serializeState } = optionsWithDefaults; - const adjacency = depthFirstTraversal( - reducer, - initialState, - optionsWithDefaults - ); + const adjacency = depthFirstTraversal(behavior, optionsWithDefaults); // weight, state, event const weightMap = new Map< @@ -219,8 +222,8 @@ export function depthShortestPaths( [number, SerializedState | undefined, SerializedEvent | undefined] >(); const stateMap = new Map(); - const initialVertex = serializeState(initialState, null); - stateMap.set(initialVertex, initialState); + const initialVertex = serializeState(behavior.initialState, null); + stateMap.set(initialVertex, behavior.initialState); weightMap.set(initialVertex, [0, undefined, undefined]); const unvisited = new Set(); @@ -294,11 +297,7 @@ export function getSimplePaths< defaultMachineStateOptions ); - return depthSimplePaths( - (state, event) => machine.transition(state, event), - machine.initialState, - resolvedOptions - ); + return depthSimplePaths(machine as SimpleBehavior, resolvedOptions); } export function toDirectedGraph(stateNode: StateNode): DirectedGraphNode { @@ -343,62 +342,53 @@ export function toDirectedGraph(stateNode: StateNode): DirectedGraphNode { } export function getPathFromEvents< - TContext = DefaultContext, + TState, TEvent extends EventObject = EventObject >( - machine: StateMachine, + behavior: SimpleBehavior, events: TEvent[] -): StatePath, TEvent> { - const optionsWithDefaults = getValueAdjMapOptions< - State, - TEvent - >({ - events: events.reduce((events, event) => { - events[event.type] ??= []; - events[event.type].push(event); - return events; - }, {}) - }); +): StatePath { + const optionsWithDefaults = resolveTraversalOptions( + { + getEvents: () => { + return events; + } + }, + defaultMachineStateOptions as any + ); - const { stateSerializer, eventSerializer } = optionsWithDefaults; + const { serializeState } = optionsWithDefaults; - if (!machine.states) { - return { - state: machine.initialState, - steps: [], - weight: 0 - }; - } + const adjacency = depthFirstTraversal(behavior, optionsWithDefaults); - const adjacency = getAdjacencyMap(machine, optionsWithDefaults); - const stateMap = new Map>(); - const path: Steps, TEvent> = []; + 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 nextState = adjacency[stateSerial][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 { @@ -413,10 +403,10 @@ interface AdjMap { } export function depthFirstTraversal( - reducer: (state: TState, event: TEvent) => TState, - initialState: TState, + behavior: SimpleBehavior, options: TraversalOptions ): AdjMap { + const { transition, initialState } = behavior; const { serializeState, getEvents } = resolveTraversalOptions(options); const adj: AdjMap = {}; @@ -431,7 +421,7 @@ export function depthFirstTraversal( const events = getEvents(state); for (const subEvent of events) { - const nextState = reducer(state, subEvent); + const nextState = transition(state, subEvent); if (!options.filter || options.filter(nextState, subEvent)) { adj[serializedState][JSON.stringify(subEvent)] = nextState; @@ -466,13 +456,13 @@ function resolveTraversalOptions( } export function depthSimplePaths( - reducer: (state: TState, event: TEvent) => TState, - initialState: TState, + behavior: SimpleBehavior, options: TraversalOptions ): StatePathsMap { + const { initialState } = behavior; const resolvedOptions = resolveTraversalOptions(options); const { serializeState, visitCondition } = resolvedOptions; - const adjacency = depthFirstTraversal(reducer, initialState, resolvedOptions); + const adjacency = depthFirstTraversal(behavior, resolvedOptions); const stateMap = new Map(); // const visited = new Set(); const visitCtx: VisitedContext = { @@ -546,3 +536,102 @@ export function depthSimplePaths( return pathMap; } + +export function filterPlans( + plans: StatePathsMap, + predicate: (state: TState, plan: StatePlan) => boolean +): Array> { + const filteredPlans = Object.values(plans).filter((plan) => + predicate(plan.state, plan) + ); + + return filteredPlans; +} + +export function depthSimplePathsTo( + behavior: SimpleBehavior, + predicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const simplePlansMap = depthSimplePaths(behavior, resolvedOptions); + + return filterPlans(simplePlansMap, predicate); +} + +export function depthSimplePathsFromTo( + behavior: SimpleBehavior, + fromPredicate: (state: TState) => boolean, + toPredicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const simplePlansMap = depthSimplePaths(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 depthShortestPathsTo( + behavior: SimpleBehavior, + predicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const simplePlansMap = depthShortestPaths(behavior, resolvedOptions); + + return filterPlans(simplePlansMap, predicate); +} + +export function depthShortestPathsFromTo( + behavior: SimpleBehavior, + fromPredicate: (state: TState) => boolean, + toPredicate: (state: TState) => boolean, + options: TraversalOptions +): Array> { + const resolvedOptions = resolveTraversalOptions(options); + const shortesPlansMap = depthShortestPaths(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)) + ); + }); +} + +// type EventCases = Array< +// Omit & { type?: TEvent['type'] } +// >; + +// export function generateEvents( +// blah: { +// [K in TEvent['type']]: TEvent extends { type: K } +// ? EventCases | ((state: TState) => EventCases) +// : never; +// } +// ): (state: TState) => TEvent[] { +// return (state) => { +// const events: TEvent[] = []; + +// Object.keys(blah).forEach((key) => { +// const cases = blah[key as TEvent['type']]; + +// const foo = +// typeof cases === 'function' ? cases(state) : (cases as TEvent[]); + +// foo.forEach((payload) => { +// events.push({ +// type: key, +// ...payload +// }); +// }); +// }); + +// return events; +// }; +// } diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index db8799fc7d..cf1ba857ac 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -137,3 +137,8 @@ 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/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 4f3896f1e1..0ea696df59 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -361,7 +361,7 @@ describe('@xstate/graph', () => { 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' }, @@ -490,19 +490,21 @@ describe('@xstate/graph', () => { it('simple paths for reducers', () => { const a = depthSimplePaths( - (s, e) => { - if (e.type === 'a') { - return 1; - } - if (e.type === 'b' && s === 1) { - return 2; - } - if (e.type === 'reset') { - return 0; - } - return s; + { + 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 }, - 0 as number, { getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], serializeState: (v, e) => @@ -515,19 +517,21 @@ it('simple paths for reducers', () => { it('shortest paths for reducers', () => { const a = depthShortestPaths( - (s, e) => { - if (e.type === 'a') { - return 1; - } - if (e.type === 'b' && s === 1) { - return 2; - } - if (e.type === 'reset') { - return 0; - } - return s; + { + 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 }, - 0 as number, { getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], serializeState: (v, e) => diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index b0304cb9e3..a2f748e040 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,25 +1,31 @@ import { getPathFromEvents, - getShortestPaths, - getSimplePaths, - getStateNodes, serializeState, - StatePathsMap, + SimpleBehavior, + StatePath, StatePlan, TraversalOptions } from '@xstate/graph'; -import { StateMachine, EventObject, State, StateValue } from 'xstate'; -import slimChalk from './slimChalk'; +import { + depthShortestPaths, + depthSimplePathsTo +} from '@xstate/graph/src/graph'; +import { + StateMachine, + EventObject, + State, + StateValue, + StateFrom, + EventFrom +} from 'xstate'; +import { isMachine } from 'xstate/src/utils'; import { TestModelCoverage, TestModelOptions, - TestPlan, StatePredicate, - TestPathResult, - TestStepResult, TestMeta, - EventExecutor, - CoverageOptions + CoverageOptions, + TestEventsConfig } from './types'; /** @@ -42,43 +48,51 @@ import { * ``` * */ -export class TestModel { +export class TestModel { public coverage: TestModelCoverage = { stateNodes: new Map(), transitions: new Map() }; - public options: TestModelOptions; - public static defaultOptions: TestModelOptions = { - events: {} - }; + public options: TestModelOptions; + public defaultTraversalOptions?: TraversalOptions; + public getDefaultOptions(): TestModelOptions { + return { + serializeState: isMachine(this.behavior) + ? (serializeState as any) + : serializeState, + getEvents: () => [], + testState: () => void 0, + execEvent: () => void 0 + }; + } constructor( - public machine: StateMachine, - options?: Partial> + public behavior: SimpleBehavior, + public testContext: TTestContext, + options?: Partial> ) { this.options = { - ...TestModel.defaultOptions, + ...this.getDefaultOptions(), ...options }; } public getShortestPathPlans( - options?: TraversalOptions, any> - ): Array> { - const shortestPaths = getShortestPaths(this.machine, { - serializeState, - getEvents: () => getEventSamples(this.options.events), - ...options - }); + options?: TraversalOptions + ): Array> { + const shortestPaths = depthShortestPaths( + this.behavior, + this.resolveOptions(options) + ); - return this.getTestPlans(shortestPaths); + return Object.values(shortestPaths); } public getShortestPathPlansTo( - stateValue: StateValue | StatePredicate - ): Array> { + stateValue: StateValue | StatePredicate + ): Array> { let minWeight = Infinity; - let shortestPlans: Array> = []; + let shortestPlans: Array> = []; const plans = this.filterPathsTo(stateValue, this.getShortestPathPlans()); @@ -96,312 +110,303 @@ export class TestModel { } private filterPathsTo( - stateValue: StateValue | StatePredicate, - testPlans: Array> - ): Array> { - const predicate = + stateValue: StateValue | StatePredicate, + testPlans: Array> + ): Array> { + const predicate: StatePredicate = typeof stateValue === 'function' - ? (plan) => stateValue(plan.state) - : (plan) => plan.state.matches(stateValue); - return testPlans.filter(predicate); + ? (state) => stateValue(state) + : (state) => (state as any).matches(stateValue); + + return testPlans.filter((testPlan) => { + return predicate(testPlan.state); + }); } public getPlanFromEvents( - events: Array, - { target }: { target: StateValue } - ): TestPlan { - const path = getPathFromEvents(this.machine, events); + events: TEvent[], + assertion: (state: TState) => boolean + ): StatePlan { + const path = getPathFromEvents(this.behavior, events); - if (!path.state.matches(target)) { + if (!assertion(path.state)) { throw new Error( `The last state ${JSON.stringify( (path.state as any).value - )} does not match the target: ${JSON.stringify(target)}` + )} does not match the target}` ); } - const plans = this.getTestPlans({ - [JSON.stringify(path.state.value)]: { - state: path.state, - paths: [path] - } - }); + const plan: StatePlan = { + state: path.state, + paths: [path] + }; - return plans[0]; + return plan; } - public getSimplePathPlans( - options?: Partial, any>> - ): Array> { - const simplePaths = getSimplePaths( - this.machine, - - { - serializeState, - getEvents: () => getEventSamples(this.options.events), - ...options - } - ); - - return this.getTestPlans(simplePaths); - } + // public getSimplePathPlans( + // options?: Partial> + // ): Array> { + // const simplePaths = depthSimplePaths( + // this.behavior.transition, + // this.behavior.initialState, + // { + // serializeState, + // getEvents: () => getEventSamples(this.options.events), + // ...options + // } + // ); + + // return this.getTestPlans(simplePaths); + // } public getSimplePathPlansTo( - stateValue: StateValue | StatePredicate - ): Array> { - return this.filterPathsTo(stateValue, this.getSimplePathPlans()); + predicate: (state: TState) => boolean + ): Array> { + return depthSimplePathsTo(this.behavior, predicate, this.options); } public async testPlan( - plan: StatePlan, any>, + plan: StatePlan, testContext: TTestContext ) { for (const path of plan.paths) { - for (const step of path.steps) { - console.log('testing state', step.state.value); - await this.testState(step.state, testContext); - console.log('executing event', step.event); - - await this.executeEvent(step.event, testContext); - } - console.log('testing state2', path.state.value); - - await this.testState(path.state, testContext); + await this.testPath(path, testContext); } } - public getTestPlans( - statePathsMap: StatePathsMap, any> - ): Array> { - return Object.keys(statePathsMap).map((key) => { - const testPlan = statePathsMap[key]; - const paths = testPlan.paths.map((path) => { - const steps = path.steps.map((step) => { - return { - ...step, - description: getDescription(step.state), - test: (testContext) => this.testState(step.state, testContext), - exec: (testContext) => this.executeEvent(step.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.steps - .map((s) => formatEvent(s.event)) - .join(' → '); - - return { - ...path, - steps, - description: `via ${eventsString}`, - test: async (testContext) => { - const testPathResult: TestPathResult = { - steps: [], - state: { - error: null - } - }; - - try { - for (const step of steps) { - const testStepResult: TestStepResult = { - step, - state: { error: null }, - event: { error: null } - }; - - testPathResult.steps.push(testStepResult); - - try { - await step.test(testContext); - } catch (err) { - testStepResult.state.error = err; - - throw err; - } - - try { - await step.exec(testContext); - } catch (err) { - testStepResult.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.steps - .map((s) => { - const stateString = `${JSON.stringify( - s.step.state.value - )} ${ - s.step.state.context === undefined - ? '' - : JSON.stringify(s.step.state.context) - }`; - const eventString = `${JSON.stringify(s.step.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, + public async testPath( + path: StatePath, 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]; + for (const step of path.steps) { + await this.testState(step.state, testContext); - if (!testEvent) { - // tslint:disable-next-line:no-console - console.warn(`Missing config for event "${event.type}".`); - return undefined; + await this.executeEvent(step.event, testContext); } - if (typeof testEvent === 'function') { - return testEvent; - } + await this.testState(path.state, testContext); + } - return testEvent.exec; + // public getTestPlans( + // statePathsMap: StatePathsMap + // ): Array> { + // return Object.keys(statePathsMap).map((key) => { + // const testPlan = statePathsMap[key]; + // const paths = testPlan.paths.map((path) => { + // const steps = path.steps.map((step) => { + // return { + // ...step, + // description: getDescription(step.state), + // test: (testContext) => this.testState(step.state, testContext), + // exec: (testContext) => this.executeEvent(step.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.steps + // .map((s) => formatEvent(s.event)) + // .join(' → '); + + // return { + // ...path, + // steps, + // description: `via ${eventsString}`, + // test: async (testContext) => { + // const testPathResult: TestPathResult = { + // steps: [], + // state: { + // error: null + // } + // }; + + // try { + // for (const step of steps) { + // const testStepResult: TestStepResult = { + // step, + // state: { error: null }, + // event: { error: null } + // }; + + // testPathResult.steps.push(testStepResult); + + // try { + // await step.test(testContext); + // } catch (err) { + // testStepResult.state.error = err; + + // throw err; + // } + + // try { + // await step.exec(testContext); + // } catch (err) { + // testStepResult.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.steps + // .map((s) => { + // const stateString = `${JSON.stringify( + // s.step.state.value + // )} ${ + // s.step.state.context === undefined + // ? '' + // : JSON.stringify(s.step.state.context) + // }`; + // const eventString = `${JSON.stringify(s.step.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: TState, testContext: TTestContext) { + return await this.options.testState(state, testContext); } - public async executeEvent(event: EventObject, testContext: TTestContext) { - const executor = this.getEventExecutor(event); + public async executeEvent(event: TEvent, testContext: TTestContext) { + return await this.options.execEvent(event, testContext); + } - if (executor) { - await executor(testContext, event); - } + public getCoverage(options?: CoverageOptions) { + return options; + // const filter = options ? options.filter : undefined; + // const stateNodes = getStateNodes(this.behavior); + // 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 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; - }, {}) - }; + public testCoverage(options?: CoverageOptions): void { + return void options; + // 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') + // ); + // } + } - for (const key of this.coverage.stateNodes.keys()) { - coverage.stateNodes[key] = this.coverage.stateNodes.get(key); - } + public withEvents( + eventMap: TestEventsConfig + ): TestModel { + return new TestModel(this.behavior, this.testContext, { + ...this.options, + getEvents: () => getEventSamples(eventMap), + execEvent: async (event, testContext) => { + const eventConfig = eventMap[event.type]; + + if (!eventConfig) { + return; + } - return coverage; - } + const exec = + typeof eventConfig === 'function' ? eventConfig : eventConfig.exec; - public testCoverage(options?: CoverageOptions): void { - const coverage = this.getCoverage(options); - const missingStateNodes = Object.keys(coverage.stateNodes).filter((id) => { - return !coverage.stateNodes[id]; + await exec?.(testContext, event); + } }); - - 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 - }); + public resolveOptions( + options?: Partial> + ): TestModelOptions { + return { ...this.defaultTraversalOptions, ...this.options, ...options }; } } -function getDescription(state: State): string { +export function getDescription( + state: State +): string { const contextString = state.context === undefined ? '' : `(${JSON.stringify(state.context)})`; @@ -429,10 +434,10 @@ function getDescription(state: State): string { ); } -export function getEventSamples( - eventsOptions: TestModelOptions['events'] -) { - const result: T[] = []; +export function getEventSamples( + eventsOptions: TestEventsConfig +): TEvent[] { + const result: TEvent[] = []; Object.keys(eventsOptions).forEach((key) => { const eventConfig = eventsOptions[key]; @@ -460,6 +465,20 @@ export function getEventSamples( return result; } +async function assertState(state: State, testContext: any) { + for (const id of Object.keys(state.meta)) { + const stateNodeMeta = state.meta[id]; + if (typeof stateNodeMeta.test === 'function' && !stateNodeMeta.skip) { + // this.coverage.stateNodes.set( + // id, + // (this.coverage.stateNodes.get(id) || 0) + 1 + // ); + + await stateNodeMeta.test(testContext, state); + } + } +} + /** * Creates a test model that represents an abstract model of a * system under test (SUT). @@ -484,9 +503,25 @@ export function getEventSamples( * - `events`: an object mapping string event types (e.g., `SUBMIT`) * to an event test config (e.g., `{exec: () => {...}, cases: [...]}`) */ -export function createTestModel( - machine: StateMachine, - options?: TestModelOptions -): TestModel { - return new TestModel(machine, options); +export function createTestModel< + TMachine extends StateMachine, + TestContext = any +>( + machine: TMachine, + testContext: TestContext, + options?: Partial< + TestModelOptions, EventFrom, TestContext> + > +): TestModel, EventFrom, TestContext> { + const testModel = new TestModel< + StateFrom, + EventFrom, + TestContext + >(machine as SimpleBehavior, testContext, { + serializeState, + testState: assertState, + ...options + }); + + return testModel; } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 406b500c1f..8b94093afb 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -1,4 +1,14 @@ -import { EventObject, State, StateNode } from 'xstate'; +import { TraversalOptions } from '@xstate/graph'; +import { + EventObject, + MachineConfig, + State, + StateMachine, + StateNode, + StateNodeConfig, + TransitionConfig, + TransitionConfigOrTarget +} from 'xstate'; export interface TestMeta { test?: (testContext: T, state: State) => Promise | void; description?: string | ((state: State) => string); @@ -40,11 +50,11 @@ export interface TestPathResult { * A collection of `paths` used to verify that the SUT reaches * the target `state`. */ -export interface TestPlan { +export interface TestPlan { /** * The target state. */ - state: State; + state: TState; /** * The paths that reach the target `state`. */ @@ -84,7 +94,7 @@ interface EventCase { [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`. @@ -128,11 +138,22 @@ export interface TestEventConfig { cases?: EventCase[]; } -export interface TestEventsConfig { - [eventType: string]: EventExecutor | TestEventConfig; +export interface TestEventsConfig { + [eventType: string]: + | EventExecutor + | TestEventConfig; } -export interface TestModelOptions { - events: TestEventsConfig; + +export interface TestModelOptions< + TState, + TEvents extends EventObject, + TTestContext +> extends TraversalOptions { + testState: (state: TState, testContext: TTestContext) => void | Promise; + execEvent: ( + event: TEvents, + testContext: TTestContext + ) => void | Promise; } export interface TestModelCoverage { stateNodes: Map; @@ -142,3 +163,48 @@ export interface TestModelCoverage { export interface CoverageOptions { filter?: (stateNode: StateNode) => boolean; } + +export interface TestTransitionConfig< + TContext, + TEvent extends EventObject, + TTestContext +> extends TransitionConfig { + test?: (state: State, testContext: TTestContext) => void; +} + +export type TestTransitionsConfigMap< + TContext, + TEvent extends EventObject, + TTestContext +> = { + [K in TEvent['type']]?: + | TestTransitionConfig< + TContext, + TEvent extends { type: K } ? TEvent : never, + TTestContext + > + | string; +} & { + ''?: TestTransitionConfig | string; +} & { + '*'?: TestTransitionConfig | string; +}; + +export interface TestStateNodeConfig< + TContext, + TEvent extends EventObject, + TTestContext +> extends StateNodeConfig { + test?: (state: State, testContext: TTestContext) => void; + on?: TestTransitionsConfigMap; +} + +export interface TestMachineConfig< + TContext, + TEvent extends EventObject, + TTestContext +> extends MachineConfig { + states?: { + [key: string]: TestStateNodeConfig; + }; +} diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 3779b74281..45ae37476f 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,8 +1,7 @@ // nothing yet -import { createTestModel } from '../src'; -import { Machine, assign } from 'xstate'; +import { createTestModel, getDescription } from '../src'; +import { assign, createMachine } from 'xstate'; import stripAnsi from 'strip-ansi'; -import { getShortestPaths } from '@xstate/graph'; interface DieHardContext { three: number; @@ -32,7 +31,7 @@ const fill5 = assign({ five: 5 }); const empty3 = assign({ three: 0 }); const empty5 = assign({ five: 0 }); -const dieHardMachine = Machine( +const dieHardMachine = createMachine( { id: 'dieHard', initial: 'pending', @@ -122,50 +121,48 @@ class Jugs { } } -const dieHardModel = createTestModel<{ 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(); - } +const dieHardModel = createTestModel(dieHardMachine, null as any).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, () => { + describe(`plan ${getDescription(plan.state)}`, () => { plan.paths.forEach((path) => { - it(path.description, () => { + it(`path ${getDescription(path.state)}`, () => { const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); + return dieHardModel.testPath(path, { jugs: testJugs }); }); }); }); @@ -174,15 +171,15 @@ describe('testing a model (shortestPathsTo)', () => { describe('testing a model (simplePathsTo)', () => { dieHardModel - .getSimplePathPlansTo('success') // ... + .getSimplePathPlansTo((state) => state.matches('success')) // ... .forEach((plan) => { describe(`reaches state ${JSON.stringify( plan.state.value )} (${JSON.stringify(plan.state.context)})`, () => { plan.paths.forEach((path) => { - it(path.description, () => { + it(`path ${getDescription(path.state)}`, () => { const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); + return dieHardModel.testPath(path, { jugs: testJugs }); }); }); }); @@ -199,27 +196,25 @@ describe('testing a model (getPlanFromEvents)', () => { { type: 'FILL_5' }, { type: 'POUR_5_TO_3' } ], - { - target: 'success' - } + (state) => state.matches('success') ); describe(`reaches state ${JSON.stringify(plan.state.value)} (${JSON.stringify( plan.state.context )})`, () => { plan.paths.forEach((path) => { - it(path.description, () => { + it(`path ${getDescription(path.state)}`, () => { const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); + return dieHardModel.testPath(path, { jugs: testJugs }); }); }); }); it('should throw if the target does not match the last entered state', () => { expect(() => { - dieHardModel.getPlanFromEvents([{ type: 'FILL_5' }], { - target: 'success' - }); + dieHardModel.getPlanFromEvents([{ type: 'FILL_5' }], (state) => + state.matches('success') + ); }).toThrow(); }); }); @@ -234,10 +229,10 @@ describe('path.test()', () => { plan.state.value )} (${JSON.stringify(plan.state.context)})`, () => { plan.paths.forEach((path) => { - describe(path.description, () => { + describe(`path ${getDescription(path.state)}`, () => { it(`reaches the target state`, () => { const testJugs = new Jugs(); - return path.test({ jugs: testJugs }); + return dieHardModel.testPath(path, { jugs: testJugs }); }); }); }); @@ -246,8 +241,8 @@ describe('path.test()', () => { }); describe('error path trace', () => { - describe('should return trace for failed state', () => { - const machine = Machine({ + describe.only('should return trace for failed state', () => { + const machine = createMachine({ initial: 'first', states: { first: { @@ -266,7 +261,7 @@ describe('error path trace', () => { } }); - const testModel = createTestModel(machine).withEvents({ + const testModel = createTestModel(machine, undefined).withEvents({ NEXT: () => { /* noop */ } @@ -276,7 +271,7 @@ describe('error path trace', () => { plan.paths.forEach((path) => { it('should show an error path trace', async () => { try { - await path.test(undefined); + await testModel.testPath(path, undefined); } catch (err) { expect(err.message).toEqual(expect.stringContaining('test error')); expect(stripAnsi(err.message)).toMatchSnapshot('error path trace'); @@ -290,149 +285,148 @@ describe('error path trace', () => { }); }); -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.only('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 - }, - on: { - NEXT: 'two' - } - }, - two: { - meta: { - test: () => true - } - }, - three: { - meta: { - test: () => true - } - } - }, - meta: { - test: () => true - } - } - } - }); - - const plansMap = getShortestPaths(machine); - - const testModel = createTestModel(machine).withEvents({ - NEXT: () => { - /* ... */ - } - }); - // TODO: remove - console.log(Object.keys(plansMap)); - for (const plan of Object.values(plansMap)) { - await testModel.testPlan(plan, undefined); - } - - // 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: { - always: 'end' - }, - end: { - type: 'final', - meta: { - test: () => { - /* ... */ - } - } - } - } - }); - - const testModel = createTestModel(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(); - }); -}); +// 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.only('tests missing state node coverage', async () => { +// const machine = createMachine({ +// id: 'missing', +// initial: 'first', +// states: { +// first: { +// on: { NEXT: 'third' }, +// meta: { +// test: () => true +// } +// }, +// second: { +// meta: { +// test: () => true +// } +// }, +// third: { +// initial: 'one', +// states: { +// one: { +// meta: { +// test: () => true +// }, +// on: { +// NEXT: 'two' +// } +// }, +// two: { +// meta: { +// test: () => true +// } +// }, +// three: { +// meta: { +// test: () => true +// } +// } +// }, +// meta: { +// test: () => true +// } +// } +// } +// }); + +// const testModel = createTestModel(machine, undefined).withEvents({ +// NEXT: () => { +// /* ... */ +// } +// }); + +// const plans = testModel.getShortestPathPlans(); + +// for (const plan of plans) { +// await testModel.testPlan(plan, undefined); +// } + +// // 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: { +// always: 'end' +// }, +// end: { +// type: 'final', +// meta: { +// test: () => { +// /* ... */ +// } +// } +// } +// } +// }); + +// const testModel = createTestModel(TestBug, undefined).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(); +// }); +// }); describe('events', () => { it('should allow for representing many cases', async () => { @@ -442,7 +436,7 @@ describe('events', () => { | { type: 'CLOSE' } | { type: 'ESC' } | { type: 'SUBMIT'; value: string }; - const feedbackMachine = Machine({ + const feedbackMachine = createMachine({ id: 'feedback', initial: 'question', states: { @@ -518,7 +512,7 @@ describe('events', () => { } }); - const testModel = createTestModel(feedbackMachine).withEvents({ + const testModel = createTestModel(feedbackMachine, undefined).withEvents({ CLICK_BAD: () => { /* ... */ }, @@ -536,14 +530,14 @@ describe('events', () => { const testPlans = testModel.getShortestPathPlans(); for (const plan of testPlans) { - await plan.test(undefined); + await testModel.testPlan(plan, undefined); } return testModel.testCoverage(); }); it('should not throw an error for unimplemented events', () => { - const testMachine = Machine({ + const testMachine = createMachine({ initial: 'idle', states: { idle: { @@ -553,13 +547,13 @@ describe('events', () => { } }); - const testModel = createTestModel(testMachine); + const testModel = createTestModel(testMachine, undefined); const testPlans = testModel.getShortestPathPlans(); expect(async () => { - for (const plan of testPlans) { - await plan.test(undefined); + for (const plan of Object.values(testPlans)) { + await testModel.testPlan(plan, undefined); } }).not.toThrow(); }); @@ -567,7 +561,7 @@ describe('events', () => { 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: { @@ -583,9 +577,10 @@ describe('state limiting', () => { } }); - const testModel = createTestModel(machine).withEvents({ + const testModel = createTestModel(machine, undefined).withEvents({ INC: () => {} }); + const testPlans = testModel.getShortestPathPlans({ filter: (state) => { return state.context.count < 5; @@ -597,7 +592,7 @@ describe('state limiting', () => { }); describe('plan description', () => { - const machine = Machine({ + const machine = createMachine({ id: 'test', initial: 'atomic', context: { count: 0 }, @@ -646,23 +641,25 @@ describe('plan description', () => { } }); - const testModel = createTestModel(machine).withEvents({ + const testModel = createTestModel(machine, undefined).withEvents({ NEXT: { exec: () => {} }, DONE: { exec: () => {} } }); const testPlans = testModel.getShortestPathPlans(); it('should give a description for every plan', () => { - const planDescriptions = testPlans.map((plan) => plan.description); + const planDescriptions = testPlans.map( + (plan) => `plan ${getDescription(plan.state)}` + ); expect(planDescriptions).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})", + "plan state: \\"#test.atomic\\" ({\\"count\\":0})", + "plan state: \\"#test.compound.child\\" ({\\"count\\":0})", + "plan state: \\"#test.final\\" ({\\"count\\":0})", + "plan state: \\"child with meta\\" ({\\"count\\":0})", + "plan states: \\"#test.parallel.one\\", \\"two description\\" ({\\"count\\":0})", + "plan state: \\"noMetaDescription\\" ({\\"count\\":0})", ] `); }); From 4f02a153787d81bf527d3480d20e96a1bfca139b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 12 Feb 2022 10:55:07 -0500 Subject: [PATCH 013/394] Move formatting to utils --- packages/xstate-graph/src/graph.ts | 18 +-- packages/xstate-graph/src/types.ts | 9 +- packages/xstate-test/src/index.ts | 202 ++++++------------------ packages/xstate-test/src/types.ts | 8 +- packages/xstate-test/src/utils.ts | 73 +++++++++ packages/xstate-test/test/index.test.ts | 51 +++--- 6 files changed, 173 insertions(+), 188 deletions(-) create mode 100644 packages/xstate-test/src/utils.ts diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 0fc19fddfc..4e2df163c3 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -81,8 +81,8 @@ export function serializeState( export function serializeEvent( event: TEvent -): string { - return JSON.stringify(event); +): SerializedEvent { + return JSON.stringify(event) as SerializedEvent; } export function deserializeEventString( @@ -183,6 +183,7 @@ export function getAdjacencyMap< const defaultMachineStateOptions: TraversalOptions, any> = { serializeState, + serializeEvent, getEvents: (state) => { return state.nextEvents.map((type) => ({ type })); } @@ -301,9 +302,7 @@ export function getSimplePaths< return depthSimplePaths(machine as SimpleBehavior, resolvedOptions); } -export function toDirectedGraph( - stateNode: AnyStateNode | StateMachine -): DirectedGraphNode { +export function toDirectedGraph(stateNode: AnyStateNode): DirectedGraphNode { const edges: DirectedGraphEdge[] = flatten( stateNode.transitions.map((t, transitionIndex) => { const targets = t.target ? t.target : [stateNode]; @@ -438,16 +437,17 @@ export function depthFirstTraversal( return adj; } -function resolveTraversalOptions( - depthOptions?: TraversalOptions, - defaultOptions?: TraversalOptions -): Required> { +function resolveTraversalOptions( + depthOptions?: Partial>, + defaultOptions?: TraversalOptions +): Required> { const serializeState = depthOptions?.serializeState ?? defaultOptions?.serializeState ?? ((state) => JSON.stringify(state) as any); return { serializeState, + serializeEvent: serializeEvent as any, // TODO fix types filter: () => true, visitCondition: (state, event, vctx) => { return vctx.vertices.has(serializeState(state, event)); diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 3c08848ae6..05f72d10a6 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -133,8 +133,13 @@ export interface VisitedContext { a?: TState | TEvent; // TODO: remove } -export interface TraversalOptions { - serializeState?: (state: TState, event: TEvent | null) => SerializedState; +export interface SerializationOptions { + serializeState: (state: TState, event: TEvent | null) => SerializedState; + serializeEvent: (event: TEvent) => SerializedEvent; +} + +export interface TraversalOptions + extends SerializationOptions { visitCondition?: ( state: TState, event: TEvent, diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index a2f748e040..a869c4dd6b 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,5 +1,6 @@ import { getPathFromEvents, + SerializedEvent, serializeState, SimpleBehavior, StatePath, @@ -25,8 +26,11 @@ import { StatePredicate, TestMeta, CoverageOptions, - TestEventsConfig + TestEventsConfig, + TestPathResult, + TestStepResult } from './types'; +import { formatPathTestResult, simpleStringify } from './utils'; /** * Creates a test model that represents an abstract model of a @@ -60,6 +64,7 @@ export class TestModel { serializeState: isMachine(this.behavior) ? (serializeState as any) : serializeState, + serializeEvent: (event) => simpleStringify(event) as SerializedEvent, getEvents: () => [], testState: () => void 0, execEvent: () => void 0 @@ -78,7 +83,7 @@ export class TestModel { } public getShortestPathPlans( - options?: TraversalOptions + options?: Partial> ): Array> { const shortestPaths = depthShortestPaths( this.behavior, @@ -180,159 +185,52 @@ export class TestModel { path: StatePath, testContext: TTestContext ) { - for (const step of path.steps) { - await this.testState(step.state, testContext); + const testPathResult: TestPathResult = { + steps: [], + state: { + error: null + } + }; - await this.executeEvent(step.event, testContext); - } + try { + for (const step of path.steps) { + const testStepResult: TestStepResult = { + step, + state: { error: null }, + event: { error: null } + }; - await this.testState(path.state, testContext); - } + testPathResult.steps.push(testStepResult); - // public getTestPlans( - // statePathsMap: StatePathsMap - // ): Array> { - // return Object.keys(statePathsMap).map((key) => { - // const testPlan = statePathsMap[key]; - // const paths = testPlan.paths.map((path) => { - // const steps = path.steps.map((step) => { - // return { - // ...step, - // description: getDescription(step.state), - // test: (testContext) => this.testState(step.state, testContext), - // exec: (testContext) => this.executeEvent(step.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.steps - // .map((s) => formatEvent(s.event)) - // .join(' → '); - - // return { - // ...path, - // steps, - // description: `via ${eventsString}`, - // test: async (testContext) => { - // const testPathResult: TestPathResult = { - // steps: [], - // state: { - // error: null - // } - // }; - - // try { - // for (const step of steps) { - // const testStepResult: TestStepResult = { - // step, - // state: { error: null }, - // event: { error: null } - // }; - - // testPathResult.steps.push(testStepResult); - - // try { - // await step.test(testContext); - // } catch (err) { - // testStepResult.state.error = err; - - // throw err; - // } - - // try { - // await step.exec(testContext); - // } catch (err) { - // testStepResult.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.steps - // .map((s) => { - // const stateString = `${JSON.stringify( - // s.step.state.value - // )} ${ - // s.step.state.context === undefined - // ? '' - // : JSON.stringify(s.step.state.context) - // }`; - // const eventString = `${JSON.stringify(s.step.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; - // }); - // } + try { + await this.testState(step.state, testContext); + } catch (err) { + testStepResult.state.error = err; + + throw err; + } + + try { + await this.executeEvent(step.event, testContext); + } catch (err) { + testStepResult.event.error = err; + + throw err; + } + } + + try { + await this.testState(path.state, testContext); + } catch (err) { + testPathResult.state.error = err.message; + throw err; + } + } catch (err) { + // TODO: make option + err.message += formatPathTestResult(path, testPathResult, this.options); + throw err; + } + } public async testState(state: TState, testContext: TTestContext) { return await this.options.testState(state, testContext); diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 8b94093afb..317535c476 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -1,13 +1,11 @@ -import { TraversalOptions } from '@xstate/graph'; +import { Step, TraversalOptions } from '@xstate/graph'; import { EventObject, MachineConfig, State, - StateMachine, StateNode, StateNodeConfig, - TransitionConfig, - TransitionConfigOrTarget + TransitionConfig } from 'xstate'; export interface TestMeta { test?: (testContext: T, state: State) => Promise | void; @@ -25,7 +23,7 @@ interface TestStateResult { error: null | Error; } export interface TestStepResult { - step: TestStep; + step: Step; state: TestStateResult; event: { error: null | Error; diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts new file mode 100644 index 0000000000..857a7beffa --- /dev/null +++ b/packages/xstate-test/src/utils.ts @@ -0,0 +1,73 @@ +import { + SerializationOptions, + SerializedEvent, + SerializedState, + StatePath +} from '@xstate/graph'; +import { 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, + ...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; +} diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 45ae37476f..4748efe2b9 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,7 +1,6 @@ // nothing yet import { createTestModel, getDescription } from '../src'; import { assign, createMachine } from 'xstate'; -import stripAnsi from 'strip-ansi'; interface DieHardContext { three: number; @@ -155,23 +154,25 @@ const dieHardModel = createTestModel(dieHardMachine, null as any).withEvents({ }); describe('testing a model (shortestPathsTo)', () => { - dieHardModel - .getShortestPathPlansTo('success') // ... - .forEach((plan) => { - describe(`plan ${getDescription(plan.state)}`, () => { - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, () => { - const testJugs = new Jugs(); - return dieHardModel.testPath(path, { jugs: testJugs }); - }); + dieHardModel.getShortestPathPlansTo('success').forEach((plan) => { + describe(`plan ${getDescription(plan.state)}`, () => { + it('should generate a single path', () => { + expect(plan.paths.length).toEqual(1); + }); + + plan.paths.forEach((path) => { + it(`path ${getDescription(path.state)}`, () => { + const testJugs = new Jugs(); + return dieHardModel.testPath(path, { jugs: testJugs }); }); }); }); + }); }); describe('testing a model (simplePathsTo)', () => { dieHardModel - .getSimplePathPlansTo((state) => state.matches('success')) // ... + .getSimplePathPlansTo((state) => state.matches('success')) .forEach((plan) => { describe(`reaches state ${JSON.stringify( plan.state.value @@ -241,7 +242,7 @@ describe('path.test()', () => { }); describe('error path trace', () => { - describe.only('should return trace for failed state', () => { + describe('should return trace for failed state', () => { const machine = createMachine({ initial: 'first', states: { @@ -274,7 +275,17 @@ describe('error path trace', () => { await testModel.testPath(path, undefined); } catch (err) { expect(err.message).toEqual(expect.stringContaining('test error')); - expect(stripAnsi(err.message)).toMatchSnapshot('error path trace'); + expect(err.message).toMatchInlineSnapshot(` + "test error + Path: + State: \\"first\\" + Event: {\\"type\\":\\"NEXT\\"} + + State: \\"second\\" + Event: {\\"type\\":\\"NEXT\\"} + + State: \\"third\\"" + `); return; } @@ -649,17 +660,17 @@ describe('plan description', () => { it('should give a description for every plan', () => { const planDescriptions = testPlans.map( - (plan) => `plan ${getDescription(plan.state)}` + (plan) => `reaches ${getDescription(plan.state)}` ); expect(planDescriptions).toMatchInlineSnapshot(` Array [ - "plan state: \\"#test.atomic\\" ({\\"count\\":0})", - "plan state: \\"#test.compound.child\\" ({\\"count\\":0})", - "plan state: \\"#test.final\\" ({\\"count\\":0})", - "plan state: \\"child with meta\\" ({\\"count\\":0})", - "plan states: \\"#test.parallel.one\\", \\"two description\\" ({\\"count\\":0})", - "plan state: \\"noMetaDescription\\" ({\\"count\\":0})", + "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})", ] `); }); From cc18a1671e80a7074ada201c092e3b3fd07376d7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 13 Feb 2022 15:37:48 -0500 Subject: [PATCH 014/394] Remove index2 --- packages/xstate-test/src/index2.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 packages/xstate-test/src/index2.ts diff --git a/packages/xstate-test/src/index2.ts b/packages/xstate-test/src/index2.ts deleted file mode 100644 index 28b86872a3..0000000000 --- a/packages/xstate-test/src/index2.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { StateMachine } from 'xstate'; -import { TestPlan } from './types'; - -export function getShortestPathPlans< - TMachine extends StateMachine ->(machine: TMachine): Array> {} - -export function getSimplePathPlans< - TMachine extends StateMachine ->(machine: TMachine): Array> {} - -const plans = generateShortestPaths(machine, { until: transitionsCovered() }); From 9aa60641b0c756befc0822c07c6f90c96453974f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 14 Feb 2022 09:03:23 -0500 Subject: [PATCH 015/394] Renaming --- packages/xstate-test/src/index.ts | 101 +++++++----------------- packages/xstate-test/src/types.ts | 4 +- packages/xstate-test/src/utils.ts | 33 +++++++- packages/xstate-test/test/index.test.ts | 16 ++-- 4 files changed, 70 insertions(+), 84 deletions(-) diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index a869c4dd6b..a989d9450e 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -9,22 +9,15 @@ import { } from '@xstate/graph'; import { depthShortestPaths, + depthSimplePaths, depthSimplePathsTo } from '@xstate/graph/src/graph'; -import { - StateMachine, - EventObject, - State, - StateValue, - StateFrom, - EventFrom -} from 'xstate'; +import { StateMachine, EventObject, State, StateFrom, EventFrom } from 'xstate'; import { isMachine } from 'xstate/src/utils'; import { TestModelCoverage, TestModelOptions, StatePredicate, - TestMeta, CoverageOptions, TestEventsConfig, TestPathResult, @@ -82,7 +75,7 @@ export class TestModel { }; } - public getShortestPathPlans( + public getShortestPlans( options?: Partial> ): Array> { const shortestPaths = depthShortestPaths( @@ -93,13 +86,13 @@ export class TestModel { return Object.values(shortestPaths); } - public getShortestPathPlansTo( - stateValue: StateValue | StatePredicate + public getShortestPlansTo( + stateValue: StatePredicate ): Array> { let minWeight = Infinity; let shortestPlans: Array> = []; - const plans = this.filterPathsTo(stateValue, this.getShortestPathPlans()); + const plans = this.filterPathsTo(stateValue, this.getShortestPlans()); for (const plan of plans) { const currWeight = plan.paths[0].weight; @@ -114,14 +107,28 @@ export class TestModel { return shortestPlans; } + public getSimplePlans( + options?: Partial> + ): Array> { + const simplePaths = depthSimplePaths( + this.behavior, + this.resolveOptions(options) + ); + + return Object.values(simplePaths); + } + + public getSimplePlansTo( + predicate: StatePredicate + ): Array> { + return depthSimplePathsTo(this.behavior, predicate, this.options); + } + private filterPathsTo( - stateValue: StateValue | StatePredicate, + statePredicate: StatePredicate, testPlans: Array> ): Array> { - const predicate: StatePredicate = - typeof stateValue === 'function' - ? (state) => stateValue(state) - : (state) => (state as any).matches(stateValue); + const predicate: StatePredicate = (state) => statePredicate(state); return testPlans.filter((testPlan) => { return predicate(testPlan.state); @@ -130,11 +137,11 @@ export class TestModel { public getPlanFromEvents( events: TEvent[], - assertion: (state: TState) => boolean + statePredicate: StatePredicate ): StatePlan { - const path = getPathFromEvents(this.behavior, events); + const path = getPathFromEvents(this.behavior, events); - if (!assertion(path.state)) { + if (!statePredicate(path.state)) { throw new Error( `The last state ${JSON.stringify( (path.state as any).value @@ -150,28 +157,6 @@ export class TestModel { return plan; } - // public getSimplePathPlans( - // options?: Partial> - // ): Array> { - // const simplePaths = depthSimplePaths( - // this.behavior.transition, - // this.behavior.initialState, - // { - // serializeState, - // getEvents: () => getEventSamples(this.options.events), - // ...options - // } - // ); - - // return this.getTestPlans(simplePaths); - // } - - public getSimplePathPlansTo( - predicate: (state: TState) => boolean - ): Array> { - return depthSimplePathsTo(this.behavior, predicate, this.options); - } - public async testPlan( plan: StatePlan, testContext: TTestContext @@ -302,36 +287,6 @@ export class TestModel { } } -export 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}` - ); -} - export function getEventSamples( eventsOptions: TestEventsConfig ): TEvent[] { diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 317535c476..15fbf70449 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -154,8 +154,8 @@ export interface TestModelOptions< ) => void | Promise; } export interface TestModelCoverage { - stateNodes: Map; - transitions: Map>; + stateNodes: Record; + transitions: Record>; } export interface CoverageOptions { diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts index 857a7beffa..42b884b5bb 100644 --- a/packages/xstate-test/src/utils.ts +++ b/packages/xstate-test/src/utils.ts @@ -4,7 +4,8 @@ import { SerializedState, StatePath } from '@xstate/graph'; -import { TestPathResult } from './types'; +import { State } from 'xstate'; +import { TestMeta, TestPathResult } from './types'; interface TestResultStringOptions extends SerializationOptions { formatColor: (color: string, string: string) => string; @@ -71,3 +72,33 @@ export function formatPathTestResult( return errMessage; } + +export 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}` + ); +} diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 4748efe2b9..b10e10a961 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -154,7 +154,7 @@ const dieHardModel = createTestModel(dieHardMachine, null as any).withEvents({ }); describe('testing a model (shortestPathsTo)', () => { - dieHardModel.getShortestPathPlansTo('success').forEach((plan) => { + dieHardModel.getShortestPlansTo('success').forEach((plan) => { describe(`plan ${getDescription(plan.state)}`, () => { it('should generate a single path', () => { expect(plan.paths.length).toEqual(1); @@ -172,7 +172,7 @@ describe('testing a model (shortestPathsTo)', () => { describe('testing a model (simplePathsTo)', () => { dieHardModel - .getSimplePathPlansTo((state) => state.matches('success')) + .getSimplePlansTo((state) => state.matches('success')) .forEach((plan) => { describe(`reaches state ${JSON.stringify( plan.state.value @@ -221,7 +221,7 @@ describe('testing a model (getPlanFromEvents)', () => { }); describe('path.test()', () => { - const plans = dieHardModel.getSimplePathPlansTo((state) => { + const plans = dieHardModel.getSimplePlansTo((state) => { return state.matches('success') && state.context.three === 0; }); @@ -268,7 +268,7 @@ describe('error path trace', () => { } }); - testModel.getShortestPathPlansTo('third').forEach((plan) => { + testModel.getShortestPlansTo('third').forEach((plan) => { plan.paths.forEach((path) => { it('should show an error path trace', async () => { try { @@ -538,7 +538,7 @@ describe('events', () => { } }); - const testPlans = testModel.getShortestPathPlans(); + const testPlans = testModel.getShortestPlans(); for (const plan of testPlans) { await testModel.testPlan(plan, undefined); @@ -560,7 +560,7 @@ describe('events', () => { const testModel = createTestModel(testMachine, undefined); - const testPlans = testModel.getShortestPathPlans(); + const testPlans = testModel.getShortestPlans(); expect(async () => { for (const plan of Object.values(testPlans)) { @@ -592,7 +592,7 @@ describe('state limiting', () => { INC: () => {} }); - const testPlans = testModel.getShortestPathPlans({ + const testPlans = testModel.getShortestPlans({ filter: (state) => { return state.context.count < 5; } @@ -656,7 +656,7 @@ describe('plan description', () => { NEXT: { exec: () => {} }, DONE: { exec: () => {} } }); - const testPlans = testModel.getShortestPathPlans(); + const testPlans = testModel.getShortestPlans(); it('should give a description for every plan', () => { const planDescriptions = testPlans.map( From 37a0e311e9e1f881ed5c9bb93d3ee6633dae6fe1 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 14 Feb 2022 09:54:14 -0500 Subject: [PATCH 016/394] Move to TestModel.ts --- packages/xstate-test/src/TestModel.ts | 303 ++++++++++++++++++++++++ packages/xstate-test/src/index.ts | 290 +---------------------- packages/xstate-test/src/types.ts | 8 +- packages/xstate-test/test/index.test.ts | 53 +++-- 4 files changed, 340 insertions(+), 314 deletions(-) create mode 100644 packages/xstate-test/src/TestModel.ts diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts new file mode 100644 index 0000000000..191bc3d2e3 --- /dev/null +++ b/packages/xstate-test/src/TestModel.ts @@ -0,0 +1,303 @@ +import { + getPathFromEvents, + SerializedEvent, + SerializedState, + SimpleBehavior, + StatePath, + StatePlan, + Step, + TraversalOptions +} from '@xstate/graph'; +import { + depthShortestPaths, + depthSimplePaths, + depthSimplePathsTo +} from '@xstate/graph/src/graph'; +import { EventObject } from 'xstate'; +import { + TestModelCoverage, + TestModelOptions, + StatePredicate, + CoverageOptions, + TestEventsConfig, + TestPathResult, + TestStepResult +} from './types'; +import { formatPathTestResult, simpleStringify } from './utils'; +import { getEventSamples } from './index'; + +/** + * 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 = { + states: {}, + transitions: {} + }; + public options: TestModelOptions; + public defaultTraversalOptions?: TraversalOptions; + public getDefaultOptions(): TestModelOptions { + return { + serializeState: (state) => simpleStringify(state) as SerializedState, + serializeEvent: (event) => simpleStringify(event) as SerializedEvent, + getEvents: () => [], + testState: () => void 0, + testTransition: () => void 0 + }; + } + + constructor( + public behavior: SimpleBehavior, + public testContext: TTestContext, + options?: Partial> + ) { + this.options = { + ...this.getDefaultOptions(), + ...options + }; + } + + public getShortestPlans( + options?: Partial> + ): Array> { + const shortestPaths = depthShortestPaths( + this.behavior, + this.resolveOptions(options) + ); + + return Object.values(shortestPaths); + } + + public getShortestPlansTo( + stateValue: StatePredicate + ): Array> { + let minWeight = Infinity; + let shortestPlans: Array> = []; + + const plans = this.filterPathsTo(stateValue, this.getShortestPlans()); + + 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; + } + + public getSimplePlans( + options?: Partial> + ): Array> { + const simplePaths = depthSimplePaths( + this.behavior, + this.resolveOptions(options) + ); + + return Object.values(simplePaths); + } + + public getSimplePlansTo( + predicate: StatePredicate + ): Array> { + return depthSimplePathsTo(this.behavior, predicate, this.options); + } + + private filterPathsTo( + statePredicate: StatePredicate, + testPlans: Array> + ): Array> { + const predicate: StatePredicate = (state) => statePredicate(state); + + return testPlans.filter((testPlan) => { + return predicate(testPlan.state); + }); + } + + public getPlanFromEvents( + events: TEvent[], + statePredicate: StatePredicate + ): StatePlan { + 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}` + ); + } + + const plan: StatePlan = { + state: path.state, + paths: [path] + }; + + return plan; + } + + public async testPlan( + plan: StatePlan, + testContext: TTestContext + ) { + for (const path of plan.paths) { + await this.testPath(path, testContext); + } + } + + public async testPath( + path: StatePath, + testContext: TTestContext + ) { + 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(step.state, testContext); + } catch (err) { + testStepResult.state.error = err; + + throw err; + } + + try { + await this.testTransition(step, testContext); + } catch (err) { + testStepResult.event.error = err; + + throw err; + } + } + + try { + await this.testState(path.state, testContext); + } catch (err) { + testPathResult.state.error = err.message; + throw err; + } + } catch (err) { + // TODO: make option + err.message += formatPathTestResult(path, testPathResult, this.options); + throw err; + } + } + + public async testState( + state: TState, + testContext: TTestContext + ): Promise { + await this.options.testState(state, testContext); + + this.addStateCoverage(state); + } + + private addStateCoverage(_state: TState) { + // TODO + } + + public async testTransition( + step: Step, + testContext: TTestContext + ): Promise { + await this.options.testTransition(step, testContext); + + this.addTransitionCoverage(step); + } + + private addTransitionCoverage(_step: Step) { + // TODO + } + + public getCoverage(options?: CoverageOptions) { + return options; + // const filter = options ? options.filter : undefined; + // const stateNodes = getStateNodes(this.behavior); + // 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 { + return void options; + // 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: TestEventsConfig + ): TestModel { + return new TestModel(this.behavior, this.testContext, { + ...this.options, + getEvents: () => getEventSamples(eventMap), + testTransition: async ({ event }, testContext) => { + const eventConfig = eventMap[event.type]; + + if (!eventConfig) { + return; + } + + const exec = + typeof eventConfig === 'function' ? eventConfig : eventConfig.exec; + + await exec?.(testContext, event); + } + }); + } + + public resolveOptions( + options?: Partial> + ): TestModelOptions { + return { ...this.defaultTraversalOptions, ...this.options, ...options }; + } +} diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index ae4ab9bf9d..405ada4e86 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,291 +1,7 @@ -import { - getPathFromEvents, - SerializedEvent, - serializeState, - SimpleBehavior, - StatePath, - StatePlan, - TraversalOptions -} from '@xstate/graph'; -import { - depthShortestPaths, - depthSimplePaths, - depthSimplePathsTo -} from '@xstate/graph/src/graph'; +import { serializeState, SimpleBehavior } from '@xstate/graph'; import { StateMachine, EventObject, State, StateFrom, EventFrom } from 'xstate'; -import { isMachine } from 'xstate/src/utils'; -import { - TestModelCoverage, - TestModelOptions, - StatePredicate, - CoverageOptions, - TestEventsConfig, - TestPathResult, - TestStepResult -} from './types'; -import { formatPathTestResult, simpleStringify } from './utils'; - -/** - * 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 = { - states: {}, - transitions: {} - }; - public options: TestModelOptions; - public defaultTraversalOptions?: TraversalOptions; - public getDefaultOptions(): TestModelOptions { - return { - serializeState: isMachine(this.behavior) - ? (serializeState as any) - : serializeState, - serializeEvent: (event) => simpleStringify(event) as SerializedEvent, - getEvents: () => [], - testState: () => void 0, - execEvent: () => void 0 - }; - } - - constructor( - public behavior: SimpleBehavior, - public testContext: TTestContext, - options?: Partial> - ) { - this.options = { - ...this.getDefaultOptions(), - ...options - }; - } - - public getShortestPlans( - options?: Partial> - ): Array> { - const shortestPaths = depthShortestPaths( - this.behavior, - this.resolveOptions(options) - ); - - return Object.values(shortestPaths); - } - - public getShortestPlansTo( - stateValue: StatePredicate - ): Array> { - let minWeight = Infinity; - let shortestPlans: Array> = []; - - const plans = this.filterPathsTo(stateValue, this.getShortestPlans()); - - 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; - } - - public getSimplePlans( - options?: Partial> - ): Array> { - const simplePaths = depthSimplePaths( - this.behavior, - this.resolveOptions(options) - ); - - return Object.values(simplePaths); - } - - public getSimplePlansTo( - predicate: StatePredicate - ): Array> { - return depthSimplePathsTo(this.behavior, predicate, this.options); - } - - private filterPathsTo( - statePredicate: StatePredicate, - testPlans: Array> - ): Array> { - const predicate: StatePredicate = (state) => statePredicate(state); - - return testPlans.filter((testPlan) => { - return predicate(testPlan.state); - }); - } - - public getPlanFromEvents( - events: TEvent[], - statePredicate: StatePredicate - ): StatePlan { - 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}` - ); - } - - const plan: StatePlan = { - state: path.state, - paths: [path] - }; - - return plan; - } - - public async testPlan( - plan: StatePlan, - testContext: TTestContext - ) { - for (const path of plan.paths) { - await this.testPath(path, testContext); - } - } - - public async testPath( - path: StatePath, - testContext: TTestContext - ) { - 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(step.state, testContext); - } catch (err) { - testStepResult.state.error = err; - - throw err; - } - - try { - await this.executeEvent(step.event, testContext); - } catch (err) { - testStepResult.event.error = err; - - throw err; - } - } - - try { - await this.testState(path.state, testContext); - } catch (err) { - testPathResult.state.error = err.message; - throw err; - } - } catch (err) { - // TODO: make option - err.message += formatPathTestResult(path, testPathResult, this.options); - throw err; - } - } - - public async testState(state: TState, testContext: TTestContext) { - return await this.options.testState(state, testContext); - } - - public async executeEvent(event: TEvent, testContext: TTestContext) { - return await this.options.execEvent(event, testContext); - } - - public getCoverage(options?: CoverageOptions) { - return options; - // const filter = options ? options.filter : undefined; - // const stateNodes = getStateNodes(this.behavior); - // 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 { - return void options; - // 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: TestEventsConfig - ): TestModel { - return new TestModel(this.behavior, this.testContext, { - ...this.options, - getEvents: () => getEventSamples(eventMap), - execEvent: async (event, testContext) => { - const eventConfig = eventMap[event.type]; - - if (!eventConfig) { - return; - } - - const exec = - typeof eventConfig === 'function' ? eventConfig : eventConfig.exec; - - await exec?.(testContext, event); - } - }); - } - - public resolveOptions( - options?: Partial> - ): TestModelOptions { - return { ...this.defaultTraversalOptions, ...this.options, ...options }; - } -} +import { TestModel } from './TestModel'; +import { TestModelOptions, TestEventsConfig } from './types'; export function getEventSamples( eventsOptions: TestEventsConfig diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index fdf1e568bd..7910c16613 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -145,12 +145,12 @@ export interface TestEventsConfig { export interface TestModelOptions< TState, - TEvents extends EventObject, + TEvent extends EventObject, TTestContext -> extends TraversalOptions { +> extends TraversalOptions { testState: (state: TState, testContext: TTestContext) => void | Promise; - execEvent: ( - event: TEvents, + testTransition: ( + step: Step, testContext: TTestContext ) => void | Promise; } diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index b10e10a961..5361427ea9 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,6 +1,7 @@ // nothing yet -import { createTestModel, getDescription } from '../src'; +import { createTestModel } from '../src'; import { assign, createMachine } from 'xstate'; +import { getDescription } from '../src/utils'; interface DieHardContext { three: number; @@ -154,20 +155,22 @@ const dieHardModel = createTestModel(dieHardMachine, null as any).withEvents({ }); describe('testing a model (shortestPathsTo)', () => { - dieHardModel.getShortestPlansTo('success').forEach((plan) => { - describe(`plan ${getDescription(plan.state)}`, () => { - it('should generate a single path', () => { - expect(plan.paths.length).toEqual(1); - }); + dieHardModel + .getShortestPlansTo((state) => state.matches('success')) + .forEach((plan) => { + describe(`plan ${getDescription(plan.state)}`, () => { + it('should generate a single path', () => { + expect(plan.paths.length).toEqual(1); + }); - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, () => { - const testJugs = new Jugs(); - return dieHardModel.testPath(path, { jugs: testJugs }); + plan.paths.forEach((path) => { + it(`path ${getDescription(path.state)}`, () => { + const testJugs = new Jugs(); + return dieHardModel.testPath(path, { jugs: testJugs }); + }); }); }); }); - }); }); describe('testing a model (simplePathsTo)', () => { @@ -268,14 +271,18 @@ describe('error path trace', () => { } }); - testModel.getShortestPlansTo('third').forEach((plan) => { - plan.paths.forEach((path) => { - it('should show an error path trace', async () => { - try { - await testModel.testPath(path, undefined); - } catch (err) { - expect(err.message).toEqual(expect.stringContaining('test error')); - expect(err.message).toMatchInlineSnapshot(` + testModel + .getShortestPlansTo((state) => state.matches('third')) + .forEach((plan) => { + plan.paths.forEach((path) => { + it('should show an error path trace', async () => { + try { + await testModel.testPath(path, undefined); + } catch (err) { + expect(err.message).toEqual( + expect.stringContaining('test error') + ); + expect(err.message).toMatchInlineSnapshot(` "test error Path: State: \\"first\\" @@ -286,13 +293,13 @@ describe('error path trace', () => { State: \\"third\\"" `); - return; - } + return; + } - throw new Error('Should have failed'); + throw new Error('Should have failed'); + }); }); }); - }); }); }); From 6f0abfba06524d336eda4316a88c76206a671467 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 14 Feb 2022 09:58:57 -0500 Subject: [PATCH 017/394] Renaming --- packages/xstate-graph/src/graph.ts | 74 ++++++++------------------- packages/xstate-test/src/TestModel.ts | 14 ++--- 2 files changed, 29 insertions(+), 59 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 80c0e8f600..81806fe21f 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -201,7 +201,7 @@ export function getShortestPaths< options, defaultMachineStateOptions ); - return depthShortestPaths( + return traverseShortestPaths( { transition: (state, event) => machine.transition(state, event), initialState: machine.initialState @@ -210,14 +210,14 @@ export function getShortestPaths< ); } -export function depthShortestPaths( +export function traverseShortestPaths( behavior: SimpleBehavior, options?: TraversalOptions ): StatePathsMap { const optionsWithDefaults = resolveTraversalOptions(options); const { serializeState } = optionsWithDefaults; - const adjacency = depthFirstTraversal(behavior, optionsWithDefaults); + const adjacency = performDepthFirstTraversal(behavior, optionsWithDefaults); // weight, state, event const weightMap = new Map< @@ -300,7 +300,10 @@ export function getSimplePaths< defaultMachineStateOptions ); - return depthSimplePaths(machine as SimpleBehavior, resolvedOptions); + return traverseSimplePaths( + machine as SimpleBehavior, + resolvedOptions + ); } export function toDirectedGraph(stateNode: AnyStateNode): DirectedGraphNode { @@ -362,7 +365,7 @@ export function getPathFromEvents< const { serializeState } = optionsWithDefaults; - const adjacency = depthFirstTraversal(behavior, optionsWithDefaults); + const adjacency = performDepthFirstTraversal(behavior, optionsWithDefaults); const stateMap = new Map(); const path: Steps = []; @@ -405,7 +408,7 @@ interface AdjMap { [key: SerializedState]: { [key: SerializedEvent]: TState }; } -export function depthFirstTraversal( +export function performDepthFirstTraversal( behavior: SimpleBehavior, options: TraversalOptions ): AdjMap { @@ -439,11 +442,11 @@ export function depthFirstTraversal( } function resolveTraversalOptions( - depthOptions?: Partial>, + traversalOptions?: Partial>, defaultOptions?: TraversalOptions ): Required> { const serializeState = - depthOptions?.serializeState ?? + traversalOptions?.serializeState ?? defaultOptions?.serializeState ?? ((state) => JSON.stringify(state) as any); return { @@ -455,20 +458,19 @@ function resolveTraversalOptions( }, getEvents: () => [], ...defaultOptions, - ...depthOptions + ...traversalOptions }; } -export function depthSimplePaths( +export function traverseSimplePaths( behavior: SimpleBehavior, options: TraversalOptions ): StatePathsMap { const { initialState } = behavior; const resolvedOptions = resolveTraversalOptions(options); const { serializeState, visitCondition } = resolvedOptions; - const adjacency = depthFirstTraversal(behavior, resolvedOptions); + const adjacency = performDepthFirstTraversal(behavior, resolvedOptions); const stateMap = new Map(); - // const visited = new Set(); const visitCtx: VisitedContext = { vertices: new Set(), edges: new Set() @@ -552,25 +554,25 @@ export function filterPlans( return filteredPlans; } -export function depthSimplePathsTo( +export function traverseSimplePathsTo( behavior: SimpleBehavior, predicate: (state: TState) => boolean, options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const simplePlansMap = depthSimplePaths(behavior, resolvedOptions); + const simplePlansMap = traverseSimplePaths(behavior, resolvedOptions); return filterPlans(simplePlansMap, predicate); } -export function depthSimplePathsFromTo( +export function traverseSimplePathsFromTo( behavior: SimpleBehavior, fromPredicate: (state: TState) => boolean, toPredicate: (state: TState) => boolean, options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const simplePlansMap = depthSimplePaths(behavior, resolvedOptions); + const simplePlansMap = traverseSimplePaths(behavior, resolvedOptions); // Return all plans that contain a "from" state and target a "to" state return filterPlans(simplePlansMap, (state, plan) => { @@ -580,25 +582,25 @@ export function depthSimplePathsFromTo( }); } -export function depthShortestPathsTo( +export function traverseShortestPathsTo( behavior: SimpleBehavior, predicate: (state: TState) => boolean, options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const simplePlansMap = depthShortestPaths(behavior, resolvedOptions); + const simplePlansMap = traverseShortestPaths(behavior, resolvedOptions); return filterPlans(simplePlansMap, predicate); } -export function depthShortestPathsFromTo( +export function traverseShortestPathsFromTo( behavior: SimpleBehavior, fromPredicate: (state: TState) => boolean, toPredicate: (state: TState) => boolean, options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const shortesPlansMap = depthShortestPaths(behavior, resolvedOptions); + const shortesPlansMap = traverseShortestPaths(behavior, resolvedOptions); // Return all plans that contain a "from" state and target a "to" state return filterPlans(shortesPlansMap, (state, plan) => { @@ -607,35 +609,3 @@ export function depthShortestPathsFromTo( ); }); } - -// type EventCases = Array< -// Omit & { type?: TEvent['type'] } -// >; - -// export function generateEvents( -// blah: { -// [K in TEvent['type']]: TEvent extends { type: K } -// ? EventCases | ((state: TState) => EventCases) -// : never; -// } -// ): (state: TState) => TEvent[] { -// return (state) => { -// const events: TEvent[] = []; - -// Object.keys(blah).forEach((key) => { -// const cases = blah[key as TEvent['type']]; - -// const foo = -// typeof cases === 'function' ? cases(state) : (cases as TEvent[]); - -// foo.forEach((payload) => { -// events.push({ -// type: key, -// ...payload -// }); -// }); -// }); - -// return events; -// }; -// } diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 191bc3d2e3..65a5d3817e 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -9,12 +9,12 @@ import { TraversalOptions } from '@xstate/graph'; import { - depthShortestPaths, - depthSimplePaths, - depthSimplePathsTo + traverseShortestPaths, + traverseSimplePaths, + traverseSimplePathsTo } from '@xstate/graph/src/graph'; import { EventObject } from 'xstate'; -import { +import type { TestModelCoverage, TestModelOptions, StatePredicate, @@ -78,7 +78,7 @@ export class TestModel { public getShortestPlans( options?: Partial> ): Array> { - const shortestPaths = depthShortestPaths( + const shortestPaths = traverseShortestPaths( this.behavior, this.resolveOptions(options) ); @@ -110,7 +110,7 @@ export class TestModel { public getSimplePlans( options?: Partial> ): Array> { - const simplePaths = depthSimplePaths( + const simplePaths = traverseSimplePaths( this.behavior, this.resolveOptions(options) ); @@ -121,7 +121,7 @@ export class TestModel { public getSimplePlansTo( predicate: StatePredicate ): Array> { - return depthSimplePathsTo(this.behavior, predicate, this.options); + return traverseSimplePathsTo(this.behavior, predicate, this.options); } private filterPathsTo( From 91e9653a1a07f86f0e910ecf2f5049ff8c6b29d4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 14 Feb 2022 10:32:06 -0500 Subject: [PATCH 018/394] Fix tests --- packages/xstate-graph/src/graph.ts | 8 +- .../test/__snapshots__/graph.test.ts.snap | 334 +++++++++--------- packages/xstate-graph/test/graph.test.ts | 10 +- 3 files changed, 176 insertions(+), 176 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 81806fe21f..94eb73077a 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -195,7 +195,7 @@ export function getShortestPaths< TEvent extends EventObject = EventObject >( machine: StateMachine, - options?: TraversalOptions, TEvent> + options?: Partial, TEvent>> ): StatePathsMap, TEvent> { const resolvedOptions = resolveTraversalOptions( options, @@ -212,7 +212,7 @@ export function getShortestPaths< export function traverseShortestPaths( behavior: SimpleBehavior, - options?: TraversalOptions + options?: Partial> ): StatePathsMap { const optionsWithDefaults = resolveTraversalOptions(options); const { serializeState } = optionsWithDefaults; @@ -293,7 +293,7 @@ export function getSimplePaths< TEvent extends EventObject = EventObject >( machine: StateMachine, - options?: TraversalOptions, TEvent> + options?: Partial, TEvent>> ): StatePathsMap, TEvent> { const resolvedOptions = resolveTraversalOptions( options, @@ -464,7 +464,7 @@ function resolveTraversalOptions( export function traverseSimplePaths( behavior: SimpleBehavior, - options: TraversalOptions + options: Partial> ): StatePathsMap { const { initialState } = behavior; const resolvedOptions = resolveTraversalOptions(options); diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index a8bb66e895..4de08376ee 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -7,22 +7,22 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": Object { "red": "walk", }, - "type": "TIMER", }, Object { + "eventType": "POWER_OUTAGE", "state": "green", - "type": "POWER_OUTAGE", }, ], } @@ -35,8 +35,8 @@ Object { "state": "bar", "steps": Array [ Object { + "eventType": "EVENT", "state": "pending", - "type": "EVENT", }, ], }, @@ -46,8 +46,8 @@ Object { "state": "foo", "steps": Array [ Object { + "eventType": "STATE", "state": "pending", - "type": "STATE", }, ], }, @@ -80,11 +80,11 @@ Object { }, "steps": Array [ Object { + "eventType": "2", "state": Object { "a": "a1", "b": "b1", }, - "type": "2", }, ], }, @@ -97,11 +97,11 @@ Object { }, "steps": Array [ Object { + "eventType": "3", "state": Object { "a": "a1", "b": "b1", }, - "type": "3", }, ], }, @@ -122,8 +122,8 @@ Object { "state": "yellow", "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, ], }, @@ -135,8 +135,8 @@ Object { }, "steps": Array [ Object { + "eventType": "POWER_OUTAGE", "state": "green", - "type": "POWER_OUTAGE", }, ], }, @@ -148,24 +148,24 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "walk", }, - "type": "PED_COUNTDOWN", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "wait", }, - "type": "PED_COUNTDOWN", }, ], }, @@ -177,18 +177,18 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "walk", }, - "type": "PED_COUNTDOWN", }, ], }, @@ -200,12 +200,12 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, ], }, @@ -226,8 +226,8 @@ Object { "state": "yellow", "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, ], }, @@ -239,30 +239,30 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "walk", }, - "type": "PED_COUNTDOWN", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "wait", }, - "type": "PED_COUNTDOWN", }, Object { + "eventType": "POWER_OUTAGE", "state": Object { "red": "stop", }, - "type": "POWER_OUTAGE", }, ], }, @@ -272,24 +272,24 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "walk", }, - "type": "PED_COUNTDOWN", }, Object { + "eventType": "POWER_OUTAGE", "state": Object { "red": "wait", }, - "type": "POWER_OUTAGE", }, ], }, @@ -299,18 +299,18 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "POWER_OUTAGE", "state": Object { "red": "walk", }, - "type": "POWER_OUTAGE", }, ], }, @@ -320,12 +320,12 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "POWER_OUTAGE", "state": "yellow", - "type": "POWER_OUTAGE", }, ], }, @@ -335,8 +335,8 @@ Object { }, "steps": Array [ Object { + "eventType": "POWER_OUTAGE", "state": "green", - "type": "POWER_OUTAGE", }, ], }, @@ -348,24 +348,24 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "walk", }, - "type": "PED_COUNTDOWN", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "wait", }, - "type": "PED_COUNTDOWN", }, ], }, @@ -377,18 +377,18 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, Object { + "eventType": "PED_COUNTDOWN", "state": Object { "red": "walk", }, - "type": "PED_COUNTDOWN", }, ], }, @@ -400,12 +400,12 @@ Object { }, "steps": Array [ Object { + "eventType": "TIMER", "state": "green", - "type": "TIMER", }, Object { + "eventType": "TIMER", "state": "yellow", - "type": "TIMER", }, ], }, @@ -432,11 +432,11 @@ Object { }, "steps": Array [ Object { + "eventType": "2", "state": Object { "a": "a1", "b": "b1", }, - "type": "2", }, ], }, @@ -449,18 +449,18 @@ Object { }, "steps": Array [ Object { + "eventType": "2", "state": Object { "a": "a1", "b": "b1", }, - "type": "2", }, Object { + "eventType": "3", "state": Object { "a": "a2", "b": "b2", }, - "type": "3", }, ], }, @@ -471,11 +471,11 @@ Object { }, "steps": Array [ Object { + "eventType": "3", "state": Object { "a": "a1", "b": "b1", }, - "type": "3", }, ], }, @@ -496,8 +496,8 @@ Object { "state": "b", "steps": Array [ Object { + "eventType": "FOO", "state": "a", - "type": "FOO", }, ], }, @@ -505,8 +505,8 @@ Object { "state": "b", "steps": Array [ Object { + "eventType": "BAR", "state": "a", - "type": "BAR", }, ], }, @@ -521,16 +521,16 @@ Object { "state": "finish", "steps": Array [ Object { + "eventType": "INC", "state": "start", - "type": "INC", }, Object { + "eventType": "INC", "state": "start", - "type": "INC", }, Object { + "eventType": "INC", "state": "start", - "type": "INC", }, ], }, @@ -546,8 +546,8 @@ Object { "state": "start", "steps": Array [ Object { + "eventType": "INC", "state": "start", - "type": "INC", }, ], }, @@ -557,12 +557,12 @@ Object { "state": "start", "steps": Array [ Object { + "eventType": "INC", "state": "start", - "type": "INC", }, Object { + "eventType": "INC", "state": "start", - "type": "INC", }, ], }, @@ -676,79 +676,20 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "a", "state": 0, - "type": "b", - }, - ], - }, - ], - "0 | {\\"type\\":\\"reset\\"}": Array [ - Object { - "state": 0, - "steps": Array [ - Object { - "state": 0, - "type": "reset", - }, - ], - }, - ], - "1 | {\\"type\\":\\"a\\"}": Array [ - Object { - "state": 1, - "steps": Array [ - Object { - "state": 0, - "type": "a", - }, - ], - }, - ], - "2 | {\\"type\\":\\"b\\"}": Array [ - Object { - "state": 2, - "steps": Array [ - Object { - "state": 0, - "type": "a", - }, - Object { - "state": 1, - "type": "b", - }, - ], - }, - ], -} -`; - -exports[`simple paths for reducers 1`] = ` -Object { - "0 | null": Array [ - Object { - "state": 0, - "steps": Array [], - }, - ], - "0 | {\\"type\\":\\"b\\"}": Array [ - Object { - "state": 0, - "steps": Array [ - Object { - "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, Object { + "eventType": "reset", "state": 2, - "type": "reset", }, Object { + "eventType": "b", "state": 0, - "type": "b", }, ], }, @@ -756,16 +697,16 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "reset", "state": 1, - "type": "reset", }, Object { + "eventType": "b", "state": 0, - "type": "b", }, ], }, @@ -773,8 +714,8 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, ], }, @@ -782,12 +723,12 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "reset", "state": 0, - "type": "reset", }, Object { + "eventType": "b", "state": 0, - "type": "b", }, ], }, @@ -797,16 +738,16 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, Object { + "eventType": "reset", "state": 2, - "type": "reset", }, ], }, @@ -814,12 +755,12 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "reset", "state": 1, - "type": "reset", }, ], }, @@ -827,20 +768,20 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, Object { + "eventType": "reset", "state": 2, - "type": "reset", }, ], }, @@ -848,16 +789,16 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "reset", "state": 1, - "type": "reset", }, ], }, @@ -865,12 +806,12 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "reset", "state": 0, - "type": "reset", }, ], }, @@ -878,8 +819,8 @@ Object { "state": 0, "steps": Array [ Object { + "eventType": "reset", "state": 0, - "type": "reset", }, ], }, @@ -889,8 +830,8 @@ Object { "state": 1, "steps": Array [ Object { + "eventType": "a", "state": 0, - "type": "a", }, ], }, @@ -898,12 +839,12 @@ Object { "state": 1, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, ], }, @@ -911,16 +852,16 @@ Object { "state": 1, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "reset", "state": 0, - "type": "reset", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, ], }, @@ -928,12 +869,12 @@ Object { "state": 1, "steps": Array [ Object { + "eventType": "reset", "state": 0, - "type": "reset", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, ], }, @@ -941,16 +882,16 @@ Object { "state": 1, "steps": Array [ Object { + "eventType": "reset", "state": 0, - "type": "reset", }, Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, ], }, @@ -960,12 +901,12 @@ Object { "state": 2, "steps": Array [ Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, ], }, @@ -973,16 +914,16 @@ Object { "state": 2, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, ], }, @@ -990,20 +931,20 @@ Object { "state": 2, "steps": Array [ Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "reset", "state": 0, - "type": "reset", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, ], }, @@ -1011,16 +952,16 @@ Object { "state": 2, "steps": Array [ Object { + "eventType": "reset", "state": 0, - "type": "reset", }, Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, ], }, @@ -1028,20 +969,79 @@ Object { "state": 2, "steps": Array [ Object { + "eventType": "reset", "state": 0, - "type": "reset", }, Object { + "eventType": "b", "state": 0, - "type": "b", }, Object { + "eventType": "a", + "state": 0, + }, + Object { + "eventType": "b", + "state": 1, + }, + ], + }, + ], +} +`; + +exports[`simple paths for reducers 1`] = ` +Object { + "0 | null": Array [ + Object { + "state": 0, + "steps": Array [], + }, + ], + "0 | {\\"type\\":\\"b\\"}": Array [ + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "b", + "state": 0, + }, + ], + }, + ], + "0 | {\\"type\\":\\"reset\\"}": Array [ + Object { + "state": 0, + "steps": Array [ + Object { + "eventType": "reset", + "state": 0, + }, + ], + }, + ], + "1 | {\\"type\\":\\"a\\"}": Array [ + Object { + "state": 1, + "steps": Array [ + Object { + "eventType": "a", + "state": 0, + }, + ], + }, + ], + "2 | {\\"type\\":\\"b\\"}": Array [ + Object { + "state": 2, + "steps": Array [ + Object { + "eventType": "a", "state": 0, - "type": "a", }, Object { + "eventType": "b", "state": 1, - "type": "b", }, ], }, diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 0ea696df59..9b32f8ee9e 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -10,8 +10,8 @@ import { } from '../src/index'; import { getAdjacencyMap, - depthSimplePaths, - depthShortestPaths + traverseShortestPaths, + traverseSimplePaths } from '../src/graph'; import { assign } from 'xstate'; import { mapValues } from 'xstate/lib/utils'; @@ -29,7 +29,7 @@ function getPathSnapshot(path: StatePath) { 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, - type: step.event.type + eventType: step.event.type })) }; } @@ -489,7 +489,7 @@ describe('@xstate/graph', () => { }); it('simple paths for reducers', () => { - const a = depthSimplePaths( + const a = traverseShortestPaths( { transition: (s, e) => { if (e.type === 'a') { @@ -516,7 +516,7 @@ it('simple paths for reducers', () => { }); it('shortest paths for reducers', () => { - const a = depthShortestPaths( + const a = traverseSimplePaths( { transition: (s, e) => { if (e.type === 'a') { From acc71948c2ea3c2019e8b15d4d874e933f866654 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 14 Feb 2022 10:43:55 -0500 Subject: [PATCH 019/394] Remove obsolette snapshot --- .../test/__snapshots__/index.test.ts.snap | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 packages/xstate-test/test/__snapshots__/index.test.ts.snap 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\\" " -`; From 74fed2a9eebba5c091c1512217f7986f9ad52f7a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 26 Feb 2022 13:54:01 -0600 Subject: [PATCH 020/394] Fix graph tests --- packages/xstate-graph/src/graph.ts | 11 ++++------- packages/xstate-graph/test/graph.test.ts | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 8b2f5a6016..e5d4d2e331 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -194,13 +194,10 @@ const defaultMachineStateOptions: TraversalOptions, any> = { } }; -export function getShortestPaths< - TContext = DefaultContext, - TEvent extends EventObject = EventObject ->( - machine: StateMachine, - options?: Partial, TEvent>> -): StatePathsMap, TEvent> { +export function getShortestPaths( + machine: TMachine, + options?: Partial> +): StatePathsMap { const resolvedOptions = resolveTraversalOptions( options, defaultMachineStateOptions diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 9b32f8ee9e..e42ba2ecd0 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -209,7 +209,7 @@ describe('@xstate/graph', () => { it('should represent conditional paths based on context', () => { // explicit type arguments could be removed once davidkpiano/xstate#652 gets resolved - const paths = getShortestPaths( + const paths = getShortestPaths( condMachine.withContext({ id: 'foo' }), From 522c78cd9d6859d86b4a148c9f11bbec5725f707 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 26 Feb 2022 16:13:04 -0600 Subject: [PATCH 021/394] Work on coverage --- packages/xstate-test/src/TestModel.ts | 31 +-- packages/xstate-test/test/index.test.ts | 290 ++++++++++++------------ 2 files changed, 158 insertions(+), 163 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 65a5d3817e..9624790ba7 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -48,7 +48,7 @@ import { getEventSamples } from './index'; */ export class TestModel { - public coverage: TestModelCoverage = { + private _coverage: TestModelCoverage = { states: {}, transitions: {} }; @@ -226,8 +226,12 @@ export class TestModel { this.addStateCoverage(state); } - private addStateCoverage(_state: TState) { - // TODO + private addStateCoverage(state: TState) { + const stateSerial = this.options.serializeState(state, null as any); // TODO: fix + + const coverage = this._coverage.states[stateSerial] ?? 0; + + this._coverage.states[stateSerial] = coverage + 1; } public async testTransition( @@ -243,23 +247,6 @@ export class TestModel { // TODO } - public getCoverage(options?: CoverageOptions) { - return options; - // const filter = options ? options.filter : undefined; - // const stateNodes = getStateNodes(this.behavior); - // 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 { return void options; // const coverage = this.getCoverage(options); @@ -300,4 +287,8 @@ export class TestModel { ): TestModelOptions { return { ...this.defaultTraversalOptions, ...this.options, ...options }; } + + public getCoverage(): any { + return this._coverage; + } } diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 5361427ea9..c115f0b5ba 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -154,7 +154,7 @@ const dieHardModel = createTestModel(dieHardMachine, null as any).withEvents({ } }); -describe('testing a model (shortestPathsTo)', () => { +describe.only('testing a model (shortestPathsTo)', () => { dieHardModel .getShortestPlansTo((state) => state.matches('success')) .forEach((plan) => { @@ -303,148 +303,152 @@ describe('error path trace', () => { }); }); -// 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.only('tests missing state node coverage', async () => { -// const machine = createMachine({ -// id: 'missing', -// initial: 'first', -// states: { -// first: { -// on: { NEXT: 'third' }, -// meta: { -// test: () => true -// } -// }, -// second: { -// meta: { -// test: () => true -// } -// }, -// third: { -// initial: 'one', -// states: { -// one: { -// meta: { -// test: () => true -// }, -// on: { -// NEXT: 'two' -// } -// }, -// two: { -// meta: { -// test: () => true -// } -// }, -// three: { -// meta: { -// test: () => true -// } -// } -// }, -// meta: { -// test: () => true -// } -// } -// } -// }); - -// const testModel = createTestModel(machine, undefined).withEvents({ -// NEXT: () => { -// /* ... */ -// } -// }); - -// const plans = testModel.getShortestPathPlans(); - -// for (const plan of plans) { -// await testModel.testPlan(plan, undefined); -// } - -// // 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: { -// always: 'end' -// }, -// end: { -// type: 'final', -// meta: { -// test: () => { -// /* ... */ -// } -// } -// } -// } -// }); - -// const testModel = createTestModel(TestBug, undefined).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(); -// }); -// }); +describe.only('coverage', () => { + it('reports state node coverage', () => { + const coverage = dieHardModel.getCoverage(); + + expect(coverage.states['"pending" | {"three":0,"five":0}']).toBeGreaterThan( + 0 + ); + expect(coverage.states['"success" | {"three":3,"five":4}']).toBeGreaterThan( + 0 + ); + }); + + it('tests missing state node coverage', async () => { + const machine = createMachine({ + id: 'missing', + initial: 'first', + states: { + first: { + on: { NEXT: 'third' }, + meta: { + test: () => true + } + }, + second: { + meta: { + test: () => true + } + }, + third: { + initial: 'one', + states: { + one: { + meta: { + test: () => true + }, + on: { + NEXT: 'two' + } + }, + two: { + meta: { + test: () => true + } + }, + three: { + meta: { + test: () => true + } + } + }, + meta: { + test: () => true + } + } + } + }); + + const testModel = createTestModel(machine, undefined).withEvents({ + NEXT: () => { + /* ... */ + } + }); + + const plans = testModel.getShortestPlans(); + + for (const plan of plans) { + await testModel.testPlan(plan, undefined); + } + + // 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.skip('skips filtered states (filter option)', async () => { + const TestBug = createMachine({ + id: 'testbug', + initial: 'idle', + context: { + retries: 0 + }, + states: { + idle: { + on: { + START: 'passthrough' + }, + meta: { + test: () => { + /* ... */ + } + } + }, + passthrough: { + always: 'end' + }, + end: { + type: 'final', + meta: { + test: () => { + /* ... */ + } + } + } + } + }); + + const testModel = createTestModel(TestBug, undefined).withEvents({ + START: () => { + /* ... */ + } + }); + + const testPlans = testModel.getShortestPlans(); + + const promises: any[] = []; + testPlans.forEach((plan) => { + plan.paths.forEach(() => { + promises.push(testModel.testPlan(plan, undefined)); + }); + }); + + await Promise.all(promises); + + expect(() => { + testModel.testCoverage({ + filter: (stateNode) => { + return !!stateNode.meta; + } + }); + }).not.toThrow(); + }); +}); describe('events', () => { it('should allow for representing many cases', async () => { From a65de17e64107ad14a86497fa2de5914f5524e57 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 28 Feb 2022 00:11:44 -0500 Subject: [PATCH 022/394] Adjust AdjMap; start trying coverage ideas --- packages/xstate-graph/src/graph.ts | 67 ++++++++++++++++--------- packages/xstate-test/src/TestModel.ts | 30 ++++++++++- packages/xstate-test/src/coverage.ts | 16 ++++++ packages/xstate-test/src/types.ts | 6 +++ packages/xstate-test/test/index.test.ts | 21 ++------ 5 files changed, 96 insertions(+), 44 deletions(-) create mode 100644 packages/xstate-test/src/coverage.ts diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index e5d4d2e331..ad76820f31 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -226,36 +226,46 @@ export function traverseShortestPaths( [number, SerializedState | undefined, SerializedEvent | undefined] >(); const stateMap = new Map(); - const initialVertex = serializeState(behavior.initialState, null); - stateMap.set(initialVertex, behavior.initialState); + const initialSerializedState = serializeState(behavior.initialState, null); + stateMap.set(initialSerializedState, behavior.initialState); - weightMap.set(initialVertex, [0, undefined, undefined]); + 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]) as SerializedEvent[]) { + for (const serializedState of unvisited) { + const [weight] = weightMap.get(serializedState)!; + for (const event of Object.keys( + adjacency[serializedState].transitions + ) as SerializedEvent[]) { const eventObject = JSON.parse(event); - const nextState = adjacency[vertex][event]; - const nextVertex = serializeState(nextState, eventObject); - stateMap.set(nextVertex, nextState); - if (!weightMap.has(nextVertex)) { - weightMap.set(nextVertex, [weight + 1, vertex, event]); + const nextState = adjacency[serializedState].transitions[event]; + const nextSerializedState = serializeState(nextState, eventObject); + stateMap.set(nextSerializedState, nextState); + if (!weightMap.has(nextSerializedState)) { + weightMap.set(nextSerializedState, [ + weight + 1, + serializedState, + event + ]); } 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, + event + ]); } } - 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); } } @@ -383,7 +393,7 @@ export function getPathFromEvents< }); const eventSerial = serializeEvent(event); - const nextState = adjacency[stateSerial][eventSerial]; + const nextState = adjacency[stateSerial].transitions[eventSerial]; if (!nextState) { throw new Error( @@ -406,7 +416,10 @@ export function getPathFromEvents< } interface AdjMap { - [key: SerializedState]: { [key: SerializedEvent]: TState }; + [key: SerializedState]: { + state: TState; + transitions: { [key: SerializedEvent]: TState }; + }; } export function performDepthFirstTraversal( @@ -423,7 +436,10 @@ export function performDepthFirstTraversal( return; } - adj[serializedState] = {}; + adj[serializedState] = { + state, + transitions: {} + }; const events = getEvents(state); @@ -431,7 +447,7 @@ export function performDepthFirstTraversal( const nextState = transition(state, subEvent); if (!options.filter || options.filter(nextState, subEvent)) { - adj[serializedState][JSON.stringify(subEvent)] = nextState; + adj[serializedState].transitions[JSON.stringify(subEvent)] = nextState; util(nextState, subEvent); } } @@ -509,12 +525,13 @@ export function traverseSimplePaths( toStatePlan.paths.push(path2); } else { for (const serializedEvent of Object.keys( - adjacency[fromStateSerial] + adjacency[fromStateSerial].transitions ) as SerializedEvent[]) { const subEvent = JSON.parse(serializedEvent); - const nextState = adjacency[fromStateSerial][serializedEvent]; + const nextState = + adjacency[fromStateSerial].transitions[serializedEvent]; - if (!(serializedEvent in adjacency[fromStateSerial])) { + if (!(serializedEvent in adjacency[fromStateSerial].transitions)) { continue; } diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 9624790ba7..b2b3a97cb7 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -9,6 +9,7 @@ import { TraversalOptions } from '@xstate/graph'; import { + performDepthFirstTraversal, traverseShortestPaths, traverseSimplePaths, traverseSimplePathsTo @@ -21,7 +22,8 @@ import type { CoverageOptions, TestEventsConfig, TestPathResult, - TestStepResult + TestStepResult, + Criterion } from './types'; import { formatPathTestResult, simpleStringify } from './utils'; import { getEventSamples } from './index'; @@ -59,6 +61,7 @@ export class TestModel { serializeState: (state) => simpleStringify(state) as SerializedState, serializeEvent: (event) => simpleStringify(event) as SerializedEvent, getEvents: () => [], + getStates: () => [], testState: () => void 0, testTransition: () => void 0 }; @@ -157,6 +160,11 @@ export class TestModel { return plan; } + public getAllStates(): TState[] { + const adj = performDepthFirstTraversal(this.behavior, this.options); + return Object.values(adj).map((x) => x.state); + } + public async testPlan( plan: StatePlan, testContext: TTestContext @@ -291,4 +299,24 @@ export class TestModel { public getCoverage(): any { return this._coverage; } + + public covers( + criteriaFn: (testModel: this) => Array> + ): boolean { + const criteria = criteriaFn(this); + const criteriaSet = new Set(criteria); + const states = this.getAllStates(); + + states.forEach((state) => { + criteriaSet.forEach((criterion) => { + if (criterion.predicate(state)) { + criteriaSet.delete(criterion); + } + }); + }); + + console.log(Array.from(criteriaSet).map((c) => c.label)); + + return criteriaSet.size === 0; + } } diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts new file mode 100644 index 0000000000..12dbdd4772 --- /dev/null +++ b/packages/xstate-test/src/coverage.ts @@ -0,0 +1,16 @@ +import type { AnyState } from 'xstate'; +import { TestModel } from './TestModel'; +import { Criterion } from './types'; + +export function stateValueCoverage(): ( + testModel: TestModel +) => Array> { + return (testModel) => { + const allStates = testModel.getAllStates(); + + return allStates.map((state) => ({ + predicate: (testedState) => testedState.matches(state.value), + description: `Matches ${JSON.stringify(state.value)}` + })); + }; +} diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 7910c16613..4c16dcb484 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -153,6 +153,7 @@ export interface TestModelOptions< step: Step, testContext: TTestContext ) => void | Promise; + getStates: () => TState[]; } export interface TestModelCoverage { states: Record; @@ -163,6 +164,11 @@ export interface CoverageOptions { filter?: (stateNode: StateNode) => boolean; } +export interface Criterion { + predicate: (state: TState) => boolean; + label?: string; +} + export interface TestTransitionConfig< TContext, TEvent extends EventObject, diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index c115f0b5ba..7ad7d02420 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -2,6 +2,7 @@ import { createTestModel } from '../src'; import { assign, createMachine } from 'xstate'; import { getDescription } from '../src/utils'; +import { stateValueCoverage } from '../src/coverage'; interface DieHardContext { three: number; @@ -372,26 +373,10 @@ describe.only('coverage', () => { await testModel.testPlan(plan, undefined); } - // 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') - // ); - // } + expect(testModel.covers(stateValueCoverage())).toBeTruthy(); }); - it.skip('skips filtered states (filter option)', async () => { + it('skips filtered states (filter option)', async () => { const TestBug = createMachine({ id: 'testbug', initial: 'idle', From 9e6dc419059bc22f8b0b3661bcbcfb1901b031a4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 6 Mar 2022 09:05:29 -0500 Subject: [PATCH 023/394] Remove comment --- packages/xstate-test/test/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 7ad7d02420..f9b930e7c2 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,4 +1,3 @@ -// nothing yet import { createTestModel } from '../src'; import { assign, createMachine } from 'xstate'; import { getDescription } from '../src/utils'; From e7728cc9788b769c8264dc68e12eda9ad83ca5fb Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 6 Mar 2022 09:41:37 -0500 Subject: [PATCH 024/394] Add `traversalLimit`, refactor traversal algorithm --- packages/xstate-graph/src/graph.ts | 24 +++++++++++++++----- packages/xstate-graph/src/types.ts | 7 ++++++ packages/xstate-graph/test/graph.test.ts | 2 +- packages/xstate-test/test/index.test.ts | 29 ++++++++++++++++++++++-- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index ad76820f31..862f8be452 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -427,13 +427,26 @@ export function performDepthFirstTraversal( options: TraversalOptions ): AdjMap { const { transition, initialState } = behavior; - const { serializeState, getEvents } = resolveTraversalOptions(options); + const { + serializeState, + getEvents, + traversalLimit: limit + } = resolveTraversalOptions(options); const adj: AdjMap = {}; - function util(state: TState, event: TEvent | null) { + 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]) { - return; + continue; } adj[serializedState] = { @@ -448,13 +461,11 @@ export function performDepthFirstTraversal( if (!options.filter || options.filter(nextState, subEvent)) { adj[serializedState].transitions[JSON.stringify(subEvent)] = nextState; - util(nextState, subEvent); + queue.push([nextState, subEvent]); } } } - util(initialState, null); - return adj; } @@ -474,6 +485,7 @@ function resolveTraversalOptions( return vctx.vertices.has(serializeState(state, event)); }, getEvents: () => [], + traversalLimit: Infinity, ...defaultOptions, ...traversalOptions }; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 05f72d10a6..35e294ff20 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -147,6 +147,13 @@ export interface TraversalOptions ) => boolean; filter?: (state: TState, event: TEvent) => boolean; getEvents?: (state: TState) => TEvent[]; + /** + * The maximum number of traversals to perform when calculating + * the state transition adjacency map. + * + * @default `Infinity` + */ + traversalLimit?: number; } type Brand = T & { __tag: Tag }; diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index e42ba2ecd0..25ce930db8 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -241,10 +241,10 @@ describe('@xstate/graph', () => { Array [ "\\"green\\"", "\\"yellow\\"", + "{\\"red\\":\\"flashing\\"}", "{\\"red\\":\\"walk\\"}", "{\\"red\\":\\"wait\\"}", "{\\"red\\":\\"stop\\"}", - "{\\"red\\":\\"flashing\\"}", ] `); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index f9b930e7c2..f1e5385a15 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -154,7 +154,7 @@ const dieHardModel = createTestModel(dieHardMachine, null as any).withEvents({ } }); -describe.only('testing a model (shortestPathsTo)', () => { +describe('testing a model (shortestPathsTo)', () => { dieHardModel .getShortestPlansTo((state) => state.matches('success')) .forEach((plan) => { @@ -303,7 +303,7 @@ describe('error path trace', () => { }); }); -describe.only('coverage', () => { +describe('coverage', () => { it('reports state node coverage', () => { const coverage = dieHardModel.getCoverage(); @@ -670,3 +670,28 @@ describe('plan description', () => { `); }); }); + +// 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, null as any).withEvents({ + TOGGLE: () => { + /**/ + } + }); + + expect(() => { + model.getShortestPlans({ traversalLimit: 100 }); + }).toThrowErrorMatchingInlineSnapshot(`"Traversal limit exceeded"`); +}); From 4e2be879a289c8914ba36dcd05d5d402bac45516 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 6 Mar 2022 16:56:57 -0500 Subject: [PATCH 025/394] Add `execute` option --- packages/xstate-graph/src/graph.ts | 10 ++-- packages/xstate-test/src/TestModel.ts | 7 +-- packages/xstate-test/src/index.ts | 30 ++++++++++- packages/xstate-test/src/types.ts | 4 ++ packages/xstate-test/test/index.test.ts | 69 ++++++++++++++++++++----- 5 files changed, 95 insertions(+), 25 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 862f8be452..0e5f940b00 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -76,12 +76,10 @@ export function getChildren(stateNode: AnyStateNode): AnyStateNode[] { export function serializeState( state: State ): SerializedState { - const { value, context } = state; - return (context === undefined - ? JSON.stringify(value) - : JSON.stringify(value) + - ' | ' + - JSON.stringify(context)) as SerializedState; + const { value, context, actions } = state; + return [value, context, actions.map((a) => a.type).join(',')] + .map((x) => JSON.stringify(x)) + .join(' | ') as SerializedState; } export function serializeEvent( diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index b2b3a97cb7..19f848d89e 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -63,7 +63,8 @@ export class TestModel { getEvents: () => [], getStates: () => [], testState: () => void 0, - testTransition: () => void 0 + testTransition: () => void 0, + execute: () => void 0 }; } @@ -231,6 +232,8 @@ export class TestModel { ): Promise { await this.options.testState(state, testContext); + await this.options.execute(state, testContext); + this.addStateCoverage(state); } @@ -315,8 +318,6 @@ export class TestModel { }); }); - console.log(Array.from(criteriaSet).map((c) => c.label)); - return criteriaSet.size === 0; } } diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 405ada4e86..aa15c66258 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,5 +1,13 @@ import { serializeState, SimpleBehavior } from '@xstate/graph'; -import { StateMachine, EventObject, State, StateFrom, EventFrom } from 'xstate'; +import { + EventObject, + State, + StateFrom, + EventFrom, + AnyStateMachine, + ActionObject, + AnyState +} from 'xstate'; import { TestModel } from './TestModel'; import { TestModelOptions, TestEventsConfig } from './types'; @@ -48,6 +56,19 @@ async function assertState(state: State, testContext: any) { } } +function executeAction( + actionObject: ActionObject, + state: AnyState +): void { + if (typeof actionObject.exec == 'function') { + actionObject.exec(state.context, state.event, { + _event: state._event, + action: actionObject, + state + }); + } +} + /** * Creates a test model that represents an abstract model of a * system under test (SUT). @@ -73,7 +94,7 @@ async function assertState(state: State, testContext: any) { * to an event test config (e.g., `{exec: () => {...}, cases: [...]}`) */ export function createTestModel< - TMachine extends StateMachine, + TMachine extends AnyStateMachine, TestContext = any >( machine: TMachine, @@ -89,6 +110,11 @@ export function createTestModel< >(machine as SimpleBehavior, testContext, { serializeState, testState: assertState, + execute: (state) => { + state.actions.forEach((action) => { + executeAction(action, state); + }); + }, ...options }); diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 4c16dcb484..177733d852 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -153,6 +153,10 @@ export interface TestModelOptions< step: Step, testContext: TTestContext ) => void | Promise; + /** + * Executes actions based on the `state` after the state is tested. + */ + execute: (state: TState, testContext: TTestContext) => void | Promise; getStates: () => TState[]; } export interface TestModelCoverage { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index f1e5385a15..b9cd4a9592 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -283,16 +283,16 @@ describe('error path trace', () => { expect.stringContaining('test error') ); expect(err.message).toMatchInlineSnapshot(` - "test error - Path: - State: \\"first\\" - Event: {\\"type\\":\\"NEXT\\"} + "test error + Path: + State: \\"first\\" | | \\"\\" + Event: {\\"type\\":\\"NEXT\\"} - State: \\"second\\" - Event: {\\"type\\":\\"NEXT\\"} + State: \\"second\\" | | \\"\\" + Event: {\\"type\\":\\"NEXT\\"} - State: \\"third\\"" - `); + State: \\"third\\" | | \\"\\"" + `); return; } @@ -307,12 +307,12 @@ describe('coverage', () => { it('reports state node coverage', () => { const coverage = dieHardModel.getCoverage(); - expect(coverage.states['"pending" | {"three":0,"five":0}']).toBeGreaterThan( - 0 - ); - expect(coverage.states['"success" | {"three":3,"five":4}']).toBeGreaterThan( - 0 - ); + expect( + coverage.states['"pending" | {"three":0,"five":0} | ""'] + ).toBeGreaterThan(0); + expect( + coverage.states['"success" | {"three":3,"five":4} | ""'] + ).toBeGreaterThan(0); }); it('tests missing state node coverage', async () => { @@ -695,3 +695,44 @@ it('prevents infinite recursion based on a provided limit', () => { model.getShortestPlans({ traversalLimit: 100 }); }).toThrowErrorMatchingInlineSnapshot(`"Traversal limit exceeded"`); }); + +it('executes actions', async () => { + let executedActive = false; + let executedDone = false; + const machine = createMachine({ + initial: 'idle', + states: { + idle: { + on: { + TOGGLE: { target: 'active', actions: 'boom' } + } + }, + active: { + entry: () => { + executedActive = true; + }, + on: { TOGGLE: 'done' } + }, + done: { + entry: () => { + executedDone = true; + } + } + } + }); + + const model = createTestModel(machine, null as any).withEvents({ + TOGGLE: () => { + /**/ + } + }); + + const testPlans = model.getShortestPlans(); + + for (const plan of testPlans) { + await model.testPlan(plan, undefined); + } + + expect(executedActive).toBe(true); + expect(executedDone).toBe(true); +}); From 161959dfca07c55cacda80cd7ea9be1369b9c92d Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 13 Mar 2022 21:10:35 -0400 Subject: [PATCH 026/394] Remove TestContext from TestModel constructor --- packages/xstate-test/src/TestModel.ts | 3 +- packages/xstate-test/src/index.ts | 7 +- packages/xstate-test/test/index.test.ts | 91 ++++++++++++++++++++++--- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 19f848d89e..b49ba4be69 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -70,7 +70,6 @@ export class TestModel { constructor( public behavior: SimpleBehavior, - public testContext: TTestContext, options?: Partial> ) { this.options = { @@ -275,7 +274,7 @@ export class TestModel { public withEvents( eventMap: TestEventsConfig ): TestModel { - return new TestModel(this.behavior, this.testContext, { + return new TestModel(this.behavior, { ...this.options, getEvents: () => getEventSamples(eventMap), testTransition: async ({ event }, testContext) => { diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index aa15c66258..61292107c2 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -98,7 +98,6 @@ export function createTestModel< TestContext = any >( machine: TMachine, - testContext: TestContext, options?: Partial< TestModelOptions, EventFrom, TestContext> > @@ -107,7 +106,7 @@ export function createTestModel< StateFrom, EventFrom, TestContext - >(machine as SimpleBehavior, testContext, { + >(machine as SimpleBehavior, { serializeState, testState: assertState, execute: (state) => { @@ -115,6 +114,10 @@ export function createTestModel< executeAction(action, state); }); }, + getEvents: (state) => + state.nextEvents.map( + (eventType) => ({ type: eventType } as EventFrom) + ), ...options }); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index b9cd4a9592..5d4f9de276 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -121,7 +121,7 @@ class Jugs { } } -const dieHardModel = createTestModel(dieHardMachine, null as any).withEvents({ +const dieHardModel = createTestModel(dieHardMachine).withEvents({ POUR_3_TO_5: { exec: async ({ jugs }) => { await jugs.transferThree(); @@ -265,7 +265,7 @@ describe('error path trace', () => { } }); - const testModel = createTestModel(machine, undefined).withEvents({ + const testModel = createTestModel(machine).withEvents({ NEXT: () => { /* noop */ } @@ -360,7 +360,7 @@ describe('coverage', () => { } }); - const testModel = createTestModel(machine, undefined).withEvents({ + const testModel = createTestModel(machine).withEvents({ NEXT: () => { /* ... */ } @@ -407,7 +407,7 @@ describe('coverage', () => { } }); - const testModel = createTestModel(TestBug, undefined).withEvents({ + const testModel = createTestModel(TestBug).withEvents({ START: () => { /* ... */ } @@ -518,7 +518,7 @@ describe('events', () => { } }); - const testModel = createTestModel(feedbackMachine, undefined).withEvents({ + const testModel = createTestModel(feedbackMachine).withEvents({ CLICK_BAD: () => { /* ... */ }, @@ -553,7 +553,7 @@ describe('events', () => { } }); - const testModel = createTestModel(testMachine, undefined); + const testModel = createTestModel(testMachine); const testPlans = testModel.getShortestPlans(); @@ -583,7 +583,7 @@ describe('state limiting', () => { } }); - const testModel = createTestModel(machine, undefined).withEvents({ + const testModel = createTestModel(machine).withEvents({ INC: () => {} }); @@ -647,7 +647,7 @@ describe('plan description', () => { } }); - const testModel = createTestModel(machine, undefined).withEvents({ + const testModel = createTestModel(machine).withEvents({ NEXT: { exec: () => {} }, DONE: { exec: () => {} } }); @@ -685,7 +685,7 @@ it('prevents infinite recursion based on a provided limit', () => { } }); - const model = createTestModel(machine, null as any).withEvents({ + const model = createTestModel(machine).withEvents({ TOGGLE: () => { /**/ } @@ -721,7 +721,7 @@ it('executes actions', async () => { } }); - const model = createTestModel(machine, null as any).withEvents({ + const model = createTestModel(machine).withEvents({ TOGGLE: () => { /**/ } @@ -736,3 +736,74 @@ it('executes actions', async () => { 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( + createMachine({ + initial: 'inactive', + states: { + inactive: { + on: { + NEXT: 'active' + } + }, + active: {} + } + }), + { + testState: (state) => { + testedStates.push(state.value); + } + } + ); + + const plans = model.getShortestPlans(); + + for (const plan of plans) { + await model.testPlan(plan, null as any); + } + + expect(testedStates).toEqual(['inactive', 'inactive', 'active']); + }); + + it('options.testTransition(...) should test transition', async () => { + const testedEvents: any[] = []; + + const model = createTestModel( + createMachine({ + initial: 'inactive', + states: { + inactive: { + on: { + NEXT: 'active' + } + }, + active: { + on: { + PREV: 'inactive' + } + } + } + }), + { + // Force traversal to consider all transitions + serializeState: (state) => + ((state.value as any) + state.event.type) as any, + testTransition: (step) => { + testedEvents.push(step.event.type); + } + } + ); + + const plans = model.getShortestPlans(); + + for (const plan of plans) { + await model.testPlan(plan, null as any); + } + + expect(testedEvents).toEqual(['NEXT', 'NEXT', 'PREV']); + }); +}); From c7a118b6213ac7ebc51391a9ce5dc84d1c8a99df Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 16 Mar 2022 09:22:35 -0600 Subject: [PATCH 027/394] Improve coverage --- packages/xstate-test/src/TestModel.ts | 52 +++++----- packages/xstate-test/src/coverage.ts | 11 +- packages/xstate-test/src/types.ts | 29 +++++- packages/xstate-test/src/utils.ts | 6 +- packages/xstate-test/test/index.test.ts | 127 ++++++++++++++++-------- 5 files changed, 149 insertions(+), 76 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index b49ba4be69..9b42a2f5ba 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -23,7 +23,8 @@ import type { TestEventsConfig, TestPathResult, TestStepResult, - Criterion + Criterion, + CriterionResult } from './types'; import { formatPathTestResult, simpleStringify } from './utils'; import { getEventSamples } from './index'; @@ -50,7 +51,7 @@ import { getEventSamples } from './index'; */ export class TestModel { - private _coverage: TestModelCoverage = { + private _coverage: TestModelCoverage = { states: {}, transitions: {} }; @@ -64,7 +65,11 @@ export class TestModel { getStates: () => [], testState: () => void 0, testTransition: () => void 0, - execute: () => void 0 + execute: () => void 0, + logger: { + log: console.log.bind(console), + error: console.error.bind(console) + } }; } @@ -239,9 +244,16 @@ export class TestModel { private addStateCoverage(state: TState) { const stateSerial = this.options.serializeState(state, null as any); // TODO: fix - const coverage = this._coverage.states[stateSerial] ?? 0; + const existingCoverage = this._coverage.states[stateSerial]; - this._coverage.states[stateSerial] = coverage + 1; + if (existingCoverage) { + existingCoverage.count++; + } else { + this._coverage.states[stateSerial] = { + state, + count: 1 + }; + } } public async testTransition( @@ -298,25 +310,17 @@ export class TestModel { return { ...this.defaultTraversalOptions, ...this.options, ...options }; } - public getCoverage(): any { - return this._coverage; - } - - public covers( - criteriaFn: (testModel: this) => Array> - ): boolean { - const criteria = criteriaFn(this); - const criteriaSet = new Set(criteria); - const states = this.getAllStates(); - - states.forEach((state) => { - criteriaSet.forEach((criterion) => { - if (criterion.predicate(state)) { - criteriaSet.delete(criterion); - } - }); + public getCoverage( + criteriaFn?: (testModel: this) => Array> + ): Array> { + const criteria = criteriaFn?.(this) ?? []; + const stateCoverages = Object.values(this._coverage.states); + + return criteria.map((c) => { + return { + criterion: c, + covered: stateCoverages.some((sc) => c.predicate(sc)) + }; }); - - return criteriaSet.size === 0; } } diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index 12dbdd4772..b138b7e6e8 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -1,4 +1,6 @@ +import { AnyStateNode } from '@xstate/graph'; import type { AnyState } from 'xstate'; +import { getAllStateNodes } from 'xstate/lib/stateUtils'; import { TestModel } from './TestModel'; import { Criterion } from './types'; @@ -6,11 +8,12 @@ export function stateValueCoverage(): ( testModel: TestModel ) => Array> { return (testModel) => { - const allStates = testModel.getAllStates(); + const allStateNodes = getAllStateNodes(testModel.behavior as AnyStateNode); - return allStates.map((state) => ({ - predicate: (testedState) => testedState.matches(state.value), - description: `Matches ${JSON.stringify(state.value)}` + return allStateNodes.map((stateNode) => ({ + predicate: (stateCoverage) => + stateCoverage.state.configuration.includes(stateNode), + description: `Visits ${JSON.stringify(stateNode.id)}` })); }; } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 177733d852..6785e87def 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -158,9 +158,22 @@ export interface TestModelOptions< */ execute: (state: TState, testContext: TTestContext) => void | Promise; getStates: () => TState[]; + logger: { + log: (msg: string) => void; + error: (msg: string) => void; + }; +} + +export interface TestStateCoverage { + state: TState; + /** + * Number of times state was visited + */ + count: number; } -export interface TestModelCoverage { - states: Record; + +export interface TestModelCoverage { + states: Record>; transitions: Record>; } @@ -169,8 +182,16 @@ export interface CoverageOptions { } export interface Criterion { - predicate: (state: TState) => boolean; - label?: string; + predicate: (stateCoverage: TestStateCoverage) => boolean; + description?: string; +} + +export interface CriterionResult { + criterion: Criterion; + /** + * Whether the criterion was covered or not + */ + covered: boolean; } export interface TestTransitionConfig< diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts index 42b884b5bb..33ffccffe1 100644 --- a/packages/xstate-test/src/utils.ts +++ b/packages/xstate-test/src/utils.ts @@ -4,7 +4,7 @@ import { SerializedState, StatePath } from '@xstate/graph'; -import { State } from 'xstate'; +import { AnyState } from 'xstate'; import { TestMeta, TestPathResult } from './types'; interface TestResultStringOptions extends SerializationOptions { @@ -73,9 +73,7 @@ export function formatPathTestResult( return errMessage; } -export function getDescription( - state: State -): string { +export function getDescription(state: AnyState): string { const contextString = state.context === undefined ? '' : `(${JSON.stringify(state.context)})`; diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 5d4f9de276..bb470fc057 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -304,67 +304,66 @@ describe('error path trace', () => { }); describe('coverage', () => { - it('reports state node coverage', () => { - const coverage = dieHardModel.getCoverage(); + it('reports state node coverage', async () => { + const plans = dieHardModel.getSimplePlansTo((state) => { + return state.matches('success') && state.context.three === 0; + }); - expect( - coverage.states['"pending" | {"three":0,"five":0} | ""'] - ).toBeGreaterThan(0); - expect( - coverage.states['"success" | {"three":3,"five":4} | ""'] - ).toBeGreaterThan(0); + for (const plan of plans) { + for (const path of plan.paths) { + const jugs = new Jugs(); + await dieHardModel.testPath(path, { jugs }); + } + } + + const coverage = dieHardModel.getCoverage(stateValueCoverage()); + + expect(coverage.every((c) => c.covered)).toEqual(true); + + expect(coverage.map((c) => [c.criterion.description, c.covered])) + .toMatchInlineSnapshot(` + Array [ + Array [ + "Visits \\"dieHard\\"", + true, + ], + Array [ + "Visits \\"dieHard.pending\\"", + true, + ], + Array [ + "Visits \\"dieHard.success\\"", + true, + ], + ] + `); }); it('tests missing state node coverage', async () => { const machine = createMachine({ - id: 'missing', + id: 'test', initial: 'first', states: { first: { - on: { NEXT: 'third' }, - meta: { - test: () => true - } - }, - second: { - meta: { - test: () => true - } + on: { NEXT: 'third' } }, + secondMissing: {}, third: { initial: 'one', states: { one: { - meta: { - test: () => true - }, on: { NEXT: 'two' } }, - two: { - meta: { - test: () => true - } - }, - three: { - meta: { - test: () => true - } - } - }, - meta: { - test: () => true + two: {}, + threeMissing: {} } } } }); - const testModel = createTestModel(machine).withEvents({ - NEXT: () => { - /* ... */ - } - }); + const testModel = createTestModel(machine); const plans = testModel.getShortestPlans(); @@ -372,7 +371,55 @@ describe('coverage', () => { await testModel.testPlan(plan, undefined); } - expect(testModel.covers(stateValueCoverage())).toBeTruthy(); + expect( + testModel + .getCoverage(stateValueCoverage()) + .filter((c) => !c.covered) + .map((c) => c.criterion.description) + ).toMatchInlineSnapshot(` + Array [ + "Visits \\"test.secondMissing\\"", + "Visits \\"test.third.threeMissing\\"", + ] + `); + }); + + it('test', async () => { + const feedbackMachine = createMachine({ + id: 'feedback', + initial: 'logon', + states: { + logon: { + initial: 'empty', + states: { + empty: { + on: { + ENTER_LOGON: 'filled' + } + }, + filled: { type: 'final' } + }, + on: { + LOGON_SUBMIT: 'ordermenu' + } + }, + ordermenu: { + type: 'final' + } + } + }); + + const model = createTestModel(feedbackMachine); + + for (const plan of model.getShortestPlans()) { + await model.testPlan(plan, null); + } + + const coverage = model.getCoverage(stateValueCoverage()); + + expect(coverage).toHaveLength(5); + + expect(coverage.filter((c) => !c.covered)).toHaveLength(0); }); it('skips filtered states (filter option)', async () => { From b19d56659352c8486e6bf6525c0661b129186c31 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 16 Mar 2022 09:23:03 -0600 Subject: [PATCH 028/394] Update coverage test --- packages/xstate-test/test/index.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index bb470fc057..ce2d11ab92 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -384,7 +384,8 @@ describe('coverage', () => { `); }); - it('test', async () => { + // https://github.com/statelyai/xstate/issues/729 + it('reports full coverage when all states are covered', async () => { const feedbackMachine = createMachine({ id: 'feedback', initial: 'logon', From 24db9479c62a7db759d9b9be50e0323b264c3a5c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 17 Mar 2022 08:43:26 -0700 Subject: [PATCH 029/394] Coverage status & options --- packages/xstate-test/src/TestModel.ts | 43 +++++----- packages/xstate-test/src/coverage.ts | 30 +++++-- packages/xstate-test/src/types.ts | 5 +- packages/xstate-test/test/index.test.ts | 100 +++++++++--------------- 4 files changed, 85 insertions(+), 93 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 9b42a2f5ba..33da2e93fc 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -19,7 +19,6 @@ import type { TestModelCoverage, TestModelOptions, StatePredicate, - CoverageOptions, TestEventsConfig, TestPathResult, TestStepResult, @@ -269,20 +268,6 @@ export class TestModel { // TODO } - public testCoverage(options?: CoverageOptions): void { - return void options; - // 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: TestEventsConfig ): TestModel { @@ -316,11 +301,33 @@ export class TestModel { const criteria = criteriaFn?.(this) ?? []; const stateCoverages = Object.values(this._coverage.states); - return criteria.map((c) => { + return criteria.map((criterion) => { return { - criterion: c, - covered: stateCoverages.some((sc) => c.predicate(sc)) + criterion, + status: criterion.skip + ? 'skipped' + : stateCoverages.some((sc) => criterion.predicate(sc)) + ? 'covered' + : 'uncovered' }; }); } + + public testCoverage( + criteriaFn?: (testModel: this) => Array> + ): void { + const criteriaResult = this.getCoverage(criteriaFn); + + const unmetCriteria = criteriaResult.filter( + (c) => c.status === 'uncovered' + ); + + if (unmetCriteria.length) { + const criteriaMessage = `Coverage criteria not met:\n${unmetCriteria + .map((c) => '\t' + c.criterion.description) + .join('\n')}`; + + throw new Error(criteriaMessage); + } + } } diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index b138b7e6e8..5c6d9f1af0 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -4,16 +4,30 @@ import { getAllStateNodes } from 'xstate/lib/stateUtils'; import { TestModel } from './TestModel'; import { Criterion } from './types'; -export function stateValueCoverage(): ( - testModel: TestModel -) => Array> { +interface StateValueCoverageOptions { + filter?: (stateNode: AnyStateNode) => boolean; +} + +export function stateValueCoverage( + options?: StateValueCoverageOptions +): (testModel: TestModel) => Array> { + const resolvedOptions: Required = { + filter: () => true, + ...options + }; + return (testModel) => { const allStateNodes = getAllStateNodes(testModel.behavior as AnyStateNode); - return allStateNodes.map((stateNode) => ({ - predicate: (stateCoverage) => - stateCoverage.state.configuration.includes(stateNode), - description: `Visits ${JSON.stringify(stateNode.id)}` - })); + return allStateNodes.map((stateNode) => { + const skipped = !resolvedOptions.filter(stateNode); + + return { + predicate: (stateCoverage) => + stateCoverage.state.configuration.includes(stateNode), + description: `Visits ${JSON.stringify(stateNode.id)}`, + skipped + }; + }); }; } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 6785e87def..5b668b89bf 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -183,7 +183,8 @@ export interface CoverageOptions { export interface Criterion { predicate: (stateCoverage: TestStateCoverage) => boolean; - description?: string; + description: string; + skip?: boolean; } export interface CriterionResult { @@ -191,7 +192,7 @@ export interface CriterionResult { /** * Whether the criterion was covered or not */ - covered: boolean; + status: 'uncovered' | 'covered' | 'skipped'; } export interface TestTransitionConfig< diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index ce2d11ab92..d84fb68e24 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -318,25 +318,27 @@ describe('coverage', () => { const coverage = dieHardModel.getCoverage(stateValueCoverage()); - expect(coverage.every((c) => c.covered)).toEqual(true); + expect(coverage.every((c) => c.status === 'covered')).toEqual(true); - expect(coverage.map((c) => [c.criterion.description, c.covered])) + expect(coverage.map((c) => [c.criterion.description, c.status])) .toMatchInlineSnapshot(` Array [ Array [ "Visits \\"dieHard\\"", - true, + "covered", ], Array [ "Visits \\"dieHard.pending\\"", - true, + "covered", ], Array [ "Visits \\"dieHard.success\\"", - true, + "covered", ], ] `); + + expect(() => dieHardModel.testCoverage(stateValueCoverage())).not.toThrow(); }); it('tests missing state node coverage', async () => { @@ -374,7 +376,7 @@ describe('coverage', () => { expect( testModel .getCoverage(stateValueCoverage()) - .filter((c) => !c.covered) + .filter((c) => c.status !== 'covered') .map((c) => c.criterion.description) ).toMatchInlineSnapshot(` Array [ @@ -382,6 +384,13 @@ describe('coverage', () => { "Visits \\"test.third.threeMissing\\"", ] `); + + expect(() => testModel.testCoverage(stateValueCoverage())) + .toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Visits \\"test.secondMissing\\" + Visits \\"test.third.threeMissing\\"" + `); }); // https://github.com/statelyai/xstate/issues/729 @@ -420,7 +429,9 @@ describe('coverage', () => { expect(coverage).toHaveLength(5); - expect(coverage.filter((c) => !c.covered)).toHaveLength(0); + expect(coverage.filter((c) => c.status !== 'covered')).toHaveLength(0); + + expect(() => model.testCoverage(stateValueCoverage())).not.toThrow(); }); it('skips filtered states (filter option)', async () => { @@ -434,32 +445,18 @@ describe('coverage', () => { idle: { on: { START: 'passthrough' - }, - meta: { - test: () => { - /* ... */ - } } }, passthrough: { always: 'end' }, end: { - type: 'final', - meta: { - test: () => { - /* ... */ - } - } + type: 'final' } } }); - const testModel = createTestModel(TestBug).withEvents({ - START: () => { - /* ... */ - } - }); + const testModel = createTestModel(TestBug); const testPlans = testModel.getShortestPlans(); @@ -473,11 +470,13 @@ describe('coverage', () => { await Promise.all(promises); expect(() => { - testModel.testCoverage({ - filter: (stateNode) => { - return !!stateNode.meta; - } - }); + testModel.testCoverage( + stateValueCoverage({ + filter: (stateNode) => { + return !!stateNode.meta; + } + }) + ); }).not.toThrow(); }); }); @@ -490,8 +489,11 @@ describe('events', () => { | { type: 'CLOSE' } | { type: 'ESC' } | { type: 'SUBMIT'; value: string }; - const feedbackMachine = createMachine({ + const feedbackMachine = createMachine({ id: 'feedback', + schema: { + events: {} as Events + }, initial: 'question', states: { question: { @@ -500,11 +502,6 @@ describe('events', () => { CLICK_BAD: 'form', CLOSE: 'closed', ESC: 'closed' - }, - meta: { - test: () => { - // ... - } } }, form: { @@ -521,47 +518,20 @@ 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' } } }); @@ -587,7 +557,7 @@ describe('events', () => { await testModel.testPlan(plan, undefined); } - return testModel.testCoverage(); + expect(() => testModel.testCoverage(stateValueCoverage())).not.toThrow(); }); it('should not throw an error for unimplemented events', () => { From 012b3eab5ff4970a44082c57bc8d18af02f9dc3c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 17 Mar 2022 08:48:16 -0700 Subject: [PATCH 030/394] Remove unused code --- packages/xstate-test/src/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 61292107c2..736e880160 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -46,11 +46,6 @@ async function assertState(state: State, testContext: any) { for (const id of Object.keys(state.meta)) { const stateNodeMeta = state.meta[id]; if (typeof stateNodeMeta.test === 'function' && !stateNodeMeta.skip) { - // this.coverage.stateNodes.set( - // id, - // (this.coverage.stateNodes.get(id) || 0) + 1 - // ); - await stateNodeMeta.test(testContext, state); } } From a559b7a523810a336dd37524d2798c89d4b16a18 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 17 Mar 2022 13:10:02 -0700 Subject: [PATCH 031/394] Remove .withEvents --- packages/xstate-test/src/TestModel.ts | 22 +-- packages/xstate-test/src/coverage.ts | 4 +- packages/xstate-test/src/index.ts | 21 ++- packages/xstate-test/src/types.ts | 10 ++ packages/xstate-test/test/index.test.ts | 180 ++++++++++++++++-------- 5 files changed, 151 insertions(+), 86 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 33da2e93fc..2c4debfd05 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -61,6 +61,7 @@ export class TestModel { serializeState: (state) => simpleStringify(state) as SerializedState, serializeEvent: (event) => simpleStringify(event) as SerializedEvent, getEvents: () => [], + events: {}, getStates: () => [], testState: () => void 0, testTransition: () => void 0, @@ -268,27 +269,6 @@ export class TestModel { // TODO } - public withEvents( - eventMap: TestEventsConfig - ): TestModel { - return new TestModel(this.behavior, { - ...this.options, - getEvents: () => getEventSamples(eventMap), - testTransition: async ({ event }, testContext) => { - const eventConfig = eventMap[event.type]; - - if (!eventConfig) { - return; - } - - const exec = - typeof eventConfig === 'function' ? eventConfig : eventConfig.exec; - - await exec?.(testContext, event); - } - }); - } - public resolveOptions( options?: Partial> ): TestModelOptions { diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index 5c6d9f1af0..0889706cd5 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -20,13 +20,13 @@ export function stateValueCoverage( const allStateNodes = getAllStateNodes(testModel.behavior as AnyStateNode); return allStateNodes.map((stateNode) => { - const skipped = !resolvedOptions.filter(stateNode); + const skip = !resolvedOptions.filter(stateNode); return { predicate: (stateCoverage) => stateCoverage.state.configuration.includes(stateNode), description: `Visits ${JSON.stringify(stateNode.id)}`, - skipped + skip }; }); }; diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 736e880160..29681e4d4d 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -110,11 +110,28 @@ export function createTestModel< }); }, getEvents: (state) => - state.nextEvents.map( - (eventType) => ({ type: eventType } as EventFrom) + flatten( + state.nextEvents.map((eventType) => { + const eventCaseGenerator = options?.events?.[eventType]?.cases; + + return ( + eventCaseGenerator?.() ?? [ + { type: eventType } as EventFrom + ] + ).map((e) => ({ type: eventType, ...e })); + }) ), + testTransition: async ({ event }, testContext) => { + const eventConfig = options?.events?.[(event as any).type]; + + await eventConfig?.exec?.(testContext, event); + }, ...options }); return testModel; } + +export function flatten(array: Array): T[] { + return ([] as T[]).concat(...array); +} diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 5b668b89bf..91206c50bc 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -2,6 +2,7 @@ import { Step, TraversalOptions } from '@xstate/graph'; import { AnyState, EventObject, + ExtractEvent, MachineConfig, State, StateNode, @@ -158,6 +159,15 @@ export interface TestModelOptions< */ execute: (state: TState, testContext: TTestContext) => void | Promise; getStates: () => TState[]; + events: { + [TEventType in TEvent['type']]?: { + cases?: () => Array< + | Omit, 'type'> + | ExtractEvent + >; + exec?: EventExecutor; + }; + }; logger: { log: (msg: string) => void; error: (msg: string) => void; diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index d84fb68e24..44a824405e 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,5 +1,5 @@ import { createTestModel } from '../src'; -import { assign, createMachine } from 'xstate'; +import { assign, createMachine, interpret } from 'xstate'; import { getDescription } from '../src/utils'; import { stateValueCoverage } from '../src/coverage'; @@ -121,35 +121,37 @@ class Jugs { } } -const dieHardModel = createTestModel(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(); +const dieHardModel = createTestModel(dieHardMachine, { + events: { + 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(); + } } } }); @@ -223,7 +225,7 @@ describe('testing a model (getPlanFromEvents)', () => { }); }); -describe('path.test()', () => { +describe('.testPath(path)', () => { const plans = dieHardModel.getSimplePlansTo((state) => { return state.matches('success') && state.context.three === 0; }); @@ -473,7 +475,7 @@ describe('coverage', () => { testModel.testCoverage( stateValueCoverage({ filter: (stateNode) => { - return !!stateNode.meta; + return stateNode.key !== 'passthrough'; } }) ); @@ -536,18 +538,9 @@ describe('events', () => { } }); - const testModel = createTestModel(feedbackMachine).withEvents({ - CLICK_BAD: () => { - /* ... */ - }, - CLICK_GOOD: () => { - /* ... */ - }, - CLOSE: () => { - /* ... */ - }, - SUBMIT: { - cases: [{ value: 'something' }, { value: '' }] + const testModel = createTestModel(feedbackMachine, { + events: { + SUBMIT: { cases: () => [{ value: 'something' }, { value: '' }] } } }); @@ -601,9 +594,7 @@ describe('state limiting', () => { } }); - const testModel = createTestModel(machine).withEvents({ - INC: () => {} - }); + const testModel = createTestModel(machine); const testPlans = testModel.getShortestPlans({ filter: (state) => { @@ -665,10 +656,7 @@ describe('plan description', () => { } }); - const testModel = createTestModel(machine).withEvents({ - NEXT: { exec: () => {} }, - DONE: { exec: () => {} } - }); + const testModel = createTestModel(machine); const testPlans = testModel.getShortestPlans(); it('should give a description for every plan', () => { @@ -703,11 +691,7 @@ it('prevents infinite recursion based on a provided limit', () => { } }); - const model = createTestModel(machine).withEvents({ - TOGGLE: () => { - /**/ - } - }); + const model = createTestModel(machine); expect(() => { model.getShortestPlans({ traversalLimit: 100 }); @@ -739,11 +723,7 @@ it('executes actions', async () => { } }); - const model = createTestModel(machine).withEvents({ - TOGGLE: () => { - /**/ - } - }); + const model = createTestModel(machine); const testPlans = model.getShortestPlans(); @@ -825,3 +805,81 @@ describe('test model options', () => { expect(testedEvents).toEqual(['NEXT', 'NEXT', 'PREV']); }); }); + +describe('invocations', () => { + it.skip('invokes', async () => { + const machine = createMachine({ + initial: 'idle', + states: { + idle: { + on: { + START: 'pending' + } + }, + pending: { + invoke: { + src: (_, e) => new Promise((res) => res(e.value)), + onDone: [ + { cond: (_, e) => e.data === 42, target: 'success' }, + { target: 'failure' } + ] + } + }, + success: {}, + failure: {} + } + }); + + const model = createTestModel(machine, { + testState: (state, service) => { + return new Promise((res) => { + let actualState; + const t = setTimeout(() => { + throw new Error( + `expected ${state.value}, got ${actualState.value}` + ); + }, 1000); + service.subscribe((s) => { + actualState = s; + if (s.matches(state.value)) { + clearTimeout(t); + res(); + } + }); + }); + }, + testTransition: (step, service) => { + if (step.event.type.startsWith('done.')) { + return; + } + + service.send(step.event); + }, + events: { + START: { + cases: () => [ + { type: 'START', value: 42 }, + { type: 'START', value: 1 } + ] + } + } + }); + + // const plans = model.getShortestPlansTo((state) => state.matches('success')); + const plans = model.getShortestPlans(); + + for (const plan of plans) { + for (const path of plan.paths) { + const service = interpret(machine).start(); + + service.subscribe((state) => { + console.log(state.event, state.value); + }); + + await model.testPath(path, service); + } + } + + model.testCoverage(stateValueCoverage()); + }); +}); From d17482be4380a6caf2cab790d020b42c0854e328 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 17 Mar 2022 13:10:25 -0700 Subject: [PATCH 032/394] Remove .withEvents --- packages/xstate-test/test/index.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 44a824405e..976caae118 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -267,11 +267,7 @@ describe('error path trace', () => { } }); - const testModel = createTestModel(machine).withEvents({ - NEXT: () => { - /* noop */ - } - }); + const testModel = createTestModel(machine); testModel .getShortestPlansTo((state) => state.matches('third')) From cfa6cffb2ee1d8c57fb8b3333967c2ab0134c472 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 17 Mar 2022 13:10:52 -0700 Subject: [PATCH 033/394] Fix types --- packages/xstate-test/src/TestModel.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 2c4debfd05..6cbb4a6dde 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -19,14 +19,12 @@ import type { TestModelCoverage, TestModelOptions, StatePredicate, - TestEventsConfig, TestPathResult, TestStepResult, Criterion, CriterionResult } from './types'; import { formatPathTestResult, simpleStringify } from './utils'; -import { getEventSamples } from './index'; /** * Creates a test model that represents an abstract model of a From d2db2d954571c68f81a12207ef7c0df7d64012d5 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 17 Mar 2022 13:29:27 -0700 Subject: [PATCH 034/394] Ensure transition argument order --- packages/xstate-test/src/index.ts | 11 +++--- packages/xstate-test/src/types.ts | 26 +++++++------- packages/xstate-test/test/index.test.ts | 46 +++++++++++++++++++++---- 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 29681e4d4d..942e79b64a 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -9,7 +9,7 @@ import { AnyState } from 'xstate'; import { TestModel } from './TestModel'; -import { TestModelOptions, TestEventsConfig } from './types'; +import { TestModelOptions, TestEventsConfig, TestEventConfig } from './types'; export function getEventSamples( eventsOptions: TestEventsConfig @@ -121,10 +121,13 @@ export function createTestModel< ).map((e) => ({ type: eventType, ...e })); }) ), - testTransition: async ({ event }, testContext) => { - const eventConfig = options?.events?.[(event as any).type]; + testTransition: async (step, testContext) => { + // TODO: fix types + const eventConfig = options?.events?.[ + (step.event as any).type + ] as TestEventConfig; - await eventConfig?.exec?.(testContext, event); + await eventConfig?.exec?.(step as any, testContext); }, ...options }); diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 91206c50bc..4e688caba4 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -100,14 +100,11 @@ export type StatePredicate = (state: TState) => boolean; * that triggers the represented `event`. */ export type EventExecutor = ( + step: TestStep, /** * The testing context used to execute the effect */ - testContext: T, - /** - * The represented event that will be triggered when executed - */ - event: EventObject + testContext: T ) => Promise | void; export interface TestEventConfig { @@ -144,6 +141,14 @@ export interface TestEventsConfig { | TestEventConfig; } +export interface TestModelEventConfig< + TEvent extends EventObject, + TTestContext +> { + cases?: () => Array | TEvent>; + exec?: EventExecutor; +} + export interface TestModelOptions< TState, TEvent extends EventObject, @@ -160,13 +165,10 @@ export interface TestModelOptions< execute: (state: TState, testContext: TTestContext) => void | Promise; getStates: () => TState[]; events: { - [TEventType in TEvent['type']]?: { - cases?: () => Array< - | Omit, 'type'> - | ExtractEvent - >; - exec?: EventExecutor; - }; + [TEventType in TEvent['type']]?: TestModelEventConfig< + ExtractEvent, + TTestContext + >; }; logger: { log: (msg: string) => void; diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 976caae118..3169928635 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -124,32 +124,32 @@ class Jugs { const dieHardModel = createTestModel(dieHardMachine, { events: { POUR_3_TO_5: { - exec: async ({ jugs }) => { + exec: async (_step, { jugs }) => { await jugs.transferThree(); } }, POUR_5_TO_3: { - exec: async ({ jugs }) => { + exec: async (_step, { jugs }) => { await jugs.transferFive(); } }, EMPTY_3: { - exec: async ({ jugs }) => { + exec: async (_step, { jugs }) => { await jugs.emptyThree(); } }, EMPTY_5: { - exec: async ({ jugs }) => { + exec: async (_step, { jugs }) => { await jugs.emptyFive(); } }, FILL_3: { - exec: async ({ jugs }) => { + exec: async (_step, { jugs }) => { await jugs.fillThree(); } }, FILL_5: { - exec: async ({ jugs }) => { + exec: async (_step, { jugs }) => { await jugs.fillFive(); } } @@ -879,3 +879,37 @@ describe('invocations', () => { model.testCoverage(stateValueCoverage()); }); }); + +// https://github.com/statelyai/xstate/issues/1538 +it('tests transitions', async () => { + expect.assertions(3); + const machine = createMachine({ + initial: 'first', + states: { + first: { + on: { NEXT: 'second' } + }, + second: {} + } + }); + + const obj = {}; + + const model = createTestModel(machine, { + events: { + NEXT: { + exec: (step, sut) => { + expect(step).toHaveProperty('event'); + expect(step).toHaveProperty('state'); + expect(sut).toBe(obj); + } + } + } + }); + + const plans = model.getShortestPlansTo((state) => state.matches('second')); + + const path = plans[0].paths[0]; + + await model.testPath(path, obj); +}); From 2cbd7b9823a696bf600085ff86739181919fa6d4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 19 Mar 2022 10:57:10 -0400 Subject: [PATCH 035/394] Add testModel.test.ts for any behavior --- packages/xstate-test/test/testModel.test.ts | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/xstate-test/test/testModel.test.ts diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts new file mode 100644 index 0000000000..0a755711b8 --- /dev/null +++ b/packages/xstate-test/test/testModel.test.ts @@ -0,0 +1,28 @@ +import { TestModel } from '../src/TestModel'; + +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 plans = model.getShortestPlansTo((state) => state === 1); + + expect(plans.length).toBeGreaterThan(0); +}); From 408f78cb7a73c26b4362a28b36527cba2ff64270 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 20 Mar 2022 13:07:37 -0400 Subject: [PATCH 036/394] Update serializeState, fix types --- packages/xstate-graph/src/graph.ts | 36 +-- .../test/__snapshots__/graph.test.ts.snap | 291 ++++++++++++++++-- packages/xstate-graph/test/graph.test.ts | 76 +++-- 3 files changed, 324 insertions(+), 79 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 0e5f940b00..22eabdfb48 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -1,13 +1,13 @@ import { - StateNode, State, DefaultContext, Event, EventObject, StateMachine, - AnyEventObject, AnyStateMachine, - AnyState + AnyState, + StateFrom, + EventFrom } from 'xstate'; import { SerializedEvent, @@ -73,9 +73,7 @@ export function getChildren(stateNode: AnyStateNode): AnyStateNode[] { return children; } -export function serializeState( - state: State -): SerializedState { +export function serializeState(state: AnyState): SerializedState { const { value, context, actions } = state; return [value, context, actions.map((a) => a.type).join(',')] .map((x) => JSON.stringify(x)) @@ -112,22 +110,20 @@ function getValueAdjMapOptions( }; } -export function getAdjacencyMap< - TContext = DefaultContext, - TEvent extends EventObject = AnyEventObject ->( - machine: - | StateNode - | StateMachine, - options?: ValueAdjMapOptions, TEvent> -): AdjacencyMap, TEvent> { +export function getAdjacencyMap( + machine: TMachine, + options?: ValueAdjMapOptions, EventFrom> +): AdjacencyMap, EventFrom> { + type TState = StateFrom; + type TEvent = EventFrom; + const optionsWithDefaults = getValueAdjMapOptions(options); const { filter, stateSerializer, eventSerializer } = optionsWithDefaults; const { events } = optionsWithDefaults; - const adjacency: AdjacencyMap, TEvent> = {}; + const adjacency: AdjacencyMap = {}; - function findAdjacencies(state: AnyState) { + function findAdjacencies(state: TState) { const { nextEvents } = state; const stateHash = stateSerializer(state); @@ -154,9 +150,9 @@ export function getAdjacencyMap< ).map((event) => toEventObject(event)); for (const event of potentialEvents) { - let nextState: AnyState; + let nextState: TState; try { - nextState = machine.transition(state, event); + nextState = machine.transition(state, event) as TState; } catch (e) { throw new Error( `Unable to transition from state ${stateSerializer( @@ -179,7 +175,7 @@ export function getAdjacencyMap< } } - findAdjacencies(machine.initialState); + findAdjacencies(machine.initialState as TState); return adjacency; } diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index 4de08376ee..f5cd0ed887 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -30,7 +30,7 @@ Object { exports[`@xstate/graph getShortestPaths() should represent conditional paths based on context: shortest paths conditional 1`] = ` Object { - "\\"bar\\" | {\\"id\\":\\"foo\\"}": Array [ + "\\"bar\\" | {\\"id\\":\\"foo\\"} | \\"\\"": Array [ Object { "state": "bar", "steps": Array [ @@ -41,7 +41,7 @@ Object { ], }, ], - "\\"foo\\" | {\\"id\\":\\"foo\\"}": Array [ + "\\"foo\\" | {\\"id\\":\\"foo\\"} | \\"\\"": Array [ Object { "state": "foo", "steps": Array [ @@ -52,7 +52,7 @@ Object { ], }, ], - "\\"pending\\" | {\\"id\\":\\"foo\\"}": Array [ + "\\"pending\\" | {\\"id\\":\\"foo\\"} | \\"\\"": Array [ Object { "state": "pending", "steps": Array [], @@ -63,7 +63,7 @@ Object { exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states (parallel): shortest paths parallel 1`] = ` Object { - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}": Array [ + "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"} | | \\"\\"": Array [ Object { "state": Object { "a": "a1", @@ -72,7 +72,7 @@ Object { "steps": Array [], }, ], - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}": Array [ + "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"} | | \\"\\"": Array [ Object { "state": Object { "a": "a2", @@ -89,7 +89,7 @@ Object { ], }, ], - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}": Array [ + "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"} | | \\"\\"": Array [ Object { "state": Object { "a": "a3", @@ -111,13 +111,24 @@ Object { exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states: shortest paths 1`] = ` Object { - "\\"green\\"": Array [ + "\\"green\\" | | \\"\\"": Array [ Object { "state": "green", "steps": Array [], }, ], - "\\"yellow\\"": Array [ + "\\"green\\" | | \\"doNothing\\"": Array [ + Object { + "state": "green", + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + ], + }, + ], + "\\"yellow\\" | | \\"\\"": Array [ Object { "state": "yellow", "steps": Array [ @@ -128,7 +139,7 @@ Object { ], }, ], - "{\\"red\\":\\"flashing\\"}": Array [ + "{\\"red\\":\\"flashing\\"} | | \\"\\"": Array [ Object { "state": Object { "red": "flashing", @@ -141,7 +152,7 @@ Object { ], }, ], - "{\\"red\\":\\"stop\\"}": Array [ + "{\\"red\\":\\"stop\\"} | | \\"\\"": Array [ Object { "state": Object { "red": "stop", @@ -170,7 +181,7 @@ Object { ], }, ], - "{\\"red\\":\\"wait\\"}": Array [ + "{\\"red\\":\\"wait\\"} | | \\"startCountdown\\"": Array [ Object { "state": Object { "red": "wait", @@ -193,7 +204,7 @@ Object { ], }, ], - "{\\"red\\":\\"walk\\"}": Array [ + "{\\"red\\":\\"walk\\"} | | \\"\\"": Array [ Object { "state": Object { "red": "walk", @@ -215,16 +226,40 @@ Object { exports[`@xstate/graph getSimplePaths() should return a mapping of arrays of simple paths to all states 2`] = ` Object { - "\\"green\\"": Array [ + "\\"green\\" | | \\"\\"": Array [ Object { "state": "green", "steps": Array [], }, ], - "\\"yellow\\"": Array [ + "\\"green\\" | | \\"doNothing\\"": Array [ + Object { + "state": "green", + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + ], + }, + ], + "\\"yellow\\" | | \\"\\"": Array [ + Object { + "state": "yellow", + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + ], + }, Object { "state": "yellow", "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -232,12 +267,123 @@ Object { ], }, ], - "{\\"red\\":\\"flashing\\"}": Array [ + "{\\"red\\":\\"flashing\\"} | | \\"\\"": Array [ + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", + }, + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", + }, + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "stop", + }, + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", + }, + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "wait", + }, + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "walk", + }, + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": "yellow", + }, + ], + }, Object { "state": Object { "red": "flashing", }, "steps": Array [ + Object { + "eventType": "POWER_OUTAGE", + "state": "green", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -271,6 +417,10 @@ Object { "red": "flashing", }, "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -298,6 +448,10 @@ Object { "red": "flashing", }, "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -319,6 +473,10 @@ Object { "red": "flashing", }, "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -334,6 +492,10 @@ Object { "red": "flashing", }, "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "POWER_OUTAGE", "state": "green", @@ -341,12 +503,43 @@ Object { ], }, ], - "{\\"red\\":\\"stop\\"}": Array [ + "{\\"red\\":\\"stop\\"} | | \\"\\"": Array [ + Object { + "state": Object { + "red": "stop", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", + }, + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", + }, + }, + ], + }, Object { "state": Object { "red": "stop", }, "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -370,12 +563,37 @@ Object { ], }, ], - "{\\"red\\":\\"wait\\"}": Array [ + "{\\"red\\":\\"wait\\"} | | \\"startCountdown\\"": Array [ + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", + }, + }, + ], + }, Object { "state": Object { "red": "wait", }, "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -393,12 +611,31 @@ Object { ], }, ], - "{\\"red\\":\\"walk\\"}": Array [ + "{\\"red\\":\\"walk\\"} | | \\"\\"": Array [ + Object { + "state": Object { + "red": "walk", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + ], + }, Object { "state": Object { "red": "walk", }, "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, Object { "eventType": "TIMER", "state": "green", @@ -415,7 +652,7 @@ Object { exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths to all states (parallel): simple paths parallel 1`] = ` Object { - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}": Array [ + "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"} | | \\"\\"": Array [ Object { "state": Object { "a": "a1", @@ -424,7 +661,7 @@ Object { "steps": Array [], }, ], - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}": Array [ + "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"} | | \\"\\"": Array [ Object { "state": Object { "a": "a2", @@ -441,7 +678,7 @@ Object { ], }, ], - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}": Array [ + "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"} | | \\"\\"": Array [ Object { "state": Object { "a": "a3", @@ -485,13 +722,13 @@ Object { exports[`@xstate/graph getSimplePaths() should return multiple paths for equivalent transitions: simple paths equal transitions 1`] = ` Object { - "\\"a\\"": Array [ + "\\"a\\" | | \\"\\"": Array [ Object { "state": "a", "steps": Array [], }, ], - "\\"b\\"": Array [ + "\\"b\\" | | \\"\\"": Array [ Object { "state": "b", "steps": Array [ @@ -516,7 +753,7 @@ Object { exports[`@xstate/graph getSimplePaths() should return value-based paths: simple paths context 1`] = ` Object { - "\\"finish\\" | {\\"count\\":3}": Array [ + "\\"finish\\" | {\\"count\\":3} | \\"\\"": Array [ Object { "state": "finish", "steps": Array [ @@ -535,13 +772,13 @@ Object { ], }, ], - "\\"start\\" | {\\"count\\":0}": Array [ + "\\"start\\" | {\\"count\\":0} | \\"\\"": Array [ Object { "state": "start", "steps": Array [], }, ], - "\\"start\\" | {\\"count\\":1}": Array [ + "\\"start\\" | {\\"count\\":1} | \\"\\"": Array [ Object { "state": "start", "steps": Array [ @@ -552,7 +789,7 @@ Object { ], }, ], - "\\"start\\" | {\\"count\\":2}": Array [ + "\\"start\\" | {\\"count\\":2} | \\"\\"": Array [ Object { "state": "start", "steps": Array [ diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 25ce930db8..688594c262 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -10,6 +10,7 @@ import { } from '../src/index'; import { getAdjacencyMap, + serializeState, traverseShortestPaths, traverseSimplePaths } from '../src/graph'; @@ -196,10 +197,10 @@ describe('@xstate/graph', () => { }); it('the initial state should have a zero-length path', () => { + const shortestPaths = getShortestPaths(lightMachine); + expect( - getShortestPaths(lightMachine)[ - JSON.stringify(lightMachine.initialState.value) - ].paths[0].steps + shortestPaths[serializeState(lightMachine.initialState)].paths[0].steps ).toHaveLength(0); }); @@ -239,12 +240,13 @@ describe('@xstate/graph', () => { expect(Object.keys(paths)).toMatchInlineSnapshot(` Array [ - "\\"green\\"", - "\\"yellow\\"", - "{\\"red\\":\\"flashing\\"}", - "{\\"red\\":\\"walk\\"}", - "{\\"red\\":\\"wait\\"}", - "{\\"red\\":\\"stop\\"}", + "\\"green\\" | | \\"\\"", + "\\"yellow\\" | | \\"\\"", + "{\\"red\\":\\"flashing\\"} | | \\"\\"", + "\\"green\\" | | \\"doNothing\\"", + "{\\"red\\":\\"walk\\"} | | \\"\\"", + "{\\"red\\":\\"wait\\"} | | \\"startCountdown\\"", + "{\\"red\\":\\"stop\\"} | | \\"\\"", ] `); @@ -264,9 +266,9 @@ describe('@xstate/graph', () => { expect(Object.keys(paths)).toMatchInlineSnapshot(` Array [ - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"}", - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"}", - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"}", + "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"} | | \\"\\"", + "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"} | | \\"\\"", + "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"} | | \\"\\"", ] `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -279,8 +281,8 @@ describe('@xstate/graph', () => { expect(Object.keys(paths)).toMatchInlineSnapshot(` Array [ - "\\"a\\"", - "\\"b\\"", + "\\"a\\" | | \\"\\"", + "\\"b\\" | | \\"\\"", ] `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -289,14 +291,22 @@ describe('@xstate/graph', () => { }); it('should return a single empty path for the initial state', () => { - expect(getSimplePaths(lightMachine)['"green"'].paths).toHaveLength(1); expect( - getSimplePaths(lightMachine)['"green"'].paths[0].steps + getSimplePaths(lightMachine)[serializeState(lightMachine.initialState)] + .paths + ).toHaveLength(1); + expect( + getSimplePaths(lightMachine)[serializeState(lightMachine.initialState)] + .paths[0].steps + ).toHaveLength(0); + expect( + getSimplePaths(equivMachine)[serializeState(equivMachine.initialState)] + .paths + ).toHaveLength(1); + expect( + getSimplePaths(equivMachine)[serializeState(equivMachine.initialState)] + .paths[0].steps ).toHaveLength(0); - expect(getSimplePaths(equivMachine)['"a"'].paths).toHaveLength(1); - expect(getSimplePaths(equivMachine)['"a"'].paths[0].steps).toHaveLength( - 0 - ); }); it('should return value-based paths', () => { @@ -337,10 +347,10 @@ describe('@xstate/graph', () => { expect(Object.keys(paths)).toMatchInlineSnapshot(` Array [ - "\\"start\\" | {\\"count\\":0}", - "\\"start\\" | {\\"count\\":1}", - "\\"start\\" | {\\"count\\":2}", - "\\"finish\\" | {\\"count\\":3}", + "\\"start\\" | {\\"count\\":0} | \\"\\"", + "\\"start\\" | {\\"count\\":1} | \\"\\"", + "\\"start\\" | {\\"count\\":2} | \\"\\"", + "\\"finish\\" | {\\"count\\":3} | \\"\\"", ] `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -410,7 +420,7 @@ describe('@xstate/graph', () => { }); // explicit type arguments could be removed once davidkpiano/xstate#652 gets resolved - const adj = getAdjacencyMap(counterMachine, { + const adj = getAdjacencyMap(counterMachine, { filter: (state) => state.context.count >= 0 && state.context.count <= 5, stateSerializer: (state) => { const ctx = { @@ -452,11 +462,13 @@ describe('@xstate/graph', () => { const adj = getAdjacencyMap(machine, { events: { - EVENT: (state) => [{ type: 'EVENT', value: state.context.count + 10 }] + EVENT: (state) => [ + { type: 'EVENT' as const, value: state.context.count + 10 } + ] // TODO: fix as const } }); - expect(adj).toHaveProperty('"second" | {"count":10}'); + expect(adj).toHaveProperty('"second" | {"count":10} | ""'); }); }); @@ -567,11 +579,11 @@ describe('filtering', () => { expect(Object.keys(sp)).toMatchInlineSnapshot(` Array [ - "\\"counting\\" | {\\"count\\":0}", - "\\"counting\\" | {\\"count\\":1}", - "\\"counting\\" | {\\"count\\":2}", - "\\"counting\\" | {\\"count\\":3}", - "\\"counting\\" | {\\"count\\":4}", + "\\"counting\\" | {\\"count\\":0} | \\"\\"", + "\\"counting\\" | {\\"count\\":1} | \\"\\"", + "\\"counting\\" | {\\"count\\":2} | \\"\\"", + "\\"counting\\" | {\\"count\\":3} | \\"\\"", + "\\"counting\\" | {\\"count\\":4} | \\"\\"", ] `); }); From c2f33901966ef3c94b478fb31acb800ea399307b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 21 Mar 2022 21:44:51 -0400 Subject: [PATCH 037/394] Add note about transient states --- packages/core/src/types.ts | 3 ++ packages/xstate-test/src/index.ts | 11 +++--- packages/xstate-test/test/index.test.ts | 47 +++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6395246cae..dc0ce6edc0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1505,6 +1505,9 @@ export interface StateConfig { */ activities?: ActivityMap; meta?: any; + /** + * @deprecated + */ events?: TEvent[]; configuration: Array>; transitions: Array>; diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 942e79b64a..c79fab403b 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -115,10 +115,13 @@ export function createTestModel< const eventCaseGenerator = options?.events?.[eventType]?.cases; return ( - eventCaseGenerator?.() ?? [ - { type: eventType } as EventFrom - ] - ).map((e) => ({ type: eventType, ...e })); + // Use generated events or a plain event without payload + ( + eventCaseGenerator?.() ?? [ + { type: eventType } as EventFrom + ] + ).map((e) => ({ type: eventType, ...e })) + ); }) ), testTransition: async (step, testContext) => { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 3169928635..19d371c5e6 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -477,6 +477,53 @@ describe('coverage', () => { ); }).not.toThrow(); }); + + // https://github.com/statelyai/xstate/issues/981 + it.skip('skips transient states (type: final)', async () => { + const machine = createMachine({ + id: 'menu', + initial: 'initial', + states: { + initial: { + initial: 'inner1', + + states: { + inner1: { + on: { + INNER2: 'inner2' + } + }, + + inner2: { + on: { + DONE: 'done' + } + }, + + done: { + type: 'final' + } + }, + + onDone: 'later' + }, + + later: {} + } + }); + + const model = createTestModel(machine); + const shortestPlans = model.getShortestPlans(); + + for (const plan of shortestPlans) { + await model.testPlan(plan, null); + } + + // TODO: determine how to handle missing coverage for transient states, + // which arguably should not be counted towards coverage, as the app is never in + // a transient state for any length of time + model.testCoverage(stateValueCoverage()); + }); }); describe('events', () => { From 3fa73493aa0a3fa79ffba17f2816441c1812ebce Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 21 Mar 2022 21:51:23 -0400 Subject: [PATCH 038/394] Move machine-specific files to test/machine.ts --- packages/xstate-test/src/index.ts | 112 +--------------------------- packages/xstate-test/src/machine.ts | 107 ++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 108 deletions(-) create mode 100644 packages/xstate-test/src/machine.ts diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index c79fab403b..a9a88d009b 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,15 +1,7 @@ -import { serializeState, SimpleBehavior } from '@xstate/graph'; -import { - EventObject, - State, - StateFrom, - EventFrom, - AnyStateMachine, - ActionObject, - AnyState -} from 'xstate'; -import { TestModel } from './TestModel'; -import { TestModelOptions, TestEventsConfig, TestEventConfig } from './types'; +import { EventObject } from 'xstate'; +import { TestEventsConfig } from './types'; + +export { createTestModel } from './machine'; export function getEventSamples( eventsOptions: TestEventsConfig @@ -42,102 +34,6 @@ export function getEventSamples( return result; } -async function assertState(state: State, testContext: any) { - for (const id of Object.keys(state.meta)) { - const stateNodeMeta = state.meta[id]; - if (typeof stateNodeMeta.test === 'function' && !stateNodeMeta.skip) { - await stateNodeMeta.test(testContext, state); - } - } -} - -function executeAction( - actionObject: ActionObject, - state: AnyState -): void { - if (typeof actionObject.exec == 'function') { - actionObject.exec(state.context, state.event, { - _event: state._event, - action: actionObject, - state - }); - } -} - -/** - * 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< - TMachine extends AnyStateMachine, - TestContext = any ->( - machine: TMachine, - options?: Partial< - TestModelOptions, EventFrom, TestContext> - > -): TestModel, EventFrom, TestContext> { - const testModel = new TestModel< - StateFrom, - EventFrom, - TestContext - >(machine as SimpleBehavior, { - serializeState, - testState: assertState, - execute: (state) => { - state.actions.forEach((action) => { - executeAction(action, state); - }); - }, - getEvents: (state) => - flatten( - state.nextEvents.map((eventType) => { - const eventCaseGenerator = options?.events?.[eventType]?.cases; - - return ( - // Use generated events or a plain event without payload - ( - eventCaseGenerator?.() ?? [ - { type: eventType } as EventFrom - ] - ).map((e) => ({ type: eventType, ...e })) - ); - }) - ), - testTransition: async (step, testContext) => { - // TODO: fix types - const eventConfig = options?.events?.[ - (step.event as any).type - ] as TestEventConfig; - - await eventConfig?.exec?.(step as any, testContext); - }, - ...options - }); - - return testModel; -} - export function flatten(array: Array): T[] { return ([] as T[]).concat(...array); } diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts new file mode 100644 index 0000000000..bda65da6ff --- /dev/null +++ b/packages/xstate-test/src/machine.ts @@ -0,0 +1,107 @@ +import { SimpleBehavior, serializeState } from '@xstate/graph'; +import type { + ActionObject, + AnyState, + AnyStateMachine, + EventFrom, + StateFrom +} from 'xstate'; +import { flatten } from '.'; +import { TestModel } from './TestModel'; +import { TestModelOptions, TestEventConfig } from './types'; + +export async function testMachineState(state: AnyState, testContext: any) { + for (const id of Object.keys(state.meta)) { + const stateNodeMeta = state.meta[id]; + if (typeof stateNodeMeta.test === 'function' && !stateNodeMeta.skip) { + await stateNodeMeta.test(testContext, state); + } + } +} + +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 + }); + } +} + +/** + * 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< + TMachine extends AnyStateMachine, + TestContext = any +>( + machine: TMachine, + options?: Partial< + TestModelOptions, EventFrom, TestContext> + > +): TestModel, EventFrom, TestContext> { + const testModel = new TestModel< + StateFrom, + EventFrom, + TestContext + >(machine as SimpleBehavior, { + serializeState, + testState: testMachineState, + execute: (state) => { + state.actions.forEach((action) => { + executeAction(action, state); + }); + }, + getEvents: (state) => + flatten( + state.nextEvents.map((eventType) => { + const eventCaseGenerator = options?.events?.[eventType]?.cases; + + return ( + // Use generated events or a plain event without payload + ( + eventCaseGenerator?.() ?? [ + { type: eventType } as EventFrom + ] + ).map((e) => ({ type: eventType, ...e })) + ); + }) + ), + testTransition: async (step, testContext) => { + // TODO: fix types + const eventConfig = options?.events?.[ + (step.event as any).type + ] as TestEventConfig; + + await eventConfig?.exec?.(step as any, testContext); + }, + ...options + }); + + return testModel; +} From eead98871b455db32bf4f4063bc3e4b1ce00698f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 26 Mar 2022 10:26:13 -0400 Subject: [PATCH 039/394] Types were acting weird here --- packages/core/src/interpreter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/interpreter.ts b/packages/core/src/interpreter.ts index 9407df4d5e..68b17d0d66 100644 --- a/packages/core/src/interpreter.ts +++ b/packages/core/src/interpreter.ts @@ -1084,7 +1084,7 @@ export class Interpreter< }) .start(); - return actor; + return actor as ActorRef>; } private spawnBehavior( behavior: Behavior, From fbbd72d1648c0f70a948d4723db233720bdd6262 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 27 Mar 2022 11:24:06 -0400 Subject: [PATCH 040/394] Ensure event case values stay intact. Fixes #982 --- packages/xstate-graph/src/graph.ts | 51 ++++++++++++++----------- packages/xstate-test/src/machine.ts | 4 +- packages/xstate-test/test/index.test.ts | 38 ++++++++++++++++++ 3 files changed, 70 insertions(+), 23 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 22eabdfb48..289a239308 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -86,12 +86,6 @@ export function serializeEvent( return JSON.stringify(event) as SerializedEvent; } -export function deserializeEventString( - eventString: string -): TEvent { - return JSON.parse(eventString) as TEvent; -} - const defaultValueAdjMapOptions: Required> = { events: {}, filter: () => true, @@ -217,7 +211,7 @@ export function traverseShortestPaths( // weight, state, event const weightMap = new Map< SerializedState, - [number, SerializedState | undefined, SerializedEvent | undefined] + [number, SerializedState | undefined, TEvent | undefined] >(); const stateMap = new Map(); const initialSerializedState = serializeState(behavior.initialState, null); @@ -234,15 +228,16 @@ export function traverseShortestPaths( for (const event of Object.keys( adjacency[serializedState].transitions ) as SerializedEvent[]) { - const eventObject = JSON.parse(event); - const nextState = adjacency[serializedState].transitions[event]; + 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, - event + eventObject ]); } else { const [nextWeight] = weightMap.get(nextSerializedState)!; @@ -250,7 +245,7 @@ export function traverseShortestPaths( weightMap.set(nextSerializedState, [ weight + 1, serializedState, - event + eventObject ]); } } @@ -282,7 +277,7 @@ export function traverseShortestPaths( state, steps: statePathMap[fromState].paths[0].steps.concat({ state: stateMap.get(fromState)!, - event: deserializeEventString(fromEvent!) as TEvent + event: fromEvent! }), weight } @@ -387,7 +382,9 @@ export function getPathFromEvents< }); const eventSerial = serializeEvent(event); - const nextState = adjacency[stateSerial].transitions[eventSerial]; + const { state: nextState, event: _nextEvent } = adjacency[ + stateSerial + ].transitions[eventSerial]; if (!nextState) { throw new Error( @@ -409,24 +406,29 @@ export function getPathFromEvents< }; } -interface AdjMap { +interface AdjMap { [key: SerializedState]: { state: TState; - transitions: { [key: SerializedEvent]: TState }; + transitions: { + [key: SerializedEvent]: { + event: TEvent; + state: TState; + }; + }; }; } export function performDepthFirstTraversal( behavior: SimpleBehavior, options: TraversalOptions -): AdjMap { +): AdjMap { const { transition, initialState } = behavior; const { serializeState, getEvents, traversalLimit: limit } = resolveTraversalOptions(options); - const adj: AdjMap = {}; + const adj: AdjMap = {}; let iterations = 0; const queue: Array<[TState, TEvent | null]> = [[initialState, null]]; @@ -454,7 +456,12 @@ export function performDepthFirstTraversal( const nextState = transition(state, subEvent); if (!options.filter || options.filter(nextState, subEvent)) { - adj[serializedState].transitions[JSON.stringify(subEvent)] = nextState; + adj[serializedState].transitions[ + JSON.stringify(subEvent) as SerializedEvent + ] = { + event: subEvent, + state: nextState + }; queue.push([nextState, subEvent]); } } @@ -533,9 +540,9 @@ export function traverseSimplePaths( for (const serializedEvent of Object.keys( adjacency[fromStateSerial].transitions ) as SerializedEvent[]) { - const subEvent = JSON.parse(serializedEvent); - const nextState = - adjacency[fromStateSerial].transitions[serializedEvent]; + const { state: nextState, event: subEvent } = adjacency[ + fromStateSerial + ].transitions[serializedEvent]; if (!(serializedEvent in adjacency[fromStateSerial].transitions)) { continue; @@ -548,7 +555,7 @@ export function traverseSimplePaths( visitCtx.edges.add(serializedEvent); path.push({ state: stateMap.get(fromStateSerial)!, - event: deserializeEventString(serializedEvent) + event: subEvent }); util(nextState, toStateSerial, subEvent); } diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index bda65da6ff..8a626dec46 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -88,7 +88,9 @@ export function createTestModel< eventCaseGenerator?.() ?? [ { type: eventType } as EventFrom ] - ).map((e) => ({ type: eventType, ...e })) + ).map((e) => { + return { type: eventType, ...e }; + }) ); }) ), diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 19d371c5e6..ad3e1d524b 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -960,3 +960,41 @@ it('tests transitions', async () => { await model.testPath(path, obj); }); + +// https://github.com/statelyai/xstate/issues/982 +it('Event in event executor should contain payload from case', async () => { + const machine = createMachine({ + initial: 'first', + states: { + first: { + on: { NEXT: 'second' } + }, + second: {} + } + }); + + const obj = {}; + + const nonSerializableData = () => 42; + + const model = createTestModel(machine, { + events: { + NEXT: { + cases: () => [{ payload: 10, fn: nonSerializableData }], + exec: (step) => { + expect(step.event).toEqual({ + type: 'NEXT', + payload: 10, + fn: nonSerializableData + }); + } + } + } + }); + + const plans = model.getShortestPlansTo((state) => state.matches('second')); + + const path = plans[0].paths[0]; + + await model.testPath(path, obj); +}); From e568a870da76e76d3404b0a2e81a29115a2bef7a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 27 Mar 2022 12:38:27 -0400 Subject: [PATCH 041/394] Add model.testPlans(), transition coverage --- packages/xstate-test/src/TestModel.ts | 30 ++++++++++++++--- packages/xstate-test/src/coverage.ts | 45 +++++++++++++++++++++++-- packages/xstate-test/src/types.ts | 9 +++-- packages/xstate-test/test/index.test.ts | 34 ++++++++++++++++++- 4 files changed, 109 insertions(+), 9 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 6cbb4a6dde..9d29ed98e8 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -168,6 +168,15 @@ export class TestModel { return Object.values(adj).map((x) => x.state); } + public async testPlans( + plans: Array>, + testContext: TTestContext + ) { + for (const plan of plans) { + await this.testPlan(plan, testContext); + } + } + public async testPlan( plan: StatePlan, testContext: TTestContext @@ -263,8 +272,22 @@ export class TestModel { this.addTransitionCoverage(step); } - private addTransitionCoverage(_step: Step) { - // TODO + private addTransitionCoverage(step: Step) { + const transitionSerial = `${this.options.serializeState( + step.state, + null as any + )} | ${this.options.serializeEvent(step.event)}`; + + const existingCoverage = this._coverage.transitions[transitionSerial]; + + if (existingCoverage) { + existingCoverage.count++; + } else { + this._coverage.transitions[transitionSerial] = { + step, + count: 1 + }; + } } public resolveOptions( @@ -277,14 +300,13 @@ export class TestModel { criteriaFn?: (testModel: this) => Array> ): Array> { const criteria = criteriaFn?.(this) ?? []; - const stateCoverages = Object.values(this._coverage.states); return criteria.map((criterion) => { return { criterion, status: criterion.skip ? 'skipped' - : stateCoverages.some((sc) => criterion.predicate(sc)) + : criterion.predicate(this._coverage) ? 'covered' : 'uncovered' }; diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index 0889706cd5..b59dc58871 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -1,6 +1,7 @@ import { AnyStateNode } from '@xstate/graph'; import type { AnyState } from 'xstate'; import { getAllStateNodes } from 'xstate/lib/stateUtils'; +import { flatten } from '.'; import { TestModel } from './TestModel'; import { Criterion } from './types'; @@ -23,11 +24,51 @@ export function stateValueCoverage( const skip = !resolvedOptions.filter(stateNode); return { - predicate: (stateCoverage) => - stateCoverage.state.configuration.includes(stateNode), + predicate: (coverage) => + Object.values(coverage.states).some(({ state }) => + state.configuration.includes(stateNode) + ), description: `Visits ${JSON.stringify(stateNode.id)}`, skip }; }); }; } + +export function transitionCoverage(): ( + testModel: TestModel +) => Array> { + return (testModel) => { + const allStateNodes = getAllStateNodes(testModel.behavior as AnyStateNode); + const allTransitions = flatten(allStateNodes.map((sn) => sn.transitions)); + + return allTransitions.map((t) => { + return { + predicate: (coverage) => + Object.values(coverage.transitions).some((transitionCoverage) => { + return ( + transitionCoverage.step.state.configuration.includes(t.source) && + t.eventType === transitionCoverage.step.event.type + ); + }), + description: `Transitions ${t.source.key} on event ${t.eventType}` + }; + }); + // return flatten( + // allStateNodes.map((sn) => { + // const transitions = sn.transitions; + // const transitionSerial = `${this.options.serializeState( + // step.state, + // null as any + // )} | ${this.options.serializeEvent(step.event)}`; + + // return { + // predicate: () => true, + // description: '' + // }; + // }) + // ); + + // return []; + }; +} diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 4e688caba4..f283a3f2f1 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -184,9 +184,14 @@ export interface TestStateCoverage { count: number; } +export interface TestTransitionCoverage { + step: Step; + count: number; +} + export interface TestModelCoverage { states: Record>; - transitions: Record>; + transitions: Record>; } export interface CoverageOptions { @@ -194,7 +199,7 @@ export interface CoverageOptions { } export interface Criterion { - predicate: (stateCoverage: TestStateCoverage) => boolean; + predicate: (coverage: TestModelCoverage) => boolean; description: string; skip?: boolean; } diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index ad3e1d524b..6797749d8c 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,7 +1,7 @@ import { createTestModel } from '../src'; import { assign, createMachine, interpret } from 'xstate'; import { getDescription } from '../src/utils'; -import { stateValueCoverage } from '../src/coverage'; +import { stateValueCoverage, transitionCoverage } from '../src/coverage'; interface DieHardContext { three: number; @@ -524,6 +524,38 @@ describe('coverage', () => { // a transient state for any length of time model.testCoverage(stateValueCoverage()); }); + + it('tests transition coverage', async () => { + const model = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT_ONE: 'b', + EVENT_TWO: 'b' + } + }, + b: {} + } + }) + ); + + await model.testPlans(model.getShortestPlans(), null); + + expect(() => { + model.testCoverage(transitionCoverage()); + }).toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Transitions a on event EVENT_TWO" + `); + + await model.testPlans(model.getSimplePlans(), null); + + expect(() => { + model.testCoverage(transitionCoverage()); + }).not.toThrow(); + }); }); describe('events', () => { From c4bd8bfb18eb46e87919c3b6b2fd587130d2d572 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 27 Mar 2022 21:52:29 -0400 Subject: [PATCH 042/394] Rename coverage options, remove TestContext (unsound), move files around --- packages/xstate-graph/src/graph.ts | 4 +- packages/xstate-test/src/TestModel.ts | 49 +- packages/xstate-test/src/coverage.ts | 8 +- packages/xstate-test/src/index.ts | 2 +- packages/xstate-test/src/machine.ts | 88 ++- packages/xstate-test/src/types.ts | 55 +- packages/xstate-test/test/coverage.test.ts | 223 +++++++ packages/xstate-test/test/dieHard.test.ts | 351 +++++++++++ packages/xstate-test/test/index.test.ts | 694 +++------------------ 9 files changed, 775 insertions(+), 699 deletions(-) create mode 100644 packages/xstate-test/test/coverage.test.ts create mode 100644 packages/xstate-test/test/dieHard.test.ts diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 289a239308..62f34966ea 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -306,7 +306,9 @@ export function getSimplePaths< ); } -export function toDirectedGraph(stateNode: AnyStateNode): DirectedGraphNode { +export function toDirectedGraph( + stateNode: AnyStateNode | AnyStateMachine +): DirectedGraphNode { const edges: DirectedGraphEdge[] = flatten( stateNode.transitions.map((t, transitionIndex) => { const targets = t.target ? t.target : [stateNode]; diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 9d29ed98e8..7ffa9666c7 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -47,14 +47,14 @@ import { formatPathTestResult, simpleStringify } from './utils'; * */ -export class TestModel { +export class TestModel { private _coverage: TestModelCoverage = { states: {}, transitions: {} }; - public options: TestModelOptions; + public options: TestModelOptions; public defaultTraversalOptions?: TraversalOptions; - public getDefaultOptions(): TestModelOptions { + public getDefaultOptions(): TestModelOptions { return { serializeState: (state) => simpleStringify(state) as SerializedState, serializeEvent: (event) => simpleStringify(event) as SerializedEvent, @@ -73,7 +73,7 @@ export class TestModel { constructor( public behavior: SimpleBehavior, - options?: Partial> + options?: Partial> ) { this.options = { ...this.getDefaultOptions(), @@ -170,26 +170,30 @@ export class TestModel { public async testPlans( plans: Array>, - testContext: TTestContext + options?: Partial> ) { for (const plan of plans) { - await this.testPlan(plan, testContext); + await this.testPlan(plan, options); } } public async testPlan( plan: StatePlan, - testContext: TTestContext + options?: Partial> ) { for (const path of plan.paths) { - await this.testPath(path, testContext); + await this.testPath(path, options); } } public async testPath( path: StatePath, - testContext: TTestContext + options?: Partial> ) { + const resolvedOptions = this.resolveOptions(options); + + await resolvedOptions.beforePath?.(); + const testPathResult: TestPathResult = { steps: [], state: { @@ -208,7 +212,7 @@ export class TestModel { testPathResult.steps.push(testStepResult); try { - await this.testState(step.state, testContext); + await this.testState(step.state, options); } catch (err) { testStepResult.state.error = err; @@ -216,7 +220,7 @@ export class TestModel { } try { - await this.testTransition(step, testContext); + await this.testTransition(step); } catch (err) { testStepResult.event.error = err; @@ -225,7 +229,7 @@ export class TestModel { } try { - await this.testState(path.state, testContext); + await this.testState(path.state, options); } catch (err) { testPathResult.state.error = err.message; throw err; @@ -234,16 +238,20 @@ export class TestModel { // TODO: make option err.message += formatPathTestResult(path, testPathResult, this.options); throw err; + } finally { + await resolvedOptions.afterPath?.(); } } public async testState( state: TState, - testContext: TTestContext + options?: Partial> ): Promise { - await this.options.testState(state, testContext); + const resolvedOptions = this.resolveOptions(options); - await this.options.execute(state, testContext); + await resolvedOptions.testState(state); + + await resolvedOptions.execute(state); this.addStateCoverage(state); } @@ -263,11 +271,8 @@ export class TestModel { } } - public async testTransition( - step: Step, - testContext: TTestContext - ): Promise { - await this.options.testTransition(step, testContext); + public async testTransition(step: Step): Promise { + await this.options.testTransition(step); this.addTransitionCoverage(step); } @@ -291,8 +296,8 @@ export class TestModel { } public resolveOptions( - options?: Partial> - ): TestModelOptions { + options?: Partial> + ): TestModelOptions { return { ...this.defaultTraversalOptions, ...this.options, ...options }; } diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index b59dc58871..5e06181963 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -9,9 +9,9 @@ interface StateValueCoverageOptions { filter?: (stateNode: AnyStateNode) => boolean; } -export function stateValueCoverage( +export function allStates( options?: StateValueCoverageOptions -): (testModel: TestModel) => Array> { +): (testModel: TestModel) => Array> { const resolvedOptions: Required = { filter: () => true, ...options @@ -35,8 +35,8 @@ export function stateValueCoverage( }; } -export function transitionCoverage(): ( - testModel: TestModel +export function allTransitions(): ( + testModel: TestModel ) => Array> { return (testModel) => { const allStateNodes = getAllStateNodes(testModel.behavior as AnyStateNode); diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index a9a88d009b..0844204e02 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -4,7 +4,7 @@ import { TestEventsConfig } from './types'; export { createTestModel } from './machine'; export function getEventSamples( - eventsOptions: TestEventsConfig + eventsOptions: TestEventsConfig // TODO ): TEvent[] { const result: TEvent[] = []; diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 8a626dec46..12d4a5f46f 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -8,13 +8,13 @@ import type { } from 'xstate'; import { flatten } from '.'; import { TestModel } from './TestModel'; -import { TestModelOptions, TestEventConfig } from './types'; +import { TestModelOptions } from './types'; -export async function testMachineState(state: AnyState, testContext: any) { +export async function testMachineState(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(testContext, state); + await stateNodeMeta.test(state); } } } @@ -56,54 +56,46 @@ export function executeAction( * - `events`: an object mapping string event types (e.g., `SUBMIT`) * to an event test config (e.g., `{exec: () => {...}, cases: [...]}`) */ -export function createTestModel< - TMachine extends AnyStateMachine, - TestContext = any ->( +export function createTestModel( machine: TMachine, - options?: Partial< - TestModelOptions, EventFrom, TestContext> - > -): TestModel, EventFrom, TestContext> { - const testModel = new TestModel< - StateFrom, - EventFrom, - TestContext - >(machine as SimpleBehavior, { - serializeState, - testState: testMachineState, - execute: (state) => { - state.actions.forEach((action) => { - executeAction(action, state); - }); - }, - getEvents: (state) => - flatten( - state.nextEvents.map((eventType) => { - const eventCaseGenerator = options?.events?.[eventType]?.cases; + options?: Partial, EventFrom>> +): TestModel, EventFrom> { + const testModel = new TestModel, EventFrom>( + machine as SimpleBehavior, + { + serializeState, + testState: testMachineState, + execute: (state) => { + state.actions.forEach((action) => { + executeAction(action, state); + }); + }, + getEvents: (state) => + flatten( + state.nextEvents.map((eventType) => { + const eventCaseGenerator = options?.events?.[eventType]?.cases; - return ( - // Use generated events or a plain event without payload - ( - eventCaseGenerator?.() ?? [ - { type: eventType } as EventFrom - ] - ).map((e) => { - return { type: eventType, ...e }; - }) - ); - }) - ), - testTransition: async (step, testContext) => { - // TODO: fix types - const eventConfig = options?.events?.[ - (step.event as any).type - ] as TestEventConfig; + return ( + // Use generated events or a plain event without payload + ( + eventCaseGenerator?.() ?? [ + { type: eventType } as EventFrom + ] + ).map((e) => { + return { type: eventType, ...(e as any) }; + }) + ); + }) + ), + testTransition: async (step) => { + // TODO: fix types + const eventConfig = options?.events?.[(step.event as any).type] as any; - await eventConfig?.exec?.(step as any, testContext); - }, - ...options - }); + await eventConfig?.exec?.(step as any); + }, + ...options + } + ); return testModel; } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index f283a3f2f1..5f87031473 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -99,15 +99,11 @@ export type StatePredicate = (state: TState) => boolean; * Executes an effect using the `testContext` and `event` * that triggers the represented `event`. */ -export type EventExecutor = ( - step: TestStep, - /** - * The testing context used to execute the effect - */ - testContext: T +export type EventExecutor = ( + step: Step ) => Promise | void; -export interface TestEventConfig { +export interface TestEventConfig { /** * Executes an effect that triggers the represented event. * @@ -119,7 +115,7 @@ export interface TestEventConfig { * } * ``` */ - exec?: EventExecutor; + exec?: EventExecutor; /** * Sample event object payloads _without_ the `type` property. * @@ -135,45 +131,44 @@ export interface TestEventConfig { cases?: EventCase[]; } -export interface TestEventsConfig { +export interface TestEventsConfig { [eventType: string]: - | EventExecutor - | TestEventConfig; + | EventExecutor + | TestEventConfig; } -export interface TestModelEventConfig< - TEvent extends EventObject, - TTestContext -> { +export interface TestModelEventConfig { cases?: () => Array | TEvent>; - exec?: EventExecutor; + exec?: EventExecutor; } -export interface TestModelOptions< - TState, - TEvent extends EventObject, - TTestContext -> extends TraversalOptions { - testState: (state: TState, testContext: TTestContext) => void | Promise; - testTransition: ( - step: Step, - testContext: TTestContext - ) => void | Promise; +export interface TestModelOptions + extends TraversalOptions { + testState: (state: TState) => void | Promise; + testTransition: (step: Step) => void | Promise; /** * Executes actions based on the `state` after the state is tested. */ - execute: (state: TState, testContext: TTestContext) => void | Promise; + execute: (state: TState) => void | Promise; getStates: () => TState[]; events: { - [TEventType in TEvent['type']]?: TestModelEventConfig< - ExtractEvent, - TTestContext + [TEventType in string /* TODO: TEvent['type'] */]?: TestModelEventConfig< + TState, + ExtractEvent >; }; logger: { log: (msg: string) => void; error: (msg: string) => void; }; + /** + * Executed before each path is tested + */ + beforePath?: () => void | Promise; + /** + * Executed after each path is tested + */ + afterPath?: () => void | Promise; } export interface TestStateCoverage { diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts new file mode 100644 index 0000000000..4135ac83ec --- /dev/null +++ b/packages/xstate-test/test/coverage.test.ts @@ -0,0 +1,223 @@ +import { createTestModel } from '../src'; +import { createMachine } from 'xstate'; +import { allStates, allTransitions } from '../src/coverage'; + +describe('coverage', () => { + it('reports missing state node coverage', async () => { + const machine = createMachine({ + id: 'test', + initial: 'first', + states: { + first: { + on: { NEXT: 'third' } + }, + secondMissing: {}, + third: { + initial: 'one', + states: { + one: { + on: { + NEXT: 'two' + } + }, + two: {}, + threeMissing: {} + } + } + } + }); + + const testModel = createTestModel(machine); + + const plans = testModel.getShortestPlans(); + + for (const plan of plans) { + await testModel.testPlan(plan, undefined); + } + + expect( + testModel + .getCoverage(allStates()) + .filter((c) => c.status !== 'covered') + .map((c) => c.criterion.description) + ).toMatchInlineSnapshot(` + Array [ + "Visits \\"test.secondMissing\\"", + "Visits \\"test.third.threeMissing\\"", + ] + `); + + expect(() => testModel.testCoverage(allStates())) + .toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Visits \\"test.secondMissing\\" + Visits \\"test.third.threeMissing\\"" + `); + }); + + // https://github.com/statelyai/xstate/issues/729 + it('reports full coverage when all states are covered', async () => { + const feedbackMachine = createMachine({ + id: 'feedback', + initial: 'logon', + states: { + logon: { + initial: 'empty', + states: { + empty: { + on: { + ENTER_LOGON: 'filled' + } + }, + filled: { type: 'final' } + }, + on: { + LOGON_SUBMIT: 'ordermenu' + } + }, + ordermenu: { + type: 'final' + } + } + }); + + const model = createTestModel(feedbackMachine); + + for (const plan of model.getShortestPlans()) { + await model.testPlan(plan); + } + + const coverage = model.getCoverage(allStates()); + + expect(coverage).toHaveLength(5); + + expect(coverage.filter((c) => c.status !== 'covered')).toHaveLength(0); + + expect(() => model.testCoverage(allStates())).not.toThrow(); + }); + + it('skips filtered states (filter option)', async () => { + const TestBug = createMachine({ + id: 'testbug', + initial: 'idle', + context: { + retries: 0 + }, + states: { + idle: { + on: { + START: 'passthrough' + } + }, + passthrough: { + always: 'end' + }, + end: { + type: 'final' + } + } + }); + + const testModel = createTestModel(TestBug); + + const testPlans = testModel.getShortestPlans(); + + const promises: any[] = []; + testPlans.forEach((plan) => { + plan.paths.forEach(() => { + promises.push(testModel.testPlan(plan, undefined)); + }); + }); + + await Promise.all(promises); + + expect(() => { + testModel.testCoverage( + allStates({ + filter: (stateNode) => { + return stateNode.key !== 'passthrough'; + } + }) + ); + }).not.toThrow(); + }); + + // https://github.com/statelyai/xstate/issues/981 + it.skip('skips transient states (type: final)', async () => { + const machine = createMachine({ + id: 'menu', + initial: 'initial', + states: { + initial: { + initial: 'inner1', + + states: { + inner1: { + on: { + INNER2: 'inner2' + } + }, + + inner2: { + on: { + DONE: 'done' + } + }, + + done: { + type: 'final' + } + }, + + onDone: 'later' + }, + + later: {} + } + }); + + const model = createTestModel(machine); + const shortestPlans = model.getShortestPlans(); + + for (const plan of shortestPlans) { + await model.testPlan(plan); + } + + // TODO: determine how to handle missing coverage for transient states, + // which arguably should not be counted towards coverage, as the app is never in + // a transient state for any length of time + model.testCoverage(allStates()); + }); + + it('tests transition coverage', async () => { + const model = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT_ONE: 'b', + EVENT_TWO: 'b' + } + }, + b: {} + } + }) + ); + + await model.testPlans(model.getShortestPlans()); + + expect(() => { + model.testCoverage(allTransitions()); + }).toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Transitions a on event EVENT_TWO" + `); + + await model.testPlans(model.getSimplePlans()); + + expect(() => { + model.testCoverage(allTransitions()); + }).not.toThrow(); + }); +}); diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts new file mode 100644 index 0000000000..8189d2ddd8 --- /dev/null +++ b/packages/xstate-test/test/dieHard.test.ts @@ -0,0 +1,351 @@ +import { createTestModel } from '../src'; +import { assign, createMachine } from 'xstate'; +import { getDescription } from '../src/utils'; +import { stateValueCoverage } from '../src/coverage'; + +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; + } + } + + const createDieHardModel = () => { + let jugs: Jugs; + + 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 + } + }, + meta: { + description: (state) => { + return `pending with (${state.context.three}, ${state.context.five})`; + }, + test: async (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 () => { + expect(jugs.five).toEqual(4); + } + } + } + } + }, + { + guards: { + weHave4Gallons: (ctx) => ctx.five === 4 + } + } + ); + + return createTestModel(dieHardMachine, { + beforePath: () => { + jugs = new Jugs(); + jugs.version = Math.random(); + }, + events: { + POUR_3_TO_5: { + exec: async () => { + await jugs.transferThree(); + } + }, + POUR_5_TO_3: { + exec: async () => { + await jugs.transferFive(); + } + }, + EMPTY_3: { + exec: async () => { + await jugs.emptyThree(); + } + }, + EMPTY_5: { + exec: async () => { + await jugs.emptyFive(); + } + }, + FILL_3: { + exec: async () => { + await jugs.fillThree(); + } + }, + FILL_5: { + exec: async () => { + await jugs.fillFive(); + } + } + } + }); + }; + + describe('testing a model (shortestPathsTo)', () => { + const dieHardModel = createDieHardModel(); + + dieHardModel + .getShortestPlansTo((state) => state.matches('success')) + .forEach((plan) => { + describe(`plan ${getDescription(plan.state)}`, () => { + it('should generate a single path', () => { + expect(plan.paths.length).toEqual(1); + }); + + plan.paths.forEach((path) => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.testPath(path); + }); + }); + }); + }); + }); + + describe('testing a model (simplePathsTo)', () => { + const dieHardModel = createDieHardModel(); + dieHardModel + .getSimplePlansTo((state) => state.matches('success')) + .forEach((plan) => { + describe(`reaches state ${JSON.stringify( + plan.state.value + )} (${JSON.stringify(plan.state.context)})`, () => { + plan.paths.forEach((path) => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.testPath(path); + }); + }); + }); + }); + }); + + describe('testing a model (getPlanFromEvents)', () => { + const dieHardModel = createDieHardModel(); + + 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' } + ], + (state) => state.matches('success') + ); + + describe(`reaches state ${JSON.stringify( + plan.state.value + )} (${JSON.stringify(plan.state.context)})`, () => { + plan.paths.forEach((path) => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.testPath(path); + }); + }); + }); + + it('should throw if the target does not match the last entered state', () => { + expect(() => { + dieHardModel.getPlanFromEvents([{ type: 'FILL_5' }], (state) => + state.matches('success') + ); + }).toThrow(); + }); + }); + + describe('.testPath(path)', () => { + const dieHardModel = createDieHardModel(); + const plans = dieHardModel.getSimplePlansTo((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 ${getDescription(path.state)}`, () => { + it(`reaches the target state`, async () => { + await dieHardModel.testPath(path); + }); + }); + }); + }); + }); + }); + + it('reports state node coverage', async () => { + const dieHardModel = createDieHardModel(); + const plans = dieHardModel.getSimplePlansTo((state) => { + return state.matches('success') && state.context.three === 0; + }); + + for (const plan of plans) { + for (const path of plan.paths) { + await dieHardModel.testPath(path); + } + } + + const coverage = dieHardModel.getCoverage(stateValueCoverage()); + + expect(coverage.every((c) => c.status === 'covered')).toEqual(true); + + expect(coverage.map((c) => [c.criterion.description, c.status])) + .toMatchInlineSnapshot(` + Array [ + Array [ + "Visits \\"dieHard\\"", + "covered", + ], + Array [ + "Visits \\"dieHard.pending\\"", + "covered", + ], + Array [ + "Visits \\"dieHard.success\\"", + "covered", + ], + ] + `); + + expect(() => dieHardModel.testCoverage(stateValueCoverage())).not.toThrow(); + }); +}); +describe('error path trace', () => { + describe('should return trace for failed state', () => { + const machine = createMachine({ + initial: 'first', + states: { + first: { + on: { NEXT: 'second' } + }, + second: { + on: { NEXT: 'third' } + }, + third: { + meta: { + test: () => { + throw new Error('test error'); + } + } + } + } + }); + + const testModel = createTestModel(machine); + + testModel + .getShortestPlansTo((state) => state.matches('third')) + .forEach((plan) => { + plan.paths.forEach((path) => { + it('should show an error path trace', async () => { + try { + await testModel.testPath(path, undefined); + } catch (err) { + expect(err.message).toEqual( + expect.stringContaining('test error') + ); + expect(err.message).toMatchInlineSnapshot(` + "test error + Path: + State: \\"first\\" | | \\"\\" + Event: {\\"type\\":\\"NEXT\\"} + + State: \\"second\\" | | \\"\\" + Event: {\\"type\\":\\"NEXT\\"} + + State: \\"third\\" | | \\"\\"" + `); + return; + } + + throw new Error('Should have failed'); + }); + }); + }); + }); +}); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 6797749d8c..464a3e7a33 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,562 +1,7 @@ import { createTestModel } from '../src'; import { assign, createMachine, interpret } from 'xstate'; import { getDescription } from '../src/utils'; -import { stateValueCoverage, transitionCoverage } from '../src/coverage'; - -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 = 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 - } - }, - 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 = createTestModel(dieHardMachine, { - events: { - POUR_3_TO_5: { - exec: async (_step, { jugs }) => { - await jugs.transferThree(); - } - }, - POUR_5_TO_3: { - exec: async (_step, { jugs }) => { - await jugs.transferFive(); - } - }, - EMPTY_3: { - exec: async (_step, { jugs }) => { - await jugs.emptyThree(); - } - }, - EMPTY_5: { - exec: async (_step, { jugs }) => { - await jugs.emptyFive(); - } - }, - FILL_3: { - exec: async (_step, { jugs }) => { - await jugs.fillThree(); - } - }, - FILL_5: { - exec: async (_step, { jugs }) => { - await jugs.fillFive(); - } - } - } -}); - -describe('testing a model (shortestPathsTo)', () => { - dieHardModel - .getShortestPlansTo((state) => state.matches('success')) - .forEach((plan) => { - describe(`plan ${getDescription(plan.state)}`, () => { - it('should generate a single path', () => { - expect(plan.paths.length).toEqual(1); - }); - - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, () => { - const testJugs = new Jugs(); - return dieHardModel.testPath(path, { jugs: testJugs }); - }); - }); - }); - }); -}); - -describe('testing a model (simplePathsTo)', () => { - dieHardModel - .getSimplePlansTo((state) => state.matches('success')) - .forEach((plan) => { - describe(`reaches state ${JSON.stringify( - plan.state.value - )} (${JSON.stringify(plan.state.context)})`, () => { - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, () => { - const testJugs = new Jugs(); - return dieHardModel.testPath(path, { 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' } - ], - (state) => state.matches('success') - ); - - describe(`reaches state ${JSON.stringify(plan.state.value)} (${JSON.stringify( - plan.state.context - )})`, () => { - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, () => { - const testJugs = new Jugs(); - return dieHardModel.testPath(path, { jugs: testJugs }); - }); - }); - }); - - it('should throw if the target does not match the last entered state', () => { - expect(() => { - dieHardModel.getPlanFromEvents([{ type: 'FILL_5' }], (state) => - state.matches('success') - ); - }).toThrow(); - }); -}); - -describe('.testPath(path)', () => { - const plans = dieHardModel.getSimplePlansTo((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 ${getDescription(path.state)}`, () => { - it(`reaches the target state`, () => { - const testJugs = new Jugs(); - return dieHardModel.testPath(path, { jugs: testJugs }); - }); - }); - }); - }); - }); -}); - -describe('error path trace', () => { - describe('should return trace for failed state', () => { - const machine = createMachine({ - initial: 'first', - states: { - first: { - on: { NEXT: 'second' } - }, - second: { - on: { NEXT: 'third' } - }, - third: { - meta: { - test: () => { - throw new Error('test error'); - } - } - } - } - }); - - const testModel = createTestModel(machine); - - testModel - .getShortestPlansTo((state) => state.matches('third')) - .forEach((plan) => { - plan.paths.forEach((path) => { - it('should show an error path trace', async () => { - try { - await testModel.testPath(path, undefined); - } catch (err) { - expect(err.message).toEqual( - expect.stringContaining('test error') - ); - expect(err.message).toMatchInlineSnapshot(` - "test error - Path: - State: \\"first\\" | | \\"\\" - Event: {\\"type\\":\\"NEXT\\"} - - State: \\"second\\" | | \\"\\" - Event: {\\"type\\":\\"NEXT\\"} - - State: \\"third\\" | | \\"\\"" - `); - return; - } - - throw new Error('Should have failed'); - }); - }); - }); - }); -}); - -describe('coverage', () => { - it('reports state node coverage', async () => { - const plans = dieHardModel.getSimplePlansTo((state) => { - return state.matches('success') && state.context.three === 0; - }); - - for (const plan of plans) { - for (const path of plan.paths) { - const jugs = new Jugs(); - await dieHardModel.testPath(path, { jugs }); - } - } - - const coverage = dieHardModel.getCoverage(stateValueCoverage()); - - expect(coverage.every((c) => c.status === 'covered')).toEqual(true); - - expect(coverage.map((c) => [c.criterion.description, c.status])) - .toMatchInlineSnapshot(` - Array [ - Array [ - "Visits \\"dieHard\\"", - "covered", - ], - Array [ - "Visits \\"dieHard.pending\\"", - "covered", - ], - Array [ - "Visits \\"dieHard.success\\"", - "covered", - ], - ] - `); - - expect(() => dieHardModel.testCoverage(stateValueCoverage())).not.toThrow(); - }); - - it('tests missing state node coverage', async () => { - const machine = createMachine({ - id: 'test', - initial: 'first', - states: { - first: { - on: { NEXT: 'third' } - }, - secondMissing: {}, - third: { - initial: 'one', - states: { - one: { - on: { - NEXT: 'two' - } - }, - two: {}, - threeMissing: {} - } - } - } - }); - - const testModel = createTestModel(machine); - - const plans = testModel.getShortestPlans(); - - for (const plan of plans) { - await testModel.testPlan(plan, undefined); - } - - expect( - testModel - .getCoverage(stateValueCoverage()) - .filter((c) => c.status !== 'covered') - .map((c) => c.criterion.description) - ).toMatchInlineSnapshot(` - Array [ - "Visits \\"test.secondMissing\\"", - "Visits \\"test.third.threeMissing\\"", - ] - `); - - expect(() => testModel.testCoverage(stateValueCoverage())) - .toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Visits \\"test.secondMissing\\" - Visits \\"test.third.threeMissing\\"" - `); - }); - - // https://github.com/statelyai/xstate/issues/729 - it('reports full coverage when all states are covered', async () => { - const feedbackMachine = createMachine({ - id: 'feedback', - initial: 'logon', - states: { - logon: { - initial: 'empty', - states: { - empty: { - on: { - ENTER_LOGON: 'filled' - } - }, - filled: { type: 'final' } - }, - on: { - LOGON_SUBMIT: 'ordermenu' - } - }, - ordermenu: { - type: 'final' - } - } - }); - - const model = createTestModel(feedbackMachine); - - for (const plan of model.getShortestPlans()) { - await model.testPlan(plan, null); - } - - const coverage = model.getCoverage(stateValueCoverage()); - - expect(coverage).toHaveLength(5); - - expect(coverage.filter((c) => c.status !== 'covered')).toHaveLength(0); - - expect(() => model.testCoverage(stateValueCoverage())).not.toThrow(); - }); - - it('skips filtered states (filter option)', async () => { - const TestBug = createMachine({ - id: 'testbug', - initial: 'idle', - context: { - retries: 0 - }, - states: { - idle: { - on: { - START: 'passthrough' - } - }, - passthrough: { - always: 'end' - }, - end: { - type: 'final' - } - } - }); - - const testModel = createTestModel(TestBug); - - const testPlans = testModel.getShortestPlans(); - - const promises: any[] = []; - testPlans.forEach((plan) => { - plan.paths.forEach(() => { - promises.push(testModel.testPlan(plan, undefined)); - }); - }); - - await Promise.all(promises); - - expect(() => { - testModel.testCoverage( - stateValueCoverage({ - filter: (stateNode) => { - return stateNode.key !== 'passthrough'; - } - }) - ); - }).not.toThrow(); - }); - - // https://github.com/statelyai/xstate/issues/981 - it.skip('skips transient states (type: final)', async () => { - const machine = createMachine({ - id: 'menu', - initial: 'initial', - states: { - initial: { - initial: 'inner1', - - states: { - inner1: { - on: { - INNER2: 'inner2' - } - }, - - inner2: { - on: { - DONE: 'done' - } - }, - - done: { - type: 'final' - } - }, - - onDone: 'later' - }, - - later: {} - } - }); - - const model = createTestModel(machine); - const shortestPlans = model.getShortestPlans(); - - for (const plan of shortestPlans) { - await model.testPlan(plan, null); - } - - // TODO: determine how to handle missing coverage for transient states, - // which arguably should not be counted towards coverage, as the app is never in - // a transient state for any length of time - model.testCoverage(stateValueCoverage()); - }); - - it('tests transition coverage', async () => { - const model = createTestModel( - createMachine({ - initial: 'a', - states: { - a: { - on: { - EVENT_ONE: 'b', - EVENT_TWO: 'b' - } - }, - b: {} - } - }) - ); - - await model.testPlans(model.getShortestPlans(), null); - - expect(() => { - model.testCoverage(transitionCoverage()); - }).toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Transitions a on event EVENT_TWO" - `); - - await model.testPlans(model.getSimplePlans(), null); - - expect(() => { - model.testCoverage(transitionCoverage()); - }).not.toThrow(); - }); -}); +import { allStates } from '../src/coverage'; describe('events', () => { it('should allow for representing many cases', async () => { @@ -625,7 +70,7 @@ describe('events', () => { await testModel.testPlan(plan, undefined); } - expect(() => testModel.testCoverage(stateValueCoverage())).not.toThrow(); + expect(() => testModel.testCoverage(allStates())).not.toThrow(); }); it('should not throw an error for unimplemented events', () => { @@ -879,6 +324,76 @@ describe('test model options', () => { expect(testedEvents).toEqual(['NEXT', 'NEXT', 'PREV']); }); + + it('options.beforePath(...) executes before each path is tested', async () => { + const counts: number[] = []; + let count = 0; + + const testModel = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + entry: () => { + counts.push(count); + }, + on: { + TO_B: 'b', + TO_C: 'c' + } + }, + b: {}, + c: {} + } + }), + { + beforePath: () => { + count++; + } + } + ); + + const shortestPlans = testModel.getShortestPlans(); + + await testModel.testPlans(shortestPlans); + + expect(counts).toEqual([1, 2, 3]); + }); + + it('options.afterPath(...) executes before each path is tested', async () => { + const counts: number[] = []; + let count = 0; + + const testModel = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + entry: () => { + counts.push(count); + }, + on: { + TO_B: 'b', + TO_C: 'c' + } + }, + b: {}, + c: {} + } + }), + { + afterPath: () => { + count++; + } + } + ); + + const shortestPlans = testModel.getShortestPlans(); + + await testModel.testPlans(shortestPlans); + + expect(counts).toEqual([0, 1, 2]); + }); }); describe('invocations', () => { @@ -906,30 +421,6 @@ describe('invocations', () => { }); const model = createTestModel(machine, { - testState: (state, service) => { - return new Promise((res) => { - let actualState; - const t = setTimeout(() => { - throw new Error( - `expected ${state.value}, got ${actualState.value}` - ); - }, 1000); - service.subscribe((s) => { - actualState = s; - if (s.matches(state.value)) { - clearTimeout(t); - res(); - } - }); - }); - }, - testTransition: (step, service) => { - if (step.event.type.startsWith('done.')) { - return; - } - - service.send(step.event); - }, events: { START: { cases: () => [ @@ -940,28 +431,48 @@ describe('invocations', () => { } }); - // const plans = model.getShortestPlansTo((state) => state.matches('success')); const plans = model.getShortestPlans(); for (const plan of plans) { for (const path of plan.paths) { const service = interpret(machine).start(); - service.subscribe((state) => { - console.log(state.event, state.value); - }); + await model.testPath(path, { + testState: (state) => { + return new Promise((res) => { + let actualState; + const t = setTimeout(() => { + throw new Error( + `expected ${state.value}, got ${actualState.value}` + ); + }, 1000); + service.subscribe((s) => { + actualState = s; + if (s.matches(state.value)) { + clearTimeout(t); + res(); + } + }); + }); + }, + testTransition: (step) => { + if (step.event.type.startsWith('done.')) { + return; + } - await model.testPath(path, service); + service.send(step.event); + } + }); } } - model.testCoverage(stateValueCoverage()); + model.testCoverage(allStates()); }); }); // https://github.com/statelyai/xstate/issues/1538 it('tests transitions', async () => { - expect.assertions(3); + expect.assertions(2); const machine = createMachine({ initial: 'first', states: { @@ -972,15 +483,12 @@ it('tests transitions', async () => { } }); - const obj = {}; - const model = createTestModel(machine, { events: { NEXT: { - exec: (step, sut) => { + exec: (step) => { expect(step).toHaveProperty('event'); expect(step).toHaveProperty('state'); - expect(sut).toBe(obj); } } } @@ -990,7 +498,7 @@ it('tests transitions', async () => { const path = plans[0].paths[0]; - await model.testPath(path, obj); + await model.testPath(path); }); // https://github.com/statelyai/xstate/issues/982 From 0f9c15250fda4c1b50dc4efd3cf14311688cfbab Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 27 Mar 2022 21:58:08 -0400 Subject: [PATCH 043/394] Fix tests --- packages/xstate-test/src/coverage.ts | 4 ++-- packages/xstate-test/test/dieHard.test.ts | 6 +++--- packages/xstate-test/test/index.test.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index 5e06181963..84ec7ce604 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -9,7 +9,7 @@ interface StateValueCoverageOptions { filter?: (stateNode: AnyStateNode) => boolean; } -export function allStates( +export function coversAllStates( options?: StateValueCoverageOptions ): (testModel: TestModel) => Array> { const resolvedOptions: Required = { @@ -35,7 +35,7 @@ export function allStates( }; } -export function allTransitions(): ( +export function coversAllTransitions(): ( testModel: TestModel ) => Array> { return (testModel) => { diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 8189d2ddd8..8728d28bd5 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -1,7 +1,7 @@ import { createTestModel } from '../src'; import { assign, createMachine } from 'xstate'; import { getDescription } from '../src/utils'; -import { stateValueCoverage } from '../src/coverage'; +import { coversAllStates } from '../src/coverage'; describe('die hard example', () => { interface DieHardContext { @@ -270,7 +270,7 @@ describe('die hard example', () => { } } - const coverage = dieHardModel.getCoverage(stateValueCoverage()); + const coverage = dieHardModel.getCoverage(coversAllStates()); expect(coverage.every((c) => c.status === 'covered')).toEqual(true); @@ -292,7 +292,7 @@ describe('die hard example', () => { ] `); - expect(() => dieHardModel.testCoverage(stateValueCoverage())).not.toThrow(); + expect(() => dieHardModel.testCoverage(coversAllStates())).not.toThrow(); }); }); describe('error path trace', () => { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 464a3e7a33..3cdf17383c 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,7 +1,7 @@ import { createTestModel } from '../src'; import { assign, createMachine, interpret } from 'xstate'; import { getDescription } from '../src/utils'; -import { allStates } from '../src/coverage'; +import { coversAllStates } from '../src/coverage'; describe('events', () => { it('should allow for representing many cases', async () => { @@ -70,7 +70,7 @@ describe('events', () => { await testModel.testPlan(plan, undefined); } - expect(() => testModel.testCoverage(allStates())).not.toThrow(); + expect(() => testModel.testCoverage(coversAllStates())).not.toThrow(); }); it('should not throw an error for unimplemented events', () => { @@ -466,7 +466,7 @@ describe('invocations', () => { } } - model.testCoverage(allStates()); + model.testCoverage(coversAllStates()); }); }); From 662b6ef93dc458630149fd48120b854308ac3616 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 27 Mar 2022 21:59:07 -0400 Subject: [PATCH 044/394] Fixing cont'd --- packages/xstate-test/test/coverage.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 4135ac83ec..948d85d5fb 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -1,6 +1,6 @@ import { createTestModel } from '../src'; import { createMachine } from 'xstate'; -import { allStates, allTransitions } from '../src/coverage'; +import { coversAllStates, coversAllTransitions } from '../src/coverage'; describe('coverage', () => { it('reports missing state node coverage', async () => { @@ -37,7 +37,7 @@ describe('coverage', () => { expect( testModel - .getCoverage(allStates()) + .getCoverage(coversAllStates()) .filter((c) => c.status !== 'covered') .map((c) => c.criterion.description) ).toMatchInlineSnapshot(` @@ -47,7 +47,7 @@ describe('coverage', () => { ] `); - expect(() => testModel.testCoverage(allStates())) + expect(() => testModel.testCoverage(coversAllStates())) .toThrowErrorMatchingInlineSnapshot(` "Coverage criteria not met: Visits \\"test.secondMissing\\" @@ -87,13 +87,13 @@ describe('coverage', () => { await model.testPlan(plan); } - const coverage = model.getCoverage(allStates()); + const coverage = model.getCoverage(coversAllStates()); expect(coverage).toHaveLength(5); expect(coverage.filter((c) => c.status !== 'covered')).toHaveLength(0); - expect(() => model.testCoverage(allStates())).not.toThrow(); + expect(() => model.testCoverage(coversAllStates())).not.toThrow(); }); it('skips filtered states (filter option)', async () => { @@ -133,7 +133,7 @@ describe('coverage', () => { expect(() => { testModel.testCoverage( - allStates({ + coversAllStates({ filter: (stateNode) => { return stateNode.key !== 'passthrough'; } @@ -186,7 +186,7 @@ describe('coverage', () => { // TODO: determine how to handle missing coverage for transient states, // which arguably should not be counted towards coverage, as the app is never in // a transient state for any length of time - model.testCoverage(allStates()); + model.testCoverage(coversAllStates()); }); it('tests transition coverage', async () => { @@ -208,7 +208,7 @@ describe('coverage', () => { await model.testPlans(model.getShortestPlans()); expect(() => { - model.testCoverage(allTransitions()); + model.testCoverage(coversAllTransitions()); }).toThrowErrorMatchingInlineSnapshot(` "Coverage criteria not met: Transitions a on event EVENT_TWO" @@ -217,7 +217,7 @@ describe('coverage', () => { await model.testPlans(model.getSimplePlans()); expect(() => { - model.testCoverage(allTransitions()); + model.testCoverage(coversAllTransitions()); }).not.toThrow(); }); }); From 556f98c0c8867f49607af4ec76199dd319514e2b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 27 Mar 2022 22:25:44 -0400 Subject: [PATCH 045/394] Add support for custom plan generation --- packages/xstate-test/src/TestModel.ts | 30 ++++++---- packages/xstate-test/src/types.ts | 12 +++- packages/xstate-test/test/plans.test.ts | 75 +++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 packages/xstate-test/test/plans.test.ts diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 7ffa9666c7..ec129d2d45 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -22,7 +22,8 @@ import type { TestPathResult, TestStepResult, Criterion, - CriterionResult + CriterionResult, + PlanGenerator } from './types'; import { formatPathTestResult, simpleStringify } from './utils'; @@ -81,15 +82,23 @@ export class TestModel { }; } + public getPlans( + planGenerator: PlanGenerator, + options?: Partial> + ): Array> { + const plans = planGenerator(this.behavior, this.resolveOptions(options)); + + return plans; + } + public getShortestPlans( options?: Partial> ): Array> { - const shortestPaths = traverseShortestPaths( - this.behavior, - this.resolveOptions(options) - ); + return this.getPlans((behavior, resolvedOptions) => { + const shortestPaths = traverseShortestPaths(behavior, resolvedOptions); - return Object.values(shortestPaths); + return Object.values(shortestPaths); + }, options); } public getShortestPlansTo( @@ -116,12 +125,11 @@ export class TestModel { public getSimplePlans( options?: Partial> ): Array> { - const simplePaths = traverseSimplePaths( - this.behavior, - this.resolveOptions(options) - ); + return this.getPlans((behavior, resolvedOptions) => { + const simplePaths = traverseSimplePaths(behavior, resolvedOptions); - return Object.values(simplePaths); + return Object.values(simplePaths); + }, options); } public getSimplePlansTo( diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 5f87031473..3291426093 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -1,4 +1,9 @@ -import { Step, TraversalOptions } from '@xstate/graph'; +import { + SimpleBehavior, + StatePlan, + Step, + TraversalOptions +} from '@xstate/graph'; import { AnyState, EventObject, @@ -251,3 +256,8 @@ export interface TestMachineConfig< [key: string]: TestStateNodeConfig; }; } + +export type PlanGenerator = ( + behavior: SimpleBehavior, + options: TraversalOptions +) => Array>; diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts new file mode 100644 index 0000000000..d2be80cf42 --- /dev/null +++ b/packages/xstate-test/test/plans.test.ts @@ -0,0 +1,75 @@ +import { createMachine } from 'xstate'; +import { createTestModel } from '../src'; +import { coversAllStates } from '../src/coverage'; + +describe('testModel.testPlans(...)', () => { + it('custom plan generators can be provided', async () => { + const testModel = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } + }) + ); + + const plans = testModel.getPlans((behavior, options) => { + const events = options.getEvents?.(behavior.initialState) ?? []; + + const nextState = behavior.transition(behavior.initialState, events[0]); + return [ + { + state: nextState, + paths: [ + { + state: nextState, + steps: [ + { + state: behavior.initialState, + event: events[0] + } + ], + weight: 1 + } + ] + } + ]; + }); + + await testModel.testPlans(plans); + + expect(testModel.getCoverage(coversAllStates())).toMatchInlineSnapshot(` + Array [ + Object { + "criterion": Object { + "description": "Visits \\"(machine)\\"", + "predicate": [Function], + "skip": false, + }, + "status": "covered", + }, + Object { + "criterion": Object { + "description": "Visits \\"(machine).a\\"", + "predicate": [Function], + "skip": false, + }, + "status": "covered", + }, + Object { + "criterion": Object { + "description": "Visits \\"(machine).b\\"", + "predicate": [Function], + "skip": false, + }, + "status": "covered", + }, + ] + `); + }); +}); From 291eef82b6bfbc61b8ddba7d8a6d4478aebf3573 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 28 Mar 2022 07:57:13 -0400 Subject: [PATCH 046/394] Cleanup coverage types --- packages/xstate-test/src/TestModel.ts | 12 ++++------ packages/xstate-test/src/coverage.ts | 28 +++++++++++++++------- packages/xstate-test/src/types.ts | 12 +++++----- packages/xstate-test/test/coverage.test.ts | 4 ++-- packages/xstate-test/test/index.test.ts | 10 ++++---- 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index ec129d2d45..c020f0c7f5 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -15,13 +15,13 @@ import { traverseSimplePathsTo } from '@xstate/graph/src/graph'; import { EventObject } from 'xstate'; +import { CoverageFunction } from './coverage'; import type { TestModelCoverage, TestModelOptions, StatePredicate, TestPathResult, TestStepResult, - Criterion, CriterionResult, PlanGenerator } from './types'; @@ -49,7 +49,7 @@ import { formatPathTestResult, simpleStringify } from './utils'; */ export class TestModel { - private _coverage: TestModelCoverage = { + private _coverage: TestModelCoverage = { states: {}, transitions: {} }; @@ -310,8 +310,8 @@ export class TestModel { } public getCoverage( - criteriaFn?: (testModel: this) => Array> - ): Array> { + criteriaFn?: CoverageFunction + ): Array> { const criteria = criteriaFn?.(this) ?? []; return criteria.map((criterion) => { @@ -326,9 +326,7 @@ export class TestModel { }); } - public testCoverage( - criteriaFn?: (testModel: this) => Array> - ): void { + public testCoverage(criteriaFn?: CoverageFunction): void { const criteriaResult = this.getCoverage(criteriaFn); const unmetCriteria = criteriaResult.filter( diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index 84ec7ce604..bf1da26a30 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -1,5 +1,5 @@ import { AnyStateNode } from '@xstate/graph'; -import type { AnyState } from 'xstate'; +import type { AnyState, EventObject } from 'xstate'; import { getAllStateNodes } from 'xstate/lib/stateUtils'; import { flatten } from '.'; import { TestModel } from './TestModel'; @@ -9,16 +9,23 @@ interface StateValueCoverageOptions { filter?: (stateNode: AnyStateNode) => boolean; } -export function coversAllStates( - options?: StateValueCoverageOptions -): (testModel: TestModel) => Array> { +export type CoverageFunction = ( + testModel: TestModel +) => Array>; + +export function coversAllStates< + TState extends AnyState, + TEvent extends EventObject +>(options?: StateValueCoverageOptions): CoverageFunction { const resolvedOptions: Required = { filter: () => true, ...options }; return (testModel) => { - const allStateNodes = getAllStateNodes(testModel.behavior as AnyStateNode); + const allStateNodes = getAllStateNodes( + (testModel.behavior as unknown) as AnyStateNode + ); return allStateNodes.map((stateNode) => { const skip = !resolvedOptions.filter(stateNode); @@ -35,11 +42,14 @@ export function coversAllStates( }; } -export function coversAllTransitions(): ( - testModel: TestModel -) => Array> { +export function coversAllTransitions< + TState extends AnyState, + TEvent extends EventObject +>(): CoverageFunction { return (testModel) => { - const allStateNodes = getAllStateNodes(testModel.behavior as AnyStateNode); + const allStateNodes = getAllStateNodes( + (testModel.behavior as unknown) as AnyStateNode + ); const allTransitions = flatten(allStateNodes.map((sn) => sn.transitions)); return allTransitions.map((t) => { diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 3291426093..fd8340fadc 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -189,23 +189,23 @@ export interface TestTransitionCoverage { count: number; } -export interface TestModelCoverage { +export interface TestModelCoverage { states: Record>; - transitions: Record>; + transitions: Record>; } export interface CoverageOptions { filter?: (stateNode: StateNode) => boolean; } -export interface Criterion { - predicate: (coverage: TestModelCoverage) => boolean; +export interface Criterion { + predicate: (coverage: TestModelCoverage) => boolean; description: string; skip?: boolean; } -export interface CriterionResult { - criterion: Criterion; +export interface CriterionResult { + criterion: Criterion; /** * Whether the criterion was covered or not */ diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 948d85d5fb..d42e6541ca 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -32,7 +32,7 @@ describe('coverage', () => { const plans = testModel.getShortestPlans(); for (const plan of plans) { - await testModel.testPlan(plan, undefined); + await testModel.testPlan(plan); } expect( @@ -125,7 +125,7 @@ describe('coverage', () => { const promises: any[] = []; testPlans.forEach((plan) => { plan.paths.forEach(() => { - promises.push(testModel.testPlan(plan, undefined)); + promises.push(testModel.testPlan(plan)); }); }); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 3cdf17383c..4a086736ad 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -67,7 +67,7 @@ describe('events', () => { const testPlans = testModel.getShortestPlans(); for (const plan of testPlans) { - await testModel.testPlan(plan, undefined); + await testModel.testPlan(plan); } expect(() => testModel.testCoverage(coversAllStates())).not.toThrow(); @@ -90,7 +90,7 @@ describe('events', () => { expect(async () => { for (const plan of Object.values(testPlans)) { - await testModel.testPlan(plan, undefined); + await testModel.testPlan(plan); } }).not.toThrow(); }); @@ -248,7 +248,7 @@ it('executes actions', async () => { const testPlans = model.getShortestPlans(); for (const plan of testPlans) { - await model.testPlan(plan, undefined); + await model.testPlan(plan); } expect(executedActive).toBe(true); @@ -281,7 +281,7 @@ describe('test model options', () => { const plans = model.getShortestPlans(); for (const plan of plans) { - await model.testPlan(plan, null as any); + await model.testPlan(plan); } expect(testedStates).toEqual(['inactive', 'inactive', 'active']); @@ -319,7 +319,7 @@ describe('test model options', () => { const plans = model.getShortestPlans(); for (const plan of plans) { - await model.testPlan(plan, null as any); + await model.testPlan(plan); } expect(testedEvents).toEqual(['NEXT', 'NEXT', 'PREV']); From 154620ec8f5482e41aaba47c3e628995f14186e7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 28 Mar 2022 22:39:11 -0400 Subject: [PATCH 047/394] Add some todos --- packages/xstate-test/src/TestModel.ts | 2 ++ packages/xstate-test/test/index.test.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index c020f0c7f5..a957002a7d 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -326,6 +326,8 @@ export class TestModel { }); } + // TODO: criteriaFn can be array + // TODO: consider options public testCoverage(criteriaFn?: CoverageFunction): void { const criteriaResult = this.getCoverage(criteriaFn); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 4a086736ad..6f9db96c90 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -218,6 +218,7 @@ it('prevents infinite recursion based on a provided limit', () => { }).toThrowErrorMatchingInlineSnapshot(`"Traversal limit exceeded"`); }); +// TODO: have this as an opt-in it('executes actions', async () => { let executedActive = false; let executedDone = false; From 54fa098517a7b46c5bf25685a0ecf9e497a9e590 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 3 Apr 2022 10:54:01 -0400 Subject: [PATCH 048/394] Add support for multiple coverage --- packages/xstate-test/src/TestModel.ts | 13 ++++++--- packages/xstate-test/test/coverage.test.ts | 32 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index a957002a7d..7cd5b704da 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -14,7 +14,8 @@ import { traverseSimplePaths, traverseSimplePathsTo } from '@xstate/graph/src/graph'; -import { EventObject } from 'xstate'; +import { EventObject, SingleOrArray } from 'xstate'; +import { flatten } from '.'; import { CoverageFunction } from './coverage'; import type { TestModelCoverage, @@ -326,10 +327,14 @@ export class TestModel { }); } - // TODO: criteriaFn can be array // TODO: consider options - public testCoverage(criteriaFn?: CoverageFunction): void { - const criteriaResult = this.getCoverage(criteriaFn); + public testCoverage( + criteriaFn?: SingleOrArray> + ): void { + const criteriaFns = Array.isArray(criteriaFn) ? criteriaFn : [criteriaFn]; + const criteriaResult = flatten( + criteriaFns.map((fn) => this.getCoverage(fn)) + ); const unmetCriteria = criteriaResult.filter( (c) => c.status === 'uncovered' diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index d42e6541ca..ae03747f1f 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -220,4 +220,36 @@ describe('coverage', () => { model.testCoverage(coversAllTransitions()); }).not.toThrow(); }); + + it('tests multiple kinds of coverage', async () => { + const model = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT_ONE: 'b', + EVENT_TWO: 'b' + } + }, + b: {} + } + }) + ); + + await model.testPlans(model.getShortestPlans()); + + expect(() => { + model.testCoverage([coversAllStates(), coversAllTransitions()]); + }).toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Transitions a on event EVENT_TWO" + `); + + await model.testPlans(model.getSimplePlans()); + + expect(() => { + model.testCoverage([coversAllStates(), coversAllTransitions()]); + }).not.toThrow(); + }); }); From 3b8c54c7846a42aa8ab0accdc38a4593d2145c45 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 3 Apr 2022 12:15:33 -0400 Subject: [PATCH 049/394] Add state tests + state matcher --- packages/xstate-test/src/TestModel.ts | 18 ++- packages/xstate-test/src/machine.ts | 7 +- packages/xstate-test/src/types.ts | 6 +- packages/xstate-test/test/index.test.ts | 149 +++++++++++++++++--- packages/xstate-test/test/testModel.test.ts | 91 +++++++++--- 5 files changed, 231 insertions(+), 40 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 7cd5b704da..182c58e10e 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -61,9 +61,10 @@ export class TestModel { serializeState: (state) => simpleStringify(state) as SerializedState, serializeEvent: (event) => simpleStringify(event) as SerializedEvent, getEvents: () => [], + states: {}, events: {}, + stateMatcher: (_, stateKey) => stateKey === '*', getStates: () => [], - testState: () => void 0, testTransition: () => void 0, execute: () => void 0, logger: { @@ -258,7 +259,20 @@ export class TestModel { ): Promise { const resolvedOptions = this.resolveOptions(options); - await resolvedOptions.testState(state); + const stateTestKeys = Object.keys(resolvedOptions.states).filter( + (stateKey) => { + return resolvedOptions.stateMatcher(state, stateKey); + } + ); + + // Fallthrough state tests + if (!stateTestKeys.length && '*' in resolvedOptions.states) { + stateTestKeys.push('*'); + } + + for (const stateTestKey of stateTestKeys) { + await resolvedOptions.states[stateTestKey](state); + } await resolvedOptions.execute(state); diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 12d4a5f46f..8f122cdc79 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -64,7 +64,12 @@ export function createTestModel( machine as SimpleBehavior, { serializeState, - testState: testMachineState, + stateMatcher: (state, key) => { + return state.matches(key); + }, + states: { + '*': testMachineState + }, execute: (state) => { state.actions.forEach((action) => { executeAction(action, state); diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index fd8340fadc..53f67a3337 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -149,13 +149,17 @@ export interface TestModelEventConfig { export interface TestModelOptions extends TraversalOptions { - testState: (state: TState) => void | Promise; + // testState: (state: TState) => void | Promise; testTransition: (step: Step) => void | Promise; /** * Executes actions based on the `state` after the state is tested. */ execute: (state: TState) => void | Promise; getStates: () => TState[]; + stateMatcher: (state: TState, stateKey: string) => boolean; + states: { + [key: string]: (state: TState) => void | Promise; + }; events: { [TEventType in string /* TODO: TEvent['type'] */]?: TestModelEventConfig< TState, diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 6f9db96c90..8ccb8d5f65 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -273,8 +273,10 @@ describe('test model options', () => { } }), { - testState: (state) => { - testedStates.push(state.value); + states: { + '*': (state) => { + testedStates.push(state.value); + } } } ); @@ -439,22 +441,24 @@ describe('invocations', () => { const service = interpret(machine).start(); await model.testPath(path, { - testState: (state) => { - return new Promise((res) => { - let actualState; - const t = setTimeout(() => { - throw new Error( - `expected ${state.value}, got ${actualState.value}` - ); - }, 1000); - service.subscribe((s) => { - actualState = s; - if (s.matches(state.value)) { - clearTimeout(t); - res(); - } + states: { + '*': (state) => { + return new Promise((res) => { + let actualState; + const t = setTimeout(() => { + throw new Error( + `expected ${state.value}, got ${actualState.value}` + ); + }, 1000); + service.subscribe((s) => { + actualState = s; + if (s.matches(state.value)) { + clearTimeout(t); + res(); + } + }); }); - }); + } }, testTransition: (step) => { if (step.event.type.startsWith('done.')) { @@ -539,3 +543,114 @@ it('Event in event executor should contain payload from case', async () => { await model.testPath(path, obj); }); + +describe('state tests', () => { + it('should test states', async () => { + // a (1) + // a -> b (2) + expect.assertions(3); + + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { NEXT: 'b' } + }, + b: {} + } + }); + + const model = createTestModel(machine, { + states: { + a: (state) => { + expect(state.value).toEqual('a'); + }, + b: (state) => { + expect(state.value).toEqual('b'); + } + } + }); + + await model.testPlans(model.getShortestPlans()); + }); + + it('should test wildcard state for non-matching states', async () => { + // a (1) + // a -> b (2) + // a -> c (2) + expect.assertions(5); + + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { NEXT: 'b', OTHER: 'c' } + }, + b: {}, + c: {} + } + }); + + const model = createTestModel(machine, { + states: { + a: (state) => { + expect(state.value).toEqual('a'); + }, + b: (state) => { + expect(state.value).toEqual('b'); + }, + '*': (state) => { + expect(state.value).toEqual('c'); + } + } + }); + + await model.testPlans(model.getShortestPlans()); + }); + + it('should test nested states', async () => { + const testedStateValues: any[] = []; + + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { NEXT: 'b' } + }, + b: { + initial: 'b1', + states: { + b1: {} + } + } + } + }); + + const model = createTestModel(machine, { + 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' }); + } + } + }); + + await model.testPlans(model.getShortestPlans()); + expect(testedStateValues).toMatchInlineSnapshot(` + Array [ + "a", + "a", + "b", + "b.b1", + ] + `); + }); +}); diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index 0a755711b8..cdd588d27d 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -1,28 +1,81 @@ import { TestModel } from '../src/TestModel'; -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; +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' }]; } } - }, - { - getEvents: (state) => { - if (state % 2 === 0) { - return [{ type: 'even' }]; + ); + + const plans = model.getShortestPlansTo((state) => state === 1); + + expect(plans.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' }]; + }, + states: { + even: (state) => { + testedStateKeys.push('even'); + expect(state % 2).toBe(0); + }, + odd: (state) => { + testedStateKeys.push('odd'); + expect(state % 2).toBe(1); + } + }, + stateMatcher: (state, key) => { + if (key === 'even') { + return state % 2 === 0; + } + if (key === 'odd') { + return state % 2 === 1; + } + return false; } - return [{ type: 'odd' }]; } - } - ); + ); + + const plans = model.getShortestPlansTo((state) => state === 1); - const plans = model.getShortestPlansTo((state) => state === 1); + await model.testPlans(plans); - expect(plans.length).toBeGreaterThan(0); + expect(testedStateKeys).toContain('even'); + expect(testedStateKeys).toContain('odd'); + }); }); From 942160ab4a576aa309321e00c44169868b36eaad Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 3 Apr 2022 12:32:42 -0400 Subject: [PATCH 050/394] Dynamic case generation based on state --- packages/xstate-test/src/machine.ts | 17 ++++--- packages/xstate-test/src/types.ts | 19 ++++--- packages/xstate-test/test/index.test.ts | 66 +++++++++++++++++++++++-- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 8f122cdc79..262a5871c3 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -8,7 +8,7 @@ import type { } from 'xstate'; import { flatten } from '.'; import { TestModel } from './TestModel'; -import { TestModelOptions } from './types'; +import { TestModelEventConfig, TestModelOptions } from './types'; export async function testMachineState(state: AnyState) { for (const id of Object.keys(state.meta)) { @@ -78,15 +78,18 @@ export function createTestModel( getEvents: (state) => flatten( state.nextEvents.map((eventType) => { - const eventCaseGenerator = options?.events?.[eventType]?.cases; + const eventCaseGenerator = options?.events?.[eventType] + ?.cases as TestModelEventConfig['cases']; + + const cases = eventCaseGenerator + ? Array.isArray(eventCaseGenerator) + ? eventCaseGenerator + : eventCaseGenerator(state) + : [{ type: eventType }]; return ( // Use generated events or a plain event without payload - ( - eventCaseGenerator?.() ?? [ - { type: eventType } as EventFrom - ] - ).map((e) => { + cases.map((e) => { return { type: eventType, ...(e as any) }; }) ); diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 53f67a3337..3a0721f8ea 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -94,10 +94,7 @@ export interface TestPlan { * } * ``` */ -interface EventCase { - type?: never; - [prop: string]: any; -} +type EventCase = Omit; export type StatePredicate = (state: TState) => boolean; /** @@ -133,17 +130,19 @@ export interface TestEventConfig { * ] * ``` */ - cases?: EventCase[]; + cases?: Array>; } -export interface TestEventsConfig { - [eventType: string]: +export type TestEventsConfig = { + [EventType in TEvent['type']]?: | EventExecutor | TestEventConfig; -} +}; export interface TestModelEventConfig { - cases?: () => Array | TEvent>; + cases?: + | ((state: TState) => Array>) + | Array>; exec?: EventExecutor; } @@ -161,7 +160,7 @@ export interface TestModelOptions [key: string]: (state: TState) => void | Promise; }; events: { - [TEventType in string /* TODO: TEvent['type'] */]?: TestModelEventConfig< + [TEventType in TEvent['type']]?: TestModelEventConfig< TState, ExtractEvent >; diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 8ccb8d5f65..5c25441d21 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -60,7 +60,7 @@ describe('events', () => { const testModel = createTestModel(feedbackMachine, { events: { - SUBMIT: { cases: () => [{ value: 'something' }, { value: '' }] } + SUBMIT: { cases: [{ value: 'something' }, { value: '' }] } } }); @@ -94,6 +94,66 @@ describe('events', () => { } }).not.toThrow(); }); + + it('should allow for dynamic generation of cases based on state', async () => { + const testMachine = createMachine({ + initial: 'a', + context: { + values: [1, 2, 3] // 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, { + events: { + EVENT: { + // Read dynamically from state context + cases: (state) => state.context.values.map((value) => ({ value })), + exec: ({ event }) => { + testedEvents.push(event); + } + } + } + }); + + const plans = testModel.getShortestPlans(); + + expect(plans.length).toBe(4); + + await testModel.testPlans(plans); + + expect(testedEvents).toMatchInlineSnapshot(` + Array [ + Object { + "type": "EVENT", + "value": 1, + }, + Object { + "type": "EVENT", + "value": 2, + }, + Object { + "type": "EVENT", + "value": 3, + }, + ] + `); + }); }); describe('state limiting', () => { @@ -426,7 +486,7 @@ describe('invocations', () => { const model = createTestModel(machine, { events: { START: { - cases: () => [ + cases: [ { type: 'START', value: 42 }, { type: 'START', value: 1 } ] @@ -525,7 +585,7 @@ it('Event in event executor should contain payload from case', async () => { const model = createTestModel(machine, { events: { NEXT: { - cases: () => [{ payload: 10, fn: nonSerializableData }], + cases: [{ payload: 10, fn: nonSerializableData }], exec: (step) => { expect(step.event).toEqual({ type: 'NEXT', From 0e63af4d4a6b445a813dc858f050df8be4c58737 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 3 Apr 2022 12:41:50 -0400 Subject: [PATCH 051/394] Replace meta tests --- packages/xstate-graph/src/graph.ts | 2 +- packages/xstate-test/src/machine.ts | 4 +-- packages/xstate-test/test/dieHard.test.ts | 44 ++++++++++------------- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 62f34966ea..bf58853b86 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -317,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, diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 262a5871c3..78c3e951e3 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -10,7 +10,7 @@ import { flatten } from '.'; import { TestModel } from './TestModel'; import { TestModelEventConfig, TestModelOptions } from './types'; -export async function testMachineState(state: AnyState) { +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) { @@ -68,7 +68,7 @@ export function createTestModel( return state.matches(key); }, states: { - '*': testMachineState + '*': testStateFromMeta }, execute: (state) => { state.actions.forEach((action) => { diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 8728d28bd5..b5461f9a81 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -96,26 +96,10 @@ describe('die hard example', () => { EMPTY_5: { actions: empty5 } - }, - meta: { - description: (state) => { - return `pending with (${state.context.three}, ${state.context.five})`; - }, - test: async (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 () => { - expect(jugs.five).toEqual(4); - } - } + type: 'final' } } }, @@ -131,6 +115,16 @@ describe('die hard example', () => { jugs = new Jugs(); jugs.version = Math.random(); }, + 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: { exec: async () => { @@ -306,17 +300,17 @@ describe('error path trace', () => { second: { on: { NEXT: 'third' } }, - third: { - meta: { - test: () => { - throw new Error('test error'); - } - } - } + third: {} } }); - const testModel = createTestModel(machine); + const testModel = createTestModel(machine, { + states: { + third: () => { + throw new Error('test error'); + } + } + }); testModel .getShortestPlansTo((state) => state.matches('third')) From 30b99829ec442a539a784cdc8223b06d1a2ac97f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 7 Apr 2022 22:11:57 -0400 Subject: [PATCH 052/394] Add events.test.ts --- packages/xstate-test/src/machine.ts | 26 +++++++--- packages/xstate-test/src/types.ts | 7 ++- packages/xstate-test/test/events.test.ts | 64 ++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 packages/xstate-test/test/events.test.ts diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 78c3e951e3..4d41f442af 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -8,7 +8,7 @@ import type { } from 'xstate'; import { flatten } from '.'; import { TestModel } from './TestModel'; -import { TestModelEventConfig, TestModelOptions } from './types'; +import { TestModelEventConfig, TestModelOptions, EventExecutor } from './types'; export async function testStateFromMeta(state: AnyState) { for (const id of Object.keys(state.meta)) { @@ -78,8 +78,14 @@ export function createTestModel( getEvents: (state) => flatten( state.nextEvents.map((eventType) => { - const eventCaseGenerator = options?.events?.[eventType] - ?.cases as TestModelEventConfig['cases']; + const eventConfig = options?.events?.[eventType]; + const eventCaseGenerator = + typeof eventConfig === 'function' + ? undefined + : (eventConfig?.cases as TestModelEventConfig< + any, + any + >['cases']); const cases = eventCaseGenerator ? Array.isArray(eventCaseGenerator) @@ -96,10 +102,18 @@ export function createTestModel( }) ), testTransition: async (step) => { - // TODO: fix types - const eventConfig = options?.events?.[(step.event as any).type] as any; + const eventConfig = + testModel.options.events?.[ + (step.event as any).type as EventFrom['type'] + ]; - await eventConfig?.exec?.(step as any); + const eventExec = + typeof eventConfig === 'function' ? eventConfig : eventConfig?.exec; + + await (eventExec as EventExecutor< + StateFrom, + EventFrom + >)?.(step); }, ...options } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 3a0721f8ea..847f15c9a1 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -160,10 +160,9 @@ export interface TestModelOptions [key: string]: (state: TState) => void | Promise; }; events: { - [TEventType in TEvent['type']]?: TestModelEventConfig< - TState, - ExtractEvent - >; + [TEventType in TEvent['type']]?: + | EventExecutor + | TestModelEventConfig>; }; logger: { log: (msg: string) => void; diff --git a/packages/xstate-test/test/events.test.ts b/packages/xstate-test/test/events.test.ts new file mode 100644 index 0000000000..4e0c5f85bf --- /dev/null +++ b/packages/xstate-test/test/events.test.ts @@ -0,0 +1,64 @@ +import { createMachine } from 'xstate'; +import { createTestModel } from '../src'; + +describe('events', () => { + it('should execute events (`exec` property)', async () => { + let executed = false; + + const testModel = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } + }), + { + events: { + EVENT: { + exec: () => { + executed = true; + } + } + } + } + ); + + await testModel.testPlans(testModel.getShortestPlans()); + + expect(executed).toBe(true); + }); + + it('should execute events (function)', async () => { + let executed = false; + + const testModel = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } + }), + { + events: { + EVENT: () => { + executed = true; + } + } + } + ); + + await testModel.testPlans(testModel.getShortestPlans()); + + expect(executed).toBe(true); + }); +}); From 0c483ec736c02f05a0b0593c2d91a88049a86e4d Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 7 Apr 2022 22:22:45 -0400 Subject: [PATCH 053/394] Test by state key or ID, add states.test.ts --- packages/xstate-test/src/machine.ts | 4 +- packages/xstate-test/test/states.test.ts | 146 +++++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 packages/xstate-test/test/states.test.ts diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 4d41f442af..6323e1e9d5 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -65,7 +65,9 @@ export function createTestModel( { serializeState, stateMatcher: (state, key) => { - return state.matches(key); + return key.startsWith('#') + ? state.configuration.includes(machine.getStateNodeById(key)) + : state.matches(key); }, states: { '*': testStateFromMeta diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts new file mode 100644 index 0000000000..50ba71d350 --- /dev/null +++ b/packages/xstate-test/test/states.test.ts @@ -0,0 +1,146 @@ +import { createMachine, StateValue } from 'xstate'; +import { createTestModel } from '../src'; + +describe('states', () => { + it('should test states by key', async () => { + const testedStateValues: StateValue[] = []; + const testModel = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + id: 'statea_a', + on: { + EVENT: 'b' + } + }, + b: { + id: 'statae_b', + initial: 'b1', + states: { + b1: { on: { NEXT: 'b2' } }, + b2: {} + } + } + } + }), + { + 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); + } + } + } + ); + + await testModel.testPlans(testModel.getShortestPlans()); + + expect(testedStateValues).toMatchInlineSnapshot(` + Array [ + "a", + "a", + Object { + "b": "b1", + }, + Object { + "b": "b1", + }, + "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( + createMachine({ + 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' + } + } + } + } + }), + { + 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); + } + } + } + ); + + await testModel.testPlans(testModel.getShortestPlans()); + + expect(testedStateValues).toMatchInlineSnapshot(` + Array [ + "a", + "a", + Object { + "b": "b1", + }, + Object { + "b": "b1", + }, + "a", + Object { + "b": "b1", + }, + Object { + "b": "b1", + }, + Object { + "b": "b2", + }, + Object { + "b": "b2", + }, + ] + `); + }); +}); From e807b5d19523c4ac2b97ce85dcc71fe88e3c7d28 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 8 Apr 2022 20:33:42 -0400 Subject: [PATCH 054/394] Remove unused ids --- packages/xstate-test/test/states.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts index 50ba71d350..ee0a31dce4 100644 --- a/packages/xstate-test/test/states.test.ts +++ b/packages/xstate-test/test/states.test.ts @@ -9,13 +9,11 @@ describe('states', () => { initial: 'a', states: { a: { - id: 'statea_a', on: { EVENT: 'b' } }, b: { - id: 'statae_b', initial: 'b1', states: { b1: { on: { NEXT: 'b2' } }, From 57acfadd5ec27ca329eb6061f94353110460e003 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 9 Apr 2022 09:10:16 -0400 Subject: [PATCH 055/394] Export TestModel + some cleanup --- packages/xstate-test/src/TestModel.ts | 2 +- packages/xstate-test/src/coverage.ts | 2 +- packages/xstate-test/src/index.ts | 39 +-------------------------- packages/xstate-test/src/machine.ts | 2 +- packages/xstate-test/src/utils.ts | 4 +++ 5 files changed, 8 insertions(+), 41 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 182c58e10e..cdb1541aae 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -15,7 +15,7 @@ import { traverseSimplePathsTo } from '@xstate/graph/src/graph'; import { EventObject, SingleOrArray } from 'xstate'; -import { flatten } from '.'; +import { flatten } from './utils'; import { CoverageFunction } from './coverage'; import type { TestModelCoverage, diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index bf1da26a30..f5e4b8bd58 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -1,7 +1,7 @@ import { AnyStateNode } from '@xstate/graph'; import type { AnyState, EventObject } from 'xstate'; import { getAllStateNodes } from 'xstate/lib/stateUtils'; -import { flatten } from '.'; +import { flatten } from './utils'; import { TestModel } from './TestModel'; import { Criterion } from './types'; diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 0844204e02..1606f0949e 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,39 +1,2 @@ -import { EventObject } from 'xstate'; -import { TestEventsConfig } from './types'; - export { createTestModel } from './machine'; - -export function getEventSamples( - eventsOptions: TestEventsConfig // TODO -): TEvent[] { - const result: TEvent[] = []; - - Object.keys(eventsOptions).forEach((key) => { - const eventConfig = eventsOptions[key]; - if (typeof eventConfig === 'function') { - result.push({ - type: key - } as any); - return; - } - - const events = eventConfig.cases - ? eventConfig.cases.map((sample) => ({ - type: key, - ...sample - })) - : [ - { - type: key - } - ]; - - result.push(...(events as any[])); - }); - - return result; -} - -export function flatten(array: Array): T[] { - return ([] as T[]).concat(...array); -} +export { TestModel } from './TestModel'; diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 6323e1e9d5..06ad25ca39 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -6,7 +6,7 @@ import type { EventFrom, StateFrom } from 'xstate'; -import { flatten } from '.'; +import { flatten } from './utils'; import { TestModel } from './TestModel'; import { TestModelEventConfig, TestModelOptions, EventExecutor } from './types'; diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts index 33ffccffe1..54bde370dc 100644 --- a/packages/xstate-test/src/utils.ts +++ b/packages/xstate-test/src/utils.ts @@ -100,3 +100,7 @@ export function getDescription(state: AnyState): string { ` ${contextString}` ); } + +export function flatten(array: Array): T[] { + return ([] as T[]).concat(...array); +} From eb370e09b5557b5c40921eb5e7c64b03099b2e75 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 9 Apr 2022 09:11:08 -0400 Subject: [PATCH 056/394] Fix test import of TestModel --- packages/xstate-test/test/testModel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index cdd588d27d..0f8011199e 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -1,4 +1,4 @@ -import { TestModel } from '../src/TestModel'; +import { TestModel } from '../src'; describe('custom test models', () => { it('tests any behavior', async () => { From f63c226cb70cde461aa84ef4bae9292e249ce1b2 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 25 Apr 2022 16:30:26 -0400 Subject: [PATCH 057/394] Renaming --- packages/xstate-graph/src/graph.ts | 62 +- packages/xstate-graph/src/index.ts | 12 +- packages/xstate-graph/src/types.ts | 6 +- .../test/__snapshots__/graph.test.ts.snap | 2091 ++++++++--------- packages/xstate-graph/test/graph.test.ts | 244 +- packages/xstate-test/src/TestModel.ts | 8 +- packages/xstate-test/test/dieHard.test.ts | 6 +- 7 files changed, 1201 insertions(+), 1228 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index bf58853b86..5e6472938f 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -1,4 +1,4 @@ -import { +import type { State, DefaultContext, Event, @@ -9,15 +9,15 @@ import { StateFrom, EventFrom } from 'xstate'; -import { +import type { SerializedEvent, SerializedState, SimpleBehavior, StatePath, StatePlan } from '.'; -import { - StatePathsMap, +import type { + StatePlanMap, AdjacencyMap, Steps, ValueAdjMapOptions, @@ -73,11 +73,9 @@ export function getChildren(stateNode: AnyStateNode): AnyStateNode[] { return children; } -export function serializeState(state: AnyState): SerializedState { +export function serializeMachineState(state: AnyState): SerializedState { const { value, context, actions } = state; - return [value, context, actions.map((a) => a.type).join(',')] - .map((x) => JSON.stringify(x)) - .join(' | ') as SerializedState; + return JSON.stringify({ value, context, actions }) as SerializedState; } export function serializeEvent( @@ -89,7 +87,7 @@ export function serializeEvent( const defaultValueAdjMapOptions: Required> = { events: {}, filter: () => true, - stateSerializer: serializeState, + stateSerializer: serializeMachineState, eventSerializer: serializeEvent }; @@ -175,22 +173,22 @@ export function getAdjacencyMap( } const defaultMachineStateOptions: TraversalOptions, any> = { - serializeState, + serializeState: serializeMachineState, serializeEvent, getEvents: (state) => { return state.nextEvents.map((type) => ({ type })); } }; -export function getShortestPaths( +export function getShortestPlans( machine: TMachine, options?: Partial> -): StatePathsMap { +): Array> { const resolvedOptions = resolveTraversalOptions( options, defaultMachineStateOptions ); - return traverseShortestPaths( + return traverseShortestPlans( { transition: (state, event) => machine.transition(state, event), initialState: machine.initialState @@ -199,10 +197,10 @@ export function getShortestPaths( ); } -export function traverseShortestPaths( +export function traverseShortestPlans( behavior: SimpleBehavior, options?: Partial> -): StatePathsMap { +): Array> { const optionsWithDefaults = resolveTraversalOptions(options); const { serializeState } = optionsWithDefaults; @@ -258,11 +256,11 @@ export function traverseShortestPaths( } } - const statePathMap: StatePathsMap = {}; + const statePlanMap: StatePlanMap = {}; weightMap.forEach(([weight, fromState, fromEvent], stateSerial) => { const state = stateMap.get(stateSerial)!; - statePathMap[stateSerial] = { + statePlanMap[stateSerial] = { state, paths: !fromState ? [ @@ -275,7 +273,7 @@ export function traverseShortestPaths( : [ { state, - steps: statePathMap[fromState].paths[0].steps.concat({ + steps: statePlanMap[fromState].paths[0].steps.concat({ state: stateMap.get(fromState)!, event: fromEvent! }), @@ -285,22 +283,22 @@ export function traverseShortestPaths( }; }); - return statePathMap; + return Object.values(statePlanMap); } -export function getSimplePaths< +export function getSimplePlans< TContext = DefaultContext, TEvent extends EventObject = EventObject >( machine: StateMachine, options?: Partial, TEvent>> -): StatePathsMap, TEvent> { +): Array, TEvent>> { const resolvedOptions = resolveTraversalOptions( options, defaultMachineStateOptions ); - return traverseSimplePaths( + return traverseSimplePlans( machine as SimpleBehavior, resolvedOptions ); @@ -494,10 +492,10 @@ function resolveTraversalOptions( }; } -export function traverseSimplePaths( +export function traverseSimplePlans( behavior: SimpleBehavior, options: Partial> -): StatePathsMap { +): Array> { const { initialState } = behavior; const resolvedOptions = resolveTraversalOptions(options); const { serializeState, visitCondition } = resolvedOptions; @@ -575,16 +573,14 @@ export function traverseSimplePaths( util(initialState, nextStateSerial, null); } - return pathMap; + return Object.values(pathMap); } export function filterPlans( - plans: StatePathsMap, + plans: Array>, predicate: (state: TState, plan: StatePlan) => boolean ): Array> { - const filteredPlans = Object.values(plans).filter((plan) => - predicate(plan.state, plan) - ); + const filteredPlans = plans.filter((plan) => predicate(plan.state, plan)); return filteredPlans; } @@ -595,7 +591,7 @@ export function traverseSimplePathsTo( options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const simplePlansMap = traverseSimplePaths(behavior, resolvedOptions); + const simplePlansMap = traverseSimplePlans(behavior, resolvedOptions); return filterPlans(simplePlansMap, predicate); } @@ -607,7 +603,7 @@ export function traverseSimplePathsFromTo( options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const simplePlansMap = traverseSimplePaths(behavior, resolvedOptions); + const simplePlansMap = traverseSimplePlans(behavior, resolvedOptions); // Return all plans that contain a "from" state and target a "to" state return filterPlans(simplePlansMap, (state, plan) => { @@ -623,7 +619,7 @@ export function traverseShortestPathsTo( options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const simplePlansMap = traverseShortestPaths(behavior, resolvedOptions); + const simplePlansMap = traverseShortestPlans(behavior, resolvedOptions); return filterPlans(simplePlansMap, predicate); } @@ -635,7 +631,7 @@ export function traverseShortestPathsFromTo( options: TraversalOptions ): Array> { const resolvedOptions = resolveTraversalOptions(options); - const shortesPlansMap = traverseShortestPaths(behavior, resolvedOptions); + const shortesPlansMap = traverseShortestPlans(behavior, resolvedOptions); // Return all plans that contain a "from" state and target a "to" state return filterPlans(shortesPlansMap, (state, plan) => { diff --git a/packages/xstate-graph/src/index.ts b/packages/xstate-graph/src/index.ts index 78e99e57a7..b379c3cce0 100644 --- a/packages/xstate-graph/src/index.ts +++ b/packages/xstate-graph/src/index.ts @@ -1,20 +1,20 @@ import { getStateNodes, getPathFromEvents, - getSimplePaths, - getShortestPaths, + getSimplePlans, + getShortestPlans, serializeEvent, - serializeState, + serializeMachineState, toDirectedGraph } from './graph'; export { getStateNodes, getPathFromEvents, - getSimplePaths, - getShortestPaths, + getSimplePlans, + getShortestPlans, serializeEvent, - serializeState, + serializeMachineState as serializeState, toDirectedGraph }; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 35e294ff20..ce9aa34fd0 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -57,8 +57,8 @@ export type DirectedGraphNode = JSONSerializable< >; export interface AdjacencyMap { - [stateId: string]: Record< - string, + [stateId: SerializedState]: Record< + SerializedState, { state: TState; event: TEvent; @@ -92,7 +92,7 @@ export interface StatePath { weight: number; } -export interface StatePathsMap { +export interface StatePlanMap { [key: string]: StatePlan; } diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index f5cd0ed887..3d0b4dfc6e 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -28,783 +28,698 @@ Object { } `; -exports[`@xstate/graph getShortestPaths() should represent conditional paths based on context: shortest paths conditional 1`] = ` -Object { - "\\"bar\\" | {\\"id\\":\\"foo\\"} | \\"\\"": Array [ - Object { - "state": "bar", - "steps": Array [ - Object { - "eventType": "EVENT", - "state": "pending", - }, - ], - }, - ], - "\\"foo\\" | {\\"id\\":\\"foo\\"} | \\"\\"": Array [ - Object { - "state": "foo", - "steps": Array [ - Object { - "eventType": "STATE", - "state": "pending", - }, - ], - }, - ], - "\\"pending\\" | {\\"id\\":\\"foo\\"} | \\"\\"": Array [ - Object { - "state": "pending", - "steps": Array [], - }, - ], -} -`; - exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states (parallel): shortest paths parallel 1`] = ` -Object { - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "a": "a1", - "b": "b1", - }, - "steps": Array [], +Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, - ], - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "a": "a2", - "b": "b2", - }, - "steps": Array [ - Object { - "eventType": "2", - "state": Object { - "a": "a1", - "b": "b1", - }, + "steps": Array [], + }, + Object { + "state": Object { + "a": "a2", + "b": "b2", + }, + "steps": Array [ + Object { + "eventType": "2", + "state": Object { + "a": "a1", + "b": "b1", }, - ], - }, - ], - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "a": "a3", - "b": "b3", }, - "steps": Array [ - Object { - "eventType": "3", - "state": Object { - "a": "a1", - "b": "b1", - }, + ], + }, + Object { + "state": Object { + "a": "a3", + "b": "b3", + }, + "steps": Array [ + Object { + "eventType": "3", + "state": Object { + "a": "a1", + "b": "b1", }, - ], - }, - ], -} + }, + ], + }, +] `; exports[`@xstate/graph getShortestPaths() should return a mapping of shortest paths to all states: shortest paths 1`] = ` -Object { - "\\"green\\" | | \\"\\"": Array [ - Object { - "state": "green", - "steps": Array [], - }, - ], - "\\"green\\" | | \\"doNothing\\"": Array [ - Object { - "state": "green", - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - ], - }, - ], - "\\"yellow\\" | | \\"\\"": Array [ - Object { - "state": "yellow", - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - ], - }, - ], - "{\\"red\\":\\"flashing\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "red": "flashing", +Array [ + Object { + "state": "green", + "steps": Array [], + }, + Object { + "state": "yellow", + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", }, - "steps": Array [ - Object { - "eventType": "POWER_OUTAGE", - "state": "green", - }, - ], - }, - ], - "{\\"red\\":\\"stop\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "red": "stop", + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "POWER_OUTAGE", + "state": "green", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "wait", - }, - }, - ], - }, - ], - "{\\"red\\":\\"wait\\"} | | \\"startCountdown\\"": Array [ - Object { - "state": Object { - "red": "wait", + ], + }, + Object { + "state": "green", + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, + ], + }, + Object { + "state": Object { + "red": "walk", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - ], - }, - ], - "{\\"red\\":\\"walk\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "red": "walk", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", + ], + }, + Object { + "state": Object { + "red": "stop", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - Object { - "eventType": "TIMER", - "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", }, - ], - }, - ], -} + }, + ], + }, +] `; exports[`@xstate/graph getSimplePaths() should return a mapping of arrays of simple paths to all states 2`] = ` -Object { - "\\"green\\" | | \\"\\"": Array [ - Object { - "state": "green", - "steps": Array [], - }, - ], - "\\"green\\" | | \\"doNothing\\"": Array [ - Object { - "state": "green", - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - ], - }, - ], - "\\"yellow\\" | | \\"\\"": Array [ - Object { - "state": "yellow", - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", +Array [ + Object { + "state": "green", + "steps": Array [], + }, + Object { + "state": "yellow", + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + ], + }, + Object { + "state": "yellow", + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - ], - }, - Object { - "state": "yellow", - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", }, - Object { - "eventType": "TIMER", - "state": "green", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "stop", }, - ], - }, - ], - "{\\"red\\":\\"flashing\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "red": "flashing", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - Object { - "eventType": "TIMER", - "state": "yellow", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "wait", }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "walk", }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "wait", - }, + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "POWER_OUTAGE", + "state": "green", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "stop", - }, + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", }, - ], - }, - Object { - "state": Object { - "red": "flashing", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "stop", }, - Object { - "eventType": "TIMER", - "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "wait", }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "wait", - }, + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": Object { + "red": "walk", }, - ], - }, - Object { - "state": Object { - "red": "flashing", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "flashing", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "POWER_OUTAGE", + "state": "green", + }, + ], + }, + Object { + "state": "green", + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + ], + }, + Object { + "state": Object { + "red": "walk", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "walk", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - Object { - "eventType": "TIMER", - "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "wait", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "walk", - }, + }, + ], + }, + Object { + "state": Object { + "red": "stop", + }, + "steps": Array [ + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - ], - }, - Object { - "state": Object { - "red": "flashing", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", }, - Object { - "eventType": "POWER_OUTAGE", - "state": "yellow", + }, + ], + }, + Object { + "state": Object { + "red": "stop", + }, + "steps": Array [ + Object { + "eventType": "PUSH_BUTTON", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "green", + }, + Object { + "eventType": "TIMER", + "state": "yellow", + }, + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "walk", }, - ], - }, - Object { - "state": Object { - "red": "flashing", }, - "steps": Array [ - Object { - "eventType": "POWER_OUTAGE", - "state": "green", + Object { + "eventType": "PED_COUNTDOWN", + "state": Object { + "red": "wait", }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "wait", - }, - }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "stop", - }, - }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "wait", - }, - }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "walk", - }, - }, - ], - }, - Object { - "state": Object { - "red": "flashing", }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "POWER_OUTAGE", - "state": "yellow", - }, - ], + ], + }, +] +`; + +exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths to all states (parallel): simple paths parallel 1`] = ` +Array [ + Object { + "state": Object { + "a": "a1", + "b": "b1", }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "POWER_OUTAGE", - "state": "green", + "steps": Array [], + }, + Object { + "state": Object { + "a": "a2", + "b": "b2", + }, + "steps": Array [ + Object { + "eventType": "2", + "state": Object { + "a": "a1", + "b": "b1", }, - ], - }, - ], - "{\\"red\\":\\"stop\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "red": "stop", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "wait", - }, + ], + }, + Object { + "state": Object { + "a": "a3", + "b": "b3", + }, + "steps": Array [ + Object { + "eventType": "2", + "state": Object { + "a": "a1", + "b": "b1", }, - ], - }, - Object { - "state": Object { - "red": "stop", }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, + Object { + "eventType": "3", + "state": Object { + "a": "a2", + "b": "b2", }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "wait", - }, - }, - ], - }, - ], - "{\\"red\\":\\"wait\\"} | | \\"startCountdown\\"": Array [ - Object { - "state": Object { - "red": "wait", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, + ], + }, + Object { + "state": Object { + "a": "a3", + "b": "b3", + }, + "steps": Array [ + Object { + "eventType": "3", + "state": Object { + "a": "a1", + "b": "b1", }, - ], - }, - Object { - "state": Object { - "red": "wait", }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - ], - }, - ], - "{\\"red\\":\\"walk\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "red": "walk", + ], + }, +] +`; + +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", }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - ], - }, - Object { - "state": Object { - "red": "walk", + ], + }, + Object { + "state": "b", + "steps": Array [ + Object { + "eventType": "BAR", + "state": "a", }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - ], - }, - ], -} + ], + }, +] `; -exports[`@xstate/graph getSimplePaths() should return a mapping of simple paths to all states (parallel): simple paths parallel 1`] = ` -Object { - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "a": "a1", - "b": "b1", +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", }, - "steps": Array [], - }, - ], - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "a": "a2", - "b": "b2", + ], + }, + Object { + "state": "start", + "steps": Array [ + Object { + "eventType": "INC", + "state": "start", }, - "steps": Array [ - Object { - "eventType": "2", - "state": Object { - "a": "a1", - "b": "b1", - }, - }, - ], - }, - ], - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"} | | \\"\\"": Array [ - Object { - "state": Object { - "a": "a3", - "b": "b3", + Object { + "eventType": "INC", + "state": "start", }, - "steps": Array [ - Object { - "eventType": "2", - "state": Object { - "a": "a1", - "b": "b1", - }, - }, - Object { - "eventType": "3", - "state": Object { - "a": "a2", - "b": "b2", - }, - }, - ], - }, - Object { - "state": Object { - "a": "a3", - "b": "b3", + ], + }, + Object { + "state": "finish", + "steps": Array [ + Object { + "eventType": "INC", + "state": "start", }, - "steps": Array [ - Object { - "eventType": "3", - "state": Object { - "a": "a1", - "b": "b1", - }, - }, - ], - }, - ], -} -`; - -exports[`@xstate/graph getSimplePaths() should return multiple paths for equivalent transitions: simple paths equal transitions 1`] = ` -Object { - "\\"a\\" | | \\"\\"": Array [ - Object { - "state": "a", - "steps": Array [], - }, - ], - "\\"b\\" | | \\"\\"": Array [ - Object { - "state": "b", - "steps": Array [ - Object { - "eventType": "FOO", - "state": "a", - }, - ], - }, - Object { - "state": "b", - "steps": Array [ - Object { - "eventType": "BAR", - "state": "a", - }, - ], - }, - ], -} -`; - -exports[`@xstate/graph getSimplePaths() should return value-based paths: simple paths context 1`] = ` -Object { - "\\"finish\\" | {\\"count\\":3} | \\"\\"": Array [ - Object { - "state": "finish", - "steps": Array [ - Object { - "eventType": "INC", - "state": "start", - }, - Object { - "eventType": "INC", - "state": "start", - }, - Object { - "eventType": "INC", - "state": "start", - }, - ], - }, - ], - "\\"start\\" | {\\"count\\":0} | \\"\\"": Array [ - Object { - "state": "start", - "steps": Array [], - }, - ], - "\\"start\\" | {\\"count\\":1} | \\"\\"": Array [ - Object { - "state": "start", - "steps": Array [ - Object { - "eventType": "INC", - "state": "start", - }, - ], - }, - ], - "\\"start\\" | {\\"count\\":2} | \\"\\"": Array [ - Object { - "state": "start", - "steps": Array [ - Object { - "eventType": "INC", - "state": "start", - }, - Object { - "eventType": "INC", - "state": "start", - }, - ], - }, - ], -} + Object { + "eventType": "INC", + "state": "start", + }, + Object { + "eventType": "INC", + "state": "start", + }, + ], + }, +] `; exports[`@xstate/graph toDirectedGraph should represent a statechart as a directed graph 1`] = ` @@ -901,387 +816,367 @@ Object { `; exports[`shortest paths for reducers 1`] = ` -Object { - "0 | null": Array [ - Object { - "state": 0, - "steps": Array [], - }, - ], - "0 | {\\"type\\":\\"b\\"}": Array [ - 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, - }, - ], - }, - ], - "0 | {\\"type\\":\\"reset\\"}": Array [ - 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, - }, - ], - }, - ], - "1 | {\\"type\\":\\"a\\"}": 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, - }, - ], - }, - ], - "2 | {\\"type\\":\\"b\\"}": Array [ - 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, - }, - ], - }, - ], -} +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`] = ` -Object { - "0 | null": Array [ - Object { - "state": 0, - "steps": Array [], - }, - ], - "0 | {\\"type\\":\\"b\\"}": Array [ - Object { - "state": 0, - "steps": Array [ - Object { - "eventType": "b", - "state": 0, - }, - ], - }, - ], - "0 | {\\"type\\":\\"reset\\"}": Array [ - Object { - "state": 0, - "steps": Array [ - Object { - "eventType": "reset", - "state": 0, - }, - ], - }, - ], - "1 | {\\"type\\":\\"a\\"}": Array [ - Object { - "state": 1, - "steps": Array [ - Object { - "eventType": "a", - "state": 0, - }, - ], - }, - ], - "2 | {\\"type\\":\\"b\\"}": Array [ - Object { - "state": 2, - "steps": Array [ - Object { - "eventType": "a", - "state": 0, - }, - Object { - "eventType": "b", - "state": 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 688594c262..e68ebcdd28 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -1,31 +1,44 @@ -import { Machine, StateNode, createMachine, State, EventObject } from 'xstate'; +import { + Machine, + StateNode, + createMachine, + State, + EventObject, + StateValue +} from 'xstate'; import { getStateNodes, getPathFromEvents, - getSimplePaths, - getShortestPaths, + getSimplePlans, + getShortestPlans, toDirectedGraph, - StatePathsMap, - StatePath + StatePath, + StatePlan } from '../src/index'; import { getAdjacencyMap, - serializeState, - traverseShortestPaths, - traverseSimplePaths + traverseShortestPlans, + traverseSimplePlans } from '../src/graph'; import { assign } from 'xstate'; -import { mapValues } from 'xstate/lib/utils'; +import { flatten } from 'xstate/lib/utils'; function getPathsMapSnapshot( - pathsMap: StatePathsMap -): Record { - return mapValues(pathsMap, (plan) => { - return plan.paths.map(getPathSnapshot); - }); + plans: Array> +): Array> { + return flatten( + plans.map((plan) => { + return plan.paths.map(getPathSnapshot); + }) + ); } -function getPathSnapshot(path: StatePath) { +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) => ({ @@ -93,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: { @@ -184,49 +197,76 @@ 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) as any; expect(getPathsMapSnapshot(paths)).toMatchSnapshot('shortest paths'); }); it('should return a mapping of shortest paths to all states (parallel)', () => { - const paths = getShortestPaths(parallelMachine) as any; + const paths = getShortestPlans(parallelMachine) as any; expect(getPathsMapSnapshot(paths)).toMatchSnapshot( 'shortest paths parallel' ); }); it('the initial state should have a zero-length path', () => { - const shortestPaths = getShortestPaths(lightMachine); + const shortestPaths = getShortestPlans(lightMachine); expect( - shortestPaths[serializeState(lightMachine.initialState)].paths[0].steps + 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' - }), - { - getEvents: () => - [ - { - type: 'EVENT', - id: 'whatever' - }, - { - type: 'STATE' - } - ] as any[] + }, + 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: {} } - ); + }); + + // explicit type arguments could be removed once davidkpiano/xstate#652 gets resolved + const paths = getShortestPlans(machine, { + getEvents: () => + [ + { + type: 'EVENT', + id: 'whatever' + }, + { + type: 'STATE' + } + ] as any[] + }); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( 'shortest paths conditional' @@ -236,17 +276,25 @@ describe('@xstate/graph', () => { describe('getSimplePaths()', () => { it('should return a mapping of arrays of simple paths to all states', () => { - const paths = getSimplePaths(lightMachine); + const paths = getSimplePlans(lightMachine); - expect(Object.keys(paths)).toMatchInlineSnapshot(` + expect(paths.map((path) => path.state.value)).toMatchInlineSnapshot(` Array [ - "\\"green\\" | | \\"\\"", - "\\"yellow\\" | | \\"\\"", - "{\\"red\\":\\"flashing\\"} | | \\"\\"", - "\\"green\\" | | \\"doNothing\\"", - "{\\"red\\":\\"walk\\"} | | \\"\\"", - "{\\"red\\":\\"wait\\"} | | \\"startCountdown\\"", - "{\\"red\\":\\"stop\\"} | | \\"\\"", + "green", + "yellow", + Object { + "red": "flashing", + }, + "green", + Object { + "red": "walk", + }, + Object { + "red": "wait", + }, + Object { + "red": "stop", + }, ] `); @@ -262,13 +310,22 @@ describe('@xstate/graph', () => { }); it('should return a mapping of simple paths to all states (parallel)', () => { - const paths = getSimplePaths(parallelMachine); + const paths = getSimplePlans(parallelMachine); - expect(Object.keys(paths)).toMatchInlineSnapshot(` + expect(paths.map((p) => p.state.value)).toMatchInlineSnapshot(` Array [ - "{\\"a\\":\\"a1\\",\\"b\\":\\"b1\\"} | | \\"\\"", - "{\\"a\\":\\"a2\\",\\"b\\":\\"b2\\"} | | \\"\\"", - "{\\"a\\":\\"a3\\",\\"b\\":\\"b3\\"} | | \\"\\"", + Object { + "a": "a1", + "b": "b1", + }, + Object { + "a": "a2", + "b": "b2", + }, + Object { + "a": "a3", + "b": "b3", + }, ] `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -277,12 +334,12 @@ describe('@xstate/graph', () => { }); it('should return multiple paths for equivalent transitions', () => { - const paths = getSimplePaths(equivMachine); + const paths = getSimplePlans(equivMachine); - expect(Object.keys(paths)).toMatchInlineSnapshot(` + expect(paths.map((p) => p.state.value)).toMatchInlineSnapshot(` Array [ - "\\"a\\" | | \\"\\"", - "\\"b\\" | | \\"\\"", + "a", + "b", ] `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -292,20 +349,24 @@ describe('@xstate/graph', () => { it('should return a single empty path for the initial state', () => { expect( - getSimplePaths(lightMachine)[serializeState(lightMachine.initialState)] - .paths + getSimplePlans(lightMachine).find((p) => + p.state.matches(lightMachine.initialState.value) + )!.paths ).toHaveLength(1); expect( - getSimplePaths(lightMachine)[serializeState(lightMachine.initialState)] - .paths[0].steps + getSimplePlans(lightMachine).find((p) => + p.state.matches(lightMachine.initialState.value) + )!.paths[0].steps ).toHaveLength(0); expect( - getSimplePaths(equivMachine)[serializeState(equivMachine.initialState)] - .paths + getSimplePlans(equivMachine).find((p) => + p.state.matches(equivMachine.initialState.value) + )!.paths ).toHaveLength(1); expect( - getSimplePaths(equivMachine)[serializeState(equivMachine.initialState)] - .paths[0].steps + getSimplePlans(equivMachine).find((p) => + p.state.matches(equivMachine.initialState.value) + )!.paths[0].steps ).toHaveLength(0); }); @@ -341,16 +402,16 @@ describe('@xstate/graph', () => { } }); - const paths = getSimplePaths(countMachine as any, { + const paths = getSimplePlans(countMachine as any, { getEvents: () => [{ type: 'INC', value: 1 }] }); - expect(Object.keys(paths)).toMatchInlineSnapshot(` + expect(paths.map((p) => p.state.value)).toMatchInlineSnapshot(` Array [ - "\\"start\\" | {\\"count\\":0} | \\"\\"", - "\\"start\\" | {\\"count\\":1} | \\"\\"", - "\\"start\\" | {\\"count\\":2} | \\"\\"", - "\\"finish\\" | {\\"count\\":3} | \\"\\"", + "start", + "start", + "start", + "finish", ] `); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( @@ -464,11 +525,22 @@ describe('@xstate/graph', () => { events: { EVENT: (state) => [ { type: 'EVENT' as const, value: state.context.count + 10 } - ] // TODO: fix as const + ] } }); - 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 } + }) + }) + ); }); }); @@ -501,7 +573,7 @@ describe('@xstate/graph', () => { }); it('simple paths for reducers', () => { - const a = traverseShortestPaths( + const a = traverseShortestPlans( { transition: (s, e) => { if (e.type === 'a') { @@ -528,7 +600,7 @@ it('simple paths for reducers', () => { }); it('shortest paths for reducers', () => { - const a = traverseSimplePaths( + const a = traverseSimplePlans( { transition: (s, e) => { if (e.type === 'a') { @@ -572,18 +644,28 @@ describe('filtering', () => { } }); - const sp = getShortestPaths(machine, { + const sp = getShortestPlans(machine, { getEvents: () => [{ type: 'INC' }], filter: (s) => s.context.count < 5 }); - expect(Object.keys(sp)).toMatchInlineSnapshot(` + expect(sp.map((p) => p.state.context)).toMatchInlineSnapshot(` Array [ - "\\"counting\\" | {\\"count\\":0} | \\"\\"", - "\\"counting\\" | {\\"count\\":1} | \\"\\"", - "\\"counting\\" | {\\"count\\":2} | \\"\\"", - "\\"counting\\" | {\\"count\\":3} | \\"\\"", - "\\"counting\\" | {\\"count\\":4} | \\"\\"", + Object { + "count": 0, + }, + Object { + "count": 1, + }, + Object { + "count": 2, + }, + Object { + "count": 3, + }, + Object { + "count": 4, + }, ] `); }); diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index cdb1541aae..86a17cdb68 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -10,8 +10,8 @@ import { } from '@xstate/graph'; import { performDepthFirstTraversal, - traverseShortestPaths, - traverseSimplePaths, + traverseShortestPlans, + traverseSimplePlans, traverseSimplePathsTo } from '@xstate/graph/src/graph'; import { EventObject, SingleOrArray } from 'xstate'; @@ -97,7 +97,7 @@ export class TestModel { options?: Partial> ): Array> { return this.getPlans((behavior, resolvedOptions) => { - const shortestPaths = traverseShortestPaths(behavior, resolvedOptions); + const shortestPaths = traverseShortestPlans(behavior, resolvedOptions); return Object.values(shortestPaths); }, options); @@ -128,7 +128,7 @@ export class TestModel { options?: Partial> ): Array> { return this.getPlans((behavior, resolvedOptions) => { - const simplePaths = traverseSimplePaths(behavior, resolvedOptions); + const simplePaths = traverseSimplePlans(behavior, resolvedOptions); return Object.values(simplePaths); }, options); diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index b5461f9a81..6ea7da299b 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -326,13 +326,13 @@ describe('error path trace', () => { expect(err.message).toMatchInlineSnapshot(` "test error Path: - State: \\"first\\" | | \\"\\" + State: {\\"value\\":\\"first\\",\\"actions\\":[]} Event: {\\"type\\":\\"NEXT\\"} - State: \\"second\\" | | \\"\\" + State: {\\"value\\":\\"second\\",\\"actions\\":[]} Event: {\\"type\\":\\"NEXT\\"} - State: \\"third\\" | | \\"\\"" + State: {\\"value\\":\\"third\\",\\"actions\\":[]}" `); return; } From c89ebabf07007dd07bd9aba5c48deb073d0c0130 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 25 Apr 2022 16:37:11 -0400 Subject: [PATCH 058/394] Fix test types --- packages/xstate-test/test/index.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 5c25441d21..25fdd176bd 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -96,7 +96,10 @@ describe('events', () => { }); it('should allow for dynamic generation of cases based on state', async () => { - const testMachine = createMachine({ + const testMachine = createMachine< + { values: number[] }, + { type: 'EVENT'; value: number } + >({ initial: 'a', context: { values: [1, 2, 3] // to be read by generator From 205f616f87b2e95c5bb39b311742fceeb3b7b513 Mon Sep 17 00:00:00 2001 From: Alex Sherwin Date: Sun, 1 May 2022 16:57:18 -0400 Subject: [PATCH 059/394] Adding useMachine/useInterpreter options clarity Added info to the `options` argument which clarifies how it interacts with any existing `options` that the machine instance was already created with. --- docs/packages/xstate-react/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/packages/xstate-react/index.md b/docs/packages/xstate-react/index.md index 58c429a563..069f9f5689 100644 --- a/docs/packages/xstate-react/index.md +++ b/docs/packages/xstate-react/index.md @@ -86,7 +86,7 @@ A [React hook](https://reactjs.org/hooks) that interprets the given `machine` an ); ``` -- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. +- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. If the machine already contains any of these options, they will be merged, with these options taking precedence. **Returns** a tuple of `[state, send, service]`: @@ -127,7 +127,7 @@ _Since 1.3.0_ **Arguments** - `machine` - An [XState machine](https://xstate.js.org/docs/guides/machines.html) or a function that lazily returns a machine. -- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. +- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. If the machine already contains any of these options, they will be merged, with these options taking precedence. - `observer` (optional) - an observer or listener that listens to state updates: - an observer (e.g., `{ next: (state) => {/* ... */} }`) - or a listener (e.g., `(state) => {/* ... */}`) From d2e54e736aa658177f0bdff2821f45db83a96c84 Mon Sep 17 00:00:00 2001 From: Serkan Sipahi Date: Mon, 2 May 2022 13:50:08 +0200 Subject: [PATCH 060/394] removed asEffect and asLayoutEffect from docs because it no longer exists --- docs/fr/packages/xstate-react/index.md | 46 -------------------------- docs/packages/xstate-react/index.md | 46 -------------------------- docs/zh/packages/xstate-react/index.md | 46 -------------------------- 3 files changed, 138 deletions(-) diff --git a/docs/fr/packages/xstate-react/index.md b/docs/fr/packages/xstate-react/index.md index 58c429a563..015e7330a1 100644 --- a/docs/fr/packages/xstate-react/index.md +++ b/docs/fr/packages/xstate-react/index.md @@ -224,52 +224,6 @@ const App = ({ service }) => { }; ``` -### `asEffect(action)` - -Ensures that the `action` is executed as an effect in `useEffect`, rather than being immediately executed. - -**Arguments** - -- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) - -**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useEffect`. - -**Example** - -```jsx -const machine = createMachine({ - initial: 'focused', - states: { - focused: { - entry: 'focus' - } - } -}); - -const Input = () => { - const inputRef = useRef(null); - const [state, send] = useMachine(machine, { - actions: { - focus: asEffect((context, event) => { - inputRef.current && inputRef.current.focus(); - }) - } - }); - - return ; -}; -``` - -### `asLayoutEffect(action)` - -Ensures that the `action` is executed as an effect in `useLayoutEffect`, rather than being immediately executed. - -**Arguments** - -- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) - -**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useLayoutEffect`. - ### `useMachine(machine)` with `@xstate/fsm` A [React hook](https://reactjs.org/hooks) that interprets the given finite state `machine` from [`@xstate/fsm`] and starts a service that runs for the lifetime of the component. diff --git a/docs/packages/xstate-react/index.md b/docs/packages/xstate-react/index.md index 58c429a563..015e7330a1 100644 --- a/docs/packages/xstate-react/index.md +++ b/docs/packages/xstate-react/index.md @@ -224,52 +224,6 @@ const App = ({ service }) => { }; ``` -### `asEffect(action)` - -Ensures that the `action` is executed as an effect in `useEffect`, rather than being immediately executed. - -**Arguments** - -- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) - -**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useEffect`. - -**Example** - -```jsx -const machine = createMachine({ - initial: 'focused', - states: { - focused: { - entry: 'focus' - } - } -}); - -const Input = () => { - const inputRef = useRef(null); - const [state, send] = useMachine(machine, { - actions: { - focus: asEffect((context, event) => { - inputRef.current && inputRef.current.focus(); - }) - } - }); - - return ; -}; -``` - -### `asLayoutEffect(action)` - -Ensures that the `action` is executed as an effect in `useLayoutEffect`, rather than being immediately executed. - -**Arguments** - -- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) - -**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useLayoutEffect`. - ### `useMachine(machine)` with `@xstate/fsm` A [React hook](https://reactjs.org/hooks) that interprets the given finite state `machine` from [`@xstate/fsm`] and starts a service that runs for the lifetime of the component. diff --git a/docs/zh/packages/xstate-react/index.md b/docs/zh/packages/xstate-react/index.md index 58c429a563..015e7330a1 100644 --- a/docs/zh/packages/xstate-react/index.md +++ b/docs/zh/packages/xstate-react/index.md @@ -224,52 +224,6 @@ const App = ({ service }) => { }; ``` -### `asEffect(action)` - -Ensures that the `action` is executed as an effect in `useEffect`, rather than being immediately executed. - -**Arguments** - -- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) - -**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useEffect`. - -**Example** - -```jsx -const machine = createMachine({ - initial: 'focused', - states: { - focused: { - entry: 'focus' - } - } -}); - -const Input = () => { - const inputRef = useRef(null); - const [state, send] = useMachine(machine, { - actions: { - focus: asEffect((context, event) => { - inputRef.current && inputRef.current.focus(); - }) - } - }); - - return ; -}; -``` - -### `asLayoutEffect(action)` - -Ensures that the `action` is executed as an effect in `useLayoutEffect`, rather than being immediately executed. - -**Arguments** - -- `action` - An action function (e.g., `(context, event) => { alert(context.message) })`) - -**Returns** a special action function that wraps the original so that `useMachine` knows to execute it in `useLayoutEffect`. - ### `useMachine(machine)` with `@xstate/fsm` A [React hook](https://reactjs.org/hooks) that interprets the given finite state `machine` from [`@xstate/fsm`] and starts a service that runs for the lifetime of the component. From 80f3d2bfb510c08868b3445bdea3dc3d8edd77bb Mon Sep 17 00:00:00 2001 From: Alex Sherwin Date: Mon, 2 May 2022 12:41:50 -0400 Subject: [PATCH 061/394] Update docs/packages/xstate-react/index.md Co-authored-by: David Khourshid --- docs/packages/xstate-react/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/packages/xstate-react/index.md b/docs/packages/xstate-react/index.md index 069f9f5689..e2fa016a22 100644 --- a/docs/packages/xstate-react/index.md +++ b/docs/packages/xstate-react/index.md @@ -127,7 +127,7 @@ _Since 1.3.0_ **Arguments** - `machine` - An [XState machine](https://xstate.js.org/docs/guides/machines.html) or a function that lazily returns a machine. -- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. If the machine already contains any of these options, they will be merged, with these options taking precedence. +- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. If the machine already contains any of these options, they will be merged, with these options taking precedence. - `observer` (optional) - an observer or listener that listens to state updates: - an observer (e.g., `{ next: (state) => {/* ... */} }`) - or a listener (e.g., `(state) => {/* ... */}`) From 0f57680081d1aa53358c1ef3f265a64fb069a176 Mon Sep 17 00:00:00 2001 From: Alex Sherwin Date: Mon, 2 May 2022 12:41:59 -0400 Subject: [PATCH 062/394] Update docs/packages/xstate-react/index.md Co-authored-by: David Khourshid --- docs/packages/xstate-react/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/packages/xstate-react/index.md b/docs/packages/xstate-react/index.md index e2fa016a22..e789c869e4 100644 --- a/docs/packages/xstate-react/index.md +++ b/docs/packages/xstate-react/index.md @@ -86,7 +86,7 @@ A [React hook](https://reactjs.org/hooks) that interprets the given `machine` an ); ``` -- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. If the machine already contains any of these options, they will be merged, with these options taking precedence. +- `options` (optional) - [Interpreter options](https://xstate.js.org/docs/guides/interpretation.html#options) and/or any of the following machine config options: `guards`, `actions`, `services`, `delays`, `immediate`, `context`, `state`. If the machine already contains any of these options, they will be merged, with these options taking precedence. **Returns** a tuple of `[state, send, service]`: From f99bccd06c63f6c6a9f197148cfa468b2cb7a078 Mon Sep 17 00:00:00 2001 From: Bruno Crosier Date: Wed, 4 May 2022 18:02:23 +0100 Subject: [PATCH 063/394] Add `context` object to typegen examples --- docs/guides/typescript.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/guides/typescript.md b/docs/guides/typescript.md index 08841c9616..bc59f37b8e 100644 --- a/docs/guides/typescript.md +++ b/docs/guides/typescript.md @@ -49,6 +49,9 @@ const machine = createMachine({ events: {} as { type: 'FOO'; value: string } | { type: 'BAR' } }, initial: 'a', + context: { + value: '', + }, states: { a: { on: { @@ -74,6 +77,9 @@ const machine = createMachine({ context: {} as { value: string }, events: {} as { type: 'FOO'; value: string } | { type: 'BAR' } }, + context: { + value: '', + }, initial: 'a', states: { /* ... */ From 1bfef36a9056f7a2a0f3696d07d294fc7ec166b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 May 2022 17:06:50 +0000 Subject: [PATCH 064/394] Version Packages --- .changeset/cool-ducks-pretend.md | 20 -------------- .changeset/fast-ways-run.md | 5 ---- .changeset/few-pianos-begin.md | 5 ---- .changeset/late-poems-move.md | 5 ---- .changeset/lucky-tables-add.md | 7 ----- .changeset/many-onions-listen.md | 5 ---- .changeset/poor-ducks-join.md | 11 -------- .changeset/rich-dots-laugh.md | 5 ---- .changeset/serious-melons-fold.md | 5 ---- .changeset/stupid-plums-smile.md | 5 ---- .changeset/tasty-chicken-itch.md | 5 ---- .changeset/thin-tools-repair.md | 5 ---- packages/core/CHANGELOG.md | 37 ++++++++++++++++++++++++++ packages/core/package.json | 2 +- packages/xstate-analytics/package.json | 2 +- packages/xstate-graph/package.json | 2 +- packages/xstate-immer/package.json | 2 +- packages/xstate-inspect/CHANGELOG.md | 12 +++++++++ packages/xstate-inspect/package.json | 4 +-- packages/xstate-react/package.json | 2 +- packages/xstate-scxml/package.json | 2 +- packages/xstate-svelte/CHANGELOG.md | 14 ++++++++++ packages/xstate-svelte/package.json | 4 +-- packages/xstate-test/package.json | 2 +- packages/xstate-vue/package.json | 2 +- 25 files changed, 75 insertions(+), 95 deletions(-) delete mode 100644 .changeset/cool-ducks-pretend.md delete mode 100644 .changeset/fast-ways-run.md delete mode 100644 .changeset/few-pianos-begin.md delete mode 100644 .changeset/late-poems-move.md delete mode 100644 .changeset/lucky-tables-add.md delete mode 100644 .changeset/many-onions-listen.md delete mode 100644 .changeset/poor-ducks-join.md delete mode 100644 .changeset/rich-dots-laugh.md delete mode 100644 .changeset/serious-melons-fold.md delete mode 100644 .changeset/stupid-plums-smile.md delete mode 100644 .changeset/tasty-chicken-itch.md delete mode 100644 .changeset/thin-tools-repair.md diff --git a/.changeset/cool-ducks-pretend.md b/.changeset/cool-ducks-pretend.md deleted file mode 100644 index b6afdcbfdb..0000000000 --- a/.changeset/cool-ducks-pretend.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'xstate': patch ---- - -Removing the timeout that's built in to `waitFor` is now supported by explicitly passing an `Infinity` value. - -Example usage: - -```js -import { waitFor } from 'xstate/lib/waitFor'; - -// This will -const loggedInState = await waitFor( - loginService, - (state) => state.hasTag('loggedIn'), - { timeout: Infinity } -); -``` - -This fixes a bug that causes `waitFor` to reject with an error immediately due to the behaviour of `setTimeout`. diff --git a/.changeset/fast-ways-run.md b/.changeset/fast-ways-run.md deleted file mode 100644 index f02c404b5b..0000000000 --- a/.changeset/fast-ways-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': patch ---- - -Fixed an issue with typegen types not being able to provide events that had a union of strings as their `type` (such as `{ type: 'INC' | 'DEC'; value: number; }`). diff --git a/.changeset/few-pianos-begin.md b/.changeset/few-pianos-begin.md deleted file mode 100644 index a42de9ecee..0000000000 --- a/.changeset/few-pianos-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': patch ---- - -Fixed an issue with `EventFrom` not being able to extract events that had a union of strings as their `type` (such as `{ type: 'INC' | 'DEC'; value: number; }`). diff --git a/.changeset/late-poems-move.md b/.changeset/late-poems-move.md deleted file mode 100644 index 9481c520c9..0000000000 --- a/.changeset/late-poems-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': patch ---- - -Fixed an issue with action objects not receiving correct event types when used in the second argument to the `createMachine`. diff --git a/.changeset/lucky-tables-add.md b/.changeset/lucky-tables-add.md deleted file mode 100644 index 16ecd10b5b..0000000000 --- a/.changeset/lucky-tables-add.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@xstate/svelte': major ---- - -The major version of this package had to be bumped to allow integrating with the typegen. This package will now require TS version 4.0 or greater. - -When using hooks from `@xstate/svelte` it's recommended to skip providing explicit generics to them. Note that that generics list has changed since v1 and we now only accept a single generic, `TMachine`. diff --git a/.changeset/many-onions-listen.md b/.changeset/many-onions-listen.md deleted file mode 100644 index ce25ef9959..0000000000 --- a/.changeset/many-onions-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@xstate/svelte': patch ---- - -Fixed an issue with the internal interpreter created by `useMachine` being unsubscribed when its subscribers' count went to zero. The lifetime of this interpreter should be bound to the lifetime of the component that has created it. diff --git a/.changeset/poor-ducks-join.md b/.changeset/poor-ducks-join.md deleted file mode 100644 index 2286bfa5c4..0000000000 --- a/.changeset/poor-ducks-join.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@xstate/inspect": minor ---- - -`@xstate/inspect` will now target `https://stately.ai/viz` by default. You can target the old inspector by setting the config options like so: - -```ts -inspect({ - url: `https://statecharts.io/inspect`, -}); -``` diff --git a/.changeset/rich-dots-laugh.md b/.changeset/rich-dots-laugh.md deleted file mode 100644 index 20408436a4..0000000000 --- a/.changeset/rich-dots-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@xstate/svelte': patch ---- - -Added ESM build to fix some bundling issues, more information can be found [here](https://github.com/statelyai/xstate/issues/2642) diff --git a/.changeset/serious-melons-fold.md b/.changeset/serious-melons-fold.md deleted file mode 100644 index 88e1127d9f..0000000000 --- a/.changeset/serious-melons-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': patch ---- - -The typings for `sendTo(...)` have been fixed. diff --git a/.changeset/stupid-plums-smile.md b/.changeset/stupid-plums-smile.md deleted file mode 100644 index 6f302c8d3d..0000000000 --- a/.changeset/stupid-plums-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': minor ---- - -Added a `StateValueFrom` helper that can be used to extract valid state values from a machine. This might specifically be useful with typegen because typegenless `state.matches` accepts `any` anyway. diff --git a/.changeset/tasty-chicken-itch.md b/.changeset/tasty-chicken-itch.md deleted file mode 100644 index 91c5aea351..0000000000 --- a/.changeset/tasty-chicken-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': patch ---- - -Fixed an issue with inline functions in the config object used as transition actions not having their argument types inferred. diff --git a/.changeset/thin-tools-repair.md b/.changeset/thin-tools-repair.md deleted file mode 100644 index acfe9f9671..0000000000 --- a/.changeset/thin-tools-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': patch ---- - -Fixed an issue with default `TEvent` (`{ type: string }`) not being correctly provided to inline transition actions. diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 666c45dd6d..13d047ff5a 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,42 @@ # xstate +## 4.32.0 + +### Minor Changes + +- [#3234](https://github.com/statelyai/xstate/pull/3234) [`ce376b388`](https://github.com/statelyai/xstate/commit/ce376b3889ea900e67d20026517b87185377c32e) Thanks [@Andarist](https://github.com/Andarist)! - Added a `StateValueFrom` helper that can be used to extract valid state values from a machine. This might specifically be useful with typegen because typegenless `state.matches` accepts `any` anyway. + +### Patch Changes + +- [#3215](https://github.com/statelyai/xstate/pull/3215) [`44c66e74f`](https://github.com/statelyai/xstate/commit/44c66e74f9eafbb326979234e2bbe51e38dc3a86) Thanks [@tom-sherman](https://github.com/tom-sherman)! - Removing the timeout that's built in to `waitFor` is now supported by explicitly passing an `Infinity` value. + + Example usage: + + ```js + import { waitFor } from 'xstate/lib/waitFor'; + + // This will + const loggedInState = await waitFor( + loginService, + state => state.hasTag('loggedIn'), + { timeout: Infinity } + ); + ``` + + This fixes a bug that causes `waitFor` to reject with an error immediately due to the behaviour of `setTimeout`. + +* [#3230](https://github.com/statelyai/xstate/pull/3230) [`780458c92`](https://github.com/statelyai/xstate/commit/780458c921d4525c7a00119c7eb43d4833978861) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with typegen types not being able to provide events that had a union of strings as their `type` (such as `{ type: 'INC' | 'DEC'; value: number; }`). + +- [#3252](https://github.com/statelyai/xstate/pull/3252) [`a94dfd467`](https://github.com/statelyai/xstate/commit/a94dfd46772cacc59154c165f27122164f48625b) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with `EventFrom` not being able to extract events that had a union of strings as their `type` (such as `{ type: 'INC' | 'DEC'; value: number; }`). + +* [#3090](https://github.com/statelyai/xstate/pull/3090) [`c4f73ca13`](https://github.com/statelyai/xstate/commit/c4f73ca1356d106423c8b4ee34865f7e4f2d2bb6) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with action objects not receiving correct event types when used in the second argument to the `createMachine`. + +- [#3238](https://github.com/statelyai/xstate/pull/3238) [`3df6335ef`](https://github.com/statelyai/xstate/commit/3df6335ef8db4edcf0a47d4c559716552ce4bbe8) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The typings for `sendTo(...)` have been fixed. + +* [#3228](https://github.com/statelyai/xstate/pull/3228) [`fe5f0e6c9`](https://github.com/statelyai/xstate/commit/fe5f0e6c9bbb6ff740673889892301c8989eacfd) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with inline functions in the config object used as transition actions not having their argument types inferred. + +- [#3252](https://github.com/statelyai/xstate/pull/3252) [`a94dfd467`](https://github.com/statelyai/xstate/commit/a94dfd46772cacc59154c165f27122164f48625b) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with default `TEvent` (`{ type: string }`) not being correctly provided to inline transition actions. + ## 4.31.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 878d763ec6..593b7ec590 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "xstate", - "version": "4.31.0", + "version": "4.32.0", "description": "Finite State Machines and Statecharts for the Modern Web.", "main": "lib/index.js", "module": "es/index.js", diff --git a/packages/xstate-analytics/package.json b/packages/xstate-analytics/package.json index 70e7e04f68..a3fd7704a3 100644 --- a/packages/xstate-analytics/package.json +++ b/packages/xstate-analytics/package.json @@ -35,7 +35,7 @@ "url": "https://github.com/statelyai/xstate/issues" }, "peerDependencies": { - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "devDependencies": { "jest": "^26.6.3", diff --git a/packages/xstate-graph/package.json b/packages/xstate-graph/package.json index bcc41c798d..29287b0c97 100644 --- a/packages/xstate-graph/package.json +++ b/packages/xstate-graph/package.json @@ -37,7 +37,7 @@ "url": "https://github.com/statelyai/xstate/issues" }, "peerDependencies": { - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "devDependencies": { "jest": "^26.6.3", diff --git a/packages/xstate-immer/package.json b/packages/xstate-immer/package.json index 460618d85a..d512ace2f1 100644 --- a/packages/xstate-immer/package.json +++ b/packages/xstate-immer/package.json @@ -38,7 +38,7 @@ "dependencies": {}, "peerDependencies": { "immer": "^9.0.6", - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "devDependencies": { "immer": "^9.0.6", diff --git a/packages/xstate-inspect/CHANGELOG.md b/packages/xstate-inspect/CHANGELOG.md index f5c9e3c507..ed98c94cba 100644 --- a/packages/xstate-inspect/CHANGELOG.md +++ b/packages/xstate-inspect/CHANGELOG.md @@ -1,5 +1,17 @@ # @xstate/inspect +## 0.7.0 + +### Minor Changes + +- [#3235](https://github.com/statelyai/xstate/pull/3235) [`f666f5823`](https://github.com/statelyai/xstate/commit/f666f5823835bd731034e7e2d64e5916b1f7dc0c) Thanks [@mattpocock](https://github.com/mattpocock)! - `@xstate/inspect` will now target `https://stately.ai/viz` by default. You can target the old inspector by setting the config options like so: + + ```ts + inspect({ + url: `https://statecharts.io/inspect` + }); + ``` + ## 0.6.5 ### Patch Changes diff --git a/packages/xstate-inspect/package.json b/packages/xstate-inspect/package.json index 6a278f78fb..ac773b62c5 100644 --- a/packages/xstate-inspect/package.json +++ b/packages/xstate-inspect/package.json @@ -1,6 +1,6 @@ { "name": "@xstate/inspect", - "version": "0.6.5", + "version": "0.7.0", "description": "XState inspection utilities", "keywords": [ "state", @@ -53,7 +53,7 @@ "peerDependencies": { "@types/ws": "^8.0.0", "ws": "^8.0.0", - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "peerDependenciesMeta": { "@types/ws": { diff --git a/packages/xstate-react/package.json b/packages/xstate-react/package.json index 12e016c278..be8d6c5766 100644 --- a/packages/xstate-react/package.json +++ b/packages/xstate-react/package.json @@ -54,7 +54,7 @@ "peerDependencies": { "@xstate/fsm": "^2.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "peerDependenciesMeta": { "@xstate/fsm": { diff --git a/packages/xstate-scxml/package.json b/packages/xstate-scxml/package.json index 6152082f4f..202ba668d5 100644 --- a/packages/xstate-scxml/package.json +++ b/packages/xstate-scxml/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "xml-js": "^1.6.11", - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "devDependencies": { "@scion-scxml/test-framework": "^2.0.15", diff --git a/packages/xstate-svelte/CHANGELOG.md b/packages/xstate-svelte/CHANGELOG.md index fc62163a5b..d963b415ab 100644 --- a/packages/xstate-svelte/CHANGELOG.md +++ b/packages/xstate-svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # @xstate/svelte +## 2.0.0 + +### Major Changes + +- [#3254](https://github.com/statelyai/xstate/pull/3254) [`c0b787d2a`](https://github.com/statelyai/xstate/commit/c0b787d2a2334665af0ecb4ebc1ce5653fa3725c) Thanks [@Andarist](https://github.com/Andarist)! - The major version of this package had to be bumped to allow integrating with the typegen. This package will now require TS version 4.0 or greater. + + When using hooks from `@xstate/svelte` it's recommended to skip providing explicit generics to them. Note that that generics list has changed since v1 and we now only accept a single generic, `TMachine`. + +### Patch Changes + +- [#3172](https://github.com/statelyai/xstate/pull/3172) [`390a115cd`](https://github.com/statelyai/xstate/commit/390a115cd1912c017654ecd367ef9a869f7e8fe8) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with the internal interpreter created by `useMachine` being unsubscribed when its subscribers' count went to zero. The lifetime of this interpreter should be bound to the lifetime of the component that has created it. + +* [#3209](https://github.com/statelyai/xstate/pull/3209) [`8520e203b`](https://github.com/statelyai/xstate/commit/8520e203b60124fb66da2cedad27d0c9771dc62c) Thanks [@schibrikov](https://github.com/schibrikov)! - Added ESM build to fix some bundling issues, more information can be found [here](https://github.com/statelyai/xstate/issues/2642) + ## 1.0.0 ### Patch Changes diff --git a/packages/xstate-svelte/package.json b/packages/xstate-svelte/package.json index 039cb3afe1..8048c27911 100644 --- a/packages/xstate-svelte/package.json +++ b/packages/xstate-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@xstate/svelte", - "version": "1.0.0", + "version": "2.0.0", "description": "XState tools for Svelte", "keywords": [ "state", @@ -47,7 +47,7 @@ "peerDependencies": { "@xstate/fsm": "^2.0.0", "svelte": "^3.24.1", - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "peerDependenciesMeta": { "@xstate/fsm": { diff --git a/packages/xstate-test/package.json b/packages/xstate-test/package.json index 51431b6a9b..b4912e5fbc 100644 --- a/packages/xstate-test/package.json +++ b/packages/xstate-test/package.json @@ -44,7 +44,7 @@ "url": "https://github.com/statelyai/xstate/issues" }, "peerDependencies": { - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "devDependencies": { "jest": "^26.6.3", diff --git a/packages/xstate-vue/package.json b/packages/xstate-vue/package.json index ae12403bbc..765a74b177 100644 --- a/packages/xstate-vue/package.json +++ b/packages/xstate-vue/package.json @@ -46,7 +46,7 @@ "peerDependencies": { "@xstate/fsm": "^2.0.0", "vue": "^3.0.0", - "xstate": "^4.31.0" + "xstate": "^4.32.0" }, "peerDependenciesMeta": { "@xstate/fsm": { From e7dd8f444f7080fd64856500f03754d680a9091a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 5 May 2022 12:51:31 -0700 Subject: [PATCH 065/394] Fix types --- packages/xstate-graph/src/index.ts | 18 ++++++------------ packages/xstate-test/src/TestModel.ts | 6 ++---- packages/xstate-test/src/types.ts | 21 --------------------- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/packages/xstate-graph/src/index.ts b/packages/xstate-graph/src/index.ts index b379c3cce0..95e0060c22 100644 --- a/packages/xstate-graph/src/index.ts +++ b/packages/xstate-graph/src/index.ts @@ -1,13 +1,3 @@ -import { - getStateNodes, - getPathFromEvents, - getSimplePlans, - getShortestPlans, - serializeEvent, - serializeMachineState, - toDirectedGraph -} from './graph'; - export { getStateNodes, getPathFromEvents, @@ -15,7 +5,11 @@ export { getShortestPlans, serializeEvent, serializeMachineState as serializeState, - toDirectedGraph -}; + toDirectedGraph, + performDepthFirstTraversal, + traverseShortestPlans, + traverseSimplePlans, + traverseSimplePathsTo +} from './graph'; export * from './types'; diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 86a17cdb68..cd822667ae 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -6,14 +6,12 @@ import { StatePath, StatePlan, Step, - TraversalOptions -} from '@xstate/graph'; -import { + TraversalOptions, performDepthFirstTraversal, traverseShortestPlans, traverseSimplePlans, traverseSimplePathsTo -} from '@xstate/graph/src/graph'; +} from '@xstate/graph'; import { EventObject, SingleOrArray } from 'xstate'; import { flatten } from './utils'; import { CoverageFunction } from './coverage'; diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 847f15c9a1..508c6bd32a 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -8,10 +8,8 @@ import { AnyState, EventObject, ExtractEvent, - MachineConfig, State, StateNode, - StateNodeConfig, TransitionConfig } from 'xstate'; export interface TestMeta { @@ -240,25 +238,6 @@ export type TestTransitionsConfigMap< '*'?: TestTransitionConfig | string; }; -export interface TestStateNodeConfig< - TContext, - TEvent extends EventObject, - TTestContext -> extends StateNodeConfig { - test?: (state: State, testContext: TTestContext) => void; - on?: TestTransitionsConfigMap; -} - -export interface TestMachineConfig< - TContext, - TEvent extends EventObject, - TTestContext -> extends MachineConfig { - states?: { - [key: string]: TestStateNodeConfig; - }; -} - export type PlanGenerator = ( behavior: SimpleBehavior, options: TraversalOptions From 6898ee62ae95526fd51a235d1006786c6bc97180 Mon Sep 17 00:00:00 2001 From: Justin Philpott <8027086+justinphilpott@users.noreply.github.com> Date: Wed, 11 May 2022 10:25:16 +0100 Subject: [PATCH 066/394] Small grammar tweak in docs --- docs/guides/transitions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/transitions.md b/docs/guides/transitions.md index 626877a5a4..459df916ca 100644 --- a/docs/guides/transitions.md +++ b/docs/guides/transitions.md @@ -168,7 +168,7 @@ Other event descriptors include: A self-transition is when a state transitions to itself, in which it _may_ exit and then reenter itself. Self-transitions can either be an **internal** or **external** transition: -- An **internal transition** will not exit nor re-enter itself, but may enter different child states. +- An **internal transition** will neither exit nor re-enter itself, but may enter different child states. - An **external transition** will exit and re-enter itself, and may also exit/enter child states. By default, all transitions with a specified target are external. From 3822d114969b1e7cdfbbc3701a255a6f08ff963e Mon Sep 17 00:00:00 2001 From: Justin Philpott <8027086+justinphilpott@users.noreply.github.com> Date: Wed, 11 May 2022 10:32:01 +0100 Subject: [PATCH 067/394] Language logic tweak For logical clarity --- docs/guides/actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/actions.md b/docs/guides/actions.md index ab1385487e..ddb723cfae 100644 --- a/docs/guides/actions.md +++ b/docs/guides/actions.md @@ -679,7 +679,7 @@ const machine = createMachine({ A [self-transition](./transitions.md#self-transitions) is when a state transitions to itself, in which it _may_ exit and then reenter itself. Self-transitions can either be an **internal** or **external** transition: -- An internal transition will _not_ exit and reenter itself, so the state node's `entry` and `exit` actions will not be executed again. +- An internal transition will _neither_ exit nor reenter itself, so the state node's `entry` and `exit` actions will not be executed again. - Internal transitions are indicated with `{ internal: true }`, or by leaving the `target` as `undefined`. - Actions defined on the transition's `actions` property will be executed. - An external transition _will_ exit and reenter itself, so the state node's `entry` and `exit` actions will be executed again. From 16514e4663deba95731a84deaee94c17edec1e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 13 May 2022 14:36:59 +0200 Subject: [PATCH 068/394] Fixed an issue in the `EmittedFrom` type helper that could prevent it from inferring the desired type from some service (#3292) * Fixed an issue in the `EmittedFrom` type helper that could prevent it from inferring the desired type from some service * Special case Interpreter in the EmittedFrom --- .changeset/popular-wolves-tap.md | 5 ++++ packages/core/src/types.ts | 4 ++- packages/core/test/typeHelpers.test.ts | 36 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .changeset/popular-wolves-tap.md diff --git a/.changeset/popular-wolves-tap.md b/.changeset/popular-wolves-tap.md new file mode 100644 index 0000000000..d5835210d5 --- /dev/null +++ b/.changeset/popular-wolves-tap.md @@ -0,0 +1,5 @@ +--- +'xstate': patch +--- + +Fixed an issue in the `EmittedFrom` type helper that could prevent it from inferring the desired type from some services. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index db75cc0395..51986d862d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1799,7 +1799,9 @@ export interface Behavior { } export type EmittedFrom = ReturnTypeOrValue extends infer R - ? R extends ActorRef + ? R extends Interpreter + ? R['initialState'] + : R extends ActorRef ? TEmitted : R extends Behavior ? TEmitted diff --git a/packages/core/test/typeHelpers.test.ts b/packages/core/test/typeHelpers.test.ts index 9ac8b9685b..4b2ee280b6 100644 --- a/packages/core/test/typeHelpers.test.ts +++ b/packages/core/test/typeHelpers.test.ts @@ -2,6 +2,7 @@ import { assign, ContextFrom, createMachine, + EmittedFrom, EventFrom, interpret, MachineOptionsFrom, @@ -368,3 +369,38 @@ describe('StateValueFrom', () => { matches('just anything'); }); }); + +describe('EmittedFrom', () => { + it('should return state type from a service that has concrete event type', () => { + const service = interpret( + createMachine({ + schema: { + events: {} as { type: 'FOO' } + } + }) + ); + + function acceptState(_state: EmittedFrom) {} + + acceptState(service.initialState); + // @ts-expect-error + acceptState("isn't any"); + }); + + it('should return state from a service created based on a model without any concrete events', () => { + const service = interpret( + createModel( + {}, + { + // this empty obj is important for this test case + } + ).createMachine({}) + ); + + function acceptState(_state: EmittedFrom) {} + + acceptState(service.initialState); + // @ts-expect-error + acceptState("isn't any"); + }); +}); From 1d1c90c4be57e606975564114b7f23d951ad5ccb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 May 2022 15:03:06 +0200 Subject: [PATCH 069/394] Version Packages (#3293) Co-authored-by: github-actions[bot] --- .changeset/popular-wolves-tap.md | 5 ----- packages/core/CHANGELOG.md | 6 ++++++ packages/core/package.json | 2 +- packages/xstate-analytics/package.json | 2 +- packages/xstate-graph/package.json | 2 +- packages/xstate-immer/package.json | 2 +- packages/xstate-inspect/package.json | 2 +- packages/xstate-react/package.json | 2 +- packages/xstate-scxml/package.json | 2 +- packages/xstate-svelte/package.json | 2 +- packages/xstate-test/package.json | 2 +- packages/xstate-vue/package.json | 2 +- 12 files changed, 16 insertions(+), 15 deletions(-) delete mode 100644 .changeset/popular-wolves-tap.md diff --git a/.changeset/popular-wolves-tap.md b/.changeset/popular-wolves-tap.md deleted file mode 100644 index d5835210d5..0000000000 --- a/.changeset/popular-wolves-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'xstate': patch ---- - -Fixed an issue in the `EmittedFrom` type helper that could prevent it from inferring the desired type from some services. diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 13d047ff5a..b9912f9d64 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # xstate +## 4.32.1 + +### Patch Changes + +- [#3292](https://github.com/statelyai/xstate/pull/3292) [`16514e466`](https://github.com/statelyai/xstate/commit/16514e4663deba95731a84deaee94c17edec1e06) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue in the `EmittedFrom` type helper that could prevent it from inferring the desired type from some services. + ## 4.32.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 593b7ec590..e40801b920 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "xstate", - "version": "4.32.0", + "version": "4.32.1", "description": "Finite State Machines and Statecharts for the Modern Web.", "main": "lib/index.js", "module": "es/index.js", diff --git a/packages/xstate-analytics/package.json b/packages/xstate-analytics/package.json index a3fd7704a3..aea8b0e1f5 100644 --- a/packages/xstate-analytics/package.json +++ b/packages/xstate-analytics/package.json @@ -35,7 +35,7 @@ "url": "https://github.com/statelyai/xstate/issues" }, "peerDependencies": { - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "devDependencies": { "jest": "^26.6.3", diff --git a/packages/xstate-graph/package.json b/packages/xstate-graph/package.json index 29287b0c97..69d31cb6cb 100644 --- a/packages/xstate-graph/package.json +++ b/packages/xstate-graph/package.json @@ -37,7 +37,7 @@ "url": "https://github.com/statelyai/xstate/issues" }, "peerDependencies": { - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "devDependencies": { "jest": "^26.6.3", diff --git a/packages/xstate-immer/package.json b/packages/xstate-immer/package.json index d512ace2f1..31d894adcb 100644 --- a/packages/xstate-immer/package.json +++ b/packages/xstate-immer/package.json @@ -38,7 +38,7 @@ "dependencies": {}, "peerDependencies": { "immer": "^9.0.6", - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "devDependencies": { "immer": "^9.0.6", diff --git a/packages/xstate-inspect/package.json b/packages/xstate-inspect/package.json index ac773b62c5..8cb0769554 100644 --- a/packages/xstate-inspect/package.json +++ b/packages/xstate-inspect/package.json @@ -53,7 +53,7 @@ "peerDependencies": { "@types/ws": "^8.0.0", "ws": "^8.0.0", - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "peerDependenciesMeta": { "@types/ws": { diff --git a/packages/xstate-react/package.json b/packages/xstate-react/package.json index be8d6c5766..355362f8ba 100644 --- a/packages/xstate-react/package.json +++ b/packages/xstate-react/package.json @@ -54,7 +54,7 @@ "peerDependencies": { "@xstate/fsm": "^2.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "peerDependenciesMeta": { "@xstate/fsm": { diff --git a/packages/xstate-scxml/package.json b/packages/xstate-scxml/package.json index 202ba668d5..1601e390f9 100644 --- a/packages/xstate-scxml/package.json +++ b/packages/xstate-scxml/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "xml-js": "^1.6.11", - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "devDependencies": { "@scion-scxml/test-framework": "^2.0.15", diff --git a/packages/xstate-svelte/package.json b/packages/xstate-svelte/package.json index 8048c27911..920bcde4b7 100644 --- a/packages/xstate-svelte/package.json +++ b/packages/xstate-svelte/package.json @@ -47,7 +47,7 @@ "peerDependencies": { "@xstate/fsm": "^2.0.0", "svelte": "^3.24.1", - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "peerDependenciesMeta": { "@xstate/fsm": { diff --git a/packages/xstate-test/package.json b/packages/xstate-test/package.json index b4912e5fbc..a62b5c155d 100644 --- a/packages/xstate-test/package.json +++ b/packages/xstate-test/package.json @@ -44,7 +44,7 @@ "url": "https://github.com/statelyai/xstate/issues" }, "peerDependencies": { - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "devDependencies": { "jest": "^26.6.3", diff --git a/packages/xstate-vue/package.json b/packages/xstate-vue/package.json index 765a74b177..41872f9e56 100644 --- a/packages/xstate-vue/package.json +++ b/packages/xstate-vue/package.json @@ -46,7 +46,7 @@ "peerDependencies": { "@xstate/fsm": "^2.0.0", "vue": "^3.0.0", - "xstate": "^4.32.0" + "xstate": "^4.32.1" }, "peerDependenciesMeta": { "@xstate/fsm": { From d7a67b59336d61b7bcf7f638d6b85bcd578ce097 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 13 May 2022 19:40:35 -0500 Subject: [PATCH 070/394] Remove chalk --- packages/xstate-test/package.json | 7 +------ packages/xstate-test/src/slimChalk.browser.ts | 3 --- packages/xstate-test/src/slimChalk.ts | 5 ----- 3 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 packages/xstate-test/src/slimChalk.browser.ts delete mode 100644 packages/xstate-test/src/slimChalk.ts diff --git a/packages/xstate-test/package.json b/packages/xstate-test/package.json index 51431b6a9b..8547a59f2f 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/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); -} From d735bed430c013dbec75520fc3ec71ff833bf836 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 15 May 2022 22:07:11 -0400 Subject: [PATCH 071/394] Add multiple criteria for coverage --- packages/xstate-test/src/TestModel.ts | 14 +++-- packages/xstate-test/src/coverage.ts | 18 +----- packages/xstate-test/test/coverage.test.ts | 67 +++++++++++++++++++++- 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index cd822667ae..71d6a913ae 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -71,6 +71,7 @@ export class TestModel { } }; } + public defaults = {}; constructor( public behavior: SimpleBehavior, @@ -323,11 +324,16 @@ export class TestModel { } public getCoverage( - criteriaFn?: CoverageFunction + criteriaFn?: SingleOrArray> ): Array> { - const criteria = criteriaFn?.(this) ?? []; - - return criteria.map((criterion) => { + const criteriaFns = criteriaFn + ? Array.isArray(criteriaFn) + ? criteriaFn + : [criteriaFn] + : []; + const criteriaResult = flatten(criteriaFns.map((fn) => fn(this))); + + return criteriaResult.map((criterion) => { return { criterion, status: criterion.skip diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts index f5e4b8bd58..c4a6c4781f 100644 --- a/packages/xstate-test/src/coverage.ts +++ b/packages/xstate-test/src/coverage.ts @@ -61,24 +61,8 @@ export function coversAllTransitions< t.eventType === transitionCoverage.step.event.type ); }), - description: `Transitions ${t.source.key} on event ${t.eventType}` + description: `Transitions to state "${t.source.key}" on event "${t.eventType}"` }; }); - // return flatten( - // allStateNodes.map((sn) => { - // const transitions = sn.transitions; - // const transitionSerial = `${this.options.serializeState( - // step.state, - // null as any - // )} | ${this.options.serializeEvent(step.event)}`; - - // return { - // predicate: () => true, - // description: '' - // }; - // }) - // ); - - // return []; }; } diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index ae03747f1f..5bdff13d66 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -211,7 +211,7 @@ describe('coverage', () => { model.testCoverage(coversAllTransitions()); }).toThrowErrorMatchingInlineSnapshot(` "Coverage criteria not met: - Transitions a on event EVENT_TWO" + Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); await model.testPlans(model.getSimplePlans()); @@ -221,6 +221,69 @@ describe('coverage', () => { }).not.toThrow(); }); + it('reports multiple kinds of coverage', async () => { + const model = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT_ONE: 'b', + EVENT_TWO: 'b' + } + }, + b: {} + } + }) + ); + + await model.testPlans(model.getShortestPlans()); + + expect(model.getCoverage([coversAllStates(), coversAllTransitions()])) + .toMatchInlineSnapshot(` + Array [ + Object { + "criterion": Object { + "description": "Visits \\"(machine)\\"", + "predicate": [Function], + "skip": false, + }, + "status": "covered", + }, + Object { + "criterion": Object { + "description": "Visits \\"(machine).a\\"", + "predicate": [Function], + "skip": false, + }, + "status": "covered", + }, + Object { + "criterion": Object { + "description": "Visits \\"(machine).b\\"", + "predicate": [Function], + "skip": false, + }, + "status": "covered", + }, + Object { + "criterion": Object { + "description": "Transitions to state \\"a\\" on event \\"EVENT_ONE\\"", + "predicate": [Function], + }, + "status": "covered", + }, + Object { + "criterion": Object { + "description": "Transitions to state \\"a\\" on event \\"EVENT_TWO\\"", + "predicate": [Function], + }, + "status": "uncovered", + }, + ] + `); + }); + it('tests multiple kinds of coverage', async () => { const model = createTestModel( createMachine({ @@ -243,7 +306,7 @@ describe('coverage', () => { model.testCoverage([coversAllStates(), coversAllTransitions()]); }).toThrowErrorMatchingInlineSnapshot(` "Coverage criteria not met: - Transitions a on event EVENT_TWO" + Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); await model.testPlans(model.getSimplePlans()); From fc5fcca6c761d5d6bbd3755fcb3076612054f441 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 15 May 2022 22:29:17 -0400 Subject: [PATCH 072/394] Add `configure()` --- packages/xstate-test/src/TestModel.ts | 48 ++++++++++----- packages/xstate-test/src/index.ts | 2 +- packages/xstate-test/test/coverage.test.ts | 72 +++++++++++++++++++++- 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 71d6a913ae..ca9be596b2 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -14,7 +14,11 @@ import { } from '@xstate/graph'; import { EventObject, SingleOrArray } from 'xstate'; import { flatten } from './utils'; -import { CoverageFunction } from './coverage'; +import { + CoverageFunction, + coversAllStates, + coversAllTransitions +} from './coverage'; import type { TestModelCoverage, TestModelOptions, @@ -26,6 +30,14 @@ import type { } from './types'; import { formatPathTestResult, simpleStringify } from './utils'; +export interface TestModelDefaults { + coverage: Array>; +} + +export const testModelDefaults: TestModelDefaults = { + coverage: [coversAllStates(), coversAllTransitions()] +}; + /** * Creates a test model that represents an abstract model of a * system under test (SUT). @@ -33,20 +45,7 @@ import { formatPathTestResult, simpleStringify } from './utils'; * 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 { private _coverage: TestModelCoverage = { states: {}, @@ -71,7 +70,7 @@ export class TestModel { } }; } - public defaults = {}; + public static defaults: TestModelDefaults = testModelDefaults; constructor( public behavior: SimpleBehavior, @@ -324,7 +323,8 @@ export class TestModel { } public getCoverage( - criteriaFn?: SingleOrArray> + criteriaFn: SingleOrArray> = TestModel + .defaults.coverage ): Array> { const criteriaFns = criteriaFn ? Array.isArray(criteriaFn) @@ -347,7 +347,8 @@ export class TestModel { // TODO: consider options public testCoverage( - criteriaFn?: SingleOrArray> + criteriaFn: SingleOrArray> = TestModel + .defaults.coverage ): void { const criteriaFns = Array.isArray(criteriaFn) ? criteriaFn : [criteriaFn]; const criteriaResult = flatten( @@ -367,3 +368,16 @@ export class TestModel { } } } + +/** + * Specifies default configuration for `TestModel` instances for coverage and plan generation options + * + * @param testModelConfiguration The partial configuration for all subsequent `TestModel` instances + */ +export function configure( + testModelConfiguration: Partial< + TestModelDefaults + > = testModelDefaults +): void { + TestModel.defaults = { ...testModelDefaults, ...testModelConfiguration }; +} diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index 1606f0949e..801da90655 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,2 +1,2 @@ export { createTestModel } from './machine'; -export { TestModel } from './TestModel'; +export { TestModel, configure } from './TestModel'; diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 5bdff13d66..5a8330aa6c 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -1,4 +1,4 @@ -import { createTestModel } from '../src'; +import { configure, createTestModel } from '../src'; import { createMachine } from 'xstate'; import { coversAllStates, coversAllTransitions } from '../src/coverage'; @@ -315,4 +315,74 @@ describe('coverage', () => { model.testCoverage([coversAllStates(), coversAllTransitions()]); }).not.toThrow(); }); + + it('tests states and transitions coverage by default', async () => { + const model = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT_ONE: 'b', + EVENT_TWO: 'b' + } + }, + b: {}, + c: {} + } + }) + ); + + await model.testPlans(model.getShortestPlans()); + + expect(() => { + model.testCoverage(); + }).toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Visits \\"(machine).c\\" + Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" + `); + }); + + it('configuration should be globally configurable', async () => { + configure({ + coverage: [coversAllStates()] + }); + + const model = createTestModel( + createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT_ONE: 'b', + EVENT_TWO: 'b' + } + }, + b: {}, + c: {} + } + }) + ); + + await model.testPlans(model.getShortestPlans()); + + expect(() => { + model.testCoverage(); + }).toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Visits \\"(machine).c\\"" + `); + + // Reset defaults + configure(); + + expect(() => { + model.testCoverage(); + }).toThrowErrorMatchingInlineSnapshot(` + "Coverage criteria not met: + Visits \\"(machine).c\\" + Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" + `); + }); }); From b8d2726761a41e1e76bc5d206d4da8b434fd7d5b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 15 May 2022 22:55:13 -0400 Subject: [PATCH 073/394] Add changeset for coverage --- .changeset/great-spies-exist.md | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .changeset/great-spies-exist.md diff --git a/.changeset/great-spies-exist.md b/.changeset/great-spies-exist.md new file mode 100644 index 0000000000..81e4b89f0b --- /dev/null +++ b/.changeset/great-spies-exist.md @@ -0,0 +1,37 @@ +--- +'@xstate/graph': major +--- + +Coverage can now be obtained from a tested test model via `testModel.getCoverage(...)`: + +```js +import { configure, createTestModel } from '@xstate/test'; +import { + coversAllStates, + coversAllTransitions +} from '@xstate/test/lib/coverage'; + +// ... + +const testModel = createTestModel(someMachine); + +const plans = testModel.getShortestPlans(); + +for (const plan of plans) { + await testModel.testPlan(plan); +} + +// Returns default coverage: +// - state coverage +// - transition coverage +const coverage = testModel.getCoverage(); + +// Returns state coverage +const stateCoverage = testModel.getCoverage([coversAllStates()]); + +// Returns state coverage +const stateCoverage = testModel.getCoverage([coversAllStates()]); + +// Throws error if state coverage not met +testModel.testCoverage([stateCoverage]); +``` From 7acc9695a812176625ab59c67aed50b07eec5585 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 15 May 2022 22:59:19 -0400 Subject: [PATCH 074/394] Update changeset --- .changeset/neat-socks-rest.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/neat-socks-rest.md diff --git a/.changeset/neat-socks-rest.md b/.changeset/neat-socks-rest.md new file mode 100644 index 0000000000..8b856bec13 --- /dev/null +++ b/.changeset/neat-socks-rest.md @@ -0,0 +1,5 @@ +--- +'@xstate/test': major +--- + +A filter can be specified for From a2e82660ce2c6b3e03742471493082ba5029d902 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 15 May 2022 23:02:36 -0400 Subject: [PATCH 075/394] Update changeset --- .changeset/great-spies-exist.md | 14 ++++++++++++++ .changeset/neat-socks-rest.md | 5 ----- 2 files changed, 14 insertions(+), 5 deletions(-) delete mode 100644 .changeset/neat-socks-rest.md diff --git a/.changeset/great-spies-exist.md b/.changeset/great-spies-exist.md index 81e4b89f0b..c8be46e4f1 100644 --- a/.changeset/great-spies-exist.md +++ b/.changeset/great-spies-exist.md @@ -32,6 +32,20 @@ const stateCoverage = testModel.getCoverage([coversAllStates()]); // Returns state coverage const stateCoverage = testModel.getCoverage([coversAllStates()]); +// Throws error for default coverage: +// - state coverage +// - transition coverage +testModel.testCoverage(); + // Throws error if state coverage not met testModel.testCoverage([stateCoverage]); + +// Filters state coverage +testModel.testCoverage( + coversAllStates({ + filter: (stateNode) => { + return stateNode.key !== 'somePassedState'; + } + }) +); ``` diff --git a/.changeset/neat-socks-rest.md b/.changeset/neat-socks-rest.md deleted file mode 100644 index 8b856bec13..0000000000 --- a/.changeset/neat-socks-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@xstate/test': major ---- - -A filter can be specified for From af2c5b04d54ef43d35176c4e027b50812228e202 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 09:53:37 +0100 Subject: [PATCH 076/394] Removed beforePath and afterPath options --- packages/xstate-test/src/TestModel.ts | 6 -- packages/xstate-test/src/types.ts | 8 --- packages/xstate-test/test/dieHard.test.ts | 14 +++-- packages/xstate-test/test/index.test.ts | 70 ----------------------- 4 files changed, 8 insertions(+), 90 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index ca9be596b2..7ef7be16c3 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -198,10 +198,6 @@ export class TestModel { path: StatePath, options?: Partial> ) { - const resolvedOptions = this.resolveOptions(options); - - await resolvedOptions.beforePath?.(); - const testPathResult: TestPathResult = { steps: [], state: { @@ -246,8 +242,6 @@ export class TestModel { // TODO: make option err.message += formatPathTestResult(path, testPathResult, this.options); throw err; - } finally { - await resolvedOptions.afterPath?.(); } } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 508c6bd32a..065d21371a 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -166,14 +166,6 @@ export interface TestModelOptions log: (msg: string) => void; error: (msg: string) => void; }; - /** - * Executed before each path is tested - */ - beforePath?: () => void | Promise; - /** - * Executed after each path is tested - */ - afterPath?: () => void | Promise; } export interface TestStateCoverage { diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 6ea7da299b..0da4e61ae5 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -62,10 +62,9 @@ describe('die hard example', () => { this.five = this.five - poured; } } + let jugs: Jugs; const createDieHardModel = () => { - let jugs: Jugs; - const dieHardMachine = createMachine( { id: 'dieHard', @@ -111,10 +110,6 @@ describe('die hard example', () => { ); return createTestModel(dieHardMachine, { - beforePath: () => { - jugs = new Jugs(); - jugs.version = Math.random(); - }, states: { pending: (state) => { expect(jugs.five).not.toEqual(4); @@ -160,6 +155,11 @@ describe('die hard example', () => { }); }; + beforeEach(() => { + jugs = new Jugs(); + jugs.version = Math.random(); + }); + describe('testing a model (shortestPathsTo)', () => { const dieHardModel = createDieHardModel(); @@ -260,6 +260,8 @@ describe('die hard example', () => { for (const plan of plans) { for (const path of plan.paths) { + jugs = new Jugs(); + jugs.version = Math.random(); await dieHardModel.testPath(path); } } diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 25fdd176bd..3cd806ddfb 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -390,76 +390,6 @@ describe('test model options', () => { expect(testedEvents).toEqual(['NEXT', 'NEXT', 'PREV']); }); - - it('options.beforePath(...) executes before each path is tested', async () => { - const counts: number[] = []; - let count = 0; - - const testModel = createTestModel( - createMachine({ - initial: 'a', - states: { - a: { - entry: () => { - counts.push(count); - }, - on: { - TO_B: 'b', - TO_C: 'c' - } - }, - b: {}, - c: {} - } - }), - { - beforePath: () => { - count++; - } - } - ); - - const shortestPlans = testModel.getShortestPlans(); - - await testModel.testPlans(shortestPlans); - - expect(counts).toEqual([1, 2, 3]); - }); - - it('options.afterPath(...) executes before each path is tested', async () => { - const counts: number[] = []; - let count = 0; - - const testModel = createTestModel( - createMachine({ - initial: 'a', - states: { - a: { - entry: () => { - counts.push(count); - }, - on: { - TO_B: 'b', - TO_C: 'c' - } - }, - b: {}, - c: {} - } - }), - { - afterPath: () => { - count++; - } - } - ); - - const shortestPlans = testModel.getShortestPlans(); - - await testModel.testPlans(shortestPlans); - - expect(counts).toEqual([0, 1, 2]); - }); }); describe('invocations', () => { From 64e8a406ac3ffd87d87ca865d75d6e9c9c7e35dc Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 11:29:31 +0100 Subject: [PATCH 077/394] Added sync methods to xstate test --- packages/xstate-test/src/TestModel.ts | 161 +++++++++++++++++++++--- packages/xstate-test/src/machine.ts | 14 --- packages/xstate-test/src/types.ts | 4 +- packages/xstate-test/test/index.test.ts | 118 +---------------- packages/xstate-test/test/sync.test.ts | 72 +++++++++++ 5 files changed, 222 insertions(+), 147 deletions(-) create mode 100644 packages/xstate-test/test/sync.test.ts diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 7ef7be16c3..28a167884b 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -1,5 +1,6 @@ import { getPathFromEvents, + performDepthFirstTraversal, SerializedEvent, SerializedState, SimpleBehavior, @@ -7,28 +8,27 @@ import { StatePlan, Step, TraversalOptions, - performDepthFirstTraversal, traverseShortestPlans, - traverseSimplePlans, - traverseSimplePathsTo + traverseSimplePathsTo, + traverseSimplePlans } from '@xstate/graph'; import { EventObject, SingleOrArray } from 'xstate'; -import { flatten } from './utils'; import { CoverageFunction, coversAllStates, coversAllTransitions } from './coverage'; import type { + CriterionResult, + EventExecutor, + PlanGenerator, + StatePredicate, TestModelCoverage, TestModelOptions, - StatePredicate, TestPathResult, - TestStepResult, - CriterionResult, - PlanGenerator + TestStepResult } from './types'; -import { formatPathTestResult, simpleStringify } from './utils'; +import { flatten, formatPathTestResult, simpleStringify } from './utils'; export interface TestModelDefaults { coverage: Array>; @@ -62,7 +62,6 @@ export class TestModel { events: {}, stateMatcher: (_, stateKey) => stateKey === '*', getStates: () => [], - testTransition: () => void 0, execute: () => void 0, logger: { log: console.log.bind(console), @@ -185,6 +184,15 @@ export class TestModel { } } + public testPlansSync( + plans: Array>, + options?: Partial> + ) { + for (const plan of plans) { + this.testPlanSync(plan, options); + } + } + public async testPlan( plan: StatePlan, options?: Partial> @@ -194,6 +202,66 @@ export class TestModel { } } + public testPlanSync( + plan: StatePlan, + options?: Partial> + ) { + for (const path of plan.paths) { + this.testPathSync(path, options); + } + } + + public testPathSync( + path: StatePath, + options?: Partial> + ) { + 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(step.state, options); + } catch (err) { + testStepResult.state.error = err; + + throw err; + } + + try { + this.testTransitionSync(step); + } catch (err) { + testStepResult.event.error = err; + + throw err; + } + } + + try { + this.testStateSync(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; + } + } + public async testPath( path: StatePath, options?: Partial> @@ -251,6 +319,19 @@ export class TestModel { ): Promise { const resolvedOptions = this.resolveOptions(options); + const stateTestKeys = this.getStateTestKeys(state, resolvedOptions); + + for (const stateTestKey of stateTestKeys) { + await resolvedOptions.states[stateTestKey](state); + } + + this.afterTestState(state, resolvedOptions); + } + + private getStateTestKeys( + state: TState, + resolvedOptions: TestModelOptions + ) { const stateTestKeys = Object.keys(resolvedOptions.states).filter( (stateKey) => { return resolvedOptions.stateMatcher(state, stateKey); @@ -262,15 +343,36 @@ export class TestModel { stateTestKeys.push('*'); } - for (const stateTestKey of stateTestKeys) { - await resolvedOptions.states[stateTestKey](state); - } + return stateTestKeys; + } - await resolvedOptions.execute(state); + private afterTestState( + state: TState, + resolvedOptions: TestModelOptions + ) { + resolvedOptions.execute(state); this.addStateCoverage(state); } + public testStateSync( + state: TState, + options?: Partial> + ): void { + const resolvedOptions = this.resolveOptions(options); + + const stateTestKeys = this.getStateTestKeys(state, resolvedOptions); + + for (const stateTestKey of stateTestKeys) { + errorIfPromise( + resolvedOptions.states[stateTestKey](state), + `The test for '${stateTestKey}' returned a promise - did you mean to use the sync method?` + ); + } + + this.afterTestState(state, resolvedOptions); + } + private addStateCoverage(state: TState) { const stateSerial = this.options.serializeState(state, null as any); // TODO: fix @@ -286,8 +388,31 @@ export class TestModel { } } + private getEventExec(step: Step) { + const eventConfig = this.options.events?.[ + (step.event as any).type as TEvent['type'] + ]; + + const eventExec = + typeof eventConfig === 'function' ? eventConfig : eventConfig?.exec; + + return eventExec; + } + public async testTransition(step: Step): Promise { - await this.options.testTransition(step); + const eventExec = this.getEventExec(step); + await (eventExec as EventExecutor)?.(step); + + this.addTransitionCoverage(step); + } + + public testTransitionSync(step: Step): void { + const eventExec = this.getEventExec(step); + + errorIfPromise( + (eventExec as EventExecutor)?.(step), + `The event '${step.event.type}' returned a promise - did you mean to use the sync method?` + ); this.addTransitionCoverage(step); } @@ -375,3 +500,9 @@ export function configure( ): 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/machine.ts b/packages/xstate-test/src/machine.ts index 06ad25ca39..a2b076c6e8 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -103,20 +103,6 @@ export function createTestModel( ); }) ), - testTransition: async (step) => { - const eventConfig = - testModel.options.events?.[ - (step.event as any).type as EventFrom['type'] - ]; - - const eventExec = - typeof eventConfig === 'function' ? eventConfig : eventConfig?.exec; - - await (eventExec as EventExecutor< - StateFrom, - EventFrom - >)?.(step); - }, ...options } ); diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 065d21371a..55c621df43 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -147,11 +147,11 @@ export interface TestModelEventConfig { export interface TestModelOptions extends TraversalOptions { // testState: (state: TState) => void | Promise; - testTransition: (step: Step) => void | Promise; + // testTransition: (step: Step) => void | Promise; /** * Executes actions based on the `state` after the state is tested. */ - execute: (state: TState) => void | Promise; + execute: (state: TState) => void; getStates: () => TState[]; stateMatcher: (state: TState, stateKey: string) => boolean; states: { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 3cd806ddfb..818ad89264 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,7 +1,7 @@ +import { assign, createMachine } from 'xstate'; import { createTestModel } from '../src'; -import { assign, createMachine, interpret } from 'xstate'; -import { getDescription } from '../src/utils'; import { coversAllStates } from '../src/coverage'; +import { getDescription } from '../src/utils'; describe('events', () => { it('should allow for representing many cases', async () => { @@ -352,120 +352,6 @@ describe('test model options', () => { expect(testedStates).toEqual(['inactive', 'inactive', 'active']); }); - - it('options.testTransition(...) should test transition', async () => { - const testedEvents: any[] = []; - - const model = createTestModel( - createMachine({ - initial: 'inactive', - states: { - inactive: { - on: { - NEXT: 'active' - } - }, - active: { - on: { - PREV: 'inactive' - } - } - } - }), - { - // Force traversal to consider all transitions - serializeState: (state) => - ((state.value as any) + state.event.type) as any, - testTransition: (step) => { - testedEvents.push(step.event.type); - } - } - ); - - const plans = model.getShortestPlans(); - - for (const plan of plans) { - await model.testPlan(plan); - } - - expect(testedEvents).toEqual(['NEXT', 'NEXT', 'PREV']); - }); -}); - -describe('invocations', () => { - it.skip('invokes', async () => { - const machine = createMachine({ - initial: 'idle', - states: { - idle: { - on: { - START: 'pending' - } - }, - pending: { - invoke: { - src: (_, e) => new Promise((res) => res(e.value)), - onDone: [ - { cond: (_, e) => e.data === 42, target: 'success' }, - { target: 'failure' } - ] - } - }, - success: {}, - failure: {} - } - }); - - const model = createTestModel(machine, { - events: { - START: { - cases: [ - { type: 'START', value: 42 }, - { type: 'START', value: 1 } - ] - } - } - }); - - const plans = model.getShortestPlans(); - - for (const plan of plans) { - for (const path of plan.paths) { - const service = interpret(machine).start(); - - await model.testPath(path, { - states: { - '*': (state) => { - return new Promise((res) => { - let actualState; - const t = setTimeout(() => { - throw new Error( - `expected ${state.value}, got ${actualState.value}` - ); - }, 1000); - service.subscribe((s) => { - actualState = s; - if (s.matches(state.value)) { - clearTimeout(t); - res(); - } - }); - }); - } - }, - testTransition: (step) => { - if (step.event.type.startsWith('done.')) { - return; - } - - service.send(step.event); - } - }); - } - } - - model.testCoverage(coversAllStates()); - }); }); // https://github.com/statelyai/xstate/issues/1538 diff --git a/packages/xstate-test/test/sync.test.ts b/packages/xstate-test/test/sync.test.ts new file mode 100644 index 0000000000..49cb4229be --- /dev/null +++ b/packages/xstate-test/test/sync.test.ts @@ -0,0 +1,72 @@ +import { createMachine } from 'xstate'; +import { createTestModel } from '../src'; + +const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { + EVENT: 'b' + } + }, + b: {} + } +}); + +const promiseStateModel = createTestModel(machine, { + states: { + a: async () => {}, + b: () => {} + }, + events: { + EVENT: () => {} + } +}); + +const promiseEventModel = createTestModel(machine, { + states: { + a: () => {}, + b: () => {} + }, + events: { + EVENT: { + exec: async () => {} + } + } +}); + +const syncModel = createTestModel(machine, { + states: { + a: () => {}, + b: () => {} + }, + events: { + EVENT: { + exec: () => {} + } + } +}); + +describe('.testPlansSync', () => { + it('Should error if it encounters a promise in a state', () => { + expect(() => + promiseStateModel.testPlansSync(promiseStateModel.getShortestPlans()) + ).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.testPlansSync(promiseEventModel.getShortestPlans()) + ).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.testPlansSync(syncModel.getShortestPlans()) + ).not.toThrow(); + }); +}); From ff270783ad546c2f3ac76ef05b74cdf0f91ca7dd Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 11:32:25 +0100 Subject: [PATCH 078/394] Removed dead import --- packages/xstate-test/src/machine.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index a2b076c6e8..8baeacc7a0 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -1,4 +1,4 @@ -import { SimpleBehavior, serializeState } from '@xstate/graph'; +import { serializeState, SimpleBehavior } from '@xstate/graph'; import type { ActionObject, AnyState, @@ -6,9 +6,9 @@ import type { EventFrom, StateFrom } from 'xstate'; -import { flatten } from './utils'; import { TestModel } from './TestModel'; -import { TestModelEventConfig, TestModelOptions, EventExecutor } from './types'; +import { TestModelEventConfig, TestModelOptions } from './types'; +import { flatten } from './utils'; export async function testStateFromMeta(state: AnyState) { for (const id of Object.keys(state.meta)) { From f5dd7dd60e49bac1cafd489a85eca2f78bfeb047 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 11:35:04 +0100 Subject: [PATCH 079/394] Added defaults to getPlans and testPlans --- packages/xstate-test/src/TestModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 7ef7be16c3..49f8ff25a6 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -83,7 +83,7 @@ export class TestModel { } public getPlans( - planGenerator: PlanGenerator, + planGenerator: PlanGenerator = traverseShortestPlans, options?: Partial> ): Array> { const plans = planGenerator(this.behavior, this.resolveOptions(options)); @@ -177,7 +177,7 @@ export class TestModel { } public async testPlans( - plans: Array>, + plans: Array> = this.getPlans(), options?: Partial> ) { for (const plan of plans) { From 291e9814fb35edd1ff15ad7c698b9c46a61b0315 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 11:46:11 +0100 Subject: [PATCH 080/394] Changed how planGenerator is passed to getPlans, and how plans are passed to testPlans --- packages/xstate-test/src/TestModel.ts | 43 +++++++++------------- packages/xstate-test/src/types.ts | 13 +++++++ packages/xstate-test/test/coverage.test.ts | 14 +++---- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 49f8ff25a6..cd0bba6810 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -1,5 +1,6 @@ import { getPathFromEvents, + performDepthFirstTraversal, SerializedEvent, SerializedState, SimpleBehavior, @@ -7,28 +8,27 @@ import { StatePlan, Step, TraversalOptions, - performDepthFirstTraversal, traverseShortestPlans, - traverseSimplePlans, - traverseSimplePathsTo + traverseSimplePathsTo, + traverseSimplePlans } from '@xstate/graph'; import { EventObject, SingleOrArray } from 'xstate'; -import { flatten } from './utils'; import { CoverageFunction, coversAllStates, coversAllTransitions } from './coverage'; import type { + CriterionResult, + GetPlansOptions, + StatePredicate, TestModelCoverage, TestModelOptions, - StatePredicate, TestPathResult, - TestStepResult, - CriterionResult, - PlanGenerator + TestPlansOptions, + TestStepResult } from './types'; -import { formatPathTestResult, simpleStringify } from './utils'; +import { flatten, formatPathTestResult, simpleStringify } from './utils'; export interface TestModelDefaults { coverage: Array>; @@ -83,9 +83,9 @@ export class TestModel { } public getPlans( - planGenerator: PlanGenerator = traverseShortestPlans, - options?: Partial> + options?: GetPlansOptions ): Array> { + const planGenerator = options?.planGenerator || traverseShortestPlans; const plans = planGenerator(this.behavior, this.resolveOptions(options)); return plans; @@ -94,11 +94,7 @@ export class TestModel { public getShortestPlans( options?: Partial> ): Array> { - return this.getPlans((behavior, resolvedOptions) => { - const shortestPaths = traverseShortestPlans(behavior, resolvedOptions); - - return Object.values(shortestPaths); - }, options); + return this.getPlans({ ...options, planGenerator: traverseShortestPlans }); } public getShortestPlansTo( @@ -125,11 +121,10 @@ export class TestModel { public getSimplePlans( options?: Partial> ): Array> { - return this.getPlans((behavior, resolvedOptions) => { - const simplePaths = traverseSimplePlans(behavior, resolvedOptions); - - return Object.values(simplePaths); - }, options); + return this.getPlans({ + ...options, + planGenerator: traverseSimplePlans + }); } public getSimplePlansTo( @@ -176,10 +171,8 @@ export class TestModel { return Object.values(adj).map((x) => x.state); } - public async testPlans( - plans: Array> = this.getPlans(), - options?: Partial> - ) { + public async testPlans(options?: TestPlansOptions) { + const plans = options?.plans || this.getPlans(options); for (const plan of plans) { await this.testPlan(plan, options); } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 065d21371a..c25041fd88 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -12,6 +12,19 @@ import { StateNode, TransitionConfig } from 'xstate'; + +export type GetPlansOptions = Partial< + TraversalOptions & { + planGenerator?: PlanGenerator; + } +>; + +export type TestPlansOptions = Partial< + TestModelOptions & { + plans?: Array>; + } +>; + export interface TestMeta { test?: (testContext: T, state: State) => Promise | void; description?: string | ((state: State) => string); diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 5a8330aa6c..ca4acae0a2 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -205,7 +205,7 @@ describe('coverage', () => { }) ); - await model.testPlans(model.getShortestPlans()); + await model.testPlans(); expect(() => { model.testCoverage(coversAllTransitions()); @@ -214,7 +214,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await model.testPlans(model.getSimplePlans()); + await model.testPlans({ plans: model.getSimplePlans() }); expect(() => { model.testCoverage(coversAllTransitions()); @@ -237,7 +237,7 @@ describe('coverage', () => { }) ); - await model.testPlans(model.getShortestPlans()); + await model.testPlans(); expect(model.getCoverage([coversAllStates(), coversAllTransitions()])) .toMatchInlineSnapshot(` @@ -300,7 +300,7 @@ describe('coverage', () => { }) ); - await model.testPlans(model.getShortestPlans()); + await model.testPlans({ plans: model.getShortestPlans() }); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); @@ -309,7 +309,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await model.testPlans(model.getSimplePlans()); + await model.testPlans({ plans: model.getSimplePlans() }); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); @@ -333,7 +333,7 @@ describe('coverage', () => { }) ); - await model.testPlans(model.getShortestPlans()); + await model.testPlans(); expect(() => { model.testCoverage(); @@ -365,7 +365,7 @@ describe('coverage', () => { }) ); - await model.testPlans(model.getShortestPlans()); + await model.testPlans(); expect(() => { model.testCoverage(); From bbeff0fd02f59210ec1e36b0d37624f5e35563a0 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 12:22:51 +0100 Subject: [PATCH 081/394] Added createTestMachine --- packages/xstate-test/src/TestModel.ts | 69 ++++++++++++-- packages/xstate-test/src/machine.ts | 26 +++++- packages/xstate-test/test/coverage.test.ts | 20 ++-- packages/xstate-test/test/dieHard.test.ts | 7 +- packages/xstate-test/test/events.test.ts | 6 +- packages/xstate-test/test/index.test.ts | 103 +++------------------ packages/xstate-test/test/plans.test.ts | 4 +- packages/xstate-test/test/states.test.ts | 7 +- 8 files changed, 117 insertions(+), 125 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 7ef7be16c3..3858c4d5b6 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -1,5 +1,6 @@ import { getPathFromEvents, + performDepthFirstTraversal, SerializedEvent, SerializedState, SimpleBehavior, @@ -7,28 +8,76 @@ import { StatePlan, Step, TraversalOptions, - performDepthFirstTraversal, traverseShortestPlans, - traverseSimplePlans, - traverseSimplePathsTo + traverseSimplePathsTo, + traverseSimplePlans } from '@xstate/graph'; -import { EventObject, SingleOrArray } from 'xstate'; -import { flatten } from './utils'; +import { + BaseActionObject, + EventObject, + MachineConfig, + MachineOptions, + MachineSchema, + ServiceMap, + SingleOrArray, + StateNodeConfig, + StateSchema, + TypegenConstraint, + TypegenDisabled +} from 'xstate'; import { CoverageFunction, coversAllStates, coversAllTransitions } from './coverage'; import type { + CriterionResult, + PlanGenerator, + StatePredicate, TestModelCoverage, TestModelOptions, - StatePredicate, TestPathResult, - TestStepResult, - CriterionResult, - PlanGenerator + TestStepResult } from './types'; -import { formatPathTestResult, simpleStringify } from './utils'; +import { flatten, formatPathTestResult, simpleStringify } from './utils'; + +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' + | '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 TestModelDefaults { coverage: Array>; diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 06ad25ca39..d01b5e3e70 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -1,14 +1,19 @@ -import { SimpleBehavior, serializeState } from '@xstate/graph'; -import type { +import { serializeState, SimpleBehavior } from '@xstate/graph'; +import { ActionObject, + AnyEventObject, AnyState, AnyStateMachine, + createMachine, EventFrom, - StateFrom + EventObject, + StateFrom, + TypegenConstraint, + TypegenDisabled } from 'xstate'; +import { TestMachineConfig, TestMachineOptions, TestModel } from './TestModel'; +import { EventExecutor, TestModelEventConfig, TestModelOptions } from './types'; import { flatten } from './utils'; -import { TestModel } from './TestModel'; -import { TestModelEventConfig, TestModelOptions, EventExecutor } from './types'; export async function testStateFromMeta(state: AnyState) { for (const id of Object.keys(state.meta)) { @@ -19,6 +24,17 @@ export async function testStateFromMeta(state: AnyState) { } } +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 diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 5a8330aa6c..7c50a8efa3 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -1,10 +1,10 @@ import { configure, createTestModel } from '../src'; -import { createMachine } from 'xstate'; import { coversAllStates, coversAllTransitions } from '../src/coverage'; +import { createTestMachine } from '../src/machine'; describe('coverage', () => { it('reports missing state node coverage', async () => { - const machine = createMachine({ + const machine = createTestMachine({ id: 'test', initial: 'first', states: { @@ -57,7 +57,7 @@ describe('coverage', () => { // https://github.com/statelyai/xstate/issues/729 it('reports full coverage when all states are covered', async () => { - const feedbackMachine = createMachine({ + const feedbackMachine = createTestMachine({ id: 'feedback', initial: 'logon', states: { @@ -97,7 +97,7 @@ describe('coverage', () => { }); it('skips filtered states (filter option)', async () => { - const TestBug = createMachine({ + const TestBug = createTestMachine({ id: 'testbug', initial: 'idle', context: { @@ -144,7 +144,7 @@ describe('coverage', () => { // https://github.com/statelyai/xstate/issues/981 it.skip('skips transient states (type: final)', async () => { - const machine = createMachine({ + const machine = createTestMachine({ id: 'menu', initial: 'initial', states: { @@ -191,7 +191,7 @@ describe('coverage', () => { it('tests transition coverage', async () => { const model = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { @@ -223,7 +223,7 @@ describe('coverage', () => { it('reports multiple kinds of coverage', async () => { const model = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { @@ -286,7 +286,7 @@ describe('coverage', () => { it('tests multiple kinds of coverage', async () => { const model = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { @@ -318,7 +318,7 @@ describe('coverage', () => { it('tests states and transitions coverage by default', async () => { const model = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { @@ -350,7 +350,7 @@ describe('coverage', () => { }); const model = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 0da4e61ae5..04654ce556 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -1,7 +1,8 @@ -import { createTestModel } from '../src'; import { assign, createMachine } from 'xstate'; -import { getDescription } from '../src/utils'; +import { createTestModel } from '../src'; import { coversAllStates } from '../src/coverage'; +import { createTestMachine } from '../src/machine'; +import { getDescription } from '../src/utils'; describe('die hard example', () => { interface DieHardContext { @@ -293,7 +294,7 @@ describe('die hard example', () => { }); describe('error path trace', () => { describe('should return trace for failed state', () => { - const machine = createMachine({ + const machine = createTestMachine({ initial: 'first', states: { first: { diff --git a/packages/xstate-test/test/events.test.ts b/packages/xstate-test/test/events.test.ts index 4e0c5f85bf..fc66f6c88d 100644 --- a/packages/xstate-test/test/events.test.ts +++ b/packages/xstate-test/test/events.test.ts @@ -1,12 +1,12 @@ -import { createMachine } from 'xstate'; import { createTestModel } from '../src'; +import { createTestMachine } from '../src/machine'; describe('events', () => { it('should execute events (`exec` property)', async () => { let executed = false; const testModel = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { @@ -37,7 +37,7 @@ describe('events', () => { let executed = false; const testModel = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 3cd806ddfb..b1ff545875 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,7 +1,8 @@ +import { assign, createMachine } from 'xstate'; import { createTestModel } from '../src'; -import { assign, createMachine, interpret } from 'xstate'; -import { getDescription } from '../src/utils'; import { coversAllStates } from '../src/coverage'; +import { createTestMachine } from '../src/machine'; +import { getDescription } from '../src/utils'; describe('events', () => { it('should allow for representing many cases', async () => { @@ -11,7 +12,7 @@ describe('events', () => { | { type: 'CLOSE' } | { type: 'ESC' } | { type: 'SUBMIT'; value: string }; - const feedbackMachine = createMachine({ + const feedbackMachine = createTestMachine({ id: 'feedback', schema: { events: {} as Events @@ -74,7 +75,7 @@ describe('events', () => { }); it('should not throw an error for unimplemented events', () => { - const testMachine = createMachine({ + const testMachine = createTestMachine({ initial: 'idle', states: { idle: { @@ -190,7 +191,7 @@ describe('state limiting', () => { }); describe('plan description', () => { - const machine = createMachine({ + const machine = createTestMachine({ id: 'test', initial: 'atomic', context: { count: 0 }, @@ -285,7 +286,7 @@ it('prevents infinite recursion based on a provided limit', () => { it('executes actions', async () => { let executedActive = false; let executedDone = false; - const machine = createMachine({ + const machine = createTestMachine({ initial: 'idle', states: { idle: { @@ -324,7 +325,7 @@ describe('test model options', () => { const testedStates: any[] = []; const model = createTestModel( - createMachine({ + createTestMachine({ initial: 'inactive', states: { inactive: { @@ -357,7 +358,7 @@ describe('test model options', () => { const testedEvents: any[] = []; const model = createTestModel( - createMachine({ + createTestMachine({ initial: 'inactive', states: { inactive: { @@ -392,86 +393,10 @@ describe('test model options', () => { }); }); -describe('invocations', () => { - it.skip('invokes', async () => { - const machine = createMachine({ - initial: 'idle', - states: { - idle: { - on: { - START: 'pending' - } - }, - pending: { - invoke: { - src: (_, e) => new Promise((res) => res(e.value)), - onDone: [ - { cond: (_, e) => e.data === 42, target: 'success' }, - { target: 'failure' } - ] - } - }, - success: {}, - failure: {} - } - }); - - const model = createTestModel(machine, { - events: { - START: { - cases: [ - { type: 'START', value: 42 }, - { type: 'START', value: 1 } - ] - } - } - }); - - const plans = model.getShortestPlans(); - - for (const plan of plans) { - for (const path of plan.paths) { - const service = interpret(machine).start(); - - await model.testPath(path, { - states: { - '*': (state) => { - return new Promise((res) => { - let actualState; - const t = setTimeout(() => { - throw new Error( - `expected ${state.value}, got ${actualState.value}` - ); - }, 1000); - service.subscribe((s) => { - actualState = s; - if (s.matches(state.value)) { - clearTimeout(t); - res(); - } - }); - }); - } - }, - testTransition: (step) => { - if (step.event.type.startsWith('done.')) { - return; - } - - service.send(step.event); - } - }); - } - } - - model.testCoverage(coversAllStates()); - }); -}); - // https://github.com/statelyai/xstate/issues/1538 it('tests transitions', async () => { expect.assertions(2); - const machine = createMachine({ + const machine = createTestMachine({ initial: 'first', states: { first: { @@ -501,7 +426,7 @@ it('tests transitions', async () => { // https://github.com/statelyai/xstate/issues/982 it('Event in event executor should contain payload from case', async () => { - const machine = createMachine({ + const machine = createTestMachine({ initial: 'first', states: { first: { @@ -543,7 +468,7 @@ describe('state tests', () => { // a -> b (2) expect.assertions(3); - const machine = createMachine({ + const machine = createTestMachine({ initial: 'a', states: { a: { @@ -573,7 +498,7 @@ describe('state tests', () => { // a -> c (2) expect.assertions(5); - const machine = createMachine({ + const machine = createTestMachine({ initial: 'a', states: { a: { @@ -604,7 +529,7 @@ describe('state tests', () => { it('should test nested states', async () => { const testedStateValues: any[] = []; - const machine = createMachine({ + const machine = createTestMachine({ initial: 'a', states: { a: { diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index d2be80cf42..308ee49912 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -1,11 +1,11 @@ -import { createMachine } from 'xstate'; import { createTestModel } from '../src'; import { coversAllStates } from '../src/coverage'; +import { createTestMachine } from '../src/machine'; describe('testModel.testPlans(...)', () => { it('custom plan generators can be provided', async () => { const testModel = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts index ee0a31dce4..42b8579d94 100644 --- a/packages/xstate-test/test/states.test.ts +++ b/packages/xstate-test/test/states.test.ts @@ -1,11 +1,12 @@ -import { createMachine, StateValue } from 'xstate'; +import { StateValue } from 'xstate'; import { createTestModel } from '../src'; +import { createTestMachine } from '../src/machine'; describe('states', () => { it('should test states by key', async () => { const testedStateValues: StateValue[] = []; const testModel = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { @@ -71,7 +72,7 @@ describe('states', () => { it('should test states by ID', async () => { const testedStateValues: StateValue[] = []; const testModel = createTestModel( - createMachine({ + createTestMachine({ initial: 'a', states: { a: { From 475c36661724c400edba388184ac10d1b2635c31 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 12:31:14 +0100 Subject: [PATCH 082/394] Fixed tsc --- packages/xstate-test/test/index.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 3cd806ddfb..bd6cfa34db 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -138,7 +138,7 @@ describe('events', () => { expect(plans.length).toBe(4); - await testModel.testPlans(plans); + await testModel.testPlans({ plans }); expect(testedEvents).toMatchInlineSnapshot(` Array [ @@ -564,7 +564,7 @@ describe('state tests', () => { } }); - await model.testPlans(model.getShortestPlans()); + await model.testPlans(); }); it('should test wildcard state for non-matching states', async () => { @@ -598,7 +598,7 @@ describe('state tests', () => { } }); - await model.testPlans(model.getShortestPlans()); + await model.testPlans(); }); it('should test nested states', async () => { @@ -636,7 +636,7 @@ describe('state tests', () => { } }); - await model.testPlans(model.getShortestPlans()); + await model.testPlans(); expect(testedStateValues).toMatchInlineSnapshot(` Array [ "a", From e51ac068815717e8966fe173c650b49294d42021 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 12:36:15 +0100 Subject: [PATCH 083/394] More ts fixes --- packages/xstate-test/test/events.test.ts | 4 +- packages/xstate-test/test/plans.test.ts | 44 +++++++++++---------- packages/xstate-test/test/states.test.ts | 4 +- packages/xstate-test/test/testModel.test.ts | 2 +- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/xstate-test/test/events.test.ts b/packages/xstate-test/test/events.test.ts index 4e0c5f85bf..3ec086aa28 100644 --- a/packages/xstate-test/test/events.test.ts +++ b/packages/xstate-test/test/events.test.ts @@ -28,7 +28,7 @@ describe('events', () => { } ); - await testModel.testPlans(testModel.getShortestPlans()); + await testModel.testPlans(); expect(executed).toBe(true); }); @@ -57,7 +57,7 @@ describe('events', () => { } ); - await testModel.testPlans(testModel.getShortestPlans()); + await testModel.testPlans(); expect(executed).toBe(true); }); diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index d2be80cf42..3fc7fd6ec9 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -18,30 +18,32 @@ describe('testModel.testPlans(...)', () => { }) ); - const plans = testModel.getPlans((behavior, options) => { - const events = options.getEvents?.(behavior.initialState) ?? []; + const plans = testModel.getPlans({ + planGenerator: (behavior, options) => { + const events = options.getEvents?.(behavior.initialState) ?? []; - const nextState = behavior.transition(behavior.initialState, events[0]); - return [ - { - state: nextState, - paths: [ - { - state: nextState, - steps: [ - { - state: behavior.initialState, - event: events[0] - } - ], - weight: 1 - } - ] - } - ]; + const nextState = behavior.transition(behavior.initialState, events[0]); + return [ + { + state: nextState, + paths: [ + { + state: nextState, + steps: [ + { + state: behavior.initialState, + event: events[0] + } + ], + weight: 1 + } + ] + } + ]; + } }); - await testModel.testPlans(plans); + await testModel.testPlans({ plans }); expect(testModel.getCoverage(coversAllStates())).toMatchInlineSnapshot(` Array [ diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts index ee0a31dce4..ade4eac7d3 100644 --- a/packages/xstate-test/test/states.test.ts +++ b/packages/xstate-test/test/states.test.ts @@ -40,7 +40,7 @@ describe('states', () => { } ); - await testModel.testPlans(testModel.getShortestPlans()); + await testModel.testPlans(); expect(testedStateValues).toMatchInlineSnapshot(` Array [ @@ -113,7 +113,7 @@ describe('states', () => { } ); - await testModel.testPlans(testModel.getShortestPlans()); + await testModel.testPlans(); expect(testedStateValues).toMatchInlineSnapshot(` Array [ diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index 0f8011199e..f939e138b6 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -73,7 +73,7 @@ describe('custom test models', () => { const plans = model.getShortestPlansTo((state) => state === 1); - await model.testPlans(plans); + await model.testPlans({ plans }); expect(testedStateKeys).toContain('even'); expect(testedStateKeys).toContain('odd'); From 4227d84769cc0c4e001bf0b80103f99679bb8556 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 12:41:08 +0100 Subject: [PATCH 084/394] Fixed TS --- packages/xstate-test/src/TestModel.ts | 52 +-------------------------- packages/xstate-test/src/machine.ts | 10 ++++-- packages/xstate-test/src/types.ts | 51 +++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 54 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 3858c4d5b6..e7d34534eb 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -12,19 +12,7 @@ import { traverseSimplePathsTo, traverseSimplePlans } from '@xstate/graph'; -import { - BaseActionObject, - EventObject, - MachineConfig, - MachineOptions, - MachineSchema, - ServiceMap, - SingleOrArray, - StateNodeConfig, - StateSchema, - TypegenConstraint, - TypegenDisabled -} from 'xstate'; +import { EventObject, SingleOrArray } from 'xstate'; import { CoverageFunction, coversAllStates, @@ -41,44 +29,6 @@ import type { } from './types'; import { flatten, formatPathTestResult, simpleStringify } from './utils'; -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' - | '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 TestModelDefaults { coverage: Array>; } diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index d01b5e3e70..5f69d9dc58 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -11,8 +11,14 @@ import { TypegenConstraint, TypegenDisabled } from 'xstate'; -import { TestMachineConfig, TestMachineOptions, TestModel } from './TestModel'; -import { EventExecutor, TestModelEventConfig, TestModelOptions } from './types'; +import { TestModel } from './TestModel'; +import { + EventExecutor, + TestMachineConfig, + TestMachineOptions, + TestModelEventConfig, + TestModelOptions +} from './types'; import { flatten } from './utils'; export async function testStateFromMeta(state: AnyState) { diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 065d21371a..59229f8688 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -6,12 +6,61 @@ import { } from '@xstate/graph'; import { AnyState, + BaseActionObject, EventObject, ExtractEvent, + MachineConfig, + MachineOptions, + MachineSchema, + ServiceMap, State, StateNode, - TransitionConfig + StateNodeConfig, + StateSchema, + TransitionConfig, + TypegenConstraint, + TypegenDisabled } from 'xstate'; + +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); From 617f35420033ec8e95477d56aefcd34a93636792 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 12:49:13 +0100 Subject: [PATCH 085/394] Fixed CI so it only runs push on main and next --- .github/workflows/nodejs.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 83a7f4a057..0b8f186396 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,6 +1,13 @@ name: Node CI -on: [push, pull_request] +on: + push: + branches: + - main + - next + pull_request: + branches: + - '**' jobs: build: From 5daaab694abfb3d2e23e6d8f1c6d8725057bd7d1 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 14:19:32 +0100 Subject: [PATCH 086/394] Added planGenerator to configure --- packages/xstate-test/src/TestModel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index cd0bba6810..42abd4e0bb 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -21,6 +21,7 @@ import { import type { CriterionResult, GetPlansOptions, + PlanGenerator, StatePredicate, TestModelCoverage, TestModelOptions, @@ -32,10 +33,12 @@ import { flatten, formatPathTestResult, simpleStringify } from './utils'; export interface TestModelDefaults { coverage: Array>; + planGenerator: PlanGenerator; } export const testModelDefaults: TestModelDefaults = { - coverage: [coversAllStates(), coversAllTransitions()] + coverage: [coversAllStates(), coversAllTransitions()], + planGenerator: traverseShortestPlans }; /** @@ -85,7 +88,8 @@ export class TestModel { public getPlans( options?: GetPlansOptions ): Array> { - const planGenerator = options?.planGenerator || traverseShortestPlans; + const planGenerator = + options?.planGenerator || TestModel.defaults.planGenerator; const plans = planGenerator(this.behavior, this.resolveOptions(options)); return plans; From 29926b588dc1e28c442c934b2ff4ca41eab3cffc Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 14:46:01 +0100 Subject: [PATCH 087/394] Ensured that disallowed machine attributes are validated --- packages/xstate-test/src/machine.ts | 3 ++ packages/xstate-test/src/validateMachine.ts | 30 ++++++++++++ .../test/forbiddenAttributes.test.ts | 49 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 packages/xstate-test/src/validateMachine.ts create mode 100644 packages/xstate-test/test/forbiddenAttributes.test.ts diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 5f69d9dc58..88a34ecd5f 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -20,6 +20,7 @@ import { TestModelOptions } from './types'; import { flatten } from './utils'; +import { validateMachine } from './validateMachine'; export async function testStateFromMeta(state: AnyState) { for (const id of Object.keys(state.meta)) { @@ -82,6 +83,8 @@ export function createTestModel( machine: TMachine, options?: Partial, EventFrom>> ): TestModel, EventFrom> { + validateMachine(machine); + const testModel = new TestModel, EventFrom>( machine as SimpleBehavior, { 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/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'); + }); +}); From e04c27346d28f2e22858ea0284468e64816fe24c Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 16 May 2022 15:52:34 +0100 Subject: [PATCH 088/394] Added deduplication of generated paths --- packages/xstate-test/src/TestModel.ts | 5 +- packages/xstate-test/src/dedupPathPlans.ts | 95 ++++++++++++++++++++++ packages/xstate-test/test/index.test.ts | 53 ++---------- packages/xstate-test/test/plans.test.ts | 73 ++++++++++++++++- packages/xstate-test/test/states.test.ts | 16 ---- 5 files changed, 176 insertions(+), 66 deletions(-) create mode 100644 packages/xstate-test/src/dedupPathPlans.ts diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index cd0bba6810..6c02a40428 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -18,6 +18,7 @@ import { coversAllStates, coversAllTransitions } from './coverage'; +import { addDedupToPlanGenerator } from './dedupPathPlans'; import type { CriterionResult, GetPlansOptions, @@ -85,7 +86,9 @@ export class TestModel { public getPlans( options?: GetPlansOptions ): Array> { - const planGenerator = options?.planGenerator || traverseShortestPlans; + const planGenerator = addDedupToPlanGenerator( + options?.planGenerator || traverseShortestPlans + ); const plans = planGenerator(this.behavior, this.resolveOptions(options)); return plans; diff --git a/packages/xstate-test/src/dedupPathPlans.ts b/packages/xstate-test/src/dedupPathPlans.ts new file mode 100644 index 0000000000..72a5af69b2 --- /dev/null +++ b/packages/xstate-test/src/dedupPathPlans.ts @@ -0,0 +1,95 @@ +import { StatePath, StatePlan } from '@xstate/graph'; +import { EventObject } from 'xstate'; +import { PlanGenerator } from './types'; + +/** + * Deduplicates your path plans so that A -> B + * is not executed separately to A -> B -> C + */ +export const addDedupToPlanGenerator = ( + planGenerator: PlanGenerator +): PlanGenerator => (behavior, options) => { + const pathPlans = planGenerator(behavior, options); + + /** + * Put all plans on the same level so we can dedup them + */ + const allPathsWithPlan: { + path: StatePath; + planIndex: number; + serialisedSteps: string[]; + }[] = []; + + pathPlans.forEach((plan, index) => { + plan.paths.forEach((path) => { + allPathsWithPlan.push({ + path, + planIndex: index, + serialisedSteps: path.steps.map((step) => + options.serializeEvent(step.event) + ) + }); + }); + }); + + /** + * Filter out the paths that are just shorter versions + * of other paths + */ + const filteredPaths = allPathsWithPlan.filter((path) => { + if (path.serialisedSteps.length === 0) return false; + + /** + * @example + * { type: 'EVENT_1' }{ type: 'EVENT_2' } + */ + const concatenatedPath = path.serialisedSteps.join(''); + + return !allPathsWithPlan.some((pathToCompare) => { + const concatenatedPathToCompare = pathToCompare.serialisedSteps.join(''); + /** + * Filter IN (return false) if it's the same as the current plan, + * because it's not a valid comparison + */ + if (concatenatedPathToCompare === concatenatedPath) { + return false; + } + + /** + * Filter IN (return false) if the plan to compare against has length 0 + */ + if (pathToCompare.serialisedSteps.length === 0) { + return false; + } + + /** + * We filter OUT (return true) if the segment to compare includes + * our current segment + */ + return concatenatedPathToCompare.includes(concatenatedPath); + }); + }); + + const newPathPlans = pathPlans + .map( + (plan, index): StatePlan => { + /** + * Grab the paths which were originally related + * to this planIndex + */ + const newPaths = filteredPaths + .filter(({ planIndex }) => planIndex === index) + .map(({ path }) => path); + return { + state: plan.state, + paths: newPaths + }; + } + ) + /** + * Filter out plans which don't have any unique paths + */ + .filter((plan) => plan.paths.length > 0); + + return newPathPlans; +}; diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index f511c5f61c..d213229953 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -137,7 +137,7 @@ describe('events', () => { const plans = testModel.getShortestPlans(); - expect(plans.length).toBe(4); + expect(plans.length).toBe(3); await testModel.testPlans({ plans }); @@ -186,7 +186,7 @@ describe('state limiting', () => { } }); - expect(testPlans).toHaveLength(5); + expect(testPlans).toHaveLength(1); }); }); @@ -250,11 +250,7 @@ describe('plan description', () => { expect(planDescriptions).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})", ] `); @@ -351,45 +347,7 @@ describe('test model options', () => { await model.testPlan(plan); } - expect(testedStates).toEqual(['inactive', 'inactive', 'active']); - }); - - it('options.testTransition(...) should test transition', async () => { - const testedEvents: any[] = []; - - const model = createTestModel( - createTestMachine({ - initial: 'inactive', - states: { - inactive: { - on: { - NEXT: 'active' - } - }, - active: { - on: { - PREV: 'inactive' - } - } - } - }), - { - // Force traversal to consider all transitions - serializeState: (state) => - ((state.value as any) + state.event.type) as any, - testTransition: (step) => { - testedEvents.push(step.event.type); - } - } - ); - - const plans = model.getShortestPlans(); - - for (const plan of plans) { - await model.testPlan(plan); - } - - expect(testedEvents).toEqual(['NEXT', 'NEXT', 'PREV']); + expect(testedStates).toEqual(['inactive', 'active']); }); }); @@ -466,7 +424,7 @@ describe('state tests', () => { it('should test states', async () => { // a (1) // a -> b (2) - expect.assertions(3); + expect.assertions(2); const machine = createTestMachine({ initial: 'a', @@ -496,7 +454,7 @@ describe('state tests', () => { // a (1) // a -> b (2) // a -> c (2) - expect.assertions(5); + expect.assertions(4); const machine = createTestMachine({ initial: 'a', @@ -564,7 +522,6 @@ describe('state tests', () => { await model.testPlans(); expect(testedStateValues).toMatchInlineSnapshot(` Array [ - "a", "a", "b", "b.b1", diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index defd1c27e5..7a610861fa 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -1,7 +1,31 @@ -import { createTestModel } from '../src'; +import { configure, createTestModel } from '../src'; import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; +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.testPlans(...)', () => { it('custom plan generators can be provided', async () => { const testModel = createTestModel( @@ -74,4 +98,51 @@ describe('testModel.testPlans(...)', () => { ] `); }); + + 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 plans = model.getPlans(); + + expect(plans).toHaveLength(1); + }); + }); + + describe('When the machine only has more than one path', () => { + it('Should create plans for each path', () => { + const model = createTestModel(multiPathMachine); + + const plans = model.getPlans(); + + expect(plans).toHaveLength(2); + }); + }); + + describe('simplePathPlans', () => { + it('Should dedup simple path plans too', () => { + const model = createTestModel(multiPathMachine); + + const plans = model.getSimplePlans(); + + expect(plans).toHaveLength(2); + }); + }); }); diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts index 43dc7a054c..93da20cfe5 100644 --- a/packages/xstate-test/test/states.test.ts +++ b/packages/xstate-test/test/states.test.ts @@ -45,14 +45,6 @@ describe('states', () => { expect(testedStateValues).toMatchInlineSnapshot(` Array [ - "a", - "a", - Object { - "b": "b1", - }, - Object { - "b": "b1", - }, "a", Object { "b": "b1", @@ -118,14 +110,6 @@ describe('states', () => { expect(testedStateValues).toMatchInlineSnapshot(` Array [ - "a", - "a", - Object { - "b": "b1", - }, - Object { - "b": "b1", - }, "a", Object { "b": "b1", From ec7e2aa07562368518549864cb8c166b6f0b3094 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 16 May 2022 21:31:24 -0400 Subject: [PATCH 089/394] Optimize algorithm --- packages/xstate-test/src/TestModel.ts | 4 +- packages/xstate-test/src/dedupPathPlans.ts | 78 +++++++++++----------- packages/xstate-test/test/plans.test.ts | 2 +- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index b9cd88e406..8fb2a4c97f 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -18,7 +18,7 @@ import { coversAllStates, coversAllTransitions } from './coverage'; -import { addDedupToPlanGenerator } from './dedupPathPlans'; +import { planGeneratorWithDedup } from './dedupPathPlans'; import type { CriterionResult, GetPlansOptions, @@ -89,7 +89,7 @@ export class TestModel { public getPlans( options?: GetPlansOptions ): Array> { - const planGenerator = addDedupToPlanGenerator( + const planGenerator = planGeneratorWithDedup( options?.planGenerator || TestModel.defaults.planGenerator ); const plans = planGenerator(this.behavior, this.resolveOptions(options)); diff --git a/packages/xstate-test/src/dedupPathPlans.ts b/packages/xstate-test/src/dedupPathPlans.ts index 72a5af69b2..56c6e0b426 100644 --- a/packages/xstate-test/src/dedupPathPlans.ts +++ b/packages/xstate-test/src/dedupPathPlans.ts @@ -6,7 +6,7 @@ import { PlanGenerator } from './types'; * Deduplicates your path plans so that A -> B * is not executed separately to A -> B -> C */ -export const addDedupToPlanGenerator = ( +export const planGeneratorWithDedup = ( planGenerator: PlanGenerator ): PlanGenerator => (behavior, options) => { const pathPlans = planGenerator(behavior, options); @@ -14,61 +14,63 @@ export const addDedupToPlanGenerator = ( /** * Put all plans on the same level so we can dedup them */ - const allPathsWithPlan: { + const allPathsWithPlan: Array<{ path: StatePath; planIndex: number; - serialisedSteps: string[]; - }[] = []; + eventSequence: string[]; + }> = []; pathPlans.forEach((plan, index) => { plan.paths.forEach((path) => { allPathsWithPlan.push({ path, planIndex: index, - serialisedSteps: path.steps.map((step) => + eventSequence: path.steps.map((step) => options.serializeEvent(step.event) ) }); }); }); - /** - * Filter out the paths that are just shorter versions - * of other paths - */ - const filteredPaths = allPathsWithPlan.filter((path) => { - if (path.serialisedSteps.length === 0) return false; + // Sort by path length, descending + allPathsWithPlan.sort((a, z) => z.path.steps.length - a.path.steps.length); - /** - * @example - * { type: 'EVENT_1' }{ type: 'EVENT_2' } - */ - const concatenatedPath = path.serialisedSteps.join(''); + const superpathsWithPlan: typeof allPathsWithPlan = []; - return !allPathsWithPlan.some((pathToCompare) => { - const concatenatedPathToCompare = pathToCompare.serialisedSteps.join(''); - /** - * Filter IN (return false) if it's the same as the current plan, - * because it's not a valid comparison - */ - if (concatenatedPathToCompare === concatenatedPath) { - return false; + /** + * Filter out the paths that are subpaths of superpaths + */ + pathLoop: for (const pathWithPlan of allPathsWithPlan) { + // Check each existing superpath to see if the path is a subpath of it + superpathLoop: for (const superpathWithPlan of superpathsWithPlan) { + for (const i in pathWithPlan.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 ( + pathWithPlan.eventSequence[i] !== superpathWithPlan.eventSequence[i] + ) { + // If the path is different from the superpath, + // continue to the next superpath + continue superpathLoop; + } } - /** - * Filter IN (return false) if the plan to compare against has length 0 - */ - if (pathToCompare.serialisedSteps.length === 0) { - return false; - } + // If we reached here, path is subpath of superpath + // Continue & do not add path to superpaths + continue pathLoop; + } - /** - * We filter OUT (return true) if the segment to compare includes - * our current segment - */ - return concatenatedPathToCompare.includes(concatenatedPath); - }); - }); + // If we reached here, path is not a subpath of any existing superpaths + // So add it to the superpaths + superpathsWithPlan.push(pathWithPlan); + } const newPathPlans = pathPlans .map( @@ -77,7 +79,7 @@ export const addDedupToPlanGenerator = ( * Grab the paths which were originally related * to this planIndex */ - const newPaths = filteredPaths + const newPaths = superpathsWithPlan .filter(({ planIndex }) => planIndex === index) .map(({ path }) => path); return { diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index 7a610861fa..54998d3576 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -1,4 +1,4 @@ -import { configure, createTestModel } from '../src'; +import { createTestModel } from '../src'; import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; From da14d5c17e03f704b25f62418e0621170254783f Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 17 May 2022 09:47:29 +0100 Subject: [PATCH 090/394] Changed testPlans to an overload --- packages/xstate-test/src/TestModel.ts | 20 +++++++++++++++++--- packages/xstate-test/src/types.ts | 6 ------ packages/xstate-test/test/coverage.test.ts | 6 +++--- packages/xstate-test/test/index.test.ts | 2 +- packages/xstate-test/test/plans.test.ts | 2 +- packages/xstate-test/test/testModel.test.ts | 2 +- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 8fb2a4c97f..611e275a6b 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -27,7 +27,6 @@ import type { TestModelCoverage, TestModelOptions, TestPathResult, - TestPlansOptions, TestStepResult } from './types'; import { flatten, formatPathTestResult, simpleStringify } from './utils'; @@ -177,8 +176,23 @@ export class TestModel { return Object.values(adj).map((x) => x.state); } - public async testPlans(options?: TestPlansOptions) { - const plans = options?.plans || this.getPlans(options); + public async testPlans( + plans: StatePlan[], + options?: TraversalOptions + ): Promise; + public async testPlans( + options?: TraversalOptions + ): Promise; + + public async testPlans(...args: any[]) { + const [plans, options]: [ + StatePlan[], + TraversalOptions + ] = + args[0] instanceof Array + ? [args[0], args[1]] + : [this.getPlans(args[0]), args[0]]; + for (const plan of plans) { await this.testPlan(plan, options); } diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 2acad65518..f678c92bb7 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -28,12 +28,6 @@ export type GetPlansOptions = Partial< } >; -export type TestPlansOptions = Partial< - TestModelOptions & { - plans?: Array>; - } ->; - export interface TestMachineConfig< TContext, TEvent extends EventObject, diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 0dc7d6de23..8c48ffdc4f 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -214,7 +214,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await model.testPlans({ plans: model.getSimplePlans() }); + await model.testPlans(model.getSimplePlans()); expect(() => { model.testCoverage(coversAllTransitions()); @@ -300,7 +300,7 @@ describe('coverage', () => { }) ); - await model.testPlans({ plans: model.getShortestPlans() }); + await model.testPlans(model.getShortestPlans()); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); @@ -309,7 +309,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await model.testPlans({ plans: model.getSimplePlans() }); + await model.testPlans(model.getSimplePlans()); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index d213229953..476bb0b3f6 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -139,7 +139,7 @@ describe('events', () => { expect(plans.length).toBe(3); - await testModel.testPlans({ plans }); + await testModel.testPlans(plans); expect(testedEvents).toMatchInlineSnapshot(` Array [ diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index 54998d3576..01ab12f230 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -67,7 +67,7 @@ describe('testModel.testPlans(...)', () => { } }); - await testModel.testPlans({ plans }); + await testModel.testPlans(plans); expect(testModel.getCoverage(coversAllStates())).toMatchInlineSnapshot(` Array [ diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index f939e138b6..0f8011199e 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -73,7 +73,7 @@ describe('custom test models', () => { const plans = model.getShortestPlansTo((state) => state === 1); - await model.testPlans({ plans }); + await model.testPlans(plans); expect(testedStateKeys).toContain('even'); expect(testedStateKeys).toContain('odd'); From 4dc9a4dc98da1b270ae124b5a1232edcaec9fe55 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 17 May 2022 14:13:54 +0100 Subject: [PATCH 091/394] Removed testPlans function --- packages/xstate-test/src/TestModel.ts | 31 --------------------- packages/xstate-test/src/testUtils.ts | 22 +++++++++++++++ packages/xstate-test/test/coverage.test.ts | 15 +++++----- packages/xstate-test/test/events.test.ts | 5 ++-- packages/xstate-test/test/index.test.ts | 9 +++--- packages/xstate-test/test/plans.test.ts | 3 +- packages/xstate-test/test/states.test.ts | 5 ++-- packages/xstate-test/test/sync.test.ts | 12 +++++--- packages/xstate-test/test/testModel.test.ts | 3 +- 9 files changed, 53 insertions(+), 52 deletions(-) create mode 100644 packages/xstate-test/src/testUtils.ts diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index b7ad5660cf..ea672ea5dc 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -176,37 +176,6 @@ export class TestModel { return Object.values(adj).map((x) => x.state); } - public async testPlans( - plans: StatePlan[], - options?: TraversalOptions - ): Promise; - public async testPlans( - options?: TraversalOptions - ): Promise; - - public async testPlans(...args: any[]) { - const [plans, options]: [ - StatePlan[], - TraversalOptions - ] = - args[0] instanceof Array - ? [args[0], args[1]] - : [this.getPlans(args[0]), args[0]]; - - for (const plan of plans) { - await this.testPlan(plan, options); - } - } - - public testPlansSync( - plans: Array>, - options?: Partial> - ) { - for (const plan of plans) { - this.testPlanSync(plan, options); - } - } - public async testPlan( plan: StatePlan, options?: Partial> diff --git a/packages/xstate-test/src/testUtils.ts b/packages/xstate-test/src/testUtils.ts new file mode 100644 index 0000000000..9e23de49f2 --- /dev/null +++ b/packages/xstate-test/src/testUtils.ts @@ -0,0 +1,22 @@ +import { StatePlan } from '@xstate/graph'; +import { TestModel } from './TestModel'; + +const testModel = async (model: TestModel) => { + for (const plan of model.getPlans()) { + await model.testPlan(plan); + } +}; + +const testPlans = async ( + model: TestModel, + plans: StatePlan[] +) => { + for (const plan of plans) { + await model.testPlan(plan); + } +}; + +export const testUtils = { + testPlans, + testModel +}; diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 8c48ffdc4f..73fd099f47 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -1,4 +1,5 @@ import { configure, createTestModel } from '../src'; +import { testUtils } from '../src/testUtils'; import { coversAllStates, coversAllTransitions } from '../src/coverage'; import { createTestMachine } from '../src/machine'; @@ -205,7 +206,7 @@ describe('coverage', () => { }) ); - await model.testPlans(); + await testUtils.testModel(model); expect(() => { model.testCoverage(coversAllTransitions()); @@ -214,7 +215,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await model.testPlans(model.getSimplePlans()); + await testUtils.testPlans(model, model.getSimplePlans()); expect(() => { model.testCoverage(coversAllTransitions()); @@ -237,7 +238,7 @@ describe('coverage', () => { }) ); - await model.testPlans(); + await testUtils.testModel(model); expect(model.getCoverage([coversAllStates(), coversAllTransitions()])) .toMatchInlineSnapshot(` @@ -300,7 +301,7 @@ describe('coverage', () => { }) ); - await model.testPlans(model.getShortestPlans()); + await testUtils.testPlans(model, model.getShortestPlans()); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); @@ -309,7 +310,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await model.testPlans(model.getSimplePlans()); + await testUtils.testPlans(model, model.getSimplePlans()); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); @@ -333,7 +334,7 @@ describe('coverage', () => { }) ); - await model.testPlans(); + await testUtils.testModel(model); expect(() => { model.testCoverage(); @@ -365,7 +366,7 @@ describe('coverage', () => { }) ); - await model.testPlans(); + await testUtils.testModel(model); expect(() => { model.testCoverage(); diff --git a/packages/xstate-test/test/events.test.ts b/packages/xstate-test/test/events.test.ts index a961a385d5..19a1bc0841 100644 --- a/packages/xstate-test/test/events.test.ts +++ b/packages/xstate-test/test/events.test.ts @@ -1,5 +1,6 @@ import { createTestModel } from '../src'; import { createTestMachine } from '../src/machine'; +import { testUtils } from '../src/testUtils'; describe('events', () => { it('should execute events (`exec` property)', async () => { @@ -28,7 +29,7 @@ describe('events', () => { } ); - await testModel.testPlans(); + await testUtils.testModel(testModel); expect(executed).toBe(true); }); @@ -57,7 +58,7 @@ describe('events', () => { } ); - await testModel.testPlans(); + await testUtils.testModel(testModel); expect(executed).toBe(true); }); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 476bb0b3f6..94f892df00 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -2,6 +2,7 @@ import { assign, createMachine } from 'xstate'; import { createTestModel } from '../src'; import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; +import { testUtils } from '../src/testUtils'; import { getDescription } from '../src/utils'; describe('events', () => { @@ -139,7 +140,7 @@ describe('events', () => { expect(plans.length).toBe(3); - await testModel.testPlans(plans); + await testUtils.testPlans(testModel, plans); expect(testedEvents).toMatchInlineSnapshot(` Array [ @@ -447,7 +448,7 @@ describe('state tests', () => { } }); - await model.testPlans(); + await testUtils.testModel(model); }); it('should test wildcard state for non-matching states', async () => { @@ -481,7 +482,7 @@ describe('state tests', () => { } }); - await model.testPlans(); + await testUtils.testModel(model); }); it('should test nested states', async () => { @@ -519,7 +520,7 @@ describe('state tests', () => { } }); - await model.testPlans(); + await testUtils.testModel(model); expect(testedStateValues).toMatchInlineSnapshot(` Array [ "a", diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index 01ab12f230..f502888347 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -1,6 +1,7 @@ import { createTestModel } from '../src'; import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; +import { testUtils } from '../src/testUtils'; const multiPathMachine = createTestMachine({ initial: 'a', @@ -67,7 +68,7 @@ describe('testModel.testPlans(...)', () => { } }); - await testModel.testPlans(plans); + await testUtils.testModel(testModel); expect(testModel.getCoverage(coversAllStates())).toMatchInlineSnapshot(` Array [ diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts index 93da20cfe5..bf2b83af92 100644 --- a/packages/xstate-test/test/states.test.ts +++ b/packages/xstate-test/test/states.test.ts @@ -1,6 +1,7 @@ import { StateValue } from 'xstate'; import { createTestModel } from '../src'; import { createTestMachine } from '../src/machine'; +import { testUtils } from '../src/testUtils'; describe('states', () => { it('should test states by key', async () => { @@ -41,7 +42,7 @@ describe('states', () => { } ); - await testModel.testPlans(); + await testUtils.testModel(testModel); expect(testedStateValues).toMatchInlineSnapshot(` Array [ @@ -106,7 +107,7 @@ describe('states', () => { } ); - await testModel.testPlans(); + await testUtils.testModel(testModel); expect(testedStateValues).toMatchInlineSnapshot(` Array [ diff --git a/packages/xstate-test/test/sync.test.ts b/packages/xstate-test/test/sync.test.ts index 49cb4229be..872a177060 100644 --- a/packages/xstate-test/test/sync.test.ts +++ b/packages/xstate-test/test/sync.test.ts @@ -47,10 +47,12 @@ const syncModel = createTestModel(machine, { } }); -describe('.testPlansSync', () => { +describe('.testPlanSync', () => { it('Should error if it encounters a promise in a state', () => { expect(() => - promiseStateModel.testPlansSync(promiseStateModel.getShortestPlans()) + promiseStateModel + .getPlans() + .forEach((plan) => promiseStateModel.testPlanSync(plan)) ).toThrowError( `The test for 'a' returned a promise - did you mean to use the sync method?` ); @@ -58,7 +60,9 @@ describe('.testPlansSync', () => { it('Should error if it encounters a promise in an event', () => { expect(() => - promiseEventModel.testPlansSync(promiseEventModel.getShortestPlans()) + promiseEventModel + .getPlans() + .forEach((plan) => promiseEventModel.testPlanSync(plan)) ).toThrowError( `The event 'EVENT' returned a promise - did you mean to use the sync method?` ); @@ -66,7 +70,7 @@ describe('.testPlansSync', () => { it('Should succeed if it encounters no promises', () => { expect(() => - syncModel.testPlansSync(syncModel.getShortestPlans()) + syncModel.getPlans().forEach((plan) => syncModel.testPlanSync(plan)) ).not.toThrow(); }); }); diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index 0f8011199e..18138ce77e 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -1,4 +1,5 @@ import { TestModel } from '../src'; +import { testUtils } from '../src/testUtils'; describe('custom test models', () => { it('tests any behavior', async () => { @@ -73,7 +74,7 @@ describe('custom test models', () => { const plans = model.getShortestPlansTo((state) => state === 1); - await model.testPlans(plans); + await testUtils.testPlans(model, plans); expect(testedStateKeys).toContain('even'); expect(testedStateKeys).toContain('odd'); From 1946816af60679201aab6da0221ab36b3847eb11 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 17 May 2022 09:36:53 -0400 Subject: [PATCH 092/394] Update .changeset/great-spies-exist.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- .changeset/great-spies-exist.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/great-spies-exist.md b/.changeset/great-spies-exist.md index c8be46e4f1..30456cbe42 100644 --- a/.changeset/great-spies-exist.md +++ b/.changeset/great-spies-exist.md @@ -1,5 +1,5 @@ --- -'@xstate/graph': major +'@xstate/test': major --- Coverage can now be obtained from a tested test model via `testModel.getCoverage(...)`: From 01cb4718b5bfea671f594406a92461de408c96a7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 17 May 2022 09:47:48 -0400 Subject: [PATCH 093/394] Update packages/xstate-graph/src/graph.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/src/graph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 5e6472938f..704dbff7e7 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -209,7 +209,7 @@ export function traverseShortestPlans( // weight, state, event const weightMap = new Map< SerializedState, - [number, SerializedState | undefined, TEvent | undefined] + [weight: number, state: SerializedState | undefined, event: TEvent | undefined] >(); const stateMap = new Map(); const initialSerializedState = serializeState(behavior.initialState, null); From 0146102abd7ff09a3ada6c7df50993e53fe73fae Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 17 May 2022 09:48:07 -0400 Subject: [PATCH 094/394] Update packages/xstate-graph/src/graph.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/src/graph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 704dbff7e7..9f08966f1d 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -217,7 +217,7 @@ export function traverseShortestPlans( weightMap.set(initialSerializedState, [0, undefined, undefined]); const unvisited = new Set(); - const visited = new Set(); + const visited = new Set(); unvisited.add(initialSerializedState); while (unvisited.size > 0) { From c2f6bf21f15502d6ea4aa5f81dd4222b36a8b70c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 17 May 2022 11:00:47 -0400 Subject: [PATCH 095/394] Update packages/xstate-graph/src/graph.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/src/graph.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 9f08966f1d..7ec20ddece 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -14,9 +14,7 @@ import type { SerializedState, SimpleBehavior, StatePath, - StatePlan -} from '.'; -import type { + StatePlan, StatePlanMap, AdjacencyMap, Steps, From e701c5a0dfe45d5078754f154059b807095cbe9a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 17 May 2022 11:01:23 -0400 Subject: [PATCH 096/394] Update packages/xstate-graph/src/graph.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/src/graph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 7ec20ddece..ae625591ff 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -574,7 +574,7 @@ export function traverseSimplePlans( return Object.values(pathMap); } -export function filterPlans( +function filterPlans( plans: Array>, predicate: (state: TState, plan: StatePlan) => boolean ): Array> { From e4b4b89386bb58731c2760c3cb56833de6343e19 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 17 May 2022 11:01:56 -0400 Subject: [PATCH 097/394] Update packages/xstate-graph/test/graph.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/test/graph.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index e68ebcdd28..068c4527a6 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -587,7 +587,7 @@ it('simple paths for reducers', () => { } return s; }, - initialState: 0 as number + initialState: 0 }, { getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], From 72e090afb1070d0842b4dbb4a1b5d32159059a7d Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 17 May 2022 17:11:42 +0100 Subject: [PATCH 098/394] Fixed tsc --- packages/xstate-test/test/plans.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index f502888347..a995a99f76 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -68,7 +68,7 @@ describe('testModel.testPlans(...)', () => { } }); - await testUtils.testModel(testModel); + await testUtils.testPlans(testModel, plans); expect(testModel.getCoverage(coversAllStates())).toMatchInlineSnapshot(` Array [ From 3f06f9f97ee6e468b786d27ef04b4cdc0a4982c5 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 17 May 2022 13:41:11 -0400 Subject: [PATCH 099/394] Update packages/xstate-graph/test/graph.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/test/graph.test.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 068c4527a6..acb45c3041 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -254,18 +254,16 @@ describe('@xstate/graph', () => { } }); - // explicit type arguments could be removed once davidkpiano/xstate#652 gets resolved const paths = getShortestPlans(machine, { - getEvents: () => - [ - { - type: 'EVENT', - id: 'whatever' - }, - { - type: 'STATE' - } - ] as any[] + getEvents: () => [ + { + type: 'EVENT', + id: 'whatever' + }, + { + type: 'STATE' + } + ] }); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( From 386954484d8cc7058f476ac2253039b5a3bae64b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 18 May 2022 08:18:53 -0400 Subject: [PATCH 100/394] Update packages/xstate-test/test/plans.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-test/test/plans.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index a995a99f76..cca3aa0fde 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -127,7 +127,7 @@ describe('testModel.testPlans(...)', () => { }); }); - describe('When the machine only has more than one path', () => { + describe('When the machine has more than one path', () => { it('Should create plans for each path', () => { const model = createTestModel(multiPathMachine); From f3561f3a0800438419a6903bde2bd1be4a6fc937 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Wed, 18 May 2022 13:48:09 +0100 Subject: [PATCH 101/394] Update packages/xstate-test/test/plans.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-test/test/plans.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index cca3aa0fde..585003ec6e 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -138,7 +138,7 @@ describe('testModel.testPlans(...)', () => { }); describe('simplePathPlans', () => { - it('Should dedup simple path plans too', () => { + it('Should dedup simple path plans', () => { const model = createTestModel(multiPathMachine); const plans = model.getSimplePlans(); From 4b3aba5730184d152f0b3ba6d61b418545122fe9 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Wed, 18 May 2022 13:48:22 +0100 Subject: [PATCH 102/394] Update packages/xstate-test/test/coverage.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-test/test/coverage.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 73fd099f47..1b7484bd21 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -143,7 +143,6 @@ describe('coverage', () => { }).not.toThrow(); }); - // https://github.com/statelyai/xstate/issues/981 it.skip('skips transient states (type: final)', async () => { const machine = createTestMachine({ id: 'menu', From f160a77aac9617d1e74b557c84882c7b8b13ac9d Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Thu, 19 May 2022 14:15:01 +0100 Subject: [PATCH 103/394] Removed plans --- packages/xstate-test/src/TestModel.ts | 120 +++++++++----------- packages/xstate-test/src/dedupPathPlans.ts | 97 ---------------- packages/xstate-test/src/dedupPaths.ts | 75 ++++++++++++ packages/xstate-test/src/index.ts | 6 +- packages/xstate-test/src/pathGenerators.ts | 27 +++++ packages/xstate-test/src/testUtils.ts | 16 +-- packages/xstate-test/src/types.ts | 9 +- packages/xstate-test/src/utils.ts | 13 ++- packages/xstate-test/test/coverage.test.ts | 30 +++-- packages/xstate-test/test/dieHard.test.ts | 98 +++++++--------- packages/xstate-test/test/index.test.ts | 116 +++---------------- packages/xstate-test/test/plans.test.ts | 43 +++---- packages/xstate-test/test/sync.test.ts | 10 +- packages/xstate-test/test/testModel.test.ts | 6 +- 14 files changed, 271 insertions(+), 395 deletions(-) delete mode 100644 packages/xstate-test/src/dedupPathPlans.ts create mode 100644 packages/xstate-test/src/dedupPaths.ts create mode 100644 packages/xstate-test/src/pathGenerators.ts diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index ea672ea5dc..0c96f5aa46 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -5,12 +5,9 @@ import { SerializedState, SimpleBehavior, StatePath, - StatePlan, Step, TraversalOptions, - traverseShortestPlans, - traverseSimplePathsTo, - traverseSimplePlans + traverseSimplePathsTo } from '@xstate/graph'; import { EventObject, SingleOrArray } from 'xstate'; import { @@ -18,28 +15,34 @@ import { coversAllStates, coversAllTransitions } from './coverage'; -import { planGeneratorWithDedup } from './dedupPathPlans'; +import { pathGeneratorWithDedup } from './dedupPaths'; +import { getShortestPaths, getSimplePaths } from './pathGenerators'; import type { CriterionResult, EventExecutor, - GetPlansOptions, - PlanGenerator, + GetPathsOptions, + PathGenerator, StatePredicate, TestModelCoverage, TestModelOptions, TestPathResult, TestStepResult } from './types'; -import { flatten, formatPathTestResult, simpleStringify } from './utils'; +import { + flatten, + formatPathTestResult, + mapPlansToPaths, + simpleStringify +} from './utils'; export interface TestModelDefaults { coverage: Array>; - planGenerator: PlanGenerator; + pathGenerator: PathGenerator; } export const testModelDefaults: TestModelDefaults = { coverage: [coversAllStates(), coversAllTransitions()], - planGenerator: traverseShortestPlans + pathGenerator: getShortestPaths }; /** @@ -85,74 +88,76 @@ export class TestModel { }; } - public getPlans( - options?: GetPlansOptions - ): Array> { - const planGenerator = planGeneratorWithDedup( - options?.planGenerator || TestModel.defaults.planGenerator - ); - const plans = planGenerator(this.behavior, this.resolveOptions(options)); - - return plans; + public getShortestPaths( + options?: Partial> + ): Array> { + return this.getPaths({ ...options, pathGenerator: getShortestPaths }); } - public getShortestPlans( - options?: Partial> - ): Array> { - return this.getPlans({ ...options, planGenerator: traverseShortestPlans }); + public getPaths( + options?: Partial> + ): Array> { + const pathGenerator = pathGeneratorWithDedup( + options?.pathGenerator || TestModel.defaults.pathGenerator + ); + const paths = pathGenerator(this.behavior, this.resolveOptions(options)); + + return paths; } - public getShortestPlansTo( + public getShortestPathsTo( stateValue: StatePredicate - ): Array> { + ): Array> { let minWeight = Infinity; - let shortestPlans: Array> = []; + let shortestPaths: Array> = []; - const plans = this.filterPathsTo(stateValue, this.getShortestPlans()); + const paths = this.filterPathsTo(stateValue, this.getShortestPaths()); - for (const plan of plans) { - const currWeight = plan.paths[0].weight; + for (const path of paths) { + const currWeight = path.weight; if (currWeight < minWeight) { minWeight = currWeight; - shortestPlans = [plan]; + shortestPaths = [path]; } else if (currWeight === minWeight) { - shortestPlans.push(plan); + shortestPaths.push(path); } } - return shortestPlans; + return shortestPaths; } - public getSimplePlans( + public getSimplePaths( options?: Partial> - ): Array> { - return this.getPlans({ + ): Array> { + return this.getPaths({ ...options, - planGenerator: traverseSimplePlans + pathGenerator: getSimplePaths }); } - public getSimplePlansTo( + public getSimplePathsTo( predicate: StatePredicate - ): Array> { - return traverseSimplePathsTo(this.behavior, predicate, this.options); + ): Array> { + return mapPlansToPaths( + traverseSimplePathsTo(this.behavior, predicate, this.options) + ); } private filterPathsTo( statePredicate: StatePredicate, - testPlans: Array> - ): Array> { + testPaths: Array> + ): Array> { const predicate: StatePredicate = (state) => statePredicate(state); - return testPlans.filter((testPlan) => { - return predicate(testPlan.state); + return testPaths.filter((testPath) => { + return predicate(testPath.state); }); } - public getPlanFromEvents( + public getPathFromEvents( events: TEvent[], statePredicate: StatePredicate - ): StatePlan { + ): StatePath { const path = getPathFromEvents(this.behavior, events); if (!statePredicate(path.state)) { @@ -163,12 +168,7 @@ export class TestModel { ); } - const plan: StatePlan = { - state: path.state, - paths: [path] - }; - - return plan; + return path; } public getAllStates(): TState[] { @@ -176,24 +176,6 @@ export class TestModel { return Object.values(adj).map((x) => x.state); } - public async testPlan( - plan: StatePlan, - options?: Partial> - ) { - for (const path of plan.paths) { - await this.testPath(path, options); - } - } - - public testPlanSync( - plan: StatePlan, - options?: Partial> - ) { - for (const path of plan.paths) { - this.testPathSync(path, options); - } - } - public testPathSync( path: StatePath, options?: Partial> diff --git a/packages/xstate-test/src/dedupPathPlans.ts b/packages/xstate-test/src/dedupPathPlans.ts deleted file mode 100644 index 56c6e0b426..0000000000 --- a/packages/xstate-test/src/dedupPathPlans.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { StatePath, StatePlan } from '@xstate/graph'; -import { EventObject } from 'xstate'; -import { PlanGenerator } from './types'; - -/** - * Deduplicates your path plans so that A -> B - * is not executed separately to A -> B -> C - */ -export const planGeneratorWithDedup = ( - planGenerator: PlanGenerator -): PlanGenerator => (behavior, options) => { - const pathPlans = planGenerator(behavior, options); - - /** - * Put all plans on the same level so we can dedup them - */ - const allPathsWithPlan: Array<{ - path: StatePath; - planIndex: number; - eventSequence: string[]; - }> = []; - - pathPlans.forEach((plan, index) => { - plan.paths.forEach((path) => { - allPathsWithPlan.push({ - path, - planIndex: index, - eventSequence: path.steps.map((step) => - options.serializeEvent(step.event) - ) - }); - }); - }); - - // Sort by path length, descending - allPathsWithPlan.sort((a, z) => z.path.steps.length - a.path.steps.length); - - const superpathsWithPlan: typeof allPathsWithPlan = []; - - /** - * Filter out the paths that are subpaths of superpaths - */ - pathLoop: for (const pathWithPlan of allPathsWithPlan) { - // Check each existing superpath to see if the path is a subpath of it - superpathLoop: for (const superpathWithPlan of superpathsWithPlan) { - for (const i in pathWithPlan.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 ( - pathWithPlan.eventSequence[i] !== superpathWithPlan.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 - superpathsWithPlan.push(pathWithPlan); - } - - const newPathPlans = pathPlans - .map( - (plan, index): StatePlan => { - /** - * Grab the paths which were originally related - * to this planIndex - */ - const newPaths = superpathsWithPlan - .filter(({ planIndex }) => planIndex === index) - .map(({ path }) => path); - return { - state: plan.state, - paths: newPaths - }; - } - ) - /** - * Filter out plans which don't have any unique paths - */ - .filter((plan) => plan.paths.length > 0); - - return newPathPlans; -}; 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 801da90655..7f653c2735 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -1,2 +1,4 @@ -export { createTestModel } from './machine'; -export { TestModel, configure } from './TestModel'; +export { createTestModel, createTestMachine } from './machine'; +export { TestModel, configure, TestModelDefaults } from './TestModel'; +export * from './types'; +export * from './pathGenerators'; 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/testUtils.ts b/packages/xstate-test/src/testUtils.ts index 9e23de49f2..972103f7d6 100644 --- a/packages/xstate-test/src/testUtils.ts +++ b/packages/xstate-test/src/testUtils.ts @@ -1,22 +1,22 @@ -import { StatePlan } from '@xstate/graph'; +import { StatePath } from '@xstate/graph'; import { TestModel } from './TestModel'; const testModel = async (model: TestModel) => { - for (const plan of model.getPlans()) { - await model.testPlan(plan); + for (const path of model.getPaths()) { + await model.testPath(path); } }; -const testPlans = async ( +const testPaths = async ( model: TestModel, - plans: StatePlan[] + paths: StatePath[] ) => { - for (const plan of plans) { - await model.testPlan(plan); + for (const path of paths) { + await model.testPath(path); } }; export const testUtils = { - testPlans, + testPaths, testModel }; diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index ff1f8cc087..19015887eb 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -1,5 +1,6 @@ import { SimpleBehavior, + StatePath, StatePlan, Step, TraversalOptions @@ -22,9 +23,9 @@ import { TypegenDisabled } from 'xstate'; -export type GetPlansOptions = Partial< +export type GetPathsOptions = Partial< TraversalOptions & { - planGenerator?: PlanGenerator; + pathGenerator?: PathGenerator; } >; @@ -285,7 +286,7 @@ export type TestTransitionsConfigMap< '*'?: TestTransitionConfig | string; }; -export type PlanGenerator = ( +export type PathGenerator = ( behavior: SimpleBehavior, options: TraversalOptions -) => Array>; +) => Array>; diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts index 54bde370dc..d4cf89a5cc 100644 --- a/packages/xstate-test/src/utils.ts +++ b/packages/xstate-test/src/utils.ts @@ -2,9 +2,10 @@ import { SerializationOptions, SerializedEvent, SerializedState, - StatePath + StatePath, + StatePlan } from '@xstate/graph'; -import { AnyState } from 'xstate'; +import { AnyState, EventObject } from 'xstate'; import { TestMeta, TestPathResult } from './types'; interface TestResultStringOptions extends SerializationOptions { @@ -104,3 +105,11 @@ export function getDescription(state: AnyState): string { 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/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 1b7484bd21..b4a7ef679f 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -30,10 +30,10 @@ describe('coverage', () => { const testModel = createTestModel(machine); - const plans = testModel.getShortestPlans(); + const paths = testModel.getShortestPaths(); - for (const plan of plans) { - await testModel.testPlan(plan); + for (const plan of paths) { + await testModel.testPath(plan); } expect( @@ -84,8 +84,8 @@ describe('coverage', () => { const model = createTestModel(feedbackMachine); - for (const plan of model.getShortestPlans()) { - await model.testPlan(plan); + for (const plan of model.getShortestPaths()) { + await model.testPath(plan); } const coverage = model.getCoverage(coversAllStates()); @@ -121,13 +121,11 @@ describe('coverage', () => { const testModel = createTestModel(TestBug); - const testPlans = testModel.getShortestPlans(); + const testPaths = testModel.getShortestPaths(); const promises: any[] = []; - testPlans.forEach((plan) => { - plan.paths.forEach(() => { - promises.push(testModel.testPlan(plan)); - }); + testPaths.forEach((path) => { + promises.push(testModel.testPath(path)); }); await Promise.all(promises); @@ -177,10 +175,10 @@ describe('coverage', () => { }); const model = createTestModel(machine); - const shortestPlans = model.getShortestPlans(); + const shortestPaths = model.getShortestPaths(); - for (const plan of shortestPlans) { - await model.testPlan(plan); + for (const plan of shortestPaths) { + await model.testPath(plan); } // TODO: determine how to handle missing coverage for transient states, @@ -214,7 +212,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await testUtils.testPlans(model, model.getSimplePlans()); + await testUtils.testPaths(model, model.getSimplePaths()); expect(() => { model.testCoverage(coversAllTransitions()); @@ -300,7 +298,7 @@ describe('coverage', () => { }) ); - await testUtils.testPlans(model, model.getShortestPlans()); + await testUtils.testPaths(model, model.getShortestPaths()); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); @@ -309,7 +307,7 @@ describe('coverage', () => { Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" `); - await testUtils.testPlans(model, model.getSimplePlans()); + await testUtils.testPaths(model, model.getSimplePaths()); expect(() => { model.testCoverage([coversAllStates(), coversAllTransitions()]); diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 04654ce556..515ba6aff2 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -165,17 +165,11 @@ describe('die hard example', () => { const dieHardModel = createDieHardModel(); dieHardModel - .getShortestPlansTo((state) => state.matches('success')) - .forEach((plan) => { - describe(`plan ${getDescription(plan.state)}`, () => { - it('should generate a single path', () => { - expect(plan.paths.length).toEqual(1); - }); - - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, async () => { - await dieHardModel.testPath(path); - }); + .getShortestPathsTo((state) => state.matches('success')) + .forEach((path) => { + describe(`path ${getDescription(path.state)}`, () => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.testPath(path); }); }); }); @@ -184,15 +178,13 @@ describe('die hard example', () => { describe('testing a model (simplePathsTo)', () => { const dieHardModel = createDieHardModel(); dieHardModel - .getSimplePlansTo((state) => state.matches('success')) - .forEach((plan) => { + .getSimplePathsTo((state) => state.matches('success')) + .forEach((path) => { describe(`reaches state ${JSON.stringify( - plan.state.value - )} (${JSON.stringify(plan.state.context)})`, () => { - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, async () => { - await dieHardModel.testPath(path); - }); + path.state.value + )} (${JSON.stringify(path.state.context)})`, () => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.testPath(path); }); }); }); @@ -201,7 +193,7 @@ describe('die hard example', () => { describe('testing a model (getPlanFromEvents)', () => { const dieHardModel = createDieHardModel(); - const plan = dieHardModel.getPlanFromEvents( + const path = dieHardModel.getPathFromEvents( [ { type: 'FILL_5' }, { type: 'POUR_5_TO_3' }, @@ -214,18 +206,16 @@ describe('die hard example', () => { ); describe(`reaches state ${JSON.stringify( - plan.state.value - )} (${JSON.stringify(plan.state.context)})`, () => { - plan.paths.forEach((path) => { - it(`path ${getDescription(path.state)}`, async () => { - await dieHardModel.testPath(path); - }); + path.state.value + )} (${JSON.stringify(path.state.context)})`, () => { + it(`path ${getDescription(path.state)}`, async () => { + await dieHardModel.testPath(path); }); }); it('should throw if the target does not match the last entered state', () => { expect(() => { - dieHardModel.getPlanFromEvents([{ type: 'FILL_5' }], (state) => + dieHardModel.getPathFromEvents([{ type: 'FILL_5' }], (state) => state.matches('success') ); }).toThrow(); @@ -234,19 +224,17 @@ describe('die hard example', () => { describe('.testPath(path)', () => { const dieHardModel = createDieHardModel(); - const plans = dieHardModel.getSimplePlansTo((state) => { + const paths = dieHardModel.getSimplePathsTo((state) => { return state.matches('success') && state.context.three === 0; }); - plans.forEach((plan) => { + paths.forEach((path) => { describe(`reaches state ${JSON.stringify( - plan.state.value - )} (${JSON.stringify(plan.state.context)})`, () => { - plan.paths.forEach((path) => { - describe(`path ${getDescription(path.state)}`, () => { - it(`reaches the target state`, async () => { - await dieHardModel.testPath(path); - }); + path.state.value + )} (${JSON.stringify(path.state.context)})`, () => { + describe(`path ${getDescription(path.state)}`, () => { + it(`reaches the target state`, async () => { + await dieHardModel.testPath(path); }); }); }); @@ -255,16 +243,14 @@ describe('die hard example', () => { it('reports state node coverage', async () => { const dieHardModel = createDieHardModel(); - const plans = dieHardModel.getSimplePlansTo((state) => { + const paths = dieHardModel.getSimplePathsTo((state) => { return state.matches('success') && state.context.three === 0; }); - for (const plan of plans) { - for (const path of plan.paths) { - jugs = new Jugs(); - jugs.version = Math.random(); - await dieHardModel.testPath(path); - } + for (const path of paths) { + jugs = new Jugs(); + jugs.version = Math.random(); + await dieHardModel.testPath(path); } const coverage = dieHardModel.getCoverage(coversAllStates()); @@ -316,17 +302,14 @@ describe('error path trace', () => { }); testModel - .getShortestPlansTo((state) => state.matches('third')) - .forEach((plan) => { - plan.paths.forEach((path) => { - it('should show an error path trace', async () => { - try { - await testModel.testPath(path, undefined); - } catch (err) { - expect(err.message).toEqual( - expect.stringContaining('test error') - ); - expect(err.message).toMatchInlineSnapshot(` + .getShortestPathsTo((state) => state.matches('third')) + .forEach((path) => { + it('should show an error path trace', async () => { + try { + await testModel.testPath(path, undefined); + } catch (err) { + expect(err.message).toEqual(expect.stringContaining('test error')); + expect(err.message).toMatchInlineSnapshot(` "test error Path: State: {\\"value\\":\\"first\\",\\"actions\\":[]} @@ -337,11 +320,10 @@ describe('error path trace', () => { State: {\\"value\\":\\"third\\",\\"actions\\":[]}" `); - return; - } + return; + } - throw new Error('Should have failed'); - }); + throw new Error('Should have failed'); }); }); }); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 94f892df00..ba134a7f75 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -3,7 +3,6 @@ import { createTestModel } from '../src'; import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; import { testUtils } from '../src/testUtils'; -import { getDescription } from '../src/utils'; describe('events', () => { it('should allow for representing many cases', async () => { @@ -66,11 +65,7 @@ describe('events', () => { } }); - const testPlans = testModel.getShortestPlans(); - - for (const plan of testPlans) { - await testModel.testPlan(plan); - } + await testUtils.testModel(testModel); expect(() => testModel.testCoverage(coversAllStates())).not.toThrow(); }); @@ -88,12 +83,8 @@ describe('events', () => { const testModel = createTestModel(testMachine); - const testPlans = testModel.getShortestPlans(); - expect(async () => { - for (const plan of Object.values(testPlans)) { - await testModel.testPlan(plan); - } + await testUtils.testModel(testModel); }).not.toThrow(); }); @@ -136,11 +127,11 @@ describe('events', () => { } }); - const plans = testModel.getShortestPlans(); + const paths = testModel.getShortestPaths(); - expect(plans.length).toBe(3); + expect(paths.length).toBe(3); - await testUtils.testPlans(testModel, plans); + await testUtils.testPaths(testModel, paths); expect(testedEvents).toMatchInlineSnapshot(` Array [ @@ -181,80 +172,13 @@ describe('state limiting', () => { const testModel = createTestModel(machine); - const testPlans = testModel.getShortestPlans({ + const testPaths = testModel.getShortestPaths({ filter: (state) => { return state.context.count < 5; } }); - expect(testPlans).toHaveLength(1); - }); -}); - -describe('plan description', () => { - const machine = createTestMachine({ - id: 'test', - initial: 'atomic', - context: { count: 0 }, - states: { - atomic: { - on: { NEXT: 'compound', DONE: 'final' } - }, - final: { - type: 'final' - }, - compound: { - initial: 'child', - states: { - child: { - on: { - NEXT: 'childWithMeta' - } - }, - childWithMeta: { - meta: { - description: 'child with meta' - } - } - }, - on: { - NEXT: 'parallel' - } - }, - parallel: { - type: 'parallel', - states: { - one: {}, - two: { - meta: { - description: 'two description' - } - } - }, - on: { - NEXT: 'noMetaDescription' - } - }, - noMetaDescription: { - meta: {} - } - } - }); - - const testModel = createTestModel(machine); - const testPlans = testModel.getShortestPlans(); - - it('should give a description for every plan', () => { - const planDescriptions = testPlans.map( - (plan) => `reaches ${getDescription(plan.state)}` - ); - - expect(planDescriptions).toMatchInlineSnapshot(` - Array [ - "reaches state: \\"#test.final\\" ({\\"count\\":0})", - "reaches state: \\"noMetaDescription\\" ({\\"count\\":0})", - ] - `); + expect(testPaths).toHaveLength(1); }); }); @@ -275,7 +199,7 @@ it('prevents infinite recursion based on a provided limit', () => { const model = createTestModel(machine); expect(() => { - model.getShortestPlans({ traversalLimit: 100 }); + model.getShortestPaths({ traversalLimit: 100 }); }).toThrowErrorMatchingInlineSnapshot(`"Traversal limit exceeded"`); }); @@ -307,11 +231,7 @@ it('executes actions', async () => { const model = createTestModel(machine); - const testPlans = model.getShortestPlans(); - - for (const plan of testPlans) { - await model.testPlan(plan); - } + await testUtils.testModel(model); expect(executedActive).toBe(true); expect(executedDone).toBe(true); @@ -342,11 +262,7 @@ describe('test model options', () => { } ); - const plans = model.getShortestPlans(); - - for (const plan of plans) { - await model.testPlan(plan); - } + await testUtils.testModel(model); expect(testedStates).toEqual(['inactive', 'active']); }); @@ -376,11 +292,9 @@ it('tests transitions', async () => { } }); - const plans = model.getShortestPlansTo((state) => state.matches('second')); + const paths = model.getShortestPathsTo((state) => state.matches('second')); - const path = plans[0].paths[0]; - - await model.testPath(path); + await model.testPath(paths[0]); }); // https://github.com/statelyai/xstate/issues/982 @@ -414,11 +328,9 @@ it('Event in event executor should contain payload from case', async () => { } }); - const plans = model.getShortestPlansTo((state) => state.matches('second')); - - const path = plans[0].paths[0]; + const paths = model.getShortestPathsTo((state) => state.matches('second')); - await model.testPath(path, obj); + await model.testPath(paths[0], obj); }); describe('state tests', () => { diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index 585003ec6e..bc7d6cd978 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -27,7 +27,7 @@ const multiPathMachine = createTestMachine({ } }); -describe('testModel.testPlans(...)', () => { +describe('testModel.testPaths(...)', () => { it('custom plan generators can be provided', async () => { const testModel = createTestModel( createTestMachine({ @@ -43,32 +43,27 @@ describe('testModel.testPlans(...)', () => { }) ); - const plans = testModel.getPlans({ - planGenerator: (behavior, options) => { + const paths = testModel.getPaths({ + pathGenerator: (behavior, options) => { const events = options.getEvents?.(behavior.initialState) ?? []; const nextState = behavior.transition(behavior.initialState, events[0]); return [ { state: nextState, - paths: [ + steps: [ { - state: nextState, - steps: [ - { - state: behavior.initialState, - event: events[0] - } - ], - weight: 1 + state: behavior.initialState, + event: events[0] } - ] + ], + weight: 1 } ]; } }); - await testUtils.testPlans(testModel, plans); + await testUtils.testPaths(testModel, paths); expect(testModel.getCoverage(coversAllStates())).toMatchInlineSnapshot(` Array [ @@ -121,29 +116,19 @@ describe('testModel.testPlans(...)', () => { const model = createTestModel(machine); - const plans = model.getPlans(); + const paths = model.getPaths(); - expect(plans).toHaveLength(1); - }); - }); - - describe('When the machine has more than one path', () => { - it('Should create plans for each path', () => { - const model = createTestModel(multiPathMachine); - - const plans = model.getPlans(); - - expect(plans).toHaveLength(2); + expect(paths).toHaveLength(1); }); }); describe('simplePathPlans', () => { - it('Should dedup simple path plans', () => { + it('Should dedup simple path paths', () => { const model = createTestModel(multiPathMachine); - const plans = model.getSimplePlans(); + const paths = model.getSimplePaths(); - expect(plans).toHaveLength(2); + expect(paths).toHaveLength(2); }); }); }); diff --git a/packages/xstate-test/test/sync.test.ts b/packages/xstate-test/test/sync.test.ts index 872a177060..8fddd830b8 100644 --- a/packages/xstate-test/test/sync.test.ts +++ b/packages/xstate-test/test/sync.test.ts @@ -51,8 +51,8 @@ describe('.testPlanSync', () => { it('Should error if it encounters a promise in a state', () => { expect(() => promiseStateModel - .getPlans() - .forEach((plan) => promiseStateModel.testPlanSync(plan)) + .getPaths() + .forEach((path) => promiseStateModel.testPathSync(path)) ).toThrowError( `The test for 'a' returned a promise - did you mean to use the sync method?` ); @@ -61,8 +61,8 @@ describe('.testPlanSync', () => { it('Should error if it encounters a promise in an event', () => { expect(() => promiseEventModel - .getPlans() - .forEach((plan) => promiseEventModel.testPlanSync(plan)) + .getPaths() + .forEach((path) => promiseEventModel.testPathSync(path)) ).toThrowError( `The event 'EVENT' returned a promise - did you mean to use the sync method?` ); @@ -70,7 +70,7 @@ describe('.testPlanSync', () => { it('Should succeed if it encounters no promises', () => { expect(() => - syncModel.getPlans().forEach((plan) => syncModel.testPlanSync(plan)) + syncModel.getPaths().forEach((path) => syncModel.testPathSync(path)) ).not.toThrow(); }); }); diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index 18138ce77e..d277fd8f93 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -24,7 +24,7 @@ describe('custom test models', () => { } ); - const plans = model.getShortestPlansTo((state) => state === 1); + const plans = model.getShortestPathsTo((state) => state === 1); expect(plans.length).toBeGreaterThan(0); }); @@ -72,9 +72,9 @@ describe('custom test models', () => { } ); - const plans = model.getShortestPlansTo((state) => state === 1); + const plans = model.getShortestPathsTo((state) => state === 1); - await testUtils.testPlans(model, plans); + await testUtils.testPaths(model, plans); expect(testedStateKeys).toContain('even'); expect(testedStateKeys).toContain('odd'); From 74997fc203937c7d3d74a9327acae6bdf0f69ecc Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Thu, 19 May 2022 14:16:44 +0100 Subject: [PATCH 104/394] Removed remaining plan references --- packages/xstate-test/src/types.ts | 31 ----------------------- packages/xstate-test/test/dieHard.test.ts | 2 +- packages/xstate-test/test/plans.test.ts | 2 +- packages/xstate-test/test/sync.test.ts | 2 +- 4 files changed, 3 insertions(+), 34 deletions(-) diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 19015887eb..5bd0a60bc1 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -1,7 +1,6 @@ import { SimpleBehavior, StatePath, - StatePlan, Step, TraversalOptions } from '@xstate/graph'; @@ -105,36 +104,6 @@ export interface TestPathResult { state: TestStateResult; } -/** - * A collection of `paths` used to verify that the SUT reaches - * the target `state`. - */ -export interface TestPlan { - /** - * The target state. - */ - state: TState; - /** - * 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). * diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 515ba6aff2..d5a57a0d63 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -190,7 +190,7 @@ describe('die hard example', () => { }); }); - describe('testing a model (getPlanFromEvents)', () => { + describe('testing a model (getPathFromEvents)', () => { const dieHardModel = createDieHardModel(); const path = dieHardModel.getPathFromEvents( diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index bc7d6cd978..69816ee2c4 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -122,7 +122,7 @@ describe('testModel.testPaths(...)', () => { }); }); - describe('simplePathPlans', () => { + describe('getSimplePaths', () => { it('Should dedup simple path paths', () => { const model = createTestModel(multiPathMachine); diff --git a/packages/xstate-test/test/sync.test.ts b/packages/xstate-test/test/sync.test.ts index 8fddd830b8..b968291fcf 100644 --- a/packages/xstate-test/test/sync.test.ts +++ b/packages/xstate-test/test/sync.test.ts @@ -47,7 +47,7 @@ const syncModel = createTestModel(machine, { } }); -describe('.testPlanSync', () => { +describe('.testPathSync', () => { it('Should error if it encounters a promise in a state', () => { expect(() => promiseStateModel From 7d80d13e267eca3f7fb70be712c6007f84d2635d Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Thu, 19 May 2022 15:38:31 +0100 Subject: [PATCH 105/394] Update packages/xstate-test/test/coverage.test.ts Co-authored-by: David Khourshid --- packages/xstate-test/test/coverage.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index b4a7ef679f..8cde793c01 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -84,8 +84,8 @@ describe('coverage', () => { const model = createTestModel(feedbackMachine); - for (const plan of model.getShortestPaths()) { - await model.testPath(plan); + for (const path of model.getShortestPaths()) { + await model.testPath(path); } const coverage = model.getCoverage(coversAllStates()); From 0c99f1efc1c9e8a118cc2d8ee9758172592793e1 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 11:19:11 +0100 Subject: [PATCH 106/394] Update packages/xstate-graph/src/graph.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/src/graph.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index ae625591ff..89e7889dd7 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -472,12 +472,8 @@ function resolveTraversalOptions( traversalOptions?: Partial>, defaultOptions?: TraversalOptions ): Required> { - const serializeState = - traversalOptions?.serializeState ?? - defaultOptions?.serializeState ?? - ((state) => JSON.stringify(state) as any); return { - serializeState, + serializeState: (state) => JSON.stringify(state) as any, serializeEvent: serializeEvent as any, // TODO fix types filter: () => true, visitCondition: (state, event, vctx) => { From b906278510a2690c8ac1e4abf88479aa1dc78749 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 11:20:27 +0100 Subject: [PATCH 107/394] Changed comment --- packages/xstate-test/src/TestModel.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 0c96f5aa46..fdf1c7c6a0 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -50,8 +50,7 @@ export const testModelDefaults: TestModelDefaults = { * 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. - * + * verify that states in the model are reachable in the SUT. */ export class TestModel { private _coverage: TestModelCoverage = { From c6fa9231b21ca2f64670c93d85c106e910b4f346 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 11:21:36 +0100 Subject: [PATCH 108/394] Changed stateValue to statePredicate --- packages/xstate-test/src/TestModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index fdf1c7c6a0..ad42e8f5c9 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -105,12 +105,12 @@ export class TestModel { } public getShortestPathsTo( - stateValue: StatePredicate + statePredicate: StatePredicate ): Array> { let minWeight = Infinity; let shortestPaths: Array> = []; - const paths = this.filterPathsTo(stateValue, this.getShortestPaths()); + const paths = this.filterPathsTo(statePredicate, this.getShortestPaths()); for (const path of paths) { const currWeight = path.weight; From 3bfa1a53cfb8dab27330f34e5fd92d0ad22e47b9 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 11:32:02 +0100 Subject: [PATCH 109/394] Fixed plan variables still left over --- packages/xstate-test/src/TestModel.ts | 4 ++-- packages/xstate-test/test/coverage.test.ts | 8 ++++---- packages/xstate-test/test/plans.test.ts | 2 +- packages/xstate-test/test/testModel.test.ts | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index ad42e8f5c9..7a1695b9ba 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -49,7 +49,7 @@ export const testModelDefaults: TestModelDefaults = { * 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 + * 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 { @@ -453,7 +453,7 @@ export class TestModel { } /** - * Specifies default configuration for `TestModel` instances for coverage and plan generation options + * Specifies default configuration for `TestModel` instances for coverage and path generation options * * @param testModelConfiguration The partial configuration for all subsequent `TestModel` instances */ diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts index 8cde793c01..efb69cbda0 100644 --- a/packages/xstate-test/test/coverage.test.ts +++ b/packages/xstate-test/test/coverage.test.ts @@ -32,8 +32,8 @@ describe('coverage', () => { const paths = testModel.getShortestPaths(); - for (const plan of paths) { - await testModel.testPath(plan); + for (const path of paths) { + await testModel.testPath(path); } expect( @@ -177,8 +177,8 @@ describe('coverage', () => { const model = createTestModel(machine); const shortestPaths = model.getShortestPaths(); - for (const plan of shortestPaths) { - await model.testPath(plan); + for (const path of shortestPaths) { + await model.testPath(path); } // TODO: determine how to handle missing coverage for transient states, diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index 69816ee2c4..fe8d18e847 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -28,7 +28,7 @@ const multiPathMachine = createTestMachine({ }); describe('testModel.testPaths(...)', () => { - it('custom plan generators can be provided', async () => { + it('custom path generators can be provided', async () => { const testModel = createTestModel( createTestMachine({ initial: 'a', diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index d277fd8f93..c84cce790a 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -24,9 +24,9 @@ describe('custom test models', () => { } ); - const plans = model.getShortestPathsTo((state) => state === 1); + const paths = model.getShortestPathsTo((state) => state === 1); - expect(plans.length).toBeGreaterThan(0); + expect(paths.length).toBeGreaterThan(0); }); it('tests states for any behavior', async () => { @@ -72,9 +72,9 @@ describe('custom test models', () => { } ); - const plans = model.getShortestPathsTo((state) => state === 1); + const paths = model.getShortestPathsTo((state) => state === 1); - await testUtils.testPaths(model, plans); + await testUtils.testPaths(model, paths); expect(testedStateKeys).toContain('even'); expect(testedStateKeys).toContain('odd'); From 0ea67ab6855f6f1abf62b0089c2977bb7691d12c Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 11:39:04 +0100 Subject: [PATCH 110/394] Revert "Update packages/xstate-graph/src/graph.ts" This reverts commit 0c99f1efc1c9e8a118cc2d8ee9758172592793e1. --- packages/xstate-graph/src/graph.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 89e7889dd7..ae625591ff 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -472,8 +472,12 @@ function resolveTraversalOptions( traversalOptions?: Partial>, defaultOptions?: TraversalOptions ): Required> { + const serializeState = + traversalOptions?.serializeState ?? + defaultOptions?.serializeState ?? + ((state) => JSON.stringify(state) as any); return { - serializeState: (state) => JSON.stringify(state) as any, + serializeState, serializeEvent: serializeEvent as any, // TODO fix types filter: () => true, visitCondition: (state, event, vctx) => { From b6a931dda2ba92115b61c79fbc099906223d7777 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 11:43:23 +0100 Subject: [PATCH 111/394] Removed testCoverage and getCoverage --- packages/xstate-test/src/TestModel.ts | 103 +----- packages/xstate-test/src/coverage.ts | 68 ---- packages/xstate-test/src/types.ts | 37 -- packages/xstate-test/test/coverage.test.ts | 386 --------------------- packages/xstate-test/test/dieHard.test.ts | 38 -- packages/xstate-test/test/index.test.ts | 3 - packages/xstate-test/test/plans.test.ts | 30 -- 7 files changed, 2 insertions(+), 663 deletions(-) delete mode 100644 packages/xstate-test/src/coverage.ts delete mode 100644 packages/xstate-test/test/coverage.test.ts diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 7a1695b9ba..63608791ad 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -9,39 +9,29 @@ import { TraversalOptions, traverseSimplePathsTo } from '@xstate/graph'; -import { EventObject, SingleOrArray } from 'xstate'; -import { - CoverageFunction, - coversAllStates, - coversAllTransitions -} from './coverage'; +import { EventObject } from 'xstate'; import { pathGeneratorWithDedup } from './dedupPaths'; import { getShortestPaths, getSimplePaths } from './pathGenerators'; import type { - CriterionResult, EventExecutor, GetPathsOptions, PathGenerator, StatePredicate, - TestModelCoverage, TestModelOptions, TestPathResult, TestStepResult } from './types'; import { - flatten, formatPathTestResult, mapPlansToPaths, simpleStringify } from './utils'; export interface TestModelDefaults { - coverage: Array>; pathGenerator: PathGenerator; } export const testModelDefaults: TestModelDefaults = { - coverage: [coversAllStates(), coversAllTransitions()], pathGenerator: getShortestPaths }; @@ -53,10 +43,6 @@ export const testModelDefaults: TestModelDefaults = { * verify that states in the model are reachable in the SUT. */ export class TestModel { - private _coverage: TestModelCoverage = { - states: {}, - transitions: {} - }; public options: TestModelOptions; public defaultTraversalOptions?: TraversalOptions; public getDefaultOptions(): TestModelOptions { @@ -315,8 +301,6 @@ export class TestModel { resolvedOptions: TestModelOptions ) { resolvedOptions.execute(state); - - this.addStateCoverage(state); } public testStateSync( @@ -337,21 +321,6 @@ export class TestModel { this.afterTestState(state, resolvedOptions); } - private addStateCoverage(state: TState) { - const stateSerial = this.options.serializeState(state, null as any); // TODO: fix - - const existingCoverage = this._coverage.states[stateSerial]; - - if (existingCoverage) { - existingCoverage.count++; - } else { - this._coverage.states[stateSerial] = { - state, - count: 1 - }; - } - } - private getEventExec(step: Step) { const eventConfig = this.options.events?.[ (step.event as any).type as TEvent['type'] @@ -366,8 +335,6 @@ export class TestModel { public async testTransition(step: Step): Promise { const eventExec = this.getEventExec(step); await (eventExec as EventExecutor)?.(step); - - this.addTransitionCoverage(step); } public testTransitionSync(step: Step): void { @@ -377,26 +344,6 @@ export class TestModel { (eventExec as EventExecutor)?.(step), `The event '${step.event.type}' returned a promise - did you mean to use the sync method?` ); - - this.addTransitionCoverage(step); - } - - private addTransitionCoverage(step: Step) { - const transitionSerial = `${this.options.serializeState( - step.state, - null as any - )} | ${this.options.serializeEvent(step.event)}`; - - const existingCoverage = this._coverage.transitions[transitionSerial]; - - if (existingCoverage) { - existingCoverage.count++; - } else { - this._coverage.transitions[transitionSerial] = { - step, - count: 1 - }; - } } public resolveOptions( @@ -404,56 +351,10 @@ export class TestModel { ): TestModelOptions { return { ...this.defaultTraversalOptions, ...this.options, ...options }; } - - public getCoverage( - criteriaFn: SingleOrArray> = TestModel - .defaults.coverage - ): Array> { - const criteriaFns = criteriaFn - ? Array.isArray(criteriaFn) - ? criteriaFn - : [criteriaFn] - : []; - const criteriaResult = flatten(criteriaFns.map((fn) => fn(this))); - - return criteriaResult.map((criterion) => { - return { - criterion, - status: criterion.skip - ? 'skipped' - : criterion.predicate(this._coverage) - ? 'covered' - : 'uncovered' - }; - }); - } - - // TODO: consider options - public testCoverage( - criteriaFn: SingleOrArray> = TestModel - .defaults.coverage - ): void { - const criteriaFns = Array.isArray(criteriaFn) ? criteriaFn : [criteriaFn]; - const criteriaResult = flatten( - criteriaFns.map((fn) => this.getCoverage(fn)) - ); - - const unmetCriteria = criteriaResult.filter( - (c) => c.status === 'uncovered' - ); - - if (unmetCriteria.length) { - const criteriaMessage = `Coverage criteria not met:\n${unmetCriteria - .map((c) => '\t' + c.criterion.description) - .join('\n')}`; - - throw new Error(criteriaMessage); - } - } } /** - * Specifies default configuration for `TestModel` instances for coverage and path generation options + * Specifies default configuration for `TestModel` instances for path generation options * * @param testModelConfiguration The partial configuration for all subsequent `TestModel` instances */ diff --git a/packages/xstate-test/src/coverage.ts b/packages/xstate-test/src/coverage.ts deleted file mode 100644 index c4a6c4781f..0000000000 --- a/packages/xstate-test/src/coverage.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { AnyStateNode } from '@xstate/graph'; -import type { AnyState, EventObject } from 'xstate'; -import { getAllStateNodes } from 'xstate/lib/stateUtils'; -import { flatten } from './utils'; -import { TestModel } from './TestModel'; -import { Criterion } from './types'; - -interface StateValueCoverageOptions { - filter?: (stateNode: AnyStateNode) => boolean; -} - -export type CoverageFunction = ( - testModel: TestModel -) => Array>; - -export function coversAllStates< - TState extends AnyState, - TEvent extends EventObject ->(options?: StateValueCoverageOptions): CoverageFunction { - const resolvedOptions: Required = { - filter: () => true, - ...options - }; - - return (testModel) => { - const allStateNodes = getAllStateNodes( - (testModel.behavior as unknown) as AnyStateNode - ); - - return allStateNodes.map((stateNode) => { - const skip = !resolvedOptions.filter(stateNode); - - return { - predicate: (coverage) => - Object.values(coverage.states).some(({ state }) => - state.configuration.includes(stateNode) - ), - description: `Visits ${JSON.stringify(stateNode.id)}`, - skip - }; - }); - }; -} - -export function coversAllTransitions< - TState extends AnyState, - TEvent extends EventObject ->(): CoverageFunction { - return (testModel) => { - const allStateNodes = getAllStateNodes( - (testModel.behavior as unknown) as AnyStateNode - ); - const allTransitions = flatten(allStateNodes.map((sn) => sn.transitions)); - - return allTransitions.map((t) => { - return { - predicate: (coverage) => - Object.values(coverage.transitions).some((transitionCoverage) => { - return ( - transitionCoverage.step.state.configuration.includes(t.source) && - t.eventType === transitionCoverage.step.event.type - ); - }), - description: `Transitions to state "${t.source.key}" on event "${t.eventType}"` - }; - }); - }; -} diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 5bd0a60bc1..7337fd78cd 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -14,7 +14,6 @@ import { MachineSchema, ServiceMap, State, - StateNode, StateNodeConfig, StateSchema, TransitionConfig, @@ -193,42 +192,6 @@ export interface TestModelOptions }; } -export interface TestStateCoverage { - state: TState; - /** - * Number of times state was visited - */ - count: number; -} - -export interface TestTransitionCoverage { - step: Step; - count: number; -} - -export interface TestModelCoverage { - states: Record>; - transitions: Record>; -} - -export interface CoverageOptions { - filter?: (stateNode: StateNode) => boolean; -} - -export interface Criterion { - predicate: (coverage: TestModelCoverage) => boolean; - description: string; - skip?: boolean; -} - -export interface CriterionResult { - criterion: Criterion; - /** - * Whether the criterion was covered or not - */ - status: 'uncovered' | 'covered' | 'skipped'; -} - export interface TestTransitionConfig< TContext, TEvent extends EventObject, diff --git a/packages/xstate-test/test/coverage.test.ts b/packages/xstate-test/test/coverage.test.ts deleted file mode 100644 index efb69cbda0..0000000000 --- a/packages/xstate-test/test/coverage.test.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { configure, createTestModel } from '../src'; -import { testUtils } from '../src/testUtils'; -import { coversAllStates, coversAllTransitions } from '../src/coverage'; -import { createTestMachine } from '../src/machine'; - -describe('coverage', () => { - it('reports missing state node coverage', async () => { - const machine = createTestMachine({ - id: 'test', - initial: 'first', - states: { - first: { - on: { NEXT: 'third' } - }, - secondMissing: {}, - third: { - initial: 'one', - states: { - one: { - on: { - NEXT: 'two' - } - }, - two: {}, - threeMissing: {} - } - } - } - }); - - const testModel = createTestModel(machine); - - const paths = testModel.getShortestPaths(); - - for (const path of paths) { - await testModel.testPath(path); - } - - expect( - testModel - .getCoverage(coversAllStates()) - .filter((c) => c.status !== 'covered') - .map((c) => c.criterion.description) - ).toMatchInlineSnapshot(` - Array [ - "Visits \\"test.secondMissing\\"", - "Visits \\"test.third.threeMissing\\"", - ] - `); - - expect(() => testModel.testCoverage(coversAllStates())) - .toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Visits \\"test.secondMissing\\" - Visits \\"test.third.threeMissing\\"" - `); - }); - - // https://github.com/statelyai/xstate/issues/729 - it('reports full coverage when all states are covered', async () => { - const feedbackMachine = createTestMachine({ - id: 'feedback', - initial: 'logon', - states: { - logon: { - initial: 'empty', - states: { - empty: { - on: { - ENTER_LOGON: 'filled' - } - }, - filled: { type: 'final' } - }, - on: { - LOGON_SUBMIT: 'ordermenu' - } - }, - ordermenu: { - type: 'final' - } - } - }); - - const model = createTestModel(feedbackMachine); - - for (const path of model.getShortestPaths()) { - await model.testPath(path); - } - - const coverage = model.getCoverage(coversAllStates()); - - expect(coverage).toHaveLength(5); - - expect(coverage.filter((c) => c.status !== 'covered')).toHaveLength(0); - - expect(() => model.testCoverage(coversAllStates())).not.toThrow(); - }); - - it('skips filtered states (filter option)', async () => { - const TestBug = createTestMachine({ - id: 'testbug', - initial: 'idle', - context: { - retries: 0 - }, - states: { - idle: { - on: { - START: 'passthrough' - } - }, - passthrough: { - always: 'end' - }, - end: { - type: 'final' - } - } - }); - - const testModel = createTestModel(TestBug); - - const testPaths = testModel.getShortestPaths(); - - const promises: any[] = []; - testPaths.forEach((path) => { - promises.push(testModel.testPath(path)); - }); - - await Promise.all(promises); - - expect(() => { - testModel.testCoverage( - coversAllStates({ - filter: (stateNode) => { - return stateNode.key !== 'passthrough'; - } - }) - ); - }).not.toThrow(); - }); - - it.skip('skips transient states (type: final)', async () => { - const machine = createTestMachine({ - id: 'menu', - initial: 'initial', - states: { - initial: { - initial: 'inner1', - - states: { - inner1: { - on: { - INNER2: 'inner2' - } - }, - - inner2: { - on: { - DONE: 'done' - } - }, - - done: { - type: 'final' - } - }, - - onDone: 'later' - }, - - later: {} - } - }); - - const model = createTestModel(machine); - const shortestPaths = model.getShortestPaths(); - - for (const path of shortestPaths) { - await model.testPath(path); - } - - // TODO: determine how to handle missing coverage for transient states, - // which arguably should not be counted towards coverage, as the app is never in - // a transient state for any length of time - model.testCoverage(coversAllStates()); - }); - - it('tests transition coverage', async () => { - const model = createTestModel( - createTestMachine({ - initial: 'a', - states: { - a: { - on: { - EVENT_ONE: 'b', - EVENT_TWO: 'b' - } - }, - b: {} - } - }) - ); - - await testUtils.testModel(model); - - expect(() => { - model.testCoverage(coversAllTransitions()); - }).toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" - `); - - await testUtils.testPaths(model, model.getSimplePaths()); - - expect(() => { - model.testCoverage(coversAllTransitions()); - }).not.toThrow(); - }); - - it('reports multiple kinds of coverage', async () => { - const model = createTestModel( - createTestMachine({ - initial: 'a', - states: { - a: { - on: { - EVENT_ONE: 'b', - EVENT_TWO: 'b' - } - }, - b: {} - } - }) - ); - - await testUtils.testModel(model); - - expect(model.getCoverage([coversAllStates(), coversAllTransitions()])) - .toMatchInlineSnapshot(` - Array [ - Object { - "criterion": Object { - "description": "Visits \\"(machine)\\"", - "predicate": [Function], - "skip": false, - }, - "status": "covered", - }, - Object { - "criterion": Object { - "description": "Visits \\"(machine).a\\"", - "predicate": [Function], - "skip": false, - }, - "status": "covered", - }, - Object { - "criterion": Object { - "description": "Visits \\"(machine).b\\"", - "predicate": [Function], - "skip": false, - }, - "status": "covered", - }, - Object { - "criterion": Object { - "description": "Transitions to state \\"a\\" on event \\"EVENT_ONE\\"", - "predicate": [Function], - }, - "status": "covered", - }, - Object { - "criterion": Object { - "description": "Transitions to state \\"a\\" on event \\"EVENT_TWO\\"", - "predicate": [Function], - }, - "status": "uncovered", - }, - ] - `); - }); - - it('tests multiple kinds of coverage', async () => { - const model = createTestModel( - createTestMachine({ - initial: 'a', - states: { - a: { - on: { - EVENT_ONE: 'b', - EVENT_TWO: 'b' - } - }, - b: {} - } - }) - ); - - await testUtils.testPaths(model, model.getShortestPaths()); - - expect(() => { - model.testCoverage([coversAllStates(), coversAllTransitions()]); - }).toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" - `); - - await testUtils.testPaths(model, model.getSimplePaths()); - - expect(() => { - model.testCoverage([coversAllStates(), coversAllTransitions()]); - }).not.toThrow(); - }); - - it('tests states and transitions coverage by default', async () => { - const model = createTestModel( - createTestMachine({ - initial: 'a', - states: { - a: { - on: { - EVENT_ONE: 'b', - EVENT_TWO: 'b' - } - }, - b: {}, - c: {} - } - }) - ); - - await testUtils.testModel(model); - - expect(() => { - model.testCoverage(); - }).toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Visits \\"(machine).c\\" - Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" - `); - }); - - it('configuration should be globally configurable', async () => { - configure({ - coverage: [coversAllStates()] - }); - - const model = createTestModel( - createTestMachine({ - initial: 'a', - states: { - a: { - on: { - EVENT_ONE: 'b', - EVENT_TWO: 'b' - } - }, - b: {}, - c: {} - } - }) - ); - - await testUtils.testModel(model); - - expect(() => { - model.testCoverage(); - }).toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Visits \\"(machine).c\\"" - `); - - // Reset defaults - configure(); - - expect(() => { - model.testCoverage(); - }).toThrowErrorMatchingInlineSnapshot(` - "Coverage criteria not met: - Visits \\"(machine).c\\" - Transitions to state \\"a\\" on event \\"EVENT_TWO\\"" - `); - }); -}); diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index d5a57a0d63..47fcaeaf93 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -1,6 +1,5 @@ import { assign, createMachine } from 'xstate'; import { createTestModel } from '../src'; -import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; import { getDescription } from '../src/utils'; @@ -240,43 +239,6 @@ describe('die hard example', () => { }); }); }); - - it('reports state node coverage', async () => { - const dieHardModel = createDieHardModel(); - const paths = dieHardModel.getSimplePathsTo((state) => { - return state.matches('success') && state.context.three === 0; - }); - - for (const path of paths) { - jugs = new Jugs(); - jugs.version = Math.random(); - await dieHardModel.testPath(path); - } - - const coverage = dieHardModel.getCoverage(coversAllStates()); - - expect(coverage.every((c) => c.status === 'covered')).toEqual(true); - - expect(coverage.map((c) => [c.criterion.description, c.status])) - .toMatchInlineSnapshot(` - Array [ - Array [ - "Visits \\"dieHard\\"", - "covered", - ], - Array [ - "Visits \\"dieHard.pending\\"", - "covered", - ], - Array [ - "Visits \\"dieHard.success\\"", - "covered", - ], - ] - `); - - expect(() => dieHardModel.testCoverage(coversAllStates())).not.toThrow(); - }); }); describe('error path trace', () => { describe('should return trace for failed state', () => { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index ba134a7f75..7195da7f53 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,6 +1,5 @@ import { assign, createMachine } from 'xstate'; import { createTestModel } from '../src'; -import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; import { testUtils } from '../src/testUtils'; @@ -66,8 +65,6 @@ describe('events', () => { }); await testUtils.testModel(testModel); - - expect(() => testModel.testCoverage(coversAllStates())).not.toThrow(); }); it('should not throw an error for unimplemented events', () => { diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/plans.test.ts index fe8d18e847..2cd1b56227 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/plans.test.ts @@ -1,5 +1,4 @@ import { createTestModel } from '../src'; -import { coversAllStates } from '../src/coverage'; import { createTestMachine } from '../src/machine'; import { testUtils } from '../src/testUtils'; @@ -64,35 +63,6 @@ describe('testModel.testPaths(...)', () => { }); await testUtils.testPaths(testModel, paths); - - expect(testModel.getCoverage(coversAllStates())).toMatchInlineSnapshot(` - Array [ - Object { - "criterion": Object { - "description": "Visits \\"(machine)\\"", - "predicate": [Function], - "skip": false, - }, - "status": "covered", - }, - Object { - "criterion": Object { - "description": "Visits \\"(machine).a\\"", - "predicate": [Function], - "skip": false, - }, - "status": "covered", - }, - Object { - "criterion": Object { - "description": "Visits \\"(machine).b\\"", - "predicate": [Function], - "skip": false, - }, - "status": "covered", - }, - ] - `); }); describe('When the machine only has one path', () => { From 7a62cd4e729b548cd6be48d41fbde203c936bf72 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 20 May 2022 07:22:57 -0400 Subject: [PATCH 112/394] Remove actions from serialized state --- packages/xstate-graph/src/graph.ts | 10 +- .../test/__snapshots__/graph.test.ts.snap | 233 ------------------ packages/xstate-graph/test/graph.test.ts | 1 - packages/xstate-test/test/dieHard.test.ts | 16 +- 4 files changed, 15 insertions(+), 245 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index ae625591ff..31cb0081f3 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -72,8 +72,8 @@ export function getChildren(stateNode: AnyStateNode): AnyStateNode[] { } export function serializeMachineState(state: AnyState): SerializedState { - const { value, context, actions } = state; - return JSON.stringify({ value, context, actions }) as SerializedState; + const { value, context } = state; + return JSON.stringify({ value, context }) as SerializedState; } export function serializeEvent( @@ -207,7 +207,11 @@ export function traverseShortestPlans( // weight, state, event const weightMap = new Map< SerializedState, - [weight: number, state: SerializedState | undefined, event: TEvent | undefined] + [ + weight: number, + state: SerializedState | undefined, + event: TEvent | undefined + ] >(); const stateMap = new Map(); const initialSerializedState = serializeState(behavior.initialState, null); diff --git a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap index 3d0b4dfc6e..20a6043832 100644 --- a/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap +++ b/packages/xstate-graph/test/__snapshots__/graph.test.ts.snap @@ -96,15 +96,6 @@ Array [ }, ], }, - Object { - "state": "green", - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - ], - }, Object { "state": Object { "red": "walk", @@ -186,135 +177,11 @@ Array [ }, ], }, - Object { - "state": "yellow", - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "wait", - }, - }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "stop", - }, - }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "wait", - }, - }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "POWER_OUTAGE", - "state": Object { - "red": "walk", - }, - }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "POWER_OUTAGE", - "state": "yellow", - }, - ], - }, - Object { - "state": Object { - "red": "flashing", - }, - "steps": Array [ - Object { - "eventType": "POWER_OUTAGE", - "state": "green", - }, - ], - }, Object { "state": Object { "red": "flashing", }, "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, Object { "eventType": "TIMER", "state": "green", @@ -348,10 +215,6 @@ Array [ "red": "flashing", }, "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, Object { "eventType": "TIMER", "state": "green", @@ -379,10 +242,6 @@ Array [ "red": "flashing", }, "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, Object { "eventType": "TIMER", "state": "green", @@ -404,10 +263,6 @@ Array [ "red": "flashing", }, "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, Object { "eventType": "TIMER", "state": "green", @@ -423,25 +278,12 @@ Array [ "red": "flashing", }, "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, Object { "eventType": "POWER_OUTAGE", "state": "green", }, ], }, - Object { - "state": "green", - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - ], - }, Object { "state": Object { "red": "walk", @@ -457,55 +299,11 @@ Array [ }, ], }, - Object { - "state": Object { - "red": "walk", - }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - ], - }, - Object { - "state": Object { - "red": "wait", - }, - "steps": Array [ - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - ], - }, Object { "state": Object { "red": "wait", }, "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, Object { "eventType": "TIMER", "state": "green", @@ -549,37 +347,6 @@ Array [ }, ], }, - Object { - "state": Object { - "red": "stop", - }, - "steps": Array [ - Object { - "eventType": "PUSH_BUTTON", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "green", - }, - Object { - "eventType": "TIMER", - "state": "yellow", - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "walk", - }, - }, - Object { - "eventType": "PED_COUNTDOWN", - "state": Object { - "red": "wait", - }, - }, - ], - }, ] `; diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index acb45c3041..4330aecd77 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -283,7 +283,6 @@ describe('@xstate/graph', () => { Object { "red": "flashing", }, - "green", Object { "red": "walk", }, diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 47fcaeaf93..59c54fa53d 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -272,16 +272,16 @@ describe('error path trace', () => { } catch (err) { expect(err.message).toEqual(expect.stringContaining('test error')); expect(err.message).toMatchInlineSnapshot(` - "test error - Path: - State: {\\"value\\":\\"first\\",\\"actions\\":[]} - Event: {\\"type\\":\\"NEXT\\"} + "test error + Path: + State: {\\"value\\":\\"first\\"} + Event: {\\"type\\":\\"NEXT\\"} - State: {\\"value\\":\\"second\\",\\"actions\\":[]} - Event: {\\"type\\":\\"NEXT\\"} + State: {\\"value\\":\\"second\\"} + Event: {\\"type\\":\\"NEXT\\"} - State: {\\"value\\":\\"third\\",\\"actions\\":[]}" - `); + State: {\\"value\\":\\"third\\"}" + `); return; } From ebe94f9a38e9f1a9227044b95ed8ca848e20f81c Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 13:41:06 +0100 Subject: [PATCH 113/394] Re-added path.test and fixed path descriptions --- packages/xstate-test/src/TestModel.ts | 64 +++++++++++++++---- packages/xstate-test/src/testUtils.ts | 11 ++-- packages/xstate-test/src/types.ts | 16 ++--- packages/xstate-test/src/utils.ts | 4 +- packages/xstate-test/test/index.test.ts | 4 +- .../test/{plans.test.ts => paths.test.ts} | 15 ++++- packages/xstate-test/test/testModel.test.ts | 2 +- 7 files changed, 77 insertions(+), 39 deletions(-) rename packages/xstate-test/test/{plans.test.ts => paths.test.ts} (81%) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 63608791ad..07d398b650 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -3,6 +3,7 @@ import { performDepthFirstTraversal, SerializedEvent, SerializedState, + serializeState, SimpleBehavior, StatePath, Step, @@ -10,6 +11,7 @@ import { traverseSimplePathsTo } from '@xstate/graph'; import { EventObject } from 'xstate'; +import { isStateLike } from 'xstate/src/utils'; import { pathGeneratorWithDedup } from './dedupPaths'; import { getShortestPaths, getSimplePaths } from './pathGenerators'; import type { @@ -18,11 +20,13 @@ import type { PathGenerator, StatePredicate, TestModelOptions, + TestPath, TestPathResult, TestStepResult } from './types'; import { formatPathTestResult, + getDescription, mapPlansToPaths, simpleStringify } from './utils'; @@ -75,26 +79,26 @@ export class TestModel { public getShortestPaths( options?: Partial> - ): Array> { + ): Array> { return this.getPaths({ ...options, pathGenerator: getShortestPaths }); } public getPaths( options?: Partial> - ): Array> { + ): Array> { const pathGenerator = pathGeneratorWithDedup( options?.pathGenerator || TestModel.defaults.pathGenerator ); const paths = pathGenerator(this.behavior, this.resolveOptions(options)); - return paths; + return paths.map(this.toTestPath); } public getShortestPathsTo( statePredicate: StatePredicate - ): Array> { + ): Array> { let minWeight = Infinity; - let shortestPaths: Array> = []; + let shortestPaths: Array> = []; const paths = this.filterPathsTo(statePredicate, this.getShortestPaths()); @@ -113,7 +117,7 @@ export class TestModel { public getSimplePaths( options?: Partial> - ): Array> { + ): Array> { return this.getPaths({ ...options, pathGenerator: getSimplePaths @@ -122,16 +126,16 @@ export class TestModel { public getSimplePathsTo( predicate: StatePredicate - ): Array> { + ): Array> { return mapPlansToPaths( traverseSimplePathsTo(this.behavior, predicate, this.options) - ); + ).map(this.toTestPath); } private filterPathsTo( statePredicate: StatePredicate, - testPaths: Array> - ): Array> { + testPaths: Array> + ): Array> { const predicate: StatePredicate = (state) => statePredicate(state); return testPaths.filter((testPath) => { @@ -139,10 +143,38 @@ export class TestModel { }); } + 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: () => this.testPath(statePath), + testSync: () => this.testPathSync(statePath), + description: isStateLike(statePath.state) + ? `Reaches ${getDescription( + statePath.state as any + ).trim()}: ${eventsString}` + : JSON.stringify(statePath.state) + }; + }; + public getPathFromEvents( events: TEvent[], statePredicate: StatePredicate - ): StatePath { + ): TestPath { const path = getPathFromEvents(this.behavior, events); if (!statePredicate(path.state)) { @@ -153,7 +185,7 @@ export class TestModel { ); } - return path; + return this.toTestPath(path); } public getAllStates(): TState[] { @@ -164,7 +196,7 @@ export class TestModel { public testPathSync( path: StatePath, options?: Partial> - ) { + ): TestPathResult { const testPathResult: TestPathResult = { steps: [], state: { @@ -210,12 +242,14 @@ export class TestModel { err.message += formatPathTestResult(path, testPathResult, this.options); throw err; } + + return testPathResult; } public async testPath( path: StatePath, options?: Partial> - ) { + ): Promise { const testPathResult: TestPathResult = { steps: [], state: { @@ -261,6 +295,8 @@ export class TestModel { err.message += formatPathTestResult(path, testPathResult, this.options); throw err; } + + return testPathResult; } public async testState( diff --git a/packages/xstate-test/src/testUtils.ts b/packages/xstate-test/src/testUtils.ts index 972103f7d6..c4fd0fb96b 100644 --- a/packages/xstate-test/src/testUtils.ts +++ b/packages/xstate-test/src/testUtils.ts @@ -1,18 +1,15 @@ -import { StatePath } from '@xstate/graph'; import { TestModel } from './TestModel'; +import { TestPath } from './types'; const testModel = async (model: TestModel) => { for (const path of model.getPaths()) { - await model.testPath(path); + await path.test(); } }; -const testPaths = async ( - model: TestModel, - paths: StatePath[] -) => { +const testPaths = async (paths: TestPath[]) => { for (const path of paths) { - await model.testPath(path); + await path.test(); } }; diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 7337fd78cd..936867d7fb 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -5,7 +5,6 @@ import { TraversalOptions } from '@xstate/graph'; import { - AnyState, BaseActionObject, EventObject, ExtractEvent, @@ -71,13 +70,6 @@ export interface TestMeta { description?: string | ((state: State) => string); skip?: boolean; } -interface TestStep { - state: AnyState; - event: EventObject; - description: string; - test: (testContext: T) => Promise; - exec: (testContext: T) => Promise; -} interface TestStateResult { error: null | Error; } @@ -88,15 +80,15 @@ export interface TestStepResult { error: null | Error; }; } -export interface TestPath { - weight: number; - steps: Array>; +export interface TestPath + extends StatePath { description: string; /** * Tests and executes each step in `steps` sequentially, and then * tests the postcondition that the `state` is reached. */ - test: (testContext: T) => Promise; + test: () => Promise; + testSync: () => TestPathResult; } export interface TestPathResult { steps: TestStepResult[]; diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts index d4cf89a5cc..3d751cfcaa 100644 --- a/packages/xstate-test/src/utils.ts +++ b/packages/xstate-test/src/utils.ts @@ -96,9 +96,9 @@ export function getDescription(state: AnyState): string { }); return ( - `state${stateStrings.length === 1 ? '' : 's'}: ` + + `state${stateStrings.length === 1 ? '' : 's'} ` + stateStrings.join(', ') + - ` ${contextString}` + ` ${contextString}`.trim() ); } diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 7195da7f53..94ae4742c8 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -128,7 +128,7 @@ describe('events', () => { expect(paths.length).toBe(3); - await testUtils.testPaths(testModel, paths); + await testUtils.testPaths(paths); expect(testedEvents).toMatchInlineSnapshot(` Array [ @@ -291,7 +291,7 @@ it('tests transitions', async () => { const paths = model.getShortestPathsTo((state) => state.matches('second')); - await model.testPath(paths[0]); + await paths[0].test(); }); // https://github.com/statelyai/xstate/issues/982 diff --git a/packages/xstate-test/test/plans.test.ts b/packages/xstate-test/test/paths.test.ts similarity index 81% rename from packages/xstate-test/test/plans.test.ts rename to packages/xstate-test/test/paths.test.ts index 2cd1b56227..782c6c9791 100644 --- a/packages/xstate-test/test/plans.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -62,7 +62,7 @@ describe('testModel.testPaths(...)', () => { } }); - await testUtils.testPaths(testModel, paths); + await testUtils.testPaths(paths); }); describe('When the machine only has one path', () => { @@ -102,3 +102,16 @@ describe('testModel.testPaths(...)', () => { }); }); }); + +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' + ]); + }); +}); diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index c84cce790a..8fee024bd9 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -74,7 +74,7 @@ describe('custom test models', () => { const paths = model.getShortestPathsTo((state) => state === 1); - await testUtils.testPaths(model, paths); + await testUtils.testPaths(paths); expect(testedStateKeys).toContain('even'); expect(testedStateKeys).toContain('odd'); From ae4a2f58e05310c397ec69e66edf886570539b4f Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 20 May 2022 13:43:42 +0100 Subject: [PATCH 114/394] Fixed TS error --- packages/xstate-test/src/TestModel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 07d398b650..3f089e4526 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -3,7 +3,6 @@ import { performDepthFirstTraversal, SerializedEvent, SerializedState, - serializeState, SimpleBehavior, StatePath, Step, From 7d5428b105eaf706b0ffdc95208e097093822f22 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 24 May 2022 11:01:33 -0600 Subject: [PATCH 115/394] Removed states, events from testmodel, removed cases from events --- packages/xstate-graph/src/graph.ts | 11 +- packages/xstate-graph/src/types.ts | 28 ++++- packages/xstate-test/src/TestModel.ts | 71 +++++++------ packages/xstate-test/src/machine.ts | 20 ++-- packages/xstate-test/src/testUtils.ts | 16 ++- packages/xstate-test/src/types.ts | 79 +++----------- packages/xstate-test/test/dieHard.test.ts | 79 +++++++------- packages/xstate-test/test/events.test.ts | 36 +++---- packages/xstate-test/test/index.test.ts | 109 ++++++++++---------- packages/xstate-test/test/paths.test.ts | 4 +- packages/xstate-test/test/states.test.ts | 70 ++++++------- packages/xstate-test/test/sync.test.ts | 74 +++++++------ packages/xstate-test/test/testModel.test.ts | 27 ++--- 13 files changed, 296 insertions(+), 328 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 31cb0081f3..f95740bfb6 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -173,7 +173,8 @@ export function getAdjacencyMap( const defaultMachineStateOptions: TraversalOptions, any> = { serializeState: serializeMachineState, serializeEvent, - getEvents: (state) => { + eventCases: {}, + getEvents: (_eventCases, state) => { return state.nextEvents.map((type) => ({ type })); } }; @@ -420,7 +421,7 @@ interface AdjMap { }; } -export function performDepthFirstTraversal( +export function performDepthFirstTraversal( behavior: SimpleBehavior, options: TraversalOptions ): AdjMap { @@ -428,6 +429,7 @@ export function performDepthFirstTraversal( const { serializeState, getEvents, + eventCases, traversalLimit: limit } = resolveTraversalOptions(options); const adj: AdjMap = {}; @@ -452,7 +454,7 @@ export function performDepthFirstTraversal( transitions: {} }; - const events = getEvents(state); + const events = getEvents(eventCases, state); for (const subEvent of events) { const nextState = transition(state, subEvent); @@ -472,7 +474,7 @@ export function performDepthFirstTraversal( return adj; } -function resolveTraversalOptions( +function resolveTraversalOptions( traversalOptions?: Partial>, defaultOptions?: TraversalOptions ): Required> { @@ -487,6 +489,7 @@ function resolveTraversalOptions( visitCondition: (state, event, vctx) => { return vctx.vertices.has(serializeState(state, event)); }, + eventCases: {}, getEvents: () => [], traversalLimit: Infinity, ...defaultOptions, diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index ce9aa34fd0..b6f2468d34 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -133,12 +133,28 @@ export interface VisitedContext { a?: TState | TEvent; // TODO: remove } -export interface SerializationOptions { +export interface SerializationOptions { + eventCases: EventCaseMap; serializeState: (state: TState, event: TEvent | null) => SerializedState; serializeEvent: (event: TEvent) => SerializedEvent; } -export interface TraversalOptions +/** + * 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 { visitCondition?: ( state: TState, @@ -146,7 +162,7 @@ export interface TraversalOptions vctx: VisitedContext ) => boolean; filter?: (state: TState, event: TEvent) => boolean; - getEvents?: (state: TState) => TEvent[]; + getEvents?: (cases: EventCaseMap, state: TState) => TEvent[]; /** * The maximum number of traversals to perform when calculating * the state transition adjacency map. @@ -156,6 +172,12 @@ export interface TraversalOptions traversalLimit?: number; } +export type EventCaseMap = { + [TEventType in TEvent['type']]?: + | ((state: TState) => Array>) + | Array>; +}; + type Brand = T & { __tag: Tag }; export type SerializedState = Brand; diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 3f089e4526..ffa7c12b4c 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -19,6 +19,7 @@ import type { PathGenerator, StatePredicate, TestModelOptions, + TestParam, TestPath, TestPathResult, TestStepResult @@ -53,10 +54,9 @@ export class TestModel { serializeState: (state) => simpleStringify(state) as SerializedState, serializeEvent: (event) => simpleStringify(event) as SerializedEvent, getEvents: () => [], - states: {}, - events: {}, stateMatcher: (_, stateKey) => stateKey === '*', getStates: () => [], + eventCases: {}, execute: () => void 0, logger: { log: console.log.bind(console), @@ -160,8 +160,10 @@ export class TestModel { .join(' → '); return { ...statePath, - test: () => this.testPath(statePath), - testSync: () => this.testPathSync(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 @@ -194,6 +196,7 @@ export class TestModel { public testPathSync( path: StatePath, + params: TestParam, options?: Partial> ): TestPathResult { const testPathResult: TestPathResult = { @@ -214,7 +217,7 @@ export class TestModel { testPathResult.steps.push(testStepResult); try { - this.testStateSync(step.state, options); + this.testStateSync(params, step.state, options); } catch (err) { testStepResult.state.error = err; @@ -222,7 +225,7 @@ export class TestModel { } try { - this.testTransitionSync(step); + this.testTransitionSync(params, step); } catch (err) { testStepResult.event.error = err; @@ -231,7 +234,7 @@ export class TestModel { } try { - this.testStateSync(path.state, options); + this.testStateSync(params, path.state, options); } catch (err) { testPathResult.state.error = err.message; throw err; @@ -247,6 +250,7 @@ export class TestModel { public async testPath( path: StatePath, + params: TestParam, options?: Partial> ): Promise { const testPathResult: TestPathResult = { @@ -267,7 +271,7 @@ export class TestModel { testPathResult.steps.push(testStepResult); try { - await this.testState(step.state, options); + await this.testState(params, step.state, options); } catch (err) { testStepResult.state.error = err; @@ -275,7 +279,7 @@ export class TestModel { } try { - await this.testTransition(step); + await this.testTransition(params, step); } catch (err) { testStepResult.event.error = err; @@ -284,7 +288,7 @@ export class TestModel { } try { - await this.testState(path.state, options); + await this.testState(params, path.state, options); } catch (err) { testPathResult.state.error = err.message; throw err; @@ -299,32 +303,33 @@ export class TestModel { } public async testState( + params: TestParam, state: TState, options?: Partial> ): Promise { const resolvedOptions = this.resolveOptions(options); - const stateTestKeys = this.getStateTestKeys(state, resolvedOptions); + const stateTestKeys = this.getStateTestKeys(params, state, resolvedOptions); for (const stateTestKey of stateTestKeys) { - await resolvedOptions.states[stateTestKey](state); + await params.states?.[stateTestKey](state); } this.afterTestState(state, resolvedOptions); } private getStateTestKeys( + params: TestParam, state: TState, resolvedOptions: TestModelOptions ) { - const stateTestKeys = Object.keys(resolvedOptions.states).filter( - (stateKey) => { - return resolvedOptions.stateMatcher(state, stateKey); - } - ); + const states = params.states || {}; + const stateTestKeys = Object.keys(states).filter((stateKey) => { + return resolvedOptions.stateMatcher(state, stateKey); + }); // Fallthrough state tests - if (!stateTestKeys.length && '*' in resolvedOptions.states) { + if (!stateTestKeys.length && '*' in states) { stateTestKeys.push('*'); } @@ -339,16 +344,17 @@ export class TestModel { } public testStateSync( + params: TestParam, state: TState, options?: Partial> ): void { const resolvedOptions = this.resolveOptions(options); - const stateTestKeys = this.getStateTestKeys(state, resolvedOptions); + const stateTestKeys = this.getStateTestKeys(params, state, resolvedOptions); for (const stateTestKey of stateTestKeys) { errorIfPromise( - resolvedOptions.states[stateTestKey](state), + params.states?.[stateTestKey](state), `The test for '${stateTestKey}' returned a promise - did you mean to use the sync method?` ); } @@ -356,24 +362,29 @@ export class TestModel { this.afterTestState(state, resolvedOptions); } - private getEventExec(step: Step) { - const eventConfig = this.options.events?.[ - (step.event as any).type as TEvent['type'] - ]; - + private getEventExec( + params: TestParam, + step: Step + ) { const eventExec = - typeof eventConfig === 'function' ? eventConfig : eventConfig?.exec; + params.events?.[(step.event as any).type as TEvent['type']]; return eventExec; } - public async testTransition(step: Step): Promise { - const eventExec = this.getEventExec(step); + public async testTransition( + params: TestParam, + step: Step + ): Promise { + const eventExec = this.getEventExec(params, step); await (eventExec as EventExecutor)?.(step); } - public testTransitionSync(step: Step): void { - const eventExec = this.getEventExec(step); + public testTransitionSync( + params: TestParam, + step: Step + ): void { + const eventExec = this.getEventExec(params, step); errorIfPromise( (eventExec as EventExecutor)?.(step), diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 22a27fc63d..5a13c0c096 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -1,4 +1,4 @@ -import { serializeState, SimpleBehavior } from '@xstate/graph'; +import { EventCaseMap, serializeState, SimpleBehavior } from '@xstate/graph'; import { ActionObject, AnyEventObject, @@ -15,7 +15,6 @@ import { TestModel } from './TestModel'; import { TestMachineConfig, TestMachineOptions, - TestModelEventConfig, TestModelOptions } from './types'; import { flatten } from './utils'; @@ -93,25 +92,18 @@ export function createTestModel( ? state.configuration.includes(machine.getStateNodeById(key)) : state.matches(key); }, - states: { - '*': testStateFromMeta - }, execute: (state) => { state.actions.forEach((action) => { executeAction(action, state); }); }, - getEvents: (state) => + getEvents: ( + eventCases: EventCaseMap, EventFrom>, + state + ) => flatten( state.nextEvents.map((eventType) => { - const eventConfig = options?.events?.[eventType]; - const eventCaseGenerator = - typeof eventConfig === 'function' - ? undefined - : (eventConfig?.cases as TestModelEventConfig< - any, - any - >['cases']); + const eventCaseGenerator = eventCases?.[eventType]; const cases = eventCaseGenerator ? Array.isArray(eventCaseGenerator) diff --git a/packages/xstate-test/src/testUtils.ts b/packages/xstate-test/src/testUtils.ts index c4fd0fb96b..e327918d26 100644 --- a/packages/xstate-test/src/testUtils.ts +++ b/packages/xstate-test/src/testUtils.ts @@ -1,15 +1,21 @@ import { TestModel } from './TestModel'; -import { TestPath } from './types'; +import { TestParam, TestPath } from './types'; -const testModel = async (model: TestModel) => { +const testModel = async ( + model: TestModel, + params: TestParam +) => { for (const path of model.getPaths()) { - await path.test(); + await path.test(params); } }; -const testPaths = async (paths: TestPath[]) => { +const testPaths = async ( + paths: TestPath[], + params: TestParam +) => { for (const path of paths) { - await path.test(); + await path.test(params); } }; diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 936867d7fb..13d76782b8 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -7,7 +7,6 @@ import { import { BaseActionObject, EventObject, - ExtractEvent, MachineConfig, MachineOptions, MachineSchema, @@ -80,6 +79,16 @@ export interface TestStepResult { error: null | Error; }; } + +export interface TestParam { + states?: { + [key: string]: (state: TState) => void | Promise; + }; + events?: { + [TEventType in TEvent['type']]?: EventExecutor; + }; +} + export interface TestPath extends StatePath { description: string; @@ -87,29 +96,14 @@ export interface TestPath * Tests and executes each step in `steps` sequentially, and then * tests the postcondition that the `state` is reached. */ - test: () => Promise; - testSync: () => TestPathResult; + test: (params: TestParam) => Promise; + testSync: (params: TestParam) => TestPathResult; } export interface TestPathResult { steps: TestStepResult[]; state: TestStateResult; } -/** - * A sample event object payload (_without_ the `type` property). - * - * @example - * - * ```js - * { - * value: 'testValue', - * other: 'something', - * id: 42 - * } - * ``` - */ -type EventCase = Omit; - export type StatePredicate = (state: TState) => boolean; /** * Executes an effect using the `testContext` and `event` @@ -119,47 +113,6 @@ 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; - /** - * Sample event object payloads _without_ the `type` property. - * - * @example - * - * ```js - * cases: [ - * { value: 'foo' }, - * { value: '' } - * ] - * ``` - */ - cases?: Array>; -} - -export type TestEventsConfig = { - [EventType in TEvent['type']]?: - | EventExecutor - | TestEventConfig; -}; - -export interface TestModelEventConfig { - cases?: - | ((state: TState) => Array>) - | Array>; - exec?: EventExecutor; -} - export interface TestModelOptions extends TraversalOptions { // testState: (state: TState) => void | Promise; @@ -170,14 +123,6 @@ export interface TestModelOptions execute: (state: TState) => void; getStates: () => TState[]; stateMatcher: (state: TState, stateKey: string) => boolean; - states: { - [key: string]: (state: TState) => void | Promise; - }; - events: { - [TEventType in TEvent['type']]?: - | EventExecutor - | TestModelEventConfig>; - }; logger: { log: (msg: string) => void; error: (msg: string) => void; diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 59c54fa53d..f569f351cc 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -109,7 +109,7 @@ describe('die hard example', () => { } ); - return createTestModel(dieHardMachine, { + const options = { states: { pending: (state) => { expect(jugs.five).not.toEqual(4); @@ -121,38 +121,31 @@ describe('die hard example', () => { } }, events: { - POUR_3_TO_5: { - exec: async () => { - await jugs.transferThree(); - } + POUR_3_TO_5: async () => { + await jugs.transferThree(); }, - POUR_5_TO_3: { - exec: async () => { - await jugs.transferFive(); - } + POUR_5_TO_3: async () => { + await jugs.transferFive(); }, - EMPTY_3: { - exec: async () => { - await jugs.emptyThree(); - } + EMPTY_3: async () => { + await jugs.emptyThree(); }, - EMPTY_5: { - exec: async () => { - await jugs.emptyFive(); - } + EMPTY_5: async () => { + await jugs.emptyFive(); }, - FILL_3: { - exec: async () => { - await jugs.fillThree(); - } + FILL_3: async () => { + await jugs.fillThree(); }, - FILL_5: { - exec: async () => { - await jugs.fillFive(); - } + FILL_5: async () => { + await jugs.fillFive(); } } - }); + }; + + return { + model: createTestModel(dieHardMachine), + options + }; }; beforeEach(() => { @@ -163,12 +156,12 @@ describe('die hard example', () => { describe('testing a model (shortestPathsTo)', () => { const dieHardModel = createDieHardModel(); - dieHardModel + dieHardModel.model .getShortestPathsTo((state) => state.matches('success')) .forEach((path) => { describe(`path ${getDescription(path.state)}`, () => { it(`path ${getDescription(path.state)}`, async () => { - await dieHardModel.testPath(path); + await dieHardModel.model.testPath(path, dieHardModel.options); }); }); }); @@ -176,14 +169,14 @@ describe('die hard example', () => { describe('testing a model (simplePathsTo)', () => { const dieHardModel = createDieHardModel(); - dieHardModel + 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.testPath(path); + await dieHardModel.model.testPath(path, dieHardModel.options); }); }); }); @@ -192,7 +185,7 @@ describe('die hard example', () => { describe('testing a model (getPathFromEvents)', () => { const dieHardModel = createDieHardModel(); - const path = dieHardModel.getPathFromEvents( + const path = dieHardModel.model.getPathFromEvents( [ { type: 'FILL_5' }, { type: 'POUR_5_TO_3' }, @@ -208,13 +201,13 @@ describe('die hard example', () => { path.state.value )} (${JSON.stringify(path.state.context)})`, () => { it(`path ${getDescription(path.state)}`, async () => { - await dieHardModel.testPath(path); + await dieHardModel.model.testPath(path, dieHardModel.options); }); }); it('should throw if the target does not match the last entered state', () => { expect(() => { - dieHardModel.getPathFromEvents([{ type: 'FILL_5' }], (state) => + dieHardModel.model.getPathFromEvents([{ type: 'FILL_5' }], (state) => state.matches('success') ); }).toThrow(); @@ -223,7 +216,7 @@ describe('die hard example', () => { describe('.testPath(path)', () => { const dieHardModel = createDieHardModel(); - const paths = dieHardModel.getSimplePathsTo((state) => { + const paths = dieHardModel.model.getSimplePathsTo((state) => { return state.matches('success') && state.context.three === 0; }); @@ -233,7 +226,7 @@ describe('die hard example', () => { )} (${JSON.stringify(path.state.context)})`, () => { describe(`path ${getDescription(path.state)}`, () => { it(`reaches the target state`, async () => { - await dieHardModel.testPath(path); + await dieHardModel.model.testPath(path, dieHardModel.options); }); }); }); @@ -255,20 +248,20 @@ describe('error path trace', () => { } }); - const testModel = createTestModel(machine, { - states: { - third: () => { - throw new Error('test error'); - } - } - }); + 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, undefined); + 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(` diff --git a/packages/xstate-test/test/events.test.ts b/packages/xstate-test/test/events.test.ts index 19a1bc0841..46d0d3de9f 100644 --- a/packages/xstate-test/test/events.test.ts +++ b/packages/xstate-test/test/events.test.ts @@ -17,19 +17,16 @@ describe('events', () => { }, b: {} } - }), - { - events: { - EVENT: { - exec: () => { - executed = true; - } - } - } - } + }) ); - await testUtils.testModel(testModel); + await testUtils.testModel(testModel, { + events: { + EVENT: () => { + executed = true; + } + } + }); expect(executed).toBe(true); }); @@ -48,17 +45,16 @@ describe('events', () => { }, b: {} } - }), - { - events: { - EVENT: () => { - executed = true; - } - } - } + }) ); - await testUtils.testModel(testModel); + await testUtils.testModel(testModel, { + events: { + EVENT: () => { + executed = true; + } + } + }); expect(executed).toBe(true); }); diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index 94ae4742c8..ef40b0358c 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -59,12 +59,12 @@ describe('events', () => { }); const testModel = createTestModel(feedbackMachine, { - events: { - SUBMIT: { cases: [{ value: 'something' }, { value: '' }] } + eventCases: { + SUBMIT: [{ value: 'something' }, { value: '' }] } }); - await testUtils.testModel(testModel); + await testUtils.testModel(testModel, {}); }); it('should not throw an error for unimplemented events', () => { @@ -81,18 +81,19 @@ describe('events', () => { const testModel = createTestModel(testMachine); expect(async () => { - await testUtils.testModel(testModel); + 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: [1, 2, 3] // to be read by generator + values // to be read by generator }, states: { a: { @@ -113,14 +114,8 @@ describe('events', () => { const testedEvents: any[] = []; const testModel = createTestModel(testMachine, { - events: { - EVENT: { - // Read dynamically from state context - cases: (state) => state.context.values.map((value) => ({ value })), - exec: ({ event }) => { - testedEvents.push(event); - } - } + eventCases: { + EVENT: (state) => state.context.values.map((value) => ({ value })) } }); @@ -128,7 +123,13 @@ describe('events', () => { expect(paths.length).toBe(3); - await testUtils.testPaths(paths); + await testUtils.testPaths(paths, { + events: { + EVENT: ({ event }) => { + testedEvents.push(event); + } + } + }); expect(testedEvents).toMatchInlineSnapshot(` Array [ @@ -228,7 +229,7 @@ it('executes actions', async () => { const model = createTestModel(machine); - await testUtils.testModel(model); + await testUtils.testModel(model, {}); expect(executedActive).toBe(true); expect(executedDone).toBe(true); @@ -249,17 +250,16 @@ describe('test model options', () => { }, active: {} } - }), - { - states: { - '*': (state) => { - testedStates.push(state.value); - } - } - } + }) ); - await testUtils.testModel(model); + await testUtils.testModel(model, { + states: { + '*': (state) => { + testedStates.push(state.value); + } + } + }); expect(testedStates).toEqual(['inactive', 'active']); }); @@ -278,20 +278,18 @@ it('tests transitions', async () => { } }); - const model = createTestModel(machine, { + const model = createTestModel(machine); + + const paths = model.getShortestPathsTo((state) => state.matches('second')); + + await paths[0].test({ events: { - NEXT: { - exec: (step) => { - expect(step).toHaveProperty('event'); - expect(step).toHaveProperty('state'); - } + NEXT: (step) => { + expect(step).toHaveProperty('event'); + expect(step).toHaveProperty('state'); } } }); - - const paths = model.getShortestPathsTo((state) => state.matches('second')); - - await paths[0].test(); }); // https://github.com/statelyai/xstate/issues/982 @@ -311,10 +309,18 @@ it('Event in event executor should contain payload from case', async () => { const nonSerializableData = () => 42; const model = createTestModel(machine, { - events: { - NEXT: { - cases: [{ payload: 10, fn: nonSerializableData }], - exec: (step) => { + 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, @@ -322,12 +328,9 @@ it('Event in event executor should contain payload from case', async () => { }); } } - } - }); - - const paths = model.getShortestPathsTo((state) => state.matches('second')); - - await model.testPath(paths[0], obj); + }, + obj + ); }); describe('state tests', () => { @@ -346,7 +349,9 @@ describe('state tests', () => { } }); - const model = createTestModel(machine, { + const model = createTestModel(machine); + + await testUtils.testModel(model, { states: { a: (state) => { expect(state.value).toEqual('a'); @@ -356,8 +361,6 @@ describe('state tests', () => { } } }); - - await testUtils.testModel(model); }); it('should test wildcard state for non-matching states', async () => { @@ -377,7 +380,9 @@ describe('state tests', () => { } }); - const model = createTestModel(machine, { + const model = createTestModel(machine); + + await testUtils.testModel(model, { states: { a: (state) => { expect(state.value).toEqual('a'); @@ -390,8 +395,6 @@ describe('state tests', () => { } } }); - - await testUtils.testModel(model); }); it('should test nested states', async () => { @@ -412,7 +415,9 @@ describe('state tests', () => { } }); - const model = createTestModel(machine, { + const model = createTestModel(machine); + + await testUtils.testModel(model, { states: { a: (state) => { testedStateValues.push('a'); @@ -428,8 +433,6 @@ describe('state tests', () => { } } }); - - await testUtils.testModel(model); expect(testedStateValues).toMatchInlineSnapshot(` Array [ "a", diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index 782c6c9791..a1fe6cc557 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -44,7 +44,7 @@ describe('testModel.testPaths(...)', () => { const paths = testModel.getPaths({ pathGenerator: (behavior, options) => { - const events = options.getEvents?.(behavior.initialState) ?? []; + const events = options.getEvents?.({}, behavior.initialState) ?? []; const nextState = behavior.transition(behavior.initialState, events[0]); return [ @@ -62,7 +62,7 @@ describe('testModel.testPaths(...)', () => { } }); - await testUtils.testPaths(paths); + await testUtils.testPaths(paths, {}); }); describe('When the machine only has one path', () => { diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts index bf2b83af92..9b911aa90b 100644 --- a/packages/xstate-test/test/states.test.ts +++ b/packages/xstate-test/test/states.test.ts @@ -23,26 +23,25 @@ describe('states', () => { } } } - }), - { - 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); - } - } - } + }) ); - await testUtils.testModel(testModel); + 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 [ @@ -88,26 +87,25 @@ describe('states', () => { } } } - }), - { - 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); - } - } - } + }) ); - await testUtils.testModel(testModel); + 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 [ diff --git a/packages/xstate-test/test/sync.test.ts b/packages/xstate-test/test/sync.test.ts index b968291fcf..cfe2559fa7 100644 --- a/packages/xstate-test/test/sync.test.ts +++ b/packages/xstate-test/test/sync.test.ts @@ -13,46 +13,26 @@ const machine = createMachine({ } }); -const promiseStateModel = createTestModel(machine, { - states: { - a: async () => {}, - b: () => {} - }, - events: { - EVENT: () => {} - } -}); +const promiseStateModel = createTestModel(machine); -const promiseEventModel = createTestModel(machine, { - states: { - a: () => {}, - b: () => {} - }, - events: { - EVENT: { - exec: async () => {} - } - } -}); +const promiseEventModel = createTestModel(machine); -const syncModel = createTestModel(machine, { - states: { - a: () => {}, - b: () => {} - }, - events: { - EVENT: { - exec: () => {} - } - } -}); +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)) + 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?` ); @@ -60,9 +40,17 @@ describe('.testPathSync', () => { it('Should error if it encounters a promise in an event', () => { expect(() => - promiseEventModel - .getPaths() - .forEach((path) => promiseEventModel.testPathSync(path)) + 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?` ); @@ -70,7 +58,17 @@ describe('.testPathSync', () => { it('Should succeed if it encounters no promises', () => { expect(() => - syncModel.getPaths().forEach((path) => syncModel.testPathSync(path)) + 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 index 8fee024bd9..ab6cf74ece 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -15,7 +15,7 @@ describe('custom test models', () => { } }, { - getEvents: (state) => { + getEvents: (cases, state) => { if (state % 2 === 0) { return [{ type: 'even' }]; } @@ -44,22 +44,12 @@ describe('custom test models', () => { } }, { - getEvents: (state) => { + getEvents: (cases, state) => { if (state % 2 === 0) { return [{ type: 'even' }]; } return [{ type: 'odd' }]; }, - states: { - even: (state) => { - testedStateKeys.push('even'); - expect(state % 2).toBe(0); - }, - odd: (state) => { - testedStateKeys.push('odd'); - expect(state % 2).toBe(1); - } - }, stateMatcher: (state, key) => { if (key === 'even') { return state % 2 === 0; @@ -74,7 +64,18 @@ describe('custom test models', () => { const paths = model.getShortestPathsTo((state) => state === 1); - await testUtils.testPaths(paths); + 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'); From 3c49f42e6dbde1f2a8e4a22f62aa43ac2212dbc2 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 24 May 2022 11:08:44 -0600 Subject: [PATCH 116/394] Fixed tests --- packages/xstate-test/test/testModel.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index ab6cf74ece..40e1bfa6d0 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -15,7 +15,7 @@ describe('custom test models', () => { } }, { - getEvents: (cases, state) => { + getEvents: (_cases, state) => { if (state % 2 === 0) { return [{ type: 'even' }]; } @@ -44,7 +44,7 @@ describe('custom test models', () => { } }, { - getEvents: (cases, state) => { + getEvents: (_cases, state) => { if (state % 2 === 0) { return [{ type: 'even' }]; } From e57615022ba4d62c42d8da4eb598bae9c5f2db35 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 24 May 2022 11:09:39 -0600 Subject: [PATCH 117/394] Fixed eventCases types --- packages/xstate-test/src/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts index 3d751cfcaa..5468245e16 100644 --- a/packages/xstate-test/src/utils.ts +++ b/packages/xstate-test/src/utils.ts @@ -26,6 +26,7 @@ export function formatPathTestResult( serializeState: (state, _event) => simpleStringify(state) as SerializedState, serializeEvent: (event) => simpleStringify(event) as SerializedEvent, + eventCases: {}, ...options }; From 65ad559d4cfc0531ae71ce68d3b662ffd74d8b5f Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 24 May 2022 11:24:37 -0600 Subject: [PATCH 118/394] Switched around testCases and state in getEvents --- packages/xstate-graph/src/graph.ts | 4 ++-- packages/xstate-graph/src/types.ts | 2 +- packages/xstate-test/src/machine.ts | 5 +---- packages/xstate-test/test/paths.test.ts | 2 +- packages/xstate-test/test/testModel.test.ts | 4 ++-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index f95740bfb6..877eff0146 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -174,7 +174,7 @@ const defaultMachineStateOptions: TraversalOptions, any> = { serializeState: serializeMachineState, serializeEvent, eventCases: {}, - getEvents: (_eventCases, state) => { + getEvents: (state) => { return state.nextEvents.map((type) => ({ type })); } }; @@ -454,7 +454,7 @@ export function performDepthFirstTraversal( transitions: {} }; - const events = getEvents(eventCases, state); + const events = getEvents(state, eventCases); for (const subEvent of events) { const nextState = transition(state, subEvent); diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index b6f2468d34..6fb20eb857 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -162,7 +162,7 @@ export interface TraversalOptions vctx: VisitedContext ) => boolean; filter?: (state: TState, event: TEvent) => boolean; - getEvents?: (cases: EventCaseMap, state: TState) => TEvent[]; + getEvents?: (state: TState, cases: EventCaseMap) => TEvent[]; /** * The maximum number of traversals to perform when calculating * the state transition adjacency map. diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 5a13c0c096..cad69235e3 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -97,10 +97,7 @@ export function createTestModel( executeAction(action, state); }); }, - getEvents: ( - eventCases: EventCaseMap, EventFrom>, - state - ) => + getEvents: (state, eventCases) => flatten( state.nextEvents.map((eventType) => { const eventCaseGenerator = eventCases?.[eventType]; diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index a1fe6cc557..d09c4f18be 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -44,7 +44,7 @@ describe('testModel.testPaths(...)', () => { const paths = testModel.getPaths({ pathGenerator: (behavior, options) => { - const events = options.getEvents?.({}, behavior.initialState) ?? []; + const events = options.getEvents?.(behavior.initialState, {}) ?? []; const nextState = behavior.transition(behavior.initialState, events[0]); return [ diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index 40e1bfa6d0..4f3c448130 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -15,7 +15,7 @@ describe('custom test models', () => { } }, { - getEvents: (_cases, state) => { + getEvents: (state) => { if (state % 2 === 0) { return [{ type: 'even' }]; } @@ -44,7 +44,7 @@ describe('custom test models', () => { } }, { - getEvents: (_cases, state) => { + getEvents: (state) => { if (state % 2 === 0) { return [{ type: 'even' }]; } From fd17d6742a850d0d4cc4cea695b62394645fd26a Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 24 May 2022 11:27:58 -0600 Subject: [PATCH 119/394] Fixed ts error --- packages/xstate-test/src/machine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index cad69235e3..85fc2ac893 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -1,4 +1,4 @@ -import { EventCaseMap, serializeState, SimpleBehavior } from '@xstate/graph'; +import { serializeState, SimpleBehavior } from '@xstate/graph'; import { ActionObject, AnyEventObject, From 46e5762a63681275e36b0925287fbf26df1de6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 12:51:51 +0200 Subject: [PATCH 120/394] Adjust some types in `@xstate/graph` to be more strict --- packages/xstate-graph/src/graph.ts | 6 +- packages/xstate-graph/src/types.ts | 11 ++- packages/xstate-graph/test/graph.test.ts | 19 ++--- packages/xstate-graph/test/types.test.ts | 97 ++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 packages/xstate-graph/test/types.test.ts diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 877eff0146..0e6c02d0b0 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -181,8 +181,8 @@ const defaultMachineStateOptions: TraversalOptions, any> = { export function getShortestPlans( machine: TMachine, - options?: Partial> -): Array> { + options?: Partial, EventFrom>> +): Array, EventFrom>> { const resolvedOptions = resolveTraversalOptions( options, defaultMachineStateOptions @@ -193,7 +193,7 @@ export function getShortestPlans( initialState: machine.initialState }, resolvedOptions - ); + ) as Array, EventFrom>>; } export function traverseShortestPlans( diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 6fb20eb857..f6359b2721 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -162,7 +162,10 @@ export interface TraversalOptions vctx: VisitedContext ) => boolean; filter?: (state: TState, event: TEvent) => boolean; - getEvents?: (state: TState, cases: EventCaseMap) => TEvent[]; + getEvents?: ( + state: TState, + cases: EventCaseMap + ) => ReadonlyArray; /** * The maximum number of traversals to perform when calculating * the state transition adjacency map. @@ -173,9 +176,9 @@ export interface TraversalOptions } export type EventCaseMap = { - [TEventType in TEvent['type']]?: - | ((state: TState) => Array>) - | Array>; + [E in TEvent as E['type']]?: + | ((state: TState) => Array>) + | Array>; }; type Brand = T & { __tag: Tag }; diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 4330aecd77..fa29cc39e3 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -255,15 +255,16 @@ describe('@xstate/graph', () => { }); const paths = getShortestPlans(machine, { - getEvents: () => [ - { - type: 'EVENT', - id: 'whatever' - }, - { - type: 'STATE' - } - ] + getEvents: () => + [ + { + type: 'EVENT', + id: 'whatever' + }, + { + type: 'STATE' + } + ] as const }); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( diff --git a/packages/xstate-graph/test/types.test.ts b/packages/xstate-graph/test/types.test.ts new file mode 100644 index 0000000000..c6eec030df --- /dev/null +++ b/packages/xstate-graph/test/types.test.ts @@ -0,0 +1,97 @@ +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' + } + ] + } + }); + }); +}); From 5bc85eb53de39143337515f641446e2164359080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 13:20:44 +0200 Subject: [PATCH 121/394] Avoid abbreviating adjacency to adj in public APIs --- packages/xstate-graph/src/graph.ts | 34 +++++++++++++----------- packages/xstate-graph/src/types.ts | 4 +-- packages/xstate-graph/test/graph.test.ts | 9 +++---- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 877eff0146..3d499f6e2d 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -16,9 +16,9 @@ import type { StatePath, StatePlan, StatePlanMap, - AdjacencyMap, + ValueAdjacencyMap, Steps, - ValueAdjMapOptions, + ValueAdjacencyMapOptions, DirectedGraphEdge, DirectedGraphNode, TraversalOptions, @@ -82,36 +82,38 @@ export function serializeEvent( return JSON.stringify(event) as SerializedEvent; } -const defaultValueAdjMapOptions: Required> = { +const defaultValueAdjacencyMapOptions: Required< + ValueAdjacencyMapOptions +> = { events: {}, filter: () => true, 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( +export function getValueAdjacencyMap( machine: TMachine, - options?: ValueAdjMapOptions, EventFrom> -): AdjacencyMap, EventFrom> { + options?: ValueAdjacencyMapOptions, EventFrom> +): ValueAdjacencyMap, EventFrom> { type TState = StateFrom; type TEvent = EventFrom; - const optionsWithDefaults = getValueAdjMapOptions(options); + const optionsWithDefaults = getValueAdjacencyMapOptions(options); const { filter, stateSerializer, eventSerializer } = optionsWithDefaults; const { events } = optionsWithDefaults; - const adjacency: AdjacencyMap = {}; + const adjacency: ValueAdjacencyMap = {}; function findAdjacencies(state: TState) { const { nextEvents } = state; @@ -409,7 +411,7 @@ export function getPathFromEvents< }; } -interface AdjMap { +interface AdjacencyMap { [key: SerializedState]: { state: TState; transitions: { @@ -424,7 +426,7 @@ interface AdjMap { export function performDepthFirstTraversal( behavior: SimpleBehavior, options: TraversalOptions -): AdjMap { +): AdjacencyMap { const { transition, initialState } = behavior; const { serializeState, @@ -432,7 +434,7 @@ export function performDepthFirstTraversal( eventCases, traversalLimit: limit } = resolveTraversalOptions(options); - const adj: AdjMap = {}; + const adj: AdjacencyMap = {}; let iterations = 0; const queue: Array<[TState, TEvent | null]> = [[initialState, null]]; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 6fb20eb857..6a2b351509 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -56,7 +56,7 @@ export type DirectedGraphNode = JSONSerializable< } >; -export interface AdjacencyMap { +export interface ValueAdjacencyMap { [stateId: SerializedState]: Record< SerializedState, { @@ -116,7 +116,7 @@ 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> diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 4330aecd77..22a1e5fe9b 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -16,7 +16,7 @@ import { StatePlan } from '../src/index'; import { - getAdjacencyMap, + getValueAdjacencyMap, traverseShortestPlans, traverseSimplePlans } from '../src/graph'; @@ -439,7 +439,7 @@ describe('@xstate/graph', () => { }); }); - describe('getAdjacencyMap', () => { + describe('getValueAdjacencyMap', () => { it('should map adjacencies', () => { interface Ctx { count: number; @@ -477,8 +477,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 = { @@ -518,7 +517,7 @@ describe('@xstate/graph', () => { } }); - const adj = getAdjacencyMap(machine, { + const adj = getValueAdjacencyMap(machine, { events: { EVENT: (state) => [ { type: 'EVENT' as const, value: state.context.count + 10 } From dda42e4c0fcbacc87c05ed5c8caf9765cb72021e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 13:28:56 +0200 Subject: [PATCH 122/394] Fixed test title and removed redundant casts to `any` --- packages/xstate-graph/test/graph.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index fa29cc39e3..9df574cc2a 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -164,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); @@ -197,13 +197,13 @@ describe('@xstate/graph', () => { describe('getShortestPaths()', () => { it('should return a mapping of shortest paths to all states', () => { - const paths = getShortestPlans(lightMachine) as any; + const paths = getShortestPlans(lightMachine); expect(getPathsMapSnapshot(paths)).toMatchSnapshot('shortest paths'); }); it('should return a mapping of shortest paths to all states (parallel)', () => { - const paths = getShortestPlans(parallelMachine) as any; + const paths = getShortestPlans(parallelMachine); expect(getPathsMapSnapshot(paths)).toMatchSnapshot( 'shortest paths parallel' ); From e54ba49d51e76e47801d86ab6d5263aaaf997d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 13:32:31 +0200 Subject: [PATCH 123/394] Adjusted signature of `getSimplePlans` to match the one for `getShortestPlans` --- packages/xstate-graph/src/graph.ts | 13 ++++--------- packages/xstate-graph/test/graph.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 0e6c02d0b0..e187d3d427 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -1,9 +1,7 @@ import type { State, - DefaultContext, Event, EventObject, - StateMachine, AnyStateMachine, AnyState, StateFrom, @@ -289,13 +287,10 @@ export function traverseShortestPlans( return Object.values(statePlanMap); } -export function getSimplePlans< - TContext = DefaultContext, - TEvent extends EventObject = EventObject ->( - machine: StateMachine, - options?: Partial, TEvent>> -): Array, TEvent>> { +export function getSimplePlans( + machine: TMachine, + options?: Partial, EventFrom>> +): Array, EventFrom>> { const resolvedOptions = resolveTraversalOptions( options, defaultMachineStateOptions diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 9df574cc2a..a5a8a55029 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -400,8 +400,8 @@ describe('@xstate/graph', () => { } }); - const paths = getSimplePlans(countMachine as any, { - getEvents: () => [{ type: 'INC', value: 1 }] + const paths = getSimplePlans(countMachine, { + getEvents: () => [{ type: 'INC', value: 1 }] as const }); expect(paths.map((p) => p.state.value)).toMatchInlineSnapshot(` From d35d83a58850f31b1b6a1d9b6d3648706be7cf46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 13:53:18 +0200 Subject: [PATCH 124/394] Move `testUtils` out of `src` directory --- packages/xstate-test/test/events.test.ts | 2 +- packages/xstate-test/test/index.test.ts | 2 +- packages/xstate-test/test/paths.test.ts | 2 +- packages/xstate-test/test/states.test.ts | 2 +- packages/xstate-test/test/testModel.test.ts | 2 +- packages/xstate-test/{src => test}/testUtils.ts | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) rename packages/xstate-test/{src => test}/testUtils.ts (79%) diff --git a/packages/xstate-test/test/events.test.ts b/packages/xstate-test/test/events.test.ts index 46d0d3de9f..da79906034 100644 --- a/packages/xstate-test/test/events.test.ts +++ b/packages/xstate-test/test/events.test.ts @@ -1,6 +1,6 @@ import { createTestModel } from '../src'; import { createTestMachine } from '../src/machine'; -import { testUtils } from '../src/testUtils'; +import { testUtils } from './testUtils'; describe('events', () => { it('should execute events (`exec` property)', async () => { diff --git a/packages/xstate-test/test/index.test.ts b/packages/xstate-test/test/index.test.ts index ef40b0358c..7218d4f038 100644 --- a/packages/xstate-test/test/index.test.ts +++ b/packages/xstate-test/test/index.test.ts @@ -1,7 +1,7 @@ import { assign, createMachine } from 'xstate'; import { createTestModel } from '../src'; import { createTestMachine } from '../src/machine'; -import { testUtils } from '../src/testUtils'; +import { testUtils } from './testUtils'; describe('events', () => { it('should allow for representing many cases', async () => { diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index d09c4f18be..7e46d4cd5c 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -1,6 +1,6 @@ import { createTestModel } from '../src'; import { createTestMachine } from '../src/machine'; -import { testUtils } from '../src/testUtils'; +import { testUtils } from './testUtils'; const multiPathMachine = createTestMachine({ initial: 'a', diff --git a/packages/xstate-test/test/states.test.ts b/packages/xstate-test/test/states.test.ts index 9b911aa90b..c17cb41f45 100644 --- a/packages/xstate-test/test/states.test.ts +++ b/packages/xstate-test/test/states.test.ts @@ -1,7 +1,7 @@ import { StateValue } from 'xstate'; import { createTestModel } from '../src'; import { createTestMachine } from '../src/machine'; -import { testUtils } from '../src/testUtils'; +import { testUtils } from './testUtils'; describe('states', () => { it('should test states by key', async () => { diff --git a/packages/xstate-test/test/testModel.test.ts b/packages/xstate-test/test/testModel.test.ts index 4f3c448130..e141f51fc3 100644 --- a/packages/xstate-test/test/testModel.test.ts +++ b/packages/xstate-test/test/testModel.test.ts @@ -1,5 +1,5 @@ import { TestModel } from '../src'; -import { testUtils } from '../src/testUtils'; +import { testUtils } from './testUtils'; describe('custom test models', () => { it('tests any behavior', async () => { diff --git a/packages/xstate-test/src/testUtils.ts b/packages/xstate-test/test/testUtils.ts similarity index 79% rename from packages/xstate-test/src/testUtils.ts rename to packages/xstate-test/test/testUtils.ts index e327918d26..15e52aaa5f 100644 --- a/packages/xstate-test/src/testUtils.ts +++ b/packages/xstate-test/test/testUtils.ts @@ -1,5 +1,5 @@ -import { TestModel } from './TestModel'; -import { TestParam, TestPath } from './types'; +import { TestModel } from '../src/TestModel'; +import { TestParam, TestPath } from '../src/types'; const testModel = async ( model: TestModel, From fe2b45f586fc37f4b899151f6e97bac4df92b77b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 25 May 2022 06:37:02 -0600 Subject: [PATCH 125/394] Update packages/xstate-test/src/types.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-test/src/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 13d76782b8..d12ce73b1e 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -115,8 +115,6 @@ export type EventExecutor = ( export interface TestModelOptions extends TraversalOptions { - // testState: (state: TState) => void | Promise; - // testTransition: (step: Step) => void | Promise; /** * Executes actions based on the `state` after the state is tested. */ From b66f42306c2582d91022be1115bc66a5bab098ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 15:28:59 +0200 Subject: [PATCH 126/394] Refactor `TestTransitionsConfigMap` to use a similar mapped type as the one in `TransitionsConfigMap` (#3335) --- packages/xstate-test/src/types.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index d12ce73b1e..e8581cd9a2 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -16,7 +16,8 @@ import { StateSchema, TransitionConfig, TypegenConstraint, - TypegenDisabled + TypegenDisabled, + ExtractEvent } from 'xstate'; export type GetPathsOptions = Partial< @@ -140,17 +141,11 @@ export type TestTransitionsConfigMap< TEvent extends EventObject, TTestContext > = { - [K in TEvent['type']]?: - | TestTransitionConfig< - TContext, - TEvent extends { type: K } ? TEvent : never, - TTestContext - > - | string; -} & { - ''?: TestTransitionConfig | string; -} & { - '*'?: TestTransitionConfig | string; + [K in TEvent['type'] | '' | '*']?: K extends '' | '*' + ? TestTransitionConfig | string + : + | TestTransitionConfig, TTestContext> + | string; }; export type PathGenerator = ( From 3c0200df6d877e0f67349aa0cb3f431fbc4b2559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 15:29:18 +0200 Subject: [PATCH 127/394] Fixed missing `serializeEvent` call (#3332) --- packages/xstate-graph/src/graph.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 877eff0146..63caf529fe 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -427,6 +427,7 @@ export function performDepthFirstTraversal( ): AdjMap { const { transition, initialState } = behavior; const { + serializeEvent, serializeState, getEvents, eventCases, @@ -461,7 +462,7 @@ export function performDepthFirstTraversal( if (!options.filter || options.filter(nextState, subEvent)) { adj[serializedState].transitions[ - JSON.stringify(subEvent) as SerializedEvent + serializeEvent(subEvent) as SerializedEvent ] = { event: subEvent, state: nextState From b83d0bec5ed957d84dfdb5f3efe4b1b480472a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 15:29:32 +0200 Subject: [PATCH 128/394] Remove `getStates` (#3333) --- packages/xstate-test/src/TestModel.ts | 1 - packages/xstate-test/src/types.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index ffa7c12b4c..29cdd70a7d 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -55,7 +55,6 @@ export class TestModel { serializeEvent: (event) => simpleStringify(event) as SerializedEvent, getEvents: () => [], stateMatcher: (_, stateKey) => stateKey === '*', - getStates: () => [], eventCases: {}, execute: () => void 0, logger: { diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index e8581cd9a2..d343c27efd 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -120,7 +120,6 @@ export interface TestModelOptions * Executes actions based on the `state` after the state is tested. */ execute: (state: TState) => void; - getStates: () => TState[]; stateMatcher: (state: TState, stateKey: string) => boolean; logger: { log: (msg: string) => void; From 76aef8e99582a38c1cb08f9be777012cd70ea13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 25 May 2022 15:30:00 +0200 Subject: [PATCH 129/394] Remove the requirement to return branded string from publicly-customizable options (#3329) * Remove the requirement to return branded string from publicly-customizable options * Removed redundant casts --- packages/xstate-graph/src/graph.ts | 15 ++++++++++----- packages/xstate-graph/src/types.ts | 4 ++-- packages/xstate-graph/test/graph.test.ts | 6 ++---- packages/xstate-graph/test/types.test.ts | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index e187d3d427..c1c6536a52 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -199,7 +199,9 @@ export function traverseShortestPlans( options?: Partial> ): Array> { const optionsWithDefaults = resolveTraversalOptions(options); - const { serializeState } = optionsWithDefaults; + const serializeState = optionsWithDefaults.serializeState as ( + ...args: Parameters + ) => SerializedState; const adjacency = performDepthFirstTraversal(behavior, optionsWithDefaults); @@ -476,13 +478,13 @@ function resolveTraversalOptions( const serializeState = traversalOptions?.serializeState ?? defaultOptions?.serializeState ?? - ((state) => JSON.stringify(state) as any); + ((state) => JSON.stringify(state)); return { serializeState, - serializeEvent: serializeEvent as any, // TODO fix types + serializeEvent, filter: () => true, visitCondition: (state, event, vctx) => { - return vctx.vertices.has(serializeState(state, event)); + return vctx.vertices.has(serializeState(state, event) as SerializedState); }, eventCases: {}, getEvents: () => [], @@ -498,7 +500,10 @@ export function traverseSimplePlans( ): Array> { const { initialState } = behavior; const resolvedOptions = resolveTraversalOptions(options); - const { serializeState, visitCondition } = resolvedOptions; + const { visitCondition } = resolvedOptions; + const serializeState = resolvedOptions.serializeState as ( + ...args: Parameters + ) => SerializedState; const adjacency = performDepthFirstTraversal(behavior, resolvedOptions); const stateMap = new Map(); const visitCtx: VisitedContext = { diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index f6359b2721..05784df326 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -135,8 +135,8 @@ export interface VisitedContext { export interface SerializationOptions { eventCases: EventCaseMap; - serializeState: (state: TState, event: TEvent | null) => SerializedState; - serializeEvent: (event: TEvent) => SerializedEvent; + serializeState: (state: TState, event: TEvent | null) => string; + serializeEvent: (event: TEvent) => string; } /** diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index a5a8a55029..869d1da72b 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -589,8 +589,7 @@ it('simple paths for reducers', () => { }, { getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], - serializeState: (v, e) => - (JSON.stringify(v) + ' | ' + JSON.stringify(e)) as any + serializeState: (v, e) => JSON.stringify(v) + ' | ' + JSON.stringify(e) } ); @@ -616,8 +615,7 @@ it('shortest paths for reducers', () => { }, { getEvents: () => [{ type: 'a' }, { type: 'b' }, { type: 'reset' }], - serializeState: (v, e) => - (JSON.stringify(v) + ' | ' + JSON.stringify(e)) as any + serializeState: (v, e) => JSON.stringify(v) + ' | ' + JSON.stringify(e) } ); diff --git a/packages/xstate-graph/test/types.test.ts b/packages/xstate-graph/test/types.test.ts index c6eec030df..eebd1e7ca5 100644 --- a/packages/xstate-graph/test/types.test.ts +++ b/packages/xstate-graph/test/types.test.ts @@ -94,4 +94,20 @@ describe('types', () => { } }); }); + + 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: () => '' + }); + }); }); From e99f70a6e0a86b5b5ed038c33aac56f4501812c1 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 25 May 2022 07:30:33 -0600 Subject: [PATCH 130/394] Add transition coverage to xstate/test --- packages/xstate-graph/src/graph.ts | 2 +- packages/xstate-test/src/TestModel.ts | 4 ++ packages/xstate-test/src/machine.ts | 23 +++++++- packages/xstate-test/src/types.ts | 1 + packages/xstate-test/test/paths.test.ts | 73 +++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 877eff0146..ec6c8cd043 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -366,7 +366,7 @@ export function getPathFromEvents< defaultMachineStateOptions as any ); - const { serializeState } = optionsWithDefaults; + const { serializeState, serializeEvent } = optionsWithDefaults; const adjacency = performDepthFirstTraversal(behavior, optionsWithDefaults); diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index ffa7c12b4c..f95f662a11 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -53,6 +53,10 @@ export class TestModel { 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 === '*', getStates: () => [], diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 85fc2ac893..9bbdb702d7 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -1,4 +1,4 @@ -import { serializeState, SimpleBehavior } from '@xstate/graph'; +import { SerializedState, serializeState, SimpleBehavior } from '@xstate/graph'; import { ActionObject, AnyEventObject, @@ -53,6 +53,16 @@ export function executeAction( } } +function serializeMachineTransition(state: AnyState): string { + return state.transitions + .map((t) => { + const guardName = t.cond?.name; + const guardString = guardName ? `[${guardName}]` : ''; + return `${t.eventType}${guardString}`; + }) + .join(','); +} + /** * Creates a test model that represents an abstract model of a * system under test (SUT). @@ -83,10 +93,19 @@ export function createTestModel( ): TestModel, EventFrom> { validateMachine(machine); + const serializeTransition = + options?.serializeTransition ?? serializeMachineTransition; + const testModel = new TestModel, EventFrom>( machine as SimpleBehavior, { - serializeState, + serializeState: (state, event) => { + const serializedTransition = serializeTransition(state, event); + + return `${serializeState(state)}${ + serializedTransition ? ` via ${serializedTransition}` : '' + }` as SerializedState; + }, stateMatcher: (state, key) => { return key.startsWith('#') ? state.configuration.includes(machine.getStateNodeById(key)) diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 13d76782b8..b5e19c7ffb 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -127,6 +127,7 @@ export interface TestModelOptions log: (msg: string) => void; error: (msg: string) => void; }; + serializeTransition: (state: TState, event: TEvent | null) => string; } export interface TestTransitionConfig< diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index d09c4f18be..4e89529ebe 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -115,3 +115,76 @@ describe('path.description', () => { ]); }); }); + +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})", + ] + `); + }); +}); From f8db8162326f587ddf0bad85ed23bc18752c7f9f Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Wed, 25 May 2022 07:42:51 -0600 Subject: [PATCH 131/394] Added readme --- packages/xstate-test/README.md | 144 +++++++++++++++------------------ 1 file changed, 64 insertions(+), 80 deletions(-) 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 + }); }); }); ``` From ae37040b373e4539fa9624797c0699c3b6a3cc1b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 25 May 2022 07:50:00 -0600 Subject: [PATCH 132/394] Update snapshot --- packages/xstate-test/test/dieHard.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index f569f351cc..44c92123a2 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -270,10 +270,10 @@ describe('error path trace', () => { State: {\\"value\\":\\"first\\"} Event: {\\"type\\":\\"NEXT\\"} - State: {\\"value\\":\\"second\\"} + State: {\\"value\\":\\"second\\"} via NEXT Event: {\\"type\\":\\"NEXT\\"} - State: {\\"value\\":\\"third\\"}" + State: {\\"value\\":\\"third\\"} via NEXT" `); return; } From 15111433b034d277ff751c66079a230b1fc36651 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 25 May 2022 15:00:32 -0600 Subject: [PATCH 133/394] Remove visitContext as option --- packages/xstate-graph/src/graph.ts | 6 +----- packages/xstate-graph/src/types.ts | 5 ----- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index bcb2f8f680..2522135718 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -486,9 +486,6 @@ function resolveTraversalOptions( serializeState, serializeEvent, filter: () => true, - visitCondition: (state, event, vctx) => { - return vctx.vertices.has(serializeState(state, event) as SerializedState); - }, eventCases: {}, getEvents: () => [], traversalLimit: Infinity, @@ -503,7 +500,6 @@ export function traverseSimplePlans( ): Array> { const { initialState } = behavior; const resolvedOptions = resolveTraversalOptions(options); - const { visitCondition } = resolvedOptions; const serializeState = resolvedOptions.serializeState as ( ...args: Parameters ) => SerializedState; @@ -559,7 +555,7 @@ export function traverseSimplePlans( const nextStateSerial = serializeState(nextState, subEvent); stateMap.set(nextStateSerial, nextState); - if (!visitCondition(nextState, subEvent, visitCtx)) { + if (!visitCtx.vertices.has(serializeState(nextState, subEvent))) { visitCtx.edges.add(serializedEvent); path.push({ state: stateMap.get(fromStateSerial)!, diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index 5a6f8913ad..a3eac716e2 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -156,11 +156,6 @@ type EventCase = Omit; export interface TraversalOptions extends SerializationOptions { - visitCondition?: ( - state: TState, - event: TEvent, - vctx: VisitedContext - ) => boolean; filter?: (state: TState, event: TEvent) => boolean; getEvents?: ( state: TState, From d62a792b0aa84b3ca0a907cf7c174438e0589a20 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 25 May 2022 17:15:10 -0600 Subject: [PATCH 134/394] Consider all cases --- packages/xstate-test/src/machine.ts | 33 +++++++++++++---------- packages/xstate-test/test/dieHard.test.ts | 4 +-- packages/xstate-test/test/paths.test.ts | 1 + 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 9bbdb702d7..0f7b422db5 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -17,7 +17,7 @@ import { TestMachineOptions, TestModelOptions } from './types'; -import { flatten } from './utils'; +import { flatten, simpleStringify } from './utils'; import { validateMachine } from './validateMachine'; export async function testStateFromMeta(state: AnyState) { @@ -53,14 +53,20 @@ export function executeAction( } } -function serializeMachineTransition(state: AnyState): string { - return state.transitions - .map((t) => { - const guardName = t.cond?.name; - const guardString = guardName ? `[${guardName}]` : ''; - return `${t.eventType}${guardString}`; - }) - .join(','); +function serializeMachineTransition( + state: AnyState, + event: AnyEventObject | null, + { serializeEvent }: { serializeEvent: (event: AnyEventObject) => string } +): string { + if (!event) { + return ''; + } + + const causedTransition = state.transitions.find( + (t) => t.eventType === event.type + ); + + return causedTransition ? ` via ${serializeEvent(event)}` : ''; } /** @@ -93,6 +99,7 @@ export function createTestModel( ): TestModel, EventFrom> { validateMachine(machine); + const serializeEvent = options?.serializeEvent ?? simpleStringify; const serializeTransition = options?.serializeTransition ?? serializeMachineTransition; @@ -100,11 +107,9 @@ export function createTestModel( machine as SimpleBehavior, { serializeState: (state, event) => { - const serializedTransition = serializeTransition(state, event); - - return `${serializeState(state)}${ - serializedTransition ? ` via ${serializedTransition}` : '' - }` as SerializedState; + return `${serializeState(state)}${serializeTransition(state, event, { + serializeEvent + })}` as SerializedState; }, stateMatcher: (state, key) => { return key.startsWith('#') diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 44c92123a2..3c4873426a 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -270,10 +270,10 @@ describe('error path trace', () => { State: {\\"value\\":\\"first\\"} Event: {\\"type\\":\\"NEXT\\"} - State: {\\"value\\":\\"second\\"} via NEXT + State: {\\"value\\":\\"second\\"} via {\\"type\\":\\"NEXT\\"} Event: {\\"type\\":\\"NEXT\\"} - State: {\\"value\\":\\"third\\"} via NEXT" + State: {\\"value\\":\\"third\\"}" `); return; } diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index 83a406bd19..b440653075 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -184,6 +184,7 @@ describe('transition coverage', () => { Array [ "Reaches state \\"#(machine).b\\": NEXT ({\\"value\\":0})", "Reaches state \\"#(machine).b\\": NEXT ({\\"value\\":100})", + "Reaches state \\"#(machine).b\\": NEXT ({\\"value\\":1000})", ] `); }); From 9b96eba8dde6e9804bb713f8166b83886a9c7a98 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 26 May 2022 10:12:29 -0600 Subject: [PATCH 135/394] Simplify machine transition serialization logic --- packages/xstate-test/src/machine.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 0f7b422db5..580f486f50 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -58,15 +58,13 @@ function serializeMachineTransition( event: AnyEventObject | null, { serializeEvent }: { serializeEvent: (event: AnyEventObject) => string } ): string { - if (!event) { + // Only consider the transition via the serialized event if there actually + // was a defined transition for the event + if (!event || state.transitions.length === 0) { return ''; } - const causedTransition = state.transitions.find( - (t) => t.eventType === event.type - ); - - return causedTransition ? ` via ${serializeEvent(event)}` : ''; + return ` via ${serializeEvent(event)}`; } /** From 7cea8a69074c0dd9cd4bdab3e5f289dd15f0fc5a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 26 May 2022 11:30:29 -0600 Subject: [PATCH 136/394] Rework serializeMachineTransition to use event from state --- packages/xstate-test/src/TestModel.ts | 4 +--- packages/xstate-test/src/machine.ts | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index f125bce152..3c03fb9ba4 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -138,10 +138,8 @@ export class TestModel { statePredicate: StatePredicate, testPaths: Array> ): Array> { - const predicate: StatePredicate = (state) => statePredicate(state); - return testPaths.filter((testPath) => { - return predicate(testPath.state); + return statePredicate(testPath.state); }); } diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 580f486f50..d3f14861ba 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -55,16 +55,16 @@ export function executeAction( function serializeMachineTransition( state: AnyState, - event: AnyEventObject | null, + _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 (!event || state.transitions.length === 0) { + if (!state.event || state.transitions.length === 0) { return ''; } - return ` via ${serializeEvent(event)}`; + return ` via ${serializeEvent(state.event)}`; } /** From 79a2495496f5ba0e03b9bcfa0d9810e144f97d31 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 26 May 2022 11:37:21 -0600 Subject: [PATCH 137/394] Update test --- packages/xstate-test/test/dieHard.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/test/dieHard.test.ts b/packages/xstate-test/test/dieHard.test.ts index 3c4873426a..810d88701b 100644 --- a/packages/xstate-test/test/dieHard.test.ts +++ b/packages/xstate-test/test/dieHard.test.ts @@ -273,7 +273,7 @@ describe('error path trace', () => { State: {\\"value\\":\\"second\\"} via {\\"type\\":\\"NEXT\\"} Event: {\\"type\\":\\"NEXT\\"} - State: {\\"value\\":\\"third\\"}" + State: {\\"value\\":\\"third\\"} via {\\"type\\":\\"NEXT\\"}" `); return; } From 09b7d49dcde8535edf20fb1a1d4d6eedd5a194e6 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 27 May 2022 16:18:33 +0100 Subject: [PATCH 138/394] Fixed issue found in testing --- packages/xstate-react/test/useSpawn.test.tsx | 2 +- packages/xstate-test/src/TestModel.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index 3c03fb9ba4..a1f6b5ed6c 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -10,7 +10,7 @@ import { traverseSimplePathsTo } from '@xstate/graph'; import { EventObject } from 'xstate'; -import { isStateLike } from 'xstate/src/utils'; +import { isStateLike } from 'xstate/lib/utils'; import { pathGeneratorWithDedup } from './dedupPaths'; import { getShortestPaths, getSimplePaths } from './pathGenerators'; import type { From e2a99426cb1261d6ca329f9f8988c5976b990130 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Fri, 27 May 2022 16:57:18 +0100 Subject: [PATCH 139/394] Added a failing test to cover multiple transitions with the same target but different sources --- packages/xstate-test/test/paths.test.ts | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index b440653075..6d0b50c77a 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -188,4 +188,37 @@ describe('transition coverage', () => { ] `); }); + + it('transition coverage should consider multiple transitions with the same target', () => { + const machine = createTestMachine({ + initial: 'a', + states: { + a: { + on: { + GO_TO_B: 'b', + GO_TO_C: 'c' + } + }, + b: { + on: { + GO_TO_A: 'a' + } + }, + c: { + on: { + GO_TO_A: 'a' + } + } + } + }); + + const model = createTestModel(machine); + + const paths = model.getPaths(); + + expect(paths.map((p) => p.description)).toEqual([ + `Reaches state "#(machine).a": GO_TO_B → GO_TO_A`, + `Reaches state "#(machine).a": GO_TO_C → GO_TO_A` + ]); + }); }); From 1118bf4d707c1d8124ea6ba19d235abbcb9e0a7a Mon Sep 17 00:00:00 2001 From: Adham Moussa Date: Sat, 28 May 2022 17:27:24 +0700 Subject: [PATCH 140/394] Update tutorials.md Fix broken url for tutorial "Creating a Complex IVR System with Ease with XState" --- docs/about/tutorials.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about/tutorials.md b/docs/about/tutorials.md index 388ac23555..c2a4e75202 100644 --- a/docs/about/tutorials.md +++ b/docs/about/tutorials.md @@ -7,7 +7,7 @@ - [XState - a TypeScript state machine with a lot of features](http://realfiction.net/2019/01/30/xstate-a-typescript-state-machine-with-a-lot-of-features) by Frank Quednau - An overview of creating state machines with XState and TypeScript - Includes a [full React example](https://github.com/flq/test-of-xstate) -- [Creating a Complex IVR System with Ease with XState](https://www.nexmo.com/blog/2019/06/20/creating-a-complex-ivr-system-with-ease-with-xstate/) by Yonatan Mevorach +- [Creating a Complex IVR System with Ease with XState](https://developer.vonage.com/blog/19/06/20/creating-a-complex-ivr-system-with-ease-with-xstate-dr) by Yonatan Mevorach - Tutorial on how to create an IVR (interactive voice response) system with XState, Express, and Nexmo - [My love letter to XState and statecharts ♥](https://dev.to/timdeschryver/my-love-letter-to-xstate-and-statecharts-287b) by [Tim Deschryver](https://dev.to/timdeschryver) - Overview of the benefits and experience of developing with XState From ae673e443aab1e42e26fbe16ea1e9dab784d99be Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 30 May 2022 12:17:08 +0100 Subject: [PATCH 141/394] Added changesets (#3346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added changesets * Added section on new event case behaviour * Update .changeset/lazy-turtles-bread.md Co-authored-by: David Khourshid * Update .changeset/lazy-turtles-grand.md Co-authored-by: David Khourshid * Update .changeset/lazy-turtles-great.md Co-authored-by: David Khourshid * Update .changeset/curly-windows-burn.md Co-authored-by: Mateusz Burzyński * Update .changeset/great-lions-buy.md Co-authored-by: Mateusz Burzyński * Added pr: 3036 * Fix * Apply suggestions from code review Co-authored-by: David Khourshid * Fixed feedback Co-authored-by: David Khourshid Co-authored-by: Mateusz Burzyński --- .changeset/curly-windows-burn.md | 8 +++++ .changeset/curly-windows-learn.md | 10 ++++++ .changeset/great-lions-buy.md | 12 ++++++++ .changeset/great-spies-exist.md | 51 ------------------------------- .changeset/lazy-turtles-brand.md | 37 ++++++++++++++++++++++ .changeset/lazy-turtles-bread.md | 8 +++++ .changeset/lazy-turtles-grand.md | 10 ++++++ .changeset/lazy-turtles-grate.md | 47 ++++++++++++++++++++++++++++ .changeset/lazy-turtles-great.md | 8 +++++ .changeset/lazy-turtles-mate.md | 28 +++++++++++++++++ .changeset/lazy-turtles-trade.md | 21 +++++++++++++ 11 files changed, 189 insertions(+), 51 deletions(-) create mode 100644 .changeset/curly-windows-burn.md create mode 100644 .changeset/curly-windows-learn.md create mode 100644 .changeset/great-lions-buy.md delete mode 100644 .changeset/great-spies-exist.md create mode 100644 .changeset/lazy-turtles-brand.md create mode 100644 .changeset/lazy-turtles-bread.md create mode 100644 .changeset/lazy-turtles-grand.md create mode 100644 .changeset/lazy-turtles-grate.md create mode 100644 .changeset/lazy-turtles-great.md create mode 100644 .changeset/lazy-turtles-mate.md create mode 100644 .changeset/lazy-turtles-trade.md 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/great-spies-exist.md b/.changeset/great-spies-exist.md deleted file mode 100644 index 30456cbe42..0000000000 --- a/.changeset/great-spies-exist.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@xstate/test': major ---- - -Coverage can now be obtained from a tested test model via `testModel.getCoverage(...)`: - -```js -import { configure, createTestModel } from '@xstate/test'; -import { - coversAllStates, - coversAllTransitions -} from '@xstate/test/lib/coverage'; - -// ... - -const testModel = createTestModel(someMachine); - -const plans = testModel.getShortestPlans(); - -for (const plan of plans) { - await testModel.testPlan(plan); -} - -// Returns default coverage: -// - state coverage -// - transition coverage -const coverage = testModel.getCoverage(); - -// Returns state coverage -const stateCoverage = testModel.getCoverage([coversAllStates()]); - -// Returns state coverage -const stateCoverage = testModel.getCoverage([coversAllStates()]); - -// Throws error for default coverage: -// - state coverage -// - transition coverage -testModel.testCoverage(); - -// Throws error if state coverage not met -testModel.testCoverage([stateCoverage]); - -// Filters state coverage -testModel.testCoverage( - coversAllStates({ - filter: (stateNode) => { - return stateNode.key !== 'somePassedState'; - } - }) -); -``` 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. From 252a824646b7bf019c3b5bb247fb91781015e3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 30 May 2022 13:27:17 +0200 Subject: [PATCH 142/394] Enter pre mode for `@xstate/test@alpha` release --- .changeset/pre.json | 29 ++++++ packages/xstate-graph/CHANGELOG.md | 14 +++ packages/xstate-graph/package.json | 2 +- packages/xstate-test/CHANGELOG.md | 149 +++++++++++++++++++++++++++++ packages/xstate-test/package.json | 4 +- 5 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..bff9f654cc --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,29 @@ +{ + "mode": "pre", + "tag": "alpha", + "initialVersions": { + "xstate": "4.32.1", + "@xstate/analytics": "0.0.1", + "@xstate/fsm": "2.0.0", + "@xstate/graph": "2.0.0-alpha.0", + "@xstate/immer": "0.3.1", + "@xstate/inspect": "0.7.0", + "@xstate/react": "3.0.0", + "@xstate/scxml": "0.2.1", + "@xstate/svelte": "2.0.0", + "@xstate/test": "1.0.0-alpha.0", + "@xstate/vue": "2.0.0" + }, + "changesets": [ + "curly-windows-burn", + "curly-windows-learn", + "great-lions-buy", + "lazy-turtles-brand", + "lazy-turtles-bread", + "lazy-turtles-grand", + "lazy-turtles-grate", + "lazy-turtles-great", + "lazy-turtles-mate", + "lazy-turtles-trade" + ] +} diff --git a/packages/xstate-graph/CHANGELOG.md b/packages/xstate-graph/CHANGELOG.md index e63253a588..65d9160d50 100644 --- a/packages/xstate-graph/CHANGELOG.md +++ b/packages/xstate-graph/CHANGELOG.md @@ -1,5 +1,19 @@ # @xstate/graph +## 2.0.0-alpha.0 + +### Major Changes + +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @davidkpiano + + Renamed `getAdjacencyMap` to `getValueAdjacencyMap`. + +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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[]`. + ## 1.4.2 ### Patch Changes diff --git a/packages/xstate-graph/package.json b/packages/xstate-graph/package.json index 69d31cb6cb..d8c370df45 100644 --- a/packages/xstate-graph/package.json +++ b/packages/xstate-graph/package.json @@ -1,6 +1,6 @@ { "name": "@xstate/graph", - "version": "1.4.2", + "version": "2.0.0-alpha.0", "description": "XState graph utilities", "keywords": [ "state", diff --git a/packages/xstate-test/CHANGELOG.md b/packages/xstate-test/CHANGELOG.md index faf5f9d3b8..1f9dfaabdf 100644 --- a/packages/xstate-test/CHANGELOG.md +++ b/packages/xstate-test/CHANGELOG.md @@ -1,5 +1,154 @@ # @xstate/test +## 1.0.0-alpha.0 + +### Major Changes + +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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. + +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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. + +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @davidkpiano + + Removed `.testCoverage()`, and instead made `getPlans`, `getShortestPlans` and `getSimplePlans` cover all states and transitions enabled by event cases by default. + +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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. + +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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 + ``` + +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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. + +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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. + +### Minor Changes + +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @mattpocock @davidkpiano + + Added `path.testSync(...)` to allow for testing paths in sync-only environments, such as Cypress. + +### Patch Changes + +- Updated dependencies [[`ae673e443`](https://github.com/statelyai/xstate/commit/ae673e443aab1e42e26fbe16ea1e9dab784d99be), [`ae673e443`](https://github.com/statelyai/xstate/commit/ae673e443aab1e42e26fbe16ea1e9dab784d99be)]: + - @xstate/graph@2.0.0-alpha.0 + ## 0.5.1 ### Patch Changes diff --git a/packages/xstate-test/package.json b/packages/xstate-test/package.json index 6af5f053b0..b9dfefeed5 100644 --- a/packages/xstate-test/package.json +++ b/packages/xstate-test/package.json @@ -1,6 +1,6 @@ { "name": "@xstate/test", - "version": "0.5.1", + "version": "1.0.0-alpha.0", "description": "XState test utilities", "keywords": [ "state", @@ -51,6 +51,6 @@ "xstate": "*" }, "dependencies": { - "@xstate/graph": "^1.4.2" + "@xstate/graph": "^2.0.0-alpha.0" } } From b1bb81012f4cf400dfa6c81f913133f3e7f1adcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 30 May 2022 13:35:59 +0200 Subject: [PATCH 143/394] Move `@xstate/test@alpha` changesets to a separate directory --- .changeset-xstate-test-alpha/config.json | 13 ++++++++ .../curly-windows-burn.md | 2 +- .../curly-windows-learn.md | 2 +- .../great-lions-buy.md | 4 +-- .../lazy-turtles-brand.md | 2 +- .../lazy-turtles-bread.md | 2 +- .../lazy-turtles-grand.md | 2 +- .../lazy-turtles-grate.md | 2 +- .../lazy-turtles-great.md | 3 +- .../lazy-turtles-mate.md | 3 +- .../lazy-turtles-trade.md | 3 +- .../pre.json | 0 packages/xstate-graph/CHANGELOG.md | 8 ++--- packages/xstate-test/CHANGELOG.md | 33 +++++-------------- 14 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 .changeset-xstate-test-alpha/config.json rename {.changeset => .changeset-xstate-test-alpha}/curly-windows-burn.md (81%) rename {.changeset => .changeset-xstate-test-alpha}/curly-windows-learn.md (95%) rename {.changeset => .changeset-xstate-test-alpha}/great-lions-buy.md (90%) rename {.changeset => .changeset-xstate-test-alpha}/lazy-turtles-brand.md (96%) rename {.changeset => .changeset-xstate-test-alpha}/lazy-turtles-bread.md (90%) rename {.changeset => .changeset-xstate-test-alpha}/lazy-turtles-grand.md (94%) rename {.changeset => .changeset-xstate-test-alpha}/lazy-turtles-grate.md (97%) rename {.changeset => .changeset-xstate-test-alpha}/lazy-turtles-great.md (77%) rename {.changeset => .changeset-xstate-test-alpha}/lazy-turtles-mate.md (89%) rename {.changeset => .changeset-xstate-test-alpha}/lazy-turtles-trade.md (89%) rename {.changeset => .changeset-xstate-test-alpha}/pre.json (100%) diff --git a/.changeset-xstate-test-alpha/config.json b/.changeset-xstate-test-alpha/config.json new file mode 100644 index 0000000000..9dd1c40500 --- /dev/null +++ b/.changeset-xstate-test-alpha/config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "statelyai/xstate" }], + "commit": false, + "linked": [], + "access": "public", + "baseBranch": "main", + "ignore": ["@xstate/analytics", "@xstate/scxml"], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true, + "useCalculatedVersionForSnapshots": true + } +} diff --git a/.changeset/curly-windows-burn.md b/.changeset-xstate-test-alpha/curly-windows-burn.md similarity index 81% rename from .changeset/curly-windows-burn.md rename to .changeset-xstate-test-alpha/curly-windows-burn.md index 94574203d0..42604e8ce4 100644 --- a/.changeset/curly-windows-burn.md +++ b/.changeset-xstate-test-alpha/curly-windows-burn.md @@ -3,6 +3,6 @@ --- pr: #3036 -@author: @davidkpiano +author: @davidkpiano Renamed `getAdjacencyMap` to `getValueAdjacencyMap`. diff --git a/.changeset/curly-windows-learn.md b/.changeset-xstate-test-alpha/curly-windows-learn.md similarity index 95% rename from .changeset/curly-windows-learn.md rename to .changeset-xstate-test-alpha/curly-windows-learn.md index ad7dd7e3ca..e97fe43e83 100644 --- a/.changeset/curly-windows-learn.md +++ b/.changeset-xstate-test-alpha/curly-windows-learn.md @@ -3,7 +3,7 @@ --- pr: #3036 -@author: @davidkpiano +author: @davidkpiano Changed `getSimplePaths` to `getSimplePlans`, and `getShortestPaths` to `getShortestPlans`. Both of these functions can be passed a machine, and return `StatePlan[]`. diff --git a/.changeset/great-lions-buy.md b/.changeset-xstate-test-alpha/great-lions-buy.md similarity index 90% rename from .changeset/great-lions-buy.md rename to .changeset-xstate-test-alpha/great-lions-buy.md index 450cc32ebb..7f9699aa7b 100644 --- a/.changeset/great-lions-buy.md +++ b/.changeset-xstate-test-alpha/great-lions-buy.md @@ -4,8 +4,8 @@ pr: #3036 -@author: @mattpocock -@author: @davidkpiano +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. diff --git a/.changeset/lazy-turtles-brand.md b/.changeset-xstate-test-alpha/lazy-turtles-brand.md similarity index 96% rename from .changeset/lazy-turtles-brand.md rename to .changeset-xstate-test-alpha/lazy-turtles-brand.md index 92aeac2e30..2fa4eb8a18 100644 --- a/.changeset/lazy-turtles-brand.md +++ b/.changeset-xstate-test-alpha/lazy-turtles-brand.md @@ -3,7 +3,7 @@ --- pr: #3036 -@author: @mattpocock +author: @mattpocock Moved event cases out of `events`, and into their own attribute called `eventCases`: diff --git a/.changeset/lazy-turtles-bread.md b/.changeset-xstate-test-alpha/lazy-turtles-bread.md similarity index 90% rename from .changeset/lazy-turtles-bread.md rename to .changeset-xstate-test-alpha/lazy-turtles-bread.md index d8b7f86588..78b5e21fe8 100644 --- a/.changeset/lazy-turtles-bread.md +++ b/.changeset-xstate-test-alpha/lazy-turtles-bread.md @@ -3,6 +3,6 @@ --- pr: #3036 -@author: @davidkpiano +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-xstate-test-alpha/lazy-turtles-grand.md similarity index 94% rename from .changeset/lazy-turtles-grand.md rename to .changeset-xstate-test-alpha/lazy-turtles-grand.md index 2890e97ab5..80fdb1529a 100644 --- a/.changeset/lazy-turtles-grand.md +++ b/.changeset-xstate-test-alpha/lazy-turtles-grand.md @@ -3,7 +3,7 @@ --- pr: #3036 -@author: @davidkpiano +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`. diff --git a/.changeset/lazy-turtles-grate.md b/.changeset-xstate-test-alpha/lazy-turtles-grate.md similarity index 97% rename from .changeset/lazy-turtles-grate.md rename to .changeset-xstate-test-alpha/lazy-turtles-grate.md index a6c172df56..16d727aa79 100644 --- a/.changeset/lazy-turtles-grate.md +++ b/.changeset-xstate-test-alpha/lazy-turtles-grate.md @@ -3,7 +3,7 @@ --- pr: #3036 -@author: @davidkpiano +author: @davidkpiano `getShortestPaths()` and `getPaths()` will now traverse all _transitions_ by default, not just all events. diff --git a/.changeset/lazy-turtles-great.md b/.changeset-xstate-test-alpha/lazy-turtles-great.md similarity index 77% rename from .changeset/lazy-turtles-great.md rename to .changeset-xstate-test-alpha/lazy-turtles-great.md index 0d715f2542..fb046d7657 100644 --- a/.changeset/lazy-turtles-great.md +++ b/.changeset-xstate-test-alpha/lazy-turtles-great.md @@ -3,6 +3,7 @@ --- pr: #3036 -@author: @mattpocock @davidkpiano +author: @mattpocock +author: @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-xstate-test-alpha/lazy-turtles-mate.md similarity index 89% rename from .changeset/lazy-turtles-mate.md rename to .changeset-xstate-test-alpha/lazy-turtles-mate.md index 2f3e816355..76b9d88938 100644 --- a/.changeset/lazy-turtles-mate.md +++ b/.changeset-xstate-test-alpha/lazy-turtles-mate.md @@ -3,7 +3,8 @@ --- pr: #3036 -@author: @mattpocock @davidkpiano +author: @mattpocock +author: @davidkpiano Moved `events` from `createTestModel` to `path.test`. diff --git a/.changeset/lazy-turtles-trade.md b/.changeset-xstate-test-alpha/lazy-turtles-trade.md similarity index 89% rename from .changeset/lazy-turtles-trade.md rename to .changeset-xstate-test-alpha/lazy-turtles-trade.md index d0ca4744bf..242b8083a0 100644 --- a/.changeset/lazy-turtles-trade.md +++ b/.changeset-xstate-test-alpha/lazy-turtles-trade.md @@ -3,7 +3,8 @@ --- pr: #3036 -@author: @mattpocock @davidkpiano +author: @mattpocock +author: @davidkpiano Added `states` to `path.test()`: diff --git a/.changeset/pre.json b/.changeset-xstate-test-alpha/pre.json similarity index 100% rename from .changeset/pre.json rename to .changeset-xstate-test-alpha/pre.json diff --git a/packages/xstate-graph/CHANGELOG.md b/packages/xstate-graph/CHANGELOG.md index 65d9160d50..af6bfca11e 100644 --- a/packages/xstate-graph/CHANGELOG.md +++ b/packages/xstate-graph/CHANGELOG.md @@ -4,13 +4,9 @@ ### Major Changes -- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @davidkpiano +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Renamed `getAdjacencyMap` to `getValueAdjacencyMap`. - Renamed `getAdjacencyMap` to `getValueAdjacencyMap`. - -* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @davidkpiano - - Changed `getSimplePaths` to `getSimplePlans`, and `getShortestPaths` to `getShortestPlans`. Both of these functions can be passed a machine, and return `StatePlan[]`. +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/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/packages/xstate-test/CHANGELOG.md b/packages/xstate-test/CHANGELOG.md index 1f9dfaabdf..4b8413f077 100644 --- a/packages/xstate-test/CHANGELOG.md +++ b/packages/xstate-test/CHANGELOG.md @@ -4,16 +4,11 @@ ### Major Changes -- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @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. +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@mattpocock](https://github.com/mattpocock), [@davidkpiano](https://github.com/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. -* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @mattpocock - - Moved event cases out of `events`, and into their own attribute called `eventCases`: +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@mattpocock](https://github.com/mattpocock)! - Moved event cases out of `events`, and into their own attribute called `eventCases`: ```ts const model = createTestModel(machine, { @@ -44,19 +39,13 @@ `eventCases` will also now always produce a new path, instead of only creating a path for the first case which matches. -- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @davidkpiano - - Removed `.testCoverage()`, and instead made `getPlans`, `getShortestPlans` and `getSimplePlans` cover all states and transitions enabled by event cases by default. - -* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @davidkpiano +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Removed `.testCoverage()`, and instead made `getPlans`, `getShortestPlans` and `getSimplePlans` cover all states and transitions enabled by event cases by default. - 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`. +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/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. -- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @davidkpiano - - `getShortestPaths()` and `getPaths()` will now traverse all _transitions_ by default, not just all events. +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - `getShortestPaths()` and `getPaths()` will now traverse all _transitions_ by default, not just all events. Take this machine: @@ -97,9 +86,7 @@ toggledOn -> TOGGLE -> toggledOff -> TOGGLE -> toggledOn ``` -* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @mattpocock @davidkpiano - - Moved `events` from `createTestModel` to `path.test`. +* [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@mattpocock](https://github.com/mattpocock), [@davidkpiano](https://github.com/davidkpiano)! - Moved `events` from `createTestModel` to `path.test`. Old: @@ -121,9 +108,7 @@ This allows for easier usage of per-test mocks and per-test context. -- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @mattpocock @davidkpiano - - Added `states` to `path.test()`: +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@mattpocock](https://github.com/mattpocock), [@davidkpiano](https://github.com/davidkpiano)! - Added `states` to `path.test()`: ```ts const paths = model.getPaths().forEach((path) => { @@ -140,9 +125,7 @@ ### Minor Changes -- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@davidkpiano](https://github.com/davidkpiano)! - @author: @mattpocock @davidkpiano - - Added `path.testSync(...)` to allow for testing paths in sync-only environments, such as Cypress. +- [#3036](https://github.com/statelyai/xstate/pull/3036) Thanks [@mattpocock](https://github.com/mattpocock), [@davidkpiano](https://github.com/davidkpiano)! - Added `path.testSync(...)` to allow for testing paths in sync-only environments, such as Cypress. ### Patch Changes From a0063f2bbed5bbc3379c62fc60d56359135ae829 Mon Sep 17 00:00:00 2001 From: Chris Field Date: Mon, 30 May 2022 16:48:49 +0200 Subject: [PATCH 144/394] Fixed a dead link Fixed dead link to the introduction page of immer. --- docs/packages/xstate-immer/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/packages/xstate-immer/index.md b/docs/packages/xstate-immer/index.md index 8cf4c3a17b..bf1dd85380 100644 --- a/docs/packages/xstate-immer/index.md +++ b/docs/packages/xstate-immer/index.md @@ -11,7 +11,7 @@

-The [@xstate/immer package](https://github.com/statelyai/xstate/tree/main/packages/xstate-immer) contains utilities for using [Immer](https://immerjs.github.io/immer/docs/introduction) with [XState](https://github.com/statelyai/xstate). +The [@xstate/immer package](https://github.com/statelyai/xstate/tree/main/packages/xstate-immer) contains utilities for using [Immer](https://immerjs.github.io/immer) with [XState](https://github.com/statelyai/xstate). From d545f0dd9a3df155ecc909c12f1888ef6cc67152 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 30 May 2022 19:14:02 -0400 Subject: [PATCH 145/394] Add prevState to serializeState --- packages/xstate-graph/src/graph.ts | 68 ++++++++++++++++++++--------- packages/xstate-graph/src/types.ts | 6 ++- packages/xstate-test/src/machine.ts | 22 +++++++--- packages/xstate-test/src/types.ts | 6 ++- 4 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 2522135718..caabcac6a9 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -217,7 +217,11 @@ export function traverseShortestPlans( ] >(); const stateMap = new Map(); - const initialSerializedState = serializeState(behavior.initialState, null); + const initialSerializedState = serializeState( + behavior.initialState, + undefined, + undefined + ); stateMap.set(initialSerializedState, behavior.initialState); weightMap.set(initialSerializedState, [0, undefined, undefined]); @@ -234,7 +238,12 @@ export function traverseShortestPlans( const { state: nextState, event: eventObject } = adjacency[ serializedState ].transitions[event]; - const nextSerializedState = serializeState(nextState, eventObject); + const prevState = stateMap.get(serializedState); + const nextSerializedState = serializeState( + nextState, + eventObject, + prevState + ); stateMap.set(nextSerializedState, nextState); if (!weightMap.has(nextSerializedState)) { weightMap.set(nextSerializedState, [ @@ -369,10 +378,13 @@ export function getPathFromEvents< const adjacency = performDepthFirstTraversal(behavior, optionsWithDefaults); - const stateMap = new Map(); + const stateMap = new Map(); const path: Steps = []; - const initialStateSerial = serializeState(behavior.initialState, null); + const initialStateSerial = serializeState( + behavior.initialState, + undefined + ) as SerializedState; stateMap.set(initialStateSerial, behavior.initialState); let stateSerial = initialStateSerial; @@ -393,8 +405,12 @@ export function getPathFromEvents< `Invalid transition from ${stateSerial} with ${eventSerial}` ); } - - const nextStateSerial = serializeState(nextState, event); + const prevState = stateMap.get(stateSerial); + const nextStateSerial = serializeState( + nextState, + event, + prevState + ) as SerializedState; stateMap.set(nextStateSerial, nextState); stateSerial = nextStateSerial; @@ -435,19 +451,31 @@ export function performDepthFirstTraversal( const adj: AdjacencyMap = {}; let iterations = 0; - const queue: Array<[TState, TEvent | null]> = [[initialState, null]]; + const queue: Array< + [ + nextState: TState, + event: TEvent | undefined, + prevState: TState | undefined + ] + > = [[initialState, undefined, undefined]]; + const stateMap = new Map(); while (queue.length) { - const [state, event] = queue.shift()!; + const [state, event, prevState] = queue.shift()!; if (iterations++ > limit) { throw new Error('Traversal limit exceeded'); } - const serializedState = serializeState(state, event); + const serializedState = serializeState( + state, + event, + prevState + ) as SerializedState; if (adj[serializedState]) { continue; } + stateMap.set(serializedState, state); adj[serializedState] = { state, @@ -466,7 +494,7 @@ export function performDepthFirstTraversal( event: subEvent, state: nextState }; - queue.push([nextState, subEvent]); + queue.push([nextState, subEvent, state]); } } } @@ -504,7 +532,7 @@ export function traverseSimplePlans( ...args: Parameters ) => SerializedState; const adjacency = performDepthFirstTraversal(behavior, resolvedOptions); - const stateMap = new Map(); + const stateMap = new Map(); const visitCtx: VisitedContext = { vertices: new Set(), edges: new Set() @@ -516,11 +544,10 @@ export function traverseSimplePlans( > = {}; function util( - fromState: TState, - toStateSerial: SerializedState, - event: TEvent | null + fromStateSerial: SerializedState, + toStateSerial: SerializedState ) { - const fromStateSerial = serializeState(fromState, event); + const fromState = stateMap.get(fromStateSerial)!; visitCtx.vertices.add(fromStateSerial); if (fromStateSerial === toStateSerial) { @@ -551,17 +578,18 @@ export function traverseSimplePlans( if (!(serializedEvent in adjacency[fromStateSerial].transitions)) { continue; } + const prevState = stateMap.get(fromStateSerial); - const nextStateSerial = serializeState(nextState, subEvent); + const nextStateSerial = serializeState(nextState, subEvent, prevState); stateMap.set(nextStateSerial, nextState); - if (!visitCtx.vertices.has(serializeState(nextState, subEvent))) { + if (!visitCtx.vertices.has(nextStateSerial)) { visitCtx.edges.add(serializedEvent); path.push({ state: stateMap.get(fromStateSerial)!, event: subEvent }); - util(nextState, toStateSerial, subEvent); + util(nextStateSerial, toStateSerial); } } } @@ -570,11 +598,11 @@ export function traverseSimplePlans( visitCtx.vertices.delete(fromStateSerial); } - const initialStateSerial = serializeState(initialState, null); + const initialStateSerial = serializeState(initialState, undefined); stateMap.set(initialStateSerial, initialState); for (const nextStateSerial of Object.keys(adjacency) as SerializedState[]) { - util(initialState, nextStateSerial, null); + util(initialStateSerial, nextStateSerial); } return Object.values(pathMap); diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index a3eac716e2..dfa68cee42 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -135,7 +135,11 @@ export interface VisitedContext { export interface SerializationOptions { eventCases: EventCaseMap; - serializeState: (state: TState, event: TEvent | null) => string; + serializeState: ( + state: TState, + event: TEvent | undefined, + prevState?: TState + ) => string; serializeEvent: (event: TEvent) => string; } diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index d3f14861ba..726f46301b 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -55,7 +55,8 @@ export function executeAction( function serializeMachineTransition( state: AnyState, - _event: AnyEventObject | null, + _event: AnyEventObject | undefined, + prevState: AnyState | undefined, { serializeEvent }: { serializeEvent: (event: AnyEventObject) => string } ): string { // Only consider the transition via the serialized event if there actually @@ -64,7 +65,11 @@ function serializeMachineTransition( return ''; } - return ` via ${serializeEvent(state.event)}`; + const prevStateString = prevState + ? `from ${simpleStringify(prevState.value)}` + : ''; + + return ` via ${serializeEvent(state.event)}${prevStateString}`; } /** @@ -104,10 +109,15 @@ export function createTestModel( const testModel = new TestModel, EventFrom>( machine as SimpleBehavior, { - serializeState: (state, event) => { - return `${serializeState(state)}${serializeTransition(state, event, { - serializeEvent - })}` as SerializedState; + serializeState: (state, event, prevState) => { + return `${serializeState(state)}${serializeTransition( + state, + event, + prevState, + { + serializeEvent + } + )}` as SerializedState; }, stateMatcher: (state, key) => { return key.startsWith('#') diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 299972f15a..3f9ed802e0 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -125,7 +125,11 @@ export interface TestModelOptions log: (msg: string) => void; error: (msg: string) => void; }; - serializeTransition: (state: TState, event: TEvent | null) => string; + serializeTransition: ( + state: TState, + event: TEvent | undefined, + prevState: TState | undefined + ) => string; } export interface TestTransitionConfig< From 506786bacd36b83cfd3e61321f875c41975b1fa7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 30 May 2022 19:14:41 -0400 Subject: [PATCH 146/394] Make prevState optional in serializeTransition --- packages/xstate-test/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 3f9ed802e0..08f55a6bd5 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -128,7 +128,7 @@ export interface TestModelOptions serializeTransition: ( state: TState, event: TEvent | undefined, - prevState: TState | undefined + prevState?: TState ) => string; } From cde271bec78969c026f971db479ac7ea1f9b61b9 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 30 May 2022 23:31:06 -0400 Subject: [PATCH 147/394] Add previous state test to @xstate/graph --- packages/xstate-graph/src/graph.ts | 10 +++++-- packages/xstate-graph/src/types.ts | 4 +-- packages/xstate-graph/test/graph.test.ts | 36 +++++++++++++++++++++++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index caabcac6a9..2ee81bbb9a 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -85,8 +85,8 @@ const defaultValueAdjacencyMapOptions: Required< > = { events: {}, filter: () => true, - stateSerializer: serializeMachineState, - eventSerializer: serializeEvent + serializeState: serializeMachineState, + serializeEvent }; function getValueAdjacencyMapOptions( @@ -108,7 +108,11 @@ export function getValueAdjacencyMap( type TEvent = EventFrom; const optionsWithDefaults = getValueAdjacencyMapOptions(options); - const { filter, stateSerializer, eventSerializer } = optionsWithDefaults; + const { + filter, + serializeState: stateSerializer, + serializeEvent: eventSerializer + } = optionsWithDefaults; const { events } = optionsWithDefaults; const adjacency: ValueAdjacencyMap = {}; diff --git a/packages/xstate-graph/src/types.ts b/packages/xstate-graph/src/types.ts index dfa68cee42..3e4c2ebebe 100644 --- a/packages/xstate-graph/src/types.ts +++ b/packages/xstate-graph/src/types.ts @@ -123,8 +123,8 @@ export interface ValueAdjacencyMapOptions { | ((state: TState) => Array>); }; filter?: (state: TState) => boolean; - stateSerializer?: (state: TState) => string; - eventSerializer?: (event: TEvent) => string; + serializeState?: (state: TState) => string; + serializeEvent?: (event: TEvent) => string; } export interface VisitedContext { diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 5f1179e924..4f5c3dd0a9 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -480,7 +480,7 @@ describe('@xstate/graph', () => { const adj = getValueAdjacencyMap(counterMachine, { filter: (state) => state.context.count >= 0 && state.context.count <= 5, - stateSerializer: (state) => { + serializeState: (state) => { const ctx = { count: state.context.count }; @@ -665,3 +665,37 @@ describe('filtering', () => { `); }); }); + +it('should provide previous state for serializeState()', () => { + const machine = createMachine({ + initial: 'a', + states: { + a: { + on: { toB: 'b' } + }, + b: { + on: { toC: 'c' } + }, + c: { + on: { toA: 'a' } + } + } + }); + + const shortestPaths = getShortestPlans(machine, { + serializeState: (state, event, prevState) => { + return `${JSON.stringify(state.value)} via ${event?.type}${ + prevState ? ` via ${JSON.stringify(prevState.value)}` : '' + }`; + } + }); + + // Should be [0, 3]: + // 0 (a) + // 3 (a -> b -> c -> a) + expect( + shortestPaths + .filter((path) => path.state.matches('a')) + .map((plan) => plan.paths[0].steps.length) + ).toEqual([0, 3]); +}); From 3a63789a7704b69dda17b7536ea93b80616e3e76 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 30 May 2022 23:33:12 -0400 Subject: [PATCH 148/394] Add changeset --- .changeset/mighty-terms-shake.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/mighty-terms-shake.md diff --git a/.changeset/mighty-terms-shake.md b/.changeset/mighty-terms-shake.md new file mode 100644 index 0000000000..a89ce5a7a2 --- /dev/null +++ b/.changeset/mighty-terms-shake.md @@ -0,0 +1,10 @@ +--- +'@xstate/graph': patch +'@xstate/test': patch +--- + +The `serializeState()` path traversal option now provides 3 arguments to the function passed in: + +1. `state` - the current state +2. `event` - the event that caused traversal to this state +3. `prevState` 🆕 - the state before the current state (may be `undefined`) From d9c13f5669cb886c01aad1c3525ea5d96f476c4a Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 31 May 2022 09:30:37 -0400 Subject: [PATCH 149/394] Update packages/xstate-graph/src/graph.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-graph/src/graph.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index caabcac6a9..9485d54d62 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -381,8 +381,9 @@ export function getPathFromEvents< const stateMap = new Map(); const path: Steps = []; - const initialStateSerial = serializeState( + const initialSerializedState = serializeState( behavior.initialState, + undefined, undefined ) as SerializedState; stateMap.set(initialStateSerial, behavior.initialState); From 6b991ebc775eaa89d90b3523aa6fba1cfca978e8 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 31 May 2022 09:44:41 -0400 Subject: [PATCH 150/394] Add comment --- packages/xstate-test/src/machine.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 726f46301b..df5ae4c379 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -110,6 +110,7 @@ export function createTestModel( machine as SimpleBehavior, { serializeState: (state, event, prevState) => { + // Only consider the `state` if `serializeTransition()` is opted out (empty string) return `${serializeState(state)}${serializeTransition( state, event, From 46e886deb1abd39256cfd7b61033b72d34f17a1c Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 31 May 2022 14:47:28 +0100 Subject: [PATCH 151/394] Improved test output by removing ugly id from description --- .changeset/good-countries-allow.md | 5 +++++ packages/xstate-test/src/utils.ts | 4 ++-- packages/xstate-test/test/paths.test.ts | 16 ++++++++-------- 3 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 .changeset/good-countries-allow.md diff --git a/.changeset/good-countries-allow.md b/.changeset/good-countries-allow.md new file mode 100644 index 0000000000..2d8523f177 --- /dev/null +++ b/.changeset/good-countries-allow.md @@ -0,0 +1,5 @@ +--- +'@xstate/test': minor +--- + +Improved test output by removing id from description diff --git a/packages/xstate-test/src/utils.ts b/packages/xstate-test/src/utils.ts index 5468245e16..e32ea7dbfe 100644 --- a/packages/xstate-test/src/utils.ts +++ b/packages/xstate-test/src/utils.ts @@ -81,10 +81,10 @@ export function getDescription(state: AnyState): string { const stateStrings = state.configuration .filter((sn) => sn.type === 'atomic' || sn.type === 'final') - .map(({ id }) => { + .map(({ id, path }) => { const meta = state.meta[id] as TestMeta; if (!meta) { - return `"#${id}"`; + return `"${path.join('.')}"`; } const { description } = meta; diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index b440653075..be5bfbb713 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -110,8 +110,8 @@ describe('path.description', () => { 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' + 'Reaches state "d": EVENT → EVENT → EVENT', + 'Reaches state "e": EVENT → EVENT → EVENT_2' ]); }); }); @@ -142,9 +142,9 @@ describe('transition coverage', () => { 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", + "Reaches state \\"a\\": NEXT → PREV", + "Reaches state \\"a\\": NEXT → RESTART", + "Reaches state \\"b\\": END", ] `); }); @@ -182,9 +182,9 @@ describe('transition coverage', () => { // { 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})", + "Reaches state \\"b\\": NEXT ({\\"value\\":0})", + "Reaches state \\"b\\": NEXT ({\\"value\\":100})", + "Reaches state \\"b\\": NEXT ({\\"value\\":1000})", ] `); }); From ed595e362ffc0393f01831ecd8d002c495507c71 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 31 May 2022 09:59:37 -0400 Subject: [PATCH 152/394] Fixing name --- packages/xstate-graph/src/graph.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index ab6790eb6d..431e21dece 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -390,9 +390,9 @@ export function getPathFromEvents< undefined, undefined ) as SerializedState; - stateMap.set(initialStateSerial, behavior.initialState); + stateMap.set(initialSerializedState, behavior.initialState); - let stateSerial = initialStateSerial; + let stateSerial = initialSerializedState; let state = behavior.initialState; for (const event of events) { path.push({ From 3b116e5dbbde6bf147f355fc1cfb59a19a33ea76 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 31 May 2022 17:22:28 +0100 Subject: [PATCH 153/394] Fixed `getShortestPathsTo` issue that caused candidate paths to be incorrectly removed while deduplicating generated paths (#3362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added failing test for getShortestPathsTo * Fixed `getShortestPathsTo` issue that caused candidate paths to be incorrectly removed while deduplicating generated paths * Configure `getShortestPaths` path generator explicitly Co-authored-by: Mateusz Burzyński --- .changeset/flat-trees-suffer.md | 5 +++ packages/xstate-test/src/TestModel.ts | 35 ++++++++++++------- .../{dedupPaths.ts => deduplicatePaths.ts} | 15 ++++---- packages/xstate-test/test/paths.test.ts | 33 +++++++++++++++++ 4 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 .changeset/flat-trees-suffer.md rename packages/xstate-test/src/{dedupPaths.ts => deduplicatePaths.ts} (84%) diff --git a/.changeset/flat-trees-suffer.md b/.changeset/flat-trees-suffer.md new file mode 100644 index 0000000000..5da53f614d --- /dev/null +++ b/.changeset/flat-trees-suffer.md @@ -0,0 +1,5 @@ +--- +'@xstate/test': patch +--- + +Fixed `getShortestPathsTo` issue that caused candidate paths to be incorrectly removed while deduplicating generated paths. diff --git a/packages/xstate-test/src/TestModel.ts b/packages/xstate-test/src/TestModel.ts index a1f6b5ed6c..53a07e028d 100644 --- a/packages/xstate-test/src/TestModel.ts +++ b/packages/xstate-test/src/TestModel.ts @@ -11,7 +11,7 @@ import { } from '@xstate/graph'; import { EventObject } from 'xstate'; import { isStateLike } from 'xstate/lib/utils'; -import { pathGeneratorWithDedup } from './dedupPaths'; +import { deduplicatePaths } from './deduplicatePaths'; import { getShortestPaths, getSimplePaths } from './pathGenerators'; import type { EventExecutor, @@ -85,15 +85,21 @@ export class TestModel { return this.getPaths({ ...options, pathGenerator: getShortestPaths }); } + private _getStatePaths( + options?: Partial> + ): Array> { + const pathGenerator = + options?.pathGenerator || TestModel.defaults.pathGenerator; + return pathGenerator(this.behavior, this.resolveOptions(options)); + } + 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); + return deduplicatePaths( + this._getStatePaths(options), + options?.serializeEvent + ).map(this.toTestPath); } public getShortestPathsTo( @@ -102,7 +108,12 @@ export class TestModel { let minWeight = Infinity; let shortestPaths: Array> = []; - const paths = this.filterPathsTo(statePredicate, this.getShortestPaths()); + const paths = deduplicatePaths( + this.filterPathsTo( + statePredicate, + this._getStatePaths({ pathGenerator: getShortestPaths }) + ) + ).map(this.toTestPath); for (const path of paths) { const currWeight = path.weight; @@ -136,10 +147,10 @@ export class TestModel { private filterPathsTo( statePredicate: StatePredicate, - testPaths: Array> - ): Array> { - return testPaths.filter((testPath) => { - return statePredicate(testPath.state); + statePaths: Array> + ): Array> { + return statePaths.filter((statePath) => { + return statePredicate(statePath.state); }); } diff --git a/packages/xstate-test/src/dedupPaths.ts b/packages/xstate-test/src/deduplicatePaths.ts similarity index 84% rename from packages/xstate-test/src/dedupPaths.ts rename to packages/xstate-test/src/deduplicatePaths.ts index 7efbc96dce..1f07355b39 100644 --- a/packages/xstate-test/src/dedupPaths.ts +++ b/packages/xstate-test/src/deduplicatePaths.ts @@ -1,16 +1,15 @@ import { StatePath } from '@xstate/graph'; import { EventObject } from 'xstate'; -import { PathGenerator } from './types'; +import { simpleStringify } from './utils'; /** * 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); - +export const deduplicatePaths = ( + paths: StatePath[], + serializeEvent: (event: TEvent) => string = simpleStringify +): StatePath[] => { /** * Put all plans on the same level so we can dedup them */ @@ -22,9 +21,7 @@ export const pathGeneratorWithDedup = ( paths.forEach((path) => { allPathsWithEventSequence.push({ path, - eventSequence: path.steps.map((step) => - options.serializeEvent(step.event) - ) + eventSequence: path.steps.map((step) => serializeEvent(step.event)) }); }); diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index b7ada19e6f..b6eead193c 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -222,3 +222,36 @@ describe('transition coverage', () => { ]); }); }); + +describe('getShortestPathsTo', () => { + const machine = createTestMachine({ + initial: 'open', + states: { + open: { + on: { + CLOSE: 'closed' + } + }, + closed: { + on: { + OPEN: 'open' + } + } + } + }); + it('Should find a path to a non-initial target state', () => { + const closedPaths = createTestModel(machine).getShortestPathsTo((state) => + state.matches('closed') + ); + + expect(closedPaths).toHaveLength(1); + }); + + it('Should find a path to an initial target state', () => { + const openPaths = createTestModel(machine).getShortestPathsTo((state) => + state.matches('open') + ); + + expect(openPaths).toHaveLength(1); + }); +}); From 3fbe9499b8b28b3da02f657441c8205de6fbe754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 31 May 2022 18:59:19 +0200 Subject: [PATCH 154/394] Remove redundant `while` loop from `@xstate/graph` (#3368) --- packages/xstate-graph/src/graph.ts | 60 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 431e21dece..d00834d351 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -233,45 +233,43 @@ export function traverseShortestPlans( const visited = new Set(); unvisited.add(initialSerializedState); - while (unvisited.size > 0) { - 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 prevState = stateMap.get(serializedState); - const nextSerializedState = serializeState( - nextState, - eventObject, - prevState - ); - stateMap.set(nextSerializedState, nextState); - if (!weightMap.has(nextSerializedState)) { + 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 prevState = stateMap.get(serializedState); + const nextSerializedState = serializeState( + nextState, + eventObject, + prevState + ); + stateMap.set(nextSerializedState, nextState); + if (!weightMap.has(nextSerializedState)) { + weightMap.set(nextSerializedState, [ + weight + 1, + serializedState, + eventObject + ]); + } else { + const [nextWeight] = weightMap.get(nextSerializedState)!; + if (nextWeight > weight + 1) { weightMap.set(nextSerializedState, [ weight + 1, serializedState, eventObject ]); - } else { - const [nextWeight] = weightMap.get(nextSerializedState)!; - if (nextWeight > weight + 1) { - weightMap.set(nextSerializedState, [ - weight + 1, - serializedState, - eventObject - ]); - } - } - if (!visited.has(nextSerializedState)) { - unvisited.add(nextSerializedState); } } - visited.add(serializedState); - unvisited.delete(serializedState); + if (!visited.has(nextSerializedState)) { + unvisited.add(nextSerializedState); + } } + visited.add(serializedState); + unvisited.delete(serializedState); } const statePlanMap: StatePlanMap = {}; From f6d20164c7bc4340f62bf1670edcec099ee0c1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 31 May 2022 18:59:33 +0200 Subject: [PATCH 155/394] Fixed `@xstate/test` tests after test output change (#3371) --- packages/xstate-test/test/paths.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/xstate-test/test/paths.test.ts b/packages/xstate-test/test/paths.test.ts index b6eead193c..fa308caaba 100644 --- a/packages/xstate-test/test/paths.test.ts +++ b/packages/xstate-test/test/paths.test.ts @@ -217,8 +217,8 @@ describe('transition coverage', () => { const paths = model.getPaths(); expect(paths.map((p) => p.description)).toEqual([ - `Reaches state "#(machine).a": GO_TO_B → GO_TO_A`, - `Reaches state "#(machine).a": GO_TO_C → GO_TO_A` + `Reaches state "a": GO_TO_B → GO_TO_A`, + `Reaches state "a": GO_TO_C → GO_TO_A` ]); }); }); From f2e3abe9fca40731b6869027818dd3b01fb4983a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 31 May 2022 18:59:48 +0200 Subject: [PATCH 156/394] Add missing whitespace in the serialized transition output (#3370) --- packages/xstate-test/src/machine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index df5ae4c379..93f7db4152 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -66,7 +66,7 @@ function serializeMachineTransition( } const prevStateString = prevState - ? `from ${simpleStringify(prevState.value)}` + ? ` from ${simpleStringify(prevState.value)}` : ''; return ` via ${serializeEvent(state.event)}${prevStateString}`; From 78ce9572ee0a50b41cd5eab8ab0c2e8cf82b2e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 31 May 2022 19:06:56 +0200 Subject: [PATCH 157/394] Fixed changeset attributions --- .changeset/flat-trees-suffer.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.changeset/flat-trees-suffer.md b/.changeset/flat-trees-suffer.md index 5da53f614d..9e66aadc29 100644 --- a/.changeset/flat-trees-suffer.md +++ b/.changeset/flat-trees-suffer.md @@ -2,4 +2,8 @@ '@xstate/test': patch --- +author: @Andarist +pr: #3367 +commit: b8cf8cfdc5d0e4c0b8f864d55a0f3126de3357f2 + Fixed `getShortestPathsTo` issue that caused candidate paths to be incorrectly removed while deduplicating generated paths. From a16f64d6049376500fab92f3b6f5646ac765f9ec Mon Sep 17 00:00:00 2001 From: laurakalbag Date: Wed, 1 Jun 2022 15:40:32 +0100 Subject: [PATCH 158/394] Add share by link and autolayout improvements to Q2 roadmap --- docs/roadmap/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md index f7595f8132..230259a78f 100644 --- a/docs/roadmap/README.md +++ b/docs/roadmap/README.md @@ -1,5 +1,5 @@ --- -reviewed: 2022-04-05 +reviewed: 2022-06-01 styleGuideVersion: 0 --- @@ -19,6 +19,8 @@ What we’re working on right now. Features planned for April - June 2022. ### Editor +- Share by link +- Autolayout improvements - Bug fixes - Discover page - Billing and Teams, including private machines @@ -80,4 +82,4 @@ We prioritize features based on your feedback. Please let us know if you have fe --- -Last updated: April 5, 2022. +Last updated: June 1, 2022. From 5b4675344fea90ffa8085497067b54f435f4b1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 7 Jun 2022 14:52:44 +0200 Subject: [PATCH 159/394] Update a bunch of GitHub actions and bump node version on CI (#3386) * Update a bunch of GitHub actions and bump node version on CI * Add `.node-version` file --- .codesandbox/ci.json | 2 +- .github/actions/ci-setup/action.yml | 6 +++--- .github/workflows/codesee-arch-diagram.yml | 8 ++++---- .github/workflows/docs.yml | 4 ++-- .github/workflows/nodejs.yml | 2 +- .github/workflows/release.yml | 2 +- .node-version | 1 + 7 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 .node-version diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 9832a2d45a..29a6449206 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -7,5 +7,5 @@ "packages/xstate-test" ], "sandboxes": ["xstate-example-template-m4ckv", "xstate-react-template-3t2tg"], - "node": "14" + "node": "16" } diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index b5544914d9..04c462899a 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -2,10 +2,10 @@ name: 'CI setup' runs: using: 'composite' steps: - - name: Use Node.js 14.x - uses: actions/setup-node@v2 + - name: Use Node.js 16.x + uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 16.x - name: Install Dependencies run: yarn diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml index 16b971e994..5f63bae2b4 100644 --- a/.github/workflows/codesee-arch-diagram.yml +++ b/.github/workflows/codesee-arch-diagram.yml @@ -15,7 +15,7 @@ jobs: steps: - name: checkout id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} @@ -35,11 +35,11 @@ jobs: # CodeSee Maps Go support uses a static binary so there's no setup step required. - - name: Configure Node.js 14 + - name: Configure Node.js 16 uses: actions/setup-node@v2 if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }} with: - node-version: '14' + node-version: '16' - name: Configure Python 3.x uses: actions/setup-python@v2 @@ -71,7 +71,7 @@ jobs: step: mapUpload api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} github_ref: ${{ github.ref }} - + - name: Insights id: insights uses: Codesee-io/codesee-map-action@latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 28bf91aa20..5831acb41e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,10 +11,10 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build docs working-directory: docs diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 0b8f186396..6aca7310e6 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -16,6 +16,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ./.github/actions/ci-setup - uses: ./.github/actions/ci-checks diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 683be94681..790703a639 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ./.github/actions/ci-setup - uses: ./.github/actions/ci-checks diff --git a/.node-version b/.node-version new file mode 100644 index 0000000000..7fd023741b --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v16.15.0 From cec85877a4afad9b6c6a32447bab3ad114b65dc0 Mon Sep 17 00:00:00 2001 From: Cyril Silverman Date: Tue, 7 Jun 2022 13:53:27 +0100 Subject: [PATCH 160/394] Narrow down the `event` type passed to `EventExecutor` based on the key (#3380) * Fix event type passed to EventExecutor * Create flat-melons-tan.md * add type tests --- .changeset/flat-melons-tan.md | 5 +++ packages/xstate-test/src/types.ts | 2 +- packages/xstate-test/test/types.test.ts | 42 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .changeset/flat-melons-tan.md create mode 100644 packages/xstate-test/test/types.test.ts diff --git a/.changeset/flat-melons-tan.md b/.changeset/flat-melons-tan.md new file mode 100644 index 0000000000..25fe73afdf --- /dev/null +++ b/.changeset/flat-melons-tan.md @@ -0,0 +1,5 @@ +--- +"@xstate/test": patch +--- + +Narrow down the `event` type passed to `EventExecutor` from the corresponding key of the `events` object diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 08f55a6bd5..c0f673b2bf 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -86,7 +86,7 @@ export interface TestParam { [key: string]: (state: TState) => void | Promise; }; events?: { - [TEventType in TEvent['type']]?: EventExecutor; + [TEventType in TEvent['type']]?: EventExecutor>; }; } diff --git a/packages/xstate-test/test/types.test.ts b/packages/xstate-test/test/types.test.ts new file mode 100644 index 0000000000..acac90debd --- /dev/null +++ b/packages/xstate-test/test/types.test.ts @@ -0,0 +1,42 @@ +import { createMachine } from 'xstate'; +import { createTestModel } from '../src'; + +describe('types', () => { + it('`EventExecutor` should be passed event with type that corresponds to its key', () => { + const machine = createMachine({ + id: 'test', + schema: { + context: {} as any, + events: {} as + | { type: 'a'; valueA: boolean } + | { type: 'b'; valueB: number } + }, + initial: 'a', + states: { + a: { + on: { + a: { target: '#test.b' } + } + }, + b: { + on: { + b: { target: '#test.a' } + } + } + } + }); + + for (const path of createTestModel(machine).getPaths()) { + path.test({ + events: { + a: ({ event }) => { + console.log(event.valueA); + }, + b: ({ event }) => { + console.log(event.valueB); + } + } + }); + } + }); +}); From a039ba05f8c0a8840ee5680e26783a6cc2841540 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 7 Jun 2022 08:55:07 -0400 Subject: [PATCH 161/394] Add pure assignment warning --- docs/guides/context.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/guides/context.md b/docs/guides/context.md index 3e9ebde1bf..b8c6ba2997 100644 --- a/docs/guides/context.md +++ b/docs/guides/context.md @@ -188,6 +188,21 @@ The `meta` object contains: - `state` - the current state in a normal transition (`undefined` for the initial state transition) - `action` - the assign action +::: warning +Assigners **must be pure**; they should not contain any side-effects. + +```js +actions: assign({ + count: (context) => { + doSomeSideEffect(); // ❌ No side-effects in assignment functions + + return context.count + 1; + } +}); +``` + +::: + ::: warning The `assign(...)` function is an **action creator**; it is a pure function that only returns an action object and does _not_ imperatively make assignments to the context. ::: From 5a4171992e72f51ddf0fe09bc14b8028de914543 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 14 Jun 2022 07:15:06 -0400 Subject: [PATCH 162/394] Replace two-argument send functions in documentation --- docs/fr/packages/xstate-react/index.md | 2 +- docs/fr/packages/xstate-svelte/index.md | 2 +- docs/fr/packages/xstate-vue/index.md | 2 +- docs/packages/xstate-react/index.md | 2 +- docs/packages/xstate-svelte/index.md | 2 +- docs/packages/xstate-vue/index.md | 2 +- docs/tutorials/reddit.md | 4 ++-- docs/zh/packages/xstate-react/index.md | 2 +- docs/zh/packages/xstate-svelte/index.md | 2 +- docs/zh/packages/xstate-vue/index.md | 2 +- docs/zh/tutorials/reddit.md | 4 ++-- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/fr/packages/xstate-react/index.md b/docs/fr/packages/xstate-react/index.md index 015e7330a1..1b311a8459 100644 --- a/docs/fr/packages/xstate-react/index.md +++ b/docs/fr/packages/xstate-react/index.md @@ -365,7 +365,7 @@ const Fetcher = ({ onResolve }) => { switch (state.value) { case 'idle': return ( - ); diff --git a/docs/fr/packages/xstate-svelte/index.md b/docs/fr/packages/xstate-svelte/index.md index bbb283af8f..85b1d10b58 100644 --- a/docs/fr/packages/xstate-svelte/index.md +++ b/docs/fr/packages/xstate-svelte/index.md @@ -283,7 +283,7 @@ Example: the `'fetchData'` service and `'notifySuccess'` action are both configu {#if $state.value === 'idle'} - {:else if $state.value === 'loading'} diff --git a/docs/fr/packages/xstate-vue/index.md b/docs/fr/packages/xstate-vue/index.md index e222a33fee..6e84854a3a 100644 --- a/docs/fr/packages/xstate-vue/index.md +++ b/docs/fr/packages/xstate-vue/index.md @@ -256,7 +256,7 @@ Example: the `'fetchData'` service and `'notifySuccess'` action are both configu ```vue