This commit is contained in:
Nathan Bierema 2021-10-28 16:17:09 -04:00
parent 39b0fe377e
commit e972239059
74 changed files with 1381 additions and 1034 deletions

View File

@ -3,5 +3,5 @@ module.exports = {
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json'],
} },
}; };

View File

@ -1,5 +1,4 @@
Remote Redux DevTools # Remote Redux DevTools
=====================
![Demo](demo.gif) ![Demo](demo.gif)
@ -18,60 +17,79 @@ There are 2 ways of usage depending if you're using other store enhancers (middl
#### Add DevTools enhancer to your store #### Add DevTools enhancer to your store
If you have a basic [store](http://redux.js.org/docs/api/createStore.html) as described in the official [redux-docs](http://redux.js.org/index.html), simply replace: If you have a basic [store](http://redux.js.org/docs/api/createStore.html) as described in the official [redux-docs](http://redux.js.org/index.html), simply replace:
```javascript
import { createStore } from 'redux'; ```javascript
const store = createStore(reducer); import { createStore } from 'redux';
``` const store = createStore(reducer);
with ```
```javascript
import { createStore } from 'redux'; with
import devToolsEnhancer from '@redux-devtools/remote';
const store = createStore(reducer, devToolsEnhancer()); ```javascript
// or const store = createStore(reducer, preloadedState, devToolsEnhancer()); import { createStore } from 'redux';
``` import devToolsEnhancer from '@redux-devtools/remote';
const store = createStore(reducer, devToolsEnhancer());
// or const store = createStore(reducer, preloadedState, devToolsEnhancer());
```
> Note: passing enhancer as last argument requires redux@>=3.1.0 > Note: passing enhancer as last argument requires redux@>=3.1.0
#### When to use DevTools compose helper #### When to use DevTools compose helper
If you setup your store with [middlewares and enhancers](http://redux.js.org/docs/api/applyMiddleware.html) like [redux-saga](https://github.com/redux-saga/redux-saga) and similar, it is crucial to use `composeWithDevTools` export. Otherwise, actions dispatched from Redux DevTools will not flow to your middlewares. If you setup your store with [middlewares and enhancers](http://redux.js.org/docs/api/applyMiddleware.html) like [redux-saga](https://github.com/redux-saga/redux-saga) and similar, it is crucial to use `composeWithDevTools` export. Otherwise, actions dispatched from Redux DevTools will not flow to your middlewares.
In that case change this: In that case change this:
```javascript
import { createStore, applyMiddleware, compose } from 'redux';
const store = createStore(reducer, preloadedState, compose( ```javascript
applyMiddleware(...middleware), import { createStore, applyMiddleware, compose } from 'redux';
const store = createStore(
reducer,
preloadedState,
compose(
applyMiddleware(...middleware)
// other store enhancers if any // other store enhancers if any
)); )
``` );
to: ```
```javascript
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/remote';
const store = createStore(reducer, /* preloadedState, */ composeWithDevTools( to:
applyMiddleware(...middleware),
// other store enhancers if any
));
```
or with devTools' options:
```javascript
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/remote';
const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); ```javascript
const store = createStore(reducer, /* preloadedState, */ composeEnhancers( import { createStore, applyMiddleware } from 'redux';
applyMiddleware(...middleware), import { composeWithDevTools } from '@redux-devtools/remote';
const store = createStore(
reducer,
/* preloadedState, */ composeWithDevTools(
applyMiddleware(...middleware)
// other store enhancers if any // other store enhancers if any
)); )
``` );
```
or with devTools' options:
```javascript
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from '@redux-devtools/remote';
const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 });
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware)
// other store enhancers if any
)
);
```
### Enabling ### Enabling
In order not to allow it in production by default, the enhancer will have effect only when `process.env.NODE_ENV === 'development'`. In order not to allow it in production by default, the enhancer will have effect only when `process.env.NODE_ENV === 'development'`.
For Webpack you should add it as following (`webpack.config.dev.js`): For Webpack you should add it as following (`webpack.config.dev.js`):
```js ```js
// ... // ...
plugins: [ plugins: [
@ -91,11 +109,12 @@ const store = createStore(reducer, devToolsEnhancer({ realtime: true }));
### Monitoring ### Monitoring
Use one of our monitor apps to inspect and dispatch actions: Use one of our monitor apps to inspect and dispatch actions:
* [redux-devtools-extension](https://github.com/reduxjs/redux-devtools/tree/main/extension) - Click "Remote" button (or press [`Cmd+Ctrl+Arrow up`](https://github.com/zalmoxisus/redux-devtools-extension#keyboard-shortcuts)) to open remote monitoring.
* [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger) - Used in React Native debugger as a dock monitor. - [redux-devtools-extension](https://github.com/reduxjs/redux-devtools/tree/main/extension) - Click "Remote" button (or press [`Cmd+Ctrl+Arrow up`](https://github.com/zalmoxisus/redux-devtools-extension#keyboard-shortcuts)) to open remote monitoring.
* [atom-redux-devtools](https://github.com/zalmoxisus/atom-redux-devtools) - Used in Atom editor. - [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger) - Used in React Native debugger as a dock monitor.
* [redux-dispatch-cli](https://github.com/jhen0409/redux-dispatch-cli) - A CLI tool for Redux remote dispatch. - [atom-redux-devtools](https://github.com/zalmoxisus/atom-redux-devtools) - Used in Atom editor.
* [vscode-redux-devtools](https://github.com/jkzing/vscode-redux-devtools) - Used in Visual Studio Code. - [redux-dispatch-cli](https://github.com/jhen0409/redux-dispatch-cli) - A CLI tool for Redux remote dispatch.
- [vscode-redux-devtools](https://github.com/jkzing/vscode-redux-devtools) - Used in Visual Studio Code.
Use [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-app) to create your own monitor app. Use [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-app) to create your own monitor app.
@ -103,57 +122,63 @@ Use [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/pa
Use [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). Use [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli).
You can import it in your `server.js` script and start remotedev server together with your development server: You can import it in your `server.js` script and start remotedev server together with your development server:
```js ```js
var reduxDevTools = require('@redux-devtools/cli'); var reduxDevTools = require('@redux-devtools/cli');
reduxDevTools({ hostname: 'localhost', port: 8000 }); reduxDevTools({ hostname: 'localhost', port: 8000 });
``` ```
See [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli) for more details. See [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli) for more details.
For React Native you can use [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger), which already include `@redux-devtools/cli`. For React Native you can use [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger), which already include `@redux-devtools/cli`.
### Parameters ### Parameters
Name | Description | Name | Description |
------------- | ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
`name` | *String* representing the instance name to be shown on the remote monitor. | `name` | _String_ representing the instance name to be shown on the remote monitor. |
`realtime` | *Boolean* specifies whether to allow remote monitoring. By default is `process.env.NODE_ENV === 'development'`. | `realtime` | _Boolean_ specifies whether to allow remote monitoring. By default is `process.env.NODE_ENV === 'development'`. |
`hostname` | *String* used to specify host for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). If `port` is specified, default value is `localhost`. | `hostname` | _String_ used to specify host for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). If `port` is specified, default value is `localhost`. |
`port` | *Number* used to specify host's port for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). | `port` | _Number_ used to specify host's port for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). |
`secure` | *Boolean* specifies whether to use `https` protocol for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). | `secure` | _Boolean_ specifies whether to use `https` protocol for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). |
`maxAge` | *Number* of maximum allowed actions to be stored on the history tree, the oldest actions are removed once maxAge is reached. Default is `30`. | `maxAge` | _Number_ of maximum allowed actions to be stored on the history tree, the oldest actions are removed once maxAge is reached. Default is `30`. |
`actionsBlacklist` | *array* of actions to be hidden in DevTools. Overwrites corresponding global setting in the options page. See the example bellow. | `actionsBlacklist` | _array_ of actions to be hidden in DevTools. Overwrites corresponding global setting in the options page. See the example bellow. |
`actionsWhitelist` | *array* of actions to be shown. All other actions will be hidden in DevTools. | `actionsWhitelist` | _array_ of actions to be shown. All other actions will be hidden in DevTools. |
`actionSanitizer` | *Function* which takes action object and id number as arguments, and should return action object back. See the example bellow. | `actionSanitizer` | _Function_ which takes action object and id number as arguments, and should return action object back. See the example bellow. |
`stateSanitizer` | *Function* which takes state object and index as arguments, and should return state object back. See the example bellow. | `stateSanitizer` | _Function_ which takes state object and index as arguments, and should return state object back. See the example bellow. |
`startOn` | *String* or *Array of strings* indicating an action or a list of actions, which should start remote monitoring (when `realtime` is `false`). | `startOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should start remote monitoring (when `realtime` is `false`). |
`stopOn` | *String* or *Array of strings* indicating an action or a list of actions, which should stop remote monitoring. | `stopOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should stop remote monitoring. |
`sendOn` | *String* or *Array of strings* indicating an action or a list of actions, which should trigger sending the history to the monitor (without starting it). *Note*: when using it, add a `fetch` polyfill if needed. | `sendOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should trigger sending the history to the monitor (without starting it). _Note_: when using it, add a `fetch` polyfill if needed. |
`sendOnError` | *Numeric* code: `0` - disabled (default), `1` - send all uncaught exception messages, `2` - send only reducers error messages. | `sendOnError` | _Numeric_ code: `0` - disabled (default), `1` - send all uncaught exception messages, `2` - send only reducers error messages. |
`sendTo` | *String* url of the monitor to send the history when `sendOn` is triggered. By default is `${secure ? 'https' : 'http'}://${hostname}:${port}`. | `sendTo` | _String_ url of the monitor to send the history when `sendOn` is triggered. By default is `${secure ? 'https' : 'http'}://${hostname}:${port}`. |
`actionCreators` | *Array* or *Object* of action creators to dispatch remotely. See [the example](https://github.com/zalmoxisus/remote-redux-devtools/commit/b54652930dfd4e057991df8471c343957fd7bff7). | `actionCreators` | _Array_ or _Object_ of action creators to dispatch remotely. See [the example](https://github.com/zalmoxisus/remote-redux-devtools/commit/b54652930dfd4e057991df8471c343957fd7bff7). |
`shouldHotReload` | *Boolean* - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`. | `shouldHotReload` | _Boolean_ - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`. |
`shouldRecordChanges` | *Boolean* - if specified as `false`, it will not record the changes till clicked on "Start recording" button on the monitor app. Default is `true`. | `shouldRecordChanges` | _Boolean_ - if specified as `false`, it will not record the changes till clicked on "Start recording" button on the monitor app. Default is `true`. |
`shouldStartLocked` | *Boolean* - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`. | `shouldStartLocked` | _Boolean_ - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`. |
`id` | *String* to identify the instance when sending the history triggered by `sendOn`. You can use, for example, user id here, to know who sent the data. | `id` | _String_ to identify the instance when sending the history triggered by `sendOn`. You can use, for example, user id here, to know who sent the data. |
`suppressConnectErrors` | *Boolean* - if set to `false`, all socket errors thrown while trying to connect will be printed to the console, regardless of if they've been thrown before. This is primarily for suppressing `SocketProtocolError` errors, which get repeatedly thrown when trying to make a connection. Default is `true`. | `suppressConnectErrors` | _Boolean_ - if set to `false`, all socket errors thrown while trying to connect will be printed to the console, regardless of if they've been thrown before. This is primarily for suppressing `SocketProtocolError` errors, which get repeatedly thrown when trying to make a connection. Default is `true`. |
All parameters are optional. You have to provide at least `port` property to use `localhost` instead of `remotedev.io` server. All parameters are optional. You have to provide at least `port` property to use `localhost` instead of `remotedev.io` server.
Example: Example:
```js ```js
export default function configureStore(preloadedState) { export default function configureStore(preloadedState) {
const store = createStore( const store = createStore(
reducer, reducer,
preloadedState, preloadedState,
devToolsEnhancer({ devToolsEnhancer({
name: 'Android app', realtime: true, name: 'Android app',
hostname: 'localhost', port: 8000, realtime: true,
maxAge: 30, actionsBlacklist: ['EFFECT_RESOLVED'], hostname: 'localhost',
actionSanitizer: (action) => ( port: 8000,
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data ? maxAge: 30,
{ ...action, data: '<<LONG_BLOB>>' } : action actionsBlacklist: ['EFFECT_RESOLVED'],
), actionSanitizer: (action) =>
stateSanitizer: (state) => state.data ? { ...state, data: '<<LONG_BLOB>>' } : state action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
? { ...action, data: '<<LONG_BLOB>>' }
: action,
stateSanitizer: (state) =>
state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
}) })
); );
return store; return store;
@ -161,9 +186,11 @@ export default function configureStore(preloadedState) {
``` ```
### Demo ### Demo
- [Toggle monitoring](http://zalmoxisus.github.io/monitoring/) - [Toggle monitoring](http://zalmoxisus.github.io/monitoring/)
### Examples ### Examples
- [Web](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-remote/examples) - [Web](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-remote/examples)
- [React Native](https://github.com/chentsulin/react-native-counter-ios-android) - [React Native](https://github.com/chentsulin/react-native-counter-ios-android)

View File

@ -13,7 +13,7 @@ var exampleDirs = fs.readdirSync(__dirname).filter((file) => {
// Ordering is important here. `npm install` must come first. // Ordering is important here. `npm install` must come first.
var cmdArgs = [ var cmdArgs = [
{ cmd: 'npm', args: ['install'] }, { cmd: 'npm', args: ['install'] },
{ cmd: 'webpack', args: ['index.js'] } { cmd: 'webpack', args: ['index.js'] },
]; ];
for (const dir of exampleDirs) { for (const dir of exampleDirs) {
@ -21,7 +21,7 @@ for (const dir of exampleDirs) {
// declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158
const opts = { const opts = {
cwd: path.join(__dirname, dir), cwd: path.join(__dirname, dir),
stdio: 'inherit' stdio: 'inherit',
}; };
let result = {}; let result = {};
if (process.platform === 'win32') { if (process.platform === 'win32') {

View File

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

View File

@ -3,13 +3,13 @@ export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
export function increment() { export function increment() {
return { return {
type: INCREMENT_COUNTER type: INCREMENT_COUNTER,
}; };
} }
export function decrement() { export function decrement() {
return { return {
type: DECREMENT_COUNTER type: DECREMENT_COUNTER,
}; };
} }
@ -26,7 +26,7 @@ export function incrementIfOdd() {
} }
export function incrementAsync(delay = 1000) { export function incrementAsync(delay = 1000) {
return dispatch => { return (dispatch) => {
setTimeout(() => { setTimeout(() => {
dispatch(increment()); dispatch(increment());
}, delay); }, delay);

View File

@ -2,17 +2,13 @@ import React, { Component, PropTypes } from 'react';
class Counter extends Component { class Counter extends Component {
render() { render() {
const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props; const { increment, incrementIfOdd, incrementAsync, decrement, counter } =
this.props;
return ( return (
<p> <p>
Clicked: {counter} times Clicked: {counter} times <button onClick={increment}>+</button>{' '}
{' '} <button onClick={decrement}>-</button>{' '}
<button onClick={increment}>+</button> <button onClick={incrementIfOdd}>Increment if odd</button>{' '}
{' '}
<button onClick={decrement}>-</button>
{' '}
<button onClick={incrementIfOdd}>Increment if odd</button>
{' '}
<button onClick={() => incrementAsync()}>Increment async</button> <button onClick={() => incrementAsync()}>Increment async</button>
</p> </p>
); );
@ -24,7 +20,7 @@ Counter.propTypes = {
incrementIfOdd: PropTypes.func.isRequired, incrementIfOdd: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired, incrementAsync: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired, decrement: PropTypes.func.isRequired,
counter: PropTypes.number.isRequired counter: PropTypes.number.isRequired,
}; };
export default Counter; export default Counter;

View File

@ -5,7 +5,7 @@ import * as CounterActions from '../actions/counter';
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
counter: state.counter counter: state.counter,
}; };
} }

View File

@ -4,8 +4,7 @@
<title>Redux counter example</title> <title>Redux counter example</title>
</head> </head>
<body> <body>
<div id="root"> <div id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -2,11 +2,11 @@ import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';
export default function counter(state = 0, action) { export default function counter(state = 0, action) {
switch (action.type) { switch (action.type) {
case INCREMENT_COUNTER: case INCREMENT_COUNTER:
return state + 1; return state + 1;
case DECREMENT_COUNTER: case DECREMENT_COUNTER:
return state - 1; return state - 1;
default: default:
return state; return state;
} }
} }

View File

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

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4001; var port = 4001;
var compiler = webpack(config); var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(
webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
})
);
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
app.get("/", function(req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.listen(port, function(error) { app.listen(port, function (error) {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); console.info(
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
port,
port
);
} }
}); });

View File

@ -6,11 +6,16 @@ import reducer from '../reducers';
import * as actionCreators from '../actions/counter'; import * as actionCreators from '../actions/counter';
export default function configureStore(initialState) { export default function configureStore(initialState) {
const composeEnhancers = composeWithDevTools({
const composeEnhancers = composeWithDevTools({ realtime: true, actionCreators, trace: true }); realtime: true,
const store = createStore(reducer, initialState, composeEnhancers( actionCreators,
applyMiddleware(invariant(), thunk) trace: true,
)); });
const store = createStore(
reducer,
initialState,
composeEnhancers(applyMiddleware(invariant(), thunk))
);
if (module.hot) { if (module.hot) {
// Enable Webpack hot module replacement for reducers // Enable Webpack hot module replacement for reducers

View File

@ -12,16 +12,17 @@ function mockStore(getState, expectedActions, onLastAction) {
if (!Array.isArray(expectedActions)) { if (!Array.isArray(expectedActions)) {
throw new Error('expectedActions should be an array of expected actions.'); throw new Error('expectedActions should be an array of expected actions.');
} }
if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') { if (
typeof onLastAction !== 'undefined' &&
typeof onLastAction !== 'function'
) {
throw new Error('onLastAction should either be undefined or function.'); throw new Error('onLastAction should either be undefined or function.');
} }
function mockStoreWithoutMiddleware() { function mockStoreWithoutMiddleware() {
return { return {
getState() { getState() {
return typeof getState === 'function' ? return typeof getState === 'function' ? getState() : getState;
getState() :
getState;
}, },
dispatch(action) { dispatch(action) {
@ -31,13 +32,13 @@ function mockStore(getState, expectedActions, onLastAction) {
onLastAction(); onLastAction();
} }
return action; return action;
} },
}; };
} }
const mockStoreWithMiddleware = applyMiddleware( const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
...middlewares mockStoreWithoutMiddleware
)(mockStoreWithoutMiddleware); );
return mockStoreWithMiddleware(); return mockStoreWithMiddleware();
} }
@ -52,9 +53,7 @@ describe('actions', () => {
}); });
it('incrementIfOdd should create increment action', (done) => { it('incrementIfOdd should create increment action', (done) => {
const expectedActions = [ const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
{ type: actions.INCREMENT_COUNTER }
];
const store = mockStore({ counter: 1 }, expectedActions, done); const store = mockStore({ counter: 1 }, expectedActions, done);
store.dispatch(actions.incrementIfOdd()); store.dispatch(actions.incrementIfOdd());
}); });
@ -67,9 +66,7 @@ describe('actions', () => {
}); });
it('incrementAsync should create increment action', (done) => { it('incrementAsync should create increment action', (done) => {
const expectedActions = [ const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
{ type: actions.INCREMENT_COUNTER }
];
const store = mockStore({ counter: 0 }, expectedActions, done); const store = mockStore({ counter: 0 }, expectedActions, done);
store.dispatch(actions.incrementAsync(100)); store.dispatch(actions.incrementAsync(100));
}); });

View File

@ -8,14 +8,16 @@ function setup() {
increment: expect.createSpy(), increment: expect.createSpy(),
incrementIfOdd: expect.createSpy(), incrementIfOdd: expect.createSpy(),
incrementAsync: expect.createSpy(), incrementAsync: expect.createSpy(),
decrement: expect.createSpy() decrement: expect.createSpy(),
}; };
const component = TestUtils.renderIntoDocument(<Counter counter={1} {...actions} />); const component = TestUtils.renderIntoDocument(
<Counter counter={1} {...actions} />
);
return { return {
component: component, component: component,
actions: actions, actions: actions,
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'), buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p') p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
}; };
} }

View File

@ -15,7 +15,7 @@ function setup(initialState) {
return { return {
app: app, app: app,
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'), buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p') p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
}; };
} }

View File

@ -3,28 +3,27 @@ var webpack = require('webpack');
module.exports = { module.exports = {
devtool: 'source-map', devtool: 'source-map',
entry: [ entry: ['webpack-hot-middleware/client', './index'],
'webpack-hot-middleware/client',
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
plugins: [ plugins: [
new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin() new webpack.NoErrorsPlugin(),
], ],
module: { module: {
loaders: [{ loaders: [
test: /\.js$/, {
loaders: ['babel'], test: /\.js$/,
exclude: /node_modules/, loaders: ['babel'],
include: __dirname exclude: /node_modules/,
}] include: __dirname,
} },
],
},
}; };
var src = path.join(__dirname, '..', '..', 'src'); var src = path.join(__dirname, '..', '..', 'src');
@ -37,6 +36,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
module.exports.module.loaders.push({ module.exports.module.loaders.push({
test: /\.js$/, test: /\.js$/,
loaders: ['babel'], loaders: ['babel'],
include: src include: src,
}); });
} }

View File

@ -1,26 +1,28 @@
var createStore = require('redux').createStore var createStore = require('redux').createStore;
var devTools = require('remote-redux-devtools').default var devTools = require('remote-redux-devtools').default;
function counter(state, action) { function counter(state, action) {
if (state === undefined) state = 0 if (state === undefined) state = 0;
switch (action.type) { switch (action.type) {
case 'INCREMENT': case 'INCREMENT':
return state + 1 return state + 1;
case 'DECREMENT': case 'DECREMENT':
return state - 1 return state - 1;
default: default:
return state return state;
} }
} }
var store = createStore(counter, devTools({realtime: true})) var store = createStore(counter, devTools({ realtime: true }));
store.subscribe(function() { console.log(store.getState()) }) store.subscribe(function () {
console.log(store.getState());
});
function incrementer() { function incrementer() {
setTimeout(function() { setTimeout(function () {
store.dispatch({ type: 'INCREMENT' }) store.dispatch({ type: 'INCREMENT' });
incrementer() incrementer();
}, 1000) }, 1000);
} }
incrementer() incrementer();

View File

@ -1,4 +1,4 @@
{ {
"presets": [ "es2015", "stage-0", "react" ], "presets": ["es2015", "stage-0", "react"],
"plugins": [ "transform-decorators-legacy" ] "plugins": ["transform-decorators-legacy"]
} }

View File

@ -1,11 +1,15 @@
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const FILTER_TITLES = { const FILTER_TITLES = {
[SHOW_ALL]: 'All', [SHOW_ALL]: 'All',
[SHOW_ACTIVE]: 'Active', [SHOW_ACTIVE]: 'Active',
[SHOW_COMPLETED]: 'Completed' [SHOW_COMPLETED]: 'Completed',
}; };
class Footer extends Component { class Footer extends Component {
@ -25,9 +29,11 @@ class Footer extends Component {
const { filter: selectedFilter, onShow } = this.props; const { filter: selectedFilter, onShow } = this.props;
return ( return (
<a className={classnames({ selected: filter === selectedFilter })} <a
style={{ cursor: 'pointer' }} className={classnames({ selected: filter === selectedFilter })}
onClick={() => onShow(filter)}> style={{ cursor: 'pointer' }}
onClick={() => onShow(filter)}
>
{title} {title}
</a> </a>
); );
@ -37,8 +43,7 @@ class Footer extends Component {
const { completedCount, onClearCompleted } = this.props; const { completedCount, onClearCompleted } = this.props;
if (completedCount > 0) { if (completedCount > 0) {
return ( return (
<button className="clear-completed" <button className="clear-completed" onClick={onClearCompleted}>
onClick={onClearCompleted} >
Clear completed Clear completed
</button> </button>
); );
@ -50,11 +55,9 @@ class Footer extends Component {
<footer className="footer"> <footer className="footer">
{this.renderTodoCount()} {this.renderTodoCount()}
<ul className="filters"> <ul className="filters">
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter => {[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
<li key={filter}> <li key={filter}>{this.renderFilterLink(filter)}</li>
{this.renderFilterLink(filter)} ))}
</li>
)}
</ul> </ul>
{this.renderClearButton()} {this.renderClearButton()}
</footer> </footer>
@ -67,7 +70,7 @@ Footer.propTypes = {
activeCount: PropTypes.number.isRequired, activeCount: PropTypes.number.isRequired,
filter: PropTypes.string.isRequired, filter: PropTypes.string.isRequired,
onClearCompleted: PropTypes.func.isRequired, onClearCompleted: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired onShow: PropTypes.func.isRequired,
}; };
export default Footer; export default Footer;

View File

@ -12,17 +12,19 @@ class Header extends Component {
const { path } = this.props; const { path } = this.props;
return ( return (
<header className="header"> <header className="header">
<h1 style={{ fontSize: 80 }}>{path}</h1> <h1 style={{ fontSize: 80 }}>{path}</h1>
<TodoTextInput newTodo <TodoTextInput
onSave={this.handleSave.bind(this)} newTodo
placeholder="What needs to be done?" /> onSave={this.handleSave.bind(this)}
placeholder="What needs to be done?"
/>
</header> </header>
); );
} }
} }
Header.propTypes = { Header.propTypes = {
addTodo: PropTypes.func.isRequired addTodo: PropTypes.func.isRequired,
}; };
export default Header; export default Header;

View File

@ -1,12 +1,16 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import TodoItem from './TodoItem'; import TodoItem from './TodoItem';
import Footer from './Footer'; import Footer from './Footer';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const TODO_FILTERS = { const TODO_FILTERS = {
[SHOW_ALL]: () => true, [SHOW_ALL]: () => true,
[SHOW_ACTIVE]: todo => !todo.completed, [SHOW_ACTIVE]: (todo) => !todo.completed,
[SHOW_COMPLETED]: todo => todo.completed [SHOW_COMPLETED]: (todo) => todo.completed,
}; };
class MainSection extends Component { class MainSection extends Component {
@ -16,7 +20,7 @@ class MainSection extends Component {
} }
handleClearCompleted() { handleClearCompleted() {
const atLeastOneCompleted = this.props.todos.some(todo => todo.completed); const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
if (atLeastOneCompleted) { if (atLeastOneCompleted) {
this.props.actions.clearCompleted(); this.props.actions.clearCompleted();
} }
@ -30,10 +34,12 @@ class MainSection extends Component {
const { todos, actions } = this.props; const { todos, actions } = this.props;
if (todos.length > 0) { if (todos.length > 0) {
return ( return (
<input className="toggle-all" <input
type="checkbox" className="toggle-all"
checked={completedCount === todos.length} type="checkbox"
onChange={actions.completeAll} /> checked={completedCount === todos.length}
onChange={actions.completeAll}
/>
); );
} }
} }
@ -45,11 +51,13 @@ class MainSection extends Component {
if (todos.length) { if (todos.length) {
return ( return (
<Footer completedCount={completedCount} <Footer
activeCount={activeCount} completedCount={completedCount}
filter={filter} activeCount={activeCount}
onClearCompleted={this.handleClearCompleted.bind(this)} filter={filter}
onShow={this.handleShow.bind(this)} /> onClearCompleted={this.handleClearCompleted.bind(this)}
onShow={this.handleShow.bind(this)}
/>
); );
} }
} }
@ -59,8 +67,8 @@ class MainSection extends Component {
const { filter } = this.state; const { filter } = this.state;
const filteredTodos = todos.filter(TODO_FILTERS[filter]); const filteredTodos = todos.filter(TODO_FILTERS[filter]);
const completedCount = todos.reduce((count, todo) => const completedCount = todos.reduce(
todo.completed ? count + 1 : count, (count, todo) => (todo.completed ? count + 1 : count),
0 0
); );
@ -68,9 +76,9 @@ class MainSection extends Component {
<section className="main"> <section className="main">
{this.renderToggleAll(completedCount)} {this.renderToggleAll(completedCount)}
<ul className="todo-list"> <ul className="todo-list">
{filteredTodos.map(todo => {filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} {...actions} /> <TodoItem key={todo.id} todo={todo} {...actions} />
)} ))}
</ul> </ul>
{this.renderFooter(completedCount)} {this.renderFooter(completedCount)}
</section> </section>
@ -80,7 +88,7 @@ class MainSection extends Component {
MainSection.propTypes = { MainSection.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
export default MainSection; export default MainSection;

View File

@ -6,7 +6,7 @@ class TodoItem extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
editing: false editing: false,
}; };
} }
@ -24,36 +24,41 @@ class TodoItem extends Component {
} }
render() { render() {
const {todo, completeTodo, deleteTodo} = this.props; const { todo, completeTodo, deleteTodo } = this.props;
let element; let element;
if (this.state.editing) { if (this.state.editing) {
element = ( element = (
<TodoTextInput text={todo.text} <TodoTextInput
editing={this.state.editing} text={todo.text}
onSave={(text) => this.handleSave(todo.id, text)} /> editing={this.state.editing}
onSave={(text) => this.handleSave(todo.id, text)}
/>
); );
} else { } else {
element = ( element = (
<div className="view"> <div className="view">
<input className="toggle" <input
type="checkbox" className="toggle"
checked={todo.completed} type="checkbox"
onChange={() => completeTodo(todo.id)} /> checked={todo.completed}
onChange={() => completeTodo(todo.id)}
/>
<label onDoubleClick={this.handleDoubleClick.bind(this)}> <label onDoubleClick={this.handleDoubleClick.bind(this)}>
{todo.text} {todo.text}
</label> </label>
<button className="destroy" <button className="destroy" onClick={() => deleteTodo(todo.id)} />
onClick={() => deleteTodo(todo.id)} />
</div> </div>
); );
} }
return ( return (
<li className={classnames({ <li
completed: todo.completed, className={classnames({
editing: this.state.editing completed: todo.completed,
})}> editing: this.state.editing,
})}
>
{element} {element}
</li> </li>
); );
@ -64,7 +69,7 @@ TodoItem.propTypes = {
todo: PropTypes.object.isRequired, todo: PropTypes.object.isRequired,
editTodo: PropTypes.func.isRequired, editTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired, deleteTodo: PropTypes.func.isRequired,
completeTodo: PropTypes.func.isRequired completeTodo: PropTypes.func.isRequired,
}; };
export default TodoItem; export default TodoItem;

View File

@ -5,7 +5,7 @@ class TodoTextInput extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
text: this.props.text || '' text: this.props.text || '',
}; };
} }
@ -31,10 +31,10 @@ class TodoTextInput extends Component {
render() { render() {
return ( return (
<input className={ <input
classnames({ className={classnames({
edit: this.props.editing, edit: this.props.editing,
'new-todo': this.props.newTodo 'new-todo': this.props.newTodo,
})} })}
type="text" type="text"
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
@ -42,7 +42,8 @@ class TodoTextInput extends Component {
value={this.state.text} value={this.state.text}
onBlur={this.handleBlur.bind(this)} onBlur={this.handleBlur.bind(this)}
onChange={this.handleChange.bind(this)} onChange={this.handleChange.bind(this)}
onKeyDown={this.handleSubmit.bind(this)} /> onKeyDown={this.handleSubmit.bind(this)}
/>
); );
} }
} }
@ -52,7 +53,7 @@ TodoTextInput.propTypes = {
text: PropTypes.string, text: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
editing: PropTypes.bool, editing: PropTypes.bool,
newTodo: PropTypes.bool newTodo: PropTypes.bool,
}; };
export default TodoTextInput; export default TodoTextInput;

