Skip to main content
Module

x/xstate/test/actions.test.ts

State machines and statecharts for the modern web.
Go to Latest
File
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879
import { Machine, createMachine, assign, forwardTo, interpret, spawn, ActorRefFrom} from '../src/index';import { pure, sendParent, log, choose, sendTo } from '../src/actions';
describe('entry/exit actions', () => { const pedestrianStates = { initial: 'walk', states: { walk: { on: { PED_COUNTDOWN: 'wait' }, entry: 'enter_walk', exit: 'exit_walk' }, wait: { on: { PED_COUNTDOWN: 'stop' }, entry: 'enter_wait', exit: 'exit_wait' }, stop: { entry: ['enter_stop'], exit: ['exit_stop'] } } };
const lightMachine = Machine({ key: 'light', initial: 'green', states: { green: { on: { TIMER: 'yellow', POWER_OUTAGE: 'red', NOTHING: 'green' }, entry: 'enter_green', exit: 'exit_green' }, yellow: { on: { TIMER: 'red', POWER_OUTAGE: 'red' }, entry: 'enter_yellow', exit: 'exit_yellow' }, red: { on: { TIMER: 'green', POWER_OUTAGE: 'red', NOTHING: 'red' }, entry: 'enter_red', exit: 'exit_red', ...pedestrianStates } } });
const newPedestrianStates = { initial: 'walk', states: { walk: { on: { PED_COUNTDOWN: 'wait' }, entry: 'enter_walk', exit: 'exit_walk' }, wait: { on: { PED_COUNTDOWN: 'stop' }, entry: 'enter_wait', exit: 'exit_wait' }, stop: { entry: ['enter_stop'], exit: ['exit_stop'] } } };
const newLightMachine = Machine({ key: 'light', initial: 'green', states: { green: { on: { TIMER: 'yellow', POWER_OUTAGE: 'red', NOTHING: 'green' }, entry: 'enter_green', exit: 'exit_green' }, yellow: { on: { TIMER: 'red', POWER_OUTAGE: 'red' }, entry: 'enter_yellow', exit: 'exit_yellow' }, red: { on: { TIMER: 'green', POWER_OUTAGE: 'red', NOTHING: 'red' }, entry: 'enter_red', exit: 'exit_red', ...newPedestrianStates } } });
const parallelMachine = Machine({ type: 'parallel', states: { a: { initial: 'a1', states: { a1: { on: { CHANGE: { target: 'a2', actions: ['do_a2', 'another_do_a2'] } }, entry: 'enter_a1', exit: 'exit_a1' }, a2: { entry: 'enter_a2', exit: 'exit_a2' } }, entry: 'enter_a', exit: 'exit_a' }, b: { initial: 'b1', states: { b1: { on: { CHANGE: { target: 'b2', actions: 'do_b2' } }, entry: 'enter_b1', exit: 'exit_b1' }, b2: { entry: 'enter_b2', exit: 'exit_b2' } }, entry: 'enter_b', exit: 'exit_b' } } });
const deepMachine = Machine({ initial: 'a', states: { a: { initial: 'a1', states: { a1: { on: { NEXT: 'a2', NEXT_FN: 'a3' }, entry: 'enter_a1', exit: 'exit_a1' }, a2: { entry: 'enter_a2', exit: 'exit_a2' }, a3: { on: { NEXT: { target: 'a2', actions: [ function do_a3_to_a2() { return; } ] } }, entry: function enter_a3_fn() { return; }, exit: function exit_a3_fn() { return; } } }, entry: 'enter_a', exit: ['exit_a', 'another_exit_a'], on: { CHANGE: 'b' } }, b: { entry: ['enter_b', 'another_enter_b'], exit: 'exit_b', initial: 'b1', states: { b1: { entry: 'enter_b1', exit: 'exit_b1' } } } } });
const parallelMachine2 = Machine({ initial: 'A', states: { A: { on: { 'to-B': 'B' } }, B: { type: 'parallel', on: { 'to-A': 'A' }, states: { C: { initial: 'C1', states: { C1: {}, C2: {} } }, D: { initial: 'D1', states: { D1: { on: { 'to-D2': 'D2' } }, D2: { entry: ['D2 Entry'], exit: ['D2 Exit'] } } } } } } });
describe('State.actions', () => { it('should return the entry actions of an initial state', () => { expect(lightMachine.initialState.actions.map((a) => a.type)).toEqual([ 'enter_green' ]); });
it('should return the entry actions of an initial state (deep)', () => { expect(deepMachine.initialState.actions.map((a) => a.type)).toEqual([ 'enter_a', 'enter_a1' ]); });
it('should return the entry actions of an initial state (parallel)', () => { expect(parallelMachine.initialState.actions.map((a) => a.type)).toEqual([ 'enter_a', 'enter_a1', 'enter_b', 'enter_b1' ]); });
it('should return the entry and exit actions of a transition', () => { expect( lightMachine.transition('green', 'TIMER').actions.map((a) => a.type) ).toEqual(['exit_green', 'enter_yellow']); });
it('should return the entry and exit actions of a deep transition', () => { expect( lightMachine.transition('yellow', 'TIMER').actions.map((a) => a.type) ).toEqual(['exit_yellow', 'enter_red', 'enter_walk']); });
it('should return the entry and exit actions of a nested transition', () => { expect( lightMachine .transition('red.walk', 'PED_COUNTDOWN') .actions.map((a) => a.type) ).toEqual(['exit_walk', 'enter_wait']); });
it('should not have actions for unhandled events (shallow)', () => { expect( lightMachine.transition('green', 'FAKE').actions.map((a) => a.type) ).toEqual([]); });
it('should not have actions for unhandled events (deep)', () => { expect( lightMachine.transition('red', 'FAKE').actions.map((a) => a.type) ).toEqual([]); });
it('should exit and enter the state for self-transitions (shallow)', () => { expect( lightMachine.transition('green', 'NOTHING').actions.map((a) => a.type) ).toEqual(['exit_green', 'enter_green']); });
it('should exit and enter the state for self-transitions (deep)', () => { // 'red' state resolves to 'red.walk' expect( lightMachine.transition('red', 'NOTHING').actions.map((a) => a.type) ).toEqual(['exit_walk', 'exit_red', 'enter_red', 'enter_walk']); });
it('should return actions for parallel machines', () => { expect( parallelMachine .transition(parallelMachine.initialState, 'CHANGE') .actions.map((a) => a.type) ).toEqual([ 'exit_b1', // reverse document order 'exit_a1', 'do_a2', 'another_do_a2', 'do_b2', 'enter_a2', 'enter_b2' ]); });
it('should return nested actions in the correct (child to parent) order', () => { expect( deepMachine.transition('a.a1', 'CHANGE').actions.map((a) => a.type) ).toEqual([ 'exit_a1', 'exit_a', 'another_exit_a', 'enter_b', 'another_enter_b', 'enter_b1' ]); });
it('should ignore parent state actions for same-parent substates', () => { expect( deepMachine.transition('a.a1', 'NEXT').actions.map((a) => a.type) ).toEqual(['exit_a1', 'enter_a2']); });
it('should work with function actions', () => { expect( deepMachine .transition(deepMachine.initialState, 'NEXT_FN') .actions.map((action) => action.type) ).toEqual(['exit_a1', 'enter_a3_fn']);
expect( deepMachine .transition('a.a3', 'NEXT') .actions.map((action) => action.type) ).toEqual(['exit_a3_fn', 'do_a3_to_a2', 'enter_a2']); });
it('should exit children of parallel state nodes', () => { const stateB = parallelMachine2.transition( parallelMachine2.initialState, 'to-B' ); const stateD2 = parallelMachine2.transition(stateB, 'to-D2'); const stateA = parallelMachine2.transition(stateD2, 'to-A');
expect(stateA.actions.map((action) => action.type)).toEqual(['D2 Exit']); });
describe('should ignore same-parent state actions (sparse)', () => { const fooBar = { initial: 'foo', states: { foo: { on: { TACK: 'bar', ABSOLUTE_TACK: '#machine.ping.bar' } }, bar: { on: { TACK: 'foo' } } } };
const pingPong = Machine({ initial: 'ping', key: 'machine', states: { ping: { entry: ['entryEvent'], on: { TICK: 'pong' }, ...fooBar }, pong: { on: { TICK: 'ping' } } } });
it('with a relative transition', () => { expect(pingPong.transition('ping.foo', 'TACK').actions).toHaveLength(0); });
it('with an absolute transition', () => { expect( pingPong.transition('ping.foo', 'ABSOLUTE_TACK').actions ).toHaveLength(0); }); }); });
describe('State.actions (with entry/exit instead of onEntry/onExit)', () => { it('should return the entry actions of an initial state', () => { expect(newLightMachine.initialState.actions.map((a) => a.type)).toEqual([ 'enter_green' ]); });
it('should return the entry and exit actions of a transition', () => { expect( newLightMachine.transition('green', 'TIMER').actions.map((a) => a.type) ).toEqual(['exit_green', 'enter_yellow']); });
it('should return the entry and exit actions of a deep transition', () => { expect( newLightMachine.transition('yellow', 'TIMER').actions.map((a) => a.type) ).toEqual(['exit_yellow', 'enter_red', 'enter_walk']); });
it('should return the entry and exit actions of a nested transition', () => { expect( newLightMachine .transition('red.walk', 'PED_COUNTDOWN') .actions.map((a) => a.type) ).toEqual(['exit_walk', 'enter_wait']); });
it('should not have actions for unhandled events (shallow)', () => { expect( newLightMachine.transition('green', 'FAKE').actions.map((a) => a.type) ).toEqual([]); });
it('should not have actions for unhandled events (deep)', () => { expect( newLightMachine.transition('red', 'FAKE').actions.map((a) => a.type) ).toEqual([]); });
it('should exit and enter the state for self-transitions (shallow)', () => { expect( newLightMachine .transition('green', 'NOTHING') .actions.map((a) => a.type) ).toEqual(['exit_green', 'enter_green']); });
it('should exit and enter the state for self-transitions (deep)', () => { // 'red' state resolves to 'red.walk' expect( newLightMachine.transition('red', 'NOTHING').actions.map((a) => a.type) ).toEqual(['exit_walk', 'exit_red', 'enter_red', 'enter_walk']); }); });
describe('parallel states', () => { it('should return entry action defined on parallel state', () => { const parallelMachineWithOnEntry = Machine({ id: 'fetch', context: { attempts: 0 }, initial: 'start', states: { start: { on: { ENTER_PARALLEL: 'p1' } }, p1: { type: 'parallel', entry: 'enter_p1', states: { nested: { initial: 'inner', states: { inner: { entry: 'enter_inner' } } } } } } });
expect( parallelMachineWithOnEntry .transition('start', 'ENTER_PARALLEL') .actions.map((a) => a.type) ).toEqual(['enter_p1', 'enter_inner']); }); });
describe('targetless transitions', () => { it("shouldn't exit a state on a parent's targetless transition", (done) => { const actual: string[] = [];
const parent = Machine({ initial: 'one', on: { WHATEVER: { actions: () => { actual.push('got WHATEVER'); } } }, states: { one: { entry: () => { actual.push('entered one'); }, always: 'two' }, two: { exit: () => { actual.push('exited two'); } } } });
const service = interpret(parent).start();
Promise.resolve() .then(() => { service.send('WHATEVER'); }) .then(() => { expect(actual).toEqual(['entered one', 'got WHATEVER']); done(); }) .catch(done); });
it("shouldn't exit (and reenter) state on targetless delayed transition", (done) => { const actual: string[] = [];
const machine = Machine({ initial: 'one', states: { one: { entry: () => { actual.push('entered one'); }, exit: () => { actual.push('exited one'); }, after: { 10: { actions: () => { actual.push('got FOO'); } } } } } });
interpret(machine).start();
setTimeout(() => { expect(actual).toEqual(['entered one', 'got FOO']); done(); }, 50); }); });
describe('when reaching a final state', () => { // https://github.com/statelyai/xstate/issues/1109 it('exit actions should be called when invoked machine reaches its final state', (done) => { let exitCalled = false; let childExitCalled = false; const childMachine = Machine({ exit: () => { exitCalled = true; }, initial: 'a', states: { a: { type: 'final', exit: () => { childExitCalled = true; } } } });
const parentMachine = Machine({ initial: 'active', states: { active: { invoke: { src: childMachine, onDone: 'finished' } }, finished: { type: 'final' } } });
interpret(parentMachine) .onDone(() => { expect(exitCalled).toBeTruthy(); expect(childExitCalled).toBeTruthy(); done(); }) .start(); }); });
describe('when stopped', () => { it('exit actions should be called when stopping a machine', () => { let exitCalled = false; let childExitCalled = false;
const machine = Machine({ exit: () => { exitCalled = true; }, initial: 'a', states: { a: { exit: () => { childExitCalled = true; } } } });
const service = interpret(machine).start(); service.stop();
expect(exitCalled).toBeTruthy(); expect(childExitCalled).toBeTruthy(); });
it('should call each exit handler only once when the service gets stopped', () => { const actual: string[] = []; const machine = createMachine({ exit: () => actual.push('root'), initial: 'a', states: { a: { exit: () => actual.push('a'), initial: 'a1', states: { a1: { exit: () => actual.push('a1') } } } } });
interpret(machine).start().stop(); expect(actual).toEqual(['a1', 'a', 'root']); });
it('should call exit actions in reversed document order when the service gets stopped', () => { const actual: string[] = []; const machine = createMachine({ exit: () => actual.push('root'), initial: 'a', states: { a: { exit: () => actual.push('a'), on: { EV: { // just a noop action to ensure that a transition is selected when we send an event actions: () => {} } } } } });
const service = interpret(machine).start(); // it's important to send an event here that results in a transition that computes new `state.configuration` // and that could impact the order in which exit actions are called service.send({ type: 'EV' }); service.stop();
expect(actual).toEqual(['a', 'root']); });
it('should call exit actions of parallel states in reversed document order when the service gets stopped after earlier region transition', () => { const actual: string[] = []; const machine = createMachine({ exit: () => actual.push('root'), type: 'parallel', states: { a: { exit: () => actual.push('a'), initial: 'child_a', states: { child_a: { exit: () => actual.push('child_a'), on: { EV: { // just a noop action to ensure that a transition is selected when we send an event actions: () => {} } } } } }, b: { exit: () => actual.push('b'), initial: 'child_b', states: { child_b: { exit: () => actual.push('child_b') } } } } });
const service = interpret(machine).start(); // it's important to send an event here that results in a transition as that computes new `state.configuration` // and that could impact the order in which exit actions are called service.send({ type: 'EV' }); service.stop();
expect(actual).toEqual(['child_b', 'b', 'child_a', 'a', 'root']); });
it('should call exit actions of parallel states in reversed document order when the service gets stopped after later region transition', () => { const actual: string[] = []; const machine = createMachine({ exit: () => actual.push('root'), type: 'parallel', states: { a: { exit: () => actual.push('a'), initial: 'child_a', states: { child_a: { exit: () => actual.push('child_a') } } }, b: { exit: () => actual.push('b'), initial: 'child_b', states: { child_b: { exit: () => actual.push('child_b'), on: { EV: { // just a noop action to ensure that a transition is selected when we send an event actions: () => {} } } } } } } });
const service = interpret(machine).start(); // it's important to send an event here that results in a transition as that computes new `state.configuration` // and that could impact the order in which exit actions are called service.send({ type: 'EV' }); service.stop();
expect(actual).toEqual(['child_b', 'b', 'child_a', 'a', 'root']); });
it('should call exit actions of parallel states in reversed document order when the service gets stopped after multiple regions transition', () => { const actual: string[] = []; const machine = createMachine({ exit: () => actual.push('root'), type: 'parallel', states: { a: { exit: () => actual.push('a'), initial: 'child_a', states: { child_a: { exit: () => actual.push('child_a'), on: { EV: { // just a noop action to ensure that a transition is selected when we send an event actions: () => {} } } } } }, b: { exit: () => actual.push('b'), initial: 'child_b', states: { child_b: { exit: () => actual.push('child_b'), on: { EV: { // just a noop action to ensure that a transition is selected when we send an event actions: () => {} } } } } } } });
const service = interpret(machine).start(); // it's important to send an event here that results in a transition as that computes new `state.configuration` // and that could impact the order in which exit actions are called service.send({ type: 'EV' }); service.stop();
expect(actual).toEqual(['child_b', 'b', 'child_a', 'a', 'root']); }); });});
describe('actions on invalid transition', () => { const stopMachine = Machine({ initial: 'idle', states: { idle: { on: { STOP: { target: 'stop', actions: ['action1'] } } }, stop: {} } });
it('should not recall previous actions', () => { const nextState = stopMachine.transition('idle', 'STOP'); expect(stopMachine.transition(nextState, 'INVALID').actions).toHaveLength( 0 ); });});
describe('actions config', () => { type EventType = | { type: 'definedAction' } | { type: 'updateContext' } | { type: 'EVENT' } | { type: 'E' }; interface Context { count: number; } interface State { states: { a: {}; b: {}; }; }
// tslint:disable-next-line:no-empty const definedAction = () => {}; const simpleMachine = Machine<Context, State, EventType>( { initial: 'a', context: { count: 0 }, states: { a: { entry: [ 'definedAction', { type: 'definedAction' }, 'undefinedAction' ], on: { EVENT: { target: 'b', actions: [{ type: 'definedAction' }, { type: 'updateContext' }] } } }, b: {} }, on: { E: 'a' } }, { actions: { definedAction, updateContext: assign({ count: 10 }) } } ); it('should reference actions defined in actions parameter of machine options', () => { const { initialState } = simpleMachine; const nextState = simpleMachine.transition(initialState, 'E');
expect(nextState.actions.map((a) => a.type)).toEqual( expect.arrayContaining(['definedAction', 'undefinedAction']) );
expect(nextState.actions).toEqual([ expect.objectContaining({ type: 'definedAction' }), expect.objectContaining({ type: 'definedAction' }), expect.objectContaining({ type: 'undefinedAction' }) ]); });
it('should reference actions defined in actions parameter of machine options (initial state)', () => { const { initialState } = simpleMachine;
expect(initialState.actions.map((a) => a.type)).toEqual( expect.arrayContaining(['definedAction', 'undefinedAction']) ); });
it('should be able to reference action implementations from action objects', () => { const state = simpleMachine.transition('a', 'EVENT');
expect(state.actions).toEqual([ expect.objectContaining({ type: 'definedAction' }) ]);
expect(state.context).toEqual({ count: 10 }); });
it('should work with anonymous functions (with warning)', () => { let onEntryCalled = false; let actionCalled = false; let onExitCalled = false;
const anonMachine = Machine({ id: 'anon', initial: 'active', states: { active: { entry: () => (onEntryCalled = true), exit: () => (onExitCalled = true), on: { EVENT: { target: 'inactive', actions: [() => (actionCalled = true)] } } }, inactive: {} } });
const { initialState } = anonMachine;
initialState.actions.forEach((action) => { if (action.exec) { action.exec( initialState.context, { type: 'any' }, { action, state: initialState, _event: initialState._event } ); } });
expect(onEntryCalled).toBe(true);
const inactiveState = anonMachine.transition(initialState, 'EVENT');
expect(inactiveState.actions.length).toBe(2);
inactiveState.actions.forEach((action) => { if (action.exec) { action.exec( inactiveState.context, { type: 'EVENT' }, { action, state: initialState, _event: initialState._event } ); } });
expect(onExitCalled).toBe(true); expect(actionCalled).toBe(true); });});
describe('action meta', () => { it('should provide the original action and state to the exec function', (done) => { const testMachine = Machine( { id: 'test', initial: 'foo', states: { foo: { entry: { type: 'entryAction', value: 'something' } } } }, { actions: { entryAction: (_, __, meta) => { expect(meta.state.value).toEqual('foo'); expect(meta.action.type).toEqual('entryAction'); expect(meta.action.value).toEqual('something'); done(); } } } );
interpret(testMachine).start(); });});
describe('purely defined actions', () => { interface Ctx { items: Array<{ id: number }>; } type Events = | { type: 'SINGLE'; id: number } | { type: 'NONE'; id: number } | { type: 'EACH' };
const dynamicMachine = Machine<Ctx, Events>({ id: 'dynamic', initial: 'idle', context: { items: [{ id: 1 }, { id: 2 }, { id: 3 }] }, states: { idle: { on: { SINGLE: { actions: pure<any, any>((ctx, e) => { if (ctx.items.length > 0) { return { type: 'SINGLE_EVENT', length: ctx.items.length, id: e.id }; } }) }, NONE: { actions: pure<any, any>((ctx, e) => { if (ctx.items.length > 5) { return { type: 'SINGLE_EVENT', length: ctx.items.length, id: e.id }; } }) }, EACH: { actions: pure<any, any>((ctx) => ctx.items.map((item: any, index: number) => ({ type: 'EVENT', item, index })) ) } } } } });
it('should allow for a purely defined dynamic action', () => { const nextState = dynamicMachine.transition(dynamicMachine.initialState, { type: 'SINGLE', id: 3 });
expect(nextState.actions).toEqual([ { type: 'SINGLE_EVENT', length: 3, id: 3 } ]); });
it('should allow for purely defined lack of actions', () => { const nextState = dynamicMachine.transition(dynamicMachine.initialState, { type: 'NONE', id: 3 });
expect(nextState.actions).toEqual([]); });
it('should allow for purely defined dynamic actions', () => { const nextState = dynamicMachine.transition( dynamicMachine.initialState, 'EACH' );
expect(nextState.actions).toEqual([ { type: 'EVENT', item: { id: 1 }, index: 0 }, { type: 'EVENT', item: { id: 2 }, index: 1 }, { type: 'EVENT', item: { id: 3 }, index: 2 } ]); });});
describe('forwardTo()', () => { it('should forward an event to a service', (done) => { const child = Machine<void, { type: 'EVENT'; value: number }>({ id: 'child', initial: 'active', states: { active: { on: { EVENT: { actions: sendParent('SUCCESS'), cond: (_, e) => e.value === 42 } } } } });
const parent = Machine({ id: 'parent', initial: 'first', states: { first: { invoke: { src: child, id: 'myChild' }, on: { EVENT: { actions: forwardTo('myChild') }, SUCCESS: 'last' } }, last: { type: 'final' } } });
const service = interpret(parent) .onDone(() => done()) .start();
service.send('EVENT', { value: 42 }); });
it('should forward an event to a service (dynamic)', (done) => { const child = Machine<void, { type: 'EVENT'; value: number }>({ id: 'child', initial: 'active', states: { active: { on: { EVENT: { actions: sendParent('SUCCESS'), cond: (_, e) => e.value === 42 } } } } });
const parent = Machine<{ child: any }>({ id: 'parent', initial: 'first', context: { child: null }, states: { first: { entry: assign({ child: () => spawn(child) }), on: { EVENT: { actions: forwardTo((ctx) => ctx.child) }, SUCCESS: 'last' } }, last: { type: 'final' } } });
const service = interpret(parent) .onDone(() => done()) .start();
service.send('EVENT', { value: 42 }); });});
describe('log()', () => { const logMachine = Machine<{ count: number }>({ id: 'log', initial: 'string', context: { count: 42 }, states: { string: { entry: log('some string', 'string label'), on: { EXPR: { actions: log((ctx) => `expr ${ctx.count}`, 'expr label') } } } } });
it('should log a string', () => { expect(logMachine.initialState.actions[0]).toMatchInlineSnapshot(` Object { "expr": "some string", "label": "string label", "type": "xstate.log", "value": "some string", } `); });
it('should log an expression', () => { const nextState = logMachine.transition(logMachine.initialState, 'EXPR'); expect(nextState.actions[0]).toMatchInlineSnapshot(` Object { "expr": [Function], "label": "expr label", "type": "xstate.log", "value": "expr 42", } `); });});
describe('choose', () => { it('should execute a single conditional action', () => { interface Ctx { answer?: number; }
const machine = createMachine<Ctx>({ context: {}, initial: 'foo', states: { foo: { entry: choose([ { cond: () => true, actions: assign<Ctx>({ answer: 42 }) } ]) } } });
const service = interpret(machine).start();
expect(service.state.context).toEqual({ answer: 42 }); });
it('should execute a multiple conditional actions', () => { let executed = false;
interface Ctx { answer?: number; }
const machine = createMachine<Ctx>({ context: {}, initial: 'foo', states: { foo: { entry: choose([ { cond: () => true, actions: [() => (executed = true), assign<Ctx>({ answer: 42 })] } ]) } } });
const service = interpret(machine).start();
expect(service.state.context).toEqual({ answer: 42 }); expect(executed).toBeTruthy(); });
it('should only execute matched actions', () => { interface Ctx { answer?: number; shouldNotAppear?: boolean; }
const machine = createMachine<Ctx>({ context: {}, initial: 'foo', states: { foo: { entry: choose([ { cond: () => false, actions: assign<Ctx>({ shouldNotAppear: true }) }, { cond: () => true, actions: assign<Ctx>({ answer: 42 }) } ]) } } });
const service = interpret(machine).start();
expect(service.state.context).toEqual({ answer: 42 }); });
it('should allow for fallback unguarded actions', () => { interface Ctx { answer?: number; shouldNotAppear?: boolean; }
const machine = createMachine<Ctx>({ context: {}, initial: 'foo', states: { foo: { entry: choose([ { cond: () => false, actions: assign<Ctx>({ shouldNotAppear: true }) }, { actions: assign<Ctx>({ answer: 42 }) } ]) } } });
const service = interpret(machine).start();
expect(service.state.context).toEqual({ answer: 42 }); });
it('should allow for nested conditional actions', () => { interface Ctx { firstLevel: boolean; secondLevel: boolean; thirdLevel: boolean; }
const machine = createMachine<Ctx>({ context: { firstLevel: false, secondLevel: false, thirdLevel: false }, initial: 'foo', states: { foo: { entry: choose([ { cond: () => true, actions: [ assign<Ctx>({ firstLevel: true }), choose([ { cond: () => true, actions: [ assign<Ctx>({ secondLevel: true }), choose([ { cond: () => true, actions: [assign<Ctx>({ thirdLevel: true })] } ]) ] } ]) ] } ]) } } });
const service = interpret(machine).start();
expect(service.state.context).toEqual({ firstLevel: true, secondLevel: true, thirdLevel: true }); });
it('should provide context to a condition expression', () => { interface Ctx { counter: number; answer?: number; } const machine = createMachine<Ctx>({ context: { counter: 101 }, initial: 'foo', states: { foo: { entry: choose([ { cond: (ctx) => ctx.counter > 100, actions: assign<Ctx>({ answer: 42 }) } ]) } } });
const service = interpret(machine).start();
expect(service.state.context).toEqual({ counter: 101, answer: 42 }); });
it('should provide event to a condition expression', () => { interface Ctx { answer?: number; } interface Events { type: 'NEXT'; counter: number; }
const machine = createMachine<Ctx, Events>({ context: {}, initial: 'foo', states: { foo: { on: { NEXT: { target: 'bar', actions: choose<Ctx, Events>([ { cond: (_, event) => event.counter > 100, actions: assign<Ctx, Events>({ answer: 42 }) } ]) } } }, bar: {} } });
const service = interpret(machine).start(); service.send({ type: 'NEXT', counter: 101 }); expect(service.state.context).toEqual({ answer: 42 }); });
it('should provide stateGuard.state to a condition expression', () => { type Ctx = { counter: number; answer?: number }; const machine = createMachine<Ctx>({ context: { counter: 101 }, type: 'parallel', states: { foo: { initial: 'waiting', states: { waiting: { on: { GIVE_ANSWER: 'answering' } }, answering: { entry: choose([ { cond: (_, __, { state }) => state.matches('bar'), actions: assign<Ctx>({ answer: 42 }) } ]) } } }, bar: {} } });
const service = interpret(machine).start(); service.send('GIVE_ANSWER');
expect(service.state.context).toEqual({ counter: 101, answer: 42 }); });
it('should be able to use actions and guards defined in options', () => { interface Ctx { answer?: number; }
const machine = createMachine<Ctx>( { context: {}, initial: 'foo', states: { foo: { entry: choose([{ cond: 'worstGuard', actions: 'revealAnswer' }]) } } }, { guards: { worstGuard: () => true }, actions: { revealAnswer: assign<Ctx>({ answer: 42 }) } } );
const service = interpret(machine).start();
expect(service.state.context).toEqual({ answer: 42 }); });
it('should be able to use choose actions from within options', () => { interface Ctx { answer?: number; }
const machine = createMachine<Ctx>( { context: {}, initial: 'foo', states: { foo: { entry: 'conditionallyRevealAnswer' } } }, { guards: { worstGuard: () => true }, actions: { revealAnswer: assign<Ctx>({ answer: 42 }), conditionallyRevealAnswer: choose([ { cond: 'worstGuard', actions: 'revealAnswer' } ]) } } );
const service = interpret(machine).start();
expect(service.state.context).toEqual({ answer: 42 }); });});
describe('sendParent', () => { // https://github.com/statelyai/xstate/issues/711 it('TS: should compile for any event', () => { interface ChildContext {} interface ChildEvent { type: 'CHILD'; }
const child = Machine<ChildContext, any, ChildEvent>({ id: 'child', initial: 'start', states: { start: { // This should not be a TypeScript error entry: [sendParent({ type: 'PARENT' })] } } });
expect(child).toBeTruthy(); });});
describe('sendTo', () => { it('should be able to send an event to an actor', (done) => { const childMachine = createMachine<any, { type: 'EVENT' }>({ initial: 'waiting', states: { waiting: { on: { EVENT: { actions: () => done() } } } } });
const parentMachine = createMachine<{ child: ActorRefFrom<typeof childMachine>; }>({ context: () => ({ child: spawn(childMachine) }), entry: sendTo((ctx) => ctx.child, { type: 'EVENT' }) });
interpret(parentMachine).start(); });
it('should be able to send an event from expression to an actor', (done) => { const childMachine = createMachine<any, { type: 'EVENT'; count: number }>({ initial: 'waiting', states: { waiting: { on: { EVENT: { cond: (_, e) => e.count === 42, actions: () => done() } } } } });
const parentMachine = createMachine<{ child: ActorRefFrom<typeof childMachine>; count: number; }>({ context: () => ({ child: spawn(childMachine), count: 42 }), entry: sendTo( (ctx) => ctx.child, (ctx) => ({ type: 'EVENT', count: ctx.count }) ) });
interpret(parentMachine).start(); });
it('should report a type error for an invalid event', () => { const childMachine = createMachine<any, { type: 'EVENT' }>({ initial: 'waiting', states: { waiting: { on: { EVENT: {} } } } });
createMachine<{ child: ActorRefFrom<typeof childMachine>; }>({ context: () => ({ child: spawn(childMachine) }), entry: sendTo((ctx) => ctx.child, { // @ts-expect-error type: 'UNKNOWN' }) }); });});
it('should call transition actions in document order for same-level parallel regions', () => { const actual: string[] = [];
const machine = createMachine({ type: 'parallel', states: { a: { on: { FOO: { actions: () => actual.push('a') } } }, b: { on: { FOO: { actions: () => actual.push('b') } } } } }); const service = interpret(machine).start(); service.send({ type: 'FOO' });
expect(actual).toEqual(['a', 'b']);});
it('should call transition actions in document order for states at different levels of parallel regions', () => { const actual: string[] = [];
const machine = createMachine({ type: 'parallel', states: { a: { initial: 'a1', states: { a1: { on: { FOO: { actions: () => actual.push('a1') } } } } }, b: { on: { FOO: { actions: () => actual.push('b') } } } } }); const service = interpret(machine).start(); service.send({ type: 'FOO' });
expect(actual).toEqual(['a1', 'b']);});
describe('assign action order', () => { it('should preserve action order when .preserveActionOrder = true', () => { const captured: number[] = [];
const machine = createMachine<{ count: number }>({ context: { count: 0 }, entry: [ (ctx) => captured.push(ctx.count), // 0 assign({ count: (ctx) => ctx.count + 1 }), (ctx) => captured.push(ctx.count), // 1 assign({ count: (ctx) => ctx.count + 1 }), (ctx) => captured.push(ctx.count) // 2 ], preserveActionOrder: true });
interpret(machine).start();
expect(captured).toEqual([0, 1, 2]); });
it('should deeply preserve action order when .preserveActionOrder = true', () => { const captured: number[] = [];
interface CountCtx { count: number; }
const machine = createMachine<CountCtx>({ context: { count: 0 }, entry: [ (ctx) => captured.push(ctx.count), // 0 pure(() => { return [ assign<CountCtx>({ count: (ctx) => ctx.count + 1 }), { type: 'capture', exec: (ctx: any) => captured.push(ctx.count) }, // 1 assign<CountCtx>({ count: (ctx) => ctx.count + 1 }) ]; }), (ctx) => captured.push(ctx.count) // 2 ], preserveActionOrder: true });
interpret(machine).start();
expect(captured).toEqual([0, 1, 2]); });
it('should capture correct context values on subsequent transitions', () => { let captured: number[] = [];
const machine = createMachine<{ counter: number }>({ context: { counter: 0 }, on: { EV: { actions: [ assign({ counter: (ctx) => ctx.counter + 1 }), (ctx) => captured.push(ctx.counter) ] } }, preserveActionOrder: true });
const service = interpret(machine).start();
service.send('EV'); service.send('EV');
expect(captured).toEqual([1, 2]); });
it.each([undefined, false])( 'should prioritize assign actions when .preserveActionOrder = %i', (preserveActionOrder) => { const captured: number[] = [];
const machine = createMachine<{ count: number }>({ context: { count: 0 }, entry: [ (ctx) => captured.push(ctx.count), assign({ count: (ctx) => ctx.count + 1 }), (ctx) => captured.push(ctx.count), assign({ count: (ctx) => ctx.count + 1 }), (ctx) => captured.push(ctx.count) ], preserveActionOrder });
interpret(machine).start();
expect(captured).toEqual([2, 2, 2]); } );});