mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-01-31 11:51:41 +03:00
Add @redux-devtools/remote (#928)
This commit is contained in:
parent
d807663302
commit
9a130ce9d1
|
@ -8,3 +8,4 @@ coverage
|
||||||
node_modules
|
node_modules
|
||||||
__snapshots__
|
__snapshots__
|
||||||
.yarn/*
|
.yarn/*
|
||||||
|
storybook-static
|
||||||
|
|
|
@ -11,3 +11,4 @@ dev
|
||||||
.yarn/*
|
.yarn/*
|
||||||
.pnp.*
|
.pnp.*
|
||||||
**/demo/public/**
|
**/demo/public/**
|
||||||
|
storybook-static
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import mapValues from 'lodash/mapValues';
|
import mapValues from 'lodash/mapValues';
|
||||||
import { Config } from '../../browser/extension/inject/pageScript';
|
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import { LiftedState, PerformAction } from '@redux-devtools/instrument';
|
import { LiftedState, PerformAction } from '@redux-devtools/instrument';
|
||||||
|
import { LocalFilter } from '@redux-devtools/utils/lib/filters';
|
||||||
|
|
||||||
export type FilterStateValue =
|
export type FilterStateValue =
|
||||||
| 'DO_NOT_FILTER'
|
| 'DO_NOT_FILTER'
|
||||||
|
@ -14,27 +14,6 @@ export const FilterState: { [K in FilterStateValue]: FilterStateValue } = {
|
||||||
ALLOWLIST_SPECIFIC: 'ALLOWLIST_SPECIFIC',
|
ALLOWLIST_SPECIFIC: 'ALLOWLIST_SPECIFIC',
|
||||||
};
|
};
|
||||||
|
|
||||||
function isArray(arg: unknown): arg is readonly unknown[] {
|
|
||||||
return Array.isArray(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LocalFilter {
|
|
||||||
readonly allowlist: string | undefined;
|
|
||||||
readonly denylist: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLocalFilter(config: Config): LocalFilter | undefined {
|
|
||||||
const denylist = config.actionsDenylist ?? config.actionsBlacklist;
|
|
||||||
const allowlist = config.actionsAllowlist ?? config.actionsWhitelist;
|
|
||||||
if (denylist || allowlist) {
|
|
||||||
return {
|
|
||||||
allowlist: isArray(allowlist) ? allowlist.join('|') : allowlist,
|
|
||||||
denylist: isArray(denylist) ? denylist.join('|') : denylist,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const noFiltersApplied = (localFilter: LocalFilter | undefined) =>
|
export const noFiltersApplied = (localFilter: LocalFilter | undefined) =>
|
||||||
// !predicate &&
|
// !predicate &&
|
||||||
!localFilter &&
|
!localFilter &&
|
||||||
|
|
|
@ -2,7 +2,8 @@ import jsan, { Options } from 'jsan';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import serializeImmutable from '@redux-devtools/serialize/lib/immutable/serialize';
|
import serializeImmutable from '@redux-devtools/serialize/lib/immutable/serialize';
|
||||||
import { getActionsArray } from '@redux-devtools/utils';
|
import { getActionsArray } from '@redux-devtools/utils';
|
||||||
import { getLocalFilter, isFiltered, PartialLiftedState } from './filters';
|
import { getLocalFilter } from '@redux-devtools/utils/lib/filters';
|
||||||
|
import { isFiltered, PartialLiftedState } from './filters';
|
||||||
import importState from './importState';
|
import importState from './importState';
|
||||||
import generateId from './generateInstanceId';
|
import generateId from './generateInstanceId';
|
||||||
import { Config } from '../../browser/extension/inject/pageScript';
|
import { Config } from '../../browser/extension/inject/pageScript';
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { isAllowed, Options } from '../options/syncOptions';
|
||||||
import Monitor from '../../../app/service/Monitor';
|
import Monitor from '../../../app/service/Monitor';
|
||||||
import {
|
import {
|
||||||
noFiltersApplied,
|
noFiltersApplied,
|
||||||
getLocalFilter,
|
|
||||||
isFiltered,
|
isFiltered,
|
||||||
filterState,
|
filterState,
|
||||||
startingFrom,
|
startingFrom,
|
||||||
|
@ -52,6 +51,7 @@ import {
|
||||||
} from '@redux-devtools/app/lib/actions';
|
} from '@redux-devtools/app/lib/actions';
|
||||||
import { ContentScriptToPageScriptMessage } from './contentScript';
|
import { ContentScriptToPageScriptMessage } from './contentScript';
|
||||||
import { Features } from '@redux-devtools/app/lib/reducers/instances';
|
import { Features } from '@redux-devtools/app/lib/reducers/instances';
|
||||||
|
import { getLocalFilter } from '@redux-devtools/utils/lib/filters';
|
||||||
|
|
||||||
type EnhancedStoreWithInitialDispatch<
|
type EnhancedStoreWithInitialDispatch<
|
||||||
S,
|
S,
|
||||||
|
|
3
packages/redux-devtools-remote/.babelrc
Normal file
3
packages/redux-devtools-remote/.babelrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env", "@babel/preset-typescript"]
|
||||||
|
}
|
2
packages/redux-devtools-remote/.eslintignore
Normal file
2
packages/redux-devtools-remote/.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
examples
|
||||||
|
lib
|
7
packages/redux-devtools-remote/.eslintrc.js
Normal file
7
packages/redux-devtools-remote/.eslintrc.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: '../../eslintrc.ts.base.json',
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
};
|
21
packages/redux-devtools-remote/LICENSE.md
Normal file
21
packages/redux-devtools-remote/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Mihail Diordiev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
199
packages/redux-devtools-remote/README.md
Normal file
199
packages/redux-devtools-remote/README.md
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
# Remote Redux DevTools
|
||||||
|
|
||||||
|
![Demo](demo.gif)
|
||||||
|
|
||||||
|
Use [Redux DevTools](https://github.com/reduxjs/redux-devtools) remotely for React Native, hybrid, desktop and server side Redux apps.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn add @redux-devtools/remote
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
There are 2 ways of usage depending if you're using other store enhancers (middlewares) or not.
|
||||||
|
|
||||||
|
#### 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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
const store = createStore(reducer);
|
||||||
|
```
|
||||||
|
|
||||||
|
with
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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
|
||||||
|
|
||||||
|
#### 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.
|
||||||
|
|
||||||
|
In that case change this:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
|
||||||
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
preloadedState,
|
||||||
|
compose(
|
||||||
|
applyMiddleware(...middleware)
|
||||||
|
// other store enhancers if any
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
|
import { composeWithDevTools } from '@redux-devtools/remote';
|
||||||
|
|
||||||
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
/* preloadedState, */ composeWithDevTools(
|
||||||
|
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 });
|
||||||
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
/* preloadedState, */ composeEnhancers(
|
||||||
|
applyMiddleware(...middleware)
|
||||||
|
// other store enhancers if any
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enabling
|
||||||
|
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ...
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('development')
|
||||||
|
})
|
||||||
|
],
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you don't set `NODE_ENV`, you can set `realtime` parameter to `true` or to other global variable to turn it off in production:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const store = createStore(reducer, devToolsEnhancer({ realtime: true }));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
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.
|
||||||
|
- [atom-redux-devtools](https://github.com/zalmoxisus/atom-redux-devtools) - Used in Atom editor.
|
||||||
|
- [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.
|
||||||
|
|
||||||
|
### Communicate via local server
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var reduxDevTools = require('@redux-devtools/cli');
|
||||||
|
reduxDevTools({ hostname: 'localhost', port: 8000 });
|
||||||
|
```
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `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'`. |
|
||||||
|
| `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). |
|
||||||
|
| `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`. |
|
||||||
|
| `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. |
|
||||||
|
| `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. |
|
||||||
|
| `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. |
|
||||||
|
| `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. |
|
||||||
|
| `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). |
|
||||||
|
| `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`. |
|
||||||
|
| `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. |
|
||||||
|
| `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.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function configureStore(preloadedState) {
|
||||||
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
preloadedState,
|
||||||
|
devToolsEnhancer({
|
||||||
|
name: 'Android app',
|
||||||
|
realtime: true,
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 8000,
|
||||||
|
maxAge: 30,
|
||||||
|
actionsBlacklist: ['EFFECT_RESOLVED'],
|
||||||
|
actionSanitizer: (action) =>
|
||||||
|
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
|
||||||
|
? { ...action, data: '<<LONG_BLOB>>' }
|
||||||
|
: action,
|
||||||
|
stateSanitizer: (state) =>
|
||||||
|
state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo
|
||||||
|
|
||||||
|
- [Toggle monitoring](http://zalmoxisus.github.io/monitoring/)
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
MIT
|
BIN
packages/redux-devtools-remote/demo.gif
Normal file
BIN
packages/redux-devtools-remote/demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
36
packages/redux-devtools-remote/examples/buildAll.js
Normal file
36
packages/redux-devtools-remote/examples/buildAll.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Runs an ordered set of commands within each of the build directories.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
|
var exampleDirs = fs.readdirSync(__dirname).filter((file) => {
|
||||||
|
return fs.statSync(path.join(__dirname, file)).isDirectory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ordering is important here. `npm install` must come first.
|
||||||
|
var cmdArgs = [
|
||||||
|
{ cmd: 'npm', args: ['install'] },
|
||||||
|
{ cmd: 'webpack', args: ['index.js'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of exampleDirs) {
|
||||||
|
for (const cmdArg of cmdArgs) {
|
||||||
|
// declare opts in this scope to avoid https://github.com/joyent/node/issues/9158
|
||||||
|
const opts = {
|
||||||
|
cwd: path.join(__dirname, dir),
|
||||||
|
stdio: 'inherit',
|
||||||
|
};
|
||||||
|
let result = {};
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts);
|
||||||
|
} else {
|
||||||
|
result = spawnSync(cmdArg.cmd, cmdArg.args, opts);
|
||||||
|
}
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error('Building examples exited with non-zero');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
packages/redux-devtools-remote/examples/counter/.babelrc
Normal file
3
packages/redux-devtools-remote/examples/counter/.babelrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"]
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
|
||||||
|
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
|
||||||
|
|
||||||
|
export function increment() {
|
||||||
|
return {
|
||||||
|
type: INCREMENT_COUNTER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrement() {
|
||||||
|
return {
|
||||||
|
type: DECREMENT_COUNTER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementIfOdd() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { counter } = getState();
|
||||||
|
|
||||||
|
if (counter % 2 === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(increment());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementAsync(delay = 1000) {
|
||||||
|
return (dispatch) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(increment());
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
render() {
|
||||||
|
const { increment, incrementIfOdd, incrementAsync, decrement, counter } =
|
||||||
|
this.props;
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Clicked: {counter} times <button onClick={increment}>+</button>{' '}
|
||||||
|
<button onClick={decrement}>-</button>{' '}
|
||||||
|
<button onClick={incrementIfOdd}>Increment if odd</button>{' '}
|
||||||
|
<button onClick={() => incrementAsync()}>Increment async</button>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Counter.propTypes = {
|
||||||
|
increment: PropTypes.func.isRequired,
|
||||||
|
incrementIfOdd: PropTypes.func.isRequired,
|
||||||
|
incrementAsync: PropTypes.func.isRequired,
|
||||||
|
decrement: PropTypes.func.isRequired,
|
||||||
|
counter: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Counter;
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Counter from '../components/Counter';
|
||||||
|
import * as CounterActions from '../actions/counter';
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
counter: state.counter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(CounterActions, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
|
10
packages/redux-devtools-remote/examples/counter/index.html
Normal file
10
packages/redux-devtools-remote/examples/counter/index.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redux counter example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
packages/redux-devtools-remote/examples/counter/index.js
Normal file
14
packages/redux-devtools-remote/examples/counter/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from './containers/App';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
46
packages/redux-devtools-remote/examples/counter/package.json
Normal file
46
packages/redux-devtools-remote/examples/counter/package.json
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "redux-counter-example",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Redux counter example",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js",
|
||||||
|
"test:watch": "npm test -- --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rackt/redux.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/rackt/redux/issues"
|
||||||
|
},
|
||||||
|
"homepage": "http://rackt.github.io/redux",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^0.14.0",
|
||||||
|
"react-dom": "^0.14.0",
|
||||||
|
"react-redux": "^4.0.0",
|
||||||
|
"redux": "^3.5.2",
|
||||||
|
"redux-thunk": "^0.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.3.15",
|
||||||
|
"babel-loader": "^6.2.0",
|
||||||
|
"babel-plugin-react-transform": "^2.0.0-beta1",
|
||||||
|
"babel-polyfill": "^6.3.14",
|
||||||
|
"babel-preset-es2015": "^6.3.13",
|
||||||
|
"babel-preset-react": "^6.3.13",
|
||||||
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
|
"expect": "^1.6.0",
|
||||||
|
"express": "^4.13.3",
|
||||||
|
"jsdom": "^5.6.1",
|
||||||
|
"mocha": "^2.2.5",
|
||||||
|
"node-libs-browser": "^0.5.2",
|
||||||
|
"react-addons-test-utils": "^0.14.0",
|
||||||
|
"react-transform-hmr": "^1.0.0",
|
||||||
|
"redux-immutable-state-invariant": "^1.1.1",
|
||||||
|
"webpack": "^1.13.1",
|
||||||
|
"webpack-dev-middleware": "^1.2.0",
|
||||||
|
"webpack-hot-middleware": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';
|
||||||
|
|
||||||
|
export default function counter(state = 0, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case INCREMENT_COUNTER:
|
||||||
|
return state + 1;
|
||||||
|
case DECREMENT_COUNTER:
|
||||||
|
return state - 1;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import counter from './counter';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
counter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
32
packages/redux-devtools-remote/examples/counter/server.js
Normal file
32
packages/redux-devtools-remote/examples/counter/server.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
var webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
|
||||||
|
var app = new require('express')();
|
||||||
|
var port = 4001;
|
||||||
|
|
||||||
|
var compiler = webpack(config);
|
||||||
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.get('/', function (req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import invariant from 'redux-immutable-state-invariant';
|
||||||
|
import { composeWithDevTools } from 'remote-redux-devtools';
|
||||||
|
import reducer from '../reducers';
|
||||||
|
import * as actionCreators from '../actions/counter';
|
||||||
|
|
||||||
|
export default function configureStore(initialState) {
|
||||||
|
const composeEnhancers = composeWithDevTools({
|
||||||
|
realtime: true,
|
||||||
|
actionCreators,
|
||||||
|
trace: true,
|
||||||
|
});
|
||||||
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
initialState,
|
||||||
|
composeEnhancers(applyMiddleware(invariant(), thunk))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
// Enable Webpack hot module replacement for reducers
|
||||||
|
module.hot.accept('../reducers', () => {
|
||||||
|
const nextReducer = require('../reducers').default;
|
||||||
|
store.replaceReducer(nextReducer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import { applyMiddleware } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import * as actions from '../../actions/counter';
|
||||||
|
|
||||||
|
const middlewares = [thunk];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates a mock of Redux store with middleware.
|
||||||
|
*/
|
||||||
|
function mockStore(getState, expectedActions, onLastAction) {
|
||||||
|
if (!Array.isArray(expectedActions)) {
|
||||||
|
throw new Error('expectedActions should be an array of expected actions.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof onLastAction !== 'undefined' &&
|
||||||
|
typeof onLastAction !== 'function'
|
||||||
|
) {
|
||||||
|
throw new Error('onLastAction should either be undefined or function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockStoreWithoutMiddleware() {
|
||||||
|
return {
|
||||||
|
getState() {
|
||||||
|
return typeof getState === 'function' ? getState() : getState;
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatch(action) {
|
||||||
|
const expectedAction = expectedActions.shift();
|
||||||
|
expect(action).toEqual(expectedAction);
|
||||||
|
if (onLastAction && !expectedActions.length) {
|
||||||
|
onLastAction();
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
|
||||||
|
mockStoreWithoutMiddleware
|
||||||
|
);
|
||||||
|
|
||||||
|
return mockStoreWithMiddleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
it('increment should create increment action', () => {
|
||||||
|
expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrement should create decrement action', () => {
|
||||||
|
expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementIfOdd should create increment action', (done) => {
|
||||||
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
|
const store = mockStore({ counter: 1 }, expectedActions, done);
|
||||||
|
store.dispatch(actions.incrementIfOdd());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementIfOdd shouldnt create increment action if counter is even', (done) => {
|
||||||
|
const expectedActions = [];
|
||||||
|
const store = mockStore({ counter: 2 }, expectedActions);
|
||||||
|
store.dispatch(actions.incrementIfOdd());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementAsync should create increment action', (done) => {
|
||||||
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
|
const store = mockStore({ counter: 0 }, expectedActions, done);
|
||||||
|
store.dispatch(actions.incrementAsync(100));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Counter from '../../components/Counter';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const actions = {
|
||||||
|
increment: expect.createSpy(),
|
||||||
|
incrementIfOdd: expect.createSpy(),
|
||||||
|
incrementAsync: expect.createSpy(),
|
||||||
|
decrement: expect.createSpy(),
|
||||||
|
};
|
||||||
|
const component = TestUtils.renderIntoDocument(
|
||||||
|
<Counter counter={1} {...actions} />
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
component: component,
|
||||||
|
actions: actions,
|
||||||
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
|
||||||
|
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Counter component', () => {
|
||||||
|
it('should display count', () => {
|
||||||
|
const { p } = setup();
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('first button should call increment', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[0]);
|
||||||
|
expect(actions.increment).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('second button should call decrement', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[1]);
|
||||||
|
expect(actions.decrement).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('third button should call incrementIfOdd', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(actions.incrementIfOdd).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fourth button should call incrementAsync', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[3]);
|
||||||
|
expect(actions.incrementAsync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from '../../containers/App';
|
||||||
|
import configureStore from '../../store/configureStore';
|
||||||
|
|
||||||
|
function setup(initialState) {
|
||||||
|
const store = configureStore(initialState);
|
||||||
|
const app = TestUtils.renderIntoDocument(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
app: app,
|
||||||
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
|
||||||
|
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('containers', () => {
|
||||||
|
describe('App', () => {
|
||||||
|
it('should display initial count', () => {
|
||||||
|
const { p } = setup();
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 0 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display updated count after increment button click', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[0]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display updated count after decrement button click', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[1]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: -1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt change if even and if odd button clicked', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 0 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change if odd and if odd button clicked', () => {
|
||||||
|
const { buttons, p } = setup({ counter: 1 });
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 2 times/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import counter from '../../reducers/counter';
|
||||||
|
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../actions/counter';
|
||||||
|
|
||||||
|
describe('reducers', () => {
|
||||||
|
describe('counter', () => {
|
||||||
|
it('should handle initial state', () => {
|
||||||
|
expect(counter(undefined, {})).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle INCREMENT_COUNTER', () => {
|
||||||
|
expect(counter(1, { type: INCREMENT_COUNTER })).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle DECREMENT_COUNTER', () => {
|
||||||
|
expect(counter(1, { type: DECREMENT_COUNTER })).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unknown action type', () => {
|
||||||
|
expect(counter(1, { type: 'unknown' })).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { jsdom } from 'jsdom';
|
||||||
|
|
||||||
|
global.document = jsdom('<!doctype html><html><body></body></html>');
|
||||||
|
global.window = document.defaultView;
|
||||||
|
global.navigator = global.window.navigator;
|
|
@ -0,0 +1,41 @@
|
||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
entry: ['webpack-hot-middleware/client', './index'],
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
publicPath: '/static/',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.NoErrorsPlugin(),
|
||||||
|
],
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
include: __dirname,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var src = path.join(__dirname, '..', '..', 'src');
|
||||||
|
var nodeModules = path.join(__dirname, '..', '..', 'node_modules');
|
||||||
|
var fs = require('fs');
|
||||||
|
if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
|
||||||
|
// Resolve to source
|
||||||
|
module.exports.resolve = { alias: { 'remote-redux-devtools': src } };
|
||||||
|
// Compile from source
|
||||||
|
module.exports.module.loaders.push({
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
include: src,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
var createStore = require('redux').createStore;
|
||||||
|
var devTools = require('remote-redux-devtools').default;
|
||||||
|
|
||||||
|
function counter(state, action) {
|
||||||
|
if (state === undefined) state = 0;
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INCREMENT':
|
||||||
|
return state + 1;
|
||||||
|
case 'DECREMENT':
|
||||||
|
return state - 1;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = createStore(counter, devTools({ realtime: true }));
|
||||||
|
store.subscribe(function () {
|
||||||
|
console.log(store.getState());
|
||||||
|
});
|
||||||
|
|
||||||
|
function incrementer() {
|
||||||
|
setTimeout(function () {
|
||||||
|
store.dispatch({ type: 'INCREMENT' });
|
||||||
|
incrementer();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementer();
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "redux-remote-devtools-node-counter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Very simple counter for redux remote devtools in node",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"redux": "^3.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"remote-redux-devtools": "^0.5.7"
|
||||||
|
}
|
||||||
|
}
|
4
packages/redux-devtools-remote/examples/router/.babelrc
Normal file
4
packages/redux-devtools-remote/examples/router/.babelrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"],
|
||||||
|
"plugins": ["transform-decorators-legacy"]
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as types from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
export function addTodo(text) {
|
||||||
|
return { type: types.ADD_TODO, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTodo(id) {
|
||||||
|
return { type: types.DELETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editTodo(id, text) {
|
||||||
|
return { type: types.EDIT_TODO, id, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeTodo(id) {
|
||||||
|
return { type: types.COMPLETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeAll() {
|
||||||
|
return { type: types.COMPLETE_ALL };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCompleted() {
|
||||||
|
return { type: types.CLEAR_COMPLETED };
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { PropTypes, Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
|
const FILTER_TITLES = {
|
||||||
|
[SHOW_ALL]: 'All',
|
||||||
|
[SHOW_ACTIVE]: 'Active',
|
||||||
|
[SHOW_COMPLETED]: 'Completed',
|
||||||
|
};
|
||||||
|
|
||||||
|
class Footer extends Component {
|
||||||
|
renderTodoCount() {
|
||||||
|
const { activeCount } = this.props;
|
||||||
|
const itemWord = activeCount === 1 ? 'item' : 'items';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="todo-count">
|
||||||
|
<strong>{activeCount || 'No'}</strong> {itemWord} left
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFilterLink(filter) {
|
||||||
|
const title = FILTER_TITLES[filter];
|
||||||
|
const { filter: selectedFilter, onShow } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={classnames({ selected: filter === selectedFilter })}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => onShow(filter)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderClearButton() {
|
||||||
|
const { completedCount, onClearCompleted } = this.props;
|
||||||
|
if (completedCount > 0) {
|
||||||
|
return (
|
||||||
|
<button className="clear-completed" onClick={onClearCompleted}>
|
||||||
|
Clear completed
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<footer className="footer">
|
||||||
|
{this.renderTodoCount()}
|
||||||
|
<ul className="filters">
|
||||||
|
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
|
||||||
|
<li key={filter}>{this.renderFilterLink(filter)}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{this.renderClearButton()}
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Footer.propTypes = {
|
||||||
|
completedCount: PropTypes.number.isRequired,
|
||||||
|
activeCount: PropTypes.number.isRequired,
|
||||||
|
filter: PropTypes.string.isRequired,
|
||||||
|
onClearCompleted: PropTypes.func.isRequired,
|
||||||
|
onShow: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { PropTypes, Component } from 'react';
|
||||||
|
import TodoTextInput from './TodoTextInput';
|
||||||
|
|
||||||
|
class Header extends Component {
|
||||||
|
handleSave(text) {
|
||||||
|
if (text.length !== 0) {
|
||||||
|
this.props.addTodo(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { path } = this.props;
|
||||||
|
return (
|
||||||
|
<header className="header">
|
||||||
|
<h1 style={{ fontSize: 80 }}>{path}</h1>
|
||||||
|
<TodoTextInput
|
||||||
|
newTodo
|
||||||
|
onSave={this.handleSave.bind(this)}
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
addTodo: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import TodoItem from './TodoItem';
|
||||||
|
import Footer from './Footer';
|
||||||
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
|
const TODO_FILTERS = {
|
||||||
|
[SHOW_ALL]: () => true,
|
||||||
|
[SHOW_ACTIVE]: (todo) => !todo.completed,
|
||||||
|
[SHOW_COMPLETED]: (todo) => todo.completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
class MainSection extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = { filter: SHOW_ALL };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClearCompleted() {
|
||||||
|
const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
|
||||||
|
if (atLeastOneCompleted) {
|
||||||
|
this.props.actions.clearCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShow(filter) {
|
||||||
|
this.setState({ filter });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToggleAll(completedCount) {
|
||||||
|
const { todos, actions } = this.props;
|
||||||
|
if (todos.length > 0) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className="toggle-all"
|
||||||
|
type="checkbox"
|
||||||
|
checked={completedCount === todos.length}
|
||||||
|
onChange={actions.completeAll}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooter(completedCount) {
|
||||||
|
const { todos } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
const activeCount = todos.length - completedCount;
|
||||||
|
|
||||||
|
if (todos.length) {
|
||||||
|
return (
|
||||||
|
<Footer
|
||||||
|
completedCount={completedCount}
|
||||||
|
activeCount={activeCount}
|
||||||
|
filter={filter}
|
||||||
|
onClearCompleted={this.handleClearCompleted.bind(this)}
|
||||||
|
onShow={this.handleShow.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { todos, actions } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
|
||||||
|
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
|
||||||
|
const completedCount = todos.reduce(
|
||||||
|
(count, todo) => (todo.completed ? count + 1 : count),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="main">
|
||||||
|
{this.renderToggleAll(completedCount)}
|
||||||
|
<ul className="todo-list">
|
||||||
|
{filteredTodos.map((todo) => (
|
||||||
|
<TodoItem key={todo.id} todo={todo} {...actions} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{this.renderFooter(completedCount)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MainSection.propTypes = {
|
||||||
|
todos: PropTypes.array.isRequired,
|
||||||
|
actions: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainSection;
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import TodoTextInput from './TodoTextInput';
|
||||||
|
|
||||||
|
class TodoItem extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
|
editing: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoubleClick() {
|
||||||
|
this.setState({ editing: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSave(id, text) {
|
||||||
|
if (text.length === 0) {
|
||||||
|
this.props.deleteTodo(id);
|
||||||
|
} else {
|
||||||
|
this.props.editTodo(id, text);
|
||||||
|
}
|
||||||
|
this.setState({ editing: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { todo, completeTodo, deleteTodo } = this.props;
|
||||||
|
|
||||||
|
let element;
|
||||||
|
if (this.state.editing) {
|
||||||
|
element = (
|
||||||
|
<TodoTextInput
|
||||||
|
text={todo.text}
|
||||||
|
editing={this.state.editing}
|
||||||
|
onSave={(text) => this.handleSave(todo.id, text)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
element = (
|
||||||
|
<div className="view">
|
||||||
|
<input
|
||||||
|
className="toggle"
|
||||||
|
type="checkbox"
|
||||||
|
checked={todo.completed}
|
||||||
|
onChange={() => completeTodo(todo.id)}
|
||||||
|
/>
|
||||||
|
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
|
||||||
|
{todo.text}
|
||||||
|
</label>
|
||||||
|
<button className="destroy" onClick={() => deleteTodo(todo.id)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={classnames({
|
||||||
|
completed: todo.completed,
|
||||||
|
editing: this.state.editing,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{element}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoItem.propTypes = {
|
||||||
|
todo: PropTypes.object.isRequired,
|
||||||
|
editTodo: PropTypes.func.isRequired,
|
||||||
|
deleteTodo: PropTypes.func.isRequired,
|
||||||
|
completeTodo: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoItem;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
class TodoTextInput extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
|
text: this.props.text || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
const text = e.target.value.trim();
|
||||||
|
if (e.which === 13) {
|
||||||
|
this.props.onSave(text);
|
||||||
|
if (this.props.newTodo) {
|
||||||
|
this.setState({ text: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
this.setState({ text: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur(e) {
|
||||||
|
if (!this.props.newTodo) {
|
||||||
|
this.props.onSave(e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={classnames({
|
||||||
|
edit: this.props.editing,
|
||||||
|
'new-todo': this.props.newTodo,
|
||||||
|
})}
|
||||||
|
type="text"
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
autoFocus="true"
|
||||||
|
value={this.state.text}
|
||||||
|
onBlur={this.handleBlur.bind(this)}
|
||||||
|
onChange={this.handleChange.bind(this)}
|
||||||
|
onKeyDown={this.handleSubmit.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoTextInput.propTypes = {
|
||||||
|
onSave: PropTypes.func.isRequired,
|
||||||
|
text: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
editing: PropTypes.bool,
|
||||||
|
newTodo: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoTextInput;
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const ADD_TODO = 'ADD_TODO';
|
||||||
|
export const DELETE_TODO = 'DELETE_TODO';
|
||||||
|
export const EDIT_TODO = 'EDIT_TODO';
|
||||||
|
export const COMPLETE_TODO = 'COMPLETE_TODO';
|
||||||
|
export const COMPLETE_ALL = 'COMPLETE_ALL';
|
||||||
|
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const SHOW_ALL = 'show_all';
|
||||||
|
export const SHOW_COMPLETED = 'show_completed';
|
||||||
|
export const SHOW_ACTIVE = 'show_active';
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import MainSection from '../components/MainSection';
|
||||||
|
import * as TodoActions from '../actions/todos';
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
const { todos, path, actions } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header addTodo={actions.addTodo} path={path} />
|
||||||
|
<MainSection todos={todos} actions={actions} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
todos: PropTypes.array.isRequired,
|
||||||
|
actions: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
todos: state.todos,
|
||||||
|
path: state.router.location.pathname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Route, Redirect } from 'react-router';
|
||||||
|
import { ReduxRouter } from 'redux-router';
|
||||||
|
import Wrapper from './Wrapper';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ReduxRouter>
|
||||||
|
<Redirect from="/" to="Standard Todo" />
|
||||||
|
<Route path="/" component={Wrapper}>
|
||||||
|
<Route path="/:id" component={App} />
|
||||||
|
</Route>
|
||||||
|
</ReduxRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Root;
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { pushState } from 'redux-router';
|
||||||
|
import { Route, Link } from 'react-router';
|
||||||
|
import * as TodoActions from '../actions/todos';
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
pushState: bindActionCreators(pushState, dispatch),
|
||||||
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@connect((state) => ({}), mapDispatchToProps)
|
||||||
|
class Wrapper extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const { actions, pushState } = this.props;
|
||||||
|
const path = event.target.innerText;
|
||||||
|
|
||||||
|
pushState(null, path);
|
||||||
|
console.log('Navigate to', path);
|
||||||
|
|
||||||
|
if (this.timeout) clearInterval(this.timeout);
|
||||||
|
if (path === 'AutoTodo') {
|
||||||
|
console.log('!');
|
||||||
|
this.timeout = setInterval(() => {
|
||||||
|
actions.addTodo('Auto generated task');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
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>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wrapper;
|
10
packages/redux-devtools-remote/examples/router/index.html
Normal file
10
packages/redux-devtools-remote/examples/router/index.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redux TodoMVC example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="todoapp" id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
packages/redux-devtools-remote/examples/router/index.js
Normal file
16
packages/redux-devtools-remote/examples/router/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'babel-polyfill';
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import Root from './containers/Root';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
import 'todomvc-app-css/index.css';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Root />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
52
packages/redux-devtools-remote/examples/router/package.json
Normal file
52
packages/redux-devtools-remote/examples/router/package.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"name": "redux-todomvc-example",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Redux TodoMVC example",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js",
|
||||||
|
"test:watch": "npm test -- --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rackt/redux.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/rackt/redux/issues"
|
||||||
|
},
|
||||||
|
"homepage": "http://rackt.github.io/redux",
|
||||||
|
"dependencies": {
|
||||||
|
"classnames": "^2.1.2",
|
||||||
|
"history": "^1.13.1",
|
||||||
|
"react": "^0.14.0",
|
||||||
|
"react-dom": "^0.14.0",
|
||||||
|
"react-redux": "^4.0.0",
|
||||||
|
"react-router": "^1.0.2",
|
||||||
|
"redux": "^3.0.0",
|
||||||
|
"redux-router": "^1.0.0-beta5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.3.15",
|
||||||
|
"babel-loader": "^6.2.0",
|
||||||
|
"babel-plugin-react-transform": "^2.0.0-beta1",
|
||||||
|
"babel-plugin-transform-decorators-legacy": "^1.2.0",
|
||||||
|
"babel-polyfill": "^6.3.14",
|
||||||
|
"babel-preset-es2015": "^6.3.13",
|
||||||
|
"babel-preset-react": "^6.3.13",
|
||||||
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
|
"expect": "^1.8.0",
|
||||||
|
"express": "^4.13.3",
|
||||||
|
"jsdom": "^5.6.1",
|
||||||
|
"mocha": "^2.2.5",
|
||||||
|
"node-libs-browser": "^0.5.2",
|
||||||
|
"raw-loader": "^0.5.1",
|
||||||
|
"react-addons-test-utils": "^0.14.0",
|
||||||
|
"react-transform-hmr": "^1.0.0",
|
||||||
|
"style-loader": "^0.12.3",
|
||||||
|
"todomvc-app-css": "^2.0.1",
|
||||||
|
"webpack": "^1.9.11",
|
||||||
|
"webpack-dev-middleware": "^1.2.0",
|
||||||
|
"webpack-hot-middleware": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { routerStateReducer } from 'redux-router';
|
||||||
|
import todos from './todos';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
todos,
|
||||||
|
router: routerStateReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
ADD_TODO,
|
||||||
|
DELETE_TODO,
|
||||||
|
EDIT_TODO,
|
||||||
|
COMPLETE_TODO,
|
||||||
|
COMPLETE_ALL,
|
||||||
|
CLEAR_COMPLETED,
|
||||||
|
} from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
const initialState = [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function todos(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_TODO:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
||||||
|
completed: false,
|
||||||
|
text: action.text,
|
||||||
|
},
|
||||||
|
...state,
|
||||||
|
];
|
||||||
|
|
||||||
|
case DELETE_TODO:
|
||||||
|
return state.filter((todo) => todo.id !== action.id);
|
||||||
|
|
||||||
|
case EDIT_TODO:
|
||||||
|
return state.map((todo) =>
|
||||||
|
todo.id === action.id
|
||||||
|
? Object.assign({}, todo, { text: action.text })
|
||||||
|
: todo
|
||||||
|
);
|
||||||
|
|
||||||
|
case COMPLETE_TODO:
|
||||||
|
return state.map((todo) =>
|
||||||
|
todo.id === action.id
|
||||||
|
? Object.assign({}, todo, { completed: !todo.completed })
|
||||||
|
: todo
|
||||||
|
);
|
||||||
|
|
||||||
|
case COMPLETE_ALL:
|
||||||
|
const areAllMarked = state.every((todo) => todo.completed);
|
||||||
|
return state.map((todo) =>
|
||||||
|
Object.assign({}, todo, {
|
||||||
|
completed: !areAllMarked,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
case CLEAR_COMPLETED:
|
||||||
|
return state.filter((todo) => todo.completed === false);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
32
packages/redux-devtools-remote/examples/router/server.js
Normal file
32
packages/redux-devtools-remote/examples/router/server.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
var webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
|
||||||
|
var app = new require('express')();
|
||||||
|
var port = 4002;
|
||||||
|
|
||||||
|
var compiler = webpack(config);
|
||||||
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.get('/', function (req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { createStore, compose } from 'redux';
|
||||||
|
import {
|
||||||
|
reduxReactRouter,
|
||||||
|
routerStateReducer,
|
||||||
|
ReduxRouter,
|
||||||
|
} from 'redux-router';
|
||||||
|
//import createHistory from 'history/lib/createBrowserHistory';
|
||||||
|
import devTools from 'remote-redux-devtools';
|
||||||
|
import createHistory from 'history/lib/createHashHistory';
|
||||||
|
import rootReducer from '../reducers';
|
||||||
|
|
||||||
|
export default function configureStore(initialState) {
|
||||||
|
let finalCreateStore = compose(
|
||||||
|
reduxReactRouter({ createHistory }),
|
||||||
|
devTools({ realtime: true })
|
||||||
|
)(createStore);
|
||||||
|
|
||||||
|
const store = finalCreateStore(rootReducer, initialState);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
// Enable Webpack hot module replacement for reducers
|
||||||
|
module.hot.accept('../reducers', () => {
|
||||||
|
const nextReducer = require('../reducers').default;
|
||||||
|
store.replaceReducer(nextReducer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
import * as actions from '../../actions/todos';
|
||||||
|
|
||||||
|
describe('todo actions', () => {
|
||||||
|
it('addTodo should create ADD_TODO action', () => {
|
||||||
|
expect(actions.addTodo('Use Redux')).toEqual({
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Use Redux',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deleteTodo should create DELETE_TODO action', () => {
|
||||||
|
expect(actions.deleteTodo(1)).toEqual({
|
||||||
|
type: types.DELETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editTodo should create EDIT_TODO action', () => {
|
||||||
|
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
||||||
|
type: types.EDIT_TODO,
|
||||||
|
id: 1,
|
||||||
|
text: 'Use Redux everywhere',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeTodo should create COMPLETE_TODO action', () => {
|
||||||
|
expect(actions.completeTodo(1)).toEqual({
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeAll should create COMPLETE_ALL action', () => {
|
||||||
|
expect(actions.completeAll()).toEqual({
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
||||||
|
expect(actions.clearCompleted('Use Redux')).toEqual({
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,108 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
completedCount: 0,
|
||||||
|
activeCount: 0,
|
||||||
|
filter: SHOW_ALL,
|
||||||
|
onClearCompleted: expect.createSpy(),
|
||||||
|
onShow: expect.createSpy(),
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Footer {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextContent(elem) {
|
||||||
|
const children = Array.isArray(elem.props.children)
|
||||||
|
? elem.props.children
|
||||||
|
: [elem.props.children];
|
||||||
|
|
||||||
|
return children.reduce(function concatText(out, child) {
|
||||||
|
// Children are either elements or text strings
|
||||||
|
return out + (child.props ? getTextContent(child) : child);
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Footer', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('footer');
|
||||||
|
expect(output.props.className).toBe('footer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 0 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('No items left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when above 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 1 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('1 item left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render filters', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
expect(filters.type).toBe('ul');
|
||||||
|
expect(filters.props.className).toBe('filters');
|
||||||
|
expect(filters.props.children.length).toBe(3);
|
||||||
|
filters.props.children.forEach(function checkFilter(filter, i) {
|
||||||
|
expect(filter.type).toBe('li');
|
||||||
|
const a = filter.props.children;
|
||||||
|
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
||||||
|
expect(a.props.children).toBe(
|
||||||
|
{
|
||||||
|
0: 'All',
|
||||||
|
1: 'Active',
|
||||||
|
2: 'Completed',
|
||||||
|
}[i]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onShow when a filter is clicked', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
const filterLink = filters.props.children[1].props.children;
|
||||||
|
filterLink.props.onClick({});
|
||||||
|
expect(props.onShow).toHaveBeenCalledWith(SHOW_ACTIVE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt show clear button when no completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 0 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render clear button when completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear.type).toBe('button');
|
||||||
|
expect(clear.props.children).toBe('Clear completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onClearCompleted on clear button click', () => {
|
||||||
|
const { output, props } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
clear.props.onClick({});
|
||||||
|
expect(props.onClearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Header from '../../components/Header';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const props = {
|
||||||
|
addTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Header {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Header', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('header');
|
||||||
|
expect(output.props.className).toBe('header');
|
||||||
|
|
||||||
|
const [h1, input] = output.props.children;
|
||||||
|
|
||||||
|
expect(h1.type).toBe('h1');
|
||||||
|
expect(h1.props.children).toBe('todos');
|
||||||
|
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.newTodo).toBe(true);
|
||||||
|
expect(input.props.placeholder).toBe('What needs to be done?');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call call addTodo if length of text is greater than 0', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children[1];
|
||||||
|
input.props.onSave('');
|
||||||
|
expect(props.addTodo.calls.length).toBe(0);
|
||||||
|
input.props.onSave('Use Redux');
|
||||||
|
expect(props.addTodo.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,150 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import MainSection from '../../components/MainSection';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: {
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
completeAll: expect.createSpy(),
|
||||||
|
clearCompleted: expect.createSpy(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<MainSection {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('MainSection', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('section');
|
||||||
|
expect(output.props.className).toBe('main');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggle all input', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.type).toBe('input');
|
||||||
|
expect(toggle.props.type).toBe('checkbox');
|
||||||
|
expect(toggle.props.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be checked if all todos completed', () => {
|
||||||
|
const { output } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.props.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call completeAll on change', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
toggle.props.onChange({});
|
||||||
|
expect(props.actions.completeAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('footer', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
expect(footer.type).toBe(Footer);
|
||||||
|
expect(footer.props.completedCount).toBe(1);
|
||||||
|
expect(footer.props.activeCount).toBe(1);
|
||||||
|
expect(footer.props.filter).toBe(SHOW_ALL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onShow should set the filter', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, , updatedFooter] = updated.props.children;
|
||||||
|
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted should call clearCompleted', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
||||||
|
const { output, props } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todo list', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, list] = output.props.children;
|
||||||
|
expect(list.type).toBe('ul');
|
||||||
|
expect(list.props.children.length).toBe(2);
|
||||||
|
list.props.children.forEach((item, i) => {
|
||||||
|
expect(item.type).toBe(TodoItem);
|
||||||
|
expect(item.props.todo).toBe(props.todos[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter items', () => {
|
||||||
|
const { output, renderer, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, updatedList] = updated.props.children;
|
||||||
|
expect(updatedList.props.children.length).toBe(1);
|
||||||
|
expect(updatedList.props.children[0].props.todo).toBe(props.todos[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,118 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(editing = false) {
|
||||||
|
const props = {
|
||||||
|
todo: {
|
||||||
|
id: 0,
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoItem {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoItem', () => {
|
||||||
|
it('initial render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('');
|
||||||
|
|
||||||
|
const div = output.props.children;
|
||||||
|
|
||||||
|
expect(div.type).toBe('div');
|
||||||
|
expect(div.props.className).toBe('view');
|
||||||
|
|
||||||
|
const [input, label, button] = div.props.children;
|
||||||
|
|
||||||
|
expect(input.type).toBe('input');
|
||||||
|
expect(input.props.checked).toBe(false);
|
||||||
|
|
||||||
|
expect(label.type).toBe('label');
|
||||||
|
expect(label.props.children).toBe('Use Redux');
|
||||||
|
|
||||||
|
expect(button.type).toBe('button');
|
||||||
|
expect(button.props.className).toBe('destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input onChange should call completeTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children.props.children[0];
|
||||||
|
input.props.onChange({});
|
||||||
|
expect(props.completeTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('button onClick should call deleteTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const button = output.props.children.props.children[2];
|
||||||
|
button.props.onClick({});
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('label onDoubleClick should put component in edit state', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('editing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edit state render', () => {
|
||||||
|
const { output } = setup(true);
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('editing');
|
||||||
|
|
||||||
|
const input = output.props.children;
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.text).toBe('Use Redux');
|
||||||
|
expect(input.props.editing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call editTodo', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
expect(props.editTodo).toHaveBeenCalledWith(0, 'Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('');
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should exit component from edit state', () => {
|
||||||
|
const { output, renderer } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,84 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
onSave: expect.createSpy(),
|
||||||
|
text: 'Use Redux',
|
||||||
|
placeholder: 'What needs to be done?',
|
||||||
|
editing: false,
|
||||||
|
newTodo: false,
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoTextInput {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoTextInput', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.props.placeholder).toEqual('What needs to be done?');
|
||||||
|
expect(output.props.value).toEqual('Use Redux');
|
||||||
|
expect(output.props.className).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when editing=true', () => {
|
||||||
|
const { output } = setup({ editing: true });
|
||||||
|
expect(output.props.className).toEqual('edit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when newTodo=true', () => {
|
||||||
|
const { output } = setup({ newTodo: true });
|
||||||
|
expect(output.props.className).toEqual('new-todo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update value on change', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
output.props.onChange({ target: { value: 'Use Radox' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('Use Radox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on return key press', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset state on return key press if newTodo', () => {
|
||||||
|
const { output, renderer } = setup({ newTodo: true });
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on blur', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt call onSave on blur if newTodo', () => {
|
||||||
|
const { output, props } = setup({ newTodo: true });
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,325 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import todos from '../../reducers/todos';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
|
||||||
|
describe('todos reducer', () => {
|
||||||
|
it('should handle initial state', () => {
|
||||||
|
expect(todos(undefined, {})).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ADD_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos([], {
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Run the tests',
|
||||||
|
})
|
||||||
|
).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',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
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', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle EDIT_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_TODO', () => {
|
||||||
|
expect(
|
||||||
|
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',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_ALL', () => {
|
||||||
|
expect(
|
||||||
|
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',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Unmark if all todos are currently completed
|
||||||
|
expect(
|
||||||
|
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',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { jsdom } from 'jsdom';
|
||||||
|
|
||||||
|
global.document = jsdom('<!doctype html><html><body></body></html>');
|
||||||
|
global.window = document.defaultView;
|
||||||
|
global.navigator = global.window.navigator;
|
|
@ -0,0 +1,46 @@
|
||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: 'cheap-module-eval-source-map',
|
||||||
|
entry: ['webpack-hot-middleware/client', './index'],
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
publicPath: '/static/',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.NoErrorsPlugin(),
|
||||||
|
],
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
include: __dirname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css?$/,
|
||||||
|
loaders: ['style', 'raw'],
|
||||||
|
include: __dirname,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var src = path.join(__dirname, '..', '..', 'src');
|
||||||
|
var nodeModules = path.join(__dirname, '..', '..', 'node_modules');
|
||||||
|
var fs = require('fs');
|
||||||
|
if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
|
||||||
|
// Resolve to source
|
||||||
|
module.exports.resolve = { alias: { 'remote-redux-devtools': src } };
|
||||||
|
// Compile from source
|
||||||
|
module.exports.module.loaders.push({
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
include: src,
|
||||||
|
});
|
||||||
|
}
|
3
packages/redux-devtools-remote/examples/todomvc/.babelrc
Normal file
3
packages/redux-devtools-remote/examples/todomvc/.babelrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"]
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as types from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
export function addTodo(text) {
|
||||||
|
return { type: types.ADD_TODO, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTodo(id) {
|
||||||
|
return { type: types.DELETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editTodo(id, text) {
|
||||||
|
return { type: types.EDIT_TODO, id, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeTodo(id) {
|
||||||
|
return { type: types.COMPLETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeAll() {
|
||||||
|
return { type: types.COMPLETE_ALL };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCompleted() {
|
||||||
|
return { type: types.CLEAR_COMPLETED };
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { PropTypes, Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
|
const FILTER_TITLES = {
|
||||||
|
[SHOW_ALL]: 'All',
|
||||||
|
[SHOW_ACTIVE]: 'Active',
|
||||||
|
[SHOW_COMPLETED]: 'Completed',
|
||||||
|
};
|
||||||
|
|
||||||
|
class Footer extends Component {
|
||||||
|
renderTodoCount() {
|
||||||
|
const { activeCount } = this.props;
|
||||||
|
const itemWord = activeCount === 1 ? 'item' : 'items';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="todo-count">
|
||||||
|
<strong>{activeCount || 'No'}</strong> {itemWord} left
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFilterLink(filter) {
|
||||||
|
const title = FILTER_TITLES[filter];
|
||||||
|
const { filter: selectedFilter, onShow } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={classnames({ selected: filter === selectedFilter })}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => onShow(filter)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderClearButton() {
|
||||||
|
const { completedCount, onClearCompleted } = this.props;
|
||||||
|
if (completedCount > 0) {
|
||||||
|
return (
|
||||||
|
<button className="clear-completed" onClick={onClearCompleted}>
|
||||||
|
Clear completed
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<footer className="footer">
|
||||||
|
{this.renderTodoCount()}
|
||||||
|
<ul className="filters">
|
||||||
|
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
|
||||||
|
<li key={filter}>{this.renderFilterLink(filter)}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{this.renderClearButton()}
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Footer.propTypes = {
|
||||||
|
completedCount: PropTypes.number.isRequired,
|
||||||
|
activeCount: PropTypes.number.isRequired,
|
||||||
|
filter: PropTypes.string.isRequired,
|
||||||
|
onClearCompleted: PropTypes.func.isRequired,
|
||||||
|
onShow: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { PropTypes, Component } from 'react';
|
||||||
|
import TodoTextInput from './TodoTextInput';
|
||||||
|
|
||||||
|
class Header extends Component {
|
||||||
|
handleSave(text) {
|
||||||
|
if (text.length !== 0) {
|
||||||
|
this.props.addTodo(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<header className="header">
|
||||||
|
<h1>todos</h1>
|
||||||
|
<TodoTextInput
|
||||||
|
newTodo
|
||||||
|
onSave={this.handleSave.bind(this)}
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
addTodo: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import TodoItem from './TodoItem';
|
||||||
|
import Footer from './Footer';
|
||||||
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
|
const TODO_FILTERS = {
|
||||||
|
[SHOW_ALL]: () => true,
|
||||||
|
[SHOW_ACTIVE]: (todo) => !todo.completed,
|
||||||
|
[SHOW_COMPLETED]: (todo) => todo.completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
class MainSection extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = { filter: SHOW_ALL };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClearCompleted() {
|
||||||
|
const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
|
||||||
|
if (atLeastOneCompleted) {
|
||||||
|
this.props.actions.clearCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShow(filter) {
|
||||||
|
this.setState({ filter });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToggleAll(completedCount) {
|
||||||
|
const { todos, actions } = this.props;
|
||||||
|
if (todos.length > 0) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className="toggle-all"
|
||||||
|
type="checkbox"
|
||||||
|
checked={completedCount === todos.length}
|
||||||
|
onChange={actions.completeAll}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooter(completedCount) {
|
||||||
|
const { todos } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
const activeCount = todos.length - completedCount;
|
||||||
|
|
||||||
|
if (todos.length) {
|
||||||
|
return (
|
||||||
|
<Footer
|
||||||
|
completedCount={completedCount}
|
||||||
|
activeCount={activeCount}
|
||||||
|
filter={filter}
|
||||||
|
onClearCompleted={this.handleClearCompleted.bind(this)}
|
||||||
|
onShow={this.handleShow.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { todos, actions } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
|
||||||
|
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
|
||||||
|
const completedCount = todos.reduce(
|
||||||
|
(count, todo) => (todo.completed ? count + 1 : count),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="main">
|
||||||
|
{this.renderToggleAll(completedCount)}
|
||||||
|
<ul className="todo-list">
|
||||||
|
{filteredTodos.map((todo) => (
|
||||||
|
<TodoItem key={todo.id} todo={todo} {...actions} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{this.renderFooter(completedCount)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MainSection.propTypes = {
|
||||||
|
todos: PropTypes.array.isRequired,
|
||||||
|
actions: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainSection;
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import TodoTextInput from './TodoTextInput';
|
||||||
|
|
||||||
|
class TodoItem extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
|
editing: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoubleClick() {
|
||||||
|
this.setState({ editing: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSave(id, text) {
|
||||||
|
if (text.length === 0) {
|
||||||
|
this.props.deleteTodo(id);
|
||||||
|
} else {
|
||||||
|
this.props.editTodo(id, text);
|
||||||
|
}
|
||||||
|
this.setState({ editing: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { todo, completeTodo, deleteTodo } = this.props;
|
||||||
|
|
||||||
|
let element;
|
||||||
|
if (this.state.editing) {
|
||||||
|
element = (
|
||||||
|
<TodoTextInput
|
||||||
|
text={todo.text}
|
||||||
|
editing={this.state.editing}
|
||||||
|
onSave={(text) => this.handleSave(todo.id, text)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
element = (
|
||||||
|
<div className="view">
|
||||||
|
<input
|
||||||
|
className="toggle"
|
||||||
|
type="checkbox"
|
||||||
|
checked={todo.completed}
|
||||||
|
onChange={() => completeTodo(todo.id)}
|
||||||
|
/>
|
||||||
|
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
|
||||||
|
{todo.text}
|
||||||
|
</label>
|
||||||
|
<button className="destroy" onClick={() => deleteTodo(todo.id)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={classnames({
|
||||||
|
completed: todo.completed,
|
||||||
|
editing: this.state.editing,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{element}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoItem.propTypes = {
|
||||||
|
todo: PropTypes.object.isRequired,
|
||||||
|
editTodo: PropTypes.func.isRequired,
|
||||||
|
deleteTodo: PropTypes.func.isRequired,
|
||||||
|
completeTodo: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoItem;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
class TodoTextInput extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
|
text: this.props.text || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
const text = e.target.value.trim();
|
||||||
|
if (e.which === 13) {
|
||||||
|
this.props.onSave(text);
|
||||||
|
if (this.props.newTodo) {
|
||||||
|
this.setState({ text: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
this.setState({ text: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur(e) {
|
||||||
|
if (!this.props.newTodo) {
|
||||||
|
this.props.onSave(e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={classnames({
|
||||||
|
edit: this.props.editing,
|
||||||
|
'new-todo': this.props.newTodo,
|
||||||
|
})}
|
||||||
|
type="text"
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
autoFocus="true"
|
||||||
|
value={this.state.text}
|
||||||
|
onBlur={this.handleBlur.bind(this)}
|
||||||
|
onChange={this.handleChange.bind(this)}
|
||||||
|
onKeyDown={this.handleSubmit.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoTextInput.propTypes = {
|
||||||
|
onSave: PropTypes.func.isRequired,
|
||||||
|
text: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
editing: PropTypes.bool,
|
||||||
|
newTodo: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoTextInput;
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const ADD_TODO = 'ADD_TODO';
|
||||||
|
export const DELETE_TODO = 'DELETE_TODO';
|
||||||
|
export const EDIT_TODO = 'EDIT_TODO';
|
||||||
|
export const COMPLETE_TODO = 'COMPLETE_TODO';
|
||||||
|
export const COMPLETE_ALL = 'COMPLETE_ALL';
|
||||||
|
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const SHOW_ALL = 'show_all';
|
||||||
|
export const SHOW_COMPLETED = 'show_completed';
|
||||||
|
export const SHOW_ACTIVE = 'show_active';
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import MainSection from '../components/MainSection';
|
||||||
|
import * as TodoActions from '../actions/todos';
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
const { todos, actions } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header addTodo={actions.addTodo} />
|
||||||
|
<MainSection todos={todos} actions={actions} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
todos: PropTypes.array.isRequired,
|
||||||
|
actions: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
todos: state.todos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
10
packages/redux-devtools-remote/examples/todomvc/index.html
Normal file
10
packages/redux-devtools-remote/examples/todomvc/index.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redux TodoMVC example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="todoapp" id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
packages/redux-devtools-remote/examples/todomvc/index.js
Normal file
16
packages/redux-devtools-remote/examples/todomvc/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'babel-polyfill';
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from './containers/App';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
import 'todomvc-app-css/index.css';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
48
packages/redux-devtools-remote/examples/todomvc/package.json
Normal file
48
packages/redux-devtools-remote/examples/todomvc/package.json
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"name": "redux-todomvc-example",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Redux TodoMVC example",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js",
|
||||||
|
"test:watch": "npm test -- --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rackt/redux.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/rackt/redux/issues"
|
||||||
|
},
|
||||||
|
"homepage": "http://rackt.github.io/redux",
|
||||||
|
"dependencies": {
|
||||||
|
"classnames": "^2.1.2",
|
||||||
|
"react": "^0.14.0",
|
||||||
|
"react-dom": "^0.14.0",
|
||||||
|
"react-redux": "^4.0.0",
|
||||||
|
"redux": "^3.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.3.15",
|
||||||
|
"babel-loader": "^6.2.0",
|
||||||
|
"babel-plugin-react-transform": "^2.0.0-beta1",
|
||||||
|
"babel-polyfill": "^6.3.14",
|
||||||
|
"babel-preset-es2015": "^6.3.13",
|
||||||
|
"babel-preset-react": "^6.3.13",
|
||||||
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
|
"expect": "^1.8.0",
|
||||||
|
"express": "^4.13.3",
|
||||||
|
"jsdom": "^5.6.1",
|
||||||
|
"mocha": "^2.2.5",
|
||||||
|
"node-libs-browser": "^0.5.2",
|
||||||
|
"raw-loader": "^0.5.1",
|
||||||
|
"react-addons-test-utils": "^0.14.0",
|
||||||
|
"react-transform-hmr": "^1.0.0",
|
||||||
|
"style-loader": "^0.12.3",
|
||||||
|
"todomvc-app-css": "^2.0.1",
|
||||||
|
"webpack": "^1.13.1",
|
||||||
|
"webpack-dev-middleware": "^1.2.0",
|
||||||
|
"webpack-hot-middleware": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import todos from './todos';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
todos,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
ADD_TODO,
|
||||||
|
DELETE_TODO,
|
||||||
|
EDIT_TODO,
|
||||||
|
COMPLETE_TODO,
|
||||||
|
COMPLETE_ALL,
|
||||||
|
CLEAR_COMPLETED,
|
||||||
|
} from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
const initialState = [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function todos(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_TODO:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
||||||
|
completed: false,
|
||||||
|
text: action.text,
|
||||||
|
},
|
||||||
|
...state,
|
||||||
|
];
|
||||||
|
|
||||||
|
case DELETE_TODO:
|
||||||
|
return state.filter((todo) => todo.id !== action.id);
|
||||||
|
|
||||||
|
case EDIT_TODO:
|
||||||
|
return state.map((todo) =>
|
||||||
|
todo.id === action.id
|
||||||
|
? Object.assign({}, todo, { text: action.text })
|
||||||
|
: todo
|
||||||
|
);
|
||||||
|
|
||||||
|
case COMPLETE_TODO:
|
||||||
|
return state.map((todo) =>
|
||||||
|
todo.id === action.id
|
||||||
|
? Object.assign({}, todo, { completed: !todo.completed })
|
||||||
|
: todo
|
||||||
|
);
|
||||||
|
|
||||||
|
case COMPLETE_ALL:
|
||||||
|
const areAllMarked = state.every((todo) => todo.completed);
|
||||||
|
return state.map((todo) =>
|
||||||
|
Object.assign({}, todo, {
|
||||||
|
completed: !areAllMarked,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
case CLEAR_COMPLETED:
|
||||||
|
return state.filter((todo) => todo.completed === false);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
32
packages/redux-devtools-remote/examples/todomvc/server.js
Normal file
32
packages/redux-devtools-remote/examples/todomvc/server.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
var webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
|
||||||
|
var app = new require('express')();
|
||||||
|
var port = 4002;
|
||||||
|
|
||||||
|
var compiler = webpack(config);
|
||||||
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.get('/', function (req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import devTools from 'remote-redux-devtools';
|
||||||
|
import rootReducer from '../reducers';
|
||||||
|
import * as actionCreators from '../actions/todos';
|
||||||
|
|
||||||
|
export default function configureStore(initialState) {
|
||||||
|
const store = createStore(
|
||||||
|
rootReducer,
|
||||||
|
initialState,
|
||||||
|
devTools({
|
||||||
|
realtime: true,
|
||||||
|
actionCreators,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
// Enable Webpack hot module replacement for reducers
|
||||||
|
module.hot.accept('../reducers', () => {
|
||||||
|
const nextReducer = require('../reducers').default;
|
||||||
|
store.replaceReducer(nextReducer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
import * as actions from '../../actions/todos';
|
||||||
|
|
||||||
|
describe('todo actions', () => {
|
||||||
|
it('addTodo should create ADD_TODO action', () => {
|
||||||
|
expect(actions.addTodo('Use Redux')).toEqual({
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Use Redux',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deleteTodo should create DELETE_TODO action', () => {
|
||||||
|
expect(actions.deleteTodo(1)).toEqual({
|
||||||
|
type: types.DELETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editTodo should create EDIT_TODO action', () => {
|
||||||
|
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
||||||
|
type: types.EDIT_TODO,
|
||||||
|
id: 1,
|
||||||
|
text: 'Use Redux everywhere',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeTodo should create COMPLETE_TODO action', () => {
|
||||||
|
expect(actions.completeTodo(1)).toEqual({
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeAll should create COMPLETE_ALL action', () => {
|
||||||
|
expect(actions.completeAll()).toEqual({
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
||||||
|
expect(actions.clearCompleted('Use Redux')).toEqual({
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,108 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
completedCount: 0,
|
||||||
|
activeCount: 0,
|
||||||
|
filter: SHOW_ALL,
|
||||||
|
onClearCompleted: expect.createSpy(),
|
||||||
|
onShow: expect.createSpy(),
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Footer {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextContent(elem) {
|
||||||
|
const children = Array.isArray(elem.props.children)
|
||||||
|
? elem.props.children
|
||||||
|
: [elem.props.children];
|
||||||
|
|
||||||
|
return children.reduce(function concatText(out, child) {
|
||||||
|
// Children are either elements or text strings
|
||||||
|
return out + (child.props ? getTextContent(child) : child);
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Footer', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('footer');
|
||||||
|
expect(output.props.className).toBe('footer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 0 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('No items left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when above 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 1 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('1 item left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render filters', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
expect(filters.type).toBe('ul');
|
||||||
|
expect(filters.props.className).toBe('filters');
|
||||||
|
expect(filters.props.children.length).toBe(3);
|
||||||
|
filters.props.children.forEach(function checkFilter(filter, i) {
|
||||||
|
expect(filter.type).toBe('li');
|
||||||
|
const a = filter.props.children;
|
||||||
|
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
||||||
|
expect(a.props.children).toBe(
|
||||||
|
{
|
||||||
|
0: 'All',
|
||||||
|
1: 'Active',
|
||||||
|
2: 'Completed',
|
||||||
|
}[i]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onShow when a filter is clicked', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
const filterLink = filters.props.children[1].props.children;
|
||||||
|
filterLink.props.onClick({});
|
||||||
|
expect(props.onShow).toHaveBeenCalledWith(SHOW_ACTIVE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt show clear button when no completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 0 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render clear button when completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear.type).toBe('button');
|
||||||
|
expect(clear.props.children).toBe('Clear completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onClearCompleted on clear button click', () => {
|
||||||
|
const { output, props } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
clear.props.onClick({});
|
||||||
|
expect(props.onClearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Header from '../../components/Header';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const props = {
|
||||||
|
addTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Header {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Header', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('header');
|
||||||
|
expect(output.props.className).toBe('header');
|
||||||
|
|
||||||
|
const [h1, input] = output.props.children;
|
||||||
|
|
||||||
|
expect(h1.type).toBe('h1');
|
||||||
|
expect(h1.props.children).toBe('todos');
|
||||||
|
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.newTodo).toBe(true);
|
||||||
|
expect(input.props.placeholder).toBe('What needs to be done?');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call call addTodo if length of text is greater than 0', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children[1];
|
||||||
|
input.props.onSave('');
|
||||||
|
expect(props.addTodo.calls.length).toBe(0);
|
||||||
|
input.props.onSave('Use Redux');
|
||||||
|
expect(props.addTodo.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,150 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import MainSection from '../../components/MainSection';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: {
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
completeAll: expect.createSpy(),
|
||||||
|
clearCompleted: expect.createSpy(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<MainSection {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('MainSection', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('section');
|
||||||
|
expect(output.props.className).toBe('main');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggle all input', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.type).toBe('input');
|
||||||
|
expect(toggle.props.type).toBe('checkbox');
|
||||||
|
expect(toggle.props.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be checked if all todos completed', () => {
|
||||||
|
const { output } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.props.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call completeAll on change', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
toggle.props.onChange({});
|
||||||
|
expect(props.actions.completeAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('footer', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
expect(footer.type).toBe(Footer);
|
||||||
|
expect(footer.props.completedCount).toBe(1);
|
||||||
|
expect(footer.props.activeCount).toBe(1);
|
||||||
|
expect(footer.props.filter).toBe(SHOW_ALL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onShow should set the filter', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, , updatedFooter] = updated.props.children;
|
||||||
|
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted should call clearCompleted', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
||||||
|
const { output, props } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todo list', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, list] = output.props.children;
|
||||||
|
expect(list.type).toBe('ul');
|
||||||
|
expect(list.props.children.length).toBe(2);
|
||||||
|
list.props.children.forEach((item, i) => {
|
||||||
|
expect(item.type).toBe(TodoItem);
|
||||||
|
expect(item.props.todo).toBe(props.todos[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter items', () => {
|
||||||
|
const { output, renderer, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, updatedList] = updated.props.children;
|
||||||
|
expect(updatedList.props.children.length).toBe(1);
|
||||||
|
expect(updatedList.props.children[0].props.todo).toBe(props.todos[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,118 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(editing = false) {
|
||||||
|
const props = {
|
||||||
|
todo: {
|
||||||
|
id: 0,
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoItem {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoItem', () => {
|
||||||
|
it('initial render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('');
|
||||||
|
|
||||||
|
const div = output.props.children;
|
||||||
|
|
||||||
|
expect(div.type).toBe('div');
|
||||||
|
expect(div.props.className).toBe('view');
|
||||||
|
|
||||||
|
const [input, label, button] = div.props.children;
|
||||||
|
|
||||||
|
expect(input.type).toBe('input');
|
||||||
|
expect(input.props.checked).toBe(false);
|
||||||
|
|
||||||
|
expect(label.type).toBe('label');
|
||||||
|
expect(label.props.children).toBe('Use Redux');
|
||||||
|
|
||||||
|
expect(button.type).toBe('button');
|
||||||
|
expect(button.props.className).toBe('destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input onChange should call completeTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children.props.children[0];
|
||||||
|
input.props.onChange({});
|
||||||
|
expect(props.completeTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('button onClick should call deleteTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const button = output.props.children.props.children[2];
|
||||||
|
button.props.onClick({});
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('label onDoubleClick should put component in edit state', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('editing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edit state render', () => {
|
||||||
|
const { output } = setup(true);
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('editing');
|
||||||
|
|
||||||
|
const input = output.props.children;
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.text).toBe('Use Redux');
|
||||||
|
expect(input.props.editing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call editTodo', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
expect(props.editTodo).toHaveBeenCalledWith(0, 'Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('');
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should exit component from edit state', () => {
|
||||||
|
const { output, renderer } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,84 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
onSave: expect.createSpy(),
|
||||||
|
text: 'Use Redux',
|
||||||
|
placeholder: 'What needs to be done?',
|
||||||
|
editing: false,
|
||||||
|
newTodo: false,
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoTextInput {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoTextInput', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.props.placeholder).toEqual('What needs to be done?');
|
||||||
|
expect(output.props.value).toEqual('Use Redux');
|
||||||
|
expect(output.props.className).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when editing=true', () => {
|
||||||
|
const { output } = setup({ editing: true });
|
||||||
|
expect(output.props.className).toEqual('edit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when newTodo=true', () => {
|
||||||
|
const { output } = setup({ newTodo: true });
|
||||||
|
expect(output.props.className).toEqual('new-todo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update value on change', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
output.props.onChange({ target: { value: 'Use Radox' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('Use Radox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on return key press', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset state on return key press if newTodo', () => {
|
||||||
|
const { output, renderer } = setup({ newTodo: true });
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on blur', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt call onSave on blur if newTodo', () => {
|
||||||
|
const { output, props } = setup({ newTodo: true });
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,325 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import todos from '../../reducers/todos';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
|
||||||
|
describe('todos reducer', () => {
|
||||||
|
it('should handle initial state', () => {
|
||||||
|
expect(todos(undefined, {})).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ADD_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos([], {
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Run the tests',
|
||||||
|
})
|
||||||
|
).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',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
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', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle EDIT_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_TODO', () => {
|
||||||
|
expect(
|
||||||
|
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',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_ALL', () => {
|
||||||
|
expect(
|
||||||
|
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',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Unmark if all todos are currently completed
|
||||||
|
expect(
|
||||||
|
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',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { jsdom } from 'jsdom';
|
||||||
|
|
||||||
|
global.document = jsdom('<!doctype html><html><body></body></html>');
|
||||||
|
global.window = document.defaultView;
|
||||||
|
global.navigator = global.window.navigator;
|
|
@ -0,0 +1,46 @@
|
||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: 'cheap-module-eval-source-map',
|
||||||
|
entry: ['webpack-hot-middleware/client', './index'],
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
filename: 'bundle.js',
|
||||||
|
publicPath: '/static/',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.NoErrorsPlugin(),
|
||||||
|
],
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
include: __dirname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css?$/,
|
||||||
|
loaders: ['style', 'raw'],
|
||||||
|
include: __dirname,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var src = path.join(__dirname, '..', '..', 'src');
|
||||||
|
var nodeModules = path.join(__dirname, '..', '..', 'node_modules');
|
||||||
|
var fs = require('fs');
|
||||||
|
if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
|
||||||
|
// Resolve to source
|
||||||
|
module.exports.resolve = { alias: { 'remote-redux-devtools': src } };
|
||||||
|
// Compile from source
|
||||||
|
module.exports.module.loaders.push({
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
include: src,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"]
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
|
||||||
|
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
|
||||||
|
|
||||||
|
export function increment() {
|
||||||
|
return {
|
||||||
|
type: INCREMENT_COUNTER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrement() {
|
||||||
|
return {
|
||||||
|
type: DECREMENT_COUNTER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementIfOdd() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { counter } = getState();
|
||||||
|
|
||||||
|
if (counter % 2 === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(increment());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementAsync(delay = 1000) {
|
||||||
|
return (dispatch) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(increment());
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
export const START_MONITORING = 'START_MONITORING';
|
||||||
|
export const STOP_MONITORING = 'STOP_MONITORING';
|
||||||
|
export const SEND_TO_MONITOR = 'SEND_TO_MONITOR';
|
||||||
|
|
||||||
|
export function startMonitoring() {
|
||||||
|
return {
|
||||||
|
type: START_MONITORING,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopMonitoring() {
|
||||||
|
return {
|
||||||
|
type: STOP_MONITORING,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendToMonitor() {
|
||||||
|
return {
|
||||||
|
type: SEND_TO_MONITOR,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
startMonitoring,
|
||||||
|
stopMonitoring,
|
||||||
|
sendToMonitor,
|
||||||
|
increment,
|
||||||
|
decrement,
|
||||||
|
counter,
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Clicked: {counter} times <button onClick={increment}>+</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>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Counter.propTypes = {
|
||||||
|
startMonitoring: PropTypes.func.isRequired,
|
||||||
|
stopMonitoring: PropTypes.func.isRequired,
|
||||||
|
sendToMonitor: PropTypes.func.isRequired,
|
||||||
|
increment: PropTypes.func.isRequired,
|
||||||
|
decrement: PropTypes.func.isRequired,
|
||||||
|
counter: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Counter;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Counter from '../components/Counter';
|
||||||
|
import * as CounterActions from '../actions/counter';
|
||||||
|
import * as MonitorActions from '../actions/monitoring';
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
counter: state.counter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({ ...CounterActions, ...MonitorActions }, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redux counter example</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root,
|
||||||
|
#container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#container > p {
|
||||||
|
padding: 10px;
|
||||||
|
margin: auto;
|
||||||
|
font: 20px Arial;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 5px 15px;
|
||||||
|
font: bold 16px Arial;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 55px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from './containers/App';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<div id="container">
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
<iframe src="http://remotedev.io/local/" />
|
||||||
|
</div>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "redux-counter-example",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Redux counter example",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js",
|
||||||
|
"test:watch": "npm test -- --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rackt/redux.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/rackt/redux/issues"
|
||||||
|
},
|
||||||
|
"homepage": "http://rackt.github.io/redux",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^0.14.0",
|
||||||
|
"react-dom": "^0.14.0",
|
||||||
|
"react-redux": "^4.0.0",
|
||||||
|
"redux": "^3.5.2",
|
||||||
|
"redux-thunk": "^0.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.3.15",
|
||||||
|
"babel-loader": "^6.2.0",
|
||||||
|
"babel-plugin-react-transform": "^2.0.0-beta1",
|
||||||
|
"babel-polyfill": "^6.3.14",
|
||||||
|
"babel-preset-es2015": "^6.3.13",
|
||||||
|
"babel-preset-react": "^6.3.13",
|
||||||
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
|
"expect": "^1.6.0",
|
||||||
|
"express": "^4.13.3",
|
||||||
|
"jsdom": "^5.6.1",
|
||||||
|
"mocha": "^2.2.5",
|
||||||
|
"node-libs-browser": "^0.5.2",
|
||||||
|
"react-addons-test-utils": "^0.14.0",
|
||||||
|
"react-transform-hmr": "^1.0.0",
|
||||||
|
"redux-immutable-state-invariant": "^1.1.1",
|
||||||
|
"webpack": "^1.9.11",
|
||||||
|
"webpack-dev-middleware": "^1.2.0",
|
||||||
|
"webpack-hot-middleware": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';
|
||||||
|
|
||||||
|
export default function counter(state = 0, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case INCREMENT_COUNTER:
|
||||||
|
return state + 1;
|
||||||
|
case DECREMENT_COUNTER:
|
||||||
|
return state - 1;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import counter from './counter';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
counter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
|
@ -0,0 +1,32 @@
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
var webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
|
||||||
|
var app = new require('express')();
|
||||||
|
var port = 4001;
|
||||||
|
|
||||||
|
var compiler = webpack(config);
|
||||||
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.get('/', function (req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import invariant from 'redux-immutable-state-invariant';
|
||||||
|
import devTools from 'remote-redux-devtools';
|
||||||
|
import reducer from '../reducers';
|
||||||
|
import {
|
||||||
|
START_MONITORING,
|
||||||
|
STOP_MONITORING,
|
||||||
|
SEND_TO_MONITOR,
|
||||||
|
} from '../actions/monitoring';
|
||||||
|
|
||||||
|
export default function configureStore(initialState) {
|
||||||
|
const enhancer = compose(
|
||||||
|
applyMiddleware(invariant(), thunk),
|
||||||
|
devTools({
|
||||||
|
realtime: false,
|
||||||
|
startOn: START_MONITORING,
|
||||||
|
stopOn: STOP_MONITORING,
|
||||||
|
sendOn: SEND_TO_MONITOR,
|
||||||
|
sendOnError: 1,
|
||||||
|
maxAge: 30,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = createStore(reducer, initialState, enhancer);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
// Enable Webpack hot module replacement for reducers
|
||||||
|
module.hot.accept('../reducers', () => {
|
||||||
|
const nextReducer = require('../reducers').default;
|
||||||
|
store.replaceReducer(nextReducer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import { applyMiddleware } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import * as actions from '../../actions/counter';
|
||||||
|
|
||||||
|
const middlewares = [thunk];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates a mock of Redux store with middleware.
|
||||||
|
*/
|
||||||
|
function mockStore(getState, expectedActions, onLastAction) {
|
||||||
|
if (!Array.isArray(expectedActions)) {
|
||||||
|
throw new Error('expectedActions should be an array of expected actions.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof onLastAction !== 'undefined' &&
|
||||||
|
typeof onLastAction !== 'function'
|
||||||
|
) {
|
||||||
|
throw new Error('onLastAction should either be undefined or function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockStoreWithoutMiddleware() {
|
||||||
|
return {
|
||||||
|
getState() {
|
||||||
|
return typeof getState === 'function' ? getState() : getState;
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatch(action) {
|
||||||
|
const expectedAction = expectedActions.shift();
|
||||||
|
expect(action).toEqual(expectedAction);
|
||||||
|
if (onLastAction && !expectedActions.length) {
|
||||||
|
onLastAction();
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
|
||||||
|
mockStoreWithoutMiddleware
|
||||||
|
);
|
||||||
|
|
||||||
|
return mockStoreWithMiddleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
it('increment should create increment action', () => {
|
||||||
|
expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrement should create decrement action', () => {
|
||||||
|
expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementIfOdd should create increment action', (done) => {
|
||||||
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
|
const store = mockStore({ counter: 1 }, expectedActions, done);
|
||||||
|
store.dispatch(actions.incrementIfOdd());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementIfOdd shouldnt create increment action if counter is even', (done) => {
|
||||||
|
const expectedActions = [];
|
||||||
|
const store = mockStore({ counter: 2 }, expectedActions);
|
||||||
|
store.dispatch(actions.incrementIfOdd());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementAsync should create increment action', (done) => {
|
||||||
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
|
const store = mockStore({ counter: 0 }, expectedActions, done);
|
||||||
|
store.dispatch(actions.incrementAsync(100));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Counter from '../../components/Counter';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const actions = {
|
||||||
|
increment: expect.createSpy(),
|
||||||
|
incrementIfOdd: expect.createSpy(),
|
||||||
|
incrementAsync: expect.createSpy(),
|
||||||
|
decrement: expect.createSpy(),
|
||||||
|
};
|
||||||
|
const component = TestUtils.renderIntoDocument(
|
||||||
|
<Counter counter={1} {...actions} />
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
component: component,
|
||||||
|
actions: actions,
|
||||||
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
|
||||||
|
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Counter component', () => {
|
||||||
|
it('should display count', () => {
|
||||||
|
const { p } = setup();
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('first button should call increment', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[0]);
|
||||||
|
expect(actions.increment).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('second button should call decrement', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[1]);
|
||||||
|
expect(actions.decrement).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('third button should call incrementIfOdd', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(actions.incrementIfOdd).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fourth button should call incrementAsync', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[3]);
|
||||||
|
expect(actions.incrementAsync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from '../../containers/App';
|
||||||
|
import configureStore from '../../store/configureStore';
|
||||||
|
|
||||||
|
function setup(initialState) {
|
||||||
|
const store = configureStore(initialState);
|
||||||
|
const app = TestUtils.renderIntoDocument(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
app: app,
|
||||||
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
|
||||||
|
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('containers', () => {
|
||||||
|
describe('App', () => {
|
||||||
|
it('should display initial count', () => {
|
||||||
|
const { p } = setup();
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 0 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display updated count after increment button click', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[0]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display updated count after decrement button click', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[1]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: -1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt change if even and if odd button clicked', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 0 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change if odd and if odd button clicked', () => {
|
||||||
|
const { buttons, p } = setup({ counter: 1 });
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 2 times/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user