mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2026-03-08 05:41:21 +03:00
Compare commits
97 Commits
remotedev-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd709cf7ed | ||
|
|
c6fd44977c | ||
|
|
648138652a | ||
|
|
12849a4982 | ||
|
|
804d6bd061 | ||
|
|
3f90241dd7 | ||
|
|
804e729e79 | ||
|
|
d61d31a690 | ||
|
|
12e561c24f | ||
|
|
d574d6c038 | ||
|
|
c752dde196 | ||
|
|
ab0a944f93 | ||
|
|
ed6eb100eb | ||
|
|
bdb08fca6c | ||
|
|
e0f127ceea | ||
|
|
55bcccb4b8 | ||
|
|
0cd341fe35 | ||
|
|
62cb6ddc25 | ||
|
|
ed6d65bd30 | ||
|
|
8f5cf1b5bc | ||
|
|
21d5699045 | ||
|
|
d609bfb14d | ||
|
|
b350e7bae9 | ||
|
|
35aa092d45 | ||
|
|
102c21d43e | ||
|
|
b8219c2afc | ||
|
|
1e701e45cb | ||
|
|
78ed3b1efe | ||
|
|
fbdbb56d19 | ||
|
|
c7e77652c2 | ||
|
|
558e2ecfd3 | ||
|
|
d0e04165ab | ||
|
|
aebfca4f93 | ||
|
|
56de415db0 | ||
|
|
36f1f36471 | ||
|
|
1039d6c827 | ||
|
|
ebaf7cf102 | ||
|
|
a4343c3116 | ||
|
|
75cafed485 | ||
|
|
a663401e42 | ||
|
|
60a027e016 | ||
|
|
85e0eb9a67 | ||
|
|
8e6014f9d3 | ||
|
|
70c98a0c05 | ||
|
|
ebaf3e0516 | ||
|
|
10f112c0ee | ||
|
|
ca3782c545 | ||
|
|
9d4924ef1d | ||
|
|
16087352e1 | ||
|
|
f80b555457 | ||
|
|
c7a9b746d5 | ||
|
|
f0330162e6 | ||
|
|
531aa338e7 | ||
|
|
8308db6832 | ||
|
|
4e44c2ca50 | ||
|
|
4e81dc99c4 | ||
|
|
e9e6b33a6d | ||
|
|
20883e5bdf | ||
|
|
0e9528ea37 | ||
|
|
e327727f91 | ||
|
|
4a616983aa | ||
|
|
13e65e0f73 | ||
|
|
c3e35815d8 | ||
|
|
b1e2d4bd58 | ||
|
|
68f82b6994 | ||
|
|
30308f54ab | ||
|
|
5b28b69261 | ||
|
|
7a48268a74 | ||
|
|
fa20f09fc1 | ||
|
|
1e0d09251b | ||
|
|
2b7e1b890a | ||
|
|
119864cd95 | ||
|
|
dfa6c937c3 | ||
|
|
8b554db5bf | ||
|
|
842419b802 | ||
|
|
1e3fb5e27a | ||
|
|
da5e9f0c1a | ||
|
|
54c40135e5 | ||
|
|
61632768a7 | ||
|
|
585d6b9220 | ||
|
|
6468c48b75 | ||
|
|
41f3c1a7b6 | ||
|
|
c82e511929 | ||
|
|
832f4dc195 | ||
|
|
4b35476610 | ||
|
|
7873dd23ae | ||
|
|
9444ca7e87 | ||
|
|
b2f01026cb | ||
|
|
04c234dd49 | ||
|
|
c394803622 | ||
|
|
fd56ac7d92 | ||
|
|
65dce0864e | ||
|
|
e02cfd327c | ||
|
|
9ddcef672f | ||
|
|
3f52f16473 | ||
|
|
9f12777827 | ||
|
|
5dc8611c38 |
4
.github/workflows/CI.yml
vendored
4
.github/workflows/CI.yml
vendored
|
|
@ -11,9 +11,9 @@ jobs:
|
||||||
runs-on: 'ubuntu-22.04'
|
runs-on: 'ubuntu-22.04'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version: 'lts/*'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
|
||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
||||||
runs-on: 'ubuntu-22.04'
|
runs-on: 'ubuntu-22.04'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
@ -21,7 +21,7 @@ jobs:
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version: 'lts/*'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
@ -40,19 +40,19 @@ jobs:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Archive Chrome Extension
|
- name: Archive Chrome Extension
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: chrome
|
name: chrome
|
||||||
path: extension/chrome/dist
|
path: extension/chrome/dist
|
||||||
|
|
||||||
- name: Archive Edge Extension
|
- name: Archive Edge Extension
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: edge
|
name: edge
|
||||||
path: extension/edge/dist
|
path: extension/edge/dist
|
||||||
|
|
||||||
- name: Archive Firefox Extension
|
- name: Archive Firefox Extension
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: firefox
|
name: firefox
|
||||||
path: extension/firefox/dist
|
path: extension/firefox/dist
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default [eslint.configs.recommended, eslintConfigPrettier];
|
export default defineConfig([eslint.configs.recommended, eslintConfigPrettier]);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import react from 'eslint-plugin-react';
|
import react from 'eslint-plugin-react';
|
||||||
import { fixupPluginRules } from '@eslint/compat';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
|
|
||||||
import jest from 'eslint-plugin-jest';
|
import jest from 'eslint-plugin-jest';
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default [
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
...eslint.configs.recommended,
|
...eslint.configs.recommended,
|
||||||
|
|
@ -24,9 +24,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
plugins: {
|
...reactHooks.configs.flat.recommended,
|
||||||
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
|
|
@ -40,4 +38,4 @@ export default [
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
...eslintConfigPrettier,
|
...eslintConfigPrettier,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default (tsconfigRootDir, files = ['**/*.ts'], project = true) => [
|
export default (tsconfigRootDir, files = ['**/*.ts'], project = true) =>
|
||||||
|
defineConfig([
|
||||||
{
|
{
|
||||||
files,
|
files,
|
||||||
...eslint.configs.recommended,
|
...eslint.configs.recommended,
|
||||||
|
|
@ -52,4 +54,4 @@ export default (tsconfigRootDir, files = ['**/*.ts'], project = true) => [
|
||||||
'@typescript-eslint/prefer-function-type': 'off',
|
'@typescript-eslint/prefer-function-type': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
import jest from 'eslint-plugin-jest';
|
import jest from 'eslint-plugin-jest';
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default (tsconfigRootDir) => [
|
export default (tsconfigRootDir) =>
|
||||||
|
defineConfig([
|
||||||
{
|
{
|
||||||
files: ['test/**/*.ts'],
|
files: ['test/**/*.ts'],
|
||||||
...eslint.configs.recommended,
|
...eslint.configs.recommended,
|
||||||
|
|
@ -61,4 +63,4 @@ export default (tsconfigRootDir) => [
|
||||||
'@typescript-eslint/prefer-function-type': 'off',
|
'@typescript-eslint/prefer-function-type': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
import react from 'eslint-plugin-react';
|
import react from 'eslint-plugin-react';
|
||||||
import { fixupPluginRules } from '@eslint/compat';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
|
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
tsconfigRootDir,
|
tsconfigRootDir,
|
||||||
files = ['**/*.ts', '**/*.tsx'],
|
files = ['**/*.ts', '**/*.tsx'],
|
||||||
project = true,
|
project = true,
|
||||||
) => [
|
) =>
|
||||||
|
defineConfig([
|
||||||
{
|
{
|
||||||
files,
|
files,
|
||||||
...eslint.configs.recommended,
|
...eslint.configs.recommended,
|
||||||
|
|
@ -45,9 +46,7 @@ export default (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files,
|
files,
|
||||||
plugins: {
|
...reactHooks.configs.flat.recommended,
|
||||||
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files,
|
files,
|
||||||
|
|
@ -86,4 +85,4 @@ export default (
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
import react from 'eslint-plugin-react';
|
import react from 'eslint-plugin-react';
|
||||||
import { fixupPluginRules } from '@eslint/compat';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
|
|
||||||
import jest from 'eslint-plugin-jest';
|
import jest from 'eslint-plugin-jest';
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default (tsconfigRootDir) => [
|
export default (tsconfigRootDir) =>
|
||||||
|
defineConfig([
|
||||||
{
|
{
|
||||||
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
||||||
...eslint.configs.recommended,
|
...eslint.configs.recommended,
|
||||||
|
|
@ -42,9 +43,7 @@ export default (tsconfigRootDir) => [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
||||||
plugins: {
|
...reactHooks.configs.flat.recommended,
|
||||||
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
||||||
|
|
@ -82,4 +81,4 @@ export default (tsconfigRootDir) => [
|
||||||
'@typescript-eslint/prefer-function-type': 'off',
|
'@typescript-eslint/prefer-function-type': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,32 @@
|
||||||
# remotedev-redux-devtools-extension
|
# remotedev-redux-devtools-extension
|
||||||
|
|
||||||
|
## 3.2.12
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [3f90241]
|
||||||
|
- Updated dependencies [d61d31a]
|
||||||
|
- Updated dependencies [804e729]
|
||||||
|
- Updated dependencies [12849a4]
|
||||||
|
- Updated dependencies [804d6bd]
|
||||||
|
- Updated dependencies [6481386]
|
||||||
|
- @redux-devtools/instrument@3.0.0
|
||||||
|
- @redux-devtools/ui@3.0.0
|
||||||
|
- @redux-devtools/slider-monitor@7.0.0
|
||||||
|
- @redux-devtools/core@5.0.0
|
||||||
|
- @redux-devtools/app@8.0.0
|
||||||
|
- @redux-devtools/serialize@1.0.0
|
||||||
|
- @redux-devtools/utils@4.0.0
|
||||||
|
|
||||||
|
## 3.2.11
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [6163276]
|
||||||
|
- @redux-devtools/app@7.0.0
|
||||||
|
- @redux-devtools/slider-monitor@6.0.0
|
||||||
|
- @redux-devtools/ui@2.0.0
|
||||||
|
|
||||||
## 3.2.10
|
## 3.2.10
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ const composeEnhancers =
|
||||||
compose;
|
compose;
|
||||||
```
|
```
|
||||||
|
|
||||||
> For TypeScript use [`redux-devtools-extension` npm package](#13-use-redux-devtoolsextension-package-from-npm), which contains all the definitions, or just use `(window as any)` (see [Recipes](/docs/Recipes.md#using-in-a-typescript-project) for an example).
|
> For TypeScript use [`redux-devtools-extension` npm package](#13-use-redux-devtoolsextension-package-from-npm), which contains all the definitions, or just use `(window as any)` (see [Recipes](docs/Recipes.md#using-in-a-typescript-project) for an example).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
|
@ -228,7 +228,7 @@ See [integrations](docs/Integrations.md) and [the blog post](https://medium.com/
|
||||||
- [Methods (advanced API)](docs/API/Methods.md)
|
- [Methods (advanced API)](docs/API/Methods.md)
|
||||||
- [FAQ](docs/FAQ.md)
|
- [FAQ](docs/FAQ.md)
|
||||||
- Features
|
- Features
|
||||||
- [Trace actions calls](/docs/Features/Trace.md)
|
- [Trace actions calls](docs/Features/Trace.md)
|
||||||
- [Troubleshooting](docs/Troubleshooting.md)
|
- [Troubleshooting](docs/Troubleshooting.md)
|
||||||
- [Articles](docs/Articles.md)
|
- [Articles](docs/Articles.md)
|
||||||
- [Videos](docs/Videos.md)
|
- [Videos](docs/Videos.md)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ _number_ - maximum stack trace frames to be stored (in case `trace` option was p
|
||||||
_boolean_ or _object_ which contains:
|
_boolean_ or _object_ which contains:
|
||||||
|
|
||||||
- **options** `object or boolean`:
|
- **options** `object or boolean`:
|
||||||
|
|
||||||
- `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
|
- `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
|
||||||
- `false` - will handle also circular references.
|
- `false` - will handle also circular references.
|
||||||
- `true` - will handle also date, regex, undefined, primitives, error objects, symbols, maps, sets and functions.
|
- `true` - will handle also date, regex, undefined, primitives, error objects, symbols, maps, sets and functions.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export default [
|
||||||
'edge',
|
'edge',
|
||||||
'examples',
|
'examples',
|
||||||
'firefox',
|
'firefox',
|
||||||
|
'jest.config.ts',
|
||||||
'test/electron/fixture/dist',
|
'test/electron/fixture/dist',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
/**
|
|
||||||
* 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["es2015", "stage-0", "react"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
|
|
||||||
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
|
|
||||||
|
|
||||||
let t;
|
|
||||||
|
|
||||||
export function increment() {
|
|
||||||
return {
|
|
||||||
type: INCREMENT_COUNTER,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decrement() {
|
|
||||||
return {
|
|
||||||
type: DECREMENT_COUNTER,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function autoIncrement(delay = 10) {
|
|
||||||
return (dispatch) => {
|
|
||||||
if (t) {
|
|
||||||
clearInterval(t);
|
|
||||||
t = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
t = setInterval(() => {
|
|
||||||
dispatch(increment());
|
|
||||||
}, delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function incrementAsync(delay = 1000) {
|
|
||||||
return (dispatch) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
dispatch(increment());
|
|
||||||
}, delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
class Counter extends Component {
|
|
||||||
render() {
|
|
||||||
const { increment, autoIncrement, incrementAsync, decrement, counter } =
|
|
||||||
this.props;
|
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
Clicked: {counter} times <button onClick={increment}>+</button>{' '}
|
|
||||||
<button onClick={decrement}>-</button>{' '}
|
|
||||||
<button onClick={incrementAsync}>Increment async</button>{' '}
|
|
||||||
<button onClick={autoIncrement}>Auto increment</button>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Counter.propTypes = {
|
|
||||||
increment: PropTypes.func.isRequired,
|
|
||||||
autoIncrement: PropTypes.func.isRequired,
|
|
||||||
incrementAsync: PropTypes.func.isRequired,
|
|
||||||
decrement: PropTypes.func.isRequired,
|
|
||||||
counter: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Counter;
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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);
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Redux counter example</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script src="/static/bundle.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
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'),
|
|
||||||
);
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"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": {
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"react": "^16.7.0",
|
|
||||||
"react-dom": "^16.7.0",
|
|
||||||
"react-redux": "^6.0.0",
|
|
||||||
"redux": "^4.0.1",
|
|
||||||
"redux-devtools-extension": "^2.13.7",
|
|
||||||
"redux-thunk": "^2.3.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-cli": "^6.3.17",
|
|
||||||
"babel-core": "^6.3.17",
|
|
||||||
"babel-loader": "^7.0.0",
|
|
||||||
"babel-preset-es2015": "^6.0.0",
|
|
||||||
"babel-preset-react": "6.3.13",
|
|
||||||
"babel-preset-stage-0": "^6.3.13",
|
|
||||||
"express": "^4.13.3",
|
|
||||||
"redux-immutable-state-invariant": "^2.1.0",
|
|
||||||
"webpack": "^4.0.0",
|
|
||||||
"webpack-dev-server": "^3.0.0",
|
|
||||||
"webpack-hot-middleware": "^2.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { combineReducers } from 'redux';
|
|
||||||
import counter from './counter';
|
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
|
||||||
counter,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default rootReducer;
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
|
||||||
import thunk from 'redux-thunk';
|
|
||||||
import invariant from 'redux-immutable-state-invariant';
|
|
||||||
import reducer from '../reducers';
|
|
||||||
import * as actionCreators from '../actions/counter';
|
|
||||||
|
|
||||||
export default function configureStore(preloadedState) {
|
|
||||||
const composeEnhancers = composeWithDevTools({
|
|
||||||
actionCreators,
|
|
||||||
trace: true,
|
|
||||||
traceLimit: 25,
|
|
||||||
});
|
|
||||||
const store = createStore(
|
|
||||||
reducer,
|
|
||||||
preloadedState,
|
|
||||||
composeEnhancers(applyMiddleware(invariant(), thunk)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (module.hot) {
|
|
||||||
// Enable Webpack hot module replacement for reducers
|
|
||||||
module.hot.accept('../reducers', () => {
|
|
||||||
store.replaceReducer(require('../reducers').default);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
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/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { jsdom } from 'jsdom';
|
|
||||||
|
|
||||||
global.document = jsdom('<!doctype html><html><body></body></html>');
|
|
||||||
global.window = document.defaultView;
|
|
||||||
global.navigator = global.window.navigator;
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
var path = require('path');
|
|
||||||
var webpack = require('webpack');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: 'development',
|
|
||||||
devtool: 'source-map',
|
|
||||||
entry: ['webpack-hot-middleware/client', './index'],
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'dist'),
|
|
||||||
filename: 'bundle.js',
|
|
||||||
publicPath: '/static/',
|
|
||||||
},
|
|
||||||
plugins: [new webpack.HotModuleReplacementPlugin()],
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
loaders: ['babel-loader'],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["es2015", "stage-0", "react"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
const withDevTools =
|
|
||||||
// process.env.NODE_ENV === 'development' &&
|
|
||||||
typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__;
|
|
||||||
|
|
||||||
class Counter extends Component {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.state = { counter: 0 };
|
|
||||||
|
|
||||||
this.increment = this.increment.bind(this);
|
|
||||||
this.decrement = this.decrement.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
if (withDevTools) {
|
|
||||||
this.devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect();
|
|
||||||
this.unsubscribe = this.devTools.subscribe((message) => {
|
|
||||||
// Implement monitors actions.
|
|
||||||
// For example time traveling:
|
|
||||||
if (
|
|
||||||
message.type === 'DISPATCH' &&
|
|
||||||
message.payload.type === 'JUMP_TO_STATE'
|
|
||||||
) {
|
|
||||||
this.setState(message.state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (withDevTools) {
|
|
||||||
this.unsubscribe(); // Use if you have other subscribers from other components.
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION__.disconnect(); // If there aren't other subscribers.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
const state = { counter: this.state.counter + 1 };
|
|
||||||
if (withDevTools) this.devTools.send('increment', state);
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
decrement() {
|
|
||||||
const state = { counter: this.state.counter - 1 };
|
|
||||||
if (withDevTools) this.devTools.send('decrement', state);
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { counter } = this.state;
|
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
Clicked: {counter} times <button onClick={this.increment}>+</button>{' '}
|
|
||||||
<button onClick={this.decrement}>-</button>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Counter;
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>React counter example</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script src="/static/bundle.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
import Counter from './components/Counter';
|
|
||||||
|
|
||||||
render(<Counter />, document.getElementById('root'));
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"name": "react-counter-example",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "React counter example",
|
|
||||||
"scripts": {
|
|
||||||
"start": "webpack-dev-server --progress"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/zalmoxisus/redux-devtools-extension.git"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/zalmoxisus/redux-devtools-extension/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/zalmoxisus/redux-devtools-extension",
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^16.0.0",
|
|
||||||
"react-dom": "^16.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-cli": "^6.3.17",
|
|
||||||
"babel-core": "^6.3.17",
|
|
||||||
"babel-loader": "^7.0.0",
|
|
||||||
"babel-preset-es2015": "^6.0.0",
|
|
||||||
"babel-preset-react": "6.3.13",
|
|
||||||
"babel-preset-stage-0": "^6.3.13",
|
|
||||||
"webpack": "^4.0.0",
|
|
||||||
"webpack-cli": "^3.2.0",
|
|
||||||
"webpack-dev-server": "^3.0.0",
|
|
||||||
"webpack-hot-middleware": "^2.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
var path = require('path');
|
|
||||||
var webpack = require('webpack');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: 'development',
|
|
||||||
devtool: 'source-map',
|
|
||||||
entry: ['./index'],
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'dist'),
|
|
||||||
filename: 'bundle.js',
|
|
||||||
publicPath: '/static/',
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
loaders: ['babel-loader'],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
port: 4004,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["es2015", "stage-0", "react"],
|
|
||||||
"plugins": ["add-module-exports", "transform-decorators-legacy"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
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';
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const SHOW_ALL = 'show_all';
|
|
||||||
export const SHOW_COMPLETED = 'show_completed';
|
|
||||||
export const SHOW_ACTIVE = 'show_active';
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
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);
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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'),
|
|
||||||
);
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
{
|
|
||||||
"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-add-module-exports": "^0.1.1",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { combineReducers } from 'redux';
|
|
||||||
import { routerStateReducer } from 'redux-router';
|
|
||||||
import todos from './todos';
|
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
|
||||||
todos,
|
|
||||||
router: routerStateReducer,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default rootReducer;
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { createStore, compose } from 'redux';
|
|
||||||
import {
|
|
||||||
reduxReactRouter,
|
|
||||||
routerStateReducer,
|
|
||||||
ReduxRouter,
|
|
||||||
} from 'redux-router';
|
|
||||||
//import createHistory from 'history/lib/createBrowserHistory';
|
|
||||||
import createHistory from 'history/lib/createHashHistory';
|
|
||||||
import rootReducer from '../reducers';
|
|
||||||
|
|
||||||
export default function configureStore(initialState) {
|
|
||||||
let finalCreateStore = compose(
|
|
||||||
reduxReactRouter({ createHistory }),
|
|
||||||
global.devToolsExtension ? global.devToolsExtension() : (f) => f,
|
|
||||||
)(createStore);
|
|
||||||
|
|
||||||
const store = finalCreateStore(rootReducer, initialState);
|
|
||||||
|
|
||||||
if (module.hot) {
|
|
||||||
// Enable Webpack hot module replacement for reducers
|
|
||||||
module.hot.accept('../reducers', () => {
|
|
||||||
const nextReducer = require('../reducers');
|
|
||||||
store.replaceReducer(nextReducer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
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]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
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('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,325 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { jsdom } from 'jsdom';
|
|
||||||
|
|
||||||
global.document = jsdom('<!doctype html><html><body></body></html>');
|
|
||||||
global.window = document.defaultView;
|
|
||||||
global.navigator = global.window.navigator;
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["es2015", "stage-0", "react"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Redux Saga Counter example</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/static/bundle.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"name": "redux-counter-example",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "Redux counter example",
|
|
||||||
"scripts": {
|
|
||||||
"start": "webpack-dev-server --progress"
|
|
||||||
},
|
|
||||||
"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": {
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"react": "^16.0.0",
|
|
||||||
"react-dom": "^16.0.0",
|
|
||||||
"react-redux": "^6.0.0",
|
|
||||||
"redux": "^4.0.0",
|
|
||||||
"redux-saga": "^0.10.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-cli": "^6.3.17",
|
|
||||||
"babel-core": "^6.3.17",
|
|
||||||
"babel-loader": "^7.0.0",
|
|
||||||
"babel-preset-es2015": "^6.0.0",
|
|
||||||
"babel-preset-react": "6.3.13",
|
|
||||||
"babel-preset-stage-0": "^6.3.13",
|
|
||||||
"webpack": "^4.0.0",
|
|
||||||
"webpack-cli": "^3.2.0",
|
|
||||||
"webpack-dev-server": "^3.1.0",
|
|
||||||
"webpack-hot-middleware": "^2.24.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const Counter = ({
|
|
||||||
value,
|
|
||||||
onIncrement,
|
|
||||||
onIncrementAsync,
|
|
||||||
onDecrement,
|
|
||||||
onIncrementIfOdd,
|
|
||||||
}) => (
|
|
||||||
<p>
|
|
||||||
Clicked: {value} times <button onClick={onIncrement}>+</button>{' '}
|
|
||||||
<button onClick={onDecrement}>-</button>{' '}
|
|
||||||
<button onClick={onIncrementIfOdd}>Increment if odd</button>{' '}
|
|
||||||
<button onClick={onIncrementAsync}>Increment async</button>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
Counter.propTypes = {
|
|
||||||
value: PropTypes.number.isRequired,
|
|
||||||
onIncrement: PropTypes.func.isRequired,
|
|
||||||
onDecrement: PropTypes.func.isRequired,
|
|
||||||
onIncrementAsync: PropTypes.func.isRequired,
|
|
||||||
onIncrementIfOdd: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Counter;
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import 'babel-polyfill';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
|
||||||
import createSagaMiddleware from 'redux-saga';
|
|
||||||
// import sagaMonitor from './sagaMonitor'
|
|
||||||
|
|
||||||
import Counter from './components/Counter';
|
|
||||||
import reducer from './reducers';
|
|
||||||
import rootSaga from './sagas';
|
|
||||||
|
|
||||||
const sagaMiddleware = createSagaMiddleware(/* {sagaMonitor} */);
|
|
||||||
const composeEnhancers =
|
|
||||||
(window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
|
||||||
trace: true,
|
|
||||||
traceLimit: 25,
|
|
||||||
})) ||
|
|
||||||
compose;
|
|
||||||
const store = createStore(
|
|
||||||
reducer,
|
|
||||||
composeEnhancers(applyMiddleware(sagaMiddleware)),
|
|
||||||
);
|
|
||||||
sagaMiddleware.run(rootSaga);
|
|
||||||
|
|
||||||
const action = (type) => store.dispatch({ type });
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
ReactDOM.render(
|
|
||||||
<Counter
|
|
||||||
value={store.getState()}
|
|
||||||
onIncrement={() => action('INCREMENT')}
|
|
||||||
onDecrement={() => action('DECREMENT')}
|
|
||||||
onIncrementIfOdd={() => action('INCREMENT_IF_ODD')}
|
|
||||||
onIncrementAsync={() => action('INCREMENT_ASYNC')}
|
|
||||||
/>,
|
|
||||||
document.getElementById('root'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render();
|
|
||||||
store.subscribe(render);
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export default function counter(state = 0, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'INCREMENT':
|
|
||||||
return state + 1;
|
|
||||||
case 'INCREMENT_IF_ODD':
|
|
||||||
return state % 2 !== 0 ? state + 1 : state;
|
|
||||||
case 'DECREMENT':
|
|
||||||
return state - 1;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
/* eslint-disable no-constant-condition */
|
|
||||||
|
|
||||||
import { takeEvery } from 'redux-saga';
|
|
||||||
import { put, call } from 'redux-saga/effects';
|
|
||||||
import { delay } from 'redux-saga';
|
|
||||||
|
|
||||||
export function* incrementAsync() {
|
|
||||||
yield call(delay, 1000);
|
|
||||||
yield put({ type: 'INCREMENT' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function* rootSaga() {
|
|
||||||
yield* takeEvery('INCREMENT_ASYNC', incrementAsync);
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
var path = require('path');
|
|
||||||
var webpack = require('webpack');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: 'development',
|
|
||||||
devtool: 'source-map',
|
|
||||||
entry: [path.join(__dirname, 'src', 'main')],
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'dist'),
|
|
||||||
filename: 'bundle.js',
|
|
||||||
publicPath: '/static/',
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
loaders: ['babel-loader'],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
port: 4003,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["es2015", "stage-0", "react"]
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from './todos';
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
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;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
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;
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
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;
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
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;
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
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;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
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';
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const SHOW_ALL = 'show_all';
|
|
||||||
export const SHOW_COMPLETED = 'show_completed';
|
|
||||||
export const SHOW_ACTIVE = 'show_active';
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
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);
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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'),
|
|
||||||
);
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"react": "^16.0.0",
|
|
||||||
"react-dom": "^16.0.0",
|
|
||||||
"react-redux": "^6.0.0",
|
|
||||||
"redux": "^4.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-cli": "^6.3.17",
|
|
||||||
"babel-core": "^6.3.17",
|
|
||||||
"babel-loader": "^7.0.0",
|
|
||||||
"babel-preset-es2015": "^6.0.0",
|
|
||||||
"babel-preset-react": "6.3.13",
|
|
||||||
"babel-preset-stage-0": "^6.3.13",
|
|
||||||
"express": "^4.13.3",
|
|
||||||
"raw-loader": "^1.0.0",
|
|
||||||
"style-loader": "^0.23.0",
|
|
||||||
"todomvc-app-css": "^2.0.1",
|
|
||||||
"webpack": "^4.0.0",
|
|
||||||
"webpack-dev-server": "^3.0.0",
|
|
||||||
"webpack-hot-middleware": "^2.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { combineReducers } from 'redux';
|
|
||||||
import todos from './todos';
|
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
|
||||||
todos,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default rootReducer;
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import {
|
|
||||||
ADD_TODO,
|
|
||||||
DELETE_TODO,
|
|
||||||
EDIT_TODO,
|
|
||||||
COMPLETE_TODO,
|
|
||||||
COMPLETE_ALL,
|
|
||||||
CLEAR_COMPLETED,
|
|
||||||
} from '../constants/ActionTypes';
|
|
||||||
|
|
||||||
const initialState = [
|
|
||||||
{
|
|
||||||
text: 'Use Redux',
|
|
||||||
completed: false,
|
|
||||||
modified: new Date(),
|
|
||||||
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,
|
|
||||||
modified: new Date(),
|
|
||||||
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, modified: new Date() })
|
|
||||||
: todo,
|
|
||||||
);
|
|
||||||
|
|
||||||
case COMPLETE_TODO:
|
|
||||||
return state.map((todo) =>
|
|
||||||
todo.id === action.id
|
|
||||||
? Object.assign({}, todo, {
|
|
||||||
completed: !todo.completed,
|
|
||||||
modified: new Date(),
|
|
||||||
})
|
|
||||||
: todo,
|
|
||||||
);
|
|
||||||
|
|
||||||
case COMPLETE_ALL:
|
|
||||||
const areAllMarked = state.every((todo) => todo.completed);
|
|
||||||
return state.map((todo) =>
|
|
||||||
Object.assign({}, todo, {
|
|
||||||
completed: !areAllMarked,
|
|
||||||
modified: new Date(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
case CLEAR_COMPLETED:
|
|
||||||
return state.filter((todo) => todo.completed === false);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { createStore } from 'redux';
|
|
||||||
import rootReducer from '../reducers';
|
|
||||||
import * as actionCreators from '../actions';
|
|
||||||
|
|
||||||
export default function configureStore(preloadedState) {
|
|
||||||
const enhancer =
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION__ &&
|
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION__({
|
|
||||||
actionCreators,
|
|
||||||
serialize: true,
|
|
||||||
trace: true,
|
|
||||||
});
|
|
||||||
if (!enhancer) {
|
|
||||||
console.warn(
|
|
||||||
'Install Redux DevTools Extension to inspect the app state: ' +
|
|
||||||
'https://github.com/zalmoxisus/redux-devtools-extension#installation',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = createStore(rootReducer, preloadedState, enhancer);
|
|
||||||
|
|
||||||
if (module.hot) {
|
|
||||||
// Enable Webpack hot module replacement for reducers
|
|
||||||
module.hot.accept('../reducers', () => {
|
|
||||||
store.replaceReducer(require('../reducers').default);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
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]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
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('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,325 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { jsdom } from 'jsdom';
|
|
||||||
|
|
||||||
global.document = jsdom('<!doctype html><html><body></body></html>');
|
|
||||||
global.window = document.defaultView;
|
|
||||||
global.navigator = global.window.navigator;
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
var path = require('path');
|
|
||||||
var webpack = require('webpack');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: 'development',
|
|
||||||
devtool: 'source-map',
|
|
||||||
entry: ['webpack-hot-middleware/client', './index'],
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'dist'),
|
|
||||||
filename: 'bundle.js',
|
|
||||||
publicPath: '/static/',
|
|
||||||
},
|
|
||||||
plugins: [new webpack.HotModuleReplacementPlugin()],
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
loaders: ['babel-loader'],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css?$/,
|
|
||||||
loaders: ['style-loader', 'raw-loader'],
|
|
||||||
include: __dirname,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
|
|
||||||
testPathIgnorePatterns: ['<rootDir>/examples'],
|
|
||||||
testEnvironment: 'jsdom',
|
|
||||||
moduleNameMapper: {
|
|
||||||
'\\.css$': '<rootDir>/test/__mocks__/styleMock.js',
|
|
||||||
},
|
|
||||||
transformIgnorePatterns: [
|
|
||||||
'node_modules/(?!.pnpm|@babel/code-frame|@babel/highlight|@babel/helper-validator-identifier|chalk|d3|dateformat|delaunator|internmap|jsondiffpatch|lodash-es|nanoid|robust-predicates|uuid)',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user