View File

@ -19,23 +19,20 @@ class App extends Component {
App.propTypes = { App.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
todos: state.todos, todos: state.todos,
path: state.router.location.pathname path: state.router.location.pathname,
}; };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
actions: bindActionCreators(TodoActions, dispatch) actions: bindActionCreators(TodoActions, dispatch),
}; };
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(App);
mapStateToProps,
mapDispatchToProps
)(App);

View File

@ -9,9 +9,9 @@ class Root extends Component {
render() { render() {
return ( return (
<ReduxRouter> <ReduxRouter>
<Redirect from="/" to="Standard Todo"/> <Redirect from="/" to="Standard Todo" />
<Route path="/" component={Wrapper}> <Route path="/" component={Wrapper}>
<Route path="/:id" component={App}/> <Route path="/:id" component={App} />
</Route> </Route>
</ReduxRouter> </ReduxRouter>
); );

View File

@ -8,14 +8,14 @@ import * as TodoActions from '../actions/todos';
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
pushState: bindActionCreators(pushState, dispatch), pushState: bindActionCreators(pushState, dispatch),
actions: bindActionCreators(TodoActions, dispatch) actions: bindActionCreators(TodoActions, dispatch),
}; };
} }
@connect((state) => ({}), mapDispatchToProps) @connect((state) => ({}), mapDispatchToProps)
class Wrapper extends Component { class Wrapper extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node children: PropTypes.node,
}; };
constructor(props) { constructor(props) {
@ -41,11 +41,23 @@ class Wrapper extends Component {
} }
render() { render() {
return ( return (
<div> <div>
<div style={{ padding: 20, backgroundColor: '#eee', fontWeight: 'bold', textAlign: 'center' }}> <div
<a href="#" onClick={this.handleClick}>Standard Todo</a> | <a href="#" onClick={this.handleClick}>AutoTodo</a> style={{
padding: 20,
backgroundColor: '#eee',
fontWeight: 'bold',
textAlign: 'center',
}}
>
<a href="#" onClick={this.handleClick}>
Standard Todo
</a>{' '}
|{' '}
<a href="#" onClick={this.handleClick}>
AutoTodo
</a>
</div> </div>
{this.props.children} {this.props.children}
</div> </div>

View File

@ -4,8 +4,7 @@
<title>Redux TodoMVC example</title> <title>Redux TodoMVC example</title>
</head> </head>
<body> <body>
<div class="todoapp" id="root"> <div class="todoapp" id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -4,7 +4,7 @@ import todos from './todos';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
todos, todos,
router: routerStateReducer router: routerStateReducer,
}); });
export default rootReducer; export default rootReducer;

View File

@ -1,49 +1,61 @@
import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'; import {
ADD_TODO,
DELETE_TODO,
EDIT_TODO,
COMPLETE_TODO,
COMPLETE_ALL,
CLEAR_COMPLETED,
} from '../constants/ActionTypes';
const initialState = [{ const initialState = [
text: 'Use Redux', {
completed: false, text: 'Use Redux',
id: 0 completed: false,
}]; id: 0,
},
];
export default function todos(state = initialState, action) { export default function todos(state = initialState, action) {
switch (action.type) { switch (action.type) {
case ADD_TODO: case ADD_TODO:
return [{ return [
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, {
completed: false, id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
text: action.text completed: false,
}, ...state]; text: action.text,
},
...state,
];
case DELETE_TODO: case DELETE_TODO:
return state.filter(todo => return state.filter((todo) => todo.id !== action.id);
todo.id !== action.id
);
case EDIT_TODO: case EDIT_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { text: action.text }) : ? Object.assign({}, todo, { text: action.text })
todo : todo
); );
case COMPLETE_TODO: case COMPLETE_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { completed: !todo.completed }) : ? Object.assign({}, todo, { completed: !todo.completed })
todo : todo
); );
case COMPLETE_ALL: case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed); const areAllMarked = state.every((todo) => todo.completed);
return state.map(todo => Object.assign({}, todo, { return state.map((todo) =>
completed: !areAllMarked Object.assign({}, todo, {
})); completed: !areAllMarked,
})
);
case CLEAR_COMPLETED: case CLEAR_COMPLETED:
return state.filter(todo => todo.completed === false); return state.filter((todo) => todo.completed === false);
default: default:
return state; return state;
} }
} }

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4002; var port = 4002;
var compiler = webpack(config); var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(
webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
})
);
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
app.get("/", function(req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.listen(port, function(error) { app.listen(port, function (error) {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); console.info(
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
port,
port
);
} }
}); });

