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 (
- -
-
-
- {account}
-
+ return (
+ -
+
+
+ {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()
+ })
+})