diff --git a/.babelrc b/.babelrc index a3d157c..d5a26dc 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,14 @@ { - "presets": ["@babel/env", "@babel/react"], + "presets": [ + [ + "@babel/env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/react" + ], "plugins": ["@babel/plugin-proposal-class-properties"] } diff --git a/.gitignore b/.gitignore index 03f0caf..0a06796 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ yarn.lock package-lock.json build dist +coverage diff --git a/.travis.yml b/.travis.yml index 4936717..3095778 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -osx_image: xcode10.2 +osx_image: xcode11 os: osx language: node_js node_js: node @@ -15,8 +15,15 @@ cache: - $HOME/.cache/electron-builder - $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: - - 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 branches: @@ -27,9 +34,9 @@ notifications: email: false slack: template: - - "`%{repository_slug}#%{branch}`" - - "*%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>" - - "Execution time: *%{duration}*" - - "Message: %{message}" + - '`%{repository_slug}#%{branch}`' + - '*%{result}* build (<%{build_url}|#%{build_number}>) for <%{compare_url}|%{commit}>' + - 'Execution time: *%{duration}*' + - 'Message: %{message}' 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= diff --git a/README.md b/README.md index a4352bd..5c85f16 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@

+

diff --git a/package.json b/package.json index d6ecb81..fc1dc43 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "description": "🐡 Simple Electron-based desktop app to retrieve and display your total Ocean Token balances.", "main": "./src/main/index.js", "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", "build": "cross-env NODE_ENV=production webpack --config webpack.common.config.js", "package": "electron-builder build -mwl -p never && open ./dist", @@ -24,47 +26,54 @@ }, "license": "MIT", "dependencies": { - "@coingecko/cryptoformat": "^0.3.2", + "@coingecko/cryptoformat": "^0.3.3", "ethereum-address": "0.0.4", "ethereum-blockies": "github:MyEtherWallet/blockies", "ms": "^2.1.2" }, "devDependencies": { - "@babel/core": "^7.6.0", + "@babel/core": "^7.6.2", "@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", + "@jest-runner/electron": "^2.0.2", "@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", "babel-eslint": "^10.0.3", + "babel-jest": "^24.9.0", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^5.0.4", - "cross-env": "^6.0.0", + "cross-env": "^6.0.3", "css-loader": "^3.2.0", - "electron": "^6.0.7", + "electron": "^6.0.11", "electron-builder": "^21.2.0", "electron-devtools-installer": "^2.2.4", "electron-store": "^5.0.0", - "eslint": "^6.3.0", - "eslint-config-prettier": "^6.2.0", - "eslint-plugin-react": "^7.14.3", + "eslint": "^6.5.1", + "eslint-config-prettier": "^6.4.0", + "eslint-plugin-react": "^7.16.0", "file-loader": "^4.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", "prettier": "^1.18.2", "prettier-stylelint": "^0.4.2", - "react": "^16.9.0", - "react-dom": "^16.9.0", + "react": "^16.10.2", + "react-dom": "^16.10.2", "react-pose": "^4.0.8", - "release-it": "^12.3.6", + "release-it": "^12.4.2", "style-loader": "^1.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", - "webpack": "^4.39.3", - "webpack-cli": "^3.3.8", - "webpack-dev-server": "^3.8.0" + "webpack": "^4.41.0", + "webpack-cli": "^3.3.9", + "webpack-dev-server": "^3.8.2" }, "browserslist": "electron >= 6.0", "build": { @@ -115,5 +124,22 @@ "npm": { "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)$": "/jest/__mocks__/file-mock.js", + "\\.svg": "/jest/__mocks__/svgr-mock.js" + }, + "testURL": "http://localhost", + "setupFilesAfterEnv": [ + "/jest/setup-test-env.js" + ], + "runner": "@jest-runner/electron", + "testEnvironment": "@jest-runner/electron/environment" } } diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 6454683..a1c75af 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react' +import React, { useEffect } from 'react' import PropTypes from 'prop-types' import { Router, @@ -44,24 +44,22 @@ PosedRouter.propTypes = { children: PropTypes.any.isRequired } -export default class App extends PureComponent { - componentDidMount() { +export default function App() { + useEffect(() => { ipcRenderer.on('goTo', (evt, route) => { navigate(route) }) - } + }, []) - render() { - return ( - <> - {process.platform === 'darwin' && } -
- - - - -
- - ) - } + return ( + <> + {process.platform === 'darwin' && } +
+ + + + +
+ + ) } diff --git a/src/renderer/App.test.jsx b/src/renderer/App.test.jsx new file mode 100644 index 0000000..7145615 --- /dev/null +++ b/src/renderer/App.test.jsx @@ -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( + + + + ) + expect(container.firstChild).toBeInTheDocument() + }) +}) diff --git a/src/renderer/components/Animations.js b/src/renderer/components/Animations.js index b6ae698..e16c5f5 100644 --- a/src/renderer/components/Animations.js +++ b/src/renderer/components/Animations.js @@ -22,15 +22,3 @@ export const fadeIn = { transition: { duration: 100 } } } - -export const characterAnimation = { - exit: { opacity: 0, y: 10 }, - enter: { - opacity: 1, - y: 0, - transition: ({ charInWordIndex }) => ({ - type: 'spring', - delay: charInWordIndex * 20 - }) - } -} diff --git a/src/renderer/jest/__fixtures__/context.js b/src/renderer/jest/__fixtures__/context.js new file mode 100644 index 0000000..77049b6 --- /dev/null +++ b/src/renderer/jest/__fixtures__/context.js @@ -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 +} diff --git a/src/renderer/jest/__mocks__/file-mock.js b/src/renderer/jest/__mocks__/file-mock.js new file mode 100644 index 0000000..6f60e34 --- /dev/null +++ b/src/renderer/jest/__mocks__/file-mock.js @@ -0,0 +1 @@ +module.exports = 'div' diff --git a/src/renderer/jest/__mocks__/svgr-mock.js b/src/renderer/jest/__mocks__/svgr-mock.js new file mode 100644 index 0000000..5a0f236 --- /dev/null +++ b/src/renderer/jest/__mocks__/svgr-mock.js @@ -0,0 +1 @@ +module.exports = 'svg-mock' diff --git a/src/renderer/jest/setup-test-env.js b/src/renderer/jest/setup-test-env.js new file mode 100644 index 0000000..264828a --- /dev/null +++ b/src/renderer/jest/setup-test-env.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom/extend-expect' diff --git a/src/renderer/components/Welcome.jsx b/src/renderer/screens/Home/Welcome.jsx similarity index 80% rename from src/renderer/components/Welcome.jsx rename to src/renderer/screens/Home/Welcome.jsx index 648b3c1..bf26368 100644 --- a/src/renderer/components/Welcome.jsx +++ b/src/renderer/screens/Home/Welcome.jsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react' import { Link } from '@reach/router' -import { AppContext } from '../store/createContext' -import IconRocket from '../images/rocket.svg' +import { AppContext } from '../../store/createContext' +import IconRocket from '../../images/rocket.svg' import styles from './Welcome.module.css' const Welcome = () => { diff --git a/src/renderer/components/Welcome.module.css b/src/renderer/screens/Home/Welcome.module.css similarity index 100% rename from src/renderer/components/Welcome.module.css rename to src/renderer/screens/Home/Welcome.module.css diff --git a/src/renderer/screens/Home/index.jsx b/src/renderer/screens/Home/index.jsx index 5bd4259..5927904 100644 --- a/src/renderer/screens/Home/index.jsx +++ b/src/renderer/screens/Home/index.jsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react' import { Link } from '@reach/router' import { AppContext } from '../../store/createContext' -import Welcome from '../../components/Welcome' +import Welcome from './Welcome' import Spinner from '../../components/Spinner' import Divider from '../../components/Divider' import Total from './Total' @@ -10,7 +10,7 @@ import Accounts from './Accounts' import IconCog from '../../images/cog.svg' import styles from './index.module.css' -const Home = () => { +export default function Home() { const { isLoading, needsConfig } = useContext(AppContext) return ( @@ -37,5 +37,3 @@ const Home = () => { ) } - -export default Home diff --git a/src/renderer/screens/Home/index.test.jsx b/src/renderer/screens/Home/index.test.jsx new file mode 100644 index 0000000..82bbcd3 --- /dev/null +++ b/src/renderer/screens/Home/index.test.jsx @@ -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( + + + + ) + expect(container.firstChild).toBeInTheDocument() + fireEvent.click(getByText(/Ξ/)) + }) + + it('renders Welcome without config', () => { + const { container } = render( + + + + ) + expect(container.firstChild).toHaveTextContent( + 'Add your first address to get started.' + ) + }) +}) diff --git a/src/renderer/screens/Preferences/Accounts/New.jsx b/src/renderer/screens/Preferences/Accounts/New.jsx index c72a0a8..6e09acf 100644 --- a/src/renderer/screens/Preferences/Accounts/New.jsx +++ b/src/renderer/screens/Preferences/Accounts/New.jsx @@ -2,25 +2,32 @@ import React from 'react' import PropTypes from 'prop-types' import styles from './New.module.css' -const New = ({ input, handleInputChange, handleSave, accentColor }) => ( -
  • - handleInputChange(e)} - className={styles.input} - /> +export default function New({ + input, + handleInputChange, + handleSave, + accentColor +}) { + return ( +
  • + handleInputChange(e)} + className={styles.input} + /> - -
  • -) + + + ) +} New.propTypes = { input: PropTypes.string.isRequired, @@ -28,5 +35,3 @@ New.propTypes = { handleSave: PropTypes.func.isRequired, accentColor: PropTypes.string.isRequired } - -export default New diff --git a/src/renderer/screens/Preferences/Accounts/Saved.jsx b/src/renderer/screens/Preferences/Accounts/Saved.jsx index 774e949..a298c7a 100644 --- a/src/renderer/screens/Preferences/Accounts/Saved.jsx +++ b/src/renderer/screens/Preferences/Accounts/Saved.jsx @@ -5,36 +5,40 @@ import posed, { PoseGroup } from 'react-pose' import { fadeIn } from '../../../components/Animations' 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 }) => ( - - {accounts.map(account => { - const identicon = account && toDataUrl(account) + return ( + + {accounts.map(account => { + const identicon = toDataUrl(account) - return ( - -
    - Blockies - {account} -
    + return ( + +
    + Blockies + {account} +
    - -
    - ) - })} -
    -) + + + ) + })} +
    + ) +} Saved.propTypes = { accounts: PropTypes.array.isRequired, handleDelete: PropTypes.func.isRequired } - -export default Saved diff --git a/src/renderer/screens/Preferences/Accounts/index.test.jsx b/src/renderer/screens/Preferences/Accounts/index.test.jsx new file mode 100644 index 0000000..dc366c1 --- /dev/null +++ b/src/renderer/screens/Preferences/Accounts/index.test.jsx @@ -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 = ( + + + + + + ) + + 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')) + }) +}) diff --git a/src/renderer/screens/Preferences/index.test.jsx b/src/renderer/screens/Preferences/index.test.jsx new file mode 100644 index 0000000..43d0ebf --- /dev/null +++ b/src/renderer/screens/Preferences/index.test.jsx @@ -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( + + + + ) + expect(container.firstChild).toBeInTheDocument() + }) +})