deno.land / x / xstate@xstate@4.33.6 / test / invoke.test.ts
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984import { Machine, interpret, assign, sendParent, send, EventObject, StateValue, createMachine, Behavior, ActorContext, SpecialTargets, AnyState} from '../src';import { fromReducer } from '../src/behaviors';import { actionTypes, done as _done, doneInvoke, escalate, forwardTo, raise} from '../src/actions';import { interval } from 'rxjs';import { map, take } from 'rxjs/operators';
const user = { name: 'David' };
const fetchMachine = Machine<{ userId: string | undefined }>({ id: 'fetch', context: { userId: undefined }, initial: 'pending', states: { pending: { entry: send({ type: 'RESOLVE', user }), on: { RESOLVE: { target: 'success', cond: (ctx) => ctx.userId !== undefined } } }, success: { type: 'final', data: { user: (_: any, e: any) => e.user } }, failure: { entry: sendParent('REJECT') } }});
const fetcherMachine = Machine({ id: 'fetcher', initial: 'idle', context: { selectedUserId: '42', user: undefined }, states: { idle: { on: { GO_TO_WAITING: 'waiting', GO_TO_WAITING_MACHINE: 'waitingInvokeMachine' } }, waiting: { invoke: { src: fetchMachine, data: { userId: (ctx: any) => ctx.selectedUserId }, onDone: { target: 'received', cond: (_, e) => { // Should receive { user: { name: 'David' } } as event data return e.data.user.name === 'David'; } } } }, waitingInvokeMachine: { invoke: { src: fetchMachine.withContext({ userId: '55' }), onDone: 'received' } }, received: { type: 'final' } }});
const intervalMachine = Machine<{ interval: number; count: number;}>({ id: 'interval', initial: 'counting', context: { interval: 10, count: 0 }, states: { counting: { invoke: { id: 'intervalService', src: (ctx) => (cb) => { const ivl = setInterval(() => { cb('INC'); }, ctx.interval);
return () => clearInterval(ivl); } }, always: { target: 'finished', cond: (ctx) => ctx.count === 3 }, on: { INC: { actions: assign({ count: (ctx) => ctx.count + 1 }) }, SKIP: 'wait' } }, wait: { on: { // this should never be called if interval service is properly disposed INC: { actions: assign({ count: (ctx) => ctx.count + 1 }) } }, after: { 50: 'finished' } }, finished: { type: 'final' } }});
describe('invoke', () => { it('should start services (external machines)', (done) => { const childMachine = Machine({ id: 'child', initial: 'init', states: { init: { entry: [sendParent('INC'), sendParent('INC')] } } });
const someParentMachine = Machine<{ count: number }>( { id: 'parent', context: { count: 0 }, initial: 'start', states: { start: { invoke: { src: 'child', id: 'someService', autoForward: true }, always: { target: 'stop', cond: (ctx) => ctx.count === 2 }, on: { INC: { actions: assign({ count: (ctx) => ctx.count + 1 }) } } }, stop: { type: 'final' } } }, { services: { child: childMachine } } );
let count: number;
interpret(someParentMachine) .onTransition((state) => { count = state.context.count; }) .onDone(() => { // 1. The 'parent' machine will enter 'start' state // 2. The 'child' service will be run with ID 'someService' // 3. The 'child' machine will enter 'init' state // 4. The 'entry' action will be executed, which sends 'INC' to 'parent' machine twice // 5. The context will be updated to increment count to 2
expect(count).toEqual(2); done(); }) .start(); });
it('should forward events to services if autoForward: true', () => { const childMachine = Machine({ id: 'child', initial: 'init', states: { init: { on: { FORWARD_DEC: { actions: [sendParent('DEC'), sendParent('DEC'), sendParent('DEC')] } } } } });
const someParentMachine = Machine<{ count: number }>( { id: 'parent', context: { count: 0 }, initial: 'start', states: { start: { invoke: { src: 'child', id: 'someService', autoForward: true }, always: { target: 'stop', cond: (ctx) => ctx.count === -3 }, on: { DEC: { actions: assign({ count: (ctx) => ctx.count - 1 }) }, FORWARD_DEC: undefined } }, stop: { type: 'final' } } }, { services: { child: childMachine } } );
let state: any; const service = interpret(someParentMachine) .onTransition((s) => { state = s; }) .onDone(() => { // 1. The 'parent' machine will not do anything (inert transition) // 2. The 'FORWARD_DEC' event will be forwarded to the 'child' machine (autoForward: true) // 3. On the 'child' machine, the 'FORWARD_DEC' event sends the 'DEC' action to the 'parent' thrice // 4. The context of the 'parent' machine will be updated from 2 to -1
expect(state.context).toEqual({ count: -3 }); }) .start();
service.send('FORWARD_DEC'); });
it('should forward events to services if autoForward: true before processing them', (done) => { const actual: string[] = [];
const childMachine = Machine<{ count: number }>({ id: 'child', context: { count: 0 }, initial: 'counting', states: { counting: { on: { INCREMENT: [ { target: 'done', cond: (ctx) => { actual.push('child got INCREMENT'); return ctx.count >= 2; }, actions: assign((ctx) => ({ count: ++ctx.count })) }, { target: undefined, actions: assign((ctx) => ({ count: ++ctx.count })) } ] } }, done: { type: 'final', data: (ctx) => ({ countedTo: ctx.count }) } }, on: { START: { actions: () => { throw new Error('Should not receive START action here.'); } } } });
const parentMachine = Machine<{ countedTo: number }>({ id: 'parent', context: { countedTo: 0 }, initial: 'idle', states: { idle: { on: { START: 'invokeChild' } }, invokeChild: { invoke: { src: childMachine, autoForward: true, onDone: { target: 'done', actions: assign((_ctx, event) => ({ countedTo: event.data.countedTo })) } }, on: { INCREMENT: { actions: () => { actual.push('parent got INCREMENT'); } } } }, done: { type: 'final' } } });
let state: any; const service = interpret(parentMachine) .onTransition((s) => { state = s; }) .onDone(() => { expect(state.context).toEqual({ countedTo: 3 }); expect(actual).toEqual([ 'child got INCREMENT', 'parent got INCREMENT', 'child got INCREMENT', 'parent got INCREMENT', 'child got INCREMENT', 'parent got INCREMENT' ]); done(); }) .start();
service.send('START'); service.send('INCREMENT'); service.send('INCREMENT'); service.send('INCREMENT'); });
it('should forward events to services if autoForward: true before processing them (when sending batches)', (done) => { const actual: string[] = [];
const childMachine = Machine<{ count: number }>({ id: 'child', context: { count: 0 }, initial: 'counting', states: { counting: { on: { INCREMENT: [ { target: 'done', cond: (ctx) => { actual.push('child got INCREMENT'); return ctx.count >= 2; }, actions: assign((ctx) => ({ count: ++ctx.count })) }, { target: undefined, actions: assign((ctx) => ({ count: ++ctx.count })) } ] } }, done: { type: 'final', data: (ctx) => ({ countedTo: ctx.count }) } }, on: { START: { actions: () => { throw new Error('Should not receive START action here.'); } } } });
const parentMachine = Machine<{ countedTo: number }>({ id: 'parent', context: { countedTo: 0 }, initial: 'idle', states: { idle: { on: { START: 'invokeChild' } }, invokeChild: { invoke: { src: childMachine, autoForward: true, onDone: { target: 'done', actions: assign((_ctx, event) => ({ countedTo: event.data.countedTo })) } }, on: { INCREMENT: { actions: () => { actual.push('parent got INCREMENT'); } } } }, done: { type: 'final' } } });
let state: any; const service = interpret(parentMachine) .onTransition((s) => { state = s; }) .onDone(() => { expect(state.context).toEqual({ countedTo: 3 }); expect(actual).toEqual([ 'child got INCREMENT', 'parent got INCREMENT', 'child got INCREMENT', 'child got INCREMENT', 'parent got INCREMENT', 'parent got INCREMENT' ]); done(); }) .start();
service.send(['START']); service.send(['INCREMENT']); service.send(['INCREMENT', 'INCREMENT']); });
it('should start services (explicit machine, invoke = config)', (done) => { interpret(fetcherMachine) .onDone(() => { done(); }) .start() .send('GO_TO_WAITING'); });
it('should start services (explicit machine, invoke = machine)', (done) => { interpret(fetcherMachine) .onDone((_) => { done(); }) .start() .send('GO_TO_WAITING_MACHINE'); });
it('should start services (machine as invoke config)', (done) => { const machineInvokeMachine = Machine< void, { type: 'SUCCESS'; data: number } >({ id: 'machine-invoke', initial: 'pending', states: { pending: { invoke: Machine({ id: 'child', initial: 'sending', states: { sending: { entry: sendParent({ type: 'SUCCESS', data: 42 }) } } }), on: { SUCCESS: { target: 'success', cond: (_, e) => { return e.data === 42; } } } }, success: { type: 'final' } } });
interpret(machineInvokeMachine) .onDone(() => done()) .start(); });
it('should start deeply nested service (machine as invoke config)', (done) => { const machineInvokeMachine = Machine< void, { type: 'SUCCESS'; data: number } >({ id: 'parent', initial: 'a', states: { a: { initial: 'b', states: { b: { invoke: Machine({ id: 'child', initial: 'sending', states: { sending: { entry: sendParent({ type: 'SUCCESS', data: 42 }) } } }) } } }, success: { id: 'success', type: 'final' } }, on: { SUCCESS: { target: 'success', cond: (_, e) => { return e.data === 42; } } } });
interpret(machineInvokeMachine) .onDone(() => done()) .start(); });
it('should use the service overwritten by withConfig', (done) => { const childMachine = Machine({ id: 'child', initial: 'init', states: { init: {} } });
const someParentMachine = Machine( { id: 'parent', context: { count: 0 }, initial: 'start', states: { start: { invoke: { src: 'child', id: 'someService', autoForward: true }, on: { STOP: 'stop' } }, stop: { type: 'final' } } }, { services: { child: childMachine } } );
interpret( someParentMachine.withConfig({ services: { child: Machine({ id: 'child', initial: 'init', states: { init: { entry: [sendParent('STOP')] } } }) } }) ) .onDone(() => { done(); }) .start(); });
it('should not start services only once when using withContext', () => { let startCount = 0;
const startMachine = Machine({ id: 'start', initial: 'active', context: { foo: true }, states: { active: { invoke: { src: () => () => { startCount++; } } } } });
const startService = interpret(startMachine.withContext({ foo: false }));
startService.start();
expect(startCount).toEqual(1); });
describe('parent to child', () => { const subMachine = Machine({ id: 'child', initial: 'one', states: { one: { on: { NEXT: 'two' } }, two: { entry: sendParent('NEXT') } } });
it('should communicate with the child machine (invoke on machine)', (done) => { const mainMachine = Machine({ id: 'parent', initial: 'one', invoke: { id: 'foo-child', src: subMachine }, states: { one: { entry: send('NEXT', { to: 'foo-child' }), on: { NEXT: 'two' } }, two: { type: 'final' } } });
interpret(mainMachine) .onDone(() => { done(); }) .start(); });
it('should communicate with the child machine (invoke on created machine)', (done) => { interface MainMachineCtx { machine: typeof subMachine; }
const mainMachine = Machine<MainMachineCtx>({ id: 'parent', initial: 'one', context: { machine: subMachine }, invoke: { id: 'foo-child', src: (ctx) => ctx.machine }, states: { one: { entry: send('NEXT', { to: 'foo-child' }), on: { NEXT: 'two' } }, two: { type: 'final' } } });
interpret(mainMachine) .onDone(() => { done(); }) .start(); });
it('should communicate with the child machine (invoke on state)', (done) => { const mainMachine = Machine({ id: 'parent', initial: 'one', states: { one: { invoke: { id: 'foo-child', src: subMachine }, entry: send('NEXT', { to: 'foo-child' }), on: { NEXT: 'two' } }, two: { type: 'final' } } });
interpret(mainMachine) .onDone(() => { done(); }) .start(); });
it('should transition correctly if child invocation causes it to directly go to final state', (done) => { const doneSubMachine = Machine({ id: 'child', initial: 'one', states: { one: { on: { NEXT: 'two' } }, two: { type: 'final' } } });
const mainMachine = Machine({ id: 'parent', initial: 'one', states: { one: { invoke: { id: 'foo-child', src: doneSubMachine, onDone: 'two' }, entry: send('NEXT', { to: 'foo-child' }) }, two: { on: { NEXT: 'three' } }, three: { type: 'final' } } });
const expectedStateValue = 'two'; let currentState: AnyState; interpret(mainMachine) .onTransition((current) => (currentState = current)) .start(); setTimeout(() => { expect(currentState.value).toEqual(expectedStateValue); done(); }, 30); });
it('should work with invocations defined in orthogonal state nodes', (done) => { const pongMachine = Machine({ id: 'pong', initial: 'active', states: { active: { type: 'final', data: { secret: 'pingpong' } } } });
const pingMachine = Machine({ id: 'ping', type: 'parallel', states: { one: { initial: 'active', states: { active: { invoke: { id: 'pong', src: pongMachine, onDone: { target: 'success', cond: (_, e) => e.data.secret === 'pingpong' } } }, success: { type: 'final' } } } } });
interpret(pingMachine) .onDone(() => { done(); }) .start(); });
it('should not reinvoke root-level invocations', (done) => { // https://github.com/statelyai/xstate/issues/2147
let invokeCount = 0; let invokeDisposeCount = 0; let actionsCount = 0; let entryActionsCount = 0;
const machine = createMachine({ invoke: { src: () => () => { invokeCount++;
return () => { invokeDisposeCount++; }; } }, entry: () => entryActionsCount++, on: { UPDATE: { internal: true, actions: () => { actionsCount++; } } } });
const service = interpret(machine).start(); expect(entryActionsCount).toEqual(1); expect(invokeCount).toEqual(1); expect(invokeDisposeCount).toEqual(0); expect(actionsCount).toEqual(0);
service.send('UPDATE'); expect(entryActionsCount).toEqual(1); expect(invokeCount).toEqual(1); expect(invokeDisposeCount).toEqual(0); expect(actionsCount).toEqual(1);
service.send('UPDATE'); expect(entryActionsCount).toEqual(1); expect(invokeCount).toEqual(1); expect(invokeDisposeCount).toEqual(0); expect(actionsCount).toEqual(2); done(); });
it('should stop a child actor when reaching a final state', () => { let actorStopped = false;
const machine = createMachine({ id: 'machine', invoke: { src: () => () => () => (actorStopped = true) }, initial: 'running', states: { running: { on: { finished: 'complete' } }, complete: { type: 'final' } } });
const service = interpret(machine).start();
service.send({ type: 'finished' });
expect(actorStopped).toBe(true); });
it('child should not invoke an actor when it transitions to an invoking state when it gets stopped by its parent', (done) => { let invokeCount = 0;
const child = createMachine({ id: 'child', initial: 'idle', states: { idle: { invoke: { src: () => { invokeCount++;
if (invokeCount > 1) { // prevent a potential infinite loop throw new Error('This should be impossible.'); }
return (sendBack) => { // it's important for this test to send the event back when the parent is *not* currently processing an event // this ensures that the parent can process the received event immediately and can stop the child immediately setTimeout(() => sendBack({ type: 'STARTED' })); }; } }, on: { STARTED: 'active' } }, active: { invoke: { src: () => { return (sendBack) => { sendBack({ type: 'STOPPED' }); }; } }, on: { STOPPED: { target: 'idle', actions: forwardTo(SpecialTargets.Parent) } } } } }); const parent = createMachine({ id: 'parent', initial: 'idle', states: { idle: { on: { START: 'active' } }, active: { invoke: { src: child }, on: { STOPPED: 'done' } }, done: { type: 'final' } } });
const service = interpret(parent) .onDone(() => { expect(invokeCount).toBe(1); done(); }) .start();
service.send('START'); }); });
type PromiseExecutor = ( resolve: (value?: any) => void, reject: (reason?: any) => void ) => void;
const promiseTypes = [ { type: 'Promise', createPromise(executor: PromiseExecutor): Promise<any> { return new Promise(executor); } }, { type: 'PromiseLike', createPromise(executor: PromiseExecutor): PromiseLike<any> { // Simulate a Promise/A+ thenable / polyfilled Promise. function createThenable(promise: Promise<any>): PromiseLike<any> { return { then(onfulfilled, onrejected) { return createThenable(promise.then(onfulfilled, onrejected)); } }; } return createThenable(new Promise(executor)); } } ];
promiseTypes.forEach(({ type, createPromise }) => { describe(`with promises (${type})`, () => { const invokePromiseMachine = Machine({ id: 'invokePromise', initial: 'pending', context: { id: 42, succeed: true }, states: { pending: { invoke: { src: (ctx) => createPromise((resolve) => { if (ctx.succeed) { resolve(ctx.id); } else { throw new Error(`failed on purpose for: ${ctx.id}`); } }), onDone: { target: 'success', cond: (ctx, e) => { return e.data === ctx.id; } }, onError: 'failure' } }, success: { type: 'final' }, failure: { type: 'final' } } });
it('should be invoked with a promise factory and resolve through onDone', (done) => { const service = interpret(invokePromiseMachine) .onDone(() => { expect(service.state._event.origin).toBeDefined(); done(); }) .start(); });
it('should be invoked with a promise factory and reject with ErrorExecution', (done) => { interpret(invokePromiseMachine.withContext({ id: 31, succeed: false })) .onDone(() => done()) .start(); });
it('should be invoked with a promise factory and ignore unhandled onError target', (done) => { const doneSpy = jest.fn(); const stopSpy = jest.fn();
const promiseMachine = Machine({ id: 'invokePromise', initial: 'pending', states: { pending: { invoke: { src: () => createPromise(() => { throw new Error('test'); }), onDone: 'success' } }, success: { type: 'final' } } });
interpret(promiseMachine).onDone(doneSpy).onStop(stopSpy).start();
// assumes that error was ignored before the timeout is processed setTimeout(() => { expect(doneSpy).not.toHaveBeenCalled(); expect(stopSpy).not.toHaveBeenCalled(); done(); }, 10); });
// tslint:disable-next-line:max-line-length it('should be invoked with a promise factory and stop on unhandled onError target when on strict mode', (done) => { const doneSpy = jest.fn();
const promiseMachine = Machine({ id: 'invokePromise', initial: 'pending', strict: true, states: { pending: { invoke: { src: () => createPromise(() => { throw new Error('test'); }), onDone: 'success' } }, success: { type: 'final' } } });
interpret(promiseMachine) .onDone(doneSpy) .onStop(() => { expect(doneSpy).not.toHaveBeenCalled(); done(); }) .start(); });
it('should be invoked with a promise factory and resolve through onDone for compound state nodes', (done) => { const promiseMachine = Machine({ id: 'promise', initial: 'parent', states: { parent: { initial: 'pending', states: { pending: { invoke: { src: () => createPromise((resolve) => resolve()), onDone: 'success' } }, success: { type: 'final' } }, onDone: 'success' }, success: { type: 'final' } } });
interpret(promiseMachine) .onDone(() => done()) .start(); });
it('should be invoked with a promise service and resolve through onDone for compound state nodes', (done) => { const promiseMachine = Machine( { id: 'promise', initial: 'parent', states: { parent: { initial: 'pending', states: { pending: { invoke: { src: 'somePromise', onDone: 'success' } }, success: { type: 'final' } }, onDone: 'success' }, success: { type: 'final' } } }, { services: { somePromise: () => createPromise((resolve) => resolve()) } } );
interpret(promiseMachine) .onDone(() => done()) .start(); });
it('should assign the resolved data when invoked with a promise factory', (done) => { const promiseMachine = Machine<{ count: number }>({ id: 'promise', context: { count: 0 }, initial: 'pending', states: { pending: { invoke: { src: () => createPromise((resolve) => resolve({ count: 1 })), onDone: { target: 'success', actions: assign({ count: (_, e) => e.data.count }) } } }, success: { type: 'final' } } });
let state: any; interpret(promiseMachine) .onTransition((s) => { state = s; }) .onDone(() => { expect(state.context.count).toEqual(1); done(); }) .start(); });
it('should assign the resolved data when invoked with a promise service', (done) => { const promiseMachine = Machine<{ count: number }>( { id: 'promise', context: { count: 0 }, initial: 'pending', states: { pending: { invoke: { src: 'somePromise', onDone: { target: 'success', actions: assign({ count: (_, e) => e.data.count }) } } }, success: { type: 'final' } } }, { services: { somePromise: () => createPromise((resolve) => resolve({ count: 1 })) } } );
let state: any; interpret(promiseMachine) .onTransition((s) => { state = s; }) .onDone(() => { expect(state.context.count).toEqual(1); done(); }) .start(); });
it('should provide the resolved data when invoked with a promise factory', (done) => { let count = 0;
const promiseMachine = Machine({ id: 'promise', context: { count: 0 }, initial: 'pending', states: { pending: { invoke: { src: () => createPromise((resolve) => resolve({ count: 1 })), onDone: { target: 'success', actions: (_, e) => { count = e.data.count; } } } }, success: { type: 'final' } } });
interpret(promiseMachine) .onDone(() => { expect(count).toEqual(1); done(); }) .start(); });
it('should provide the resolved data when invoked with a promise service', (done) => { let count = 0;
const promiseMachine = Machine( { id: 'promise', initial: 'pending', states: { pending: { invoke: { src: 'somePromise', onDone: { target: 'success', actions: (_, e) => { count = e.data.count; } } } }, success: { type: 'final' } } }, { services: { somePromise: () => createPromise((resolve) => resolve({ count: 1 })) } } );
interpret(promiseMachine) .onDone(() => { expect(count).toEqual(1); done(); }) .start(); });
it('should be able to specify a Promise as a service', (done) => { interface BeginEvent { type: 'BEGIN'; payload: boolean; } const promiseMachine = Machine<{ foo: boolean }, BeginEvent>( { id: 'promise', initial: 'pending', context: { foo: true }, states: { pending: { on: { BEGIN: 'first' } }, first: { invoke: { src: 'somePromise', onDone: 'last' } }, last: { type: 'final' } } }, { services: { somePromise: (ctx, e: BeginEvent) => { return createPromise((resolve, reject) => { ctx.foo && e.payload ? resolve() : reject(); }); } } } );
interpret(promiseMachine) .onDone(() => done()) .start() .send({ type: 'BEGIN', payload: true }); }); }); });
describe('with callbacks', () => { it('should be able to specify a callback as a service', (done) => { interface BeginEvent { type: 'BEGIN'; payload: boolean; } interface CallbackEvent { type: 'CALLBACK'; data: number; } const callbackMachine = Machine< { foo: boolean; }, BeginEvent | CallbackEvent >( { id: 'callback', initial: 'pending', context: { foo: true }, states: { pending: { on: { BEGIN: 'first' } }, first: { invoke: { src: 'someCallback' }, on: { CALLBACK: { target: 'last', cond: (_, e) => e.data === 42 } } }, last: { type: 'final' } } }, { services: { someCallback: (ctx, e) => (cb: (ev: CallbackEvent) => void) => { if (ctx.foo && 'payload' in e) { cb({ type: 'CALLBACK', data: 40 }); cb({ type: 'CALLBACK', data: 41 }); cb({ type: 'CALLBACK', data: 42 }); } } } } );
interpret(callbackMachine) .onDone(() => done()) .start() .send({ type: 'BEGIN', payload: true }); });
it('should transition correctly if callback function sends an event', () => { const callbackMachine = Machine( { id: 'callback', initial: 'pending', context: { foo: true }, states: { pending: { on: { BEGIN: 'first' } }, first: { invoke: { src: 'someCallback' }, on: { CALLBACK: 'intermediate' } }, intermediate: { on: { NEXT: 'last' } }, last: { type: 'final' } } }, { services: { someCallback: () => (cb) => { cb('CALLBACK'); } } } );
const expectedStateValues = ['pending', 'first', 'intermediate']; const stateValues: StateValue[] = []; interpret(callbackMachine) .onTransition((current) => stateValues.push(current.value)) .start() .send('BEGIN'); for (let i = 0; i < expectedStateValues.length; i++) { expect(stateValues[i]).toEqual(expectedStateValues[i]); } });
it('should transition correctly if callback function invoked from start and sends an event', () => { const callbackMachine = Machine( { id: 'callback', initial: 'idle', context: { foo: true }, states: { idle: { invoke: { src: 'someCallback' }, on: { CALLBACK: 'intermediate' } }, intermediate: { on: { NEXT: 'last' } }, last: { type: 'final' } } }, { services: { someCallback: () => (cb) => { cb('CALLBACK'); } } } );
const expectedStateValues = ['idle', 'intermediate']; const stateValues: StateValue[] = []; interpret(callbackMachine) .onTransition((current) => stateValues.push(current.value)) .start() .send('BEGIN'); for (let i = 0; i < expectedStateValues.length; i++) { expect(stateValues[i]).toEqual(expectedStateValues[i]); } });
// tslint:disable-next-line:max-line-length it('should transition correctly if transient transition happens before current state invokes callback function and sends an event', () => { const callbackMachine = Machine( { id: 'callback', initial: 'pending', context: { foo: true }, states: { pending: { on: { BEGIN: 'first' } }, first: { always: 'second' }, second: { invoke: { src: 'someCallback' }, on: { CALLBACK: 'third' } }, third: { on: { NEXT: 'last' } }, last: { type: 'final' } } }, { services: { someCallback: () => (cb) => { cb('CALLBACK'); } } } );
const expectedStateValues = ['pending', 'second', 'third']; const stateValues: StateValue[] = []; interpret(callbackMachine) .onTransition((current) => stateValues.push(current.value)) .start() .send('BEGIN'); for (let i = 0; i < expectedStateValues.length; i++) { expect(stateValues[i]).toEqual(expectedStateValues[i]); } });
it('should treat a callback source as an event stream', (done) => { interpret(intervalMachine) .onDone(() => done()) .start(); });
it('should dispose of the callback (if disposal function provided)', (done) => { let state: any; const service = interpret(intervalMachine) .onTransition((s) => { state = s; }) .onDone(() => { // if intervalService isn't disposed after skipping, 'INC' event will // keep being sent expect(state.context.count).toEqual(0); done(); }) .start();
// waits 50 milliseconds before going to final state. service.send('SKIP'); });
it('callback should be able to receive messages from parent', (done) => { const pingPongMachine = Machine({ id: 'ping-pong', initial: 'active', states: { active: { invoke: { id: 'child', src: () => (callback, onReceive) => { onReceive((e) => { if (e.type === 'PING') { callback('PONG'); } }); } }, entry: send('PING', { to: 'child' }), on: { PONG: 'done' } }, done: { type: 'final' } } });
interpret(pingPongMachine) .onDone(() => done()) .start(); });
it('should call onError upon error (sync)', (done) => { const errorMachine = Machine({ id: 'error', initial: 'safe', states: { safe: { invoke: { src: () => () => { throw new Error('test'); }, onError: { target: 'failed', cond: (_, e) => { return e.data instanceof Error && e.data.message === 'test'; } } } }, failed: { type: 'final' } } });
interpret(errorMachine) .onDone(() => done()) .start(); });
it('should transition correctly upon error (sync)', () => { const errorMachine = Machine({ id: 'error', initial: 'safe', states: { safe: { invoke: { src: () => () => { throw new Error('test'); }, onError: 'failed' } }, failed: { on: { RETRY: 'safe' } } } });
const expectedStateValue = 'failed'; const service = interpret(errorMachine).start(); expect(service.state.value).toEqual(expectedStateValue); });
it('should call onError upon error (async)', (done) => { const errorMachine = Machine({ id: 'asyncError', initial: 'safe', states: { safe: { invoke: { src: () => async () => { await true; throw new Error('test'); }, onError: { target: 'failed', cond: (_, e) => { return e.data instanceof Error && e.data.message === 'test'; } } } }, failed: { type: 'final' } } });
interpret(errorMachine) .onDone(() => done()) .start(); });
it('should call onDone when resolved (async)', (done) => { let state: any;
const asyncWithDoneMachine = Machine<{ result?: any }>({ id: 'async', initial: 'fetch', context: { result: undefined }, states: { fetch: { invoke: { src: () => async () => { await true; return 42; }, onDone: { target: 'success', actions: assign((_, { data: result }) => ({ result })) } } }, success: { type: 'final' } } });
interpret(asyncWithDoneMachine) .onTransition((s) => { state = s; }) .onDone(() => { expect(state.context.result).toEqual(42); done(); }) .start(); });
it('should call onError only on the state which has invoked failed service', () => { let errorHandlersCalled = 0;
const errorMachine = Machine({ initial: 'start', states: { start: { on: { FETCH: 'fetch' } }, fetch: { type: 'parallel', states: { first: { invoke: { src: () => () => { throw new Error('test'); }, onError: { target: 'failed', cond: () => { errorHandlersCalled++; return false; } } } }, second: { invoke: { src: () => () => { // empty }, onError: { target: 'failed', cond: () => { errorHandlersCalled++; return false; } } } }, failed: { type: 'final' } } } } });
interpret(errorMachine).start().send('FETCH');
expect(errorHandlersCalled).toEqual(1); });
it('should be able to be stringified', () => { const waitingState = fetcherMachine.transition( fetcherMachine.initialState, 'GO_TO_WAITING' );
expect(() => { JSON.stringify(waitingState); }).not.toThrow();
expect(typeof waitingState.actions[0].activity!.src).toBe('string'); });
it('should throw error if unhandled (sync)', () => { const errorMachine = Machine({ id: 'asyncError', initial: 'safe', states: { safe: { invoke: { src: () => () => { throw new Error('test'); } } }, failed: { type: 'final' } } });
const service = interpret(errorMachine); expect(() => service.start()).toThrow(); });
it('should stop machine if unhandled error and on strict mode (async)', (done) => { const errorMachine = Machine({ id: 'asyncError', initial: 'safe', // if not in strict mode we have no way to know if there // was an error with processing rejected promise strict: true, states: { safe: { invoke: { src: () => async () => { await true; throw new Error('test'); } } }, failed: { type: 'final' } } });
interpret(errorMachine) .onStop(() => done()) .start(); });
it('should ignore error if unhandled error and not on strict mode (async)', (done) => { const doneSpy = jest.fn(); const stopSpy = jest.fn();
const errorMachine = Machine({ id: 'asyncError', initial: 'safe', // if not in strict mode we have no way to know if there // was an error with processing rejected promise strict: false, states: { safe: { invoke: { src: () => async () => { await true; throw new Error('test'); } } }, failed: { type: 'final' } } });
interpret(errorMachine).onDone(doneSpy).onStop(stopSpy).start();
// assumes that error was ignored before the timeout is processed setTimeout(() => { expect(doneSpy).not.toHaveBeenCalled(); expect(stopSpy).not.toHaveBeenCalled(); done(); }, 20); });
describe('sub invoke race condition', () => { const anotherChildMachine = Machine({ id: 'child', initial: 'start', states: { start: { on: { STOP: 'end' } }, end: { type: 'final' } } });
const anotherParentMachine = Machine({ id: 'parent', initial: 'begin', states: { begin: { invoke: { src: anotherChildMachine, id: 'invoked.child', onDone: 'completed' }, on: { STOPCHILD: { actions: send('STOP', { to: 'invoked.child' }) } } }, completed: { type: 'final' } } });
it('ends on the completed state', (done) => { const events: EventObject[] = []; let state: any; const service = interpret(anotherParentMachine) .onTransition((s) => { state = s; }) .onEvent((e) => { events.push(e); }) .onDone(() => { expect(events.map((e) => e.type)).toEqual([ actionTypes.init, 'STOPCHILD', doneInvoke('invoked.child').type ]); expect(state.value).toEqual('completed'); done(); }) .start();
service.send('STOPCHILD'); }); }); });
describe('with observables', () => { const infinite$ = interval(10);
it('should work with an infinite observable', (done) => { interface Events { type: 'COUNT'; value: number; } const obsMachine = Machine<{ count: number | undefined }, Events>({ id: 'obs', initial: 'counting', context: { count: undefined }, states: { counting: { invoke: { src: () => infinite$.pipe( map((value) => { return { type: 'COUNT', value }; }) ) }, always: { target: 'counted', cond: (ctx) => ctx.count === 5 }, on: { COUNT: { actions: assign({ count: (_, e) => e.value }) } } }, counted: { type: 'final' } } });
const service = interpret(obsMachine) .onDone(() => { expect(service.state._event.origin).toBeDefined(); done(); }) .start(); });
it('should work with a finite observable', (done) => { interface Ctx { count: number | undefined; } interface Events { type: 'COUNT'; value: number; } const obsMachine = Machine<Ctx, Events>({ id: 'obs', initial: 'counting', context: { count: undefined }, states: { counting: { invoke: { src: () => infinite$.pipe( take(5), map((value) => { return { type: 'COUNT', value }; }) ), onDone: { target: 'counted', cond: (ctx) => ctx.count === 4 } }, on: { COUNT: { actions: assign({ count: (_, e) => e.value }) } } }, counted: { type: 'final' } } });
interpret(obsMachine) .onDone(() => { done(); }) .start(); });
it('should receive an emitted error', (done) => { interface Ctx { count: number | undefined; } interface Events { type: 'COUNT'; value: number; } const obsMachine = Machine<Ctx, Events>({ id: 'obs', initial: 'counting', context: { count: undefined }, states: { counting: { invoke: { src: () => infinite$.pipe( map((value) => { if (value === 5) { throw new Error('some error'); }
return { type: 'COUNT', value }; }) ), onError: { target: 'success', cond: (ctx, e) => { expect(e.data.message).toEqual('some error'); return ctx.count === 4 && e.data.message === 'some error'; } } }, on: { COUNT: { actions: assign({ count: (_, e) => e.value }) } } }, success: { type: 'final' } } });
interpret(obsMachine) .onDone(() => { done(); }) .start(); }); });
describe('with behaviors', () => { it('should work with a behavior', (done) => { const countBehavior: Behavior<EventObject, number> = { transition: (count, event) => { if (event.type === 'INC') { return count + 1; } else { return count - 1; } }, initialState: 0 };
const countMachine = createMachine({ invoke: { id: 'count', src: () => countBehavior }, on: { INC: { actions: forwardTo('count') } } });
const countService = interpret(countMachine) .onTransition((state) => { if (state.children['count']?.getSnapshot() === 2) { done(); } }) .start();
countService.send('INC'); countService.send('INC'); });
it('behaviors should have reference to the parent', (done) => { const pongBehavior: Behavior<EventObject, undefined> = { transition: (_, event, { parent }) => { if (event.type === 'PING') { parent?.send({ type: 'PONG' }); }
return undefined; }, initialState: undefined };
const pingMachine = createMachine({ initial: 'waiting', states: { waiting: { entry: send('PING', { to: 'ponger' }), invoke: { id: 'ponger', src: () => pongBehavior }, on: { PONG: 'success' } }, success: { type: 'final' } } });
const pingService = interpret(pingMachine).onDone(() => { done(); }); pingService.start(); }); });
describe('with reducers', () => { it('should work with a reducer', (done) => { const countReducer = (count: number, event: { type: 'INC' }): number => { if (event.type === 'INC') { return count + 1; } else { return count - 1; } };
const countMachine = createMachine({ invoke: { id: 'count', src: () => fromReducer(countReducer, 0) }, on: { INC: { actions: forwardTo('count') } } });
const countService = interpret(countMachine) .onTransition((state) => { if (state.children['count']?.getSnapshot() === 2) { done(); } }) .start();
countService.send('INC'); countService.send('INC'); });
it('should schedule events in a FIFO queue', (done) => { type CountEvents = { type: 'INC' } | { type: 'DOUBLE' };
const countReducer = ( count: number, event: { type: 'INC' } | { type: 'DOUBLE' }, { self }: ActorContext<CountEvents, any> ): number => { if (event.type === 'INC') { self.send({ type: 'DOUBLE' }); return count + 1; } if (event.type === 'DOUBLE') { return count * 2; }
return count; };
const countMachine = createMachine({ invoke: { id: 'count', src: () => fromReducer(countReducer, 0) }, on: { INC: { actions: forwardTo('count') } } });
const countService = interpret(countMachine) .onTransition((state) => { if (state.children['count']?.getSnapshot() === 2) { done(); } }) .start();
countService.send('INC'); }); });
describe('nested invoked machine', () => { const pongMachine = Machine({ id: 'pong', initial: 'active', states: { active: { on: { PING: { // Sends 'PONG' event to parent machine actions: sendParent('PONG') } } } } });
// Parent machine const pingMachine = Machine({ id: 'ping', initial: 'innerMachine', states: { innerMachine: { initial: 'active', states: { active: { invoke: { id: 'pong', src: pongMachine }, // Sends 'PING' event to child machine with ID 'pong' entry: send('PING', { to: 'pong' }), on: { PONG: 'innerSuccess' } }, innerSuccess: { type: 'final' } }, onDone: 'success' }, success: { type: 'final' } } });
it('should create invocations from machines in nested states', (done) => { interpret(pingMachine) .onDone(() => done()) .start(); }); });
describe('multiple simultaneous services', () => { const multiple = Machine<any>({ id: 'machine', initial: 'one',
context: {},
on: { ONE: { actions: assign({ one: 'one' }) },
TWO: { actions: assign({ two: 'two' }), target: '.three' } },
states: { one: { initial: 'two', states: { two: { invoke: [ { id: 'child', src: () => (cb) => cb('ONE') }, { id: 'child2', src: () => (cb) => cb('TWO') } ] } } }, three: { type: 'final' } } });
it('should start all services at once', (done) => { let state: any; const service = interpret(multiple) .onTransition((s) => { state = s; }) .onDone(() => { expect(state.context).toEqual({ one: 'one', two: 'two' }); done(); });
service.start(); });
const parallel = Machine<any>({ id: 'machine', initial: 'one',
context: {},
on: { ONE: { actions: assign({ one: 'one' }) },
TWO: { actions: assign({ two: 'two' }), target: '.three' } },
states: { one: { initial: 'two', states: { two: { type: 'parallel', states: { a: { invoke: { id: 'child', src: () => (cb) => cb('ONE') } }, b: { invoke: { id: 'child2', src: () => (cb) => cb('TWO') } } } } } }, three: { type: 'final' } } });
it('should run services in parallel', (done) => { let state: any; const service = interpret(parallel) .onTransition((s) => { state = s; }) .onDone(() => { expect(state.context).toEqual({ one: 'one', two: 'two' }); done(); });
service.start(); });
it('should not invoke a service if it gets stopped immediately by transitioning away in microstep', (done) => { // Since an invocation will be canceled when the state machine leaves the // invoking state, it does not make sense to start an invocation in a state // that will be exited immediately let serviceCalled = false; const transientMachine = Machine({ id: 'transient', initial: 'active', states: { active: { invoke: { id: 'doNotInvoke', src: () => async () => { serviceCalled = true; } }, always: 'inactive' }, inactive: { after: { 10: 'complete' } }, complete: { type: 'final' } } });
const service = interpret(transientMachine);
service .onDone(() => { expect(serviceCalled).toBe(false); done(); }) .start(); });
it('should invoke a service if other service gets stopped in subsequent microstep (#1180)', (done) => { const machine = createMachine({ initial: 'running', states: { running: { type: 'parallel', states: { one: { initial: 'active', on: { STOP_ONE: '.idle' }, states: { idle: {}, active: { invoke: { id: 'active', src: () => () => { /* ... */ } }, on: { NEXT: { actions: raise('STOP_ONE') } } } } }, two: { initial: 'idle', on: { NEXT: '.active' }, states: { idle: {}, active: { invoke: { id: 'post', src: () => Promise.resolve(42), onDone: '#done' } } } } } }, done: { id: 'done', type: 'final' } } });
const service = interpret(machine) .onDone(() => done()) .start();
service.send('NEXT'); }); });
describe('error handling', () => { it('handles escalated errors', (done) => { const child = Machine({ initial: 'die',
states: { die: { entry: escalate('oops') } } });
const parent = Machine({ initial: 'one',
states: { one: { invoke: { id: 'child', src: child, onError: { target: 'two', cond: (_, event) => event.data === 'oops' } } }, two: { type: 'final' } } });
interpret(parent) .onDone(() => { done(); }) .start(); });
it('handles escalated errors as an expression', (done) => { interface ChildContext { id: number; }
const child = Machine<ChildContext>({ initial: 'die', context: { id: 42 }, states: { die: { entry: escalate((ctx) => ctx.id) } } });
const parent = Machine({ initial: 'one',
states: { one: { invoke: { id: 'child', src: child, onError: { target: 'two', cond: (_, event) => { expect(event.data).toEqual(42); return true; } } } }, two: { type: 'final' } } });
interpret(parent) .onDone(() => { done(); }) .start(); }); });
it('invoke `src` should accept invoke source definition', (done) => { const machine = createMachine( { initial: 'searching', states: { searching: { invoke: { src: { type: 'search', endpoint: 'example.com' }, onDone: 'success' } }, success: { type: 'final' } } }, { services: { search: async (_, __, meta) => { expect(meta.src.endpoint).toEqual('example.com');
return await 42; } } } );
interpret(machine) .onDone(() => done()) .start(); });
describe('meta data', () => { it('should show meta data', () => { const machine = createMachine({ invoke: { src: 'someSource', meta: { url: 'stately.ai' } } });
expect(machine.invoke[0].meta).toEqual({ url: 'stately.ai' }); });
it('meta data should be available in the invoke source function', () => { expect.assertions(1); const machine = createMachine({ invoke: { src: (_ctx, _e, { meta }) => { expect(meta).toEqual({ url: 'stately.ai' }); return Promise.resolve(); }, meta: { url: 'stately.ai' } } });
interpret(machine).start(); }); });
it('invoke generated ID should be predictable based on the state node where it is defined', (done) => { const machine = createMachine( { initial: 'a', states: { a: { invoke: { src: 'someSrc', onDone: { cond: (_, e) => { // invoke ID should not be 'someSrc' const expectedType = 'done.invoke.(machine).a:invocation[0]'; expect(e.type).toEqual(expectedType); return e.type === expectedType; }, target: 'b' } } }, b: { type: 'final' } } }, { services: { someSrc: () => Promise.resolve() } } );
interpret(machine) .onDone(() => { done(); }) .start(); });
it.each([ ['src with string reference', { src: 'someSrc' }], ['machine', createMachine({ id: 'someId' })], [ 'src containing a machine directly', { src: createMachine({ id: 'someId' }) } ], [ 'src containing a callback actor directly', { src: () => () => { /* ... */ } } ], [ 'src containing a parametrized invokee with id parameter', { src: { type: 'someSrc', id: 'h4sh' } } ] ])( 'invoke config defined as %s should register unique and predictable child in state', (_type, invokeConfig) => { const machine = createMachine( { id: 'machine', initial: 'a', states: { a: { invoke: invokeConfig } } }, { services: { someSrc: () => () => { /* ... */ } } } );
expect( machine.initialState.children['machine.a:invocation[0]'] ).toBeDefined(); } );
// https://github.com/statelyai/xstate/issues/464 it('done.invoke events should only select onDone transition on the invoking state when invokee is referenced using a string', (done) => { let counter = 0; let invoked = false;
const createSingleState = (): any => ({ initial: 'fetch', states: { fetch: { invoke: { src: 'fetchSmth', onDone: { actions: 'handleSuccess' } } } } });
const testMachine = createMachine( { type: 'parallel', states: { first: createSingleState(), second: createSingleState() } }, { actions: { handleSuccess: () => { ++counter; } }, services: { fetchSmth: () => { if (invoked) { // create a promise that won't ever resolve for the second invoking state return new Promise(() => {}); } invoked = true; return Promise.resolve(42); } } } );
interpret(testMachine).start();
// check within a macrotask so all promise-induced microtasks have a chance to resolve first setTimeout(() => { expect(counter).toEqual(1); done(); }, 0); });
it('done.invoke events should have unique names when invokee is a machine with an id property', (done) => { const actual: string[] = [];
const childMachine = createMachine({ id: 'child', initial: 'a', states: { a: { invoke: { src: () => Promise.resolve(42), onDone: 'b' } }, b: { type: 'final' } } });
const createSingleState = (): any => ({ initial: 'fetch', states: { fetch: { invoke: childMachine } } });
const testMachine = createMachine({ type: 'parallel', states: { first: createSingleState(), second: createSingleState() }, on: { '*': { actions: (_ctx, ev) => { actual.push(ev.type); } } } });
interpret(testMachine).start();
// check within a macrotask so all promise-induced microtasks have a chance to resolve first setTimeout(() => { expect(actual).toEqual([ 'done.invoke.(machine).first.fetch:invocation[0]', 'done.invoke.(machine).second.fetch:invocation[0]' ]); done(); }, 0); });});
describe('services option', () => { it('should provide data params to a service creator', (done) => { const machine = createMachine( { initial: 'pending', context: { count: 42 }, states: { pending: { invoke: { src: 'stringService', data: { staticVal: 'hello', newCount: (ctx: any) => ctx.count * 2 }, onDone: 'success' } }, success: { type: 'final' } } }, { services: { stringService: (ctx, _, { data }) => { expect(ctx).toEqual({ count: 42 });
expect(data).toEqual({ newCount: 84, staticVal: 'hello' });
return new Promise<void>((res) => { res(); }); } } } );
const service = interpret(machine).onDone(() => { done(); });
service.start(); });});
Version Info