diff --git a/js/package.json b/js/package.json index 08af16a0aa1..29bb7079131 100644 --- a/js/package.json +++ b/js/package.json @@ -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", diff --git a/js/src/3rdparty/etherscan/account.js b/js/src/3rdparty/etherscan/account.js index 7b8c431a0ce..52a08ef4b6b 100644 --- a/js/src/3rdparty/etherscan/account.js +++ b/js/src/3rdparty/etherscan/account.js @@ -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 }; }); diff --git a/js/src/3rdparty/etherscan/helpers.spec.js b/js/src/3rdparty/etherscan/helpers.spec.js new file mode 100644 index 00000000000..508a7b47a96 --- /dev/null +++ b/js/src/3rdparty/etherscan/helpers.spec.js @@ -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 . + +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 +}; diff --git a/js/src/api/subscriptions/eth.spec.js b/js/src/api/subscriptions/eth.spec.js index 87cc76d0346..680ff881e2a 100644 --- a/js/src/api/subscriptions/eth.spec.js +++ b/js/src/api/subscriptions/eth.spec.js @@ -16,7 +16,6 @@ import BigNumber from 'bignumber.js'; import sinon from 'sinon'; -import 'sinon-as-promised'; import Eth from './eth'; diff --git a/js/src/api/subscriptions/personal.spec.js b/js/src/api/subscriptions/personal.spec.js index b00354f6493..2359192f09e 100644 --- a/js/src/api/subscriptions/personal.spec.js +++ b/js/src/api/subscriptions/personal.spec.js @@ -15,7 +15,6 @@ // along with Parity. If not, see . import sinon from 'sinon'; -import 'sinon-as-promised'; import Personal from './personal'; diff --git a/js/src/ui/Actionbar/actionbar.js b/js/src/ui/Actionbar/actionbar.js index 0141016aba4..49cc77df131 100644 --- a/js/src/ui/Actionbar/actionbar.js +++ b/js/src/ui/Actionbar/actionbar.js @@ -50,8 +50,7 @@ export default class Actionbar extends Component { } return ( - + { buttons } ); diff --git a/js/src/ui/Certifications/certifications.js b/js/src/ui/Certifications/certifications.js index bafd06f35bb..5604ab90afa 100644 --- a/js/src/ui/Certifications/certifications.js +++ b/js/src/ui/Certifications/certifications.js @@ -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 }; diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js index 4cf5a2d7d26..1e0f938096e 100644 --- a/js/src/ui/Icons/index.js +++ b/js/src/ui/Icons/index.js @@ -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 }; diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index 6e508d05ee5..f5694177aef 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . 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 - :
uuid: { uuid }
; + const { address } = account; + const meta = account.meta || {}; return (
- +
- { this.renderName(address) } - + { this.renderName() }
{ address }
- - { uuidText } + { this.renderUuid() }
{ meta.description }
{ this.renderTxCount() }
-
@@ -77,9 +73,7 @@ export default class Header extends Component { - +
{ children } @@ -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 ( - } /> + + } /> ); } @@ -114,7 +115,31 @@ export default class Header extends Component { return (
- { txCount.toFormat() } outgoing transactions + +
+ ); + } + + renderUuid () { + const { uuid } = this.props.account; + + if (!uuid) { + return null; + } + + return ( +
+
); } diff --git a/js/src/views/Account/Header/header.spec.js b/js/src/views/Account/Header/header.spec.js new file mode 100644 index 00000000000..5ae5104d2e4 --- /dev/null +++ b/js/src/views/Account/Header/header.spec.js @@ -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 . + +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( +
+ ); + 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; + }); + }); +}); diff --git a/js/src/views/Account/Transactions/store.js b/js/src/views/Account/Transactions/store.js new file mode 100644 index 00000000000..d59595c441c --- /dev/null +++ b/js/src/views/Account/Transactions/store.js @@ -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 . + +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 + }; + }); + }); + } +} diff --git a/js/src/views/Account/Transactions/store.spec.js b/js/src/views/Account/Transactions/store.spec.js new file mode 100644 index 00000000000..a25b58d2982 --- /dev/null +++ b/js/src/views/Account/Transactions/store.spec.js @@ -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 . + +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 + } + ]); + }); + }); + }); + }); +}); diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index eb11e8def4a..5e48d5c5c3d 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -14,15 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +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 ( - + + }> { this.renderTransactionList() } { this.renderEtherscanFooter() } @@ -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 ( ); @@ -81,85 +84,29 @@ class Transactions extends Component { return ( ); } renderEtherscanFooter () { - const { traceMode } = this.props; + const { isTracing } = this.store; - if (traceMode) { + if (isTracing) { return null; } return (
- Transaction list powered by etherscan.io + etherscan.io + } } />
); } - - 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) { diff --git a/js/src/views/Account/Transactions/transactions.spec.js b/js/src/views/Account/Transactions/transactions.spec.js new file mode 100644 index 00000000000..53f55b524bb --- /dev/null +++ b/js/src/views/Account/Transactions/transactions.spec.js @@ -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 . + +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( + , + { 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/); + }); + }); +}); diff --git a/js/src/views/Account/Transactions/transactions.test.js b/js/src/views/Account/Transactions/transactions.test.js new file mode 100644 index 00000000000..4b7b679b651 --- /dev/null +++ b/js/src/views/Account/Transactions/transactions.test.js @@ -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 . + +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 +}; diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index f274d8fbe2b..e3c4d977612 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -14,47 +14,38 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +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 shapeshiftBtn from '~/../assets/images/shapeshift-btn.png'; 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 shapeshiftBtn from '~/../assets/images/shapeshift-btn.png'; +import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons'; import Header from './Header'; +import Store from './store'; import Transactions from './Transactions'; -import { setVisibleAccounts } from '~/redux/providers/personalActions'; -import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions'; - 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) }
+ balance={ balance } /> @@ -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 = [