Skip to content

Commit

Permalink
add: SimpleStateMachine transition returns the new state
Browse files Browse the repository at this point in the history
  • Loading branch information
ealush committed Jul 11, 2023
1 parent ed49c8c commit bca9421
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 8 deletions.
26 changes: 18 additions & 8 deletions packages/vest-utils/src/SimpleStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,33 @@ export type TStateMachine<S extends string, A extends string> = {
}>;
};

export type TStateMachineApi<S extends string, A extends string> = {
getState: CB<S>;
transition: (action: A, payload?: any) => void;
transitionFrom: (from: S, action: A, payload?: any) => S;
};

export function StateMachine<S extends string, A extends string>(
machine: TStateMachine<S, A>
): { getState: CB<S>; transition: (action: A, payload?: any) => void } {
): TStateMachineApi<S, A> {
let state = machine.initial;

const api = { getState, transition };
const api = { getState, transition, transitionFrom };

return api;

function getState(): S {
return state;
}

function transition(action: A, payload?: any): S {
return (state = transitionFrom(state, action, payload));
}

// eslint-disable-next-line complexity
function transition(action: A, payload?: any): void {
function transitionFrom(from: S, action: A, payload?: any): S {
const transitionTo =
machine.states[state]?.[action] ??
machine.states[from]?.[action] ??
// @ts-expect-error - This is a valid state
machine.states[STATE_WILD_CARD]?.[action];

Expand All @@ -37,16 +47,16 @@ export function StateMachine<S extends string, A extends string>(
if (Array.isArray(target)) {
const [, conditional] = target;
if (!conditional(payload)) {
return;
return from;
}

target = target[0];
}

if (!target || target === state) {
return;
if (!target || target === from) {
return from;
}

state = target as S;
return target as S;
}
}
201 changes: 201 additions & 0 deletions packages/vest-utils/src/__tests__/SimpleStateMachine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,205 @@ describe('SimpleStateMachine', () => {
expect(machine.getState()).toBe('x_x');
});
});

describe('transition output value', () => {
describe('when transition is valid', () => {
it('Should return the new state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: 'loading',
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');

expect(machine.transition('click')).toBe('loading');
});
});

describe('When transitioning to the same state', () => {
it('Should return the same state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: 'loading',
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');

expect(machine.transition('click')).toBe('loading');
expect(machine.transition('click')).toBe('loading');
});
});

describe('When target state does not exist', () => {
it('Should return the previous state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: 'loading',
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');

expect(machine.transition('click')).toBe('loading');
expect(machine.transition('finish')).toBe('loading');
});
});

describe('When transition is invalid', () => {
it('Should return the previous state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: 'loading',
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');

expect(machine.transition('click')).toBe('loading');
expect(machine.transition('click')).toBe('loading');
});
});

describe('When the transition is disallowed by a conditional', () => {
it('Should return the previous state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: ['loading', () => false],
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');

expect(machine.transition('click')).toBe('idle');
});
});
});

describe('transitionFrom', () => {
describe('When the transition is valid', () => {
it('Should return the new state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: 'loading',
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');
expect(machine.transitionFrom('idle', 'click')).toBe('loading');
});
});

describe('When the transition is invalid', () => {
it('Should return the previous state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: 'loading',
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');
expect(machine.transitionFrom('idle', 'finish')).toBe('idle');
});
});

describe('When the transition is disallowed by a conditional', () => {
it('Should return the previous state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: ['loading', () => false],
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');
expect(machine.transitionFrom('idle', 'click')).toBe('idle');
});
});

describe('When the transition is allowed by a conditional', () => {
it('Should return the new state', () => {
const machine = StateMachine({
initial: 'idle',
states: {
error: {},
idle: {
click: ['loading', () => true],
},
loading: {
success: 'success',
error: 'error',
},
success: {},
},
});
expect(machine.getState()).toBe('idle');
expect(machine.transitionFrom('idle', 'click')).toBe('loading');
});
});
});
});

0 comments on commit bca9421

Please sign in to comment.