diff --git a/src/reducers/undo.js b/src/reducers/undo.js index 2a669383..ce16c108 100644 --- a/src/reducers/undo.js +++ b/src/reducers/undo.js @@ -4,6 +4,7 @@ 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 MAX_STACK_SIZE = 100; const initialState = { stack: [], pointer: -1 @@ -35,6 +36,14 @@ const reducer = function (state, action) { log.warn(`Couldn't create undo snapshot, no data provided`); return state; } + // Overflowed or about to overflow + if (state.pointer >= MAX_STACK_SIZE - 1) { + return { + // Make a stack of size MAX_STACK_SIZE, cutting off the oldest snapshots. + stack: state.stack.slice(state.pointer - MAX_STACK_SIZE + 2, state.pointer + 1).concat(action.snapshot), + pointer: MAX_STACK_SIZE - 1 + }; + } return { // Performing an action clears the redo stack stack: state.stack.slice(0, state.pointer + 1).concat(action.snapshot), @@ -75,5 +84,6 @@ export { undo, redo, undoSnapshot, - clearUndoState + clearUndoState, + MAX_STACK_SIZE }; diff --git a/test/unit/undo-reducer.test.js b/test/unit/undo-reducer.test.js index 6201d9e3..e5332e38 100644 --- a/test/unit/undo-reducer.test.js +++ b/test/unit/undo-reducer.test.js @@ -1,6 +1,6 @@ /* eslint-env jest */ import undoReducer from '../../src/reducers/undo'; -import {undoSnapshot, undo, redo, clearUndoState} from '../../src/reducers/undo'; +import {undoSnapshot, undo, redo, clearUndoState, MAX_STACK_SIZE} from '../../src/reducers/undo'; test('initialState', () => { let defaultState; @@ -139,3 +139,75 @@ test('undoSnapshotCantRedo', () => { expect(newReduxState.stack[0]).toEqual(reduxState.stack[0]); expect(newReduxState.stack[1]).toEqual(state3); }); + +test('snapshotAtMaxStackSize', () => { + let defaultState; + const getState = function (num) { + return {state: num}; + }; + // Push MAX_STACK_SIZE states + let num = 1; + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([getState(num)]) /* action */); + for (num = 2; num <= MAX_STACK_SIZE; num++) { + reduxState = undoReducer(reduxState /* state */, undoSnapshot([getState(num)]) /* action */); + } + + expect(reduxState.pointer).toEqual(MAX_STACK_SIZE - 1); + expect(reduxState.stack).toHaveLength(MAX_STACK_SIZE); + expect(reduxState.stack[0].state).toEqual(1); + + // Push one more + reduxState = undoReducer(reduxState /* state */, undoSnapshot([getState(num)]) /* action */); + + // Stack size stays the same + expect(reduxState.pointer).toEqual(MAX_STACK_SIZE - 1); + expect(reduxState.stack).toHaveLength(MAX_STACK_SIZE); + expect(reduxState.stack[0].state).toEqual(2); // State 1 was cut off + expect(reduxState.stack[MAX_STACK_SIZE - 1].state).toEqual(MAX_STACK_SIZE + 1); // Newest added state is at end +}); + +test('undoRedoAtMaxStackSize', () => { + let defaultState; + const getState = function (num) { + return {state: num}; + }; + // Push MAX_STACK_SIZE states + let num = 1; + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([getState(num)]) /* action */); + for (num = 2; num <= MAX_STACK_SIZE; num++) { + reduxState = undoReducer(reduxState /* state */, undoSnapshot([getState(num)]) /* action */); + } + + // Undo twice and redo + reduxState = undoReducer(reduxState /* state */, undo() /* action */); + reduxState = undoReducer(reduxState /* state */, undo() /* action */); + reduxState = undoReducer(reduxState /* state */, redo() /* action */); + + expect(reduxState.pointer).toEqual(MAX_STACK_SIZE - 2); + expect(reduxState.stack).toHaveLength(MAX_STACK_SIZE); + expect(reduxState.stack[0].state).toEqual(1); +}); + +test('undoSnapshotAtMaxStackSize', () => { + let defaultState; + const getState = function (num) { + return {state: num}; + }; + // Push MAX_STACK_SIZE states + let num = 1; + let reduxState = undoReducer(defaultState /* state */, undoSnapshot([getState(num)]) /* action */); + for (num = 2; num <= MAX_STACK_SIZE; num++) { + reduxState = undoReducer(reduxState /* state */, undoSnapshot([getState(num)]) /* action */); + } + + // Undo twice and then take a snapshot + reduxState = undoReducer(reduxState /* state */, undo() /* action */); + reduxState = undoReducer(reduxState /* state */, undo() /* action */); + reduxState = undoReducer(reduxState /* state */, undoSnapshot([getState(num)]) /* action */); + + expect(reduxState.pointer).toEqual(MAX_STACK_SIZE - 2); + expect(reduxState.stack).toHaveLength(MAX_STACK_SIZE - 1); + expect(reduxState.stack[0].state).toEqual(1); + expect(reduxState.stack[MAX_STACK_SIZE - 2].state).toEqual(MAX_STACK_SIZE + 1); // Newest added state is at end + expect(reduxState.stack[MAX_STACK_SIZE - 3].state).toEqual(MAX_STACK_SIZE - 2); // Old redo state is gone +});