Account view updates (#4008)

* Fix null account render issue, add tests

* Add tests for #3999 fix (merged in #4000)

* Only include sinon-as-promised globally for mocha

* Move transactions state into tested store

* Add esjify for mocha + ejs (cherry-picked)

* Extract store state into store, test it

* Use address (as per PR comments)

* Fix failing test after master merge
This commit is contained in:
Jaco Greeff 2017-01-05 12:06:35 +01:00 committed by GitHub
parent 881066243b
commit 602a4429cc
22 changed files with 1210 additions and 281 deletions

View File

@ -43,8 +43,8 @@
"lint:css": "stylelint ./src/**/*.css",
"lint:js": "eslint --ignore-path .gitignore ./src/",
"lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/",
"test": "NODE_ENV=test mocha 'src/**/*.spec.js'",
"test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'",
"test": "NODE_ENV=test mocha --compilers ejs:ejsify 'src/**/*.spec.js'",
"test:coverage": "NODE_ENV=test istanbul cover _mocha -- --compilers ejs:ejsify 'src/**/*.spec.js'",
"test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
"test:npm": "(cd .npmjs && npm i) && node test/npmParity && (rm -rf .npmjs/node_modules)",
"prepush": "npm run lint:cached"
@ -80,6 +80,7 @@
"coveralls": "2.11.15",
"css-loader": "0.26.1",
"ejs-loader": "0.3.0",
"ejsify": "1.0.0",
"enzyme": "2.7.0",
"eslint": "3.11.1",
"eslint-config-semistandard": "7.0.0",

View File

@ -49,17 +49,17 @@ function transactions (address, page, test = false) {
// page offset from 0
return _call('txlist', {
address: address,
page: (page || 0) + 1,
offset: PAGE_SIZE,
page: (page || 0) + 1,
sort: 'desc'
}, test).then((transactions) => {
return transactions.map((tx) => {
return {
blockNumber: new BigNumber(tx.blockNumber || 0),
from: util.toChecksumAddress(tx.from),
to: util.toChecksumAddress(tx.to),
hash: tx.hash,
blockNumber: new BigNumber(tx.blockNumber),
timeStamp: tx.timeStamp,
to: util.toChecksumAddress(tx.to),
value: tx.value
};
});

View File

@ -0,0 +1,38 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import nock from 'nock';
import { stringify } from 'qs';
import { url } from './links';
function mockget (requests, test) {
let scope = nock(url(test));
requests.forEach((request) => {
scope = scope
.get(`/api?${stringify(request.query)}`)
.reply(request.code || 200, () => {
return { result: request.reply };
});
});
return scope;
}
export {
mockget
};

View File

@ -16,7 +16,6 @@
import BigNumber from 'bignumber.js';
import sinon from 'sinon';
import 'sinon-as-promised';
import Eth from './eth';

View File

@ -15,7 +15,6 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import 'sinon-as-promised';
import Personal from './personal';

View File

@ -50,8 +50,7 @@ export default class Actionbar extends Component {
}
return (
<ToolbarGroup
className={ styles.toolbuttons }>
<ToolbarGroup className={ styles.toolbuttons }>
{ buttons }
</ToolbarGroup>
);

View File

@ -25,7 +25,7 @@ import styles from './certifications.css';
class Certifications extends Component {
static propTypes = {
account: PropTypes.string.isRequired,
address: PropTypes.string.isRequired,
certifications: PropTypes.array.isRequired,
dappsUrl: PropTypes.string.isRequired
}
@ -60,10 +60,10 @@ class Certifications extends Component {
}
function mapStateToProps (_, initProps) {
const { account } = initProps;
const { address } = initProps;
return (state) => {
const certifications = state.certifications[account] || [];
const certifications = state.certifications[address] || [];
const dappsUrl = state.api.dappsUrl;
return { certifications, dappsUrl };

View File

@ -22,13 +22,16 @@ import CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
import ContractIcon from 'material-ui/svg-icons/action/code';
import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
import DeleteIcon from 'material-ui/svg-icons/action/delete';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
import EditIcon from 'material-ui/svg-icons/content/create';
import LockedIcon from 'material-ui/svg-icons/action/lock';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
@ -41,13 +44,16 @@ export {
ComputerIcon,
ContractIcon,
DashboardIcon,
DeleteIcon,
DoneIcon,
EditIcon,
LockedIcon,
NextIcon,
PrevIcon,
SaveIcon,
SendIcon,
SnoozeIcon,
VerifyIcon,
VisibleIcon,
VpnIcon
};

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui';
import CopyToClipboard from '~/ui/CopyToClipboard';
@ -26,50 +27,45 @@ export default class Header extends Component {
static propTypes = {
account: PropTypes.object,
balance: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node,
isContract: PropTypes.bool,
hideName: PropTypes.bool
className: PropTypes.string,
hideName: PropTypes.bool,
isContract: PropTypes.bool
};
static defaultProps = {
className: '',
children: null,
isContract: false,
hideName: false
className: '',
hideName: false,
isContract: false
};
render () {
const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account;
const { account, balance, children, className, hideName } = this.props;
if (!account) {
return null;
}
const uuidText = !uuid
? null
: <div className={ styles.uuidline }>uuid: { uuid }</div>;
const { address } = account;
const meta = account.meta || {};
return (
<div className={ className }>
<Container>
<IdentityIcon
address={ address } />
<IdentityIcon address={ address } />
<div className={ styles.floatleft }>
{ this.renderName(address) }
{ this.renderName() }
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
<CopyToClipboard data={ address } />
<div className={ styles.address }>{ address }</div>
</div>
{ uuidText }
{ this.renderUuid() }
<div className={ styles.infoline }>
{ meta.description }
</div>
{ this.renderTxCount() }
</div>
<div className={ styles.tags }>
<Tags tags={ meta.tags } />
</div>
@ -77,9 +73,7 @@ export default class Header extends Component {
<Balance
account={ account }
balance={ balance } />
<Certifications
account={ account.address }
/>
<Certifications address={ address } />
</div>
{ children }
</Container>
@ -87,15 +81,22 @@ export default class Header extends Component {
);
}
renderName (address) {
renderName () {
const { hideName } = this.props;
if (hideName) {
return null;
}
const { address } = this.props.account;
return (
<ContainerTitle title={ <IdentityName address={ address } unknown /> } />
<ContainerTitle
title={
<IdentityName
address={ address }
unknown />
} />
);
}
@ -114,7 +115,31 @@ export default class Header extends Component {
return (
<div className={ styles.infoline }>
{ txCount.toFormat() } outgoing transactions
<FormattedMessage
id='account.header.outgoingTransactions'
defaultMessage='{count} outgoing transactions'
values={ {
count: txCount.toFormat()
} } />
</div>
);
}
renderUuid () {
const { uuid } = this.props.account;
if (!uuid) {
return null;
}
return (
<div className={ styles.uuidline }>
<FormattedMessage
id='account.header.uuid'
defaultMessage='uuid: {uuid}'
values={ {
uuid
} } />
</div>
);
}

View File

@ -0,0 +1,156 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { shallow } from 'enzyme';
import React from 'react';
import Header from './';
const ACCOUNT = {
address: '0x0123456789012345678901234567890123456789',
meta: {
description: 'the description',
tags: ['taga', 'tagb']
},
uuid: '0xabcdef'
};
let component;
let instance;
function render (props = {}) {
if (props && !props.account) {
props.account = ACCOUNT;
}
component = shallow(
<Header { ...props } />
);
instance = component.instance();
return component;
}
describe('views/Account/Header', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('renders null with no account', () => {
expect(render(null).find('div')).to.have.length(0);
});
it('renders when no account meta', () => {
expect(render({ account: { address: ACCOUNT.address } })).to.be.ok;
});
it('renders when no account description', () => {
expect(render({ account: { address: ACCOUNT.address, meta: { tags: [] } } })).to.be.ok;
});
it('renders when no account tags', () => {
expect(render({ account: { address: ACCOUNT.address, meta: { description: 'something' } } })).to.be.ok;
});
describe('sections', () => {
it('renders the Balance', () => {
render({ balance: { balance: 'testing' } });
const balance = component.find('Connect(Balance)');
expect(balance).to.have.length(1);
expect(balance.props().account).to.deep.equal(ACCOUNT);
expect(balance.props().balance).to.deep.equal({ balance: 'testing' });
});
it('renders the Certifications', () => {
render();
const certs = component.find('Connect(Certifications)');
expect(certs).to.have.length(1);
expect(certs.props().address).to.deep.equal(ACCOUNT.address);
});
it('renders the IdentityIcon', () => {
render();
const icon = component.find('Connect(IdentityIcon)');
expect(icon).to.have.length(1);
expect(icon.props().address).to.equal(ACCOUNT.address);
});
it('renders the Tags', () => {
render();
const tags = component.find('Tags');
expect(tags).to.have.length(1);
expect(tags.props().tags).to.deep.equal(ACCOUNT.meta.tags);
});
});
});
describe('renderName', () => {
it('renders null with hideName', () => {
render({ hideName: true });
expect(instance.renderName()).to.be.null;
});
it('renders the name', () => {
render();
expect(instance.renderName()).not.to.be.null;
});
it('renders when no address specified', () => {
render({ account: {} });
expect(instance.renderName()).to.be.ok;
});
});
describe('renderTxCount', () => {
it('renders null when contract', () => {
render({ balance: { txCount: new BigNumber(1) }, isContract: true });
expect(instance.renderTxCount()).to.be.null;
});
it('renders null when no balance', () => {
render({ balance: null, isContract: false });
expect(instance.renderTxCount()).to.be.null;
});
it('renders null when txCount is null', () => {
render({ balance: { txCount: null }, isContract: false });
expect(instance.renderTxCount()).to.be.null;
});
it('renders the tx count', () => {
render({ balance: { txCount: new BigNumber(1) }, isContract: false });
expect(instance.renderTxCount()).not.to.be.null;
});
});
describe('renderUuid', () => {
it('renders null with no uuid', () => {
render({ account: Object.assign({}, ACCOUNT, { uuid: null }) });
expect(instance.renderUuid()).to.be.null;
});
it('renders the uuid', () => {
render();
expect(instance.renderUuid()).not.to.be.null;
});
});
});

View File

@ -0,0 +1,118 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable, transaction } from 'mobx';
import etherscan from '~/3rdparty/etherscan';
export default class Store {
@observable address = null;
@observable isLoading = false;
@observable isTest = undefined;
@observable isTracing = false;
@observable txHashes = [];
constructor (api) {
this._api = api;
}
@action setHashes = (transactions) => {
transaction(() => {
this.setLoading(false);
this.txHashes = transactions.map((transaction) => transaction.hash);
});
}
@action setAddress = (address) => {
this.address = address;
}
@action setLoading = (isLoading) => {
this.isLoading = isLoading;
}
@action setTest = (isTest) => {
this.isTest = isTest;
}
@action setTracing = (isTracing) => {
this.isTracing = isTracing;
}
@action updateProps = (props) => {
transaction(() => {
this.setAddress(props.address);
this.setTest(props.isTest);
// TODO: When tracing is enabled again, adjust to actually set
this.setTracing(false && props.traceMode);
});
return this.getTransactions();
}
getTransactions () {
if (this.isTest === undefined) {
return Promise.resolve();
}
this.setLoading(true);
// TODO: When supporting other chains (eg. ETC). call to be made to other endpoints
return (
this.isTracing
? this.fetchTraceTransactions()
: this.fetchEtherscanTransactions()
)
.then((transactions) => {
this.setHashes(transactions);
})
.catch((error) => {
console.warn('getTransactions', error);
this.setLoading(false);
});
}
fetchEtherscanTransactions () {
return etherscan.account.transactions(this.address, 0, this.isTest);
}
fetchTraceTransactions () {
return Promise
.all([
this._api.trace.filter({
fromAddress: this.address,
fromBlock: 0
}),
this._api.trace.filter({
fromBlock: 0,
toAddress: this.address
})
])
.then(([fromTransactions, toTransactions]) => {
return fromTransactions
.concat(toTransactions)
.map((transaction) => {
return {
blockNumber: transaction.blockNumber,
from: transaction.action.from,
hash: transaction.transactionHash,
to: transaction.action.to
};
});
});
}
}

View File

@ -0,0 +1,193 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import sinon from 'sinon';
import { mockget as mockEtherscan } from '~/3rdparty/etherscan/helpers.spec.js';
import { ADDRESS, createApi } from './transactions.test.js';
import Store from './store';
let api;
let store;
function createStore () {
api = createApi();
store = new Store(api);
return store;
}
function mockQuery () {
mockEtherscan([{
query: {
module: 'account',
action: 'txlist',
address: ADDRESS,
offset: 25,
page: 1,
sort: 'desc'
},
reply: [{ hash: '123' }]
}], true);
}
describe('views/Account/Transactions/store', () => {
beforeEach(() => {
mockQuery();
createStore();
});
describe('constructor', () => {
it('sets the api', () => {
expect(store._api).to.deep.equals(api);
});
it('starts with isLoading === false', () => {
expect(store.isLoading).to.be.false;
});
it('starts with isTracing === false', () => {
expect(store.isTracing).to.be.false;
});
});
describe('@action', () => {
describe('setHashes', () => {
it('clears the loading state', () => {
store.setLoading(true);
store.setHashes([]);
expect(store.isLoading).to.be.false;
});
it('sets the hashes from the transactions', () => {
store.setHashes([{ hash: '123' }, { hash: '456' }]);
expect(store.txHashes.peek()).to.deep.equal(['123', '456']);
});
});
describe('setAddress', () => {
it('sets the address', () => {
store.setAddress(ADDRESS);
expect(store.address).to.equal(ADDRESS);
});
});
describe('setLoading', () => {
it('sets the isLoading flag', () => {
store.setLoading(true);
expect(store.isLoading).to.be.true;
});
});
describe('setTest', () => {
it('sets the isTest flag', () => {
store.setTest(true);
expect(store.isTest).to.be.true;
});
});
describe('setTracing', () => {
it('sets the isTracing flag', () => {
store.setTracing(true);
expect(store.isTracing).to.be.true;
});
});
describe('updateProps', () => {
it('retrieves transactions once updated', () => {
sinon.spy(store, 'getTransactions');
store.updateProps({});
expect(store.getTransactions).to.have.been.called;
store.getTransactions.restore();
});
});
});
describe('operations', () => {
describe('getTransactions', () => {
it('retrieves the hashes via etherscan', () => {
sinon.spy(store, 'fetchEtherscanTransactions');
store.setAddress(ADDRESS);
store.setTest(true);
store.setTracing(false);
return store.getTransactions().then(() => {
expect(store.fetchEtherscanTransactions).to.have.been.called;
expect(store.txHashes.peek()).to.deep.equal(['123']);
store.fetchEtherscanTransactions.restore();
});
});
it('retrieves the hashes via tracing', () => {
sinon.spy(store, 'fetchTraceTransactions');
store.setAddress(ADDRESS);
store.setTest(true);
store.setTracing(true);
return store.getTransactions().then(() => {
expect(store.fetchTraceTransactions).to.have.been.called;
expect(store.txHashes.peek()).to.deep.equal(['123', '098']);
store.fetchTraceTransactions.restore();
});
});
});
describe('fetchEtherscanTransactions', () => {
it('retrieves the transactions', () => {
store.setAddress(ADDRESS);
store.setTest(true);
return store.fetchEtherscanTransactions().then((transactions) => {
expect(transactions).to.deep.equal([{
blockNumber: new BigNumber(0),
from: '',
hash: '123',
timeStamp: undefined,
to: '',
value: undefined
}]);
});
});
});
describe('fetchTraceTransactions', () => {
it('retrieves the transactions', () => {
store.setAddress(ADDRESS);
store.setTest(true);
return store.fetchTraceTransactions().then((transactions) => {
expect(transactions).to.deep.equal([
{
blockNumber: undefined,
from: undefined,
hash: '123',
to: undefined
},
{
blockNumber: undefined,
from: undefined,
hash: '098',
to: undefined
}
]);
});
});
});
});
});

View File

@ -14,15 +14,18 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import etherscan from '~/3rdparty/etherscan';
import { Container, TxList, Loading } from '~/ui';
import Store from './store';
import styles from './transactions.css';
@observer
class Transactions extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
@ -34,34 +37,35 @@ class Transactions extends Component {
traceMode: PropTypes.bool
}
state = {
hashes: [],
loading: true,
callInfo: {}
}
store = new Store(this.context.api);
componentDidMount () {
this.getTransactions(this.props);
componentWillMount () {
this.store.updateProps(this.props);
}
componentWillReceiveProps (newProps) {
if (this.props.traceMode === undefined && newProps.traceMode !== undefined) {
this.getTransactions(newProps);
this.store.updateProps(newProps);
return;
}
const hasChanged = [ 'isTest', 'address' ]
const hasChanged = ['isTest', 'address']
.map(key => newProps[key] !== this.props[key])
.reduce((truth, keyTruth) => truth || keyTruth, false);
if (hasChanged) {
this.getTransactions(newProps);
this.store.updateProps(newProps);
}
}
render () {
return (
<Container title='transactions'>
<Container
title={
<FormattedMessage
id='account.transactions.title'
defaultMessage='transactions' />
}>
{ this.renderTransactionList() }
{ this.renderEtherscanFooter() }
</Container>
@ -69,10 +73,9 @@ class Transactions extends Component {
}
renderTransactionList () {
const { address } = this.props;
const { hashes, loading } = this.state;
const { address, isLoading, txHashes } = this.store;
if (loading) {
if (isLoading) {
return (
<Loading />
);
@ -81,85 +84,29 @@ class Transactions extends Component {
return (
<TxList
address={ address }
hashes={ hashes }
hashes={ txHashes }
/>
);
}
renderEtherscanFooter () {
const { traceMode } = this.props;
const { isTracing } = this.store;
if (traceMode) {
if (isTracing) {
return null;
}
return (
<div className={ styles.etherscan }>
Transaction list powered by <a href='https://etherscan.io/' target='_blank'>etherscan.io</a>
<FormattedMessage
id='account.transactions.poweredBy'
defaultMessage='Transaction list powered by {etherscan}'
values={ {
etherscan: <a href='https://etherscan.io/' target='_blank'>etherscan.io</a>
} } />
</div>
);
}
getTransactions = (props) => {
const { isTest, address, traceMode } = props;
// Don't fetch the transactions if we don't know in which
// network we are yet...
if (isTest === undefined) {
return;
}
return this
.fetchTransactions(isTest, address, traceMode)
.then((transactions) => {
this.setState({
hashes: transactions.map((transaction) => transaction.hash),
loading: false
});
});
}
fetchTransactions = (isTest, address, traceMode) => {
// if (traceMode) {
// return this.fetchTraceTransactions(address);
// }
return this.fetchEtherscanTransactions(isTest, address);
}
fetchEtherscanTransactions = (isTest, address) => {
return etherscan.account
.transactions(address, 0, isTest)
.catch((error) => {
console.error('getTransactions', error);
});
}
fetchTraceTransactions = (address) => {
return Promise
.all([
this.context.api.trace
.filter({
fromBlock: 0,
fromAddress: address
}),
this.context.api.trace
.filter({
fromBlock: 0,
toAddress: address
})
])
.then(([fromTransactions, toTransactions]) => {
const transactions = [].concat(fromTransactions, toTransactions);
return transactions.map(transaction => ({
from: transaction.action.from,
to: transaction.action.to,
blockNumber: transaction.blockNumber,
hash: transaction.transactionHash
}));
});
}
}
function mapStateToProps (state) {

View File

@ -0,0 +1,55 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import { ADDRESS, createApi, createRedux } from './transactions.test.js';
import Transactions from './';
let component;
let instance;
function render (props) {
component = shallow(
<Transactions
address={ ADDRESS }
{ ...props } />,
{ context: { store: createRedux() } }
).find('Transactions').shallow({ context: { api: createApi() } });
instance = component.instance();
return component;
}
describe('views/Account/Transactions', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('renderTransactionList', () => {
it('renders Loading when isLoading === true', () => {
instance.store.setLoading(true);
expect(instance.renderTransactionList().type).to.match(/Loading/);
});
it('renders TxList when isLoading === true', () => {
instance.store.setLoading(false);
expect(instance.renderTransactionList().type).to.match(/Connect/);
});
});
});

View File

@ -0,0 +1,31 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { ADDRESS, createRedux } from '../account.test.js';
function createApi () {
return {
trace: {
filter: (options) => Promise.resolve([{ transactionHash: options.fromAddress ? '123' : '098', action: {} }])
}
};
}
export {
ADDRESS,
createApi,
createRedux
};

View File

@ -14,47 +14,38 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ActionDelete from 'material-ui/svg-icons/action/delete';
import ContentCreate from 'material-ui/svg-icons/content/create';
import ContentSend from 'material-ui/svg-icons/content/send';
import LockIcon from 'material-ui/svg-icons/action/lock';
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
import { Actionbar, Button, Page } from '~/ui';
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
import Header from './Header';
import Transactions from './Transactions';
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { Actionbar, Button, Page } from '~/ui';
import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
import Header from './Header';
import Store from './store';
import Transactions from './Transactions';
import styles from './account.css';
@observer
class Account extends Component {
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
images: PropTypes.object.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
params: PropTypes.object,
accounts: PropTypes.object,
balances: PropTypes.object
balances: PropTypes.object,
params: PropTypes.object
}
state = {
showDeleteDialog: false,
showEditDialog: false,
showFundDialog: false,
showVerificationDialog: false,
showTransferDialog: false,
showPasswordDialog: false
}
store = new Store();
componentDidMount () {
this.props.fetchCertifiers();
@ -76,7 +67,8 @@ class Account extends Component {
setVisibleAccounts (props = this.props) {
const { params, setVisibleAccounts, fetchCertifications } = props;
const addresses = [ params.address ];
const addresses = [params.address];
setVisibleAccounts(addresses);
fetchCertifications(params.address);
}
@ -97,15 +89,14 @@ class Account extends Component {
{ this.renderDeleteDialog(account) }
{ this.renderEditDialog(account) }
{ this.renderFundDialog() }
{ this.renderPasswordDialog(account) }
{ this.renderTransferDialog(account, balance) }
{ this.renderVerificationDialog() }
{ this.renderTransferDialog() }
{ this.renderPasswordDialog() }
{ this.renderActionbar() }
{ this.renderActionbar(balance) }
<Page>
<Header
account={ account }
balance={ balance }
/>
balance={ balance } />
<Transactions
accounts={ accounts }
address={ address } />
@ -114,86 +105,108 @@ class Account extends Component {
);
}
renderActionbar () {
const { address } = this.props.params;
const { balances } = this.props;
const balance = balances[address];
renderActionbar (balance) {
const showTransferButton = !!(balance && balance.tokens);
const buttons = [
<Button
key='transferFunds'
icon={ <ContentSend /> }
label='transfer'
disabled={ !showTransferButton }
onClick={ this.onTransferClick } />,
icon={ <SendIcon /> }
key='transferFunds'
label={
<FormattedMessage
id='account.button.transfer'
defaultMessage='transfer' />
}
onClick={ this.store.toggleTransferDialog } />,
<Button
icon={
<img
className={ styles.btnicon }
src={ shapeshiftBtn } />
}
key='shapeshift'
icon={ <img src={ shapeshiftBtn } className={ styles.btnicon } /> }
label='shapeshift'
onClick={ this.onShapeshiftAccountClick } />,
label={
<FormattedMessage
id='account.button.shapeshift'
defaultMessage='shapeshift' />
}
onClick={ this.store.toggleFundDialog } />,
<Button
key='sms-verification'
icon={ <VerifyIcon /> }
label='Verify'
onClick={ this.openVerification } />,
key='sms-verification'
label={
<FormattedMessage
id='account.button.verify'
defaultMessage='verify' />
}
onClick={ this.store.toggleVerificationDialog } />,
<Button
icon={ <EditIcon /> }
key='editmeta'
icon={ <ContentCreate /> }
label='edit'
onClick={ this.onEditClick } />,
label={
<FormattedMessage
id='account.button.edit'
defaultMessage='edit' />
}
onClick={ this.store.toggleEditDialog } />,
<Button
icon={ <LockedIcon /> }
key='passwordManager'
icon={ <LockIcon /> }
label='password'
onClick={ this.onPasswordClick } />,
label={
<FormattedMessage
id='account.button.password'
defaultMessage='password' />
}
onClick={ this.store.togglePasswordDialog } />,
<Button
icon={ <DeleteIcon /> }
key='delete'
icon={ <ActionDelete /> }
label='delete account'
onClick={ this.onDeleteClick } />
label={
<FormattedMessage
id='account.button.delete'
defaultMessage='delete account' />
}
onClick={ this.store.toggleDeleteDialog } />
];
return (
<Actionbar
title='Account Management'
buttons={ buttons } />
buttons={ buttons }
title={
<FormattedMessage
id='account.title'
defaultMessage='Account Management' />
} />
);
}
renderDeleteDialog (account) {
const { showDeleteDialog } = this.state;
if (!showDeleteDialog) {
if (!this.store.isDeleteVisible) {
return null;
}
return (
<DeleteAccount
account={ account }
onClose={ this.onDeleteClose } />
onClose={ this.store.toggleDeleteDialog } />
);
}
renderEditDialog (account) {
const { showEditDialog } = this.state;
if (!showEditDialog) {
if (!this.store.isEditVisible) {
return null;
}
return (
<EditMeta
account={ account }
onClose={ this.onEditClick } />
onClose={ this.store.toggleEditDialog } />
);
}
renderFundDialog () {
const { showFundDialog } = this.state;
if (!showFundDialog) {
if (!this.store.isFundVisible) {
return null;
}
@ -202,12 +215,41 @@ class Account extends Component {
return (
<Shapeshift
address={ address }
onClose={ this.onShapeshiftAccountClose } />
onClose={ this.store.toggleFundDialog } />
);
}
renderPasswordDialog (account) {
if (!this.store.isPasswordVisible) {
return null;
}
return (
<PasswordManager
account={ account }
onClose={ this.store.togglePasswordDialog } />
);
}
renderTransferDialog (account, balance) {
if (!this.store.isTransferVisible) {
return null;
}
const { balances, images } = this.props;
return (
<Transfer
account={ account }
balance={ balance }
balances={ balances }
images={ images }
onClose={ this.store.toggleTransferDialog } />
);
}
renderVerificationDialog () {
if (!this.state.showVerificationDialog) {
if (!this.store.isVerificationVisible) {
return null;
}
@ -216,102 +258,9 @@ class Account extends Component {
return (
<Verification
account={ address }
onClose={ this.onVerificationClose }
/>
onClose={ this.store.toggleVerificationDialog } />
);
}
renderTransferDialog () {
const { showTransferDialog } = this.state;
if (!showTransferDialog) {
return null;
}
const { address } = this.props.params;
const { accounts, balances, images } = this.props;
const account = accounts[address];
const balance = balances[address];
return (
<Transfer
account={ account }
balance={ balance }
balances={ balances }
images={ images }
onClose={ this.onTransferClose } />
);
}
renderPasswordDialog () {
const { showPasswordDialog } = this.state;
if (!showPasswordDialog) {
return null;
}
const { address } = this.props.params;
const { accounts } = this.props;
const account = accounts[address];
return (
<PasswordManager
account={ account }
onClose={ this.onPasswordClose } />
);
}
onDeleteClick = () => {
this.setState({ showDeleteDialog: true });
}
onDeleteClose = () => {
this.setState({ showDeleteDialog: false });
}
onEditClick = () => {
this.setState({
showEditDialog: !this.state.showEditDialog
});
}
onShapeshiftAccountClick = () => {
this.setState({
showFundDialog: !this.state.showFundDialog
});
}
onShapeshiftAccountClose = () => {
this.onShapeshiftAccountClick();
}
openVerification = () => {
this.setState({ showVerificationDialog: true });
}
onVerificationClose = () => {
this.setState({ showVerificationDialog: false });
}
onTransferClick = () => {
this.setState({
showTransferDialog: !this.state.showTransferDialog
});
}
onTransferClose = () => {
this.onTransferClick();
}
onPasswordClick = () => {
this.setState({
showPasswordDialog: !this.state.showPasswordDialog
});
}
onPasswordClose = () => {
this.onPasswordClick();
}
}
function mapStateToProps (state) {
@ -328,9 +277,9 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return bindActionCreators({
setVisibleAccounts,
fetchCertifiers,
fetchCertifications
fetchCertifications,
setVisibleAccounts
}, dispatch);
}

View File

@ -0,0 +1,226 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import { ADDRESS, createRedux } from './account.test.js';
import Account from './';
let component;
let instance;
let store;
function render (props) {
component = shallow(
<Account
params={ { address: ADDRESS } }
{ ...props } />,
{ context: { store: createRedux() } }
).find('Account').shallow();
instance = component.instance();
store = instance.store;
return component;
}
describe('views/Account', () => {
describe('rendering', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('sections', () => {
it('renders the Actionbar', () => {
expect(component.find('Actionbar')).to.have.length(1);
});
it('renders the Page', () => {
expect(component.find('Page')).to.have.length(1);
});
it('renders the Header', () => {
expect(component.find('Header')).to.have.length(1);
});
it('renders the Transactions', () => {
expect(component.find('Connect(Transactions)')).to.have.length(1);
});
it('renders no other sections', () => {
expect(component.find('div').children()).to.have.length(2);
});
});
});
describe('sub-renderers', () => {
describe('renderActionBar', () => {
let bar;
let barShallow;
beforeEach(() => {
render();
bar = instance.renderActionbar({ tokens: {} });
barShallow = shallow(bar);
});
it('renders the bar', () => {
expect(bar.type).to.match(/Actionbar/);
});
// TODO: Finding by index is not optimal, however couldn't find a better method atm
// since we cannot find by key (prop not visible in shallow debug())
describe('clicks', () => {
it('toggles transfer on click', () => {
barShallow.find('Button').at(0).simulate('click');
expect(store.isTransferVisible).to.be.true;
});
it('toggles fund on click', () => {
barShallow.find('Button').at(1).simulate('click');
expect(store.isFundVisible).to.be.true;
});
it('toggles fund on click', () => {
barShallow.find('Button').at(1).simulate('click');
expect(store.isFundVisible).to.be.true;
});
it('toggles verify on click', () => {
barShallow.find('Button').at(2).simulate('click');
expect(store.isVerificationVisible).to.be.true;
});
it('toggles edit on click', () => {
barShallow.find('Button').at(3).simulate('click');
expect(store.isEditVisible).to.be.true;
});
it('toggles password on click', () => {
barShallow.find('Button').at(4).simulate('click');
expect(store.isPasswordVisible).to.be.true;
});
it('toggles delete on click', () => {
barShallow.find('Button').at(5).simulate('click');
expect(store.isDeleteVisible).to.be.true;
});
});
});
describe('renderDeleteDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isDeleteVisible).to.be.false;
expect(instance.renderDeleteDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleDeleteDialog();
expect(instance.renderDeleteDialog().type).to.match(/Connect/);
});
});
describe('renderEditDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isEditVisible).to.be.false;
expect(instance.renderEditDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleEditDialog();
expect(instance.renderEditDialog({ address: ADDRESS }).type).to.match(/Connect/);
});
});
describe('renderFundDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isFundVisible).to.be.false;
expect(instance.renderFundDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleFundDialog();
expect(instance.renderFundDialog().type).to.match(/Shapeshift/);
});
});
describe('renderPasswordDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isPasswordVisible).to.be.false;
expect(instance.renderPasswordDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.togglePasswordDialog();
expect(instance.renderPasswordDialog({ address: ADDRESS }).type).to.match(/Connect/);
});
});
describe('renderTransferDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isTransferVisible).to.be.false;
expect(instance.renderTransferDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleTransferDialog();
expect(instance.renderTransferDialog().type).to.match(/Connect/);
});
});
describe('renderVerificationDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isVerificationVisible).to.be.false;
expect(instance.renderVerificationDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleVerificationDialog();
expect(instance.renderVerificationDialog().type).to.match(/Connect/);
});
});
});
});

View File

@ -0,0 +1,52 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
const ADDRESS = '0x0123456789012345678901234567890123456789';
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
balances: {
[ADDRESS]: {}
}
},
images: {},
nodeStatus: {
isTest: false,
traceMode: false
},
personal: {
accounts: {
[ADDRESS]: {
address: ADDRESS
}
}
}
};
}
};
}
export {
ADDRESS,
createRedux
};

View File

@ -0,0 +1,50 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable } from 'mobx';
export default class Store {
@observable isDeleteVisible = false;
@observable isEditVisible = false;
@observable isFundVisible = false;
@observable isPasswordVisible = false;
@observable isTransferVisible = false;
@observable isVerificationVisible = false;
@action toggleDeleteDialog = () => {
this.isDeleteVisible = !this.isDeleteVisible;
}
@action toggleEditDialog = () => {
this.isEditVisible = !this.isEditVisible;
}
@action toggleFundDialog = () => {
this.isFundVisible = !this.isFundVisible;
}
@action togglePasswordDialog = () => {
this.isPasswordVisible = !this.isPasswordVisible;
}
@action toggleTransferDialog = () => {
this.isTransferVisible = !this.isTransferVisible;
}
@action toggleVerificationDialog = () => {
this.isVerificationVisible = !this.isVerificationVisible;
}
}

View File

@ -0,0 +1,84 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Store from './store';
let store;
function createStore () {
store = new Store();
}
describe('views/Account/Store', () => {
beforeEach(() => {
createStore();
});
describe('constructor', () => {
it('sets all modal visibility to false', () => {
expect(store.isDeleteVisible).to.be.false;
expect(store.isEditVisible).to.be.false;
expect(store.isFundVisible).to.be.false;
expect(store.isPasswordVisible).to.be.false;
expect(store.isTransferVisible).to.be.false;
expect(store.isVerificationVisible).to.be.false;
});
});
describe('@action', () => {
describe('toggleDeleteDialog', () => {
it('toggles the visibility', () => {
store.toggleDeleteDialog();
expect(store.isDeleteVisible).to.be.true;
});
});
describe('toggleEditDialog', () => {
it('toggles the visibility', () => {
store.toggleEditDialog();
expect(store.isEditVisible).to.be.true;
});
});
describe('toggleFundDialog', () => {
it('toggles the visibility', () => {
store.toggleFundDialog();
expect(store.isFundVisible).to.be.true;
});
});
describe('togglePasswordDialog', () => {
it('toggles the visibility', () => {
store.togglePasswordDialog();
expect(store.isPasswordVisible).to.be.true;
});
});
describe('toggleTransferDialog', () => {
it('toggles the visibility', () => {
store.toggleTransferDialog();
expect(store.isTransferVisible).to.be.true;
});
});
describe('toggleVerificationDialog', () => {
it('toggles the visibility', () => {
store.toggleVerificationDialog();
expect(store.isVerificationVisible).to.be.true;
});
});
});
});

View File

@ -197,7 +197,7 @@ export default class Summary extends Component {
}
return (
<Certifications account={ account.address } />
<Certifications address={ account.address } />
);
}
}

View File

@ -26,6 +26,7 @@ injectTapEventPlugin();
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import chaiEnzyme from 'chai-enzyme';
import 'sinon-as-promised';
import sinonChai from 'sinon-chai';
import { WebSocket } from 'mock-socket';
import jsdom from 'jsdom';