useDispatchingReducer.spec.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import {act, renderHook, waitFor} from 'sentry-test/reactTestingLibrary';
  2. import {makeCombinedReducers} from 'sentry/utils/useCombinedReducer';
  3. import {useDispatchingReducer} from 'sentry/utils/useDispatchingReducer';
  4. describe('useDispatchingReducer', () => {
  5. beforeEach(() => {
  6. window.requestAnimationFrame = jest.fn().mockImplementation(cb => {
  7. return setTimeout(cb, 0);
  8. });
  9. window.cancelAnimationFrame = jest.fn().mockImplementation(id => {
  10. return clearTimeout(id);
  11. });
  12. jest.useFakeTimers();
  13. });
  14. afterEach(() => {
  15. jest.useRealTimers();
  16. });
  17. it('initializes state with initializer', () => {
  18. const reducer = jest.fn().mockImplementation(s => s) as () => {};
  19. const initialState = {type: 'initial'};
  20. const {result} = renderHook(() => useDispatchingReducer(reducer, initialState));
  21. expect(result.current[0]).toBe(initialState);
  22. });
  23. it('initializes state with fn initializer arg', () => {
  24. const reducer = jest.fn().mockImplementation(s => s) as () => {};
  25. const initialState = {type: 'initial'};
  26. const {result} = renderHook(() =>
  27. // @ts-expect-error force undfined
  28. useDispatchingReducer(reducer, undefined, () => initialState)
  29. );
  30. expect(result.current[0]).toBe(initialState);
  31. });
  32. describe('action dispatching', () => {
  33. const reducer = jest.fn().mockImplementation((_s, action: string) => {
  34. switch (action) {
  35. case 'action':
  36. return {type: 'action'};
  37. default:
  38. throw new Error('unknown action');
  39. }
  40. });
  41. it('calls reducer and updates state', async () => {
  42. const initialState = {type: 'initial'};
  43. const {result} = renderHook(() => useDispatchingReducer(reducer, initialState));
  44. act(() => result.current[1]('action'));
  45. act(() => {
  46. jest.runAllTimers();
  47. });
  48. await waitFor(() => {
  49. expect(reducer).toHaveBeenCalledTimes(1);
  50. expect(result.current[0]).toEqual({type: 'action'});
  51. });
  52. });
  53. it('calls before action with state and action args', () => {
  54. const initialState = {type: 'initial'};
  55. const {result} = renderHook(() => useDispatchingReducer(reducer, initialState));
  56. const beforeAction = jest.fn();
  57. result.current[2].on('before action', beforeAction);
  58. act(() => result.current[1]('action'));
  59. act(() => {
  60. jest.runAllTimers();
  61. });
  62. expect(beforeAction).toHaveBeenCalledTimes(1);
  63. expect(beforeAction).toHaveBeenCalledWith(initialState, 'action');
  64. });
  65. it('calls after action with previous, new state and action args', () => {
  66. const initialState = {type: 'initial'};
  67. const {result} = renderHook(() => useDispatchingReducer(reducer, initialState));
  68. const beforeNextState = jest.fn();
  69. result.current[2].on('before next state', beforeNextState);
  70. act(() => result.current[1]('action'));
  71. act(() => {
  72. jest.runAllTimers();
  73. });
  74. expect(beforeNextState).toHaveBeenCalledTimes(1);
  75. expect(beforeNextState).toHaveBeenCalledWith(
  76. initialState,
  77. {type: 'action'},
  78. 'action'
  79. );
  80. });
  81. it('updates to final state if multiple calls', () => {
  82. const initialState = {};
  83. const action_storing_reducer = jest
  84. .fn()
  85. .mockImplementation((state, action: string) => {
  86. switch (action) {
  87. default:
  88. return {
  89. ...state,
  90. [action]: 1,
  91. };
  92. }
  93. });
  94. const {result} = renderHook(() =>
  95. useDispatchingReducer(action_storing_reducer, initialState)
  96. );
  97. act(() => {
  98. result.current[1]('action');
  99. result.current[1]('another');
  100. });
  101. act(() => {
  102. jest.runAllTimers();
  103. });
  104. expect(result.current[0]).toEqual({action: 1, another: 1});
  105. });
  106. });
  107. it('supports combined reducer', () => {
  108. function reducerA(state: Record<any, any>, action: string) {
  109. if (action !== 'a') return state;
  110. return {...state, [action]: 1};
  111. }
  112. function reducerB(state: Record<any, any>, action: string) {
  113. if (action !== 'b') return state;
  114. return {...state, [action]: 1};
  115. }
  116. const finalReducer = makeCombinedReducers({
  117. a: reducerA,
  118. b: reducerB,
  119. });
  120. const initialState = {a: {}, b: {}};
  121. const {result} = renderHook(() => useDispatchingReducer(finalReducer, initialState));
  122. act(() => {
  123. result.current[1]('a');
  124. result.current[1]('b');
  125. });
  126. act(() => {
  127. jest.runAllTimers();
  128. });
  129. expect(result.current[0]).toEqual({a: {a: 1}, b: {b: 1}});
  130. });
  131. it('emitter supports side effect dispatching', () => {
  132. const reducer = jest.fn().mockImplementation(function reducer(
  133. state: Record<any, any>,
  134. action: string
  135. ) {
  136. const nextState = {...state, [action]: 1};
  137. return nextState;
  138. });
  139. const initialState = {};
  140. const {result} = renderHook(() => useDispatchingReducer(reducer, initialState));
  141. result.current[2].on('before action', (_state, action) => {
  142. if (action === 'a') {
  143. result.current[1]('b');
  144. }
  145. });
  146. act(() => result.current[1]('a'));
  147. act(() => {
  148. jest.runAllTimers();
  149. });
  150. expect(reducer).toHaveBeenCalledTimes(2);
  151. });
  152. });