Merge pull request #132 from gaearon/next

New API
This commit is contained in:
Dan Abramov 2015-12-14 04:22:37 +00:00
commit cfdf756e12
92 changed files with 1280 additions and 2148 deletions

332
README.md
View File

@ -1,8 +1,3 @@
>### React 0.14 Support
>React 0.14 support is coming in the `next` branch.
>[Helps us test it](https://github.com/gaearon/redux-devtools/releases/tag/v3.0.0-beta-2) to get it sooner.
Redux DevTools
=========================
@ -14,7 +9,6 @@ A live-editing time travel environment for [Redux](https://github.com/rackt/redu
[![npm downloads](https://img.shields.io/npm/dm/redux-devtools.svg?style=flat-square)](https://www.npmjs.com/package/redux-devtools)
[![redux channel on discord](https://img.shields.io/badge/discord-redux@reactiflux-738bd7.svg?style=flat-square)](https://discord.gg/0ZcbPKXt5bWb10Ma)
![](http://i.imgur.com/J4GeW0M.gif)
### Features
@ -24,9 +18,10 @@ A live-editing time travel environment for [Redux](https://github.com/rackt/redu
* If you change the reducer code, each “staged” action will be re-evaluated
* If the reducers throw, you will see during which action this happened, and what the error was
* With `persistState()` store enhancer, you can persist debug sessions across page reloads
* To monitor a part of the state, you can set a `select` prop on the DevTools component: `<DevTools select={state => state.todos} store={store} monitor={LogMonitor} />`
* Toggle visibility with Ctrl+H
* To hide the devtools on load, set `visibleOnLoad` to false, e.g.: `<DevTools store={store} monitor={LogMonitor} visibleOnLoad={false} />`
### Overview
Redux DevTools is a development time package that provides power-ups for your Redux development workflow. Be careful to strip its code in production! To use Redux DevTools, you need to choose a “monitor”—a React component that will serve as a UI for the DevTools. Different tasks and workflows require different UIs, so Redux DevTools is built to be flexible in this regard. We recommend using [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) for inspecting the state and time travel, and wrap it in a [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) to quckly move it across the screen. That said, when youre comfortable rolling up your own setup, feel free to do this, and share it with us.
### Installation
@ -34,83 +29,322 @@ A live-editing time travel environment for [Redux](https://github.com/rackt/redu
npm install --save-dev redux-devtools
```
DevTools is a [store enhancer](http://rackt.github.io/redux/docs/Glossary.html#store-enhancer).
To install, firstly import `devTools` into your root React component:
Youll also likely want to install some monitors:
```js
// Redux utility functions
import { compose, createStore, applyMiddleware } from 'redux';
// Redux DevTools store enhancers
import { devTools, persistState } from 'redux-devtools';
// React components for Redux DevTools
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
```
npm install --save-dev redux-devtools-log-monitor
npm install --save-dev redux-devtools-dock-monitor
```
Then, add `devTools` to your store enhancers, and create your store:
### Usage
#### Create a `DevTools` Component
Somewhere in your project, create a `DevTools` component by passing a `monitor` element to `createDevTools`. In the following example our `monitor` consists of [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) docked within [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor):
##### `containers/DevTools.js`
```js
import React from 'react';
// Exported from redux-devtools
import { createDevTools } from 'redux-devtools';
// Monitors are separate packages, and you can make a custom one
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
// createDevTools takes a monitor and produces a DevTools component
const DevTools = createDevTools(
// Monitors are individually adjustable with props
// Consult their repositories to learn about those props
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'>
<LogMonitor theme='tomorrow' />
</DockMonitor>
);
export default DevTools;
```
Note that you can use `LogMonitor` directly without wrapping it in `DockMonitor` if youd like to display the DevTools UI somewhere right inside your application:
```js
// If you'd rather not use docking UI, use <LogMonitor> directly
const DevTools = createDevTools(
<LogMonitor theme='solarized' />
);
```
#### Use `DevTools.instrument()` Store Enhancer
The `DevTools` component you created with `createDevTools()` has a special static method called `instrument()`. It returns a [store enhancer](http://rackt.github.io/redux/docs/Glossary.html#store-enhancer) that you need to use in development.
A store enhancer is a function that takes `createStore` and returns an enhanced version of it that you will use instead. You probably already used another store enhancer—[`applyMiddleware()`](http://redux.js.org/docs/api/applyMiddleware.html). Unlike `applyMiddleware()`, you will need to be careful to only use `DevTools.instrument()` in development environment, and never in production.
The easiest way to apply several store enhancers in a row is to use the [`compose()`](http://redux.js.org/docs/api/compose.html) utility function that ships with Redux. It is the same `compose()` that you can find in Underscore and Lodash. In our case, we would use it to compose several store enhancers into one: `compose(applyMiddleware(m1, m2, m3), DevTools.instrument())`.
Its important that you should add `DevTools.instrument()` *after* `applyMiddleware` in your `compose()` function arguments. This is because `applyMiddleware` is potentially asynchronous, but `DevTools.instrument()` expects all actions to be plain objects rather than actions interpreted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk). So make sure `applyMiddleware()` goes first in the `compose()` call, and `DevTools.instrument()` goes after it.
If youd like, you may add another store enhancer called `persistState()`. It ships with this package, and it lets you serialize whole sessions (including all dispatched actions and the state of the monitors) by a URL key. So if you visit `http://localhost:3000/?debug_session=reproducing_weird_bug`, do something in the app, then open `http://localhost:3000/?debug_session=some_other_feature`, and then go back to `http://localhost:3000/?debug_session=reproducing_weird_bug`, the state will be restored. The implementation of `persistState()` is fairly naïve but you can take it as an inspiration and build a proper UI for it if you feel like it!
#### Exclude DevTools from Production Builds
Finally, to make sure were not pulling any DevTools-related code in the production builds, we will envify our code. With Webpack, you can use `DefinePlugin` (Browserify equivalent is called [`envify`](https://github.com/zertosh/loose-envify)) to turn magic constants like `process.env.NODE_ENV` into `'production'` or `'development'` strings depending on the environment, and import and render `redux-devtools` conditionally when `process.env.NODE_ENV` is not `'production'`. Then, if you have an Uglify step before production, Uglify will eliminate dead `if (false)` branches with `redux-devtools` imports.
If you are using ES6 modules with Webpack 1.x and Babel, you might try putting your `import` statement inside an `if (process.env.NODE_ENV !== 'production)` to exclude the DevTools package from your production bundle. However this ES6 specification forbids it, so this wont compile. Instead, you can use a conditional CommonJS `require`. Babel will let it compile, and Uglify will eliminate the dead branches before Webpack creates a bundle. This is why we recommend creating a `configureStore.js` file that either directs you to `configureStore.dev.js` or `configureStore.prod.js` depending on the configuration. While it is a little bit more maintenance, the upside is that you can be sure you wont pull any development dependencies into the production builds, and that you can easily enable different middleware (e.g. crash reporting, logging) in the production environment.
##### `store/configureStore.js`
```js
// Use ProvidePlugin (Webpack) or loose-envify (Browserify)
// together with Uglify to strip the dev branch in prod build.
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}
```
##### `store/configureStore.prod.js`
```js
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers';
const finalCreateStore = compose(
// Enables your middleware:
applyMiddleware(m1, m2, m3), // any Redux middleware, e.g. redux-thunk
// Provides support for DevTools:
devTools(),
// Lets you write ?debug_session=<name> in address bar to persist debug sessions
persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
// Middleware you want to use in production:
applyMiddleware(p1, p2, p3),
// Other store enhancers if you use any
)(createStore);
const store = finalCreateStore(reducer);
export default function configureStore(initialState) {
return finalCreateStore(rootReducer, initialState);
};
```
Finally, include the `DevTools` in your page. You may pass either `LogMonitor` (the default one) or any of the custom monitors described below. For convenience, you can use `DebugPanel` to dock `DevTools` to some part of the screen, but you can put it also somewhere else in the component tree.
##### `store/configureStore.dev.js`
```js
import { createStore, applyMiddleware, compose } from 'redux';
import { persistState } from 'redux-devtools';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
// Middleware you want to use in development:
applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument(),
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey())
)(createStore);
function getDebugSessionKey() {
// You can write custom logic here!
// By default we try to read the key from ?debug_session=<key> in the address bar
const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/);
return (matches && matches.length > 0)? matches[1] : null;
}
export default function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
// Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers')/*.default if you use Babel 6+ */)
);
}
return store;
}
```
#### Render `<DevTools>` in Your App...
Finally, include the `DevTools` component in your page.
A naïve way to do this would be to render it right in your `index.js`:
##### `index.js`
```js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import TodoApp from './containers/TodoApp';
// Don't do this! Youre bringing DevTools into the production bundle.
import DevTools from './containers/DevTools';
const store = configureStore();
render(
<Provider store={store}>
<div>
<TodoApp />
<DevTools />
</div>
</Provider>
document.getElementById('app')
);
```
We recommend a different approach. Create a `Root.js` component that renders the root of your application (usually some component surrounded by a `<Provider>`). Then use the same trick with conditional `require` statements to have two versions of it, one for development, and one for production:
##### `containers/Root.js`
```js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./Root.prod');
} else {
module.exports = require('./Root.dev');
}
```
##### `containers/Root.dev.js`
```js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
import DevTools from './DevTools';
export default class Root extends Component {
render() {
const { store } = this.props;
return (
<div>
<Provider store={store}>
{() => <CounterApp />}
</Provider>
<DebugPanel top right bottom>
<DevTools store={store} monitor={LogMonitor} />
</DebugPanel>
</div>
<Provider store={store}>
<div>
<TodoApp />
<DevTools />
</div>
</Provider>
);
}
}
```
##### `containers/Root.prod.js`
```js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
export default class Root extends Component {
render() {
const { store } = this.props;
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
}
```
#### ...Or Open Them in a New Window
When you use [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor), you usually want to render `<DevTools>` at the root of your app. It will appear in a docked container above it. However, you can also render it anywhere else in your React component tree. In this case, youd create a development and a production version of some other component that would either include or exclude `<DevTools>`.
For example (you dont have to do that!), you may prefer to display the DevTools in a separate window instead of rendering it inside the page. In this case, you can remove `DockMonitor` from `DevTools.js` and just use the `LogMonitor`, and have some code like this:
##### `index.js`
```js
import React from 'react';
import { Provider } from 'react-redux';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import App from './containers/App';
const store = configureStore();
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
if (process.env.NODE_ENV !== 'production') {
const showDevTools = require('./showDevTools');
showDevTools(store);
}
```
##### `showDevTools.js`
```js
import React from 'react';
import { render } from 'react-dom';
import DevTools from './containers/DevTools';
export default function showDevTools(store) {
const popup = window.open(null, 'Redux DevTools', 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no');
// Reload in case it already exists
popup.location.reload();
setTimeout(() => {
popup.document.write('<div id="react-devtools-root"></div>');
render(
<DevTools store={store} />,
popup.document.getElementById('react-devtools-root')
);
}, 10);
}
```
Personal preferences vary, and whether to put the DevTools in a separate window, in a dock, or right inside you apps user interface, is up to you. Make sure to check the documentation for the monitors you use and learn about the different props they support for customizing the appearance and the behavior.
Note that there are no useful props you can pass to the `DevTools` component other than the `store`. The `store` prop is needed if you dont wrap `<DevTools>` in a `<Provider>`—just like with any connected component. To adjust the monitors, you need to pass props to them inside `DevTools.js` itself inside the `createDevTools()` call when they are used.
### Gotchas
* **Your reducers have to be pure and free of side effects to work correctly with DevTools.** For example, even generating a random ID in reducer makes it impure and non-deterministic. Instead, do this in action creators.
* **Make sure to only apply `devTools()` in development!** In production, this will be terribly slow because actions just accumulate forever. (We'll need to implement a rolling window for dev too.) For example, in Webpack, you can use `DefinePlugin` to turn magic constants like `__DEV__` into `true` or `false` depending on the environment, and import and render `redux-devtools` conditionally behind `if (__DEV__)`. Then, if you have an Uglify step before production, Uglify will eliminate dead `if (false)` branches with `redux-devtools` imports. Here is [an example](https://github.com/erikras/react-redux-universal-hot-example/) that adds Redux DevTools handling the production case correctly.
* **Make sure to only apply `DevTools.instrument()` and render `<DevTools>` in development!** In production, this will be terribly slow because actions just accumulate forever. As described above, you need to use conditional `require`s and use `ProvidePlugin` (Webpack) or `loose-envify` (Browserify) together with Uglify to remove the dead code. Here is [an example](https://github.com/erikras/react-redux-universal-hot-example/) that adds Redux DevTools handling the production case correctly.
* **It is important that `devTools()` store enhancer should be added to your middleware stack *after* [`applyMiddleware`](http://rackt.github.io/redux/docs/api/applyMiddleware.html) in the `compose`d functions, as `applyMiddleware` is potentially asynchronous.** Otherwise, DevTools wont see the raw actions emitted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk).
* **It is important that `DevTools.instrument()` store enhancer should be added to your middleware stack *after* `applyMiddleware` in the `compose`d functions, as `applyMiddleware` is potentially asynchronous.** Otherwise, DevTools wont see the raw actions emitted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk).
### Running Examples
You can do this:
Clone the project:
```
git clone https://github.com/gaearon/redux-devtools.git
cd redux-devtools
npm install
```
cd examples/counter
Run `npm install` in the root folder:
```
npm install
```
Now you can open an example folder and run `npm install` there:
```
cd examples/counter # or examples/todomvc
npm install
```
Finally, run the development server and open the page:
```
npm start
open http://localhost:3000
```
Try clicking on actions in the log, or changing some code inside `examples/counter/reducers/counter`.
For fun, you can also open `http://localhost:3000/?debug_session=123`, click around, and then refresh.
Try clicking on actions in the log, or changing some code inside the reducers. You should see the action log re-evaluate the state on every code change.
Oh, and you can do the same with the TodoMVC example as well.
Also try opening `http://localhost:3000/?debug_session=123`, click around, and then refresh. You should see that all actions have been restored from the local storage.
### Custom Monitors
**You can build a completely custom UI for it** because `<DevTools>` accepts a `monitor` React component prop. The included `LogMonitor` is just an example.
**DevTools accepts monitor components so you can build a completely custom UI.** [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) and [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) are just examples of what is possible.
**[I challenge you to build a custom monitor for Redux DevTools!](https://github.com/gaearon/redux-devtools/issues/3)**
@ -119,22 +353,14 @@ Some crazy ideas for custom monitors:
* A slider that lets you jump between computed states just by dragging it
* An in-app layer that shows the last N states right in the app (e.g. for animation)
* A time machine like interface where the last N states of your app reside on different Z layers
* Feel free to come up with and implement your own! Check `LogMonitor` propTypes to see what you can do.
* Feel free to come up with and implement your own! Check [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) `propTypes` to see what you can do.
In fact some of these are implemented already:
#### [redux-devtools-diff-monitor](https://github.com/whetstone/redux-devtools-diff-monitor)
![](http://i.imgur.com/rvCR9OQ.png)
#### [redux-slider-monitor](https://github.com/calesce/redux-slider-monitor)
![](https://camo.githubusercontent.com/d61984306d27d5e0739efc2d57c56ba7aed7996c/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f662e636c2e6c792f6974656d732f3269314c3147316e3161316833793161324f31772f53637265656e2532305265636f7264696e67253230323031352d30382d3034253230617425323030372e3435253230504d2e676966)
#### [redux-devtools-gentest-plugin](https://github.com/lapanoid/redux-devtools-gentest-plugin)
![](https://camo.githubusercontent.com/71452cc55bc2ac2016dc05e4b6207c5777028a67/687474703a2f2f646c312e6a6f78692e6e65742f64726976652f303031302f333937372f3639323130352f3135303731362f643235343637613236362e706e67)
#### Keep them coming!
Create a PR to add your custom monitor.

View File

@ -1,14 +1,30 @@
# Redux DevTools Counter example
## Running example
## Running Example
First, clone the project:
```
git clone https://github.com/gaearon/redux-devtools.git
```
Then install the dependencies in the root folder:
```
cd redux-devtools
npm install
```
Install the dependencies in the example folder:
```
cd examples/counter
npm install
```
Finally, run the project:
```
npm start
open http://localhost:3000
```

View File

@ -1,40 +0,0 @@
import React, { Component } from 'react';
import CounterApp from './CounterApp';
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import { devTools, persistState } from 'redux-devtools';
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import * as reducers from '../reducers';
const finalCreateStore = compose(
applyMiddleware(thunk),
devTools(),
persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
)(createStore);
const reducer = combineReducers(reducers);
const store = finalCreateStore(reducer);
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(combineReducers(require('../reducers')))
);
}
export default class App extends Component {
render() {
return (
<div>
<Provider store={store}>
{() => <CounterApp />}
</Provider>
<DebugPanel top right bottom>
<DevTools store={store}
monitor={LogMonitor}
visibleOnLoad={true} />
</DebugPanel>
</div>
);
}
}

View File

@ -1,7 +0,0 @@
import React from 'react';
import App from './containers/App';
React.render(
<App />,
document.getElementById('root')
);

View File

@ -16,8 +16,9 @@
},
"homepage": "https://github.com/gaearon/redux-devtools#readme",
"dependencies": {
"react": "^0.13.3",
"react-redux": "^3.0.0",
"react": "^0.14.0",
"react-dom": "^0.14.0",
"react-redux": "^4.0.0",
"redux": "^3.0.0",
"redux-thunk": "^1.0.0"
},
@ -26,6 +27,9 @@
"babel-loader": "^5.1.4",
"node-libs-browser": "^0.5.2",
"react-hot-loader": "^1.3.0",
"redux-devtools": "^3.0.0",
"redux-devtools-log-monitor": "^1.0.1",
"redux-devtools-dock-monitor": "^1.0.1",
"webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0"
}

View File

@ -1 +0,0 @@
export { default as counter } from './counter';

View File

@ -0,0 +1,11 @@
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
export default createDevTools(
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'>
<LogMonitor />
</DockMonitor>
);

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import CounterApp from './CounterApp';
import DevTools from './DevTools';
export default class Root extends Component {
render() {
const { store } = this.props;
return (
<Provider store={store}>
<div>
<CounterApp />
<DevTools />
</div>
</Provider>
);
}
}

View File

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./Root.prod');
} else {
module.exports = require('./Root.dev');
}

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import CounterApp from './CounterApp';
export default class Root extends Component {
render() {
const { store } = this.props;
return (
<Provider store={store}>
<CounterApp />
</Provider>
);
}
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import Root from './containers/Root';
const store = configureStore();
render(
<Root store={store} />,
document.getElementById('root')
);

View File

@ -0,0 +1,8 @@
import { combineReducers } from 'redux';
import counter from './counter';
const rootReducer = combineReducers({
counter
});
export default rootReducer;

View File

@ -0,0 +1,27 @@
import { createStore, applyMiddleware, compose } from 'redux';
import { persistState } from 'redux-devtools';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
applyMiddleware(thunk),
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
)(createStore);
export default function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers'))
);
}
return store;
}

View File

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}

View File

@ -0,0 +1,11 @@
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const finalCreateStore = compose(
applyMiddleware(thunk)
)(createStore);
export default function configureStore(initialState) {
return finalCreateStore(rootReducer, initialState);
}

View File

@ -6,7 +6,7 @@ module.exports = {
entry: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./index'
'./src/index'
],
output: {
path: path.join(__dirname, 'dist'),
@ -18,11 +18,9 @@ module.exports = {
],
resolve: {
alias: {
'redux-devtools/lib': path.join(__dirname, '..', '..', 'src'),
'redux-devtools': path.join(__dirname, '..', '..', 'src'),
'react': path.join(__dirname, 'node_modules', 'react')
},
extensions: ['', '.js']
}
},
resolveLoader: {
'fallback': path.join(__dirname, 'node_modules')
@ -32,7 +30,7 @@ module.exports = {
test: /\.js$/,
loaders: ['react-hot', 'babel'],
exclude: /node_modules/,
include: __dirname
include: path.join(__dirname, 'src')
}, {
test: /\.js$/,
loaders: ['react-hot', 'babel'],

View File

@ -1,7 +1,30 @@
# Redux DevTools TodoMVC example
# Redux DevTools Counter example
## Getting Started
## Running Example
1. Install dependencies in the root folder: `cd ../..` and `npm i`
2. Install dependencies in the example folder: `cd examples/todomvc` and `npm i`
3. Start the development server: `npm start`
First, clone the project:
```
git clone https://github.com/gaearon/redux-devtools.git
```
Then install the dependencies in the root folder:
```
cd redux-devtools
npm install
```
Install the dependencies in the example folder:
```
cd examples/todomvc
npm install
```
Finally, run the project:
```
npm start
open http://localhost:3000
```

View File

@ -1,38 +0,0 @@
import React, { Component } from 'react';
import TodoApp from './TodoApp';
import { createStore, combineReducers, compose } from 'redux';
import { devTools, persistState } from 'redux-devtools';
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
import { Provider } from 'react-redux';
import * as reducers from '../reducers';
const finalCreateStore = compose(
devTools(),
persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
)(createStore);
const reducer = combineReducers(reducers);
const store = finalCreateStore(reducer);
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(combineReducers(require('../reducers')))
);
}
export default class App extends Component {
render() {
return (
<div>
<Provider store={store}>
{() => <TodoApp /> }
</Provider>
<DebugPanel top right bottom>
<DevTools store={store}
monitor={LogMonitor}
visibleOnLoad={true} />
</DebugPanel>
</div>
);
}
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
export default createDevTools(
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'>
<LogMonitor />
</DockMonitor>
);

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
import DevTools from './DevTools';
export default class Root extends Component {
render() {
const { store } = this.props;
return (
<Provider store={store}>
<div>
<TodoApp />
<DevTools />
</div>
</Provider>
);
}
}

View File

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./Root.prod');
} else {
module.exports = require('./Root.dev');
}

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
export default class Root extends Component {
render() {
const { store } = this.props;
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
}

View File

@ -1,8 +1,12 @@
import React from 'react';
import App from './containers/App';
import 'todomvc-app-css/index.css';
import React from 'react';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import Root from './containers/Root';
React.render(
<App />,
const store = configureStore();
render(
<Root store={store} />,
document.getElementById('root')
);

View File

@ -29,8 +29,9 @@
"homepage": "https://github.com/gaearon/redux-devtools#readme",
"dependencies": {
"classnames": "^2.1.2",
"react": "^0.13.3",
"react-redux": "^3.0.0",
"react": "^0.14.0",
"react-dom": "^0.14.0",
"react-redux": "^4.0.0",
"redux": "^3.0.0"
},
"devDependencies": {
@ -39,6 +40,9 @@
"node-libs-browser": "^0.5.2",
"raw-loader": "^0.5.1",
"react-hot-loader": "^1.3.0",
"redux-devtools": "^3.0.0",
"redux-devtools-log-monitor": "^1.0.1",
"redux-devtools-dock-monitor": "^1.0.1",
"style-loader": "^0.12.3",
"todomvc-app-css": "^2.0.1",
"webpack": "^1.9.11",

View File

@ -1 +1,8 @@
export { default as todos } from './todos';
import { combineReducers } from 'redux';
import todos from './todos';
const rootReducer = combineReducers({
todos
});
export default rootReducer;

View File

@ -0,0 +1,25 @@
import { createStore, compose } from 'redux';
import { persistState } from 'redux-devtools';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
)(createStore);
export default function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers'))
);
}
return store;
}

View File

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}

View File

@ -0,0 +1,6 @@
import { createStore } from 'redux';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(rootReducer, initialState);
}

View File

@ -1,6 +1,6 @@
{
"name": "redux-devtools",
"version": "2.1.5",
"version": "3.0.0",
"description": "Redux DevTools with hot reloading and time travel",
"main": "lib/index.js",
"scripts": {
@ -43,19 +43,19 @@
"jsdom": "^6.5.1",
"mocha": "^2.2.5",
"mocha-jsdom": "^1.0.0",
"react": "^0.14.0-rc1",
"react-addons-test-utils": "^0.14.0-rc1",
"react-dom": "^0.14.0-rc1",
"react": "^0.14.0",
"react-addons-test-utils": "^0.14.0",
"react-dom": "^0.14.0",
"rimraf": "^2.3.4",
"webpack": "^1.11.0"
},
"peerDependencies": {
"redux": "^2.0.0 || ^3.0.0"
"redux": "^3.0.0",
"react": "^0.14.0"
},
"dependencies": {
"react-json-tree": "^0.1.9",
"react-mixin": "^1.7.0",
"react-redux": "^3.0.0",
"redux": "^2.0.0 || ^3.0.0"
"lodash": "^3.10.1",
"react-redux": "^4.0.0",
"redux": "^3.0.0"
}
}

View File

@ -1,46 +1,42 @@
import createAll from 'react-redux/lib/components/createAll';
import { ActionCreators } from './devTools';
import React, { Children, Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import instrument from './instrument';
export default function createDevTools(React) {
const { PropTypes, Component } = React;
const { connect } = createAll(React);
export default function createDevTools(children) {
const monitorElement = Children.only(children);
const monitorProps = monitorElement.props;
const Monitor = monitorElement.type;
const ConnectedMonitor = connect(state => state)(Monitor);
const enhancer = instrument((state, action) =>
Monitor.update(monitorProps, state, action)
);
@connect(
state => state,
ActionCreators
)
class DevTools extends Component {
render() {
const { monitor: Monitor } = this.props;
return <Monitor {...this.props} />;
}
}
return class DevToolsWrapper extends Component {
static propTypes = {
monitor: PropTypes.func.isRequired,
store: PropTypes.shape({
devToolsStore: PropTypes.shape({
dispatch: PropTypes.func.isRequired
}).isRequired
}).isRequired
return class DevTools extends Component {
static contextTypes = {
store: PropTypes.object
};
static propTypes = {
store: PropTypes.object
};
static instrument = () => enhancer;
constructor(props, context) {
if (props.store && !props.store.devToolsStore) {
console.error(
'Could not find the devTools store inside your store. ' +
'Have you applied devTools() store enhancer?'
);
}
super(props, context);
if (context.store) {
this.liftedStore = context.store.liftedStore;
} else {
this.liftedStore = props.store.liftedStore;
}
}
render() {
return (
<DevTools {...this.props}
store={this.props.store.devToolsStore} />
<ConnectedMonitor {...monitorProps}
store={this.liftedStore} />
);
}
};
}

View File

@ -1,292 +0,0 @@
const ActionTypes = {
PERFORM_ACTION: 'PERFORM_ACTION',
RESET: 'RESET',
ROLLBACK: 'ROLLBACK',
COMMIT: 'COMMIT',
SWEEP: 'SWEEP',
TOGGLE_ACTION: 'TOGGLE_ACTION',
JUMP_TO_STATE: 'JUMP_TO_STATE',
SET_MONITOR_STATE: 'SET_MONITOR_STATE',
RECOMPUTE_STATES: 'RECOMPUTE_STATES'
};
const INIT_ACTION = {
type: '@@INIT'
};
function toggle(obj, key) {
const clone = { ...obj };
if (clone[key]) {
delete clone[key];
} else {
clone[key] = true;
}
return clone;
}
/**
* Computes the next entry in the log by applying an action.
*/
function computeNextEntry(reducer, action, state, error) {
if (error) {
return {
state,
error: 'Interrupted by an error up the chain'
};
}
let nextState = state;
let nextError;
try {
nextState = reducer(state, action);
} catch (err) {
nextError = err.toString();
console.error(err.stack || err);
}
return {
state: nextState,
error: nextError
};
}
/**
* Runs the reducer on all actions to get a fresh computation log.
* It's probably a good idea to do this only if the code has changed,
* but until we have some tests we'll just do it every time an action fires.
*/
function recomputeStates(reducer, committedState, stagedActions, skippedActions) {
const computedStates = [];
for (let i = 0; i < stagedActions.length; i++) {
const action = stagedActions[i];
const previousEntry = computedStates[i - 1];
const previousState = previousEntry ? previousEntry.state : committedState;
const previousError = previousEntry ? previousEntry.error : undefined;
const shouldSkip = Boolean(skippedActions[i]);
const entry = shouldSkip ?
previousEntry :
computeNextEntry(reducer, action, previousState, previousError);
computedStates.push(entry);
}
return computedStates;
}
/**
* Lifts the app state reducer into a DevTools state reducer.
*/
function liftReducer(reducer, initialState) {
const initialLiftedState = {
committedState: initialState,
stagedActions: [INIT_ACTION],
skippedActions: {},
currentStateIndex: 0,
monitorState: {
isVisible: true
},
timestamps: [Date.now()]
};
/**
* Manages how the DevTools actions modify the DevTools state.
*/
return function liftedReducer(liftedState = initialLiftedState, liftedAction) {
let shouldRecomputeStates = true;
let {
committedState,
stagedActions,
skippedActions,
computedStates,
currentStateIndex,
monitorState,
timestamps
} = liftedState;
switch (liftedAction.type) {
case ActionTypes.RESET:
committedState = initialState;
stagedActions = [INIT_ACTION];
skippedActions = {};
currentStateIndex = 0;
timestamps = [liftedAction.timestamp];
break;
case ActionTypes.COMMIT:
committedState = computedStates[currentStateIndex].state;
stagedActions = [INIT_ACTION];
skippedActions = {};
currentStateIndex = 0;
timestamps = [liftedAction.timestamp];
break;
case ActionTypes.ROLLBACK:
stagedActions = [INIT_ACTION];
skippedActions = {};
currentStateIndex = 0;
timestamps = [liftedAction.timestamp];
break;
case ActionTypes.TOGGLE_ACTION:
skippedActions = toggle(skippedActions, liftedAction.index);
break;
case ActionTypes.JUMP_TO_STATE:
currentStateIndex = liftedAction.index;
// Optimization: we know the history has not changed.
shouldRecomputeStates = false;
break;
case ActionTypes.SWEEP:
stagedActions = stagedActions.filter((_, i) => !skippedActions[i]);
timestamps = timestamps.filter((_, i) => !skippedActions[i]);
skippedActions = {};
currentStateIndex = Math.min(currentStateIndex, stagedActions.length - 1);
break;
case ActionTypes.PERFORM_ACTION:
if (currentStateIndex === stagedActions.length - 1) {
currentStateIndex++;
}
stagedActions = [...stagedActions, liftedAction.action];
timestamps = [...timestamps, liftedAction.timestamp];
// Optimization: we know that the past has not changed.
shouldRecomputeStates = false;
// Instead of recomputing the states, append the next one.
const previousEntry = computedStates[computedStates.length - 1];
const nextEntry = computeNextEntry(
reducer,
liftedAction.action,
previousEntry.state,
previousEntry.error
);
computedStates = [...computedStates, nextEntry];
break;
case ActionTypes.SET_MONITOR_STATE:
monitorState = liftedAction.monitorState;
break;
case ActionTypes.RECOMPUTE_STATES:
stagedActions = liftedAction.stagedActions;
timestamps = liftedAction.timestamps;
committedState = liftedAction.committedState;
currentStateIndex = stagedActions.length - 1;
skippedActions = {};
break;
default:
break;
}
if (shouldRecomputeStates) {
computedStates = recomputeStates(
reducer,
committedState,
stagedActions,
skippedActions
);
}
return {
committedState,
stagedActions,
skippedActions,
computedStates,
currentStateIndex,
monitorState,
timestamps
};
};
}
/**
* Lifts an app action to a DevTools action.
*/
function liftAction(action) {
const liftedAction = {
type: ActionTypes.PERFORM_ACTION,
action,
timestamp: Date.now()
};
return liftedAction;
}
/**
* Unlifts the DevTools state to the app state.
*/
function unliftState(liftedState) {
const { computedStates, currentStateIndex } = liftedState;
const { state } = computedStates[currentStateIndex];
return state;
}
/**
* Unlifts the DevTools store to act like the app's store.
*/
function unliftStore(liftedStore, reducer) {
let lastDefinedState;
return {
...liftedStore,
devToolsStore: liftedStore,
dispatch(action) {
liftedStore.dispatch(liftAction(action));
return action;
},
getState() {
const state = unliftState(liftedStore.getState());
if (state !== undefined) {
lastDefinedState = state;
}
return lastDefinedState;
},
getReducer() {
return reducer;
},
replaceReducer(nextReducer) {
liftedStore.replaceReducer(liftReducer(nextReducer));
}
};
}
/**
* Action creators to change the DevTools state.
*/
export const ActionCreators = {
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(index) {
return { type: ActionTypes.TOGGLE_ACTION, index };
},
jumpToState(index) {
return { type: ActionTypes.JUMP_TO_STATE, index };
},
setMonitorState(monitorState) {
return { type: ActionTypes.SET_MONITOR_STATE, monitorState };
},
recomputeStates(committedState, stagedActions) {
return {
type: ActionTypes.RECOMPUTE_STATES,
committedState,
stagedActions
};
}
};
/**
* Redux DevTools middleware.
*/
export default function devTools() {
return next => (reducer, initialState) => {
const liftedReducer = liftReducer(reducer, initialState);
const liftedStore = next(liftedReducer);
const store = unliftStore(liftedStore, reducer);
return store;
};
}

View File

@ -1,2 +1,3 @@
export { default as devTools } from './devTools';
export { default as instrument, ActionCreators, ActionTypes } from './instrument';
export { default as persistState } from './persistState';
export { default as createDevTools } from './createDevTools';

340
src/instrument.js Normal file
View File

@ -0,0 +1,340 @@
import difference from 'lodash/array/difference';
export const ActionTypes = {
PERFORM_ACTION: 'PERFORM_ACTION',
RESET: 'RESET',
ROLLBACK: 'ROLLBACK',
COMMIT: 'COMMIT',
SWEEP: 'SWEEP',
TOGGLE_ACTION: 'TOGGLE_ACTION',
JUMP_TO_STATE: 'JUMP_TO_STATE',
IMPORT_STATE: 'IMPORT_STATE'
};
/**
* Action creators to change the History state.
*/
export const ActionCreators = {
performAction(action) {
return { type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now() };
},
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 };
},
jumpToState(index) {
return { type: ActionTypes.JUMP_TO_STATE, index };
},
importState(nextLiftedState) {
return { type: ActionTypes.IMPORT_STATE, nextLiftedState };
}
};
const INIT_ACTION = { type: '@@INIT' };
/**
* Computes the next entry in the log by applying an action.
*/
function computeNextEntry(reducer, action, state, error) {
if (error) {
return {
state,
error: 'Interrupted by an error up the chain'
};
}
let nextState = state;
let nextError;
try {
nextState = reducer(state, action);
} catch (err) {
nextError = err.toString();
console.error(err.stack || err);
}
return {
state: nextState,
error: nextError
};
}
/**
* Runs the reducer on invalidated actions to get a fresh computation log.
*/
function recomputeStates(
computedStates,
minInvalidatedStateIndex,
reducer,
committedState,
actionsById,
stagedActionIds,
skippedActionIds
) {
// Optimization: exit early and return the same reference
// if we know nothing could have changed.
if (
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 previousError = previousEntry ? previousEntry.error : undefined;
const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
const entry = shouldSkip ?
previousEntry :
computeNextEntry(reducer, action, previousState, previousError);
nextComputedStates.push(entry);
}
return nextComputedStates;
}
/**
* Lifts an app's action into an action on the lifted store.
*/
function liftAction(action) {
return ActionCreators.performAction(action);
}
/**
* Creates a history state reducer from an app's reducer.
*/
function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
const initialLiftedState = {
monitorState: monitorReducer(undefined, {}),
nextActionId: 1,
actionsById: { 0: liftAction(INIT_ACTION) },
stagedActionIds: [0],
skippedActionIds: [],
committedState: initialCommittedState,
currentStateIndex: 0,
computedStates: []
};
/**
* Manages how the history actions modify the history state.
*/
return (liftedState = initialLiftedState, liftedAction) => {
let {
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates
} = liftedState;
// 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;
switch (liftedAction.type) {
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.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.SWEEP: {
// Forget any actions that are currently being skipped.
stagedActionIds = difference(stagedActionIds, skippedActionIds);
skippedActionIds = [];
currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
break;
}
case ActionTypes.PERFORM_ACTION: {
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.IMPORT_STATE: {
// Completely replace everything.
({
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates
} = liftedAction.nextLiftedState);
break;
}
case '@@redux/INIT': {
// Always recompute states on hot reload and init.
minInvalidatedStateIndex = 0;
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
);
monitorState = monitorReducer(monitorState, liftedAction);
return {
monitorState,
actionsById,
nextActionId,
stagedActionIds,
skippedActionIds,
committedState,
currentStateIndex,
computedStates
};
};
}
/**
* Provides an app's view into the state of the lifted store.
*/
function unliftState(liftedState) {
const { computedStates, currentStateIndex } = liftedState;
const { state } = computedStates[currentStateIndex];
return state;
}
/**
* Provides an app's view into the lifted store.
*/
function unliftStore(liftedStore, liftReducer) {
let lastDefinedState;
return {
...liftedStore,
liftedStore,
dispatch(action) {
liftedStore.dispatch(liftAction(action));
return action;
},
getState() {
const state = unliftState(liftedStore.getState());
if (state !== undefined) {
lastDefinedState = state;
}
return lastDefinedState;
},
replaceReducer(nextReducer) {
liftedStore.replaceReducer(liftReducer(nextReducer));
}
};
}
/**
* Redux instrumentation store enhancer.
*/
export default function instrument(monitorReducer = () => null) {
return createStore => (reducer, initialState) => {
function liftReducer(r) {
return liftReducerWith(r, initialState, monitorReducer);
}
const liftedStore = createStore(liftReducer(reducer));
return unliftStore(liftedStore, liftReducer);
};
}

View File

@ -1,51 +1,36 @@
export default function persistState(sessionId, stateDeserializer = null, actionDeserializer = null) {
import mapValues from 'lodash/object/mapValues';
import identity from 'lodash/utility/identity';
export default function persistState(sessionId, deserializeState = identity, deserializeAction = identity) {
if (!sessionId) {
return next => (...args) => next(...args);
}
function deserializeState(fullState) {
function deserialize(state) {
return {
...fullState,
committedState: stateDeserializer(fullState.committedState),
computedStates: fullState.computedStates.map((computedState) => {
return {
...computedState,
state: stateDeserializer(computedState.state)
};
})
...state,
actionsById: mapValues(state.actionsById, liftedAction => ({
...liftedAction,
action: deserializeAction(liftedAction.action)
})),
committedState: deserializeState(state.committedState),
computedStates: state.computedStates.map(computedState => ({
...computedState,
state: deserializeState(computedState.state)
}))
};
}
function deserializeActions(fullState) {
return {
...fullState,
stagedActions: fullState.stagedActions.map((action) => {
return actionDeserializer(action);
})
};
}
function deserialize(fullState) {
if (!fullState) {
return fullState;
}
let deserializedState = fullState;
if (typeof stateDeserializer === 'function') {
deserializedState = deserializeState(deserializedState);
}
if (typeof actionDeserializer === 'function') {
deserializedState = deserializeActions(deserializedState);
}
return deserializedState;
}
return next => (reducer, initialState) => {
const key = `redux-dev-session-${sessionId}`;
let finalInitialState;
try {
finalInitialState = deserialize(JSON.parse(localStorage.getItem(key))) || initialState;
next(reducer, initialState);
const json = localStorage.getItem(key);
if (json) {
finalInitialState = deserialize(JSON.parse(json)) || initialState;
next(reducer, initialState);
}
} catch (e) {
console.warn('Could not read debug session from localStorage:', e);
try {

View File

@ -1,51 +0,0 @@
import React, { PropTypes, Component } from 'react';
export function getDefaultStyle(props) {
let { left, right, bottom, top } = props;
if (typeof left === 'undefined' && typeof right === 'undefined') {
right = true;
}
if (typeof top === 'undefined' && typeof bottom === 'undefined') {
bottom = true;
}
return {
position: 'fixed',
zIndex: 10000,
fontSize: 17,
overflow: 'hidden',
opacity: 1,
color: 'white',
left: left ? 0 : undefined,
right: right ? 0 : undefined,
top: top ? 0 : undefined,
bottom: bottom ? 0 : undefined,
maxHeight: (bottom && top) ? '100%' : '30%',
maxWidth: (left && right) ? '100%' : '30%',
wordWrap: 'break-word',
boxSizing: 'border-box',
boxShadow: '-2px 0 7px 0 rgba(0, 0, 0, 0.5)'
};
}
export default class DebugPanel extends Component {
static propTypes = {
left: PropTypes.bool,
right: PropTypes.bool,
bottom: PropTypes.bool,
top: PropTypes.bool,
getStyle: PropTypes.func.isRequired
};
static defaultProps = {
getStyle: getDefaultStyle
};
render() {
return (
<div style={{...this.props.getStyle(this.props), ...this.props.style}}>
{this.props.children}
</div>
);
}
}

View File

@ -1,194 +0,0 @@
import React, { PropTypes, findDOMNode, Component } from 'react';
import LogMonitorEntry from './LogMonitorEntry';
import LogMonitorButton from './LogMonitorButton';
import * as themes from './themes';
const styles = {
container: {
fontFamily: 'monaco, Consolas, Lucida Console, monospace',
position: 'relative',
overflowY: 'hidden',
width: '100%',
height: '100%',
minWidth: 300
},
buttonBar: {
textAlign: 'center',
borderBottomWidth: 1,
borderBottomStyle: 'solid',
borderColor: 'transparent',
zIndex: 1,
display: 'flex',
flexDirection: 'row'
},
elements: {
position: 'absolute',
left: 0,
right: 0,
top: 38,
bottom: 0,
overflowX: 'hidden',
overflowY: 'auto'
}
};
export default class LogMonitor extends Component {
constructor(props) {
super(props);
if (typeof window !== 'undefined') {
window.addEventListener('keydown', ::this.handleKeyPress);
}
}
static propTypes = {
computedStates: PropTypes.array.isRequired,
currentStateIndex: PropTypes.number.isRequired,
monitorState: PropTypes.object.isRequired,
stagedActions: PropTypes.array.isRequired,
skippedActions: PropTypes.object.isRequired,
reset: PropTypes.func.isRequired,
commit: PropTypes.func.isRequired,
rollback: PropTypes.func.isRequired,
sweep: PropTypes.func.isRequired,
toggleAction: PropTypes.func.isRequired,
jumpToState: PropTypes.func.isRequired,
setMonitorState: PropTypes.func.isRequired,
select: PropTypes.func.isRequired,
visibleOnLoad: PropTypes.bool,
theme: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
])
};
static defaultProps = {
select: (state) => state,
monitorState: { isVisible: true },
theme: 'nicinabox',
visibleOnLoad: true
};
componentWillReceiveProps(nextProps) {
const node = findDOMNode(this.refs.elements);
if (!node) {
this.scrollDown = true;
} else if (
this.props.stagedActions.length < nextProps.stagedActions.length
) {
const { scrollTop, offsetHeight, scrollHeight } = node;
this.scrollDown = Math.abs(
scrollHeight - (scrollTop + offsetHeight)
) < 20;
} else {
this.scrollDown = false;
}
}
componentDidUpdate() {
const node = findDOMNode(this.refs.elements);
if (!node) {
return;
}
if (this.scrollDown) {
const { offsetHeight, scrollHeight } = node;
node.scrollTop = scrollHeight - offsetHeight;
this.scrollDown = false;
}
}
componentWillMount() {
let visibleOnLoad = this.props.visibleOnLoad;
const { monitorState } = this.props;
this.props.setMonitorState({
...monitorState,
isVisible: visibleOnLoad
});
}
handleRollback() {
this.props.rollback();
}
handleSweep() {
this.props.sweep();
}
handleCommit() {
this.props.commit();
}
handleToggleAction(index) {
this.props.toggleAction(index);
}
handleReset() {
this.props.reset();
}
handleKeyPress(event) {
const { monitorState } = this.props;
if (event.ctrlKey && event.keyCode === 72) { // Ctrl+H
event.preventDefault();
this.props.setMonitorState({
...monitorState,
isVisible: !monitorState.isVisible
});
}
}
render() {
const elements = [];
const { monitorState, skippedActions, stagedActions, computedStates, select } = this.props;
let theme;
if (typeof this.props.theme === 'string') {
if (typeof themes[this.props.theme] !== 'undefined') {
theme = themes[this.props.theme];
} else {
console.warn('DevTools theme ' + this.props.theme + ' not found, defaulting to nicinabox');
theme = themes.nicinabox;
}
} else {
theme = this.props.theme;
}
if (!monitorState.isVisible) {
return null;
}
for (let i = 0; i < stagedActions.length; i++) {
const action = stagedActions[i];
const { state, error } = computedStates[i];
let previousState;
if (i > 0) {
previousState = computedStates[i - 1].state;
}
elements.push(
<LogMonitorEntry key={i}
index={i}
theme={theme}
select={select}
action={action}
state={state}
previousState={previousState}
collapsed={skippedActions[i]}
error={error}
onActionClick={::this.handleToggleAction} />
);
}
return (
<div style={{...styles.container, backgroundColor: theme.base00}}>
<div style={{...styles.buttonBar, borderColor: theme.base02}}>
<LogMonitorButton theme={theme} onClick={::this.handleReset}>Reset</LogMonitorButton>
<LogMonitorButton theme={theme} onClick={::this.handleRollback} enabled={computedStates.length}>Revert</LogMonitorButton>
<LogMonitorButton theme={theme} onClick={::this.handleSweep} enabled={Object.keys(skippedActions).some(key => skippedActions[key])}>Sweep</LogMonitorButton>
<LogMonitorButton theme={theme} onClick={::this.handleCommit} enabled={computedStates.length > 1}>Commit</LogMonitorButton>
</div>
<div style={styles.elements} ref="elements">
{elements}
</div>
</div>
);
}
}

View File

@ -1,85 +0,0 @@
import React from 'react';
import brighten from '../utils/brighten';
const styles = {
base: {
cursor: 'pointer',
fontWeight: 'bold',
borderRadius: 3,
padding: 4,
marginLeft: 3,
marginRight: 3,
marginTop: 5,
marginBottom: 5,
flexGrow: 1,
display: 'inline-block',
fontSize: '0.8em',
color: 'white',
textDecoration: 'none'
}
};
export default class LogMonitorButton extends React.Component {
constructor(props) {
super(props);
this.state = {
hovered: false,
active: false
};
}
handleMouseEnter() {
this.setState({ hovered: true });
}
handleMouseLeave() {
this.setState({ hovered: false });
}
handleMouseDown() {
this.setState({ active: true });
}
handleMouseUp() {
this.setState({ active: false });
}
onClick() {
if (!this.props.enabled) {
return;
}
if (this.props.onClick) {
this.props.onClick();
}
}
render() {
let style = {
...styles.base,
backgroundColor: this.props.theme.base02
};
if (this.props.enabled && this.state.hovered) {
style = {
...style,
backgroundColor: brighten(this.props.theme.base02, 0.2)
};
}
if (!this.props.enabled) {
style = {
...style,
opacity: 0.2,
cursor: 'text',
backgroundColor: 'transparent'
};
}
return (
<a onMouseEnter={::this.handleMouseEnter}
onMouseLeave={::this.handleMouseLeave}
onMouseDown={::this.handleMouseDown}
onMouseUp={::this.handleMouseUp}
style={style} onClick={::this.onClick}>
{this.props.children}
</a>
);
}
}

View File

@ -1,82 +0,0 @@
import React, { PropTypes, Component } from 'react';
import JSONTree from 'react-json-tree';
import LogMonitorEntryAction from './LogMonitorEntryAction';
const styles = {
entry: {
display: 'block',
WebkitUserSelect: 'none'
},
tree: {
paddingLeft: 0
}
};
export default class LogMonitorEntry extends Component {
static propTypes = {
index: PropTypes.number.isRequired,
state: PropTypes.object.isRequired,
action: PropTypes.object.isRequired,
select: PropTypes.func.isRequired,
error: PropTypes.string,
onActionClick: PropTypes.func.isRequired,
collapsed: PropTypes.bool
};
printState(state, error) {
let errorText = error;
if (!errorText) {
try {
return <JSONTree
theme={this.props.theme}
keyName={'state'}
data={this.props.select(state)}
previousData={this.props.select(this.props.previousState)}
style={styles.tree}/>;
} catch (err) {
errorText = 'Error selecting state.';
}
}
return (
<div style={{
color: this.props.theme.base08,
paddingTop: 20,
paddingLeft: 30,
paddingRight: 30,
paddingBottom: 35
}}>
{errorText}
</div>
);
}
handleActionClick() {
const { index, onActionClick } = this.props;
if (index > 0) {
onActionClick(index);
}
}
render() {
const { index, error, action, state, collapsed } = this.props;
const styleEntry = {
opacity: collapsed ? 0.5 : 1,
cursor: (index > 0) ? 'pointer' : 'default'
};
return (
<div style={{textDecoration: collapsed ? 'line-through' : 'none'}}>
<LogMonitorEntryAction
theme={this.props.theme}
collapsed={collapsed}
action={action}
onClick={::this.handleActionClick}
style={{...styles.entry, ...styleEntry}}/>
{!collapsed &&
<div>
{this.printState(state, error)}
</div>
}
</div>
);
}
}

View File

@ -1,45 +0,0 @@
import React from 'react';
import JSONTree from 'react-json-tree';
const styles = {
actionBar: {
paddingTop: 8,
paddingBottom: 7,
paddingLeft: 16
},
payload: {
margin: 0,
overflow: 'auto'
}
};
export default class LogMonitorAction extends React.Component {
renderPayload(payload) {
return (
<div style={{
...styles.payload,
backgroundColor: this.props.theme.base00
}}>
{ Object.keys(payload).length > 0 ? <JSONTree theme={this.props.theme} keyName={'action'} data={payload}/> : '' }
</div>
);
}
render() {
const { type, ...payload } = this.props.action;
return (
<div style={{
backgroundColor: this.props.theme.base02,
color: this.props.theme.base06,
...this.props.style
}}>
<div style={styles.actionBar}
onClick={this.props.onClick}>
{type}
</div>
{!this.props.collapsed ? this.renderPayload(payload) : ''}
</div>
);
}
}

View File

@ -1,6 +0,0 @@
import React from 'react';
import createDevTools from '../createDevTools';
export const DevTools = createDevTools(React);
export { default as LogMonitor } from './LogMonitor';
export { default as DebugPanel } from './DebugPanel';

View File

@ -1 +0,0 @@
// TODO

View File

@ -1,5 +0,0 @@
export default {
ocean: {
}
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'apathy',
author: 'jannik siebert (https://github.com/janniks)',
base00: '#031A16',
base01: '#0B342D',
base02: '#184E45',
base03: '#2B685E',
base04: '#5F9C92',
base05: '#81B5AC',
base06: '#A7CEC8',
base07: '#D2E7E4',
base08: '#3E9688',
base09: '#3E7996',
base0A: '#3E4C96',
base0B: '#883E96',
base0C: '#963E4C',
base0D: '#96883E',
base0E: '#4C963E',
base0F: '#3E965B'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'ashes',
author: 'jannik siebert (https://github.com/janniks)',
base00: '#1C2023',
base01: '#393F45',
base02: '#565E65',
base03: '#747C84',
base04: '#ADB3BA',
base05: '#C7CCD1',
base06: '#DFE2E5',
base07: '#F3F4F5',
base08: '#C7AE95',
base09: '#C7C795',
base0A: '#AEC795',
base0B: '#95C7AE',
base0C: '#95AEC7',
base0D: '#AE95C7',
base0E: '#C795AE',
base0F: '#C79595'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'atelier dune',
author: 'bram de haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune)',
base00: '#20201d',
base01: '#292824',
base02: '#6e6b5e',
base03: '#7d7a68',
base04: '#999580',
base05: '#a6a28c',
base06: '#e8e4cf',
base07: '#fefbec',
base08: '#d73737',
base09: '#b65611',
base0A: '#cfb017',
base0B: '#60ac39',
base0C: '#1fad83',
base0D: '#6684e1',
base0E: '#b854d4',
base0F: '#d43552'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'atelier forest',
author: 'bram de haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest)',
base00: '#1b1918',
base01: '#2c2421',
base02: '#68615e',
base03: '#766e6b',
base04: '#9c9491',
base05: '#a8a19f',
base06: '#e6e2e0',
base07: '#f1efee',
base08: '#f22c40',
base09: '#df5320',
base0A: '#d5911a',
base0B: '#5ab738',
base0C: '#00ad9c',
base0D: '#407ee7',
base0E: '#6666ea',
base0F: '#c33ff3'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'atelier heath',
author: 'bram de haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath)',
base00: '#1b181b',
base01: '#292329',
base02: '#695d69',
base03: '#776977',
base04: '#9e8f9e',
base05: '#ab9bab',
base06: '#d8cad8',
base07: '#f7f3f7',
base08: '#ca402b',
base09: '#a65926',
base0A: '#bb8a35',
base0B: '#379a37',
base0C: '#159393',
base0D: '#516aec',
base0E: '#7b59c0',
base0F: '#cc33cc'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'atelier lakeside',
author: 'bram de haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside/)',
base00: '#161b1d',
base01: '#1f292e',
base02: '#516d7b',
base03: '#5a7b8c',
base04: '#7195a8',
base05: '#7ea2b4',
base06: '#c1e4f6',
base07: '#ebf8ff',
base08: '#d22d72',
base09: '#935c25',
base0A: '#8a8a0f',
base0B: '#568c3b',
base0C: '#2d8f6f',
base0D: '#257fad',
base0E: '#5d5db1',
base0F: '#b72dd2'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'atelier seaside',
author: 'bram de haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/)',
base00: '#131513',
base01: '#242924',
base02: '#5e6e5e',
base03: '#687d68',
base04: '#809980',
base05: '#8ca68c',
base06: '#cfe8cf',
base07: '#f0fff0',
base08: '#e6193c',
base09: '#87711d',
base0A: '#c3c322',
base0B: '#29a329',
base0C: '#1999b3',
base0D: '#3d62f5',
base0E: '#ad2bee',
base0F: '#e619c3'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'bespin',
author: 'jan t. sott',
base00: '#28211c',
base01: '#36312e',
base02: '#5e5d5c',
base03: '#666666',
base04: '#797977',
base05: '#8a8986',
base06: '#9d9b97',
base07: '#baae9e',
base08: '#cf6a4c',
base09: '#cf7d34',
base0A: '#f9ee98',
base0B: '#54be0d',
base0C: '#afc4db',
base0D: '#5ea6ea',
base0E: '#9b859d',
base0F: '#937121'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'brewer',
author: 'timothée poisot (http://github.com/tpoisot)',
base00: '#0c0d0e',
base01: '#2e2f30',
base02: '#515253',
base03: '#737475',
base04: '#959697',
base05: '#b7b8b9',
base06: '#dadbdc',
base07: '#fcfdfe',
base08: '#e31a1c',
base09: '#e6550d',
base0A: '#dca060',
base0B: '#31a354',
base0C: '#80b1d3',
base0D: '#3182bd',
base0E: '#756bb1',
base0F: '#b15928'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'bright',
author: 'chris kempson (http://chriskempson.com)',
base00: '#000000',
base01: '#303030',
base02: '#505050',
base03: '#b0b0b0',
base04: '#d0d0d0',
base05: '#e0e0e0',
base06: '#f5f5f5',
base07: '#ffffff',
base08: '#fb0120',
base09: '#fc6d24',
base0A: '#fda331',
base0B: '#a1c659',
base0C: '#76c7b7',
base0D: '#6fb3d2',
base0E: '#d381c3',
base0F: '#be643c'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'chalk',
author: 'chris kempson (http://chriskempson.com)',
base00: '#151515',
base01: '#202020',
base02: '#303030',
base03: '#505050',
base04: '#b0b0b0',
base05: '#d0d0d0',
base06: '#e0e0e0',
base07: '#f5f5f5',
base08: '#fb9fb1',
base09: '#eda987',
base0A: '#ddb26f',
base0B: '#acc267',
base0C: '#12cfc0',
base0D: '#6fc2ef',
base0E: '#e1a3ee',
base0F: '#deaf8f'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'codeschool',
author: 'brettof86',
base00: '#232c31',
base01: '#1c3657',
base02: '#2a343a',
base03: '#3f4944',
base04: '#84898c',
base05: '#9ea7a6',
base06: '#a7cfa3',
base07: '#b5d8f6',
base08: '#2a5491',
base09: '#43820d',
base0A: '#a03b1e',
base0B: '#237986',
base0C: '#b02f30',
base0D: '#484d79',
base0E: '#c59820',
base0F: '#c98344'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'colors',
author: 'mrmrs (http://clrs.cc)',
base00: '#111111',
base01: '#333333',
base02: '#555555',
base03: '#777777',
base04: '#999999',
base05: '#bbbbbb',
base06: '#dddddd',
base07: '#ffffff',
base08: '#ff4136',
base09: '#ff851b',
base0A: '#ffdc00',
base0B: '#2ecc40',
base0C: '#7fdbff',
base0D: '#0074d9',
base0E: '#b10dc9',
base0F: '#85144b'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'default',
author: 'chris kempson (http://chriskempson.com)',
base00: '#181818',
base01: '#282828',
base02: '#383838',
base03: '#585858',
base04: '#b8b8b8',
base05: '#d8d8d8',
base06: '#e8e8e8',
base07: '#f8f8f8',
base08: '#ab4642',
base09: '#dc9656',
base0A: '#f7ca88',
base0B: '#a1b56c',
base0C: '#86c1b9',
base0D: '#7cafc2',
base0E: '#ba8baf',
base0F: '#a16946'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'eighties',
author: 'chris kempson (http://chriskempson.com)',
base00: '#2d2d2d',
base01: '#393939',
base02: '#515151',
base03: '#747369',
base04: '#a09f93',
base05: '#d3d0c8',
base06: '#e8e6df',
base07: '#f2f0ec',
base08: '#f2777a',
base09: '#f99157',
base0A: '#ffcc66',
base0B: '#99cc99',
base0C: '#66cccc',
base0D: '#6699cc',
base0E: '#cc99cc',
base0F: '#d27b53'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'embers',
author: 'jannik siebert (https://github.com/janniks)',
base00: '#16130F',
base01: '#2C2620',
base02: '#433B32',
base03: '#5A5047',
base04: '#8A8075',
base05: '#A39A90',
base06: '#BEB6AE',
base07: '#DBD6D1',
base08: '#826D57',
base09: '#828257',
base0A: '#6D8257',
base0B: '#57826D',
base0C: '#576D82',
base0D: '#6D5782',
base0E: '#82576D',
base0F: '#825757'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'flat',
author: 'chris kempson (http://chriskempson.com)',
base00: '#2C3E50',
base01: '#34495E',
base02: '#7F8C8D',
base03: '#95A5A6',
base04: '#BDC3C7',
base05: '#e0e0e0',
base06: '#f5f5f5',
base07: '#ECF0F1',
base08: '#E74C3C',
base09: '#E67E22',
base0A: '#F1C40F',
base0B: '#2ECC71',
base0C: '#1ABC9C',
base0D: '#3498DB',
base0E: '#9B59B6',
base0F: '#be643c'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'google',
author: 'seth wright (http://sethawright.com)',
base00: '#1d1f21',
base01: '#282a2e',
base02: '#373b41',
base03: '#969896',
base04: '#b4b7b4',
base05: '#c5c8c6',
base06: '#e0e0e0',
base07: '#ffffff',
base08: '#CC342B',
base09: '#F96A38',
base0A: '#FBA922',
base0B: '#198844',
base0C: '#3971ED',
base0D: '#3971ED',
base0E: '#A36AC7',
base0F: '#3971ED'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'grayscale',
author: 'alexandre gavioli (https://github.com/alexx2/)',
base00: '#101010',
base01: '#252525',
base02: '#464646',
base03: '#525252',
base04: '#ababab',
base05: '#b9b9b9',
base06: '#e3e3e3',
base07: '#f7f7f7',
base08: '#7c7c7c',
base09: '#999999',
base0A: '#a0a0a0',
base0B: '#8e8e8e',
base0C: '#868686',
base0D: '#686868',
base0E: '#747474',
base0F: '#5e5e5e'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'green screen',
author: 'chris kempson (http://chriskempson.com)',
base00: '#001100',
base01: '#003300',
base02: '#005500',
base03: '#007700',
base04: '#009900',
base05: '#00bb00',
base06: '#00dd00',
base07: '#00ff00',
base08: '#007700',
base09: '#009900',
base0A: '#007700',
base0B: '#00bb00',
base0C: '#005500',
base0D: '#009900',
base0E: '#00bb00',
base0F: '#005500'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'harmonic16',
author: 'jannik siebert (https://github.com/janniks)',
base00: '#0b1c2c',
base01: '#223b54',
base02: '#405c79',
base03: '#627e99',
base04: '#aabcce',
base05: '#cbd6e2',
base06: '#e5ebf1',
base07: '#f7f9fb',
base08: '#bf8b56',
base09: '#bfbf56',
base0A: '#8bbf56',
base0B: '#56bf8b',
base0C: '#568bbf',
base0D: '#8b56bf',
base0E: '#bf568b',
base0F: '#bf5656'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'hopscotch',
author: 'jan t. sott',
base00: '#322931',
base01: '#433b42',
base02: '#5c545b',
base03: '#797379',
base04: '#989498',
base05: '#b9b5b8',
base06: '#d5d3d5',
base07: '#ffffff',
base08: '#dd464c',
base09: '#fd8b19',
base0A: '#fdcc59',
base0B: '#8fc13e',
base0C: '#149b93',
base0D: '#1290bf',
base0E: '#c85e7c',
base0F: '#b33508'
};

View File

@ -1,38 +0,0 @@
export { default as threezerotwofour } from './threezerotwofour';
export { default as apathy } from './apathy';
export { default as ashes } from './ashes';
export { default as atelierDune } from './atelier-dune';
export { default as atelierForest } from './atelier-forest';
export { default as atelierHeath } from './atelier-heath';
export { default as atelierLakeside } from './atelier-lakeside';
export { default as atelierSeaside } from './atelier-seaside';
export { default as bespin } from './bespin';
export { default as brewer } from './brewer';
export { default as bright } from './bright';
export { default as chalk } from './chalk';
export { default as codeschool } from './codeschool';
export { default as colors } from './colors';
export { default as default } from './default';
export { default as eighties } from './eighties';
export { default as embers } from './embers';
export { default as flat } from './flat';
export { default as google } from './google';
export { default as grayscale } from './grayscale';
export { default as greenscreen } from './greenscreen';
export { default as harmonic } from './harmonic';
export { default as hopscotch } from './hopscotch';
export { default as isotope } from './isotope';
export { default as marrakesh } from './marrakesh';
export { default as mocha } from './mocha';
export { default as monokai } from './monokai';
export { default as ocean } from './ocean';
export { default as paraiso } from './paraiso';
export { default as pop } from './pop';
export { default as railscasts } from './railscasts';
export { default as shapeshifter } from './shapeshifter';
export { default as solarized } from './solarized';
export { default as summerfruit } from './summerfruit';
export { default as tomorrow } from './tomorrow';
export { default as tube } from './tube';
export { default as twilight } from './twilight';
export { default as nicinabox } from './nicinabox';

View File

@ -1,20 +0,0 @@
export default {
scheme: 'isotope',
author: 'jan t. sott',
base00: '#000000',
base01: '#404040',
base02: '#606060',
base03: '#808080',
base04: '#c0c0c0',
base05: '#d0d0d0',
base06: '#e0e0e0',
base07: '#ffffff',
base08: '#ff0000',
base09: '#ff9900',
base0A: '#ff0099',
base0B: '#33ff00',
base0C: '#00ffff',
base0D: '#0066ff',
base0E: '#cc00ff',
base0F: '#3300ff'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'marrakesh',
author: 'alexandre gavioli (http://github.com/alexx2/)',
base00: '#201602',
base01: '#302e00',
base02: '#5f5b17',
base03: '#6c6823',
base04: '#86813b',
base05: '#948e48',
base06: '#ccc37a',
base07: '#faf0a5',
base08: '#c35359',
base09: '#b36144',
base0A: '#a88339',
base0B: '#18974e',
base0C: '#75a738',
base0D: '#477ca1',
base0E: '#8868b3',
base0F: '#b3588e'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'mocha',
author: 'chris kempson (http://chriskempson.com)',
base00: '#3B3228',
base01: '#534636',
base02: '#645240',
base03: '#7e705a',
base04: '#b8afad',
base05: '#d0c8c6',
base06: '#e9e1dd',
base07: '#f5eeeb',
base08: '#cb6077',
base09: '#d28b71',
base0A: '#f4bc87',
base0B: '#beb55b',
base0C: '#7bbda4',
base0D: '#8ab3b5',
base0E: '#a89bb9',
base0F: '#bb9584'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'monokai',
author: 'wimer hazenberg (http://www.monokai.nl)',
base00: '#272822',
base01: '#383830',
base02: '#49483e',
base03: '#75715e',
base04: '#a59f85',
base05: '#f8f8f2',
base06: '#f5f4f1',
base07: '#f9f8f5',
base08: '#f92672',
base09: '#fd971f',
base0A: '#f4bf75',
base0B: '#a6e22e',
base0C: '#a1efe4',
base0D: '#66d9ef',
base0E: '#ae81ff',
base0F: '#cc6633'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'nicinabox',
author: 'nicinabox (http://github.com/nicinabox)',
base00: '#2A2F3A',
base01: '#3C444F',
base02: '#4F5A65',
base03: '#BEBEBE',
base04: '#b0b0b0', // unmodified
base05: '#d0d0d0', // unmodified
base06: '#FFFFFF',
base07: '#f5f5f5', // unmodified
base08: '#fb9fb1', // unmodified
base09: '#FC6D24',
base0A: '#ddb26f', // unmodified
base0B: '#A1C659',
base0C: '#12cfc0', // unmodified
base0D: '#6FB3D2',
base0E: '#D381C3',
base0F: '#deaf8f' // unmodified
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'ocean',
author: 'chris kempson (http://chriskempson.com)',
base00: '#2b303b',
base01: '#343d46',
base02: '#4f5b66',
base03: '#65737e',
base04: '#a7adba',
base05: '#c0c5ce',
base06: '#dfe1e8',
base07: '#eff1f5',
base08: '#bf616a',
base09: '#d08770',
base0A: '#ebcb8b',
base0B: '#a3be8c',
base0C: '#96b5b4',
base0D: '#8fa1b3',
base0E: '#b48ead',
base0F: '#ab7967'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'paraiso',
author: 'jan t. sott',
base00: '#2f1e2e',
base01: '#41323f',
base02: '#4f424c',
base03: '#776e71',
base04: '#8d8687',
base05: '#a39e9b',
base06: '#b9b6b0',
base07: '#e7e9db',
base08: '#ef6155',
base09: '#f99b15',
base0A: '#fec418',
base0B: '#48b685',
base0C: '#5bc4bf',
base0D: '#06b6ef',
base0E: '#815ba4',
base0F: '#e96ba8'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'pop',
author: 'chris kempson (http://chriskempson.com)',
base00: '#000000',
base01: '#202020',
base02: '#303030',
base03: '#505050',
base04: '#b0b0b0',
base05: '#d0d0d0',
base06: '#e0e0e0',
base07: '#ffffff',
base08: '#eb008a',
base09: '#f29333',
base0A: '#f8ca12',
base0B: '#37b349',
base0C: '#00aabb',
base0D: '#0e5a94',
base0E: '#b31e8d',
base0F: '#7a2d00'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'railscasts',
author: 'ryan bates (http://railscasts.com)',
base00: '#2b2b2b',
base01: '#272935',
base02: '#3a4055',
base03: '#5a647e',
base04: '#d4cfc9',
base05: '#e6e1dc',
base06: '#f4f1ed',
base07: '#f9f7f3',
base08: '#da4939',
base09: '#cc7833',
base0A: '#ffc66d',
base0B: '#a5c261',
base0C: '#519f50',
base0D: '#6d9cbe',
base0E: '#b6b3eb',
base0F: '#bc9458'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'shapeshifter',
author: 'tyler benziger (http://tybenz.com)',
base00: '#000000',
base01: '#040404',
base02: '#102015',
base03: '#343434',
base04: '#555555',
base05: '#ababab',
base06: '#e0e0e0',
base07: '#f9f9f9',
base08: '#e92f2f',
base09: '#e09448',
base0A: '#dddd13',
base0B: '#0ed839',
base0C: '#23edda',
base0D: '#3b48e3',
base0E: '#f996e2',
base0F: '#69542d'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'solarized',
author: 'ethan schoonover (http://ethanschoonover.com/solarized)',
base00: '#002b36',
base01: '#073642',
base02: '#586e75',
base03: '#657b83',
base04: '#839496',
base05: '#93a1a1',
base06: '#eee8d5',
base07: '#fdf6e3',
base08: '#dc322f',
base09: '#cb4b16',
base0A: '#b58900',
base0B: '#859900',
base0C: '#2aa198',
base0D: '#268bd2',
base0E: '#6c71c4',
base0F: '#d33682'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'summerfruit',
author: 'christopher corley (http://cscorley.github.io/)',
base00: '#151515',
base01: '#202020',
base02: '#303030',
base03: '#505050',
base04: '#B0B0B0',
base05: '#D0D0D0',
base06: '#E0E0E0',
base07: '#FFFFFF',
base08: '#FF0086',
base09: '#FD8900',
base0A: '#ABA800',
base0B: '#00C918',
base0C: '#1faaaa',
base0D: '#3777E6',
base0E: '#AD00A1',
base0F: '#cc6633'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'threezerotwofour',
author: 'jan t. sott (http://github.com/idleberg)',
base00: '#090300',
base01: '#3a3432',
base02: '#4a4543',
base03: '#5c5855',
base04: '#807d7c',
base05: '#a5a2a2',
base06: '#d6d5d4',
base07: '#f7f7f7',
base08: '#db2d20',
base09: '#e8bbd0',
base0A: '#fded02',
base0B: '#01a252',
base0C: '#b5e4f4',
base0D: '#01a0e4',
base0E: '#a16a94',
base0F: '#cdab53'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'tomorrow',
author: 'chris kempson (http://chriskempson.com)',
base00: '#1d1f21',
base01: '#282a2e',
base02: '#373b41',
base03: '#969896',
base04: '#b4b7b4',
base05: '#c5c8c6',
base06: '#e0e0e0',
base07: '#ffffff',
base08: '#cc6666',
base09: '#de935f',
base0A: '#f0c674',
base0B: '#b5bd68',
base0C: '#8abeb7',
base0D: '#81a2be',
base0E: '#b294bb',
base0F: '#a3685a'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'london tube',
author: 'jan t. sott',
base00: '#231f20',
base01: '#1c3f95',
base02: '#5a5758',
base03: '#737171',
base04: '#959ca1',
base05: '#d9d8d8',
base06: '#e7e7e8',
base07: '#ffffff',
base08: '#ee2e24',
base09: '#f386a1',
base0A: '#ffd204',
base0B: '#00853e',
base0C: '#85cebc',
base0D: '#009ddc',
base0E: '#98005d',
base0F: '#b06110'
};

View File

@ -1,20 +0,0 @@
export default {
scheme: 'twilight',
author: 'david hart (http://hart-dev.com)',
base00: '#1e1e1e',
base01: '#323537',
base02: '#464b50',
base03: '#5f5a60',
base04: '#838184',
base05: '#a7a7a7',
base06: '#c3c3c3',
base07: '#ffffff',
base08: '#cf6a4c',
base09: '#cda869',
base0A: '#f9ee98',
base0B: '#8f9d6a',
base0C: '#afc4db',
base0D: '#7587a6',
base0E: '#9b859d',
base0F: '#9b703f'
};

View File

@ -1,16 +0,0 @@
export default function(hexColor, lightness) {
let hex = String(hexColor).replace(/[^0-9a-f]/gi, '');
if (hex.length < 6) {
hex = hex.replace(/(.)/g, '$1$1');
}
let lum = lightness || 0;
let rgb = '#';
let c;
for (let i = 0; i < 3; ++i) {
c = parseInt(hex.substr(i * 2, 2), 16);
c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
rgb += ('00' + c).substr(c.length);
}
return rgb;
}

View File

@ -1,83 +0,0 @@
import expect from 'expect';
import jsdom from 'mocha-jsdom';
import React, { Component } from 'react';
import TestUtils from 'react-addons-test-utils';
import createDevTools from '../src/createDevTools';
import devTools from '../src/devTools';
import { createStore } from 'redux';
class MockMonitor extends Component {
render() {
return null;
}
}
describe('createDevTools', () => {
jsdom();
it('should pass devToolsStore to monitor', () => {
const store = devTools()(createStore)(() => {});
const DevTools = createDevTools(React);
const tree = TestUtils.renderIntoDocument(
<DevTools monitor={MockMonitor} store={store} />
);
const mockMonitor = TestUtils.findRenderedComponentWithType(tree, MockMonitor);
expect(mockMonitor.props.store).toBe(store.devToolsStore);
});
it('should pass props to monitor', () => {
const store = devTools()(createStore)(() => {});
const DevTools = createDevTools(React);
const tree = TestUtils.renderIntoDocument(
<DevTools monitor={MockMonitor} store={store} one={1} two={2}/>
);
const mockMonitor = TestUtils.findRenderedComponentWithType(tree, MockMonitor);
expect(mockMonitor.props.one).toBe(1);
expect(mockMonitor.props.two).toBe(2);
});
it('should subscribe monitor to store updates', () => {
const DevTools = createDevTools(React);
const store = devTools()(createStore)(
(state, action) => {
switch (action.type) {
case 'INC':
return state + 1;
default:
return state;
}
},
0
);
const tree = TestUtils.renderIntoDocument(
<DevTools monitor={MockMonitor} store={store} />
);
store.dispatch({type: 'INC'});
const mockMonitor = TestUtils.findRenderedComponentWithType(tree, MockMonitor);
const currentStateIndex = mockMonitor.props.currentStateIndex;
const computedStates = mockMonitor.props.computedStates;
expect(computedStates[currentStateIndex].state).toBe(1);
});
it('should warn if devTools() not in middleware', () => {
const spy = expect.spyOn(console, 'error');
const store = createStore(() => {});
const DevTools = createDevTools(React);
expect(
TestUtils.renderIntoDocument,
).withArgs(
<DevTools monitor={MockMonitor} store={store} />
).toThrow();
expect(spy.calls).toContain(
/Could not find the devTools store/,
(call, errMsg) => call.arguments[0].match(errMsg)
);
spy.restore();
});
});

View File

@ -1,246 +0,0 @@
import expect, { spyOn } from 'expect';
import { createStore } from 'redux';
import devTools, { ActionCreators } from '../src/devTools';
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 doubleCounter(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 2;
case 'DECREMENT': return state - 2;
default: return state;
}
}
describe('devTools', () => {
let store;
let devToolsStore;
beforeEach(() => {
store = devTools()(createStore)(counter);
devToolsStore = store.devToolsStore;
});
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 rollback state to the last committed state', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
devToolsStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(4);
devToolsStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2);
store.dispatch({ type: 'DECREMENT' });
expect(store.getState()).toBe(1);
devToolsStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2);
});
it('should reset to initial state', () => {
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
devToolsStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
devToolsStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
devToolsStore.dispatch(ActionCreators.reset());
expect(store.getState()).toBe(0);
});
it('should toggle an action', () => {
// stateIndex 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
devToolsStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(2);
devToolsStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(1);
});
it('should sweep disabled actions', () => {
// stateIndex 0 = @@INIT
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2);
devToolsStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(3);
devToolsStore.dispatch(ActionCreators.sweep());
expect(store.getState()).toBe(3);
devToolsStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(2);
devToolsStore.dispatch(ActionCreators.sweep());
expect(store.getState()).toBe(2);
});
it('should jump to state', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
devToolsStore.dispatch(ActionCreators.jumpToState(0));
expect(store.getState()).toBe(0);
devToolsStore.dispatch(ActionCreators.jumpToState(1));
expect(store.getState()).toBe(1);
devToolsStore.dispatch(ActionCreators.jumpToState(2));
expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(0);
devToolsStore.dispatch(ActionCreators.jumpToState(4));
expect(store.getState()).toBe(2);
});
it('should set monitor state', () => {
devToolsStore.dispatch(ActionCreators.setMonitorState({test: 'test'}));
expect(devToolsStore.getState().monitorState.test).toBe('test');
});
it('should recompute', () => {
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1);
let stagedActions = [...store.devToolsStore.getState().stagedActions];
// replace DECREMENT with INCREMENT (stagedAction[0] is @@INIT)
stagedActions[2] = { type: 'INCREMENT' };
const committedState = 10;
devToolsStore.dispatch(ActionCreators.recomputeStates(
committedState,
stagedActions
));
expect(store.getState()).toBe(13);
});
it('should get the reducer', () => {
expect(store.getReducer()).toBe(counter);
});
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 catch and record errors', () => {
let spy = spyOn(console, 'error');
let storeWithBug = devTools()(createStore)(counterWithBug);
storeWithBug.dispatch({ type: 'INCREMENT' });
storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' });
let devStoreState = storeWithBug.devToolsStore.getState();
expect(devStoreState.computedStates[2].error).toMatch(
/ReferenceError/
);
expect(devStoreState.computedStates[3].error).toMatch(
/Interrupted by an error up the chain/
);
expect(spy.calls[0].arguments[0]).toMatch(
/ReferenceError/
);
spy.restore();
});
it('should return the last non-undefined state from getState', () => {
let storeWithBug = devTools()(createStore)(counterWithBug);
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 = devTools()(createStore)(() => reducerCalls++);
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
});
it('should not recompute states when jumping to state', () => {
let reducerCalls = 0;
let monitoredStore = devTools()(createStore)(() => reducerCalls++);
let monitoredDevToolsStore = monitoredStore.devToolsStore;
expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
monitoredStore.dispatch({ type: 'INCREMENT' });
expect(reducerCalls).toBe(4);
monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(0));
expect(reducerCalls).toBe(4);
monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(1));
expect(reducerCalls).toBe(4);
monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(3));
expect(reducerCalls).toBe(4);
});
});

View File

325
test/instrument.spec.js Normal file
View File

@ -0,0 +1,325 @@
import expect, { spyOn } from 'expect';
import { createStore } from 'redux';
import instrument, { ActionCreators } from '../src/instrument';
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 doubleCounter(state = 0, action) {
switch (action.type) {
case 'INCREMENT': return state + 2;
case 'DECREMENT': return state - 2;
default: return state;
}
}
describe('instrument', () => {
let store;
let liftedStore;
beforeEach(() => {
store = instrument()(createStore)(counter);
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 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 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 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 catch and record errors', () => {
let spy = spyOn(console, 'error');
let storeWithBug = instrument()(createStore)(counterWithBug);
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]).toMatch(
/ReferenceError/
);
spy.restore();
});
it('should return the last non-undefined state from getState', () => {
let storeWithBug = instrument()(createStore)(counterWithBug);
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 = instrument()(createStore)(() => reducerCalls++);
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 = instrument()(createStore)(() => reducerCalls++);
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 = instrument()(createStore)(() => reducerCalls++);
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 = instrument()(createStore)(() => reducerCalls++);
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('Import State', () => {
let monitoredStore;
let monitoredLiftedStore;
let exportedState;
beforeEach(() => {
monitoredStore = instrument()(createStore)(counter);
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 = instrument()(createStore)(counter);
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 = instrument()(createStore)(counter);
let importMonitoredLiftedStore = importMonitoredStore.liftedStore;
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredStore.dispatch({ type: 'DECREMENT' });
importMonitoredLiftedStore.dispatch(ActionCreators.importState(exportedState));
expect(importMonitoredLiftedStore.getState()).toEqual(exportedState);
});
});
});

View File

@ -1,6 +1,5 @@
import expect from 'expect';
import devTools from '../src/devTools';
import persistState from '../src/persistState';
import { instrument, persistState } from '../src';
import { compose, createStore } from 'redux';
describe('persistState', () => {
@ -40,7 +39,7 @@ describe('persistState', () => {
};
it('should persist state', () => {
const finalCreateStore = compose(devTools(), persistState('id'))(createStore);
const finalCreateStore = compose(instrument(), persistState('id'))(createStore);
const store = finalCreateStore(reducer);
expect(store.getState()).toBe(0);
@ -53,7 +52,7 @@ describe('persistState', () => {
});
it('should not persist state if no session id', () => {
const finalCreateStore = compose(devTools(), persistState())(createStore);
const finalCreateStore = compose(instrument(), persistState())(createStore);
const store = finalCreateStore(reducer);
expect(store.getState()).toBe(0);
@ -67,7 +66,7 @@ describe('persistState', () => {
it('should run with a custom state deserializer', () => {
const oneLess = state => state === undefined ? -1 : state - 1;
const finalCreateStore = compose(devTools(), persistState('id', oneLess))(createStore);
const finalCreateStore = compose(instrument(), persistState('id', oneLess))(createStore);
const store = finalCreateStore(reducer);
expect(store.getState()).toBe(0);
@ -81,7 +80,7 @@ describe('persistState', () => {
it('should run with a custom action deserializer', () => {
const incToDec = action => action.type === 'INCREMENT' ? { type: 'DECREMENT' } : action;
const finalCreateStore = compose(devTools(), persistState('id', null, incToDec))(createStore);
const finalCreateStore = compose(instrument(), persistState('id', undefined, incToDec))(createStore);
const store = finalCreateStore(reducer);
expect(store.getState()).toBe(0);
@ -95,7 +94,7 @@ describe('persistState', () => {
it('should warn if read from localStorage fails', () => {
const spy = expect.spyOn(console, 'warn');
const finalCreateStore = compose(devTools(), persistState('id'))(createStore);
const finalCreateStore = compose(instrument(), persistState('id'))(createStore);
delete global.localStorage.getItem;
finalCreateStore(reducer);
@ -108,7 +107,7 @@ describe('persistState', () => {
});
it('should warn if write to localStorage fails', () => {
const spy = expect.spyOn(console, 'warn');
const finalCreateStore = compose(devTools(), persistState('id'))(createStore);
const finalCreateStore = compose(instrument(), persistState('id'))(createStore);
delete global.localStorage.setItem;
const store = finalCreateStore(reducer);