import { machine, transition } from '~/lib/utils/finite_state_machine'; describe('Finite State Machine', () => { const STATE_IDLE = 'idle'; const STATE_LOADING = 'loading'; const STATE_ERRORED = 'errored'; const TRANSITION_START_LOAD = 'START_LOAD'; const TRANSITION_LOAD_ERROR = 'LOAD_ERROR'; const TRANSITION_LOAD_SUCCESS = 'LOAD_SUCCESS'; const TRANSITION_ACKNOWLEDGE_ERROR = 'ACKNOWLEDGE_ERROR'; const definition = { initial: STATE_IDLE, states: { [STATE_IDLE]: { on: { [TRANSITION_START_LOAD]: STATE_LOADING, }, }, [STATE_LOADING]: { on: { [TRANSITION_LOAD_ERROR]: STATE_ERRORED, [TRANSITION_LOAD_SUCCESS]: STATE_IDLE, }, }, [STATE_ERRORED]: { on: { [TRANSITION_ACKNOWLEDGE_ERROR]: STATE_IDLE, [TRANSITION_START_LOAD]: STATE_LOADING, }, }, }, }; describe('machine', () => { const STATE_IMPOSSIBLE = 'impossible'; const badDefinition = { init: definition.initial, badKeyShouldBeStates: definition.states, }; const unstartableDefinition = { initial: STATE_IMPOSSIBLE, states: definition.states, }; let liveMachine; beforeEach(() => { liveMachine = machine(definition); }); it('throws an error if the machine definition is invalid', () => { expect(() => machine(badDefinition)).toThrow( 'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)', ); }); it('throws an error if the initial state is invalid', () => { expect(() => machine(unstartableDefinition)).toThrow( `Cannot initialize the state machine to state '${STATE_IMPOSSIBLE}'. Is that one of the machine's defined states?`, ); }); it.each` partOfMachine | equals | description | eqDescription ${'keys'} | ${['is', 'send', 'value', 'states']} | ${'keys'} | ${'the correct array'} ${'is'} | ${expect.any(Function)} | ${'`is` property'} | ${'a function'} ${'send'} | ${expect.any(Function)} | ${'`send` property'} | ${'a function'} ${'value'} | ${definition.initial} | ${'`value` property'} | ${'the same as the `initial` value of the machine definition'} ${'states'} | ${definition.states} | ${'`states` property'} | ${'the same as the `states` value of the machine definition'} `("The machine's $description should be $eqDescription", ({ partOfMachine, equals }) => { const test = partOfMachine === 'keys' ? Object.keys(liveMachine) : liveMachine[partOfMachine]; expect(test).toEqual(equals); }); it.each` initialState | transitionEvent | expectedState ${definition.initial} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} `( 'properly steps from $initialState to $expectedState when the event "$transitionEvent" is sent', ({ initialState, transitionEvent, expectedState }) => { liveMachine.value = initialState; liveMachine.send(transitionEvent); expect(liveMachine.is(expectedState)).toBe(true); expect(liveMachine.value).toBe(expectedState); }, ); it.each` initialState | transitionEvent ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} ${STATE_IDLE} | ${'RANDOM_FOO'} ${STATE_LOADING} | ${TRANSITION_START_LOAD} ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_LOADING} | ${'RANDOM_FOO'} ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} ${STATE_ERRORED} | ${'RANDOM_FOO'} `( `does not perform any transition if the machine can't move from "$initialState" using the "$transitionEvent" event`, ({ initialState, transitionEvent }) => { liveMachine.value = initialState; liveMachine.send(transitionEvent); expect(liveMachine.is(initialState)).toBe(true); expect(liveMachine.value).toBe(initialState); }, ); describe('send', () => { it.each` startState | transitionEvent | result ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} `( 'successfully transitions to $result from $startState when the transition $transitionEvent is received', ({ startState, transitionEvent, result }) => { liveMachine.value = startState; expect(liveMachine.send(transitionEvent)).toEqual(result); }, ); it.each` startState | transitionEvent ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} ${STATE_IDLE} | ${'RANDOM_FOO'} ${STATE_LOADING} | ${TRANSITION_START_LOAD} ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_LOADING} | ${'RANDOM_FOO'} ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} ${STATE_ERRORED} | ${'RANDOM_FOO'} `( 'remains as $startState if an undefined transition ($transitionEvent) is received', ({ startState, transitionEvent }) => { liveMachine.value = startState; expect(liveMachine.send(transitionEvent)).toEqual(startState); }, ); describe('detached', () => { it.each` startState | transitionEvent | result ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} `( 'successfully transitions to $result from $startState when the transition $transitionEvent is received outside the context of the machine', ({ startState, transitionEvent, result }) => { const liveSend = machine({ ...definition, initial: startState, }).send; expect(liveSend(transitionEvent)).toEqual(result); }, ); it.each` startState | transitionEvent ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} ${STATE_IDLE} | ${'RANDOM_FOO'} ${STATE_LOADING} | ${TRANSITION_START_LOAD} ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_LOADING} | ${'RANDOM_FOO'} ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} ${STATE_ERRORED} | ${'RANDOM_FOO'} `( 'remains as $startState if an undefined transition ($transitionEvent) is received', ({ startState, transitionEvent }) => { const liveSend = machine({ ...definition, initial: startState, }).send; expect(liveSend(transitionEvent)).toEqual(startState); }, ); }); }); describe('is', () => { it.each` bool | test | actual ${true} | ${STATE_IDLE} | ${STATE_IDLE} ${false} | ${STATE_LOADING} | ${STATE_IDLE} ${false} | ${STATE_ERRORED} | ${STATE_IDLE} ${true} | ${STATE_LOADING} | ${STATE_LOADING} ${false} | ${STATE_IDLE} | ${STATE_LOADING} ${false} | ${STATE_ERRORED} | ${STATE_LOADING} ${true} | ${STATE_ERRORED} | ${STATE_ERRORED} ${false} | ${STATE_IDLE} | ${STATE_ERRORED} ${false} | ${STATE_LOADING} | ${STATE_ERRORED} `( 'returns "$bool" for "$test" when the current state is "$actual"', ({ bool, test, actual }) => { liveMachine = machine({ ...definition, initial: actual, }); expect(liveMachine.is(test)).toEqual(bool); }, ); describe('detached', () => { it.each` bool | test | actual ${true} | ${STATE_IDLE} | ${STATE_IDLE} ${false} | ${STATE_LOADING} | ${STATE_IDLE} ${false} | ${STATE_ERRORED} | ${STATE_IDLE} ${true} | ${STATE_LOADING} | ${STATE_LOADING} ${false} | ${STATE_IDLE} | ${STATE_LOADING} ${false} | ${STATE_ERRORED} | ${STATE_LOADING} ${true} | ${STATE_ERRORED} | ${STATE_ERRORED} ${false} | ${STATE_IDLE} | ${STATE_ERRORED} ${false} | ${STATE_LOADING} | ${STATE_ERRORED} `( 'returns "$bool" for "$test" when the current state is "$actual"', ({ bool, test, actual }) => { const liveIs = machine({ ...definition, initial: actual, }).is; expect(liveIs(test)).toEqual(bool); }, ); }); }); }); describe('transition', () => { it.each` startState | transitionEvent | result ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE} ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE} ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING} `( 'successfully transitions to $result from $startState when the transition $transitionEvent is received', ({ startState, transitionEvent, result }) => { expect(transition(definition, startState, transitionEvent)).toEqual(result); }, ); it.each` startState | transitionEvent ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS} ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR} ${STATE_IDLE} | ${'RANDOM_FOO'} ${STATE_LOADING} | ${TRANSITION_START_LOAD} ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR} ${STATE_LOADING} | ${'RANDOM_FOO'} ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR} ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS} ${STATE_ERRORED} | ${'RANDOM_FOO'} `( 'remains as $startState if an undefined transition ($transitionEvent) is received', ({ startState, transitionEvent }) => { expect(transition(definition, startState, transitionEvent)).toEqual(startState); }, ); it('remains as the provided starting state if it is an unrecognized state', () => { expect(transition(definition, 'RANDOM_FOO', TRANSITION_START_LOAD)).toEqual('RANDOM_FOO'); }); }); });