Skip to main content
Module

x/xstate/test/invoke.test.ts

State machines and statecharts for the modern web.
Go to Latest
File
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956
import { 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('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(); });});