useUndoableReducer.spec.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import {useReducer} from 'react';
  2. import {act, renderHook} from 'sentry-test/reactTestingLibrary';
  3. import {makeCombinedReducers} from 'sentry/utils/useCombinedReducer';
  4. import type {UndoableNode} from 'sentry/utils/useUndoableReducer';
  5. import {makeUndoableReducer, useUndoableReducer} from 'sentry/utils/useUndoableReducer';
  6. describe('makeUndoableReducer', () => {
  7. it('does not overflow undo/redo', () => {
  8. const mockFirstReducer = jest.fn().mockImplementation(v => ++v);
  9. const reducer = makeUndoableReducer(mockFirstReducer);
  10. expect(() =>
  11. reducer({previous: undefined, current: 0, next: undefined}, {type: 'undo'})
  12. ).not.toThrow();
  13. expect(() =>
  14. reducer({previous: undefined, current: 0, next: undefined}, {type: 'redo'})
  15. ).not.toThrow();
  16. });
  17. it('calls undo/redo if action matches', () => {
  18. const mockFirstReducer = jest.fn().mockImplementation(v => {
  19. return ++v;
  20. });
  21. const reducer = makeUndoableReducer(mockFirstReducer);
  22. const first: UndoableNode<number> = {
  23. previous: undefined,
  24. current: 0,
  25. next: undefined,
  26. };
  27. const current = {
  28. previous: first,
  29. current: 1,
  30. next: undefined,
  31. };
  32. first.next = current;
  33. expect(reducer(first, {type: 'redo'})).toEqual(current);
  34. expect(reducer(current, {type: 'undo'})).toEqual(first);
  35. expect(mockFirstReducer).not.toHaveBeenCalled();
  36. expect(reducer(current, 'add')).toEqual({
  37. previous: current,
  38. current: 2,
  39. next: undefined,
  40. });
  41. expect(mockFirstReducer).toHaveBeenLastCalledWith(current.current, 'add');
  42. });
  43. describe('useUndoableReducer', () => {
  44. it('initializes with init state', () => {
  45. const reducer = jest
  46. .fn()
  47. .mockImplementation((state: number, action: 'add' | 'subtract') =>
  48. action === 'add' ? state + 1 : state - 1
  49. );
  50. const {result} = renderHook(
  51. (args: Parameters<typeof useUndoableReducer>) =>
  52. useUndoableReducer(args[0], args[1]),
  53. {
  54. initialProps: [reducer, 100],
  55. }
  56. );
  57. expect(reducer).not.toHaveBeenCalled();
  58. expect(result.current[0]).toEqual(100);
  59. });
  60. it('updates state', () => {
  61. const reducer = jest
  62. .fn()
  63. .mockImplementation((state: number, action: 'add' | 'subtract') =>
  64. action === 'add' ? state + 1 : state - 1
  65. );
  66. const {result} = renderHook(
  67. (args: Parameters<typeof useUndoableReducer>) =>
  68. useUndoableReducer(args[0], args[1]),
  69. {initialProps: [reducer, 0]}
  70. );
  71. act(() => result.current[1]('add'));
  72. expect(result.current[0]).toEqual(1);
  73. expect(reducer).toHaveBeenNthCalledWith(1, 0, 'add');
  74. act(() => result.current[1]('add'));
  75. expect(result.current[0]).toEqual(2);
  76. // TODO(react18): switch back to .toHaveBeenNthCalledWith(2, 1, 'add');
  77. expect(reducer).toHaveBeenLastCalledWith(1, 'add');
  78. });
  79. it('can undo state', () => {
  80. const {result} = renderHook(
  81. (args: Parameters<typeof useUndoableReducer>) =>
  82. useUndoableReducer(args[0], args[1]),
  83. {initialProps: [jest.fn().mockImplementation(s => s + 1), 0]}
  84. );
  85. act(() => result.current[1](0));
  86. expect(result.current[0]).toEqual(1);
  87. act(() => result.current[1]({type: 'undo'}));
  88. expect(result.current[0]).toEqual(0);
  89. });
  90. it('can redo state', () => {
  91. const {result} = renderHook(
  92. (args: Parameters<typeof useUndoableReducer>) =>
  93. useUndoableReducer(args[0], args[1]),
  94. {initialProps: [jest.fn().mockImplementation(s => s + 1), 0]}
  95. );
  96. act(() => result.current[1](0)); // 0 + 1
  97. act(() => result.current[1](1)); // 1 + 1
  98. act(() => result.current[1]({type: 'undo'})); // 2 -> 1
  99. act(() => result.current[1]({type: 'undo'})); // 1 -> 0
  100. expect(result.current[0]).toEqual(0);
  101. act(() => result.current[1]({type: 'redo'})); // 0 -> 1
  102. act(() => result.current[1]({type: 'redo'})); // 1 -> 2
  103. expect(result.current[0]).toEqual(2);
  104. act(() => result.current[1]({type: 'redo'})); // 2 -> undefined
  105. expect(result.current[0]).toEqual(2);
  106. });
  107. });
  108. it('can peek previous and next state', () => {
  109. const simpleReducer = (state, action) =>
  110. action.type === 'add' ? state + 1 : state - 1;
  111. const {result} = renderHook(
  112. (args: Parameters<typeof useUndoableReducer>) =>
  113. useUndoableReducer(args[0], args[1]),
  114. {
  115. initialProps: [simpleReducer, 0],
  116. }
  117. );
  118. act(() => result.current[1]({type: 'add'}));
  119. expect(result.current?.[2].previousState).toEqual(0);
  120. act(() => result.current[1]({type: 'undo'}));
  121. expect(result.current?.[2].nextState).toEqual(1);
  122. });
  123. it('can work with primitives', () => {
  124. const simpleReducer = (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
  125. action.type === 'add' ? state + 1 : state - 1;
  126. const {result} = renderHook(
  127. (args: Parameters<typeof useReducer>) => useReducer(args[0], args[1]),
  128. {
  129. initialProps: [
  130. makeUndoableReducer(makeCombinedReducers({simple: simpleReducer})),
  131. {
  132. previous: undefined,
  133. current: {
  134. simple: 0,
  135. },
  136. next: undefined,
  137. },
  138. ],
  139. }
  140. );
  141. act(() => result.current[1]({type: 'add'}));
  142. expect(result.current[0].current.simple).toBe(1);
  143. act(() => result.current[1]({type: 'undo'}));
  144. expect(result.current[0].current.simple).toBe(0);
  145. act(() => result.current[1]({type: 'redo'}));
  146. expect(result.current[0].current.simple).toBe(1);
  147. });
  148. it('can work with objects', () => {
  149. const combinedReducers = makeCombinedReducers({
  150. math: (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
  151. action.type === 'add' ? state + 1 : state - 1,
  152. });
  153. const {result} = renderHook(
  154. (args: Parameters<typeof useReducer>) => useReducer(args[0], args[1]),
  155. {
  156. initialProps: [
  157. makeUndoableReducer(combinedReducers),
  158. {
  159. previous: undefined,
  160. current: {
  161. math: 0,
  162. },
  163. next: undefined,
  164. },
  165. ],
  166. }
  167. );
  168. act(() => result.current[1]({type: 'add'}));
  169. expect(result.current[0].current.math).toBe(1);
  170. act(() => result.current[1]({type: 'undo'}));
  171. expect(result.current[0].current.math).toBe(0);
  172. act(() => result.current[1]({type: 'redo'}));
  173. expect(result.current[0].current.math).toBe(1);
  174. });
  175. });