diff --git a/packages/fether-react/package.json b/packages/fether-react/package.json
index 8915aad56a967c73f853483f1257d5d91b20c8e1..8cf42e8c59b24e1754c27d9558ed7dc0575d6edf 100644
--- a/packages/fether-react/package.json
+++ b/packages/fether-react/package.json
@@ -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",
diff --git a/packages/fether-react/src/App/App.test.js b/packages/fether-react/src/App/App.test.js
deleted file mode 100644
index a858f91e19731f54dcc3da1f5be8aa8c148c0b7e..0000000000000000000000000000000000000000
--- a/packages/fether-react/src/App/App.test.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// 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(, div);
- ReactDOM.unmountComponentAtNode(div);
-});
diff --git a/packages/fether-react/src/Send/Send.js b/packages/fether-react/src/Send/Send.js
index 41efc09a8536f5916004e45b19938f11ee31d916..f460e2724f0bd28f221bbef50c06739cb7408c07 100644
--- a/packages/fether-react/src/Send/Send.js
+++ b/packages/fether-react/src/Send/Send.js
@@ -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 ;
}
diff --git a/packages/fether-react/src/Send/Signer/Signer.js b/packages/fether-react/src/Send/Signer/Signer.js
index 3793ed5dcc3e3ad41828d0a2ac8bb9463e90a69d..c91324f215b41abed11d39b66fd1f35b99d9da29 100644
--- a/packages/fether-react/src/Send/Signer/Signer.js
+++ b/packages/fether-react/src/Send/Signer/Signer.js
@@ -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 (
diff --git a/packages/fether-react/src/Send/TxForm/TxForm.js b/packages/fether-react/src/Send/TxForm/TxForm.js
index 0ce5df404d62c2b34be04a5dcfe1e3bc9ac3c341..14f6283842375f5df7469b6e86bc5f968a909cfe 100644
--- a/packages/fether-react/src/Send/TxForm/TxForm.js
+++ b/packages/fether-react/src/Send/TxForm/TxForm.js
@@ -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 (
diff --git a/packages/fether-react/src/stores/createAccountStore.js b/packages/fether-react/src/stores/createAccountStore.js
index 0c605a2fc443312232dd4b5961734e170178b0f9..c8bc77fb9d12fe7794f0364f47705fd7fe2e6ed1 100644
--- a/packages/fether-react/src/stores/createAccountStore.js
+++ b/packages/fether-react/src/stores/createAccountStore.js
@@ -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
diff --git a/packages/fether-react/src/stores/createAccountStore.spec.js b/packages/fether-react/src/stores/createAccountStore.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b977bbfe981fdff6a9de3ec29379d73944327227
--- /dev/null
+++ b/packages/fether-react/src/stores/createAccountStore.spec.js
@@ -0,0 +1,78 @@
+// 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');
+ });
+});
diff --git a/packages/fether-react/src/stores/healthStore.js b/packages/fether-react/src/stores/healthStore.js
index 99c340a02cbc13407104981c69b6bef05f2bf26c..a0457e850352791c5d46732ea993d6e25f1ad487 100644
--- a/packages/fether-react/src/stores/healthStore.js
+++ b/packages/fether-react/src/stores/healthStore.js
@@ -22,7 +22,7 @@ export const STATUS = {
SYNCING: 'SYNCING' // Obvious
};
-class HealthStore {
+export class HealthStore {
@observable nodeHealth;
@observable syncing;
diff --git a/packages/fether-react/src/stores/onboardingStore.js b/packages/fether-react/src/stores/onboardingStore.js
index ab1fd7f3fd6df4bb0e960a7a410399eb329d2430..a5270ce7de66d64a3df51d33bc231e4d40b2db8d 100644
--- a/packages/fether-react/src/stores/onboardingStore.js
+++ b/packages/fether-react/src/stores/onboardingStore.js
@@ -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 () {
diff --git a/packages/fether-react/src/stores/parityStore.js b/packages/fether-react/src/stores/parityStore.js
index d8f4ac8f5810d2a0ae21b98bb9ada4f931a97d29..dd0adbb3096457be0b83703eb966b5fbe6a40f36 100644
--- a/packages/fether-react/src/stores/parityStore.js
+++ b/packages/fether-react/src/stores/parityStore.js
@@ -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;
diff --git a/packages/fether-react/src/stores/sendStore.js b/packages/fether-react/src/stores/sendStore.js
index 76a0d48e2beaae7c71cee597c11568140ad8d787..65ac2a07e84eec119ef2e4b0a450dc78141d7d58 100644
--- a/packages/fether-react/src/stores/sendStore.js
+++ b/packages/fether-react/src/stores/sendStore.js
@@ -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
diff --git a/packages/fether-react/src/stores/sendStore.spec.js b/packages/fether-react/src/stores/sendStore.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..5fcbd556a120da58ab471cabe2d9e459d4325409
--- /dev/null
+++ b/packages/fether-react/src/stores/sendStore.spec.js
@@ -0,0 +1,258 @@
+// 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');
diff --git a/packages/fether-react/src/stores/tokensStore.js b/packages/fether-react/src/stores/tokensStore.js
index 937f86f36367450969cefc90da0db508d729622b..637731426ffe969c6a8cdc3159bb1a15b3a5b899 100644
--- a/packages/fether-react/src/stores/tokensStore.js
+++ b/packages/fether-react/src/stores/tokensStore.js
@@ -13,7 +13,7 @@ import LS_PREFIX from './utils/lsPrefix';
const LS_KEY = `${LS_PREFIX}::tokens`;
-class TokensStore {
+export class TokensStore {
@observable tokens = {};
constructor () {
diff --git a/packages/fether-react/src/utils/testHelpers/storeTests.js b/packages/fether-react/src/utils/testHelpers/storeTests.js
new file mode 100644
index 0000000000000000000000000000000000000000..6788a4dba66e05ebe27cc22162a24b159ade0a46
--- /dev/null
+++ b/packages/fether-react/src/utils/testHelpers/storeTests.js
@@ -0,0 +1,22 @@
+// 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');
+ });
+ });
diff --git a/yarn.lock b/yarn.lock
index bdc3dfe2694bcd3102be2040520b67d55d4f9306..90f7dd459b7db5c6477e6a27b126b7732d335527 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"