diff --git a/.eslintrc.js b/.eslintrc.js index aed0f44..d25e44d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,10 +4,12 @@ module.exports = { 'node': true, 'es6': true, 'browser': true, + 'jest/globals': true, }, 'plugins': [ 'react', 'react-hooks', + 'jest', ], 'parser': 'babel-eslint', 'extends': [ diff --git a/jsconfig.json b/jsconfig.json index 2671c82..0189221 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -7,7 +7,8 @@ ], "paths": { "assets": ["./assets/*"], - "components": ["./components/*"] - "modules": ["./modules/*"] + "components": ["./components/*"], + "modules": ["./modules/*"], + "utils": ["./utils/*"] } } diff --git a/package.json b/package.json index a869a6c..e7985be 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@testing-library/user-event": "^12.6.3", "babel-eslint": "^10.1.0", "eslint": "^7.21.0", + "eslint-plugin-jest": "^24.2.1", "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0" } diff --git a/src/components/Game/Game.test.js b/src/components/Game/Game.test.js new file mode 100644 index 0000000..2e0ce3e --- /dev/null +++ b/src/components/Game/Game.test.js @@ -0,0 +1,40 @@ +import React from 'react' +import { Game } from './Game' +import { Provider } from 'react-redux' +import { MemoryRouter as Router } from 'react-router-dom' +import { render, screen, waitFor } from '@testing-library/react' +import configureStore from '../../configureStore' + +describe('Game', () => { + it('it renders correctly', async () => { + const initialState = { + game: { + lines: 6, + linesPerMillisecond: 2, + skills: {} + } + } + + render( + + + + + + ) + + expect(screen.getByText(/6 lines/)).toBeInTheDocument() + expect(screen.getByText(/per second: 20/)).toBeInTheDocument() + expect(screen.getByText(/Skills/)).toBeInTheDocument() + expect(screen.getByText(/Store/)).toBeInTheDocument() + + await waitFor( + () => expect(screen.getByText(/8 lines/)).toBeInTheDocument(), + { timeout: 150 } + ) + await waitFor( + () => expect(screen.getByText(/10 lines/)).toBeInTheDocument(), + { timeout: 150 } + ) + }) +}) diff --git a/src/components/Gitcoin/Gitcoin.js b/src/components/Gitcoin/Gitcoin.js index dfb2741..da6d851 100644 --- a/src/components/Gitcoin/Gitcoin.js +++ b/src/components/Gitcoin/Gitcoin.js @@ -13,6 +13,7 @@ export const Gitcoin = () => { diff --git a/src/components/Gitcoin/Gitcoin.test.js b/src/components/Gitcoin/Gitcoin.test.js new file mode 100644 index 0000000..8c93861 --- /dev/null +++ b/src/components/Gitcoin/Gitcoin.test.js @@ -0,0 +1,36 @@ +import React from 'react' +import { Gitcoin } from './Gitcoin' +import { render, screen, fireEvent } from '@testing-library/react' +import { click } from 'modules/game' +import configureStore from '../../configureStore' + +jest.mock('react-redux', () => { + const dispatch = jest.fn() + + return { + ...jest.requireActual('react-redux'), + useDispatch: () => dispatch + } +}) +import { Provider, useDispatch } from 'react-redux' + +describe('Gitcoin', () => { + it('Allows to click', () => { + const initialState = { + game: { lines: 6, linesPerMillisecond: 2 } + } + + render( + + + + ) + + expect(screen.getByAltText(/Gitcoin/i)).toBeInTheDocument() + + const dispatch = useDispatch() + + fireEvent.click(screen.getByLabelText(/Gitcoin/)) + expect(dispatch).toHaveBeenCalledWith(click()) + }) +}) diff --git a/src/components/Home.test.js b/src/components/Home.test.js new file mode 100644 index 0000000..486fb07 --- /dev/null +++ b/src/components/Home.test.js @@ -0,0 +1,17 @@ +import React from 'react' +import { Home } from './Home' +import { MemoryRouter as Router } from 'react-router-dom' +import { render, screen } from '@testing-library/react' + +describe('Home', () => { + it('renders correctly', () => { + render( + + + + ) + + expect(screen.getByText(/Dogs have boundless enthusiasm/i)).toBeInTheDocument() + expect(screen.getByText(/Play/i)).toBeInTheDocument() + }) +}) diff --git a/src/components/Score.js b/src/components/Score.js index 4f0afc0..89aa066 100644 --- a/src/components/Score.js +++ b/src/components/Score.js @@ -1,9 +1,10 @@ import React from 'react' import { useSelector } from 'react-redux' +import numberFormat from 'utils/numberFormat' export const Score = () => { - const lines = useSelector(state => parseInt(state.game.lines)) - const linesPerSecond = useSelector(state => parseInt(state.game.linesPerMillisecond * 10)) + const lines = useSelector(state => numberFormat(state.game.lines)) + const linesPerSecond = useSelector(state => numberFormat(state.game.linesPerMillisecond * 10)) return ( <> diff --git a/src/components/Score.test.js b/src/components/Score.test.js new file mode 100644 index 0000000..2780a42 --- /dev/null +++ b/src/components/Score.test.js @@ -0,0 +1,22 @@ +import React from 'react' +import { Score } from './Score' +import { Provider } from 'react-redux' +import { render, screen } from '@testing-library/react' +import configureStore from '../configureStore' + +describe('Score', () => { + it('should displays the number of lines', () => { + const initialState = { + game: { lines: 6, linesPerMillisecond: 2 } + } + + render( + + + + ) + + expect(screen.getByText(/6 lines/i)).toBeInTheDocument() + expect(screen.getByText(/per second: 20/i)).toBeInTheDocument() + }) +}) diff --git a/src/components/Skills/Section.test.js b/src/components/Skills/Section.test.js new file mode 100644 index 0000000..13ca42a --- /dev/null +++ b/src/components/Skills/Section.test.js @@ -0,0 +1,13 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { Section } from './Section' + +describe('Section', () => { + it('Displays the owned skills', () => { + render(
) + + expect(screen.getByText('Bash')).toBeInTheDocument() + expect(screen.getAllByAltText('Bash')).toHaveLength(3) + }) +}) + diff --git a/src/components/Skills/Skills.test.js b/src/components/Skills/Skills.test.js new file mode 100644 index 0000000..64385c9 --- /dev/null +++ b/src/components/Skills/Skills.test.js @@ -0,0 +1,27 @@ +import React from 'react' +import { Provider } from 'react-redux' +import { render, screen } from '@testing-library/react' +import { Skills } from './Skills' +import configureStore from '../../configureStore' + + +describe('Store', () => { + it('Renders correctly', () => { + const initialState = { + game: { + skills: { 'Bash': 2, 'Git': 3, 'Javascript': 4 } + } + } + + render( + + + + ) + + expect(screen.getByText(/Bash/i)).toBeInTheDocument() + expect(screen.getByText(/Git/i)).toBeInTheDocument() + expect(screen.getByText(/Javascript/i)).toBeInTheDocument() + }) +}) + diff --git a/src/components/Store/Item.js b/src/components/Store/Item.js index 66300f6..d7fa7a9 100644 --- a/src/components/Store/Item.js +++ b/src/components/Store/Item.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import Button from '@material-ui/core/Button' import Typography from '@material-ui/core/Typography' +import numberFormat from 'utils/numberFormat' import './Item.css' export const Item = ({ item, lines, onBuy }) => { @@ -20,15 +21,16 @@ export const Item = ({ item, lines, onBuy }) => { {item.name}
{item.name} - {linePerSecond} lines per second + {numberFormat(linePerSecond)} lines per second
) diff --git a/src/components/Store/Item.test.js b/src/components/Store/Item.test.js new file mode 100644 index 0000000..643115a --- /dev/null +++ b/src/components/Store/Item.test.js @@ -0,0 +1,61 @@ +import React from 'react' +import BashIcon from 'devicon/icons/bash/bash-original.svg' +import { render, screen, fireEvent } from '@testing-library/react' +import { Item } from './Item' + +describe('Item', () => { + it('Renders a buyable item', () => { + const item = { + name: 'Bash', + price: 10, + multiplier: 0.1, + icon: BashIcon + } + + const onBuy = jest.fn() + + render( + + ) + + expect(screen.getByText(/Bash/i)).toBeInTheDocument() + expect(screen.getByText(/1 lines per second/i)).toBeInTheDocument() + expect(screen.getByRole('button')).not.toBeDisabled() + + fireEvent.click(screen.getByRole('button')) + + expect(onBuy).toHaveBeenCalledWith(item) + }) + + it('Renders a non buyable item', () => { + const item = { + name: 'Bash', + price: 10, + multiplier: 0.1, + icon: BashIcon + } + + const onBuy = jest.fn() + + render( + + ) + + expect(screen.getByText(/Bash/i)).toBeInTheDocument() + expect(screen.getByText(/1 lines per second/i)).toBeInTheDocument() + expect(screen.getByRole(/button/i)).toBeDisabled() + + fireEvent.click(screen.getByRole('button')) + + expect(onBuy).not.toHaveBeenCalledWith(item) + }) +}) + diff --git a/src/components/Store/Store.test.js b/src/components/Store/Store.test.js new file mode 100644 index 0000000..5e04c6f --- /dev/null +++ b/src/components/Store/Store.test.js @@ -0,0 +1,26 @@ +import React from 'react' +import { Provider } from 'react-redux' +import { render, screen } from '@testing-library/react' +import { Store } from './Store' +import configureStore from '../../configureStore' + + +describe('Store', () => { + it('Renders correctly', () => { + const initialState = { + game: { lines: 6 } + } + + render( + + + + ) + + expect(screen.getByText(/Bash/i)).toBeInTheDocument() + expect(screen.getByText(/Git/i)).toBeInTheDocument() + expect(screen.getByText(/Javascript/i)).toBeInTheDocument() + expect(screen.getByText(/React/i)).toBeInTheDocument() + expect(screen.getByText(/Vim/i)).toBeInTheDocument() + }) +}) diff --git a/src/configureStore.js b/src/configureStore.js index 4982f93..fcb60fb 100644 --- a/src/configureStore.js +++ b/src/configureStore.js @@ -1,8 +1,8 @@ import { createStore } from 'redux' import { rootReducer } from './modules' -export default () => { - const store = createStore(rootReducer) +export default (initialState = {}) => { + const store = createStore(rootReducer, initialState) return store } diff --git a/src/modules/game.js b/src/modules/game.js index 523842c..eea6a79 100644 --- a/src/modules/game.js +++ b/src/modules/game.js @@ -1,7 +1,7 @@ // Actions -const CLICK = 'game::CLICK' -const BUY_ITEM = 'game::BUY_ITEM' -const LOOP = 'game::LOOP' +export const CLICK = 'game::CLICK' +export const BUY_ITEM = 'game::BUY_ITEM' +export const LOOP = 'game::LOOP' // Action creators export const click = () => ({ @@ -20,7 +20,7 @@ export const loop = () => ({ const INITIAL_STATE = { lines: 0, linesPerMillisecond: 0, - skills: [] + skills: {} } export const reducer = (state = INITIAL_STATE, action) => { diff --git a/src/modules/game.test.js b/src/modules/game.test.js new file mode 100644 index 0000000..4e984fd --- /dev/null +++ b/src/modules/game.test.js @@ -0,0 +1,159 @@ +import { + CLICK, + BUY_ITEM, + LOOP, + click, + buyItem, + loop, + reducer +} from './game' + +describe('Actions creators', () => { + it('should create a click action', () => { + const expectedAction = { type: CLICK } + + expect(click()).toEqual(expectedAction) + }) + + it('should create a buy item action', () => { + const item = { name: 'Bash' } + const expectedAction = { type: BUY_ITEM, item } + + expect(buyItem(item)).toEqual(expectedAction) + }) + + it('should create a loop action', () => { + const expectedAction = { type: LOOP } + + expect(loop()).toEqual(expectedAction) + }) +}) + +describe('Reducer', () => { + it(LOOP, () => { + const state = { + lines: 6, + linesPerMillisecond: 6 + } + const action = loop() + + const exectedState = { + lines: 12, + linesPerMillisecond: 6 + } + + expect(reducer(state, action)).toEqual(exectedState) + }) + + it(CLICK, () => { + const state = { + lines: 6, + linesPerMillisecond: 6 + } + const action = click() + + const exectedState = { + lines: 7, + linesPerMillisecond: 6 + } + + expect(reducer(state, action)).toEqual(exectedState) + }) + + it(`${BUY_ITEM} with no existing skill`, () => { + const item = { + name: 'Bash', + price: 10, + multiplier: 0.5, + icon: '/some/icon/path.svg' + } + const action = buyItem(item) + + const state = { + lines: 25, + linesPerMillisecond: 6, + skills: {} + } + + const exectedState = { + lines: 15, + linesPerMillisecond: 6.5, + skills: { + [item.name]: 1 + } + } + + expect(reducer(state, action)).toEqual(exectedState) + }) + + it(`${BUY_ITEM} with same existing skills`, () => { + const item = { + name: 'Bash', + price: 10, + multiplier: 0.5, + icon: '/some/icon/path.svg' + } + const action = buyItem(item) + + const state = { + lines: 25, + linesPerMillisecond: 6, + skills: { + [item.name]: 4 + } + } + + const exectedState = { + lines: 15, + linesPerMillisecond: 6.5, + skills: { + [item.name]: 5 + } + } + + expect(reducer(state, action)).toEqual(exectedState) + }) + + it(`${BUY_ITEM} with multiple existing skills`, () => { + const item = { + name: 'Bash', + price: 10, + multiplier: 0.5, + icon: '/some/icon/path.svg' + } + const action = buyItem(item) + + const state = { + lines: 25, + linesPerMillisecond: 6, + skills: { + [item.name]: 4, + 'Javascript': 2, + 'Vim': 1 + } + } + + const exectedState = { + lines: 15, + linesPerMillisecond: 6.5, + skills: { + [item.name]: 5, + 'Javascript': 2, + 'Vim': 1 + } + } + + expect(reducer(state, action)).toEqual(exectedState) + }) + + it('Unknown action', () => { + const state = { + lines: 6, + linesPerMillisecond: 6 + } + + const action = { type: 'UNKNOWN ACTION' } + + expect(reducer(state, action)).toEqual(state) + }) +}) diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 0000000..c44951a --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom' diff --git a/src/utils/numberFormat.js b/src/utils/numberFormat.js new file mode 100644 index 0000000..0c2c3d0 --- /dev/null +++ b/src/utils/numberFormat.js @@ -0,0 +1,15 @@ +function numberWithDot(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.') +} + +function numberFormat(x) { + const number = parseInt(x) + + if (number < 1000000) return numberWithDot(number) + + const numberOfMillion = Math.floor(number / 1000000 * 1000) / 1000 + + return `${numberWithDot(numberOfMillion)} millions` +} + +export default numberFormat diff --git a/src/utils/numberFormat.test.js b/src/utils/numberFormat.test.js new file mode 100644 index 0000000..0982b04 --- /dev/null +++ b/src/utils/numberFormat.test.js @@ -0,0 +1,18 @@ +import numberFormat from './numberFormat' + +describe('numberFormat', () => { + it('format numbers bellow 1.000', () => { + const number = 234 + expect(numberFormat(number)).toBe('234') + }) + + it('format numbers bellow 1.000.000', () => { + const number = 123234 + expect(numberFormat(number)).toBe('123.234') + }) + + it('format numbers above 1.000.000', () => { + const number = 12123234 + expect(numberFormat(number)).toBe('12.123 millions') + }) +}) diff --git a/yarn.lock b/yarn.lock index 52b8105..2fb2ecd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4939,6 +4939,13 @@ eslint-plugin-jest@^24.1.0: dependencies: "@typescript-eslint/experimental-utils" "^4.0.1" +eslint-plugin-jest@^24.2.1: + version "24.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.2.1.tgz#7e84f16a3ca6589b86be9732a93d71367a4ed627" + integrity sha512-s24ve8WUu3DLVidvlSzaqlOpTZre9lTkZTAO+a7X0WMtj8HraWTiTEkW3pbDT1xVxqEHMWSv+Kx7MyqR50nhBw== + dependencies: + "@typescript-eslint/experimental-utils" "^4.0.1" + eslint-plugin-jsx-a11y@^6.3.1: version "6.4.1" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"