├── .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 |
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 |
--------------------------------------------------------------------------------