;
- }
-
- const tree = TestUtils.renderIntoDocument(
-
- {() => (
- ({ string: state.string })}>
- {render}
-
- )}
-
- );
-
- const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div');
- expect(spy.calls.length).toBe(1);
- expect(div.props.string).toBe('');
- redux.dispatch({ type: 'APPEND', body: 'a'});
- expect(spy.calls.length).toBe(2);
- redux.dispatch({ type: 'APPEND', body: 'b'});
- expect(spy.calls.length).toBe(3);
- redux.dispatch({ type: 'APPEND', body: ''});
- expect(spy.calls.length).toBe(3);
- });
-
- it('recomputes the state slice when `select` prop changes', () => {
- const redux = createRedux({ a: () => 42, b: () => 72 });
-
- function selectA(state) {
- return { result: state.a };
- }
-
- function selectB(state) {
- return { result: state.b };
- }
-
- function render({ result }) {
- return
{result}
;
- }
-
- class Container extends Component {
- constructor() {
- super();
- this.state = { select: selectA };
- }
-
- render() {
- return (
-
- {() =>
-
- {render}
-
- }
-
- );
- }
- }
-
- let tree = TestUtils.renderIntoDocument(
);
- let div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div');
- expect(div.props.children).toBe(42);
-
- tree.setState({ select: selectB });
- expect(div.props.children).toBe(72);
- });
-
- it('passes `dispatch()` to child function', () => {
- const redux = createRedux({ test: () => 'test' });
-
- const tree = TestUtils.renderIntoDocument(
-
- {() => (
-
- {({ dispatch }) => }
-
- )}
-
- );
-
- const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div');
- expect(div.props.dispatch).toBe(redux.dispatch);
- });
-
- it('should throw an error if `state` returns anything but a plain object', () => {
- const redux = createRedux(() => {});
-
- expect(() => {
- TestUtils.renderIntoDocument(
-
- {() => (
- 1}>
- {() => }
-
- )}
-
- );
- }).toThrow(/select/);
- });
-
- it('does not throw error when `renderToString` is called on server', () => {
- const { renderToString } = React;
- const redux = createRedux({ string: stringBuilder });
- class TestComp extends Component {
- componentWillMount() {
- // simulate response action on data returning
- redux.dispatch({ type: 'APPEND', body: 'a'});
- }
- render() {
- return (
{this.props.string}
);
- }
- }
- const el = (
-
- {() => (
- ({ string: state.string })}>
- {({ string }) => }
-
- )}
-
- );
- expect(() => renderToString(el)).toNotThrow();
-
- });
- });
-});
diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js
deleted file mode 100644
index a94a9abfe5..0000000000
--- a/test/components/Provider.spec.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import expect from 'expect';
-import jsdomReact from './jsdomReact';
-import React, { PropTypes, Component } from 'react/addons';
-import { createRedux } from '../../src';
-import { Provider } from '../../src/react';
-
-const { TestUtils } = React.addons;
-
-describe('React', () => {
- describe('Provider', () => {
- jsdomReact();
-
- class Child extends Component {
- static contextTypes = {
- redux: PropTypes.object.isRequired
- }
-
- render() {
- return
;
- }
- }
-
- it('adds Redux to child context', () => {
- const redux = createRedux({ test: () => 'test' });
-
- const tree = TestUtils.renderIntoDocument(
-
- {() => }
-
- );
-
- const child = TestUtils.findRenderedComponentWithType(tree, Child);
- expect(child.context.redux).toBe(redux);
- });
-
- it('does not lose subscribers when receiving new props', () => {
- const redux1 = createRedux({ test: () => 'test' });
- const redux2 = createRedux({ test: () => 'test' });
- const spy = expect.createSpy(() => {});
-
- class ProviderContainer extends Component {
- state = { redux: redux1 };
-
- render() {
- return (
-
- {() => }
-
- );
- }
- }
-
- const container = TestUtils.renderIntoDocument(
);
- const child = TestUtils.findRenderedComponentWithType(container, Child);
-
- child.context.redux.subscribe(spy);
- child.context.redux.dispatch({});
- expect(spy.calls.length).toEqual(1);
-
- container.setState({ redux: redux2 });
- expect(spy.calls.length).toEqual(2);
- child.context.redux.dispatch({});
- expect(spy.calls.length).toEqual(3);
- });
- });
-});
diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js
deleted file mode 100644
index 0fce239d36..0000000000
--- a/test/components/connect.spec.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import expect from 'expect';
-import jsdomReact from './jsdomReact';
-import React, { PropTypes, Component } from 'react/addons';
-import { createRedux } from '../../src';
-import { connect, Connector } from '../../src/react';
-
-const { TestUtils } = React.addons;
-
-describe('React', () => {
- describe('provide', () => {
- jsdomReact();
-
- // Mock minimal Provider interface
- class Provider extends Component {
- static childContextTypes = {
- redux: PropTypes.object.isRequired
- }
-
- getChildContext() {
- return { redux: this.props.redux };
- }
-
- render() {
- return this.props.children();
- }
- }
-
- it('wraps component with Provider', () => {
- const redux = createRedux({ test: () => 'test' });
-
- @connect(state => state)
- class Container extends Component {
- render() {
- return
;
- }
- }
-
- const container = TestUtils.renderIntoDocument(
-
- {() => }
-
- );
- const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div');
- expect(div.props.pass).toEqual('through');
- expect(div.props.test).toEqual('test');
- expect(() => TestUtils.findRenderedComponentWithType(container, Connector))
- .toNotThrow();
- });
-
- it('sets displayName correctly', () => {
- @connect(state => state)
- class Container extends Component {
- render() {
- return
;
- }
- }
-
- expect(Container.displayName).toBe('Connector(Container)');
- });
-
- it('sets DecoratedComponent to wrapped component', () => {
- class Container extends Component {
- render() {
- return
;
- }
- }
-
- let decorator = connect(state => state);
- let ConnectorDecorator = decorator(Container);
-
- expect(ConnectorDecorator.DecoratedComponent).toBe(Container);
- });
- });
-});
diff --git a/test/components/provide.spec.js b/test/components/provide.spec.js
deleted file mode 100644
index 53ac234960..0000000000
--- a/test/components/provide.spec.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import expect from 'expect';
-import jsdomReact from './jsdomReact';
-import React, { PropTypes, Component } from 'react/addons';
-import { createRedux } from '../../src';
-import { provide, Provider } from '../../src/react';
-
-const { TestUtils } = React.addons;
-
-describe('React', () => {
- describe('provide', () => {
- jsdomReact();
-
- class Child extends Component {
- static contextTypes = {
- redux: PropTypes.object.isRequired
- }
-
- render() {
- return
;
- }
- }
-
- it('wraps component with Provider', () => {
- const redux = createRedux({ test: () => 'test' });
-
- @provide(redux)
- class Container extends Component {
- render() {
- return
;
- }
- }
-
- const container = TestUtils.renderIntoDocument(
);
- const child = TestUtils.findRenderedComponentWithType(container, Child);
- expect(child.props.pass).toEqual('through');
- expect(() => TestUtils.findRenderedComponentWithType(container, Provider))
- .toNotThrow();
- expect(child.context.redux).toBe(redux);
- });
-
- it('sets displayName correctly', () => {
- @provide(createRedux({ test: () => 'test' }))
- class Container extends Component {
- render() {
- return
;
- }
- }
-
- expect(Container.displayName).toBe('Provider(Container)');
- });
-
- it('sets DecoratedComponent to wrapped component', () => {
- class Container extends Component {
- render() {
- return
;
- }
- }
-
- let decorator = provide(state => state);
- let ProviderDecorator = decorator(Container);
-
- expect(ProviderDecorator.DecoratedComponent).toBe(Container);
- });
- });
-});
diff --git a/test/composeMiddleware.spec.js b/test/composeMiddleware.spec.js
deleted file mode 100644
index 95d916afe6..0000000000
--- a/test/composeMiddleware.spec.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import expect from 'expect';
-import { composeMiddleware } from '../src';
-
-describe('Utils', () => {
- describe('composeMiddleware', () => {
- it('should return combined middleware that executes from left to right', () => {
- const a = next => action => next(action + 'a');
- const b = next => action => next(action + 'b');
- const c = next => action => next(action + 'c');
- const dispatch = action => action;
-
- expect(composeMiddleware(a, b, c, dispatch)('')).toBe('abc');
- expect(composeMiddleware(b, c, a, dispatch)('')).toBe('bca');
- expect(composeMiddleware(c, a, b, dispatch)('')).toBe('cab');
- });
- });
-});
diff --git a/test/composeStores.spec.js b/test/composeStores.spec.js
deleted file mode 100644
index 551b275e59..0000000000
--- a/test/composeStores.spec.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import expect from 'expect';
-import { composeStores } from '../src';
-
-describe('Utils', () => {
- describe('composeStores', () => {
- it('should return a store that maps state keys to reducer functions', () => {
- const store = composeStores({
- counter: (state = 0, action) =>
- action.type === 'increment' ? state + 1 : state,
- stack: (state = [], action) =>
- action.type === 'push' ? [...state, action.value] : state
- });
-
- const s1 = store({}, { type: 'increment' });
- expect(s1).toEqual({ counter: 1, stack: [] });
- const s2 = store(s1, { type: 'push', value: 'a' });
- expect(s2).toEqual({ counter: 1, stack: ['a'] });
- });
-
- it('should ignore all props which are not a function', () => {
- const store = composeStores({
- fake: true,
- broken: 'string',
- another: {nested: 'object'},
- stack: (state = []) => state
- });
-
- expect(Object.keys(store({}, {type: 'push'}))).toEqual(['stack']);
- });
- });
-});
diff --git a/test/createDispatcher.spec.js b/test/createDispatcher.spec.js
deleted file mode 100644
index 40991bfcec..0000000000
--- a/test/createDispatcher.spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import expect from 'expect';
-import { createDispatcher, composeStores } from '../src';
-import thunkMiddleware from '../src/middleware/thunk';
-import * as helpers from './_helpers';
-
-const { constants, defaultText, todoActions, todoStore } = helpers;
-const { addTodo, addTodoAsync } = todoActions;
-const { ADD_TODO } = constants;
-
-describe('createDispatcher', () => {
-
- it('should handle sync and async dispatches', done => {
- const spy = expect.createSpy(
- nextState => nextState
- ).andCallThrough();
-
- const dispatcher = createDispatcher(
- composeStores({ todoStore }),
- // we need this middleware to handle async actions
- getState => [thunkMiddleware(getState)]
- );
-
- expect(dispatcher).toBeA('function');
-
- const dispatchFn = dispatcher(undefined, spy);
- expect(spy.calls.length).toBe(1);
- expect(spy).toHaveBeenCalledWith({ todoStore: [] });
-
- const addTodoAction = dispatchFn(addTodo(defaultText));
- expect(addTodoAction).toEqual({ type: ADD_TODO, text: defaultText });
- expect(spy.calls.length).toBe(2);
- expect(spy).toHaveBeenCalledWith({ todoStore: [
- { id: 1, text: defaultText }
- ] });
-
- dispatchFn(addTodoAsync(('Say hi!'), () => {
- expect(spy.calls.length).toBe(3);
- expect(spy).toHaveBeenCalledWith({ todoStore: [
- { id: 2, text: 'Say hi!' },
- { id: 1, text: defaultText }
- ] });
- done();
- }));
- });
-});
diff --git a/test/createRedux.spec.js b/test/createRedux.spec.js
deleted file mode 100644
index 1900de351c..0000000000
--- a/test/createRedux.spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import expect from 'expect';
-import { createRedux } from '../src';
-import * as helpers from './_helpers';
-
-const { defaultText, todoActions, todoStore } = helpers;
-const { addTodo } = todoActions;
-
-describe('createRedux', () => {
-
- let redux;
-
- beforeEach(() => {
- redux = createRedux({ todoStore });
- });
-
- it('should expose Redux public API', () => {
- const methods = Object.keys(redux);
-
- expect(methods.length).toBe(5);
- expect(methods).toContain('subscribe');
- expect(methods).toContain('dispatch');
- expect(methods).toContain('getState');
- expect(methods).toContain('getDispatcher');
- expect(methods).toContain('replaceDispatcher');
- });
-
- it('should subscribe to changes', done => {
- let state = redux.getState();
- expect(state.todoStore).toEqual({});
- redux.subscribe(() => {
- state = redux.getState();
- expect(state.todoStore).toEqual([{ id: 1, text: 'Hello World!' }]);
- done();
- });
- redux.dispatch(addTodo(defaultText));
- });
-
- it('should unsubscribe a listener', () => {
- const changeListenerSpy = expect.createSpy(() => {});
- const unsubscribe = redux.subscribe(changeListenerSpy);
-
- expect(changeListenerSpy.calls.length).toBe(0);
-
- redux.dispatch(addTodo('Hello'));
- expect(redux.getState().todoStore).toEqual([{ id: 1, text: 'Hello'}]);
- expect(changeListenerSpy.calls.length).toBe(1);
-
- unsubscribe();
- redux.dispatch(addTodo('World'));
- expect(redux.getState().todoStore).toEqual([
- { id: 2, text: 'World'},
- { id: 1, text: 'Hello'}
- ]);
- expect(changeListenerSpy.calls.length).toBe(1);
- });
-
- it('should use existing state when replacing the dispatcher', () => {
- redux.dispatch(addTodo('Hello'));
-
- let nextRedux = createRedux({ todoStore });
- redux.replaceDispatcher(nextRedux.getDispatcher());
-
- let state;
- let action = (_, getState) => {
- state = getState().todoStore;
- };
-
- redux.dispatch(action);
-
- expect(state).toEqual(redux.getState().todoStore);
- });
-});
diff --git a/test/createStore.spec.js b/test/createStore.spec.js
new file mode 100644
index 0000000000..32aa2ef333
--- /dev/null
+++ b/test/createStore.spec.js
@@ -0,0 +1,298 @@
+import expect from 'expect';
+import { createStore, combineReducers } from '../src/index';
+import { addTodo, dispatchInMiddle, throwError } from './helpers/actionCreators';
+import * as reducers from './helpers/reducers';
+
+describe('createStore', () => {
+ it('should expose the public API', () => {
+ const store = createStore(combineReducers(reducers));
+ const methods = Object.keys(store);
+
+ expect(methods.length).toBe(5);
+ expect(methods).toContain('subscribe');
+ expect(methods).toContain('dispatch');
+ expect(methods).toContain('getState');
+ expect(methods).toContain('getReducer');
+ expect(methods).toContain('replaceReducer');
+ });
+
+ it('should require a reducer function', () => {
+ expect(() =>
+ createStore()
+ ).toThrow();
+
+ expect(() =>
+ createStore('test')
+ ).toThrow();
+
+ expect(() =>
+ createStore({})
+ ).toThrow();
+
+ expect(() =>
+ createStore(() => {})
+ ).toNotThrow();
+ });
+
+ it('should pass the initial action and the initial state', () => {
+ const store = createStore(reducers.todos, [{
+ id: 1,
+ text: 'Hello'
+ }]);
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }]);
+ });
+
+ it('should apply the reducer to the previous state', () => {
+ const store = createStore(reducers.todos);
+ expect(store.getState()).toEqual([]);
+
+ store.dispatch({});
+ expect(store.getState()).toEqual([]);
+
+ store.dispatch(addTodo('Hello'));
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }]);
+
+ store.dispatch(addTodo('World'));
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }]);
+ });
+
+ it('should apply the reducer to the initial state', () => {
+ const store = createStore(reducers.todos, [{
+ id: 1,
+ text: 'Hello'
+ }]);
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }]);
+
+ store.dispatch({});
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }]);
+
+ store.dispatch(addTodo('World'));
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }]);
+ });
+
+ it('should preserve the state when replacing a reducer', () => {
+ const store = createStore(reducers.todos);
+ store.dispatch(addTodo('Hello'));
+ store.dispatch(addTodo('World'));
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }]);
+
+ let nextStore = createStore(reducers.todosReverse);
+ store.replaceReducer(nextStore.getReducer());
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }]);
+
+ store.dispatch(addTodo('Perhaps'));
+ expect(store.getState()).toEqual([{
+ id: 3,
+ text: 'Perhaps'
+ }, {
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }]);
+
+ nextStore = createStore(reducers.todos);
+ store.replaceReducer(nextStore.getReducer());
+ expect(store.getState()).toEqual([{
+ id: 3,
+ text: 'Perhaps'
+ }, {
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }]);
+
+ store.dispatch(addTodo('Surely'));
+ expect(store.getState()).toEqual([{
+ id: 3,
+ text: 'Perhaps'
+ }, {
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }, {
+ id: 4,
+ text: 'Surely'
+ }]);
+ });
+
+ it('should support multiple subscriptions', () => {
+ const store = createStore(reducers.todos);
+ const listenerA = expect.createSpy(() => {});
+ const listenerB = expect.createSpy(() => {});
+
+ let unsubscribeA = store.subscribe(listenerA);
+ store.dispatch({});
+ expect(listenerA.calls.length).toBe(1);
+ expect(listenerB.calls.length).toBe(0);
+
+ store.dispatch({});
+ expect(listenerA.calls.length).toBe(2);
+ expect(listenerB.calls.length).toBe(0);
+
+ const unsubscribeB = store.subscribe(listenerB);
+ expect(listenerA.calls.length).toBe(2);
+ expect(listenerB.calls.length).toBe(0);
+
+ store.dispatch({});
+ expect(listenerA.calls.length).toBe(3);
+ expect(listenerB.calls.length).toBe(1);
+
+ unsubscribeA();
+ expect(listenerA.calls.length).toBe(3);
+ expect(listenerB.calls.length).toBe(1);
+
+ store.dispatch({});
+ expect(listenerA.calls.length).toBe(3);
+ expect(listenerB.calls.length).toBe(2);
+
+ unsubscribeB();
+ expect(listenerA.calls.length).toBe(3);
+ expect(listenerB.calls.length).toBe(2);
+
+ store.dispatch({});
+ expect(listenerA.calls.length).toBe(3);
+ expect(listenerB.calls.length).toBe(2);
+
+ unsubscribeA = store.subscribe(listenerA);
+ expect(listenerA.calls.length).toBe(3);
+ expect(listenerB.calls.length).toBe(2);
+
+ store.dispatch({});
+ expect(listenerA.calls.length).toBe(4);
+ expect(listenerB.calls.length).toBe(2);
+ });
+
+ it('should support removing a subscription within a subscription', () => {
+ const store = createStore(reducers.todos);
+ const listenerA = expect.createSpy(() => {});
+ const listenerB = expect.createSpy(() => {});
+ const listenerC = expect.createSpy(() => {});
+
+ store.subscribe(listenerA);
+ const unSubB = store.subscribe(() => {
+ listenerB();
+ unSubB();
+ });
+ store.subscribe(listenerC);
+
+ store.dispatch({});
+ store.dispatch({});
+
+ expect(listenerA.calls.length).toBe(2);
+ expect(listenerB.calls.length).toBe(1);
+ expect(listenerC.calls.length).toBe(2);
+
+ });
+
+ it('should provide an up-to-date state when a subscriber is notified', done => {
+ const store = createStore(reducers.todos);
+ store.subscribe(() => {
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }]);
+ done();
+ });
+ store.dispatch(addTodo('Hello'));
+ });
+
+ it('should only accept plain object actions', () => {
+ const store = createStore(reducers.todos);
+ expect(() =>
+ store.dispatch({})
+ ).toNotThrow();
+
+ function AwesomeMap() { }
+ [null, undefined, 42, 'hey', new AwesomeMap()].forEach(nonObject =>
+ expect(() =>
+ store.dispatch(nonObject)
+ ).toThrow(/plain/)
+ );
+ });
+
+ it('should handle nested dispatches gracefully', () => {
+ function foo(state = 0, action) {
+ return action.type === 'foo' ? 1 : state;
+ }
+
+ function bar(state = 0, action) {
+ return action.type === 'bar' ? 2 : state;
+ }
+
+ const store = createStore(combineReducers({ foo, bar }));
+
+ store.subscribe(function kindaComponentDidUpdate() {
+ const state = store.getState();
+ if (state.bar === 0) {
+ store.dispatch({ type: 'bar' });
+ }
+ });
+
+ store.dispatch({ type: 'foo' });
+ expect(store.getState()).toEqual({
+ foo: 1,
+ bar: 2
+ });
+ });
+
+ it('should not allow dispatch() from within a reducer', () => {
+ const store = createStore(reducers.dispatchInTheMiddleOfReducer);
+
+ expect(() =>
+ store.dispatch(dispatchInMiddle(store.dispatch.bind(store, {})))
+ ).toThrow(/may not dispatch/);
+ });
+
+ it('recovers from an error within a reducer', () => {
+ const store = createStore(reducers.errorThrowingReducer);
+ expect(() =>
+ store.dispatch(throwError())
+ ).toThrow();
+
+ expect(() =>
+ store.dispatch({})
+ ).toNotThrow();
+ });
+});
diff --git a/test/getDisplayName.spec.js b/test/getDisplayName.spec.js
deleted file mode 100644
index 8b18e0dad2..0000000000
--- a/test/getDisplayName.spec.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import expect from 'expect';
-import getDisplayName from '../src/utils/getDisplayName';
-
-describe('Utils', () => {
- describe('getDisplayName', () => {
-
- it('should ensure a name for the given component', () => {
- const names = [
- { displayName: 'Foo'},
- { name: 'Bar' },
- {}
- ].map(getDisplayName);
-
- expect(names).toEqual(['Foo', 'Bar', 'Component']);
- });
- });
-});
diff --git a/test/helpers/actionCreators.js b/test/helpers/actionCreators.js
new file mode 100644
index 0000000000..8bcdf55790
--- /dev/null
+++ b/test/helpers/actionCreators.js
@@ -0,0 +1,33 @@
+import { ADD_TODO, DISPATCH_IN_MIDDLE, THROW_ERROR } from './actionTypes';
+
+export function addTodo(text) {
+ return { type: ADD_TODO, text };
+}
+
+export function addTodoAsync(text) {
+ return dispatch => new Promise(resolve => setImmediate(() => {
+ dispatch(addTodo(text));
+ resolve();
+ }));
+}
+
+export function addTodoIfEmpty(text) {
+ return (dispatch, getState) => {
+ if (!getState().length) {
+ dispatch(addTodo(text));
+ }
+ };
+}
+
+export function dispatchInMiddle(boundDispatchFn) {
+ return {
+ type: DISPATCH_IN_MIDDLE,
+ boundDispatchFn
+ };
+}
+
+export function throwError() {
+ return {
+ type: THROW_ERROR
+ };
+}
diff --git a/test/helpers/actionTypes.js b/test/helpers/actionTypes.js
new file mode 100644
index 0000000000..fdb7517cff
--- /dev/null
+++ b/test/helpers/actionTypes.js
@@ -0,0 +1,3 @@
+export const ADD_TODO = 'ADD_TODO';
+export const DISPATCH_IN_MIDDLE = 'DISPATCH_IN_MIDDLE';
+export const THROW_ERROR = 'THROW_ERROR';
diff --git a/test/helpers/middleware.js b/test/helpers/middleware.js
new file mode 100644
index 0000000000..009a5dfd8c
--- /dev/null
+++ b/test/helpers/middleware.js
@@ -0,0 +1,6 @@
+export function thunk({ dispatch, getState }) {
+ return next => action =>
+ typeof action === 'function' ?
+ action(dispatch, getState) :
+ next(action);
+}
diff --git a/test/helpers/reducers.js b/test/helpers/reducers.js
new file mode 100644
index 0000000000..9daae222ec
--- /dev/null
+++ b/test/helpers/reducers.js
@@ -0,0 +1,51 @@
+import { ADD_TODO, DISPATCH_IN_MIDDLE, THROW_ERROR } from './actionTypes';
+
+
+function id(state = []) {
+ return state.reduce((result, item) => (
+ item.id > result ? item.id : result
+ ), 0) + 1;
+}
+
+export function todos(state = [], action) {
+ switch (action.type) {
+ case ADD_TODO:
+ return [...state, {
+ id: id(state),
+ text: action.text
+ }];
+ default:
+ return state;
+ }
+}
+
+export function todosReverse(state = [], action) {
+ switch (action.type) {
+ case ADD_TODO:
+ return [{
+ id: id(state),
+ text: action.text
+ }, ...state];
+ default:
+ return state;
+ }
+}
+
+export function dispatchInTheMiddleOfReducer(state = [], action) {
+ switch (action.type) {
+ case DISPATCH_IN_MIDDLE:
+ action.boundDispatchFn();
+ return state;
+ default:
+ return state;
+ }
+}
+
+export function errorThrowingReducer(state = [], action) {
+ switch (action.type) {
+ case THROW_ERROR:
+ throw new Error();
+ default:
+ return state;
+ }
+}
diff --git a/test/utils/applyMiddleware.spec.js b/test/utils/applyMiddleware.spec.js
new file mode 100644
index 0000000000..0ca73d84c0
--- /dev/null
+++ b/test/utils/applyMiddleware.spec.js
@@ -0,0 +1,86 @@
+import expect from 'expect';
+import { createStore, applyMiddleware } from '../../src/index';
+import * as reducers from '../helpers/reducers';
+import { addTodo, addTodoAsync, addTodoIfEmpty } from '../helpers/actionCreators';
+import { thunk } from '../helpers/middleware';
+
+describe('applyMiddleware', () => {
+ it('wraps dispatch method with middleware once', () => {
+ function test(spyOnMethods) {
+ return methods => {
+ spyOnMethods(methods);
+ return next => action => next(action);
+ };
+ }
+
+ const spy = expect.createSpy(() => {});
+ const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos);
+
+ store.dispatch(addTodo('Use Redux'));
+ store.dispatch(addTodo('Flux FTW!'));
+
+ expect(spy.calls.length).toEqual(1);
+
+ expect(Object.keys(spy.calls[0].arguments[0])).toEqual([
+ 'getState',
+ 'dispatch'
+ ]);
+
+ expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' }, { id: 2, text: 'Flux FTW!' } ]);
+ });
+
+ it('should pass recursive dispatches through the middleware chain', () => {
+ function test(spyOnMethods) {
+ return () => next => action => {
+ spyOnMethods(action);
+ return next(action);
+ };
+ }
+
+ const spy = expect.createSpy(() => {});
+ const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos);
+
+ return store.dispatch(addTodoAsync('Use Redux')).then(() => {
+ expect(spy.calls.length).toEqual(2);
+ });
+ });
+
+ it('works with thunk middleware', done => {
+ const store = applyMiddleware(thunk)(createStore)(reducers.todos);
+
+ store.dispatch(addTodoIfEmpty('Hello'));
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }]);
+
+ store.dispatch(addTodoIfEmpty('Hello'));
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }]);
+
+ store.dispatch(addTodo('World'));
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }]);
+
+ store.dispatch(addTodoAsync('Maybe')).then(() => {
+ expect(store.getState()).toEqual([{
+ id: 1,
+ text: 'Hello'
+ }, {
+ id: 2,
+ text: 'World'
+ }, {
+ id: 3,
+ text: 'Maybe'
+ }]);
+ done();
+ });
+ });
+});
diff --git a/test/utils/bindActionCreators.spec.js b/test/utils/bindActionCreators.spec.js
index a4acd6840d..2c55e4ad82 100644
--- a/test/utils/bindActionCreators.spec.js
+++ b/test/utils/bindActionCreators.spec.js
@@ -1,37 +1,67 @@
import expect from 'expect';
-import { bindActionCreators, createRedux } from '../../src';
-import * as helpers from '../_helpers';
-
-const { todoActions, todoStore } = helpers;
-
-describe('Utils', () => {
- describe('bindActionCreators', () => {
-
- let redux;
-
- beforeEach(() => {
- redux = createRedux({ todoStore });
- });
-
- it('should bind given actions to the dispatcher', done => {
- let expectedCallCount = 2;
- // just for monitoring the dispatched actions
- redux.subscribe(() => {
- expectedCallCount--;
- if (expectedCallCount === 0) {
- const state = redux.getState();
- expect(state.todoStore).toEqual([
- { id: 2, text: 'World' },
- { id: 1, text: 'Hello' }
- ]);
- done();
- }
- });
- const actions = bindActionCreators(todoActions, redux.dispatch);
- expect(Object.keys(actions)).toEqual(Object.keys(todoActions));
-
- actions.addTodo('Hello');
- actions.addTodoAsync('World');
- });
+import { bindActionCreators, createStore } from '../../src';
+import { todos } from '../helpers/reducers';
+import * as actionCreators from '../helpers/actionCreators';
+
+describe('bindActionCreators', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore(todos);
+ });
+
+ it('should wrap the action creators with the dispatch function', () => {
+ const boundActionCreators = bindActionCreators(actionCreators, store.dispatch);
+ expect(
+ Object.keys(boundActionCreators)
+ ).toEqual(
+ Object.keys(actionCreators)
+ );
+
+ const action = boundActionCreators.addTodo('Hello');
+ expect(action).toEqual(
+ actionCreators.addTodo('Hello')
+ );
+ expect(store.getState()).toEqual([
+ { id: 1, text: 'Hello' }
+ ]);
+ });
+
+ it('should support wrapping a single function only', () => {
+ const actionCreator = actionCreators.addTodo;
+ const boundActionCreator = bindActionCreators(actionCreator, store.dispatch);
+
+ const action = boundActionCreator('Hello');
+ expect(action).toEqual(actionCreator('Hello'));
+ expect(store.getState()).toEqual([
+ { id: 1, text: 'Hello' }
+ ]);
+ });
+
+ it('should throw an invariant violation for an undefined actionCreator', () => {
+ expect(() => {
+ bindActionCreators(undefined, store.dispatch);
+ }).toThrow(
+ 'bindActionCreators expected an object or a function, instead received undefined. ' +
+ 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?'
+ );
+ });
+
+ it('should throw an invariant violation for a null actionCreator', () => {
+ expect(() => {
+ bindActionCreators(null, store.dispatch);
+ }).toThrow(
+ 'bindActionCreators expected an object or a function, instead received null. ' +
+ 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?'
+ );
+ });
+
+ it('should throw an invariant violation for a primitive actionCreator', () => {
+ expect(() => {
+ bindActionCreators('string', store.dispatch);
+ }).toThrow(
+ 'bindActionCreators expected an object or a function, instead received string. ' +
+ 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?'
+ );
});
});
diff --git a/test/utils/combineReducers.spec.js b/test/utils/combineReducers.spec.js
new file mode 100644
index 0000000000..d6ae96cba4
--- /dev/null
+++ b/test/utils/combineReducers.spec.js
@@ -0,0 +1,192 @@
+import expect from 'expect';
+import { combineReducers } from '../../src';
+import { ActionTypes } from '../../src/createStore';
+
+describe('Utils', () => {
+ describe('combineReducers', () => {
+ it('should return a composite reducer that maps the state keys to given reducers', () => {
+ const reducer = combineReducers({
+ counter: (state = 0, action) =>
+ action.type === 'increment' ? state + 1 : state,
+ stack: (state = [], action) =>
+ action.type === 'push' ? [...state, action.value] : state
+ });
+
+ const s1 = reducer({}, { type: 'increment' });
+ expect(s1).toEqual({ counter: 1, stack: [] });
+ const s2 = reducer(s1, { type: 'push', value: 'a' });
+ expect(s2).toEqual({ counter: 1, stack: ['a'] });
+ });
+
+ it('ignores all props which are not a function', () => {
+ const reducer = combineReducers({
+ fake: true,
+ broken: 'string',
+ another: { nested: 'object' },
+ stack: (state = []) => state
+ });
+
+ expect(
+ Object.keys(reducer({}, { type: 'push' }))
+ ).toEqual(['stack']);
+ });
+
+ it('should throw an error if a reducer returns undefined handling an action', () => {
+ const reducer = combineReducers({
+ counter(state = 0, action) {
+ switch (action && action.type) {
+ case 'increment':
+ return state + 1;
+ case 'decrement':
+ return state - 1;
+ case 'whatever':
+ case null:
+ case undefined:
+ return undefined;
+ default:
+ return state;
+ }
+ }
+ });
+
+ expect(
+ () => reducer({ counter: 0 }, { type: 'whatever' })
+ ).toThrow(
+ /"counter".*"whatever"/
+ );
+ expect(
+ () => reducer({ counter: 0 }, null)
+ ).toThrow(
+ /"counter".*an action/
+ );
+ expect(
+ () => reducer({ counter: 0 }, {})
+ ).toThrow(
+ /"counter".*an action/
+ );
+ });
+
+ it('should throw an error if a reducer returns undefined initializing', () => {
+ expect(() => combineReducers({
+ counter(state, action) {
+ switch (action.type) {
+ case 'increment':
+ return state + 1;
+ case 'decrement':
+ return state - 1;
+ default:
+ return state;
+ }
+ }
+ })).toThrow(
+ /"counter".*initialization/
+ );
+ });
+
+ it('should allow a symbol to be used as an action type', () => {
+ const increment = Symbol('INCREMENT');
+
+ const reducer = combineReducers({
+ counter(state = 0, action) {
+ switch (action.type) {
+ case increment:
+ return state + 1;
+ default:
+ return state;
+ }
+ }
+ });
+
+ expect(reducer({counter: 0}, { type: increment }).counter).toEqual(1);
+ });
+
+ it('should throw an error if a reducer attempts to handle a private action', () => {
+ expect(() => combineReducers({
+ counter(state, action) {
+ switch (action.type) {
+ case 'increment':
+ return state + 1;
+ case 'decrement':
+ return state - 1;
+ // Never do this in your code:
+ case ActionTypes.INIT:
+ return 0;
+ default:
+ return undefined;
+ }
+ }
+ })).toThrow(
+ /"counter".*private/
+ );
+ });
+
+ it('should warn if no reducers are passed to combineReducers', () => {
+ const spy = expect.spyOn(console, 'error');
+ const reducer = combineReducers({});
+ reducer({});
+ expect(spy.calls[0].arguments[0]).toMatch(
+ /Store does not have a valid reducer/
+ );
+ spy.restore();
+ });
+
+ it('should warn if initial state object does not match state object returned by reducer', () => {
+ const spy = expect.spyOn(console, 'error');
+ const reducerCreator = () => {
+ return combineReducers({
+ foo(state = {bar: 1}) {
+ return state;
+ },
+ baz(state = {qux: 3}) {
+ return state;
+ }
+ });
+ };
+
+ reducerCreator()({foo: {bar: 2}});
+ expect(spy.calls.length).toBe(0);
+
+ reducerCreator()({
+ foo: {bar: 2},
+ baz: {qux: 4}
+ });
+ expect(spy.calls.length).toBe(0);
+
+ reducerCreator()({bar: 2});
+ expect(spy.calls[0].arguments[0]).toMatch(
+ /Unexpected key "bar".*instead: "foo", "baz"/
+ );
+
+ reducerCreator()({bar: 2, qux: 4});
+ expect(spy.calls[1].arguments[0]).toMatch(
+ /Unexpected keys "bar", "qux".*instead: "foo", "baz"/
+ );
+
+ reducerCreator()(1);
+ expect(spy.calls[2].arguments[0]).toMatch(
+ /unexpected type of "Number".*keys: "foo", "baz"/
+ );
+
+ spy.restore();
+ });
+
+ it('should only check state shape on init', () => {
+ const spy = expect.spyOn(console, 'error');
+ const reducer = combineReducers({
+ foo(state = {bar: 1}) {
+ return state;
+ }
+ });
+
+ reducer({bar: 1});
+ expect(spy.calls[0].arguments[0]).toMatch(
+ /Unexpected key "bar".*instead: "foo"/
+ );
+
+ reducer({bar: 1});
+ expect(spy.calls.length).toBe(1);
+
+ spy.restore();
+ });
+ });
+});
diff --git a/test/utils/compose.spec.js b/test/utils/compose.spec.js
new file mode 100644
index 0000000000..dd4dd5c9bb
--- /dev/null
+++ b/test/utils/compose.spec.js
@@ -0,0 +1,17 @@
+import expect from 'expect';
+import { compose } from '../../src';
+
+describe('Utils', () => {
+ describe('compose', () => {
+ it('composes functions from left to right', () => {
+ const a = next => x => next(x + 'a');
+ const b = next => x => next(x + 'b');
+ const c = next => x => next(x + 'c');
+ const final = x => x;
+
+ expect(compose(a, b, c, final)('')).toBe('abc');
+ expect(compose(b, c, a, final)('')).toBe('bca');
+ expect(compose(c, a, b, final)('')).toBe('cab');
+ });
+ });
+});
diff --git a/test/utils/identity.spec.js b/test/utils/identity.spec.js
deleted file mode 100644
index 87783b8117..0000000000
--- a/test/utils/identity.spec.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import expect from 'expect';
-import identity from '../../src/utils/identity';
-
-describe('Utils', () => {
- describe('identity', () => {
- it('should return first argument passed to it', () => {
- const test = { 'a': 1 };
- expect(identity(test, 'test')).toBe(test);
- });
- });
-});
diff --git a/test/utils/isPlainObject.spec.js b/test/utils/isPlainObject.spec.js
index 13cd49a1cf..963c7f610c 100644
--- a/test/utils/isPlainObject.spec.js
+++ b/test/utils/isPlainObject.spec.js
@@ -1,19 +1,24 @@
import expect from 'expect';
import isPlainObject from '../../src/utils/isPlainObject';
+import contextify from 'contextify';
-describe('Utils', () => {
- describe('isPlainObject', () => {
- it('should return true only if plain object', () => {
- function Test() {
- this.prop = 1;
- }
+describe('isPlainObject', () => {
+ it('should return true only if plain object', () => {
+ function Test() {
+ this.prop = 1;
+ }
- expect(isPlainObject(new Test())).toBe(false);
- expect(isPlainObject(new Date())).toBe(false);
- expect(isPlainObject([1, 2, 3])).toBe(false);
- expect(isPlainObject(null)).toBe(false);
- expect(isPlainObject()).toBe(false);
- expect(isPlainObject({ 'x': 1, 'y': 2 })).toBe(true);
- });
+ const sandbox = contextify();
+ sandbox.run('var fromAnotherRealm = {};');
+
+ expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true);
+ expect(isPlainObject(new Test())).toBe(false);
+ expect(isPlainObject(new Date())).toBe(false);
+ expect(isPlainObject([1, 2, 3])).toBe(false);
+ expect(isPlainObject(null)).toBe(false);
+ expect(isPlainObject()).toBe(false);
+ expect(isPlainObject({ 'x': 1, 'y': 2 })).toBe(true);
+
+ sandbox.dispose();
});
});
diff --git a/test/utils/mapValues.spec.js b/test/utils/mapValues.spec.js
index 36c3c6b2c9..007848af81 100644
--- a/test/utils/mapValues.spec.js
+++ b/test/utils/mapValues.spec.js
@@ -1,11 +1,15 @@
import expect from 'expect';
import mapValues from '../../src/utils/mapValues';
-describe('Utils', () => {
- describe('mapValues', () => {
- it('should return object with mapped values', () => {
- const test = { 'a': 'c', 'b': 'd' };
- expect(mapValues(test, (val, key) => val + key)).toEqual({ 'a': 'ca', 'b': 'db' });
+describe('mapValues', () => {
+ it('should return object with mapped values', () => {
+ const test = {
+ a: 'c',
+ b: 'd'
+ };
+ expect(mapValues(test, (val, key) => val + key)).toEqual({
+ a: 'ca',
+ b: 'db'
});
});
});
diff --git a/test/utils/pick.spec.js b/test/utils/pick.spec.js
index 6cd95f7395..14e02d5db0 100644
--- a/test/utils/pick.spec.js
+++ b/test/utils/pick.spec.js
@@ -1,11 +1,16 @@
import expect from 'expect';
import pick from '../../src/utils/pick';
-describe('Utils', () => {
- describe('pick', () => {
- it('should return object with picked values', () => {
- const test = { 'name': 'lily', 'age': 20 };
- expect(pick(test, x => typeof x === 'string')).toEqual({ 'name': 'lily' });
+describe('pick', () => {
+ it('should return object with picked values', () => {
+ const test = {
+ name: 'lily',
+ age: 20
+ };
+ expect(
+ pick(test, x => typeof x === 'string')
+ ).toEqual({
+ name: 'lily'
});
});
});
diff --git a/test/utils/shallowEquality.spec.js b/test/utils/shallowEquality.spec.js
deleted file mode 100644
index 8ba48e6bdd..0000000000
--- a/test/utils/shallowEquality.spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import expect from 'expect';
-import shallowEqualScalar from '../../src/utils/shallowEqualScalar';
-import shallowEqual from '../../src/utils/shallowEqual';
-
-describe('Utils', () => {
- // More info: https://github.com/gaearon/redux/pull/75#issuecomment-111635748
- describe('shallowEqualScalar', () => {
- it('returns true if both arguments are the same object', () => {
- const o = { a: 1, b: 2 };
- expect(shallowEqualScalar(o, o)).toBe(true);
- });
-
- it('returns false if either argument is null', () => {
- expect(shallowEqualScalar(null, {})).toBe(false);
- expect(shallowEqualScalar({}, null)).toBe(false);
- });
-
- it('returns true if arguments fields are equal', () => {
- expect(
- shallowEqualScalar(
- { a: 1, b: 2, c: undefined },
- { a: 1, b: 2, c: undefined }
- )
- ).toBe(true);
-
- expect(
- shallowEqualScalar(
- { a: 1, b: 2, c: 3 },
- { a: 1, b: 2, c: 3 }
- )
- ).toBe(true);
- });
-
- it('returns false if first argument has too many keys', () => {
- expect(
- shallowEqualScalar(
- { a: 1, b: 2, c: 3 },
- { a: 1, b: 2 }
- )
- ).toBe(false);
- });
-
- it('returns false if second argument has too many keys', () => {
- expect(
- shallowEqualScalar(
- { a: 1, b: 2 },
- { a: 1, b: 2, c: 3 }
- )
- ).toBe(false);
- });
-
- it('returns false if arguments have keys dont have same value', () => {
- expect(
- shallowEqualScalar(
- { a: 1, b: 2 },
- { a: 1, b: 3 }
- )
- ).toBe(false);
- });
-
- it('returns false if arguments have field that are objects', () => {
- const o = {};
- expect(
- shallowEqualScalar(
- { a: 1, b: 2, c: o },
- { a: 1, b: 2, c: o }
- )
- ).toBe(false);
- });
-
- it('returns false if arguments have different keys', () => {
- expect(
- shallowEqualScalar(
- { a: 1, b: 2, c: undefined },
- { a: 1, bb: 2, c: undefined }
- )
- ).toBe(false);
- });
- });
-
- describe('shallowEqual', () => {
- it('returns true if arguments fields are equal', () => {
- expect(
- shallowEqual(
- { a: 1, b: 2, c: undefined },
- { a: 1, b: 2, c: undefined }
- )
- ).toBe(true);
-
- expect(
- shallowEqual(
- { a: 1, b: 2, c: 3 },
- { a: 1, b: 2, c: 3 }
- )
- ).toBe(true);
-
- const o = {};
- expect(
- shallowEqual(
- { a: 1, b: 2, c: o },
- { a: 1, b: 2, c: o }
- )
- ).toBe(true);
- });
-
- it('returns false if first argument has too many keys', () => {
- expect(
- shallowEqual(
- { a: 1, b: 2, c: 3 },
- { a: 1, b: 2 }
- )
- ).toBe(false);
- });
-
- it('returns false if second argument has too many keys', () => {
- expect(
- shallowEqual(
- { a: 1, b: 2 },
- { a: 1, b: 2, c: 3 }
- )
- ).toBe(false);
- });
-
- it('returns false if arguments have different keys', () => {
- expect(
- shallowEqual(
- { a: 1, b: 2, c: undefined },
- { a: 1, bb: 2, c: undefined }
- )
- ).toBe(false);
- });
- });
-
-});
diff --git a/webpack.config.base.js b/webpack.config.base.js
new file mode 100644
index 0000000000..ddf3bdec35
--- /dev/null
+++ b/webpack.config.base.js
@@ -0,0 +1,18 @@
+'use strict';
+
+var webpack = require('webpack');
+
+module.exports = {
+ module: {
+ loaders: [
+ { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }
+ ]
+ },
+ output: {
+ library: 'Redux',
+ libraryTarget: 'umd'
+ },
+ resolve: {
+ extensions: ['', '.js']
+ }
+};
diff --git a/webpack.config.development.js b/webpack.config.development.js
new file mode 100644
index 0000000000..e509d7cd24
--- /dev/null
+++ b/webpack.config.development.js
@@ -0,0 +1,14 @@
+'use strict';
+
+var webpack = require('webpack');
+var baseConfig = require('./webpack.config.base');
+
+var config = Object.create(baseConfig);
+config.plugins = [
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify('development')
+ })
+];
+
+module.exports = config;
diff --git a/webpack.config.js b/webpack.config.js
deleted file mode 100644
index d7c5f5a61c..0000000000
--- a/webpack.config.js
+++ /dev/null
@@ -1,48 +0,0 @@
-'use strict';
-
-var webpack = require('webpack');
-
-var plugins = [
- new webpack.DefinePlugin({
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
- }),
- new webpack.optimize.OccurenceOrderPlugin()
-];
-
-if (process.env.NODE_ENV === 'production') {
- plugins.push(
- new webpack.optimize.UglifyJsPlugin({
- compressor: {
- screw_ie8: true,
- warnings: false
- }
- })
- );
-}
-
-var reactExternal = {
- root: 'React',
- commonjs2: 'react',
- commonjs: 'react',
- amd: 'react'
-};
-
-module.exports = {
- externals: {
- 'react': reactExternal,
- 'react-native': reactExternal
- },
- module: {
- loaders: [
- { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }
- ]
- },
- output: {
- library: 'Redux',
- libraryTarget: 'umd'
- },
- plugins: plugins,
- resolve: {
- extensions: ['', '.js']
- }
-};
diff --git a/webpack.config.production.js b/webpack.config.production.js
new file mode 100644
index 0000000000..f4589cc06d
--- /dev/null
+++ b/webpack.config.production.js
@@ -0,0 +1,20 @@
+'use strict';
+
+var webpack = require('webpack');
+var baseConfig = require('./webpack.config.base');
+
+var config = Object.create(baseConfig);
+config.plugins = [
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify('production')
+ }),
+ new webpack.optimize.UglifyJsPlugin({
+ compressor: {
+ screw_ie8: true,
+ warnings: false
+ }
+ })
+];
+
+module.exports = config;