add undo reducer

This commit is contained in:
DD 2017-10-04 17:18:00 -04:00
parent 582ab61665
commit 8baf731328
2 changed files with 212 additions and 0 deletions

79
src/reducers/undo.js Normal file
View file

@ -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
};

View file

@ -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);
});