deno.land / x / xstate@xstate@4.33.6 / test / guards.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416import { Machine, interpret, createMachine, actions } from '../src';
describe('guard conditions', () => { type LightMachineCtx = { elapsed: number; }; type LightMachineEvents = | { type: 'TIMER'; elapsed: number } | { type: 'EMERGENCY'; isEmergency?: boolean; } | { type: 'TIMER_COND_OBJ' } | { type: 'BAD_COND' };
const lightMachine = Machine<LightMachineCtx, LightMachineEvents>( { key: 'light', initial: 'green', states: { green: { on: { TIMER: [ { target: 'green', cond: ({ elapsed }) => elapsed < 100 }, { target: 'yellow', cond: ({ elapsed }) => elapsed >= 100 && elapsed < 200 } ], EMERGENCY: { target: 'red', cond: (_, event) => !!event.isEmergency } } }, yellow: { on: { TIMER: { target: 'red', cond: 'minTimeElapsed' }, TIMER_COND_OBJ: { target: 'red', cond: { type: 'minTimeElapsed' } } } }, red: { on: { BAD_COND: { target: 'red', cond: 'doesNotExist' } } } } }, { guards: { minTimeElapsed: ({ elapsed }) => elapsed >= 100 && elapsed < 200 } } );
it('should transition only if condition is met', () => { expect( lightMachine.transition('green', 'TIMER', { elapsed: 50 }).value ).toEqual('green');
expect( lightMachine.transition('green', 'TIMER', { elapsed: 120 }).value ).toEqual('yellow'); });
it('should transition if condition based on event is met', () => { expect( lightMachine.transition('green', { type: 'EMERGENCY', isEmergency: true }).value ).toEqual('red'); });
it('should not transition if condition based on event is not met', () => { expect( lightMachine.transition('green', { type: 'EMERGENCY' }).value ).toEqual('green'); });
it('should not transition if no condition is met', () => { const nextState = lightMachine.transition('green', 'TIMER', { elapsed: 9000 }); expect(nextState.value).toEqual('green'); expect(nextState.actions).toEqual([]); });
it('should work with defined string transitions', () => { const nextState = lightMachine.transition('yellow', 'TIMER', { elapsed: 150 }); expect(nextState.value).toEqual('red'); });
it('should work with guard objects', () => { const nextState = lightMachine.transition('yellow', 'TIMER_COND_OBJ', { elapsed: 150 }); expect(nextState.value).toEqual('red'); });
it('should work with defined string transitions (condition not met)', () => { const nextState = lightMachine.transition('yellow', 'TIMER', { elapsed: 10 }); expect(nextState.value).toEqual('yellow'); });
it('should throw if string transition is not defined', () => { expect(() => lightMachine.transition('red', 'BAD_COND')).toThrow(); });});
describe('guard conditions', () => { const machine = Machine({ key: 'microsteps', type: 'parallel', states: { A: { initial: 'A0', states: { A0: { on: { A: 'A1' } }, A1: { on: { A: 'A2' } }, A2: { on: { A: 'A3' } }, A3: { always: 'A4' }, A4: { always: 'A5' }, A5: {} } }, B: { initial: 'B0', states: { B0: { always: [ { target: 'B4', cond: (_state, _event, { state: s }) => s.matches('A.A4') } ], on: { T1: [ { target: 'B1', cond: (_state: any, _event: any, { state: s }: any) => s.matches('A.A1') } ], T2: [ { target: 'B2', cond: (_state: any, _event: any, { state: s }: any) => s.matches('A.A2') } ], T3: [ { target: 'B3', cond: (_state: any, _event: any, { state: s }: any) => s.matches('A.A3') } ] } }, B1: {}, B2: {}, B3: {}, B4: {} } } } });
it('should guard against transition', () => { expect(machine.transition({ A: 'A2', B: 'B0' }, 'T1').value).toEqual({ A: 'A2', B: 'B0' }); });
it('should allow a matching transition', () => { expect(machine.transition({ A: 'A2', B: 'B0' }, 'T2').value).toEqual({ A: 'A2', B: 'B2' }); });
it('should check guards with interim states', () => { expect(machine.transition({ A: 'A2', B: 'B0' }, 'A').value).toEqual({ A: 'A5', B: 'B4' }); });
it('should be able to check source state tags when checking', () => { const machine = createMachine({ initial: 'a', states: { a: { on: { MACRO: 'b' } }, b: { entry: actions.raise('MICRO'), tags: 'theTag', on: { MICRO: { cond: (_ctx: any, _event: any, { state }: any) => state.hasTag('theTag'), target: 'c' } } }, c: {} } });
const service = interpret(machine).start(); service.send('MACRO');
expect(service.state.value).toBe('c'); });});
describe('custom guards', () => { type Ctx = { count: number }; type Events = { type: 'EVENT'; value: number }; const machine = Machine<Ctx, Events>( { id: 'custom', initial: 'inactive', context: { count: 0 }, states: { inactive: { on: { EVENT: { target: 'active', cond: { type: 'custom', prop: 'count', op: 'greaterThan', compare: 3 } } } }, active: {} } }, { guards: { custom: (ctx, e: Extract<Events, { type: 'EVENT' }>, meta) => { const { prop, compare, op } = meta.cond as any; // TODO: fix if (op === 'greaterThan') { return ctx[prop as keyof typeof ctx] + e.value > compare; }
return false; } } } );
it('should evaluate custom guards', () => { const passState = machine.transition(machine.initialState, { type: 'EVENT', value: 4 });
expect(passState.value).toEqual('active');
const failState = machine.transition(machine.initialState, { type: 'EVENT', value: 3 });
expect(failState.value).toEqual('inactive'); });});
describe('referencing guards', () => { const stringGuardFn = () => true; const guardsMachine = Machine( { id: 'guards', initial: 'active', states: { active: { on: { EVENT: [ { cond: 'string' }, { cond: function guardFn() { return true; } }, { cond: { type: 'object', foo: 'bar' } } ] } } } }, { guards: { string: stringGuardFn } } );
const def = guardsMachine.definition; const [stringGuard, functionGuard, objectGuard] = def.states.active.on.EVENT;
it('guard predicates should be able to be referenced from a string', () => { expect(stringGuard.cond!.predicate).toBeDefined(); expect(stringGuard.cond!.name).toEqual('string'); });
it('guard predicates should be able to be referenced from a function', () => { expect(functionGuard.cond!.predicate).toBeDefined(); expect(functionGuard.cond!.name).toEqual('guardFn'); });
it('guard predicates should be able to be referenced from an object', () => { expect(objectGuard.cond).toBeDefined(); expect(objectGuard.cond).toEqual({ type: 'object', foo: 'bar' }); });
it('should throw for guards with missing predicates', () => { const machine = Machine({ id: 'invalid-predicate', initial: 'active', states: { active: { on: { EVENT: { target: 'inactive', cond: 'missing-predicate' } } }, inactive: {} } });
expect(() => { machine.transition(machine.initialState, 'EVENT'); }).toThrow(); });});
describe('guards - other', () => { it('should allow for a fallback target to be a simple string', () => { const machine = Machine({ initial: 'a', states: { a: { on: { EVENT: [{ target: 'b', cond: () => false }, 'c'] } }, b: {}, c: {} } });
const service = interpret(machine).start(); service.send('EVENT');
expect(service.state.value).toBe('c'); });});
Version Info