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