diff --git a/.gitignore b/.gitignore index 4d29575..69f4b80 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# env +.env.* +.env diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a1fa563 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "stable" +cache: + directories: + - node_modules +script: + - yarn test + - yarn build + - ./scripts/deploy.sh \ No newline at end of file diff --git a/README.md b/README.md index 9d9614c..06ace34 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,29 @@ -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +# ShiftLeft demo (Simpsons trivia) -## Available Scripts +## How to use -In the project directory, you can run: +Clone this repo -### `npm start` +```bash +git clone git@github.com:javascriptmid/react-shift-left.git +``` -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +Install the dependencies -The page will reload if you make edits.
-You will also see any lint errors in the console. +```bash +yarn +``` -### `npm test` +Run the local server -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +```bash +yarn start +``` -### `npm run build` +Build the app -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. +```bash +yarn build +``` -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting - -### Analyzing the Bundle Size - -This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size - -### Making a Progressive Web App - -This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app - -### Advanced Configuration - -This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration - -### Deployment - -This section has moved here: https://facebook.github.io/create-react-app/docs/deployment - -### `npm run build` fails to minify - -This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify +![away-from-javascript](/screen.png) diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..f671719 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['@babel/preset-env', '@babel/preset-react'], +}; diff --git a/e2e/__tests__/trivia.js b/e2e/__tests__/trivia.js new file mode 100644 index 0000000..7fdef8e --- /dev/null +++ b/e2e/__tests__/trivia.js @@ -0,0 +1,61 @@ +import 'babel-polyfill'; + +import driver from '../driver'; +import config from '../config'; + +import waitForElement from '../waitForElement'; + +// eslint-disable-next-line no-undef +jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000; + +describe('HomeScreen', () => { + beforeAll(async () => { + await driver.init({ browserName: config.E2E_DEVICE }); + }); + + afterAll(async () => { + await driver.quit(); + }); + + it('load the app', async () => { + await driver.get('http://localhost:3000/'); + const homeScreen = await waitForElement('home-screen'); + expect(homeScreen).toBeDefined(); + }); + + it('start the trivia', async () => { + const startPlayingButton = await waitForElement('start-playing-button'); + expect(startPlayingButton).toBeDefined(); + + await driver.moveTo(startPlayingButton); + await driver.click(); + + const triviaScreen = await waitForElement('trivia-screen'); + expect(triviaScreen).toBeDefined(); + }); + + it('answer the 5 questions', async () => { + for (let i = 0; i < 5; i++) { + const answerOptionButton = await waitForElement('answer-option'); + expect(answerOptionButton).toBeDefined(); + + await driver.moveTo(answerOptionButton); + await driver.click(); + await driver.sleep(1000); + } + + const triviaResultScreen = await waitForElement('trivia-result-screen'); + expect(triviaResultScreen).toBeDefined(); + }); + + it('restarts trivia', async () => { + const restartTriviaButton = await waitForElement('restart-trivia-button'); + expect(restartTriviaButton).toBeDefined(); + + await driver.moveTo(restartTriviaButton); + await driver.click(); + + const homeScreen = await waitForElement('home-screen'); + expect(homeScreen).toBeDefined(); + }); +}); diff --git a/e2e/config.js b/e2e/config.js new file mode 100644 index 0000000..eb3d41c --- /dev/null +++ b/e2e/config.js @@ -0,0 +1,33 @@ +require('custom-env').env('e2e'); + +const { + E2E_DEVICE, + E2E_SERVER, + BROWSERSTACK_USERNAME, + BROWSERSTACK_ACCESS_KEY, + BROWSERSTACK_APP_URL, + BROWSERSTACK_DEVICE, +} = process.env; + +const defaults = { + E2E_DEVICE: null, + E2E_SERVER: null, +}; + +// Alert to fill the necessary environment variables +Object.keys(defaults).forEach(key => { + if (!process.env[key]) { + throw new Error( + `Please enter a custom ${key} in .env on the root directory` + ); + } +}); + +export default { + E2E_DEVICE, + E2E_SERVER, + BROWSERSTACK_USERNAME, + BROWSERSTACK_ACCESS_KEY, + BROWSERSTACK_APP_URL, + BROWSERSTACK_DEVICE, +}; diff --git a/e2e/driver.js b/e2e/driver.js new file mode 100644 index 0000000..faf41d2 --- /dev/null +++ b/e2e/driver.js @@ -0,0 +1,7 @@ +import wd from 'wd'; + +import server from './server'; + +const driver = wd.promiseChainRemote(server.url, server.port); + +export default driver; diff --git a/e2e/server.js b/e2e/server.js new file mode 100644 index 0000000..a7d2403 --- /dev/null +++ b/e2e/server.js @@ -0,0 +1,16 @@ +import config from './config'; + +const URL = { + local: 'localhost', + remote: 'hub-cloud.browserstack.com', +}; + +const PORT = { + local: 4444, + remote: 80, +}; + +export default { + url: URL[config.E2E_SERVER], + port: PORT[config.E2E_SERVER], +}; diff --git a/e2e/waitForElement.js b/e2e/waitForElement.js new file mode 100644 index 0000000..e5e8519 --- /dev/null +++ b/e2e/waitForElement.js @@ -0,0 +1,26 @@ +import { asserters } from 'wd'; + +import driver from './driver'; + +export default async function waitForElement(key, tag, timeout = 5000) { + function waitFor() { + if (tag) { + return driver.waitForElement( + tag, + key, + asserters.isDisplayed, + timeout, + 100 + ); + } + + return driver.waitForElementById(key, asserters.isDisplayed, timeout, 100); + } + + try { + const existingElement = await waitFor(); + return existingElement; + } catch { + return undefined; + } +} diff --git a/jest.e2e.json b/jest.e2e.json new file mode 100644 index 0000000..de5a77b --- /dev/null +++ b/jest.e2e.json @@ -0,0 +1,3 @@ +{ + "testPathIgnorePatterns": ["/node_modules/", "src/"] +} diff --git a/package.json b/package.json index d6a0a9e..506d74f 100644 --- a/package.json +++ b/package.json @@ -5,24 +5,51 @@ "dependencies": { "@material-ui/core": "^4.1.2", "@material-ui/icons": "^4.2.1", + "jest-dom": "^3.5.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-router-dom": "^5.0.1", - "react-scripts": "3.0.1" + "react-scripts": "3.0.1", + "react-testing-library": "^8.0.1" }, "devDependencies": { + "@testing-library/react": "^8.0.4", + "babel-polyfill": "^6.26.0", + "custom-env": "^1.0.2", "husky": ">=1", "lint-staged": ">=8", - "prettier": "1.17.1" + "prettier": "1.17.1", + "react-test-renderer": "^16.8.6", + "selenium-standalone": "^6.16.0", + "wd": "^1.11.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test" + "test": "react-scripts test", + "coverage": "npm test -- --coverage --watchAll", + "e2e": "jest --config jest.e2e.json", + "selenium": "selenium-standalone install && selenium-standalone start" }, "eslintConfig": { "extends": "react-app" }, + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!/node_modules/", + "!/src/serviceWorker.js", + "!/src/setupTests.js" + ], + "coverageThreshold": { + "global": { + "branches": 90, + "functions": 90, + "lines": 90, + "statements": 90 + } + } + }, "browserslist": { "production": [ ">0.2%", diff --git a/public/index.html b/public/index.html index 54758ee..5c2e40d 100644 --- a/public/index.html +++ b/public/index.html @@ -23,7 +23,7 @@ - diff --git a/screen.png b/screen.png new file mode 100644 index 0000000..ea5421e Binary files /dev/null and b/screen.png differ diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..3caf493 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X POST -d {} https://api.netlify.com/build_hooks/5d12f8b218c6e3ced89d110b \ No newline at end of file diff --git a/src/__tests__/App-test.js b/src/__tests__/App-test.js new file mode 100644 index 0000000..9b363a1 --- /dev/null +++ b/src/__tests__/App-test.js @@ -0,0 +1,10 @@ +import React from 'react'; +import App from '../App'; + +import renderer from 'react-test-renderer'; + +describe('', () => { + it('renders correctly', () => { + renderer.create(); + }); +}); diff --git a/src/__tests__/index-test.js b/src/__tests__/index-test.js new file mode 100644 index 0000000..e851f08 --- /dev/null +++ b/src/__tests__/index-test.js @@ -0,0 +1,7 @@ +jest.mock('react-dom', () => ({ + render: jest.fn(), +})); + +it('instances app without crashing', () => { + require('../index'); +}); diff --git a/src/api/__tests__/api-test.js b/src/api/__tests__/api-test.js new file mode 100644 index 0000000..e6600be --- /dev/null +++ b/src/api/__tests__/api-test.js @@ -0,0 +1,18 @@ +import api, { LIMIT } from '../index'; + +describe('api', () => { + it('initialices trivia', () => { + const triviaSource = api(); + expect(triviaSource).toBeDefined(); + + for (let i = 0; i < LIMIT; i++) { + const question = triviaSource.next(); + expect(question.value._id).toBeDefined(); + expect(question.done).toBe(false); + } + + const emptyQuestion = triviaSource.next(); + expect(emptyQuestion.value).toBeUndefined(); + expect(emptyQuestion.done).toBe(true); + }); +}); diff --git a/src/components/HomeScreen/HomeScreen.js b/src/components/HomeScreen/HomeScreen.js index 0fb18bb..d926905 100644 --- a/src/components/HomeScreen/HomeScreen.js +++ b/src/components/HomeScreen/HomeScreen.js @@ -6,14 +6,14 @@ import Page from '../Page'; import { TitleLogo } from './components'; import Styles from './HomeScreen.module.css'; -export default function HomeScreen({ navigation }) { +export default function HomeScreen() { return ( - +
- diff --git a/src/components/HomeScreen/__tests__/HomeScreen-test.js b/src/components/HomeScreen/__tests__/HomeScreen-test.js new file mode 100644 index 0000000..14e1600 --- /dev/null +++ b/src/components/HomeScreen/__tests__/HomeScreen-test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import HomeScreen from '../HomeScreen'; + +import renderer from 'react-test-renderer'; + +describe('', () => { + it('starts trivia', () => { + const component = renderer.create(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/components/HomeScreen/__tests__/__snapshots__/HomeScreen-test.js.snap b/src/components/HomeScreen/__tests__/__snapshots__/HomeScreen-test.js.snap new file mode 100644 index 0000000..e7d87eb --- /dev/null +++ b/src/components/HomeScreen/__tests__/__snapshots__/HomeScreen-test.js.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` starts trivia 1`] = ` +
+
+
+ App logo + + +
+
+
+`; diff --git a/src/components/TriviaResultScreen/App.test.js b/src/components/TriviaResultScreen/App.test.js deleted file mode 100644 index a754b20..0000000 --- a/src/components/TriviaResultScreen/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/src/components/TriviaResultScreen/TriviaResultScreen.js b/src/components/TriviaResultScreen/TriviaResultScreen.js index 1b950e0..e0c6d9d 100644 --- a/src/components/TriviaResultScreen/TriviaResultScreen.js +++ b/src/components/TriviaResultScreen/TriviaResultScreen.js @@ -11,7 +11,7 @@ export default function TriviaResultScreen({ history, location }) { }; return ( - +
{location.state.isWinner @@ -19,7 +19,12 @@ export default function TriviaResultScreen({ history, location }) { : 'Eres tonto como una piedra y feo como una blasfemia'} -
diff --git a/src/components/TriviaResultScreen/__tests__/TriviaResultScreen-test.js b/src/components/TriviaResultScreen/__tests__/TriviaResultScreen-test.js new file mode 100644 index 0000000..2ff3c56 --- /dev/null +++ b/src/components/TriviaResultScreen/__tests__/TriviaResultScreen-test.js @@ -0,0 +1,61 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import renderer from 'react-test-renderer'; + +import TriviaResultScreen from '../TriviaResultScreen'; + +describe('', () => { + it('display as winner', () => { + const location = { + state: { + isWinner: true, + }, + }; + + const component = renderer.create( + + ); + + const textTree = component.root.findByType(Typography); + expect(textTree.props.children).toBe('Soy intelectual muy inteligente!'); + }); + + it('display as looser', () => { + const location = { + state: { + isWinner: false, + }, + }; + + const component = renderer.create( + + ); + + const textTree = component.root.findByType(Typography); + expect(textTree.props.children).toBe( + 'Eres tonto como una piedra y feo como una blasfemia' + ); + }); + + it('restarts the trivia', () => { + const history = { + push: jest.fn(), + }; + + const location = { + state: { + isWinner: true, + }, + }; + + const component = renderer.create( + + ); + + const buttonTree = component.root.findByType(Button); + + buttonTree.props.onClick(); + expect(history.push).toBeCalledWith('/'); + }); +}); diff --git a/src/components/TriviaScreen/TriviaScreen.js b/src/components/TriviaScreen/TriviaScreen.js index 611a30a..d319413 100644 --- a/src/components/TriviaScreen/TriviaScreen.js +++ b/src/components/TriviaScreen/TriviaScreen.js @@ -43,7 +43,7 @@ export default function TriviaScreen({ history }) { }; return ( - + {currentQuestion.text} diff --git a/src/components/TriviaScreen/__tests__/TriviaScreen-test.js b/src/components/TriviaScreen/__tests__/TriviaScreen-test.js new file mode 100644 index 0000000..6ab0d3d --- /dev/null +++ b/src/components/TriviaScreen/__tests__/TriviaScreen-test.js @@ -0,0 +1,32 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import TriviaScreen from '../TriviaScreen'; +import { AnswerOptionList } from '../components'; +import { LIMIT } from '../../../api'; + +describe('', () => { + it('render list and answer questions', () => { + jest.useFakeTimers(); + + const history = { + push: jest.fn(), + }; + + let component; + + renderer.act(() => { + component = renderer.create(); + }); + + const answerOptionList = component.root.findByType(AnswerOptionList); + + for (let i = 0; i < LIMIT; i++) { + renderer.act(() => { + const option = answerOptionList.props.options[0]; + answerOptionList.props.onOptionPress(option); + jest.runAllTimers(); + }); + } + }); +}); diff --git a/src/components/TriviaScreen/components/AnswerOptionList/AnswerOptionsList.js b/src/components/TriviaScreen/components/AnswerOptionList/AnswerOptionsList.js index 9dd16d2..60ad24c 100644 --- a/src/components/TriviaScreen/components/AnswerOptionList/AnswerOptionsList.js +++ b/src/components/TriviaScreen/components/AnswerOptionList/AnswerOptionsList.js @@ -8,6 +8,7 @@ export default function AnswerOptionList({ options, onOptionPress }) { return options.map(option => (