diff --git a/src/components/createAll.js b/src/components/createAll.js index bd6383ccc..4699d819b 100644 --- a/src/components/createAll.js +++ b/src/components/createAll.js @@ -1,17 +1,15 @@ import createProvider from './createProvider'; import createProvideDecorator from './createProvideDecorator'; -import createConnector from './createConnector'; import createConnectDecorator from './createConnectDecorator'; export default function createAll(React) { // Wrapper components const Provider = createProvider(React); - const Connector = createConnector(React); // Higher-order components (decorators) const provide = createProvideDecorator(React, Provider); - const connect = createConnectDecorator(React, Connector); + const connect = createConnectDecorator(React); - return { Provider, Connector, provide, connect }; + return { Provider, provide, connect }; } diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js index a3196e1c1..51510e361 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnectDecorator.js @@ -1,24 +1,125 @@ +import createStoreShape from '../utils/createStoreShape'; import getDisplayName from '../utils/getDisplayName'; import shallowEqualScalar from '../utils/shallowEqualScalar'; +import shallowEqual from '../utils/shallowEqual'; +import isPlainObject from '../utils/isPlainObject'; +import wrapActionCreators from '../utils/wrapActionCreators'; +import invariant from 'invariant'; -export default function createConnectDecorator(React, Connector) { - const { Component } = React; +const emptySelector = () => ({}); - return function connect(select) { - return DecoratedComponent => class ConnectorDecorator extends Component { - static displayName = `Connector(${getDisplayName(DecoratedComponent)})`; +const emptyBinder = () => ({}); + +const identityMerge = (slice, actionsCreators, props) => ({ ...props, ...slice, ...actionsCreators}); + + +export default function createConnectDecorator(React) { + const { Component, PropTypes } = React; + const storeShape = createStoreShape(PropTypes); + + return function connect(select, dispatchBinder = emptyBinder, mergeHandler = identityMerge) { + + const subscribing = select ? true : false; + const selectState = select || emptySelector; + const bindDispatch = isPlainObject(dispatchBinder) ? wrapActionCreators(dispatchBinder) : dispatchBinder; + const merge = mergeHandler; + + return DecoratedComponent => class ConnectDecorator extends Component { + static displayName = `ConnectDecorator(${getDisplayName(DecoratedComponent)})`; static DecoratedComponent = DecoratedComponent; - shouldComponentUpdate(nextProps) { - return !shallowEqualScalar(this.props, nextProps); + static contextTypes = { + store: storeShape.isRequired + }; + + shouldComponentUpdate(nextProps, nextState) { + return (this.subscribed && !this.isSliceEqual(this.state.slice, nextState.slice)) || + !shallowEqualScalar(this.props, nextProps); } - render() { - return ( - select(state, this.props)}> - {stuff => } - + isSliceEqual(slice, nextSlice) { + const isRefEqual = slice === nextSlice; + if (isRefEqual) { + return true; + } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { + return isRefEqual; + } + return shallowEqual(slice, nextSlice); + } + + constructor(props, context) { + super(props, context); + this.state = { + ...this.selectState(props, context), + ...this.bindDispatch(context) + }; + } + + componentDidMount() { + if (subscribing) { + this.subscribed = true; + this.unsubscribe = this.context.store.subscribe(::this.handleChange); + } + } + + componentWillUnmount() { + if (subscribing) { + this.unsubscribe(); + } + } + + handleChange(props = this.props) { + const nextState = this.selectState(props, this.context); + if (!this.isSliceEqual(this.state.slice, nextState.slice)) { + this.setState(nextState); + } + } + + selectState(props = this.props, context = this.context) { + const state = context.store.getState(); + const slice = selectState(state); + + invariant( + isPlainObject(slice), + 'The return value of `select` prop must be an object. Instead received %s.', + slice ); + + return { slice }; + } + + bindDispatch(context = this.context) { + const { dispatch } = context.store; + const actionCreators = bindDispatch(dispatch); + + invariant( + isPlainObject(actionCreators), + 'The return value of `bindDispatch` prop must be an object. Instead received %s.', + actionCreators + ); + + return { actionCreators }; + } + + merge(props = this.props, state = this.state) { + const { slice, actionCreators } = state; + const merged = merge(slice, actionCreators, props); + + invariant( + isPlainObject(merged), + 'The return value of `merge` prop must be an object. Instead received %s.', + merged + ); + + return merged; + } + + getUnderlyingRef() { + return this.underlyingRef; + } + + render() { + return (this.underlyingRef = component)} {...this.merge()} />; } }; }; diff --git a/src/components/createConnector.js b/src/components/createConnector.js deleted file mode 100644 index 2318092e5..000000000 --- a/src/components/createConnector.js +++ /dev/null @@ -1,88 +0,0 @@ -import createStoreShape from '../utils/createStoreShape'; -import shallowEqual from '../utils/shallowEqual'; -import isPlainObject from '../utils/isPlainObject'; -import invariant from 'invariant'; - -export default function createConnector(React) { - const { Component, PropTypes } = React; - const storeShape = createStoreShape(PropTypes); - - return class Connector extends Component { - static contextTypes = { - store: storeShape.isRequired - }; - - static propTypes = { - children: PropTypes.func.isRequired, - select: PropTypes.func.isRequired - }; - - static defaultProps = { - select: state => state - }; - - shouldComponentUpdate(nextProps, nextState) { - return !this.isSliceEqual(this.state.slice, nextState.slice) || - !shallowEqual(this.props, nextProps); - } - - isSliceEqual(slice, nextSlice) { - const isRefEqual = slice === nextSlice; - if (isRefEqual) { - return true; - } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { - return isRefEqual; - } - return shallowEqual(slice, nextSlice); - } - - constructor(props, context) { - super(props, context); - this.state = this.selectState(props, context); - } - - componentDidMount() { - this.unsubscribe = this.context.store.subscribe(::this.handleChange); - this.handleChange(); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.select !== this.props.select) { - // Force the state slice recalculation - this.handleChange(nextProps); - } - } - - componentWillUnmount() { - this.unsubscribe(); - } - - handleChange(props = this.props) { - const nextState = this.selectState(props, this.context); - if (!this.isSliceEqual(this.state.slice, nextState.slice)) { - this.setState(nextState); - } - } - - selectState(props, context) { - const state = context.store.getState(); - const slice = props.select(state); - - invariant( - isPlainObject(slice), - 'The return value of `select` prop must be an object. Instead received %s.', - slice - ); - - return { slice }; - } - - render() { - const { children } = this.props; - const { slice } = this.state; - const { store: { dispatch } } = this.context; - - return children({ dispatch, ...slice }); - } - }; -} diff --git a/src/index.js b/src/index.js index a2058d695..f09a801ee 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ import React from 'react'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect } = createAll(React); +export const { Provider, Connector, provide, connect, connectDeprecated } = createAll(React); diff --git a/src/native.js b/src/native.js index c6fc5363e..16471679b 100644 --- a/src/native.js +++ b/src/native.js @@ -1,4 +1,4 @@ import React from 'react-native'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect } = createAll(React); +export const { Provider, Connector, provide, connect, connectDeprecated } = createAll(React); diff --git a/src/utils/wrapActionCreators.js b/src/utils/wrapActionCreators.js new file mode 100644 index 000000000..983fbe606 --- /dev/null +++ b/src/utils/wrapActionCreators.js @@ -0,0 +1,5 @@ +import { bindActionCreators } from 'redux'; + +export default function wrapActionCreators(actionCreators) { + return dispatch => bindActionCreators(actionCreators, dispatch); +} diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js deleted file mode 100644 index bb4cef68b..000000000 --- a/test/components/Connector.spec.js +++ /dev/null @@ -1,295 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from 'redux'; -import { Connector } from '../../src/index'; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('Connector', () => { - jsdomReact(); - - // Mock minimal Provider interface - class Provider extends Component { - static childContextTypes = { - store: PropTypes.object.isRequired - } - - getChildContext() { - return { store: this.props.store }; - } - - render() { - return this.props.children(); - } - } - - function stringBuilder(prev = '', action) { - return action.type === 'APPEND' - ? prev + action.body - : prev; - } - - it('should receive the store in the context', () => { - const store = createStore({}); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - - {() =>
} - - )} - - ); - - const connector = TestUtils.findRenderedComponentWithType(tree, Connector); - expect(connector.context.store).toBe(store); - }); - - it('should subscribe to the store changes', () => { - const store = createStore(stringBuilder); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) =>
} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.string).toBe(''); - store.dispatch({ type: 'APPEND', body: 'a'}); - expect(div.props.string).toBe('a'); - store.dispatch({ type: 'APPEND', body: 'b'}); - expect(div.props.string).toBe('ab'); - }); - - it('should unsubscribe before unmounting', () => { - const store = createStore(stringBuilder); - const subscribe = store.subscribe; - - // Keep track of unsubscribe by wrapping subscribe() - const spy = expect.createSpy(() => {}); - store.subscribe = (listener) => { - const unsubscribe = subscribe(listener); - return () => { - spy(); - return unsubscribe(); - }; - }; - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) =>
} - - )} - - ); - - const connector = TestUtils.findRenderedComponentWithType(tree, Connector); - expect(spy.calls.length).toBe(0); - connector.componentWillUnmount(); - expect(spy.calls.length).toBe(1); - }); - - it('should shallowly compare the selected state to prevent unnecessary updates', () => { - const store = createStore(stringBuilder); - const spy = expect.createSpy(() => {}); - function render({ string }) { - spy(); - return
; - } - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {render} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(spy.calls.length).toBe(1); - expect(div.props.string).toBe(''); - store.dispatch({ type: 'APPEND', body: 'a'}); - expect(spy.calls.length).toBe(2); - store.dispatch({ type: 'APPEND', body: 'b'}); - expect(spy.calls.length).toBe(3); - store.dispatch({ type: 'APPEND', body: ''}); - expect(spy.calls.length).toBe(3); - }); - - it('should recompute the state slice when the select prop changes', () => { - const store = createStore({ - 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('should pass dispatch() to the child function', () => { - const store = createStore({}); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - - {({ dispatch }) =>
} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.dispatch).toBe(store.dispatch); - }); - - it('should throw an error if select returns anything but a plain object', () => { - const store = createStore({}); - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - 1}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - 'hey'}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - - function AwesomeMap() { } - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - new AwesomeMap()}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - }); - - it('should not setState when renderToString is called on the server', () => { - const { renderToString } = React; - const store = createStore(stringBuilder); - - class TestComp extends Component { - componentWillMount() { - store.dispatch({ - type: 'APPEND', - body: 'a' - }); - } - - render() { - return
{this.props.string}
; - } - } - - const el = ( - - {() => ( - ({ string })}> - {({ string }) => } - - )} - - ); - - expect(() => renderToString(el)).toNotThrow(); - }); - - it('should handle dispatch inside componentDidMount', () => { - const store = createStore(stringBuilder); - - class TestComp extends Component { - componentDidMount() { - store.dispatch({ - type: 'APPEND', - body: 'a' - }); - } - - render() { - return
{this.props.string}
; - } - } - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) => } - - )} - - ); - - const testComp = TestUtils.findRenderedComponentWithType(tree, TestComp); - expect(testComp.props.string).toBe('a'); - }); - }); -}); diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index d0daf08f1..a85f1b2b7 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -21,7 +21,7 @@ describe('React', () => { } it('should add the store to the child context', () => { - const store = createStore({}); + const store = createStore(() => ({})); const tree = TestUtils.renderIntoDocument( @@ -36,7 +36,7 @@ describe('React', () => { it('should replace just the reducer when receiving a new store in props', () => { const store1 = createStore((state = 10) => state + 1); const store2 = createStore((state = 10) => state * 2); - const spy = expect.createSpy(() => {}); + const spy = expect.createSpy(() => ({})); class ProviderContainer extends Component { state = { store: store1 }; diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 743881097..227d5f123 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1,8 +1,8 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from 'redux'; -import { connect, Connector } from '../../src/index'; +import { createStore, combineReducers } from 'redux'; +import { connect } from '../../src/index'; const { TestUtils } = React.addons; @@ -25,6 +25,34 @@ describe('React', () => { } } + function stringBuilder(prev = '', action) { + return action.type === 'APPEND' + ? prev + action.body + : prev; + } + + it('should receive the store in the context', () => { + const store = createStore(() => ({})); + + @connect() + class Container extends Component { + render() { + return
; + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const container = TestUtils.findRenderedComponentWithType(tree, Container); + expect(container.context.store).toBe(store); + }); + it('should wrap the component into Provider', () => { const store = createStore(() => ({ foo: 'bar' @@ -46,10 +74,37 @@ describe('React', () => { expect(div.props.pass).toEqual('through'); expect(div.props.foo).toEqual('bar'); expect(() => - TestUtils.findRenderedComponentWithType(container, Connector) + TestUtils.findRenderedComponentWithType(container, Container) ).toNotThrow(); }); + it('should subscribe to the store changes', () => { + const store = createStore(stringBuilder); + + @connect(state => ({string: state}) ) + class Container extends Component { + render() { + return
; + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + + expect(div.props.string).toBe(''); + store.dispatch({ type: 'APPEND', body: 'a'}); + expect(div.props.string).toBe('a'); + store.dispatch({ type: 'APPEND', body: 'b'}); + expect(div.props.string).toBe('ab'); + }); + it('should handle additional prop changes in addition to slice', () => { const store = createStore(() => ({ foo: 'bar' @@ -96,17 +151,101 @@ describe('React', () => { expect(div.props.pass).toEqual('through'); }); - it('should pass the only argument as the select prop down', () => { + it('should allow for merge to incorporate state and prop changes', () => { + const store = createStore(stringBuilder); + + function doSomething(thing) { + return { + type: 'APPEND', + body: thing + }; + } + + @connect( + state => ({stateThing: state}), + dispatch => ({doSomething: (whatever) => dispatch(doSomething(whatever)) }), + (stateProps, actionProps, parentProps) => ({ + ...stateProps, + ...actionProps, + mergedDoSomething: (thing) => { + const seed = stateProps.stateThing === '' ? 'HELLO ' : ''; + actionProps.doSomething(seed + thing + parentProps.extra); + } + }) + ) + class Container extends Component { + render() { + return
; + }; + } + + class OuterContainer extends Component { + constructor() { + super(); + this.state = { extra: 'z' }; + } + + render() { + return ( + + {() => } + + ); + } + } + + const tree = TestUtils.renderIntoDocument(); + const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + + expect(div.props.stateThing).toBe(''); + div.props.mergedDoSomething('a'); + expect(div.props.stateThing).toBe('HELLO az'); + div.props.mergedDoSomething('b'); + expect(div.props.stateThing).toBe('HELLO azbz'); + tree.setState({extra: 'Z'}); + div.props.mergedDoSomething('c'); + expect(div.props.stateThing).toBe('HELLO azbzcZ'); + }); + + it('should merge actionProps into DecoratedComponent', () => { const store = createStore(() => ({ - foo: 'baz', - bar: 'baz' + foo: 'bar' })); - function select({ foo }) { - return { foo }; + @connect( + state => state, + dispatch => ({ dispatch }) + ) + class Container extends Component { + render() { + return
; + } } - @connect(select) + const container = TestUtils.renderIntoDocument( + + {() => } + + ); + const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); + expect(div.props.dispatch).toEqual(store.dispatch); + expect(div.props.foo).toEqual('bar'); + expect(() => + TestUtils.findRenderedComponentWithType(container, Container) + ).toNotThrow(); + const decorated = TestUtils.findRenderedComponentWithType(container, Container); + expect(decorated.subscribed).toBe(true); + }); + + it('should not subscribe to stores if select argument is null', () => { + const store = createStore(() => ({ + foo: 'bar' + })); + + @connect( + null, + dispatch => ({ dispatch }) + ) class Container extends Component { render() { return
; @@ -118,13 +257,179 @@ describe('React', () => { {() => } ); - const connector = TestUtils.findRenderedComponentWithType(container, Connector); - expect(connector.props.select({ - foo: 5, - bar: 7 - })).toEqual({ - foo: 5 - }); + const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); + expect(div.props.dispatch).toEqual(store.dispatch); + expect(div.props.foo).toBe(undefined); + expect(() => + TestUtils.findRenderedComponentWithType(container, Container) + ).toNotThrow(); + const decorated = TestUtils.findRenderedComponentWithType(container, Container); + expect(decorated.subscribed).toNotBe(true); + + }); + + it('should unsubscribe before unmounting', () => { + const store = createStore(stringBuilder); + const subscribe = store.subscribe; + + // Keep track of unsubscribe by wrapping subscribe() + const spy = expect.createSpy(() => ({})); + store.subscribe = (listener) => { + const unsubscribe = subscribe(listener); + return () => { + spy(); + return unsubscribe(); + }; + }; + + @connect( + state => ({string: state}), + dispatch => ({ dispatch }) + ) + class Container extends Component { + render() { + return
; + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const connector = TestUtils.findRenderedComponentWithType(tree, Container); + expect(spy.calls.length).toBe(0); + connector.componentWillUnmount(); + expect(spy.calls.length).toBe(1); + }); + + it('should shallowly compare the selected state to prevent unnecessary updates', () => { + const store = createStore(stringBuilder); + const spy = expect.createSpy(() => ({})); + function render({ string }) { + spy(); + return
; + } + + @connect( + state => ({string: state}), + dispatch => ({ dispatch }) + ) + class Container extends Component { + render() { + return render(this.props); + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + expect(spy.calls.length).toBe(1); + expect(div.props.string).toBe(''); + store.dispatch({ type: 'APPEND', body: 'a'}); + expect(spy.calls.length).toBe(2); + store.dispatch({ type: 'APPEND', body: 'b'}); + expect(spy.calls.length).toBe(3); + store.dispatch({ type: 'APPEND', body: ''}); + expect(spy.calls.length).toBe(3); + }); + + it('should throw an error if select, bindDispatch, or merge returns anything but a plain object', () => { + const store = createStore(() => ({})); + + function makeContainer(select, bindActionCreators, merge) { + return React.createElement( + @connect(select, bindActionCreators, merge) + class Container extends Component { + render() { + return
; + } + } + ); + } + + function AwesomeMap() { } + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => 1, () => ({}), () => ({})) } + + ); + }).toThrow(/select/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => 'hey', () => ({}), () => ({})) } + + ); + }).toThrow(/select/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => new AwesomeMap(), () => ({}), () => ({})) } + + ); + }).toThrow(/select/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => 1, () => ({})) } + + ); + }).toThrow(/bindDispatch/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => 'hey', () => ({})) } + + ); + }).toThrow(/bindDispatch/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => new AwesomeMap(), () => ({})) } + + ); + }).toThrow(/bindDispatch/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => ({}), () => 1) } + + ); + }).toThrow(/merge/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => ({}), () => 'hey') } + + ); + }).toThrow(/merge/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => ({}), () => new AwesomeMap()) } + + ); + }).toThrow(/merge/); }); it('should set the displayName correctly', () => { @@ -135,7 +440,7 @@ describe('React', () => { } } - expect(Container.displayName).toBe('Connector(Container)'); + expect(Container.displayName).toBe('ConnectDecorator(Container)'); }); it('should expose the wrapped component as DecoratedComponent', () => { @@ -150,5 +455,39 @@ describe('React', () => { expect(decorated.DecoratedComponent).toBe(Container); }); + + it('should return the instance of the wrapped component for use in calling child methods', () => { + const store = createStore(() => ({})); + + const someData = { + some: 'data' + }; + + class Container extends Component { + someInstanceMethod() { + return someData; + } + + render() { + return
; + } + } + + const decorator = connect(state => state); + const Decorated = decorator(Container); + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const decorated = TestUtils.findRenderedComponentWithType(tree, Decorated); + + expect(() => decorated.someInstanceMethod()).toThrow(); + expect(decorated.getUnderlyingRef().someInstanceMethod()).toBe(someData); + }); }); }); diff --git a/test/components/provide.spec.js b/test/components/provide.spec.js index add035f15..ea328d9a4 100644 --- a/test/components/provide.spec.js +++ b/test/components/provide.spec.js @@ -21,7 +21,7 @@ describe('React', () => { } it('should wrap the component into Provider', () => { - const store = createStore({}); + const store = createStore(() => ({})); @provide(store) class Container extends Component { @@ -42,7 +42,7 @@ describe('React', () => { }); it('sets the displayName correctly', () => { - @provide(createStore({})) + @provide(createStore(() => ({}))) class Container extends Component { render() { return
; diff --git a/test/utils/wrapActionCreators.js b/test/utils/wrapActionCreators.js new file mode 100644 index 000000000..af31ce4d6 --- /dev/null +++ b/test/utils/wrapActionCreators.js @@ -0,0 +1,31 @@ +import expect from 'expect'; +import wrapActionCreators from '../../src/utils/wrapActionCreators'; + +describe('Utils', () => { + describe('wrapActionCreators', () => { + it('should return a function that wraps argument in a call to bindActionCreators', () => { + + function dispatch(action) { + return { + dispatched: action + }; + } + + const actionResult = {an: 'action'}; + + const actionCreators = { + action: () => actionResult + }; + + const wrapped = wrapActionCreators(actionCreators); + expect(wrapped).toBeA(Function); + expect(() => wrapped(dispatch)).toNotThrow(); + expect(() => wrapped().action()).toThrow(); + + const bound = wrapped(dispatch); + expect(bound.action).toNotThrow(); + expect(bound.action().dispatched).toBe(actionResult); + + }); + }); +});