useUndoableReducer.spec.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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(() => useUndoableReducer(reducer, 100));
  54. expect(reducer).not.toHaveBeenCalled();
  55. expect(result.current[0]).toEqual(100);
  56. });
  57. it('updates state', () => {
  58. const reducer = jest
  59. .fn()
  60. .mockImplementation((state: number, action: 'add' | 'subtract') =>
  61. action === 'add' ? state + 1 : state - 1
  62. );
  63. const {result} = reactHooks.renderHook(() => useUndoableReducer(reducer, 0));
  64. reactHooks.act(() => result.current[1]('add'));
  65. expect(result.current[0]).toEqual(1);
  66. expect(reducer).toHaveBeenNthCalledWith(1, 0, 'add');
  67. reactHooks.act(() => result.current[1]('add'));
  68. expect(result.current[0]).toEqual(2);
  69. expect(reducer).toHaveBeenNthCalledWith(2, 0, 'add');
  70. });
  71. it('can undo state', () => {
  72. const {result} = reactHooks.renderHook(() =>
  73. useUndoableReducer(
  74. jest.fn().mockImplementation(s => s + 1),
  75. 0
  76. )
  77. );
  78. reactHooks.act(() => result.current[1](0));
  79. expect(result.current[0]).toEqual(1);
  80. reactHooks.act(() => result.current[1]({type: 'undo'}));
  81. expect(result.current[0]).toEqual(0);
  82. });
  83. it('can redo state', () => {
  84. const {result} = reactHooks.renderHook(() =>
  85. useUndoableReducer(
  86. jest.fn().mockImplementation(s => {
  87. return s + 1;
  88. }),
  89. 0
  90. )
  91. );
  92. reactHooks.act(() => result.current[1](0)); // 0 + 1
  93. reactHooks.act(() => result.current[1](1)); // 1 + 1
  94. reactHooks.act(() => result.current[1]({type: 'undo'})); // 2 -> 1
  95. reactHooks.act(() => result.current[1]({type: 'undo'})); // 1 -> 0
  96. expect(result.current[0]).toEqual(0);
  97. reactHooks.act(() => result.current[1]({type: 'redo'})); // 0 -> 1
  98. reactHooks.act(() => result.current[1]({type: 'redo'})); // 1 -> 2
  99. expect(result.current[0]).toEqual(2);
  100. reactHooks.act(() => result.current[1]({type: 'redo'})); // 2 -> undefined
  101. expect(result.current[0]).toEqual(2);
  102. });
  103. });
  104. it('can peek previous and next state', () => {
  105. const simpleReducer = (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
  106. action.type === 'add' ? state + 1 : state - 1;
  107. const {result} = reactHooks.renderHook(() => useUndoableReducer(simpleReducer, 0));
  108. reactHooks.act(() => result.current[1]({type: 'add'}));
  109. expect(result.current?.[2].previousState).toEqual(0);
  110. reactHooks.act(() => result.current[1]({type: 'undo'}));
  111. expect(result.current?.[2].nextState).toEqual(1);
  112. });
  113. it('can work with primitives', () => {
  114. const simpleReducer = (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
  115. action.type === 'add' ? state + 1 : state - 1;
  116. const {result} = reactHooks.renderHook(() =>
  117. useReducer(makeUndoableReducer(makeCombinedReducers({simple: simpleReducer})), {
  118. previous: undefined,
  119. current: {
  120. simple: 0,
  121. },
  122. next: undefined,
  123. })
  124. );
  125. reactHooks.act(() => result.current[1]({type: 'add'}));
  126. expect(result.current[0].current.simple).toBe(1);
  127. reactHooks.act(() => result.current[1]({type: 'undo'}));
  128. expect(result.current[0].current.simple).toBe(0);
  129. reactHooks.act(() => result.current[1]({type: 'redo'}));
  130. expect(result.current[0].current.simple).toBe(1);
  131. });
  132. it('can work with objects', () => {
  133. const combinedReducers = makeCombinedReducers({
  134. math: (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
  135. action.type === 'add' ? state + 1 : state - 1,
  136. });
  137. const {result} = reactHooks.renderHook(() =>
  138. useReducer(makeUndoableReducer(combinedReducers), {
  139. previous: undefined,
  140. current: {
  141. math: 0,
  142. },
  143. next: undefined,
  144. })
  145. );
  146. reactHooks.act(() => result.current[1]({type: 'add'}));
  147. expect(result.current[0].current.math).toBe(1);
  148. reactHooks.act(() => result.current[1]({type: 'undo'}));
  149. expect(result.current[0].current.math).toBe(0);
  150. reactHooks.act(() => result.current[1]({type: 'redo'}));
  151. expect(result.current[0].current.math).toBe(1);
  152. });
  153. });