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:
parent
881066243b
commit
602a4429cc
@ -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",
|
||||
|
6
js/src/3rdparty/etherscan/account.js
vendored
6
js/src/3rdparty/etherscan/account.js
vendored
@ -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
|
||||
};
|
||||
});
|
||||
|
38
js/src/3rdparty/etherscan/helpers.spec.js
vendored
Normal file
38
js/src/3rdparty/etherscan/helpers.spec.js
vendored
Normal 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
|
||||
};
|
@ -16,7 +16,6 @@
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import sinon from 'sinon';
|
||||
import 'sinon-as-promised';
|
||||
|
||||
import Eth from './eth';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -50,8 +50,7 @@ export default class Actionbar extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarGroup
|
||||
className={ styles.toolbuttons }>
|
||||
<ToolbarGroup className={ styles.toolbuttons }>
|
||||
{ buttons }
|
||||
</ToolbarGroup>
|
||||
);
|
||||
|
@ -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 };
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
156
js/src/views/Account/Header/header.spec.js
Normal file
156
js/src/views/Account/Header/header.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
118
js/src/views/Account/Transactions/store.js
Normal file
118
js/src/views/Account/Transactions/store.js
Normal 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
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
193
js/src/views/Account/Transactions/store.spec.js
Normal file
193
js/src/views/Account/Transactions/store.spec.js
Normal 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
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
55
js/src/views/Account/Transactions/transactions.spec.js
Normal file
55
js/src/views/Account/Transactions/transactions.spec.js
Normal 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/);
|
||||
});
|
||||
});
|
||||
});
|
31
js/src/views/Account/Transactions/transactions.test.js
Normal file
31
js/src/views/Account/Transactions/transactions.test.js
Normal 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
|
||||
};
|
@ -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);
|
||||
}
|
||||
|
||||
|
226
js/src/views/Account/account.spec.js
Normal file
226
js/src/views/Account/account.spec.js
Normal 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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
52
js/src/views/Account/account.test.js
Normal file
52
js/src/views/Account/account.test.js
Normal 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
|
||||
};
|
50
js/src/views/Account/store.js
Normal file
50
js/src/views/Account/store.js
Normal 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;
|
||||
}
|
||||
}
|
84
js/src/views/Account/store.spec.js
Normal file
84
js/src/views/Account/store.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -197,7 +197,7 @@ export default class Summary extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Certifications account={ account.address } />
|
||||
<Certifications address={ account.address } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user