diff --git a/examples/counter/actions/CounterActions.js b/examples/counter/actions/CounterActions.js
index 9ddaa8351d..032de23ecd 100644
--- a/examples/counter/actions/CounterActions.js
+++ b/examples/counter/actions/CounterActions.js
@@ -24,10 +24,10 @@ export function incrementIfOdd() {
};
}
-export function incrementAsync() {
+export function incrementAsync(delay=1000) {
return dispatch => {
setTimeout(() => {
dispatch(increment());
- }, 1000);
+ }, delay);
};
}
diff --git a/examples/counter/components/Counter.js b/examples/counter/components/Counter.js
index c58dc31626..fac93348c5 100644
--- a/examples/counter/components/Counter.js
+++ b/examples/counter/components/Counter.js
@@ -4,12 +4,13 @@ export default class Counter extends Component {
static propTypes = {
increment: PropTypes.func.isRequired,
incrementIfOdd: PropTypes.func.isRequired,
+ incrementAsync: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
counter: PropTypes.number.isRequired
};
render() {
- const { increment, incrementIfOdd, decrement, counter } = this.props;
+ const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props;
return (
Clicked: {counter} times
@@ -19,6 +20,8 @@ export default class Counter extends Component {
{' '}
+ {' '}
+
);
}
diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js
index 970195d202..6a56741563 100644
--- a/examples/counter/containers/App.js
+++ b/examples/counter/containers/App.js
@@ -1,13 +1,9 @@
import React, { Component } from 'react';
-import CounterApp from './CounterApp';
-import { createStore, applyMiddleware, combineReducers } from 'redux';
-import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
-import * as reducers from '../reducers';
+import CounterApp from './CounterApp';
+import createCounterStore from '../store/createCounterStore';
-const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
-const reducer = combineReducers(reducers);
-const store = createStoreWithMiddleware(reducer);
+const store = createCounterStore();
export default class App extends Component {
render() {
diff --git a/examples/counter/package.json b/examples/counter/package.json
index 6e7e689762..dff02d9744 100644
--- a/examples/counter/package.json
+++ b/examples/counter/package.json
@@ -4,7 +4,8 @@
"description": "Counter example for redux",
"main": "server.js",
"scripts": {
- "start": "node server.js"
+ "start": "node server.js",
+ "test": "mocha --recursive --compilers js:babel/register"
},
"repository": {
"type": "git",
@@ -35,6 +36,10 @@
"devDependencies": {
"babel-core": "^5.6.18",
"babel-loader": "^5.1.4",
+ "expect": "^1.6.0",
+ "jsdom": "^5.6.1",
+ "mocha": "^2.2.5",
+ "mocha-jsdom": "^1.0.0",
"node-libs-browser": "^0.5.2",
"react-hot-loader": "^1.2.7",
"webpack": "^1.9.11",
diff --git a/examples/counter/store/createCounterStore.js b/examples/counter/store/createCounterStore.js
new file mode 100644
index 0000000000..f86abee36e
--- /dev/null
+++ b/examples/counter/store/createCounterStore.js
@@ -0,0 +1,10 @@
+import { createStore, applyMiddleware, combineReducers } from 'redux';
+import thunk from 'redux-thunk';
+import * as reducers from '../reducers';
+
+const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
+const reducer = combineReducers(reducers);
+
+export default function createCounterStore(initialState) {
+ return createStoreWithMiddleware(reducer, initialState);
+}
diff --git a/examples/counter/test/actions/CounterActions.spec.js b/examples/counter/test/actions/CounterActions.spec.js
new file mode 100644
index 0000000000..0fe06d12cb
--- /dev/null
+++ b/examples/counter/test/actions/CounterActions.spec.js
@@ -0,0 +1,44 @@
+import expect from 'expect';
+import * as actions from '../../actions/CounterActions';
+import * as types from '../../constants/ActionTypes';
+
+describe('actions', () => {
+
+ it('increment should create increment action', () => {
+ expect(actions.increment()).toEqual({ type: types.INCREMENT_COUNTER });
+ });
+
+ it('decrement should create decrement action', () => {
+ expect(actions.decrement()).toEqual({ type: types.DECREMENT_COUNTER });
+ });
+
+ it('incrementIfOdd should create increment action', () => {
+ let fn = actions.incrementIfOdd();
+ expect(fn).toBeA('function');
+ let dispatch = expect.createSpy();
+ let getState = () => ({ counter: 1 });
+ fn(dispatch, getState);
+ expect(dispatch).toHaveBeenCalledWith({ type: types.INCREMENT_COUNTER });
+ });
+
+ it('incrementIfOdd shouldnt create increment action if counter is even', () => {
+ let fn = actions.incrementIfOdd();
+ let dispatch = expect.createSpy();
+ let getState = () => ({ counter: 2 });
+ fn(dispatch, getState);
+ expect(dispatch.calls.length).toBe(0);
+ });
+
+ // There's no nice way to test this at the moment...
+ it('incrementAsync', (done) => {
+ let fn = actions.incrementAsync(1);
+ expect(fn).toBeA('function');
+ let dispatch = expect.createSpy();
+ fn(dispatch);
+ setTimeout(() => {
+ expect(dispatch).toHaveBeenCalledWith({ type: types.INCREMENT_COUNTER });
+ done();
+ }, 5);
+ });
+});
+
diff --git a/examples/counter/test/components/Counter.spec.js b/examples/counter/test/components/Counter.spec.js
new file mode 100644
index 0000000000..66f985b28f
--- /dev/null
+++ b/examples/counter/test/components/Counter.spec.js
@@ -0,0 +1,57 @@
+import expect from 'expect';
+import jsdomReact from '../jsdomReact';
+import React from 'react/addons';
+import Counter from '../../components/Counter';
+
+const { TestUtils } = React.addons;
+
+function setup() {
+ const actions = {
+ increment: expect.createSpy(),
+ incrementIfOdd: expect.createSpy(),
+ incrementAsync: expect.createSpy(),
+ decrement: expect.createSpy()
+ };
+ const component = TestUtils.renderIntoDocument();
+ return {
+ component: component,
+ actions: actions,
+ buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button').map(button => {
+ return button.getDOMNode();
+ }),
+ p: TestUtils.findRenderedDOMComponentWithTag(component, 'p').getDOMNode()
+ };
+}
+
+describe('Counter component', () => {
+ jsdomReact();
+
+ it('should display count', () => {
+ const { p } = setup();
+ expect(p.textContent).toMatch(/^Clicked: 1 times/);
+ });
+
+ it('first button should call increment', () => {
+ const { buttons, actions } = setup();
+ TestUtils.Simulate.click(buttons[0]);
+ expect(actions.increment).toHaveBeenCalled();
+ });
+
+ it('second button should call decrement', () => {
+ const { buttons, actions } = setup();
+ TestUtils.Simulate.click(buttons[1]);
+ expect(actions.decrement).toHaveBeenCalled();
+ });
+
+ it('third button should call incrementIfOdd', () => {
+ const { buttons, actions } = setup();
+ TestUtils.Simulate.click(buttons[2]);
+ expect(actions.incrementIfOdd).toHaveBeenCalled();
+ });
+
+ it('fourth button should call incrementAsync', () => {
+ const { buttons, actions } = setup();
+ TestUtils.Simulate.click(buttons[3]);
+ expect(actions.incrementAsync).toHaveBeenCalled();
+ });
+});
diff --git a/examples/counter/test/containers/CounterApp.spec.js b/examples/counter/test/containers/CounterApp.spec.js
new file mode 100644
index 0000000000..9c0c8344d7
--- /dev/null
+++ b/examples/counter/test/containers/CounterApp.spec.js
@@ -0,0 +1,60 @@
+import expect from 'expect';
+import jsdomReact from '../jsdomReact';
+import React from 'react/addons';
+import { Provider } from 'react-redux';
+import CounterApp from '../../containers/CounterApp';
+import createCounterStore from '../../store/createCounterStore';
+
+const { TestUtils } = React.addons;
+
+function setup(initialState) {
+ const store = createCounterStore(initialState);
+ const app = TestUtils.renderIntoDocument(
+
+ {() => }
+
+ );
+ return {
+ app: app,
+ buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button').map(button => {
+ return button.getDOMNode();
+ }),
+ p: TestUtils.findRenderedDOMComponentWithTag(app, 'p').getDOMNode()
+ };
+}
+
+describe('containers', () => {
+ jsdomReact();
+
+ describe('App', () => {
+
+ it('should display initial count', () => {
+ const { p } = setup();
+ expect(p.textContent).toMatch(/^Clicked: 0 times/);
+ });
+
+ it('should display updated count after increment button click', () => {
+ const { buttons, p } = setup();
+ TestUtils.Simulate.click(buttons[0]);
+ expect(p.textContent).toMatch(/^Clicked: 1 times/);
+ });
+
+ it('should display updated count after descrement button click', () => {
+ const { buttons, p } = setup();
+ TestUtils.Simulate.click(buttons[1]);
+ expect(p.textContent).toMatch(/^Clicked: -1 times/);
+ });
+
+ it('shouldnt change if even and if odd button clicked', () => {
+ const { buttons, p } = setup();
+ TestUtils.Simulate.click(buttons[2]);
+ expect(p.textContent).toMatch(/^Clicked: 0 times/);
+ });
+
+ it('should change if odd and if odd button clicked', () => {
+ const { buttons, p } = setup({ counter: 1 });
+ TestUtils.Simulate.click(buttons[2]);
+ expect(p.textContent).toMatch(/^Clicked: 2 times/);
+ });
+ });
+});
diff --git a/examples/counter/test/jsdomReact.js b/examples/counter/test/jsdomReact.js
new file mode 100644
index 0000000000..0083824baf
--- /dev/null
+++ b/examples/counter/test/jsdomReact.js
@@ -0,0 +1,7 @@
+import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';
+import jsdom from 'mocha-jsdom';
+
+export default function jsdomReact() {
+ jsdom();
+ ExecutionEnvironment.canUseDOM = true;
+}
diff --git a/examples/counter/test/reducers/counter.spec.js b/examples/counter/test/reducers/counter.spec.js
new file mode 100644
index 0000000000..af7ceeefe3
--- /dev/null
+++ b/examples/counter/test/reducers/counter.spec.js
@@ -0,0 +1,24 @@
+import expect from 'expect';
+import counter from '../../reducers/counter';
+import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../constants/ActionTypes';
+
+describe('reducers', () => {
+ describe('counter', () => {
+
+ it('should handle initial state', () => {
+ expect(counter(undefined, {})).toBe(0);
+ });
+
+ it('should handle INCREMENT_COUNTER', () => {
+ expect(counter(1, { type: INCREMENT_COUNTER })).toBe(2);
+ });
+
+ it('should handle DECREMENT_COUNTER', () => {
+ expect(counter(1, { type: DECREMENT_COUNTER })).toBe(0);
+ });
+
+ it('should handle unknown action type', () => {
+ expect(counter(1, { type: 'unknown' })).toBe(1);
+ });
+ });
+});
diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json
index e95fa73784..3c51e75f05 100644
--- a/examples/todomvc/package.json
+++ b/examples/todomvc/package.json
@@ -4,7 +4,8 @@
"description": "TodoMVC example for redux",
"main": "server.js",
"scripts": {
- "start": "node server.js"
+ "start": "node server.js",
+ "test": "mocha --recursive --compilers js:babel/register"
},
"repository": {
"type": "git",
@@ -36,6 +37,10 @@
"devDependencies": {
"babel-core": "^5.6.18",
"babel-loader": "^5.1.4",
+ "expect": "^1.8.0",
+ "jsdom": "^5.6.1",
+ "mocha": "^2.2.5",
+ "mocha-jsdom": "^1.0.0",
"node-libs-browser": "^0.5.2",
"raw-loader": "^0.5.1",
"react-hot-loader": "^1.2.7",
diff --git a/examples/todomvc/test/actions/TodoActions.spec.js b/examples/todomvc/test/actions/TodoActions.spec.js
new file mode 100644
index 0000000000..f899ed6ce3
--- /dev/null
+++ b/examples/todomvc/test/actions/TodoActions.spec.js
@@ -0,0 +1,48 @@
+import expect from 'expect';
+import * as types from '../../constants/ActionTypes';
+import * as actions from '../../actions/TodoActions';
+
+describe('todo actions', () => {
+
+ it('addTodo should create ADD_TODO action', () => {
+ expect(actions.addTodo('Use Redux')).toEqual({
+ type: types.ADD_TODO,
+ text: 'Use Redux'
+ });
+ });
+
+ it('deleteTodo should create DELETE_TODO action', () => {
+ expect(actions.deleteTodo(1)).toEqual({
+ type: types.DELETE_TODO,
+ id: 1
+ });
+ });
+
+ it('editTodo should create EDIT_TODO action', () => {
+ expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
+ type: types.EDIT_TODO,
+ id: 1,
+ text: 'Use Redux everywhere'
+ });
+ });
+
+ it('markTodo should create MARK_TODO action', () => {
+ expect(actions.markTodo(1)).toEqual({
+ type: types.MARK_TODO,
+ id: 1
+ });
+ });
+
+ it('markAll should create MARK_ALL action', () => {
+ expect(actions.markAll()).toEqual({
+ type: types.MARK_ALL
+ });
+ });
+
+ it('clearMarked should create CLEAR_MARKED action', () => {
+ expect(actions.clearMarked('Use Redux')).toEqual({
+ type: types.CLEAR_MARKED
+ });
+ });
+});
+
diff --git a/examples/todomvc/test/components/Footer.spec.js b/examples/todomvc/test/components/Footer.spec.js
new file mode 100644
index 0000000000..922abffd0e
--- /dev/null
+++ b/examples/todomvc/test/components/Footer.spec.js
@@ -0,0 +1,109 @@
+import expect from 'expect';
+import jsdomReact from '../jsdomReact';
+import React from 'react/addons';
+import Footer from '../../components/Footer';
+import { SHOW_ALL, SHOW_UNMARKED } from '../../constants/TodoFilters';
+
+const { TestUtils } = React.addons;
+
+function setup(propOverrides) {
+ let props = {
+ markedCount: 0,
+ unmarkedCount: 0,
+ filter: SHOW_ALL,
+ onClearMarked: expect.createSpy(),
+ onShow: expect.createSpy(),
+ ...propOverrides
+ };
+
+ let renderer = TestUtils.createRenderer();
+ renderer.render();
+ let output = renderer.getRenderOutput();
+
+ return {
+ props: props,
+ output: output
+ };
+}
+
+function getTextContent(elem) {
+ let children = Array.isArray(elem.props.children) ?
+ elem.props.children : [elem.props.children];
+
+ return children.reduce(function concatText(out, child) {
+ // Children are either elements or text strings
+ return out + (child.props ? getTextContent(child) : child);
+ }, '');
+}
+
+describe('components', () => {
+ jsdomReact();
+
+ describe('Footer', () => {
+
+ it('should render container', () => {
+ const { output } = setup();
+ expect(output.type).toBe('footer');
+ expect(output.props.className).toBe('footer');
+ });
+
+ it('should display unmarked count when 0', () => {
+ let { output } = setup({ unmarkedCount: 0 });
+ let [count] = output.props.children;
+ expect(getTextContent(count)).toBe('No items left');
+ });
+
+ it('should display unmarked count when above 0', () => {
+ let { output } = setup({ unmarkedCount: 1 });
+ let [count] = output.props.children;
+ expect(getTextContent(count)).toBe('1 item left');
+ });
+
+ it('should render filters', () => {
+ const { output } = setup();
+ let [, filters] = output.props.children;
+ expect(filters.type).toBe('ul');
+ expect(filters.props.className).toBe('filters');
+ expect(filters.props.children.length).toBe(3);
+ filters.props.children.forEach(function checkFilter(filter, i) {
+ expect(filter.type).toBe('li');
+ let a = filter.props.children;
+ expect(a.props.className).toBe(i === 0 ? 'selected' : '');
+ expect(a.props.children).toBe({
+ 0: 'All',
+ 1: 'Active',
+ 2: 'Completed'
+ }[i]);
+ });
+ });
+
+ it('should call onShow when a filter is clicked', () => {
+ const { output, props } = setup();
+ let [, filters] = output.props.children;
+ let filterLink = filters.props.children[1].props.children;
+ filterLink.props.onClick({});
+ expect(props.onShow).toHaveBeenCalledWith(SHOW_UNMARKED);
+ });
+
+ it('shouldnt show clear button when no marked todos', () => {
+ const { output } = setup({ markedCount: 0 });
+ let [,, clear] = output.props.children;
+ expect(clear).toBe(undefined);
+ });
+
+ it('should render clear button when marked todos', () => {
+ const { output } = setup({ markedCount: 1 });
+ let [,, clear] = output.props.children;
+ expect(clear.type).toBe('button');
+ expect(clear.props.children).toBe('Clear completed');
+ });
+
+ it('should call onClearMarked on clear button click', () => {
+ const { output, props } = setup({ markedCount: 1 });
+ let [,, clear] = output.props.children;
+ clear.props.onClick({});
+ expect(props.onClearMarked).toHaveBeenCalled();
+ });
+ });
+});
+
diff --git a/examples/todomvc/test/components/Header.spec.js b/examples/todomvc/test/components/Header.spec.js
new file mode 100644
index 0000000000..714ff4d51a
--- /dev/null
+++ b/examples/todomvc/test/components/Header.spec.js
@@ -0,0 +1,55 @@
+import expect from 'expect';
+import jsdomReact from '../jsdomReact';
+import React from 'react/addons';
+import Header from '../../components/Header';
+import TodoTextInput from '../../components/TodoTextInput';
+
+const { TestUtils } = React.addons;
+
+function setup() {
+ let props = {
+ addTodo: expect.createSpy()
+ };
+
+ let renderer = TestUtils.createRenderer();
+ renderer.render();
+ let output = renderer.getRenderOutput();
+
+ return {
+ props: props,
+ output: output,
+ renderer: renderer
+ };
+}
+
+describe('components', () => {
+ jsdomReact();
+
+ describe('Header', () => {
+
+ it('should render correctly', () => {
+ const { output } = setup();
+
+ expect(output.type).toBe('header');
+ expect(output.props.className).toBe('header');
+
+ let [h1, input] = output.props.children;
+
+ expect(h1.type).toBe('h1');
+ expect(h1.props.children).toBe('todos');
+
+ expect(input.type).toBe(TodoTextInput);
+ expect(input.props.newTodo).toBe(true);
+ expect(input.props.placeholder).toBe('What needs to be done?');
+ });
+
+ it('should call call addTodo if length of text is greater than 0', () => {
+ const { output, props } = setup();
+ let input = output.props.children[1];
+ input.props.onSave('');
+ expect(props.addTodo.calls.length).toBe(0);
+ input.props.onSave('Use Redux');
+ expect(props.addTodo.calls.length).toBe(1);
+ });
+ });
+});
diff --git a/examples/todomvc/test/components/MainSection.spec.js b/examples/todomvc/test/components/MainSection.spec.js
new file mode 100644
index 0000000000..f026d30550
--- /dev/null
+++ b/examples/todomvc/test/components/MainSection.spec.js
@@ -0,0 +1,145 @@
+import expect from 'expect';
+import jsdomReact from '../jsdomReact';
+import React from 'react/addons';
+import MainSection from '../../components/MainSection';
+import TodoItem from '../../components/TodoItem';
+import Footer from '../../components/Footer';
+import { SHOW_ALL, SHOW_MARKED } from '../../constants/TodoFilters';
+
+const { TestUtils } = React.addons;
+
+function setup(propOverrides) {
+ let props = {
+ todos: [{
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }, {
+ text: 'Run the tests',
+ marked: true,
+ id: 1
+ }],
+ actions: {
+ editTodo: expect.createSpy(),
+ deleteTodo: expect.createSpy(),
+ markTodo: expect.createSpy(),
+ markAll: expect.createSpy(),
+ clearMarked: expect.createSpy()
+ },
+ ...propOverrides
+ };
+
+ let renderer = TestUtils.createRenderer();
+ renderer.render();
+ let output = renderer.getRenderOutput();
+
+ return {
+ props: props,
+ output: output,
+ renderer: renderer
+ };
+}
+
+describe('components', () => {
+ jsdomReact();
+
+ describe('MainSection', () => {
+
+ it('should render container', () => {
+ const { output } = setup();
+ expect(output.type).toBe('section');
+ expect(output.props.className).toBe('main');
+ });
+
+ describe('toggle all input', () => {
+
+ it('should render', () => {
+ const { output } = setup();
+ let [toggle] = output.props.children;
+ expect(toggle.type).toBe('input');
+ expect(toggle.props.type).toBe('checkbox');
+ expect(toggle.props.checked).toBe(false);
+ });
+
+ it('should be checked if all todos marked', () => {
+ const { output } = setup({ todos: [{
+ text: 'Use Redux',
+ marked: true,
+ id: 0
+ }]});
+ let [toggle] = output.props.children;
+ expect(toggle.props.checked).toBe(true);
+ });
+
+ it('should call markAll on change', () => {
+ const { output, props } = setup();
+ let [toggle] = output.props.children;
+ toggle.props.onChange({});
+ expect(props.actions.markAll).toHaveBeenCalled();
+ });
+ });
+
+ describe('footer', () => {
+
+ it('should render', () => {
+ const { output } = setup();
+ let [,, footer] = output.props.children;
+ expect(footer.type).toBe(Footer);
+ expect(footer.props.markedCount).toBe(1);
+ expect(footer.props.unmarkedCount).toBe(1);
+ expect(footer.props.filter).toBe(SHOW_ALL);
+ });
+
+ it('onShow should set the filter', () => {
+ const { output, renderer } = setup();
+ let [,, footer] = output.props.children;
+ footer.props.onShow(SHOW_MARKED);
+ let updated = renderer.getRenderOutput();
+ let [,, updatedFooter] = updated.props.children;
+ expect(updatedFooter.props.filter).toBe(SHOW_MARKED);
+ });
+
+ it('onClearMarked should call clearMarked', () => {
+ const { output, props } = setup();
+ let [,, footer] = output.props.children;
+ footer.props.onClearMarked();
+ expect(props.actions.clearMarked).toHaveBeenCalled();
+ });
+
+ it('onClearMarked shouldnt call clearMarked if no todos marked', () => {
+ const { output, props } = setup({ todos: [{
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]});
+ let [,, footer] = output.props.children;
+ footer.props.onClearMarked();
+ expect(props.actions.clearMarked.calls.length).toBe(0);
+ });
+ });
+
+ describe('todo list', () => {
+
+ it('should render', () => {
+ const { output, props } = setup();
+ let [, list] = output.props.children;
+ expect(list.type).toBe('ul');
+ expect(list.props.children.length).toBe(2);
+ list.props.children.forEach((item, i) => {
+ expect(item.type).toBe(TodoItem);
+ expect(item.props.todo).toBe(props.todos[i]);
+ });
+ });
+
+ it('should filter items', () => {
+ const { output, renderer, props } = setup();
+ let [,, footer] = output.props.children;
+ footer.props.onShow(SHOW_MARKED);
+ let updated = renderer.getRenderOutput();
+ let [, updatedList] = updated.props.children;
+ expect(updatedList.props.children.length).toBe(1);
+ expect(updatedList.props.children[0].props.todo).toBe(props.todos[1]);
+ });
+ });
+ });
+});
diff --git a/examples/todomvc/test/components/TodoItem.spec.js b/examples/todomvc/test/components/TodoItem.spec.js
new file mode 100644
index 0000000000..f6fa9e923d
--- /dev/null
+++ b/examples/todomvc/test/components/TodoItem.spec.js
@@ -0,0 +1,125 @@
+import expect from 'expect';
+import jsdomReact from '../jsdomReact';
+import React from 'react/addons';
+import TodoItem from '../../components/TodoItem';
+import TodoTextInput from '../../components/TodoTextInput';
+
+const { TestUtils } = React.addons;
+
+function setup(editing=false) {
+ let props = {
+ todo: {
+ id: 0,
+ text: 'Use Redux',
+ marked: false
+ },
+ editTodo: expect.createSpy(),
+ deleteTodo: expect.createSpy(),
+ markTodo: expect.createSpy()
+ };
+
+ let renderer = TestUtils.createRenderer();
+
+ renderer.render(
+
+ );
+
+ let output = renderer.getRenderOutput();
+
+ if (editing) {
+ let label = output.props.children.props.children[1];
+ label.props.onDoubleClick({});
+ output = renderer.getRenderOutput();
+ }
+
+ return {
+ props: props,
+ output: output,
+ renderer: renderer
+ };
+}
+
+describe('components', () => {
+ jsdomReact();
+
+ describe('TodoItem', () => {
+
+ it('initial render', () => {
+ const { output } = setup();
+
+ expect(output.type).toBe('li');
+ expect(output.props.className).toBe('');
+
+ let div = output.props.children;
+
+ expect(div.type).toBe('div');
+ expect(div.props.className).toBe('view');
+
+ let [input, label, button] = div.props.children;
+
+ expect(input.type).toBe('input');
+ expect(input.props.checked).toBe(false);
+
+ expect(label.type).toBe('label');
+ expect(label.props.children).toBe('Use Redux');
+
+ expect(button.type).toBe('button');
+ expect(button.props.className).toBe('destroy');
+ });
+
+ it('input onChange should call markTodo', () => {
+ const { output, props } = setup();
+ let input = output.props.children.props.children[0];
+ input.props.onChange({});
+ expect(props.markTodo).toHaveBeenCalledWith(0);
+ });
+
+ it('button onClick should call deleteTodo', () => {
+ const { output, props } = setup();
+ let button = output.props.children.props.children[2];
+ button.props.onClick({});
+ expect(props.deleteTodo).toHaveBeenCalledWith(0);
+ });
+
+ it('label onDoubleClick should put component in edit state', () => {
+ const { output, renderer } = setup();
+ let label = output.props.children.props.children[1];
+ label.props.onDoubleClick({});
+ let updated = renderer.getRenderOutput();
+ expect(updated.type).toBe('li');
+ expect(updated.props.className).toBe('editing');
+ });
+
+ it('edit state render', () => {
+ const { output } = setup(true);
+
+ expect(output.type).toBe('li');
+ expect(output.props.className).toBe('editing');
+
+ let input = output.props.children;
+ expect(input.type).toBe(TodoTextInput);
+ expect(input.props.text).toBe('Use Redux');
+ expect(input.props.editing).toBe(true);
+ });
+
+ it('TodoTextInput onSave should call editTodo', () => {
+ const { output, props } = setup(true);
+ output.props.children.props.onSave('Use Redux');
+ expect(props.editTodo).toHaveBeenCalledWith(0, 'Use Redux');
+ });
+
+ it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
+ const { output, props } = setup(true);
+ output.props.children.props.onSave('');
+ expect(props.deleteTodo).toHaveBeenCalledWith(0);
+ });
+
+ it('TodoTextInput onSave should exit component from edit state', () => {
+ const { output, renderer } = setup(true);
+ output.props.children.props.onSave('Use Redux');
+ let updated = renderer.getRenderOutput();
+ expect(updated.type).toBe('li');
+ expect(updated.props.className).toBe('');
+ });
+ });
+});
diff --git a/examples/todomvc/test/components/TodoTextItem.spec.js b/examples/todomvc/test/components/TodoTextItem.spec.js
new file mode 100644
index 0000000000..1f5c41ecba
--- /dev/null
+++ b/examples/todomvc/test/components/TodoTextItem.spec.js
@@ -0,0 +1,89 @@
+import expect from 'expect';
+import jsdomReact from '../jsdomReact';
+import React from 'react/addons';
+import TodoTextInput from '../../components/TodoTextInput';
+
+const { TestUtils } = React.addons;
+
+function setup(propOverrides) {
+ let props = {
+ onSave: expect.createSpy(),
+ text: 'Use Redux',
+ placeholder: 'What needs to be done?',
+ editing: false,
+ newTodo: false,
+ ...propOverrides
+ };
+
+ let renderer = TestUtils.createRenderer();
+
+ renderer.render(
+
+ );
+
+ let output = renderer.getRenderOutput();
+
+ output = renderer.getRenderOutput();
+
+ return {
+ props: props,
+ output: output,
+ renderer: renderer
+ };
+}
+
+describe('components', () => {
+ jsdomReact();
+
+ describe('TodoTextInput', () => {
+
+ it('should render correctly', () => {
+ const { output } = setup();
+ expect(output.props.placeholder).toEqual('What needs to be done?');
+ expect(output.props.value).toEqual('Use Redux');
+ expect(output.props.className).toEqual('');
+ });
+
+ it('should render correctly when editing=true', () => {
+ const { output } = setup({ editing: true });
+ expect(output.props.className).toEqual('edit');
+ });
+
+ it('should render correctly when newTodo=true', () => {
+ const { output } = setup({ newTodo: true });
+ expect(output.props.className).toEqual('new-todo');
+ });
+
+ it('should update value on change', () => {
+ const { output, renderer } = setup();
+ output.props.onChange({ target: { value: 'Use Radox' }});
+ let updated = renderer.getRenderOutput();
+ expect(updated.props.value).toEqual('Use Radox');
+ });
+
+ it('should call onSave on return key press', () => {
+ const { output, props } = setup();
+ output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }});
+ expect(props.onSave).toHaveBeenCalledWith('Use Redux');
+ });
+
+ it('should reset state on return key press if newTodo', () => {
+ const { output, renderer } = setup({ newTodo: true });
+ output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }});
+ let updated = renderer.getRenderOutput();
+ expect(updated.props.value).toEqual('');
+ });
+
+ it('should call onSave on blur', () => {
+ const { output, props } = setup();
+ output.props.onBlur({ target: { value: 'Use Redux' }});
+ expect(props.onSave).toHaveBeenCalledWith('Use Redux');
+ });
+
+ it('shouldnt call onSave on blur if newTodo', () => {
+ const { output, props } = setup({ newTodo: true });
+ output.props.onBlur({ target: { value: 'Use Redux' }});
+ expect(props.onSave.calls.length).toBe(0);
+ });
+ });
+});
diff --git a/examples/todomvc/test/jsdomReact.js b/examples/todomvc/test/jsdomReact.js
new file mode 100644
index 0000000000..0083824baf
--- /dev/null
+++ b/examples/todomvc/test/jsdomReact.js
@@ -0,0 +1,7 @@
+import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';
+import jsdom from 'mocha-jsdom';
+
+export default function jsdomReact() {
+ jsdom();
+ ExecutionEnvironment.canUseDOM = true;
+}
diff --git a/examples/todomvc/test/reducers/todos.spec.js b/examples/todomvc/test/reducers/todos.spec.js
new file mode 100644
index 0000000000..d884dc0575
--- /dev/null
+++ b/examples/todomvc/test/reducers/todos.spec.js
@@ -0,0 +1,188 @@
+import expect from 'expect';
+import reducer from '../../reducers/todos';
+import * as types from '../../constants/ActionTypes';
+
+describe('todos reducer', () => {
+
+ it('should handle initial state', () => {
+ expect(
+ reducer(undefined, {})
+ ).toEqual([{
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]);
+ });
+
+ it('should handle ADD_TODO', () => {
+ expect(
+ reducer([], {
+ type: types.ADD_TODO,
+ text: 'Run the tests'
+ })
+ ).toEqual([
+ {
+ text: 'Run the tests',
+ marked: false,
+ id: 0
+ }]);
+
+ expect(
+ reducer([{
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }], {
+ type: types.ADD_TODO,
+ text: 'Run the tests'
+ })
+ ).toEqual([{
+ text: 'Run the tests',
+ marked: false,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]);
+ });
+
+ it('should handle DELETE_TODO', () => {
+ expect(
+ reducer([{
+ text: 'Run the tests',
+ marked: false,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }], {
+ type: types.DELETE_TODO,
+ id: 1
+ })
+ ).toEqual([{
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]);
+ });
+
+ it('should handle EDIT_TODO', () => {
+ expect(
+ reducer([{
+ text: 'Run the tests',
+ marked: false,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }], {
+ type: types.EDIT_TODO,
+ text: 'Fix the tests',
+ id: 1
+ })
+ ).toEqual([{
+ text: 'Fix the tests',
+ marked: false,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]);
+ });
+
+ it('should handle MARK_TODO', () => {
+ expect(
+ reducer([{
+ text: 'Run the tests',
+ marked: false,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }], {
+ type: types.MARK_TODO,
+ id: 1
+ })
+ ).toEqual([{
+ text: 'Run the tests',
+ marked: true,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]);
+ });
+
+ it('should handle MARK_ALL', () => {
+ expect(
+ reducer([{
+ text: 'Run the tests',
+ marked: true,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }], {
+ type: types.MARK_ALL
+ })
+ ).toEqual([{
+ text: 'Run the tests',
+ marked: true,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: true,
+ id: 0
+ }]);
+
+ // Unmark if all todos are currently marked
+ expect(
+ reducer([{
+ text: 'Run the tests',
+ marked: true,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: true,
+ id: 0
+ }], {
+ type: types.MARK_ALL
+ })
+ ).toEqual([{
+ text: 'Run the tests',
+ marked: false,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]);
+ });
+
+ it('should handle CLEAR_MARKED', () => {
+ expect(
+ reducer([{
+ text: 'Run the tests',
+ marked: true,
+ id: 1
+ }, {
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }], {
+ type: types.CLEAR_MARKED
+ })
+ ).toEqual([{
+ text: 'Use Redux',
+ marked: false,
+ id: 0
+ }]);
+ });
+});