├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .stylelintrc ├── .travis.yml ├── README.md ├── app ├── .htaccess ├── index.ejs ├── robots.txt ├── scripts │ ├── actions │ │ ├── app.js │ │ ├── github.js │ │ ├── index.js │ │ └── user.js │ ├── components │ │ ├── Alert.jsx │ │ ├── Footer.jsx │ │ ├── Header.jsx │ │ ├── Loader.jsx │ │ ├── Logo.jsx │ │ ├── SystemAlerts.jsx │ │ └── Transition.jsx │ ├── config.js │ ├── constants │ │ └── index.js │ ├── containers │ │ └── App.jsx │ ├── epics │ │ ├── github.js │ │ ├── index.js │ │ └── user.js │ ├── index.jsx │ ├── modules │ │ ├── RoutePrivate.jsx │ │ ├── RoutePublic.jsx │ │ ├── client.js │ │ ├── helpers.js │ │ └── history.js │ ├── reducers │ │ ├── app.js │ │ ├── github.js │ │ ├── index.js │ │ └── user.js │ ├── routes │ │ ├── Home.jsx │ │ ├── Login.jsx │ │ ├── NotFound.jsx │ │ └── Private.jsx │ ├── store │ │ └── index.js │ └── vendor │ │ ├── modernizr-custom.js │ │ └── rxjs.js └── styles │ ├── components │ ├── _alert.scss │ ├── _footer.scss │ ├── _header.scss │ ├── _loader.scss │ ├── _logo.scss │ └── _system-alerts.scss │ ├── layout │ ├── _app.scss │ └── _root.scss │ ├── main.scss │ ├── modules │ ├── _animations.scss │ ├── _buttons.scss │ ├── _icons.scss │ └── _transitions.scss │ ├── routes │ ├── _home.scss │ ├── _not-found.scss │ └── _private.scss │ ├── utilities │ ├── _functions.scss │ ├── _grid.scss │ └── _mixins.scss │ ├── variables │ ├── _app.scss │ ├── _bootstrap.scss │ └── _typography.scss │ └── vendor │ └── _bootstrap.scss ├── assets ├── fonts │ ├── icon-font-v3.svg │ ├── icon-font-v3.ttf │ └── icon-font-v3.woff ├── index.html ├── manifest.json └── media │ ├── brand │ ├── icon.png │ └── icon.svg │ ├── icons │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── icon-144x144.png │ ├── icon-192x192.png │ ├── icon-512x512.png │ ├── icon-96x96.png │ └── safari-pinned-tab.svg │ ├── images │ └── og-image-v1.png │ └── logos │ ├── jest.svg │ ├── nightwatch.svg │ ├── react-router.svg │ ├── react.svg │ ├── reactivex.svg │ ├── redux-observable.svg │ ├── redux.svg │ └── webpack.svg ├── config ├── env.js ├── esdoc.json ├── jest.config.js ├── modernizrrc.json ├── paths.js ├── webpack.config.base.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.js ├── package-lock.json ├── package.json ├── test ├── .eslintrc ├── __mocks__ │ └── react-router-dom.js ├── __setup__ │ ├── fileMock.js │ ├── index.js │ ├── mockedStore.js │ ├── moduleMock.js │ ├── nightwatch.conf.js │ ├── shim.js │ └── styleMock.js ├── __snapshots__ │ └── index.spec.js.snap ├── actions │ └── app.spec.js ├── components │ ├── Alert.spec.js │ ├── Footer.spec.js │ ├── Header.spec.js │ ├── Loader.spec.js │ ├── Logo.spec.js │ ├── SystemAlerts.spec.js │ └── __snapshots__ │ │ ├── Footer.spec.js.snap │ │ ├── Header.spec.js.snap │ │ ├── Loader.spec.js.snap │ │ └── Logo.spec.js.snap ├── containers │ └── App.spec.js ├── epics │ ├── github.spec.js │ └── user.spec.js ├── index.spec.js ├── modules │ ├── RoutePrivate.spec.js │ ├── RoutePublic.spec.js │ ├── __snapshots__ │ │ ├── RoutePrivate.spec.js.snap │ │ ├── RoutePublic.spec.js.snap │ │ ├── client.spec.js.snap │ │ └── helpers.spec.js.snap │ ├── client.spec.js │ └── helpers.spec.js ├── reducers │ ├── __snapshots__ │ │ ├── app.spec.js.snap │ │ ├── github.spec.js.snap │ │ └── user.spec.js.snap │ ├── app.spec.js │ ├── github.spec.js │ └── user.spec.js ├── routes │ ├── Home.spec.js │ ├── Login.spec.js │ ├── NotFound.spec.js │ ├── Private.spec.js │ └── __snapshots__ │ │ ├── Home.spec.js.snap │ │ ├── Login.spec.js.snap │ │ ├── NotFound.spec.js.snap │ │ └── Private.spec.js.snap ├── store │ └── index.spec.js └── ui │ └── ui.nightwatch.js └── tools ├── index.js └── start.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", { 5 | "targets": { 6 | "browsers": ["last 2 versions", "safari >= 8", "ios >= 8"] 7 | }, 8 | "modules": false 9 | } 10 | ], 11 | "react", 12 | "stage-1" 13 | ], 14 | "plugins": [ 15 | [ 16 | "transform-runtime", { 17 | "polyfill": false, 18 | "regenerator": true 19 | } 20 | ] 21 | ], 22 | "compact": "true", 23 | "env": { 24 | "production": { 25 | "plugins": [ 26 | "lodash", 27 | "array-includes", 28 | "transform-flow-strip-types", 29 | "transform-object-assign" 30 | ] 31 | }, 32 | "development": { 33 | "plugins": [ 34 | "react-hot-loader/babel" 35 | ] 36 | }, 37 | "test": { 38 | "plugins": [ 39 | "transform-es2015-modules-commonjs" 40 | ], 41 | "sourceMaps": "both" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | eslint: 4 | enabled: true 5 | channel: "eslint-4" 6 | config: 7 | extensions: 8 | - .js 9 | - .jsx 10 | checks: 11 | import/no-duplicates: 12 | enabled: false 13 | import/no-extraneous-dependencies: 14 | enabled: false 15 | import/no-named-as-default-member: 16 | enabled: false 17 | import/no-unresolved: 18 | enabled: false 19 | import/extensions: 20 | enabled: false 21 | stylelint: 22 | enabled: true 23 | ratings: 24 | paths: 25 | - "**.js" 26 | - "**.jsx" 27 | - "**.scss" 28 | exclude_paths: 29 | - coverage/**/* 30 | - dist/**/* 31 | - documentation/**/* 32 | - node_modules/**/* 33 | - test/**/* 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "jest": true 7 | }, 8 | "globals": { 9 | "$": true, 10 | "fetch": false, 11 | "ga": false, 12 | "mobileDetect": false, 13 | "Modernizr": false, 14 | "APP__BRANCH": false, 15 | "APP__BUILD_DATE": false, 16 | "APP__GITHASH": false, 17 | "APP__VERSION": false 18 | }, 19 | "plugins": [ 20 | "babel", 21 | "flowtype" 22 | ], 23 | "settings": { 24 | "import/resolver": { 25 | "webpack": { 26 | "config": "config/webpack.config.base.js" 27 | } 28 | }, 29 | "flowtype": { 30 | "onlyFilesWithFlowAnnotation": true 31 | } 32 | }, 33 | "rules": { 34 | "arrow-body-style": [ 35 | "warn", 36 | "as-needed" 37 | ], 38 | "arrow-parens": "off", 39 | "block-spacing": "warn", 40 | "brace-style": ["warn", "stroustrup", { "allowSingleLine": true }], 41 | "camelcase": "off", 42 | "class-methods-use-this": "off", 43 | "comma-dangle": [ 44 | "warn", { 45 | "arrays": "always-multiline", 46 | "imports": "always-multiline", 47 | "objects": "always-multiline", 48 | "functions": "only-multiline" 49 | } 50 | ], 51 | "dot-notation": "warn", 52 | "function-paren-newline": "off", 53 | "generator-star-spacing": "off", 54 | "global-require": "off", 55 | "indent": ["warn", 2, { "SwitchCase": 1 }], 56 | "max-len": "off", 57 | "newline-per-chained-call": ["warn", { "ignoreChainWithDepth": 5 }], 58 | "no-case-declarations": "warn", 59 | "no-confusing-arrow": ["warn", { "allowParens": true }], 60 | "no-mixed-spaces-and-tabs": ["warn", "smart-tabs"], 61 | "no-multi-spaces": [ 62 | "warn", { 63 | "exceptions": { 64 | "VariableDeclarator": true, 65 | "Property": false 66 | } 67 | } 68 | ], 69 | "no-nested-ternary": "warn", 70 | "no-param-reassign": ["warn", { "props": false }], 71 | "no-plusplus": "off", 72 | "no-restricted-globals": ["error", "fdescribe", "fit"], 73 | "no-restricted-syntax": [ 74 | "error", 75 | "DebuggerStatement", 76 | "LabeledStatement", 77 | "WithStatement" 78 | ], 79 | "no-return-assign": ["error", "except-parens"], 80 | "no-template-curly-in-string": "warn", 81 | "no-trailing-spaces": "warn", 82 | "no-underscore-dangle": "off", 83 | "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }], 84 | "object-curly-newline": "off", 85 | "object-shorthand": ["warn", "always"], 86 | "one-var": "warn", 87 | "padded-blocks": "warn", 88 | "prefer-const": "warn", 89 | "prefer-destructuring": "warn", 90 | "prefer-template": "warn", 91 | "prefer-promise-reject-errors": "off", 92 | "quotes": ["warn", "single", "avoid-escape"], 93 | "require-jsdoc": [ 94 | "off", { 95 | "require": { 96 | "FunctionDeclaration": true, 97 | "MethodDefinition": false, 98 | "ClassDeclaration": false 99 | } 100 | } 101 | ], 102 | "space-before-function-paren": [ 103 | "warn", { 104 | "anonymous": "never", 105 | "named": "never", 106 | "asyncArrow": "always" 107 | } 108 | ], 109 | "space-in-parens": "warn", 110 | "spaced-comment": [ 111 | "warn", 112 | "always", { 113 | "exceptions": [ 114 | "-+" 115 | ], 116 | "markers": [ 117 | "eslint-enable", 118 | "eslint-disable", 119 | "eslint-disable-line" 120 | ] 121 | } 122 | ], 123 | "flowtype/require-parameter-type": ["warn", { "excludeArrowFunctions": true }], 124 | "flowtype/require-return-type": ["warn", "always", { "excludeArrowFunctions": true }], 125 | "flowtype/space-after-type-colon": [ 126 | "error", 127 | "always" 128 | ], 129 | "import/no-dynamic-require": "off", 130 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 131 | "import/no-unresolved": "warn", 132 | "import/no-webpack-loader-syntax": "off", 133 | "import/no-named-as-default": "off", 134 | "import/prefer-default-export": "off", 135 | "jsx-a11y/anchor-has-content": "off", 136 | "jsx-a11y/anchor-is-valid": "off", 137 | "jsx-a11y/click-events-have-key-events": "off", 138 | "jsx-a11y/label-has-for": "off", 139 | "jsx-a11y/no-static-element-interactions": "off", 140 | "jsx-quotes": "warn", 141 | "react/forbid-prop-types": "off", 142 | "react/jsx-closing-bracket-location": ["warn", "line-aligned"], 143 | "react/jsx-first-prop-new-line": ["warn", "multiline"], 144 | "react/jsx-indent": ["warn", 2], 145 | "react/jsx-indent-props": ["warn", 2], 146 | "react/jsx-key": "warn", 147 | "react/jsx-max-props-per-line": ["warn", { "maximum": 4 }], 148 | "react/jsx-no-target-blank": "off", 149 | "react/no-array-index-key": "off", 150 | "react/no-danger": "off", 151 | "react/no-did-mount-set-state": "warn", 152 | "react/no-did-update-set-state": "warn", 153 | "react/jsx-boolean-value": "off", 154 | "react/jsx-no-duplicate-props": "warn", 155 | "react/jsx-pascal-case": "warn", 156 | "react/no-direct-mutation-state": "warn", 157 | "react/no-unused-prop-types": "warn", 158 | "react/no-unused-state": "warn", 159 | "react/no-unescaped-entities": "off", 160 | "react/prefer-stateless-function": "off", 161 | "react/prop-types": "error", 162 | "react/require-default-props": "off", 163 | "react/sort-prop-types": "warn", 164 | "react/sort-comp": [ 165 | "warn", 166 | { 167 | "order": [ 168 | "constructor", 169 | "lifecycle", 170 | "everything-else", 171 | "render" 172 | ], 173 | "groups": { 174 | "lifecycle": [ 175 | "state", 176 | "statics", 177 | "contextTypes", 178 | "childContextTypes", 179 | "getChildContext", 180 | "propTypes", 181 | "defaultProps", 182 | "shouldComponentUpdate", 183 | "componentWillMount", 184 | "componentDidMount", 185 | "componentWillReceiveProps", 186 | "componentWillUpdate", 187 | "componentDidUpdate", 188 | "componentWillUnmount" 189 | ] 190 | } 191 | } 192 | ] 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/test/broken.json 3 | .*/node_modules/cypress/.* 4 | .*/node_modules/enzyme-matchers/.* 5 | .*/node_modules/jest-enzyme/.* 6 | .*/node_modules/npmconf/test/.* 7 | .*/node_modules/stylelint/.* 8 | 9 | [include] 10 | 11 | [libs] 12 | 13 | [options] 14 | module.name_mapper='^actions\/(.*)$' -> '/app/scripts/actions/\1' 15 | module.name_mapper='^components\/(.*)$' -> '/app/scripts/components/\1' 16 | module.name_mapper='^config$' -> '/app/scripts/config' 17 | module.name_mapper='^constants\/index$' -> '/app/scripts/constants/' 18 | module.name_mapper='^containers\/(.*)$' -> '/app/scripts/containers/\1' 19 | module.name_mapper='^modules\/(.*)$' -> '/app/scripts/modules/\1' 20 | module.name_mapper='^reducers\/(.*)$' -> '/app/scripts/reducers/\1' 21 | module.name_mapper='^routes\/(.*)$' -> '/app/scripts/routes/\1' 22 | module.name_mapper='^sagas\/(.*)$' -> '/app/scripts/sagas/\1' 23 | module.name_mapper='^utils\/(.*)$' -> '/app/scripts/utils/\1' 24 | 25 | munge_underscores=true 26 | 27 | suppress_type=$FlowIssue 28 | suppress_type=$FlowFixMe 29 | suppress_type=$FixMe 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | .sass-cache 3 | .publish 4 | coverage/ 5 | dist/ 6 | node_modules/ 7 | reports/ 8 | webpack.stats.json 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "plugins": [ 4 | "stylelint-declaration-strict-value", 5 | "stylelint-order", 6 | "stylelint-scss" 7 | ], 8 | "rules": { 9 | "at-rule-empty-line-before": [ 10 | "always", 11 | { "except": ["blockless-after-blockless", "first-nested"], "ignore": ["after-comment"] } 12 | ], 13 | "at-rule-no-unknown": null, 14 | "color-named": "never", 15 | "declaration-colon-newline-after": null, 16 | "declaration-block-no-redundant-longhand-properties": null, 17 | "declaration-empty-line-before": null, 18 | "declaration-property-value-blacklist": { 19 | "/^border/": ["none"] 20 | }, 21 | "function-url-quotes": "always", 22 | "indentation": [2, { "ignore": ["value"] }], 23 | "max-nesting-depth": 5, 24 | "no-descending-specificity": null, 25 | "no-duplicate-selectors": true, 26 | "no-empty-source": [true, { "severity": "warning" }], 27 | "no-missing-end-of-source-newline": true, 28 | "number-max-precision": 4, 29 | "property-no-vendor-prefix": true, 30 | "selector-class-pattern": "^((?:-{1,2}|_{2})?[a-z0-9]+(?:(?:-{1,2}|_{2})[a-z0-9]+)*)(?:-{1,2}|_{2})?$", 31 | "selector-max-compound-selectors": 5, 32 | "selector-max-specificity": ["1,6,4"], 33 | "selector-no-qualifying-type": [true, { "ignore": ["class"] }], 34 | "selector-pseudo-element-colon-notation": "single", 35 | "string-quotes": "single", 36 | "unit-blacklist": [ 37 | ["px", "em"], { 38 | "ignoreProperties": { 39 | "px": ["max-width"] 40 | } 41 | } 42 | ], 43 | "order/order": [ 44 | "custom-properties", 45 | "dollar-variables", 46 | { "type": "at-rule", "name": "extend" }, 47 | { "type": "at-rule", "name": "include", "hasBlock": false }, 48 | "declarations", 49 | { 50 | "type": "at-rule", 51 | "name": "include", 52 | "hasBlock": true 53 | }, 54 | "rules", 55 | "at-rules" 56 | ], 57 | "order/properties-alphabetical-order": true, 58 | "scale-unlimited/declaration-strict-value": [ 59 | ["/colore/"], 60 | { 61 | "ignoreKeywords": { 62 | "/color/": ["transparent", "inherit"] 63 | } 64 | } 65 | ], 66 | "scss/at-import-no-partial-leading-underscore": true, 67 | "scss/at-import-partial-extension-blacklist": ["scss"], 68 | "scss/at-rule-no-unknown": true, 69 | "scss/dollar-variable-colon-space-after": "always", 70 | "scss/dollar-variable-colon-space-before": "never", 71 | "scss/dollar-variable-no-missing-interpolation": true, 72 | "scss/double-slash-comment-whitespace-inside": "always", 73 | "scss/operator-no-unspaced": true, 74 | "scss/selector-no-redundant-nesting-selector": true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '9' 4 | cache: 5 | directories: 6 | - "node_modules" 7 | before_script: 8 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 9 | - chmod +x ./cc-test-reporter 10 | - ./cc-test-reporter before-build 11 | script: 12 | - npm run test 13 | after_script: 14 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 15 | addons: 16 | code_climate: true 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React-Redux-Observables Boilerplate 2 | === 3 | 4 | [![Build Status](https://travis-ci.org/gilbarbara/react-redux-observables-boilerplate.svg?branch=master)](https://travis-ci.org/gilbarbara/react-redux-observables-boilerplate) 5 | [![Dependencies Status](https://david-dm.org/gilbarbara/react-redux-observables-boilerplate/status.svg)](https://david-dm.org/gilbarbara/react-redux-observables-boilerplate) 6 | [![Code Climate](https://codeclimate.com/github/gilbarbara/react-redux-observables-boilerplate/badges/gpa.svg)](https://codeclimate.com/github/gilbarbara/react-redux-observables-boilerplate) 7 | [![Test Coverage](https://codeclimate.com/github/gilbarbara/react-redux-observables-boilerplate/badges/coverage.svg)](https://codeclimate.com/github/gilbarbara/react-redux-observables-boilerplate/coverage) 8 | 9 | [Demo](http://gilbarbara.github.io/react-redux-observables-boilerplate) 10 | 11 | ### Provides 12 | - react ^16.x 13 | - react-router ^4.x 14 | - redux ^3.x 15 | - redux-observable ^0.17 16 | - redux-persist ^5.x 17 | - react-helmet ^5.x 18 | - rxjs ^5.x 19 | 20 | ### Building 21 | - webpack ^3.x 22 | 23 | `npm run build` 24 | 25 | ### Development 26 | - webpack-dev-server ^2.x 27 | - react-dev-utils ^4.x 28 | - react-error-overlay ^3.x 29 | - react-hot-loader ^3.x 30 | 31 | `npm start` 32 | 33 | ### Tests 34 | - jest ^22.x 35 | - enzyme ^3.x 36 | 37 | `npm test` 38 | `npm run test:watch` 39 | 40 | ### Browser Automation 41 | - nightwatch ^0.9 42 | - selenium ^3.4 43 | 44 | `npm run test:automation` (with dev-server already running) 45 | `npm run test:automation:start` (start dev-server, run tests and exit) 46 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | Disallow: 5 | -------------------------------------------------------------------------------- /app/scripts/actions/app.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * @module Actions/App 4 | * @desc App Actions 5 | */ 6 | import { go, goBack, push, replace } from 'react-router-redux'; 7 | import uuid from 'uuid/v4'; 8 | 9 | import { ActionTypes } from 'constants/index'; 10 | 11 | export { go, goBack, push, replace }; 12 | 13 | /** 14 | * Hide alert. 15 | * 16 | * @param {string} id 17 | * @returns {Object} 18 | */ 19 | export function hideAlert(id: string): Object { 20 | return { 21 | type: ActionTypes.HIDE_ALERT, 22 | payload: { id }, 23 | }; 24 | } 25 | 26 | /** 27 | * Show an alert. 28 | * 29 | * @param {string} message 30 | * @param {Object} options 31 | * @param {string} options.type - Type of the alert. Available: success, error, warning, info, black 32 | * @param {number} [options.timeout] - Delay in seconds for the notification go away. Set this to 0 to not auto-dismiss the notification 33 | * @param {string} [options.position] 34 | * @param {string} [options.icon] 35 | * 36 | * @returns {Object} 37 | */ 38 | export function showAlert(message: string, options: Object): Object { 39 | const timeout = options.type === 'error' ? 0 : 5; 40 | 41 | return { 42 | type: ActionTypes.SHOW_ALERT, 43 | payload: { 44 | id: options.id || uuid(), 45 | icon: options.icon, 46 | message, 47 | position: options.position || 'bottom-right', 48 | type: options.type, 49 | timeout: !isNaN(options.timeout) ? options.timeout : timeout, 50 | }, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /app/scripts/actions/github.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * @module Actions/Github 4 | * @desc Actions for Github 5 | */ 6 | 7 | import { ActionTypes } from 'constants/index'; 8 | 9 | /** 10 | * fetchPopularRepos 11 | * 12 | * @returns {Object} 13 | */ 14 | export function fetchPopularRepos(): Object { 15 | return { 16 | type: ActionTypes.FETCH_POPULAR_REPOS_REQUEST, 17 | payload: {}, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /app/scripts/actions/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * @module Actions/Root 4 | * @desc Actions Root 5 | */ 6 | 7 | export * from './app'; 8 | export * from './github'; 9 | export * from './user'; 10 | -------------------------------------------------------------------------------- /app/scripts/actions/user.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * @module Actions/User 4 | * @desc Actions for User 5 | */ 6 | 7 | import { ActionTypes } from 'constants/index'; 8 | 9 | /** 10 | * Login 11 | * 12 | * @returns {Object} 13 | */ 14 | export function login(): Object { 15 | return { 16 | type: ActionTypes.USER_LOGIN_REQUEST, 17 | payload: {}, 18 | }; 19 | } 20 | 21 | /** 22 | * Logout 23 | * 24 | * @returns {Object} 25 | */ 26 | export function logOut(): Object { 27 | return { 28 | type: ActionTypes.USER_LOGOUT_REQUEST, 29 | payload: {}, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /app/scripts/components/Alert.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Alert = ({ children, handleClickClose, id, icon, type }) => { 5 | const output = {}; 6 | const typeClass = type ? ` is-${type}` : ''; 7 | 8 | switch (type) { 9 | case 'success': { 10 | output.icon = icon || 'i-check-circle'; 11 | break; 12 | } 13 | case 'error': { 14 | output.icon = icon || 'i-times-circle'; 15 | break; 16 | } 17 | case 'warning': { 18 | output.icon = icon || 'i-exclamation-circle'; 19 | break; 20 | } 21 | case 'info': { 22 | output.icon = icon || 'i-question-circle'; 23 | break; 24 | } 25 | case 'black': { 26 | output.icon = icon || 'i-bell-o'; 27 | break; 28 | } 29 | default: { 30 | output.icon = icon || 'i-dot-circle-o'; 31 | } 32 | } 33 | 34 | if (handleClickClose) { 35 | output.button = ( 36 | 43 | ); 44 | } 45 | 46 | return ( 47 |
48 |
49 | 50 |
51 |
{children}
52 | {output.button} 53 |
54 | ); 55 | }; 56 | 57 | Alert.propTypes = { 58 | children: PropTypes.node.isRequired, 59 | handleClickClose: PropTypes.func, 60 | icon: PropTypes.string, 61 | id: PropTypes.string, 62 | type: PropTypes.string, 63 | }; 64 | 65 | export default Alert; 66 | -------------------------------------------------------------------------------- /app/scripts/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => ( 4 |
5 |
6 |
7 |
"`; 4 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Header.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Header should render properly 1`] = `"
"`; 4 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Loader.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Loader should render properly with pulse type 1`] = `"
"`; 4 | 5 | exports[`Loader should render properly with rotate type 1`] = `"
"`; 6 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Logo.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Logo should render properly 1`] = `"
\\"React-Redux-Observables
"`; 4 | -------------------------------------------------------------------------------- /test/containers/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import immutable from 'immutability-helper'; 4 | 5 | import { App } from 'containers/App'; 6 | 7 | jest.mock('uuid/v4', () => () => '123-456'); 8 | 9 | const mockDispatch = jest.fn(); 10 | const props = { 11 | app: { 12 | alerts: [], 13 | }, 14 | dispatch: mockDispatch, 15 | router: {}, 16 | user: { 17 | isAuthenticated: false, 18 | }, 19 | }; 20 | 21 | function setup(ownProps = props) { 22 | return shallow(); 23 | } 24 | 25 | describe('App', () => { 26 | const wrapper = setup(); 27 | 28 | it('should be a Component', () => { 29 | expect(wrapper.instance() instanceof React.Component).toBe(true); 30 | }); 31 | 32 | it('should render properly', () => { 33 | expect(wrapper.find('.app')).toBePresent(); 34 | expect(wrapper.find('Header')).toBePresent(); 35 | expect(wrapper.find('Footer')).toBePresent(); 36 | expect(wrapper.find('SystemAlerts')).toBePresent(); 37 | }); 38 | 39 | it('should have all the Router components', () => { 40 | expect(wrapper.find('ConnectedRouter')).toBePresent(); 41 | expect(wrapper.find('Route').length).toBe(2); 42 | expect(wrapper.find('RoutePrivate')).toBePresent(); 43 | expect(wrapper.find('RoutePublic')).toBePresent(); 44 | }); 45 | 46 | it('should handle authentication changes', () => { 47 | const instance = wrapper.instance(); 48 | wrapper.setProps(immutable(instance.props, { 49 | user: { 50 | isAuthenticated: { $set: true }, 51 | }, 52 | })); 53 | 54 | expect(mockDispatch).toHaveBeenCalledWith({ 55 | type: 'SHOW_ALERT', 56 | payload: { 57 | icon: 'i-flash', 58 | id: '123-456', 59 | message: 'Hello!', 60 | position: 'bottom-right', 61 | timeout: 5, 62 | type: 'primary', 63 | }, 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/epics/github.spec.js: -------------------------------------------------------------------------------- 1 | import xhrMock from 'xhr-mock'; 2 | import { ActionTypes } from 'constants/index'; 3 | import rootEpic from 'epics'; 4 | import mockStore, { epicMiddleware } from '../__setup__/mockedStore'; 5 | 6 | describe('github', () => { 7 | let store; 8 | 9 | beforeEach(() => { 10 | xhrMock.setup(); 11 | store = mockStore(); 12 | }); 13 | 14 | afterEach(() => { 15 | epicMiddleware.replaceEpic(rootEpic); 16 | }); 17 | 18 | it('fetchPopularRepos should return SUCCESS', done => { 19 | const payload = { items: [{ id: 123 }] }; 20 | 21 | xhrMock.get('https://api.github.com/search/repositories?q=+language:javascript+created:%3E2016-10-01&sort=stars&order=desc', (req, res) => 22 | res 23 | .status(200) 24 | .header('Content-Type', 'application/json') 25 | .body(payload) 26 | ); 27 | 28 | store.dispatch({ type: ActionTypes.FETCH_POPULAR_REPOS_REQUEST }); 29 | 30 | setTimeout(() => { 31 | expect(store.getActions()).toEqual([ 32 | { type: ActionTypes.FETCH_POPULAR_REPOS_REQUEST }, 33 | { 34 | type: ActionTypes.FETCH_POPULAR_REPOS_SUCCESS, 35 | payload: { data: [{ id: 123 }] }, 36 | }, 37 | ]); 38 | 39 | done(); 40 | }, 100); 41 | }); 42 | 43 | it('fetchPopularRepos should return FAILURE', done => { 44 | xhrMock.get('https://api.github.com/search/repositories?q=+language:javascript+created:%3E2016-10-01&sort=stars&order=desc', (req, res) => 45 | res 46 | .status(404) 47 | ); 48 | 49 | store.dispatch({ type: ActionTypes.FETCH_POPULAR_REPOS_REQUEST }); 50 | 51 | setTimeout(() => { 52 | expect(store.getActions()).toEqual([ 53 | { type: ActionTypes.FETCH_POPULAR_REPOS_REQUEST }, 54 | { 55 | type: ActionTypes.FETCH_POPULAR_REPOS_FAILURE, 56 | payload: { message: 'ajax error 404', status: 404 }, 57 | error: true, 58 | }, 59 | ]); 60 | 61 | done(); 62 | }, 100); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/epics/user.spec.js: -------------------------------------------------------------------------------- 1 | import rootEpic from 'epics'; 2 | import { ActionTypes } from 'constants/index'; 3 | import mockStore, { epicMiddleware } from '../__setup__/mockedStore'; 4 | 5 | describe('user', () => { 6 | let store; 7 | 8 | beforeEach(() => { 9 | store = mockStore(); 10 | }); 11 | 12 | afterEach(() => { 13 | epicMiddleware.replaceEpic(rootEpic); 14 | }); 15 | 16 | it('userLogin should return SUCCESS', done => { 17 | store.dispatch({ type: ActionTypes.USER_LOGIN_REQUEST }); 18 | 19 | setTimeout(() => { 20 | expect(store.getActions()).toEqual([ 21 | { type: ActionTypes.USER_LOGIN_REQUEST }, 22 | { type: ActionTypes.USER_LOGIN_SUCCESS }, 23 | ]); 24 | 25 | done(); 26 | }, 1000); 27 | }); 28 | 29 | it('userLogout epic', done => { 30 | store.dispatch({ type: ActionTypes.USER_LOGOUT_REQUEST }); 31 | 32 | setTimeout(() => { 33 | expect(store.getActions()).toEqual([ 34 | { type: ActionTypes.USER_LOGOUT_REQUEST }, 35 | { type: ActionTypes.USER_LOGOUT_SUCCESS }, 36 | ]); 37 | 38 | done(); 39 | }, 1000); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { init } from 'index'; 3 | import { ActionTypes, XHR } from 'constants/index'; 4 | 5 | jest.mock('redux-persist/lib/integration/react', () => ({ 6 | PersistGate: () => (
), 7 | })); 8 | 9 | describe('index/init', () => { 10 | beforeAll(() => { 11 | process.env.NODE_ENV = 'production'; 12 | }); 13 | 14 | afterAll(() => { 15 | process.env.NODE_ENV = 'test'; 16 | }); 17 | 18 | it('should initiate the app', () => { 19 | init.run().then(environment => { 20 | expect(environment).toBe('production'); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('Constants:ActionTypes', () => { 26 | it('should match the snapshot', () => { 27 | expect(ActionTypes).toMatchSnapshot(); 28 | }); 29 | }); 30 | 31 | describe('Constants:XHR', () => { 32 | it('should match the snapshot', () => { 33 | expect(XHR).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/modules/RoutePrivate.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router-dom/MemoryRouter'; 3 | import { renderToString } from 'react-dom/server'; 4 | import RoutePrivate from 'modules/RoutePrivate'; 5 | 6 | describe('modules/RoutePrivate', () => { 7 | it('should redirect for unauthenticated access', () => { 8 | const render = renderToString( 9 | 10 | (
PRIVATE
)} 14 | isAuthenticated={false} 15 | /> 16 |
); 17 | 18 | expect(render).toMatchSnapshot(); 19 | }); 20 | 21 | it('should allow navigation for authenticated access', () => { 22 | const render = renderToString( 23 | 24 | (
PRIVATE
)} 28 | isAuthenticated={true} 29 | /> 30 |
31 | ); 32 | 33 | expect(render).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/modules/RoutePublic.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router-dom/MemoryRouter'; 3 | import { renderToString } from 'react-dom/server'; 4 | import RoutePublic from 'modules/RoutePublic'; 5 | 6 | describe('modules/RoutePublic', () => { 7 | it('should render the Login component for unauthenticated access', () => { 8 | const render = renderToString( 9 | 10 | (
LOGIN
)} 14 | isAuthenticated={false} 15 | /> 16 |
17 | ); 18 | 19 | expect(render).toMatchSnapshot(); 20 | }); 21 | 22 | it('should redirect to /private for authenticated access', () => { 23 | const render = renderToString( 24 | 25 | (
LOGIN
)} 29 | isAuthenticated={true} 30 | /> 31 |
32 | ); 33 | 34 | expect(render).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/modules/__snapshots__/RoutePrivate.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`modules/RoutePrivate should allow navigation for authenticated access 1`] = `"
PRIVATE
"`; 4 | 5 | exports[`modules/RoutePrivate should redirect for unauthenticated access 1`] = `"
"`; 6 | -------------------------------------------------------------------------------- /test/modules/__snapshots__/RoutePublic.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`modules/RoutePublic should redirect to /private for authenticated access 1`] = `"
"`; 4 | 5 | exports[`modules/RoutePublic should render the Login component for unauthenticated access 1`] = `"
LOGIN
"`; 6 | -------------------------------------------------------------------------------- /test/modules/__snapshots__/client.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`utils/api should execute a GET successfully with json 1`] = ` 4 | Object { 5 | "hello": "world", 6 | } 7 | `; 8 | 9 | exports[`utils/api should execute a POST successfully 1`] = `"ok"`; 10 | 11 | exports[`utils/api should reject for a not found error 1`] = ` 12 | Object { 13 | "error": true, 14 | "message": null, 15 | "status": 0, 16 | } 17 | `; 18 | 19 | exports[`utils/api should reject for a server error with JSON response 1`] = ` 20 | Object { 21 | "error": true, 22 | "message": null, 23 | "status": 0, 24 | } 25 | `; 26 | 27 | exports[`utils/api should reject for a server error with no response 1`] = ` 28 | Object { 29 | "error": true, 30 | "status": 0, 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /test/modules/__snapshots__/helpers.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`helpers createReducer should return a proper object 1`] = `[Function]`; 4 | -------------------------------------------------------------------------------- /test/modules/client.spec.js: -------------------------------------------------------------------------------- 1 | import xhrMock from 'xhr-mock'; 2 | 3 | import { request } from 'modules/client'; 4 | 5 | describe('utils/api', () => { 6 | beforeEach(() => { 7 | xhrMock.setup(); 8 | }); 9 | 10 | it('should fail without param', () => { 11 | expect(() => request()).toThrow('Error! You must pass `url`'); 12 | }); 13 | 14 | it('should execute a GET successfully with json', done => { 15 | xhrMock.get('/token', (req, res) => 16 | res 17 | .status(200) 18 | .header('Content-Type', 'application/json') 19 | .body({ hello: 'world' }) 20 | ); 21 | 22 | request({ url: '/token' }) 23 | .then(ajax => { 24 | expect(ajax.response).toMatchSnapshot(); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should execute a POST successfully', done => { 30 | xhrMock.post('/token', (req, res) => 31 | res 32 | .status(201) 33 | .body('ok') 34 | ); 35 | 36 | request({ url: '/token', method: 'POST', payload: { a: 1 } }) 37 | .then(data => { 38 | expect(data.response).toMatchSnapshot(); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('should fail to a PUT without payload', () => { 44 | expect(() => request({ url: '/token', method: 'PUT' })) 45 | .toThrow('Error! You must pass `payload`'); 46 | }); 47 | 48 | it('should reject for a server error with JSON response', done => { 49 | xhrMock.get('token', (req, res) => 50 | res 51 | .status(500) 52 | .header('Content-Type', 'application/json') 53 | .body({ error: 'FAILED' }) 54 | ); 55 | 56 | request({ url: '/token' }) 57 | .catch(error => { 58 | expect({ 59 | error: true, 60 | message: error.xhr.response, 61 | status: error.status, 62 | }).toMatchSnapshot(); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should reject for a server error with no response', done => { 68 | xhrMock.get('token', (req, res) => 69 | res.status(500) 70 | ); 71 | 72 | request({ url: '/token' }) 73 | .catch(error => { 74 | expect({ 75 | error: true, 76 | status: error.status, 77 | }).toMatchSnapshot(); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('should reject for a not found error', done => { 83 | xhrMock.get('token', (req, res) => 84 | res 85 | .status(404) 86 | .header('Content-Type', 'application/json') 87 | .body({ error: 'NOT FOUND' }) 88 | ); 89 | 90 | request({ url: '/token' }) 91 | .catch(error => { 92 | expect({ 93 | error: true, 94 | message: error.xhr.response, 95 | status: error.status, 96 | }).toMatchSnapshot(); 97 | done(); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/modules/helpers.spec.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'modules/helpers'; 2 | 3 | describe('helpers', () => { 4 | describe('createReducer', () => { 5 | it('should return a proper object', () => { 6 | expect(createReducer('REQUEST')).toMatchSnapshot(); 7 | }); 8 | }); 9 | }); 10 | 11 | -------------------------------------------------------------------------------- /test/reducers/__snapshots__/app.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App should handle HIDE_ALERT 1`] = `undefined`; 4 | 5 | exports[`App should handle SHOW_ALERT 1`] = `undefined`; 6 | 7 | exports[`App should return the initial state 1`] = ` 8 | Object { 9 | "alerts": Array [], 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /test/reducers/__snapshots__/github.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Github should handle FETCH_POPULAR_REPOS_FAILURE 1`] = ` 4 | Object { 5 | "popularRepos": Object { 6 | "data": Array [], 7 | "message": "ajax error 404", 8 | "status": "error", 9 | }, 10 | } 11 | `; 12 | 13 | exports[`Github should handle FETCH_POPULAR_REPOS_REQUEST 1`] = ` 14 | Object { 15 | "popularRepos": Object { 16 | "data": Array [], 17 | "message": "", 18 | "status": "running", 19 | }, 20 | } 21 | `; 22 | 23 | exports[`Github should handle FETCH_POPULAR_REPOS_SUCCESS 1`] = ` 24 | Object { 25 | "popularRepos": Object { 26 | "data": Array [ 27 | Object { 28 | "id": 1, 29 | }, 30 | ], 31 | "message": "", 32 | "status": "loaded", 33 | }, 34 | } 35 | `; 36 | 37 | exports[`Github should return the initial state 1`] = ` 38 | Object { 39 | "popularRepos": Object { 40 | "data": Array [], 41 | "message": "", 42 | "status": "idle", 43 | }, 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /test/reducers/__snapshots__/user.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`User should handle USER_LOGIN_REQUEST 1`] = ` 4 | Object { 5 | "isAuthenticated": false, 6 | "status": "running", 7 | } 8 | `; 9 | 10 | exports[`User should handle USER_LOGIN_SUCCESS 1`] = ` 11 | Object { 12 | "isAuthenticated": true, 13 | "status": "idle", 14 | } 15 | `; 16 | 17 | exports[`User should handle USER_LOGOUT_REQUEST 1`] = ` 18 | Object { 19 | "isAuthenticated": false, 20 | "status": "running", 21 | } 22 | `; 23 | 24 | exports[`User should handle USER_LOGOUT_SUCCESS 1`] = ` 25 | Object { 26 | "isAuthenticated": false, 27 | "status": "idle", 28 | } 29 | `; 30 | 31 | exports[`User should return the initial state 1`] = ` 32 | Object { 33 | "isAuthenticated": false, 34 | "status": "idle", 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /test/reducers/app.spec.js: -------------------------------------------------------------------------------- 1 | import reducers from 'reducers'; 2 | import * as Actions from 'actions'; 3 | import { ActionTypes } from 'constants/index'; 4 | 5 | describe('App', () => { 6 | it('should return the initial state', () => { 7 | expect(reducers.app(undefined, {})).toMatchSnapshot(); 8 | }); 9 | 10 | it(`should handle ${ActionTypes.SHOW_ALERT}`, () => { 11 | expect(reducers.app(undefined, Actions.showAlert('success', 'hello')).notifications).toMatchSnapshot(); 12 | }); 13 | 14 | it(`should handle ${ActionTypes.HIDE_ALERT}`, () => { 15 | expect(reducers.app(undefined, Actions.hideAlert()).notifications).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/reducers/github.spec.js: -------------------------------------------------------------------------------- 1 | import reducers from 'reducers'; 2 | import { ActionTypes } from 'constants/index'; 3 | 4 | describe('Github', () => { 5 | it('should return the initial state', () => { 6 | expect(reducers.github(undefined, {})).toMatchSnapshot(); 7 | }); 8 | 9 | it(`should handle ${ActionTypes.FETCH_POPULAR_REPOS_REQUEST}`, () => { 10 | expect(reducers.github(undefined, { type: ActionTypes.FETCH_POPULAR_REPOS_REQUEST })).toMatchSnapshot(); 11 | }); 12 | 13 | it(`should handle ${ActionTypes.FETCH_POPULAR_REPOS_SUCCESS}`, () => { 14 | expect(reducers.github(undefined, { 15 | type: ActionTypes.FETCH_POPULAR_REPOS_SUCCESS, 16 | payload: { data: [{ id: 1 }] }, 17 | })) 18 | .toMatchSnapshot(); 19 | }); 20 | 21 | it(`should handle ${ActionTypes.FETCH_POPULAR_REPOS_FAILURE}`, () => { 22 | expect(reducers.github(undefined, { 23 | type: ActionTypes.FETCH_POPULAR_REPOS_FAILURE, 24 | payload: { message: 'ajax error 404', status: 404 }, 25 | })).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/reducers/user.spec.js: -------------------------------------------------------------------------------- 1 | import reducers from 'reducers'; 2 | import { ActionTypes } from 'constants/index'; 3 | 4 | describe('User', () => { 5 | it('should return the initial state', () => { 6 | expect(reducers.user(undefined, {})).toMatchSnapshot(); 7 | }); 8 | 9 | it(`should handle ${ActionTypes.USER_LOGIN_REQUEST}`, () => { 10 | expect(reducers.user(undefined, { type: ActionTypes.USER_LOGIN_REQUEST })).toMatchSnapshot(); 11 | }); 12 | 13 | it(`should handle ${ActionTypes.USER_LOGIN_SUCCESS}`, () => { 14 | expect(reducers.user(undefined, { type: ActionTypes.USER_LOGIN_SUCCESS })).toMatchSnapshot(); 15 | }); 16 | 17 | it(`should handle ${ActionTypes.USER_LOGOUT_REQUEST}`, () => { 18 | expect(reducers.user(undefined, { type: ActionTypes.USER_LOGOUT_REQUEST })).toMatchSnapshot(); 19 | }); 20 | 21 | it(`should handle ${ActionTypes.USER_LOGOUT_SUCCESS}`, () => { 22 | expect(reducers.user(undefined, { type: ActionTypes.USER_LOGOUT_SUCCESS })).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/routes/Home.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import { Home } from 'routes/Home'; 5 | 6 | const mockDispatch = jest.fn(); 7 | 8 | function setup() { 9 | const props = { 10 | dispatch: mockDispatch, 11 | location: {}, 12 | }; 13 | 14 | return mount(); 15 | } 16 | 17 | describe('Home', () => { 18 | const wrapper = setup(true); 19 | 20 | it('should be a Component', () => { 21 | expect(wrapper.instance() instanceof React.Component).toBe(true); 22 | }); 23 | 24 | it('should render properly', () => { 25 | expect(wrapper.html()).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/routes/Login.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import Login from 'routes/Login'; 5 | 6 | function setup() { 7 | const props = { 8 | location: {}, 9 | }; 10 | 11 | return mount(); 12 | } 13 | 14 | describe('Login', () => { 15 | const wrapper = setup(true); 16 | 17 | it('should be a StatelessComponent', () => { 18 | expect(wrapper.instance()).toBeNull(); 19 | }); 20 | 21 | it('should render properly', () => { 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | 25 | it('should render the redirect location', () => { 26 | wrapper.setProps({ 27 | location: { 28 | state: { from: '/private' }, 29 | }, 30 | }); 31 | 32 | expect(wrapper.html()).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/routes/NotFound.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import NotFound from 'routes/NotFound'; 5 | 6 | function setup() { 7 | return mount(); 8 | } 9 | 10 | describe('NotFound', () => { 11 | const wrapper = setup(true); 12 | 13 | it('should be a StatelessComponent', () => { 14 | expect(wrapper.instance()).toBeNull(); 15 | }); 16 | 17 | it('should render properly', () => { 18 | expect(wrapper.html()).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/routes/Private.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import { Private } from 'routes/Private'; 5 | 6 | const mockDispatch = jest.fn(); 7 | 8 | function setup() { 9 | const props = { 10 | dispatch: mockDispatch, 11 | location: {}, 12 | github: { 13 | popularRepos: { 14 | isReady: true, 15 | isLoading: false, 16 | data: [ 17 | { 18 | name: 'Repo', 19 | html_url: 'http://...', 20 | owner: { login: 'github' }, 21 | description: 'Oh Hai', 22 | }, 23 | ], 24 | }, 25 | }, 26 | }; 27 | 28 | return mount(); 29 | } 30 | 31 | describe('Private', () => { 32 | const wrapper = setup(true); 33 | 34 | it('should be a Component', () => { 35 | expect(wrapper.instance() instanceof React.Component).toBe(true); 36 | }); 37 | 38 | it('should render properly', () => { 39 | expect(wrapper.html()).toMatchSnapshot(); 40 | }); 41 | 42 | it('should handle clicks', () => { 43 | wrapper.setProps({ 44 | github: { 45 | popularRepos: { 46 | isReady: false, 47 | data: [], 48 | }, 49 | }, 50 | }); 51 | 52 | wrapper.find('.btn').simulate('click'); 53 | expect(mockDispatch.mock.calls[0][0]).toEqual({ 54 | type: 'FETCH_POPULAR_REPOS_REQUEST', 55 | payload: {}, 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/routes/__snapshots__/Home.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Home should render properly 1`] = `"
\\"React-Redux-Observables

React-Redux-Observables Boilerplate

A starter kit for React with Router, Redux, Observable + RxJS

Get it!

Provides

  • React

  • Redux

  • Redux Observable

  • react-router

  • RxJS

  • Webpack 2.x

"`; 4 | -------------------------------------------------------------------------------- /test/routes/__snapshots__/Login.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Login should render properly 1`] = `""`; 4 | 5 | exports[`Login should render the redirect location 1`] = `""`; 6 | -------------------------------------------------------------------------------- /test/routes/__snapshots__/NotFound.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NotFound should render properly 1`] = `"

404

"`; 4 | -------------------------------------------------------------------------------- /test/routes/__snapshots__/Private.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Private should render properly 1`] = `"

Popular Repos

"`; 4 | -------------------------------------------------------------------------------- /test/store/index.spec.js: -------------------------------------------------------------------------------- 1 | import { store, persistor } from 'app-store'; 2 | 3 | describe('store', () => { 4 | it('should have a store', () => { 5 | expect(store.getState()).toEqual({ 6 | _persist: { rehydrated: true, version: -1 }, 7 | app: { 8 | alerts: [], 9 | }, 10 | github: { 11 | popularRepos: { 12 | data: [], 13 | message: '', 14 | status: 'idle', 15 | }, 16 | }, 17 | router: { location: null }, 18 | user: { 19 | isAuthenticated: false, 20 | status: 'idle', 21 | }, 22 | }); 23 | }); 24 | 25 | it('should have a persistor', () => { 26 | expect(persistor.getState()).toEqual({ 27 | bootstrapped: true, 28 | registry: [], 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/ui/ui.nightwatch.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console, no-var, no-unused-expressions no-console, func-names, prefer-arrow-callback, object-shorthand */ 2 | var timer = 2500; 3 | var wait = 1000; 4 | 5 | module.exports = { 6 | after: function(browser) { 7 | console.log('Closing down...'); 8 | browser.end(); 9 | }, 10 | 11 | 'should be able to init a session': browser => { 12 | browser 13 | .url('http://localhost:3000/') 14 | .resizeWindow(1280, 800) 15 | .waitForElementVisible('#react', timer); 16 | }, 17 | 18 | 'should be able to see the App UI': browser => { 19 | browser.assert.elementPresent('.app'); 20 | }, 21 | 22 | 'should be able to see Home': browser => { 23 | browser.assert.elementPresent('.app__home'); 24 | browser.assert.elementPresent('.app__logo'); 25 | browser.assert.elementPresent('.app__home__download'); 26 | browser.assert.elementPresent('.app__footer'); 27 | }, 28 | 29 | 'should be able to click Login': browser => { 30 | browser.click('.btn-primary'); 31 | }, 32 | 33 | 'should be able to navigate to /private': browser => { 34 | browser.click('a[href="/private"]'); 35 | }, 36 | 37 | 'should have navigated to /private': browser => { 38 | browser.waitForElementVisible('.app__private', timer); 39 | browser.assert.urlContains('/private'); 40 | 41 | browser.assert.elementPresent('.app__header__logout'); 42 | browser.assert.containsText('h2', 'Popular Repos'); 43 | }, 44 | 45 | 'should be able to logout': browser => { 46 | browser.click('.app__header__logout'); 47 | browser.pause(wait); 48 | browser.assert.urlContains('/login'); 49 | }, 50 | 51 | 'should block navigation to /private if not logged': browser => { 52 | browser.url('http://localhost:3000/private'); 53 | browser.pause(wait); 54 | browser.assert.urlEquals('http://localhost:3000/login'); 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /tools/index.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-var, vars-on-top, no-console */ 2 | const { promisify } = require('util'); 3 | const { exec } = require('child_process'); 4 | const chalk = require('chalk'); 5 | const yargs = require('yargs'); 6 | const Rsync = require('rsync'); 7 | 8 | const paths = require('../config/paths'); 9 | 10 | const run = promisify(exec); 11 | 12 | function publish() { 13 | console.log(chalk.blue('Publishing...')); 14 | const rsync = new Rsync() 15 | .shell('ssh') 16 | .exclude('.DS_Store') 17 | .flags('az') 18 | .source(`${paths.destination}/`) 19 | .destination('gbarbara@gilbarbara.com:/home/gbarbara/public_html/react-redux-observables-boilerplate'); 20 | 21 | rsync.execute((error, code, cmd) => { 22 | if (error) { 23 | console.log(chalk.red('Something went wrong...', error, code, cmd)); 24 | process.exit(1); 25 | } 26 | 27 | console.log(chalk.green('Published')); 28 | }); 29 | } 30 | 31 | function deploy() { 32 | const start = Date.now(); 33 | console.log(chalk.green('Bundling...')); 34 | 35 | return exec('npm run build', errBuild => { 36 | if (errBuild) { 37 | console.log(chalk.red(errBuild)); 38 | process.exit(1); 39 | } 40 | 41 | console.log(`Bundled in ${(Date.now() - start) / 1000} s`); 42 | 43 | publish(); 44 | }); 45 | } 46 | 47 | function updateDependencies() { 48 | return run('git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD') 49 | .then(({ stdout }) => { 50 | if (stdout.match('package.json')) { 51 | console.log(chalk.yellow('▼ Updating...')); 52 | exec('npm update').stdout.pipe(process.stdout); 53 | } 54 | else { 55 | console.log(chalk.green('✔ Nothing to update')); 56 | } 57 | }) 58 | .catch(err => { 59 | throw new Error(err); 60 | }); 61 | } 62 | 63 | function checkUpstream() { 64 | return run('git rev-parse --is-inside-work-tree') 65 | .then(() => 66 | run('git remote -v update') 67 | .then(() => { 68 | Promise.all([ 69 | run('git rev-parse @'), 70 | run('git rev-parse @{u}'), 71 | run('git merge-base @ @{u}'), 72 | ]) 73 | .then(([ 74 | { stdout: $local }, 75 | { stdout: $remote }, 76 | { stdout: $base }, 77 | ]) => { 78 | if ($local === $remote) { 79 | console.log(chalk.green('✔ Repo is up-to-date!')); 80 | } 81 | else if ($local === $base) { 82 | console.log(chalk.red('⊘ Error'), 'You need to pull, there are new commits.'); 83 | process.exit(1); 84 | } 85 | }) 86 | .catch(err => { 87 | if (err.message.includes('no upstream configured ')) { 88 | console.log(chalk.yellow('⚠ Warning'), 'No upstream. Is this a new branch?'); 89 | return; 90 | } 91 | 92 | console.log(chalk.yellow('⚠ Warning'), err.message); 93 | }); 94 | }) 95 | .catch(err => { 96 | throw new Error(err); 97 | }) 98 | ) 99 | .catch(() => { 100 | console.log('not under git'); 101 | }); 102 | } 103 | 104 | module.exports = yargs 105 | .command({ 106 | command: 'publish', 107 | desc: 'publish last build to pages', 108 | handler: publish, 109 | }) 110 | .command({ 111 | command: 'deploy', 112 | desc: 'build and publish to pages', 113 | handler: deploy, 114 | }) 115 | .command({ 116 | command: 'upstream', 117 | desc: 'has new remote commits', 118 | handler: checkUpstream, 119 | }) 120 | .command({ 121 | command: 'dependencies', 122 | desc: 'run `npm update` if package.json has changed', 123 | handler: updateDependencies, 124 | }) 125 | .demandCommand() 126 | .help() 127 | .wrap(72) 128 | .version(false) 129 | .strict() 130 | .fail((msg, err, instance) => { 131 | if (err) { 132 | throw new Error(err); 133 | } 134 | 135 | console.error(`${chalk.red(msg)}`); 136 | console.log(instance.help()); 137 | process.exit(1); 138 | }) 139 | .argv; 140 | -------------------------------------------------------------------------------- /tools/start.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable func-names, prefer-arrow-callback, no-console */ 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const { spawn, spawnSync } = require('child_process'); 19 | const chalk = require('chalk'); 20 | const dateFns = require('date-fns'); 21 | const webpack = require('webpack'); 22 | const WebpackDevServer = require('webpack-dev-server'); 23 | const clearConsole = require('react-dev-utils/clearConsole'); 24 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 25 | const { 26 | choosePort, 27 | createCompiler, 28 | prepareProxy, 29 | prepareUrls, 30 | } = require('react-dev-utils/WebpackDevServerUtils'); 31 | const openBrowser = require('react-dev-utils/openBrowser'); 32 | const paths = require('../config/paths'); 33 | const config = require('../config/webpack.config.dev'); 34 | const createDevServerConfig = require('../config/webpackDevServer'); 35 | 36 | const args = process.argv.slice(2); 37 | const isInteractive = process.stdout.isTTY; 38 | const isAutomationTest = args[0] && args[0] === 'automation'; 39 | 40 | // Warn and crash if required files are missing 41 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 42 | process.exit(1); 43 | } 44 | 45 | // Tools like Cloud9 rely on this. 46 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 47 | const HOST = process.env.HOST || '0.0.0.0'; 48 | 49 | // We attempt to use the default port but if it is busy, we offer the user to 50 | // run on a different port. `detect()` Promise resolves to the next free port. 51 | choosePort(HOST, DEFAULT_PORT) 52 | .then(port => { 53 | if (port === null) { 54 | // We have not found a port. 55 | return; 56 | } 57 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 58 | const appName = require(paths.packageJson).name; 59 | const urls = prepareUrls(protocol, HOST, port); 60 | const compiler = createCompiler(webpack, config, appName, urls); 61 | 62 | // Load proxy config 63 | const proxySetting = require(paths.packageJson).proxy; 64 | const proxyConfig = prepareProxy(proxySetting, paths.assets); 65 | // Serve webpack assets generated by the compiler over a web sever. 66 | const serverConfig = createDevServerConfig( 67 | proxyConfig, 68 | urls.lanUrlForConfig 69 | ); 70 | 71 | let start; 72 | 73 | compiler.plugin('compile', function() { 74 | start = new Date(); 75 | }); 76 | 77 | compiler.plugin('emit', function(compilation, callback) { 78 | const now = new Date(); 79 | console.log(chalk.yellow(`Duration: ${dateFns.differenceInSeconds(now, start)}s - ${compilation.hash}`)); 80 | 81 | if (isAutomationTest) { 82 | spawnSync('pkill', ['-f', 'selenium']); 83 | 84 | const nightwatch = spawn(path.join(__dirname, '../node_modules/.bin/nightwatch'), [ 85 | '-c', 86 | path.join(__dirname, '../test/__setup__/nightwatch.conf.js'), 87 | ]); 88 | 89 | nightwatch.stdout.on('data', (data) => { 90 | process.stdout.write(data.toString()); 91 | }); 92 | 93 | nightwatch.stderr.on('data', (data) => { 94 | process.stdout.write(data.toString()); 95 | }); 96 | 97 | nightwatch.on('close', () => { 98 | process.exit(0); 99 | }); 100 | } 101 | 102 | callback(); 103 | }); 104 | 105 | const devServer = new WebpackDevServer(compiler, serverConfig); 106 | // Launch WebpackDevServer. 107 | devServer.listen(port, HOST, err => { 108 | if (err) { 109 | console.log(err); 110 | return; 111 | } 112 | 113 | if (isInteractive) { 114 | clearConsole(); 115 | } 116 | 117 | console.log(chalk.cyan('Starting the development server...')); 118 | 119 | if (!isAutomationTest) { 120 | openBrowser(urls.localUrlForBrowser); 121 | } 122 | }); 123 | 124 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 125 | process.on(sig, function() { 126 | devServer.close(); 127 | process.exit(); 128 | }); 129 | }); 130 | }) 131 | .catch(err => { 132 | if (err && err.message) { 133 | console.log(err.message); 134 | } 135 | process.exit(1); 136 | }); 137 | --------------------------------------------------------------------------------