Story: As a user, I want to add a todo to my todos list
Since this is our first story, we'll need to create some scaffolding to complete it. It will probably be our largest single story, creating basic React/Redux structures like Actions, Action Types, the Reducer, and the Component hierarchy, all the way up to the top-level Container. In a real project team, we might choose to split this story into several sub-stories or sub-tasks that can each be completed by a separate engineer, in parallel.
TodoMVC actions
It should create an action to add a todo
Write the test.
// tests/todomvc/actions.spec.js'use strict';import{expect}from'chai'importtodomvcfrom'../../src/todomvc'describe('TodoMVC actions',()=>{it('Should create an action to add a todo',()=>{constdescription='My todo';constexpectedAction={type:'todomvc/ADD',description:description,completed:false};expect(todomvc.actions.addTodo(description)).to.deep.equal(expectedAction)})});
Run the test and watch it fail.
Write the code to make the test pass. In this case, the test failed because we haven't yet written any production code, so we couldn't find the todomvc module.
We will also need to a place to define our action type constants:
Finally, we will need to create an index file to export modules from our application:
Run the test and watch it pass.
Commit changes.
TodoMVC reducer
It should handle initial state
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
And export the new reducer from the application.
Run the test and watch it pass.
Commit changes.
It should handle ADD todo
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
TodoTextInput component
It should render correctly
It should be a TodoTextInput component
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
Should behave correctly
It should update value on change
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
It should call onSave() on return key press
Write the test. We need to import sinon to spy on the onSave
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
It should reset state on return key press if isNew
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
It should call onSave() on blur if not isNew
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
It should not call onSave() on blur if isNew
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
Header component
Should render correctly
It should be a Header component
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
It should have a title
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
It should have a TodoTextInput field
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Note the warning, below. We'll implement onSave in the next test.
Commit changes.
Should behave correctly
It should call addTodo() if length of text is greater than 0
Write the test.
Run the test and watch it fail.
Write the code to make the test pass.
Run the test and watch it pass.
Commit changes.
TodoApp component
This is the top-level container component of our application. To render it, we'll need a Redux store. However, we don't want to pull the whole application template into these tests. Instead, we'll just create our own, simple Redux store using our existing reducer.
Interestingly, there is not a lot to test in this component, since it's mostly just responsible for rendering its child components and passing them the mapped store and actions.
Type "Hello" into the text input box and see what happens. Then enter "World" You can watch state update with each action in the Redux Dev Tools window on the right, like this:
// src/todomvc/index.js
'use strict';
import * as types from './ActionTypes'
import * as actions from './actions'
export default {types, actions}
TodoMVC actions
✓ Should create an action to add a todo
1 passing (21ms)
$ git add .
$ git commit -m 'created an action to add a todo'
// tests/todomvc/reducer.spec.js
'use strict';
import {expect} from 'chai'
import {fromJS} from 'immutable'
import todomvc from '../../src/todomvc'
describe('TodoMVC reducer', () => {
it('Should handle initial state', () => {
const state = todomvc.reducer(undefined, {});
expect(state).to.equal(fromJS([]))
})
});
TodoMVC actions
✓ Should create an action to add a todo
TodoMVC reducer
1) Should handle initial state
1 passing (24ms)
1 failing
1) TodoMVC reducer should handle initial state:
TypeError: _todomvc2.default.reducer is not a function
at Context.<anonymous> (reducer.spec.js:10:27)
// src/todomvc/reducer.js
'use strict';
import { List } from 'immutable'
export default function reducer (state = List ([]), action) {
switch (action.type) {
default:
// just return the same state
return (state)
}
}
diff --git a/src/todomvc/index.js b/src/todomvc/index.js
index 5fd0f5c..4c98f49 100644
--- a/src/todomvc/index.js
+++ b/src/todomvc/index.js
@@ -2,5 +2,6 @@
import * as types from './ActionTypes'
import * as actions from './actions'
+import reducer from './reducer'
-export default { types, actions }
+export default { types, actions, reducer }
TodoMVC actions
✓ Should create an action to add a todo
TodoMVC reducer
✓ Should handle initial state
2 passing (24ms)
TodoMVC actions
✓ Should create an action to add a todo
TodoMVC reducer
✓ Should handle initial state
1) Should handle ADD todo
2 passing (30ms)
1 failing
1) TodoMVC reducer Should handle ADD todo:
TypeError: Cannot read property 'filterNot' of undefined
at Context.<anonymous> (tests/todomvc/reducer.spec.js:22:12)
diff --git a/src/todomvc/reducer.js b/src/todomvc/reducer.js
index fd31038..ea9495e 100644
--- a/src/todomvc/reducer.js
+++ b/src/todomvc/reducer.js
@@ -1,9 +1,14 @@
'use strict';
-import { List } from 'immutable'
+import {List, Map} from 'immutable'
+import uuid from 'uuid'
-export default function reducer (state = List ([]), action) {
+import * as types from "./ActionTypes";
+
+export default function reducer(state = List([]), action) {
switch (action.type) {
+ case types.ADD:
+ return (state.push(Map({id: uuid.v4(), description: action.description, completed: false})));
default:
// just return the same state
return (state)
(END)
TodoMVC actions
✓ Should create an action to add a todo
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
3 passing (25ms)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
4 passing (36ms)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
1) Should update value on change
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
4 passing (51ms)
1 failing
1) TodoTextInput component Should behave correctly Should update value on change:
AssertionError: expected 'my todo' to equal 'todo'
+ expected - actual
-my todo
+todo
at Context.<anonymous> (tests/todomvc/components/TodoTextInput.spec.js:40:62)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
5 passing (43ms)
$ git add .
$ git commit -m 'update value on change'
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
1) Should call onSave() on return key press
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
5 passing (88ms)
1 failing
1) TodoTextInput component Should behave correctly Should call onSave() on return key press:
AssertionError: expected false to be true
+ expected - actual
-false
+true
at Context.<anonymous> (tests/todomvc/components/TodoTextInput.spec.js:52:7)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
6 passing (51ms)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
1) Should reset state on return key press if isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
6 passing (74ms)
1 failing
1) TodoTextInput component Should behave correctly Should reset state on return key press if isNew:
AssertionError: expected 'my todo' to equal ''
+ expected - actual
-my todo
at Context.<anonymous> (tests/todomvc/components/TodoTextInput.spec.js:64:62)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
7 passing (52ms)
$ git add .
$ git commit -m 'reset state on return key press if isNew'
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
1) Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
7 passing (89ms)
1 failing
1) TodoTextInput component Should behave correctly Should call onSave() on blur if not isNew:
AssertionError: expected false to be true
+ expected - actual
-false
+true
at Context.<anonymous> (tests/todomvc/components/TodoTextInput.spec.js:71:7)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
8 passing (54ms)
$ git add .
$ git commit -m 'call onSave() on blur if not isNew'
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
1) Should not call onSave() on blur if isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
8 passing (104ms)
1 failing
1) TodoTextInput component Should behave correctly Should not call onSave() on blur if isNew:
AssertionError: expected true to be false
+ expected - actual
-true
+false
at Context.<anonymous> (tests/todomvc/components/TodoTextInput.spec.js:79:7)
TodoMVC actions
✓ Should create an action to add a todo
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
9 passing (56ms)
$ git add .
$ git commit -m 'not call onSave() on blur if isNew'
// tests/todomvc/components/Header.spec.js
'use strict';
import React from 'react'
import {expect} from 'chai'
import {shallow} from 'enzyme'
import Header from '../../../src/todomvc/components/Header'
function setup() {
const component = shallow(
<Header/>
);
return {
component: component
}
}
describe('Header component', () => {
describe('Should render correctly', () => {
it('Should be a Header', () => {
const {component} = setup();
expect(component.type()).to.equal('header')
})
})
});
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
✓ Should be a Header
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
10 passing (58ms)
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
✓ Should be a Header
1) Should have a title
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
10 passing (72ms)
1 failing
1) Header component Should render correctly Should have a title:
Error: Method “type” is only meant to be run on a single node. 0 found instead.
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1516:17)
at ShallowWrapper.type (node_modules/enzyme/build/ShallowWrapper.js:1110:21)
at Context.<anonymous> (tests/todomvc/components/Header.spec.js:32:17)
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
✓ Should be a Header
✓ Should have a title
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
11 passing (59ms)
$ git add .
$ git commit -m 'have a title'
diff --git a/tests/todomvc/components/Header.spec.js b/tests/todomvc/components/Header.spec.js
index b02f0a8..84c2564 100644
--- a/tests/todomvc/components/Header.spec.js
+++ b/tests/todomvc/components/Header.spec.js
@@ -5,6 +5,7 @@ import {expect} from 'chai'
import {shallow} from 'enzyme'
import Header from '../../../src/todomvc/components/Header'
+import TodoTextInput from '../../../src/todomvc/components/TodoTextInput'
function setup() {
const component = shallow(
@@ -30,6 +31,15 @@ describe('Header component', () => {
expect(h1.type()).to.equal('h1');
expect(h1.text()).to.equal('todos')
+ });
+
+ it('Should have a TodoTextInput field', () => {
+ const {component} = setup();
+ const input = component.children(TodoTextInput);
+
+ expect(input.type()).to.equal(TodoTextInput);
+ expect(input.props().placeholder).to.equal('What needs to be done?');
+ expect(input.props().isNew).to.equal(true)
})
})
});
(END)
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
✓ Should be a Header
✓ Should have a title
1) Should have a TodoTextInput field
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
11 passing (65ms)
1 failing
1) Header component Should render correctly Should have a TodoTextInput field:
Error: Method “type” is only meant to be run on a single node. 0 found instead.
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1516:17)
at ShallowWrapper.type (node_modules/enzyme/build/ShallowWrapper.js:1110:21)
at Context.<anonymous> (tests/todomvc/components/Header.spec.js:41:20)
diff --git a/src/todomvc/components/Header.js b/src/todomvc/components/Header.js
index e3e6e11..da78cb7 100644
--- a/src/todomvc/components/Header.js
+++ b/src/todomvc/components/Header.js
@@ -1,12 +1,14 @@
'use strict';
import React, {Component} from 'react'
+import TodoTextInput from './TodoTextInput'
export default class Header extends Component {
render() {
return (
<header>
<h1>todos</h1>
+ <TodoTextInput placeholder="What needs to be done?" isNew />
</header>
)
}
(END)
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
Warning: Failed prop type: The prop `onSave` is marked as required in `TodoTextInput`, but its value is `undefined`.
in TodoTextInput
✓ Should be a Header
✓ Should have a title
✓ Should have a TodoTextInput field
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
12 passing (61ms)
diff --git a/tests/todomvc/components/Header.spec.js b/tests/todomvc/components/Header.spec.js
index 84c2564..d7db404 100644
--- a/tests/todomvc/components/Header.spec.js
+++ b/tests/todomvc/components/Header.spec.js
@@ -3,17 +3,25 @@
import React from 'react'
import {expect} from 'chai'
import {shallow} from 'enzyme'
+import sinon from 'sinon'
import Header from '../../../src/todomvc/components/Header'
import TodoTextInput from '../../../src/todomvc/components/TodoTextInput'
function setup() {
+ const props = {
+ actions: {
+ addTodo: sinon.spy(),
+ }
+ };
+
const component = shallow(
- <Header/>
+ <Header {...props} />
);
return {
- component: component
+ component: component,
+ props: props
}
}
@@ -41,5 +49,17 @@ describe('Header component', () => {
expect(input.props().placeholder).to.equal('What needs to be done?');
expect(input.props().isNew).to.equal(true)
})
+ });
+
+ describe('Should behave correctly', () => {
+ it('Should call addTodo() if length of text is greater than 0', () => {
+ const {component, props} = setup();
+ const input = component.children(TodoTextInput);
+
+ input.props().onSave('');
+ expect(props.actions.addTodo.called).to.be.false;
+ input.props().onSave('Use Redux');
+ expect(props.actions.addTodo.called).to.be.true
+ })
})
});
(END)
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
Warning: Failed prop type: The prop `onSave` is marked as required in `TodoTextInput`, but its value is `undefined`.
in TodoTextInput
✓ Should be a Header
✓ Should have a title
✓ Should have a TodoTextInput field
Should behave correctly
1) Should call addTodo() if length of text is greater than 0
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
12 passing (69ms)
1 failing
1) Header component Should behave correctly Should call addTodo() if length of text is greater than 0:
TypeError: input.props(...).onSave is not a function
at Context.<anonymous> (tests/todomvc/components/Header.spec.js:60:21)
diff --git a/src/todomvc/components/Header.js b/src/todomvc/components/Header.js
index da78cb7..022a8fd 100644
--- a/src/todomvc/components/Header.js
+++ b/src/todomvc/components/Header.js
@@ -1,14 +1,26 @@
'use strict';
import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+
import TodoTextInput from './TodoTextInput'
export default class Header extends Component {
+ static propTypes = {
+ actions: PropTypes.object.isRequired
+ };
+
+ handleSave(text) {
+ if (text.length !== 0) {
+ this.props.actions.addTodo(text)
+ }
+ }
+
render() {
return (
<header>
<h1>todos</h1>
- <TodoTextInput placeholder="What needs to be done?" isNew />
+ <TodoTextInput placeholder="What needs to be done?" isNew onSave={this.handleSave.bind(this)}/>
</header>
)
}
(END)
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
✓ Should be a Header
✓ Should have a title
✓ Should have a TodoTextInput field
Should behave correctly
✓ Should call addTodo() if length of text is greater than 0
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
13 passing (60ms)
$ git add .
$ git commit -m 'call addTodo() if length of text is greater than 0'
// tests/todomvc/components/TodoApp.spec.js
'use strict';
import React from 'react'
import {List} from 'immutable'
import {expect} from 'chai'
import {shallow} from 'enzyme'
import TodoApp from '../../../src/todomvc/components/TodoApp'
function setup() {
const props = {
todos: List([]),
actions: {}
};
const component = shallow(
<TodoApp.WrappedComponent {...props} />
);
return {
component: component
}
}
describe('TodoApp component', () => {
describe('Should render correctly', () => {
it('Should be a TodoApp', () => {
const {component} = setup();
expect(component.name()).to.equal('Header')
})
})
});
// src/todomvc/components/TodoApp.js
'use strict';
import React, {Component} from 'react'
import {bindActionCreators} from 'redux'
import {connect} from 'react-redux'
import Header from '../components/Header'
import * as actions from '../actions'
class TodoApp extends Component {
render() {
const props = this.props;
return (
<Header actions={props.actions}/>
)
}
}
function mapStateToProps(state) {
return {
todos: state.todos
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
TodoMVC actions
✓ Should create an action to add a todo
Header component
Should render correctly
✓ Should be a Header
✓ Should have a title
✓ Should have a TodoTextInput field
Should behave correctly
✓ Should call addTodo() if length of text is greater than 0
TodoApp component
Should render correctly
✓ Should be a TodoApp
TodoTextInput component
✓ Should not call onSave() on blur if isNew
Should render correctly
✓ Should be a TodoTextInput component
Should behave correctly
✓ Should update value on change
✓ Should call onSave() on return key press
✓ Should reset state on return key press if isNew
✓ Should call onSave() on blur if not isNew
TodoMVC reducer
✓ Should handle initial state
✓ Should handle ADD todo
14 passing (60ms)
$ git add .
$ git commit -m 'have a header'
diff --git a/src/containers/Root.dev.js b/src/containers/Root.dev.js
index 9660277..7106be3 100644
--- a/src/containers/Root.dev.js
+++ b/src/containers/Root.dev.js
@@ -2,7 +2,7 @@
import React, {Component} from 'react'
import {Provider} from 'react-redux'
-import {CounterApp} from '../counter'
+import {TodoApp} from '../todomvc'
import PropTypes from '../PropTypes'
import DevTools from './DevTools'
@@ -12,7 +12,7 @@ class Root extends Component {
return (
<Provider store={store}>
<div>
- <CounterApp/>
+ <TodoApp/>
<DevTools/>
</div>
</Provider>
(END)
diff --git a/src/containers/Root.prod.js b/src/containers/Root.prod.js
index 057f44a..27d8458 100644
--- a/src/containers/Root.prod.js
+++ b/src/containers/Root.prod.js
@@ -2,7 +2,7 @@
import React, {Component} from 'react'
import {Provider} from 'react-redux'
-import {CounterApp} from '../counter'
+import {TodoApp} from '../todomvc'
import PropTypes from '../PropTypes'
class Root extends Component {
@@ -10,7 +10,7 @@ class Root extends Component {
const {store} = this.props;
return (
<Provider store={store}>
- <CounterApp/>
+ <TodoApp/>
</Provider>
)
}
(END)