├── .gitignore ├── app ├── add-todo.js ├── index.js ├── state-functions.js ├── todo.js └── todos.js ├── index.html ├── package.json ├── test ├── add-todo-test.js ├── babel-register-setup.js ├── setup.js ├── state-functions-test.js ├── todo-test.js └── todos-test.js ├── todo-screen.png └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | article.md 3 | bundle.js 4 | -------------------------------------------------------------------------------- /app/add-todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class AddTodo extends React.Component { 4 | addTodo(e) { 5 | e.preventDefault(); 6 | const newTodoName = this.refs.todoTitle.value; 7 | if (newTodoName) { 8 | this.props.onNewTodo({ 9 | name: newTodoName 10 | }); 11 | 12 | this.refs.todoTitle.value = ''; 13 | } 14 | } 15 | render() { 16 | return ( 17 |
18 | 19 | 22 |
23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import Todos from './todos'; 5 | 6 | class AppComponent extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | 16 | render( 17 | , 18 | document.getElementById('app') 19 | ); 20 | -------------------------------------------------------------------------------- /app/state-functions.js: -------------------------------------------------------------------------------- 1 | export function toggleDone(state, id) { 2 | const todos = state.todos.map((todo) => { 3 | if (todo.id === id) { 4 | todo.done = !todo.done; 5 | } 6 | 7 | return todo; 8 | }); 9 | 10 | return { todos }; 11 | } 12 | 13 | export function addTodo(state, todo) { 14 | const lastTodo = state.todos[state.todos.length - 1]; 15 | todo.id = lastTodo.id + 1; 16 | todo.done = false; 17 | 18 | return { 19 | todos: state.todos.concat([todo]) 20 | }; 21 | } 22 | 23 | export function deleteTodo(state, id) { 24 | return { 25 | todos: state.todos.filter((todo) => todo.id !== id) 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /app/todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Todo extends React.Component { 4 | toggleDone() { 5 | this.props.doneChange(this.props.todo.id); 6 | } 7 | 8 | deleteTodo(e) { 9 | e.preventDefault(); 10 | this.props.deleteTodo(this.props.todo.id); 11 | } 12 | 13 | render() { 14 | const { todo } = this.props; 15 | 16 | const className = todo.done ? 'done-todo' : ''; 17 | 18 | return ( 19 |
20 |

this.toggleDone() }>{ todo.name }

21 | this.deleteTodo(e) }>Delete 22 |
23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/todos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Todo from './todo'; 3 | import AddTodo from './add-todo'; 4 | 5 | import { 6 | toggleDone, 7 | addTodo, 8 | deleteTodo 9 | } from './state-functions'; 10 | 11 | export default class Todos extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | todos: [ 16 | { id: 1, name: 'Write the blog post', done: false }, 17 | { id: 2, name: 'Buy Christmas presents', done: false }, 18 | { id: 3, name: 'Leave Santa his mince pies', done: false }, 19 | ] 20 | } 21 | } 22 | 23 | toggleDone(id) { 24 | this.setState(toggleDone(this.state, id)); 25 | } 26 | 27 | addTodo(todo) { 28 | this.setState(addTodo(this.state, todo)); 29 | } 30 | 31 | deleteTodo(id) { 32 | this.setState(deleteTodo(this.state, id)); 33 | } 34 | 35 | renderTodos() { 36 | return this.state.todos.map((todo) => { 37 | return ( 38 |
  • 39 | this.toggleDone(id)} 42 | deleteTodo={(id) => this.deleteTodo(id)} /> 43 |
  • 44 | ); 45 | }); 46 | } 47 | 48 | render() { 49 | return ( 50 |
    51 |

    The best todo app out there.

    52 |

    Things to get done:

    53 | 54 | this.addTodo(todo)} /> 55 |
    56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Testing 5 | 66 | 67 | 68 |
    69 |
    70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "webpack -w", 8 | "start": "live-server --port=8081", 9 | "test": "tape -r ./test/babel-register-setup test/*-test.js | faucet", 10 | "browser-test": "browserify test/*-test.js -t [ babelify --presets [ es2015 react ] ] -u 'react/lib/ReactContext' -u 'react/lib/ExecutionEnvironment' | tape-run -b chrome" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "react": "^0.14.3", 17 | "react-dom": "^0.14.3" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.3.17", 21 | "babel-loader": "^6.2.0", 22 | "babel-preset-es2015": "^6.3.13", 23 | "babel-preset-node5": "^11.0.1", 24 | "babel-preset-react": "^6.3.13", 25 | "babel-register": "^6.7.2", 26 | "babelify": "^7.2.0", 27 | "browser-run": "^3.0.5", 28 | "browserify": "^12.0.1", 29 | "doubler": "^0.6.0", 30 | "enzyme": "^1.2.0", 31 | "faucet": "0.0.1", 32 | "jsdom": "^7.2.1", 33 | "live-server": "^0.9.0", 34 | "phantomjs": "^1.9.19", 35 | "react-addons-test-utils": "^0.14.5", 36 | "tape": "^4.2.2", 37 | "tape-run": "^2.1.0", 38 | "testling": "^1.7.1", 39 | "webpack": "^1.12.9" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/add-todo-test.js: -------------------------------------------------------------------------------- 1 | import './setup'; 2 | 3 | import React from 'react'; 4 | import AddTodo from '../app/add-todo'; 5 | 6 | import { mount } from 'enzyme'; 7 | import { Double } from 'doubler'; 8 | 9 | import test from 'tape'; 10 | 11 | test('Add Todo component', (t) => { 12 | t.test('it calls the given callback prop with the new text', (t) => { 13 | t.plan(2); 14 | 15 | const todoCallback = Double.function(); 16 | 17 | const form = mount(); 18 | 19 | const input = form.find('input').get(0); 20 | input.value = 'Buy Milk'; 21 | 22 | form.find('button').simulate('click'); 23 | t.equal(todoCallback.callCount, 1); 24 | t.deepEqual(todoCallback.args[0][0], { name: 'Buy Milk' }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/babel-register-setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')({ 2 | presets: ['node5', 'react'] 3 | }); 4 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | 3 | function setupDom() { 4 | if (typeof document === 'undefined') { 5 | global.document = jsdom.jsdom(''); 6 | global.window = document.defaultView; 7 | global.navigator = window.navigator; 8 | } 9 | } 10 | 11 | setupDom(); 12 | 13 | -------------------------------------------------------------------------------- /test/state-functions-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | addTodo, 3 | toggleDone, 4 | deleteTodo 5 | } from '../app/state-functions'; 6 | 7 | import test from 'tape'; 8 | 9 | test('toggleDone', (t) => { 10 | t.test('with an incomplete todo it updates done to true', (t) => { 11 | t.plan(1); 12 | 13 | const initialState = { 14 | todos: [{ id: 1, name: 'Buy Milk', done: false }] 15 | }; 16 | 17 | const newState = toggleDone(initialState, 1); 18 | 19 | t.ok(newState.todos[0].done); 20 | }); 21 | 22 | t.test('with a complete todo it updates done to false', (t) => { 23 | t.plan(1); 24 | 25 | const initialState = { 26 | todos: [{ id: 1, name: 'Buy Milk', done: true }] 27 | }; 28 | 29 | const newState = toggleDone(initialState, 1); 30 | 31 | t.equal(newState.todos[0].done, false); 32 | }); 33 | }); 34 | 35 | test('addTodo', (t) => { 36 | t.test('it can add a new todo and set the right id', (t) => { 37 | t.plan(1); 38 | 39 | const initialState = { 40 | todos: [{ id: 1, name: 'Buy Milk', done: true }] 41 | }; 42 | 43 | const newState = addTodo(initialState, { name: 'Get bread' }); 44 | 45 | t.deepEqual(newState.todos[1], { 46 | name: 'Get bread', 47 | id: 2, 48 | done: false 49 | }); 50 | }); 51 | }); 52 | 53 | test('deleteTodo', (t) => { 54 | t.test('it deletes the todo matching the given ID', (t) => { 55 | t.plan(1); 56 | 57 | const initialState = { 58 | todos: [ 59 | { id: 1, name: 'Buy Milk', done: true }, 60 | { id: 2, name: 'Get bread', done: true }, 61 | ] 62 | }; 63 | 64 | const newState = deleteTodo(initialState, 1); 65 | 66 | t.deepEqual(newState.todos, [{ 67 | name: 'Get bread', 68 | id: 2, 69 | done: true 70 | }]); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/todo-test.js: -------------------------------------------------------------------------------- 1 | import './setup'; 2 | 3 | import { shallow, mount } from 'enzyme'; 4 | 5 | import React from 'react'; 6 | import { Double } from 'doubler'; 7 | import Todo from '../app/todo'; 8 | 9 | import test from 'tape'; 10 | 11 | function shallowRenderTodo(todo) { 12 | return shallow(); 13 | } 14 | 15 | test('Todo component', (t) => { 16 | t.test('rendering a not-done tweet', (t) => { 17 | const todo = { id: 1, name: 'Buy Milk', done: false }; 18 | const result = shallowRenderTodo(todo); 19 | 20 | t.test('It renders the text of the todo', (t) => { 21 | t.plan(1); 22 | t.equal(result.find('p').text(), 'Buy Milk'); 23 | }); 24 | 25 | t.test('The todo does not have the done class', (t) => { 26 | t.plan(1); 27 | t.equal(result.hasClass('done-todo'), false); 28 | }); 29 | }); 30 | 31 | t.test('rendering a done tweet', (t) => { 32 | const todo = { id: 1, name: 'Buy Milk', done: true }; 33 | const result = shallowRenderTodo(todo); 34 | 35 | t.test('The todo does have the done class', (t) => { 36 | t.plan(1); 37 | t.ok(result.hasClass('done-todo')); 38 | }); 39 | }); 40 | 41 | t.test('toggling a TODO calls the given prop', (t) => { 42 | t.plan(2); 43 | 44 | const doneCallback = Double.function(); 45 | const todo = { id: 1, name: 'Buy Milk', done: false }; 46 | 47 | const result = mount( 48 | 49 | ); 50 | 51 | result.find('p').simulate('click'); 52 | 53 | t.equal(doneCallback.callCount, 1); 54 | t.deepEqual(doneCallback.args[0], [1]); 55 | }); 56 | 57 | t.test('deleting a TODO calls the given prop', (t) => { 58 | t.plan(1); 59 | const deleteCallback = (id) => t.equal(id, 1); 60 | const todo = { id: 1, name: 'Buy Milk', done: false }; 61 | 62 | const result = mount( 63 | 64 | ); 65 | 66 | result.find('a').simulate('click'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/todos-test.js: -------------------------------------------------------------------------------- 1 | import './setup'; 2 | 3 | import { shallow, mount } from 'enzyme'; 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import Todos from '../app/todos'; 8 | import Todo from '../app/todo'; 9 | 10 | import test from 'tape'; 11 | 12 | 13 | test('Todos component', (t) => { 14 | t.test('it renders a list of todos', (t) => { 15 | t.plan(1); 16 | const result = shallow(); 17 | t.equal(result.find(Todo).length, 3); 18 | }); 19 | 20 | t.test('Marking a todo as done', (t) => { 21 | t.plan(1); 22 | 23 | const result = mount(); 24 | const firstToggle = result.find('.toggle-todo').at(0); 25 | firstToggle.simulate('click'); 26 | 27 | const firstTodo = result.find('.todo-1').at(0); 28 | t.ok(firstTodo.hasClass('done-todo')); 29 | }); 30 | 31 | t.test('Deleting a todo', (t) => { 32 | t.plan(1); 33 | const result = mount(); 34 | const firstDelete = result.find('.delete-todo').at(0); 35 | firstDelete.simulate('click'); 36 | 37 | t.equal(result.find('.todo').length, 2); 38 | }); 39 | 40 | t.test('Adding a todo', (t) => { 41 | t.plan(1); 42 | const result = mount(); 43 | const formInput = result.find('input').get(0); 44 | formInput.value = 'Buy Milk'; 45 | result.find('button').simulate('click'); 46 | t.equal(result.find('.todo').length, 4); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /todo-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackfranklin/todo-react-testing/e1c4d36651c70ea80405f0f7ae62092b549d37ac/todo-screen.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './app/index.js', 3 | output: { 4 | filename: 'bundle.js' 5 | }, 6 | module: { 7 | loaders: [{ 8 | test: /\.js$/, 9 | exclude: /node_modules/, 10 | loader: 'babel', 11 | query: { 12 | presets: ['react', 'es2015'] 13 | } 14 | }] 15 | } 16 | } 17 | --------------------------------------------------------------------------------