Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Add selected votes counts bar - Closes #445 #754

Merged
merged 11 commits into from
Sep 26, 2017
9 changes: 7 additions & 2 deletions features/step_definitions/generic.step.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,13 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => {
.and.notify(callback);
});

Then('I should see text "{text}" in "{fieldName}" element', (text, fieldName, callback) => {
const selectorClass = `.${fieldName.replace(/ /g, '-')}`;
Then('I should see text "{text}" in "{elementName}" element', (text, elementName, 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);
});

Expand Down
24 changes: 24 additions & 0 deletions features/voting.feature
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ Feature: Voting tab
Then I should see "Insufficient funds for 1 LSK fee" error message
And "submit button" should be disabled

Scenario: should display voting bar with numbers of selected votes if any selected
Given I'm logged in as "delegate candidate"
When I click tab number 2
And I should see no "voting bar"
And I click checkbox on table row no. 3
Then I should see element "voting bar" that contains text:
"""
Upvotes: 1
Downvotes: 0
Total new votes: 1 / 33
Total votes: 1 / 101
"""
And I click checkbox on table row no. 5
And I should see element "voting bar" that contains text:
"""
Upvotes: 2
Downvotes: 0
Total new votes: 2 / 33
Total votes: 2 / 101
"""
And I click checkbox on table row no. 3
And I click checkbox on table row no. 5
And I should see no "voting bar"

Scenario: should allow to select delegates in the "Voting" tab and vote for them
Given I'm logged in as "delegate candidate"
When I click tab number 2
Expand Down
11 changes: 7 additions & 4 deletions src/components/voteDialog/voteDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import React from 'react';
import InfoParagraph from '../infoParagraph';
import ActionBar from '../actionBar';
import Fees from '../../constants/fees';
import votingConst from '../../constants/voting';
import Autocomplete from './voteAutocomplete';
import styles from './voteDialog.css';
import AuthInputs from '../authInputs';
import { authStatePrefill, authStateIsValid } from '../../utils/form';

const { maxCountOfVotes, maxCountOfVotesInOneTurn } = votingConst;

export default class VoteDialog extends React.Component {
constructor() {
super();
Expand Down Expand Up @@ -58,9 +61,9 @@ export default class VoteDialog extends React.Component {
<article className={styles.info}>
<InfoParagraph>
<p >
You can select up to 33 delegates in one voting turn.
You can select up to {maxCountOfVotesInOneTurn} delegates in one voting turn.
</p>
You can vote for up to 101 delegates in total.
You can vote for up to {maxCountOfVotes} delegates in total.
</InfoParagraph>
</article>

Expand All @@ -72,9 +75,9 @@ export default class VoteDialog extends React.Component {
label: 'Confirm',
fee: Fees.vote,
disabled: (
totalVotes > 101 ||
totalVotes > maxCountOfVotes ||
votesList.length === 0 ||
votesList.length > 33 ||
votesList.length > maxCountOfVotesInOneTurn ||
!authStateIsValid(this.state)
),
onClick: this.confirm.bind(this),
Expand Down
11 changes: 7 additions & 4 deletions src/components/voting/voting.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';
import { themr } from 'react-css-themr';
import { TABLE } from 'react-toolbox/lib/identifiers';
import { tableFactory } from 'react-toolbox/lib/table/Table';
import { TableHead, TableCell } from 'react-toolbox/lib/table';
import { tableFactory } from 'react-toolbox/lib/table/Table';
import { themr } from 'react-css-themr';
import React from 'react';
import TableTheme from 'react-toolbox/lib/table/theme.css';
import Waypoint from 'react-waypoint';

import Header from './votingHeader';
import VotingBar from './votingBar';
import VotingRow from './votingRow';

// Create a new Table component injecting Head and Row
Expand Down Expand Up @@ -40,7 +42,7 @@ class Voting extends React.Component {
}

loadVotedDelegates(refresh) {
/* istanbul-ignore-else */
/* istanbul-ignore-else */
if (!this.freezeLoading) {
this.props.votesFetched({
activePeer: this.props.activePeer,
Expand Down Expand Up @@ -126,6 +128,7 @@ class Voting extends React.Component {
scrollableAncestor={window}
key={this.props.delegates.length}
onEnter={this.loadMore.bind(this)}></Waypoint>
<VotingBar votes={this.props.votes} />
</div>
);
}
Expand Down
9 changes: 9 additions & 0 deletions src/components/voting/votingBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.red {
color: #c62828;
}

.fixedAtBottom {
position: fixed;
bottom: 0;
left: 8px;
}
50 changes: 50 additions & 0 deletions src/components/voting/votingBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import grid from 'flexboxgrid/dist/flexboxgrid.css';

import votingConst from '../../constants/voting';
import style from './votingBar.css';

const VotingBar = ({ votes }) => {
const { maxCountOfVotes, maxCountOfVotesInOneTurn } = votingConst;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent verbosity please perform this shorthand assignment while importing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use it like that, but it doesn't work.

import { maxCountOfVotes, maxCountOfVotesInOneTurn } from '../../constants/voting';

ends up in both maxCountOfVotes and maxCountOfVotesInOneTurn being undefined

const votedList = Object.keys(votes).filter(key => votes[key].confirmed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a single forEach loop instead of 3 filters and file the lists thought the same iteration

Copy link
Contributor Author

@slaweet slaweet Sep 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set up a jsperf to see the difference:
https://jsperf.com/multiple-filters-vs-foreach

ForEach is 3 or 4 times faster, but the absolute values of how long it takes on a list of 100 still make the difference negligible.

I like using filters for their conciseness and readability.

const voteList = Object.keys(votes).filter(
key => votes[key].unconfirmed && !votes[key].confirmed);
const unvoteList = Object.keys(votes).filter(
key => !votes[key].unconfirmed && votes[key].confirmed);
const totalVotesCount = (votedList.length - unvoteList.length) + voteList.length;
const totalNewVotesCount = voteList.length + unvoteList.length;

return (voteList.length + unvoteList.length ?
<div className={`${grid.row} ${style.fixedAtBottom} box voting-bar`}>
<div className={
`${grid['col-sm-12']} ${grid['col-md-10']} ${grid['col-md-offset-1']}
${grid.row} ${grid['center-xs']} ${grid['middle-xs']}`}>
<span className={`${grid['col-sm-3']} ${grid['col-xs-12']} upvotes`}>
<span>Upvotes: </span>
<strong>{voteList.length}</strong>
</span>
<span className={`${grid['col-sm-3']} ${grid['col-xs-12']} downvotes`}>
<span>Downvotes: </span>
<strong>{unvoteList.length}</strong>
</span>
<span className={`${grid['col-sm-3']} ${grid['col-xs-12']} total-new-votes`}>
<span>Total new votes: </span>
<strong className={totalNewVotesCount > maxCountOfVotesInOneTurn && style.red}>
{totalNewVotesCount}
</strong>
<span> / {maxCountOfVotesInOneTurn}</span>
</span>
<span className={`${grid['col-sm-3']} ${grid['col-xs-12']} total-votes`}>
<span>Total votes: </span>
<strong className={totalVotesCount > 101 && style.red}>
{totalVotesCount}
</strong>
<span> / {maxCountOfVotes}</span>
</span>
</div>
</div> :
null
);
};

export default VotingBar;
86 changes: 86 additions & 0 deletions src/components/voting/votingBar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';

import VotingBar from './votingBar';

import styles from './votingBar.css';

describe('VotingBar', () => {
let wrapper;
const props = {
votes: {
voted: {
confirmed: true,
unconfirmed: false,
},
downvote: {
confirmed: true,
unconfirmed: true,
},
upvote: {
confirmed: false,
unconfirmed: true,
},
upvote2: {
confirmed: false,
unconfirmed: true,
},
notVoted: {
confirmed: false,
unconfirmed: false,
},
},
};

const generateNVotes = n => (
[...Array(n)].map((item, i) => i).reduce(
(dict, value) => {
dict[`genesis_${value}`] = { unconfirmed: true };
return dict;
}, {})
);

beforeEach(() => {
wrapper = mount(<VotingBar {...props} />);
});

it('should render number of upvotes', () => {
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 downvotes', () => {
expect(wrapper.find('.total-new-votes')).to.have.text('Total new votes: 3 / 33');
});

it('should render number of total votes', () => {
expect(wrapper.find('.total-votes')).to.have.text('Total votes: 3 / 101');
});

it('should not render if no upvotes or downvotes', () => {
wrapper.setProps({ votes: {} });
expect(wrapper.html()).to.equal(null);
});

it('should render number of total votes in red if 101 exceeded', () => {
const votes = generateNVotes(102);

expect(wrapper.find(`.total-votes .${styles.red}`)).to.be.not.present();
wrapper.setProps({ votes });
expect(wrapper.find('.total-votes')).to.have.text('Total votes: 102 / 101');
expect(wrapper.find(`.total-votes .${styles.red}`)).to.have.text('102');
});

it('should render number of total new votes in red if 33 exceeded', () => {
const votes = generateNVotes(34);
expect(wrapper.find(`.total-new-votes .${styles.red}`)).to.be.not.present();
wrapper.setProps({ votes });
expect(wrapper.find('.total-new-votes')).to.have.text('Total new votes: 34 / 33');
expect(wrapper.find(`.total-new-votes .${styles.red}`)).to.have.text('34');
});
});

6 changes: 6 additions & 0 deletions src/constants/voting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const votingConst = {
maxCountOfVotesInOneTurn: 33,
maxCountOfVotes: 101,
};

export default votingConst;
2 changes: 2 additions & 0 deletions src/store/middlewares/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import addedTransactionMiddleware from './addedTransaction';
import loadingBarMiddleware from './loadingBar';
import offlineMiddleware from './offline';
import notificationMiddleware from './notification';
import votingMiddleware from './voting';

export default [
thunk,
Expand All @@ -16,4 +17,5 @@ export default [
loadingBarMiddleware,
offlineMiddleware,
notificationMiddleware,
votingMiddleware,
];
23 changes: 23 additions & 0 deletions src/store/middlewares/voting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import actionTypes from '../../constants/actions';
import votingConst from '../../constants/voting';
import { errorToastDisplayed } from '../../actions/toaster';

const votingMiddleware = store => next => (action) => {
next(action);
if (action.type === actionTypes.voteToggled) {
const { votes } = store.getState().voting;
const voteCount = Object.keys(votes).filter(
key => votes[key].confirmed !== votes[key].unconfirmed).length;
const currentVote = votes[action.data.username] || { unconfirmed: true, confirmed: false };
console.log(voteCount, votingConst.maxCountOfVotesInOneTurn, currentVote);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove console.log. It's interesting that you have a log here and eslint doesn't fail

if (voteCount === votingConst.maxCountOfVotesInOneTurn + 1 &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if we show a toast for when we exceed 101 overall votes.

currentVote.unconfirmed !== currentVote.confirmed) {
const label = `Maximum of ${votingConst.maxCountOfVotesInOneTurn} votes in one transaction exceeded.`;
const newAction = errorToastDisplayed({ label });
store.dispatch(newAction);
}
}
};

export default votingMiddleware;

68 changes: 68 additions & 0 deletions src/store/middlewares/voting.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect } from 'chai';
import { spy, stub } from 'sinon';
import { errorToastDisplayed } from '../../actions/toaster';
import middleware from './voting';
import actionTypes from '../../constants/actions';
import votingConst from '../../constants/voting';

describe('voting middleware', () => {
let store;
let next;
const label = `Maximum of ${votingConst.maxCountOfVotesInOneTurn} votes in one transaction exceeded.`;

const generateNVotes = n => (
[...Array(n)].map((item, i) => i).reduce(
(dict, value) => {
dict[`genesis_${value}`] = { confirmed: false, unconfirmed: true };
return dict;
}, {})
);

beforeEach(() => {
store = stub();
store.getState = () => ({
voting: {
votes: {
...generateNVotes(votingConst.maxCountOfVotesInOneTurn + 1),
test2: {
unconfirmed: false,
confirmed: false,
},
},
},
});
store.dispatch = spy();
next = spy();
});

it('should passes the action to next middleware', () => {
const givenAction = {
type: 'TEST_ACTION',
};

middleware(store)(next)(givenAction);
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 }));
});

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 }));
});
});