Unverified Commit a44ec62d authored by Amaury Martiny's avatar Amaury Martiny Committed by GitHub

Merge pull request #121 from paritytech/am-tests

Add tests for important logic
parents b1ef46c7 a0bd59ad
......@@ -31,7 +31,7 @@
"start": "npm-run-all -p start-*",
"start-css": "npm run build-css -- --watch --recursive",
"start-js": "react-app-rewired start",
"test": "echo Skipped."
"test": "react-app-rewired test --env=jsdom"
},
"dependencies": {
"@parity/api": "^2.1.22",
......@@ -56,6 +56,7 @@
},
"devDependencies": {
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"capitalize": "^1.0.0",
"node-sass": "^4.9.0",
"node-sass-chokidar": "^1.2.2",
"npm-run-all": "^4.1.2",
......
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
/* eslint-env mocha */
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
......@@ -16,12 +16,12 @@ import TxForm from './TxForm';
class Send extends Component {
render () {
const {
sendStore: { token }
sendStore: { tokenAddress }
} = this.props;
// We only show then Send components if we have already selected a token to
// send.
if (!token) {
if (!tokenAddress) {
return <Redirect to='/' />;
}
......
......@@ -12,7 +12,7 @@ import ReactTooltip from 'react-tooltip';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
@inject('sendStore')
@inject('sendStore', 'tokensStore')
@observer
class Signer extends Component {
state = {
......@@ -58,9 +58,11 @@ class Signer extends Component {
render () {
const {
sendStore: { token, tx }
sendStore: { tokenAddress, tx },
tokensStore
} = this.props;
const { error, isSending, password } = this.state;
const token = tokensStore.tokens[tokenAddress];
return (
<div>
......
......@@ -18,8 +18,11 @@ import withBalance from '../../utils/withBalance';
const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
@inject('sendStore')
@withBalance(({ sendStore: { token } }) => token)
@inject('sendStore', 'tokensStore')
@withBalance(
({ sendStore: { tokenAddress }, tokensStore }) =>
tokensStore.tokens[tokenAddress]
)
@observer
class Send extends Component {
state = {
......@@ -106,10 +109,12 @@ class Send extends Component {
render () {
const {
sendStore: { token }
sendStore: { tokenAddress },
tokensStore
} = this.props;
const { amount, gasPrice, maxAmount, to } = this.state;
const token = tokensStore.tokens[tokenAddress];
const error = this.hasError();
return (
......
......@@ -7,7 +7,7 @@ import { action, observable } from 'mobx';
import parityStore from './parityStore';
class CreateAccountStore {
export class CreateAccountStore {
@observable address = null;
@observable isImport = false; // Are we creating a new account, or importing via phrase?
@observable name = ''; // Account name
......
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
/* eslint-env jest */
import { CreateAccountStore } from './createAccountStore';
import parityStore from './parityStore';
import * as storeTests from '../utils/testHelpers/storeTests';
jest.mock('./parityStore', () => ({
api: {
parity: {
generateSecretPhrase: jest.fn(() => Promise.resolve('foo')),
newAccountFromPhrase: jest.fn(() => Promise.resolve()),
phraseToAddress: jest.fn(() => Promise.resolve('0x123')),
setAccountName: jest.fn(() => Promise.resolve()),
setAccountMeta: jest.fn(() => Promise.resolve())
}
}
}));
let createAccountStore; // Will hold the newly created instance of createAccountStore in each test
beforeEach(() => {
createAccountStore = new CreateAccountStore();
});
describe('method clear', () => {
test('should call setAddress and setName', () => {
createAccountStore.setAddress = jest.fn();
createAccountStore.setName = jest.fn();
createAccountStore.clear();
expect(createAccountStore.setAddress).toHaveBeenCalledWith(null);
expect(createAccountStore.setName).toHaveBeenCalledWith('');
});
});
describe('method generateNewAccount', () => {
test('should call api.parity.generateSecretPhrase', () => {
createAccountStore.generateNewAccount();
expect(parityStore.api.parity.generateSecretPhrase).toHaveBeenCalledWith();
});
});
describe('method saveAccountToParity', () => {
beforeAll(() => {
createAccountStore.setPhrase('foo');
createAccountStore.saveAccountToParity('bar');
});
test('should call api.parity.newAccountFromPhrase', () => {
expect(parityStore.api.parity.newAccountFromPhrase).toHaveBeenCalledWith(
'foo',
'bar'
);
});
test('should call api.parity.setAccountName', () => {
expect(parityStore.api.parity.setAccountName).toHaveBeenCalled();
});
test('should call api.parity.setAccountMeta', () => {
expect(parityStore.api.parity.setAccountMeta).toHaveBeenCalled();
});
});
storeTests.setterTest(CreateAccountStore, 'address');
storeTests.setterTest(CreateAccountStore, 'isImport');
storeTests.setterTest(CreateAccountStore, 'name');
describe('setter phrase', () => {
test('should set correct value and call api.parity.phraseToAddress', () => {
createAccountStore.setPhrase('foo');
expect(parityStore.api.parity.phraseToAddress).toHaveBeenCalledWith('foo');
expect(createAccountStore.phrase).toEqual('foo');
});
});
......@@ -22,7 +22,7 @@ export const STATUS = {
SYNCING: 'SYNCING' // Obvious
};
class HealthStore {
export class HealthStore {
@observable nodeHealth;
@observable syncing;
......
......@@ -10,7 +10,7 @@ import LS_PREFIX from './utils/lsPrefix';
const LS_KEY = `${LS_PREFIX}::firstRun`;
class OnboardingStore {
export class OnboardingStore {
@observable isFirstRun; // If it's the 1st time the user is running the app
constructor () {
......
......@@ -17,7 +17,7 @@ const electron = isElectron() ? window.require('electron') : null;
const LS_KEY = `${LS_PREFIX}::secureToken`;
class ParityStore {
export class ParityStore {
@observable downloadProgress = 0;
@observable isApiConnected = false;
@observable isParityRunning = false;
......
......@@ -19,7 +19,7 @@ const debug = Debug('sendStore');
const DEFAULT_GAS = new BigNumber(21000); // Default gas amount to show
const GAS_MULT_FACTOR = 1.33; // Since estimateGas is not always accurate, we add a 33% factor for buffer.
class SendStore {
export class SendStore {
@observable blockNumber; // Current block number, used to calculate tx confirmations.
@observable estimated = DEFAULT_GAS; // Estimated gas amount for this transaction.
@observable tokenAddress; // 'ETH', or the token contract address
......@@ -70,6 +70,10 @@ class SendStore {
* Estimate the amount of gas for our transaction.
*/
estimateGas = () => {
if (!this.tx || !Object.keys(this.tx).length) {
return;
}
if (this.tokenAddress === 'ETH') {
return this.estimateGasForEth(this.txForEth);
} else {
......@@ -130,11 +134,6 @@ class SendStore {
});
};
@computed
get token () {
return tokensStore.tokens[this.tokenAddress];
}
/**
* This.tx is a user-friendly tx object. We convert it now as it can be
* passed to makeContract$(...).
......@@ -145,7 +144,7 @@ class SendStore {
args: [
this.tx.to,
new BigNumber(this.tx.amount).mul(
new BigNumber(10).pow(this.token.decimals)
new BigNumber(10).pow(tokensStore.tokens[this.tokenAddress].decimals)
)
],
options: {
......@@ -175,6 +174,7 @@ class SendStore {
@action
setEstimated = estimated => {
this.estimated = estimated.mul(GAS_MULT_FACTOR);
debug('Estimated gas.', +estimated);
};
@action
......
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
/* eslint-env jest */
import abi from '@parity/shared/lib/contracts/abi/eip20';
import BigNumber from 'bignumber.js';
import lightJs from '@parity/light.js'; // Mocked
import parityStore from './parityStore';
import { SendStore } from './sendStore';
import * as storeTests from '../utils/testHelpers/storeTests';
jest.mock('@parity/light.js', () => ({
blockNumber$: jest.fn(() => ({
subscribe: () =>
jest.fn(() => ({
unsubscribe: jest.fn()
}))
})),
makeContract$: jest.fn(() => ({
contractObject: {
instance: {
transfer: { estimateGas: jest.fn(() => Promise.resolve(123)) }
}
},
transfer$: jest.fn(() => ({ subscribe: jest.fn() }))
})),
post$: jest.fn(() => ({
subscribe: jest.fn(callback => {
setTimeout(callback({ estimating: true }), 100); // eslint-disable-line standard/no-callback-literal
setTimeout(callback({ requested: 1 }), 200); // eslint-disable-line standard/no-callback-literal
})
}))
}));
jest.mock('./parityStore', () => ({
api: {
eth: {
estimateGas: jest.fn(() => Promise.resolve(123))
},
signer: {
confirmRequest: jest.fn(() => Promise.resolve(true))
}
}
}));
jest.mock('./tokensStore', () => ({
tokens: {
ETH: { decimals: 18 },
foo: { decimals: 18 }
}
}));
const mockTx = {
amount: 0.01, // In Ether or in token
gasPrice: 4, // in Gwei
to: '0x123'
};
let sendStore; // Will hold the newly created instance of SendStore in each test
beforeEach(() => {
sendStore = new SendStore();
});
describe('method acceptRequest', () => {
test('should call api.signer.confirmRequest', () => {
sendStore.acceptRequest(1, 'foo');
expect(parityStore.api.signer.confirmRequest).toHaveBeenCalledWith(
1,
null,
'foo'
);
});
test('should set a subscription on blockNumber$', () => {
sendStore.acceptRequest(1, 'foo');
expect(lightJs.blockNumber$).toHaveBeenCalled();
});
});
describe('method clear', () => {
test('should clear tx', () => {
sendStore.setTx(mockTx);
sendStore.clear();
expect(sendStore.tx).toEqual({});
});
test('should unsubscribe', () => {
sendStore.subscription = { unsubscribe: jest.fn() };
sendStore.clear();
expect(sendStore.subscription.unsubscribe).toHaveBeenCalled();
});
});
describe('@computed confirmations', () => {
test('should return correct value if txStatus is not set', () => {
sendStore.setTxStatus(null);
expect(sendStore.confirmations).toBe(-1);
});
test('should return correct value if txStatus is not `confirmed`', () => {
sendStore.setTxStatus({ estimating: true });
expect(sendStore.confirmations).toBe(-1);
});
test('should return correct value if txStatus is `confirmed`', () => {
sendStore.setBlockNumber(5);
sendStore.setTxStatus({ confirmed: { blockNumber: 4 } });
expect(sendStore.confirmations).toBe(1);
});
});
describe('@computed contract', () => {
test('should create a contract with correct token address if the current token Erc20', () => {
sendStore.setTokenAddress('foo');
sendStore.contract; // eslint-disable-line
expect(lightJs.makeContract$).toHaveBeenCalledWith('foo', abi);
});
test('should return null if the current token is ETH', () => {
sendStore.setTokenAddress('ETH');
expect(sendStore.contract).toBe(null);
});
});
describe('method estimateGas', () => {
test('should not estimate if no tx is set', () => {
sendStore.estimateGasForErc20 = jest.fn();
sendStore.estimateGasForEth = jest.fn();
expect(sendStore.estimateGas()).toBe(undefined);
expect(sendStore.estimateGasForErc20).not.toHaveBeenCalled();
expect(sendStore.estimateGasForEth).not.toHaveBeenCalled();
});
test('should call estimateGasForErc20 if the current token is Erc20', () => {
sendStore.estimateGasForErc20 = jest.fn(() => 'estimateGasForErc20');
sendStore.setTokenAddress('foo');
sendStore.setTx(mockTx);
expect(sendStore.estimateGas()).toBe('estimateGasForErc20');
expect(sendStore.estimateGasForErc20).toHaveBeenCalled();
});
test('should call estimateGasForEth if the current token is ETH', () => {
sendStore.estimateGasForEth = jest.fn(() => 'estimateGasForEth');
sendStore.setTokenAddress('ETH');
sendStore.setTx(mockTx);
expect(sendStore.estimateGas()).toBe('estimateGasForEth');
expect(sendStore.estimateGasForEth).toHaveBeenCalled();
});
});
describe('method estimateGasForErc20', () => {
beforeEach(() => {
sendStore.setTokenAddress('foo');
});
test.skip('should call the transfer method on the contract', () => {
sendStore.estimateGasForErc20(mockTx);
expect(
sendStore.contract.contractObject.instance.transfer.estimateGas
).toHaveBeenCalledWith(mockTx);
});
test('should memoize result', () => {
const a = sendStore.estimateGasForErc20(mockTx);
const b = sendStore.estimateGasForErc20(mockTx);
expect(a).toBe(b);
});
});
describe('method estimateGasForEth', () => {
beforeEach(() => {
sendStore.setTokenAddress('ETH');
});
test('should call api.eth.estimateGas', () => {
sendStore.estimateGasForEth(mockTx);
expect(parityStore.api.eth.estimateGas).toHaveBeenCalledWith(mockTx);
});
test('should memoize result', () => {
const a = sendStore.estimateGasForEth(mockTx);
const b = sendStore.estimateGasForEth(mockTx);
expect(a).toBe(b);
});
});
describe('method send', () => {
beforeEach(() => {
sendStore.setTx(mockTx);
});
test.skip('should call transfer$ if the token is Erc20 and subscribe to it', () => {
sendStore.setTokenAddress('foo');
sendStore.send();
expect(sendStore.contract.transfer$).toHaveBeenCalledWith(
sendStore.txForErc20
);
});
test('should call post$ if the token is ETH and subscribe to it', () => {
sendStore.setTokenAddress('ETH');
sendStore.send();
expect(lightJs.post$).toHaveBeenCalledWith(sendStore.txForEth);
});
test('should update txStatus', () => {
sendStore.setTxStatus = jest.fn();
sendStore.setTokenAddress('ETH');
sendStore.send();
expect(sendStore.setTxStatus).toHaveBeenCalledWith({ estimating: true });
});
test('should call acceptRequest when txStatus is requested', () => {
sendStore.acceptRequest = jest.fn(() => Promise.resolve(true));
sendStore.setTokenAddress('ETH');
sendStore.send('foo');
expect(sendStore.acceptRequest).toHaveBeenCalledWith(1, 'foo');
});
});
describe('setter setEstimated', () => {
test('should add a 1.33 factor', () => {
sendStore.setEstimated(new BigNumber(2));
expect(sendStore.estimated).toEqual(new BigNumber(2 * 1.33));
});
});
describe('@computed txForErc20', () => {
test('should return correct value', () => {
sendStore.setTokenAddress('foo');
sendStore.setTx(mockTx);
expect(sendStore.txForErc20).toEqual({
args: ['0x123', new BigNumber('10000000000000000')],
options: { gasPrice: new BigNumber('4000000000') }
});
});
});
describe('@computed txForEth', () => {
test('should return correct value', () => {
sendStore.setTokenAddress('foo');
sendStore.setTx(mockTx);
expect(sendStore.txForEth).toEqual({
gasPrice: new BigNumber('4000000000'),
to: '0x123',
value: new BigNumber('10000000000000000')
});
});
});
storeTests.setterTest(SendStore, 'blockNumber');
storeTests.setterTest(SendStore, 'tokenAddress');
storeTests.setterTest(SendStore, 'tx');
storeTests.setterTest(SendStore, 'txStatus');
......@@ -13,7 +13,7 @@ import LS_PREFIX from './utils/lsPrefix';
const LS_KEY = `${LS_PREFIX}::tokens`;
class TokensStore {
export class TokensStore {
@observable tokens = {};
constructor () {
......
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
/* eslint-env jest */
import capitalize from 'capitalize';
let store;
export const setterTest = (Store, variableName) =>
describe(`setter ${variableName}`, () => {
beforeEach(() => {
store = new Store();
});
test(`should correctly set ${variableName}`, () => {
store[`set${capitalize(variableName)}`]('foo');
expect(store[variableName]).toEqual('foo');
});
});
......@@ -2866,6 +2866,10 @@ caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.300008
version "1.0.30000858"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000858.tgz#f6f203a9128bac507136de1cf6cfd966d2df027c"
capitalize@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/capitalize/-/capitalize-1.0.0.tgz#dc802c580aee101929020d2ca14b4ca8a0ae44be"
capture-stack-trace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment