Add a Todo

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'

import todomvc from '../../src/todomvc'

describe('TodoMVC actions', () => {
  it('Should create an action to add a todo', () => {
    const description = 'My todo';
    const expectedAction = {
      type: 'todomvc/ADD', description: description, completed: false
    };

    expect(todomvc.actions.addTodo(description)).to.deep.equal(expectedAction)
  })
});

Run the test and watch it fail.

module.js:487
    throw err;
    ^

Error: Cannot find module '../../src/todomvc'

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.

// src/todomvc/actions.js

'use strict';

import * as types from './ActionTypes'

export function addTodo(text) {
  return {type: types.ADD, description: text, completed: false}
}

We will also need to a place to define our action type constants:

// src/todomvc/ActionTypes.js

'use strict';

export const ADD = 'todomvc/ADD';

Finally, we will need to create an index file to export modules from our application:

// src/todomvc/index.js

'use strict';

import * as types from './ActionTypes'
import * as actions from './actions'

export default {types, actions}

Run the test and watch it pass.

  TodoMVC actions
    ✓ Should create an action to add a todo

  1 passing (21ms)

Commit changes.

$ git add .
$ git commit -m 'created an action to add a todo'

TodoMVC reducer

It should handle initial state

Write the test.

// 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([]))
  })
});

Run the test and watch it fail.

  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)

Write the code to make the test pass.

// 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)
  }
}

And export the new reducer from the application.

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 }

Run the test and watch it pass.

  TodoMVC actions
    ✓ Should create an action to add a todo

  TodoMVC reducer
    ✓ Should handle initial state

  2 passing (24ms)

Commit changes.

$ git add .
$ git commit -m 'handle initial state'

It should handle ADD todo

Write the test.

diff --git a/tests/todomvc/reducer.spec.js b/tests/todomvc/reducer.spec.js
index 0fc5a63..9788529 100644
--- a/tests/todomvc/reducer.spec.js
+++ b/tests/todomvc/reducer.spec.js
@@ -1,7 +1,7 @@
 'use strict';

 import {expect} from 'chai'
-import {fromJS} from 'immutable'
+import {fromJS, List} from 'immutable'

 import todomvc from '../../src/todomvc'

@@ -10,5 +10,17 @@ describe('TodoMVC reducer', () => {
     const state = todomvc.reducer(undefined, {});

     expect(state).to.equal(fromJS([]))
+  });
+
+  it('Should handle ADD todo', () => {
+    const state = List([]);
+    const description = 'My todo';
+    const new_state = todomvc.reducer(state, {
+      type: todomvc.types.ADD, description: description, completed: false
+    });
+
+    expect(new_state.first().filterNot((v, k) => k === 'id')).to.equal(fromJS({ // skip uuid id field
+      description: description, completed: false
+    }))
   })
 });
(END)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

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)

Run the test and watch it pass.

  TodoMVC actions
    ✓ Should create an action to add a todo

  TodoMVC reducer
    ✓ Should handle initial state
    ✓ Should handle ADD todo


  3 passing (25ms)

Commit changes.

$ git add .
$ git commit -m 'handle add todo'

TodoTextInput component

It should render correctly

It should be a TodoTextInput component

Write the test.

// tests/todomvc/components/TodoTextInput.spec.js

'use strict';

import React from 'react'
import {expect} from 'chai'
import {shallow} from 'enzyme'

import TodoTextInput from '../../../src/todomvc/components/TodoTextInput'

function setup() {
  const props = {
    text: 'my todo',
    placeholder: 'do it',
  };
  const component = shallow(
    <TodoTextInput {...props} />
  );

  return {
    props: props,
    component: component
  }
}

describe('TodoTextInput component', () => {
  describe('Should render correctly', () => {
    it('Should be a TodoTextInput component', () => {
      const {props, component} = setup();

      expect(component.find('input')).to.have.length(1);
      expect(component.find('input').at(0).prop('placeholder')).to.equal(props.placeholder);
      expect(component.find('input').at(0).prop('value')).to.equal(props.text)
    })
  })
});

Run the test and watch it fail.

module.js:487
    throw err;
    ^

Error: Cannot find module '../../../src/todomvc/components/TodoTextInput'

Write the code to make the test pass.

// src/todomvc/components/TodoTextInput.js

'use strict';

import React, {Component} from 'react'
import PropTypes from 'prop-types'

export default class TodoTextInput extends Component {
  static propTypes = {
    text: PropTypes.string,
    placeholder: PropTypes.string
  };

  constructor(props, context) {
    super(props, context);
    this.state = {
      text: this.props.text || ''
    }
  }

