Skip to content

Commit db40ae1

Browse files
committed
add tests and some improvements
Description: - add tests based on 'mocha', 'enzyme' and 'expect' packages - functionality improvements: allow action dispatchers to handle more than one argument. also, raise meaningful errors when something was not done right
1 parent f43ba49 commit db40ae1

File tree

7 files changed

+490
-23
lines changed

7 files changed

+490
-23
lines changed

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"build": "babel src --out-dir lib",
88
"clear": "rimraf lib",
99
"rebuild": "npm run clear && npm run build",
10-
"test": "echo \"Error: no test specified\" && exit 1"
10+
"test": "mocha --compilers js:babel-register --recursive --require ./test/setup.js",
11+
"test:watch": "npm test -- --watch"
1112
},
1213
"repository": {
1314
"type": "git",
@@ -32,7 +33,14 @@
3233
"babel-preset-es2015": "^6.16.0",
3334
"babel-preset-react": "^6.16.0",
3435
"babel-preset-stage-1": "^6.16.0",
36+
"enzyme": "^2.4.1",
37+
"expect": "^1.20.2",
38+
"jsdom": "^9.5.0",
39+
"mocha": "^3.1.0",
3540
"react": "^15.3.2",
41+
"react-addons-pure-render-mixin": "^15.3.2",
42+
"react-addons-test-utils": "^15.3.2",
43+
"react-dom": "^15.3.2",
3644
"redux": "^3.6.0",
3745
"rimraf": "^2.5.4"
3846
},

src/components/Connector.jsx

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
import React, { PropTypes, Component } from 'react';
22
import PureRenderMixin from 'react-addons-pure-render-mixin';
33
import get from 'lodash/get';
4+
import isPlainObject from 'lodash/isPlainObject';
45
import storeShape from '../utils/storeShape';
56

67
const ownProps = Object.getOwnPropertyNames;
78
const proto = Object.getPrototypeOf;
89

