Merge redux-devtools-instrument to packages

This commit is contained in:
Zalmoxisus 2018-12-02 23:49:12 +02:00
parent 0e0740e6d7
commit 6279ce8350
9 changed files with 1803 additions and 0 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules
.DS_Store
lib
coverage
.idea

View File

@ -0,0 +1,3 @@
{
"presets": ["es2015-loose", "stage-0"]
}

View File

@ -0,0 +1,4 @@
lib
**/node_modules
**/webpack.config.js
examples/**/server.js

View File

@ -0,0 +1,20 @@
{
"extends": "eslint-config-airbnb",
"env": {
"browser": true,
"mocha": true,
"node": true
},
"rules": {
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"no-console": 0,
// Temporarily disabled due to babel-eslint issues:
"block-scoped-var": 0,
"padded-blocks": 0,
},
"plugins": [
"react"
]
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Dan Abramov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,57 @@
Redux DevTools Instrumentation
==============================
Redux enhancer used along with [Redux DevTools](https://github.com/gaearon/redux-devtools) or [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools).
### Installation
```
npm install --save-dev redux-devtools-instrument
```
### Usage
Add the store enhancer:
##### `store/configureStore.js`
```js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import devTools from 'remote-redux-devtools';
import reducer from '../reducers';
// Usually you import the reducer from the monitor
// or apply with createDevTools as explained in Redux DevTools
const monitorReducer = (state = {}, action) => state;
export default function configureStore(initialState) {
const enhancer = compose(
applyMiddleware(...middlewares),
// other enhancers and applyMiddleware should be added before the instrumentation
instrument(monitorReducer, { maxAge: 50 })
);
// Note: passing enhancer as last argument requires redux@>=3.1.0
return createStore(reducer, initialState, enhancer);
}
```
### API
`instrument(monitorReducer, [options])`
- arguments
- **monitorReducer** *function* called whenever an action is dispatched ([see the example of a monitor reducer](https://github.com/gaearon/redux-devtools-log-monitor/blob/master/src/reducers.js#L13)).
- **options** *object*
- **maxAge** *number* or *function*(currentLiftedAction, previousLiftedState) - maximum allowed actions to be stored on the history tree, the oldest actions are removed once `maxAge` is reached. Can be generated dynamically with a function getting current action as argument.
- **shouldCatchErrors** *boolean* - if specified as `true`, whenever there's an exception in reducers, the monitors will show the error message, and next actions will not be dispatched.
- **shouldRecordChanges** *boolean* - if specified as `false`, it will not record the changes till `pauseRecording(false)` is dispatched. Default is `true`.
- **pauseActionType** *string* - if specified, whenever `pauseRecording(false)` lifted action is dispatched and there are actions in the history log, will add this action type. If not specified, will commit when paused.
- **shouldStartLocked** *boolean* - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`.
- **shouldHotReload** *boolean* - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`.
- **shouldIncludeCallstack** *boolean* - if set to `true`, will include callstack for every dispatched action. Default to `false`.
### License
MIT

View File

@ -0,0 +1,59 @@
{
"name": "redux-devtools-instrument",
"version": "1.9.3",
"description": "Redux DevTools instrumentation",
"main": "lib/instrument.js",
"scripts": {
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"lint": "eslint src test",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive",
"test:watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive --watch",
"test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha -- --recursive",
"prepublish": "npm run lint && npm run test && npm run clean && npm run build"
},
"files": [
"lib",
"src"
],
"repository": {
"type": "git",
"url": "https://github.com/zalmoxisus/redux-devtools-instrument.git"
},
"keywords": [
"redux",
"devtools",
"flux",
"hot reloading",
"time travel",
"live edit"
],
"author": "Dan Abramov <dan.abramov@me.com> (http://github.com/gaearon)",
"license": "MIT",
"bugs": {
"url": "https://github.com/zalmoxisus/redux-devtools-instrument/issues"
},
"homepage": "https://github.com/zalmoxisus/redux-devtools-instrument",
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-core": "^6.3.17",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.0",
"babel-preset-es2015-loose": "^6.1.3",
"babel-preset-stage-0": "^6.3.13",
"eslint": "^0.23",
"eslint-config-airbnb": "0.0.6",
"eslint-plugin-react": "^2.3.0",
"expect": "^1.6.0",
"isparta": "^3.0.3",
"mocha": "^2.2.5",
"redux": "^4.0.0",
"rimraf": "^2.3.4",
"rxjs": "^5.0.0-beta.6",
"webpack": "^1.11.0"
},
"dependencies": {
"lodash": "^4.2.0",
"symbol-observable": "^1.0.2"
}
}

View File

@ -0,0 +1,685 @@
import difference from 'lodash/difference';
import union from 'lodash/union';
import isPlainObject from 'lodash/isPlainObject';
import $$observable from 'symbol-observable';
export const ActionTypes = {
PERFORM_ACTION: 'PERFORM_ACTION',
RESET: 'RESET',
ROLLBACK: 'ROLLBACK',
COMMIT: 'COMMIT',
SWEEP: 'SWEEP',
TOGGLE_ACTION: 'TOGGLE_ACTION',
SET_ACTIONS_ACTIVE: 'SET_ACTIONS_ACTIVE',
JUMP_TO_STATE: 'JUMP_TO_STATE',
JUMP_TO_ACTION: 'JUMP_TO_ACTION',
REORDER_ACTION: 'REORDER_ACTION',
IMPORT_STATE: 'IMPORT_STATE',
LOCK_CHANGES: 'LOCK_CHANGES',
PAUSE_RECORDING: 'PAUSE_RECORDING'
};
/**
* Action creators to change the History state.
*/
export const ActionCreators = {
performAction(action, shouldIncludeCallstack) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
);
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
);
}
return {
type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now(),
stack: shouldIncludeCallstack ? Error().stack : undefined
};
},
reset() {
return { type: ActionTypes.RESET, timestamp: Date.now() };
},
rollback() {
return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
},
commit() {
return { type: ActionTypes.COMMIT, timestamp: Date.now() };
},
sweep() {
return { type: ActionTypes.SWEEP };
},
toggleAction(id) {
return { type: ActionTypes.TOGGLE_ACTION, id };
},
setActionsActive(start, end, active=true) {
return { type: ActionTypes.SET_ACTIONS_ACTIVE, start, end, active };
},
reorderAction(actionId, beforeActionId) {
return { type: ActionTypes.REORDER_ACTION, actionId, beforeActionId };
},
jumpToState(index) {
return { type: ActionTypes.JUMP_TO_STATE, index };
},
jumpToAction(actionId) {
return { type: ActionTypes.JUMP_TO_ACTION, actionId };
},
importState(nextLiftedState, noRecompute) {
return { type: ActionTypes.IMPORT_STATE, nextLiftedState, noRecompute };
},
lockChanges(status) {
return { type: ActionTypes.LOCK_CHANGES, status };
},
pauseRecording(status) {
return { type: ActionTypes.PAUSE_RECORDING, status };
}
};
export const INIT_ACTION = { type: '@@INIT' };
/**
* Computes the next entry with exceptions catching.
*/
function computeWithTryCatch(reducer, action, state) {
let nextState = state;
let nextError;
try {
nextState = reducer(state, action);
} catch (err) {
nextError = err.toString();
if (
typeof window === 'object' && (
typeof window.chrome !== 'undefined' ||
typeof window.process !== 'undefined' &&
window.process.type === 'renderer'
)) {
// In Chrome, rethrowing provides better source map support
setTimeout(() => { throw err; });
} else {
console.error(err);
}
}
return {
state: nextState,
error: nextError
};
}
/**
* Computes the next entry in the log by applying an action.
*/
function computeNextEntry(reducer, action, state, shouldCatchErrors) {
if (!shouldCatchErrors) {
return { state: reducer(state, action) };
}
return computeWithTryCatch(reducer, action, state);
}
/**
* Runs the reducer on invalidated actions to get a fresh computation log.
*/
function recomputeStates(
computedStates,
minInvalidatedStateIndex,
reducer,
committedState,
actionsById,
stagedActionIds,
skippedActionIds,
shouldCatchErrors
) {
// Optimization: exit early and return the same reference
// if we know nothing could have changed.
if (
!computedStates || minInvalidatedStateIndex === -1 ||
(minInvalidatedStateIndex >= computedStates.length &&
computedStates.length === stagedActionIds.length)
) {
return computedStates;
}
const nextComputedStates = computedStates.slice(0, minInvalidatedStateIndex);
for (let i = minInvalidatedStateIndex; i < stagedActionIds.length; i++) {
const actionId = stagedActionIds[i];
const action = actionsById[actionId].action;
const previousEntry = nextComputedStates[i - 1];
const previousState = previousEntry ? previousEntry.state : committedState;
const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
let entry;
if (shouldSkip) {
entry = previousEntry;
} else {
if (shouldCatchErrors && previousEntry && previousEntry.error) {
entry = {
state: previousState,
error: 'Interrupted by an error up the chain'
};
} else {
entry = computeNextEntry(reducer, action, previousState, shouldCatchErrors);
}
}
nextComputedStates.push(entry);
}
return nextComputedStates;
}
/**
* Lifts an app's action into an action on the lifted store.
*/
export function liftAction(action, shouldIncludeCallstack) {
return ActionCreators.performAction(action, shouldIncludeCallstack);
}
/**
* Creates a history state reducer from an app's reducer.
*/
export function liftReducerWith(reducer, initialCommittedState, monitorReducer, options) {
const initialLiftedState = {
monitorState: monitorReducer(undefined, {}),
nextActionId: 1,
actionsById: { 0: liftAction(INIT_ACTION) },
stagedActionIds: [0],
skippedActionIds: [],
committedState: initialCommittedState,
currentStateIndex: 0,
computedStates: [],
isLocked: options.shouldStartLocked === true,
isPaused: options.shouldRecordChanges === false
};
/**
* Manages how the history actions modify the history state.
*/
return (liftedState, liftedAction) => {
let {
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates,
isLocked,
isPaused
} = liftedState || initialLiftedState;
if (!liftedState) {
// Prevent mutating initialLiftedState
actionsById = { ...actionsById };
}
function commitExcessActions(n) {
// Auto-commits n-number of excess actions.
let excess = n;
let idsToDelete = stagedActionIds.slice(1, excess + 1);
for (let i = 0; i < idsToDelete.length; i++) {
if (computedStates[i + 1].error) {
// Stop if error is found. Commit actions up to error.
excess = i;
idsToDelete = stagedActionIds.slice(1, excess + 1);
break;
} else {
delete actionsById[idsToDelete[i]];
}
}
skippedActionIds = skippedActionIds.filter(id => idsToDelete.indexOf(id) === -1);
stagedActionIds = [0, ...stagedActionIds.slice(excess + 1)];
committedState = computedStates[excess].state;
computedStates = computedStates.slice(excess);
currentStateIndex = currentStateIndex > excess
? currentStateIndex - excess
: 0;
}
function computePausedAction(shouldInit) {
let computedState;
if (shouldInit) {
computedState = computedStates[currentStateIndex];
monitorState = monitorReducer(monitorState, liftedAction);
} else {
computedState = computeNextEntry(
reducer, liftedAction.action, computedStates[currentStateIndex].state, false
);
}
if (!options.pauseActionType || nextActionId === 1) {
return {
monitorState,
actionsById: { 0: liftAction(INIT_ACTION) },
nextActionId: 1,
stagedActionIds: [0],
skippedActionIds: [],
committedState: computedState.state,
currentStateIndex: 0,
computedStates: [computedState],
isLocked,
isPaused: true
};
}
if (shouldInit) {
if (currentStateIndex === stagedActionIds.length - 1) {
currentStateIndex++;
}
stagedActionIds = [...stagedActionIds, nextActionId];
nextActionId++;
}
return {
monitorState,
actionsById: {
...actionsById,
[nextActionId - 1]: liftAction({ type: options.pauseActionType })
},
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates: [...computedStates.slice(0, stagedActionIds.length - 1), computedState],
isLocked,
isPaused: true
};
}
// By default, agressively recompute every state whatever happens.
// This has O(n) performance, so we'll override this to a sensible
// value whenever we feel like we don't have to recompute the states.
let minInvalidatedStateIndex = 0;
// maxAge number can be changed dynamically
let maxAge = options.maxAge;
if (typeof maxAge === 'function') maxAge = maxAge(liftedAction, liftedState);
if (/^@@redux\/(INIT|REPLACE)/.test(liftedAction.type)) {
if (options.shouldHotReload === false) {
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = computedStates.length === 0 ? initialCommittedState :
computedStates[currentStateIndex].state;
currentStateIndex = 0;
computedStates = [];
}
// Recompute states on hot reload and init.
minInvalidatedStateIndex = 0;
if (maxAge && stagedActionIds.length > maxAge) {
// States must be recomputed before committing excess.
computedStates = recomputeStates(
computedStates,
minInvalidatedStateIndex,
reducer,
committedState,
actionsById,
stagedActionIds,
skippedActionIds,
options.shouldCatchErrors
);
commitExcessActions(stagedActionIds.length - maxAge);
// Avoid double computation.
minInvalidatedStateIndex = Infinity;
}
} else {
switch (liftedAction.type) {
case ActionTypes.PERFORM_ACTION: {
if (isLocked) return liftedState || initialLiftedState;
if (isPaused) return computePausedAction();
// Auto-commit as new actions come in.
if (maxAge && stagedActionIds.length >= maxAge) {
commitExcessActions(stagedActionIds.length - maxAge + 1);
}
if (currentStateIndex === stagedActionIds.length - 1) {
currentStateIndex++;
}
const actionId = nextActionId++;
// Mutation! This is the hottest path, and we optimize on purpose.
// It is safe because we set a new key in a cache dictionary.
actionsById[actionId] = liftedAction;
stagedActionIds = [...stagedActionIds, actionId];
// Optimization: we know that only the new action needs computing.
minInvalidatedStateIndex = stagedActionIds.length - 1;
break;
}
case ActionTypes.RESET: {
// Get back to the state the store was created with.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = initialCommittedState;
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.COMMIT: {
// Consider the last committed state the new starting point.
// Squash any staged actions into a single committed state.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = computedStates[currentStateIndex].state;
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.ROLLBACK: {
// Forget about any staged actions.
// Start again from the last committed state.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.TOGGLE_ACTION: {
// Toggle whether an action with given ID is skipped.
// Being skipped means it is a no-op during the computation.
const { id: actionId } = liftedAction;
const index = skippedActionIds.indexOf(actionId);
if (index === -1) {
skippedActionIds = [actionId, ...skippedActionIds];
} else {
skippedActionIds = skippedActionIds.filter(id => id !== actionId);
}
// Optimization: we know history before this action hasn't changed
minInvalidatedStateIndex = stagedActionIds.indexOf(actionId);
break;
}
case ActionTypes.SET_ACTIONS_ACTIVE: {
// Toggle whether an action with given ID is skipped.
// Being skipped means it is a no-op during the computation.
const { start, end, active } = liftedAction;
const actionIds = [];
for (let i = start; i < end; i++) actionIds.push(i);
if (active) {
skippedActionIds = difference(skippedActionIds, actionIds);
} else {
skippedActionIds = union(skippedActionIds, actionIds);
}
// Optimization: we know history before this action hasn't changed
minInvalidatedStateIndex = stagedActionIds.indexOf(start);
break;
}
case ActionTypes.JUMP_TO_STATE: {
// Without recomputing anything, move the pointer that tell us
// which state is considered the current one. Useful for sliders.
currentStateIndex = liftedAction.index;
// Optimization: we know the history has not changed.
minInvalidatedStateIndex = Infinity;
break;
}
case ActionTypes.JUMP_TO_ACTION: {
// Jumps to a corresponding state to a specific action.
// Useful when filtering actions.
const index = stagedActionIds.indexOf(liftedAction.actionId);
if (index !== -1) currentStateIndex = index;
minInvalidatedStateIndex = Infinity;
break;
}
case ActionTypes.SWEEP: {
// Forget any actions that are currently being skipped.
stagedActionIds = difference(stagedActionIds, skippedActionIds);
skippedActionIds = [];
currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
break;
}
case ActionTypes.REORDER_ACTION: {
// Recompute actions in a new order.
const actionId = liftedAction.actionId;
const idx = stagedActionIds.indexOf(actionId);
// do nothing in case the action is already removed or trying to move the first action
if (idx < 1) break;
const beforeActionId = liftedAction.beforeActionId;
let newIdx = stagedActionIds.indexOf(beforeActionId);
if (newIdx < 1) { // move to the beginning or to the end
const count = stagedActionIds.length;
newIdx = beforeActionId > stagedActionIds[count - 1] ? count : 1;
}
const diff = idx - newIdx;
if (diff > 0) { // move left
stagedActionIds = [
...stagedActionIds.slice(0, newIdx),
actionId,
...stagedActionIds.slice(newIdx, idx),
...stagedActionIds.slice(idx + 1)
];
minInvalidatedStateIndex = newIdx;
} else if (diff < 0) { // move right
stagedActionIds = [
...stagedActionIds.slice(0, idx),
...stagedActionIds.slice(idx + 1, newIdx),
actionId,
...stagedActionIds.slice(newIdx)
];
minInvalidatedStateIndex = idx;
}
break;
}
case ActionTypes.IMPORT_STATE: {
if (Array.isArray(liftedAction.nextLiftedState)) {
// recompute array of actions
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
currentStateIndex = liftedAction.nextLiftedState.length;
computedStates = [];
committedState = liftedAction.preloadedState;
minInvalidatedStateIndex = 0;
// iterate through actions
liftedAction.nextLiftedState.forEach(action => {
actionsById[nextActionId] = liftAction(action, options.shouldIncludeCallstack);
stagedActionIds.push(nextActionId);
nextActionId++;
});
} else {
// Completely replace everything.
({
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates
} = liftedAction.nextLiftedState);
if (liftedAction.noRecompute) {
minInvalidatedStateIndex = Infinity;
}
}
break;
}
case ActionTypes.LOCK_CHANGES: {
isLocked = liftedAction.status;
minInvalidatedStateIndex = Infinity;
break;
}
case ActionTypes.PAUSE_RECORDING: {
isPaused = liftedAction.status;
if (isPaused) {
return computePausedAction(true);
}
// Commit when unpausing
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = computedStates[currentStateIndex].state;
currentStateIndex = 0;
computedStates = [];
break;
}
default: {
// If the action is not recognized, it's a monitor action.
// Optimization: a monitor action can't change history.
minInvalidatedStateIndex = Infinity;
break;
}
}
}
computedStates = recomputeStates(
computedStates,
minInvalidatedStateIndex,
reducer,
committedState,
actionsById,
stagedActionIds,
skippedActionIds,
options.shouldCatchErrors
);
monitorState = monitorReducer(monitorState, liftedAction);
return {
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates,
isLocked,
isPaused
};
};
}
/**
* Provides an app's view into the state of the lifted store.
*/
export function unliftState(liftedState) {
const { computedStates, currentStateIndex } = liftedState;
const { state } = computedStates[currentStateIndex];
return state;
}
/**
* Provides an app's view into the lifted store.
*/
export function unliftStore(liftedStore, liftReducer, options) {
let lastDefinedState;
const { shouldIncludeCallstack } = options;
function getState() {
const state = unliftState(liftedStore.getState());
if (state !== undefined) {
lastDefinedState = state;
}
return lastDefinedState;
}
return {
...liftedStore,
liftedStore,
dispatch(action) {
liftedStore.dispatch(liftAction(action, shouldIncludeCallstack));
return action;
},
getState,
replaceReducer(nextReducer) {
liftedStore.replaceReducer(liftReducer(nextReducer));
},
[$$observable]() {
return {
...liftedStore[$$observable](),
subscribe(observer) {
if (typeof observer !== 'object') {
throw new TypeError('Expected the observer to be an object.');
}
function observeState() {
if (observer.next) {
observer.next(getState());
}
}
observeState();
const unsubscribe = liftedStore.subscribe(observeState);
return { unsubscribe };
}
};
}
};
}
/**
* Redux instrumentation store enhancer.
*/
export default function instrument(monitorReducer = () => null, options = {}) {
if (typeof options.maxAge === 'number' && options.maxAge < 2) {
throw new Error(
'DevTools.instrument({ maxAge }) option, if specified, ' +
'may not be less than 2.'
);
}
return createStore => (reducer, initialState, enhancer) => {
function liftReducer(r) {
if (typeof r !== 'function') {
if (r && typeof r.default === 'function') {
throw new Error(
'Expected the reducer to be a function. ' +
'Instead got an object with a "default" field. ' +
'Did you pass a module instead of the default export? ' +
'Try passing require(...).default instead.'
);
}
throw new Error('Expected the reducer to be a function.');
}
return liftReducerWith(r, initialState, monitorReducer, options);
}
const liftedStore = createStore(liftReducer(reducer), enhancer);
if (liftedStore.liftedStore) {
throw new Error(
'DevTools instrumentation should not be applied more than once. ' +
'Check your store configuration.'
);
}
return unliftStore(liftedStore, liftReducer, options);
};
}

View File

@ -0,0 +1,953 @@
import expect, { spyOn } from 'expect';
import { createStore, compose } from 'redux';
import instrument, { ActionCreators } from '../src/instrument';
import { Observable } from 'rxjs';
import _ from 'lodash';
import 'rxjs/add/observable/from';
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
default: return state;
}
}
function counterWithBug(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return mistake - 1; // eslint-disable-line no-undef
case 'SET_UNDEFINED': return undefined;
default: return state;
}
}
function counterWithAnotherBug(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return mistake + 1; // eslint-disable-line no-undef
case 'DECREMENT': return state - 1;
case 'SET_UNDEFINED': return undefined;
default: return state;
}
}
function doubleCounter(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 2;
case 'DECREMENT': return state - 2;
default: return state;
}
}
function counterWithMultiply(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
case 'MULTIPLY': return state * 2;
default: return state;
}
}
describe('instrument', () => {
let store;
let liftedStore;
beforeEach(() => {
store = createStore(counter, instrument());
liftedStore = store.liftedStore;
});
it('should perform actions', () => {
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
});
it('should provide observable', () => {
let lastValue;
let calls = 0;
Observable.from(store)
.subscribe(state => {
lastValue = state;
calls++;
});
expect(lastValue).toBe(0);
store.dispatch({ type: 'INCREMENT' });
expect(lastValue).toBe(1);
});
it('should rollback state to the last committed state', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(4);
liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2);
store.dispatch({ type: 'DECREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2);
});
it('should reset to initial state', () => {
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.reset());
expect(store.getState()).toBe(0);
});
it('should toggle an action', () => {
// actionId 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(1);
});
it('should set multiple action skip', () => {
// actionId 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(3);
liftedStore.dispatch(ActionCreators.setActionsActive(1, 3, false));
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.setActionsActive(0, 2, true));
expect(store.getState()).toBe(2);
liftedStore.dispatch(ActionCreators.setActionsActive(0, 1, true));
expect(store.getState()).toBe(2);
});
it('should sweep disabled actions', () => {
// actionId 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
expect(liftedStore.getState().stagedActionIds).toEqual([0, 1, 2, 3, 4]);
expect(liftedStore.getState().skippedActionIds).toEqual([]);
liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(3);
expect(liftedStore.getState().stagedActionIds).toEqual([0, 1, 2, 3, 4]);
expect(liftedStore.getState().skippedActionIds).toEqual([2]);
liftedStore.dispatch(ActionCreators.sweep());
expect(store.getState()).toBe(3);
expect(liftedStore.getState().stagedActionIds).toEqual([0, 1, 3, 4]);
expect(liftedStore.getState().skippedActionIds).toEqual([]);
});
it('should jump to state', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToState(0));
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.jumpToState(1));
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToState(2));
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.jumpToState(4));
expect(store.getState()).toBe(2);
});
it('should jump to action', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToAction(0));
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.jumpToAction(1));
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.jumpToAction(10));
expect(store.getState()).toBe(1);
});
it('should reorder actions', () => {
store = createStore(counterWithMultiply, instrument());
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'MULTIPLY' });
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 1, 2, 3, 4]);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.reorderAction(4, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 1, 2, 3]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(4, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 1, 2, 3]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(4, 2));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 1, 4, 2, 3]);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.reorderAction(1, 10));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 2, 3, 1]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(10, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 4, 2, 3, 1]);
expect(store.getState()).toBe(1);
store.liftedStore.dispatch(ActionCreators.reorderAction(1, -2));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 1, 4, 2, 3]);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.reorderAction(0, 1));
expect(store.liftedStore.getState().stagedActionIds).toEqual([0, 1, 4, 2, 3]);
expect(store.getState()).toBe(2);
});
it('should replace the reducer', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
store.replaceReducer(doubleCounter);
expect(store.getState()).toBe(2);
});
it('should replace the reducer without recomputing actions', () => {
store = createStore(counter, instrument(undefined, { shouldHotReload: false }));
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
store.replaceReducer(doubleCounter);
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(3);
store.replaceReducer(() => ({ test: true }));
expect(store.getState()).toEqual({ test: true });
});
it('should catch and record errors', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { shouldCatchErrors: true })
);
storeWithBug.dispatch({ type: 'INCREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
let { computedStates } = storeWithBug.liftedStore.getState();
expect(computedStates[2].error).toMatch(
/ReferenceError/
);
expect(computedStates[3].error).toMatch(
/Interrupted by an error up the chain/
);
expect(spy.calls[0].arguments[0].toString()).toMatch(
/ReferenceError/
);
spy.restore();
});
it('should catch invalid action type', () => {
expect(() => {
store.dispatch({ type: undefined });
}).toThrow(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
);
});
it('should catch invalid action type', () => {
function ActionClass() {
this.type = 'test';
}
expect(() => {
store.dispatch(new ActionClass());
}).toThrow(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
);
});
it('should return the last non-undefined state from getState', () => {
let storeWithBug = createStore(counterWithBug, instrument());
storeWithBug.dispatch({ type: 'INCREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
expect(storeWithBug.getState()).toBe(2);
storeWithBug.dispatch({ type: 'SET_UNDEFINED' });
expect(storeWithBug.getState()).toBe(2);
});
it('should not recompute states on every action', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
});
it('should not recompute old states when toggling an action', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
let monitoredLiftedStore = monitoredStore.liftedStore;
expect(reducerCalls).toBe(1);
// actionId 0 = @@INIT
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(5);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(6);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(8);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(1));
expect(reducerCalls).toBe(10);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(11);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(11);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(1));
expect(reducerCalls).toBe(12);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(3));
expect(reducerCalls).toBe(13);
monitoredLiftedStore.dispatch(ActionCreators.toggleAction(2));
expect(reducerCalls).toBe(15);
});
it('should not recompute states when jumping to state', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
let monitoredLiftedStore = monitoredStore.liftedStore;
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
let savedComputedStates = monitoredLiftedStore.getState().computedStates;
monitoredLiftedStore.dispatch(ActionCreators.jumpToState(0));
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.jumpToState(1));
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch(ActionCreators.jumpToState(3));
expect(reducerCalls).toBe(4);
expect(monitoredLiftedStore.getState().computedStates).toBe(savedComputedStates);
});
it('should not recompute states on monitor actions', () => {
let reducerCalls = 0;
let monitoredStore = createStore(() => reducerCalls++, instrument());
let monitoredLiftedStore = monitoredStore.liftedStore;
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
let savedComputedStates = monitoredLiftedStore.getState().computedStates;
monitoredLiftedStore.dispatch({ type: 'lol' });
expect(reducerCalls).toBe(4);
monitoredLiftedStore.dispatch({ type: 'wat' });
expect(reducerCalls).toBe(4);
expect(monitoredLiftedStore.getState().computedStates).toBe(savedComputedStates);
});
describe('maxAge option', () => {
let configuredStore;
let configuredLiftedStore;
beforeEach(() => {
configuredStore = createStore(counter, instrument(undefined, { maxAge: 3 }));
configuredLiftedStore = configuredStore.liftedStore;
});
it('should auto-commit earliest non-@@INIT action when maxAge is reached', () => {
configuredStore.dispatch({ type: 'INCREMENT' });
configuredStore.dispatch({ type: 'INCREMENT' });
let liftedStoreState = configuredLiftedStore.getState();
expect(configuredStore.getState()).toBe(2);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.committedState).toBe(undefined);
expect(liftedStoreState.stagedActionIds).toInclude(1);
// Trigger auto-commit.
configuredStore.dispatch({ type: 'INCREMENT' });
liftedStoreState = configuredLiftedStore.getState();
expect(configuredStore.getState()).toBe(3);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(1);
expect(liftedStoreState.committedState).toBe(1);
expect(liftedStoreState.currentStateIndex).toBe(2);
});
it('should remove skipped actions once committed', () => {
configuredStore.dispatch({ type: 'INCREMENT' });
configuredLiftedStore.dispatch(ActionCreators.toggleAction(1));
configuredStore.dispatch({ type: 'INCREMENT' });
expect(configuredLiftedStore.getState().skippedActionIds).toInclude(1);
configuredStore.dispatch({ type: 'INCREMENT' });
expect(configuredLiftedStore.getState().skippedActionIds).toExclude(1);
});
it('should not auto-commit errors', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3);
storeWithBug.dispatch({ type: 'INCREMENT' });
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(4);
spy.restore();
});
it('should auto-commit actions after hot reload fixes error', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(7);
// Auto-commit 2 actions by "fixing" reducer bug, but introducing another.
storeWithBug.replaceReducer(counterWithAnotherBug);
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(5);
// Auto-commit 2 more actions by "fixing" other reducer bug.
storeWithBug.replaceReducer(counter);
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3);
spy.restore();
});
it('should update currentStateIndex when auto-committing', () => {
let liftedStoreState;
let currentComputedState;
configuredStore.dispatch({ type: 'INCREMENT' });
configuredStore.dispatch({ type: 'INCREMENT' });
liftedStoreState = configuredLiftedStore.getState();
expect(liftedStoreState.currentStateIndex).toBe(2);
// currentStateIndex should stay at 2 as actions are committed.
configuredStore.dispatch({ type: 'INCREMENT' });
liftedStoreState = configuredLiftedStore.getState();
currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
expect(liftedStoreState.currentStateIndex).toBe(2);
expect(currentComputedState.state).toBe(3);
});
it('should continue to increment currentStateIndex while error blocks commit', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
let liftedStoreState = liftedStoreWithBug.getState();
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
expect(liftedStoreState.currentStateIndex).toBe(4);
expect(currentComputedState.state).toBe(0);
expect(currentComputedState.error).toExist();
spy.restore();
});
it('should adjust currentStateIndex correctly when multiple actions are committed', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
// Auto-commit 2 actions by "fixing" reducer bug.
storeWithBug.replaceReducer(counter);
let liftedStoreState = liftedStoreWithBug.getState();
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
expect(liftedStoreState.currentStateIndex).toBe(2);
expect(currentComputedState.state).toBe(-4);
spy.restore();
});
it('should not allow currentStateIndex to drop below 0', () => {
let spy = spyOn(console, 'error');
let storeWithBug = createStore(
counterWithBug,
instrument(undefined, { maxAge: 3, shouldCatchErrors: true })
);
let liftedStoreWithBug = storeWithBug.liftedStore;
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
liftedStoreWithBug.dispatch(ActionCreators.jumpToState(1));
// Auto-commit 2 actions by "fixing" reducer bug.
storeWithBug.replaceReducer(counter);
let liftedStoreState = liftedStoreWithBug.getState();
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
expect(liftedStoreState.currentStateIndex).toBe(0);
expect(currentComputedState.state).toBe(-2);
spy.restore();
});
it('should use dynamic maxAge', () => {
let max = 3;
const getMaxAge = expect.createSpy().andCall(() => max);
store = createStore(counter, instrument(undefined, { maxAge: getMaxAge }));
expect(getMaxAge.calls.length).toEqual(1);
store.dispatch({ type: 'INCREMENT' });
expect(getMaxAge.calls.length).toEqual(2);
store.dispatch({ type: 'INCREMENT' });
expect(getMaxAge.calls.length).toEqual(3);
let liftedStoreState = store.liftedStore.getState();
expect(getMaxAge.calls[0].arguments[0].type).toInclude('INIT');
expect(getMaxAge.calls[0].arguments[1]).toBe(undefined);
expect(getMaxAge.calls[1].arguments[0].type).toBe('PERFORM_ACTION');
expect(getMaxAge.calls[1].arguments[1].nextActionId).toBe(1);
expect(getMaxAge.calls[1].arguments[1].stagedActionIds).toEqual([0]);
expect(getMaxAge.calls[2].arguments[1].nextActionId).toBe(2);
expect(getMaxAge.calls[2].arguments[1].stagedActionIds).toEqual([0, 1]);
expect(store.getState()).toBe(2);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.committedState).toBe(undefined);
expect(liftedStoreState.stagedActionIds).toInclude(1);
// Trigger auto-commit.
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(3);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(1);
expect(liftedStoreState.committedState).toBe(1);
expect(liftedStoreState.currentStateIndex).toBe(2);
max = 4;
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(4);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(4);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(1);
expect(liftedStoreState.committedState).toBe(1);
expect(liftedStoreState.currentStateIndex).toBe(3);
max = 3;
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(5);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(3);
expect(liftedStoreState.committedState).toBe(3);
expect(liftedStoreState.currentStateIndex).toBe(2);
store.dispatch({ type: 'INCREMENT' });
liftedStoreState = store.liftedStore.getState();
expect(store.getState()).toBe(6);
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
expect(liftedStoreState.stagedActionIds).toExclude(1);
expect(liftedStoreState.computedStates[0].state).toBe(4);
expect(liftedStoreState.committedState).toBe(4);
expect(liftedStoreState.currentStateIndex).toBe(2);
});
it('should throw error when maxAge < 2', () => {
expect(() => {
createStore(counter, instrument(undefined, { maxAge: 1 }));
}).toThrow(/may not be less than 2/);
});
});
describe('Import State', () => {
let monitoredStore;
let monitoredLiftedStore;
let exportedState;
beforeEach(() => {
monitoredStore = createStore(counter, instrument());
monitoredLiftedStore = monitoredStore.liftedStore;
// Set up state to export
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
exportedState = monitoredLiftedStore.getState();
});
it('should replay all the steps when a state is imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredLiftedStore.dispatch(ActionCreators.importState(exportedState));
expect(importMonitoredLiftedStore.getState()).toEqual(exportedState);
});
it('should replace the existing action log with the one imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredLiftedStore.dispatch(ActionCreators.importState(exportedState));
expect(importMonitoredLiftedStore.getState()).toEqual(exportedState);
});
it('should allow for state to be imported without replaying actions', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
let noComputedExportedState = Object.assign({}, exportedState);
delete noComputedExportedState.computedStates;
importMonitoredLiftedStore.dispatch(ActionCreators.importState(noComputedExportedState, true));
let expectedImportedState = Object.assign({}, noComputedExportedState, {
computedStates: undefined
});
expect(importMonitoredLiftedStore.getState()).toEqual(expectedImportedState);
});
it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
const oldState = importMonitoredLiftedStore.getState();
expect(oldState.actionsById[0].stack).toBe(undefined);
expect(oldState.actionsById[1].stack).toBeA('string');
importMonitoredLiftedStore.dispatch(ActionCreators.importState(oldState));
expect(importMonitoredLiftedStore.getState()).toEqual(oldState);
expect(importMonitoredLiftedStore.getState().actionsById[0].stack).toBe(undefined);
expect(importMonitoredLiftedStore.getState().actionsById[1]).toEqual(oldState.actionsById[1]);
});
});
function filterStackAndTimestamps(state) {
state.actionsById = _.mapValues(state.actionsById, (action) => {
delete action.timestamp;
delete action.stack;
return action;
});
return state;
}
describe('Import Actions', () => {
let monitoredStore;
let monitoredLiftedStore;
let exportedState;
let savedActions = [
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
{ type: 'INCREMENT' }
];
beforeEach(() => {
monitoredStore = createStore(counter, instrument());
monitoredLiftedStore = monitoredStore.liftedStore;
// Pass actions through component
savedActions.forEach(action => monitoredStore.dispatch(action));
// get the final state
exportedState = filterStackAndTimestamps(monitoredLiftedStore.getState());
});
it('should replay all the steps when a state is imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredLiftedStore.dispatch(ActionCreators.importState(savedActions));
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});
it('should replace the existing action log with the one imported', () => {
let importMonitoredStore = createStore(counter, instrument());
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredLiftedStore.dispatch(ActionCreators.importState(savedActions));
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});
it('should include callstack', () => {
let importMonitoredStore = createStore(counter, instrument(undefined, { shouldIncludeCallstack: true }));
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredLiftedStore.dispatch(ActionCreators.importState(savedActions));
expect(importMonitoredLiftedStore.getState().actionsById[0].stack).toBe(undefined);
expect(importMonitoredLiftedStore.getState().actionsById[1].stack).toBeA('string');
expect(filterStackAndTimestamps(importMonitoredLiftedStore.getState())).toEqual(exportedState);
});
});
describe('Lock Changes', () => {
it('should lock', () => {
store.dispatch({ type: 'INCREMENT' });
store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: true });
expect(store.liftedStore.getState().isLocked).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(2);
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(2);
expect(store.getState()).toBe(1);
liftedStore.dispatch(ActionCreators.toggleAction(1));
expect(store.getState()).toBe(0);
liftedStore.dispatch(ActionCreators.toggleAction(1));
expect(store.getState()).toBe(1);
store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: false });
expect(store.liftedStore.getState().isLocked).toBe(false);
expect(store.liftedStore.getState().nextActionId).toBe(2);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.getState()).toBe(2);
});
it('should start locked', () => {
store = createStore(counter, instrument(undefined, { shouldStartLocked: true }));
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().isLocked).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.getState()).toBe(0);
const savedActions = [{ type: 'INCREMENT' }, { type: 'INCREMENT' }];
store.liftedStore.dispatch(ActionCreators.importState(savedActions));
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch({ type: 'LOCK_CHANGES', status: false });
expect(store.liftedStore.getState().isLocked).toBe(false);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(4);
expect(store.getState()).toBe(3);
});
});
describe('Pause recording', () => {
it('should pause', () => {
expect(store.liftedStore.getState().isPaused).toBe(false);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.getState()).toBe(2);
store.liftedStore.dispatch(ActionCreators.pauseRecording(true));
expect(store.liftedStore.getState().isPaused).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.liftedStore.getState().actionsById[0].action).toEqual({ type: '@@INIT' });
expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.liftedStore.getState().actionsById[0].action).toEqual({ type: '@@INIT' });
expect(store.getState()).toBe(4);
store.liftedStore.dispatch(ActionCreators.pauseRecording(false));
expect(store.liftedStore.getState().isPaused).toBe(false);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.liftedStore.getState().actionsById[2].action).toEqual({ type: 'INCREMENT' });
expect(store.getState()).toBe(6);
});
it('should maintain the history while paused', () => {
store = createStore(counter, instrument(undefined, { pauseActionType: '@@PAUSED' }));
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.liftedStore.getState().isPaused).toBe(false);
store.liftedStore.dispatch(ActionCreators.pauseRecording(true));
expect(store.liftedStore.getState().isPaused).toBe(true);
expect(store.liftedStore.getState().nextActionId).toBe(4);
expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(4);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(4);
expect(store.getState()).toBe(4);
store.liftedStore.dispatch(ActionCreators.pauseRecording(false));
expect(store.liftedStore.getState().isPaused).toBe(false);
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.getState()).toBe(4);
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(2);
expect(store.getState()).toBe(5);
store.liftedStore.dispatch(ActionCreators.commit());
store.liftedStore.dispatch(ActionCreators.pauseRecording(true));
store.dispatch({ type: 'INCREMENT' });
expect(store.liftedStore.getState().nextActionId).toBe(1);
expect(store.getState()).toBe(6);
});
});
it('throws if reducer is not a function', () => {
expect(() =>
createStore(undefined, instrument())
).toThrow('Expected the reducer to be a function.');
});
it('warns if the reducer is not a function but has a default field that is', () => {
expect(() =>
createStore(({ 'default': () => {} }), instrument())
).toThrow(
'Expected the reducer to be a function. ' +
'Instead got an object with a "default" field. ' +
'Did you pass a module instead of the default export? ' +
'Try passing require(...).default instead.'
);
});
it('throws if there are more than one instrument enhancer included', () => {
expect(() => {
createStore(counter, compose(instrument(), instrument()));
}).toThrow(
'DevTools instrumentation should not be applied more than once. ' +
'Check your store configuration.'
);
});
});