  render() {
    return (
      <input type="text" placeholder={this.props.placeholder} value={this.state.text}/>
    )
  }
}

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'TodoTextInput component'

Should behave correctly

It should update value on change

Write the test.

diff --git a/tests/todomvc/components/TodoTextInput.spec.js b/tests/todomvc/components/TodoTextInput.spec.js
index 27cfb0c..fc4103a 100644
--- a/tests/todomvc/components/TodoTextInput.spec.js
+++ b/tests/todomvc/components/TodoTextInput.spec.js
@@ -30,5 +30,14 @@ describe('TodoTextInput component', () => {
       expect(component.find('input').at(0).prop('placeholder')).to.equal(props.placeholder);
       expect(component.find('input').at(0).prop('value')).to.equal(props.text)
     })
+  });
+
+  describe('Should behave correctly', () => {
+    it('Should update value on change', () => {
+      const {component} = setup();
+
+      component.find('input').at(0).simulate('change', {target: {value: 'todo'}});
+      expect(component.find('input').at(0).prop('value')).to.equal('todo')
+    })
   })
 });
(END)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

diff --git a/src/todomvc/components/TodoTextInput.js b/src/todomvc/components/TodoTextInput.js
index 52548ad..44413b8 100644
--- a/src/todomvc/components/TodoTextInput.js
+++ b/src/todomvc/components/TodoTextInput.js
@@ -16,9 +16,14 @@ export default class TodoTextInput extends Component {
     }
   }

+  handleChange(e) {
+    this.setState({text: e.target.value})
+  }
+
   render() {
     return (
-      <input type="text" placeholder={this.props.placeholder} value={this.state.text}/>
+      <input type="text" placeholder={this.props.placeholder} value={this.state.text}
+             onChange={this.handleChange.bind(this)}/>
     )
   }
 }
(END)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'update value on change'

It should call onSave() on return key press

Write the test. We need to import sinon to spy on the onSave

diff --git a/tests/todomvc/components/TodoTextInput.spec.js b/tests/todomvc/components/TodoTextInput.spec.js
index fc4103a..76a8efe 100644
--- a/tests/todomvc/components/TodoTextInput.spec.js
+++ b/tests/todomvc/components/TodoTextInput.spec.js
@@ -3,6 +3,7 @@
 import React from 'react'
 import {expect} from 'chai'
 import {shallow} from 'enzyme'
+import sinon from 'sinon'

 import TodoTextInput from '../../../src/todomvc/components/TodoTextInput'

@@ -10,6 +11,7 @@ function setup() {
   const props = {
     text: 'my todo',
     placeholder: 'do it',
+    onSave: sinon.spy(),
   };
   const component = shallow(
     <TodoTextInput {...props} />
@@ -38,6 +40,17 @@ describe('TodoTextInput component', () => {

       component.find('input').at(0).simulate('change', {target: {value: 'todo'}});
       expect(component.find('input').at(0).prop('value')).to.equal('todo')
+    });
+
+    it('Should call onSave() on return key press', () => {
+      const {props, component} = setup();
+
+      component.find('input').at(0).simulate('keydown', {
+        which: 13, // RETURN KEY
+        target: {value: 'new todo'}
+      });
+      expect(props.onSave.called).to.be.true;
+      expect(props.onSave.args[0][0]).to.equal('new todo')
     })
   })
 });
(END)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

