From df35416fcb838670129d6918c0e8c4f5725fd2a0 Mon Sep 17 00:00:00 2001 From: nanovazquez Date: Sun, 19 Jun 2016 12:53:40 -0300 Subject: [PATCH] Implement redux-connect for server-side rendering --- package.json | 6 ++++ src/App.js | 3 +- src/components/Home/Home.js | 42 +++++++++--------------- src/containers/Home/Home.js | 13 ++++++-- src/containers/async.js | 21 ++++++++++++ src/domains/index.js | 5 ++- src/services/featureService.js | 2 ++ src/services/productService.js | 2 ++ src/store/configureStore.js | 9 ++--- test/index.js | 13 ++++++-- test/src/components/Home.spec.js | 2 -- test/src/services/featureService.spec.js | 32 ++++++++++++++++++ test/src/services/productService.spec.js | 32 ++++++++++++++++++ webpack/configs/config.test.js | 4 ++- 14 files changed, 143 insertions(+), 43 deletions(-) create mode 100644 src/containers/async.js create mode 100644 test/src/services/featureService.spec.js create mode 100644 test/src/services/productService.spec.js diff --git a/package.json b/package.json index f6fb003..3173b85 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "autoprefixer": "^6.3.6", "config": "^1.21.0", + "isomorphic-fetch": "^2.2.1", "normalizr": "^2.1.0", "postcss-import": "^8.1.2", "postcss-nested": "^1.0.0", @@ -42,6 +43,7 @@ "react-router": "^2.4.1", "redux": "^3.5.2", "redux-actions": "^0.10.0", + "redux-connect": "^2.4.0", "redux-promise": "^0.5.3", "redux-thunk": "^2.1.0", "reselect": "^2.5.1", @@ -62,7 +64,9 @@ "babel-preset-es2015": "^6.9.0", "babel-preset-react": "^6.5.0", "chai": "^3.5.0", + "chai-as-promised": "^5.3.0", "chai-enzyme": "^0.5.0", + "chai-things": "^0.2.0", "cheerio": "^0.20.0", "coveralls": "^2.11.9", "css-loader": "^0.23.1", @@ -91,6 +95,8 @@ "react-transform-catch-errors": "^1.0.2", "react-transform-hmr": "^1.0.4", "redbox-react": "^1.2.5", + "sinon": "^1.17.4", + "sinon-chai": "^2.8.0", "style-loader": "^0.13.1", "webpack-dev-middleware": "^1.6.1", "webpack-dev-server": "^1.14.1", diff --git a/src/App.js b/src/App.js index c2f0b1f..5ef1708 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,7 @@ import 'babel-polyfill'; import React from 'react'; import { Router, Route, browserHistory } from 'react-router'; import { Provider } from 'react-redux'; +import { ReduxAsyncConnect } from 'redux-connect'; import { Home } from 'containers'; import { configureStore } from './store'; @@ -13,7 +14,7 @@ const App = ({ const store = configureStore({ initialState, enhancers }); return ( - + }> diff --git a/src/components/Home/Home.js b/src/components/Home/Home.js index be75f71..7b26d2f 100644 --- a/src/components/Home/Home.js +++ b/src/components/Home/Home.js @@ -14,37 +14,27 @@ const Home = ({ isSelected, matchingProducts, match, - fetchFeatures, - fetchProducts, selectFeature, ignoreFeature -}) => { - if (!allFeatures) { - // Initialize features and products - fetchFeatures(); - fetchProducts(); - } - - return ( -
-
-
-
-

Mule match

-

Find the right app for you with this simple swiping app!

-
+}) => ( +
+
+
+
+

Mule match

+

Find the right app for you with this simple swiping app!

-
-
- - -
- +
+
+
+ +
- +
- ); -}; + +
+); Home.propTypes = propTypes; export default Home; diff --git a/src/containers/Home/Home.js b/src/containers/Home/Home.js index ca1bd05..51e98d5 100644 --- a/src/containers/Home/Home.js +++ b/src/containers/Home/Home.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; import { Home } from 'components'; +import async from 'containers/async'; import { actions, selectors @@ -14,10 +15,16 @@ const homeState = (state) => ({ }); const homeActions = (dispatch) => ({ - fetchFeatures: (payload) => dispatch(actions.fetchFeatures(payload)), - fetchProducts: (payload) => dispatch(actions.fetchProducts(payload)), selectFeature: (payload) => dispatch(actions.selectFeature(payload)), ignoreFeature: (payload) => dispatch(actions.ignoreFeature(payload)) }); -export default connect(homeState, homeActions)(Home); +const resolve = ({ dispatch }) => { + // Initialize features and products + const featuresPromise = dispatch(actions.fetchFeatures()); + const productsPromise = dispatch(actions.fetchProducts()); + + return Promise.all([featuresPromise, productsPromise]); +}; + +export default async(resolve)(connect(homeState, homeActions)(Home)); diff --git a/src/containers/async.js b/src/containers/async.js new file mode 100644 index 0000000..85d7ea0 --- /dev/null +++ b/src/containers/async.js @@ -0,0 +1,21 @@ +import { asyncConnect } from 'redux-connect'; + +const defaultPromise = Promise.resolve(); + +const async = ( + resolve = defaultPromise +) => ( + asyncConnect([{ + promise: (options) => { + const payload = { + ...options, + getState: options.store.getState, + dispatch: options.store.dispatch + }; + + return resolve(payload); + } + }]) +); + +export default async; diff --git a/src/domains/index.js b/src/domains/index.js index fc87d51..7d9b927 100644 --- a/src/domains/index.js +++ b/src/domains/index.js @@ -1,4 +1,3 @@ -import { combineReducers } from 'redux'; import { actions as featuresActions, actionTypes as featuresActionTypes, @@ -32,11 +31,11 @@ const selectors = { ...uiSelectors }; -const reducers = combineReducers({ +const reducers = { features: featuresReducers, products: productsReducers, ui: uiReducers -}); +}; export { actionTypes, diff --git a/src/services/featureService.js b/src/services/featureService.js index c279030..f869436 100644 --- a/src/services/featureService.js +++ b/src/services/featureService.js @@ -1,3 +1,5 @@ +import 'isomorphic-fetch'; + const baseUri = CONFIG.BACKEND.URI; const featureService = { diff --git a/src/services/productService.js b/src/services/productService.js index e15e194..602105e 100644 --- a/src/services/productService.js +++ b/src/services/productService.js @@ -1,3 +1,5 @@ +import 'isomorphic-fetch'; + const baseUri = CONFIG.BACKEND.URI; const productService = { diff --git a/src/store/configureStore.js b/src/store/configureStore.js index 3ced132..d8082bd 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -1,6 +1,7 @@ -import { applyMiddleware, createStore, compose } from 'redux'; -import promiseMiddleware from 'redux-promise'; -import { reducers } from 'domains'; +import { applyMiddleware, createStore, compose, combineReducers } from 'redux'; +import promiseMiddleware from 'redux-promise'; +import { reducer as reduxAsyncReducer } from 'redux-connect'; +import { reducers } from 'domains'; const defaultState = {}; const defaultEnhancers = applyMiddleware(promiseMiddleware); @@ -10,7 +11,7 @@ const configureStore = ({ enhancers = [] }) => ( createStore( - reducers, + combineReducers({ ...reducers, reduxAsyncConnect: reduxAsyncReducer }), initialState, compose(defaultEnhancers, enhancers) ) diff --git a/test/index.js b/test/index.js index 596925d..cf04c17 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,9 @@ import 'babel-polyfill'; -import chai from 'chai'; -import chaiEnzyme from 'chai-enzyme'; +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinonChai from 'sinon-chai'; +import chaiThings from 'chai-things'; +import chaiEnzyme from 'chai-enzyme'; // To create a code coverage report for all components you have to require all the sources and test // See https://github.com/deepsweet/isparta-loader @@ -26,5 +29,9 @@ const servicesContext = require.context('../src/services/', false, /index\.js$/) servicesContext.keys().forEach(servicesContext); // Setup libraries used for testing -global.should = chai.should(); +chai.use(chaiAsPromised); +chai.use(sinonChai); +chai.use(chaiThings); chai.use(chaiEnzyme()); + +global.should = chai.should(); diff --git a/test/src/components/Home.spec.js b/test/src/components/Home.spec.js index 7ef12d7..f217b04 100644 --- a/test/src/components/Home.spec.js +++ b/test/src/components/Home.spec.js @@ -20,8 +20,6 @@ describe('Home', () => { isSelected = true; matchingProducts = []; match = {}; - fetchFeatures = () => {}; - fetchProducts = () => {}; selectFeature = () => {}; ignoreFeature = () => {}; component = shallow( diff --git a/test/src/services/featureService.spec.js b/test/src/services/featureService.spec.js new file mode 100644 index 0000000..708a530 --- /dev/null +++ b/test/src/services/featureService.spec.js @@ -0,0 +1,32 @@ +import sinon from 'sinon'; +import { featureService } from 'services'; + +describe('featureService', () => { + let response; + let expectedResult; + let mockResponseJson; + let mockFetch; + let result; + + beforeEach(() => { + response = { json: () => {} }; + expectedResult = { status: 200, data: 'result' }; + + mockResponseJson = sinon.stub(response, 'json').returns(Promise.resolve(expectedResult)); + mockFetch = sinon.stub(window, 'fetch').returns(Promise.resolve(response)); + }); + + afterEach(() => { + window.fetch.restore(); + }); + + describe('getFeatures', () => { + beforeEach(async () => { + result = await featureService.getFeatures(); + }); + + it('should call fetch to retrieve the features', () => mockFetch.should.have.been.called); + it('should convert the result to JSON', () => mockResponseJson.should.have.been.called); + it('should retrieve the result', () => result.should.be.equal(expectedResult)); + }); +}); diff --git a/test/src/services/productService.spec.js b/test/src/services/productService.spec.js new file mode 100644 index 0000000..6c8fff5 --- /dev/null +++ b/test/src/services/productService.spec.js @@ -0,0 +1,32 @@ +import sinon from 'sinon'; +import { productService } from 'services'; + +describe('productService', () => { + let response; + let expectedResult; + let mockResponseJson; + let mockFetch; + let result; + + beforeEach(() => { + response = { json: () => {} }; + expectedResult = { status: 200, data: 'result' }; + + mockResponseJson = sinon.stub(response, 'json').returns(Promise.resolve(expectedResult)); + mockFetch = sinon.stub(window, 'fetch').returns(Promise.resolve(response)); + }); + + afterEach(() => { + window.fetch.restore(); + }); + + describe('getProducts', () => { + beforeEach(async () => { + result = await productService.getProducts(); + }); + + it('should call fetch to retrieve the items', () => mockFetch.should.have.been.called); + it('should convert the result to JSON', () => mockResponseJson.should.have.been.called); + it('should retrieve the result', () => result.should.be.equal(expectedResult)); + }); +}); diff --git a/webpack/configs/config.test.js b/webpack/configs/config.test.js index 30c298a..3b9731d 100644 --- a/webpack/configs/config.test.js +++ b/webpack/configs/config.test.js @@ -4,7 +4,9 @@ var webpack = require('webpack'); var aliases = require('../aliases'); var rootPath = path.resolve(__dirname, '../../'); -aliases['test'] = 'src/test/src'; +// Add new keys for tests +aliases['test'] = 'src/test/src'; +aliases['sinon'] = 'sinon/pkg/sinon', module.exports = { devtool: 'inline-source-map',