mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 13:32:28 -05:00
add undo reducer
This commit is contained in:
parent
582ab61665
commit
8baf731328
2 changed files with 212 additions and 0 deletions
79
src/reducers/undo.js
Normal file
79
src/reducers/undo.js
Normal 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
|
||||
};
|
133
test/unit/undo-reducer.test.js
Normal file
133
test/unit/undo-reducer.test.js
Normal 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);
|
||||
});
|
Loading…
Reference in a new issue