View File

@ -1,5 +1,9 @@
import { createStore, compose } from 'redux'; import { createStore, compose } from 'redux';
import { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router'; import {
reduxReactRouter,
routerStateReducer,
ReduxRouter,
} from 'redux-router';
//import createHistory from 'history/lib/createBrowserHistory'; //import createHistory from 'history/lib/createBrowserHistory';
import devTools from 'remote-redux-devtools'; import devTools from 'remote-redux-devtools';
import createHistory from 'history/lib/createHashHistory'; import createHistory from 'history/lib/createHashHistory';

View File

@ -6,14 +6,14 @@ describe('todo actions', () => {
it('addTodo should create ADD_TODO action', () => { it('addTodo should create ADD_TODO action', () => {
expect(actions.addTodo('Use Redux')).toEqual({ expect(actions.addTodo('Use Redux')).toEqual({
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Use Redux' text: 'Use Redux',
}); });
}); });
it('deleteTodo should create DELETE_TODO action', () => { it('deleteTodo should create DELETE_TODO action', () => {
expect(actions.deleteTodo(1)).toEqual({ expect(actions.deleteTodo(1)).toEqual({
type: types.DELETE_TODO, type: types.DELETE_TODO,
id: 1 id: 1,
}); });
}); });
@ -21,26 +21,26 @@ describe('todo actions', () => {
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
type: types.EDIT_TODO, type: types.EDIT_TODO,
id: 1, id: 1,
text: 'Use Redux everywhere' text: 'Use Redux everywhere',
}); });
}); });
it('completeTodo should create COMPLETE_TODO action', () => { it('completeTodo should create COMPLETE_TODO action', () => {
expect(actions.completeTodo(1)).toEqual({ expect(actions.completeTodo(1)).toEqual({
type: types.COMPLETE_TODO, type: types.COMPLETE_TODO,
id: 1 id: 1,
}); });
}); });
it('completeAll should create COMPLETE_ALL action', () => { it('completeAll should create COMPLETE_ALL action', () => {
expect(actions.completeAll()).toEqual({ expect(actions.completeAll()).toEqual({
type: types.COMPLETE_ALL type: types.COMPLETE_ALL,
}); });
}); });
it('clearCompleted should create CLEAR_COMPLETED action', () => { it('clearCompleted should create CLEAR_COMPLETED action', () => {
expect(actions.clearCompleted('Use Redux')).toEqual({ expect(actions.clearCompleted('Use Redux')).toEqual({
type: types.CLEAR_COMPLETED type: types.CLEAR_COMPLETED,
}); });
}); });
}); });

