Skip to main content
Module

x/xstate/test/state.test.ts

State machines and statecharts for the modern web.
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928
import { Machine, State, StateFrom, interpret, createMachine, spawn} from '../src/index';import { initEvent, assign } from '../src/actions';import { toSCXMLEvent } from '../src/utils';
type Events = | { type: 'BAR_EVENT' } | { type: 'DEEP_EVENT' } | { type: 'EXTERNAL' } | { type: 'FOO_EVENT' } | { type: 'FORBIDDEN_EVENT' } | { type: 'INERT' } | { type: 'INTERNAL' } | { type: 'MACHINE_EVENT' } | { type: 'P31' } | { type: 'P32' } | { type: 'THREE_EVENT' } | { type: 'TO_THREE' } | { type: 'TO_TWO'; foo: string } | { type: 'TO_TWO_MAYBE' } | { type: 'TO_FINAL' };
const machine = Machine<any, Events>({ initial: 'one', states: { one: { entry: ['enter'], on: { EXTERNAL: { target: 'one', internal: false }, INERT: { target: 'one', internal: true }, INTERNAL: { target: 'one', internal: true, actions: ['doSomething'] }, TO_TWO: 'two', TO_TWO_MAYBE: { target: 'two', cond: function maybe() { return true; } }, TO_THREE: 'three', FORBIDDEN_EVENT: undefined, TO_FINAL: 'success' } }, two: { initial: 'deep', states: { deep: { initial: 'foo', states: { foo: { on: { FOO_EVENT: 'bar', FORBIDDEN_EVENT: undefined } }, bar: { on: { BAR_EVENT: 'foo' } } } } }, on: { DEEP_EVENT: '.' } }, three: { type: 'parallel', states: { first: { initial: 'p31', states: { p31: { on: { P31: '.' } } } }, second: { initial: 'p32', states: { p32: { on: { P32: '.' } } } } }, on: { THREE_EVENT: '.' } }, success: { type: 'final' } }, on: { MACHINE_EVENT: '.two' }});
describe('State', () => { describe('.changed', () => { it('should indicate that it is not changed if initial state', () => { expect(machine.initialState.changed).not.toBeDefined(); });
it('states from external transitions with entry actions should be changed', () => { const changedState = machine.transition(machine.initialState, 'EXTERNAL'); expect(changedState.changed).toBe(true); });
it('states from internal transitions with no actions should be unchanged', () => { const changedState = machine.transition(machine.initialState, 'EXTERNAL'); const unchangedState = machine.transition(changedState, 'INERT'); expect(unchangedState.changed).toBe(false); });
it('states from internal transitions with actions should be changed', () => { const changedState = machine.transition(machine.initialState, 'INTERNAL'); expect(changedState.changed).toBe(true); });
it('normal state transitions should be changed (initial state)', () => { const changedState = machine.transition(machine.initialState, 'TO_TWO'); expect(changedState.changed).toBe(true); });
it('normal state transitions should be changed', () => { const twoState = machine.transition(machine.initialState, 'TO_TWO'); const changedState = machine.transition(twoState, 'FOO_EVENT'); expect(changedState.changed).toBe(true); });
it('normal state transitions with unknown event should be unchanged', () => { const twoState = machine.transition(machine.initialState, 'TO_TWO'); const changedState = machine.transition(twoState, 'UNKNOWN_EVENT' as any); expect(changedState.changed).toBe(false); });
it('should report entering a final state as changed', () => { const finalMachine = Machine({ id: 'final', initial: 'one', states: { one: { on: { DONE: 'two' } },
two: { type: 'final' } } });
const twoState = finalMachine.transition('one', 'DONE');
expect(twoState.changed).toBe(true); });
it('should report any internal transition assignments as changed', () => { const assignMachine = Machine<{ count: number }>({ id: 'assign', initial: 'same', context: { count: 0 }, states: { same: { on: { EVENT: { actions: assign({ count: (ctx) => ctx.count + 1 }) } } } } });
const { initialState } = assignMachine; const changedState = assignMachine.transition(initialState, 'EVENT'); expect(changedState.changed).toBe(true); expect(initialState.value).toEqual(changedState.value); });
it('should not escape targetless child state nodes', () => { interface Ctx { value: string; } type ToggleEvents = | { type: 'CHANGE'; value: string; } | { type: 'SAVE'; }; const toggleMachine = Machine<Ctx, ToggleEvents>({ id: 'input', context: { value: '' }, type: 'parallel', states: { edit: { on: { CHANGE: { actions: assign({ value: (_, e) => { return e.value; } }) } } }, validity: { initial: 'invalid', states: { invalid: {}, valid: {} }, on: { CHANGE: [ { target: '.valid', cond: () => true }, { target: '.invalid' } ] } } } });
const nextState = toggleMachine.transition(toggleMachine.initialState, { type: 'CHANGE', value: 'whatever' });
expect(nextState.changed).toBe(true); expect(nextState.value).toEqual({ edit: {}, validity: 'valid' }); }); });
describe('.nextEvents', () => { it('returns the next possible events for the current state', () => { expect(machine.initialState.nextEvents.sort()).toEqual([ 'EXTERNAL', 'INERT', 'INTERNAL', 'MACHINE_EVENT', 'TO_FINAL', 'TO_THREE', 'TO_TWO', 'TO_TWO_MAYBE' ]);
expect( machine.transition(machine.initialState, 'TO_TWO').nextEvents.sort() ).toEqual(['DEEP_EVENT', 'FOO_EVENT', 'MACHINE_EVENT']);
expect( machine.transition(machine.initialState, 'TO_THREE').nextEvents.sort() ).toEqual(['MACHINE_EVENT', 'P31', 'P32', 'THREE_EVENT']); });
it('returns events when transitioned from StateValue', () => { const A = machine.transition(machine.initialState, 'TO_THREE'); const B = machine.transition(A.value, 'TO_THREE');
expect(B.nextEvents.sort()).toEqual([ 'MACHINE_EVENT', 'P31', 'P32', 'THREE_EVENT' ]); });
it('returns no next events if there are none', () => { const noEventsMachine = Machine({ id: 'no-events', initial: 'idle', states: { idle: { on: {} } } });
expect(noEventsMachine.initialState.nextEvents).toEqual([]); }); });
describe('State.create()', () => { it('should be able to create a state from a JSON config', () => { const { initialState } = machine; const jsonInitialState = JSON.parse(JSON.stringify(initialState));
const stateFromConfig = State.create(jsonInitialState) as StateFrom< typeof machine >;
expect(machine.transition(stateFromConfig, 'TO_TWO').value).toEqual({ two: { deep: 'foo' } }); });
it('should preserve state.nextEvents using machine.resolveState', () => { const { initialState } = machine; const { nextEvents } = initialState; const jsonInitialState = JSON.parse(JSON.stringify(initialState));
const stateFromConfig = State.create(jsonInitialState) as StateFrom< typeof machine >;
expect(machine.resolveState(stateFromConfig).nextEvents.sort()).toEqual( nextEvents.sort() ); }); });
describe('State.inert()', () => { it('should create an inert instance of the given State', () => { const { initialState } = machine;
expect(State.inert(initialState, undefined).actions).toEqual([]); });
it('should create an inert instance of the given stateValue and context', () => { const { initialState } = machine; const inertState = State.inert(initialState.value, { foo: 'bar' });
expect(inertState.actions).toEqual([]); expect(inertState.context).toEqual({ foo: 'bar' }); });
it('should preserve the given State if there are no actions', () => { const naturallyInertState = State.from('foo');
expect(State.inert(naturallyInertState, undefined)).toEqual( naturallyInertState ); }); });
describe('.event', () => { it('the .event prop should be the event (string) that caused the transition', () => { const { initialState } = machine;
const nextState = machine.transition(initialState, 'TO_TWO');
expect(nextState.event).toEqual({ type: 'TO_TWO' }); });
it('the .event prop should be the event (object) that caused the transition', () => { const { initialState } = machine;
const nextState = machine.transition(initialState, { type: 'TO_TWO', foo: 'bar' });
expect(nextState.event).toEqual({ type: 'TO_TWO', foo: 'bar' }); });
it('the ._event prop should be the initial event for the initial state', () => { const { initialState } = machine;
expect(initialState._event).toEqual(initEvent); }); });
describe('._event', () => { it('the ._event prop should be the SCXML event (string) that caused the transition', () => { const { initialState } = machine;
const nextState = machine.transition(initialState, 'TO_TWO');
expect(nextState._event).toEqual(toSCXMLEvent('TO_TWO')); });
it('the ._event prop should be the SCXML event (object) that caused the transition', () => { const { initialState } = machine;
const nextState = machine.transition(initialState, { type: 'TO_TWO', foo: 'bar' });
expect(nextState._event).toEqual( toSCXMLEvent({ type: 'TO_TWO', foo: 'bar' }) ); });
it('the ._event prop should be the initial SCXML event for the initial state', () => { const { initialState } = machine;
expect(initialState._event).toEqual(toSCXMLEvent(initEvent)); });
it('the ._event prop should be the SCXML event (SCXML metadata) that caused the transition', () => { const { initialState } = machine;
const nextState = machine.transition( initialState, toSCXMLEvent( { type: 'TO_TWO', foo: 'bar' }, { sendid: 'test' } ) );
expect(nextState._event).toEqual( toSCXMLEvent( { type: 'TO_TWO', foo: 'bar' }, { sendid: 'test' } ) ); });
describe('_sessionid', () => { it('_sessionid should be null for non-invoked machines', () => { const testMachine = Machine({ initial: 'active', states: { active: {} } });
expect(testMachine.initialState._sessionid).toBeNull(); });
it('_sessionid should be the service sessionId for invoked machines', (done) => { const testMachine = Machine({ initial: 'active', states: { active: { on: { TOGGLE: 'inactive' } }, inactive: { type: 'final' } } });
const service = interpret(testMachine);
service .onTransition((state) => { expect(state._sessionid).toEqual(service.sessionId); }) .onDone(() => { done(); }) .start();
service.send('TOGGLE'); });
it('_sessionid should persist through states (manual)', () => { const testMachine = Machine({ initial: 'active', states: { active: { on: { TOGGLE: 'inactive' } }, inactive: { type: 'final' } } });
const { initialState } = testMachine;
initialState._sessionid = 'somesessionid';
const nextState = testMachine.transition(initialState, 'TOGGLE');
expect(nextState._sessionid).toEqual('somesessionid'); }); }); });
describe('.transitions', () => { const { initialState } = machine;
it('should have no transitions for the initial state', () => { expect(initialState.transitions).toHaveLength(0); });
it('should have transitions for the sent event', () => { expect( machine.transition(initialState, 'TO_TWO').transitions ).toContainEqual(expect.objectContaining({ eventType: 'TO_TWO' })); });
it('should have condition in the transition', () => { expect( machine.transition(initialState, 'TO_TWO_MAYBE').transitions ).toContainEqual( expect.objectContaining({ eventType: 'TO_TWO_MAYBE', cond: expect.objectContaining({ name: 'maybe' }) }) ); }); });
describe('State.prototype.matches', () => { it('should keep reference to state instance after destructuring', () => { const { initialState } = machine; const { matches } = initialState;
expect(matches('one')).toBe(true); }); });
describe('State.prototype.toStrings', () => { it('should return all state paths as strings', () => { const twoState = machine.transition('one', 'TO_TWO');
expect(twoState.toStrings()).toEqual(['two', 'two.deep', 'two.deep.foo']); });
it('should respect `delimiter` option for deeply nested states', () => { const twoState = machine.transition('one', 'TO_TWO');
expect(twoState.toStrings(undefined, ':')).toEqual([ 'two', 'two:deep', 'two:deep:foo' ]); });
it('should keep reference to state instance after destructuring', () => { const { initialState } = machine; const { toStrings } = initialState;
expect(toStrings()).toEqual(['one']); }); });
describe('.done', () => { it('should show that a machine has not reached its final state', () => { expect(machine.initialState.done).toBeFalsy(); });
it('should show that a machine has reached its final state', () => { expect(machine.transition(undefined, 'TO_FINAL').done).toBeTruthy(); }); });
describe('.can', () => { it('should return true for a simple event that results in a transition to a different state', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { NEXT: 'b' } }, b: {} } });
expect(machine.initialState.can('NEXT')).toBe(true); });
it('should return true for an event object that results in a transition to a different state', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { NEXT: 'b' } }, b: {} } });
expect(machine.initialState.can({ type: 'NEXT' })).toBe(true); });
it('should return true for an event object that results in a new action', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { NEXT: { actions: 'newAction' } } } } });
expect(machine.initialState.can({ type: 'NEXT' })).toBe(true); });
it('should return true for an event object that results in a context change', () => { const machine = createMachine({ initial: 'a', context: { count: 0 }, states: { a: { on: { NEXT: { actions: assign({ count: 1 }) } } } } });
expect(machine.initialState.can({ type: 'NEXT' })).toBe(true); });
it('should return true for an external self-transition without actions', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { EV: 'a' } } } });
expect(machine.initialState.can({ type: 'EV' })).toBe(true); });
it('should return true for an external self-transition with reentry action', () => { const machine = createMachine({ initial: 'a', states: { a: { entry: () => {}, on: { EV: 'a' } } } });
expect(machine.initialState.can({ type: 'EV' })).toBe(true); });
it('should return true for an external self-transition with transition action', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { EV: { target: 'a', actions: () => {} } } } } });
expect(machine.initialState.can({ type: 'EV' })).toBe(true); });
it('should return true for a targetless transition with actions', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { EV: { actions: () => {} } } } } });
expect(machine.initialState.can({ type: 'EV' })).toBe(true); });
it('should return false for a forbidden transition', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { EV: undefined } } } });
expect(machine.initialState.can({ type: 'EV' })).toBe(false); });
it('should return false for an unknown event', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { NEXT: 'b' } }, b: {} } });
expect(machine.initialState.can({ type: 'UNKNOWN' })).toBe(false); });
it('should return true when a guarded transition allows the transition', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { CHECK: { target: 'b', cond: () => true } } }, b: {} } });
expect( machine.initialState.can({ type: 'CHECK' }) ).toBe(true); });
it('should return false when a guarded transition disallows the transition', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { CHECK: { target: 'b', cond: () => false } } }, b: {} } });
expect( machine.initialState.can({ type: 'CHECK' }) ).toBe(false); });
it('should not spawn actors when determining if an event is accepted', () => { let spawned = false; const machine = createMachine({ context: {}, initial: 'a', states: { a: { on: { SPAWN: { actions: assign(() => ({ ref: spawn(() => { spawned = true; }) })) } } }, b: {} } });
const service = interpret(machine).start(); service.state.can('SPAWN'); expect(spawned).toBe(false); });
it('should return false for states created without a machine', () => { const state = State.from('test');
expect(state.can({ type: 'ANY_EVENT' })).toEqual(false); });
it('should not execute assignments', () => { let executed = false; const machine = createMachine({ context: {}, on: { EVENT: { actions: assign((ctx) => { // Side-effect just for testing executed = true; return ctx; }) } } });
const { initialState } = machine;
expect(initialState.can('EVENT')).toBeTruthy();
expect(executed).toBeFalsy(); });
it('should return true when non-first parallel region changes value', () => { const machine = createMachine({ type: 'parallel', states: { a: { initial: 'a1', states: { a1: { id: 'foo', on: { // first region doesn't change value here EVENT: { target: ['#foo', '#bar'] } } } } }, b: { initial: 'b1', states: { b1: {}, b2: { id: 'bar' } } } } });
expect(machine.initialState.can('EVENT')).toBeTruthy(); });
it('should return true when transition targets a state that is already part of the current configuration but the final state value changes', () => { const machine = createMachine({ initial: 'a', states: { a: { id: 'foo', initial: 'a1', states: { a1: { on: { NEXT: 'a2' } }, a2: { on: { NEXT: '#foo' } } } } } });
const nextState = machine.transition(undefined, { type: 'NEXT' });
expect(nextState.can({ type: 'NEXT' })).toBeTruthy(); }); });
describe('.hasTag', () => { it('should be able to check a tag after recreating a persisted state', () => { const machine = createMachine({ initial: 'a', states: { a: { tags: 'foo' } } });
const persistedState = JSON.stringify(machine.initialState); const restoredState = State.create(JSON.parse(persistedState));
expect(restoredState.hasTag('foo')).toBe(true); }); });});