10+
function generateDispatchers(actionNames) {
11+
actionNames.forEach(name => {
12+
const type = this.action(name);
13+
if (typeof this.prototype[`$${name}`] !== 'function') {
14+
this.prototype[`$${name}`] = function() {
15+
this.dispatch(type, ...arguments);
16+
};
17+
}
18+
});
19+
}
20+
921
export default class Connector extends Component {
1022
static reduce(namespace, stateToHandlers) {
1123
this.$namespace = namespace;
1224
const initialState = this.$state;
1325
const actionNames = ownProps(stateToHandlers());
14-
this.__generateDispatchers(actionNames);
26+
generateDispatchers.call(this, actionNames);
1527

1628
return function(state = initialState, action) {
29+
if (!action.type) {
30+
return state;
31+
}
32+
1733
if (action.type === `${namespace}/$reset`) {
1834
return initialState;
1935
}
@@ -23,24 +39,13 @@ export default class Connector extends Component {
2339
if (actionNamespace === namespace) {
2440
const handler = stateToHandlers(state)[actionType];
2541
if (handler) {
26-
return handler(action.data);
42+
return handler(...action.args);
2743
}
2844
}
2945
return state;
3046
};
3147
}
3248

33-
static __generateDispatchers(actionNames) {
34-
actionNames.forEach(name => {
35-
const type = this.action(name);
36-
if (typeof this.prototype[`$${name}`] !== 'function') {
37-
this.prototype[`$${name}`] = function(data) {
38-
this.dispatch(type, data);
39-
};
40-
}
41-
});
42-
}
43-
4449
static $namespace = 'global';
4550

4651
static action(name) {
@@ -62,6 +67,11 @@ export default class Connector extends Component {
6267
constructor(props, context) {
6368
super(props, context);
6469
this.store = props.store || context.store;
70+
71+
if (!this.store) {
72+
throw new Error(`${this.constructor.name} instance expects store object in props or in context`);
73+
}
74+
6575
this.state = this.getExposedState();
6676

6777
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
@@ -101,11 +111,17 @@ export default class Connector extends Component {
101111

102112
getExposedState() {
103113
const state = this.store.getState();
104-
return this.$expose(get(state, this.constructor.$namespace) || this.constructor.$state, state);
114+
const result = this.$expose(get(state, this.constructor.$namespace) || this.constructor.$state, state);
115+
if (!isPlainObject(result)) {
116+
throw new Error(`${this.constructor.name}.$state should be a plain object` +
117+
'or there should be a $expose instance method defined that returns a plain object'
118+
);
119+
}
120+
return result;
105121
}
106122

107-
dispatch(type, data) {
108-
return this.store.dispatch({ type, data });
123+
dispatch(type, ...args) {
124+
return this.store.dispatch({ type, args });
109125
}
110126

111127
$expose($state) {
@@ -126,6 +142,12 @@ export default class Connector extends Component {
126142
}
127143

128144
render() {
129-
return React.createElement(this.getConnection(), { ...this.props, ...this.state });
145+
const connectionType = this.getConnection();
146+
147+
if (!connectionType) {
148+
throw new Error(`${this.constructor.name} should define a $connection class property`);
149+
}
150+
151+
return React.createElement(connectionType, { ...this.props, ...this.state });
130152
}
131153
}

src/components/Reductor.jsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import storeShape from '../utils/storeShape';
1010
export default class Reductor extends Component {
1111
static propTypes = {
1212
createStore: PropTypes.func.isRequired,
13-
children: PropTypes.element.isRequired
13+
connectorProp: PropTypes.string,
14+
children: PropTypes.node.isRequired
15+
};
16+
17+
static defaultProps = {
18+
connectorProp: 'component'
1419
};
1520

1621
static childContextTypes = {
@@ -30,8 +35,8 @@ export default class Reductor extends Component {
3035
getConnectors(childrenToProcess = this.props.children) {
3136
const tmp = Children.map(childrenToProcess, (child) => {
3237
const connectors = [];
33-
const { component, children } = child.props;
34-
if (component && component.$namespace) {
38+
const { [this.props.connectorProp]: component, children } = (child.props || {});
39+
if (component && component.$reducer) {
3540
connectors.push(component);
3641
}
3742
if (children) {
@@ -45,7 +50,7 @@ export default class Reductor extends Component {
4550
}
4651

4752
getReducer() {
48-
const connectors = groupBy(this.getConnectors().filter(c => c.$reducer), '$namespace');
53+
const connectors = groupBy(this.getConnectors(), '$namespace');
4954

5055
return function(state = {}, action) {
5156
const newState = {};
@@ -61,6 +66,6 @@ export default class Reductor extends Component {
6166
}
6267

6368
render() {
64-
return this.props.children;
69+
return <div>{ this.props.children }</div>;
6570
}
6671
}

test/components/Connection.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
import { createStore } from 'redux';
3+
import { Connector, Connection } from '../../src';
4+
import { mount } from 'enzyme';
5+
import expect from 'expect';
6+
7+
describe('<Connection />', function() {
8+
class TestConnection extends Connection {
9+
load() {
10+
return this.$load([1, 2, 3])
11+
}
12+
render() {
13+
return <div className="connection" onClick={() => this.load()} />
14+
}
15+
}
16+
17+
class TestConnector extends Connector {
18+
static $connection = TestConnection;
19+
static $state = []
20+
static $reducer = TestConnector.reduce('foo', (state) => ({
21+
$receive: (items) => items
22+
}));
23+
$expose($state) { return { items: $state } }
24+
$load(items) { return this.$$receive(items) }
25+
}
26+
27+
it('has event-handler methods (that start with $) provided by Connector', function() {
28+
const store = createStore(TestConnector.$reducer);
29+
const wrapper = mount(<TestConnector store={store} />);
30+
wrapper.find('.connection').simulate('click');
31+
expect(store.getState()).toEqual([1, 2, 3]);
32+
});
33+
});

0 commit comments

Comments
 (0)