diff --git a/app/src/renderer/components/staking/CardCandidate.vue b/app/src/renderer/components/staking/CardCandidate.vue index 0937f66da7..f747e33ec4 100644 --- a/app/src/renderer/components/staking/CardCandidate.vue +++ b/app/src/renderer/components/staking/CardCandidate.vue @@ -1,25 +1,25 @@ @@ -28,7 +28,7 @@ transition(name='ts-card-candidate'): div(:class='cssClass') import { mapGetters } from 'vuex' import num from 'scripts/num' import Btn from '@nylira/vue-button' -// import { maxBy } from 'lodash' +import { maxBy } from 'lodash' export default { name: 'card-candidate', props: ['candidate'], @@ -36,52 +36,44 @@ export default { Btn }, computed: { - ...mapGetters(['shoppingCart', 'candidates', 'user']), - cssClass () { + ...mapGetters(['shoppingCart', 'candidates']), + styles () { let value = 'card-candidate' if (this.inCart) value += ' card-candidate-active ' return value }, - signedIn () { - return this.user.signedIn + vpMax () { + if (this.candidates.length > 0) { + let richestCandidate = maxBy(this.candidates, 'voting_power') + return richestCandidate.voting_power + } else { return 0 } }, - maxAtoms () { - // if (this.candidates.length > 0) { - // let richestCandidate = maxBy(this.candidates, 'atoms') - // return richestCandidate.atoms - // } else { return 0 } - return 0 - }, - atomsCss () { - let percentage = Math.round((this.candidate.atoms / this.maxAtoms) * 100) + vpStyles () { + let percentage = + Math.round((this.candidate.voting_power / this.vpMax) * 100) return { width: percentage + '%' } }, - maxDelegatedAtoms () { - // if (this.candidates) { - // let richestCandidate = maxBy(this.candidates, 'computed.delegatedAtoms') - // return richestCandidate.computed.delegatedAtoms - // } else { return 0 } - return 0 + sharesMax () { + if (this.candidates) { + let richestCandidate = maxBy(this.candidates, 'shares') + return richestCandidate.shares + } else { return 0 } }, - delegatedAtomsCss () { - let percentage = Math.round((this.candidate.delegatedAtoms / - this.maxDelegatedAtoms) * 100) + sharesStyles () { + let percentage = + Math.round((this.candidate.shares / this.sharesMax) * 100) return { width: percentage + '%' } }, inCart () { - return this.shoppingCart.find(c => c.id === this.candidate.id) + return this.shoppingCart.candidates.find(c => c.id === this.candidate.id) } }, data: () => ({ num: num }), methods: { - add (candidate) { - this.$store.commit('addToCart', candidate) - }, - rm (candidate) { - this.$store.commit('removeFromCart', candidate.id) - } + add (candidate) { this.$store.commit('addToCart', candidate) }, + rm (candidate) { this.$store.commit('removeFromCart', candidate.id) } } } @@ -99,6 +91,9 @@ export default { .card-candidate-container position relative + &:hover + menu + display block .values display flex @@ -111,9 +106,7 @@ export default { align-items center justify-content space-between - color dim padding 0 0.25rem - font-size sm min-width 0 @@ -121,11 +114,9 @@ export default { i.fa margin-right 0.5rem a - color txt + color link &:hover - color bright - &.num - mono() + color hover &.bar position relative @@ -138,16 +129,14 @@ export default { line-height 2rem padding 0 0.375rem + color txt .bar height 1.5rem - background darken(app-bg, 20%) - margin-right 1rem - &.delegated - span - color dim - .bar - background accent + background alpha(link, 33.3%) + + &.delegated .bar + background alpha(accent, 33.3%) span display block @@ -166,6 +155,7 @@ export default { display flex align-items center justify-content center + display none @media screen and (max-width: 414px) .card-candidate-container menu .ni-btn .ni-btn-value diff --git a/app/src/renderer/components/staking/PageCandidates.vue b/app/src/renderer/components/staking/PageCandidates.vue index ed0775b4a8..1dc8f530b5 100644 --- a/app/src/renderer/components/staking/PageCandidates.vue +++ b/app/src/renderer/components/staking/PageCandidates.vue @@ -4,7 +4,7 @@ page(:title='pageTitle') a(@click='setSearch(true)') i.material-icons search .label Search - router-link(v-if="" to='/staking/delegate') + router-link(to='/staking/delegate') i.material-icons check_circle .label Delegate modal-search(v-if="filters.candidates.search.visible" type="candidates") @@ -42,41 +42,34 @@ export default { ToolBar }, computed: { - ...mapGetters(['candidates', 'filters', 'shoppingCart', 'user']), + ...mapGetters(['candidates', 'filters', 'shoppingCart']), pageTitle () { - if (this.user.signedIn) return `Candidates (${this.candidatesNum} Selected)` - else return 'Candidates' + return `Delegate (${this.candidatesNum} Candidates Selected)` }, filteredCandidates () { let query = this.filters.candidates.search.query let list = orderBy(this.candidates, [this.sort.property], [this.sort.order]) if (this.filters.candidates.search.visible) { - return list.filter(i => includes(i.keybaseID.toLowerCase(), query)) + return list.filter(i => includes(i.keybaseID.toLowerCase(), query.toLowerCase())) } else { return list } }, - candidatesNum () { - return this.shoppingCart.length - }, - sort () { - let props = [ - { id: 1, title: 'Keybase ID', value: 'keybaseID' }, - { id: 2, title: 'Public Key', value: 'id' }, - { id: 3, title: 'Delegated', value: 'voting_power', initial: true } - ] - if (this.user.signedIn) { - props.push({ id: 4, title: 'Delegated (Yours)', value: 'delegated' }) - } - return { - property: 'voting_power', - order: 'desc', - properties: props - } - } + candidatesNum () { return this.shoppingCart.candidates.length } }, data: () => ({ - query: '' + query: '', + sort: { + property: 'keybaseID', + order: 'asc', + properties: [ + { id: 1, title: 'Keybase ID', value: 'keybaseID', initial: true }, + { id: 2, title: 'Country', value: 'country' }, + { id: 3, title: 'Voting Power', value: 'voting_power' }, + { id: 4, title: 'Delegated Power', value: 'shares' }, + { id: 5, title: 'Commission', value: 'commission' } + ] + } }), methods: { setSearch (bool) { this.$store.commit('setSearchVisible', ['candidates', bool]) } diff --git a/app/src/renderer/components/staking/PanelSort.vue b/app/src/renderer/components/staking/PanelSort.vue index 33a9cb033b..2222f4d087 100644 --- a/app/src/renderer/components/staking/PanelSort.vue +++ b/app/src/renderer/components/staking/PanelSort.vue @@ -14,10 +14,8 @@ export default { methods: { orderBy (property, event) { let sortBys = $(this.$el).find('.sort-by') - console.log(sortBys) $(sortBys).removeClass('active desc asc') let el = $(event.target).parent() - console.log('el', el) if (this.sort.property === property) { if (this.sort.order === 'asc') { @@ -65,6 +63,7 @@ export default { .label font-size sm color dim + text-transform uppercase padding-right 0.5rem white-space nowrap @@ -75,8 +74,10 @@ export default { display block font-family FontAwesome color dim + &.asc:after content '\f0d8' + &.desc:after content '\f0d7' diff --git a/app/src/renderer/vuex/modules/shoppingCart.js b/app/src/renderer/vuex/modules/shoppingCart.js index 5004784a39..bb4d3378d6 100644 --- a/app/src/renderer/vuex/modules/shoppingCart.js +++ b/app/src/renderer/vuex/modules/shoppingCart.js @@ -1,4 +1,3 @@ -import { findIndex } from 'lodash' export default ({ commit, basecoin }) => { let state = { candidates: [] } @@ -9,16 +8,9 @@ export default ({ commit, basecoin }) => { candidate: Object.assign({}, candidate), atoms: 0 }) - console.log(`+ ADD ${candidate.keybaseID} to cart`) }, removeFromCart (state, candidate) { - let index = findIndex(state.candidates, c => { - return c.candidate.id === candidate - }) - // console.log(`- RM ${JSON.stringify(state.candidates[index])} from cart[${index}]`) - let candidates = state.candidates.slice() - candidates.splice(index, 1) - state.candidates = candidates + state.candidates = state.candidates.filter(c => c.id !== candidate) } } diff --git a/test/unit/specs/CardCandidate.spec.js b/test/unit/specs/CardCandidate.spec.js new file mode 100644 index 0000000000..58c4e933cf --- /dev/null +++ b/test/unit/specs/CardCandidate.spec.js @@ -0,0 +1,104 @@ +import Vuex from 'vuex' +import { mount, createLocalVue } from 'vue-test-utils' +import CardCandidate from 'renderer/components/staking/CardCandidate' + +const shoppingCart = require('renderer/vuex/modules/shoppingCart').default({}) +const candidates = require('renderer/vuex/modules/candidates').default({}) + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('CardCandidate', () => { + let wrapper, store, candidate + + beforeEach(() => { + store = new Vuex.Store({ + getters: { + shoppingCart: () => shoppingCart.state, + candidates: () => candidates.state + }, + modules: { + shoppingCart, + candidates + } + }) + + store.commit('addCandidate', { + pubkey: 'pubkeyX', + description: JSON.stringify({ + id: 'idX', + description: 'descriptionX', + voting_power: 10000, + shares: 5000, + keybaseID: 'keybaseX', + country: 'USA' + }) + }) + store.commit('addCandidate', { + pubkey: 'pubkeyY', + description: JSON.stringify({ + id: 'idY', + description: 'descriptionY', + voting_power: 30000, + shares: 10000, + keybaseID: 'keybaseY', + country: 'Canada' + }) + }) + + candidate = store.state.candidates[0] + + wrapper = mount(CardCandidate, { + localVue, + store, + propsData: { + candidate + } + }) + + jest.spyOn(store, 'commit') + }) + + it('has the expected html structure', () => { + expect(wrapper.vm.$el).toMatchSnapshot() + }) + + it('should show the country', () => { + expect(wrapper.html()).toContain('USA') + }) + + it('should show the voting power', () => { + expect(wrapper.html()).toContain('10,000') + }) + + it('should show the relative voting power as a bar', () => { + expect(wrapper.vm.$el.querySelector('.voting_power .bar').style.width).toBe(Math.floor(10000 / 30000 * 100) + '%') + }) + + it('should show the relative shares hold as a bar', () => { + expect(wrapper.vm.$el.querySelector('.voting_power .bar').style.width).toBe(Math.floor(5000 / 15000 * 100) + '%') + }) + + it('should add to cart', () => { + expect(wrapper.vm.shoppingCart.candidates).toEqual([]) + expect(wrapper.vm.inCart).toBeFalsy() + expect(wrapper.find('menu .ni-btn').text()).toContain('Add') + expect(wrapper.html()).not.toContain('card-candidate-active') + wrapper.find('menu .ni-btn').trigger('click') + expect(wrapper.vm.inCart).toBeTruthy() + expect(store.commit).toHaveBeenCalledWith('addToCart', store.state.candidates[0]) + expect(wrapper.html()).toContain('card-candidate-active') + }) + + it('should remove from cart', () => { + store.commit('addToCart', store.state.candidates[0]) + wrapper.update() + expect(wrapper.vm.inCart).toBeTruthy() + expect(wrapper.find('menu .ni-btn').text()).toContain('Remove') + wrapper.find('menu .ni-btn').trigger('click') + expect(store.commit).toHaveBeenCalledWith('removeFromCart', candidate.id) + expect(wrapper.vm.shoppingCart.candidates).toEqual([]) + expect(wrapper.vm.inCart).toBeFalsy() + expect(wrapper.html()).not.toContain('card-candidate-active') + }) +}) diff --git a/test/unit/specs/PageCandidates.spec.js b/test/unit/specs/PageCandidates.spec.js new file mode 100644 index 0000000000..8dc54cda7e --- /dev/null +++ b/test/unit/specs/PageCandidates.spec.js @@ -0,0 +1,118 @@ +import Vuex from 'vuex' +import { mount, createLocalVue } from 'vue-test-utils' +import PageCandidates from 'renderer/components/staking/PageCandidates' + +const shoppingCart = require('renderer/vuex/modules/shoppingCart').default({}) +const candidates = require('renderer/vuex/modules/candidates').default({}) +const filters = require('renderer/vuex/modules/filters').default({}) + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('PageCandidates', () => { + let wrapper, store + + beforeEach(() => { + store = new Vuex.Store({ + getters: { + shoppingCart: () => shoppingCart.state, + candidates: () => candidates.state, + filters: () => filters.state + }, + modules: { + shoppingCart, + candidates, + filters + } + }) + + store.commit('addCandidate', { + pubkey: 'pubkeyY', + description: JSON.stringify({ + id: 'idY', + description: 'descriptionY', + voting_power: 30000, + shares: 10000, + keybaseID: 'keybaseY', + country: 'Canada' + }) + }) + store.commit('addCandidate', { + pubkey: 'pubkeyX', + description: JSON.stringify({ + id: 'idX', + description: 'descriptionX', + voting_power: 2000, + shares: 5000, + keybaseID: 'keybaseX', + country: 'USA' + }) + }) + + wrapper = mount(PageCandidates, { + localVue, + store, + stubs: { + 'data-error': '' + } + }) + + jest.spyOn(store, 'commit') + }) + + it('has the expected html structure', () => { + expect(wrapper.vm.$el).toMatchSnapshot() + }) + + it('should show the search on click', () => { + wrapper.find('.ni-tool-bar i').trigger('click') + expect(wrapper.contains('.ni-modal-search')).toBe(true) + }) + + it('should sort the candidates by selected property', () => { + expect(wrapper.vm.filteredCandidates.map(x => x.id)).toEqual(['idX', 'idY']) + wrapper.vm.sort = 'voting_power' + expect(wrapper.vm.filteredCandidates.map(x => x.id)).toEqual(['idY', 'idX']) + }) + + it('should filter the candidates', () => { + store.commit('setSearchVisible', ['candidates', true]) + store.commit('setSearchQuery', ['candidates', 'baseX']) + expect(wrapper.vm.filteredCandidates.map(x => x.id)).toEqual(['idX']) + expect(wrapper.html()).toMatchSnapshot() + store.commit('setSearchQuery', ['candidates', 'baseY']) + expect(wrapper.vm.filteredCandidates.map(x => x.id)).toEqual(['idY']) + }) + + it('should show the amount of selected candidates', () => { + store.commit('addToCart', store.state.candidates[0]) + store.commit('addToCart', store.state.candidates[1]) + wrapper.update() + expect(wrapper.html()).toContain('2 Candidates Selected') + }) + + it('should show an error if there are no candidates', () => { + let store = new Vuex.Store({ + getters: { + shoppingCart: () => shoppingCart.state, + candidates: () => [], + filters: () => filters.state + }, + modules: { + shoppingCart, + candidates, + filters + } + }) + + let wrapper = mount(PageCandidates, { + localVue, + store, + stubs: { + 'data-error': '' + } + }) + + expect(wrapper.contains('data-error')).toBe(true) + }) +}) diff --git a/test/unit/specs/__snapshots__/CardCandidate.spec.js.snap b/test/unit/specs/__snapshots__/CardCandidate.spec.js.snap new file mode 100644 index 0000000000..e47ac1093f --- /dev/null +++ b/test/unit/specs/__snapshots__/CardCandidate.spec.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CardCandidate has the expected html structure 1`] = ` +
+
+
+
+ + + + keybaseX + + +
+
+ USA +
+
+ + 10,000 + +
+
+
+ + 5,000 + +
+
+
+ NaN% +
+
+ + + +
+
+`; diff --git a/test/unit/specs/__snapshots__/PageCandidates.spec.js.snap b/test/unit/specs/__snapshots__/PageCandidates.spec.js.snap new file mode 100644 index 0000000000..c4685e269b --- /dev/null +++ b/test/unit/specs/__snapshots__/PageCandidates.spec.js.snap @@ -0,0 +1,315 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageCandidates has the expected html structure 1`] = ` +
+
+
+
+
+ +
+ Delegate (0 Candidates Selected) +
+
+
+
+ +
+
+
+ +
+
+
+
+
+ + + search + +
+ Search +
+
+ + + check_circle + +
+ Delegate +
+
+
+ + + help_outline + + +
+
+
+
+
+
+
+
+ +
+
+
+
+ Keybase ID +
+
+
+
+ Country +
+
+
+
+ Voting Power +
+
+
+
+ Delegated Power +
+
+
+
+ Commission +
+
+
+
+
+
+
+
+ + + + keybaseX + + +
+
+ USA +
+
+ + 2,000 + +
+
+
+ + 5,000 + +
+
+
+ NaN% +
+
+ + + +
+
+
+
+
+
+ + + + keybaseY + + +
+
+ Canada +
+
+ + 30,000 + +
+
+
+ + 10,000 + +
+
+
+ NaN% +
+
+ + + +
+
+
+
+
+
+
+`; + +exports[`PageCandidates should filter the candidates 1`] = `"
Delegate (0 Candidates Selected)
search
Search
check_circle
Delegate
help_outline
Keybase ID
Country
Voting Power
Delegated Power
Commission
keybaseX
USA
2,000
5,000
NaN%
keybaseY
Canada
30,000
10,000
NaN%
"`;