1
0
mirror of https://github.com/kremalicious/blowfish.git synced 2024-12-28 23:57:52 +01:00

Merge pull request #44 from kremalicious/feature/tests

Add test setup and tests
This commit is contained in:
Matthias Kretschmann 2019-10-08 00:07:25 +02:00 committed by GitHub
commit 6797dd37f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 287 additions and 104 deletions

View File

@ -1,4 +1,14 @@
{ {
"presets": ["@babel/env", "@babel/react"], "presets": [
[
"@babel/env",
{
"targets": {
"node": "current"
}
}
],
"@babel/react"
],
"plugins": ["@babel/plugin-proposal-class-properties"] "plugins": ["@babel/plugin-proposal-class-properties"]
} }

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ yarn.lock
package-lock.json package-lock.json
build build
dist dist
coverage

View File

@ -1,4 +1,4 @@
osx_image: xcode10.2 osx_image: xcode11
os: osx os: osx
language: node_js language: node_js
node_js: node node_js: node
@ -15,8 +15,15 @@ cache:
- $HOME/.cache/electron-builder - $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds - $HOME/.npm/_prebuilds
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-darwin-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
script: script:
- npm test - npm test || travis_terminate 1
- ./cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.json src/renderer/coverage/lcov.info
- if [[ "$TRAVIS_TEST_RESULT" == 0 ]]; then ./cc-test-reporter upload-coverage; fi
- npm run dist - npm run dist
branches: branches:
@ -27,9 +34,9 @@ notifications:
email: false email: false
slack: slack:
template: template:
- "`%{repository_slug}#%{branch}`" - '`%{repository_slug}#%{branch}`'
- "*%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>" - '*%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>'
- "Execution time: *%{duration}*" - 'Execution time: *%{duration}*'
- "Message: %{message}" - 'Message: %{message}'
rooms: rooms:
- secure: r6kVJw3zS4raTXgeBEYZYO/5YawnLoi1vO4zG3obhcNFRLm9FxlzuXfulFhjQA4viPQUW07m5UGud4bPTrDIAE35GUcLRlyisH/odahgsrmqLrBvz9CB+/V5WrEsCGpE9G3I/y5JGSRavjs+5qfVqJZaAI9Ox7bCcw+Msa5r/p7/yJw5di4EzgNLFWQswyio0zeOdjtCYgqpngWtLGpn0ksSwqNyqp/kntoHSz4nDdO/6GWS1q5K9mOfGMXr/wwiYuQrgDPpygRWETy9F8qh9yH2cseJmCZaXvSTSU1L9yV01qrBP5zDTTM2jPUGMQKY4JBoxFtU29G1BLWGAgMW9ymKe9V+f8FgbirZ+O1Vp87QAZPJXx5kO+pgqBtGewoYfp0k9HJ5xQAhr83l82w8BAEHVS3G/Y7cKKK9QNH9Z6gpdx6Y3s9YkpGqkv79MRvZo0tJV+XTOldCCfUFVxXXuZofuswWGUgt2h9qNoFY+AZc0G1TV/XVDHbDm32JNiGkuk+uO83HT9VI7G5PRWNcD8kP7ZS6XThiU2qOGr4OPGggmpFpJ7Yqc3LNFOjhFunKSzGOrZrc0GLZAbAR7qHkWNpiqQQ/RSpfnXfbPlAIJY6w5Vuzh9KhIIPkbWdP89Bc2Kw+W+ACFjStO7s298/8dty44EvJ2TS9CCjOhYtgaxk= - secure: r6kVJw3zS4raTXgeBEYZYO/5YawnLoi1vO4zG3obhcNFRLm9FxlzuXfulFhjQA4viPQUW07m5UGud4bPTrDIAE35GUcLRlyisH/odahgsrmqLrBvz9CB+/V5WrEsCGpE9G3I/y5JGSRavjs+5qfVqJZaAI9Ox7bCcw+Msa5r/p7/yJw5di4EzgNLFWQswyio0zeOdjtCYgqpngWtLGpn0ksSwqNyqp/kntoHSz4nDdO/6GWS1q5K9mOfGMXr/wwiYuQrgDPpygRWETy9F8qh9yH2cseJmCZaXvSTSU1L9yV01qrBP5zDTTM2jPUGMQKY4JBoxFtU29G1BLWGAgMW9ymKe9V+f8FgbirZ+O1Vp87QAZPJXx5kO+pgqBtGewoYfp0k9HJ5xQAhr83l82w8BAEHVS3G/Y7cKKK9QNH9Z6gpdx6Y3s9YkpGqkv79MRvZo0tJV+XTOldCCfUFVxXXuZofuswWGUgt2h9qNoFY+AZc0G1TV/XVDHbDm32JNiGkuk+uO83HT9VI7G5PRWNcD8kP7ZS6XThiU2qOGr4OPGggmpFpJ7Yqc3LNFOjhFunKSzGOrZrc0GLZAbAR7qHkWNpiqQQ/RSpfnXfbPlAIJY6w5Vuzh9KhIIPkbWdP89Bc2Kw+W+ACFjStO7s298/8dty44EvJ2TS9CCjOhYtgaxk=

