useUndoableReducer.spec.tsx 6.6 KB

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