├── src ├── static │ ├── robots.txt │ ├── favicon.ico │ └── humans.txt ├── components │ └── Header │ │ ├── index.js │ │ ├── Header.scss │ │ └── Header.js ├── layouts │ └── CoreLayout │ │ ├── CoreLayout.scss │ │ ├── index.js │ │ └── CoreLayout.js ├── routes │ ├── Home │ │ ├── components │ │ │ ├── HomeView.scss │ │ │ └── HomeView.js │ │ ├── assets │ │ │ └── Duck.jpg │ │ └── index.js │ ├── Counter │ │ ├── components │ │ │ └── Counter.js │ │ ├── index.js │ │ ├── containers │ │ │ └── CounterContainer.js │ │ └── modules │ │ │ └── counter.js │ └── index.js ├── styles │ ├── _base.scss │ └── core.scss ├── index.html ├── store │ ├── reducers.js │ ├── location.js │ └── createStore.js ├── containers │ └── AppContainer.js └── main.js ├── .eslintignore ├── .gitignore ├── tests ├── e2e │ ├── pages │ │ ├── page.js │ │ ├── homePage.js │ │ └── counterPage.js │ ├── specs │ │ └── testSuite.spec.js │ └── wdio.conf.js ├── .eslintrc └── unit │ ├── routes │ ├── Home │ │ ├── index.spec.js │ │ └── components │ │ │ └── HomeView.spec.js │ └── Counter │ │ ├── index.spec.js │ │ ├── components │ │ └── Counter.spec.js │ │ └── modules │ │ └── counter.spec.js │ ├── store │ ├── createStore.spec.js │ └── location.spec.js │ ├── layouts │ └── CoreLayout.spec.js │ ├── components │ └── Header │ │ └── Header.spec.js │ └── test-bundler.js ├── bin ├── server.js └── compile.js ├── .travis.yml ├── .eslintrc ├── .editorconfig ├── LICENSE ├── config ├── environments.js └── index.js ├── CONTRIBUTING.md ├── server └── main.js ├── package.json ├── README.md └── CHANGELOG.md /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | 3 | export default Header 4 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.scss: -------------------------------------------------------------------------------- 1 | .core-layout__viewport { 2 | padding-top: 4rem; 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | blueprints/**/files/** 2 | coverage/** 3 | node_modules/** 4 | dist/** 5 | src/index.html 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | node_modules 4 | dist 5 | coverage 6 | .idea/ 7 | test/screenshots/ 8 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout' 2 | 3 | export default CoreLayout 4 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .route--active { 2 | font-weight: bold; 3 | text-decoration: underline; 4 | } 5 | -------------------------------------------------------------------------------- /src/routes/Home/components/HomeView.scss: -------------------------------------------------------------------------------- 1 | .duck { 2 | display: block; 3 | width: 120px; 4 | margin: 1.5rem auto; 5 | } 6 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/react-redux-webdriverIO-starter-kit/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/routes/Home/assets/Duck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/react-redux-webdriverIO-starter-kit/HEAD/src/routes/Home/assets/Duck.jpg -------------------------------------------------------------------------------- /src/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | import HomeView from './components/HomeView' 2 | 3 | // Sync route definition 4 | export default { 5 | component : HomeView 6 | } 7 | -------------------------------------------------------------------------------- /src/static/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | 3 | # TEAM 4 | 5 | -- -- 6 | 7 | # THANKS 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/e2e/pages/page.js: -------------------------------------------------------------------------------- 1 | function Page() { 2 | } 3 | 4 | Page.prototype.open = function (path) { 5 | browser.url('/' + path) 6 | } 7 | 8 | module.exports = new Page() 9 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | const config = require('../config') 2 | const server = require('../server/main') 3 | const debug = require('debug')('app:bin:server') 4 | const port = config.server_port 5 | 6 | server.listen(port) 7 | debug(`Server is now running at http://localhost:${port}.`) 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm run deploy:dev 15 | - npm run deploy:prod 16 | 17 | after_success: 18 | - npm run codecov 19 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Application Settings Go Here 3 | ------------------------------------ 4 | This file acts as a bundler for all variables/mixins/themes, so they 5 | can easily be swapped out without `core.scss` ever having to know. 6 | 7 | For example: 8 | 9 | @import './variables/colors'; 10 | @import './variables/components'; 11 | @import './themes/default'; 12 | */ 13 | -------------------------------------------------------------------------------- /src/routes/Home/components/HomeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DuckImage from '../assets/Duck.jpg' 3 | import './HomeView.scss' 4 | 5 | export const HomeView = () => ( 6 |
7 |

Welcome!

8 | This is a duck, because Redux! 12 |
13 | ) 14 | 15 | export default HomeView 16 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import '~normalize.css/normalize'; 3 | 4 | // Some best-practice CSS that's useful for most apps 5 | // Just remove them if they're not what you want 6 | html { 7 | box-sizing: border-box; 8 | } 9 | 10 | html, 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | height: 100%; 15 | } 16 | 17 | *, 18 | *:before, 19 | *:after { 20 | box-sizing: inherit; 21 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Redux WebdriverIO Starter Kit 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/unit/routes/Home/index.spec.js: -------------------------------------------------------------------------------- 1 | import HomeRoute from 'routes/Home' 2 | 3 | describe('(Route) Home', () => { 4 | let _component 5 | 6 | beforeEach(() => { 7 | _component = HomeRoute.component() 8 | }) 9 | 10 | it('Should return a route configuration object', () => { 11 | expect(typeof HomeRoute).to.equal('object') 12 | }) 13 | 14 | it('Should define a route component', () => { 15 | expect(_component.type).to.equal('div') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/unit/routes/Counter/index.spec.js: -------------------------------------------------------------------------------- 1 | import CounterRoute from 'routes/Counter' 2 | 3 | describe('(Route) Counter', () => { 4 | let _route 5 | 6 | beforeEach(() => { 7 | _route = CounterRoute({}) 8 | }) 9 | 10 | it('Should return a route configuration object', () => { 11 | expect(typeof _route).to.equal('object') 12 | }) 13 | 14 | it('Configuration should contain path `counter`', () => { 15 | expect(_route.path).to.equal('counter') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IndexLink, Link } from 'react-router' 3 | import './Header.scss' 4 | 5 | export const Header = () => ( 6 |
7 |

React Redux WebdriverIO Starter Kit

8 | 9 | Home 10 | 11 | {' · '} 12 | 13 | Counter 14 | 15 |
16 | ) 17 | 18 | export default Header 19 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../../components/Header' 3 | import './CoreLayout.scss' 4 | import '../../styles/core.scss' 5 | 6 | export const CoreLayout = ({ children }) => ( 7 |
8 |
9 |
10 | {children} 11 |
12 |
13 | ) 14 | 15 | CoreLayout.propTypes = { 16 | children : React.PropTypes.element.isRequired 17 | } 18 | 19 | export default CoreLayout 20 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import locationReducer from './location' 3 | 4 | export const makeRootReducer = (asyncReducers) => { 5 | return combineReducers({ 6 | location: locationReducer, 7 | ...asyncReducers 8 | }) 9 | } 10 | 11 | export const injectReducer = (store, { key, reducer }) => { 12 | if (Object.hasOwnProperty.call(store.asyncReducers, key)) return 13 | 14 | store.asyncReducers[key] = reducer 15 | store.replaceReducer(makeRootReducer(store.asyncReducers)) 16 | } 17 | 18 | export default makeRootReducer 19 | -------------------------------------------------------------------------------- /tests/e2e/pages/homePage.js: -------------------------------------------------------------------------------- 1 | var page = require('./page'); 2 | 3 | var homePage = Object.create(page, { 4 | 5 | counterButton: { get: function () { return browser.element('=Counter'); } }, 6 | 7 | validateHomePage: { 8 | value: function () { 9 | browser.url('/'); 10 | var pageTitle = browser.getTitle(); 11 | expect(pageTitle).toBe('React Redux WebdriverIO Starter Kit'); 12 | } 13 | }, 14 | 15 | goToCounterPage: { 16 | value: function () { 17 | this.counterButton.click(); 18 | } 19 | } 20 | 21 | }); 22 | 23 | module.exports = homePage; 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "plugins": [ 8 | "babel", 9 | "react", 10 | "promise" 11 | ], 12 | "env": { 13 | "browser" : true 14 | }, 15 | "globals": { 16 | "__DEV__" : false, 17 | "__TEST__" : false, 18 | "__PROD__" : false, 19 | "__COVERAGE__" : false 20 | }, 21 | "rules": { 22 | "key-spacing" : 0, 23 | "jsx-quotes" : [2, "prefer-single"], 24 | "max-len" : [2, 120, 2], 25 | "object-curly-spacing" : [2, "always"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/routes/Counter/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Counter = (props) => ( 4 |
5 |

Counter: {props.counter}

6 | 9 | {' '} 10 | 13 |
14 | ) 15 | 16 | Counter.propTypes = { 17 | counter : React.PropTypes.number.isRequired, 18 | doubleAsync : React.PropTypes.func.isRequired, 19 | increment : React.PropTypes.func.isRequired 20 | } 21 | 22 | export default Counter 23 | -------------------------------------------------------------------------------- /tests/unit/routes/Home/components/HomeView.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HomeView } from 'routes/Home/components/HomeView' 3 | import { render } from 'enzyme' 4 | 5 | describe('(View) Home', () => { 6 | let _component 7 | 8 | beforeEach(() => { 9 | _component = render() 10 | }) 11 | 12 | it('Renders a welcome message', () => { 13 | const welcome = _component.find('h4') 14 | expect(welcome).to.exist 15 | expect(welcome.text()).to.match(/Welcome!/) 16 | }) 17 | 18 | it('Renders an awesome duck image', () => { 19 | const duck = _component.find('img') 20 | expect(duck).to.exist 21 | expect(duck.attr('alt')).to.match(/This is a duck, because Redux!/) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { browserHistory, Router } from 'react-router' 3 | import { Provider } from 'react-redux' 4 | 5 | class AppContainer extends Component { 6 | static propTypes = { 7 | routes : PropTypes.object.isRequired, 8 | store : PropTypes.object.isRequired 9 | } 10 | 11 | shouldComponentUpdate () { 12 | return false 13 | } 14 | 15 | render () { 16 | const { routes, store } = this.props 17 | 18 | return ( 19 | 20 |
21 | 22 |
23 |
24 | ) 25 | } 26 | } 27 | 28 | export default AppContainer 29 | -------------------------------------------------------------------------------- /tests/e2e/specs/testSuite.spec.js: -------------------------------------------------------------------------------- 1 | var homePage = require('../pages/homePage.js'); 2 | var counterPage = require('../pages/counterPage.js'); 3 | 4 | describe('Welcome Page', function() { 5 | 'use strict'; 6 | 7 | it('When: User accesses the web app, he should land on the home page', function () { 8 | homePage.validateHomePage(); 9 | }); 10 | 11 | it('Then: User wants to navigate to the counter page', function () { 12 | homePage.goToCounterPage(); 13 | }); 14 | 15 | it('Then: User should have landed successfully on the counter page', function () { 16 | counterPage.validateCounterPage(); 17 | }); 18 | 19 | it('When: User clicks on increment button', function () { 20 | counterPage.incrementValue(); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /tests/e2e/pages/counterPage.js: -------------------------------------------------------------------------------- 1 | var page = require('./page'); 2 | 3 | var counterPage = Object.create(page, { 4 | 5 | incrementButton: { get: function () { return browser.element('button=Increment'); } }, 6 | incrementedValue: { get: function () { return browser.element('*=Counter'); } }, 7 | 8 | validateCounterPage: { 9 | value: function () { 10 | var url = browser.getUrl(); 11 | expect(url).toBe("http://localhost:3000/counter"); 12 | } 13 | }, 14 | 15 | incrementValue: { 16 | value: function () { 17 | this.incrementButton.click(); 18 | browser.pause(1000); 19 | this.incrementButton.click(); 20 | browser.pause(1000); 21 | this.incrementButton.click(); 22 | } 23 | } 24 | 25 | 26 | }); 27 | 28 | module.exports = counterPage; 29 | -------------------------------------------------------------------------------- /tests/unit/store/createStore.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as createStore 3 | } from 'store/createStore' 4 | 5 | describe('(Store) createStore', () => { 6 | let store 7 | 8 | before(() => { 9 | store = createStore() 10 | }) 11 | 12 | it('should have an empty asyncReducers object', () => { 13 | expect(store.asyncReducers).to.be.an('object') 14 | expect(store.asyncReducers).to.be.empty 15 | }) 16 | 17 | describe('(Location)', () => { 18 | it('store should be initialized with Location state', () => { 19 | const location = { 20 | pathname : '/echo' 21 | } 22 | store.dispatch({ 23 | type : 'LOCATION_CHANGE', 24 | payload : location 25 | }) 26 | expect(store.getState().location).to.deep.equal(location) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/unit/layouts/CoreLayout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-addons-test-utils' 3 | import CoreLayout from 'layouts/CoreLayout/CoreLayout' 4 | 5 | function shallowRender (component) { 6 | const renderer = TestUtils.createRenderer() 7 | 8 | renderer.render(component) 9 | return renderer.getRenderOutput() 10 | } 11 | 12 | function shallowRenderWithProps (props = {}) { 13 | return shallowRender() 14 | } 15 | 16 | describe('(Layout) Core', function () { 17 | let _component 18 | let _props 19 | let _child 20 | 21 | beforeEach(function () { 22 | _child =

Child

23 | _props = { 24 | children : _child 25 | } 26 | 27 | _component = shallowRenderWithProps(_props) 28 | }) 29 | 30 | it('Should render as a
.', function () { 31 | expect(_component.type).to.equal('div') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /src/routes/Counter/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducers' 2 | 3 | export default (store) => ({ 4 | path : 'counter', 5 | /* Async getComponent is only invoked when route matches */ 6 | getComponent (nextState, cb) { 7 | /* Webpack - use 'require.ensure' to create a split point 8 | and embed an async module loader (jsonp) when bundling */ 9 | require.ensure([], (require) => { 10 | /* Webpack - use require callback to define 11 | dependencies for bundling */ 12 | const Counter = require('./containers/CounterContainer').default 13 | const reducer = require('./modules/counter').default 14 | 15 | /* Add the reducer to the store on key 'counter' */ 16 | injectReducer(store, { key: 'counter', reducer }) 17 | 18 | /* Return getComponent */ 19 | cb(null, Counter) 20 | 21 | /* Webpack named bundle */ 22 | }, 'counter') 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/store/location.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | export const LOCATION_CHANGE = 'LOCATION_CHANGE' 5 | 6 | // ------------------------------------ 7 | // Actions 8 | // ------------------------------------ 9 | export function locationChange (location = '/') { 10 | return { 11 | type : LOCATION_CHANGE, 12 | payload : location 13 | } 14 | } 15 | 16 | // ------------------------------------ 17 | // Specialized Action Creator 18 | // ------------------------------------ 19 | export const updateLocation = ({ dispatch }) => { 20 | return (nextLocation) => dispatch(locationChange(nextLocation)) 21 | } 22 | 23 | // ------------------------------------ 24 | // Reducer 25 | // ------------------------------------ 26 | const initialState = null 27 | export default function locationReducer (state = initialState, action) { 28 | return action.type === LOCATION_CHANGE 29 | ? action.payload 30 | : state 31 | } 32 | -------------------------------------------------------------------------------- /bin/compile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const debug = require('debug')('app:bin:compile') 3 | const webpackCompiler = require('../build/webpack-compiler') 4 | const webpackConfig = require('../build/webpack.config') 5 | const config = require('../config') 6 | 7 | const paths = config.utils_paths 8 | 9 | const compile = () => { 10 | debug('Starting compiler.') 11 | return Promise.resolve() 12 | .then(() => webpackCompiler(webpackConfig)) 13 | .then(stats => { 14 | if (stats.warnings.length && config.compiler_fail_on_warning) { 15 | throw new Error('Config set to fail on warning, exiting with status code "1".') 16 | } 17 | debug('Copying static assets to dist folder.') 18 | fs.copySync(paths.client('static'), paths.dist()) 19 | }) 20 | .then(() => { 21 | debug('Compilation completed successfully.') 22 | }) 23 | .catch((err) => { 24 | debug('Compiler encountered an error.', err) 25 | process.exit(1) 26 | }) 27 | } 28 | 29 | compile() 30 | -------------------------------------------------------------------------------- /tests/unit/components/Header/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Header } from 'components/Header/Header' 3 | import { IndexLink, Link } from 'react-router' 4 | import { shallow } from 'enzyme' 5 | 6 | describe('(Component) Header', () => { 7 | let _wrapper 8 | 9 | beforeEach(() => { 10 | _wrapper = shallow(
) 11 | }) 12 | 13 | it('Renders a welcome message', () => { 14 | const welcome = _wrapper.find('h1') 15 | expect(welcome).to.exist 16 | expect(welcome.text()).to.match(/React Redux Starter Kit/) 17 | }) 18 | 19 | describe('Navigation links...', () => { 20 | it('Should render a Link to Home route', () => { 21 | expect(_wrapper.contains( 22 | 23 | Home 24 | 25 | )).to.be.true 26 | }) 27 | 28 | it('Should render a Link to Counter route', () => { 29 | expect(_wrapper.contains( 30 | 31 | Counter 32 | 33 | )).to.be.true 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Zukowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | // We only need to import the modules necessary for initial render 2 | import CoreLayout from '../layouts/CoreLayout/CoreLayout' 3 | import Home from './Home' 4 | import CounterRoute from './Counter' 5 | 6 | /* Note: Instead of using JSX, we recommend using react-router 7 | PlainRoute objects to build route definitions. */ 8 | 9 | export const createRoutes = (store) => ({ 10 | path : '/', 11 | component : CoreLayout, 12 | indexRoute : Home, 13 | childRoutes : [ 14 | CounterRoute(store) 15 | ] 16 | }) 17 | 18 | /* Note: childRoutes can be chunked or otherwise loaded programmatically 19 | using getChildRoutes with the following signature: 20 | 21 | getChildRoutes (location, cb) { 22 | require.ensure([], (require) => { 23 | cb(null, [ 24 | // Remove imports! 25 | require('./Counter').default(store) 26 | ]) 27 | }) 28 | } 29 | 30 | However, this is not necessary for code-splitting! It simply provides 31 | an API for async route definitions. Your code splitting should occur 32 | inside the route `getComponent` function, since it is only invoked 33 | when the route exists and matches. 34 | */ 35 | 36 | export default createRoutes 37 | -------------------------------------------------------------------------------- /tests/e2e/wdio.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | 3 | /** 4 | * server configurations 5 | */ 6 | maxInstances: 1, 7 | host: '0.0.0.0', 8 | port: 4444, 9 | 10 | /** 11 | * specify test files 12 | */ 13 | specs: [ 14 | './tests/e2e/specs/*.spec.js' 15 | ], 16 | exclude: [ 17 | 'tests/e2e/specs/multibrowser/**', 18 | 'tests/e2e/specs/mobile/**' 19 | ], 20 | 21 | /** 22 | * capabilities 23 | */ 24 | capabilities: [{ 25 | browserName: 'chrome' 26 | }], 27 | 28 | /** 29 | * test configurations 30 | */ 31 | logLevel: 'silent', 32 | sync: true, 33 | coloredLogs: true, 34 | screenshotPath: './tests/e2e/screenshots', 35 | baseUrl: 'http://localhost:3000', 36 | waitforTimeout: 10000, 37 | framework: 'jasmine', 38 | 39 | reporters: ['spec'], 40 | reporterOptions: { 41 | outputDir: './' 42 | }, 43 | 44 | jasmineNodeOpts: { 45 | defaultTimeoutInterval: 9999999 46 | }, 47 | 48 | /** 49 | * hooks 50 | */ 51 | onPrepare: function() { 52 | console.log('Starting end2end tests'); 53 | }, 54 | onComplete: function() { 55 | console.log('All done!'); 56 | } 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /config/environments.js: -------------------------------------------------------------------------------- 1 | // Here is where you can define configuration overrides based on the execution environment. 2 | // Supply a key to the default export matching the NODE_ENV that you wish to target, and 3 | // the base configuration will apply your overrides before exporting itself. 4 | module.exports = { 5 | // ====================================================== 6 | // Overrides when NODE_ENV === 'development' 7 | // ====================================================== 8 | // NOTE: In development, we use an explicit public path when the assets 9 | // are served webpack by to fix this issue: 10 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 11 | development : (config) => ({ 12 | compiler_public_path : `http://${config.server_host}:${config.server_port}/` 13 | }), 14 | 15 | // ====================================================== 16 | // Overrides when NODE_ENV === 'production' 17 | // ====================================================== 18 | production : (config) => ({ 19 | compiler_public_path : '/', 20 | compiler_fail_on_warning : false, 21 | compiler_hash_type : 'chunkhash', 22 | compiler_devtool : null, 23 | compiler_stats : { 24 | chunks : true, 25 | chunkModules : true, 26 | colors : true 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /tests/unit/test-bundler.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Test Environment Setup 3 | // --------------------------------------- 4 | import sinon from 'sinon' 5 | import chai from 'chai' 6 | import sinonChai from 'sinon-chai' 7 | import chaiAsPromised from 'chai-as-promised' 8 | import chaiEnzyme from 'chai-enzyme' 9 | 10 | chai.use(sinonChai) 11 | chai.use(chaiAsPromised) 12 | chai.use(chaiEnzyme()) 13 | 14 | global.chai = chai 15 | global.sinon = sinon 16 | global.expect = chai.expect 17 | global.should = chai.should() 18 | 19 | // --------------------------------------- 20 | // Require Tests 21 | // --------------------------------------- 22 | // for use with karma-webpack-with-fast-source-maps 23 | const __karmaWebpackManifest__ = []; // eslint-disable-line 24 | const inManifest = (path) => ~__karmaWebpackManifest__.indexOf(path) 25 | 26 | // require all `tests/**/*.spec.js` 27 | const testsContext = require.context('./', true, /\.spec\.js$/) 28 | 29 | // only run tests that have changed after the first pass. 30 | const testsToRun = testsContext.keys().filter(inManifest) 31 | ;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext) 32 | 33 | // require all `src/**/*.js` except for `main.js` (for isparta coverage reporting) 34 | if (__COVERAGE__) { 35 | const componentsContext = require.context('../src/', true, /^((?!main|reducers).)*\.js$/) 36 | componentsContext.keys().forEach(componentsContext) 37 | } 38 | -------------------------------------------------------------------------------- /src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import { browserHistory } from 'react-router' 4 | import makeRootReducer from './reducers' 5 | import { updateLocation } from './location' 6 | 7 | export default (initialState = {}) => { 8 | // ====================================================== 9 | // Middleware Configuration 10 | // ====================================================== 11 | const middleware = [thunk] 12 | 13 | // ====================================================== 14 | // Store Enhancers 15 | // ====================================================== 16 | const enhancers = [] 17 | if (__DEV__) { 18 | const devToolsExtension = window.devToolsExtension 19 | if (typeof devToolsExtension === 'function') { 20 | enhancers.push(devToolsExtension()) 21 | } 22 | } 23 | 24 | // ====================================================== 25 | // Store Instantiation and HMR Setup 26 | // ====================================================== 27 | const store = createStore( 28 | makeRootReducer(), 29 | initialState, 30 | compose( 31 | applyMiddleware(...middleware), 32 | ...enhancers 33 | ) 34 | ) 35 | store.asyncReducers = {} 36 | 37 | // To unsubscribe, invoke `store.unsubscribeHistory()` anytime 38 | store.unsubscribeHistory = browserHistory.listen(updateLocation(store)) 39 | 40 | if (module.hot) { 41 | module.hot.accept('./reducers', () => { 42 | const reducers = require('./reducers').default 43 | store.replaceReducer(reducers(store.asyncReducers)) 44 | }) 45 | } 46 | 47 | return store 48 | } 49 | -------------------------------------------------------------------------------- /src/routes/Counter/containers/CounterContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { increment, doubleAsync } from '../modules/counter' 3 | 4 | /* This is a container component. Notice it does not contain any JSX, 5 | nor does it import React. This component is **only** responsible for 6 | wiring in the actions and state necessary to render a presentational 7 | component - in this case, the counter: */ 8 | 9 | import Counter from '../components/Counter' 10 | 11 | /* Object of action creators (can also be function that returns object). 12 | Keys will be passed as props to presentational components. Here we are 13 | implementing our wrapper around increment; the component doesn't care */ 14 | 15 | const mapDispatchToProps = { 16 | increment : () => increment(1), 17 | doubleAsync 18 | } 19 | 20 | const mapStateToProps = (state) => ({ 21 | counter : state.counter 22 | }) 23 | 24 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 25 | 26 | import { createSelector } from 'reselect' 27 | const counter = (state) => state.counter 28 | const tripleCount = createSelector(counter, (count) => count * 3) 29 | const mapStateToProps = (state) => ({ 30 | counter: tripleCount(state) 31 | }) 32 | 33 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 34 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 35 | Selectors are composable. They can be used as input to other selectors. 36 | https://github.com/reactjs/reselect */ 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(Counter) 39 | -------------------------------------------------------------------------------- /src/routes/Counter/modules/counter.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | export const COUNTER_INCREMENT = 'COUNTER_INCREMENT' 5 | 6 | // ------------------------------------ 7 | // Actions 8 | // ------------------------------------ 9 | export function increment (value = 1) { 10 | return { 11 | type : COUNTER_INCREMENT, 12 | payload : value 13 | } 14 | } 15 | 16 | /* This is a thunk, meaning it is a function that immediately 17 | returns a function for lazy evaluation. It is incredibly useful for 18 | creating async actions, especially when combined with redux-thunk! 19 | 20 | NOTE: This is solely for demonstration purposes. In a real application, 21 | you'd probably want to dispatch an action of COUNTER_DOUBLE and let the 22 | reducer take care of this logic. */ 23 | 24 | export const doubleAsync = () => { 25 | return (dispatch, getState) => { 26 | return new Promise((resolve) => { 27 | setTimeout(() => { 28 | dispatch(increment(getState().counter)) 29 | resolve() 30 | }, 200) 31 | }) 32 | } 33 | } 34 | 35 | export const actions = { 36 | increment, 37 | doubleAsync 38 | } 39 | 40 | // ------------------------------------ 41 | // Action Handlers 42 | // ------------------------------------ 43 | const ACTION_HANDLERS = { 44 | [COUNTER_INCREMENT] : (state, action) => state + action.payload 45 | } 46 | 47 | // ------------------------------------ 48 | // Reducer 49 | // ------------------------------------ 50 | const initialState = 0 51 | export default function counterReducer (state = initialState, action) { 52 | const handler = ACTION_HANDLERS[action.type] 53 | 54 | return handler ? handler(state, action) : state 55 | } 56 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import createStore from './store/createStore' 4 | import AppContainer from './containers/AppContainer' 5 | 6 | // ======================================================== 7 | // Store Instantiation 8 | // ======================================================== 9 | const initialState = window.___INITIAL_STATE__ 10 | const store = createStore(initialState) 11 | 12 | // ======================================================== 13 | // Render Setup 14 | // ======================================================== 15 | const MOUNT_NODE = document.getElementById('root') 16 | 17 | let render = () => { 18 | const routes = require('./routes/index').default(store) 19 | 20 | ReactDOM.render( 21 | , 22 | MOUNT_NODE 23 | ) 24 | } 25 | 26 | // ======================================================== 27 | // Developer Tools Setup 28 | // ======================================================== 29 | if (__DEV__) { 30 | if (window.devToolsExtension) { 31 | window.devToolsExtension.open() 32 | } 33 | } 34 | 35 | // This code is excluded from production bundle 36 | if (__DEV__) { 37 | if (module.hot) { 38 | // Development render functions 39 | const renderApp = render 40 | const renderError = (error) => { 41 | const RedBox = require('redbox-react').default 42 | 43 | ReactDOM.render(, MOUNT_NODE) 44 | } 45 | 46 | // Wrap render in try/catch 47 | render = () => { 48 | try { 49 | renderApp() 50 | } catch (error) { 51 | renderError(error) 52 | } 53 | } 54 | 55 | // Setup hot module replacement 56 | module.hot.accept('./routes/index', () => 57 | setImmediate(() => { 58 | ReactDOM.unmountComponentAtNode(MOUNT_NODE) 59 | render() 60 | }) 61 | ) 62 | } 63 | } 64 | 65 | // ======================================================== 66 | // Go! 67 | // ======================================================== 68 | render() 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #271 feat(standard): add style config and refactor to match 29 | #270 fix(config): only override publicPath when served by webpack 30 | #269 feat(eslint-config-defaults): replace eslint-config-airbnb 31 | #268 feat(config): allow user to configure webpack stats output 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `babel`, `redux` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const debug = require('debug')('app:server') 3 | const webpack = require('webpack') 4 | const webpackConfig = require('../build/webpack.config') 5 | const config = require('../config') 6 | const compress = require('compression') 7 | 8 | const app = express() 9 | const paths = config.utils_paths 10 | 11 | // This rewrites all routes requests to the root /index.html file 12 | // (ignoring file requests). If you want to implement universal 13 | // rendering, you'll want to remove this middleware. 14 | app.use(require('connect-history-api-fallback')()) 15 | 16 | // Apply gzip compression 17 | app.use(compress()) 18 | 19 | // ------------------------------------ 20 | // Apply Webpack HMR Middleware 21 | // ------------------------------------ 22 | if (config.env === 'development') { 23 | const compiler = webpack(webpackConfig) 24 | 25 | debug('Enable webpack dev and HMR middleware') 26 | app.use(require('webpack-dev-middleware')(compiler, { 27 | publicPath : webpackConfig.output.publicPath, 28 | contentBase : paths.client(), 29 | hot : true, 30 | quiet : config.compiler_quiet, 31 | noInfo : config.compiler_quiet, 32 | lazy : false, 33 | stats : config.compiler_stats 34 | })) 35 | app.use(require('webpack-hot-middleware')(compiler)) 36 | 37 | // Serve static assets from ~/src/static since Webpack is unaware of 38 | // these files. This middleware doesn't need to be enabled outside 39 | // of development since this directory will be copied into ~/dist 40 | // when the application is compiled. 41 | app.use(express.static(paths.client('static'))) 42 | } else { 43 | debug( 44 | 'Server is being run outside of live development mode, meaning it will ' + 45 | 'only serve the compiled application bundle in ~/dist. Generally you ' + 46 | 'do not need an application server for this and can instead use a web ' + 47 | 'server such as nginx to serve your static files. See the "deployment" ' + 48 | 'section in the README for more information on deployment strategies.' 49 | ) 50 | 51 | // Serving ~/dist by default. Ideally these files should be served by 52 | // the web server and not the app server, but this helps to demo the 53 | // server in production. 54 | app.use(express.static(paths.dist())) 55 | } 56 | 57 | module.exports = app 58 | -------------------------------------------------------------------------------- /tests/unit/routes/Counter/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { Counter } from 'routes/Counter/components/Counter' 4 | import { shallow } from 'enzyme' 5 | 6 | describe('(Component) Counter', () => { 7 | let _props, _spies, _wrapper 8 | 9 | beforeEach(() => { 10 | _spies = {} 11 | _props = { 12 | counter : 5, 13 | ...bindActionCreators({ 14 | doubleAsync : (_spies.doubleAsync = sinon.spy()), 15 | increment : (_spies.increment = sinon.spy()) 16 | }, _spies.dispatch = sinon.spy()) 17 | } 18 | _wrapper = shallow() 19 | }) 20 | 21 | it('Should render as a
.', () => { 22 | expect(_wrapper.is('div')).to.equal(true) 23 | }) 24 | 25 | it('Should render with an

that includes Sample Counter text.', () => { 26 | expect(_wrapper.find('h2').text()).to.match(/Counter:/) 27 | }) 28 | 29 | it('Should render props.counter at the end of the sample counter

.', () => { 30 | expect(_wrapper.find('h2').text()).to.match(/5$/) 31 | _wrapper.setProps({ counter: 8 }) 32 | expect(_wrapper.find('h2').text()).to.match(/8$/) 33 | }) 34 | 35 | it('Should render exactly two buttons.', () => { 36 | expect(_wrapper.find('button')).to.have.length(2) 37 | }) 38 | 39 | describe('An increment button...', () => { 40 | let _button 41 | 42 | beforeEach(() => { 43 | _button = _wrapper.find('button').filterWhere(a => a.text() === 'Increment') 44 | }) 45 | 46 | it('has bootstrap classes', () => { 47 | expect(_button.hasClass('btn btn-default')).to.be.true 48 | }) 49 | 50 | it('Should dispatch a `increment` action when clicked', () => { 51 | _spies.dispatch.should.have.not.been.called 52 | 53 | _button.simulate('click') 54 | 55 | _spies.dispatch.should.have.been.called 56 | _spies.increment.should.have.been.called 57 | }) 58 | }) 59 | 60 | describe('A Double (Async) button...', () => { 61 | let _button 62 | 63 | beforeEach(() => { 64 | _button = _wrapper.find('button').filterWhere(a => a.text() === 'Double (Async)') 65 | }) 66 | 67 | it('has bootstrap classes', () => { 68 | expect(_button.hasClass('btn btn-default')).to.be.true 69 | }) 70 | 71 | it('Should dispatch a `doubleAsync` action when clicked', () => { 72 | _spies.dispatch.should.have.not.been.called 73 | 74 | _button.simulate('click') 75 | 76 | _spies.dispatch.should.have.been.called 77 | _spies.doubleAsync.should.have.been.called 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /tests/unit/store/location.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOCATION_CHANGE, 3 | locationChange, 4 | updateLocation, 5 | default as locationReducer 6 | } from 'store/location' 7 | 8 | describe('(Internal Module) Location', () => { 9 | it('Should export a constant LOCATION_CHANGE.', () => { 10 | expect(LOCATION_CHANGE).to.equal('LOCATION_CHANGE') 11 | }) 12 | 13 | describe('(Reducer)', () => { 14 | it('Should be a function.', () => { 15 | expect(locationReducer).to.be.a('function') 16 | }) 17 | 18 | it('Should initialize with a state of null.', () => { 19 | expect(locationReducer(undefined, {})).to.equal(null) 20 | }) 21 | 22 | it('Should return the previous state if an action was not matched.', () => { 23 | let state = locationReducer(undefined, {}) 24 | expect(state).to.equal(null) 25 | state = locationReducer(state, { type: '@@@@@@@' }) 26 | expect(state).to.equal(null) 27 | 28 | const locationState = { pathname: '/yup' } 29 | state = locationReducer(state, locationChange(locationState)) 30 | expect(state).to.equal(locationState) 31 | state = locationReducer(state, { type: '@@@@@@@' }) 32 | expect(state).to.equal(locationState) 33 | }) 34 | }) 35 | 36 | describe('(Action Creator) locationChange', () => { 37 | it('Should be exported as a function.', () => { 38 | expect(locationChange).to.be.a('function') 39 | }) 40 | 41 | it('Should return an action with type "LOCATION_CHANGE".', () => { 42 | expect(locationChange()).to.have.property('type', LOCATION_CHANGE) 43 | }) 44 | 45 | it('Should assign the first argument to the "payload" property.', () => { 46 | const locationState = { pathname: '/yup' } 47 | expect(locationChange(locationState)).to.have.property('payload', locationState) 48 | }) 49 | 50 | it('Should default the "payload" property to "/" if not provided.', () => { 51 | expect(locationChange()).to.have.property('payload', '/') 52 | }) 53 | }) 54 | 55 | describe('(Specialized Action Creator) updateLocation', () => { 56 | let _globalState 57 | let _dispatchSpy 58 | 59 | beforeEach(() => { 60 | _globalState = { 61 | location : locationReducer(undefined, {}) 62 | } 63 | _dispatchSpy = sinon.spy((action) => { 64 | _globalState = { 65 | ..._globalState, 66 | location : locationReducer(_globalState.location, action) 67 | } 68 | }) 69 | }) 70 | 71 | it('Should be exported as a function.', () => { 72 | expect(updateLocation).to.be.a('function') 73 | }) 74 | 75 | it('Should return a function (is a thunk).', () => { 76 | expect(updateLocation({ dispatch: _dispatchSpy })).to.be.a('function') 77 | }) 78 | 79 | it('Should call dispatch exactly once.', () => { 80 | updateLocation({ dispatch: _dispatchSpy })('/') 81 | expect(_dispatchSpy.should.have.been.calledOnce) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 spaced-comment:0 */ 2 | const path = require('path') 3 | const debug = require('debug')('app:config') 4 | const argv = require('yargs').argv 5 | const ip = require('ip') 6 | 7 | debug('Creating default configuration.') 8 | // ======================================================== 9 | // Default Configuration 10 | // ======================================================== 11 | const config = { 12 | env : process.env.NODE_ENV || 'development', 13 | 14 | // ---------------------------------- 15 | // Project Structure 16 | // ---------------------------------- 17 | path_base : path.resolve(__dirname, '..'), 18 | dir_client : 'src', 19 | dir_dist : 'dist', 20 | dir_server : 'server', 21 | dir_test : 'tests', 22 | 23 | // ---------------------------------- 24 | // Server Configuration 25 | // ---------------------------------- 26 | server_host : ip.address(), // use string 'localhost' to prevent exposure on local network 27 | server_port : process.env.PORT || 3000, 28 | 29 | // ---------------------------------- 30 | // Compiler Configuration 31 | // ---------------------------------- 32 | compiler_babel : { 33 | cacheDirectory : true, 34 | plugins : ['transform-runtime'], 35 | presets : ['es2015', 'react', 'stage-0'] 36 | }, 37 | compiler_devtool : 'source-map', 38 | compiler_hash_type : 'hash', 39 | compiler_fail_on_warning : false, 40 | compiler_quiet : false, 41 | compiler_public_path : '/', 42 | compiler_stats : { 43 | chunks : false, 44 | chunkModules : false, 45 | colors : true 46 | }, 47 | compiler_vendors : [ 48 | 'react', 49 | 'react-redux', 50 | 'react-router', 51 | 'redux' 52 | ], 53 | 54 | // ---------------------------------- 55 | // Test Configuration 56 | // ---------------------------------- 57 | coverage_reporters : [ 58 | { type : 'text-summary' }, 59 | { type : 'lcov', dir : 'coverage' } 60 | ] 61 | } 62 | 63 | /************************************************ 64 | ------------------------------------------------- 65 | 66 | All Internal Configuration Below 67 | Edit at Your Own Risk 68 | 69 | ------------------------------------------------- 70 | ************************************************/ 71 | 72 | // ------------------------------------ 73 | // Environment 74 | // ------------------------------------ 75 | // N.B.: globals added here must _also_ be added to .eslintrc 76 | config.globals = { 77 | 'process.env' : { 78 | 'NODE_ENV' : JSON.stringify(config.env) 79 | }, 80 | 'NODE_ENV' : config.env, 81 | '__DEV__' : config.env === 'development', 82 | '__PROD__' : config.env === 'production', 83 | '__TEST__' : config.env === 'test', 84 | '__COVERAGE__' : !argv.watch && config.env === 'test', 85 | '__BASENAME__' : JSON.stringify(process.env.BASENAME || '') 86 | } 87 | 88 | // ------------------------------------ 89 | // Validate Vendor Dependencies 90 | // ------------------------------------ 91 | const pkg = require('../package.json') 92 | 93 | config.compiler_vendors = config.compiler_vendors 94 | .filter((dep) => { 95 | if (pkg.dependencies[dep]) return true 96 | 97 | debug( 98 | `Package "${dep}" was not found as an npm dependency in package.json; ` + 99 | `it won't be included in the webpack vendor bundle. 100 | Consider removing it from \`compiler_vendors\` in ~/config/index.js` 101 | ) 102 | }) 103 | 104 | // ------------------------------------ 105 | // Utilities 106 | // ------------------------------------ 107 | function base () { 108 | const args = [config.path_base].concat([].slice.call(arguments)) 109 | return path.resolve.apply(path, args) 110 | } 111 | 112 | config.utils_paths = { 113 | base : base, 114 | client : base.bind(null, config.dir_client), 115 | dist : base.bind(null, config.dir_dist) 116 | } 117 | 118 | // ======================================================== 119 | // Environment Configuration 120 | // ======================================================== 121 | debug(`Looking for environment overrides for NODE_ENV "${config.env}".`) 122 | const environments = require('./environments') 123 | const overrides = environments[config.env] 124 | if (overrides) { 125 | debug('Found overrides, applying to default configuration.') 126 | Object.assign(config, overrides(config)) 127 | } else { 128 | debug('No environment overrides found, defaults will be used.') 129 | } 130 | 131 | module.exports = config 132 | -------------------------------------------------------------------------------- /tests/unit/routes/Counter/modules/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | COUNTER_INCREMENT, 3 | increment, 4 | doubleAsync, 5 | default as counterReducer 6 | } from 'routes/Counter/modules/counter' 7 | 8 | describe('(Redux Module) Counter', () => { 9 | it('Should export a constant COUNTER_INCREMENT.', () => { 10 | expect(COUNTER_INCREMENT).to.equal('COUNTER_INCREMENT') 11 | }) 12 | 13 | describe('(Reducer)', () => { 14 | it('Should be a function.', () => { 15 | expect(counterReducer).to.be.a('function') 16 | }) 17 | 18 | it('Should initialize with a state of 0 (Number).', () => { 19 | expect(counterReducer(undefined, {})).to.equal(0) 20 | }) 21 | 22 | it('Should return the previous state if an action was not matched.', () => { 23 | let state = counterReducer(undefined, {}) 24 | expect(state).to.equal(0) 25 | state = counterReducer(state, { type: '@@@@@@@' }) 26 | expect(state).to.equal(0) 27 | state = counterReducer(state, increment(5)) 28 | expect(state).to.equal(5) 29 | state = counterReducer(state, { type: '@@@@@@@' }) 30 | expect(state).to.equal(5) 31 | }) 32 | }) 33 | 34 | describe('(Action Creator) increment', () => { 35 | it('Should be exported as a function.', () => { 36 | expect(increment).to.be.a('function') 37 | }) 38 | 39 | it('Should return an action with type "COUNTER_INCREMENT".', () => { 40 | expect(increment()).to.have.property('type', COUNTER_INCREMENT) 41 | }) 42 | 43 | it('Should assign the first argument to the "payload" property.', () => { 44 | expect(increment(5)).to.have.property('payload', 5) 45 | }) 46 | 47 | it('Should default the "payload" property to 1 if not provided.', () => { 48 | expect(increment()).to.have.property('payload', 1) 49 | }) 50 | }) 51 | 52 | describe('(Action Creator) doubleAsync', () => { 53 | let _globalState 54 | let _dispatchSpy 55 | let _getStateSpy 56 | 57 | beforeEach(() => { 58 | _globalState = { 59 | counter : counterReducer(undefined, {}) 60 | } 61 | _dispatchSpy = sinon.spy((action) => { 62 | _globalState = { 63 | ..._globalState, 64 | counter : counterReducer(_globalState.counter, action) 65 | } 66 | }) 67 | _getStateSpy = sinon.spy(() => { 68 | return _globalState 69 | }) 70 | }) 71 | 72 | it('Should be exported as a function.', () => { 73 | expect(doubleAsync).to.be.a('function') 74 | }) 75 | 76 | it('Should return a function (is a thunk).', () => { 77 | expect(doubleAsync()).to.be.a('function') 78 | }) 79 | 80 | it('Should return a promise from that thunk that gets fulfilled.', () => { 81 | return doubleAsync()(_dispatchSpy, _getStateSpy).should.eventually.be.fulfilled 82 | }) 83 | 84 | it('Should call dispatch and getState exactly once.', () => { 85 | return doubleAsync()(_dispatchSpy, _getStateSpy) 86 | .then(() => { 87 | _dispatchSpy.should.have.been.calledOnce 88 | _getStateSpy.should.have.been.calledOnce 89 | }) 90 | }) 91 | 92 | it('Should produce a state that is double the previous state.', () => { 93 | _globalState = { counter: 2 } 94 | 95 | return doubleAsync()(_dispatchSpy, _getStateSpy) 96 | .then(() => { 97 | _dispatchSpy.should.have.been.calledOnce 98 | _getStateSpy.should.have.been.calledOnce 99 | expect(_globalState.counter).to.equal(4) 100 | return doubleAsync()(_dispatchSpy, _getStateSpy) 101 | }) 102 | .then(() => { 103 | _dispatchSpy.should.have.been.calledTwice 104 | _getStateSpy.should.have.been.calledTwice 105 | expect(_globalState.counter).to.equal(8) 106 | }) 107 | }) 108 | }) 109 | 110 | // NOTE: if you have a more complex state, you will probably want to verify 111 | // that you did not mutate the state. In this case our state is just a number 112 | // (which cannot be mutated). 113 | describe('(Action Handler) COUNTER_INCREMENT', () => { 114 | it('Should increment the state by the action payload\'s "value" property.', () => { 115 | let state = counterReducer(undefined, {}) 116 | expect(state).to.equal(0) 117 | state = counterReducer(state, increment(1)) 118 | expect(state).to.equal(1) 119 | state = counterReducer(state, increment(2)) 120 | expect(state).to.equal(3) 121 | state = counterReducer(state, increment(-3)) 122 | expect(state).to.equal(0) 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-starter-kit", 3 | "version": "3.0.0-alpha.2", 4 | "description": "Get started with React, Redux, and React-Router!", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4.5.0", 8 | "npm": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "compile": "better-npm-run compile", 13 | "lint": "eslint bin build config server src tests", 14 | "lint:fix": "npm run lint -- --fix", 15 | "start": "better-npm-run start", 16 | "dev": "better-npm-run dev", 17 | "test": "better-npm-run test", 18 | "test:dev": "npm run test -- --watch", 19 | "deploy": "better-npm-run deploy", 20 | "deploy:dev": "better-npm-run deploy:dev", 21 | "deploy:prod": "better-npm-run deploy:prod", 22 | "codecov": "cat coverage/*/lcov.info | codecov", 23 | "e2e-setup": "node_modules/selenium-standalone/bin/selenium-standalone install", 24 | "e2e": "node_modules/.bin/wdio tests/e2e/wdio.conf.js", 25 | "selenium-server": "node_modules/selenium-standalone/bin/selenium-standalone start" 26 | }, 27 | "betterScripts": { 28 | "compile": { 29 | "command": "node bin/compile", 30 | "env": { 31 | "DEBUG": "app:*" 32 | } 33 | }, 34 | "dev": { 35 | "command": "nodemon bin/server --ignore dist --ignore coverage --ignore tests --ignore src", 36 | "env": { 37 | "NODE_ENV": "development", 38 | "DEBUG": "app:*" 39 | } 40 | }, 41 | "deploy": { 42 | "command": "npm run lint && npm run test && npm run clean && npm run compile", 43 | "env": { 44 | "DEBUG": "app:*" 45 | } 46 | }, 47 | "deploy:dev": { 48 | "command": "npm run deploy", 49 | "env": { 50 | "NODE_ENV": "development", 51 | "DEBUG": "app:*" 52 | } 53 | }, 54 | "deploy:prod": { 55 | "command": "npm run deploy", 56 | "env": { 57 | "NODE_ENV": "production", 58 | "DEBUG": "app:*" 59 | } 60 | }, 61 | "start": { 62 | "command": "node bin/server", 63 | "env": { 64 | "DEBUG": "app:*" 65 | } 66 | }, 67 | "test": { 68 | "command": "node ./node_modules/karma/bin/karma start build/karma.conf", 69 | "env": { 70 | "NODE_ENV": "test", 71 | "DEBUG": "app:*" 72 | } 73 | } 74 | }, 75 | "repository": { 76 | "type": "git", 77 | "url": "git+https://github.com/davezuko/react-redux-starter-kit.git" 78 | }, 79 | "author": "David Zukowski (http://zuko.me)", 80 | "license": "MIT", 81 | "dependencies": { 82 | "babel-core": "^6.14.0", 83 | "babel-loader": "^6.2.5", 84 | "babel-plugin-transform-runtime": "^6.15.0", 85 | "babel-preset-es2015": "^6.14.0", 86 | "babel-preset-react": "^6.11.1", 87 | "babel-preset-stage-0": "^6.3.13", 88 | "babel-runtime": "^6.11.6", 89 | "better-npm-run": "0.0.13", 90 | "compression": "^1.6.2", 91 | "css-loader": "^0.25.0", 92 | "cssnano": "^3.7.4", 93 | "debug": "^2.2.0", 94 | "extract-text-webpack-plugin": "^1.0.0", 95 | "file-loader": "^0.9.0", 96 | "fs-extra": "^1.0.0", 97 | "html-webpack-plugin": "^2.22.0", 98 | "imports-loader": "^0.6.5", 99 | "ip": "^1.1.2", 100 | "json-loader": "^0.5.4", 101 | "node-sass": "^3.7.0", 102 | "normalize.css": "^5.0.0", 103 | "postcss-loader": "^1.1.0", 104 | "react": "^15.0.0", 105 | "react-dom": "^15.0.0", 106 | "react-redux": "^4.4.5", 107 | "react-router": "^3.0.0", 108 | "redux": "^3.6.0", 109 | "redux-thunk": "^2.0.0", 110 | "rimraf": "^2.5.4", 111 | "sass-loader": "^4.0.0", 112 | "style-loader": "^0.13.1", 113 | "url-loader": "^0.5.6", 114 | "webpack": "^1.12.14", 115 | "yargs": "^6.3.0" 116 | }, 117 | "devDependencies": { 118 | "babel-core": "^6.17.0", 119 | "babel-eslint": "^7.1.0", 120 | "babel-plugin-istanbul": "^2.0.1", 121 | "chai": "^3.4.1", 122 | "chai-as-promised": "^6.0.0", 123 | "chai-enzyme": "^0.5.0", 124 | "cheerio": "^0.20.0", 125 | "codecov": "^1.0.1", 126 | "connect-history-api-fallback": "^1.3.0", 127 | "enzyme": "^2.0.0", 128 | "eslint": "^3.0.1", 129 | "eslint-config-standard": "^6.0.0", 130 | "eslint-config-standard-react": "^4.0.0", 131 | "eslint-plugin-babel": "^3.2.0", 132 | "eslint-plugin-promise": "^3.0.0", 133 | "eslint-plugin-react": "^6.0.0", 134 | "eslint-plugin-standard": "^2.0.0", 135 | "express": "^4.14.0", 136 | "jasmine": "^2.5.2", 137 | "karma": "^1.0.0", 138 | "karma-coverage": "^1.0.0", 139 | "karma-mocha": "^1.0.1", 140 | "karma-mocha-reporter": "^2.0.0", 141 | "karma-phantomjs-launcher": "^1.0.2", 142 | "karma-webpack-with-fast-source-maps": "^1.9.2", 143 | "mocha": "^3.0.1", 144 | "nodemon": "^1.10.2", 145 | "phantomjs-prebuilt": "^2.1.12", 146 | "react-addons-test-utils": "^15.0.0", 147 | "redbox-react": "^1.2.10", 148 | "selenium-standalone": "^5.8.0", 149 | "sinon": "^1.17.5", 150 | "sinon-chai": "^2.8.0", 151 | "wdio-jasmine-framework": "^0.2.16", 152 | "wdio-spec-reporter": "0.0.3", 153 | "webdriverio": "^4.4.0", 154 | "webpack-dev-middleware": "^1.6.1", 155 | "webpack-hot-middleware": "^2.12.2" 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![No longer maintained](https://img.shields.io/badge/Maintenance-OFF-red.svg) 2 | ### [DEPRECATED] This repository is no longer maintained 3 | > While this project is fully functional, the dependencies are no longer up to date. You are still welcome to explore, learn, and use the code provided here. 4 | > 5 | > Modus is dedicated to supporting the community with innovative ideas, best-practice patterns, and inspiring open source solutions. Check out the latest [Modus Labs](https://labs.moduscreate.com?utm_source=github&utm_medium=readme&utm_campaign=deprecated) projects. 6 | 7 | [![Modus Labs](https://res.cloudinary.com/modus-labs/image/upload/h_80/v1531492623/labs/logo-black.png)](https://labs.moduscreate.com?utm_source=github&utm_medium=readme&utm_campaign=deprecated) 8 | 9 | --- 10 | 11 | # React Redux WebdriverIO Starter Kit 12 | 13 | [![Join the chat at https://gitter.im/davezuko/react-redux-starter-kit](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/davezuko/react-redux-starter-kit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 14 | [![Build Status](https://travis-ci.org/davezuko/react-redux-starter-kit.svg?branch=master)](https://travis-ci.org/davezuko/react-redux-starter-kit?branch=master) 15 | [![dependencies](https://david-dm.org/davezuko/react-redux-starter-kit.svg)](https://david-dm.org/davezuko/react-redux-starter-kit) 16 | [![devDependency Status](https://david-dm.org/davezuko/react-redux-starter-kit/dev-status.svg)](https://david-dm.org/davezuko/react-redux-starter-kit#info=devDependencies) 17 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 18 | 19 | This starter kit is designed to get you up and running with a bunch of awesome new front-end technologies, all on top of a configurable, feature-rich webpack build system that's already setup to provide hot reloading, CSS preprocessing with Sass, unit testing, UI testing, code coverage reports, bundle splitting, and more. 20 | 21 | The primary goal of this project is to remain as **unopinionated** as possible. Its purpose is not to dictate your project structure or to demonstrate a complete sample application, but to provide a set of tools intended to make front-end development robust, easy, and, most importantly, fun. Check out the full feature list below! 22 | 23 | Finally, This project wouldn't be possible without the help of our many contributors, so [thank you](#thank-you) for all of your help. 24 | 25 | ## Table of Contents 26 | 1. [Features](#features) 27 | 1. [Requirements](#requirements) 28 | 1. [Getting Started](#getting-started) 29 | 1. [Application Structure](#application-structure) 30 | 1. [Development](#development) 31 | 1. [Developer Tools](#developer-tools) 32 | 1. [Routing](#routing) 33 | 1. [Testing](#testing) 34 | 1. [Deployment](#deployment) 35 | 1. [Build System](#build-system) 36 | 1. [Configuration](#configuration) 37 | 1. [Globals](#globals) 38 | 1. [Styles](#styles) 39 | 1. [Server](#server) 40 | 1. [Production Optimization](#production-optimization) 41 | 1. [Learning Resources](#learning-resources) 42 | 1. [FAQ](#troubleshooting) 43 | 1. [Thank You](#thank-you) 44 | 45 | ## Features 46 | * [react](https://github.com/facebook/react) 47 | * [redux](https://github.com/rackt/redux) 48 | * [react-router](https://github.com/rackt/react-router) 49 | * [webpack](https://github.com/webpack/webpack) 50 | * [babel](https://github.com/babel/babel) 51 | * [express](https://github.com/expressjs/express) 52 | * [karma](https://github.com/karma-runner/karma) 53 | * [eslint](http://eslint.org) 54 | * [WebdriverIO](https://github.com/webdriverio/webdriverio) 55 | 56 | ## Requirements 57 | * node `^4.5.0` 58 | * npm `^3.0.0` 59 | 60 | ## Getting Started 61 | 62 | After confirming that your development environment meets the specified [requirements](#requirements), you can create a new project based on `react-redux-webdriverIO-starter-kit` in one of two ways: 63 | 64 | ### Install from source 65 | 66 | First, clone the project: 67 | 68 | ```bash 69 | $ git clone 70 | $ cd 71 | ``` 72 | 73 | Then install dependencies and check to see it works 74 | 75 | ```bash 76 | $ npm install # Install project dependencies 77 | $ npm start # Compile and launch 78 | ``` 79 | If everything works, you should see the following: 80 | 81 | 82 | 83 | While developing, you will probably rely mostly on `npm start`; however, there are additional scripts at your disposal: 84 | 85 | |`npm run