From e555c124b6b96ba9d617bc74f942cba52cbc7fc2 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Tue, 10 Oct 2017 12:38:17 +0200 Subject: [PATCH 01/26] Fix login redirect to preserve search params --- src/components/privateRoute/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/privateRoute/index.js b/src/components/privateRoute/index.js index 186e55b8a..e7b980823 100644 --- a/src/components/privateRoute/index.js +++ b/src/components/privateRoute/index.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; export const PrivateRouteRender = ({ render, isAuthenticated, ...rest }) => ( ( - isAuthenticated ? render(matchProps) : + isAuthenticated ? render(matchProps) : )}/> ); From d80f06bda27cd80a491b2f67b1b43f56781a84c0 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Tue, 10 Oct 2017 13:46:57 +0200 Subject: [PATCH 02/26] Change getDelegate to accept options instead of publicKey so that it can be used with "address" --- src/store/middlewares/account.js | 2 +- src/store/middlewares/login.js | 2 +- src/utils/api/delegate.js | 4 ++-- src/utils/api/delegate.test.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/store/middlewares/account.js b/src/store/middlewares/account.js index 95d41a2ce..e6329c672 100644 --- a/src/store/middlewares/account.js +++ b/src/store/middlewares/account.js @@ -65,7 +65,7 @@ const delegateRegistration = (store, action) => { const state = store.getState(); if (delegateRegistrationTx) { - getDelegate(state.peers.data, state.account.publicKey) + getDelegate(state.peers.data, { publicKey: state.account.publicKey }) .then((delegateData) => { store.dispatch(accountLoggedIn(Object.assign({}, { delegate: delegateData.delegate, isDelegate: true }))); diff --git a/src/store/middlewares/login.js b/src/store/middlewares/login.js index d8033ee79..c4716a8b4 100644 --- a/src/store/middlewares/login.js +++ b/src/store/middlewares/login.js @@ -24,7 +24,7 @@ const loginMiddleware = store => next => (action) => { // redirect to main/transactions return getAccount(activePeer, address).then(accountData => - getDelegate(activePeer, publicKey) + getDelegate(activePeer, { publicKey }) .then((delegateData) => { store.dispatch(accountLoggedIn(Object.assign({}, accountData, accountBasics, { delegate: delegateData.delegate, isDelegate: true }))); diff --git a/src/utils/api/delegate.js b/src/utils/api/delegate.js index 1f8532a4a..56e6c5e80 100644 --- a/src/utils/api/delegate.js +++ b/src/utils/api/delegate.js @@ -7,8 +7,8 @@ export const listAccountDelegates = (activePeer, address) => export const listDelegates = (activePeer, options) => requestToActivePeer(activePeer, `delegates/${options.q ? 'search' : ''}`, options); -export const getDelegate = (activePeer, publicKey) => - requestToActivePeer(activePeer, 'delegates/get', { publicKey }); +export const getDelegate = (activePeer, options) => + requestToActivePeer(activePeer, 'delegates/get', options); export const vote = (activePeer, secret, publicKey, voteList, unvoteList, secondSecret = null) => requestToActivePeer(activePeer, 'accounts/delegates', { diff --git a/src/utils/api/delegate.test.js b/src/utils/api/delegate.test.js index f4ef70bcb..2e86123c6 100644 --- a/src/utils/api/delegate.test.js +++ b/src/utils/api/delegate.test.js @@ -63,7 +63,7 @@ describe('Utils: Delegate', () => { const mockedPromise = new Promise((resolve) => { resolve(); }); peersMock.expects('requestToActivePeer').withArgs(activePeer, 'delegates/get', options).returns(mockedPromise); - const returnedPromise = getDelegate(activePeer, options.publicKey); + const returnedPromise = getDelegate(activePeer, options); expect(returnedPromise).to.equal(mockedPromise); }); }); From 6dbf057d95ea2573d630ca45a79fb421ccbd2155 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Tue, 10 Oct 2017 13:49:25 +0200 Subject: [PATCH 03/26] Create voteUrlProcessor component --- src/components/voteUrlProcessor/index.js | 23 ++++ .../voteUrlProcessor/voteUrlProcessor.css | 24 ++++ .../voteUrlProcessor/voteUrlProcessor.js | 108 ++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/components/voteUrlProcessor/index.js create mode 100644 src/components/voteUrlProcessor/voteUrlProcessor.css create mode 100644 src/components/voteUrlProcessor/voteUrlProcessor.js diff --git a/src/components/voteUrlProcessor/index.js b/src/components/voteUrlProcessor/index.js new file mode 100644 index 000000000..880bd62a5 --- /dev/null +++ b/src/components/voteUrlProcessor/index.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { translate } from 'react-i18next'; +import { withRouter } from 'react-router'; + +import { votePlaced, voteToggled } from '../../actions/voting'; +import VoteUrlProcessor from './voteUrlProcessor'; + +const mapStateToProps = state => ({ + votes: state.voting.votes, + activePeer: state.peers.data, +}); + +const mapDispatchToProps = dispatch => ({ + votePlaced: data => dispatch(votePlaced(data)), + voteToggled: data => dispatch(voteToggled(data)), +}); + +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)( + translate()(VoteUrlProcessor), + ), +); + diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.css b/src/components/voteUrlProcessor/voteUrlProcessor.css new file mode 100644 index 000000000..a3534a4de --- /dev/null +++ b/src/components/voteUrlProcessor/voteUrlProcessor.css @@ -0,0 +1,24 @@ +.error, +.success { + padding: 16px 24px; + margin: 0 -24px; + color: black; + border-bottom: 1px solid #e6e6e6; +} + +.error:first-child, +.success:first-child { + margin-top: -24px; +} + +.error { + background-color: rgb(255, 228, 220) !important; +} + +.success { + background-color: rgb(226, 238, 213) !important; +} + +.chip { + margin: 6px 1px -8px 1px; +} diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js new file mode 100644 index 000000000..9bf180641 --- /dev/null +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -0,0 +1,108 @@ +import Chip from 'react-toolbox/lib/chip'; +import React from 'react'; + +import { getDelegate } from '../../utils/api/delegate'; +import styles from './voteUrlProcessor.css'; + +export default class VoteUrlProcessor extends React.Component { + constructor() { + super(); + this.state = { + upvotes: [], + downvotes: [], + notFound: [], + alreadyVoted: [], + notVotedYet: [], + }; + } + // eslint-disable-next-line class-methods-use-this + parseParams(search) { + return search.replace(/^\?/, '').split('&').reduce((acc, param) => { + const keyValue = param.split('='); + if (keyValue[0] !== '' && keyValue[1] !== 'undefined') { + acc[keyValue[0]] = keyValue[1]; + } + return acc; + }, {}); + } + + componentDidMount() { + const params = this.parseParams(this.props.history.location.search); + if (params.upvote) { + params.upvote.split(',').forEach(this.processUpvote.bind(this)); + } + if (params.downvote) { + params.downvote.split(',').forEach(this.processDownvote.bind(this)); + } + } + + processUpvote(username) { + getDelegate(this.props.activePeer, { username }).then((data) => { + const vote = this.props.votes[username]; + if (!vote || (!vote.confirmed && !vote.unconfirmed)) { + this.props.voteToggled({ username, publicKey: data.delegate.publicKey }); + this.pushLookupResult('upvotes', username); + } else { + this.pushLookupResult('alreadyVoted', username); + } + }).catch(() => { + this.pushLookupResult('notFound', username); + }); + } + + processDownvote(username) { + getDelegate(this.props.activePeer, { username }).then((data) => { + const vote = this.props.votes[username]; + if (vote && vote.confirmed && !vote.unconfirmed) { + this.props.voteToggled({ username, publicKey: data.delegate.publicKey }); + this.pushLookupResult('downvotes', username); + } else { + this.pushLookupResult('notVotedYet', username); + } + }).catch(() => { + this.pushLookupResult('notFound', username); + }); + } + + pushLookupResult(list, username) { + this.setState({ + [list]: [...this.state[list], username], + }); + } + + render() { + const errorStates = { + notFound: this.props.t('{{count}} delegate names could not be resolved:', + { count: this.state.notFound.length }), + alreadyVoted: this.props.t('{{count}} delegate names selected for upvote were already voted for:', + { count: this.state.alreadyVoted.length }), + notVotedYet: this.props.t('{{count}} delegate names selected for downvote were not voted for:', + { count: this.state.notVotedYet.length }), + }; + const successStates = { + upvotes: this.props.t('{{count}} delegate names successfully resolved to add vote to.', + { count: this.state.upvotes.length }), + downvotes: this.props.t('{{count}} delegate names successfully resolved to remove vote from.', + { count: this.state.upvotes.length }), + }; + return ( +
+ {Object.keys(errorStates).map(list => ( + this.state[list].length ? ( +
+ {errorStates[list]} + {this.state[list].map(username => ( + {username} + ))} +
+ ) : null + ))} + {Object.keys(successStates).map(list => ( + this.state[list].length ? ( +
{successStates[list]}
+ ) : null + ))} +
+ ); + } +} From 145a3593edd27a2c26cd085c5c5e3d717d29da05 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Tue, 10 Oct 2017 13:49:39 +0200 Subject: [PATCH 04/26] Use voteUrlPreprocessor in voteDialog --- src/components/voteDialog/index.test.js | 18 +++++---- src/components/voteDialog/voteDialog.js | 12 +++--- src/components/voteDialog/voteDialog.test.js | 39 +++++++++++++++----- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/components/voteDialog/index.test.js b/src/components/voteDialog/index.test.js index d9cd01aaf..f932ebad0 100644 --- a/src/components/voteDialog/index.test.js +++ b/src/components/voteDialog/index.test.js @@ -1,9 +1,9 @@ import React from 'react'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; import chai, { expect } from 'chai'; +import { BrowserRouter as Router } from 'react-router-dom'; import { mount } from 'enzyme'; import chaiEnzyme from 'chai-enzyme'; +import PropTypes from 'prop-types'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import configureMockStore from 'redux-mock-store'; @@ -49,12 +49,16 @@ const store = configureMockStore([])({ describe('VoteDialog HOC', () => { let wrapper; + const options = { + context: { store, history, i18n }, + childContextTypes: { + store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + i18n: PropTypes.object.isRequired, + }, + }; beforeEach(() => { - wrapper = mount( - - - - ); + wrapper = mount(, options); }); it('should render VoteDialog', () => { diff --git a/src/components/voteDialog/voteDialog.js b/src/components/voteDialog/voteDialog.js index 60653ea66..8420226a4 100644 --- a/src/components/voteDialog/voteDialog.js +++ b/src/components/voteDialog/voteDialog.js @@ -1,12 +1,13 @@ import React from 'react'; -import InfoParagraph from '../infoParagraph'; +import { authStatePrefill, authStateIsValid } from '../../utils/form'; import ActionBar from '../actionBar'; -import Fees from '../../constants/fees'; -import votingConst from '../../constants/voting'; +import AuthInputs from '../authInputs'; import Autocomplete from './voteAutocomplete'; +import Fees from '../../constants/fees'; +import InfoParagraph from '../infoParagraph'; +import VoteUrlProcessor from '../voteUrlProcessor'; import styles from './voteDialog.css'; -import AuthInputs from '../authInputs'; -import { authStatePrefill, authStateIsValid } from '../../utils/form'; +import votingConst from '../../constants/voting'; const { maxCountOfVotes, maxCountOfVotesInOneTurn } = votingConst; @@ -51,6 +52,7 @@ export default class VoteDialog extends React.Component { return (
+ mount({node}, context); + const ordinaryAccount = { passphrase: 'pass', publicKey: 'key', @@ -15,9 +18,11 @@ const ordinaryAccount = { balance: 10e8, }; const accountWithSecondPassphrase = { - passphrase: 'pass', + passphrase: 'awkward service glimpse punch genre calm grow life bullet boil match like', + secondPassphrase: 'forest around decrease farm vanish permit hotel clay senior matter endorse domain', publicKey: 'key', secondSignature: 1, + balance: 10e8, }; const votes = { username1: { publicKey: 'sample_key', confirmed: true, unconfirmed: false }, @@ -28,14 +33,15 @@ const delegates = [ { username: 'username2', publicKey: '123HG3522345L' }, ]; -const store = configureMockStore([])({ +const state = { account: ordinaryAccount, voting: { votes, delegates, }, peers: { data: {} }, -}); +}; +const store = configureMockStore([])(state); let props; describe('VoteDialog', () => { @@ -60,7 +66,7 @@ describe('VoteDialog', () => { describe('Ordinary account', () => { beforeEach(() => { - wrapper = mount(, options); + wrapper = mountWithRouter(, options); }); it('should render an InfoParagraph', () => { @@ -103,7 +109,8 @@ describe('VoteDialog', () => { votePlaced: () => {}, t: key => key, }; - const mounted = mount(, options); + const mounted = mountWithRouter( + , options); const primaryButton = mounted.find('VoteDialog .primary-button button'); expect(primaryButton.props().disabled).to.be.equal(true); @@ -112,9 +119,20 @@ describe('VoteDialog', () => { describe('Account with second passphrase', () => { it('should fire votePlaced action with the provided secondPassphrase', () => { - wrapper = mount(, options); - const secondPassphrase = 'test second passphrase'; - wrapper.instance().handleChange('secondPassphrase', secondPassphrase); + wrapper = mountWithRouter( + , + { + ...options, + context: { + ...options.context, + store: configureMockStore([])({ + ...state, + account: accountWithSecondPassphrase, + }), + }, + }); + + wrapper.find('.second-passphrase input').simulate('change', { target: { value: accountWithSecondPassphrase.secondPassphrase } }); wrapper.find('.primary-button button').simulate('click'); expect(props.votePlaced).to.have.been.calledWith({ @@ -122,7 +140,7 @@ describe('VoteDialog', () => { account: accountWithSecondPassphrase, votes, passphrase: accountWithSecondPassphrase.passphrase, - secondSecret: secondPassphrase, + secondSecret: accountWithSecondPassphrase.secondPassphrase, }); }); }); @@ -133,7 +151,8 @@ describe('VoteDialog', () => { extraVotes[`standby_${i}`] = { confirmed: false, unconfirmed: true, publicKey: `public_key_${i}` }; } const noVoteProps = Object.assign({}, props, { votes: extraVotes }); - const mounted = mount(, options); + const mounted = mountWithRouter( + , options); const primaryButton = mounted.find('VoteDialog .primary-button button'); expect(primaryButton.props().disabled).to.be.equal(true); From 49c9c302d9afaf316a51c5fc5eb827532aeca842 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Tue, 10 Oct 2017 14:58:52 +0200 Subject: [PATCH 05/26] Update voteUrlProcessor to fetch votes first --- src/components/voteUrlProcessor/index.js | 5 +- .../voteUrlProcessor/voteUrlProcessor.css | 4 + .../voteUrlProcessor/voteUrlProcessor.js | 74 +++++++++++++------ 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/components/voteUrlProcessor/index.js b/src/components/voteUrlProcessor/index.js index 880bd62a5..2199aa6cb 100644 --- a/src/components/voteUrlProcessor/index.js +++ b/src/components/voteUrlProcessor/index.js @@ -2,17 +2,19 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; import { withRouter } from 'react-router'; -import { votePlaced, voteToggled } from '../../actions/voting'; +import { votePlaced, voteToggled, votesAdded } from '../../actions/voting'; import VoteUrlProcessor from './voteUrlProcessor'; const mapStateToProps = state => ({ votes: state.voting.votes, + account: state.account, activePeer: state.peers.data, }); const mapDispatchToProps = dispatch => ({ votePlaced: data => dispatch(votePlaced(data)), voteToggled: data => dispatch(voteToggled(data)), + votesAdded: data => dispatch(votesAdded(data)), }); export default withRouter( @@ -20,4 +22,3 @@ export default withRouter( translate()(VoteUrlProcessor), ), ); - diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.css b/src/components/voteUrlProcessor/voteUrlProcessor.css index a3534a4de..b5b439135 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.css +++ b/src/components/voteUrlProcessor/voteUrlProcessor.css @@ -22,3 +22,7 @@ .chip { margin: 6px 1px -8px 1px; } + +.center { + text-align: center; +} diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index 9bf180641..883cb8147 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -1,7 +1,8 @@ import Chip from 'react-toolbox/lib/chip'; +import ProgressBar from 'react-toolbox/lib/progress_bar'; import React from 'react'; -import { getDelegate } from '../../utils/api/delegate'; +import { getDelegate, listAccountDelegates } from '../../utils/api/delegate'; import styles from './voteUrlProcessor.css'; export default class VoteUrlProcessor extends React.Component { @@ -14,7 +15,9 @@ export default class VoteUrlProcessor extends React.Component { alreadyVoted: [], notVotedYet: [], }; + this.voteCount = 0; } + // eslint-disable-next-line class-methods-use-this parseParams(search) { return search.replace(/^\?/, '').split('&').reduce((acc, param) => { @@ -28,11 +31,21 @@ export default class VoteUrlProcessor extends React.Component { componentDidMount() { const params = this.parseParams(this.props.history.location.search); - if (params.upvote) { - params.upvote.split(',').forEach(this.processUpvote.bind(this)); - } - if (params.downvote) { - params.downvote.split(',').forEach(this.processDownvote.bind(this)); + if (params.upvote || params.downvote) { + listAccountDelegates(this.props.activePeer, this.props.account.address) + .then(({ delegates }) => { + this.props.votesAdded({ list: delegates }); + if (params.downvote) { + const downvotes = params.downvote.split(','); + downvotes.forEach(this.processDownvote.bind(this)); + this.voteCount += downvotes.length; + } + if (params.upvote) { + const upvotes = params.upvote.split(','); + upvotes.forEach(this.processUpvote.bind(this)); + this.voteCount += upvotes.length; + } + }); } } @@ -53,7 +66,7 @@ export default class VoteUrlProcessor extends React.Component { processDownvote(username) { getDelegate(this.props.activePeer, { username }).then((data) => { const vote = this.props.votes[username]; - if (vote && vote.confirmed && !vote.unconfirmed) { + if (vote && vote.confirmed && vote.unconfirmed) { this.props.voteToggled({ username, publicKey: data.delegate.publicKey }); this.pushLookupResult('downvotes', username); } else { @@ -70,6 +83,14 @@ export default class VoteUrlProcessor extends React.Component { }); } + getProccessedCount() { + return this.state.upvotes.length + + this.state.downvotes.length + + this.state.notFound.length + + this.state.notVotedYet.length + + this.state.alreadyVoted.length; + } + render() { const errorStates = { notFound: this.props.t('{{count}} delegate names could not be resolved:', @@ -83,25 +104,34 @@ export default class VoteUrlProcessor extends React.Component { upvotes: this.props.t('{{count}} delegate names successfully resolved to add vote to.', { count: this.state.upvotes.length }), downvotes: this.props.t('{{count}} delegate names successfully resolved to remove vote from.', - { count: this.state.upvotes.length }), + { count: this.state.downvotes.length }), }; return (
- {Object.keys(errorStates).map(list => ( - this.state[list].length ? ( -
- {errorStates[list]} - {this.state[list].map(username => ( - {username} - ))} + {this.getProccessedCount() < this.voteCount ? + (
+ +
+ {this.props.t('Processing delegate names: ')} + {this.getProccessedCount()} / {this.voteCount}
- ) : null - ))} - {Object.keys(successStates).map(list => ( - this.state[list].length ? ( -
{successStates[list]}
- ) : null - ))} +
) : + ({Object.keys(errorStates).map(list => ( + this.state[list].length ? ( +
+ {errorStates[list]} + {this.state[list].map((username, i) => ( + {username} + ))} +
+ ) : null + ))} + {Object.keys(successStates).map(list => ( + this.state[list].length ? ( +
{successStates[list]}
+ ) : null + ))}
)}
); } From ce26e61a24836f58af416a30536849ddf5e42650 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 08:57:07 +0200 Subject: [PATCH 06/26] Refactor voteUrlProcessor --- .../voteUrlProcessor/voteUrlProcessor.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index 883cb8147..f502e2a26 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -92,15 +92,15 @@ export default class VoteUrlProcessor extends React.Component { } render() { - const errorStates = { - notFound: this.props.t('{{count}} delegate names could not be resolved:', + const errorMessages = { + notFound: this.props.t('{{count}} of entered delegate names could not be resolved:', { count: this.state.notFound.length }), - alreadyVoted: this.props.t('{{count}} delegate names selected for upvote were already voted for:', + alreadyVoted: this.props.t('{{count}} of delegate names selected for upvote were already voted for:', { count: this.state.alreadyVoted.length }), - notVotedYet: this.props.t('{{count}} delegate names selected for downvote were not voted for:', + notVotedYet: this.props.t('{{count}} of delegate names selected for downvote were not voted for:', { count: this.state.notVotedYet.length }), }; - const successStates = { + const successMessages = { upvotes: this.props.t('{{count}} delegate names successfully resolved to add vote to.', { count: this.state.upvotes.length }), downvotes: this.props.t('{{count}} delegate names successfully resolved to remove vote from.', @@ -117,19 +117,19 @@ export default class VoteUrlProcessor extends React.Component { {this.getProccessedCount()} / {this.voteCount}
) : - ({Object.keys(errorStates).map(list => ( + ({Object.keys(errorMessages).map(list => ( this.state[list].length ? ( -
- {errorStates[list]} +
+ {errorMessages[list]} {this.state[list].map((username, i) => ( {username} ))}
) : null ))} - {Object.keys(successStates).map(list => ( + {Object.keys(successMessages).map(list => ( this.state[list].length ? ( -
{successStates[list]}
+
{successMessages[list]}
) : null ))})}
From 6cef916c7b70bc17475eedb95ee4a8e91ceceddf Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 08:57:15 +0200 Subject: [PATCH 07/26] Add e2e test for voting dialog launch protocol --- features/step_definitions/generic.step.js | 9 +++++++++ features/voting.feature | 14 ++++++++++++++ src/components/voteDialog/voteAutocomplete.js | 4 ++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/features/step_definitions/generic.step.js b/features/step_definitions/generic.step.js index f749f811b..277453a9c 100644 --- a/features/step_definitions/generic.step.js +++ b/features/step_definitions/generic.step.js @@ -119,6 +119,11 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => { waitForElemAndCheckItsText(selectorClass, text, callback); }); + Then('I should see "{elementName}" element with text:', (elementName, text, callback) => { + const selectorClass = `.${elementName.replace(/ /g, '-')}`; + waitForElemAndCheckItsText(selectorClass, text, callback); + }); + Then('I should see element "{elementName}" that contains text:', (elementName, text, callback) => { const selectorClass = `.${elementName.replace(/ /g, '-')}`; waitForElemAndCheckItsText(selectorClass, text, callback); @@ -136,6 +141,10 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => { waitForElemAndClickIt('.login-button', callback); }); + When('I go to "{url}"', (url, callback) => { + browser.get(`http://localhost:8080/#${url}`).then(callback); + }); + When('I {iterations} times move mouse randomly', (iterations, callback) => { const actions = browser.actions(); /** diff --git a/features/voting.feature b/features/voting.feature index c90313778..4d22fa950 100644 --- a/features/voting.feature +++ b/features/voting.feature @@ -111,3 +111,17 @@ Feature: Voting tab And I click "vote button" And I click "cancel button" Then I should see no "modal dialog" + + Scenario: should allow to select delegates by URL + Given I'm logged in as "delegate candidate" + When I go to "/main/voting/vote?upvote=standby_27,standby_28,standby_29,nonexisting_22&downvote=standby_33" + Then I should see text "3 delegate names successfully resolved to add vote to." in "upvotes message" element + And I should see text "1 of delegate names selected for downvote were not voted for:standby_33" in "notVotedYet message" element + And I should see text "1 of entered delegate names could not be resolved:nonexisting_22" in "notFound message" element + And I should see "vote list" element with text: + """ + standby_27 + standby_28 + standby_29 + """ + diff --git a/src/components/voteDialog/voteAutocomplete.js b/src/components/voteDialog/voteAutocomplete.js index 7ac34b4fa..d705b2bea 100644 --- a/src/components/voteDialog/voteAutocomplete.js +++ b/src/components/voteDialog/voteAutocomplete.js @@ -196,7 +196,7 @@ export class VoteAutocompleteRaw extends React.Component { return (

{this.props.t('Add vote to')}

-
+
{votedList.map( item =>

{this.props.t('Remove vote from')}

-
+
{unvotedList.map( item => Date: Wed, 11 Oct 2017 09:04:42 +0200 Subject: [PATCH 08/26] Fix voteUrlProcessor for new account --- .../voteUrlProcessor/voteUrlProcessor.js | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index f502e2a26..eb23da8bc 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -33,19 +33,21 @@ export default class VoteUrlProcessor extends React.Component { const params = this.parseParams(this.props.history.location.search); if (params.upvote || params.downvote) { listAccountDelegates(this.props.activePeer, this.props.account.address) - .then(({ delegates }) => { - this.props.votesAdded({ list: delegates }); - if (params.downvote) { - const downvotes = params.downvote.split(','); - downvotes.forEach(this.processDownvote.bind(this)); - this.voteCount += downvotes.length; - } - if (params.upvote) { - const upvotes = params.upvote.split(','); - upvotes.forEach(this.processUpvote.bind(this)); - this.voteCount += upvotes.length; - } - }); + .then(({ delegates }) => { this.processUrlVotes(params, delegates); }) + .catch(() => { this.processUrlVotes(params, []); }); + } + } + processUrlVotes(params, votes) { + this.props.votesAdded({ list: votes }); + if (params.downvote) { + const downvotes = params.downvote.split(','); + downvotes.forEach(this.processDownvote.bind(this)); + this.voteCount += downvotes.length; + } + if (params.upvote) { + const upvotes = params.upvote.split(','); + upvotes.forEach(this.processUpvote.bind(this)); + this.voteCount += upvotes.length; } } From 24a28d70789436fc5ae7b8ec630af530f73d21e8 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 15:53:06 +0200 Subject: [PATCH 09/26] Refactor delegate name lookup from voteUrlProcessor to redux --- src/actions/voting.js | 40 ++++++- src/components/voteUrlProcessor/index.js | 20 +++- .../voteUrlProcessor/voteUrlProcessor.js | 104 +++++------------- src/constants/actions.js | 2 + src/store/middlewares/voting.js | 97 ++++++++++++---- src/store/reducers/voting.js | 22 +++- 6 files changed, 182 insertions(+), 103 deletions(-) diff --git a/src/actions/voting.js b/src/actions/voting.js index 7dee77a66..cb47bdc14 100644 --- a/src/actions/voting.js +++ b/src/actions/voting.js @@ -1,9 +1,13 @@ -import { vote, listAccountDelegates, listDelegates } from '../utils/api/delegate'; -import { transactionAdded } from './transactions'; import { errorAlertDialogDisplayed } from './dialog'; +import { + listAccountDelegates, + listDelegates, + vote, +} from '../utils/api/delegate'; import { passphraseUsed } from './account'; -import actionTypes from '../constants/actions'; +import { transactionAdded } from './transactions'; import Fees from '../constants/fees'; +import actionTypes from '../constants/actions'; import transactionTypes from '../constants/transactionTypes'; /** @@ -45,6 +49,22 @@ export const voteToggled = data => ({ data, }); + +/** + * Updates vote lookup status of the given delegate name + */ +export const voteLookupStatusUpdated = data => ({ + type: actionTypes.voteLookupStatusUpdated, + data, +}); + +/** + * Clears all vote lookup statuses + */ +export const voteLookupStatusCleared = () => ({ + type: actionTypes.voteLookupStatusCleared, +}); + /** * Makes Api call to register votes * Adds pending state and then after the duration of one round @@ -122,3 +142,17 @@ export const delegatesFetched = ({ activePeer, q, offset, refresh }) => dispatch(delegatesAdded({ list: delegates, totalDelegates: totalCount, refresh })); }); }; + + +/** + * Get list of delegates current account has voted for and dispatch it with votes from url + */ +export const urlVotesFound = ({ activePeer, upvotes, downvotes, address }) => + (dispatch) => { + const processUrlVotes = (votes) => { + dispatch(votesAdded({ list: votes, upvotes, downvotes })); + }; + listAccountDelegates(activePeer, address) + .then(({ delegates }) => { processUrlVotes(delegates); }) + .catch(() => { processUrlVotes([]); }); + }; diff --git a/src/components/voteUrlProcessor/index.js b/src/components/voteUrlProcessor/index.js index 2199aa6cb..b2805f6bb 100644 --- a/src/components/voteUrlProcessor/index.js +++ b/src/components/voteUrlProcessor/index.js @@ -2,11 +2,27 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; import { withRouter } from 'react-router'; -import { votePlaced, voteToggled, votesAdded } from '../../actions/voting'; +import { + urlVotesFound, + voteLookupStatusCleared, + votePlaced, + voteToggled, + votesAdded, +} from '../../actions/voting'; import VoteUrlProcessor from './voteUrlProcessor'; +const filterObjectPropsWithValue = (object = {}, value) => ( + Object.keys(object).filter(key => object[key] === value) +); + const mapStateToProps = state => ({ votes: state.voting.votes, + voteCount: Object.keys(state.voting.voteLookupStatus || {}).length, + downvotes: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'downvotes'), + upvotes: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'upvotes'), + alreadyVoted: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'alreadyVoted'), + notVotedYet: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'notVotedYet'), + notFound: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'notFound'), account: state.account, activePeer: state.peers.data, }); @@ -15,6 +31,8 @@ const mapDispatchToProps = dispatch => ({ votePlaced: data => dispatch(votePlaced(data)), voteToggled: data => dispatch(voteToggled(data)), votesAdded: data => dispatch(votesAdded(data)), + urlVotesFound: data => dispatch(urlVotesFound(data)), + clearVoteLookupStatus: () => dispatch(voteLookupStatusCleared()), }); export default withRouter( diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index eb23da8bc..1db125a17 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -2,22 +2,9 @@ import Chip from 'react-toolbox/lib/chip'; import ProgressBar from 'react-toolbox/lib/progress_bar'; import React from 'react'; -import { getDelegate, listAccountDelegates } from '../../utils/api/delegate'; import styles from './voteUrlProcessor.css'; export default class VoteUrlProcessor extends React.Component { - constructor() { - super(); - this.state = { - upvotes: [], - downvotes: [], - notFound: [], - alreadyVoted: [], - notVotedYet: [], - }; - this.voteCount = 0; - } - // eslint-disable-next-line class-methods-use-this parseParams(search) { return search.replace(/^\?/, '').split('&').reduce((acc, param) => { @@ -30,107 +17,66 @@ export default class VoteUrlProcessor extends React.Component { } componentDidMount() { + this.props.clearVoteLookupStatus(); const params = this.parseParams(this.props.history.location.search); if (params.upvote || params.downvote) { - listAccountDelegates(this.props.activePeer, this.props.account.address) - .then(({ delegates }) => { this.processUrlVotes(params, delegates); }) - .catch(() => { this.processUrlVotes(params, []); }); + const upvotes = params.upvote ? params.upvote.split(',') : []; + const downvotes = params.downvote ? params.downvote.split(',') : []; + this.props.urlVotesFound({ + activePeer: this.props.activePeer, + upvotes, + downvotes, + address: this.props.account.address, + }); } } - processUrlVotes(params, votes) { - this.props.votesAdded({ list: votes }); - if (params.downvote) { - const downvotes = params.downvote.split(','); - downvotes.forEach(this.processDownvote.bind(this)); - this.voteCount += downvotes.length; - } - if (params.upvote) { - const upvotes = params.upvote.split(','); - upvotes.forEach(this.processUpvote.bind(this)); - this.voteCount += upvotes.length; - } - } - - processUpvote(username) { - getDelegate(this.props.activePeer, { username }).then((data) => { - const vote = this.props.votes[username]; - if (!vote || (!vote.confirmed && !vote.unconfirmed)) { - this.props.voteToggled({ username, publicKey: data.delegate.publicKey }); - this.pushLookupResult('upvotes', username); - } else { - this.pushLookupResult('alreadyVoted', username); - } - }).catch(() => { - this.pushLookupResult('notFound', username); - }); - } - - processDownvote(username) { - getDelegate(this.props.activePeer, { username }).then((data) => { - const vote = this.props.votes[username]; - if (vote && vote.confirmed && vote.unconfirmed) { - this.props.voteToggled({ username, publicKey: data.delegate.publicKey }); - this.pushLookupResult('downvotes', username); - } else { - this.pushLookupResult('notVotedYet', username); - } - }).catch(() => { - this.pushLookupResult('notFound', username); - }); - } - - pushLookupResult(list, username) { - this.setState({ - [list]: [...this.state[list], username], - }); - } getProccessedCount() { - return this.state.upvotes.length + - this.state.downvotes.length + - this.state.notFound.length + - this.state.notVotedYet.length + - this.state.alreadyVoted.length; + return this.props.upvotes.length + + this.props.downvotes.length + + this.props.notFound.length + + this.props.notVotedYet.length + + this.props.alreadyVoted.length; } render() { const errorMessages = { notFound: this.props.t('{{count}} of entered delegate names could not be resolved:', - { count: this.state.notFound.length }), + { count: this.props.notFound.length }), alreadyVoted: this.props.t('{{count}} of delegate names selected for upvote were already voted for:', - { count: this.state.alreadyVoted.length }), + { count: this.props.alreadyVoted.length }), notVotedYet: this.props.t('{{count}} of delegate names selected for downvote were not voted for:', - { count: this.state.notVotedYet.length }), + { count: this.props.notVotedYet.length }), }; const successMessages = { upvotes: this.props.t('{{count}} delegate names successfully resolved to add vote to.', - { count: this.state.upvotes.length }), + { count: this.props.upvotes.length }), downvotes: this.props.t('{{count}} delegate names successfully resolved to remove vote from.', - { count: this.state.downvotes.length }), + { count: this.props.downvotes.length }), }; return (
- {this.getProccessedCount() < this.voteCount ? + {this.getProccessedCount() < this.props.voteCount ? (
+ value={this.getProccessedCount()} max={this.props.voteCount}/>
{this.props.t('Processing delegate names: ')} - {this.getProccessedCount()} / {this.voteCount} + {this.getProccessedCount()} / {this.props.voteCount}
) : ({Object.keys(errorMessages).map(list => ( - this.state[list].length ? ( + this.props[list].length ? (
{errorMessages[list]} - {this.state[list].map((username, i) => ( + {this.props[list].map((username, i) => ( {username} ))}
) : null ))} {Object.keys(successMessages).map(list => ( - this.state[list].length ? ( + this.props[list].length ? (
{successMessages[list]}
) : null ))}
)} diff --git a/src/constants/actions.js b/src/constants/actions.js index 6cb29238d..657e590b1 100644 --- a/src/constants/actions.js +++ b/src/constants/actions.js @@ -15,6 +15,8 @@ const actionTypes = { voteToggled: 'VOTE_TOGGLED', votesAdded: 'VOTES_ADDED', votesUpdated: 'VOTES_UPDATED', + voteLookupStatusCleared: 'VOTE_LOOKUP_STATUS_CLEARED', + voteLookupStatusUpdated: 'VOTE_LOOKUP_STATUS_UPDATED', delegatesAdded: 'DELEGATES_ADDED', pendingVotesAdded: 'PENDING_VOTES_ADDED', toastDisplayed: 'TOAST_DISPLAYED', diff --git a/src/store/middlewares/voting.js b/src/store/middlewares/voting.js index a9de5e7c3..24a8a8dc3 100644 --- a/src/store/middlewares/voting.js +++ b/src/store/middlewares/voting.js @@ -1,31 +1,90 @@ import i18next from 'i18next'; + import { errorToastDisplayed } from '../../actions/toaster'; +import { getDelegate } from '../../utils/api/delegate'; +import { voteLookupStatusUpdated, voteToggled } from '../../actions/voting'; import actionTypes from '../../constants/actions'; import votingConst from '../../constants/voting'; -const votingMiddleware = store => next => (action) => { - next(action); - if (action.type === actionTypes.voteToggled) { - const { votes } = store.getState().voting; - const currentVote = votes[action.data.username] || { unconfirmed: true, confirmed: false }; +const updateLookupStatus = (store, list, username) => { + store.dispatch(voteLookupStatusUpdated({ + username, status: list, + })); +}; - const newVoteCount = Object.keys(votes).filter( - key => votes[key].confirmed !== votes[key].unconfirmed).length; - if (newVoteCount === votingConst.maxCountOfVotesInOneTurn + 1 && - currentVote.unconfirmed !== currentVote.confirmed) { - const label = i18next.t('Maximum of {{n}} votes in one transaction exceeded.', { n: votingConst.maxCountOfVotesInOneTurn }); - const newAction = errorToastDisplayed({ label }); - store.dispatch(newAction); + +const processUpvote = (store, activePeer, username) => { + updateLookupStatus(store, 'pending', username); + getDelegate(activePeer, { username }).then((data) => { + const vote = store.getState().voting.votes[username]; + if (!vote || (!vote.confirmed && !vote.unconfirmed)) { + store.dispatch(voteToggled({ username, publicKey: data.delegate.publicKey })); + updateLookupStatus(store, 'upvotes', username); + } else { + updateLookupStatus(store, 'alreadyVoted', username); } + }).catch(() => { + updateLookupStatus(store, 'notFound', username); + }); +}; - const voteCount = Object.keys(votes).filter( - key => (votes[key].confirmed && !votes[key].unconfirmed) || votes[key].unconfirmed).length; - if (voteCount === votingConst.maxCountOfVotes + 1 && - currentVote.unconfirmed !== currentVote.confirmed) { - const label = i18next.t('Maximum of {{n}} votes exceeded.', { n: votingConst.maxCountOfVotes }); - const newAction = errorToastDisplayed({ label }); - store.dispatch(newAction); +const processDownvote = (store, activePeer, username) => { + updateLookupStatus(store, 'pending', username); + getDelegate(activePeer, { username }).then((data) => { + const vote = store.getState().voting.votes[username]; + if (vote && vote.confirmed && vote.unconfirmed) { + store.dispatch(voteToggled({ username, publicKey: data.delegate.publicKey })); + updateLookupStatus(store, 'downvotes', username); + } else { + updateLookupStatus(store, 'notVotedYet', username); } + }).catch(() => { + updateLookupStatus(store, 'notFound', username); + }); +}; + +const lookupDelegatesFromUrl = (store, action) => { + const { upvotes, downvotes } = action.data; + if (upvotes && downvotes) { + const activePeer = store.getState().peers.data; + downvotes.forEach(processDownvote.bind(this, store, activePeer)); + upvotes.forEach(processUpvote.bind(this, store, activePeer)); + } +}; + +const checkVoteLimits = (store, action) => { + const { votes } = store.getState().voting; + const currentVote = votes[action.data.username] || { unconfirmed: true, confirmed: false }; + + const newVoteCount = Object.keys(votes).filter( + key => votes[key].confirmed !== votes[key].unconfirmed).length; + if (newVoteCount === votingConst.maxCountOfVotesInOneTurn + 1 && + currentVote.unconfirmed !== currentVote.confirmed) { + const label = i18next.t('Maximum of {{n}} votes in one transaction exceeded.', { n: votingConst.maxCountOfVotesInOneTurn }); + const newAction = errorToastDisplayed({ label }); + store.dispatch(newAction); + } + + const voteCount = Object.keys(votes).filter( + key => (votes[key].confirmed && !votes[key].unconfirmed) || votes[key].unconfirmed).length; + if (voteCount === votingConst.maxCountOfVotes + 1 && + currentVote.unconfirmed !== currentVote.confirmed) { + const label = i18next.t('Maximum of {{n}} votes exceeded.', { n: votingConst.maxCountOfVotes }); + const newAction = errorToastDisplayed({ label }); + store.dispatch(newAction); + } +}; + +const votingMiddleware = store => next => (action) => { + next(action); + switch (action.type) { + case actionTypes.voteToggled: + checkVoteLimits(store, action); + break; + case actionTypes.votesAdded: + lookupDelegatesFromUrl(store, action); + break; + default: break; } }; diff --git a/src/store/reducers/voting.js b/src/store/reducers/voting.js index 01926943e..e7395681b 100644 --- a/src/store/reducers/voting.js +++ b/src/store/reducers/voting.js @@ -37,7 +37,12 @@ const mergeVotes = (newList, oldDict) => { * @param {Object} state * @param {Object} action */ -const voting = (state = { votes: {}, delegates: [], totalDelegates: 0 }, action) => { +const voting = (state = { + votes: {}, + delegates: [], + totalDelegates: 0, + voteLookupStatus: {}, +}, action) => { switch (action.type) { case actionTypes.votesAdded: return Object.assign({}, state, { @@ -120,6 +125,21 @@ const voting = (state = { votes: {}, delegates: [], totalDelegates: 0 }, action) }, {}), }); + case actionTypes.voteLookupStatusUpdated: + return { + ...state, + voteLookupStatus: { + ...state.voteLookupStatus, + [action.data.username]: action.data.status, + }, + }; + + case actionTypes.voteLookupStatusCleared: + return { + ...state, + voteLookupStatus: { }, + }; + default: return state; } From 6fa27ba379ee43b09d57b11c663448558c62810c Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 16:16:12 +0200 Subject: [PATCH 10/26] Update voting middleware to get delegate api call ... if it is already in the redux store --- src/store/middlewares/voting.js | 47 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/store/middlewares/voting.js b/src/store/middlewares/voting.js index 24a8a8dc3..36f788f9c 100644 --- a/src/store/middlewares/voting.js +++ b/src/store/middlewares/voting.js @@ -12,31 +12,27 @@ const updateLookupStatus = (store, list, username) => { })); }; - -const processUpvote = (store, activePeer, username) => { - updateLookupStatus(store, 'pending', username); - getDelegate(activePeer, { username }).then((data) => { - const vote = store.getState().voting.votes[username]; - if (!vote || (!vote.confirmed && !vote.unconfirmed)) { - store.dispatch(voteToggled({ username, publicKey: data.delegate.publicKey })); - updateLookupStatus(store, 'upvotes', username); - } else { - updateLookupStatus(store, 'alreadyVoted', username); - } - }).catch(() => { - updateLookupStatus(store, 'notFound', username); - }); +const lookupDelegate = (store, username) => { + const state = store.getState(); + const activePeer = state.peers.data; + const delegate = state.voting.delegates.filter(d => d.username === username)[0]; + if (delegate) { + return new Promise((resolve) => { + resolve({ delegate }); + }); + } + return getDelegate(activePeer, { username }); }; -const processDownvote = (store, activePeer, username) => { +const processVote = (store, options, username) => { updateLookupStatus(store, 'pending', username); - getDelegate(activePeer, { username }).then((data) => { + lookupDelegate(store, username).then((data) => { const vote = store.getState().voting.votes[username]; - if (vote && vote.confirmed && vote.unconfirmed) { + if (options.isValid(vote)) { store.dispatch(voteToggled({ username, publicKey: data.delegate.publicKey })); - updateLookupStatus(store, 'downvotes', username); + updateLookupStatus(store, options.successState, username); } else { - updateLookupStatus(store, 'notVotedYet', username); + updateLookupStatus(store, options.invalidState, username); } }).catch(() => { updateLookupStatus(store, 'notFound', username); @@ -46,9 +42,16 @@ const processDownvote = (store, activePeer, username) => { const lookupDelegatesFromUrl = (store, action) => { const { upvotes, downvotes } = action.data; if (upvotes && downvotes) { - const activePeer = store.getState().peers.data; - downvotes.forEach(processDownvote.bind(this, store, activePeer)); - upvotes.forEach(processUpvote.bind(this, store, activePeer)); + downvotes.forEach(processVote.bind(this, store, { + successState: 'downvotes', + invalidState: 'notVotedYet', + isValid: vote => (vote && vote.confirmed && vote.unconfirmed), + })); + upvotes.forEach(processVote.bind(this, store, { + successState: 'upvotes', + invalidState: 'alreadyVoted', + isValid: vote => (!vote || (!vote.confirmed && !vote.unconfirmed)), + })); } }; From 0fc234af3ce0890bb0e70167f4ae55ac42fa05e9 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 16:31:59 +0200 Subject: [PATCH 11/26] Extract parseSearchParams function to utils --- src/components/dialog/dialog.js | 14 ++------------ .../voteUrlProcessor/voteUrlProcessor.js | 14 ++------------ src/utils/searchParams.js | 11 +++++++++++ 3 files changed, 15 insertions(+), 24 deletions(-) create mode 100644 src/utils/searchParams.js diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index dcd640861..362313409 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -3,6 +3,7 @@ import Dialog from 'react-toolbox/lib/dialog'; import Navigation from 'react-toolbox/lib/navigation'; import AppBar from 'react-toolbox/lib/app_bar'; import { IconButton } from 'react-toolbox/lib/button'; +import { parseSearchParams } from '../../utils/searchParams'; import styles from './dialog.css'; import getDialogs from './dialogs'; import routesReg from '../../utils/routes'; @@ -43,24 +44,13 @@ class DialogElement extends Component { } } - // eslint-disable-next-line class-methods-use-this - parseParams(search) { - return search.replace(/^\?/, '').split('&&').reduce((acc, param) => { - const keyValue = param.split('='); - if (keyValue[0] !== '' && keyValue[1] !== 'undefined') { - acc[keyValue[0]] = keyValue[1]; - } - return acc; - }, {}); - } - open(config, dialog) { clearTimeout(this.timeout); this.setState({ hidden: false }); this.props.dialogDisplayed({ title: dialog.title, childComponent: dialog.component, - childComponentProps: this.parseParams(this.props.history.location.search), + childComponentProps: parseSearchParams(this.props.history.location.search), }); } diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index 1db125a17..d15120739 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -2,23 +2,13 @@ import Chip from 'react-toolbox/lib/chip'; import ProgressBar from 'react-toolbox/lib/progress_bar'; import React from 'react'; +import { parseSearchParams } from '../../utils/searchParams'; import styles from './voteUrlProcessor.css'; export default class VoteUrlProcessor extends React.Component { - // eslint-disable-next-line class-methods-use-this - parseParams(search) { - return search.replace(/^\?/, '').split('&').reduce((acc, param) => { - const keyValue = param.split('='); - if (keyValue[0] !== '' && keyValue[1] !== 'undefined') { - acc[keyValue[0]] = keyValue[1]; - } - return acc; - }, {}); - } - componentDidMount() { this.props.clearVoteLookupStatus(); - const params = this.parseParams(this.props.history.location.search); + const params = parseSearchParams(this.props.history.location.search); if (params.upvote || params.downvote) { const upvotes = params.upvote ? params.upvote.split(',') : []; const downvotes = params.downvote ? params.downvote.split(',') : []; diff --git a/src/utils/searchParams.js b/src/utils/searchParams.js new file mode 100644 index 000000000..c2d0d7b82 --- /dev/null +++ b/src/utils/searchParams.js @@ -0,0 +1,11 @@ + +// eslint-disable-next-line import/prefer-default-export +export const parseSearchParams = (search) => { + const searchParams = new URLSearchParams(search); + const parsedParams = {}; + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of searchParams.entries()) { + parsedParams[key] = value; + } + return parsedParams; +}; From aeb507b1ed7056ce1e8d558049a744f5b4267492 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 16:52:18 +0200 Subject: [PATCH 12/26] Refactor voteUrlProcessor --- src/components/voteUrlProcessor/index.js | 3 ++- src/components/voteUrlProcessor/voteUrlProcessor.js | 12 ++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/voteUrlProcessor/index.js b/src/components/voteUrlProcessor/index.js index b2805f6bb..9faefe633 100644 --- a/src/components/voteUrlProcessor/index.js +++ b/src/components/voteUrlProcessor/index.js @@ -17,7 +17,8 @@ const filterObjectPropsWithValue = (object = {}, value) => ( const mapStateToProps = state => ({ votes: state.voting.votes, - voteCount: Object.keys(state.voting.voteLookupStatus || {}).length, + urlVoteCount: Object.keys(state.voting.voteLookupStatus || {}).length, + pending: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'pending'), downvotes: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'downvotes'), upvotes: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'upvotes'), alreadyVoted: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'alreadyVoted'), diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index d15120739..9b8d94c7e 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -22,11 +22,7 @@ export default class VoteUrlProcessor extends React.Component { } getProccessedCount() { - return this.props.upvotes.length + - this.props.downvotes.length + - this.props.notFound.length + - this.props.notVotedYet.length + - this.props.alreadyVoted.length; + return this.props.urlVoteCount - this.props.pending.length; } render() { @@ -46,13 +42,13 @@ export default class VoteUrlProcessor extends React.Component { }; return (
- {this.getProccessedCount() < this.props.voteCount ? + {this.getProccessedCount() < this.props.urlVoteCount ? (
+ value={this.getProccessedCount()} max={this.props.urlVoteCount}/>
{this.props.t('Processing delegate names: ')} - {this.getProccessedCount()} / {this.props.voteCount} + {this.getProccessedCount()} / {this.props.urlVoteCount}
) : ({Object.keys(errorMessages).map(list => ( From 0e3a4d248b8760b4c7a89a8bc08f1c2d6acae274 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 17:08:45 +0200 Subject: [PATCH 13/26] Fix clickToSend component not to put undefined in url --- src/components/clickToSend/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/clickToSend/index.js b/src/components/clickToSend/index.js index fea11486a..1d1d713d6 100644 --- a/src/components/clickToSend/index.js +++ b/src/components/clickToSend/index.js @@ -6,12 +6,19 @@ import { fromRawLsk } from '../../utils/lsk'; const ClickToSend = ({ rawAmount, amount, className, recipient, children, disabled }) => { const normalizedAmount = rawAmount ? fromRawLsk(rawAmount) : amount; + const urlParams = new URLSearchParams(); + if (normalizedAmount) { + urlParams.set('amount', normalizedAmount); + } + if (recipient) { + urlParams.set('recipient', recipient); + } return ( disabled ? children : + to={`send?${urlParams}`}> {children} ); From 076ba9c9de7f9ced72354316d8faf5f98c435d57 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 11 Oct 2017 18:16:48 +0200 Subject: [PATCH 14/26] Fix unit test coverage of voting actions --- src/actions/voting.test.js | 96 +++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/src/actions/voting.test.js b/src/actions/voting.test.js index 9e6614b18..7b61bdaeb 100644 --- a/src/actions/voting.test.js +++ b/src/actions/voting.test.js @@ -6,8 +6,10 @@ import { votesUpdated, votesAdded, voteToggled, + voteLookupStatusUpdated, votePlaced, votesFetched, + urlVotesFound, delegatesFetched, delegatesAdded, } from './voting'; @@ -36,6 +38,20 @@ describe('actions: voting', () => { }); }); + describe('voteLookupStatusUpdated', () => { + it('should create an action to update lookup status of any given delegate name', () => { + const data = { + label: 'dummy', + }; + const expectedAction = { + data, + type: actionTypes.voteLookupStatusUpdated, + }; + + expect(voteLookupStatusUpdated(data)).to.be.deep.equal(expectedAction); + }); + }); + describe('votesAdded', () => { it('should create an action to remove data from vote list', () => { const data = delegateList; @@ -132,27 +148,45 @@ describe('actions: voting', () => { }); describe('votesFetched', () => { + let delegateApiMock; const data = { activePeer: {}, address: '8096217735672704724L', }; const delegates = delegateList; - const actionFunction = votesFetched(data); + + beforeEach(() => { + delegateApiMock = sinon.stub(delegateApi, 'listAccountDelegates').returnsPromise(); + }); + + afterEach(() => { + delegateApiMock.restore(); + }); + it('should create an action function', () => { + const actionFunction = votesFetched(data); expect(typeof actionFunction).to.be.deep.equal('function'); }); - it.skip('should dispatch votesAdded action if resolved', () => { - const delegateApiMock = sinon.stub(delegateApi, 'listAccountDelegates'); + it('should dispatch votesAdded action when resolved if type !== \'update\'', () => { const dispatch = sinon.spy(); - delegateApiMock.returnsPromise().resolves({ delegates }); + delegateApiMock.resolves({ delegates }); const expectedAction = { list: delegates }; - actionFunction(dispatch); + votesFetched(data)(dispatch); expect(dispatch).to.have.been.calledWith(votesAdded(expectedAction)); - delegateApiMock.restore(); + }); + + it('should dispatch votesUpdated action when resolved if type === \'update\'', () => { + const dispatch = sinon.spy(); + + delegateApiMock.resolves({ delegates }); + const expectedAction = { list: delegates }; + + votesFetched({ ...data, type: 'update' })(dispatch); + expect(dispatch).to.have.been.calledWith(votesUpdated(expectedAction)); }); }); @@ -182,4 +216,54 @@ describe('actions: voting', () => { delegateApiMock.restore(); }); }); + + describe('urlVotesFound', () => { + let delegateApiMock; + const data = { + activePeer: {}, + address: '8096217735672704724L', + upvotes: [], + downvotes: [], + }; + const delegates = delegateList; + let expectedAction = { + list: delegates, + upvotes: [], + downvotes: [], + }; + + beforeEach(() => { + delegateApiMock = sinon.stub(delegateApi, 'listAccountDelegates').returnsPromise(); + }); + + afterEach(() => { + delegateApiMock.restore(); + }); + + it('should create an action function', () => { + expect(typeof urlVotesFound(data)).to.be.deep.equal('function'); + }); + + it('should dispatch votesAdded action when resolved', () => { + const dispatch = sinon.spy(); + + + urlVotesFound(data)(dispatch); + delegateApiMock.resolves({ delegates }); + expect(dispatch).to.have.been.calledWith(votesAdded(expectedAction)); + }); + + it('should dispatch votesAdded action when rejected', () => { + const dispatch = sinon.spy(); + + expectedAction = { + ...expectedAction, + list: [], + }; + + urlVotesFound(data)(dispatch); + delegateApiMock.rejects(); + expect(dispatch).to.have.been.calledWith(votesAdded(expectedAction)); + }); + }); }); From 3d5b7ee811138f9718e4598b90c04003a5d9c1f8 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 10:30:20 +0200 Subject: [PATCH 15/26] Remove unused code --- src/components/voteUrlProcessor/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/voteUrlProcessor/index.js b/src/components/voteUrlProcessor/index.js index 9faefe633..61b80fa33 100644 --- a/src/components/voteUrlProcessor/index.js +++ b/src/components/voteUrlProcessor/index.js @@ -5,7 +5,6 @@ import { withRouter } from 'react-router'; import { urlVotesFound, voteLookupStatusCleared, - votePlaced, voteToggled, votesAdded, } from '../../actions/voting'; @@ -29,7 +28,6 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - votePlaced: data => dispatch(votePlaced(data)), voteToggled: data => dispatch(voteToggled(data)), votesAdded: data => dispatch(votesAdded(data)), urlVotesFound: data => dispatch(urlVotesFound(data)), From 384a6be43718682d92f7ec26fa3a7b79d20b5394 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 10:53:30 +0200 Subject: [PATCH 16/26] Rename downvote to unvote --- features/voting.feature | 4 ++-- src/actions/voting.js | 4 ++-- src/actions/voting.test.js | 4 ++-- src/components/voteUrlProcessor/index.js | 2 +- src/components/voteUrlProcessor/voteUrlProcessor.js | 12 ++++++------ src/components/voting/votingBar.js | 2 +- src/components/voting/votingBar.test.js | 10 +++++----- src/store/middlewares/voting.js | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/features/voting.feature b/features/voting.feature index 4d22fa950..15b947d24 100644 --- a/features/voting.feature +++ b/features/voting.feature @@ -114,9 +114,9 @@ Feature: Voting tab Scenario: should allow to select delegates by URL Given I'm logged in as "delegate candidate" - When I go to "/main/voting/vote?upvote=standby_27,standby_28,standby_29,nonexisting_22&downvote=standby_33" + When I go to "/main/voting/vote?upvote=standby_27,standby_28,standby_29,nonexisting_22&unvote=standby_33" Then I should see text "3 delegate names successfully resolved to add vote to." in "upvotes message" element - And I should see text "1 of delegate names selected for downvote were not voted for:standby_33" in "notVotedYet message" element + And I should see text "1 of delegate names selected for unvote were not voted for:standby_33" in "notVotedYet message" element And I should see text "1 of entered delegate names could not be resolved:nonexisting_22" in "notFound message" element And I should see "vote list" element with text: """ diff --git a/src/actions/voting.js b/src/actions/voting.js index cb47bdc14..d17776ff6 100644 --- a/src/actions/voting.js +++ b/src/actions/voting.js @@ -147,10 +147,10 @@ export const delegatesFetched = ({ activePeer, q, offset, refresh }) => /** * Get list of delegates current account has voted for and dispatch it with votes from url */ -export const urlVotesFound = ({ activePeer, upvotes, downvotes, address }) => +export const urlVotesFound = ({ activePeer, upvotes, unvotes, address }) => (dispatch) => { const processUrlVotes = (votes) => { - dispatch(votesAdded({ list: votes, upvotes, downvotes })); + dispatch(votesAdded({ list: votes, upvotes, unvotes })); }; listAccountDelegates(activePeer, address) .then(({ delegates }) => { processUrlVotes(delegates); }) diff --git a/src/actions/voting.test.js b/src/actions/voting.test.js index 7b61bdaeb..ab6742902 100644 --- a/src/actions/voting.test.js +++ b/src/actions/voting.test.js @@ -223,13 +223,13 @@ describe('actions: voting', () => { activePeer: {}, address: '8096217735672704724L', upvotes: [], - downvotes: [], + unvotes: [], }; const delegates = delegateList; let expectedAction = { list: delegates, upvotes: [], - downvotes: [], + unvotes: [], }; beforeEach(() => { diff --git a/src/components/voteUrlProcessor/index.js b/src/components/voteUrlProcessor/index.js index 61b80fa33..ff5c2a8e4 100644 --- a/src/components/voteUrlProcessor/index.js +++ b/src/components/voteUrlProcessor/index.js @@ -18,7 +18,7 @@ const mapStateToProps = state => ({ votes: state.voting.votes, urlVoteCount: Object.keys(state.voting.voteLookupStatus || {}).length, pending: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'pending'), - downvotes: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'downvotes'), + unvotes: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'unvotes'), upvotes: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'upvotes'), alreadyVoted: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'alreadyVoted'), notVotedYet: filterObjectPropsWithValue(state.voting.voteLookupStatus, 'notVotedYet'), diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index 9b8d94c7e..47d947100 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -9,13 +9,13 @@ export default class VoteUrlProcessor extends React.Component { componentDidMount() { this.props.clearVoteLookupStatus(); const params = parseSearchParams(this.props.history.location.search); - if (params.upvote || params.downvote) { + if (params.upvote || params.unvote) { const upvotes = params.upvote ? params.upvote.split(',') : []; - const downvotes = params.downvote ? params.downvote.split(',') : []; + const unvotes = params.unvote ? params.unvote.split(',') : []; this.props.urlVotesFound({ activePeer: this.props.activePeer, upvotes, - downvotes, + unvotes, address: this.props.account.address, }); } @@ -31,14 +31,14 @@ export default class VoteUrlProcessor extends React.Component { { count: this.props.notFound.length }), alreadyVoted: this.props.t('{{count}} of delegate names selected for upvote were already voted for:', { count: this.props.alreadyVoted.length }), - notVotedYet: this.props.t('{{count}} of delegate names selected for downvote were not voted for:', + notVotedYet: this.props.t('{{count}} of delegate names selected for unvote were not voted for:', { count: this.props.notVotedYet.length }), }; const successMessages = { upvotes: this.props.t('{{count}} delegate names successfully resolved to add vote to.', { count: this.props.upvotes.length }), - downvotes: this.props.t('{{count}} delegate names successfully resolved to remove vote from.', - { count: this.props.downvotes.length }), + unvotes: this.props.t('{{count}} delegate names successfully resolved to remove vote from.', + { count: this.props.unvotes.length }), }; return (
diff --git a/src/components/voting/votingBar.js b/src/components/voting/votingBar.js index ac8897859..350a7688b 100644 --- a/src/components/voting/votingBar.js +++ b/src/components/voting/votingBar.js @@ -24,7 +24,7 @@ const VotingBar = ({ votes, t }) => { {t('Upvotes:')} {voteList.length} - + {t('Downvotes:')} {unvoteList.length} diff --git a/src/components/voting/votingBar.test.js b/src/components/voting/votingBar.test.js index c8ac18b90..7c1c8f8d9 100644 --- a/src/components/voting/votingBar.test.js +++ b/src/components/voting/votingBar.test.js @@ -16,7 +16,7 @@ describe('VotingBar', () => { confirmed: true, unconfirmed: false, }, - downvote: { + unvote: { confirmed: true, unconfirmed: true, }, @@ -57,11 +57,11 @@ describe('VotingBar', () => { expect(wrapper.find('.upvotes')).to.have.text('Upvotes: 2'); }); - it('should render number of downvotes', () => { - expect(wrapper.find('.downvotes')).to.have.text('Downvotes: 1'); + it('should render number of unvotes', () => { + expect(wrapper.find('.unvotes')).to.have.text('Downvotes: 1'); }); - it('should render number of downvotes', () => { + it('should render number of unvotes', () => { expect(wrapper.find('.total-new-votes')).to.have.text('Total new votes: 3 / 33'); }); @@ -69,7 +69,7 @@ describe('VotingBar', () => { expect(wrapper.find('.total-votes')).to.have.text('Total votes: 3 / 101'); }); - it('should not render if no upvotes or downvotes', () => { + it('should not render if no upvotes or unvotes', () => { wrapper.setProps({ votes: {} }); expect(wrapper.html()).to.equal(null); }); diff --git a/src/store/middlewares/voting.js b/src/store/middlewares/voting.js index 36f788f9c..807d58d38 100644 --- a/src/store/middlewares/voting.js +++ b/src/store/middlewares/voting.js @@ -40,10 +40,10 @@ const processVote = (store, options, username) => { }; const lookupDelegatesFromUrl = (store, action) => { - const { upvotes, downvotes } = action.data; - if (upvotes && downvotes) { - downvotes.forEach(processVote.bind(this, store, { - successState: 'downvotes', + const { upvotes, unvotes } = action.data; + if (upvotes && unvotes) { + unvotes.forEach(processVote.bind(this, store, { + successState: 'unvotes', invalidState: 'notVotedYet', isValid: vote => (vote && vote.confirmed && vote.unconfirmed), })); From e173810d3c22420fe1422db7dd4b0694366e9ecc Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 12:19:12 +0200 Subject: [PATCH 17/26] Change voting params to votes and unvotes --- features/voting.feature | 2 +- src/components/voteUrlProcessor/voteUrlProcessor.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/features/voting.feature b/features/voting.feature index 15b947d24..c20029afd 100644 --- a/features/voting.feature +++ b/features/voting.feature @@ -114,7 +114,7 @@ Feature: Voting tab Scenario: should allow to select delegates by URL Given I'm logged in as "delegate candidate" - When I go to "/main/voting/vote?upvote=standby_27,standby_28,standby_29,nonexisting_22&unvote=standby_33" + When I go to "/main/voting/vote?votes=standby_27,standby_28,standby_29,nonexisting_22&unvotes=standby_33" Then I should see text "3 delegate names successfully resolved to add vote to." in "upvotes message" element And I should see text "1 of delegate names selected for unvote were not voted for:standby_33" in "notVotedYet message" element And I should see text "1 of entered delegate names could not be resolved:nonexisting_22" in "notFound message" element diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.js b/src/components/voteUrlProcessor/voteUrlProcessor.js index 47d947100..ba8d14883 100644 --- a/src/components/voteUrlProcessor/voteUrlProcessor.js +++ b/src/components/voteUrlProcessor/voteUrlProcessor.js @@ -9,9 +9,9 @@ export default class VoteUrlProcessor extends React.Component { componentDidMount() { this.props.clearVoteLookupStatus(); const params = parseSearchParams(this.props.history.location.search); - if (params.upvote || params.unvote) { - const upvotes = params.upvote ? params.upvote.split(',') : []; - const unvotes = params.unvote ? params.unvote.split(',') : []; + if (params.votes || params.unvotes) { + const upvotes = params.votes ? params.votes.split(',') : []; + const unvotes = params.unvotes ? params.unvotes.split(',') : []; this.props.urlVotesFound({ activePeer: this.props.activePeer, upvotes, @@ -29,7 +29,7 @@ export default class VoteUrlProcessor extends React.Component { const errorMessages = { notFound: this.props.t('{{count}} of entered delegate names could not be resolved:', { count: this.props.notFound.length }), - alreadyVoted: this.props.t('{{count}} of delegate names selected for upvote were already voted for:', + alreadyVoted: this.props.t('{{count}} of delegate names selected for vote were already voted for:', { count: this.props.alreadyVoted.length }), notVotedYet: this.props.t('{{count}} of delegate names selected for unvote were not voted for:', { count: this.props.notVotedYet.length }), From e4fff4187fc164be04304c84563bd0f4bdfdb251 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 14:13:04 +0200 Subject: [PATCH 18/26] Add unit tests for voting reducer --- src/store/reducers/voting.test.js | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/store/reducers/voting.test.js b/src/store/reducers/voting.test.js index 129f31c24..f7ba5203b 100644 --- a/src/store/reducers/voting.test.js +++ b/src/store/reducers/voting.test.js @@ -181,4 +181,49 @@ describe('Reducer: voting(state, action)', () => { expect(changedState).to.be.deep.equal(expectedState); }); + + it('should set voteLookupStatus of given username to given status, with action: voteLookupStatusUpdated', () => { + const action = { + type: actionTypes.voteLookupStatusUpdated, + data: { + username: 'username1', + status: 'upvoted', + }, + }; + const state = { + voteLookupStatus: { + [action.data.username]: 'pending', + username2: 'pending', + }, + }; + + const expectedState = { + voteLookupStatus: { + [action.data.username]: action.data.status, + username2: 'pending', + }, + }; + const changedState = voting(state, action); + + expect(changedState).to.be.deep.equal(expectedState); + }); + + it('should set voteLookupStatus to {}, with action: voteLookupStatusCleared', () => { + const action = { + type: actionTypes.voteLookupStatusCleared, + }; + const state = { + voteLookupStatus: { + username1: 'upvoted', + username2: 'unvoted', + }, + }; + + const expectedState = { + voteLookupStatus: {}, + }; + const changedState = voting(state, action); + + expect(changedState).to.be.deep.equal(expectedState); + }); }); From 049838e8122ec2f02c73bd7af45295aa4cdf564b Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 14:13:32 +0200 Subject: [PATCH 19/26] Add unit tests for voting middleware --- src/store/middlewares/voting.test.js | 240 ++++++++++++++++++++++----- 1 file changed, 196 insertions(+), 44 deletions(-) diff --git a/src/store/middlewares/voting.test.js b/src/store/middlewares/voting.test.js index f0bfbeb5e..d8913a2c2 100644 --- a/src/store/middlewares/voting.test.js +++ b/src/store/middlewares/voting.test.js @@ -1,8 +1,12 @@ import { expect } from 'chai'; -import { spy, stub } from 'sinon'; +import { spy, stub, mock } from 'sinon'; + + import { errorToastDisplayed } from '../../actions/toaster'; -import middleware from './voting'; +import { voteLookupStatusUpdated } from '../../actions/voting'; +import * as delegateApi from '../../utils/api/delegate'; import actionTypes from '../../constants/actions'; +import middleware from './voting'; import votingConst from '../../constants/voting'; describe('voting middleware', () => { @@ -51,53 +55,201 @@ describe('voting middleware', () => { expect(next).to.have.been.calledWith(givenAction); }); - it('should dispatch errorToastDisplayed if 34 new votes and new vote unconfirmed !== confirmed ', () => { - const givenAction = { - type: actionTypes.voteToggled, - data: { - username: 'test', - }, - }; - middleware(store)(next)(givenAction); - expect(store.dispatch).to.have.been.calledWith(errorToastDisplayed({ label })); - }); + describe('on voteToggled action', () => { + it('should dispatch errorToastDisplayed if 34 new votes and new vote unconfirmed !== confirmed ', () => { + const givenAction = { + type: actionTypes.voteToggled, + data: { + username: 'test', + }, + }; + middleware(store)(next)(givenAction); + expect(store.dispatch).to.have.been.calledWith(errorToastDisplayed({ label })); + }); - it('should not dispatch errorToastDisplayed if 34 new votes and new vote unconfirmed === confirmed ', () => { - const givenAction = { - type: actionTypes.voteToggled, - data: { - username: 'test2', - }, - }; - middleware(store)(next)(givenAction); - expect(store.dispatch).to.not.have.been.calledWith(errorToastDisplayed({ label })); - }); + it('should not dispatch errorToastDisplayed if 34 new votes and new vote unconfirmed === confirmed ', () => { + const givenAction = { + type: actionTypes.voteToggled, + data: { + username: 'test2', + }, + }; + middleware(store)(next)(givenAction); + expect(store.dispatch).to.not.have.been.calledWith(errorToastDisplayed({ label })); + }); - it('should dispatch errorToastDisplayed if 102 votes and new vote unconfirmed !== confirmed ', () => { - initStoreWithNVotes( - votingConst.maxCountOfVotes + 1, - { confirmed: true, unconfirmed: true }); - const givenAction = { - type: actionTypes.voteToggled, - data: { - username: 'test', - }, - }; - middleware(store)(next)(givenAction); - expect(store.dispatch).to.have.been.calledWith(errorToastDisplayed({ label: label2 })); + it('should dispatch errorToastDisplayed if 102 votes and new vote unconfirmed !== confirmed ', () => { + initStoreWithNVotes( + votingConst.maxCountOfVotes + 1, + { confirmed: true, unconfirmed: true }); + const givenAction = { + type: actionTypes.voteToggled, + data: { + username: 'test', + }, + }; + middleware(store)(next)(givenAction); + expect(store.dispatch).to.have.been.calledWith(errorToastDisplayed({ label: label2 })); + }); + + it('should not dispatch errorToastDisplayed if 102 votes and new vote unconfirmed === confirmed ', () => { + initStoreWithNVotes( + votingConst.maxCountOfVotes + 1, + { confirmed: true, unconfirmed: true }); + const givenAction = { + type: actionTypes.voteToggled, + data: { + username: 'genesis_42', + }, + }; + middleware(store)(next)(givenAction); + expect(store.dispatch).to.not.have.been.calledWith(errorToastDisplayed({ label: label2 })); + }); }); - it('should not dispatch errorToastDisplayed if 102 votes and new vote unconfirmed === confirmed ', () => { - initStoreWithNVotes( - votingConst.maxCountOfVotes + 1, - { confirmed: true, unconfirmed: true }); - const givenAction = { - type: actionTypes.voteToggled, - data: { - username: 'genesis_42', + describe('on votesAdded action', () => { + const state = { + voting: { + delegates: [ + { + username: 'delegate_in_store', + publicKey: 'some publicKey', + }, + ], + votes: { + delegate_voted: { + unconfirmed: true, + confirmed: true, + }, + delegate_unvoted: { + unconfirmed: false, + confirmed: false, + }, + }, + }, + peers: { + data: {}, }, }; - middleware(store)(next)(givenAction); - expect(store.dispatch).to.not.have.been.calledWith(errorToastDisplayed({ label: label2 })); + let getDelegateMock; + + beforeEach(() => { + getDelegateMock = mock(delegateApi).expects('getDelegate').returnsPromise(); + store.getState = () => (state); + }); + + afterEach(() => { + getDelegateMock.restore(); + }); + + it('should do nothing if !action.upvotes or !action.dowvotes ', () => { + const givenAction = { + type: actionTypes.votesAdded, + data: { }, + }; + middleware(store)(next)(givenAction); + expect(store.dispatch).to.not.have.been.calledWith(); + }); + + it('should dispatch voteLookupStatusUpdated with username from action.data.upvotes and status \'upvotes\'', () => { + const username = 'delegate_unvoted'; + const status = 'upvotes'; + const givenAction = { + type: actionTypes.votesAdded, + data: { + upvotes: [username], + unvotes: [], + }, + }; + + middleware(store)(next)(givenAction); + getDelegateMock.resolves({ delegate: { username, publicKey: 'whatever' } }); + + expect(store.dispatch).to.have.been.calledWith(voteLookupStatusUpdated({ username, status })); + }); + + it('should dispatch voteLookupStatusUpdated with username from action.data.upvotes and status \'alreadyVoted\'', () => { + const username = 'delegate_voted'; + const status = 'alreadyVoted'; + const givenAction = { + type: actionTypes.votesAdded, + data: { + upvotes: [username], + unvotes: [], + }, + }; + + middleware(store)(next)(givenAction); + getDelegateMock.resolves({ delegate: { username, publicKey: 'whatever' } }); + + expect(store.dispatch).to.have.been.calledWith(voteLookupStatusUpdated({ username, status })); + }); + + it('should dispatch voteLookupStatusUpdated with username from action.data.unvotes and status \'unvotes\'', () => { + const username = 'delegate_voted'; + const status = 'unvotes'; + const givenAction = { + type: actionTypes.votesAdded, + data: { + upvotes: [], + unvotes: [username], + }, + }; + + middleware(store)(next)(givenAction); + getDelegateMock.resolves({ delegate: { username, publicKey: 'whatever' } }); + + expect(store.dispatch).to.have.been.calledWith(voteLookupStatusUpdated({ username, status })); + }); + + it('should dispatch voteLookupStatusUpdated with username from action.data.unvotes and status \'notVotedYet\'', () => { + const username = 'delegate_unvoted'; + const status = 'notVotedYet'; + const givenAction = { + type: actionTypes.votesAdded, + data: { + upvotes: [], + unvotes: [username], + }, + }; + + middleware(store)(next)(givenAction); + getDelegateMock.resolves({ delegate: { username, publicKey: 'whatever' } }); + + expect(store.dispatch).to.have.been.calledWith(voteLookupStatusUpdated({ username, status })); + }); + + it('should dispatch voteLookupStatusUpdated with username from action.data.unvotes and status \'notFound\' if delegate not found', () => { + const username = 'delegate_invalid'; + const status = 'notFound'; + const givenAction = { + type: actionTypes.votesAdded, + data: { + upvotes: [], + unvotes: [username], + }, + }; + + middleware(store)(next)(givenAction); + getDelegateMock.rejects(); + + expect(store.dispatch).to.have.been.calledWith(voteLookupStatusUpdated({ username, status })); + }); + + it('should not call getDelegate API if given delegate is in store', () => { + const username = 'delegate_in_store'; + const givenAction = { + type: actionTypes.votesAdded, + data: { + upvotes: [username], + unvotes: [], + }, + }; + + middleware(store)(next)(givenAction); + getDelegateMock.rejects(); + + expect(getDelegateMock).to.not.have.been.calledWith(); + }); }); }); From 85a97bb2f9247a131057ecf2feccfc7008d3336c Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 15:10:46 +0200 Subject: [PATCH 20/26] Update translation sources --- src/locales/en/common.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 486ca6b6f..ca7fef355 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -68,6 +68,7 @@ "Please click Next, then move around your mouse randomly to generate a random passphrase.": "Please click Next, then move around your mouse randomly to generate a random passphrase.", "Please keep it safe!": "Please keep it safe!", "Press #{key} to copy": "Press #{key} to copy", + "Processing delegate names: ": "Processing delegate names: ", "Public Key": "Public Key", "Rank": "Rank", "Receive LSK": "Receive LSK", @@ -141,5 +142,15 @@ "my votes": "my votes", "send": "send", "your passphrase will be required for logging in to your account.": "your passphrase will be required for logging in to your account.", - "your second passphrase will be required for all transactions sent from this account": "your second passphrase will be required for all transactions sent from this account" + "your second passphrase will be required for all transactions sent from this account": "your second passphrase will be required for all transactions sent from this account", + "{{count}} delegate names successfully resolved to add vote to.": "{{count}} delegate names successfully resolved to add vote to.", + "{{count}} delegate names successfully resolved to add vote to._plural": "", + "{{count}} delegate names successfully resolved to remove vote from.": "{{count}} delegate names successfully resolved to remove vote from.", + "{{count}} delegate names successfully resolved to remove vote from._plural": "", + "{{count}} of delegate names selected for unvote were not voted for:": "{{count}} of delegate names selected for unvote were not voted for:", + "{{count}} of delegate names selected for unvote were not voted for:_plural": "", + "{{count}} of delegate names selected for vote were already voted for:": "{{count}} of delegate names selected for vote were already voted for:", + "{{count}} of delegate names selected for vote were already voted for:_plural": "", + "{{count}} of entered delegate names could not be resolved:": "{{count}} of entered delegate names could not be resolved:", + "{{count}} of entered delegate names could not be resolved:_plural": "" } From f315189e058dbc537482eb39a7c3cd737bb8178a Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 15:11:02 +0200 Subject: [PATCH 21/26] Add unit tests for voteUrlProcessor component --- src/components/voteDialog/index.test.js | 1 + src/components/voteUrlProcessor/index.test.js | 73 +++++++++++++ .../voteUrlProcessor/voteUrlProcessor.test.js | 101 ++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 src/components/voteUrlProcessor/index.test.js create mode 100644 src/components/voteUrlProcessor/voteUrlProcessor.test.js diff --git a/src/components/voteDialog/index.test.js b/src/components/voteDialog/index.test.js index f932ebad0..fbe68d4bf 100644 --- a/src/components/voteDialog/index.test.js +++ b/src/components/voteDialog/index.test.js @@ -79,5 +79,6 @@ describe('VoteDialog HOC', () => { const actionsSpy = sinon.spy(votingActions, 'voteToggled'); wrapper.find('VoteDialog').props().voteToggled([]); expect(actionsSpy).to.be.calledWith(); + actionsSpy.restore(); }); }); diff --git a/src/components/voteUrlProcessor/index.test.js b/src/components/voteUrlProcessor/index.test.js new file mode 100644 index 000000000..40e8979a4 --- /dev/null +++ b/src/components/voteUrlProcessor/index.test.js @@ -0,0 +1,73 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { BrowserRouter as Router } from 'react-router-dom'; +import configureMockStore from 'redux-mock-store'; +import sinon from 'sinon'; +import i18n from '../../i18n'; // initialized i18next instance +import * as votingActions from '../../actions/voting'; +import VoteUrlProcessor from './voteUrlProcessor'; +import VoteUrlProcessorHOC from './index'; + + +describe('VoteUrlProcessorHOC', () => { + let wrapper; + const actionData = { + dummy: 'dummy', + }; + const state = { + peers: { data: {} }, + account: {}, + voting: { + voteLookupStatus: { + delegate_name: 'pending', + }, + }, + }; + const store = configureMockStore([])(state); + const options = { + context: { i18n, store }, + childContextTypes: { + i18n: PropTypes.object.isRequired, + store: PropTypes.object.isRequired, + }, + }; + + beforeEach(() => { + wrapper = mount(, options); + }); + + it('should render VoteUrlProcessor', () => { + expect(wrapper.find(VoteUrlProcessor)).to.have.lengthOf(1); + }); + + it('should bind voteToggled action to VoteUrlProcessor props.voteToggled', () => { + const actionsSpy = sinon.spy(votingActions, 'voteToggled'); + wrapper.find(VoteUrlProcessor).props().voteToggled(actionData); + expect(actionsSpy).to.be.calledWith(actionData); + actionsSpy.restore(); + }); + + it('should bind votesAdded action to VoteUrlProcessor props.votesAdded', () => { + const actionsSpy = sinon.spy(votingActions, 'votesAdded'); + wrapper.find(VoteUrlProcessor).props().votesAdded(actionData); + expect(actionsSpy).to.be.calledWith(actionData); + actionsSpy.restore(); + }); + + it('should bind urlVotesFound action to VoteUrlProcessor props.urlVotesFound', () => { + const actionMock = sinon.mock(votingActions); + actionMock.expects('urlVotesFound').withExactArgs(actionData).returns({ type: 'DUMMY' }); + wrapper.find(VoteUrlProcessor).props().urlVotesFound(actionData); + actionMock.restore(); + }); + + it('should bind voteLookupStatusCleared action to VoteUrlProcessor props.clearVoteLookupStatus', () => { + const actionsSpy = sinon.spy(votingActions, 'voteLookupStatusCleared'); + wrapper.find(VoteUrlProcessor).props().clearVoteLookupStatus(); + expect(actionsSpy).to.be.calledWith(); + actionsSpy.restore(); + }); +}); + diff --git a/src/components/voteUrlProcessor/voteUrlProcessor.test.js b/src/components/voteUrlProcessor/voteUrlProcessor.test.js new file mode 100644 index 000000000..48c28b88b --- /dev/null +++ b/src/components/voteUrlProcessor/voteUrlProcessor.test.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import sinon from 'sinon'; +import VoteUrlProcessor from './voteUrlProcessor'; + +describe('VoteUrlProcessor', () => { + let wrapper; + let props; + + beforeEach(() => { + const account = { + balance: 1000e8, + address: '16313739661670634666L', + passphrase: 'recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit', + }; + + props = { + activePeer: {}, + account, + clearVoteLookupStatus: sinon.spy(), + urlVotesFound: sinon.spy(), + notVotedYet: [], + notFound: [], + alreadyVoted: [], + upvotes: [], + unvotes: [], + pending: [], + history: { + location: { + search: '', + }, + }, + urlVoteCount: 0, + t: key => key, + }; + wrapper = mount(); + }); + + it('renders ProgressBar component if props.pending.length > 0', () => { + wrapper.setProps({ + pending: ['delegate_name'], + urlVoteCount: 1, + }); + expect(wrapper.find('ProgressBar')).to.have.length(1); + }); + + it('calls props.urlVotesFound with upvotes if URL contains ?votes=delegate_name', () => { + wrapper = mount(); + expect(props.urlVotesFound).to.have.been.calledWith({ + activePeer: props.activePeer, + upvotes: ['delegate_name'], + unvotes: [], + address: props.account.address, + }); + }); + + + it('calls props.urlVotesFound with unvotes if URL contains ?unvotes=delegate_name', () => { + wrapper = mount(); + expect(props.urlVotesFound).to.have.been.calledWith({ + activePeer: props.activePeer, + upvotes: [], + unvotes: ['delegate_name'], + address: props.account.address, + }); + }); + + it('renders .upvotes-message element with a message if props.upvotes.length > 0', () => { + wrapper.setProps({ + upvotes: ['delegate_name'], + urlVoteCount: 1, + }); + expect(wrapper.find('.upvotes-message')).to.have.length(1); + expect(wrapper.find('.upvotes-message').text()).to.equal('{{count}} delegate names successfully resolved to add vote to.'); + }); + + it('renders .notFound-message element with a message if props.notFound.length > 0', () => { + wrapper.setProps({ + notFound: ['delegate_name'], + urlVoteCount: 1, + }); + expect(wrapper.find('.notFound-message')).to.have.length(1); + expect(wrapper.find('.notFound-message').text()).to.equal('{{count}} of entered delegate names could not be resolved:delegate_name'); + }); +}); + From 32ceca142de2a439429d39c299dbc91259bc84f8 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 15:31:01 +0200 Subject: [PATCH 22/26] Fix modal dialog overflow --- src/components/dialog/dialog.css | 20 ++++++++++++++++++-- src/components/dialog/dialog.js | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/dialog/dialog.css b/src/components/dialog/dialog.css index 24a78f789..63673dbd5 100644 --- a/src/components/dialog/dialog.css +++ b/src/components/dialog/dialog.css @@ -14,8 +14,6 @@ } & header { - margin: -24px; - margin-bottom: 24px; border-radius: 2px 2px 0 0; color: rgba(255, 255, 255, 0.87); @@ -32,12 +30,29 @@ } } +.body { + border-radius: 2px 2px 0 0; + padding: 0px; +} + +.innerBody { + padding: 24px; + max-height: calc(100vh - 64px); /* stylelint-disable-line */ + overflow: auto; +} + @media screen and (min-width: 960px) { .fullscreen { width: 75vw; /* stylelint-disable-line */ } } +@media screen and (min-width: 600px) { + .innerBody { + max-height: calc(100vh - 100px); /* stylelint-disable-line */ + } +} + .error { background-color: #c62828; } @@ -45,3 +60,4 @@ .success { background-color: #7cb342; } + diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index 362313409..141bcebc1 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -78,7 +78,7 @@ class DialogElement extends Component { -
+
{this.props.dialog.childComponent ? Date: Thu, 12 Oct 2017 15:35:08 +0200 Subject: [PATCH 23/26] Fix plural translation strings --- src/locales/en/common.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/locales/en/common.json b/src/locales/en/common.json index ca7fef355..ce0c19b50 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -127,9 +127,9 @@ "When you have the signature, you only need the publicKey of the signer in order to verify that the message came from the right private/publicKey pair. Be aware, everybody knowing the signature and the publicKey can verify the message. If ever there is a dispute, everybody can take the publicKey and signature to a judge and prove that the message is coming from the specific private/publicKey pair.": "When you have the signature, you only need the publicKey of the signer in order to verify that the message came from the right private/publicKey pair. Be aware, everybody knowing the signature and the publicKey can verify the message. If ever there is a dispute, everybody can take the publicKey and signature to a judge and prove that the message is coming from the specific private/publicKey pair.", "Yes! It's safe": "Yes! It's safe", "You can select up to {{count}} delegates in one voting turn.": "You can select up to {{count}} delegates in one voting turn.", - "You can select up to {{count}} delegates in one voting turn._plural": "", + "You can select up to {{count}} delegates in one voting turn._plural": "You can select up to {{count}} delegates in one voting turn.", "You can vote for up to {{count}} delegates in total.": "You can vote for up to {{count}} delegates in total.", - "You can vote for up to {{count}} delegates in total._plural": "", + "You can vote for up to {{count}} delegates in total._plural": "You can vote for up to {{count}} delegates in total.", "You have not forged any blocks yet": "You have not forged any blocks yet", "You need to become a delegate to start forging. If you already registered to become a delegate, your registration hasn't been processed, yet.": "You need to become a delegate to start forging. If you already registered to become a delegate, your registration hasn't been processed, yet.", "You've received {{value}} LSK.": "You've received {{value}} LSK.", @@ -143,14 +143,14 @@ "send": "send", "your passphrase will be required for logging in to your account.": "your passphrase will be required for logging in to your account.", "your second passphrase will be required for all transactions sent from this account": "your second passphrase will be required for all transactions sent from this account", - "{{count}} delegate names successfully resolved to add vote to.": "{{count}} delegate names successfully resolved to add vote to.", - "{{count}} delegate names successfully resolved to add vote to._plural": "", - "{{count}} delegate names successfully resolved to remove vote from.": "{{count}} delegate names successfully resolved to remove vote from.", - "{{count}} delegate names successfully resolved to remove vote from._plural": "", + "{{count}} delegate names successfully resolved to add vote to.": "{{count}} delegate name successfully resolved to add vote to.", + "{{count}} delegate names successfully resolved to add vote to._plural": "{{count}} delegate names successfully resolved to add vote to.", + "{{count}} delegate names successfully resolved to remove vote from.": "{{count}} delegate name successfully resolved to remove vote from.", + "{{count}} delegate names successfully resolved to remove vote from._plural": "{{count}} delegate names successfully resolved to remove vote from.", "{{count}} of delegate names selected for unvote were not voted for:": "{{count}} of delegate names selected for unvote were not voted for:", - "{{count}} of delegate names selected for unvote were not voted for:_plural": "", + "{{count}} of delegate names selected for unvote were not voted for:_plural": "{{count}} of delegate names selected for unvote were not voted for:", "{{count}} of delegate names selected for vote were already voted for:": "{{count}} of delegate names selected for vote were already voted for:", - "{{count}} of delegate names selected for vote were already voted for:_plural": "", + "{{count}} of delegate names selected for vote were already voted for:_plural": "{{count}} of delegate names selected for vote were already voted for:", "{{count}} of entered delegate names could not be resolved:": "{{count}} of entered delegate names could not be resolved:", - "{{count}} of entered delegate names could not be resolved:_plural": "" + "{{count}} of entered delegate names could not be resolved:_plural": "{{count}} of entered delegate names could not be resolved:" } From cb28be1ce1fe9177b1bba2381bafe49c58e88e05 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 16:06:32 +0200 Subject: [PATCH 24/26] Fix i18n-scanner to preserve already translated values --- src/i18n-scanner.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n-scanner.js b/src/i18n-scanner.js index 8fbbc08ed..702fb5a06 100644 --- a/src/i18n-scanner.js +++ b/src/i18n-scanner.js @@ -5,6 +5,8 @@ const Parser = require('i18next-scanner').Parser; const translationFunctionNames = ['i18next.t', 'props.t', 'this.props.t', 't']; const outputFilePath = './src/locales/en/common.json'; +const translationsSource = JSON.parse(fs.readFileSync(outputFilePath, 'utf8')); + const parser = new Parser({ keySeparator: '>', nsSeparator: '|', @@ -12,13 +14,14 @@ const parser = new Parser({ const customHandler = function (key, options) { - const value = key; + const value = translationsSource[key] || key; if (options.context) { key += `_${options.context}`; } parser.set(key, value); if (options.count !== undefined) { - parser.set(`${key}_plural`); + key = `${key}_plural`; + parser.set(key, translationsSource[key] || ''); } }; From 9dd4b39daebbfb577453f89b0a2a3c30446d5841 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Thu, 12 Oct 2017 16:31:47 +0200 Subject: [PATCH 25/26] Increase e2e test wait time from 4 to 10 seconds ... to see if it makes them more stable --- features/support/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/support/util.js b/features/support/util.js index ba95c7315..4ba37f43e 100644 --- a/features/support/util.js +++ b/features/support/util.js @@ -5,7 +5,7 @@ const chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised); const expect = chai.expect; const EC = protractor.ExpectedConditions; -const waitTime = 4000; +const waitTime = 10000; function waitForElemAndCheckItsText(selector, text, callback) { const elem = element(by.css(selector)); From 16eb189261447389a54c7e2c5f55e2c462aa7010 Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Fri, 13 Oct 2017 08:41:38 +0200 Subject: [PATCH 26/26] Fix baseURL of "I go to {url}" e2e step --- features/step_definitions/generic.step.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/step_definitions/generic.step.js b/features/step_definitions/generic.step.js index 277453a9c..865c6fdfe 100644 --- a/features/step_definitions/generic.step.js +++ b/features/step_definitions/generic.step.js @@ -142,7 +142,7 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => { }); When('I go to "{url}"', (url, callback) => { - browser.get(`http://localhost:8080/#${url}`).then(callback); + browser.get(`${browser.params.baseURL}#${url}`).then(callback); }); When('I {iterations} times move mouse randomly', (iterations, callback) => {