View File

@ -7,6 +7,7 @@
<p align="center"> <p align="center">
<a href="https://travis-ci.com/kremalicious/blowfish"><img src="https://travis-ci.com/kremalicious/blowfish.svg?branch=master" /></a> <a href="https://travis-ci.com/kremalicious/blowfish"><img src="https://travis-ci.com/kremalicious/blowfish.svg?branch=master" /></a>
<a href="https://codeclimate.com/github/kremalicious/blowfish/maintainability"><img src="https://api.codeclimate.com/v1/badges/beeab7902ee5307fc0a1/maintainability" /></a> <a href="https://codeclimate.com/github/kremalicious/blowfish/maintainability"><img src="https://api.codeclimate.com/v1/badges/beeab7902ee5307fc0a1/maintainability" /></a>
<a href="https://codeclimate.com/github/kremalicious/blowfish/test_coverage"><img src="https://api.codeclimate.com/v1/badges/beeab7902ee5307fc0a1/test_coverage" /></a>
<a href="https://greenkeeper.io/"><img src="https://badges.greenkeeper.io/kremalicious/blowfish.svg" /></a> <a href="https://greenkeeper.io/"><img src="https://badges.greenkeeper.io/kremalicious/blowfish.svg" /></a>
</p> </p>

View File

@ -5,7 +5,9 @@
"description": "🐡 Simple Electron-based desktop app to retrieve and display your total Ocean Token balances.", "description": "🐡 Simple Electron-based desktop app to retrieve and display your total Ocean Token balances.",
"main": "./src/main/index.js", "main": "./src/main/index.js",
"scripts": { "scripts": {
"test": "eslint --ignore-path .gitignore ./src/**/*.{js,jsx} && stylelint --ignore-path .gitignore ./src/**/*.{css,scss}", "test": "npm run lint && jest --coverage",
"test:watch": "jest --coverage --watch",
"lint": "eslint --ignore-path .gitignore ./src/**/*.{js,jsx} && stylelint --ignore-path .gitignore ./src/**/*.{css,scss}",
"start": "webpack-dev-server --hot --host 0.0.0.0 --config=./webpack.dev.config.js", "start": "webpack-dev-server --hot --host 0.0.0.0 --config=./webpack.dev.config.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.common.config.js", "build": "cross-env NODE_ENV=production webpack --config webpack.common.config.js",
"package": "electron-builder build -mwl -p never && open ./dist", "package": "electron-builder build -mwl -p never && open ./dist",
@ -24,47 +26,54 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@coingecko/cryptoformat": "^0.3.2", "@coingecko/cryptoformat": "^0.3.3",
"ethereum-address": "0.0.4", "ethereum-address": "0.0.4",
"ethereum-blockies": "github:MyEtherWallet/blockies", "ethereum-blockies": "github:MyEtherWallet/blockies",
"ms": "^2.1.2" "ms": "^2.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.6.0", "@babel/core": "^7.6.2",
"@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/preset-env": "^7.6.0", "@babel/preset-env": "^7.6.2",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@jest-runner/electron": "^2.0.2",
"@reach/router": "^1.2.1", "@reach/router": "^1.2.1",
"@svgr/webpack": "^4.3.2", "@react-mock/state": "^0.1.8",
"@svgr/webpack": "^4.3.3",
"@testing-library/jest-dom": "^4.1.0",
"@testing-library/react": "^9.3.0",
"auto-changelog": "^1.16.1", "auto-changelog": "^1.16.1",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.0.4", "copy-webpack-plugin": "^5.0.4",
"cross-env": "^6.0.0", "cross-env": "^6.0.3",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",
"electron": "^6.0.7", "electron": "^6.0.11",
"electron-builder": "^21.2.0", "electron-builder": "^21.2.0",
"electron-devtools-installer": "^2.2.4", "electron-devtools-installer": "^2.2.4",
"electron-store": "^5.0.0", "electron-store": "^5.0.0",
"eslint": "^6.3.0", "eslint": "^6.5.1",
"eslint-config-prettier": "^6.2.0", "eslint-config-prettier": "^6.4.0",
"eslint-plugin-react": "^7.14.3", "eslint-plugin-react": "^7.16.0",
"file-loader": "^4.2.0", "file-loader": "^4.2.0",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.9.0",
"mini-css-extract-plugin": "^0.8.0", "mini-css-extract-plugin": "^0.8.0",
"prettier": "^1.18.2", "prettier": "^1.18.2",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"react": "^16.9.0", "react": "^16.10.2",
"react-dom": "^16.9.0", "react-dom": "^16.10.2",
"react-pose": "^4.0.8", "react-pose": "^4.0.8",
"release-it": "^12.3.6", "release-it": "^12.4.2",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"stylelint": "^11.0.0", "stylelint": "^11.0.0",
"stylelint-config-css-modules": "^1.4.0", "stylelint-config-css-modules": "^1.5.0",
"stylelint-config-standard": "^19.0.0", "stylelint-config-standard": "^19.0.0",
"webpack": "^4.39.3", "webpack": "^4.41.0",
"webpack-cli": "^3.3.8", "webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.0" "webpack-dev-server": "^3.8.2"
}, },
"browserslist": "electron >= 6.0", "browserslist": "electron >= 6.0",
"build": { "build": {
@ -115,5 +124,22 @@
"npm": { "npm": {
"publish": false "publish": false
} }
},
"jest": {
"rootDir": "src/renderer",
"transform": {
"^.+\\.jsx?$": "babel-jest"
},
"moduleNameMapper": {
".+\\.(css|styl|less|sass|scss)$": "identity-obj-proxy",
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/jest/__mocks__/file-mock.js",
"\\.svg": "<rootDir>/jest/__mocks__/svgr-mock.js"
},
"testURL": "http://localhost",
"setupFilesAfterEnv": [
"<rootDir>/jest/setup-test-env.js"
],
"runner": "@jest-runner/electron",
"testEnvironment": "@jest-runner/electron/environment"
} }
} }

