UI component updates (#4010)

* Update blockStatus & test results

* IdentityIcon rendering tests for #3950

* Update IdentityName with external messages

* Expand to cover basic layout sections

* ConfirmDialog rendering tests

* TxHash expansion & tests

* Cleanup ui/*.spec.js PropType warnings

* Use react-intl plural for confirmation/confirmations (verified manually)
This commit is contained in:
Jaco Greeff 2017-01-05 12:06:46 +01:00 committed by GitHub
parent 602a4429cc
commit ddeb06d9cc
16 changed files with 652 additions and 69 deletions

View File

@ -21,7 +21,7 @@ const TEST_ENV = process.env.NODE_ENV === 'test';
export function createIdentityImg (address, scale = 8) { export function createIdentityImg (address, scale = 8) {
return TEST_ENV return TEST_ENV
? '' ? 'test-createIdentityImg'
: blockies({ : blockies({
seed: (address || '').toLowerCase(), seed: (address || '').toLowerCase(),
size: 8, size: 8,

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -39,7 +40,12 @@ class BlockStatus extends Component {
if (!syncing) { if (!syncing) {
return ( return (
<div className={ styles.blockNumber }> <div className={ styles.blockNumber }>
{ blockNumber.toFormat() } best block <FormattedMessage
id='ui.blockStatus.bestBlock'
defaultMessage='{blockNumber} best block'
values={ {
blockNumber: blockNumber.toFormat()
} } />
</div> </div>
); );
} }
@ -47,26 +53,45 @@ class BlockStatus extends Component {
if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) { if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
return ( return (
<div className={ styles.syncStatus }> <div className={ styles.syncStatus }>
{ syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2) }% warp restore <FormattedMessage
id='ui.blockStatus.warpRestore'
defaultMessage='{percentage}% warp restore'
values={ {
percentage: syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2)
} } />
</div> </div>
); );
} }
let syncStatus = null;
let warpStatus = null; let warpStatus = null;
if (syncing.currentBlock && syncing.highestBlock) {
syncStatus = (
<span>
<FormattedMessage
id='ui.blockStatus.syncStatus'
defaultMessage='{currentBlock}/{highestBlock} syncing'
values={ {
currentBlock: syncing.currentBlock.toFormat(),
highestBlock: syncing.highestBlock.toFormat()
} } />
</span>
);
}
if (syncing.blockGap) { if (syncing.blockGap) {
const [first, last] = syncing.blockGap; const [first, last] = syncing.blockGap;
warpStatus = ( warpStatus = (
<span>, { first.mul(100).div(last).toFormat(2) }% historic</span> <span>
); <FormattedMessage
} id='ui.blockStatus.warpStatus'
defaultMessage=', {percentage}% historic'
let syncStatus = null; values={ {
percentage: first.mul(100).div(last).toFormat(2)
if (syncing && syncing.currentBlock && syncing.highestBlock) { } } />
syncStatus = ( </span>
<span>{ syncing.currentBlock.toFormat() }/{ syncing.highestBlock.toFormat() } syncing</span>
); );
} }

View File

@ -0,0 +1,94 @@
// 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 sinon from 'sinon';
import BlockStatus from './';
let component;
function createRedux (syncing = false, blockNumber = new BigNumber(123)) {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
nodeStatus: {
blockNumber,
syncing
}
};
}
};
}
function render (reduxStore = createRedux(), props) {
component = shallow(
<BlockStatus { ...props } />,
{ context: { store: reduxStore } }
).find('BlockStatus').shallow();
return component;
}
describe('ui/BlockStatus', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('renders null with no blockNumber', () => {
expect(render(createRedux(false, null)).find('div')).to.have.length(0);
});
it('renders only the best block when syncing === false', () => {
const messages = render().find('FormattedMessage');
expect(messages).to.have.length(1);
expect(messages).to.have.id('ui.blockStatus.bestBlock');
});
it('renders only the warp restore status when restoring', () => {
const messages = render(createRedux({
warpChunksAmount: new BigNumber(100),
warpChunksProcessed: new BigNumber(5)
})).find('FormattedMessage');
expect(messages).to.have.length(1);
expect(messages).to.have.id('ui.blockStatus.warpRestore');
});
it('renders the current/highest when syncing', () => {
const messages = render(createRedux({
currentBlock: new BigNumber(123),
highestBlock: new BigNumber(456)
})).find('FormattedMessage');
expect(messages).to.have.length(1);
expect(messages).to.have.id('ui.blockStatus.syncStatus');
});
it('renders warp blockGap when catching up', () => {
const messages = render(createRedux({
blockGap: [new BigNumber(123), new BigNumber(456)]
})).find('FormattedMessage');
expect(messages).to.have.length(1);
expect(messages).to.have.id('ui.blockStatus.warpStatus');
});
});

View File

@ -19,7 +19,11 @@ import { shallow } from 'enzyme';
import Button from './button'; import Button from './button';
function renderShallow (props) { function render (props = {}) {
if (props && props.label === undefined) {
props.label = 'test';
}
return shallow( return shallow(
<Button { ...props } /> <Button { ...props } />
); );
@ -28,11 +32,11 @@ function renderShallow (props) {
describe('ui/Button', () => { describe('ui/Button', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
expect(renderShallow()).to.be.ok; expect(render()).to.be.ok;
}); });
it('renders with the specified className', () => { it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass'); expect(render({ className: 'testClass' })).to.have.className('testClass');
}); });
}); });
}); });

View File

@ -15,16 +15,27 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ActionDone from 'material-ui/svg-icons/action/done'; import { FormattedMessage } from 'react-intl';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { nodeOrStringProptype } from '~/util/proptypes'; import { nodeOrStringProptype } from '~/util/proptypes';
import Button from '../Button'; import Button from '../Button';
import Modal from '../Modal'; import Modal from '../Modal';
import { CancelIcon, CheckIcon } from '../Icons';
import styles from './confirmDialog.css'; import styles from './confirmDialog.css';
const DEFAULT_NO = (
<FormattedMessage
id='ui.confirmDialog.no'
defaultMessage='no' />
);
const DEFAULT_YES = (
<FormattedMessage
id='ui.confirmDialog.yes'
defaultMessage='yes' />
);
export default class ConfirmDialog extends Component { export default class ConfirmDialog extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
@ -33,10 +44,10 @@ export default class ConfirmDialog extends Component {
iconDeny: PropTypes.node, iconDeny: PropTypes.node,
labelConfirm: PropTypes.string, labelConfirm: PropTypes.string,
labelDeny: PropTypes.string, labelDeny: PropTypes.string,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onDeny: PropTypes.func.isRequired onDeny: PropTypes.func.isRequired,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired
} }
render () { render () {
@ -60,12 +71,12 @@ export default class ConfirmDialog extends Component {
return [ return [
<Button <Button
label={ labelDeny || 'no' } icon={ iconDeny || <CancelIcon /> }
icon={ iconDeny || <ContentClear /> } label={ labelDeny || DEFAULT_NO }
onClick={ onDeny } />, onClick={ onDeny } />,
<Button <Button
label={ labelConfirm || 'yes' } icon={ iconConfirm || <CheckIcon /> }
icon={ iconConfirm || <ActionDone /> } label={ labelConfirm || DEFAULT_YES }
onClick={ onConfirm } /> onClick={ onConfirm } />
]; ];
} }

View File

@ -0,0 +1,157 @@
// 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, { PropTypes } from 'react';
import sinon from 'sinon';
import muiTheme from '../Theme';
import ConfirmDialog from './';
let component;
let instance;
let onConfirm;
let onDeny;
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
settings: {
backgroundSeed: 'xyz'
}
};
}
};
}
function render (props = {}) {
onConfirm = sinon.stub();
onDeny = sinon.stub();
if (props.visible === undefined) {
props.visible = true;
}
const baseComponent = shallow(
<ConfirmDialog
{ ...props }
title='test title'
onConfirm={ onConfirm }
onDeny={ onDeny }>
<div id='testContent'>
some test content
</div>
</ConfirmDialog>
);
instance = baseComponent.instance();
component = baseComponent.find('Connect(Modal)').shallow({
childContextTypes: {
muiTheme: PropTypes.object,
store: PropTypes.object
},
context: {
muiTheme,
store: createRedux()
}
});
return component;
}
describe('ui/ConfirmDialog', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('renders the body as provided', () => {
expect(render().find('div[id="testContent"]').text()).to.equal('some test content');
});
describe('properties', () => {
let props;
beforeEach(() => {
props = render().props();
});
it('passes the actions', () => {
expect(props.actions).to.deep.equal(instance.renderActions());
});
it('passes title', () => {
expect(props.title).to.equal('test title');
});
it('passes visiblity flag', () => {
expect(props.visible).to.be.true;
});
});
describe('renderActions', () => {
describe('defaults', () => {
let buttons;
beforeEach(() => {
render();
buttons = instance.renderActions();
});
it('renders with supplied onConfim/onDeny callbacks', () => {
expect(buttons[0].props.onClick).to.deep.equal(onDeny);
expect(buttons[1].props.onClick).to.deep.equal(onConfirm);
});
it('renders default labels', () => {
expect(buttons[0].props.label.props.id).to.equal('ui.confirmDialog.no');
expect(buttons[1].props.label.props.id).to.equal('ui.confirmDialog.yes');
});
it('renders default icons', () => {
expect(buttons[0].props.icon.type.displayName).to.equal('ContentClear');
expect(buttons[1].props.icon.type.displayName).to.equal('NavigationCheck');
});
});
describe('overrides', () => {
let buttons;
beforeEach(() => {
render({
labelConfirm: 'labelConfirm',
labelDeny: 'labelDeny',
iconConfirm: 'iconConfirm',
iconDeny: 'iconDeny'
});
buttons = instance.renderActions();
});
it('renders supplied labels', () => {
expect(buttons[0].props.label).to.equal('labelDeny');
expect(buttons[1].props.label).to.equal('labelConfirm');
});
it('renders supplied icons', () => {
expect(buttons[0].props.icon).to.equal('iconDeny');
expect(buttons[1].props.icon).to.equal('iconConfirm');
});
});
});
});

View File

@ -19,7 +19,7 @@ import { shallow } from 'enzyme';
import Container from './container'; import Container from './container';
function renderShallow (props) { function render (props) {
return shallow( return shallow(
<Container { ...props } /> <Container { ...props } />
); );
@ -28,11 +28,24 @@ function renderShallow (props) {
describe('ui/Container', () => { describe('ui/Container', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
expect(renderShallow()).to.be.ok; expect(render()).to.be.ok;
}); });
it('renders with the specified className', () => { it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass'); expect(render({ className: 'testClass' })).to.have.className('testClass');
});
});
describe('sections', () => {
it('renders the Card', () => {
expect(render().find('Card')).to.have.length(1);
});
it('renders the Title', () => {
const title = render({ title: 'title' }).find('Title');
expect(title).to.have.length(1);
expect(title.props().title).to.equal('title');
}); });
}); });
}); });

View File

@ -29,6 +29,7 @@ const api = {
const store = { const store = {
estimated: '123', estimated: '123',
histogram: {},
priceDefault: '456', priceDefault: '456',
totalValue: '789', totalValue: '789',
setGas: sinon.stub(), setGas: sinon.stub(),

View File

@ -34,8 +34,8 @@ class IdentityIcon extends Component {
button: PropTypes.bool, button: PropTypes.bool,
center: PropTypes.bool, center: PropTypes.bool,
className: PropTypes.string, className: PropTypes.string,
inline: PropTypes.bool,
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
inline: PropTypes.bool,
padded: PropTypes.bool, padded: PropTypes.bool,
tiny: PropTypes.bool tiny: PropTypes.bool
} }

View File

@ -0,0 +1,120 @@
// 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 { mount } from 'enzyme';
import React, { PropTypes } from 'react';
import sinon from 'sinon';
import muiTheme from '../Theme';
import IdentityIcon from './';
const ADDRESS0 = '0x0000000000000000000000000000000000000000';
const ADDRESS1 = '0x0123456789012345678901234567890123456789';
const ADDRESS2 = '0x9876543210987654321098765432109876543210';
let component;
function createApi () {
return {
dappsUrl: 'dappsUrl/'
};
}
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
images: {
[ADDRESS2]: 'reduxImage'
}
};
}
};
}
function render (props = {}) {
if (props && props.address === undefined) {
props.address = ADDRESS1;
}
component = mount(
<IdentityIcon { ...props } />,
{
childContextTypes: {
api: PropTypes.object,
muiTheme: PropTypes.object
},
context: {
api: createApi(),
muiTheme,
store: createRedux()
}
}
);
return component;
}
describe('ui/IdentityIcon', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('images', () => {
it('renders an <img> with address specified', () => {
const img = render().find('img');
expect(img).to.have.length(1);
expect(img.props().src).to.equal('test-createIdentityImg');
});
it('renders an <img> with redux source when available', () => {
const img = render({ address: ADDRESS2 }).find('img');
expect(img).to.have.length(1);
expect(img.props().src).to.equal('dappsUrl/reduxImage');
});
it('renders an <ContractIcon> with no address specified', () => {
expect(render({ address: null }).find('ActionCode')).to.have.length(1);
});
it('renders an <CancelIcon> with 0x00..00 address specified', () => {
expect(render({ address: ADDRESS0 }).find('ContentClear')).to.have.length(1);
});
});
describe('sizes', () => {
it('renders 56px by default', () => {
expect(render().find('img').props().width).to.equal('56px');
});
it('renders 16px for tiny', () => {
expect(render({ tiny: true }).find('img').props().width).to.equal('16px');
});
it('renders 24px for button', () => {
expect(render({ button: true }).find('img').props().width).to.equal('24px');
});
it('renders 32px for inline', () => {
expect(render({ inline: true }).find('img').props().width).to.equal('32px');
});
});
});

View File

@ -15,13 +15,23 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { isNullAddress } from '~/util/validation'; import { isNullAddress } from '~/util/validation';
import ShortenedHash from '../ShortenedHash'; import ShortenedHash from '../ShortenedHash';
const defaultName = 'UNNAMED'; const defaultName = (
<FormattedMessage
id='ui.identityName.unnamed'
defaultMessage='UNNAMED' />
);
const defaultNameNull = (
<FormattedMessage
id='ui.identityName.null'
defaultMessage='NULL' />
);
class IdentityName extends Component { class IdentityName extends Component {
static propTypes = { static propTypes = {
@ -43,7 +53,7 @@ class IdentityName extends Component {
return null; return null;
} }
const nullName = isNullAddress(address) ? 'null' : null; const nullName = isNullAddress(address) ? defaultNameNull : null;
const addressFallback = nullName || (shorten ? (<ShortenedHash data={ address } />) : address); const addressFallback = nullName || (shorten ? (<ShortenedHash data={ address } />) : address);
const fallback = unknown ? defaultName : addressFallback; const fallback = unknown ? defaultName : addressFallback;
const isUuid = account && account.name === account.uuid; const isUuid = account && account.name === account.uuid;

View File

@ -14,8 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import React from 'react';
import { IntlProvider } from 'react-intl';
import sinon from 'sinon'; import sinon from 'sinon';
import IdentityName from './identityName'; import IdentityName from './identityName';
@ -44,9 +46,11 @@ const STORE = {
function render (props) { function render (props) {
return mount( return mount(
<IdentityName <IntlProvider locale='en'>
store={ STORE } <IdentityName
{ ...props } /> store={ STORE }
{ ...props } />
</IntlProvider>
); );
} }
@ -74,7 +78,7 @@ describe('ui/IdentityName', () => {
}); });
it('renders 0x000...000 as null', () => { it('renders 0x000...000 as null', () => {
expect(render({ address: ADDR_NULL }).text()).to.equal('null'); expect(render({ address: ADDR_NULL }).text()).to.equal('NULL');
}); });
}); });
}); });

View File

@ -16,6 +16,7 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { LinearProgress } from 'material-ui'; import { LinearProgress } from 'material-ui';
@ -33,8 +34,8 @@ class TxHash extends Component {
static propTypes = { static propTypes = {
hash: PropTypes.string.isRequired, hash: PropTypes.string.isRequired,
isTest: PropTypes.bool, isTest: PropTypes.bool,
summary: PropTypes.bool, maxConfirmations: PropTypes.number,
maxConfirmations: PropTypes.number summary: PropTypes.bool
} }
static defaultProps = { static defaultProps = {
@ -43,14 +44,14 @@ class TxHash extends Component {
state = { state = {
blockNumber: new BigNumber(0), blockNumber: new BigNumber(0),
transaction: null, subscriptionId: null,
subscriptionId: null transaction: null
} }
componentDidMount () { componentDidMount () {
const { api } = this.context; const { api } = this.context;
api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => { return api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => {
this.setState({ subscriptionId }); this.setState({ subscriptionId });
}); });
} }
@ -59,28 +60,28 @@ class TxHash extends Component {
const { api } = this.context; const { api } = this.context;
const { subscriptionId } = this.state; const { subscriptionId } = this.state;
api.unsubscribe(subscriptionId); return api.unsubscribe(subscriptionId);
} }
render () { render () {
const { hash, isTest, summary } = this.props; const { hash, isTest, summary } = this.props;
const link = ( const hashLink = (
<a href={ txLink(hash, isTest) } target='_blank'> <a href={ txLink(hash, isTest) } target='_blank'>
<ShortenedHash data={ hash } /> <ShortenedHash data={ hash } />
</a> </a>
); );
let header = (
<p>The transaction has been posted to the network, with a hash of { link }.</p>
);
if (summary) {
header = (<p>{ link }</p>);
}
return ( return (
<div> <div>
{ header } <p>{
summary
? hashLink
: <FormattedMessage
id='ui.txHash.posted'
defaultMessage='The transaction has been posted to the network with a hash of {hashLink}'
values={ { hashLink } } />
}</p>
{ this.renderConfirmations() } { this.renderConfirmations() }
</div> </div>
); );
@ -98,20 +99,22 @@ class TxHash extends Component {
color='white' color='white'
mode='indeterminate' mode='indeterminate'
/> />
<div className={ styles.progressinfo }>waiting for confirmations</div> <div className={ styles.progressinfo }>
<FormattedMessage
id='ui.txHash.waiting'
defaultMessage='waiting for confirmations' />
</div>
</div> </div>
); );
} }
const confirmations = blockNumber.minus(transaction.blockNumber).plus(1); const confirmations = blockNumber.minus(transaction.blockNumber).plus(1);
const value = Math.min(confirmations.toNumber(), maxConfirmations); const value = Math.min(confirmations.toNumber(), maxConfirmations);
let count;
if (confirmations.gt(maxConfirmations)) { let count = confirmations.toFormat(0);
count = confirmations.toFormat(0); if (confirmations.lte(maxConfirmations)) {
} else { count = `${count}/${maxConfirmations}`;
count = confirmations.toFormat(0) + `/${maxConfirmations}`;
} }
const unit = value === 1 ? 'confirmation' : 'confirmations';
return ( return (
<div className={ styles.confirm }> <div className={ styles.confirm }>
@ -121,10 +124,17 @@ class TxHash extends Component {
max={ maxConfirmations } max={ maxConfirmations }
value={ value } value={ value }
color='white' color='white'
mode='determinate' mode='determinate' />
/>
<div className={ styles.progressinfo }> <div className={ styles.progressinfo }>
<abbr title={ `block #${blockNumber.toFormat(0)}` }>{ count } { unit }</abbr> <abbr title={ `block #${blockNumber.toFormat(0)}` }>
<FormattedMessage
id='ui.txHash.confirmations'
defaultMessage='{count} {value, plural, one {confirmation} other {confirmations}}'
values={ {
count,
value
} } />
</abbr>
</div> </div>
</div> </div>
); );
@ -138,15 +148,17 @@ class TxHash extends Component {
return; return;
} }
this.setState({ blockNumber }); return api.eth
api.eth
.getTransactionReceipt(hash) .getTransactionReceipt(hash)
.then((transaction) => { .then((transaction) => {
this.setState({ transaction }); this.setState({
blockNumber,
transaction
});
}) })
.catch((error) => { .catch((error) => {
console.warn('onBlockNumber', error); console.warn('onBlockNumber', error);
this.setState({ blockNumber });
}); });
} }
} }

View File

@ -0,0 +1,132 @@
// 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 sinon from 'sinon';
import TxHash from './';
const TXHASH = '0xabcdef123454321abcdef';
let api;
let blockNumber;
let callback;
let component;
let instance;
function createApi () {
blockNumber = new BigNumber(100);
api = {
eth: {
getTransactionReceipt: (hash) => {
return Promise.resolve({
blockNumber: new BigNumber(100),
hash
});
}
},
nextBlock: (increment = 1) => {
blockNumber = blockNumber.plus(increment);
return callback(null, blockNumber);
},
subscribe: (type, _callback) => {
callback = _callback;
return callback(null, blockNumber).then(() => {
return Promise.resolve(1);
});
},
unsubscribe: sinon.stub().resolves(true)
};
return api;
}
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
nodeStatus: { isTest: true }
};
}
};
}
function render (props) {
const baseComponent = shallow(
<TxHash
hash={ TXHASH }
{ ...props } />,
{ context: { store: createRedux() } }
);
component = baseComponent.find('TxHash').shallow({ context: { api: createApi() } });
instance = component.instance();
return component;
}
describe('ui/TxHash', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders the summary', () => {
expect(component.find('p').find('FormattedMessage').props().id).to.equal('ui.txHash.posted');
});
describe('renderConfirmations', () => {
describe('with no transaction retrieved', () => {
let child;
beforeEach(() => {
child = shallow(instance.renderConfirmations());
});
it('renders indeterminate progressbar', () => {
expect(child.find('LinearProgress[mode="indeterminate"]')).to.have.length(1);
});
it('renders waiting text', () => {
expect(child.find('FormattedMessage').props().id).to.equal('ui.txHash.waiting');
});
});
describe('with transaction retrieved', () => {
let child;
beforeEach(() => {
return instance.componentDidMount().then(() => {
child = shallow(instance.renderConfirmations());
});
});
it('renders determinate progressbar', () => {
expect(child.find('LinearProgress[mode="determinate"]')).to.have.length(1);
});
it('renders confirmation text', () => {
expect(child.find('FormattedMessage').props().id).to.equal('ui.txHash.confirmations');
});
});
});
});

View File

@ -25,7 +25,7 @@ import TxRow from './txRow';
const api = new Api({ execute: sinon.stub() }); const api = new Api({ execute: sinon.stub() });
function renderShallow (props) { function render (props) {
return shallow( return shallow(
<TxRow <TxRow
{ ...props } />, { ...props } />,
@ -33,7 +33,7 @@ function renderShallow (props) {
); );
} }
describe('ui/TxRow', () => { describe('ui/TxList/TxRow', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
const block = { const block = {
@ -45,7 +45,7 @@ describe('ui/TxRow', () => {
value: new BigNumber(1) value: new BigNumber(1)
}; };
expect(renderShallow({ block, tx })).to.be.ok; expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok;
}); });
}); });
}); });

View File

@ -36,7 +36,7 @@ const STORE = {
} }
}; };
function renderShallow (props) { function render (props) {
return shallow( return shallow(
<TxList <TxList
store={ STORE } store={ STORE }
@ -48,7 +48,7 @@ function renderShallow (props) {
describe('ui/TxList', () => { describe('ui/TxList', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
expect(renderShallow()).to.be.ok; expect(render({ address: '0x123', hashes: [] })).to.be.ok;
}); });
}); });
}); });