mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2026-03-10 14:42:41 +03:00
Compare commits
No commits in common. "main" and "remotedev-redux-devtools-extension@3.2.7" have entirely different histories.
main
...
remotedev-
6
.github/workflows/CI.yml
vendored
6
.github/workflows/CI.yml
vendored
|
|
@ -8,12 +8,12 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: 'ubuntu-22.04'
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version: 'lts/*'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
|
||||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -10,10 +10,10 @@ permissions: write-all
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: 'ubuntu-22.04'
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
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@v6
|
uses: actions/setup-node@v4
|
||||||
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@v7
|
uses: actions/upload-artifact@v4
|
||||||
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@v7
|
uses: actions/upload-artifact@v4
|
||||||
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@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: firefox
|
name: firefox
|
||||||
path: extension/firefox/dist
|
path: extension/firefox/dist
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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 defineConfig([eslint.configs.recommended, eslintConfigPrettier]);
|
export default [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 reactHooks from 'eslint-plugin-react-hooks';
|
import { fixupPluginRules } from '@eslint/compat';
|
||||||
|
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 defineConfig([
|
export default [
|
||||||
{
|
{
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
...eslint.configs.recommended,
|
...eslint.configs.recommended,
|
||||||
|
|
@ -24,7 +24,9 @@ export default defineConfig([
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
...reactHooks.configs.flat.recommended,
|
plugins: {
|
||||||
|
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
|
|
@ -38,4 +40,4 @@ export default defineConfig([
|
||||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||||
...eslintConfigPrettier,
|
...eslintConfigPrettier,
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
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,
|
||||||
|
|
@ -54,4 +52,4 @@ export default (tsconfigRootDir, files = ['**/*.ts'], project = true) =>
|
||||||
'@typescript-eslint/prefer-function-type': 'off',
|
'@typescript-eslint/prefer-function-type': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
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,
|
||||||
|
|
@ -63,4 +61,4 @@ export default (tsconfigRootDir) =>
|
||||||
'@typescript-eslint/prefer-function-type': 'off',
|
'@typescript-eslint/prefer-function-type': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
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 reactHooks from 'eslint-plugin-react-hooks';
|
import { fixupPluginRules } from '@eslint/compat';
|
||||||
|
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,
|
||||||
|
|
@ -46,7 +45,9 @@ export default (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files,
|
files,
|
||||||
...reactHooks.configs.flat.recommended,
|
plugins: {
|
||||||
|
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files,
|
files,
|
||||||
|
|
@ -85,4 +86,4 @@ export default (
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
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 reactHooks from 'eslint-plugin-react-hooks';
|
import { fixupPluginRules } from '@eslint/compat';
|
||||||
|
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,
|
||||||
|
|
@ -43,7 +42,9 @@ export default (tsconfigRootDir) =>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
||||||
...reactHooks.configs.flat.recommended,
|
plugins: {
|
||||||
|
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
files: ['test/**/*.ts', 'test/**/*.tsx'],
|
||||||
|
|
@ -81,4 +82,4 @@ export default (tsconfigRootDir) =>
|
||||||
'@typescript-eslint/prefer-function-type': 'off',
|
'@typescript-eslint/prefer-function-type': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,5 @@
|
||||||
# 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
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- @redux-devtools/app@6.2.2
|
|
||||||
|
|
||||||
## 3.2.9
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [91f21b2]
|
|
||||||
- @redux-devtools/core@4.1.1
|
|
||||||
- @redux-devtools/slider-monitor@5.1.1
|
|
||||||
- @redux-devtools/utils@3.1.1
|
|
||||||
- @redux-devtools/app@6.2.1
|
|
||||||
|
|
||||||
## 3.2.8
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [6830118]
|
|
||||||
- react-json-tree@0.20.0
|
|
||||||
- @redux-devtools/app@6.2.0
|
|
||||||
- @redux-devtools/slider-monitor@6.0.0
|
|
||||||
- @redux-devtools/ui@1.4.0
|
|
||||||
- @redux-devtools/core@4.1.0
|
|
||||||
- @redux-devtools/utils@4.0.0
|
|
||||||
|
|
||||||
## 3.2.7
|
## 3.2.7
|
||||||
|
|
||||||
### 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)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "3.2.10",
|
"version": "3.2.7",
|
||||||
"name": "Redux DevTools",
|
"name": "Redux DevTools",
|
||||||
"description": "Redux DevTools for debugging application's state changes.",
|
"description": "Redux DevTools for debugging application's state changes.",
|
||||||
"homepage_url": "https://github.com/reduxjs/redux-devtools",
|
"homepage_url": "https://github.com/reduxjs/redux-devtools",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ _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.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "3.2.10",
|
"version": "3.2.7",
|
||||||
"name": "Redux DevTools",
|
"name": "Redux DevTools",
|
||||||
"description": "Redux DevTools for debugging application's state changes.",
|
"description": "Redux DevTools for debugging application's state changes.",
|
||||||
"homepage_url": "https://github.com/reduxjs/redux-devtools",
|
"homepage_url": "https://github.com/reduxjs/redux-devtools",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ export default [
|
||||||
'edge',
|
'edge',
|
||||||
'examples',
|
'examples',
|
||||||
'firefox',
|
'firefox',
|
||||||
'jest.config.ts',
|
|
||||||
'test/electron/fixture/dist',
|
'test/electron/fixture/dist',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
36
extension/examples/buildAll.js
Normal file
36
extension/examples/buildAll.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Runs an ordered set of commands within each of the build directories.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
|
var exampleDirs = fs.readdirSync(__dirname).filter((file) => {
|
||||||
|
return fs.statSync(path.join(__dirname, file)).isDirectory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ordering is important here. `npm install` must come first.
|
||||||
|
var cmdArgs = [
|
||||||
|
{ cmd: 'npm', args: ['install'] },
|
||||||
|
{ cmd: 'webpack', args: ['index.js'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of exampleDirs) {
|
||||||
|
for (const cmdArg of cmdArgs) {
|
||||||
|
// declare opts in this scope to avoid https://github.com/joyent/node/issues/9158
|
||||||
|
const opts = {
|
||||||
|
cwd: path.join(__dirname, dir),
|
||||||
|
stdio: 'inherit',
|
||||||
|
};
|
||||||
|
let result = {};
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts);
|
||||||
|
} else {
|
||||||
|
result = spawnSync(cmdArg.cmd, cmdArg.args, opts);
|
||||||
|
}
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error('Building examples exited with non-zero');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
extension/examples/counter/.babelrc
Normal file
3
extension/examples/counter/.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"]
|
||||||
|
}
|
||||||
37
extension/examples/counter/actions/counter.js
Normal file
37
extension/examples/counter/actions/counter.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
27
extension/examples/counter/components/Counter.js
Normal file
27
extension/examples/counter/components/Counter.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
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;
|
||||||
16
extension/examples/counter/containers/App.js
Normal file
16
extension/examples/counter/containers/App.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Counter from '../components/Counter';
|
||||||
|
import * as CounterActions from '../actions/counter';
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
counter: state.counter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(CounterActions, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
|
||||||
10
extension/examples/counter/index.html
Normal file
10
extension/examples/counter/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redux counter example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
extension/examples/counter/index.js
Normal file
14
extension/examples/counter/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from './containers/App';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
||||||
41
extension/examples/counter/package.json
Normal file
41
extension/examples/counter/package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
extension/examples/counter/reducers/counter.js
Normal file
12
extension/examples/counter/reducers/counter.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';
|
||||||
|
|
||||||
|
export default function counter(state = 0, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case INCREMENT_COUNTER:
|
||||||
|
return state + 1;
|
||||||
|
case DECREMENT_COUNTER:
|
||||||
|
return state - 1;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
extension/examples/counter/reducers/index.js
Normal file
8
extension/examples/counter/reducers/index.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import counter from './counter';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
counter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
||||||
32
extension/examples/counter/server.js
Normal file
32
extension/examples/counter/server.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
var webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
|
||||||
|
var app = new require('express')();
|
||||||
|
var port = 4001;
|
||||||
|
|
||||||
|
var compiler = webpack(config);
|
||||||
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.get('/', function (req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
28
extension/examples/counter/store/configureStore.js
Normal file
28
extension/examples/counter/store/configureStore.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
73
extension/examples/counter/test/actions/counter.spec.js
Normal file
73
extension/examples/counter/test/actions/counter.spec.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import { applyMiddleware } from 'redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import * as actions from '../../actions/counter';
|
||||||
|
|
||||||
|
const middlewares = [thunk];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates a mock of Redux store with middleware.
|
||||||
|
*/
|
||||||
|
function mockStore(getState, expectedActions, onLastAction) {
|
||||||
|
if (!Array.isArray(expectedActions)) {
|
||||||
|
throw new Error('expectedActions should be an array of expected actions.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof onLastAction !== 'undefined' &&
|
||||||
|
typeof onLastAction !== 'function'
|
||||||
|
) {
|
||||||
|
throw new Error('onLastAction should either be undefined or function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockStoreWithoutMiddleware() {
|
||||||
|
return {
|
||||||
|
getState() {
|
||||||
|
return typeof getState === 'function' ? getState() : getState;
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatch(action) {
|
||||||
|
const expectedAction = expectedActions.shift();
|
||||||
|
expect(action).toEqual(expectedAction);
|
||||||
|
if (onLastAction && !expectedActions.length) {
|
||||||
|
onLastAction();
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
|
||||||
|
mockStoreWithoutMiddleware,
|
||||||
|
);
|
||||||
|
|
||||||
|
return mockStoreWithMiddleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
it('increment should create increment action', () => {
|
||||||
|
expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrement should create decrement action', () => {
|
||||||
|
expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementIfOdd should create increment action', (done) => {
|
||||||
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
|
const store = mockStore({ counter: 1 }, expectedActions, done);
|
||||||
|
store.dispatch(actions.incrementIfOdd());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementIfOdd shouldnt create increment action if counter is even', (done) => {
|
||||||
|
const expectedActions = [];
|
||||||
|
const store = mockStore({ counter: 2 }, expectedActions);
|
||||||
|
store.dispatch(actions.incrementIfOdd());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('incrementAsync should create increment action', (done) => {
|
||||||
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
|
const store = mockStore({ counter: 0 }, expectedActions, done);
|
||||||
|
store.dispatch(actions.incrementAsync(100));
|
||||||
|
});
|
||||||
|
});
|
||||||
53
extension/examples/counter/test/components/Counter.spec.js
Normal file
53
extension/examples/counter/test/components/Counter.spec.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Counter from '../../components/Counter';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const actions = {
|
||||||
|
increment: expect.createSpy(),
|
||||||
|
incrementIfOdd: expect.createSpy(),
|
||||||
|
incrementAsync: expect.createSpy(),
|
||||||
|
decrement: expect.createSpy(),
|
||||||
|
};
|
||||||
|
const component = TestUtils.renderIntoDocument(
|
||||||
|
<Counter counter={1} {...actions} />,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
component: component,
|
||||||
|
actions: actions,
|
||||||
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
|
||||||
|
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Counter component', () => {
|
||||||
|
it('should display count', () => {
|
||||||
|
const { p } = setup();
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('first button should call increment', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[0]);
|
||||||
|
expect(actions.increment).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('second button should call decrement', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[1]);
|
||||||
|
expect(actions.decrement).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('third button should call incrementIfOdd', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(actions.incrementIfOdd).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fourth button should call incrementAsync', () => {
|
||||||
|
const { buttons, actions } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[3]);
|
||||||
|
expect(actions.incrementAsync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
53
extension/examples/counter/test/containers/App.spec.js
Normal file
53
extension/examples/counter/test/containers/App.spec.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from '../../containers/App';
|
||||||
|
import configureStore from '../../store/configureStore';
|
||||||
|
|
||||||
|
function setup(initialState) {
|
||||||
|
const store = configureStore(initialState);
|
||||||
|
const app = TestUtils.renderIntoDocument(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
app: app,
|
||||||
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
|
||||||
|
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('containers', () => {
|
||||||
|
describe('App', () => {
|
||||||
|
it('should display initial count', () => {
|
||||||
|
const { p } = setup();
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 0 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display updated count after increment button click', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[0]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display updated count after decrement button click', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[1]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: -1 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt change if even and if odd button clicked', () => {
|
||||||
|
const { buttons, p } = setup();
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 0 times/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change if odd and if odd button clicked', () => {
|
||||||
|
const { buttons, p } = setup({ counter: 1 });
|
||||||
|
TestUtils.Simulate.click(buttons[2]);
|
||||||
|
expect(p.textContent).toMatch(/^Clicked: 2 times/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
23
extension/examples/counter/test/reducers/counter.spec.js
Normal file
23
extension/examples/counter/test/reducers/counter.spec.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import counter from '../../reducers/counter';
|
||||||
|
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../actions/counter';
|
||||||
|
|
||||||
|
describe('reducers', () => {
|
||||||
|
describe('counter', () => {
|
||||||
|
it('should handle initial state', () => {
|
||||||
|
expect(counter(undefined, {})).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle INCREMENT_COUNTER', () => {
|
||||||
|
expect(counter(1, { type: INCREMENT_COUNTER })).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle DECREMENT_COUNTER', () => {
|
||||||
|
expect(counter(1, { type: DECREMENT_COUNTER })).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unknown action type', () => {
|
||||||
|
expect(counter(1, { type: 'unknown' })).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
5
extension/examples/counter/test/setup.js
Normal file
5
extension/examples/counter/test/setup.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { jsdom } from 'jsdom';
|
||||||
|
|
||||||
|
global.document = jsdom('<!doctype html><html><body></body></html>');
|
||||||
|
global.window = document.defaultView;
|
||||||
|
global.navigator = global.window.navigator;
|
||||||
23
extension/examples/counter/webpack.config.js
Normal file
23
extension/examples/counter/webpack.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
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/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
3
extension/examples/react-counter-messaging/.babelrc
Normal file
3
extension/examples/react-counter-messaging/.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"]
|
||||||
|
}
|
||||||
62
extension/examples/react-counter-messaging/components/Counter.js
vendored
Normal file
62
extension/examples/react-counter-messaging/components/Counter.js
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
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;
|
||||||
10
extension/examples/react-counter-messaging/index.html
Normal file
10
extension/examples/react-counter-messaging/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>React counter example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
extension/examples/react-counter-messaging/index.js
vendored
Normal file
5
extension/examples/react-counter-messaging/index.js
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import Counter from './components/Counter';
|
||||||
|
|
||||||
|
render(<Counter />, document.getElementById('root'));
|
||||||
33
extension/examples/react-counter-messaging/package.json
Normal file
33
extension/examples/react-counter-messaging/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
extension/examples/react-counter-messaging/webpack.config.js
Normal file
25
extension/examples/react-counter-messaging/webpack.config.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
4
extension/examples/router/.babelrc
Normal file
4
extension/examples/router/.babelrc
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"],
|
||||||
|
"plugins": ["add-module-exports", "transform-decorators-legacy"]
|
||||||
|
}
|
||||||
25
extension/examples/router/actions/todos.js
Normal file
25
extension/examples/router/actions/todos.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as types from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
export function addTodo(text) {
|
||||||
|
return { type: types.ADD_TODO, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTodo(id) {
|
||||||
|
return { type: types.DELETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editTodo(id, text) {
|
||||||
|
return { type: types.EDIT_TODO, id, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeTodo(id) {
|
||||||
|
return { type: types.COMPLETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeAll() {
|
||||||
|
return { type: types.COMPLETE_ALL };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCompleted() {
|
||||||
|
return { type: types.CLEAR_COMPLETED };
|
||||||
|
}
|
||||||
76
extension/examples/router/components/Footer.js
Normal file
76
extension/examples/router/components/Footer.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { PropTypes, Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
|
const FILTER_TITLES = {
|
||||||
|
[SHOW_ALL]: 'All',
|
||||||
|
[SHOW_ACTIVE]: 'Active',
|
||||||
|
[SHOW_COMPLETED]: 'Completed',
|
||||||
|
};
|
||||||
|
|
||||||
|
class Footer extends Component {
|
||||||
|
renderTodoCount() {
|
||||||
|
const { activeCount } = this.props;
|
||||||
|
const itemWord = activeCount === 1 ? 'item' : 'items';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="todo-count">
|
||||||
|
<strong>{activeCount || 'No'}</strong> {itemWord} left
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFilterLink(filter) {
|
||||||
|
const title = FILTER_TITLES[filter];
|
||||||
|
const { filter: selectedFilter, onShow } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={classnames({ selected: filter === selectedFilter })}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => onShow(filter)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderClearButton() {
|
||||||
|
const { completedCount, onClearCompleted } = this.props;
|
||||||
|
if (completedCount > 0) {
|
||||||
|
return (
|
||||||
|
<button className="clear-completed" onClick={onClearCompleted}>
|
||||||
|
Clear completed
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<footer className="footer">
|
||||||
|
{this.renderTodoCount()}
|
||||||
|
<ul className="filters">
|
||||||
|
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
|
||||||
|
<li key={filter}>{this.renderFilterLink(filter)}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{this.renderClearButton()}
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Footer.propTypes = {
|
||||||
|
completedCount: PropTypes.number.isRequired,
|
||||||
|
activeCount: PropTypes.number.isRequired,
|
||||||
|
filter: PropTypes.string.isRequired,
|
||||||
|
onClearCompleted: PropTypes.func.isRequired,
|
||||||
|
onShow: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
30
extension/examples/router/components/Header.js
Normal file
30
extension/examples/router/components/Header.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { PropTypes, Component } from 'react';
|
||||||
|
import TodoTextInput from './TodoTextInput';
|
||||||
|
|
||||||
|
class Header extends Component {
|
||||||
|
handleSave(text) {
|
||||||
|
if (text.length !== 0) {
|
||||||
|
this.props.addTodo(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { path } = this.props;
|
||||||
|
return (
|
||||||
|
<header className="header">
|
||||||
|
<h1 style={{ fontSize: 80 }}>{path}</h1>
|
||||||
|
<TodoTextInput
|
||||||
|
newTodo
|
||||||
|
onSave={this.handleSave.bind(this)}
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
addTodo: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
94
extension/examples/router/components/MainSection.js
Normal file
94
extension/examples/router/components/MainSection.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import TodoItem from './TodoItem';
|
||||||
|
import Footer from './Footer';
|
||||||
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
|
const TODO_FILTERS = {
|
||||||
|
[SHOW_ALL]: () => true,
|
||||||
|
[SHOW_ACTIVE]: (todo) => !todo.completed,
|
||||||
|
[SHOW_COMPLETED]: (todo) => todo.completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
class MainSection extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = { filter: SHOW_ALL };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClearCompleted() {
|
||||||
|
const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
|
||||||
|
if (atLeastOneCompleted) {
|
||||||
|
this.props.actions.clearCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShow(filter) {
|
||||||
|
this.setState({ filter });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToggleAll(completedCount) {
|
||||||
|
const { todos, actions } = this.props;
|
||||||
|
if (todos.length > 0) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className="toggle-all"
|
||||||
|
type="checkbox"
|
||||||
|
checked={completedCount === todos.length}
|
||||||
|
onChange={actions.completeAll}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooter(completedCount) {
|
||||||
|
const { todos } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
const activeCount = todos.length - completedCount;
|
||||||
|
|
||||||
|
if (todos.length) {
|
||||||
|
return (
|
||||||
|
<Footer
|
||||||
|
completedCount={completedCount}
|
||||||
|
activeCount={activeCount}
|
||||||
|
filter={filter}
|
||||||
|
onClearCompleted={this.handleClearCompleted.bind(this)}
|
||||||
|
onShow={this.handleShow.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { todos, actions } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
|
||||||
|
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
|
||||||
|
const completedCount = todos.reduce(
|
||||||
|
(count, todo) => (todo.completed ? count + 1 : count),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="main">
|
||||||
|
{this.renderToggleAll(completedCount)}
|
||||||
|
<ul className="todo-list">
|
||||||
|
{filteredTodos.map((todo) => (
|
||||||
|
<TodoItem key={todo.id} todo={todo} {...actions} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{this.renderFooter(completedCount)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MainSection.propTypes = {
|
||||||
|
todos: PropTypes.array.isRequired,
|
||||||
|
actions: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainSection;
|
||||||
75
extension/examples/router/components/TodoItem.js
Normal file
75
extension/examples/router/components/TodoItem.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import TodoTextInput from './TodoTextInput';
|
||||||
|
|
||||||
|
class TodoItem extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
|
editing: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoubleClick() {
|
||||||
|
this.setState({ editing: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSave(id, text) {
|
||||||
|
if (text.length === 0) {
|
||||||
|
this.props.deleteTodo(id);
|
||||||
|
} else {
|
||||||
|
this.props.editTodo(id, text);
|
||||||
|
}
|
||||||
|
this.setState({ editing: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { todo, completeTodo, deleteTodo } = this.props;
|
||||||
|
|
||||||
|
let element;
|
||||||
|
if (this.state.editing) {
|
||||||
|
element = (
|
||||||
|
<TodoTextInput
|
||||||
|
text={todo.text}
|
||||||
|
editing={this.state.editing}
|
||||||
|
onSave={(text) => this.handleSave(todo.id, text)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
element = (
|
||||||
|
<div className="view">
|
||||||
|
<input
|
||||||
|
className="toggle"
|
||||||
|
type="checkbox"
|
||||||
|
checked={todo.completed}
|
||||||
|
onChange={() => completeTodo(todo.id)}
|
||||||
|
/>
|
||||||
|
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
|
||||||
|
{todo.text}
|
||||||
|
</label>
|
||||||
|
<button className="destroy" onClick={() => deleteTodo(todo.id)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={classnames({
|
||||||
|
completed: todo.completed,
|
||||||
|
editing: this.state.editing,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{element}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoItem.propTypes = {
|
||||||
|
todo: PropTypes.object.isRequired,
|
||||||
|
editTodo: PropTypes.func.isRequired,
|
||||||
|
deleteTodo: PropTypes.func.isRequired,
|
||||||
|
completeTodo: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoItem;
|
||||||
59
extension/examples/router/components/TodoTextInput.js
Normal file
59
extension/examples/router/components/TodoTextInput.js
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
class TodoTextInput extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
|
text: this.props.text || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
const text = e.target.value.trim();
|
||||||
|
if (e.which === 13) {
|
||||||
|
this.props.onSave(text);
|
||||||
|
if (this.props.newTodo) {
|
||||||
|
this.setState({ text: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
this.setState({ text: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur(e) {
|
||||||
|
if (!this.props.newTodo) {
|
||||||
|
this.props.onSave(e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={classnames({
|
||||||
|
edit: this.props.editing,
|
||||||
|
'new-todo': this.props.newTodo,
|
||||||
|
})}
|
||||||
|
type="text"
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
autoFocus="true"
|
||||||
|
value={this.state.text}
|
||||||
|
onBlur={this.handleBlur.bind(this)}
|
||||||
|
onChange={this.handleChange.bind(this)}
|
||||||
|
onKeyDown={this.handleSubmit.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TodoTextInput.propTypes = {
|
||||||
|
onSave: PropTypes.func.isRequired,
|
||||||
|
text: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
editing: PropTypes.bool,
|
||||||
|
newTodo: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoTextInput;
|
||||||
6
extension/examples/router/constants/ActionTypes.js
Normal file
6
extension/examples/router/constants/ActionTypes.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const ADD_TODO = 'ADD_TODO';
|
||||||
|
export const DELETE_TODO = 'DELETE_TODO';
|
||||||
|
export const EDIT_TODO = 'EDIT_TODO';
|
||||||
|
export const COMPLETE_TODO = 'COMPLETE_TODO';
|
||||||
|
export const COMPLETE_ALL = 'COMPLETE_ALL';
|
||||||
|
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
|
||||||
3
extension/examples/router/constants/TodoFilters.js
Normal file
3
extension/examples/router/constants/TodoFilters.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const SHOW_ALL = 'show_all';
|
||||||
|
export const SHOW_COMPLETED = 'show_completed';
|
||||||
|
export const SHOW_ACTIVE = 'show_active';
|
||||||
38
extension/examples/router/containers/App.js
Normal file
38
extension/examples/router/containers/App.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import MainSection from '../components/MainSection';
|
||||||
|
import * as TodoActions from '../actions/todos';
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
const { todos, path, actions } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header addTodo={actions.addTodo} path={path} />
|
||||||
|
<MainSection todos={todos} actions={actions} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
todos: PropTypes.array.isRequired,
|
||||||
|
actions: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
todos: state.todos,
|
||||||
|
path: state.router.location.pathname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||||
21
extension/examples/router/containers/Root.js
Normal file
21
extension/examples/router/containers/Root.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Route, Redirect } from 'react-router';
|
||||||
|
import { ReduxRouter } from 'redux-router';
|
||||||
|
import Wrapper from './Wrapper';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
class Root extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ReduxRouter>
|
||||||
|
<Redirect from="/" to="Standard Todo" />
|
||||||
|
<Route path="/" component={Wrapper}>
|
||||||
|
<Route path="/:id" component={App} />
|
||||||
|
</Route>
|
||||||
|
</ReduxRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Root;
|
||||||
68
extension/examples/router/containers/Wrapper.js
Normal file
68
extension/examples/router/containers/Wrapper.js
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { pushState } from 'redux-router';
|
||||||
|
import { Route, Link } from 'react-router';
|
||||||
|
import * as TodoActions from '../actions/todos';
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
pushState: bindActionCreators(pushState, dispatch),
|
||||||
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@connect((state) => ({}), mapDispatchToProps)
|
||||||
|
class Wrapper extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const { actions, pushState } = this.props;
|
||||||
|
const path = event.target.innerText;
|
||||||
|
|
||||||
|
pushState(null, path);
|
||||||
|
console.log('Navigate to', path);
|
||||||
|
|
||||||
|
if (this.timeout) clearInterval(this.timeout);
|
||||||
|
if (path === 'AutoTodo') {
|
||||||
|
console.log('!');
|
||||||
|
this.timeout = setInterval(() => {
|
||||||
|
actions.addTodo('Auto generated task');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#eee',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a href="#" onClick={this.handleClick}>
|
||||||
|
Standard Todo
|
||||||
|
</a>{' '}
|
||||||
|
|{' '}
|
||||||
|
<a href="#" onClick={this.handleClick}>
|
||||||
|
AutoTodo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wrapper;
|
||||||
10
extension/examples/router/index.html
Normal file
10
extension/examples/router/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redux TodoMVC example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="todoapp" id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
extension/examples/router/index.js
Normal file
16
extension/examples/router/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'babel-polyfill';
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import Root from './containers/Root';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
import 'todomvc-app-css/index.css';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Root />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
||||||
53
extension/examples/router/package.json
Normal file
53
extension/examples/router/package.json
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
extension/examples/router/reducers/index.js
Normal file
10
extension/examples/router/reducers/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { routerStateReducer } from 'redux-router';
|
||||||
|
import todos from './todos';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
todos,
|
||||||
|
router: routerStateReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
||||||
61
extension/examples/router/reducers/todos.js
Normal file
61
extension/examples/router/reducers/todos.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
ADD_TODO,
|
||||||
|
DELETE_TODO,
|
||||||
|
EDIT_TODO,
|
||||||
|
COMPLETE_TODO,
|
||||||
|
COMPLETE_ALL,
|
||||||
|
CLEAR_COMPLETED,
|
||||||
|
} from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
const initialState = [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function todos(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_TODO:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
||||||
|
completed: false,
|
||||||
|
text: action.text,
|
||||||
|
},
|
||||||
|
...state,
|
||||||
|
];
|
||||||
|
|
||||||
|
case DELETE_TODO:
|
||||||
|
return state.filter((todo) => todo.id !== action.id);
|
||||||
|
|
||||||
|
case EDIT_TODO:
|
||||||
|
return state.map((todo) =>
|
||||||
|
todo.id === action.id
|
||||||
|
? Object.assign({}, todo, { text: action.text })
|
||||||
|
: todo,
|
||||||
|
);
|
||||||
|
|
||||||
|
case COMPLETE_TODO:
|
||||||
|
return state.map((todo) =>
|
||||||
|
todo.id === action.id
|
||||||
|
? Object.assign({}, todo, { completed: !todo.completed })
|
||||||
|
: todo,
|
||||||
|
);
|
||||||
|
|
||||||
|
case COMPLETE_ALL:
|
||||||
|
const areAllMarked = state.every((todo) => todo.completed);
|
||||||
|
return state.map((todo) =>
|
||||||
|
Object.assign({}, todo, {
|
||||||
|
completed: !areAllMarked,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
case CLEAR_COMPLETED:
|
||||||
|
return state.filter((todo) => todo.completed === false);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
extension/examples/router/server.js
Normal file
32
extension/examples/router/server.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
var webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
|
||||||
|
var app = new require('express')();
|
||||||
|
var port = 4002;
|
||||||
|
|
||||||
|
var compiler = webpack(config);
|
||||||
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.get('/', function (req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
28
extension/examples/router/store/configureStore.js
Normal file
28
extension/examples/router/store/configureStore.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
46
extension/examples/router/test/actions/todos.spec.js
Normal file
46
extension/examples/router/test/actions/todos.spec.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
import * as actions from '../../actions/todos';
|
||||||
|
|
||||||
|
describe('todo actions', () => {
|
||||||
|
it('addTodo should create ADD_TODO action', () => {
|
||||||
|
expect(actions.addTodo('Use Redux')).toEqual({
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Use Redux',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deleteTodo should create DELETE_TODO action', () => {
|
||||||
|
expect(actions.deleteTodo(1)).toEqual({
|
||||||
|
type: types.DELETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editTodo should create EDIT_TODO action', () => {
|
||||||
|
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
||||||
|
type: types.EDIT_TODO,
|
||||||
|
id: 1,
|
||||||
|
text: 'Use Redux everywhere',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeTodo should create COMPLETE_TODO action', () => {
|
||||||
|
expect(actions.completeTodo(1)).toEqual({
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeAll should create COMPLETE_ALL action', () => {
|
||||||
|
expect(actions.completeAll()).toEqual({
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
||||||
|
expect(actions.clearCompleted('Use Redux')).toEqual({
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
108
extension/examples/router/test/components/Footer.spec.js
Normal file
108
extension/examples/router/test/components/Footer.spec.js
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
completedCount: 0,
|
||||||
|
activeCount: 0,
|
||||||
|
filter: SHOW_ALL,
|
||||||
|
onClearCompleted: expect.createSpy(),
|
||||||
|
onShow: expect.createSpy(),
|
||||||
|
},
|
||||||
|
propOverrides,
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Footer {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextContent(elem) {
|
||||||
|
const children = Array.isArray(elem.props.children)
|
||||||
|
? elem.props.children
|
||||||
|
: [elem.props.children];
|
||||||
|
|
||||||
|
return children.reduce(function concatText(out, child) {
|
||||||
|
// Children are either elements or text strings
|
||||||
|
return out + (child.props ? getTextContent(child) : child);
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Footer', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('footer');
|
||||||
|
expect(output.props.className).toBe('footer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 0 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('No items left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when above 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 1 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('1 item left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render filters', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
expect(filters.type).toBe('ul');
|
||||||
|
expect(filters.props.className).toBe('filters');
|
||||||
|
expect(filters.props.children.length).toBe(3);
|
||||||
|
filters.props.children.forEach(function checkFilter(filter, i) {
|
||||||
|
expect(filter.type).toBe('li');
|
||||||
|
const a = filter.props.children;
|
||||||
|
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
||||||
|
expect(a.props.children).toBe(
|
||||||
|
{
|
||||||
|
0: 'All',
|
||||||
|
1: 'Active',
|
||||||
|
2: 'Completed',
|
||||||
|
}[i],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onShow when a filter is clicked', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
const filterLink = filters.props.children[1].props.children;
|
||||||
|
filterLink.props.onClick({});
|
||||||
|
expect(props.onShow).toHaveBeenCalledWith(SHOW_ACTIVE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt show clear button when no completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 0 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render clear button when completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear.type).toBe('button');
|
||||||
|
expect(clear.props.children).toBe('Clear completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onClearCompleted on clear button click', () => {
|
||||||
|
const { output, props } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
clear.props.onClick({});
|
||||||
|
expect(props.onClearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
50
extension/examples/router/test/components/Header.spec.js
Normal file
50
extension/examples/router/test/components/Header.spec.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Header from '../../components/Header';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const props = {
|
||||||
|
addTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Header {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Header', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('header');
|
||||||
|
expect(output.props.className).toBe('header');
|
||||||
|
|
||||||
|
const [h1, input] = output.props.children;
|
||||||
|
|
||||||
|
expect(h1.type).toBe('h1');
|
||||||
|
expect(h1.props.children).toBe('todos');
|
||||||
|
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.newTodo).toBe(true);
|
||||||
|
expect(input.props.placeholder).toBe('What needs to be done?');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call call addTodo if length of text is greater than 0', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children[1];
|
||||||
|
input.props.onSave('');
|
||||||
|
expect(props.addTodo.calls.length).toBe(0);
|
||||||
|
input.props.onSave('Use Redux');
|
||||||
|
expect(props.addTodo.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
150
extension/examples/router/test/components/MainSection.spec.js
Normal file
150
extension/examples/router/test/components/MainSection.spec.js
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import MainSection from '../../components/MainSection';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: {
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
completeAll: expect.createSpy(),
|
||||||
|
clearCompleted: expect.createSpy(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propOverrides,
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<MainSection {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('MainSection', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('section');
|
||||||
|
expect(output.props.className).toBe('main');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggle all input', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.type).toBe('input');
|
||||||
|
expect(toggle.props.type).toBe('checkbox');
|
||||||
|
expect(toggle.props.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be checked if all todos completed', () => {
|
||||||
|
const { output } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.props.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call completeAll on change', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
toggle.props.onChange({});
|
||||||
|
expect(props.actions.completeAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('footer', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
expect(footer.type).toBe(Footer);
|
||||||
|
expect(footer.props.completedCount).toBe(1);
|
||||||
|
expect(footer.props.activeCount).toBe(1);
|
||||||
|
expect(footer.props.filter).toBe(SHOW_ALL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onShow should set the filter', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, , updatedFooter] = updated.props.children;
|
||||||
|
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted should call clearCompleted', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
||||||
|
const { output, props } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todo list', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, list] = output.props.children;
|
||||||
|
expect(list.type).toBe('ul');
|
||||||
|
expect(list.props.children.length).toBe(2);
|
||||||
|
list.props.children.forEach((item, i) => {
|
||||||
|
expect(item.type).toBe(TodoItem);
|
||||||
|
expect(item.props.todo).toBe(props.todos[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter items', () => {
|
||||||
|
const { output, renderer, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, updatedList] = updated.props.children;
|
||||||
|
expect(updatedList.props.children.length).toBe(1);
|
||||||
|
expect(updatedList.props.children[0].props.todo).toBe(props.todos[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
118
extension/examples/router/test/components/TodoItem.spec.js
Normal file
118
extension/examples/router/test/components/TodoItem.spec.js
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(editing = false) {
|
||||||
|
const props = {
|
||||||
|
todo: {
|
||||||
|
id: 0,
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoItem {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoItem', () => {
|
||||||
|
it('initial render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('');
|
||||||
|
|
||||||
|
const div = output.props.children;
|
||||||
|
|
||||||
|
expect(div.type).toBe('div');
|
||||||
|
expect(div.props.className).toBe('view');
|
||||||
|
|
||||||
|
const [input, label, button] = div.props.children;
|
||||||
|
|
||||||
|
expect(input.type).toBe('input');
|
||||||
|
expect(input.props.checked).toBe(false);
|
||||||
|
|
||||||
|
expect(label.type).toBe('label');
|
||||||
|
expect(label.props.children).toBe('Use Redux');
|
||||||
|
|
||||||
|
expect(button.type).toBe('button');
|
||||||
|
expect(button.props.className).toBe('destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input onChange should call completeTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children.props.children[0];
|
||||||
|
input.props.onChange({});
|
||||||
|
expect(props.completeTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('button onClick should call deleteTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const button = output.props.children.props.children[2];
|
||||||
|
button.props.onClick({});
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('label onDoubleClick should put component in edit state', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('editing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edit state render', () => {
|
||||||
|
const { output } = setup(true);
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('editing');
|
||||||
|
|
||||||
|
const input = output.props.children;
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.text).toBe('Use Redux');
|
||||||
|
expect(input.props.editing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call editTodo', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
expect(props.editTodo).toHaveBeenCalledWith(0, 'Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('');
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should exit component from edit state', () => {
|
||||||
|
const { output, renderer } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
onSave: expect.createSpy(),
|
||||||
|
text: 'Use Redux',
|
||||||
|
placeholder: 'What needs to be done?',
|
||||||
|
editing: false,
|
||||||
|
newTodo: false,
|
||||||
|
},
|
||||||
|
propOverrides,
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoTextInput {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoTextInput', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.props.placeholder).toEqual('What needs to be done?');
|
||||||
|
expect(output.props.value).toEqual('Use Redux');
|
||||||
|
expect(output.props.className).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when editing=true', () => {
|
||||||
|
const { output } = setup({ editing: true });
|
||||||
|
expect(output.props.className).toEqual('edit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when newTodo=true', () => {
|
||||||
|
const { output } = setup({ newTodo: true });
|
||||||
|
expect(output.props.className).toEqual('new-todo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update value on change', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
output.props.onChange({ target: { value: 'Use Radox' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('Use Radox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on return key press', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset state on return key press if newTodo', () => {
|
||||||
|
const { output, renderer } = setup({ newTodo: true });
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on blur', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt call onSave on blur if newTodo', () => {
|
||||||
|
const { output, props } = setup({ newTodo: true });
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
325
extension/examples/router/test/reducers/todos.spec.js
Normal file
325
extension/examples/router/test/reducers/todos.spec.js
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import todos from '../../reducers/todos';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
|
||||||
|
describe('todos reducer', () => {
|
||||||
|
it('should handle initial state', () => {
|
||||||
|
expect(todos(undefined, {})).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ADD_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos([], {
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Run the tests',
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Run the tests',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Fix the tests',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Fix the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle DELETE_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.DELETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle EDIT_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.EDIT_TODO,
|
||||||
|
text: 'Fix the tests',
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Fix the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_ALL', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Unmark if all todos are currently completed
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Write more tests',
|
||||||
|
},
|
||||||
|
].reduce(todos, [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
completed: false,
|
||||||
|
text: 'Use Redux',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
completed: false,
|
||||||
|
text: 'Write tests',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Write more tests',
|
||||||
|
completed: false,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Write tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
5
extension/examples/router/test/setup.js
Normal file
5
extension/examples/router/test/setup.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { jsdom } from 'jsdom';
|
||||||
|
|
||||||
|
global.document = jsdom('<!doctype html><html><body></body></html>');
|
||||||
|
global.window = document.defaultView;
|
||||||
|
global.navigator = global.window.navigator;
|
||||||
32
extension/examples/router/webpack.config.js
Normal file
32
extension/examples/router/webpack.config.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
3
extension/examples/saga-counter/.babelrc
Normal file
3
extension/examples/saga-counter/.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"]
|
||||||
|
}
|
||||||
13
extension/examples/saga-counter/index.html
Normal file
13
extension/examples/saga-counter/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!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>
|
||||||
37
extension/examples/saga-counter/package.json
Normal file
37
extension/examples/saga-counter/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
extension/examples/saga-counter/src/components/Counter.js
Normal file
27
extension/examples/saga-counter/src/components/Counter.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
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;
|
||||||
43
extension/examples/saga-counter/src/main.js
Normal file
43
extension/examples/saga-counter/src/main.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
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);
|
||||||
12
extension/examples/saga-counter/src/reducers/index.js
Normal file
12
extension/examples/saga-counter/src/reducers/index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
extension/examples/saga-counter/src/sagas/index.js
Normal file
14
extension/examples/saga-counter/src/sagas/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
25
extension/examples/saga-counter/webpack.config.js
Normal file
25
extension/examples/saga-counter/webpack.config.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
3
extension/examples/todomvc/.babelrc
Normal file
3
extension/examples/todomvc/.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015", "stage-0", "react"]
|
||||||
|
}
|
||||||
1
extension/examples/todomvc/actions/index.js
Normal file
1
extension/examples/todomvc/actions/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './todos';
|
||||||
25
extension/examples/todomvc/actions/todos.js
Normal file
25
extension/examples/todomvc/actions/todos.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as types from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
export function addTodo(text) {
|
||||||
|
return { type: types.ADD_TODO, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTodo(id) {
|
||||||
|
return { type: types.DELETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editTodo(id, text) {
|
||||||
|
return { type: types.EDIT_TODO, id, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeTodo(id) {
|
||||||
|
return { type: types.COMPLETE_TODO, id };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeAll() {
|
||||||
|
return { type: types.COMPLETE_ALL };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCompleted() {
|
||||||
|
return { type: types.CLEAR_COMPLETED };
|
||||||
|
}
|
||||||
77
extension/examples/todomvc/components/Footer.js
Normal file
77
extension/examples/todomvc/components/Footer.js
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
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;
|
||||||
30
extension/examples/todomvc/components/Header.js
Normal file
30
extension/examples/todomvc/components/Header.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
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;
|
||||||
95
extension/examples/todomvc/components/MainSection.js
Normal file
95
extension/examples/todomvc/components/MainSection.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
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;
|
||||||
76
extension/examples/todomvc/components/TodoItem.js
Normal file
76
extension/examples/todomvc/components/TodoItem.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
60
extension/examples/todomvc/components/TodoTextInput.js
Normal file
60
extension/examples/todomvc/components/TodoTextInput.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
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;
|
||||||
6
extension/examples/todomvc/constants/ActionTypes.js
Normal file
6
extension/examples/todomvc/constants/ActionTypes.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const ADD_TODO = 'ADD_TODO';
|
||||||
|
export const DELETE_TODO = 'DELETE_TODO';
|
||||||
|
export const EDIT_TODO = 'EDIT_TODO';
|
||||||
|
export const COMPLETE_TODO = 'COMPLETE_TODO';
|
||||||
|
export const COMPLETE_ALL = 'COMPLETE_ALL';
|
||||||
|
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
|
||||||
3
extension/examples/todomvc/constants/TodoFilters.js
Normal file
3
extension/examples/todomvc/constants/TodoFilters.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const SHOW_ALL = 'show_all';
|
||||||
|
export const SHOW_COMPLETED = 'show_completed';
|
||||||
|
export const SHOW_ACTIVE = 'show_active';
|
||||||
38
extension/examples/todomvc/containers/App.js
Normal file
38
extension/examples/todomvc/containers/App.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
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);
|
||||||
10
extension/examples/todomvc/index.html
Normal file
10
extension/examples/todomvc/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redux TodoMVC example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="todoapp" id="root"></div>
|
||||||
|
<script src="/static/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
extension/examples/todomvc/index.js
Normal file
16
extension/examples/todomvc/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'babel-polyfill';
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import App from './containers/App';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
import 'todomvc-app-css/index.css';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
||||||
42
extension/examples/todomvc/package.json
Normal file
42
extension/examples/todomvc/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
extension/examples/todomvc/reducers/index.js
Normal file
8
extension/examples/todomvc/reducers/index.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import todos from './todos';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
todos,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
||||||
67
extension/examples/todomvc/reducers/todos.js
Normal file
67
extension/examples/todomvc/reducers/todos.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
extension/examples/todomvc/server.js
Normal file
32
extension/examples/todomvc/server.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
var webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
|
||||||
|
var app = new require('express')();
|
||||||
|
var port = 4002;
|
||||||
|
|
||||||
|
var compiler = webpack(config);
|
||||||
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.get('/', function (req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
30
extension/examples/todomvc/store/configureStore.js
Normal file
30
extension/examples/todomvc/store/configureStore.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
46
extension/examples/todomvc/test/actions/todos.spec.js
Normal file
46
extension/examples/todomvc/test/actions/todos.spec.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
import * as actions from '../../actions/todos';
|
||||||
|
|
||||||
|
describe('todo actions', () => {
|
||||||
|
it('addTodo should create ADD_TODO action', () => {
|
||||||
|
expect(actions.addTodo('Use Redux')).toEqual({
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Use Redux',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deleteTodo should create DELETE_TODO action', () => {
|
||||||
|
expect(actions.deleteTodo(1)).toEqual({
|
||||||
|
type: types.DELETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editTodo should create EDIT_TODO action', () => {
|
||||||
|
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
||||||
|
type: types.EDIT_TODO,
|
||||||
|
id: 1,
|
||||||
|
text: 'Use Redux everywhere',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeTodo should create COMPLETE_TODO action', () => {
|
||||||
|
expect(actions.completeTodo(1)).toEqual({
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completeAll should create COMPLETE_ALL action', () => {
|
||||||
|
expect(actions.completeAll()).toEqual({
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
||||||
|
expect(actions.clearCompleted('Use Redux')).toEqual({
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
108
extension/examples/todomvc/test/components/Footer.spec.js
Normal file
108
extension/examples/todomvc/test/components/Footer.spec.js
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
completedCount: 0,
|
||||||
|
activeCount: 0,
|
||||||
|
filter: SHOW_ALL,
|
||||||
|
onClearCompleted: expect.createSpy(),
|
||||||
|
onShow: expect.createSpy(),
|
||||||
|
},
|
||||||
|
propOverrides,
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Footer {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextContent(elem) {
|
||||||
|
const children = Array.isArray(elem.props.children)
|
||||||
|
? elem.props.children
|
||||||
|
: [elem.props.children];
|
||||||
|
|
||||||
|
return children.reduce(function concatText(out, child) {
|
||||||
|
// Children are either elements or text strings
|
||||||
|
return out + (child.props ? getTextContent(child) : child);
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Footer', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('footer');
|
||||||
|
expect(output.props.className).toBe('footer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 0 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('No items left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active count when above 0', () => {
|
||||||
|
const { output } = setup({ activeCount: 1 });
|
||||||
|
const [count] = output.props.children;
|
||||||
|
expect(getTextContent(count)).toBe('1 item left');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render filters', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
expect(filters.type).toBe('ul');
|
||||||
|
expect(filters.props.className).toBe('filters');
|
||||||
|
expect(filters.props.children.length).toBe(3);
|
||||||
|
filters.props.children.forEach(function checkFilter(filter, i) {
|
||||||
|
expect(filter.type).toBe('li');
|
||||||
|
const a = filter.props.children;
|
||||||
|
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
||||||
|
expect(a.props.children).toBe(
|
||||||
|
{
|
||||||
|
0: 'All',
|
||||||
|
1: 'Active',
|
||||||
|
2: 'Completed',
|
||||||
|
}[i],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onShow when a filter is clicked', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, filters] = output.props.children;
|
||||||
|
const filterLink = filters.props.children[1].props.children;
|
||||||
|
filterLink.props.onClick({});
|
||||||
|
expect(props.onShow).toHaveBeenCalledWith(SHOW_ACTIVE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt show clear button when no completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 0 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render clear button when completed todos', () => {
|
||||||
|
const { output } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
expect(clear.type).toBe('button');
|
||||||
|
expect(clear.props.children).toBe('Clear completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onClearCompleted on clear button click', () => {
|
||||||
|
const { output, props } = setup({ completedCount: 1 });
|
||||||
|
const [, , clear] = output.props.children;
|
||||||
|
clear.props.onClick({});
|
||||||
|
expect(props.onClearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
50
extension/examples/todomvc/test/components/Header.spec.js
Normal file
50
extension/examples/todomvc/test/components/Header.spec.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import Header from '../../components/Header';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const props = {
|
||||||
|
addTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<Header {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('Header', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('header');
|
||||||
|
expect(output.props.className).toBe('header');
|
||||||
|
|
||||||
|
const [h1, input] = output.props.children;
|
||||||
|
|
||||||
|
expect(h1.type).toBe('h1');
|
||||||
|
expect(h1.props.children).toBe('todos');
|
||||||
|
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.newTodo).toBe(true);
|
||||||
|
expect(input.props.placeholder).toBe('What needs to be done?');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call call addTodo if length of text is greater than 0', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children[1];
|
||||||
|
input.props.onSave('');
|
||||||
|
expect(props.addTodo.calls.length).toBe(0);
|
||||||
|
input.props.onSave('Use Redux');
|
||||||
|
expect(props.addTodo.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
150
extension/examples/todomvc/test/components/MainSection.spec.js
Normal file
150
extension/examples/todomvc/test/components/MainSection.spec.js
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import MainSection from '../../components/MainSection';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: {
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
completeAll: expect.createSpy(),
|
||||||
|
clearCompleted: expect.createSpy(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propOverrides,
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
renderer.render(<MainSection {...props} />);
|
||||||
|
const output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('MainSection', () => {
|
||||||
|
it('should render container', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.type).toBe('section');
|
||||||
|
expect(output.props.className).toBe('main');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggle all input', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.type).toBe('input');
|
||||||
|
expect(toggle.props.type).toBe('checkbox');
|
||||||
|
expect(toggle.props.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be checked if all todos completed', () => {
|
||||||
|
const { output } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
expect(toggle.props.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call completeAll on change', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [toggle] = output.props.children;
|
||||||
|
toggle.props.onChange({});
|
||||||
|
expect(props.actions.completeAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('footer', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
expect(footer.type).toBe(Footer);
|
||||||
|
expect(footer.props.completedCount).toBe(1);
|
||||||
|
expect(footer.props.activeCount).toBe(1);
|
||||||
|
expect(footer.props.filter).toBe(SHOW_ALL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onShow should set the filter', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, , updatedFooter] = updated.props.children;
|
||||||
|
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted should call clearCompleted', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
||||||
|
const { output, props } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onClearCompleted();
|
||||||
|
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todo list', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const [, list] = output.props.children;
|
||||||
|
expect(list.type).toBe('ul');
|
||||||
|
expect(list.props.children.length).toBe(2);
|
||||||
|
list.props.children.forEach((item, i) => {
|
||||||
|
expect(item.type).toBe(TodoItem);
|
||||||
|
expect(item.props.todo).toBe(props.todos[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter items', () => {
|
||||||
|
const { output, renderer, props } = setup();
|
||||||
|
const [, , footer] = output.props.children;
|
||||||
|
footer.props.onShow(SHOW_COMPLETED);
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
const [, updatedList] = updated.props.children;
|
||||||
|
expect(updatedList.props.children.length).toBe(1);
|
||||||
|
expect(updatedList.props.children[0].props.todo).toBe(props.todos[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
118
extension/examples/todomvc/test/components/TodoItem.spec.js
Normal file
118
extension/examples/todomvc/test/components/TodoItem.spec.js
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoItem from '../../components/TodoItem';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(editing = false) {
|
||||||
|
const props = {
|
||||||
|
todo: {
|
||||||
|
id: 0,
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
editTodo: expect.createSpy(),
|
||||||
|
deleteTodo: expect.createSpy(),
|
||||||
|
completeTodo: expect.createSpy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoItem {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoItem', () => {
|
||||||
|
it('initial render', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('');
|
||||||
|
|
||||||
|
const div = output.props.children;
|
||||||
|
|
||||||
|
expect(div.type).toBe('div');
|
||||||
|
expect(div.props.className).toBe('view');
|
||||||
|
|
||||||
|
const [input, label, button] = div.props.children;
|
||||||
|
|
||||||
|
expect(input.type).toBe('input');
|
||||||
|
expect(input.props.checked).toBe(false);
|
||||||
|
|
||||||
|
expect(label.type).toBe('label');
|
||||||
|
expect(label.props.children).toBe('Use Redux');
|
||||||
|
|
||||||
|
expect(button.type).toBe('button');
|
||||||
|
expect(button.props.className).toBe('destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input onChange should call completeTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const input = output.props.children.props.children[0];
|
||||||
|
input.props.onChange({});
|
||||||
|
expect(props.completeTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('button onClick should call deleteTodo', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
const button = output.props.children.props.children[2];
|
||||||
|
button.props.onClick({});
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('label onDoubleClick should put component in edit state', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
const label = output.props.children.props.children[1];
|
||||||
|
label.props.onDoubleClick({});
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('editing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edit state render', () => {
|
||||||
|
const { output } = setup(true);
|
||||||
|
|
||||||
|
expect(output.type).toBe('li');
|
||||||
|
expect(output.props.className).toBe('editing');
|
||||||
|
|
||||||
|
const input = output.props.children;
|
||||||
|
expect(input.type).toBe(TodoTextInput);
|
||||||
|
expect(input.props.text).toBe('Use Redux');
|
||||||
|
expect(input.props.editing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call editTodo', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
expect(props.editTodo).toHaveBeenCalledWith(0, 'Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
|
||||||
|
const { output, props } = setup(true);
|
||||||
|
output.props.children.props.onSave('');
|
||||||
|
expect(props.deleteTodo).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TodoTextInput onSave should exit component from edit state', () => {
|
||||||
|
const { output, renderer } = setup(true);
|
||||||
|
output.props.children.props.onSave('Use Redux');
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.type).toBe('li');
|
||||||
|
expect(updated.props.className).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import React from 'react';
|
||||||
|
import TestUtils from 'react-addons-test-utils';
|
||||||
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
|
function setup(propOverrides) {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
onSave: expect.createSpy(),
|
||||||
|
text: 'Use Redux',
|
||||||
|
placeholder: 'What needs to be done?',
|
||||||
|
editing: false,
|
||||||
|
newTodo: false,
|
||||||
|
},
|
||||||
|
propOverrides,
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
|
renderer.render(<TodoTextInput {...props} />);
|
||||||
|
|
||||||
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: props,
|
||||||
|
output: output,
|
||||||
|
renderer: renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
describe('TodoTextInput', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { output } = setup();
|
||||||
|
expect(output.props.placeholder).toEqual('What needs to be done?');
|
||||||
|
expect(output.props.value).toEqual('Use Redux');
|
||||||
|
expect(output.props.className).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when editing=true', () => {
|
||||||
|
const { output } = setup({ editing: true });
|
||||||
|
expect(output.props.className).toEqual('edit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly when newTodo=true', () => {
|
||||||
|
const { output } = setup({ newTodo: true });
|
||||||
|
expect(output.props.className).toEqual('new-todo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update value on change', () => {
|
||||||
|
const { output, renderer } = setup();
|
||||||
|
output.props.onChange({ target: { value: 'Use Radox' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('Use Radox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on return key press', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset state on return key press if newTodo', () => {
|
||||||
|
const { output, renderer } = setup({ newTodo: true });
|
||||||
|
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
|
||||||
|
const updated = renderer.getRenderOutput();
|
||||||
|
expect(updated.props.value).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave on blur', () => {
|
||||||
|
const { output, props } = setup();
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave).toHaveBeenCalledWith('Use Redux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldnt call onSave on blur if newTodo', () => {
|
||||||
|
const { output, props } = setup({ newTodo: true });
|
||||||
|
output.props.onBlur({ target: { value: 'Use Redux' } });
|
||||||
|
expect(props.onSave.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
325
extension/examples/todomvc/test/reducers/todos.spec.js
Normal file
325
extension/examples/todomvc/test/reducers/todos.spec.js
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
import expect from 'expect';
|
||||||
|
import todos from '../../reducers/todos';
|
||||||
|
import * as types from '../../constants/ActionTypes';
|
||||||
|
|
||||||
|
describe('todos reducer', () => {
|
||||||
|
it('should handle initial state', () => {
|
||||||
|
expect(todos(undefined, {})).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ADD_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos([], {
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Run the tests',
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Run the tests',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Fix the tests',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Fix the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle DELETE_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.DELETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle EDIT_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.EDIT_TODO,
|
||||||
|
text: 'Fix the tests',
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Fix the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_TODO', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle COMPLETE_ALL', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Unmark if all todos are currently completed
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: true,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_ALL,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Run the tests',
|
||||||
|
completed: true,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Use Redux',
|
||||||
|
completed: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: types.COMPLETE_TODO,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.ADD_TODO,
|
||||||
|
text: 'Write more tests',
|
||||||
|
},
|
||||||
|
].reduce(todos, [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
completed: false,
|
||||||
|
text: 'Use Redux',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
completed: false,
|
||||||
|
text: 'Write tests',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
text: 'Write more tests',
|
||||||
|
completed: false,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Write tests',
|
||||||
|
completed: false,
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
5
extension/examples/todomvc/test/setup.js
Normal file
5
extension/examples/todomvc/test/setup.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { jsdom } from 'jsdom';
|
||||||
|
|
||||||
|
global.document = jsdom('<!doctype html><html><body></body></html>');
|
||||||
|
global.window = document.defaultView;
|
||||||
|
global.navigator = global.window.navigator;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user