View File

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react' import React, { useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { import {
Router, Router,
@ -44,24 +44,22 @@ PosedRouter.propTypes = {
children: PropTypes.any.isRequired children: PropTypes.any.isRequired
} }
export default class App extends PureComponent { export default function App() {
componentDidMount() { useEffect(() => {
ipcRenderer.on('goTo', (evt, route) => { ipcRenderer.on('goTo', (evt, route) => {
navigate(route) navigate(route)
}) })
} }, [])
render() { return (
return ( <>
<> {process.platform === 'darwin' && <Titlebar />}
{process.platform === 'darwin' && <Titlebar />} <div className={styles.app}>
<div className={styles.app}> <PosedRouter>
<PosedRouter> <Home path="/" default />
<Home path="/" default /> <Preferences path="/preferences" />
<Preferences path="/preferences" /> </PosedRouter>
</PosedRouter> </div>
</div> </>
</> )
)
}
} }

15
src/renderer/App.test.jsx Normal file
View File

@ -0,0 +1,15 @@
import React from 'react'
import { render } from '@testing-library/react'
import AppProvider from './store/AppProvider'
import App from './App'
describe('App', () => {
it('renders correctly', () => {
const { container } = render(
<AppProvider>
<App />
</AppProvider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -22,15 +22,3 @@ export const fadeIn = {
transition: { duration: 100 } transition: { duration: 100 }
} }
} }
export const characterAnimation = {
exit: { opacity: 0, y: 10 },
enter: {
opacity: 1,
y: 0,
transition: ({ charInWordIndex }) => ({
type: 'spring',
delay: charInWordIndex * 20
})
}
}

View File

@ -0,0 +1,34 @@
const prices = new Map()
prices.set('ocean', 1)
prices.set('eur', 1)
prices.set('usd', 1)
prices.set('btc', 1)
prices.set('eth', 1)
const priceChanges = {
eur: -14.051056318029353,
usd: -14.051056318029268,
btc: -14.761248039421442,
eth: -17.538786176215627
}
export default {
accentColor: '#0a5fff',
accounts: [
{
address: '0xxxxxxxxxxxxxxxxxxx',
balance: {
ocean: 10000,
btc: 1.9,
eth: 10000,
eur: 10000,
usd: 10000
}
}
],
currency: 'ocean',
isLoading: false,
needsConfig: false,
prices,
priceChanges
}

View File

@ -0,0 +1 @@
module.exports = 'div'

View File

@ -0,0 +1 @@
module.exports = 'svg-mock'

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect'

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Link } from '@reach/router' import { Link } from '@reach/router'
import { AppContext } from '../store/createContext' import { AppContext } from '../../store/createContext'
import IconRocket from '../images/rocket.svg' import IconRocket from '../../images/rocket.svg'
import styles from './Welcome.module.css' import styles from './Welcome.module.css'
const Welcome = () => { const Welcome = () => {

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Link } from '@reach/router' import { Link } from '@reach/router'
import { AppContext } from '../../store/createContext' import { AppContext } from '../../store/createContext'
import Welcome from '../../components/Welcome' import Welcome from './Welcome'
import Spinner from '../../components/Spinner' import Spinner from '../../components/Spinner'
import Divider from '../../components/Divider' import Divider from '../../components/Divider'
import Total from './Total' import Total from './Total'
@ -10,7 +10,7 @@ import Accounts from './Accounts'
import IconCog from '../../images/cog.svg' import IconCog from '../../images/cog.svg'
import styles from './index.module.css' import styles from './index.module.css'
const Home = () => { export default function Home() {
const { isLoading, needsConfig } = useContext(AppContext) const { isLoading, needsConfig } = useContext(AppContext)
return ( return (
@ -37,5 +37,3 @@ const Home = () => {
</> </>
) )
} }
export default Home

View File

@ -0,0 +1,28 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { AppContext } from '../../store/createContext'
import context from '../../jest/__fixtures__/context'
import Home from '.'
describe('Home', () => {
it('renders correctly', () => {
const { container, getByText } = render(
<AppContext.Provider value={context}>
<Home />
</AppContext.Provider>
)
expect(container.firstChild).toBeInTheDocument()
fireEvent.click(getByText(/Ξ/))
})
it('renders Welcome without config', () => {
const { container } = render(
<AppContext.Provider value={{ ...context, needsConfig: true }}>
<Home />
</AppContext.Provider>
)
expect(container.firstChild).toHaveTextContent(
'Add your first address to get started.'
)
})
})

View File

@ -2,25 +2,32 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styles from './New.module.css' import styles from './New.module.css'
const New = ({ input, handleInputChange, handleSave, accentColor }) => ( export default function New({
<li> input,
<input handleInputChange,
type="text" handleSave,
placeholder="0xxxxxxxx" accentColor
value={input} }) {
onChange={e => handleInputChange(e)} return (
className={styles.input} <li>
/> <input
type="text"
placeholder="0xxxxxxxx"
value={input}
onChange={e => handleInputChange(e)}
className={styles.input}
/>
<button <button
className={styles.button} className={styles.button}
onClick={e => handleSave(e)} onClick={e => handleSave(e)}
style={{ color: accentColor }} style={{ color: accentColor }}
> >
Add Add
</button> </button>
</li> </li>
) )
}
New.propTypes = { New.propTypes = {
input: PropTypes.string.isRequired, input: PropTypes.string.isRequired,
@ -28,5 +35,3 @@ New.propTypes = {
handleSave: PropTypes.func.isRequired, handleSave: PropTypes.func.isRequired,
accentColor: PropTypes.string.isRequired accentColor: PropTypes.string.isRequired
} }
export default New

View File

@ -5,36 +5,40 @@ import posed, { PoseGroup } from 'react-pose'
import { fadeIn } from '../../../components/Animations' import { fadeIn } from '../../../components/Animations'
import styles from './Saved.module.css' import styles from './Saved.module.css'
const Item = posed.li(fadeIn) export default function Saved({ accounts, handleDelete }) {
const Item = posed.li(fadeIn)
const Saved = ({ accounts, handleDelete }) => ( return (
<PoseGroup> <PoseGroup>
{accounts.map(account => { {accounts.map(account => {
const identicon = account && toDataUrl(account) const identicon = toDataUrl(account)
return ( return (
<Item key={account}> <Item key={account}>
<div> <div>
<img className={styles.identicon} src={identicon} alt="Blockies" /> <img
{account} className={styles.identicon}
</div> src={identicon}
alt="Blockies"
/>
{account}
</div>
<button <button
className={styles.delete} className={styles.delete}
onClick={e => handleDelete(e, account)} onClick={e => handleDelete(e, account)}
title="Remove account" title="Remove account"
> >
&times; &times;
</button> </button>
</Item> </Item>
) )
})} })}
</PoseGroup> </PoseGroup>
) )
}
Saved.propTypes = { Saved.propTypes = {
accounts: PropTypes.array.isRequired, accounts: PropTypes.array.isRequired,
handleDelete: PropTypes.func.isRequired handleDelete: PropTypes.func.isRequired
} }
export default Saved

View File

@ -0,0 +1,50 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { AppContext } from '../../../store/createContext'
import { StateMock } from '@react-mock/state'
import Accounts from '.'
describe('Accounts', () => {
const ui = (
<AppContext.Provider value={{ accentColor: '#0a5fff' }}>
<StateMock state={{ accounts: ['xxx'], input: '', error: '' }}>
<Accounts />
</StateMock>
</AppContext.Provider>
)
it('renders correctly', () => {
const { container } = render(ui)
expect(container.firstChild).toBeInTheDocument()
})
it('Address can be removed', () => {
const { getByTitle } = render(ui)
fireEvent.click(getByTitle('Remove account'))
})
it('New address can be added', () => {
const { getByPlaceholderText, getByText } = render(ui)
// error: empty
fireEvent.click(getByText('Add'))
// success
fireEvent.change(getByPlaceholderText('0xxxxxxxx'), {
target: { value: '0x139361162Fb034fa021d347848F0B1c593D1f53C' }
})
fireEvent.click(getByText('Add'))
// error: duplicate
fireEvent.change(getByPlaceholderText('0xxxxxxxx'), {
target: { value: '0x139361162Fb034fa021d347848F0B1c593D1f53C' }
})
fireEvent.click(getByText('Add'))
// error: not an ETH address
fireEvent.change(getByPlaceholderText('0xxxxxxxx'), {
target: { value: '0x000' }
})
fireEvent.click(getByText('Add'))
})
})

View File

@ -0,0 +1,15 @@
import React from 'react'
import { render } from '@testing-library/react'
import { AppContext } from '../../store/createContext'
import Preferences from '.'
describe('Preferences', () => {
it('renders correctly', () => {
const { container } = render(
<AppContext.Provider value={{ accentColor: '#0a5fff' }}>
<Preferences />
</AppContext.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})