123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- import {useReducer} from 'react';
- import {reactHooks} from 'sentry-test/reactTestingLibrary';
- import {makeCombinedReducers} from 'sentry/utils/useCombinedReducer';
- import {
- makeUndoableReducer,
- UndoableNode,
- useUndoableReducer,
- } from 'sentry/utils/useUndoableReducer';
- describe('makeUndoableReducer', () => {
- it('does not overflow undo/redo', () => {
- const mockFirstReducer = jest.fn().mockImplementation(v => ++v);
- const reducer = makeUndoableReducer(mockFirstReducer);
- expect(() =>
- reducer({previous: undefined, current: 0, next: undefined}, {type: 'undo'})
- ).not.toThrow();
- expect(() =>
- reducer({previous: undefined, current: 0, next: undefined}, {type: 'redo'})
- ).not.toThrow();
- });
- it('calls undo/redo if action matches', () => {
- const mockFirstReducer = jest.fn().mockImplementation(v => {
- return ++v;
- });
- const reducer = makeUndoableReducer(mockFirstReducer);
- const first: UndoableNode<number> = {
- previous: undefined,
- current: 0,
- next: undefined,
- };
- const current = {
- previous: first,
- current: 1,
- next: undefined,
- };
- first.next = current;
- expect(reducer(first, {type: 'redo'})).toEqual(current);
- expect(reducer(current, {type: 'undo'})).toEqual(first);
- expect(mockFirstReducer).not.toHaveBeenCalled();
- expect(reducer(current, 'add')).toEqual({
- previous: current,
- current: 2,
- next: undefined,
- });
- expect(mockFirstReducer).toHaveBeenLastCalledWith(current.current, 'add');
- });
- describe('useUndoableReducer', () => {
- it('initializes with init state', () => {
- const reducer = jest
- .fn()
- .mockImplementation((state: number, action: 'add' | 'subtract') =>
- action === 'add' ? state + 1 : state - 1
- );
- const {result} = reactHooks.renderHook(() => useUndoableReducer(reducer, 100));
- expect(reducer).not.toHaveBeenCalled();
- expect(result.current[0]).toEqual(100);
- });
- it('updates state', () => {
- const reducer = jest
- .fn()
- .mockImplementation((state: number, action: 'add' | 'subtract') =>
- action === 'add' ? state + 1 : state - 1
- );
- const {result} = reactHooks.renderHook(() => useUndoableReducer(reducer, 0));
- reactHooks.act(() => result.current[1]('add'));
- expect(result.current[0]).toEqual(1);
- expect(reducer).toHaveBeenNthCalledWith(1, 0, 'add');
- reactHooks.act(() => result.current[1]('add'));
- expect(result.current[0]).toEqual(2);
- expect(reducer).toHaveBeenNthCalledWith(2, 0, 'add');
- });
- it('can undo state', () => {
- const {result} = reactHooks.renderHook(() =>
- useUndoableReducer(
- jest.fn().mockImplementation(s => s + 1),
- 0
- )
- );
- reactHooks.act(() => result.current[1](0));
- expect(result.current[0]).toEqual(1);
- reactHooks.act(() => result.current[1]({type: 'undo'}));
- expect(result.current[0]).toEqual(0);
- });
- it('can redo state', () => {
- const {result} = reactHooks.renderHook(() =>
- useUndoableReducer(
- jest.fn().mockImplementation(s => {
- return s + 1;
- }),
- 0
- )
- );
- reactHooks.act(() => result.current[1](0)); // 0 + 1
- reactHooks.act(() => result.current[1](1)); // 1 + 1
- reactHooks.act(() => result.current[1]({type: 'undo'})); // 2 -> 1
- reactHooks.act(() => result.current[1]({type: 'undo'})); // 1 -> 0
- expect(result.current[0]).toEqual(0);
- reactHooks.act(() => result.current[1]({type: 'redo'})); // 0 -> 1
- reactHooks.act(() => result.current[1]({type: 'redo'})); // 1 -> 2
- expect(result.current[0]).toEqual(2);
- reactHooks.act(() => result.current[1]({type: 'redo'})); // 2 -> undefined
- expect(result.current[0]).toEqual(2);
- });
- });
- it('can peek previous and next state', () => {
- const simpleReducer = (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
- action.type === 'add' ? state + 1 : state - 1;
- const {result} = reactHooks.renderHook(() => useUndoableReducer(simpleReducer, 0));
- reactHooks.act(() => result.current[1]({type: 'add'}));
- expect(result.current?.[2].previousState).toEqual(0);
- reactHooks.act(() => result.current[1]({type: 'undo'}));
- expect(result.current?.[2].nextState).toEqual(1);
- });
- it('can work with primitives', () => {
- const simpleReducer = (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
- action.type === 'add' ? state + 1 : state - 1;
- const {result} = reactHooks.renderHook(() =>
- useReducer(makeUndoableReducer(makeCombinedReducers({simple: simpleReducer})), {
- previous: undefined,
- current: {
- simple: 0,
- },
- next: undefined,
- })
- );
- reactHooks.act(() => result.current[1]({type: 'add'}));
- expect(result.current[0].current.simple).toBe(1);
- reactHooks.act(() => result.current[1]({type: 'undo'}));
- expect(result.current[0].current.simple).toBe(0);
- reactHooks.act(() => result.current[1]({type: 'redo'}));
- expect(result.current[0].current.simple).toBe(1);
- });
- it('can work with objects', () => {
- const combinedReducers = makeCombinedReducers({
- math: (state: number, action: {type: 'add'} | {type: 'subtract'}) =>
- action.type === 'add' ? state + 1 : state - 1,
- });
- const {result} = reactHooks.renderHook(() =>
- useReducer(makeUndoableReducer(combinedReducers), {
- previous: undefined,
- current: {
- math: 0,
- },
- next: undefined,
- })
- );
- reactHooks.act(() => result.current[1]({type: 'add'}));
- expect(result.current[0].current.math).toBe(1);
- reactHooks.act(() => result.current[1]({type: 'undo'}));
- expect(result.current[0].current.math).toBe(0);
- reactHooks.act(() => result.current[1]({type: 'redo'}));
- expect(result.current[0].current.math).toBe(1);
- });
- });
|