diff --git a/src/todomvc/components/TodoTextInput.js b/src/todomvc/components/TodoTextInput.js
index 44413b8..c2992bb 100644
--- a/src/todomvc/components/TodoTextInput.js
+++ b/src/todomvc/components/TodoTextInput.js
@@ -6,7 +6,8 @@ import PropTypes from 'prop-types'
 export default class TodoTextInput extends Component {
   static propTypes = {
     text: PropTypes.string,
-    placeholder: PropTypes.string
+    placeholder: PropTypes.string,
+    onSave: PropTypes.func.isRequired
   };

   constructor(props, context) {
@@ -20,10 +21,17 @@ export default class TodoTextInput extends Component {
     this.setState({text: e.target.value})
   }

+  handleSubmit(e) {
+    const text = e.target.value.trim();
+    if (e.which === 13) {
+      this.props.onSave(text)
+    }
+  }
+
   render() {
     return (
       <input type="text" placeholder={this.props.placeholder} value={this.state.text}
-             onChange={this.handleChange.bind(this)}/>
+             onChange={this.handleChange.bind(this)} onKeyDown={this.handleSubmit.bind(this)}/>
     )
   }
 }
(END)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'call onSave() on return key press'

It should reset state on return key press if isNew

Write the test.

diff --git a/tests/todomvc/components/TodoTextInput.spec.js b/tests/todomvc/components/TodoTextInput.spec.js
index 76a8efe..a250270 100644
--- a/tests/todomvc/components/TodoTextInput.spec.js
+++ b/tests/todomvc/components/TodoTextInput.spec.js
@@ -7,12 +7,13 @@ import sinon from 'sinon'

 import TodoTextInput from '../../../src/todomvc/components/TodoTextInput'

-function setup() {
-  const props = {
+function setup(propOverrides) {
+  const props = Object.assign({
     text: 'my todo',
     placeholder: 'do it',
     onSave: sinon.spy(),
-  };
+    isNew: false
+  }, propOverrides);
   const component = shallow(
     <TodoTextInput {...props} />
   );
@@ -51,6 +52,16 @@ describe('TodoTextInput component', () => {
       });
       expect(props.onSave.called).to.be.true
       expect(props.onSave.args[0][0]).to.equal('new todo')
+    });
+
+    it('Should reset state on return key press if isNew', () => {
+      const {component} = setup({isNew: true});
+
+      component.find('input').at(0).simulate('keydown', {
+        which: 13, // RETURN KEY
+        target: {value: 'new todo'}
+      });
+      expect(component.find('input').at(0).prop('value')).to.equal('')
     })
   })
 });
(END)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

diff --git a/src/todomvc/components/TodoTextInput.js b/src/todomvc/components/TodoTextInput.js
index 84481fd..671ac80 100644
--- a/src/todomvc/components/TodoTextInput.js
+++ b/src/todomvc/components/TodoTextInput.js
@@ -7,7 +7,8 @@ export default class TodoTextInput extends Component {
   static propTypes = {
     text: PropTypes.string,
     placeholder: PropTypes.string,
-    onSave: PropTypes.func.isRequired
+    onSave: PropTypes.func.isRequired,
+    isNew: PropTypes.bool
   };

   constructor(props, context) {
@@ -24,7 +25,10 @@ export default class TodoTextInput extends Component {
   handleSubmit(e) {
     const text = e.target.value.trim();
     if (e.which === 13) {
-      this.props.onSave(text)
+      this.props.onSave(text);
+      if (this.props.isNew) {
+        this.setState({text: ''})
+      }
     }
   }

(END)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'reset state on return key press if isNew'

It should call onSave() on blur if not isNew

Write the test.

diff --git a/tests/todomvc/components/TodoTextInput.spec.js b/tests/todomvc/components/TodoTextInput.spec.js
index 03ff17d..21c78e0 100644
--- a/tests/todomvc/components/TodoTextInput.spec.js
+++ b/tests/todomvc/components/TodoTextInput.spec.js
@@ -62,6 +62,14 @@ describe('TodoTextInput component', () => {
         target: {value: 'new todo'}
       });
       expect(component.find('input').at(0).prop('value')).to.equal('')
+    });
+
+    it('Should call onSave() on blur if not isNew', () => {
+      const {props, component} = setup();
+
+      component.find('input').at(0).simulate('blur', {target: {value: 'new todo'}});
+      expect(props.onSave.called).to.be.true;
+      expect(props.onSave.args[0][0]).to.equal('new todo')
     })
   })
 });
(END)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

diff --git a/src/todomvc/components/TodoTextInput.js b/src/todomvc/components/TodoTextInput.js
index 671ac80..15bfbbb 100644
--- a/src/todomvc/components/TodoTextInput.js
+++ b/src/todomvc/components/TodoTextInput.js
@@ -32,10 +32,15 @@ export default class TodoTextInput extends Component {
     }
   }

+  handleBlur(e) {
+    this.props.onSave(e.target.value)
+  }
+
   render() {
     return (
       <input type="text" placeholder={this.props.placeholder} value={this.state.text}
-             onChange={this.handleChange.bind(this)} onKeyDown={this.handleSubmit.bind(this)}/>
+             onChange={this.handleChange.bind(this)} onKeyDown={this.handleSubmit.bind(this)}
+             onBlur={this.handleBlur.bind(this)}/>
     )
   }
 }
(END)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'call onSave() on blur if not isNew'

It should not call onSave() on blur if isNew

Write the test.

