From 8baf731328029d887d5e8f39d9aba6dcb1fb20cd Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 4 Oct 2017 17:18:00 -0400 Subject: [PATCH] add undo reducer --- src/reducers/undo.js | 79 ++++++++++++++++++++ test/unit/undo-reducer.test.js | 133 +++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 src/reducers/undo.js create mode 100644 test/unit/undo-reducer.test.js diff --git a/src/reducers/undo.js b/src/reducers/undo.js new file mode 100644 index 00000000..e950df8b --- /dev/null +++ b/src/reducers/undo.js @@ -0,0 +1,79 @@ +import log from '../log/log'; + +const UNDO = 'scratch-paint/undo/UNDO'; +const REDO = 'scratch-paint/undo/REDO'; +const SNAPSHOT = 'scratch-paint/undo/SNAPSHOT'; +const CLEAR = 'scratch-paint/undo/CLEAR'; +const initialState = { + stack: [], + pointer: -1 +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case UNDO: + if (state.pointer === -1) { + log.warn(`Can't undo, undo stack is empty`); + return state; + } + return { + stack: state.stack, + pointer: state.pointer - 1 + }; + case REDO: + if (state.pointer === state.stack.length - 1) { + log.warn(`Can't redo, redo stack is empty`); + return state; + } + return { + stack: state.stack, + pointer: state.pointer + 1 + }; + case SNAPSHOT: + if (!action.snapshot) { + log.warn(`Couldn't create undo snapshot, no data provided`); + return state; + } + return { + // Performing an action clears the redo stack + stack: state.stack.slice(0, state.pointer + 1).concat(action.snapshot), + pointer: state.pointer + 1 + }; + case CLEAR: + return initialState; + default: + return state; + } +}; + +// Action creators ================================== +const undoSnapshot = function (snapshot) { + return { + type: SNAPSHOT, + snapshot: snapshot + }; +}; +const undo = function () { + return { + type: UNDO + }; +}; +const redo = function () { + return { + type: REDO + }; +}; +const clearUndoState = function () { + return { + type: CLEAR + }; +}; + +export { + reducer as default, + undo, + redo, + undoSnapshot, + clearUndoState +}; diff --git a/test/unit/undo-reducer.test.js b/test/unit/undo-reducer.test.js new file mode 100644 index 00000000..4f20299c --- /dev/null +++ b/test/unit/undo-reducer.test.js @@ -0,0 +1,133 @@ +/* eslint-env jest */ +import undoReducer from '../../src/reducers/undo'; +import {undoSnapshot, undo, redo, clearUndoState} from '../../src/reducers/undo'; + +test('initialState', () => { + let defaultState; + + expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); + expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).pointer).toEqual(-1); + expect(undoReducer(defaultState /* state */, {type: 'anything'} /* action */).stack).toHaveLength(0); +}); + +test('snapshot', () => { + let defaultState; + const state1 = {state: 1}; + const state2 = {state: 2}; + + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */); + expect(reduxState.pointer).toEqual(0); + expect(reduxState.stack).toHaveLength(1); + expect(reduxState.stack[0]).toEqual(state1); + + reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */); + expect(reduxState.pointer).toEqual(1); + expect(reduxState.stack).toHaveLength(2); + expect(reduxState.stack[0]).toEqual(state1); + expect(reduxState.stack[1]).toEqual(state2); +}); + +test('invalidSnapshot', () => { + let defaultState; + const state1 = {state: 1}; + + const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */); + const newReduxState = undoReducer(reduxState /* state */, undoSnapshot() /* action */); // No snapshot provided + expect(reduxState).toEqual(newReduxState); +}); + +test('clearUndoState', () => { + let defaultState; + const state1 = {state: 1}; + const state2 = {state: 2}; + + // Push 2 states then clear + const reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */); + undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */); + const newReduxState = undoReducer(reduxState /* state */, clearUndoState() /* action */); + + expect(newReduxState.pointer).toEqual(-1); + expect(newReduxState.stack).toHaveLength(0); +}); + +test('cantUndo', () => { + let defaultState; + + // Undo when there's no undo stack + const reduxState = undoReducer(defaultState /* state */, undo() /* action */); + + expect(reduxState.pointer).toEqual(-1); + expect(reduxState.stack).toHaveLength(0); +}); + +test('cantRedo', () => { + let defaultState; + const state1 = {state: 1}; + + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */); + + // Redo when there's no redo stack + reduxState = undoReducer(reduxState /* state */, redo() /* action */); + + expect(reduxState.pointer).toEqual(0); + expect(reduxState.stack).toHaveLength(1); +}); + +test('undo', () => { + let defaultState; + const state1 = {state: 1}; + const state2 = {state: 2}; + + // Push 2 states then undo one + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */); + reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */); + reduxState = undoReducer(reduxState /* state */, undo() /* action */); + + expect(reduxState.pointer).toEqual(0); + expect(reduxState.stack).toHaveLength(2); + expect(reduxState.stack[0]).toEqual(state1); + expect(reduxState.stack[1]).toEqual(state2); +}); + +test('redo', () => { + let defaultState; + const state1 = {state: 1}; + const state2 = {state: 2}; + + // Push 2 states then undo one + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */); + reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */); + let newReduxState = undoReducer(reduxState /* state */, undo() /* action */); + + // Now redo and check equality with previous state + newReduxState = undoReducer(newReduxState /* state */, redo() /* action */); + expect(newReduxState.pointer).toEqual(reduxState.pointer); + expect(newReduxState.stack).toHaveLength(reduxState.stack.length); + expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]); + expect(reduxState.stack[1]).toEqual(reduxState.stack[1]); +}); + +test('undoSnapshotCantRedo', () => { + let defaultState; + const state1 = {state: 1}; + const state2 = {state: 2}; + + // Push 2 states then undo twice + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([state1]) /* action */); + reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */); + reduxState = undoReducer(reduxState /* state */, undo() /* action */); + reduxState = undoReducer(reduxState /* state */, undo() /* action */); + + expect(reduxState.pointer).toEqual(-1); + expect(reduxState.stack).toHaveLength(2); + + // Snapshot + reduxState = undoReducer(reduxState /* state */, undoSnapshot([state2]) /* action */); + // Redo should do nothing + const newReduxState = undoReducer(reduxState /* state */, redo() /* action */); + + expect(newReduxState.pointer).toEqual(reduxState.pointer); + expect(newReduxState.stack).toHaveLength(reduxState.stack.length); + expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]); + expect(newReduxState.stack[0]).toEqual(state2); +});