UNPKG

@fabiospampinato/fsm

Version:

Finite State Machine implementation, with support for guards and enter/exit events.

410 lines (216 loc) 8.74 kB
/* IMPORT */ import * as _ from 'lodash'; import {describe} from 'ava-spec'; import callSpy from 'call-spy' import FIFO from '@fabiospampinato/fifo'; import FSM from '../dist'; import {Model, states, initialState} from './mocks'; /* FSM */ describe ( 'FSM', it => { it.beforeEach ( t => { t.context.M = new Model (); t.context.F = new FSM ( t.context.M, _.cloneDeep ( states ), initialState ); }); describe ( 'constructor', it => { it ( 'Creates a queue', t => { t.true ( t.context.F.queue instanceof FIFO ); }); it ( 'Sets queue processing to false', t => { t.false ( t.context.F.processing.isLocked () ); }); it ( 'Sets the initial state', t => { t.is ( t.context.F.get (), initialState ); }); }); describe ( '_isValidState', it => { it ( 'Checks if a state is a known state', t => { const keys = _.keys ( states ); for ( let state in keys ) { if ( !states.hasOwnProperty ( state ) ) continue; t.true ( t.context.F._isValidState ( state ) ); } t.false ( t.context.F._isValidState ( '__test__' ) ); }); }); describe ( '_isValidTransition', it => { it ( 'Checks if a transition is possible from the current state', t => { const keys = _.keys ( states ); for ( let state in keys ) { if ( !states.hasOwnProperty ( state ) ) continue; const transitions = _.keys ( states[state] ); for ( let transition of transitions ) { t.true ( t.context.F._isValidTransition ( state, transition ) ); } t.false ( t.context.F._isValidTransition ( state, '__test__' ) ); } }); }); describe ( '_isValidTransitionGuard', it => { it.todo ( 'Checks that a guard from a state with a transition avaluates to true' ); }); describe ( '_getTransitionState', it => { it ( 'Gets the next state given the starting state and a transition', t => { t.is ( t.context.F._getTransitionState ( 'a', 'toStar' ), '*' ); t.is ( t.context.F._getTransitionState ( 'a', 'toInvalid' ), 'a' ); t.is ( t.context.F._getTransitionState ( 'b', 'toB2' ), 'b2' ); }); it ( 'Returns undefined if none is found', t => { t.is ( t.context.F._getTransitionState ( 'a', '__test__' ), undefined ); }); }); describe ( '_getTransitionGuard', it => { it ( 'Gets the guard to check given the starting state and a transition', t => { t.is ( t.context.F._getTransitionGuard ( 'a', 'toInvalid' ), states.a.transitions.toInvalid.guard ); t.is ( t.context.F._getTransitionGuard ( 'a', 'toC' ), states.a.transitions.toC.guard ); }); it ( 'Returns undefined if none is found', t => { t.is ( t.context.F._getTransitionGuard ( 'a', 'toA' ), undefined ); }); }); describe ( '_getExistsEnters', it => { it ( 'Returns an array of states to exit and one of states to enter', t => { t.deepEqual ( t.context.F._getExistsEnters ( 'a', 'a' ), [[], []] ); t.deepEqual ( t.context.F._getExistsEnters ( 'a', 'c' ), [['a'], ['c']] ); }); }); describe ( '_callModel', it => { it ( 'Calls the model by path, applies the args, and returns its return value', t => { const res1 = {}; t.context.M.true = callSpy ( t.context.M.true, res1 ); t.context.F._callModel ( 'true', [1, 2, 3] ); t.true ( res1.called ); t.deepEqual ( res1.arguments, [1, 2, 3] ); t.deepEqual ( res1.return, t.context.M.true () ); const res2 = {}; t.context.M.sub.true = callSpy ( t.context.M.sub.true, res2 ); t.context.F._callModel ( 'sub.true', [1, 2, 3] ); t.true ( res2.called ); t.deepEqual ( res2.arguments, [1, 2, 3] ); t.deepEqual ( res2.return, t.context.M.sub.true () ); }); }); describe ( 'get', it => { it ( 'Gets the current state', t => { t.is ( t.context.F.get (), initialState ); t.context.F.transition ( 'toB' ); t.is ( t.context.F.get (), 'b' ); }); }); describe ( 'set', it => { it ( 'Sets a state as the current one', t => { _.keys ( states ).forEach ( state => { t.context.F.set ( state ); t.is ( t.context.F.get (), state ); }); }); it ( 'Throws an error for invalid states', t => { t.throws ( () => t.context.F.set ( 'z' ), /Invalid state/ ); }); }); describe ( 'reset', it => { it ( 'Sets the state to the initial one', t => { t.context.F.set ( 'c' ); t.context.F.reset (); t.is ( t.context.F.get (), initialState ); }); }); describe ( 'is', it => { it ( 'Checks if a state is the current one', t => { t.is ( t.context.F.get (), 'a' ); t.context.F.transition ( 'toB' ); t.is ( t.context.F.get (), 'b' ); }); }); describe ( 'isDoable', it => { it ( 'Checks if a transition is possible', t => { t.true ( t.context.F.isDoable ( 'toA' ) ); t.true ( t.context.F.isDoable ( 'toB' ) ); t.true ( t.context.F.isDoable ( 'toC' ) ); t.false ( t.context.F.isDoable ( 'toC2' ) ); }); }); describe ( 'do', it => { it ( 'Is an alias for transition', t => { const res = {}; t.context.F.transition = callSpy ( t.context.F.transition, res ); t.context.F.do ( 'toB', 1, 2, 3 ); t.true ( res.called ); t.deepEqual ( res.arguments, ['toB', 1, 2, 3] ); }); }); describe ( 'transition', it => { it ( 'Changes state following a transition', t => { t.context.F.transition ( 'toB' ); t.is ( t.context.F.get (), 'b' ); t.context.F.transition ( 'toB2' ); t.is ( t.context.F.get (), 'b2' ); t.context.F.transition ( 'toB' ); t.is ( t.context.F.get (), 'b' ); t.context.F.transition ( 'toC' ); t.is ( t.context.F.get (), 'c' ); }); it ( 'Throws an error for invalid transitions', t => { t.throws ( () => t.context.F.transition ( 'toZ' ), /Invalid transition/ ); }); it ( 'Passes all the provided arguments to the transition function', t => { t.context.F.transition ( 'toB' ); t.context.F.transition ( 'toB2', 0, 1, 2, 3 ); t.deepEqual ( _.range ( 4 ), t.context.F.model.getTemp2 () ); }); it ( 'Calls prevStateExit (if defined), transition, nextStateEnter (if defined)', t => { t.context.F.transition ( 'toB' ); t.deepEqual ( t.context.F.model.getTemp (), [0, 1, 2] ); t.context.F.transition ( 'toC' ); t.deepEqual ( t.context.F.model.getTemp (), [0, 1, 2, 3] ); }); it ( 'Doesn\'t call enter/exit callbacks if we transitioning to the same state', t => { t.context.F.transition ( 'toA' ); t.deepEqual ( t.context.F.model.getTemp (), [0] ); }); it ( 'Queues transitions -- it\'s possible to trigger a transition from another transition function', t => { t.context.F.model.constructor.prototype.toBC = function () { this.toB (); t.context.F.transition ( 'toC' ); }; t.context.F.transition ( 'toBC' ); t.deepEqual ( t.context.F.model.getTemp (), [0, 1, 2, 3] ); t.is ( t.context.F.get (), 'c' ); }); it ( 'Checks guard conditions before evaluating a transition', t => { t.context.F.transition ( 'toC' ); t.is ( t.context.F.get (), 'c' ); t.context.F.set ( 'a' ); t.throws ( () => t.context.F.transition ( 'toC2' ), /Invalid transition/ ); }); it ( 'Can check guards on sub objects', t => { t.context.F.transition ( 'toSubTrue' ); t.is ( t.context.F.get (), initialState ); t.throws ( () => t.context.F.transition ( 'toSubFalse' ), /Invalid transition/ ); }); it ( 'Throws an error for invalid guards', t => { t.throws ( () => t.context.F.transition ( 'toInvalid' ), /Invalid guard/ ) }); it ( 'Support `*` transitions that always point to the current state', t => { t.context.F.transition ( 'toStar' ); t.is ( t.context.F.get (), initialState ); }); }); describe ( '_exit', it => { it ( 'Calls the exit state handler of the state', t => { const res = {}; t.context.M.aExit = callSpy ( t.context.M.aExit, res ); t.context.F._exit ( 'a' ); t.true ( res.called ); }); }); describe ( '_enter', it => { it ( 'Calls the enter state handler of the state, sets the state', t => { const res = {}; t.context.M.bEnter = callSpy ( t.context.M.bEnter, res ); t.not ( 'b', t.context.F.get () ); t.context.F._enter ( 'b' ); t.is ( t.context.F.get (), 'b' ); t.true ( res.called ); }); }); });