diff --git a/tests/todomvc/components/TodoTextInput.spec.js b/tests/todomvc/components/TodoTextInput.spec.js
index de5c607..bbff953 100644
--- a/tests/todomvc/components/TodoTextInput.spec.js
+++ b/tests/todomvc/components/TodoTextInput.spec.js
@@ -70,6 +70,13 @@ describe('TodoTextInput component', () => {
       component.find('input').at(0).simulate('blur', {target: {value: 'new todo'}});
       expect(props.onSave.called).to.be.true;
       expect(props.onSave.args[0][0]).to.equal('new todo')
+    });
+
+    it('Should not call onSave() on blur if isNew', () => {
+      const {props, component} = setup({isNew: true});
+
+      component.find('input').at(0).simulate('blur', {target: {value: 'new todo'}});
+      expect(props.onSave.called).to.be.false;
     })
   })
 });
(END)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

diff --git a/src/todomvc/components/TodoTextInput.js b/src/todomvc/components/TodoTextInput.js
index 15bfbbb..329e70f 100644
--- a/src/todomvc/components/TodoTextInput.js
+++ b/src/todomvc/components/TodoTextInput.js
@@ -33,7 +33,9 @@ export default class TodoTextInput extends Component {
   }

   handleBlur(e) {
-    this.props.onSave(e.target.value)
+    if (!this.props.isNew) {
+      this.props.onSave(e.target.value)
+    }
   }

   render() {
(END)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'not call onSave() on blur if isNew'

Header component

Should render correctly

It should be a Header component

Write the test.

// 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')
    })
  })
});

Run the test and watch it fail.

module.js:487
    throw err;
    ^

Error: Cannot find module '../../../src/todomvc/components/Header'

Write the code to make the test pass.

// src/todomvc/components/Header.js

'use strict';

import React, {Component} from 'react'

export default class Header extends Component {
  render() {
    return (
      <header/>
    )
  }
}

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'Header component'

It should have a title

Write the test.

diff --git a/tests/todomvc/components/Header.spec.js b/tests/todomvc/components/Header.spec.js
index 71da345..b02f0a8 100644
--- a/tests/todomvc/components/Header.spec.js
+++ b/tests/todomvc/components/Header.spec.js
@@ -22,6 +22,14 @@ describe('Header component', () => {
       const {component} = setup();

       expect(component.type()).to.equal('header')
+    });
+
+    it('Should have a title', () => {
+      const {component} = setup();
+      const h1 = component.children('h1');
+
+      expect(h1.type()).to.equal('h1');
+      expect(h1.text()).to.equal('todos')
     })
   })
 });
(END)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

diff --git a/src/todomvc/components/Header.js b/src/todomvc/components/Header.js
index 350edd5..e3e6e11 100644
--- a/src/todomvc/components/Header.js
+++ b/src/todomvc/components/Header.js
@@ -5,7 +5,9 @@ import React, {Component} from 'react'
 export default class Header extends Component {
   render() {
     return (
-      <header/>
+      <header>
+        <h1>todos</h1>
+      </header>
     )
   }
 }
(END)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'have a title'

It should have a TodoTextInput field

Write the test.

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)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

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)

Run the test and watch it pass. Note the warning, below. We'll implement onSave in the next test.

  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)

Commit changes.

$ git add .
$ git commit -m 'have a TodoTextInput field'

Should behave correctly

It should call addTodo() if length of text is greater than 0

Write the test.

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)

Run the test and watch it fail.

  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)

Write the code to make the test pass.

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)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'call addTodo() if length of text is greater than 0'

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.

Should render correctly

It should have a Header

Write the test.

// 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')
    })
  })
});

Run the test and watch it fail.

module.js:487
    throw err;
    ^

Error: Cannot find module '../../../src/todomvc/components/TodoApp'

Write the code to make the test pass.

// 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)

Run the test and watch it pass.

  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)

Commit changes.

$ git add .
$ git commit -m 'have a header'

Tie everything together

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)
diff --git a/src/reducers.js b/src/reducers.js
index e441cb9..da9183f 100644
--- a/src/reducers.js
+++ b/src/reducers.js
@@ -1,10 +1,10 @@
 'use strict';

 import {combineReducers} from 'redux'
-import counter from './counter'
+import todomvc from './todomvc'

 const rootReducer = combineReducers({
-  counter: counter.reducer
+  todos: todomvc.reducer
 });

 export default rootReducer
(END)
diff --git a/src/todomvc/index.js b/src/todomvc/index.js
index 1da3cdb..b8e96eb 100644
--- a/src/todomvc/index.js
+++ b/src/todomvc/index.js
@@ -5,3 +5,4 @@ import * as actions from './actions'
 import reducer from './reducer'

 export default {types, actions, reducer}
+export { default as TodoApp } from './components/TodoApp'
(END)

Commit changes.

$ git add .
$ git commit -m 'tie it all together'

Run It!

Now let's see what this looks like.

Open http://localhost:4000 in your web browser.

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:

Last updated

Was this helpful?