deno.land / x / xstate@xstate@4.33.6 / test / actions.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478import { Machine, createMachine, assign, forwardTo, interpret, spawn, ActorRefFrom} from '../src/index';import { pure, sendParent, log, choose, sendTo, stop, send} 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']); });
it("should reenter targeted ancestor (as it's a descendant of the transition domain)", () => { const actual: string[] = []; const machine = createMachine({ initial: 'loaded', states: { loaded: { id: 'loaded', entry: () => actual.push('loaded entry'), initial: 'idle', states: { idle: { on: { UPDATE: '#loaded' } } } } } });
const service = interpret(machine).start();
actual.length = 0; service.send('UPDATE');
expect(actual).toEqual(['loaded entry']); });
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']); });
it('should exit deep descendant during a self-transition', () => { const actual: string[] = []; const m = createMachine({ initial: 'a', states: { a: { on: { EV: 'a' }, initial: 'a1', states: { a1: { initial: 'a11', states: { a11: { exit: () => actual.push('a11.exit') } } } } } } });
const service = interpret(m).start();
service.send('EV');
expect(actual).toEqual(['a11.exit']); }); });
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']); });
it('should reenter parallel region when a parallel state gets reentered while targeting another region', () => { const actions: string[] = [];
const machine = createMachine({ initial: 'ready', states: { ready: { type: 'parallel', on: { FOO: '#cameraOff' }, states: { devicesInfo: { entry: () => actions.push('entry devicesInfo'), exit: () => actions.push('exit devicesInfo') }, camera: { entry: () => actions.push('entry camera'), exit: () => actions.push('exit camera'), initial: 'on', states: { on: {}, off: { id: 'cameraOff' } } } } } } });
const service = interpret(machine).start();
actions.length = 0; service.send('FOO');
expect(actions).toEqual([ 'exit camera', 'exit devicesInfo', 'entry devicesInfo', 'entry camera' ]); }); });
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']); });
it('an exit action executed when an interpreter gets stopped should receive `xstate.stop` event', () => { let receivedEvent; const machine = createMachine({ exit: (_ctx, ev) => { receivedEvent = ev; } });
const service = interpret(machine).start(); service.stop();
expect(receivedEvent).toEqual({ type: 'xstate.stop' }); });
it('an exit action executed when an interpreter reaches its final state should be called with the last received event', () => { let receivedEvent; const machine = createMachine({ initial: 'a', states: { a: { on: { NEXT: 'b' } }, b: { type: 'final' } }, exit: (_ctx, ev) => { receivedEvent = ev; } });
const service = interpret(machine).start(); service.send({ type: 'NEXT' });
expect(receivedEvent).toEqual({ type: 'NEXT' }); });
// https://github.com/statelyai/xstate/issues/2880 it('stopping an interpreter that receives events from its children exit handlers should not throw', () => { const child = createMachine({ id: 'child', initial: 'idle', states: { idle: { exit: sendParent('EXIT') } } });
const parent = createMachine({ id: 'parent', invoke: child });
const interpreter = interpret(parent); interpreter.start();
expect(() => interpreter.stop()).not.toThrow(); });
it('sent events from exit handlers of a stopped child should not be received by the parent', () => { const child = createMachine({ id: 'child', initial: 'idle', states: { idle: { exit: sendParent('EXIT') } } });
const parent = createMachine({ id: 'parent', context: () => ({ child: spawn(child) }), on: { STOP_CHILD: { actions: stop((ctx: any) => ctx.child) }, EXIT: { actions: () => { throw new Error('This should not be called.'); } } } });
const interpreter = interpret(parent).start(); interpreter.send({ type: 'STOP_CHILD' }); });
it('sent events from exit handlers of a done child should be received by the parent ', () => { let eventReceived = false;
const child = createMachine({ id: 'child', initial: 'active', states: { active: { on: { FINISH: 'done' } }, done: { type: 'final' } }, exit: sendParent('CHILD_DONE') });
const parent = createMachine({ id: 'parent', context: () => ({ child: spawn(child) }), on: { FINISH_CHILD: { actions: send({ type: 'FINISH' }, { to: (ctx: any) => ctx.child }) }, CHILD_DONE: { actions: () => { eventReceived = true; } } } });
const interpreter = interpret(parent).start(); interpreter.send({ type: 'FINISH_CHILD' });
expect(eventReceived).toBe(true); });
it('sent events from exit handlers of a stopped child should be received by its children ', () => { let eventReceived = false;
const grandchild = createMachine({ id: 'grandchild', on: { STOPPED: { actions: () => { eventReceived = true; } } } });
const child = createMachine({ id: 'child', invoke: { id: 'myChild', src: grandchild }, exit: send({ type: 'STOPPED' }, { to: 'myChild' }) });
const parent = createMachine({ id: 'parent', initial: 'a', states: { a: { invoke: { src: child }, on: { NEXT: 'b' } }, b: {} } });
const interpreter = interpret(parent).start(); interpreter.send({ type: 'NEXT' });
expect(eventReceived).toBe(true); });
it('sent events from exit handlers of a done child should be received by its children ', () => { let eventReceived = false;
const grandchild = createMachine({ id: 'grandchild', on: { STOPPED: { actions: () => { eventReceived = true; } } } });
const child = createMachine({ id: 'child', initial: 'a', invoke: { id: 'myChild', src: grandchild }, states: { a: { on: { FINISH: 'b' } }, b: { type: 'final' } }, exit: send({ type: 'STOPPED' }, { to: 'myChild' }) });
const parent = createMachine({ id: 'parent', invoke: { id: 'myChild', src: child }, on: { NEXT: { actions: send({ type: 'FINISH' }, { to: 'myChild' }) } } });
const interpreter = interpret(parent).start(); interpreter.send({ type: 'NEXT' });
expect(eventReceived).toBe(true); });
it('actors spawned in exit handlers of a stopped child should not be started', () => { const grandchild = createMachine({ id: 'grandchild', entry: () => { throw new Error('This should not be called.'); } });
const parent = createMachine({ id: 'parent', context: {}, exit: assign({ actorRef: () => spawn(grandchild) }) });
const interpreter = interpret(parent).start(); interpreter.stop(); });
it('should execute referenced custom actions correctly when stopping an interpreter', () => { let called = false; const parent = createMachine( { id: 'parent', context: {}, exit: 'referencedAction' }, { actions: { referencedAction: () => { called = true; } } } );
const interpreter = interpret(parent).start(); interpreter.stop();
expect(called).toBe(true); });
it('should execute builtin actions correctly when stopping an interpreter', () => { const machine = createMachine( { context: { executedAssigns: [] as string[] }, exit: [ 'referencedAction', assign({ executedAssigns: (ctx: any) => [...ctx.executedAssigns, 'inline'] }) ] }, { actions: { referencedAction: assign({ executedAssigns: (ctx) => [...ctx.executedAssigns, 'referenced'] }) } } );
const interpreter = interpret(machine).start(); interpreter.stop();
expect(interpreter.state.context.executedAssigns).toEqual([ 'referenced', 'inline' ]); });
it('should clear all scheduled events when the interpreter gets stopped', () => { const machine = createMachine({ on: { INITIALIZE_SYNC_SEQUENCE: { actions: () => { // schedule those 2 events service.send({ type: 'SOME_EVENT' }); service.send({ type: 'SOME_EVENT' }); // but also immediately stop *while* the `INITIALIZE_SYNC_SEQUENCE` is still being processed service.stop(); } }, SOME_EVENT: { actions: () => { throw new Error('This should not be called.'); } } } });
const service = interpret(machine).start();
service.send({ type: 'INITIALIZE_SYNC_SEQUENCE' }); });
it('should execute exit actions of the settled state of the last initiated microstep', () => { const exitActions: string[] = []; const machine = createMachine({ initial: 'foo', states: { foo: { exit: () => { exitActions.push('foo action'); }, on: { INITIALIZE_SYNC_SEQUENCE: { target: 'bar', actions: [ () => { // immediately stop *while* the `INITIALIZE_SYNC_SEQUENCE` is still being processed service.stop(); }, () => {} ] } } }, bar: { exit: () => { exitActions.push('bar action'); } } } });
const service = interpret(machine).start();
service.send({ type: 'INITIALIZE_SYNC_SEQUENCE' });
expect(exitActions).toEqual(['foo action', 'bar action']); });
it('should execute exit actions of the settled state of the last initiated microstep after executing all actions from that microstep', () => { const executedActions: string[] = []; const machine = createMachine({ initial: 'foo', states: { foo: { exit: () => { executedActions.push('foo exit action'); }, on: { INITIALIZE_SYNC_SEQUENCE: { target: 'bar', actions: [ () => { // immediately stop *while* the `INITIALIZE_SYNC_SEQUENCE` is still being processed service.stop(); }, () => { executedActions.push('foo transition action'); } ] } } }, bar: { exit: () => { executedActions.push('bar exit action'); } } } });
const service = interpret(machine).start();
service.send({ type: 'INITIALIZE_SYNC_SEQUENCE' });
expect(executedActions).toEqual([ 'foo exit action', 'foo transition action', 'bar exit action' ]); }); });});
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 }); });
it('should not cause an infinite loop when forwarding to undefined', () => { const machine = createMachine({ on: { '*': { cond: () => true, actions: forwardTo(undefined as any) } } });
const service = interpret(machine).start();
expect(() => service.send('TEST')).toThrowErrorMatchingInlineSnapshot( `"Attempted to forward event to undefined actor. This risks an infinite loop in the sender."` ); });});
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 be able to send an event to a named 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, 'child') }), // No type-safety for the event yet entry: sendTo('child', { type: 'EVENT' }) });
interpret(parentMachine).start(); });
it('should be able to send an event directly to an ActorRef', (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: pure< { child: ActorRefFrom<typeof childMachine>; }, any >((ctx) => { return [sendTo(ctx.child, { type: 'EVENT' })]; }) });
interpret(parentMachine).start(); });});
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]); } );});
Version Info