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(