View File

@ -5,13 +5,16 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
completedCount: 0, {
activeCount: 0, completedCount: 0,
filter: SHOW_ALL, activeCount: 0,
onClearCompleted: expect.createSpy(), filter: SHOW_ALL,
onShow: expect.createSpy() onClearCompleted: expect.createSpy(),
}, propOverrides); onShow: expect.createSpy(),
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<Footer {...props} />); renderer.render(<Footer {...props} />);
@ -19,13 +22,14 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output output: output,
}; };
} }
function getTextContent(elem) { function getTextContent(elem) {
const children = Array.isArray(elem.props.children) ? const children = Array.isArray(elem.props.children)
elem.props.children : [elem.props.children]; ? elem.props.children
: [elem.props.children];
return children.reduce(function concatText(out, child) { return children.reduce(function concatText(out, child) {
// Children are either elements or text strings // Children are either elements or text strings
@ -63,11 +67,13 @@ describe('components', () => {
expect(filter.type).toBe('li'); expect(filter.type).toBe('li');
const a = filter.props.children; const a = filter.props.children;
expect(a.props.className).toBe(i === 0 ? 'selected' : ''); expect(a.props.className).toBe(i === 0 ? 'selected' : '');
expect(a.props.children).toBe({ expect(a.props.children).toBe(
0: 'All', {
1: 'Active', 0: 'All',
2: 'Completed' 1: 'Active',
}[i]); 2: 'Completed',
}[i]
);
}); });
}); });
@ -81,20 +87,20 @@ describe('components', () => {
it('shouldnt show clear button when no completed todos', () => { it('shouldnt show clear button when no completed todos', () => {
const { output } = setup({ completedCount: 0 }); const { output } = setup({ completedCount: 0 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear).toBe(undefined); expect(clear).toBe(undefined);
}); });
it('should render clear button when completed todos', () => { it('should render clear button when completed todos', () => {
const { output } = setup({ completedCount: 1 }); const { output } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear.type).toBe('button'); expect(clear.type).toBe('button');
expect(clear.props.children).toBe('Clear completed'); expect(clear.props.children).toBe('Clear completed');
}); });
it('should call onClearCompleted on clear button click', () => { it('should call onClearCompleted on clear button click', () => {
const { output, props } = setup({ completedCount: 1 }); const { output, props } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
clear.props.onClick({}); clear.props.onClick({});
expect(props.onClearCompleted).toHaveBeenCalled(); expect(props.onClearCompleted).toHaveBeenCalled();
}); });

View File

@ -6,7 +6,7 @@ import TodoTextInput from '../../components/TodoTextInput';
function setup() { function setup() {
const props = { const props = {
addTodo: expect.createSpy() addTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
@ -16,7 +16,7 @@ function setup() {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -7,24 +7,30 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
todos: [{ {
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}, { completed: false,
text: 'Run the tests', id: 0,
completed: true, },
id: 1 {
}], text: 'Run the tests',
actions: { completed: true,
editTodo: expect.createSpy(), id: 1,
deleteTodo: expect.createSpy(), },
completeTodo: expect.createSpy(), ],
completeAll: expect.createSpy(), actions: {
clearCompleted: expect.createSpy() editTodo: expect.createSpy(),
} deleteTodo: expect.createSpy(),
}, propOverrides); completeTodo: expect.createSpy(),
completeAll: expect.createSpy(),
clearCompleted: expect.createSpy(),
},
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<MainSection {...props} />); renderer.render(<MainSection {...props} />);
@ -33,7 +39,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -55,11 +61,15 @@ describe('components', () => {
}); });
it('should be checked if all todos completed', () => { it('should be checked if all todos completed', () => {
const { output } = setup({ todos: [{ const { output } = setup({
text: 'Use Redux', todos: [
completed: true, {
id: 0 text: 'Use Redux',
}]}); completed: true,
id: 0,
},
],
});
const [toggle] = output.props.children; const [toggle] = output.props.children;
expect(toggle.props.checked).toBe(true); expect(toggle.props.checked).toBe(true);
}); });
@ -75,7 +85,7 @@ describe('components', () => {
describe('footer', () => { describe('footer', () => {
it('should render', () => { it('should render', () => {
const { output } = setup(); const { output } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
expect(footer.type).toBe(Footer); expect(footer.type).toBe(Footer);
expect(footer.props.completedCount).toBe(1); expect(footer.props.completedCount).toBe(1);
expect(footer.props.activeCount).toBe(1); expect(footer.props.activeCount).toBe(1);
@ -84,27 +94,31 @@ describe('components', () => {
it('onShow should set the filter', () => { it('onShow should set the filter', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [,, updatedFooter] = updated.props.children; const [, , updatedFooter] = updated.props.children;
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED); expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
}); });
it('onClearCompleted should call clearCompleted', () => { it('onClearCompleted should call clearCompleted', () => {
const { output, props } = setup(); const { output, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted).toHaveBeenCalled(); expect(props.actions.clearCompleted).toHaveBeenCalled();
}); });
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => { it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
const { output, props } = setup({ todos: [{ const { output, props } = setup({
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}]}); completed: false,
const [,, footer] = output.props.children; id: 0,
},
],
});
const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted.calls.length).toBe(0); expect(props.actions.clearCompleted.calls.length).toBe(0);
}); });
@ -124,7 +138,7 @@ describe('components', () => {
it('should filter items', () => { it('should filter items', () => {
const { output, renderer, props } = setup(); const { output, renderer, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [, updatedList] = updated.props.children; const [, updatedList] = updated.props.children;

View File

@ -4,23 +4,21 @@ import TestUtils from 'react-addons-test-utils';
import TodoItem from '../../components/TodoItem'; import TodoItem from '../../components/TodoItem';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup( editing = false ) { function setup(editing = false) {
const props = { const props = {
todo: { todo: {
id: 0, id: 0,
text: 'Use Redux', text: 'Use Redux',
completed: false completed: false,
}, },
editTodo: expect.createSpy(), editTodo: expect.createSpy(),
deleteTodo: expect.createSpy(), deleteTodo: expect.createSpy(),
completeTodo: expect.createSpy() completeTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoItem {...props} />);
<TodoItem {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -33,7 +31,7 @@ function setup( editing = false ) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -4,19 +4,20 @@ import TestUtils from 'react-addons-test-utils';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
onSave: expect.createSpy(), {
text: 'Use Redux', onSave: expect.createSpy(),
placeholder: 'What needs to be done?', text: 'Use Redux',
editing: false, placeholder: 'What needs to be done?',
newTodo: false editing: false,
}, propOverrides); newTodo: false,
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoTextInput {...props} />);
<TodoTextInput {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -25,7 +26,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -50,33 +51,33 @@ describe('components', () => {
it('should update value on change', () => { it('should update value on change', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
output.props.onChange({ target: { value: 'Use Radox' }}); output.props.onChange({ target: { value: 'Use Radox' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual('Use Radox'); expect(updated.props.value).toEqual('Use Radox');
}); });
it('should call onSave on return key press', () => { it('should call onSave on return key press', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('should reset state on return key press if newTodo', () => { it('should reset state on return key press if newTodo', () => {
const { output, renderer } = setup({ newTodo: true }); const { output, renderer } = setup({ newTodo: true });
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual(''); expect(updated.props.value).toEqual('');
}); });
it('should call onSave on blur', () => { it('should call onSave on blur', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('shouldnt call onSave on blur if newTodo', () => { it('shouldnt call onSave on blur if newTodo', () => {
const { output, props } = setup({ newTodo: true }); const { output, props } = setup({ newTodo: true });
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave.calls.length).toBe(0); expect(props.onSave.calls.length).toBe(0);
}); });
}); });

View File

@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
describe('todos reducer', () => { describe('todos reducer', () => {
it('should handle initial state', () => { it('should handle initial state', () => {
expect( expect(todos(undefined, {})).toEqual([
todos(undefined, {}) {
).toEqual([{ text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ]);
}); });
it('should handle ADD_TODO', () => { it('should handle ADD_TODO', () => {
expect( expect(
todos([], { todos([], {
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Run the tests' text: 'Run the tests',
}) })
).toEqual([{ ).toEqual([
text: 'Run the tests', {
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.ADD_TODO,
text: 'Run the tests'
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: false,
id: 1 id: 0,
}, { },
]);
expect(
todos(
[
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.ADD_TODO,
text: 'Run the tests',
}
)
).toEqual([
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.ADD_TODO, ]);
text: 'Fix the tests'
}) expect(
).toEqual([{ todos(
text: 'Fix the tests', [
completed: false, {
id: 2 text: 'Run the tests',
}, { completed: false,
text: 'Run the tests', id: 1,
completed: false, },
id: 1 {
}, { text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ],
{
type: types.ADD_TODO,
text: 'Fix the tests',
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false,
id: 2,
},
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
]);
}); });
it('should handle DELETE_TODO', () => { it('should handle DELETE_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: false, {
id: 1 text: 'Run the tests',
}, { completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.DELETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.DELETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle EDIT_TODO', () => { it('should handle EDIT_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.EDIT_TODO,
text: 'Fix the tests',
id: 1,
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.EDIT_TODO, ]);
text: 'Fix the tests',
id: 1
})
).toEqual([{
text: 'Fix the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_TODO', () => { it('should handle COMPLETE_TODO', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_ALL', () => { it('should handle COMPLETE_ALL', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: true,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: true,
id: 0
}]);
// Unmark if all todos are currently completed // Unmark if all todos are currently completed
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: true,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: true, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle CLEAR_COMPLETED', () => { it('should handle CLEAR_COMPLETED', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: true, {
id: 1 text: 'Run the tests',
}, { completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.CLEAR_COMPLETED,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.CLEAR_COMPLETED ]);
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should not generate duplicate ids after CLEAR_COMPLETED', () => { it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
expect( expect(
[{ [
type: types.COMPLETE_TODO, {
id: 0 type: types.COMPLETE_TODO,
}, { id: 0,
type: types.CLEAR_COMPLETED },
}, { {
type: types.ADD_TODO, type: types.CLEAR_COMPLETED,
text: 'Write more tests' },
}].reduce(todos, [{ {
id: 0, type: types.ADD_TODO,
text: 'Write more tests',
},
].reduce(todos, [
{
id: 0,
completed: false,
text: 'Use Redux',
},
{
id: 1,
completed: false,
text: 'Write tests',
},
])
).toEqual([
{
text: 'Write more tests',
completed: false,
id: 2,
},
{
text: 'Write tests',
completed: false, completed: false,
text: 'Use Redux'
}, {
id: 1, id: 1,
completed: false, },
text: 'Write tests' ]);
}])
).toEqual([{
text: 'Write more tests',
completed: false,
id: 2
}, {
text: 'Write tests',
completed: false,
id: 1
}]);
}); });
}); });

View File

@ -3,32 +3,32 @@ var webpack = require('webpack');
module.exports = { module.exports = {
devtool: 'cheap-module-eval-source-map', devtool: 'cheap-module-eval-source-map',
entry: [ entry: ['webpack-hot-middleware/client', './index'],
'webpack-hot-middleware/client',
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
plugins: [ plugins: [
new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin() new webpack.NoErrorsPlugin(),
], ],
module: { module: {
loaders: [{ loaders: [
test: /\.js$/, {
loaders: ['babel'], test: /\.js$/,
exclude: /node_modules/, loaders: ['babel'],
include: __dirname exclude: /node_modules/,
}, { include: __dirname,
test: /\.css?$/, },
loaders: ['style', 'raw'], {
include: __dirname test: /\.css?$/,
}] loaders: ['style', 'raw'],
} include: __dirname,
},
],
},
}; };
var src = path.join(__dirname, '..', '..', 'src'); var src = path.join(__dirname, '..', '..', 'src');
@ -41,6 +41,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
module.exports.module.loaders.push({ module.exports.module.loaders.push({
test: /\.js$/, test: /\.js$/,
loaders: ['babel'], loaders: ['babel'],
include: src include: src,
}); });
} }

View File

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

View File

@ -1,11 +1,15 @@
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const FILTER_TITLES = { const FILTER_TITLES = {
[SHOW_ALL]: 'All', [SHOW_ALL]: 'All',
[SHOW_ACTIVE]: 'Active', [SHOW_ACTIVE]: 'Active',
[SHOW_COMPLETED]: 'Completed' [SHOW_COMPLETED]: 'Completed',
}; };
class Footer extends Component { class Footer extends Component {
@ -25,9 +29,11 @@ class Footer extends Component {
const { filter: selectedFilter, onShow } = this.props; const { filter: selectedFilter, onShow } = this.props;
return ( return (
<a className={classnames({ selected: filter === selectedFilter })} <a
style={{ cursor: 'pointer' }} className={classnames({ selected: filter === selectedFilter })}
onClick={() => onShow(filter)}> style={{ cursor: 'pointer' }}
onClick={() => onShow(filter)}
>
{title} {title}
</a> </a>
); );
@ -37,8 +43,7 @@ class Footer extends Component {
const { completedCount, onClearCompleted } = this.props; const { completedCount, onClearCompleted } = this.props;
if (completedCount > 0) { if (completedCount > 0) {
return ( return (
<button className="clear-completed" <button className="clear-completed" onClick={onClearCompleted}>
onClick={onClearCompleted} >
Clear completed Clear completed
</button> </button>
); );
@ -50,11 +55,9 @@ class Footer extends Component {
<footer className="footer"> <footer className="footer">
{this.renderTodoCount()} {this.renderTodoCount()}
<ul className="filters"> <ul className="filters">
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter => {[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
<li key={filter}> <li key={filter}>{this.renderFilterLink(filter)}</li>
{this.renderFilterLink(filter)} ))}
</li>
)}
</ul> </ul>
{this.renderClearButton()} {this.renderClearButton()}
</footer> </footer>
@ -67,7 +70,7 @@ Footer.propTypes = {
activeCount: PropTypes.number.isRequired, activeCount: PropTypes.number.isRequired,
filter: PropTypes.string.isRequired, filter: PropTypes.string.isRequired,
onClearCompleted: PropTypes.func.isRequired, onClearCompleted: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired onShow: PropTypes.func.isRequired,
}; };
export default Footer; export default Footer;

View File

@ -11,17 +11,19 @@ class Header extends Component {
render() { render() {
return ( return (
<header className="header"> <header className="header">
<h1>todos</h1> <h1>todos</h1>
<TodoTextInput newTodo <TodoTextInput
onSave={this.handleSave.bind(this)} newTodo
placeholder="What needs to be done?" /> onSave={this.handleSave.bind(this)}
placeholder="What needs to be done?"
/>
</header> </header>
); );
} }
} }
Header.propTypes = { Header.propTypes = {
addTodo: PropTypes.func.isRequired addTodo: PropTypes.func.isRequired,
}; };
export default Header; export default Header;

View File

@ -1,12 +1,16 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import TodoItem from './TodoItem'; import TodoItem from './TodoItem';
import Footer from './Footer'; import Footer from './Footer';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const TODO_FILTERS = { const TODO_FILTERS = {
[SHOW_ALL]: () => true, [SHOW_ALL]: () => true,
[SHOW_ACTIVE]: todo => !todo.completed, [SHOW_ACTIVE]: (todo) => !todo.completed,
[SHOW_COMPLETED]: todo => todo.completed [SHOW_COMPLETED]: (todo) => todo.completed,
}; };
class MainSection extends Component { class MainSection extends Component {
@ -16,7 +20,7 @@ class MainSection extends Component {
} }
handleClearCompleted() { handleClearCompleted() {
const atLeastOneCompleted = this.props.todos.some(todo => todo.completed); const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
if (atLeastOneCompleted) { if (atLeastOneCompleted) {
this.props.actions.clearCompleted(); this.props.actions.clearCompleted();
} }
@ -30,10 +34,12 @@ class MainSection extends Component {
const { todos, actions } = this.props; const { todos, actions } = this.props;
if (todos.length > 0) { if (todos.length > 0) {
return ( return (
<input className="toggle-all" <input
type="checkbox" className="toggle-all"
checked={completedCount === todos.length} type="checkbox"
onChange={actions.completeAll} /> checked={completedCount === todos.length}
onChange={actions.completeAll}
/>
); );
} }
} }
@ -45,11 +51,13 @@ class MainSection extends Component {
if (todos.length) { if (todos.length) {
return ( return (
<Footer completedCount={completedCount} <Footer
activeCount={activeCount} completedCount={completedCount}
filter={filter} activeCount={activeCount}
onClearCompleted={this.handleClearCompleted.bind(this)} filter={filter}
onShow={this.handleShow.bind(this)} /> onClearCompleted={this.handleClearCompleted.bind(this)}
onShow={this.handleShow.bind(this)}
/>
); );
} }
} }
@ -59,8 +67,8 @@ class MainSection extends Component {
const { filter } = this.state; const { filter } = this.state;
const filteredTodos = todos.filter(TODO_FILTERS[filter]); const filteredTodos = todos.filter(TODO_FILTERS[filter]);
const completedCount = todos.reduce((count, todo) => const completedCount = todos.reduce(
todo.completed ? count + 1 : count, (count, todo) => (todo.completed ? count + 1 : count),
0 0
); );
@ -68,9 +76,9 @@ class MainSection extends Component {
<section className="main"> <section className="main">
{this.renderToggleAll(completedCount)} {this.renderToggleAll(completedCount)}
<ul className="todo-list"> <ul className="todo-list">
{filteredTodos.map(todo => {filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} {...actions} /> <TodoItem key={todo.id} todo={todo} {...actions} />
)} ))}
</ul> </ul>
{this.renderFooter(completedCount)} {this.renderFooter(completedCount)}
</section> </section>
@ -80,7 +88,7 @@ class MainSection extends Component {
MainSection.propTypes = { MainSection.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
export default MainSection; export default MainSection;

View File

@ -6,7 +6,7 @@ class TodoItem extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
editing: false editing: false,
}; };
} }
@ -24,36 +24,41 @@ class TodoItem extends Component {
} }
render() { render() {
const {todo, completeTodo, deleteTodo} = this.props; const { todo, completeTodo, deleteTodo } = this.props;
let element; let element;
if (this.state.editing) { if (this.state.editing) {
element = ( element = (
<TodoTextInput text={todo.text} <TodoTextInput
editing={this.state.editing} text={todo.text}
onSave={(text) => this.handleSave(todo.id, text)} /> editing={this.state.editing}
onSave={(text) => this.handleSave(todo.id, text)}
/>
); );
} else { } else {
element = ( element = (
<div className="view"> <div className="view">
<input className="toggle" <input
type="checkbox" className="toggle"
checked={todo.completed} type="checkbox"
onChange={() => completeTodo(todo.id)} /> checked={todo.completed}
onChange={() => completeTodo(todo.id)}
/>
<label onDoubleClick={this.handleDoubleClick.bind(this)}> <label onDoubleClick={this.handleDoubleClick.bind(this)}>
{todo.text} {todo.text}
</label> </label>
<button className="destroy" <button className="destroy" onClick={() => deleteTodo(todo.id)} />
onClick={() => deleteTodo(todo.id)} />
</div> </div>
); );
} }
return ( return (
<li className={classnames({ <li
completed: todo.completed, className={classnames({
editing: this.state.editing completed: todo.completed,
})}> editing: this.state.editing,
})}
>
{element} {element}
</li> </li>
); );
@ -64,7 +69,7 @@ TodoItem.propTypes = {
todo: PropTypes.object.isRequired, todo: PropTypes.object.isRequired,
editTodo: PropTypes.func.isRequired, editTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired, deleteTodo: PropTypes.func.isRequired,
completeTodo: PropTypes.func.isRequired completeTodo: PropTypes.func.isRequired,
}; };
export default TodoItem; export default TodoItem;

View File

@ -5,7 +5,7 @@ class TodoTextInput extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
text: this.props.text || '' text: this.props.text || '',
}; };
} }
@ -31,10 +31,10 @@ class TodoTextInput extends Component {
render() { render() {
return ( return (
<input className={ <input
classnames({ className={classnames({
edit: this.props.editing, edit: this.props.editing,
'new-todo': this.props.newTodo 'new-todo': this.props.newTodo,
})} })}
type="text" type="text"
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
@ -42,7 +42,8 @@ class TodoTextInput extends Component {
value={this.state.text} value={this.state.text}
onBlur={this.handleBlur.bind(this)} onBlur={this.handleBlur.bind(this)}
onChange={this.handleChange.bind(this)} onChange={this.handleChange.bind(this)}
onKeyDown={this.handleSubmit.bind(this)} /> onKeyDown={this.handleSubmit.bind(this)}
/>
); );
} }
} }
@ -52,7 +53,7 @@ TodoTextInput.propTypes = {
text: PropTypes.string, text: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
editing: PropTypes.bool, editing: PropTypes.bool,
newTodo: PropTypes.bool newTodo: PropTypes.bool,
}; };
export default TodoTextInput; export default TodoTextInput;

View File

@ -19,22 +19,19 @@ class App extends Component {
App.propTypes = { App.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
todos: state.todos todos: state.todos,
}; };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
actions: bindActionCreators(TodoActions, dispatch) actions: bindActionCreators(TodoActions, dispatch),
}; };
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(App);
mapStateToProps,
mapDispatchToProps
)(App);

View File

@ -4,8 +4,7 @@
<title>Redux TodoMVC example</title> <title>Redux TodoMVC example</title>
</head> </head>
<body> <body>
<div class="todoapp" id="root"> <div class="todoapp" id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

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

View File

@ -1,49 +1,61 @@
import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'; import {
ADD_TODO,
DELETE_TODO,
EDIT_TODO,
COMPLETE_TODO,
COMPLETE_ALL,
CLEAR_COMPLETED,
} from '../constants/ActionTypes';
const initialState = [{ const initialState = [
text: 'Use Redux', {
completed: false, text: 'Use Redux',
id: 0 completed: false,
}]; id: 0,
},
];
export default function todos(state = initialState, action) { export default function todos(state = initialState, action) {
switch (action.type) { switch (action.type) {
case ADD_TODO: case ADD_TODO:
return [{ return [
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, {
completed: false, id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
text: action.text completed: false,
}, ...state]; text: action.text,
},
...state,
];
case DELETE_TODO: case DELETE_TODO:
return state.filter(todo => return state.filter((todo) => todo.id !== action.id);
todo.id !== action.id
);
case EDIT_TODO: case EDIT_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { text: action.text }) : ? Object.assign({}, todo, { text: action.text })
todo : todo
); );
case COMPLETE_TODO: case COMPLETE_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { completed: !todo.completed }) : ? Object.assign({}, todo, { completed: !todo.completed })
todo : todo
); );
case COMPLETE_ALL: case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed); const areAllMarked = state.every((todo) => todo.completed);
return state.map(todo => Object.assign({}, todo, { return state.map((todo) =>
completed: !areAllMarked Object.assign({}, todo, {
})); completed: !areAllMarked,
})
);
case CLEAR_COMPLETED: case CLEAR_COMPLETED:
return state.filter(todo => todo.completed === false); return state.filter((todo) => todo.completed === false);
default: default:
return state; return state;
} }
} }

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4002; var port = 4002;
var compiler = webpack(config); var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(
webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
})
);
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
app.get("/", function(req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.listen(port, function(error) { app.listen(port, function (error) {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); console.info(
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
port,
port
);
} }
}); });

View File

@ -4,9 +4,14 @@ import rootReducer from '../reducers';
import * as actionCreators from '../actions/todos'; import * as actionCreators from '../actions/todos';
export default function configureStore(initialState) { export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState, devTools({ const store = createStore(
realtime: true, actionCreators rootReducer,
})); initialState,
devTools({
realtime: true,
actionCreators,
})
);
if (module.hot) { if (module.hot) {
// Enable Webpack hot module replacement for reducers // Enable Webpack hot module replacement for reducers

View File

@ -6,14 +6,14 @@ describe('todo actions', () => {
it('addTodo should create ADD_TODO action', () => { it('addTodo should create ADD_TODO action', () => {
expect(actions.addTodo('Use Redux')).toEqual({ expect(actions.addTodo('Use Redux')).toEqual({
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Use Redux' text: 'Use Redux',
}); });
}); });
it('deleteTodo should create DELETE_TODO action', () => { it('deleteTodo should create DELETE_TODO action', () => {
expect(actions.deleteTodo(1)).toEqual({ expect(actions.deleteTodo(1)).toEqual({
type: types.DELETE_TODO, type: types.DELETE_TODO,
id: 1 id: 1,
}); });
}); });
@ -21,26 +21,26 @@ describe('todo actions', () => {
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
type: types.EDIT_TODO, type: types.EDIT_TODO,
id: 1, id: 1,
text: 'Use Redux everywhere' text: 'Use Redux everywhere',
}); });
}); });
it('completeTodo should create COMPLETE_TODO action', () => { it('completeTodo should create COMPLETE_TODO action', () => {
expect(actions.completeTodo(1)).toEqual({ expect(actions.completeTodo(1)).toEqual({
type: types.COMPLETE_TODO, type: types.COMPLETE_TODO,
id: 1 id: 1,
}); });
}); });
it('completeAll should create COMPLETE_ALL action', () => { it('completeAll should create COMPLETE_ALL action', () => {
expect(actions.completeAll()).toEqual({ expect(actions.completeAll()).toEqual({
type: types.COMPLETE_ALL type: types.COMPLETE_ALL,
}); });
}); });
it('clearCompleted should create CLEAR_COMPLETED action', () => { it('clearCompleted should create CLEAR_COMPLETED action', () => {
expect(actions.clearCompleted('Use Redux')).toEqual({ expect(actions.clearCompleted('Use Redux')).toEqual({
type: types.CLEAR_COMPLETED type: types.CLEAR_COMPLETED,
}); });
}); });
}); });

