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}
- {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"