├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .postcssrc ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── circle.yml ├── jest.json ├── package.json ├── sass-lint.yml ├── src ├── app │ ├── Layouts │ │ └── MainLayout.jsx │ ├── Root.jsx │ ├── components │ │ ├── Answer │ │ │ ├── Answer.jsx │ │ │ └── Answer.spec.jsx │ │ ├── Error │ │ │ └── Error.jsx │ │ ├── Game │ │ │ ├── Game.jsx │ │ │ ├── Game.spec.jsx │ │ │ ├── hand.js │ │ │ └── hand.spec.js │ │ ├── Homepage │ │ │ ├── Homepage.jsx │ │ │ └── Homepage.spec.jsx │ │ ├── Loading │ │ │ └── Loading.jsx │ │ ├── NotFound │ │ │ ├── NotFound.jsx │ │ │ ├── NotFound.spec.jsx │ │ │ └── notFound-copy.js │ │ └── Question │ │ │ └── Question.jsx │ ├── polyfills │ │ ├── fetch.js │ │ ├── find.js │ │ ├── location.origin.js │ │ ├── node-fetch.js │ │ └── promise.js │ ├── routes.jsx │ ├── routes.spec.js │ └── utils │ │ ├── bem.js │ │ ├── bem.md │ │ ├── bem.spec.js │ │ ├── fetch.js │ │ ├── fetch.spec.js │ │ ├── index.js │ │ ├── randomRange.js │ │ └── randomRange.spec.js ├── client-entry.jsx ├── config │ ├── environment.js │ └── paths.js ├── index.html ├── polyfills.js ├── server-entry.js └── styles │ ├── app.scss │ ├── base │ └── resets.scss │ ├── components │ ├── answer.scss │ └── question.scss │ ├── layouts │ └── mainLayout.scss │ └── utils │ ├── _breakpoints.scss │ ├── _colours.scss │ └── _mixins.scss ├── tests ├── README.md ├── config │ ├── jest │ │ ├── enzymeSetup.js │ │ ├── fileMock.js │ │ ├── reactShim.js │ │ └── styleMock.js │ ├── nightwatch │ │ ├── nightwatch-commands │ │ │ ├── loadPage.js │ │ │ ├── resizeTo.js │ │ │ ├── safeClick.js │ │ │ └── scrollElementToCenter.js │ │ ├── nightwatch-globals.js │ │ ├── nightwatch-pages │ │ │ ├── game.js │ │ │ ├── global.js │ │ │ └── home.js │ │ ├── nightwatch.conf.js │ │ └── nightwatch.json │ └── test-server │ │ └── test-server-entry.js ├── e2e │ ├── game.e2e.js │ ├── homepage.e2e.js │ └── router.e2e.js ├── fixtures │ ├── card-80.js │ └── public │ │ ├── app-fixtures.css │ │ ├── app-fixtures.js │ │ ├── polyfills-fixtures.js │ │ └── vendor-fixtures.js └── functional │ ├── client-render.func.js │ └── routes │ ├── game-route.func.js │ └── homepage-route.func.js ├── webpack.common.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": ["@babel/react", ["@babel/env", { 4 | "targets": { 5 | "browsers": [ 6 | "safari >= 9", 7 | "ie 11", 8 | "last 2 Chrome versions", 9 | "last 2 Firefox versions", 10 | "edge 13", 11 | "ios_saf 9.0-9.2", 12 | "ie_mob 11", 13 | "Android >= 4" 14 | ], 15 | "debug": false, 16 | "loose": false, 17 | "modules": false, 18 | "useBuiltIns": true 19 | } 20 | }]], 21 | "plugins": [ 22 | ["@babel/transform-runtime", { "helpers": false, "polyfill": false, "regenerator": true }], 23 | "add-module-exports", 24 | "@babel/proposal-class-properties", 25 | "@babel/proposal-object-rest-spread" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.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 | charset = utf-8 11 | indent_style = space 12 | indent_size = 2 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /tests/* 2 | src/**/*-copy.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "jest/globals": true, 6 | "node": true, 7 | "es6": true, 8 | "browser": true 9 | }, 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "jsx": true, 13 | "modules": true 14 | } 15 | }, 16 | "plugins": ["react", "jest"], 17 | "rules": { 18 | "jest/no-disabled-tests": "warn", 19 | "jest/no-focused-tests": "error", 20 | "jest/no-identical-title": "error", 21 | "jest/prefer-to-have-length": "warn", 22 | "jest/valid-expect": "error", 23 | "react/jsx-uses-react": 2, 24 | "react/jsx-uses-vars": 2, 25 | "arrow-parens": ["error", "always"], 26 | "class-methods-use-this": 0, 27 | "comma-dangle": 0, 28 | "no-unused-vars": ["error", {"varsIgnorePattern": "^log"}], 29 | "id-length": [2, {"exceptions": ["e", "i"]}] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | dist/ 4 | compiled/ 5 | tests/coverage/ 6 | tests_output/ 7 | tests_screenshots/ 8 | .DS_Store 9 | .swp 10 | .divshot-cache 11 | *.iml 12 | *.log 13 | webpack-assets.json 14 | webpack-stats.json 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | .DS_Store 4 | .babelrc 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 9.3 2 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "autoprefixer": { 4 | browsers: [ 5 | 'Safari >= 8', 6 | 'ie >= 10', 7 | 'last 2 Chrome versions', 8 | 'last 2 Firefox versions', 9 | 'Edge >= 13', 10 | 'ios_saf >= 8', 11 | 'ie_mob >= 11', 12 | 'Android >= 4' 13 | ], 14 | cascade: false, 15 | add: true, 16 | remove: true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | * [Prerequisites](#prerequisites) 4 | * [Workflow](#workflow) 5 | * [Raise an Issue](#raise-an-issue) 6 | * [Updating `Master` (Base app)](#updating-master-base-app) 7 | * [Adding a new technology](#adding-a-new-technology) 8 | * [Developing](#developing) 9 | * [Testing](#testing) 10 | * [Releasing](#releasing) 11 | * [Styleguides](#Styleguides) 12 | 13 | ## Prerequisites 14 | 15 | > Clone the project `git clone git@github.com:peter-mouland/...` 16 | 17 | PhantomJS v2 i required for tests. If you haven't already got it installed please do the following: 18 | 19 | * `brew install upx` 20 | * `npm run phantom:install` 21 | 22 | ## Workflow 23 | 24 | ### Updating `Master` (Base app) 25 | 26 | 1. Raise an issue if there is not already one. 27 | 3. Create a branch with your feature or fix and push it to GitHub. 28 | 4. Ensure you branch includes at least one new test 29 | 6. Create a pull request. 30 | 31 | ### Adding a new technology 32 | 33 | 1. Raise an issue if there is not already one. 34 | 2. Only one new tech per branch (and PR) 35 | 3. Create a new branch with your feature. 36 | * If there are prerequisites, branch of the required branch 37 | * if there is no branch that matches the prerequisite, you must create it fist 38 | * If there are no prerequisites, branch from master 39 | 4. Ensure you branch includes at least one new test 40 | 6. Create a pull request. 41 | 42 | ### Developing 43 | 44 | * `npm run start:dev` : the app will be on http://localhost:3000 45 | 46 | ### Testing 47 | 48 | * `npm test` 49 | * `npm start && npm run test:e2e-local` 50 | 51 | To run the full browserstack suite of feature tests, first start browserstack supplying the browserstack-key 52 | 53 | * `./bin/BrowserStackLocal-osx ` 54 | * `npm start && npm run test:e2e -- --bskey=` 55 | 56 | ## Styleguides 57 | 58 | * Use the `.editorconfig` (this will ensure your IDE plays nicely with things like 2 Spaces (not tabs) 59 | * Components should work without requiring 'build' setup changes 60 | * i.e. Components can be *enhanced* with webpack updates but updates must not be mandatory 61 | * Full component functionality should be documented and demo'd 62 | * the default is demo'd and the ability to change options on the fly is provided 63 | 64 | 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _This version is currently being update for 2018. The previous react-lego, with older version of tech and upgrade paths, can be found [react-lego-2017](https://github.com/peter-mouland/react-lego-2017)_ 2 | 3 | # React Lego 2018 [![CircleCI](https://circleci.com/gh/peter-mouland/react-lego.svg?style=svg)](https://circleci.com/gh/peter-mouland/react-lego) 4 | 5 | > The building blocks of a react app 6 | 7 | This repo demonstrates how to plug in other technologies, one block at a time, into React. 8 | 9 | ### Hear me out! 10 | 11 | The concept is to use GitHub's branch-comparison screens to quickly demo the code changes that are needed for *only* the technology you are interested in. 12 | 13 | A client-side React app which is fully tested and production ready on the `master` branch. 14 | From Master, *Server-side Rendering (SSR)* has been added with `Koa v2` (for `Express` see [react-lego-2016](https://github.com/peter-mouland/react-lego-2016)). 15 | Every other branch then adds one more technology, with the smallest possible changes. 16 | 17 | All branches, have been setup with [continuous deployment](https://github.com/peter-mouland/react-lego/wiki/Continuous-Deployement). 18 | 19 | [>> More about the react-lego concept](https://github.com/peter-mouland/react-lego/wiki) 20 | 21 | ### What else the Base React app have? 22 | 23 | It is production ready and fully tested : 24 | 25 | * [x] CSS ([Sass-loader](https://github.com/jtangelder/sass-loader), [Autoprefixer](https://github.com/postcss/autoprefixer)) 26 | * [x] [tests](/tests/README.md) (unit, functional, end-to-end + smoke) 27 | * [x] Code linting with [eslint](http://eslint.org/) (CSS + JS linting) 28 | * [x] CI integration with [CircleCI](https://circleci.com/) 29 | * [x] Continuous deployment with [Heroku](http://www.heroku.com/) 30 | 31 | *All* other branches include the above and build on this base. 32 | 33 | ## Technology to Add: 34 | 35 | _All branches use [babel v7](https://github.com/babel/babel), [React v16.2](https://github.com/facebook/react), [react-router v4](https://github.com/reactjs/react-router), [Webpack v4](https://github.com/webpack/webpack) unless otherwise stated_ 36 | 37 | The `Code changes` column is where you go if you want to see how the code changed from the previous branch. 38 | This is a great place to see how to do it yourself. 39 | 40 | | Category | Code changes | App size (node_modules) | Comparator | kb | | 41 | | --- | --- | --- | --- | --- | --- | 42 | | Client-side Rendering | [React](https://github.com/peter-mouland/react-lego/tree/master/) | 23kb (+152kb) | | | 43 | | Server-side Rendering | [add Koa v2](https://github.com/peter-mouland/react-lego/compare/master...ssr) | 22kb (+153kb) | 44 | | CSS | [add CSS Imports](https://github.com/peter-mouland/react-lego/compare/ssr...ssr-css)| 22kb (+153kb ) | CSS Modules | | [>> More about adding CSS](https://github.com/peter-mouland/react-lego/wiki/CSS) | 45 | | State Management | [add Redux](https://github.com/peter-mouland/react-lego/compare/ssr-css...ssr-css-redux)| 22kb (+188kb) | | | [>> More about adding Redux](https://github.com/peter-mouland/react-lego/wiki/Redux) | 46 | | | | | | | | 47 | 48 | 49 | ### _Previous branches still to be updated_ 50 | > These branches are from React Lego 2017 and are on my 'todo' list to update! 51 | 52 | | Category | New Tech | Code changes | Client-side App (kb) | Comparator | kb | | 53 | | --- | --- | --- | --- | --- | --- | --- | 54 | | Client-side Rendering | | | | Preact > [Preact code vs React](https://github.com/peter-mouland/react-lego/compare/master...preact) | tbc | [>> More about adding Preact](https://github.com/peter-mouland/react-lego/wiki/Preact) 55 | | State Management | [Async routes](https://github.com/peter-mouland/react-lego/tree/ssr-css-redux-async) | [add async routes](https://github.com/peter-mouland/react-lego/compare/ssr-css-redux...ssr-css-redux-async) | tbc | | | [>> More about adding Promise middleware](https://github.com/peter-mouland/react-lego/wiki/Redux-Promise-Middleware) 56 | | Assets | Importing SVGs | 57 | | Assets | Responsive Images with PNGs | 58 | | Data API | [GraphQL](https://github.com/peter-mouland/react-lego/tree/ssr-css-redux-async-graphql) | [add GraphQL](https://github.com/peter-mouland/react-lego/compare/ssr-css-redux-async...ssr-css-redux-async-graphql) | tbc | Apollo | tbc | 59 | 60 | 61 | ## What else ? 62 | 63 | I have a few articles that may be interesting to read covering all the branches in this repo: [>> wiki](https://github.com/peter-mouland/react-lego/wiki) 64 | 65 | __________ 66 | ** Something missing? Please see [react-lego-2017](https://github.com/peter-mouland/react-lego-2017) or [react-lego-2016](https://github.com/peter-mouland/react-lego-2016) or submit a feature request!** 67 | __________ 68 | 69 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lego", 3 | "scripts": { 4 | }, 5 | "env": { 6 | "NODE_ENV": { 7 | "required": true 8 | } 9 | }, 10 | "addons": [ 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | timezone: 3 | Europe/London 4 | node: 5 | version: v8.11.2 6 | java: 7 | version: openjdk8 8 | environment: 9 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 10 | general: 11 | artifacts: 12 | - tests/e2e/tests_output 13 | - tests/e2e/tests_screenshots 14 | dependencies: 15 | override: 16 | - yarn 17 | cache_directories: 18 | - browserstack 19 | - node_modules 20 | - ~/.cache/yarn 21 | test: 22 | override: 23 | - yarn test:unit 24 | - yarn test:func 25 | # - yarn build 26 | # - yarn test:e2e 27 | deployment: 28 | main: 29 | branch: master 30 | commands: 31 | - wget "https://www.browserstack.com/browserstack-local/BrowserStackLocal-linux-x64.zip" 32 | - unzip BrowserStackLocal-linux-x64.zip 33 | - ./BrowserStackLocal $BROWSERSTACK_KEY -force: 34 | background: true 35 | - "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow" 36 | - git push git@heroku.com:react-lego-preprod.git $CIRCLE_SHA1:refs/heads/master -f --no-verify 37 | # - yarn test:e2e-staging -- --sha=$CIRCLE_BUILD_NUM --target=http://react-lego-preprod.herokuapp.com --retries 2 38 | - git push git@heroku.com:react-lego.git $CIRCLE_SHA1:refs/heads/master -f --no-verify 39 | # - yarn test:e2e-production -- --sha=$CIRCLE_BUILD_NUM --target=http://react-lego.herokuapp.com --retries 2 40 | -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverageThreshold": { 3 | "global": { 4 | "statements": 50, 5 | "branches": 50, 6 | "functions": 20, 7 | "lines": 50 8 | } 9 | }, 10 | "rootDir": ".", 11 | "collectCoverage": true, 12 | "coverageDirectory": "/tests/coverage", 13 | "coveragePathIgnorePatterns": ["/node_modules", "/tests"], 14 | "transform": { 15 | "^.+\\.jsx?$": "babel-jest" 16 | }, 17 | "globals": { 18 | "NODE_ENV": "test" 19 | }, 20 | "moduleNameMapper": { 21 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/tests/config/jest/fileMock.js", 22 | "\\.(css|scss)$": "/tests/config/jest/styleMock.js" 23 | }, 24 | "testRegex": "(/__tests__/.*(^scenarios)|(\\.|/)(func|spec))\\.jsx?$", 25 | "setupFiles": [ 26 | "/tests/config/jest/reactShim.js", 27 | "/tests/config/jest/enzymeSetup.js" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lego", 3 | "version": "3.0.0", 4 | "description": "", 5 | "license": "MIT", 6 | "main": "src/server-entry.js", 7 | "engines": { 8 | "node": "^8.11.2", 9 | "npm": "^5.6.0", 10 | "yarn": "^1.1.0" 11 | }, 12 | "author": "Peter Mouland", 13 | "scripts": { 14 | "------ GIT HOOKS ------": "", 15 | "postinstall": "yarn build", 16 | "------ BUILD STEPS ------": "", 17 | "build": "rm -rf compiled && yarn compile && cp src/index.html compiled", 18 | "compile": "webpack --config webpack.config.prod.js", 19 | "------ SERVER STEPS ------": "", 20 | "start": "http-server compiled", 21 | "start:dev": "ENV=dev node src/server-entry.js", 22 | "------ TESTING ------": "", 23 | "test:setup": "selenium-standalone install yarn build", 24 | "lint": "sass-lint -v -q -c ./sass-lint.yml && eslint 'src/**/*.js' 'src/**/*.jsx'", 25 | "test": "yarn test:unit && yarn test:func", 26 | "test:unit": "jest --config jest.json .*.spec.jsx?", 27 | "test:func": "jest --config jest.json .*.func.jsx? --collectCoverage false", 28 | "test:e2e": "yarn test:e2e-local", 29 | "test:e2e-local": "yarn nightwatch -- --env local --tag staging", 30 | "test:e2e-production": "yarn nightwatch -- --env chrome_win --tag production", 31 | "test:e2e-staging": "yarn nightwatch -- --env safari_osx,chrome_osx,firefox_win,firefox_osx,IE11,edge --tag staging", 32 | "nightwatch": "nightwatch -o ./tests/e2e/tests_output -c ./tests/config/nightwatch/nightwatch.conf.js", 33 | "------ UTILS ------": "", 34 | "nuke": "rm -rf node_modules && yarn && yarn test" 35 | }, 36 | "husky": { 37 | "hooks": { 38 | "pre-commit": "yarn lint && yarn test", 39 | "pre-push": "yarn build && yarn test:setup && yarn test:e2e" 40 | } 41 | }, 42 | "dependencies": { 43 | "@babel/core": "^7.0.0-beta.49", 44 | "@babel/plugin-transform-runtime": "^7.0.0-beta.49", 45 | "@babel/preset-react": "^7.0.0-beta.49", 46 | "@babel/runtime": "^7.0.0-beta.49", 47 | "assets-webpack-plugin": "^3.5.1", 48 | "babel-core": "^7.0.0-0", 49 | "babel-loader": "^8.0.0-beta.2", 50 | "css-loader": "^0.28.10", 51 | "cssnano": "^4.0.0-rc.2", 52 | "debug": "^3.1.0", 53 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 54 | "http-server": "^0.11.1", 55 | "node-fetch": "^2.1.2", 56 | "node-sass": "^4.9.0", 57 | "postcss-loader": "^2.1.1", 58 | "progress-bar-webpack-plugin": "^1.11.0", 59 | "promise-polyfill": "^8.0.0", 60 | "prop-types": "^15.6.1", 61 | "react": "^16.4.0", 62 | "react-document-meta": "^2.1.2", 63 | "react-dom": "^16.4.0", 64 | "react-router-dom": "^4.2.2", 65 | "sass-loader": "^7.0.3", 66 | "style-loader": "^0.21.0", 67 | "webpack": "^4.11.0", 68 | "webpack-cli": "^3.0.2", 69 | "webpack-visualizer-plugin": "^0.1.11", 70 | "whatwg-fetch": "^2.0.3" 71 | }, 72 | "devDependencies": { 73 | "@babel/cli": "^7.0.0-beta.49", 74 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.49", 75 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.49", 76 | "@babel/preset-env": "^7.0.0-beta.49", 77 | "@babel/register": "^7.0.0-beta.49", 78 | "babel-eslint": "^8.2.3", 79 | "babel-jest": "^23.0.1", 80 | "babel-plugin-add-module-exports": "^0.2.1", 81 | "chance": "^1.0.16", 82 | "enzyme": "^3.3.0", 83 | "enzyme-adapter-react-16": "^1.1.1", 84 | "eslint": "^4.18.1", 85 | "eslint-config-airbnb": "^16.1.0", 86 | "eslint-plugin-babel": "^5.1.0", 87 | "eslint-plugin-import": "^2.9.0", 88 | "eslint-plugin-jest": "^21.12.2", 89 | "eslint-plugin-jsx-a11y": "^6.0.3", 90 | "eslint-plugin-react": "^7.7.0", 91 | "express": "^4.16.2", 92 | "http-proxy": "^1.16.2", 93 | "husky": "^0.15.0-rc.8", 94 | "jest": "^23.1.0", 95 | "jsdom": "^11.6.2", 96 | "nightwatch": "^0.9.19", 97 | "nightwatch-html-reporter": "^2.0.5", 98 | "sass-lint": "^1.12.1", 99 | "selenium-standalone": "^6.12.0", 100 | "webpack-serve": "^1.0.2", 101 | "yargs": "^11.0.0" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /sass-lint.yml: -------------------------------------------------------------------------------- 1 | 2 | # Linter Options 3 | options: 4 | merge-default-rules: true 5 | formatter: stylish 6 | # File Options 7 | files: 8 | include: 'src/app/**/*.scss' 9 | # Rule Configuration 10 | rules: 11 | extends-before-mixins: 2 12 | extends-before-declarations: 2 13 | placeholder-in-extend: 2 14 | no-warn: 1 15 | no-debug: 1 16 | no-ids: 2 17 | no-important: 2 18 | hex-notation: 19 | - 2 20 | - 21 | style: uppercase 22 | indentation: 23 | - 2 24 | - 25 | size: 2 26 | property-sort-order: 27 | - 0 28 | - 29 | ignore-custom-properties: true 30 | variable-for-property: 31 | - 0 32 | bem-depth: 2 33 | class-name-format: 34 | - 1 35 | - 36 | convention: hyphenatedbem 37 | force-attribute-nesting: 0 38 | no-qualifying-elements: 0 39 | force-pseudo-nesting: 0 40 | force-element-nesting: 0 41 | -------------------------------------------------------------------------------- /src/app/Layouts/MainLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import debug from 'debug'; 4 | 5 | import bemHelper from '../utils/bem'; 6 | import { NamedLink } from '../routes'; 7 | 8 | const log = debug('base:mainLayout'); 9 | const cn = bemHelper({ block: 'layout' }); 10 | 11 | const MainLayout = ({ children }) => ( 12 |
13 | 18 |
19 | {children} 20 |
21 | 24 |
25 | ); 26 | 27 | MainLayout.propTypes = { 28 | children: PropTypes.element.isRequired 29 | }; 30 | 31 | export default MainLayout; 32 | -------------------------------------------------------------------------------- /src/app/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BrowserRouter from 'react-router-dom/BrowserRouter'; 3 | import StaticRouter from 'react-router-dom/StaticRouter'; 4 | import debug from 'debug'; 5 | 6 | import { makeRoutes } from './routes'; 7 | import { isBrowser } from './utils'; 8 | 9 | debug('lego:Root'); 10 | 11 | // exported to be used in tests 12 | export const Router = isBrowser ? BrowserRouter : StaticRouter; 13 | 14 | export default (props) => ( 15 | 16 | {makeRoutes()} 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /src/app/components/Answer/Answer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import config from '../../../config/environment'; 4 | 5 | const cardShape = { 6 | birth_year: PropTypes.string, 7 | created: PropTypes.string, 8 | edited: PropTypes.string, 9 | eye_color: PropTypes.string, 10 | films: PropTypes.arrayOf(PropTypes.string), 11 | gender: PropTypes.string, 12 | hair_color: PropTypes.string, 13 | height: PropTypes.string, 14 | homeworld: PropTypes.string, 15 | mass: PropTypes.string, 16 | name: PropTypes.string, 17 | skin_color: PropTypes.string, 18 | species: PropTypes.arrayOf(PropTypes.string), 19 | starships: PropTypes.arrayOf(PropTypes.string), 20 | url: PropTypes.string, 21 | vehicles: PropTypes.arrayOf(PropTypes.string), 22 | }; 23 | 24 | export const CardItemValue = ({ value }) => { 25 | const values = [].concat(value); 26 | const props = { className: 'card-item-value' }; 27 | return ( 28 |
{ 29 | values 30 | .map((val, i) => { 31 | props.key = `${i}-${val}`; 32 | const text = val.replace(config.api, ''); 33 | return val.indexOf(config.api) === 0 34 | ? { text } 35 | : { text }; 36 | }) 37 | } 38 |
39 | ); 40 | }; 41 | 42 | CardItemValue.propTypes = { 43 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired 44 | }; 45 | 46 | export const AnswerOption = ({ isAnswer, card }) => ( 47 |
48 | {Object.keys(card).map((info) => ( 49 | 50 |
{info}
51 |
52 |
53 | ))} 54 |
55 | ); 56 | 57 | AnswerOption.propTypes = { 58 | isAnswer: PropTypes.bool.isRequired, 59 | card: PropTypes.shape(cardShape).isRequired 60 | }; 61 | 62 | const Answer = ({ 63 | cards, answerId, showAnswer, ...props 64 | }) => ( 65 |
66 | 67 | 68 |
69 | ); 70 | 71 | Answer.propTypes = { 72 | cards: PropTypes.arrayOf(PropTypes.shape(cardShape)), 73 | answerId: PropTypes.string.isRequired, 74 | showAnswer: PropTypes.bool 75 | }; 76 | 77 | Answer.defaultProps = { 78 | cards: [], 79 | showAnswer: false 80 | }; 81 | 82 | export default Answer; 83 | -------------------------------------------------------------------------------- /src/app/components/Answer/Answer.spec.jsx: -------------------------------------------------------------------------------- 1 | import Chance from 'chance'; 2 | import React from 'react'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import Answer, { AnswerOption } from './Answer'; 6 | 7 | const chance = new Chance(); 8 | const fakeCard = () => ({ 9 | url: chance.url() 10 | }); 11 | const baseProps = { 12 | answerId: chance.word() 13 | }; 14 | 15 | describe('Answer Component', () => { 16 | it('is visible if showAnswer is passed', () => { 17 | const card1 = fakeCard(); 18 | const card2 = fakeCard(); 19 | const props = { ...baseProps, cards: [card1, card2], showAnswer: true }; 20 | const wrapper = shallow(); 21 | expect(wrapper.get(0).props.className).toContain('visible'); 22 | }); 23 | 24 | it('is hidden if showAnswer isn\'t passed', () => { 25 | const card1 = fakeCard(); 26 | const card2 = fakeCard(); 27 | const props = { ...baseProps, cards: [card1, card2], showAnswer: false }; 28 | const wrapper = shallow(); 29 | expect(wrapper.get(0).props.className).toContain('hidden'); 30 | }); 31 | 32 | it('2 answer options, passing in a card to each', () => { 33 | const card1 = fakeCard(); 34 | const card2 = fakeCard(); 35 | const props = { ...baseProps, cards: [card1, card2], showAnswer: true }; 36 | const wrapper = shallow(); 37 | expect(wrapper.find(AnswerOption).get(0).props).toEqual({ card: card1, isAnswer: false }); 38 | expect(wrapper.find(AnswerOption).get(1).props).toEqual({ card: card2, isAnswer: false }); 39 | }); 40 | 41 | it('sets isAnswer for an answerOptions with matching id/url', () => { 42 | const card1 = fakeCard(); 43 | const card2 = fakeCard(); 44 | const props = { 45 | ...baseProps, cards: [card1, card2], answerId: card2.url, showAnswer: false 46 | }; 47 | const wrapper = shallow(); 48 | expect(wrapper.find(AnswerOption).get(0).props.isAnswer).toBe(false); 49 | expect(wrapper.find(AnswerOption).get(1).props.isAnswer).toBe(true); 50 | }); 51 | }); 52 | 53 | 54 | describe('AnswerOption Component', () => { 55 | it('hightlights the options using class answer-option--answer', () => { 56 | }); 57 | 58 | it('doesnt hightlight the option', () => { 59 | }); 60 | 61 | it('displays a key for each item', () => { 62 | }); 63 | 64 | it('displays a value for each item', () => { 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/app/components/Error/Error.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Error = ({ error }) => { 5 | if (typeof error !== 'string') { 6 | return ( 7 |

8 | {String(error)} needs to be handled, was not a string 9 |

10 | ); 11 | } 12 | return

Error Loading cards!{error}

; 13 | }; 14 | 15 | Error.propTypes = { 16 | error: PropTypes.string.isRequired 17 | }; 18 | 19 | export default Error; 20 | -------------------------------------------------------------------------------- /src/app/components/Game/Game.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import debug from 'debug'; 3 | 4 | import { randomRange, getJSON } from '../../utils/index'; 5 | import Error from '../Error/Error'; 6 | import Loading from '../Loading/Loading'; 7 | import Hand from './hand'; 8 | import Question from '../Question/Question'; 9 | import Answer from '../Answer/Answer'; 10 | import config from '../../../config/environment'; 11 | 12 | debug('lego:Game'); 13 | 14 | const DECK = 87; 15 | 16 | const getCard = (api, cardId) => getJSON(`${config.api.host}${api}/${cardId}/`); 17 | 18 | export default class Game extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | error: null, 23 | loading: false, 24 | showAnswer: false, 25 | attempt: null, 26 | }; 27 | this.deal = this.deal.bind(this); 28 | this.viewAnswer = this.viewAnswer.bind(this); 29 | this.setAttempt = this.setAttempt.bind(this); 30 | } 31 | 32 | componentDidMount() { 33 | this.deal(); 34 | } 35 | 36 | setAttempt = (attempt) => { 37 | this.setState({ attempt }); 38 | } 39 | 40 | viewAnswer() { 41 | this.setState({ showAnswer: true }); 42 | } 43 | 44 | deal = () => { 45 | const cardsIds = randomRange(1, DECK, 2); 46 | const gameType = 'people'; 47 | const promises = [getCard(gameType, cardsIds[0]), getCard(gameType, cardsIds[1])]; 48 | this.setState({ 49 | error: null, 50 | loading: true 51 | }); 52 | return Promise.all(promises) 53 | .then((cards) => { 54 | const hand = new Hand(cards); 55 | this.setState({ 56 | hand: { 57 | cards, 58 | question: hand.question, 59 | answerId: hand.answerId, 60 | answer: hand.answer 61 | }, 62 | loading: false 63 | }); 64 | }) 65 | .catch((e) => { 66 | this.setState({ 67 | error: e.toString(), 68 | loading: false 69 | }); 70 | }); 71 | } 72 | 73 | render() { 74 | const { 75 | error, loading, showAnswer, attempt, hand: { 76 | cards = [], question, answer, answerId 77 | } = {} 78 | } = this.state; 79 | 80 | return ( 81 |
82 |
83 |

Star Wars Trivia

84 |

A simple game using {config.api.label}.

85 |
86 | 87 | {error && } 88 | {loading ? : null } 89 | {!loading && question ? 90 | 94 | {question} 95 | 96 | : null 97 | } 98 | {!!cards.length && ( 99 | 102 | ) } 103 | {showAnswer && ( 104 | 105 | ) } 106 |
107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/app/components/Game/Game.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Game from './Game'; 5 | 6 | const mockFixtures = require('../../../../tests/fixtures/card-80.js'); 7 | 8 | const baseProps = {}; 9 | const mockText = jest.fn(); 10 | 11 | describe('Game Container', () => { 12 | beforeEach(() => { 13 | global.fetch = jest.fn().mockImplementation(() => 14 | Promise.resolve({ 15 | status: 200, 16 | text: mockText.mockReturnValue(() => mockFixtures()) 17 | })); 18 | }); 19 | 20 | it('should have an id of game', () => { 21 | const wrapper = shallow(, { disableLifecycleMethods: true }); 22 | expect(wrapper.at(0).props().id).toBe('game'); 23 | }); 24 | // unit testing goes here 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/Game/hand.js: -------------------------------------------------------------------------------- 1 | import { randomRange } from '../../utils'; 2 | 3 | export default class Hand { 4 | constructor(cards = []) { 5 | if (cards.length < 2) { 6 | throw new Error('You needs more than 2 cards to play a game'); 7 | } 8 | const answerIndex = randomRange(0, 1, 1)[0]; 9 | const factIndex = randomRange(0, 7, 1)[0]; 10 | this.cards = cards; 11 | this.wrongCard = this.cards[1 - answerIndex]; 12 | this.answerCard = this.cards[answerIndex]; 13 | this.answerKey = Object.keys(this.answerCard)[factIndex]; 14 | } 15 | 16 | get question() { 17 | const fact = this.answerCard[this.answerKey]; 18 | const extra = fact > this.wrongCard[this.answerKey] ? 'taller' : 'smaller'; 19 | const answerText = this.answerKey === 'height' 20 | ? `${extra}, ${this.cards[0].name} or ${this.cards[1].name}` 21 | : fact; 22 | return `Who's ${this.answerKey} is ${answerText}?`; 23 | } 24 | 25 | get answerId() { 26 | return this.answerCard.url; 27 | } 28 | 29 | get answer() { 30 | const wrongAnswer = this.wrongCard[this.answerKey]; 31 | const answer = this.answerCard[this.answerKey]; 32 | switch (true) { 33 | case (wrongAnswer === 'unknown' && wrongAnswer !== answer): 34 | return 'unknown'; 35 | case (wrongAnswer === answer): 36 | return 'both'; 37 | default: 38 | return this.answerCard.name; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/Game/hand.spec.js: -------------------------------------------------------------------------------- 1 | import * as utils from '../../utils/randomRange'; 2 | import Hand from './hand'; 3 | 4 | let stub; 5 | const fakeCard1 = { 6 | name: 'card1', 7 | key1: 'unknown', 8 | key2: 2, 9 | key3: 3, 10 | key4: 4, 11 | key5: 5, 12 | height: 6, 13 | key7: 'unknown', 14 | url: 'url/card1', 15 | }; 16 | const fakeCard2 = { 17 | name: 'card2', 18 | key1: 11, 19 | key2: 21, 20 | key3: 31, 21 | key4: 4, 22 | key5: 5, 23 | height: 61, 24 | key7: 'unknown', 25 | url: 'url/card2', 26 | }; 27 | const cards = [fakeCard1, fakeCard2]; 28 | 29 | describe('Hand', () => { 30 | beforeEach(() => { 31 | stub = jest.spyOn(utils, 'randomRange'); 32 | }); 33 | 34 | afterEach(() => { 35 | stub.mockReset(); 36 | stub.mockRestore(); 37 | }); 38 | 39 | it('throws an error when no cards are passed', () => { 40 | const instantiateHand = () => new Hand(); 41 | expect(instantiateHand).toThrow(Error); 42 | }); 43 | 44 | it('should return a question which matches the answer and fact', () => { 45 | stub 46 | .mockReturnValueOnce([0]) 47 | .mockReturnValueOnce([2]); 48 | expect(new Hand(cards).question).toBe('Who\'s key2 is 2?'); 49 | 50 | stub 51 | .mockReturnValueOnce([1]) 52 | .mockReturnValueOnce([2]); 53 | expect(new Hand(cards).question).toBe('Who\'s key2 is 21?'); 54 | 55 | stub 56 | .mockReturnValueOnce([1]) 57 | .mockReturnValueOnce([3]); 58 | expect(new Hand(cards).question).toBe('Who\'s key3 is 31?'); 59 | }); 60 | 61 | it('should return taller/smaller when the answer key is height', () => { 62 | stub.mockReturnValueOnce([0]); 63 | stub.mockReturnValueOnce([6]); 64 | expect(new Hand(cards).question).toBe('Who\'s height is smaller, card1 or card2?'); 65 | 66 | stub.mockReturnValueOnce([1]); 67 | stub.mockReturnValueOnce([6]); 68 | expect(new Hand(cards).question).toBe('Who\'s height is taller, card1 or card2?'); 69 | }); 70 | 71 | it('should return answer `both` when both cards matches the answer and fact', () => { 72 | stub.mockReturnValueOnce([0]); 73 | stub.mockReturnValueOnce([4]); 74 | expect(new Hand(cards).answer).toBe('both'); 75 | 76 | stub.mockReturnValueOnce([1]); 77 | stub.mockReturnValueOnce([4]); 78 | expect(new Hand(cards).answer).toBe('both'); 79 | 80 | stub.mockReturnValueOnce([1]); 81 | stub.mockReturnValueOnce([5]); 82 | expect(new Hand(cards).answer).toBe('both'); 83 | }); 84 | 85 | it('should return answer key `name`', () => { 86 | stub.mockReturnValueOnce([0]); 87 | stub.mockReturnValueOnce([1]); 88 | expect(new Hand(cards).answer).toBe('card1'); 89 | 90 | stub.mockReturnValueOnce([1]); 91 | stub.mockReturnValueOnce([2]); 92 | expect(new Hand(cards).answer).toBe('card2'); 93 | }); 94 | 95 | it('should return answer unknown when wrong answer value is unknown', () => { 96 | stub.mockReturnValueOnce([0]); 97 | stub.mockReturnValueOnce([1]); 98 | expect(new Hand(cards).answer).toBe('card1'); 99 | 100 | stub.mockReturnValueOnce([1]); 101 | stub.mockReturnValueOnce([1]); 102 | expect(new Hand(cards).answer).toBe('unknown'); 103 | 104 | stub.mockReturnValueOnce([1]); 105 | stub.mockReturnValueOnce([7]); 106 | expect(new Hand(cards).answer).toBe('both'); 107 | }); 108 | 109 | it('should return the correct answer card', () => { 110 | stub.mockReturnValueOnce([0]); 111 | stub.mockReturnValueOnce([1]); 112 | expect(new Hand(cards).answerId).toBe(fakeCard1.url); 113 | 114 | stub.mockReturnValueOnce([1]); 115 | stub.mockReturnValueOnce([1]); 116 | expect(new Hand(cards).answerId).toBe(fakeCard2.url); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /src/app/components/Homepage/Homepage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import debug from 'debug'; 3 | 4 | debug('lego:Homepage.jsx'); 5 | 6 | export default () => ( 7 |
8 |
9 |

About React Lego

10 |

Iteratively add more technologies to React Applications.

11 |
12 |
13 |

The ‘Base’ App

14 |

This demo is the ‘base’ app, which includes :

15 |
    16 |
  • Rendering Universal Javascript (rendered on the server + client)
  • 17 |
  • Importing stylesheets
  • 18 |
  • 19 | Fully tested app : 20 |
      21 |
    • Unit tests
    • 22 |
    • Functional tests
    • 23 |
    • End-to-end tests
    • 24 |
    25 |
  • 26 |
27 |

It could be simpler...

28 |

This app isn't aimed to be the simplest ‘base’ React app, 29 | it's aimed at adding new technologies simple. 30 |

31 |

But, this means that when it comes to adding Redux for example, 32 | much less changes are required. 33 |

34 |
35 |
36 | ); 37 | -------------------------------------------------------------------------------- /src/app/components/Homepage/Homepage.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Homepage from './Homepage'; 5 | 6 | const baseProps = {}; 7 | 8 | describe('Settings Container', () => { 9 | it('should have an id of homepage', () => { 10 | const wrapper = shallow(); 11 | expect(wrapper.at(0).props().id).toBe('homepage'); 12 | }); 13 | // unit testing goes here 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/components/Loading/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = () =>

Loading hand....

; 4 | 5 | export default Loading; 6 | -------------------------------------------------------------------------------- /src/app/components/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { copy } from './notFound-copy'; 3 | 4 | export default () => ( 5 |
6 |

{copy.title}

7 |

{copy.blurb}

8 | 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /src/app/components/NotFound/NotFound.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import NotFound from './NotFound'; 5 | import { copy } from './notFound-copy'; 6 | 7 | describe('NotFound', () => { 8 | it('should render with an id', () => { 9 | const wrapper = shallow(); 10 | expect(wrapper.find('#not-found')).toHaveLength(1); 11 | }); 12 | 13 | it('should have a title and blurb', () => { 14 | const wrapper = shallow(); 15 | const title = wrapper.find('h2'); 16 | expect(title.text()).toBe(copy.title); 17 | expect(wrapper.text()).toContain(copy.blurb); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/components/NotFound/notFound-copy.js: -------------------------------------------------------------------------------- 1 | export const copy = { 2 | title: `How did you end up here?`, 3 | blurb: `The page you're looking for doesn't exist.`, 4 | try: { 5 | blurb: `A couple of things you can try:`, 6 | options: [ 7 | `Did you type in a web address to get here? Check you typed it in correctly.`, 8 | `Try to find the page you were looking for by clicking My Account.` 9 | ] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/components/Question/Question.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions, max-len */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const QuestionOption = ({ 6 | onClick, attempt, children, answer, showAnswer 7 | }) => { 8 | const classNames = ['question__option']; 9 | if (attempt === children) { 10 | classNames.push('question__option--selected'); 11 | } 12 | if (showAnswer) { 13 | classNames.push(answer === children ? 'question__option--correct' : 'question__option--wrong'); 14 | } 15 | return
  • {children}
  • ; 16 | }; 17 | 18 | QuestionOption.propTypes = { 19 | onClick: PropTypes.func.isRequired, 20 | children: PropTypes.string.isRequired, 21 | attempt: PropTypes.node, 22 | answer: PropTypes.node, 23 | showAnswer: PropTypes.bool, 24 | }; 25 | 26 | QuestionOption.defaultProps = { 27 | attempt: null, 28 | answer: null, 29 | showAnswer: false, 30 | }; 31 | 32 | const Question = ({ 33 | children, showAnswer, answer, cards, attempt, onClick, ...props 34 | }) => { 35 | if (!cards.length) return null; 36 | 37 | const options = [cards[0].name, cards[1].name, 'both', 'unknown']; 38 | const optionProps = { answer, attempt, showAnswer }; 39 | 40 | return ( 41 |
    42 |

    {children}

    43 |
      44 | {options.map((option) => ( 45 | onClick(option)} key={option}> 46 | {option} 47 | 48 | ))} 49 |
    50 |
    51 | ); 52 | }; 53 | 54 | Question.propTypes = { 55 | onClick: PropTypes.func.isRequired, 56 | children: PropTypes.string.isRequired, 57 | attempt: PropTypes.node, 58 | answer: PropTypes.node, 59 | showAnswer: PropTypes.bool, 60 | cards: PropTypes.arrayOf(PropTypes.shape({ 61 | name: PropTypes.string 62 | })), 63 | }; 64 | 65 | Question.defaultProps = { 66 | attempt: null, 67 | answer: null, 68 | showAnswer: false, 69 | cards: [], 70 | }; 71 | 72 | export default Question; 73 | 74 | -------------------------------------------------------------------------------- /src/app/polyfills/fetch.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // for ie 3 | require('whatwg-fetch') 4 | -------------------------------------------------------------------------------- /src/app/polyfills/find.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // for ie 3 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find 4 | // https://tc39.github.io/ecma262/#sec-array.prototype.find 5 | if (!Array.prototype.find) { 6 | Object.defineProperty(Array.prototype, 'find', { 7 | value: function(predicate) { 8 | // 1. Let O be ? ToObject(this value). 9 | if (this == null) { 10 | throw new TypeError('"this" is null or not defined'); 11 | } 12 | 13 | var o = Object(this); 14 | 15 | // 2. Let len be ? ToLength(? Get(O, "length")). 16 | var len = o.length >>> 0; 17 | 18 | // 3. If IsCallable(predicate) is false, throw a TypeError exception. 19 | if (typeof predicate !== 'function') { 20 | throw new TypeError('predicate must be a function'); 21 | } 22 | 23 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 24 | var thisArg = arguments[1]; 25 | 26 | // 5. Let k be 0. 27 | var k = 0; 28 | 29 | // 6. Repeat, while k < len 30 | while (k < len) { 31 | // a. Let Pk be ! ToString(k). 32 | // b. Let kValue be ? Get(O, Pk). 33 | // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). 34 | // d. If testResult is true, return kValue. 35 | var kValue = o[k]; 36 | if (predicate.call(thisArg, kValue, k, o)) { 37 | return kValue; 38 | } 39 | // e. Increase k by 1. 40 | k++; 41 | } 42 | 43 | // 7. Return undefined. 44 | return undefined; 45 | } 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/app/polyfills/location.origin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // for ie 3 | if (!window.location.origin) { 4 | var local = window.location; 5 | window.location.origin = local.protocol + '//' + local.hostname + ( 6 | local.port ? (':' + local.port) : '' 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/app/polyfills/node-fetch.js: -------------------------------------------------------------------------------- 1 | const realFetch = require('node-fetch'); 2 | 3 | module.exports = (url, options) => { 4 | const secureUrl = (/^\/\//.test(url)) 5 | ? `https:${url}` 6 | : url; 7 | return realFetch.call(this, secureUrl, options); 8 | }; 9 | 10 | if (!global.fetch) { 11 | global.fetch = module.exports; 12 | global.Response = realFetch.Response; 13 | global.Headers = realFetch.Headers; 14 | global.Request = realFetch.Request; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/polyfills/promise.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // for ie 3 | var Promise = require('promise-polyfill'); 4 | 5 | if (!window.Promise) { 6 | window.Promise = Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Route from 'react-router-dom/Route'; 4 | import Link from 'react-router-dom/Link'; 5 | import Switch from 'react-router-dom/Switch'; 6 | import DocumentMeta from 'react-document-meta'; 7 | import debug from 'debug'; 8 | 9 | import bemHelper from './utils/bem'; 10 | import MainLayout from './Layouts/MainLayout'; 11 | import Homepage from './components/Homepage/Homepage'; 12 | import Game from './components/Game/Game'; 13 | import NotFound from './components/NotFound/NotFound'; 14 | 15 | debug('lego:routes'); 16 | const cn = bemHelper({ block: 'link' }); 17 | 18 | const baseMetaData = { 19 | title: 'React Lego', 20 | description: 'React Lego : incrementally add more cool stuff to your react app', 21 | meta: { 22 | charSet: 'utf-8', 23 | name: { 24 | keywords: 'react,example' 25 | } 26 | } 27 | }; 28 | 29 | export const getRoutesConfig = () => [ 30 | { 31 | name: 'homepage', 32 | exact: true, 33 | path: '/', 34 | meta: { 35 | ...baseMetaData, 36 | title: 'About React SSR Base' 37 | }, 38 | label: 'About SSR Base', 39 | component: Homepage 40 | }, 41 | { 42 | name: 'game', 43 | path: '/game/', 44 | label: 'Star Wars Trivia', 45 | meta: { 46 | ...baseMetaData, 47 | title: 'Star Wars Trivia', 48 | }, 49 | component: Game 50 | } 51 | ]; 52 | 53 | export const findRoute = (to) => getRoutesConfig().find((rt) => rt.name === to); 54 | 55 | // test this active link and route matching 56 | export const NamedLink = ({ 57 | className, to, children, ...props 58 | }) => { 59 | const route = findRoute(to); 60 | if (!route) throw new Error(`Route to '${to}' not found`); 61 | return ( 62 | ( // eslint-disable-line react/no-children-prop 66 | 67 | { children || route.label } 68 | 69 | )} 70 | /> 71 | ); 72 | }; 73 | 74 | NamedLink.propTypes = { 75 | to: PropTypes.string.isRequired, 76 | className: PropTypes.string, 77 | children: PropTypes.element, 78 | }; 79 | 80 | NamedLink.defaultProps = { 81 | className: '', 82 | children: null, 83 | }; 84 | 85 | const RouteWithMeta = ({ component: Component, meta, ...props }) => ( 86 | ( 89 | 90 | 91 | 92 | 93 | )} 94 | /> 95 | ); 96 | 97 | RouteWithMeta.propTypes = { 98 | component: PropTypes.func.isRequired, 99 | meta: PropTypes.shape({ 100 | title: PropTypes.string, 101 | description: PropTypes.string, 102 | meta: PropTypes.shape({ 103 | charSet: PropTypes.string, 104 | name: PropTypes.shape({ 105 | keywords: PropTypes.string, 106 | }) 107 | }) 108 | }).isRequired, 109 | }; 110 | 111 | export function makeRoutes() { 112 | return ( 113 | 114 | 115 | {getRoutesConfig().map((route) => )} 116 | 117 | 118 | 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/app/routes.spec.js: -------------------------------------------------------------------------------- 1 | import { getRoutesConfig } from './routes'; 2 | 3 | describe('routes', () => { 4 | const routes = getRoutesConfig(); 5 | it('should always start with /', () => { 6 | Object.keys(routes).forEach((route) => { 7 | expect(routes[route].path.substr(0, 1)).toBe('/', 'route does not start with /'); 8 | }); 9 | }); 10 | 11 | it('should always end with / to allow both routes to work', () => { 12 | Object.keys(routes) 13 | .forEach((route) => { 14 | const pattern = routes[route].path; 15 | expect(pattern.substr(-1)).toBe('/', 'route does not end with /'); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/utils/bem.js: -------------------------------------------------------------------------------- 1 | function toClass(classes = '', prefix = '') { 2 | if (typeof classes === 'string') { 3 | return (prefix + classes).trim(); 4 | } 5 | 6 | const arrClasses = Array.isArray(classes) 7 | ? classes 8 | : Object.keys(classes).filter((className) => classes[className]); 9 | 10 | return arrClasses.reduce((prev, currClass) => `${prev ? `${prev} ` : ''}${prefix + currClass}`, ''); 11 | } 12 | 13 | const bem = ({ 14 | prefix = '', block, elementPrefix = '__', modifierPrefix = '--' 15 | }) => 16 | (element = '', modifier = '', utils = '') => { 17 | const blockClass = `${prefix}${block}`; 18 | const elementClass = element ? toClass(element, blockClass + elementPrefix) : ''; 19 | const blockModifier = modifier && !elementClass ? ` ${toClass(modifier, blockClass + modifierPrefix)}` : ''; 20 | const elementModifier = modifier && elementClass ? ` ${toClass(modifier, elementClass + modifierPrefix)}` : ''; 21 | const utilsClass = utils ? ` ${toClass(utils)}` : ''; 22 | const bemClasses = element ? elementClass + elementModifier : blockClass + blockModifier; 23 | return (bemClasses + utilsClass).trim(); 24 | }; 25 | 26 | module.exports = bem; 27 | -------------------------------------------------------------------------------- /src/app/utils/bem.md: -------------------------------------------------------------------------------- 1 | > ~400 bytes gzipped 2 | 3 | Bem is a _higher-order-function_... (it's just a function that creates a function) 4 | 5 | `bem = BEM(Object): // Object { prefix: String, block: String } => Returns function` 6 | 7 | `bem(String, Array|String|object, Array|String|Object)` 8 | 9 | ## Primary Options 10 | 11 | * `block` _mandatory_ : String 12 | * `prefix` _optional_ : String default `` 13 | 14 | ## Secondary Options 15 | 16 | * 1st argument (`element`) _optional_ : String 17 | * 2nd argument (`modifier`) _optional_ : String | Object | Array 18 | * 3rd argument (`util`) _optional_ : String | Object | Array 19 | 20 | ## Using Objects 21 | 22 | The class name is built using the `keys` of the Object. The values are evaluated, if they are falsey, they're ignored. 23 | 24 | ```json 25 | { 26 | 'potential-class': false, // ignored 27 | 'another-class': true, // added 28 | } 29 | 30 | ``` 31 | ## Using Arrays 32 | 33 | The class name is built using the `values` of the Array. 34 | ```json 35 | [ 'potential-class', 'another-class' ] 36 | ``` 37 | 38 | 39 | # Usage 40 | 41 | ```jsx 42 | import Bem from 'argos-utils/esnext/bem' 43 | const className = Bem({ prefix: 'ac-', block: 'my-component' }) 44 | ... 45 |
    // returns 'ac-my-component' 46 |
    // returns 'ac-my-component__element ' 47 |
    // returns 'ac-my-component__element ac-my-component__element--modifier' 48 |
    // returns 'ac-my-component__element ac-my-component__element--modifier util-class' 49 | 50 | ``` 51 | 52 | If you do not need an early argument, but _do_ need a later argument, use `falsey` 53 | 54 | ```jsx 55 | import Bem from 'argos-utils/esnext/bem' 56 | const className = Bem({ prefix: 'ac-', block: 'my-component' }) 57 | ... 58 |
    // returns 'ac-my-component ac-my-component--modifier' 59 |
    // returns 'ac-my-component__element util' 60 |
    // returns 'ac-my-component util' 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /src/app/utils/bem.spec.js: -------------------------------------------------------------------------------- 1 | import Chance from 'chance'; 2 | 3 | import Bem from './bem'; 4 | 5 | const chance = new Chance(); 6 | 7 | let block; 8 | let prefix; 9 | let element; 10 | let modifier; 11 | let modifiers; 12 | let classObj; 13 | let util; 14 | let utils; 15 | let bem; 16 | 17 | describe('bem test', () => { 18 | beforeEach(() => { 19 | element = chance.word(); 20 | util = chance.word(); 21 | utils = [chance.word(), chance.word()]; 22 | modifier = chance.word(); 23 | modifiers = [chance.word(), chance.word()]; 24 | classObj = { 'valid-class': true, 'invalid-class': false }; 25 | block = chance.word(); 26 | prefix = `${chance.word()}-`; 27 | }); 28 | 29 | describe('block class', () => { 30 | beforeEach(() => { 31 | bem = Bem({ block }); 32 | }); 33 | 34 | it('default', () => { 35 | expect(bem()).toEqual(block); 36 | }); 37 | 38 | it('with element', () => { 39 | expect(bem(element)).toEqual(`${block}__${element}`); 40 | }); 41 | 42 | it('with single string modifier', () => { 43 | expect(bem(null, modifier)).toEqual(`${block} ${block}--${modifier}`); 44 | }); 45 | 46 | it('with array of string modifiers', () => { 47 | expect(bem(null, modifiers)).toEqual(`${block} ${block}--${modifiers[0]} ${block}--${modifiers[1]}`); 48 | }); 49 | 50 | it('with object of modifiers', () => { 51 | expect(bem(null, classObj)).toEqual(`${block} ${block}--valid-class`); 52 | }); 53 | 54 | it('with single string util', () => { 55 | expect(bem(null, null, util)).toEqual(`${block} ${util}`); 56 | }); 57 | 58 | it('with array of string utils', () => { 59 | expect(bem(null, null, utils)).toEqual(`${block} ${utils[0]} ${utils[1]}`); 60 | }); 61 | 62 | it('with object of utils', () => { 63 | expect(bem(null, null, classObj)).toEqual(`${block} valid-class`); 64 | }); 65 | 66 | it('with element & single string modifier', () => { 67 | expect(bem(element, modifier)).toEqual(`${block}__${element} ${block}__${element}--${modifier}`); 68 | }); 69 | 70 | it('with element & single string util', () => { 71 | expect(bem(element, null, util)).toEqual(`${block}__${element} ${util}`); 72 | }); 73 | 74 | it('with single string modifier & single string util', () => { 75 | expect(bem(null, modifier, util)).toEqual(`${block} ${block}--${modifier} ${util}`); 76 | }); 77 | 78 | it('with element & single string modifier & single string util', () => { 79 | expect(bem(element, modifier, util)).toEqual(`${block}__${element} ${block}__${element}--${modifier} ${util}`); 80 | }); 81 | 82 | it('should handle a dynamic keys/complex structures', () => { 83 | const cn = Bem({ block: 'accordion' }); 84 | const expand = 'xs'; 85 | const wasExpanded = true; 86 | const expanded = false; 87 | const className = cn( 88 | null, 89 | { 90 | visible: wasExpanded && expanded, 91 | hidden: wasExpanded && !expanded, 92 | [`visible-${expand}`]: !!expand 93 | }, 94 | ); 95 | expect(className).toEqual('accordion accordion--hidden accordion--visible-xs'); 96 | }); 97 | 98 | it('should handle modifiers that are all false without returning an extra space', () => { 99 | const cn = Bem({ block: 'accordion' }); 100 | const className = cn(null, { visible: false }); 101 | expect(className).toEqual('accordion'); 102 | }); 103 | }); 104 | 105 | describe('block with prefix', () => { 106 | beforeEach(() => { 107 | bem = Bem({ block, prefix }); 108 | }); 109 | 110 | it('default', () => { 111 | expect(bem()).toEqual(`${prefix}${block}`); 112 | }); 113 | 114 | it('with element', () => { 115 | expect(bem(element)).toEqual(`${prefix}${block}__${element}`); 116 | }); 117 | 118 | it('with single string modifier', () => { 119 | expect(bem(null, modifier)).toEqual(`${prefix}${block} ${prefix}${block}--${modifier}`); 120 | }); 121 | 122 | it('with array of string modifiers', () => { 123 | expect(bem(null, modifiers)).toEqual(`${prefix}${block} ${prefix}${block}--${modifiers[0]} ${prefix}${block}--${modifiers[1]}`); 124 | }); 125 | 126 | it('with object of modifiers', () => { 127 | expect(bem(null, classObj)).toEqual(`${prefix}${block} ${prefix}${block}--valid-class`); 128 | }); 129 | 130 | it('with single string util', () => { 131 | expect(bem(null, null, util)).toEqual(`${prefix}${block} ${util}`); 132 | }); 133 | 134 | it('with array of string utils', () => { 135 | expect(bem(null, null, utils)).toEqual(`${prefix}${block} ${utils[0]} ${utils[1]}`); 136 | }); 137 | 138 | it('with object of utils', () => { 139 | expect(bem(null, null, classObj)).toEqual(`${prefix}${block} valid-class`); 140 | }); 141 | 142 | it('with element & single string modifier', () => { 143 | expect(bem(element, modifier)).toEqual(`${prefix}${block}__${element} ${prefix}${block}__${element}--${modifier}`); 144 | }); 145 | 146 | it('with element & single string util', () => { 147 | expect(bem(element, null, util)).toEqual(`${prefix}${block}__${element} ${util}`); 148 | }); 149 | 150 | it('with single string modifier & single string util', () => { 151 | expect(bem(null, modifier, util)).toEqual(`${prefix}${block} ${prefix}${block}--${modifier} ${util}`); 152 | }); 153 | 154 | it('with element & single string modifier & single string util', () => { 155 | expect(bem(element, modifier, util)).toEqual(`${prefix}${block}__${element} ${prefix}${block}__${element}--${modifier} ${util}`); 156 | }); 157 | 158 | it('should handle a dynamic keys/complex structures', () => { 159 | const cn = Bem({ prefix: 'ac-', block: 'accordion' }); 160 | const expand = 'xs'; 161 | const wasExpanded = true; 162 | const expanded = false; 163 | const className = cn( 164 | null, 165 | { 166 | visible: wasExpanded && expanded, 167 | hidden: wasExpanded && !expanded, 168 | [`visible-${expand}`]: !!expand 169 | } 170 | ); 171 | expect(className).toEqual('ac-accordion ac-accordion--hidden ac-accordion--visible-xs'); 172 | }); 173 | 174 | it('should handle modifiers that are all false without returning an extra space', () => { 175 | const cn = Bem({ prefix: 'ac-', block: 'accordion' }); 176 | const className = cn(null, { visible: false }); 177 | expect(className).toEqual('ac-accordion'); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /src/app/utils/fetch.js: -------------------------------------------------------------------------------- 1 | /* global fetch */ 2 | import debug from 'debug'; 3 | 4 | import { localUrl } from '../utils'; 5 | 6 | const log = debug('base:fetch'); 7 | 8 | export function checkStatus(response) { 9 | if (response.status < 200 || response.status >= 500) { 10 | const error = new Error(response.statusText); 11 | error.response = response; 12 | throw error; 13 | } 14 | return response; 15 | } 16 | 17 | const jsonOpts = (method, data) => ({ 18 | method, 19 | headers: { 20 | Accept: 'application/json', 21 | 'Content-Type': 'application/json', 22 | }, 23 | data: data && JSON.stringify(data) 24 | }); 25 | 26 | export const fetchUrl = (endpoint, opts = {}) => { 27 | const url = endpoint.indexOf('//') > -1 ? endpoint : `${localUrl}${endpoint}`; 28 | return fetch(url, { ...opts }) 29 | .then(checkStatus) 30 | .then((response) => response.text()) 31 | .catch((error) => { 32 | log('request failed', error); 33 | throw new Error('request failed'); 34 | }); 35 | }; 36 | 37 | export const getJSON = (url, options) => 38 | fetchUrl(url, jsonOpts('GET', null, options)).then((data) => JSON.parse(data)); 39 | 40 | export const postJSON = (url, data, options) => 41 | fetchUrl(url, jsonOpts('POST', data, options)); 42 | -------------------------------------------------------------------------------- /src/app/utils/fetch.spec.js: -------------------------------------------------------------------------------- 1 | import Chance from 'chance'; 2 | 3 | import config from '../../config/environment'; 4 | import { fetchUrl } from './fetch'; 5 | 6 | let stubUrl; 7 | let stubOptions; 8 | 9 | 10 | const chance = new Chance(); 11 | 12 | describe('fetch', () => { 13 | beforeEach(() => { 14 | global.fetch = jest.fn().mockImplementation((url, options) => { 15 | stubUrl = url; 16 | stubOptions = options; 17 | return Promise.resolve({ 18 | status: 200, 19 | text: jest.fn() 20 | }); 21 | }); 22 | }); 23 | 24 | describe(' URL ', () => { 25 | it('should return url with localhost by default', (done) => { 26 | const endpoint = `/${chance.word()}`; 27 | fetchUrl(endpoint).then(() => { 28 | expect(stubUrl).toEqual(`http://localhost:${config.PORT}${endpoint}`); 29 | done(); 30 | }).catch((e) => { 31 | done(e); 32 | }); 33 | }); 34 | it('should return given url if it contains double-slash', (done) => { 35 | const endpoint = `//${chance.word()}`; 36 | fetchUrl(endpoint).then(() => { 37 | expect(stubUrl).toEqual(endpoint); 38 | done(); 39 | }).catch((e) => { 40 | done(e); 41 | }); 42 | }); 43 | 44 | it('should return request options with data', (done) => { 45 | const endpoint = chance.word(); 46 | const data = chance.sentence(); 47 | fetchUrl(endpoint, { data }).then(() => { 48 | expect(stubOptions.data).toEqual(data); 49 | done(); 50 | }).catch((e) => { 51 | done(e); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/utils/index.js: -------------------------------------------------------------------------------- 1 | export { fetchUrl, getJSON, postJSON } from './fetch'; 2 | export { randomRange } from './randomRange'; 3 | 4 | const navigator = global.navigator && global.navigator.userAgent; 5 | // hasWindow = true for tests + client 6 | export const hasWindow = typeof window !== 'undefined'; 7 | // isBrowser = true for client only 8 | export const isBrowser = typeof navigator !== 'undefined' && navigator.indexOf('jsdom/') === -1 && navigator.indexOf('Node.js') === -1; 9 | 10 | const getLocalUrl = () => { 11 | if (isBrowser) { 12 | const { location } = window; 13 | return location.origin; 14 | } 15 | return `http://localhost:${process.env.PORT}`; 16 | }; 17 | 18 | export const localUrl = getLocalUrl(); 19 | -------------------------------------------------------------------------------- /src/app/utils/randomRange.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export function randomRange(from, to, length) { 3 | if (to < from) return []; 4 | const returnArray = []; 5 | let loopCount = 0; 6 | randomRangeLoop: 7 | while (loopCount < length) { 8 | const randomInt = Math.floor(Math.random() * (to - from + 1)) + from; 9 | let i = 0; 10 | while (i < loopCount) { 11 | if (returnArray[i++] === randomInt) continue randomRangeLoop; 12 | } 13 | returnArray[loopCount++] = randomInt; 14 | } 15 | return returnArray; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/utils/randomRange.spec.js: -------------------------------------------------------------------------------- 1 | import { randomRange } from './randomRange'; 2 | 3 | describe('randomRange', () => { 4 | it('should return an array', () => { 5 | expect(Array.isArray(randomRange(0, 0, 0))).toBe(true, 'is not an array'); 6 | }); 7 | 8 | it('should return an array length matching the 3rd argument', () => { 9 | const arrLength = 1; 10 | expect(randomRange(0, 0, arrLength)).toHaveLength(arrLength); 11 | }); 12 | 13 | it('should return a known value when from and to args match', () => { 14 | const arrLength = 1; 15 | const fromTo = 1; 16 | expect(randomRange(fromTo, fromTo, arrLength)[0]).toBe(fromTo); 17 | }); 18 | 19 | it('returns all values when range matches length', () => { 20 | const range = randomRange(0, 1, 2); 21 | expect(range).toHaveLength(2); 22 | expect(range.includes(0)).toBe(true, `${range} does not include 0`); 23 | expect(range.includes(1)).toBe(true, `${range} does not include 1`); 24 | 25 | const range2 = randomRange(0, 2, 3); 26 | expect(range2).toHaveLength(3); 27 | expect(range2.includes(0)).toBe(true, `${range2} does not include 0`); 28 | expect(range2.includes(1)).toBe(true, `${range2} does not include 1`); 29 | expect(range2.includes(2)).toBe(true, `${range2} does not include 2`); 30 | 31 | const range3 = randomRange(1, 3, 3); 32 | expect(range3).toHaveLength(3); 33 | expect(range3.includes(1)).toBe(true, `${range3} does not include 0`); 34 | expect(range3.includes(2)).toBe(true, `${range3} does not include 1`); 35 | expect(range3.includes(3)).toBe(true, `${range3} does not include 2`); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/client-entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import debug from 'debug'; 4 | 5 | import './config/environment'; 6 | 7 | import Root from './app/Root'; 8 | 9 | const log = debug('lego:client-entry'); 10 | 11 | try { 12 | ReactDOM.render(, document.getElementById('html')); 13 | 14 | if (typeof module.hot === 'object') { 15 | module.hot.accept((err) => { 16 | if (err) { 17 | console.error('Cannot apply HMR update.', err); // eslint-disable-line no-console 18 | } 19 | }); 20 | } 21 | } catch (err) { 22 | log('Render error', err); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/config/environment.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug'); 2 | 3 | const log = debug('lego: Environment:'); 4 | let config = {}; 5 | 6 | const setConfig = () => { 7 | // explicitly check vars so that webpack can help us 8 | if (process.env.ENV === 'dev') { 9 | // set dev envs here 10 | if (typeof process.env.NODE_ENV === 'undefined') { process.env.NODE_ENV = 'development'; } 11 | } 12 | 13 | if (process.env.NODE_ENV === 'test') { 14 | // set test envs here 15 | process.env.DEBUG = false; 16 | } 17 | 18 | // set prod / default env here 19 | if (typeof process.env.NODE_ENV === 'undefined') { process.env.NODE_ENV = 'production'; } 20 | if (typeof process.env.DEBUG === 'undefined') { process.env.DEBUG = 'lego:*'; } 21 | if (typeof process.env.PORT === 'undefined') { process.env.PORT = 3000; } 22 | 23 | if (process.env.DEBUG) debug.enable(process.env.DEBUG); 24 | 25 | config = { 26 | api: { 27 | label: 'SWAPI', 28 | host: 'https://swapi.co/api/', 29 | homepage: 'https://www.swapi.com' 30 | }, 31 | NODE_ENV: process.env.NODE_ENV, 32 | DEBUG: process.env.DEBUG, 33 | PORT: process.env.PORT 34 | }; 35 | }; 36 | 37 | if (Object.keys(config).length === 0) { 38 | setConfig(); 39 | log(config); 40 | } 41 | 42 | module.exports = config; 43 | -------------------------------------------------------------------------------- /src/config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const ROOT = path.join(process.cwd()); 4 | const SRC = path.join(ROOT, 'src'); 5 | const COMPILED = path.join(ROOT, 'compiled'); 6 | const DIST = path.join(COMPILED, 'dist'); 7 | const APP = path.join(SRC, 'app'); 8 | const ICONS = path.join(SRC, 'icons'); 9 | const STYLES = path.join(SRC, 'styles'); 10 | const TESTS = path.join(ROOT, 'tests'); 11 | 12 | module.exports = { 13 | ROOT, SRC, DIST, COMPILED, APP, ICONS, STYLES, TESTS 14 | }; 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/polyfills.js: -------------------------------------------------------------------------------- 1 | require('./app/polyfills/fetch'); 2 | require('./app/polyfills/find'); 3 | require('./app/polyfills/location.origin'); 4 | require('./app/polyfills/promise'); 5 | -------------------------------------------------------------------------------- /src/server-entry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, import/no-extraneous-dependencies */ 2 | const Webpack = require('webpack'); 3 | const WebpackServer = require('webpack-serve'); 4 | 5 | const { PORT } = require('./config/environment'); 6 | const config = require('../webpack.config.dev.js'); 7 | require('./app/polyfills/node-fetch'); 8 | 9 | const options = { ...config.serve }; 10 | delete config.serve; 11 | const compiler = Webpack(config); 12 | options.compiler = compiler; 13 | options.port = PORT; 14 | 15 | WebpackServer(options).then((server) => { 16 | server.on('listening', () => { 17 | console.log(`Starting server on http://localhost:${PORT}`); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import "base/resets"; 2 | 3 | @import "layouts/mainLayout"; 4 | 5 | @import "components/answer"; 6 | @import "components/question"; 7 | -------------------------------------------------------------------------------- /src/styles/base/resets.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | color: #444; 9 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 10 | } 11 | 12 | a { 13 | color: #4682B4; 14 | text-decoration: none; 15 | 16 | &:hover { 17 | text-decoration: underline; 18 | } 19 | } 20 | 21 | a.button, 22 | button { 23 | padding: 2px 6px; 24 | text-decoration: none; 25 | cursor: pointer; 26 | border-radius: 2px; 27 | line-height: 2em; 28 | border: 2px solid rgba(0, 0, 0, 0.2); 29 | background-color: rgba(0, 0, 0, 0.15); 30 | color: currentColor; 31 | transition: all 0.15s ease-in; 32 | 33 | &:hover { 34 | border: 2px solid rgba(0, 0, 0, 0.25); 35 | background-color: rgba(0, 0, 0, 0.2); 36 | } 37 | } 38 | 39 | h1 { 40 | margin-top: 0; 41 | } 42 | 43 | .hidden{ 44 | display:none!important; 45 | } 46 | -------------------------------------------------------------------------------- /src/styles/components/answer.scss: -------------------------------------------------------------------------------- 1 | @import '../utils/colours'; 2 | 3 | .answer-option { 4 | width: 49%; 5 | float: left; 6 | margin-right: 1%; 7 | padding: 1%; 8 | border: 1px dashed $item-color; 9 | 10 | &--answer { 11 | border: 2px solid $item-color--correct; 12 | } 13 | 14 | &__title { 15 | font-weight: bold; 16 | display: inline-block; 17 | width: 40%; 18 | min-width: 100px; 19 | } 20 | 21 | &__value { 22 | display: inline-block; 23 | width: 60%; 24 | margin: 0; 25 | min-width: 150px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/components/question.scss: -------------------------------------------------------------------------------- 1 | @import '../utils/colours'; 2 | 3 | .card-item-value { 4 | display: block; 5 | } 6 | 7 | .question__options { 8 | padding: 0; 9 | } 10 | 11 | .question__option { 12 | display: block; 13 | border: 1px dashed $item-color; 14 | padding: 4px; 15 | margin: 4px; 16 | cursor: pointer; 17 | 18 | &--selected { 19 | border: 2px solid $item-color--selected; 20 | } 21 | 22 | &--wrong { 23 | border-color: $item-color--wrong; 24 | } 25 | 26 | &--correct { 27 | border-color: $item-color--correct; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/styles/layouts/mainLayout.scss: -------------------------------------------------------------------------------- 1 | @import '../utils/colours'; 2 | @import '../utils/mixins'; 3 | 4 | $nav-height: 2em; 5 | $footer-height: 2em; 6 | $content-height: calc(100vh - #{$nav-height + $footer-height}); 7 | $col1-width: 33% !default; 8 | $col2-width: 66% !default; 9 | 10 | .layout { 11 | width: 100%; 12 | } 13 | 14 | .layout__nav { 15 | padding: 0 2.25%; 16 | height: $nav-height; 17 | } 18 | 19 | .layout__nav-header { 20 | margin-right: 10px; 21 | border-right: 6px double currentColor; 22 | padding-right: 10px; 23 | } 24 | 25 | .layout__nav-link { 26 | margin: 0 6px; 27 | line-height: 2em; 28 | 29 | &:first-of-type { 30 | margin-left: 0; 31 | } 32 | 33 | &:last-of-type { 34 | margin-right: 0; 35 | } 36 | } 37 | 38 | .layout__content { 39 | min-height: $content-height; 40 | padding: 0 2.25%; 41 | 42 | &::before , 43 | &::after { 44 | content: ''; 45 | display: table; 46 | clear: both; 47 | } 48 | } 49 | 50 | .layout__footer { 51 | padding: 0 2.25%; 52 | height: $footer-height; 53 | line-height: 2em; 54 | } 55 | -------------------------------------------------------------------------------- /src/styles/utils/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins"; 2 | 3 | .small-only { 4 | @include medium { 5 | display: none !important; 6 | } 7 | } 8 | 9 | .medium { 10 | @include small-only { 11 | display: none !important; 12 | } 13 | } 14 | 15 | .medium-only { 16 | @include small-only { 17 | display: none !important; 18 | } 19 | 20 | @include large { 21 | display: none !important; 22 | } 23 | } 24 | 25 | .small-medium-only { 26 | @include large { 27 | display: none !important; 28 | } 29 | } 30 | 31 | .large-only { 32 | @include small-only { 33 | display: none !important; 34 | } 35 | @include medium-only { 36 | display: none !important; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/styles/utils/_colours.scss: -------------------------------------------------------------------------------- 1 | $item-color: #d3d3d3; 2 | $item-color--correct: #006400; 3 | $item-color--wrong: #ff0000; 4 | $item-color--selected: #0b97c4; 5 | -------------------------------------------------------------------------------- /src/styles/utils/_mixins.scss: -------------------------------------------------------------------------------- 1 | $medium: 768px !default; 2 | $large: 1120px !default; 3 | $maxSmallWidth: $medium - 1 !default; 4 | $maxMediumWidth: $large - 1 !default; 5 | 6 | 7 | @mixin clearfix { 8 | &:before, &:after { 9 | content: " "; 10 | display: table; 11 | } 12 | &:after { 13 | clear: both; 14 | } 15 | } 16 | 17 | @mixin small-only { 18 | @media screen and (max-width: $maxSmallWidth) { 19 | @content; 20 | } 21 | } 22 | 23 | @mixin medium-only { 24 | @media screen and (min-width: $medium) and (max-width:$maxMediumWidth) { 25 | @content; 26 | } 27 | } 28 | 29 | @mixin medium { 30 | @media screen and (min-width: $medium) { 31 | @content; 32 | } 33 | } 34 | 35 | @mixin large { 36 | @media screen and (min-width: $large) { 37 | @content; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | > Unit, functional and end-to-end (e2e) tests 4 | 5 | ## Unit Testing 6 | 7 | > Test isolated functions/files, focusing on api input vs output rather than implementation. 8 | 9 | `yarn test:unit` 10 | 11 | You can find all the `*.spec.js` test file sitting next to the file they test. 12 | This way it is very quick and simple to see documentation on how to use a function. 13 | It is also very easy to see if any files are _missing_ tests 14 | 15 | ## Functional Testing 16 | 17 | > Testing a whole route, Simulating clicks, Mock api calls and test the page is rendered correctly, 18 | focusing on how interactions affect multiple components. 19 | 20 | `yarn test:func` 21 | 22 | These tests should _not_ contribute to code-coverage as they load and check multiple functions across files (not lines of code). 23 | 24 | ## e2e Testing 25 | 26 | > Make sure the whole app run on multiple _real_ browsers, 27 | focusing on potentially problematic functionality. 28 | 29 | * `yarn test:e2e` (headless browser for local testing) 30 | * `yarn test:e2e-xb` (multiple browsers for remote testing) 31 | 32 | ## Smoke Testing 33 | 34 | > A subset of e2e tests that have been chosen to prove the app has been deployed correctly. 35 | 36 | * `yarn test:smoke -- --TARGET_PATH=preprod.domain.com` 37 | -------------------------------------------------------------------------------- /tests/config/jest/enzymeSetup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | configure({ adapter: new Adapter() }) 5 | 6 | require('../../../src/app/polyfills/node-fetch'); 7 | -------------------------------------------------------------------------------- /tests/config/jest/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /tests/config/jest/reactShim.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = (callback) => { 2 | setTimeout(callback, 0) 3 | } 4 | -------------------------------------------------------------------------------- /tests/config/jest/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-commands/loadPage.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | const util = require('util'); 3 | const events = require('events'); 4 | 5 | let browser; 6 | 7 | function loadPage() { 8 | events.EventEmitter.call(this); 9 | browser = this.api; 10 | } 11 | util.inherits(loadPage, events.EventEmitter); 12 | 13 | loadPage.prototype.complete = function complete({ e, done }) { 14 | if (e) { 15 | console.log('e', e); 16 | } 17 | this.emit('complete'); 18 | if (typeof done === 'function') { 19 | done(); 20 | } 21 | }; 22 | 23 | loadPage.prototype.command = function pageLoadedFn(page, opts = {}) { 24 | const { selector = 'body', disableAnimations = true, cookie, done } = opts; 25 | const url = browser.globals.TARGET_PATH + (page || ''); 26 | console.log(url); 27 | const args = [disableAnimations ? 'disable-animations' : '']; 28 | function disableAnimationFunction(className) { 29 | document.body.className += ` ${className}`; 30 | return document.body.className; 31 | } 32 | 33 | browser 34 | .windowMaximize() 35 | .url(url) 36 | .setCookie(cookie) 37 | .url(url) 38 | .waitForElementVisible(selector, 2500) 39 | .execute(disableAnimationFunction, args, () => { 40 | this.complete({ done }); 41 | }); 42 | 43 | return this; 44 | }; 45 | 46 | module.exports = loadPage; 47 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-commands/resizeTo.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | const util = require('util'); 3 | const events = require('events'); 4 | 5 | let browser; 6 | 7 | function resizeTo() { 8 | events.EventEmitter.call(this); 9 | browser = this.api; 10 | } 11 | util.inherits(resizeTo, events.EventEmitter); 12 | 13 | resizeTo.prototype.complete = function complete({ e, done }) { 14 | if (e) { 15 | console.log('e', e); 16 | } 17 | this.emit('complete'); 18 | if (typeof done === 'function') { 19 | done(); 20 | } 21 | }; 22 | 23 | resizeTo.prototype.command = function pageLoadedFn(size, opts = {}) { 24 | const { done } = opts; 25 | const target = this.getDimensions(size) 26 | browser 27 | .resizeWindow(target.width, target.height) 28 | .getElementSize('body', (result) => ( 29 | browser 30 | .resizeWindow(target.width + (target.width - result.value.width), 31 | target.height + (target.height - result.value.height)) 32 | )) 33 | .perform(() => { 34 | this.complete({ done }) 35 | }) 36 | 37 | return this 38 | }; 39 | 40 | function getDimensions(size) { 41 | if (typeof size === 'object' && size.height && size.width) { 42 | return size; 43 | } 44 | switch (size) { 45 | case 'xs': return { height: 800, width: 320 } 46 | case 'sm': return { height: 800, width: 576 } 47 | case 'md': return { height: 800, width: 768 } 48 | case 'lg': return { height: 800, width: 992 } 49 | case 'xl': return { height: 800, width: 1200 } 50 | default: 51 | throw Error(`Unknown size '${size}'.`) 52 | } 53 | } 54 | 55 | module.exports = resizeTo; 56 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-commands/safeClick.js: -------------------------------------------------------------------------------- 1 | exports.command = function safeClick(selector, callback) { 2 | var browser = this; 3 | 4 | browser 5 | .waitForElementPresent(selector) 6 | .scrollElementToCenter(selector) 7 | .click(selector, function(){ 8 | if (typeof callback === 'function') { 9 | callback.call(browser); 10 | } 11 | }) 12 | 13 | return this; 14 | }; 15 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-commands/scrollElementToCenter.js: -------------------------------------------------------------------------------- 1 | exports.command = function scrollElementToCenter(selector, callback) { 2 | var browser = this; 3 | browser.windowHandle(function(windowHandle) { 4 | browser.windowSize(windowHandle.value, function(windowSize) { 5 | var halfHeight = windowSize.value.height / 2; 6 | browser.element('css selector', selector, function(element) { 7 | browser.elementIdSize(element.value.ELEMENT, function(elementIdSize) { 8 | var haldElementSize = elementIdSize.value.height / 2; 9 | browser.elementIdLocation(element.value.ELEMENT, function(result) { 10 | var yOffset = result.value.y; 11 | var execString = 'window.scrollTo(0, ' + (yOffset - (halfHeight - haldElementSize)) + ');'; 12 | browser.execute(execString, function() { 13 | if (typeof callback === 'function') { 14 | callback.call(browser); 15 | } 16 | }); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | 23 | return this; 24 | }; 25 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-globals.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // this controls whether to abort the test execution when an assertion failed and skip the rest 3 | // it's being used in waitFor commands and expect assertions 4 | abortOnAssertionFailure: false, 5 | 6 | // this will overwrite the default polling interval (currently 500ms) for waitFor commands 7 | // and expect assertions that use retry 8 | waitForConditionPollInterval: 500, 9 | 10 | // default timeout value in milliseconds for waitFor commands and implicit waitFor value for 11 | // expect assertions 12 | waitForConditionTimeout: 14000, // mobile testing takes ages! 13 | 14 | // this will cause waitFor commands on elements to throw an error if multiple 15 | // elements are found using the given locate strategy and selector 16 | throwOnMultipleElementsReturned: true, 17 | 18 | // controls the timeout time for async hooks. 19 | // Expects the done() callback to be invoked within this time or an error is thrown 20 | asyncHookTimeout: 14000, // mobile testing takes ages! 21 | }; 22 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-pages/game.js: -------------------------------------------------------------------------------- 1 | // https://github.com/nightwatchjs/nightwatch/wiki/Page-Object-API 2 | // http://nightwatchjs.org/guide#using-page-objects 3 | import { findRoute } from '../../../../src/app/routes'; 4 | import global from './global' 5 | 6 | module.exports = { 7 | 8 | url: function () { 9 | return findRoute('game').path; 10 | }, 11 | 12 | getOptionsText: function (browser) { 13 | return browser.getText 14 | }, 15 | 16 | elements: [ 17 | global.elements, 18 | { 19 | page: "#game", 20 | question: '.question', 21 | questionOptions: '.question__options', 22 | dealBtn: '.game__btn--deal' 23 | } 24 | ], 25 | 26 | sections: { 27 | ...global.sections, 28 | 29 | page: { 30 | 31 | selector: '#game', 32 | locateStrategy: 'css selector' 33 | 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-pages/global.js: -------------------------------------------------------------------------------- 1 | // https://github.com/nightwatchjs/nightwatch/wiki/Page-Object-API 2 | // http://nightwatchjs.org/guide#using-page-objects 3 | 4 | module.exports = { 5 | 6 | elements: { 7 | loading: ".loading", 8 | }, 9 | 10 | sections: { 11 | nav: { 12 | selector: 'nav.layout__nav', 13 | locateStrategy: 'css selector', 14 | elements: { 15 | aboutLink: 'a[href="/about/"]', 16 | gameLink: 'a[href="/game/"]', 17 | }, 18 | }, 19 | main: { 20 | selector: '.layout', 21 | locateStrategy: 'css selector', 22 | }, 23 | content: { 24 | selector: 'main.layout__content', 25 | locateStrategy: 'css selector', 26 | }, 27 | footer: { 28 | selector: 'footer.layout__footer', 29 | locateStrategy: 'css selector', 30 | }, 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch-pages/home.js: -------------------------------------------------------------------------------- 1 | // https://github.com/nightwatchjs/nightwatch/wiki/Page-Object-API 2 | // http://nightwatchjs.org/guide#using-page-objects 3 | import { findRoute } from '../../../../src/app/routes'; 4 | import global from './global' 5 | 6 | module.exports = { 7 | 8 | url: function () { 9 | return findRoute('homepage').path; 10 | }, 11 | 12 | elements: [ 13 | global.elements, 14 | { 15 | page: "#homepage", 16 | } 17 | ], 18 | 19 | sections: { 20 | ...global.sections, 21 | 22 | page: { 23 | 24 | selector: '#homepage', 25 | locateStrategy: 'css selector' 26 | 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const argv = require('yargs') 3 | .usage('Usage: $0 --target=[string] --sha=[string]') 4 | .argv; 5 | process.env.PORT = 3210; 6 | require('@babel/register')({ 7 | only: [/src/, /tests/] 8 | }); 9 | const testServer = require('../test-server/test-server-entry'); 10 | 11 | const TARGET_PATH = argv.target || `http://localhost:${process.env.PORT}`; 12 | const needLocalServer = TARGET_PATH.indexOf('localhost') > -1; 13 | const noop = (done) => { done(); }; 14 | 15 | module.exports = (function(settings) { 16 | var buildString = ""; 17 | if (argv.sha) { 18 | buildString += argv.sha 19 | } else { 20 | buildString += 'local ' + Date.now(); 21 | buildString = buildString.substring(0, buildString.length - 4); 22 | } 23 | 24 | settings.test_settings.default.globals = { 25 | TARGET_PATH : TARGET_PATH, 26 | before: needLocalServer ? testServer.start : noop, 27 | after: needLocalServer ? testServer.stop : noop, 28 | afterEach: function (client, done) { 29 | var weHaveFailures = client.currentTest.results.errors > 0 || client.currentTest.results.failed > 0; 30 | if (weHaveFailures && !client.sessionId) { 31 | console.log('Session already ended.'); 32 | done(); 33 | return; 34 | } 35 | if (weHaveFailures) { 36 | client.saveScreenshot(`${client.currentTest.name}${client.currentTest.module}.png`, function(result) { 37 | if (!result || result.status !== 0) { 38 | console.log('Error saving screenshot...', result); 39 | } 40 | client.deleteCookies().end(done); 41 | }); 42 | } else { 43 | client.deleteCookies().end(done); 44 | } 45 | } 46 | }; 47 | settings.test_settings.default.desiredCapabilities['browserstack.user'] = argv.bsuser || process.env.BROWSERSTACK_USER; 48 | settings.test_settings.default.desiredCapabilities['browserstack.key'] = argv.bskey || process.env.BROWSERSTACK_KEY; 49 | settings.test_settings.default.desiredCapabilities['build'] = buildString; 50 | return settings; 51 | })(require('./nightwatch.json')); 52 | -------------------------------------------------------------------------------- /tests/config/nightwatch/nightwatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_folders" : ["tests/e2e"], 3 | "test_workers": { 4 | "enabled": false, 5 | "workers": "auto" 6 | }, 7 | "selenium" : { 8 | "start_process" : true, 9 | "server_path" : "node_modules/selenium-standalone/.selenium/selenium-server/3.8.1-server.jar", 10 | "cli_args" : { 11 | "webdriver.chrome.driver" : "node_modules/selenium-standalone/.selenium/chromedriver/2.37-x64-chromedriver" 12 | } 13 | }, 14 | "custom_commands_path" : "./tests/config/nightwatch/nightwatch-commands", 15 | "page_objects_path" : "./tests/config/nightwatch/nightwatch-pages", 16 | "globals_path": "./tests/config/nightwatch/nightwatch-globals.js", 17 | 18 | "test_settings" : { 19 | "default" : { 20 | "launch_url" : "http://hub.browserstack.com", 21 | "selenium_port" : 80, 22 | "selenium_host" : "hub.browserstack.com", 23 | "silent": true, 24 | "screenshots" : { 25 | "enabled" : true, 26 | "on_failure" : true, 27 | "on_error" : true, 28 | "path" : "tests/e2e/tests_screenshots" 29 | }, 30 | "desiredCapabilities": { 31 | "project": "React Lego", 32 | "build": "build ", 33 | "browserName": "firefox", 34 | "javascriptEnabled": true, 35 | "acceptSslCerts": true, 36 | "browserstack.local" : true, 37 | "browserstack.debug": true, 38 | "resolution": "1024x768" 39 | } 40 | }, 41 | "local": { 42 | "launch_url" : "http://localhost", 43 | "selenium_port" : 4444, 44 | "selenium_host" : "127.0.0.1", 45 | "desiredCapabilities": { 46 | "build": "build local", 47 | "browserName": "chrome", 48 | "browserstack.local" : false 49 | } 50 | }, 51 | "chrome_win" : { 52 | "desiredCapabilities": { 53 | "os": "Windows", 54 | "os_version": "7", 55 | "browserName": "chrome" 56 | } 57 | }, 58 | "safari_osx" : { 59 | "desiredCapabilities": { 60 | "os": "OS X", 61 | "os_version": "El Capitan", 62 | "browserName": "safari" 63 | } 64 | }, 65 | "chrome_osx" : { 66 | "desiredCapabilities": { 67 | "os": "OS X", 68 | "os_version": "El Capitan", 69 | "browserName": "chrome" 70 | } 71 | }, 72 | "firefox_win" : { 73 | "desiredCapabilities": { 74 | "os": "Windows", 75 | "os_version": "7", 76 | "browserName": "firefox" 77 | } 78 | }, 79 | "firefox_osx" : { 80 | "desiredCapabilities": { 81 | "os": "OS X", 82 | "os_version": "El Capitan", 83 | "browserName": "firefox" 84 | } 85 | }, 86 | "IE11" : { 87 | "desiredCapabilities": { 88 | "os": "Windows", 89 | "os_version": "8.1", 90 | "browserName": "IE", 91 | "browser_version": "11.0" 92 | } 93 | }, 94 | "edge": { 95 | "desiredCapabilities": { 96 | "browserName": "Edge", 97 | "browser_version": "13.0", 98 | "os": "Windows", 99 | "os_version": "10" 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/config/test-server/test-server-entry.js: -------------------------------------------------------------------------------- 1 | require('../../../src/config/environment'); 2 | require('../../../src/app/polyfills/node-fetch'); 3 | 4 | const HttpServer = require('http-server').HttpServer; 5 | let openServer = new HttpServer({ root: 'compiled'}); 6 | 7 | const startLocalServers = (done) => { 8 | openServer.listen(process.env.PORT, 'localhost', () => { 9 | console.log(`Server running on port ${process.env.PORT}`); 10 | done() 11 | }); 12 | }; 13 | const stopLocalServers = (done) => { 14 | console.log('Closing server...'); 15 | openServer.close(done); 16 | }; 17 | 18 | 19 | module.exports = { 20 | start: startLocalServers, 21 | stop: stopLocalServers 22 | }; 23 | -------------------------------------------------------------------------------- /tests/e2e/game.e2e.js: -------------------------------------------------------------------------------- 1 | import {findRoute} from '../../src/app/routes'; 2 | 3 | module.exports = { 4 | '@tags': ['staging', 'production'], 5 | before({ loadPage, page: { game } }) { 6 | loadPage(findRoute('homepage').path, {selector: '#homepage'}) 7 | game().section.nav.click('@gameLink'); 8 | }, 9 | 10 | after (browser, done) { 11 | browser.end().perform(() => done()) 12 | }, 13 | 14 | ['expects a hand to be loaded by the server']({ page: { game } }) { 15 | game().expect.element('@question').to.be.present; 16 | }, 17 | 18 | ['can load a new hand'](browser) { 19 | const { page: { game } } = browser; 20 | game().expect.element('@questionOptions').to.be.present; 21 | game().getText('@questionOptions', (text) => { 22 | game() 23 | .click('@dealBtn') 24 | .waitForElementNotPresent('@loading', 1500) 25 | .expect.element('@questionOptions').text.to.not.equal(text.value); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tests/e2e/homepage.e2e.js: -------------------------------------------------------------------------------- 1 | import { findRoute } from '../../src/app/routes'; 2 | 3 | module.exports = { 4 | '@tags': ['staging', 'production'], 5 | before(browser) { 6 | browser.loadPage(findRoute('homepage').path, { selector : '#homepage' }); 7 | }, 8 | after (browser, done) { 9 | browser.end().perform(() => done()) 10 | }, 11 | 12 | ['homepage layout should include nav, footer and content blocks']({ page }) { 13 | const Home = page.home(); 14 | Home.expect.section('@page').to.be.present; 15 | Home.expect.section('@main').to.be.present; 16 | Home.expect.section('@nav').to.be.present; 17 | Home.expect.section('@content').to.be.present; 18 | Home.expect.section('@footer').to.be.present; 19 | }, 20 | 21 | ['homepage can navigate to the game page']({ page }) { 22 | page.home().section.nav.click('@gameLink'); 23 | page.game().expect.section('@page').to.be.present; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/e2e/router.e2e.js: -------------------------------------------------------------------------------- 1 | import { findRoute } from '../../src/app/routes'; 2 | 3 | module.exports = { 4 | '@tags': ['staging', 'production'], 5 | before({ loadPage }) { 6 | loadPage(findRoute('homepage').path, { selector : '#homepage' }); 7 | }, 8 | after (browser, done) { 9 | browser.end().perform(() => done()) 10 | }, 11 | 12 | // if this test fails because the url ends with '/', then the js may have an error. 13 | // This test, with BrowserStack, helped catch ie10/11 errors, 14 | // being caused by a cheeky Object.Assign being used in the router. 15 | ['Navigates displaying the route in the address bar (using the correct history API)']({ page, assert, globals }) { 16 | page.game().section.nav.click('@gameLink'); 17 | page.game().expect.element('@page').to.be.present; 18 | assert.urlEquals(`${globals.TARGET_PATH}/game/`); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/fixtures/card-80.js: -------------------------------------------------------------------------------- 1 | const Chance = require('chance') 2 | const chance = new Chance() 3 | 4 | module.exports = () => ({ 5 | "name": chance.name(), 6 | "height": "234", 7 | "mass": "136", 8 | "hair_color": chance.color(), 9 | "skin_color": chance.color(), 10 | "eye_color": chance.color(), 11 | "birth_year": "unknown", 12 | "gender": chance.gender(), 13 | "homeworld": "https://swapi.co/api/planets/14/", 14 | "films": [ 15 | "https://swapi.co/api/films/6/" 16 | ], 17 | "species": [ 18 | "https://swapi.co/api/species/3/" 19 | ], 20 | "vehicles": [], 21 | "starships": [], 22 | "created": "2014-12-20T19:46:34.209000Z", 23 | "edited": "2014-12-20T21:17:50.491000Z", 24 | "url": "https://swapi.co/api/people/80/" 25 | }) 26 | -------------------------------------------------------------------------------- /tests/fixtures/public/app-fixtures.css: -------------------------------------------------------------------------------- 1 | * { 2 | border: 9px solid pink; 3 | } 4 | * { 5 | border: 9px solid pink; 6 | } 7 | * { 8 | border: 9px solid pink; 9 | } 10 | * { 11 | border: 9px solid pink; 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/public/app-fixtures.js: -------------------------------------------------------------------------------- 1 | 'Hello, I am a test asset' 2 | /* 3 | This file is used for testing asset requests. It needs a filesize to enable gzip compression 4 | 5 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 6 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 7 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 8 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 9 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 10 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 14 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 17 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 18 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 19 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 20 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 21 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 22 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 23 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 24 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 25 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 26 | 27 | */ 28 | -------------------------------------------------------------------------------- /tests/fixtures/public/polyfills-fixtures.js: -------------------------------------------------------------------------------- 1 | 'Hello, I am a test asset' 2 | /* 3 | This file is used for testing asset requests. It needs a filesize to enable gzip compression 4 | 5 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 6 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 7 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 8 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 9 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 10 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 14 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 17 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 18 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 19 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 20 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 21 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 22 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 23 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 24 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 25 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 26 | 27 | */ 28 | -------------------------------------------------------------------------------- /tests/fixtures/public/vendor-fixtures.js: -------------------------------------------------------------------------------- 1 | 'Hello, I am a test asset' 2 | /* 3 | This file is used for testing asset requests. It needs a filesize to enable gzip compression 4 | 5 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 6 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 7 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 8 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 9 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 10 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 12 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 14 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 17 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 18 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 19 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 20 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 21 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 22 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 23 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 24 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 25 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae, sed, eveniet! Corporis distinctio quibusdam maxime a, quae deleniti et similique esse doloremque repellendus nostrum aperiam autem unde tempora perferendis nemo. 26 | 27 | */ 28 | -------------------------------------------------------------------------------- /tests/functional/client-render.func.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import Root from '../../src/app/Root'; 5 | import Homepage from '../../src/app/components/Homepage/Homepage'; 6 | import NotFound from '../../src/app/components/NotFound/NotFound'; 7 | import Game from '../../src/app/components/Game/Game'; 8 | 9 | const context = {} 10 | const mockFixtures = require('../fixtures/card-80.js'); 11 | 12 | // prevent real API calls going out 13 | jest.mock('../../src/app/utils/fetch', () => ({ 14 | getJSON: () => Promise.resolve(mockFixtures()), 15 | })); 16 | 17 | describe('Client Render', function () { 18 | it('Should render the Homepage', () => { 19 | this.wrapper = mount(); 20 | expect(this.wrapper.find(Homepage).length).toBe(1); 21 | }); 22 | 23 | describe('404', () => { 24 | it('should render the 404 route', () => { 25 | this.wrapper = mount(); 26 | expect(this.wrapper.find(NotFound).length).toBe(1); 27 | expect(this.wrapper.find('#not-found').length).toBe(1); 28 | }); 29 | }); 30 | 31 | describe('game', () => { 32 | it('should render the game page', () => { 33 | this.wrapper = mount(); 34 | expect(this.wrapper.find(Game).length).toBe(1); 35 | expect(this.wrapper.find('#game').length).toBe(1); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/functional/routes/game-route.func.js: -------------------------------------------------------------------------------- 1 | /* global jest, describe, expect, it, test, beforeAll, afterAll, beforeEach, afterEach */ 2 | import React from 'react'; 3 | import { mount, shallow } from 'enzyme'; 4 | 5 | import Game from '../../../src/app/components/Game/Game'; 6 | import Loading from '../../../src/app/components/Loading/Loading'; 7 | import Question from '../../../src/app/components/Question/Question'; 8 | import Answer from '../../../src/app/components/Answer/Answer'; 9 | import Root from '../../../src/app/Root'; 10 | import { findRoute } from '../../../src/app/routes'; 11 | 12 | const mockFixtures = require('../../fixtures/card-80.js'); 13 | const context = {} 14 | let wrapper; 15 | 16 | // prevent real API calls going out 17 | jest.mock('../../../src/app/utils/fetch', () => ({ 18 | getJSON: () => Promise.resolve(mockFixtures()), 19 | })); 20 | 21 | describe('Game Route', function () { 22 | describe(`should contain markup`, () => { 23 | beforeEach(() => { 24 | wrapper = mount(); 25 | }) 26 | 27 | afterEach(() => { 28 | wrapper.unmount(); 29 | }); 30 | 31 | it(`should contain the Game container`, () => { 32 | expect(wrapper.find(Game).exists()).toBe(true); 33 | }); 34 | 35 | it(`should contain the 'main' layout`, () => { 36 | expect(wrapper.find('.layout.layout--main').exists()).toBe(true); 37 | expect(wrapper.find('.layout__nav').exists()).toBe(true); 38 | expect(wrapper.find('.layout__content').exists()).toBe(true); 39 | expect(wrapper.find('.layout__footer').exists()).toBe(true); 40 | }); 41 | 42 | it('Should contain a title', () => { 43 | expect(document.title).toBe(findRoute('game').meta.title); 44 | }); 45 | 46 | it('should have a nav', () => { 47 | expect(wrapper.find('nav').exists()).toBe(true); 48 | }); 49 | 50 | it('should have a footer', () => { 51 | expect(wrapper.find('footer').exists()).toBe(true); 52 | }); 53 | 54 | }); 55 | 56 | describe(`be able to deal a hand`, () => { 57 | beforeEach(() => { 58 | wrapper = mount(); 59 | }); 60 | 61 | afterEach(() => { 62 | wrapper.unmount(); 63 | }); 64 | 65 | it(`is not loading before it gets mounted`, () => { 66 | wrapper = shallow(); 67 | expect(wrapper.find(Loading).exists()).toBe(false); 68 | }); 69 | 70 | it(`starts loading as soon as the page is mounted`, () => { 71 | expect(wrapper.find(Loading).exists()).toBe(true); 72 | }); 73 | 74 | it(`removes loading once the json results are returned`, () => { 75 | wrapper.update(); 76 | expect(wrapper.find(Loading).exists()).toBe(false); 77 | }); 78 | 79 | it(`renders the question`, () => { 80 | wrapper.update(); 81 | expect(wrapper.find(Question).exists()).toBe(true); 82 | }); 83 | 84 | it(`passes the json response to the Question`, () => { 85 | wrapper.update(); 86 | const questionComponent = wrapper.find(Question); 87 | expect(questionComponent.props().cards.length).toEqual(2); 88 | expect(questionComponent.props().cards[0].name).not.toEqual(undefined); 89 | expect(questionComponent.props().cards[1].name).not.toEqual(undefined); 90 | }); 91 | 92 | it(`does not render the answer by default`, () => { 93 | wrapper.update(); 94 | const answerComponent = wrapper.find(Answer); 95 | expect(answerComponent.exists()).toBe(false); 96 | }); 97 | 98 | it(`renders the answer after the 'view answer' button is clicked`, () => { 99 | wrapper.update(); 100 | wrapper.find('button.game__btn--show-answer').simulate('click'); 101 | wrapper.update(); 102 | const answerComponent = wrapper.find(Answer); 103 | expect(answerComponent.exists()).toBe(true); 104 | expect(answerComponent.props().cards.length).toEqual(2); 105 | expect(answerComponent.props().cards[0].name).not.toEqual(undefined); 106 | expect(answerComponent.props().cards[1].name).not.toEqual(undefined); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/functional/routes/homepage-route.func.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import Root from '../../../src/app/Root'; 5 | import { findRoute } from '../../../src/app/routes'; 6 | 7 | const context = {} 8 | 9 | describe('Homepage Route', function () { 10 | 11 | beforeAll(() => { 12 | this.wrapper = mount(); 13 | }); 14 | 15 | describe(`should contain markup`, () => { 16 | it(`should contain the Homepage container`, () => { 17 | expect(this.wrapper.find('#homepage').exists()).toBe(true); 18 | }); 19 | 20 | it(`should contain the 'main' layout`, () => { 21 | expect(this.wrapper.find('.layout.layout--main').exists()).toBe(true); 22 | expect(this.wrapper.find('.layout__nav').exists()).toBe(true); 23 | expect(this.wrapper.find('.layout__content').exists()).toBe(true); 24 | expect(this.wrapper.find('.layout__footer').exists()).toBe(true); 25 | }); 26 | 27 | it('Should contain a title', () => { 28 | expect(document.title).toBe(findRoute('homepage').meta.title); 29 | }); 30 | 31 | it('should have a nav', () => { 32 | expect(this.wrapper.find('nav').exists()).toBe(true); 33 | }); 34 | 35 | it('should have a footer', () => { 36 | expect(this.wrapper.find('footer').exists()).toBe(true); 37 | }); 38 | 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | const AssetsPlugin = require('assets-webpack-plugin'); 4 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 5 | 6 | const { SRC, DIST } = require('./src/config/paths'); 7 | 8 | module.exports = { 9 | devtool: 'source-map', 10 | cache: true, 11 | context: SRC, 12 | output: { 13 | path: DIST, 14 | filename: '[name].js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new ProgressBarPlugin(), 19 | new ExtractTextPlugin('[name].css'), 20 | new webpack.NoEmitOnErrorsPlugin(), 21 | new webpack.DefinePlugin({ 22 | 'process.env.PORT': JSON.stringify(process.env.PORT), 23 | 'process.env.DEBUG': JSON.stringify(process.env.DEBUG), 24 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 25 | }), 26 | new AssetsPlugin({ filename: 'compiled/webpack-assets.json' }) 27 | ], 28 | resolve: { 29 | modules: ['node_modules', SRC], 30 | extensions: ['.js', '.jsx', '.scss'] 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.jsx?$/, 36 | include: [/src/], 37 | loader: 'babel-loader', 38 | options: { 39 | cacheDirectory: true 40 | } 41 | }, 42 | { 43 | test: /\.s?css$/, 44 | include: [/src/], 45 | use: ExtractTextPlugin.extract({ 46 | fallback: 'style-loader', 47 | use: ['css-loader', 'postcss-loader', 'sass-loader'] 48 | }) 49 | }, 50 | { 51 | test: /\.(jpe?g|png|gif|svg)$/i, 52 | use: [ 53 | 'file-loader?name=[name].[ext]' 54 | ] 55 | } 56 | ] 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const { SRC } = require('./src/config/paths'); 2 | const defaultConfig = require('./webpack.common'); 3 | 4 | const devConfig = Object.assign({}, defaultConfig, { 5 | mode: 'development', 6 | entry: { 7 | app: [`${SRC}/styles/app.scss`, `${SRC}/client-entry.jsx`], 8 | polyfills: [`${SRC}/polyfills.js`] 9 | }, 10 | serve: { 11 | dev: { 12 | publicPath: '/dist/' 13 | } 14 | } 15 | }); 16 | 17 | 18 | module.exports = devConfig; 19 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const Visualizer = require('webpack-visualizer-plugin'); 3 | 4 | require('./src/config/environment'); 5 | const { SRC } = require('./src/config/paths'); 6 | const defaultConfig = require('./webpack.common'); 7 | 8 | const prodConfig = Object.assign({}, defaultConfig, { 9 | mode: 'production', 10 | entry: { 11 | app: [`${SRC}/styles/app.scss`, `${SRC}/client-entry.jsx`], 12 | polyfills: [`${SRC}/polyfills.js`] 13 | } 14 | }); 15 | 16 | prodConfig.plugins.unshift(new webpack.HashedModuleIdsPlugin()); 17 | prodConfig.plugins.unshift(new Visualizer({ filename: '../webpack-stats.html' })); 18 | 19 | module.exports = prodConfig; 20 | --------------------------------------------------------------------------------