View File

@ -5,13 +5,16 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
completedCount: 0, {
activeCount: 0, completedCount: 0,
filter: SHOW_ALL, activeCount: 0,
onClearCompleted: expect.createSpy(), filter: SHOW_ALL,
onShow: expect.createSpy() onClearCompleted: expect.createSpy(),
}, propOverrides); onShow: expect.createSpy(),
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<Footer {...props} />); renderer.render(<Footer {...props} />);
@ -19,13 +22,14 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output output: output,
}; };
} }
function getTextContent(elem) { function getTextContent(elem) {
const children = Array.isArray(elem.props.children) ? const children = Array.isArray(elem.props.children)
elem.props.children : [elem.props.children]; ? elem.props.children
: [elem.props.children];
return children.reduce(function concatText(out, child) { return children.reduce(function concatText(out, child) {
// Children are either elements or text strings // Children are either elements or text strings
@ -63,11 +67,13 @@ describe('components', () => {
expect(filter.type).toBe('li'); expect(filter.type).toBe('li');
const a = filter.props.children; const a = filter.props.children;
expect(a.props.className).toBe(i === 0 ? 'selected' : ''); expect(a.props.className).toBe(i === 0 ? 'selected' : '');
expect(a.props.children).toBe({ expect(a.props.children).toBe(
0: 'All', {
1: 'Active', 0: 'All',
2: 'Completed' 1: 'Active',
}[i]); 2: 'Completed',
}[i]
);
}); });
}); });
@ -81,20 +87,20 @@ describe('components', () => {
it('shouldnt show clear button when no completed todos', () => { it('shouldnt show clear button when no completed todos', () => {
const { output } = setup({ completedCount: 0 }); const { output } = setup({ completedCount: 0 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear).toBe(undefined); expect(clear).toBe(undefined);
}); });
it('should render clear button when completed todos', () => { it('should render clear button when completed todos', () => {
const { output } = setup({ completedCount: 1 }); const { output } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear.type).toBe('button'); expect(clear.type).toBe('button');
expect(clear.props.children).toBe('Clear completed'); expect(clear.props.children).toBe('Clear completed');
}); });
it('should call onClearCompleted on clear button click', () => { it('should call onClearCompleted on clear button click', () => {
const { output, props } = setup({ completedCount: 1 }); const { output, props } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
clear.props.onClick({}); clear.props.onClick({});
expect(props.onClearCompleted).toHaveBeenCalled(); expect(props.onClearCompleted).toHaveBeenCalled();
}); });

View File

@ -6,7 +6,7 @@ import TodoTextInput from '../../components/TodoTextInput';
function setup() { function setup() {
const props = { const props = {
addTodo: expect.createSpy() addTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
@ -16,7 +16,7 @@ function setup() {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -7,24 +7,30 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
todos: [{ {
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}, { completed: false,
text: 'Run the tests', id: 0,
completed: true, },
id: 1 {
}], text: 'Run the tests',
actions: { completed: true,
editTodo: expect.createSpy(), id: 1,
deleteTodo: expect.createSpy(), },
completeTodo: expect.createSpy(), ],
completeAll: expect.createSpy(), actions: {
clearCompleted: expect.createSpy() editTodo: expect.createSpy(),
} deleteTodo: expect.createSpy(),
}, propOverrides); completeTodo: expect.createSpy(),
completeAll: expect.createSpy(),
clearCompleted: expect.createSpy(),
},
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<MainSection {...props} />); renderer.render(<MainSection {...props} />);
@ -33,7 +39,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -55,11 +61,15 @@ describe('components', () => {
}); });
it('should be checked if all todos completed', () => { it('should be checked if all todos completed', () => {
const { output } = setup({ todos: [{ const { output } = setup({
text: 'Use Redux', todos: [
completed: true, {
id: 0 text: 'Use Redux',
}]}); completed: true,
id: 0,
},
],
});
const [toggle] = output.props.children; const [toggle] = output.props.children;
expect(toggle.props.checked).toBe(true); expect(toggle.props.checked).toBe(true);
}); });
@ -75,7 +85,7 @@ describe('components', () => {
describe('footer', () => { describe('footer', () => {
it('should render', () => { it('should render', () => {
const { output } = setup(); const { output } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
expect(footer.type).toBe(Footer); expect(footer.type).toBe(Footer);
expect(footer.props.completedCount).toBe(1); expect(footer.props.completedCount).toBe(1);
expect(footer.props.activeCount).toBe(1); expect(footer.props.activeCount).toBe(1);
@ -84,27 +94,31 @@ describe('components', () => {
it('onShow should set the filter', () => { it('onShow should set the filter', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [,, updatedFooter] = updated.props.children; const [, , updatedFooter] = updated.props.children;
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED); expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
}); });
it('onClearCompleted should call clearCompleted', () => { it('onClearCompleted should call clearCompleted', () => {
const { output, props } = setup(); const { output, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted).toHaveBeenCalled(); expect(props.actions.clearCompleted).toHaveBeenCalled();
}); });
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => { it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
const { output, props } = setup({ todos: [{ const { output, props } = setup({
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}]}); completed: false,
const [,, footer] = output.props.children; id: 0,
},
],
});
const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted.calls.length).toBe(0); expect(props.actions.clearCompleted.calls.length).toBe(0);
}); });
@ -124,7 +138,7 @@ describe('components', () => {
it('should filter items', () => { it('should filter items', () => {
const { output, renderer, props } = setup(); const { output, renderer, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [, updatedList] = updated.props.children; const [, updatedList] = updated.props.children;

View File

@ -4,23 +4,21 @@ import TestUtils from 'react-addons-test-utils';
import TodoItem from '../../components/TodoItem'; import TodoItem from '../../components/TodoItem';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup( editing = false ) { function setup(editing = false) {
const props = { const props = {
todo: { todo: {
id: 0, id: 0,
text: 'Use Redux', text: 'Use Redux',
completed: false completed: false,
}, },
editTodo: expect.createSpy(), editTodo: expect.createSpy(),
deleteTodo: expect.createSpy(), deleteTodo: expect.createSpy(),
completeTodo: expect.createSpy() completeTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoItem {...props} />);
<TodoItem {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -33,7 +31,7 @@ function setup( editing = false ) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -4,19 +4,20 @@ import TestUtils from 'react-addons-test-utils';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
onSave: expect.createSpy(), {
text: 'Use Redux', onSave: expect.createSpy(),
placeholder: 'What needs to be done?', text: 'Use Redux',
editing: false, placeholder: 'What needs to be done?',
newTodo: false editing: false,
}, propOverrides); newTodo: false,
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoTextInput {...props} />);
<TodoTextInput {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -25,7 +26,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -50,33 +51,33 @@ describe('components', () => {
it('should update value on change', () => { it('should update value on change', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
output.props.onChange({ target: { value: 'Use Radox' }}); output.props.onChange({ target: { value: 'Use Radox' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual('Use Radox'); expect(updated.props.value).toEqual('Use Radox');
}); });
it('should call onSave on return key press', () => { it('should call onSave on return key press', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('should reset state on return key press if newTodo', () => { it('should reset state on return key press if newTodo', () => {
const { output, renderer } = setup({ newTodo: true }); const { output, renderer } = setup({ newTodo: true });
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual(''); expect(updated.props.value).toEqual('');
}); });
it('should call onSave on blur', () => { it('should call onSave on blur', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('shouldnt call onSave on blur if newTodo', () => { it('shouldnt call onSave on blur if newTodo', () => {
const { output, props } = setup({ newTodo: true }); const { output, props } = setup({ newTodo: true });
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave.calls.length).toBe(0); expect(props.onSave.calls.length).toBe(0);
}); });
}); });

View File

@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
describe('todos reducer', () => { describe('todos reducer', () => {
it('should handle initial state', () => { it('should handle initial state', () => {
expect( expect(todos(undefined, {})).toEqual([
todos(undefined, {}) {
).toEqual([{ text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ]);
}); });
it('should handle ADD_TODO', () => { it('should handle ADD_TODO', () => {
expect( expect(
todos([], { todos([], {
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Run the tests' text: 'Run the tests',
}) })
).toEqual([{ ).toEqual([
text: 'Run the tests', {
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.ADD_TODO,
text: 'Run the tests'
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: false,
id: 1 id: 0,
}, { },
]);
expect(
todos(
[
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.ADD_TODO,
text: 'Run the tests',
}
)
).toEqual([
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.ADD_TODO, ]);
text: 'Fix the tests'
}) expect(
).toEqual([{ todos(
text: 'Fix the tests', [
completed: false, {
id: 2 text: 'Run the tests',
}, { completed: false,
text: 'Run the tests', id: 1,
completed: false, },
id: 1 {
}, { text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ],
{
type: types.ADD_TODO,
text: 'Fix the tests',
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false,
id: 2,
},
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
]);
}); });
it('should handle DELETE_TODO', () => { it('should handle DELETE_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: false, {
id: 1 text: 'Run the tests',
}, { completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.DELETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.DELETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle EDIT_TODO', () => { it('should handle EDIT_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.EDIT_TODO,
text: 'Fix the tests',
id: 1,
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.EDIT_TODO, ]);
text: 'Fix the tests',
id: 1
})
).toEqual([{
text: 'Fix the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_TODO', () => { it('should handle COMPLETE_TODO', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_ALL', () => { it('should handle COMPLETE_ALL', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: true,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: true,
id: 0
}]);
// Unmark if all todos are currently completed // Unmark if all todos are currently completed
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: true,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: true, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle CLEAR_COMPLETED', () => { it('should handle CLEAR_COMPLETED', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: true, {
id: 1 text: 'Run the tests',
}, { completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.CLEAR_COMPLETED,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.CLEAR_COMPLETED ]);
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should not generate duplicate ids after CLEAR_COMPLETED', () => { it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
expect( expect(
[{ [
type: types.COMPLETE_TODO, {
id: 0 type: types.COMPLETE_TODO,
}, { id: 0,
type: types.CLEAR_COMPLETED },
}, { {
type: types.ADD_TODO, type: types.CLEAR_COMPLETED,
text: 'Write more tests' },
}].reduce(todos, [{ {
id: 0, type: types.ADD_TODO,
text: 'Write more tests',
},
].reduce(todos, [
{
id: 0,
completed: false,
text: 'Use Redux',
},
{
id: 1,
completed: false,
text: 'Write tests',
},
])
).toEqual([
{
text: 'Write more tests',
completed: false,
id: 2,
},
{
text: 'Write tests',
completed: false, completed: false,
text: 'Use Redux'
}, {
id: 1, id: 1,
completed: false, },
text: 'Write tests' ]);
}])
).toEqual([{
text: 'Write more tests',
completed: false,
id: 2
}, {
text: 'Write tests',
completed: false,
id: 1
}]);
}); });
}); });

View File

@ -3,32 +3,32 @@ var webpack = require('webpack');
module.exports = { module.exports = {
devtool: 'cheap-module-eval-source-map', devtool: 'cheap-module-eval-source-map',
entry: [ entry: ['webpack-hot-middleware/client', './index'],
'webpack-hot-middleware/client',
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
plugins: [ plugins: [
new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin() new webpack.NoErrorsPlugin(),
], ],
module: { module: {
loaders: [{ loaders: [
test: /\.js$/, {
loaders: ['babel'], test: /\.js$/,
exclude: /node_modules/, loaders: ['babel'],
include: __dirname exclude: /node_modules/,
}, { include: __dirname,
test: /\.css?$/, },
loaders: ['style', 'raw'], {
include: __dirname test: /\.css?$/,
}] loaders: ['style', 'raw'],
} include: __dirname,
},
],
},
}; };
var src = path.join(__dirname, '..', '..', 'src'); var src = path.join(__dirname, '..', '..', 'src');
@ -41,6 +41,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
module.exports.module.loaders.push({ module.exports.module.loaders.push({
test: /\.js$/, test: /\.js$/,
loaders: ['babel'], loaders: ['babel'],
include: src include: src,
}); });
} }

View File

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

View File

@ -3,13 +3,13 @@ export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
export function increment() { export function increment() {
return { return {
type: INCREMENT_COUNTER type: INCREMENT_COUNTER,
}; };
} }
export function decrement() { export function decrement() {
return { return {
type: DECREMENT_COUNTER type: DECREMENT_COUNTER,
}; };
} }
@ -26,7 +26,7 @@ export function incrementIfOdd() {
} }
export function incrementAsync(delay = 1000) { export function incrementAsync(delay = 1000) {
return dispatch => { return (dispatch) => {
setTimeout(() => { setTimeout(() => {
dispatch(increment()); dispatch(increment());
}, delay); }, delay);

View File

@ -4,18 +4,18 @@ export const SEND_TO_MONITOR = 'SEND_TO_MONITOR';
export function startMonitoring() { export function startMonitoring() {
return { return {
type: START_MONITORING type: START_MONITORING,
}; };
} }
export function stopMonitoring() { export function stopMonitoring() {
return { return {
type: STOP_MONITORING type: STOP_MONITORING,
}; };
} }
export function sendToMonitor() { export function sendToMonitor() {
return { return {
type: SEND_TO_MONITOR type: SEND_TO_MONITOR,
}; };
} }

View File

@ -2,19 +2,20 @@ import React, { Component, PropTypes } from 'react';
class Counter extends Component { class Counter extends Component {
render() { render() {
const { startMonitoring, stopMonitoring, sendToMonitor, increment, decrement, counter } = this.props; const {
startMonitoring,
stopMonitoring,
sendToMonitor,
increment,
decrement,
counter,
} = this.props;
return ( return (
<p> <p>
Clicked: {counter} times Clicked: {counter} times <button onClick={increment}>+</button>{' '}
{' '} <button onClick={decrement}>-</button>{' '}
<button onClick={increment}>+</button> <button onClick={startMonitoring}>Start monitoring</button>{' '}
{' '} <button onClick={stopMonitoring}>Stop monitoring</button>{' '}
<button onClick={decrement}>-</button>
{' '}
<button onClick={startMonitoring}>Start monitoring</button>
{' '}
<button onClick={stopMonitoring}>Stop monitoring</button>
{' '}
<button onClick={sendToMonitor}>Send to the monitor</button> <button onClick={sendToMonitor}>Send to the monitor</button>
</p> </p>
); );
@ -27,7 +28,7 @@ Counter.propTypes = {
sendToMonitor: PropTypes.func.isRequired, sendToMonitor: PropTypes.func.isRequired,
increment: PropTypes.func.isRequired, increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired, decrement: PropTypes.func.isRequired,
counter: PropTypes.number.isRequired counter: PropTypes.number.isRequired,
}; };
export default Counter; export default Counter;

View File

@ -6,7 +6,7 @@ import * as MonitorActions from '../actions/monitoring';
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
counter: state.counter counter: state.counter,
}; };
} }

View File

@ -3,7 +3,10 @@
<head> <head>
<title>Redux counter example</title> <title>Redux counter example</title>
<style> <style>
html, body, #root, #container { html,
body,
#root,
#container {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
@ -19,15 +22,14 @@
font: bold 16px Arial; font: bold 16px Arial;
cursor: pointer; cursor: pointer;
} }
iframe { iframe {
width: 100%; width: 100%;
height: calc(100% - 55px); height: calc(100% - 55px);
} }
</style> </style>
</head> </head>
<body> <body>
<div id="root"> <div id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -2,11 +2,11 @@ import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';
export default function counter(state = 0, action) { export default function counter(state = 0, action) {
switch (action.type) { switch (action.type) {
case INCREMENT_COUNTER: case INCREMENT_COUNTER:
return state + 1; return state + 1;
case DECREMENT_COUNTER: case DECREMENT_COUNTER:
return state - 1; return state - 1;
default: default:
return state; return state;
} }
} }

View File

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

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4001; var port = 4001;
var compiler = webpack(config); var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(
webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
})
);
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
app.get("/", function(req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.listen(port, function(error) { app.listen(port, function (error) {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); console.info(
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
port,
port
);
} }
}); });

View File

@ -3,16 +3,22 @@ import thunk from 'redux-thunk';
import invariant from 'redux-immutable-state-invariant'; import invariant from 'redux-immutable-state-invariant';
import devTools from 'remote-redux-devtools'; import devTools from 'remote-redux-devtools';
import reducer from '../reducers'; import reducer from '../reducers';
import { START_MONITORING, STOP_MONITORING, SEND_TO_MONITOR } from '../actions/monitoring'; import {
START_MONITORING,
STOP_MONITORING,
SEND_TO_MONITOR,
} from '../actions/monitoring';
export default function configureStore(initialState) { export default function configureStore(initialState) {
const enhancer = compose( const enhancer = compose(
applyMiddleware(invariant(), thunk), applyMiddleware(invariant(), thunk),
devTools({ devTools({
realtime: false, realtime: false,
startOn: START_MONITORING, stopOn: STOP_MONITORING, startOn: START_MONITORING,
sendOn: SEND_TO_MONITOR, sendOnError: 1, stopOn: STOP_MONITORING,
maxAge: 30 sendOn: SEND_TO_MONITOR,
sendOnError: 1,
maxAge: 30,
}) })
); );

View File

@ -12,16 +12,17 @@ function mockStore(getState, expectedActions, onLastAction) {
if (!Array.isArray(expectedActions)) { if (!Array.isArray(expectedActions)) {
throw new Error('expectedActions should be an array of expected actions.'); throw new Error('expectedActions should be an array of expected actions.');
} }
if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') { if (
typeof onLastAction !== 'undefined' &&
typeof onLastAction !== 'function'
) {
throw new Error('onLastAction should either be undefined or function.'); throw new Error('onLastAction should either be undefined or function.');
} }
function mockStoreWithoutMiddleware() { function mockStoreWithoutMiddleware() {
return { return {
getState() { getState() {
return typeof getState === 'function' ? return typeof getState === 'function' ? getState() : getState;
getState() :
getState;
}, },
dispatch(action) { dispatch(action) {
@ -31,13 +32,13 @@ function mockStore(getState, expectedActions, onLastAction) {
onLastAction(); onLastAction();
} }
return action; return action;
} },
}; };
} }
const mockStoreWithMiddleware = applyMiddleware( const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
...middlewares mockStoreWithoutMiddleware
)(mockStoreWithoutMiddleware); );
return mockStoreWithMiddleware(); return mockStoreWithMiddleware();
} }
@ -52,9 +53,7 @@ describe('actions', () => {
}); });
it('incrementIfOdd should create increment action', (done) => { it('incrementIfOdd should create increment action', (done) => {
const expectedActions = [ const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
{ type: actions.INCREMENT_COUNTER }
];
const store = mockStore({ counter: 1 }, expectedActions, done); const store = mockStore({ counter: 1 }, expectedActions, done);
store.dispatch(actions.incrementIfOdd()); store.dispatch(actions.incrementIfOdd());
}); });
@ -67,9 +66,7 @@ describe('actions', () => {
}); });
it('incrementAsync should create increment action', (done) => { it('incrementAsync should create increment action', (done) => {
const expectedActions = [ const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
{ type: actions.INCREMENT_COUNTER }
];
const store = mockStore({ counter: 0 }, expectedActions, done); const store = mockStore({ counter: 0 }, expectedActions, done);
store.dispatch(actions.incrementAsync(100)); store.dispatch(actions.incrementAsync(100));
}); });

View File

@ -8,14 +8,16 @@ function setup() {
increment: expect.createSpy(), increment: expect.createSpy(),
incrementIfOdd: expect.createSpy(), incrementIfOdd: expect.createSpy(),
incrementAsync: expect.createSpy(), incrementAsync: expect.createSpy(),
decrement: expect.createSpy() decrement: expect.createSpy(),
}; };
const component = TestUtils.renderIntoDocument(<Counter counter={1} {...actions} />); const component = TestUtils.renderIntoDocument(
<Counter counter={1} {...actions} />
);
return { return {
component: component, component: component,
actions: actions, actions: actions,
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'), buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p') p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
}; };
} }

View File

@ -15,7 +15,7 @@ function setup(initialState) {
return { return {
app: app, app: app,
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'), buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p') p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
}; };
} }

View File

@ -3,28 +3,27 @@ var webpack = require('webpack');
module.exports = { module.exports = {
devtool: 'cheap-module-eval-source-map', devtool: 'cheap-module-eval-source-map',
entry: [ entry: ['webpack-hot-middleware/client', './index'],
'webpack-hot-middleware/client',
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
plugins: [ plugins: [
new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin() new webpack.NoErrorsPlugin(),
], ],
module: { module: {
loaders: [{ loaders: [
test: /\.js$/, {
loaders: ['babel'], test: /\.js$/,
exclude: /node_modules/, loaders: ['babel'],
include: __dirname exclude: /node_modules/,
}] include: __dirname,
} },
],
},
}; };
var src = path.join(__dirname, '..', '..', 'src'); var src = path.join(__dirname, '..', '..', 'src');
@ -37,6 +36,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
module.exports.module.loaders.push({ module.exports.module.loaders.push({
test: /\.js$/, test: /\.js$/,
loaders: ['babel'], loaders: ['babel'],
include: src include: src,
}); });
} }

View File

@ -4,6 +4,6 @@ export const defaultSocketOptions = {
port: 443, port: 443,
autoReconnect: true, autoReconnect: true,
autoReconnectOptions: { autoReconnectOptions: {
randomness: 30000 randomness: 30000,
} },
}; };