finite-state-machine.spec.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import { of } from 'rxjs';
  2. import { FSM, Transitions } from './finite-state-machine';
  3. describe('Finite State Machine', () => {
  4. type TestState = 'DoorsClosed' | 'DoorsOpen' | 'Moving';
  5. const transitions: Transitions<TestState> = {
  6. DoorsClosed: {
  7. to: ['Moving', 'DoorsOpen'],
  8. },
  9. DoorsOpen: {
  10. to: ['DoorsClosed'],
  11. },
  12. Moving: {
  13. to: ['DoorsClosed'],
  14. },
  15. };
  16. it('initialState works', () => {
  17. const initialState = 'DoorsClosed';
  18. const fsm = new FSM<TestState>({ transitions }, initialState);
  19. expect(fsm.initialState).toBe(initialState);
  20. });
  21. it('getNextStates() works', () => {
  22. const initialState = 'DoorsClosed';
  23. const fsm = new FSM<TestState>({ transitions }, initialState);
  24. expect(fsm.getNextStates()).toEqual(['Moving', 'DoorsOpen']);
  25. });
  26. it('allows valid transitions', () => {
  27. const initialState = 'DoorsClosed';
  28. const fsm = new FSM<TestState>({ transitions }, initialState);
  29. fsm.transitionTo('Moving');
  30. expect(fsm.currentState).toBe('Moving');
  31. fsm.transitionTo('DoorsClosed');
  32. expect(fsm.currentState).toBe('DoorsClosed');
  33. fsm.transitionTo('DoorsOpen');
  34. expect(fsm.currentState).toBe('DoorsOpen');
  35. fsm.transitionTo('DoorsClosed');
  36. expect(fsm.currentState).toBe('DoorsClosed');
  37. });
  38. it('does not allow invalid transitions', () => {
  39. const initialState = 'DoorsOpen';
  40. const fsm = new FSM<TestState>({ transitions }, initialState);
  41. fsm.transitionTo('Moving');
  42. expect(fsm.currentState).toBe('DoorsOpen');
  43. fsm.transitionTo('DoorsClosed');
  44. expect(fsm.currentState).toBe('DoorsClosed');
  45. fsm.transitionTo('Moving');
  46. expect(fsm.currentState).toBe('Moving');
  47. fsm.transitionTo('DoorsOpen');
  48. expect(fsm.currentState).toBe('Moving');
  49. });
  50. it('onTransitionStart() is invoked before a transition takes place', () => {
  51. const initialState = 'DoorsClosed';
  52. const spy = jest.fn();
  53. const data = 123;
  54. let currentStateDuringCallback = '';
  55. const fsm = new FSM<TestState>(
  56. {
  57. transitions,
  58. onTransitionStart: spy.mockImplementation(() => {
  59. currentStateDuringCallback = fsm.currentState;
  60. }),
  61. },
  62. initialState,
  63. );
  64. fsm.transitionTo('Moving', data);
  65. expect(spy).toHaveBeenCalledWith(initialState, 'Moving', data);
  66. expect(currentStateDuringCallback).toBe(initialState);
  67. });
  68. it('onTransitionEnd() is invoked after a transition takes place', () => {
  69. const initialState = 'DoorsClosed';
  70. const spy = jest.fn();
  71. const data = 123;
  72. let currentStateDuringCallback = '';
  73. const fsm = new FSM<TestState>(
  74. {
  75. transitions,
  76. onTransitionEnd: spy.mockImplementation(() => {
  77. currentStateDuringCallback = fsm.currentState;
  78. }),
  79. },
  80. initialState,
  81. );
  82. fsm.transitionTo('Moving', data);
  83. expect(spy).toHaveBeenCalledWith(initialState, 'Moving', data);
  84. expect(currentStateDuringCallback).toBe('Moving');
  85. });
  86. it('onTransitionStart() cancels transition when it returns false', async () => {
  87. const initialState = 'DoorsClosed';
  88. const fsm = new FSM<TestState>(
  89. {
  90. transitions,
  91. onTransitionStart: () => false,
  92. },
  93. initialState,
  94. );
  95. await fsm.transitionTo('Moving');
  96. expect(fsm.currentState).toBe(initialState);
  97. });
  98. it('onTransitionStart() cancels transition when it returns Promise<false>', async () => {
  99. const initialState = 'DoorsClosed';
  100. const fsm = new FSM<TestState>(
  101. {
  102. transitions,
  103. onTransitionStart: () => Promise.resolve(false),
  104. },
  105. initialState,
  106. );
  107. await fsm.transitionTo('Moving');
  108. expect(fsm.currentState).toBe(initialState);
  109. });
  110. it('onTransitionStart() cancels transition when it returns Observable<false>', async () => {
  111. const initialState = 'DoorsClosed';
  112. const fsm = new FSM<TestState>(
  113. {
  114. transitions,
  115. onTransitionStart: () => of(false),
  116. },
  117. initialState,
  118. );
  119. await fsm.transitionTo('Moving');
  120. expect(fsm.currentState).toBe(initialState);
  121. });
  122. it('onTransitionStart() cancels transition when it returns a string', async () => {
  123. const initialState = 'DoorsClosed';
  124. const fsm = new FSM<TestState>(
  125. {
  126. transitions,
  127. onTransitionStart: () => 'foo',
  128. },
  129. initialState,
  130. );
  131. await fsm.transitionTo('Moving');
  132. expect(fsm.currentState).toBe(initialState);
  133. });
  134. it('onTransitionStart() allows transition when it returns true', async () => {
  135. const initialState = 'DoorsClosed';
  136. const fsm = new FSM<TestState>(
  137. {
  138. transitions,
  139. onTransitionStart: () => true,
  140. },
  141. initialState,
  142. );
  143. await fsm.transitionTo('Moving');
  144. expect(fsm.currentState).toBe('Moving');
  145. });
  146. it('onTransitionStart() allows transition when it returns void', async () => {
  147. const initialState = 'DoorsClosed';
  148. const fsm = new FSM<TestState>(
  149. {
  150. transitions,
  151. onTransitionStart: () => {
  152. /* empty */
  153. },
  154. },
  155. initialState,
  156. );
  157. await fsm.transitionTo('Moving');
  158. expect(fsm.currentState).toBe('Moving');
  159. });
  160. it('onError() is invoked for invalid transitions', async () => {
  161. const initialState = 'DoorsOpen';
  162. const spy = jest.fn();
  163. const fsm = new FSM<TestState>(
  164. {
  165. transitions,
  166. onError: spy,
  167. },
  168. initialState,
  169. );
  170. await fsm.transitionTo('Moving');
  171. expect(spy).toHaveBeenCalledWith(initialState, 'Moving', undefined);
  172. });
  173. it('onTransitionStart() invokes onError() if it returns a string', async () => {
  174. const initialState = 'DoorsClosed';
  175. const spy = jest.fn();
  176. const fsm = new FSM<TestState>(
  177. {
  178. transitions,
  179. onTransitionStart: () => 'error',
  180. onError: spy,
  181. },
  182. initialState,
  183. );
  184. await fsm.transitionTo('Moving');
  185. expect(spy).toHaveBeenCalledWith(initialState, 'Moving', 'error');
  186. });
  187. });