Skip to content

Commit 9c3b68f

Browse files
committed
Merge pull request reduxjs#75 from gaearon/component-tests
WIP: Component tests
2 parents 067eff9 + 21243e4 commit 9c3b68f

File tree

10 files changed

+340
-15
lines changed

10 files changed

+340
-15
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
"eslint-plugin-react": "^2.3.0",
4444
"expect": "^1.6.0",
4545
"istanbul": "^0.3.15",
46+
"jsdom": "~5.4.3",
4647
"mocha": "^2.2.5",
48+
"mocha-jsdom": "~0.4.0",
4749
"react": "^0.13.0",
4850
"react-hot-loader": "^1.2.7",
4951
"rimraf": "^2.3.4",

src/components/createConnectDecorator.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import getDisplayName from '../utils/getDisplayName';
22
import shallowEqualScalar from '../utils/shallowEqualScalar';
33

44
export default function createConnectDecorator(React, Connector) {
5+
const { Component } = React;
6+
57
return function connect(select) {
6-
return DecoratedComponent => class ConnectorDecorator {
8+
return DecoratedComponent => class ConnectorDecorator extends Component {
79
static displayName = `Connector(${getDisplayName(DecoratedComponent)})`;
810

911
shouldComponentUpdate(nextProps) {
@@ -13,14 +15,10 @@ export default function createConnectDecorator(React, Connector) {
1315
render() {
1416
return (
1517
<Connector select={state => select(state, this.props)}>
16-
{this.renderChild}
18+
{stuff => <DecoratedComponent {...stuff} {...this.props} />}
1719
</Connector>
1820
);
1921
}
20-
21-
renderChild(state) {
22-
return <DecoratedComponent {...state} />;
23-
}
2422
};
2523
};
2624
}

src/components/createConnector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default function createConnector(React) {
7171
const { redux } = this.context;
7272

7373
return children({
74-
dispatch: ::redux.dispatch,
74+
dispatch: redux.dispatch,
7575
...slice
7676
});
7777
}

src/components/createProvideDecorator.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import getDisplayName from '../utils/getDisplayName';
22

33
export default function createProvideDecorator(React, Provider) {
4+
const { Component } = React;
5+
46
return function provide(redux) {
5-
return DecoratedComponent => class ProviderDecorator {
7+
return DecoratedComponent => class ProviderDecorator extends Component {
68
static displayName = `Provider(${getDisplayName(DecoratedComponent)})`;
79

810
render() {
911
return (
1012
<Provider redux={redux}>
11-
{props => <DecoratedComponent {...this.props} {...props} />}
13+
{() => <DecoratedComponent {...this.props} />}
1214
</Provider>
1315
);
1416
}

src/react-native.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import React from 'react-native';
22
import createAll from './components/createAll';
33

4-
const { Provider, Connector, provide, connect } = createAll(React);
5-
6-
export { Provider, Connector, provide, connect };
4+
export const { Provider, Connector, provide, connect } = createAll(React);

src/react.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import React from 'react';
22
import createAll from './components/createAll';
33

4-
const { Provider, Connector, provide, connect } = createAll(React);
5-
6-
export { Provider, Connector, provide, connect };
4+
export const { Provider, Connector, provide, connect } = createAll(React);

test/components/Connector.spec.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import expect from 'expect';
2+
import jsdom from 'mocha-jsdom';
3+
import React, { PropTypes, Component } from 'react/addons';
4+
import { createRedux } from '../../src';
5+
import { Connector } from '../../src/react';
6+
7+
const { TestUtils } = React.addons;
8+
9+
describe('React', () => {
10+
describe('Connector', () => {
11+
jsdom();
12+
13+
// Mock minimal Provider interface
14+
class Provider extends Component {
15+
static childContextTypes = {
16+
redux: PropTypes.object.isRequired
17+
}
18+
19+
getChildContext() {
20+
return { redux: this.props.redux };
21+
}
22+
23+
render() {
24+
return this.props.children();
25+
}
26+
}
27+
28+
const stringBuilder = (prev = '', action) => {
29+
return action.type === 'APPEND'
30+
? prev + action.body
31+
: prev;
32+
};
33+
34+
it('gets Redux from context', () => {
35+
const redux = createRedux({ test: () => 'test' });
36+
37+
const tree = TestUtils.renderIntoDocument(
38+
<Provider redux={redux}>
39+
{() => (
40+
<Connector>
41+
{() => <div />}
42+
</Connector>
43+
)}
44+
</Provider>
45+
);
46+
47+
const connector = TestUtils.findRenderedComponentWithType(tree, Connector);
48+
expect(connector.context.redux).toBe(redux);
49+
});
50+
51+
it('subscribes to Redux changes', () => {
52+
const redux = createRedux({ string: stringBuilder });
53+
54+
const tree = TestUtils.renderIntoDocument(
55+
<Provider redux={redux}>
56+
{() => (
57+
<Connector select={state => ({ string: state.string })}>
58+
{({ string }) => <div string={string} />}
59+
</Connector>
60+
)}
61+
</Provider>
62+
);
63+
64+
const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div');
65+
expect(div.props.string).toBe('');
66+
redux.dispatch({ type: 'APPEND', body: 'a'});
67+
expect(div.props.string).toBe('a');
68+
redux.dispatch({ type: 'APPEND', body: 'b'});
69+
expect(div.props.string).toBe('ab');
70+
});
71+
72+
it('unsubscribes before unmounting', () => {
73+
const redux = createRedux({ test: () => 'test' });
74+
const subscribe = redux.subscribe;
75+
76+
// Keep track of unsubscribe by wrapping `subscribe()`
77+
const spy = expect.createSpy(() => {});
78+
redux.subscribe = (listener) => {
79+
const unsubscribe = subscribe(listener);
80+
return () => {
81+
spy();
82+
return unsubscribe();
83+
};
84+
};
85+
86+
const tree = TestUtils.renderIntoDocument(
87+
<Provider redux={redux}>
88+
{() => (
89+
<Connector select={state => ({ string: state.string })}>
90+
{({ string }) => <div string={string} />}
91+
</Connector>
92+
)}
93+
</Provider>
94+
);
95+
96+
const connector = TestUtils.findRenderedComponentWithType(tree, Connector);
97+
expect(spy.calls.length).toBe(0);
98+
connector.componentWillUnmount();
99+
expect(spy.calls.length).toBe(1);
100+
});
101+
102+
it('shallow compares selected state to prevent unnecessary updates', () =>{
103+
const redux = createRedux({ string: stringBuilder });
104+
const spy = expect.createSpy(() => {});
105+
function render({ string }) {
106+
spy();
107+
return <div string={string}/>;
108+
}
109+
110+
const tree = TestUtils.renderIntoDocument(
111+
<Provider redux={redux}>
112+
{() => (
113+
<Connector select={state => ({ string: state.string })}>
114+
{render}
115+
</Connector>
116+
)}
117+
</Provider>
118+
);
119+
120+
const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div');
121+
expect(spy.calls.length).toBe(1);
122+
expect(div.props.string).toBe('');
123+
redux.dispatch({ type: 'APPEND', body: 'a'});
124+
expect(spy.calls.length).toBe(2);
125+
redux.dispatch({ type: 'APPEND', body: 'b'});
126+
expect(spy.calls.length).toBe(3);
127+
redux.dispatch({ type: 'APPEND', body: ''});
128+
expect(spy.calls.length).toBe(3);
129+
});
130+
131+
it('passes `dispatch()` to child function', () => {
132+
const redux = createRedux({ test: () => 'test' });
133+
134+
const tree = TestUtils.renderIntoDocument(
135+
<Provider redux={redux}>
136+
{() => (
137+
<Connector>
138+
{({ dispatch }) => <div dispatch={dispatch} />}
139+
</Connector>
140+
)}
141+
</Provider>
142+
);
143+
144+
const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div');
145+
expect(div.props.dispatch).toBe(redux.dispatch);
146+
});
147+
});
148+
});

test/components/Provider.spec.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import expect from 'expect';
2+
import jsdom from 'mocha-jsdom';
3+
import React, { PropTypes, Component } from 'react/addons';
4+
import { createRedux } from '../../src';
5+
import { Provider } from '../../src/react';
6+
7+
const { TestUtils } = React.addons;
8+
9+
describe('React', () => {
10+
describe('Provider', () => {
11+
jsdom();
12+
13+
class Child extends Component {
14+
static contextTypes = {
15+
redux: PropTypes.object.isRequired
16+
}
17+
18+
render() {
19+
return <div />;
20+
}
21+
}
22+
23+
it('adds Redux to child context', () => {
24+
const redux = createRedux({ test: () => 'test' });
25+
26+
const tree = TestUtils.renderIntoDocument(
27+
<Provider redux={redux}>
28+
{() => <Child />}
29+
</Provider>
30+
);
31+
32+
const child = TestUtils.findRenderedComponentWithType(tree, Child);
33+
expect(child.context.redux).toBe(redux);
34+
});
35+
36+
it('does not lose subscribers when receiving new props', () => {
37+
const redux1 = createRedux({ test: () => 'test' });
38+
const redux2 = createRedux({ test: () => 'test' });
39+
const spy = expect.createSpy(() => {});
40+
41+
class ProviderContainer extends Component {
42+
state = { redux: redux1 };
43+
44+
render() {
45+
return (
46+
<Provider redux={this.state.redux}>
47+
{() => <Child />}
48+
</Provider>
49+
);
50+
}
51+
}
52+
53+
const container = TestUtils.renderIntoDocument(<ProviderContainer />);
54+
const child = TestUtils.findRenderedComponentWithType(container, Child);
55+
56+
child.context.redux.subscribe(spy);
57+
child.context.redux.dispatch({});
58+
expect(spy.calls.length).toEqual(1);
59+
60+
container.setState({ redux: redux2 });
61+
expect(spy.calls.length).toEqual(2);
62+
child.context.redux.dispatch({});
63+
expect(spy.calls.length).toEqual(3);
64+
});
65+
});
66+
});

test/components/connect.spec.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import expect from 'expect';
2+
import jsdom from 'mocha-jsdom';
3+
import React, { PropTypes, Component } from 'react/addons';
4+
import { createRedux } from '../../src';
5+
import { connect, Connector } from '../../src/react';
6+
7+
const { TestUtils } = React.addons;
8+
9+
describe('React', () => {
10+
describe('provide', () => {
11+
jsdom();
12+
13+
// Mock minimal Provider interface
14+
class Provider extends Component {
15+
static childContextTypes = {
16+
redux: PropTypes.object.isRequired
17+
}
18+
19+
getChildContext() {
20+
return { redux: this.props.redux };
21+
}
22+
23+
render() {
24+
return this.props.children();
25+
}
26+
}
27+
28+
it('wraps component with Provider', () => {
29+
const redux = createRedux({ test: () => 'test' });
30+
31+
@connect(state => state)
32+
class Container extends Component {
33+
render() {
34+
return <div {...this.props} />;
35+
}
36+
}
37+
38+
const container = TestUtils.renderIntoDocument(
39+
<Provider redux={redux}>
40+
{() => <Container pass="through" />}
41+
</Provider>
42+
);
43+
const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div');
44+
expect(div.props.pass).toEqual('through');
45+
expect(div.props.test).toEqual('test');
46+
expect(() => TestUtils.findRenderedComponentWithType(container, Connector))
47+
.toNotThrow();
48+
});
49+
50+
it('sets displayName correctly', () => {
51+
@connect(state => state)
52+
class Container extends Component {
53+
render() {
54+
return <div />;
55+
}
56+
}
57+
58+
expect(Container.displayName).toBe('Connector(Container)');
59+
});
60+
});
61+
});

0 commit comments

Comments
 (0)