;
- }
-
- 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);
+
+ });
+ });
+});