├── test
├── .eslintrc
├── setup.js
├── actions
│ └── todos.spec.js
├── components
│ ├── Header.spec.js
│ ├── TodoTextInput.spec.js
│ ├── Footer.spec.js
│ ├── TodoItem.spec.js
│ └── MainSection.spec.js
└── reducers
│ └── todos.spec.js
├── .gitignore
├── constants
├── TodoFilters.js
└── ActionTypes.js
├── reducers
├── index.js
└── todos.js
├── index.html
├── .babelrc
├── README.md
├── index.js
├── actions
└── todos.js
├── store
└── configureStore.js
├── components
├── Header.js
├── TodoTextInput.js
├── TodoItem.js
├── Footer.js
└── MainSection.js
├── server.js
├── containers
├── DevTools.js
└── App.js
├── webpack.config.js
└── package.json
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | lib
6 | coverage
7 | .idea
8 |
--------------------------------------------------------------------------------
/constants/TodoFilters.js:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all'
2 | export const SHOW_COMPLETED = 'show_completed'
3 | export const SHOW_ACTIVE = 'show_active'
4 |
--------------------------------------------------------------------------------
/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 |
4 | const rootReducer = combineReducers({
5 | todos
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import { jsdom } from 'jsdom'
2 |
3 | global.document = jsdom('
')
4 | global.window = document.defaultView
5 | global.navigator = global.window.navigator
6 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux TodoMVC example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const DELETE_TODO = 'DELETE_TODO'
3 | export const EDIT_TODO = 'EDIT_TODO'
4 | export const COMPLETE_TODO = 'COMPLETE_TODO'
5 | export const COMPLETE_ALL = 'COMPLETE_ALL'
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "env": {
4 | "development": {
5 | "plugins": [["react-transform", {
6 | "transforms": [{
7 | "transform": "react-transform-hmr",
8 | "imports": ["react"],
9 | "locals": ["module"]
10 | }]
11 | }]]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a demo/example of how to use redux-devtools-chart-monitor in your React/Redux app.
2 |
3 | - Live demo: [here](http://romseguy.github.io/redux-store-visualizer/)
4 | - Grab the monitor: [redux-devtools-chart-monitor](https://github.com/romseguy/redux-devtools-chart-monitor)
5 | - Under the hood: [d3-state-visualizer](https://github.com/romseguy/d3-state-visualizer)
6 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import DevTools from './containers/DevTools'
5 | import App from './containers/App'
6 | import configureStore from './store/configureStore'
7 | import 'todomvc-app-css/index.css'
8 |
9 | const store = configureStore()
10 |
11 | render(
12 |
13 |
17 | ,
18 | document.getElementById('root')
19 | )
20 |
--------------------------------------------------------------------------------
/actions/todos.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 |
3 | export function addTodo(text) {
4 | return { type: types.ADD_TODO, text }
5 | }
6 |
7 | export function deleteTodo(id) {
8 | return { type: types.DELETE_TODO, id }
9 | }
10 |
11 | export function editTodo(id, text) {
12 | return { type: types.EDIT_TODO, id, text }
13 | }
14 |
15 | export function completeTodo(id) {
16 | return { type: types.COMPLETE_TODO, id }
17 | }
18 |
19 | export function completeAll() {
20 | return { type: types.COMPLETE_ALL }
21 | }
22 |
23 | export function clearCompleted() {
24 | return { type: types.CLEAR_COMPLETED }
25 | }
26 |
--------------------------------------------------------------------------------
/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, compose } from 'redux'
2 | import rootReducer from '../reducers'
3 | import DevTools from '../containers/DevTools';
4 |
5 | const finalCreateStore = compose(
6 | DevTools.instrument()
7 | )(createStore);
8 |
9 | export default function configureStore(initialState) {
10 | const store = finalCreateStore(rootReducer, initialState)
11 |
12 | if (module.hot) {
13 | // Enable Webpack hot module replacement for reducers
14 | module.hot.accept('../reducers', () => {
15 | const nextReducer = require('../reducers')
16 | store.replaceReducer(nextReducer)
17 | })
18 | }
19 |
20 | return store
21 | }
22 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import TodoTextInput from './TodoTextInput'
3 |
4 | class Header extends Component {
5 | handleSave(text) {
6 | if (text.length !== 0) {
7 | this.props.addTodo(text)
8 | }
9 | }
10 |
11 | render() {
12 | return (
13 |
19 | )
20 | }
21 | }
22 |
23 | Header.propTypes = {
24 | addTodo: PropTypes.func.isRequired
25 | }
26 |
27 | export default Header
28 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/containers/DevTools.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { createDevTools } from 'redux-devtools';
4 |
5 | import ChartMonitor from 'redux-devtools-chart-monitor';
6 | import DockMonitor from 'redux-devtools-dock-monitor';
7 |
8 | const tooltipOptions = {
9 | disabled: false,
10 | offset: {left: 30, top: 10},
11 | indentationSize: 2,
12 | style: {
13 | 'background-color': 'lightgrey',
14 | 'opacity': '0.7',
15 | 'border-radius': '5px',
16 | 'padding': '5px'
17 | }
18 | }
19 |
20 | const DevTools = createDevTools(
21 |
26 |
30 |
31 | );
32 |
33 | export default DevTools;
34 |
--------------------------------------------------------------------------------
/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { bindActionCreators } from 'redux'
3 | import { connect } from 'react-redux'
4 | import Header from '../components/Header'
5 | import MainSection from '../components/MainSection'
6 | import * as TodoActions from '../actions/todos'
7 |
8 | class App extends Component {
9 | render() {
10 | const { todos, actions } = this.props
11 | return (
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | App.propTypes = {
21 | todos: PropTypes.array.isRequired,
22 | actions: PropTypes.object.isRequired
23 | }
24 |
25 | function mapStateToProps(state) {
26 | return {
27 | todos: state.todos
28 | }
29 | }
30 |
31 | function mapDispatchToProps(dispatch) {
32 | return {
33 | actions: bindActionCreators(TodoActions, dispatch)
34 | }
35 | }
36 |
37 | export default connect(
38 | mapStateToProps,
39 | mapDispatchToProps
40 | )(App)
41 |
--------------------------------------------------------------------------------
/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import * as types from '../../constants/ActionTypes'
3 | import * as actions from '../../actions/todos'
4 |
5 | describe('todo actions', () => {
6 | it('addTodo should create ADD_TODO action', () => {
7 | expect(actions.addTodo('Use Redux')).toEqual({
8 | type: types.ADD_TODO,
9 | text: 'Use Redux'
10 | })
11 | })
12 |
13 | it('deleteTodo should create DELETE_TODO action', () => {
14 | expect(actions.deleteTodo(1)).toEqual({
15 | type: types.DELETE_TODO,
16 | id: 1
17 | })
18 | })
19 |
20 | it('editTodo should create EDIT_TODO action', () => {
21 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
22 | type: types.EDIT_TODO,
23 | id: 1,
24 | text: 'Use Redux everywhere'
25 | })
26 | })
27 |
28 | it('completeTodo should create COMPLETE_TODO action', () => {
29 | expect(actions.completeTodo(1)).toEqual({
30 | type: types.COMPLETE_TODO,
31 | id: 1
32 | })
33 | })
34 |
35 | it('completeAll should create COMPLETE_ALL action', () => {
36 | expect(actions.completeAll()).toEqual({
37 | type: types.COMPLETE_ALL
38 | })
39 | })
40 |
41 | it('clearCompleted should create CLEAR_COMPLETED action', () => {
42 | expect(actions.clearCompleted('Use Redux')).toEqual({
43 | type: types.CLEAR_COMPLETED
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | module: {
21 | loaders: [{
22 | test: /\.js$/,
23 | loaders: [ 'babel' ],
24 | exclude: /node_modules/,
25 | include: __dirname
26 | }, {
27 | test: /\.css?$/,
28 | loaders: [ 'style', 'raw' ],
29 | include: __dirname
30 | }]
31 | }
32 | }
33 |
34 |
35 | // When inside Redux repo, prefer src to compiled version.
36 | // You can safely delete these lines in your project.
37 | var reduxSrc = path.join(__dirname, '..', '..', 'src')
38 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules')
39 | var fs = require('fs')
40 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) {
41 | // Resolve Redux to source
42 | module.exports.resolve = { alias: { 'redux': reduxSrc } }
43 | // Compile Redux from source
44 | module.exports.module.loaders.push({
45 | test: /\.js$/,
46 | loaders: [ 'babel' ],
47 | include: reduxSrc
48 | })
49 | }
50 |
--------------------------------------------------------------------------------
/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'
2 |
3 | const initialState = [
4 | {
5 | text: 'Use Redux',
6 | completed: false,
7 | id: 0
8 | }
9 | ]
10 |
11 | export default function todos(state = initialState, action) {
12 | switch (action.type) {
13 | case ADD_TODO:
14 | return [
15 | {
16 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
17 | completed: false,
18 | text: action.text
19 | },
20 | ...state
21 | ]
22 |
23 | case DELETE_TODO:
24 | return state.filter(todo =>
25 | todo.id !== action.id
26 | )
27 |
28 | case EDIT_TODO:
29 | return state.map(todo =>
30 | todo.id === action.id ?
31 | Object.assign({}, todo, { text: action.text }) :
32 | todo
33 | )
34 |
35 | case COMPLETE_TODO:
36 | return state.map(todo =>
37 | todo.id === action.id ?
38 | Object.assign({}, todo, { completed: !todo.completed }) :
39 | todo
40 | )
41 |
42 | case COMPLETE_ALL:
43 | const areAllMarked = state.every(todo => todo.completed)
44 | return state.map(todo => Object.assign({}, todo, {
45 | completed: !areAllMarked
46 | }))
47 |
48 | case CLEAR_COMPLETED:
49 | return state.filter(todo => todo.completed === false)
50 |
51 | default:
52 | return state
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 |
4 | class TodoTextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context)
7 | this.state = {
8 | text: this.props.text || ''
9 | }
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim()
14 | if (e.which === 13) {
15 | this.props.onSave(text)
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' })
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value })
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value)
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | )
47 | }
48 | }
49 |
50 | TodoTextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | }
57 |
58 | export default TodoTextInput
59 |
--------------------------------------------------------------------------------
/test/components/Header.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import Header from '../../components/Header'
5 | import TodoTextInput from '../../components/TodoTextInput'
6 |
7 | function setup() {
8 | const props = {
9 | addTodo: expect.createSpy()
10 | }
11 |
12 | const renderer = TestUtils.createRenderer()
13 | renderer.render()
14 | const output = renderer.getRenderOutput()
15 |
16 | return {
17 | props: props,
18 | output: output,
19 | renderer: renderer
20 | }
21 | }
22 |
23 | describe('components', () => {
24 | describe('Header', () => {
25 | it('should render correctly', () => {
26 | const { output } = setup()
27 |
28 | expect(output.type).toBe('header')
29 | expect(output.props.className).toBe('header')
30 |
31 | const [ h1, input ] = output.props.children
32 |
33 | expect(h1.type).toBe('h1')
34 | expect(h1.props.children).toBe('todos')
35 |
36 | expect(input.type).toBe(TodoTextInput)
37 | expect(input.props.newTodo).toBe(true)
38 | expect(input.props.placeholder).toBe('What needs to be done?')
39 | })
40 |
41 | it('should call addTodo if length of text is greater than 0', () => {
42 | const { output, props } = setup()
43 | const input = output.props.children[1]
44 | input.props.onSave('')
45 | expect(props.addTodo.calls.length).toBe(0)
46 | input.props.onSave('Use Redux')
47 | expect(props.addTodo.calls.length).toBe(1)
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todomvc-example",
3 | "version": "0.0.0",
4 | "description": "Redux TodoMVC example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js",
8 | "test:watch": "npm test -- --watch",
9 | "build": "NODE_ENV=production webpack --config webpack.prod.config.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/rackt/redux.git"
14 | },
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/rackt/redux/issues"
18 | },
19 | "homepage": "http://rackt.github.io/redux",
20 | "dependencies": {
21 | "classnames": "^2.2.5",
22 | "react": "^15.4.1",
23 | "react-dom": "^15.4.1",
24 | "react-redux": "^4.4.6",
25 | "redux": "^3.6.0"
26 | },
27 | "devDependencies": {
28 | "babel-core": "^6.18.2",
29 | "babel-loader": "^6.2.8",
30 | "babel-plugin-react-transform": "^2.0.2",
31 | "babel-preset-es2015": "^6.18.0",
32 | "babel-preset-react": "^6.16.0",
33 | "expect": "^1.20.2",
34 | "express": "^4.14.0",
35 | "jsdom": "^9.8.3",
36 | "mocha": "^3.1.2",
37 | "node-libs-browser": "^2.0.0",
38 | "raw-loader": "^0.5.1",
39 | "react-addons-test-utils": "^15.4.1",
40 | "react-transform-hmr": "^1.0.4",
41 | "redux-devtools": "^3.3.1",
42 | "redux-devtools-chart-monitor": "^1.6.0",
43 | "redux-devtools-dock-monitor": "^1.1.1",
44 | "style-loader": "^0.13.1",
45 | "todomvc-app-css": "^2.0.6",
46 | "webpack": "^1.13.3",
47 | "webpack-dev-middleware": "^1.8.4",
48 | "webpack-hot-middleware": "^2.13.2"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/components/TodoItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 | import TodoTextInput from './TodoTextInput'
4 |
5 | class TodoItem extends Component {
6 | constructor(props, context) {
7 | super(props, context)
8 | this.state = {
9 | editing: false
10 | }
11 | }
12 |
13 | handleDoubleClick() {
14 | this.setState({ editing: true })
15 | }
16 |
17 | handleSave(id, text) {
18 | if (text.length === 0) {
19 | this.props.deleteTodo(id)
20 | } else {
21 | this.props.editTodo(id, text)
22 | }
23 | this.setState({ editing: false })
24 | }
25 |
26 | render() {
27 | const { todo, completeTodo, deleteTodo } = this.props
28 |
29 | let element
30 | if (this.state.editing) {
31 | element = (
32 | this.handleSave(todo.id, text)} />
35 | )
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo.id)} />
43 |
46 |
49 | )
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | )
60 | }
61 | }
62 |
63 | TodoItem.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | }
69 |
70 | export default TodoItem
71 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import classnames from 'classnames'
3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'
4 |
5 | const FILTER_TITLES = {
6 | [SHOW_ALL]: 'All',
7 | [SHOW_ACTIVE]: 'Active',
8 | [SHOW_COMPLETED]: 'Completed'
9 | }
10 |
11 | class Footer extends Component {
12 | renderTodoCount() {
13 | const { activeCount } = this.props
14 | const itemWord = activeCount === 1 ? 'item' : 'items'
15 |
16 | return (
17 |
18 | {activeCount || 'No'} {itemWord} left
19 |
20 | )
21 | }
22 |
23 | renderFilterLink(filter) {
24 | const title = FILTER_TITLES[filter]
25 | const { filter: selectedFilter, onShow } = this.props
26 |
27 | return (
28 | onShow(filter)}>
31 | {title}
32 |
33 | )
34 | }
35 |
36 | renderClearButton() {
37 | const { completedCount, onClearCompleted } = this.props
38 | if (completedCount > 0) {
39 | return (
40 |
44 | )
45 | }
46 | }
47 |
48 | render() {
49 | return (
50 |
61 | )
62 | }
63 | }
64 |
65 | Footer.propTypes = {
66 | completedCount: PropTypes.number.isRequired,
67 | activeCount: PropTypes.number.isRequired,
68 | filter: PropTypes.string.isRequired,
69 | onClearCompleted: PropTypes.func.isRequired,
70 | onShow: PropTypes.func.isRequired
71 | }
72 |
73 | export default Footer
74 |
--------------------------------------------------------------------------------
/components/MainSection.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import TodoItem from './TodoItem'
3 | import Footer from './Footer'
4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'
5 |
6 | const TODO_FILTERS = {
7 | [SHOW_ALL]: () => true,
8 | [SHOW_ACTIVE]: todo => !todo.completed,
9 | [SHOW_COMPLETED]: todo => todo.completed
10 | }
11 |
12 | class MainSection extends Component {
13 | constructor(props, context) {
14 | super(props, context)
15 | this.state = { filter: SHOW_ALL }
16 | }
17 |
18 | handleClearCompleted() {
19 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed)
20 | if (atLeastOneCompleted) {
21 | this.props.actions.clearCompleted()
22 | }
23 | }
24 |
25 | handleShow(filter) {
26 | this.setState({ filter })
27 | }
28 |
29 | renderToggleAll(completedCount) {
30 | const { todos, actions } = this.props
31 | if (todos.length > 0) {
32 | return (
33 |
37 | )
38 | }
39 | }
40 |
41 | renderFooter(completedCount) {
42 | const { todos } = this.props
43 | const { filter } = this.state
44 | const activeCount = todos.length - completedCount
45 |
46 | if (todos.length) {
47 | return (
48 |
53 | )
54 | }
55 | }
56 |
57 | render() {
58 | const { todos, actions } = this.props
59 | const { filter } = this.state
60 |
61 | const filteredTodos = todos.filter(TODO_FILTERS[filter])
62 | const completedCount = todos.reduce((count, todo) =>
63 | todo.completed ? count + 1 : count,
64 | 0
65 | )
66 |
67 | return (
68 |
69 | {this.renderToggleAll(completedCount)}
70 |
71 | {filteredTodos.map(todo =>
72 |
73 | )}
74 |
75 | {this.renderFooter(completedCount)}
76 |
77 | )
78 | }
79 | }
80 |
81 | MainSection.propTypes = {
82 | todos: PropTypes.array.isRequired,
83 | actions: PropTypes.object.isRequired
84 | }
85 |
86 | export default MainSection
87 |
--------------------------------------------------------------------------------
/test/components/TodoTextInput.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import TodoTextInput from '../../components/TodoTextInput'
5 |
6 | function setup(propOverrides) {
7 | const props = Object.assign({
8 | onSave: expect.createSpy(),
9 | text: 'Use Redux',
10 | placeholder: 'What needs to be done?',
11 | editing: false,
12 | newTodo: false
13 | }, propOverrides)
14 |
15 | const renderer = TestUtils.createRenderer()
16 |
17 | renderer.render(
18 |
19 | )
20 |
21 | let output = renderer.getRenderOutput()
22 |
23 | output = renderer.getRenderOutput()
24 |
25 | return {
26 | props: props,
27 | output: output,
28 | renderer: renderer
29 | }
30 | }
31 |
32 | describe('components', () => {
33 | describe('TodoTextInput', () => {
34 | it('should render correctly', () => {
35 | const { output } = setup()
36 | expect(output.props.placeholder).toEqual('What needs to be done?')
37 | expect(output.props.value).toEqual('Use Redux')
38 | expect(output.props.className).toEqual('')
39 | })
40 |
41 | it('should render correctly when editing=true', () => {
42 | const { output } = setup({ editing: true })
43 | expect(output.props.className).toEqual('edit')
44 | })
45 |
46 | it('should render correctly when newTodo=true', () => {
47 | const { output } = setup({ newTodo: true })
48 | expect(output.props.className).toEqual('new-todo')
49 | })
50 |
51 | it('should update value on change', () => {
52 | const { output, renderer } = setup()
53 | output.props.onChange({ target: { value: 'Use Radox' } })
54 | const updated = renderer.getRenderOutput()
55 | expect(updated.props.value).toEqual('Use Radox')
56 | })
57 |
58 | it('should call onSave on return key press', () => {
59 | const { output, props } = setup()
60 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
61 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
62 | })
63 |
64 | it('should reset state on return key press if newTodo', () => {
65 | const { output, renderer } = setup({ newTodo: true })
66 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
67 | const updated = renderer.getRenderOutput()
68 | expect(updated.props.value).toEqual('')
69 | })
70 |
71 | it('should call onSave on blur', () => {
72 | const { output, props } = setup()
73 | output.props.onBlur({ target: { value: 'Use Redux' } })
74 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
75 | })
76 |
77 | it('shouldnt call onSave on blur if newTodo', () => {
78 | const { output, props } = setup({ newTodo: true })
79 | output.props.onBlur({ target: { value: 'Use Redux' } })
80 | expect(props.onSave.calls.length).toBe(0)
81 | })
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/test/components/Footer.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import Footer from '../../components/Footer'
5 | import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters'
6 |
7 | function setup(propOverrides) {
8 | const props = Object.assign({
9 | completedCount: 0,
10 | activeCount: 0,
11 | filter: SHOW_ALL,
12 | onClearCompleted: expect.createSpy(),
13 | onShow: expect.createSpy()
14 | }, propOverrides)
15 |
16 | const renderer = TestUtils.createRenderer()
17 | renderer.render()
18 | const output = renderer.getRenderOutput()
19 |
20 | return {
21 | props: props,
22 | output: output
23 | }
24 | }
25 |
26 | function getTextContent(elem) {
27 | const children = Array.isArray(elem.props.children) ?
28 | elem.props.children : [ elem.props.children ]
29 |
30 | return children.reduce(function concatText(out, child) {
31 | // Children are either elements or text strings
32 | return out + (child.props ? getTextContent(child) : child)
33 | }, '')
34 | }
35 |
36 | describe('components', () => {
37 | describe('Footer', () => {
38 | it('should render container', () => {
39 | const { output } = setup()
40 | expect(output.type).toBe('footer')
41 | expect(output.props.className).toBe('footer')
42 | })
43 |
44 | it('should display active count when 0', () => {
45 | const { output } = setup({ activeCount: 0 })
46 | const [ count ] = output.props.children
47 | expect(getTextContent(count)).toBe('No items left')
48 | })
49 |
50 | it('should display active count when above 0', () => {
51 | const { output } = setup({ activeCount: 1 })
52 | const [ count ] = output.props.children
53 | expect(getTextContent(count)).toBe('1 item left')
54 | })
55 |
56 | it('should render filters', () => {
57 | const { output } = setup()
58 | const [ , filters ] = output.props.children
59 | expect(filters.type).toBe('ul')
60 | expect(filters.props.className).toBe('filters')
61 | expect(filters.props.children.length).toBe(3)
62 | filters.props.children.forEach(function checkFilter(filter, i) {
63 | expect(filter.type).toBe('li')
64 | const a = filter.props.children
65 | expect(a.props.className).toBe(i === 0 ? 'selected' : '')
66 | expect(a.props.children).toBe({
67 | 0: 'All',
68 | 1: 'Active',
69 | 2: 'Completed'
70 | }[i])
71 | })
72 | })
73 |
74 | it('should call onShow when a filter is clicked', () => {
75 | const { output, props } = setup()
76 | const [ , filters ] = output.props.children
77 | const filterLink = filters.props.children[1].props.children
78 | filterLink.props.onClick({})
79 | expect(props.onShow).toHaveBeenCalledWith(SHOW_ACTIVE)
80 | })
81 |
82 | it('shouldnt show clear button when no completed todos', () => {
83 | const { output } = setup({ completedCount: 0 })
84 | const [ , , clear ] = output.props.children
85 | expect(clear).toBe(undefined)
86 | })
87 |
88 | it('should render clear button when completed todos', () => {
89 | const { output } = setup({ completedCount: 1 })
90 | const [ , , clear ] = output.props.children
91 | expect(clear.type).toBe('button')
92 | expect(clear.props.children).toBe('Clear completed')
93 | })
94 |
95 | it('should call onClearCompleted on clear button click', () => {
96 | const { output, props } = setup({ completedCount: 1 })
97 | const [ , , clear ] = output.props.children
98 | clear.props.onClick({})
99 | expect(props.onClearCompleted).toHaveBeenCalled()
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/test/components/TodoItem.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import TodoItem from '../../components/TodoItem'
5 | import TodoTextInput from '../../components/TodoTextInput'
6 |
7 | function setup( editing = false ) {
8 | const props = {
9 | todo: {
10 | id: 0,
11 | text: 'Use Redux',
12 | completed: false
13 | },
14 | editTodo: expect.createSpy(),
15 | deleteTodo: expect.createSpy(),
16 | completeTodo: expect.createSpy()
17 | }
18 |
19 | const renderer = TestUtils.createRenderer()
20 |
21 | renderer.render(
22 |
23 | )
24 |
25 | let output = renderer.getRenderOutput()
26 |
27 | if (editing) {
28 | const label = output.props.children.props.children[1]
29 | label.props.onDoubleClick({})
30 | output = renderer.getRenderOutput()
31 | }
32 |
33 | return {
34 | props: props,
35 | output: output,
36 | renderer: renderer
37 | }
38 | }
39 |
40 | describe('components', () => {
41 | describe('TodoItem', () => {
42 | it('initial render', () => {
43 | const { output } = setup()
44 |
45 | expect(output.type).toBe('li')
46 | expect(output.props.className).toBe('')
47 |
48 | const div = output.props.children
49 |
50 | expect(div.type).toBe('div')
51 | expect(div.props.className).toBe('view')
52 |
53 | const [ input, label, button ] = div.props.children
54 |
55 | expect(input.type).toBe('input')
56 | expect(input.props.checked).toBe(false)
57 |
58 | expect(label.type).toBe('label')
59 | expect(label.props.children).toBe('Use Redux')
60 |
61 | expect(button.type).toBe('button')
62 | expect(button.props.className).toBe('destroy')
63 | })
64 |
65 | it('input onChange should call completeTodo', () => {
66 | const { output, props } = setup()
67 | const input = output.props.children.props.children[0]
68 | input.props.onChange({})
69 | expect(props.completeTodo).toHaveBeenCalledWith(0)
70 | })
71 |
72 | it('button onClick should call deleteTodo', () => {
73 | const { output, props } = setup()
74 | const button = output.props.children.props.children[2]
75 | button.props.onClick({})
76 | expect(props.deleteTodo).toHaveBeenCalledWith(0)
77 | })
78 |
79 | it('label onDoubleClick should put component in edit state', () => {
80 | const { output, renderer } = setup()
81 | const label = output.props.children.props.children[1]
82 | label.props.onDoubleClick({})
83 | const updated = renderer.getRenderOutput()
84 | expect(updated.type).toBe('li')
85 | expect(updated.props.className).toBe('editing')
86 | })
87 |
88 | it('edit state render', () => {
89 | const { output } = setup(true)
90 |
91 | expect(output.type).toBe('li')
92 | expect(output.props.className).toBe('editing')
93 |
94 | const input = output.props.children
95 | expect(input.type).toBe(TodoTextInput)
96 | expect(input.props.text).toBe('Use Redux')
97 | expect(input.props.editing).toBe(true)
98 | })
99 |
100 | it('TodoTextInput onSave should call editTodo', () => {
101 | const { output, props } = setup(true)
102 | output.props.children.props.onSave('Use Redux')
103 | expect(props.editTodo).toHaveBeenCalledWith(0, 'Use Redux')
104 | })
105 |
106 | it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
107 | const { output, props } = setup(true)
108 | output.props.children.props.onSave('')
109 | expect(props.deleteTodo).toHaveBeenCalledWith(0)
110 | })
111 |
112 | it('TodoTextInput onSave should exit component from edit state', () => {
113 | const { output, renderer } = setup(true)
114 | output.props.children.props.onSave('Use Redux')
115 | const updated = renderer.getRenderOutput()
116 | expect(updated.type).toBe('li')
117 | expect(updated.props.className).toBe('')
118 | })
119 | })
120 | })
121 |
--------------------------------------------------------------------------------
/test/components/MainSection.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import MainSection from '../../components/MainSection'
5 | import TodoItem from '../../components/TodoItem'
6 | import Footer from '../../components/Footer'
7 | import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters'
8 |
9 | function setup(propOverrides) {
10 | const props = Object.assign({
11 | todos: [
12 | {
13 | text: 'Use Redux',
14 | completed: false,
15 | id: 0
16 | }, {
17 | text: 'Run the tests',
18 | completed: true,
19 | id: 1
20 | }
21 | ],
22 | actions: {
23 | editTodo: expect.createSpy(),
24 | deleteTodo: expect.createSpy(),
25 | completeTodo: expect.createSpy(),
26 | completeAll: expect.createSpy(),
27 | clearCompleted: expect.createSpy()
28 | }
29 | }, propOverrides)
30 |
31 | const renderer = TestUtils.createRenderer()
32 | renderer.render()
33 | const output = renderer.getRenderOutput()
34 |
35 | return {
36 | props: props,
37 | output: output,
38 | renderer: renderer
39 | }
40 | }
41 |
42 | describe('components', () => {
43 | describe('MainSection', () => {
44 | it('should render container', () => {
45 | const { output } = setup()
46 | expect(output.type).toBe('section')
47 | expect(output.props.className).toBe('main')
48 | })
49 |
50 | describe('toggle all input', () => {
51 | it('should render', () => {
52 | const { output } = setup()
53 | const [ toggle ] = output.props.children
54 | expect(toggle.type).toBe('input')
55 | expect(toggle.props.type).toBe('checkbox')
56 | expect(toggle.props.checked).toBe(false)
57 | })
58 |
59 | it('should be checked if all todos completed', () => {
60 | const { output } = setup({ todos: [
61 | {
62 | text: 'Use Redux',
63 | completed: true,
64 | id: 0
65 | }
66 | ]
67 | })
68 | const [ toggle ] = output.props.children
69 | expect(toggle.props.checked).toBe(true)
70 | })
71 |
72 | it('should call completeAll on change', () => {
73 | const { output, props } = setup()
74 | const [ toggle ] = output.props.children
75 | toggle.props.onChange({})
76 | expect(props.actions.completeAll).toHaveBeenCalled()
77 | })
78 | })
79 |
80 | describe('footer', () => {
81 | it('should render', () => {
82 | const { output } = setup()
83 | const [ , , footer ] = output.props.children
84 | expect(footer.type).toBe(Footer)
85 | expect(footer.props.completedCount).toBe(1)
86 | expect(footer.props.activeCount).toBe(1)
87 | expect(footer.props.filter).toBe(SHOW_ALL)
88 | })
89 |
90 | it('onShow should set the filter', () => {
91 | const { output, renderer } = setup()
92 | const [ , , footer ] = output.props.children
93 | footer.props.onShow(SHOW_COMPLETED)
94 | const updated = renderer.getRenderOutput()
95 | const [ , , updatedFooter ] = updated.props.children
96 | expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED)
97 | })
98 |
99 | it('onClearCompleted should call clearCompleted', () => {
100 | const { output, props } = setup()
101 | const [ , , footer ] = output.props.children
102 | footer.props.onClearCompleted()
103 | expect(props.actions.clearCompleted).toHaveBeenCalled()
104 | })
105 |
106 | it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
107 | const { output, props } = setup({
108 | todos: [
109 | {
110 | text: 'Use Redux',
111 | completed: false,
112 | id: 0
113 | }
114 | ]
115 | })
116 | const [ , , footer ] = output.props.children
117 | footer.props.onClearCompleted()
118 | expect(props.actions.clearCompleted.calls.length).toBe(0)
119 | })
120 | })
121 |
122 | describe('todo list', () => {
123 | it('should render', () => {
124 | const { output, props } = setup()
125 | const [ , list ] = output.props.children
126 | expect(list.type).toBe('ul')
127 | expect(list.props.children.length).toBe(2)
128 | list.props.children.forEach((item, i) => {
129 | expect(item.type).toBe(TodoItem)
130 | expect(item.props.todo).toBe(props.todos[i])
131 | })
132 | })
133 |
134 | it('should filter items', () => {
135 | const { output, renderer, props } = setup()
136 | const [ , , footer ] = output.props.children
137 | footer.props.onShow(SHOW_COMPLETED)
138 | const updated = renderer.getRenderOutput()
139 | const [ , updatedList ] = updated.props.children
140 | expect(updatedList.props.children.length).toBe(1)
141 | expect(updatedList.props.children[0].props.todo).toBe(props.todos[1])
142 | })
143 | })
144 | })
145 | })
146 |
--------------------------------------------------------------------------------
/test/reducers/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import todos from '../../reducers/todos'
3 | import * as types from '../../constants/ActionTypes'
4 |
5 | describe('todos reducer', () => {
6 | it('should handle initial state', () => {
7 | expect(
8 | todos(undefined, {})
9 | ).toEqual([
10 | {
11 | text: 'Use Redux',
12 | completed: false,
13 | id: 0
14 | }
15 | ])
16 | })
17 |
18 | it('should handle ADD_TODO', () => {
19 | expect(
20 | todos([], {
21 | type: types.ADD_TODO,
22 | text: 'Run the tests'
23 | })
24 | ).toEqual([
25 | {
26 | text: 'Run the tests',
27 | completed: false,
28 | id: 0
29 | }
30 | ])
31 |
32 | expect(
33 | todos([
34 | {
35 | text: 'Use Redux',
36 | completed: false,
37 | id: 0
38 | }
39 | ], {
40 | type: types.ADD_TODO,
41 | text: 'Run the tests'
42 | })
43 | ).toEqual([
44 | {
45 | text: 'Run the tests',
46 | completed: false,
47 | id: 1
48 | }, {
49 | text: 'Use Redux',
50 | completed: false,
51 | id: 0
52 | }
53 | ])
54 |
55 | expect(
56 | todos([
57 | {
58 | text: 'Run the tests',
59 | completed: false,
60 | id: 1
61 | }, {
62 | text: 'Use Redux',
63 | completed: false,
64 | id: 0
65 | }
66 | ], {
67 | type: types.ADD_TODO,
68 | text: 'Fix the tests'
69 | })
70 | ).toEqual([
71 | {
72 | text: 'Fix the tests',
73 | completed: false,
74 | id: 2
75 | }, {
76 | text: 'Run the tests',
77 | completed: false,
78 | id: 1
79 | }, {
80 | text: 'Use Redux',
81 | completed: false,
82 | id: 0
83 | }
84 | ])
85 | })
86 |
87 | it('should handle DELETE_TODO', () => {
88 | expect(
89 | todos([
90 | {
91 | text: 'Run the tests',
92 | completed: false,
93 | id: 1
94 | }, {
95 | text: 'Use Redux',
96 | completed: false,
97 | id: 0
98 | }
99 | ], {
100 | type: types.DELETE_TODO,
101 | id: 1
102 | })
103 | ).toEqual([
104 | {
105 | text: 'Use Redux',
106 | completed: false,
107 | id: 0
108 | }
109 | ])
110 | })
111 |
112 | it('should handle EDIT_TODO', () => {
113 | expect(
114 | todos([
115 | {
116 | text: 'Run the tests',
117 | completed: false,
118 | id: 1
119 | }, {
120 | text: 'Use Redux',
121 | completed: false,
122 | id: 0
123 | }
124 | ], {
125 | type: types.EDIT_TODO,
126 | text: 'Fix the tests',
127 | id: 1
128 | })
129 | ).toEqual([
130 | {
131 | text: 'Fix the tests',
132 | completed: false,
133 | id: 1
134 | }, {
135 | text: 'Use Redux',
136 | completed: false,
137 | id: 0
138 | }
139 | ])
140 | })
141 |
142 | it('should handle COMPLETE_TODO', () => {
143 | expect(
144 | todos([
145 | {
146 | text: 'Run the tests',
147 | completed: false,
148 | id: 1
149 | }, {
150 | text: 'Use Redux',
151 | completed: false,
152 | id: 0
153 | }
154 | ], {
155 | type: types.COMPLETE_TODO,
156 | id: 1
157 | })
158 | ).toEqual([
159 | {
160 | text: 'Run the tests',
161 | completed: true,
162 | id: 1
163 | }, {
164 | text: 'Use Redux',
165 | completed: false,
166 | id: 0
167 | }
168 | ])
169 | })
170 |
171 | it('should handle COMPLETE_ALL', () => {
172 | expect(
173 | todos([
174 | {
175 | text: 'Run the tests',
176 | completed: true,
177 | id: 1
178 | }, {
179 | text: 'Use Redux',
180 | completed: false,
181 | id: 0
182 | }
183 | ], {
184 | type: types.COMPLETE_ALL
185 | })
186 | ).toEqual([
187 | {
188 | text: 'Run the tests',
189 | completed: true,
190 | id: 1
191 | }, {
192 | text: 'Use Redux',
193 | completed: true,
194 | id: 0
195 | }
196 | ])
197 |
198 | // Unmark if all todos are currently completed
199 | expect(
200 | todos([
201 | {
202 | text: 'Run the tests',
203 | completed: true,
204 | id: 1
205 | }, {
206 | text: 'Use Redux',
207 | completed: true,
208 | id: 0
209 | }
210 | ], {
211 | type: types.COMPLETE_ALL
212 | })
213 | ).toEqual([
214 | {
215 | text: 'Run the tests',
216 | completed: false,
217 | id: 1
218 | }, {
219 | text: 'Use Redux',
220 | completed: false,
221 | id: 0
222 | }
223 | ])
224 | })
225 |
226 | it('should handle CLEAR_COMPLETED', () => {
227 | expect(
228 | todos([
229 | {
230 | text: 'Run the tests',
231 | completed: true,
232 | id: 1
233 | }, {
234 | text: 'Use Redux',
235 | completed: false,
236 | id: 0
237 | }
238 | ], {
239 | type: types.CLEAR_COMPLETED
240 | })
241 | ).toEqual([
242 | {
243 | text: 'Use Redux',
244 | completed: false,
245 | id: 0
246 | }
247 | ])
248 | })
249 |
250 | it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
251 | expect(
252 | [
253 | {
254 | type: types.COMPLETE_TODO,
255 | id: 0
256 | }, {
257 | type: types.CLEAR_COMPLETED
258 | }, {
259 | type: types.ADD_TODO,
260 | text: 'Write more tests'
261 | }
262 | ].reduce(todos, [
263 | {
264 | id: 0,
265 | completed: false,
266 | text: 'Use Redux'
267 | }, {
268 | id: 1,
269 | completed: false,
270 | text: 'Write tests'
271 | }
272 | ])
273 | ).toEqual([
274 | {
275 | text: 'Write more tests',
276 | completed: false,
277 | id: 2
278 | }, {
279 | text: 'Write tests',
280 | completed: false,
281 | id: 1
282 | }
283 | ])
284 | })
285 | })
286 |
--------------------------------------------------------------------------------