From 3515e99f5f47fcb9a83265831c4f45ce88a23f66 Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Wed, 8 Mar 2017 20:09:53 +0100 Subject: [PATCH 1/5] Payments features for 1.0 Resolves #7347 Resolves #6890 Auditors: @bsclifton @bradleyrichter @mrose17 Test plan: - npm run test -- --grep="LedgerTable" - npm run test -- --grep="Ledger table" --- app/extensions/brave/img/ledger/icon_pin.svg | 14 + .../brave/img/ledger/icon_remove.svg | 8 + .../locales/en-US/preferences.properties | 4 +- app/ledger.js | 163 +++--- .../preferences/payment/advancedSettings.js | 9 - .../preferences/payment/ledgerTable.js | 282 +++++++-- .../preferences/payment/pinnedInput.js | 70 +++ docs/appActions.md | 10 + docs/state.md | 2 + js/actions/appActions.js | 12 + js/components/sortableTable.js | 161 ++++-- js/components/switchControl.js | 12 +- js/constants/appConfig.js | 2 +- js/constants/appConstants.js | 3 +- js/constants/settings.js | 2 +- js/state/syncUtil.js | 3 +- js/stores/appStore.js | 9 + less/sortableTable.less | 8 +- package.json | 2 +- test/about/ledgerTableTest.js | 268 +++++++++ test/lib/brave.js | 16 + test/unit/about/preferencesTest.js | 2 + .../renderer/components/ledgerTableTest.js | 544 ++++++++++++++++++ test/unit/app/renderer/paymentsTabTest.js | 2 + 24 files changed, 1427 insertions(+), 181 deletions(-) create mode 100644 app/extensions/brave/img/ledger/icon_pin.svg create mode 100644 app/extensions/brave/img/ledger/icon_remove.svg create mode 100644 app/renderer/components/preferences/payment/pinnedInput.js create mode 100644 test/about/ledgerTableTest.js create mode 100644 test/unit/app/renderer/components/ledgerTableTest.js diff --git a/app/extensions/brave/img/ledger/icon_pin.svg b/app/extensions/brave/img/ledger/icon_pin.svg new file mode 100644 index 00000000000..d79fdc493f0 --- /dev/null +++ b/app/extensions/brave/img/ledger/icon_pin.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/extensions/brave/img/ledger/icon_remove.svg b/app/extensions/brave/img/ledger/icon_remove.svg new file mode 100644 index 00000000000..95dc2238cb1 --- /dev/null +++ b/app/extensions/brave/img/ledger/icon_remove.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/app/extensions/brave/locales/en-US/preferences.properties b/app/extensions/brave/locales/en-US/preferences.properties index 2a8724229ea..5172dc2a4bd 100644 --- a/app/extensions/brave/locales/en-US/preferences.properties +++ b/app/extensions/brave/locales/en-US/preferences.properties @@ -122,7 +122,6 @@ done=Done off=off on=on ok=Ok -minimumPercentage=Hide sites with less than 1% usage autoSuggestSites=auto-include notifications=Show notifications moneyAdd=Use your debit/credit card @@ -152,6 +151,7 @@ site=Site views=Views timeSpent=Time Spent include=Include +actions=Actions percentage=% remove=Remove bravery=Bravery @@ -358,3 +358,5 @@ useTorrentViewer=Enable Torrent Viewer * dashboardSettingsTitle=Dashboard dashboardShowImages=Show images requiresRestart=* Requires browser restart +showAll=Show all +hideLower=Hide lower diff --git a/app/ledger.js b/app/ledger.js index 61d7197f7d1..50b9fbcdb52 100644 --- a/app/ledger.js +++ b/app/ledger.js @@ -186,10 +186,6 @@ const doAction = (action) => { updatePublisherInfo() break - case settings.MINIMUM_PERCENTAGE: - updatePublisherInfo() - break - default: break } @@ -213,6 +209,10 @@ const doAction = (action) => { if (publisherInfo._internal.verboseP) console.log('\nupdating ' + publisher + ' stickyP=' + action.value) updatePublisherInfo() verifiedP(publisher) + } else if (action.key === 'ledgerPinPercentage') { + if (!synopsis.publishers[publisher]) break + synopsis.publishers[publisher].pinPercentage = action.value + updatePublisherInfo() } break @@ -1000,7 +1000,7 @@ var stickyP = (publisher) => { delete synopsis.publishers[publisher].options.stickyP } - return (result || false) + return (result === undefined || result) } var eligibleP = (publisher) => { @@ -1022,92 +1022,125 @@ var contributeP = (publisher) => { } var synopsisNormalizer = () => { - var i, duration, minP, n, pct, publisher, results, total - var data = [] - var scorekeeper = synopsis.options.scorekeeper + // courtesy of https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100#13485888 + const roundToTarget = (l, target, property) => { + let off = target - underscore.reduce(l, (acc, x) => { return acc + Math.round(x[property]) }, 0) - results = [] - underscore.keys(synopsis.publishers).forEach((publisher) => { - if (!visibleP(publisher)) return + return underscore.sortBy(l, (x) => Math.round(x[property]) - x[property]) + .map((x, i) => { + x[property] = Math.round(x[property]) + (off > i) - (i >= (l.length + off)) + return x + }) + } - results.push(underscore.extend({ publisher: publisher }, underscore.omit(synopsis.publishers[publisher], 'window'))) - }, synopsis) - results = underscore.sortBy(results, (entry) => { return -entry.scores[scorekeeper] }) - n = results.length - - total = 0 - for (i = 0; i < n; i++) { total += results[i].scores[scorekeeper] } - if (total === 0) return data - - pct = [] - for (i = 0; i < n; i++) { - publisher = synopsis.publishers[results[i].publisher] - duration = results[i].duration - - data[i] = { - rank: i + 1, - verified: results[i].options.verified || false, - site: results[i].publisher, - views: results[i].visits, + const normalizePinned = (dataPinned, total, target) => dataPinned.map((publisher) => { + let newPer = Math.floor((publisher.pinPercentage / total) * target) + if (newPer < 1) { + newPer = 1 + } + + publisher.pinPercentage = newPer + return publisher + }) + + const getPublisherData = (result) => { + let duration = result.duration + + let data = { + verified: result.options.verified || false, + site: result.publisher, + views: result.visits, duration: duration, daysSpent: 0, hoursSpent: 0, minutesSpent: 0, secondsSpent: 0, - faviconURL: publisher.faviconURL, - score: results[i].scores[scorekeeper] + faviconURL: result.faviconURL, + score: result.scores[scorekeeper], + pinPercentage: result.pinPercentage } // HACK: Protocol is sometimes blank here, so default to http:// so we can // still generate publisherURL. - data[i].publisherURL = (results[i].protocol || 'http:') + '//' + results[i].publisher - - pct[i] = Math.round((results[i].scores[scorekeeper] * 100) / total) + data.publisherURL = (result.protocol || 'http:') + '//' + result.publisher if (duration >= msecs.day) { - data[i].daysSpent = Math.max(Math.round(duration / msecs.day), 1) + data.daysSpent = Math.max(Math.round(duration / msecs.day), 1) } else if (duration >= msecs.hour) { - data[i].hoursSpent = Math.max(Math.floor(duration / msecs.hour), 1) - data[i].minutesSpent = Math.round((duration % msecs.hour) / msecs.minute) + data.hoursSpent = Math.max(Math.floor(duration / msecs.hour), 1) + data.minutesSpent = Math.round((duration % msecs.hour) / msecs.minute) } else if (duration >= msecs.minute) { - data[i].minutesSpent = Math.max(Math.round(duration / msecs.minute), 1) - data[i].secondsSpent = Math.round((duration % msecs.minute) / msecs.second) + data.minutesSpent = Math.max(Math.round(duration / msecs.minute), 1) + data.secondsSpent = Math.round((duration % msecs.minute) / msecs.second) } else { - data[i].secondsSpent = Math.max(Math.round(duration / msecs.second), 1) + data.secondsSpent = Math.max(Math.round(duration / msecs.second), 1) } + + return data } - // courtesy of https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100#13485888 - var foo = (l, target) => { - var off = target - underscore.reduce(l, (acc, x) => { return acc + Math.round(x) }, 0) + let results + let dataPinned = [] + let dataUnPinned = [] + let dataExcluded = [] + let pinnedTotal = 0 + let unPinnedTotal = 0 + const scorekeeper = synopsis.options.scorekeeper - return underscore.chain(l) - .sortBy((x) => { return Math.round(x) - x }) - .map((x, i) => { return Math.round(x) + (off > i) - (i >= (l.length + off)) }) - .value() - } + results = [] + underscore.keys(synopsis.publishers).forEach((publisher) => { + if (!visibleP(publisher)) return - minP = getSetting(settings.MINIMUM_PERCENTAGE) - pct = foo(pct, 100) - total = 0 - for (i = 0; i < n; i++) { - if (pct[i] < 0) pct[i] = 0 - if ((minP) && (pct[i] < 1)) { - data = data.slice(0, i) - break + results.push(underscore.extend({ publisher: publisher }, underscore.omit(synopsis.publishers[publisher], 'window'))) + }, synopsis) + results = underscore.sortBy(results, (entry) => { return -entry.scores[scorekeeper] }) + + // move publisher to the correct array and get totals + results.forEach((result) => { + if (result.pinPercentage && result.pinPercentage > 0) { + // pinned + pinnedTotal += result.pinPercentage + dataPinned.push(getPublisherData(result)) + } else if (stickyP(result.publisher)) { + // unpinned + unPinnedTotal += result.scores[scorekeeper] + dataUnPinned.push(result) + } else { + // excluded + let publisher = getPublisherData(result) + publisher.percentage = 0 + dataExcluded.push(publisher) } + }) - data[i].percentage = pct[i] - total += pct[i] - } + // round if over 100% of pinned publishers + if (pinnedTotal > 100) { + dataPinned = roundToTarget(normalizePinned(dataPinned, pinnedTotal, 100), 100, 'pinPercentage') + dataUnPinned = dataUnPinned.map((result) => { + let publisher = getPublisherData(result) + publisher.percentage = 0 + return publisher + }) - for (i = data.length - 1; (total > 100) && (i >= 0); i--) { - if (data[i].percentage < 2) continue + // sync synopsis + dataPinned.forEach((item) => { + synopsis.publishers[item.site].pinPercentage = item.pinPercentage + }) + + // sync app store + appActions.changeLedgerPinnedPercentages(dataPinned) + } else { + // unpinned publishers + dataUnPinned = dataUnPinned.map((result) => { + let publisher = getPublisherData(result) + publisher.percentage = Math.round((publisher.score / unPinnedTotal) * (100 - pinnedTotal)) + return publisher + }) - data[i].percentage-- - total-- + // normalize unpinned values + dataUnPinned = roundToTarget(dataUnPinned, (100 - pinnedTotal), 'percentage') } - return data + return dataPinned.concat(dataUnPinned, dataExcluded) } /* diff --git a/app/renderer/components/preferences/payment/advancedSettings.js b/app/renderer/components/preferences/payment/advancedSettings.js index 2b5ec7c2ff6..d961d192e03 100644 --- a/app/renderer/components/preferences/payment/advancedSettings.js +++ b/app/renderer/components/preferences/payment/advancedSettings.js @@ -61,15 +61,6 @@ class AdvancedSettingsContent extends ImmutableComponent { - 0 + } + + pinPercentageValue (synopsis) { + return synopsis.get('pinPercentage') + } + banSite (hostPattern) { aboutActions.changeSiteSetting(hostPattern, 'ledgerPaymentsShown', false) } + togglePinSite (hostPattern, pinned, percentage) { + if (pinned) { + if (percentage < 1) { + percentage = 1 + } else { + percentage = Math.floor(percentage) + } + + aboutActions.changeSiteSetting(hostPattern, 'ledgerPinPercentage', percentage) + aboutActions.changeSiteSetting(hostPattern, 'ledgerPayments', true) + } else { + aboutActions.changeSiteSetting(hostPattern, 'ledgerPinPercentage', 0) + } + } + get columnClassNames () { return [ - css(styles.tableTd), - css(styles.tableTd, styles.alignRight), - css(styles.tableTd), - css(styles.tableTd), - css(styles.tableTd, styles.alignRight), - css(styles.tableTd, styles.alignRight), - css(styles.tableTd, styles.alignRight) + css(styles.tableTd, styles.alignRight, styles.verifiedTd), // verified + css(styles.tableTd, styles.alignRight), // sites + css(styles.tableTd), // include + css(styles.tableTd, styles.alignRight), // views + css(styles.tableTd, styles.alignRight), // time spent + css(styles.tableTd, styles.alignRight, styles.percTd), // percentage + css(styles.tableTd, styles.alignLeft) // actions + ] + } + + rowClassNames (pinnedRows, unPinnedRows) { + let j = -1 + + return [ + pinnedRows.map(item => { + j++ + return this.enabledForSite(item) + ? css(styles.tableTr, j % 2 && styles.tableTdBg) + : css(styles.tableTr, styles.paymentsDisabled, j % 2 && styles.tableTdBg) + }).toJS(), + unPinnedRows.map(item => { + j++ + return this.enabledForSite(item) + ? css(styles.tableTr, j % 2 && styles.tableTdBg) + : css(styles.tableTr, styles.paymentsDisabled, j % 2 && styles.tableTdBg) + }).toJS() ] } getRow (synopsis) { - if (!synopsis || !synopsis.get || !this.shouldShow(synopsis)) { - return [] - } const faviconURL = synopsis.get('faviconURL') - const rank = synopsis.get('rank') const views = synopsis.get('views') const verified = synopsis.get('verified') + const pinned = this.isPinned(synopsis) const duration = synopsis.get('duration') const publisherURL = synopsis.get('publisherURL') - const percentage = synopsis.get('percentage') + const percentage = pinned ? this.pinPercentageValue(synopsis) : synopsis.get('percentage') const site = synopsis.get('site') const defaultAutoInclude = this.enabledForSite(synopsis) + const rowRefName = 'rowPercentage_' + site + if (this.refs[rowRefName]) { + this.refs[rowRefName].value = percentage + } + return [ { - html:
- -
, + html: verified && this.getVerifiedIcon(synopsis), value: '' }, - rank, { html:
- { - verified && this.getVerifiedIcon(synopsis) - } { faviconURL ? {site} : } - {site} + {site}
, value: site }, { - html: , + html: pinned + ? {}} + /> + : , value: this.enabledForSite(synopsis) ? 1 : 0 }, views, @@ -142,7 +199,31 @@ class LedgerTable extends ImmutableComponent { html: this.getFormattedTime(synopsis), value: duration }, - percentage + { + html: + { + pinned + ? + : percentage + } + , + value: percentage + }, + { + html: + + + , + value: '' + } ] } @@ -150,6 +231,33 @@ class LedgerTable extends ImmutableComponent { if (!this.synopsis || !this.synopsis.size) { return null } + + const allRows = this.synopsis.filter(synopsis => { + return (!getSetting(settings.HIDE_EXCLUDED_SITES, this.props.settings) || this.enabledForSite(synopsis)) && + this.shouldShow(synopsis) + }) + const pinnedRows = allRows.filter(synopsis => { + return this.isPinned(synopsis) + }) + let unPinnedRows = allRows.filter(synopsis => { + return !this.isPinned(synopsis) + }) + + const totalUnPinnedRows = unPinnedRows.size + const hideLower = getSetting(settings.HIDE_LOWER_SITES, this.props.settings) + + if (hideLower && totalUnPinnedRows > 2) { + let sumUnPinned = 0 + let threshold = 90 + const limit = 0.9 // show only 90th of publishers + + threshold = unPinnedRows.reduce((value, publisher) => value + publisher.get('percentage'), 0) * limit + unPinnedRows = unPinnedRows.filter(publisher => { + sumUnPinned += publisher.get('percentage') + return !(sumUnPinned >= threshold) + }) + } + return
@@ -164,36 +272,54 @@ class LedgerTable extends ImmutableComponent {
this.enabledForSite(item) - ? css(styles.tableTr, i % 2 && styles.tableTdBg) - : css(styles.tableTr, styles.paymentsDisabled, i % 2 && styles.tableTdBg) - ).toJS()} + rowClassNames={this.rowClassNames(pinnedRows, unPinnedRows)} + bodyClassNames={[css(unPinnedRows.size > 0 && styles.pinnedBody), '']} onContextMenu={aboutActions.contextMenu} contextMenuName='synopsis' - rowObjects={this.synopsis.map(entry => { - return { - hostPattern: this.getHostPattern(entry), - location: entry.get('publisherURL') - } - }).toJS()} - rows={this.synopsis.filter(synopsis => { - return !getSetting(settings.HIDE_EXCLUDED_SITES, this.props.settings) || this.enabledForSite(synopsis) - }).map((synopsis) => this.getRow(synopsis)).toJS()} + rowObjects={[ + pinnedRows.map(entry => { + return { + hostPattern: this.getHostPattern(entry), + location: entry.get('publisherURL') + } + }).toJS(), + unPinnedRows.map(entry => { + return { + hostPattern: this.getHostPattern(entry), + location: entry.get('publisherURL') + } + }).toJS() + ]} + rows={[ + pinnedRows.map((synopsis) => this.getRow(synopsis)).toJS(), + unPinnedRows.map((synopsis) => this.getRow(synopsis)).toJS() + ]} /> + { + totalUnPinnedRows > 1 || (totalUnPinnedRows !== unPinnedRows.size && hideLower) + ?
+
+ : null + }
} } const verifiedBadge = (icon) => ({ - position: 'absolute', height: '20px', width: '20px', - left: '-10px', - top: '3px', + marginRight: '-10px', + display: 'block', background: `url(${icon}) center no-repeat` }) @@ -213,7 +339,8 @@ const styles = StyleSheet.create({ tableClass: { width: '100%', - textAlign: 'left' + textAlign: 'left', + borderCollapse: 'collapse' }, tableTh: { @@ -238,6 +365,24 @@ const styles = StyleSheet.create({ background: '#f6f7f7' }, + verifiedTd: { + padding: '0 0 0 15px' + }, + + percTd: { + width: '45px', + paddingLeft: '5px' + }, + + hideTd: { + display: 'none' + }, + + pinnedBody: { + borderBottom: `1px solid ${globalStyles.color.braveOrange}`, + borderCollapse: 'collapse' + }, + siteData: { display: 'flex', flex: '1', @@ -282,8 +427,51 @@ const styles = StyleSheet.create({ textAlign: 'right' }, + alignLeft: { + textAlign: 'left' + }, + paymentsDisabled: { opacity: 0.6 + }, + + mainIcon: { + backgroundColor: '#c4c5c5', + width: '15px', + height: '16px', + display: 'inline-block', + marginRight: '10px', + marginTop: '6px', + + ':hover': { + backgroundColor: globalStyles.color.buttonColor + } + }, + + pinIcon: { + '-webkit-mask-image': `url(${pinIcon})` + }, + + pinnedIcon: { + backgroundColor: globalStyles.color.braveOrange, + + ':hover': { + backgroundColor: globalStyles.color.braveDarkOrange + } + }, + + removeIcon: { + '-webkit-mask-image': `url(${removeIcon})` + }, + + pinnedToggle: { + right: '2px' + }, + + showAllWrap: { + textAlign: 'center', + paddingBottom: '10px', + marginTop: '-20px' } }) diff --git a/app/renderer/components/preferences/payment/pinnedInput.js b/app/renderer/components/preferences/payment/pinnedInput.js new file mode 100644 index 00000000000..35b502f3545 --- /dev/null +++ b/app/renderer/components/preferences/payment/pinnedInput.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const React = require('react') +const {StyleSheet, css} = require('aphrodite') + +// components +const ImmutableComponent = require('../../../../../js/components/immutableComponent') + +// style +const globalStyles = require('../../styles/global') + +// other +const aboutActions = require('../../../../../js/about/aboutActions') + +class PinnedInput extends ImmutableComponent { + componentDidUpdate () { + this.textInput.value = this.props.defaultValue + } + + keyPress (event) { + if (event.key === 'Enter') { + this.textInput.blur() + } + } + + pinPercentage (hostPattern, event) { + let value = parseInt(event.target.value) + + if (value < 1) { + value = 1 + this.textInput.value = 1 + } + + aboutActions.changeSiteSetting(hostPattern, 'ledgerPinPercentage', value) + } + render () { + return { this.textInput = input }} + defaultValue={this.props.defaultValue} + onBlur={this.pinPercentage.bind(this, this.props.patern)} + onKeyPress={this.keyPress.bind(this)} + className={css(styles.percInput)} + /> + } +} + +const styles = StyleSheet.create({ + percInput: { + height: '22px', + width: '50px', + borderRadius: globalStyles.radius.borderRadius, + textAlign: 'right', + backgroundColor: 'transparent', + outline: 'none', + border: `1px solid #c4c5c5`, + padding: '0 9px', + fontSize: '16px', + marginRight: '-10px', + + ':focus': { + backgroundColor: '#fff', + borderColor: globalStyles.color.highlightBlue + } + } +}) + +module.exports = PinnedInput diff --git a/docs/appActions.md b/docs/appActions.md index f2884d8431d..337c18185b8 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -809,6 +809,16 @@ also change all undefined ledgerPayments to value true +### changeLedgerPinnedPercentages(publishers) + +Update ledger publishers pinned percentages according to the new synopsis + +**Parameters** + +**publishers**: `Object`, updated publishers + + + * * * diff --git a/docs/state.md b/docs/state.md index e8f67742a94..3a2e57b9926 100644 --- a/docs/state.md +++ b/docs/state.md @@ -248,6 +248,7 @@ AppStore httpsEverywhere: boolean, ledgerPayments: boolean, // false if site should not be paid by the ledger. Defaults to true. ledgerPaymentsShown: boolean, // false if site should not be paid by the ledger and should not be shown in the UI. Defaults to true. + ledgerPinPercentage: number, // 0 if not pinned, otherwise is pinned with defined percentage mediaPermission: boolean, midiSysexPermission: boolean, notificationsPermission: boolean, @@ -581,6 +582,7 @@ WindowStore hoursSpent: number, // e.g., 2 minutesSpent: number, // e.g., 3 percentage: number, // i.e., 0, 1, ... 100 + pinPercentage: number, // i.e., 0, 1, ... 100 publisherURL: string, // publisher site, e.g., "https://wikipedia.org/" rank: number, // i.e., 1, 2, 3, ... score: number, // float indicating the current score diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 942fdbd3d6c..cd697b0d4e8 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -982,6 +982,7 @@ const appActions = { }, /** + * Change all undefined publishers in site settings to defined sites * also change all undefined ledgerPayments to value true * @param publishers {Object} publishers from the synopsis @@ -991,6 +992,17 @@ const appActions = { actionType: appConstants.APP_ENABLE_UNDEFINED_PUBLISHERS, publishers }) + }, + + /** + * Update ledger publishers pinned percentages according to the new synopsis + * @param publishers {Object} updated publishers + */ + changeLedgerPinnedPercentages: function (publishers) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_CHANGE_LEDGER_PINNED_PERCENTAGES, + publishers + }) } } diff --git a/js/components/sortableTable.js b/js/components/sortableTable.js index 401c696c514..6311635af61 100644 --- a/js/components/sortableTable.js +++ b/js/components/sortableTable.js @@ -25,9 +25,16 @@ class SortableTable extends React.Component { this.state = { selection: Immutable.Set() } + this.counter = 0 + this.sortTable = null } - componentDidMount (event) { - return tableSort(this.table) + componentDidMount () { + this.sortTable = tableSort(this.table) + return this.sortTable + } + + componentWillUpdate () { + this.sortTable.refresh() } /** * If you want multi-select to span multiple tables, you can @@ -220,20 +227,43 @@ class SortableTable extends React.Component { return this.props.columnClassNames && this.props.columnClassNames.length === this.props.headings.length } - get hasRowClassNames () { - return this.props.rowClassNames && - this.props.rowClassNames.length === this.props.rows.length + get hasBodyClassNames () { + return this.props.bodyClassNames && + this.props.bodyClassNames.length === this.props.rows.length } get hasContextMenu () { return typeof this.props.onContextMenu === 'function' && typeof this.props.contextMenuName === 'string' } + get isMultiDimensioned () { + return this.props.rows && + Array.isArray(this.props.rows[0]) && + (Array.isArray(this.props.rows[0][0]) || this.props.rows[0].length === 0) + } + get entryMultiDimension () { + for (let i = 0; i < this.props.rows.length; i++) { + if (this.props.rows[i].length > 0) { + return this.props.rows[i] + } + } + + return undefined + } get sortingDisabled () { if (typeof this.props.sortingDisabled === 'boolean') { return this.props.sortingDisabled } return false } + hasRowClassNames (bodyIndex) { + if (this.isMultiDimensioned) { + return this.props.rowClassNames && + this.props.rowClassNames[bodyIndex].length === this.props.rows[bodyIndex].length + } + + return this.props.rowClassNames && + this.props.rowClassNames.length === this.props.rows.length + } getTableAttributes () { const tableAttributes = {} if (this.props.multiSelect && this.multipleItemsSelected) { @@ -244,18 +274,31 @@ class SortableTable extends React.Component { } return tableAttributes } - getRowAttributes (row, index) { + getRowAttributes (row, index, bodyIndex) { const rowAttributes = {} + let handlerInput // Object bound to this row. Not passed to multi-select handlers. - const handlerInput = this.props.rowObjects && + if (this.isMultiDimensioned) { + // Object bound to this row. Not passed to multi-select handlers. + handlerInput = this.props.rowObjects[bodyIndex] && + (this.props.rowObjects[bodyIndex].size > 0 || this.props.rowObjects[bodyIndex].length > 0) + ? (typeof this.props.rowObjects[bodyIndex].toJS === 'function' + ? this.props.rowObjects[bodyIndex].get(index).toJS() + : (typeof this.props.rowObjects[bodyIndex][index].toJS === 'function' + ? this.props.rowObjects[bodyIndex][index].toJS() + : this.props.rowObjects[bodyIndex][index])) + : row + } else { + handlerInput = this.props.rowObjects && (this.props.rowObjects.size > 0 || this.props.rowObjects.length > 0) - ? (typeof this.props.rowObjects.toJS === 'function' - ? this.props.rowObjects.get(index).toJS() - : (typeof this.props.rowObjects[index].toJS === 'function' - ? this.props.rowObjects[index].toJS() - : this.props.rowObjects[index])) - : row + ? (typeof this.props.rowObjects.toJS === 'function' + ? this.props.rowObjects.get(index).toJS() + : (typeof this.props.rowObjects[index].toJS === 'function' + ? this.props.rowObjects[index].toJS() + : this.props.rowObjects[index])) + : row + } // Allow parent control to optionally specify context const thisArg = this.props.thisArg || this @@ -310,6 +353,61 @@ class SortableTable extends React.Component { return rowAttributes } + generateTableRows (rows, bodyIndex) { + return rows.map((row, i) => { + const entry = row.map((item, j) => { + const value = typeof item === 'object' ? item.value : item + const html = typeof item === 'object' ? item.html : item + const cell = typeof item === 'object' ? item.cell : item + return + { + cell || (value === true ? '✕' : html) + } + + }) + const currentIndex = this.counter + this.counter ++ + const rowAttributes = row.length + ? this.getRowAttributes(row, i, bodyIndex) + : null + + const classes = [] + if (rowAttributes) classes.push(rowAttributes.className) + if (this.hasRowClassNames(bodyIndex)) { + if (this.isMultiDimensioned) { + classes.push(this.props.rowClassNames[bodyIndex][i]) + } else { + classes.push(this.props.rowClassNames[i]) + } + } + if (this.stateOwner.state.selection.includes(this.getGlobalIndex(currentIndex))) classes.push('selected') + if (this.sortingDisabled) classes.push('no-sort') + + return row.length + ? {entry} + : null + }) + } + generateTableBody () { + this.counter = 0 + + if (this.isMultiDimensioned) { + return this.props.rows.map((rows, i) => { + const content = this.generateTableRows(rows, i) + return (content.length > 0) + ? + {content} + + : null + }) + } else { + return {this.generateTableRows(this.props.rows)} + } + } render () { if (!this.props.headings || !this.props.rows) { return false @@ -325,8 +423,9 @@ class SortableTable extends React.Component { {this.props.headings.map((heading, j) => { + const firstRow = this.isMultiDimensioned ? this.entryMultiDimension[0] : this.props.rows[0] const firstEntry = this.props.rows.length > 0 - ? this.props.rows[0][j] + ? firstRow[j] : undefined let dataType = typeof firstEntry if (dataType === 'object' && firstEntry.value) { @@ -353,39 +452,7 @@ class SortableTable extends React.Component { })} - - { - this.props.rows.map((row, i) => { - const entry = row.map((item, j) => { - const value = typeof item === 'object' ? item.value : item - const html = typeof item === 'object' ? item.html : item - const cell = typeof item === 'object' ? item.cell : item - return - { - cell || (value === true ? '✕' : html) - } - - }) - const rowAttributes = row.length - ? this.getRowAttributes(row, i) - : null - - const classes = [] - if (rowAttributes) classes.push(rowAttributes.className) - if (this.hasRowClassNames) classes.push(this.props.rowClassNames[i]) - if (this.stateOwner.state.selection.includes(this.getGlobalIndex(i))) classes.push('selected') - if (this.sortingDisabled) classes.push('no-sort') - - return row.length - ? {entry} - : null - }) - } - + {this.generateTableBody()} } } diff --git a/js/components/switchControl.js b/js/components/switchControl.js index eaed1ec7229..e2e21cb1a54 100644 --- a/js/components/switchControl.js +++ b/js/components/switchControl.js @@ -29,7 +29,9 @@ class SwitchControl extends ImmutableComponent { large: this.props.large, small: this.props.small, hasTopText: this.props.topl10nId - })}> + })} + data-switch-status={this.props.checkedOn} + > { this.props.leftl10nId && this.props.topl10nId ?
 
@@ -45,9 +47,13 @@ class SwitchControl extends ImmutableComponent { }
-
+
{ diff --git a/js/constants/appConfig.js b/js/constants/appConfig.js index c80e098b2e1..bd381ad97fe 100644 --- a/js/constants/appConfig.js +++ b/js/constants/appConfig.js @@ -183,8 +183,8 @@ module.exports = { 'advanced.hide-excluded-sites': false, 'advanced.minimum-visit-time': 8, 'advanced.minimum-visits': 1, - 'advanced.minimum-percentage': true, 'advanced.auto-suggest-sites': true, + 'advanced.hide-lower-sites': true, 'shutdown.clear-history': false, 'shutdown.clear-downloads': false, 'shutdown.clear-cache': false, diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 262874397d6..02fe03c4b53 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -99,7 +99,8 @@ const appConstants = { APP_TAB_MESSAGE_BOX_UPDATED: _, APP_NAVIGATOR_HANDLER_REGISTERED: _, APP_NAVIGATOR_HANDLER_UNREGISTERED: _, - APP_ENABLE_UNDEFINED_PUBLISHERS: _ + APP_ENABLE_UNDEFINED_PUBLISHERS: _, + APP_CHANGE_LEDGER_PINNED_PERCENTAGES: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/js/constants/settings.js b/js/constants/settings.js index f42ab8d5de8..606530bf966 100644 --- a/js/constants/settings.js +++ b/js/constants/settings.js @@ -65,9 +65,9 @@ const settings = { SEND_USAGE_STATISTICS: 'advanced.send-usage-statistics', ADBLOCK_CUSTOM_RULES: 'adblock.customRules', HIDE_EXCLUDED_SITES: 'advanced.hide-excluded-sites', + HIDE_LOWER_SITES: 'advanced.hide-lower-sites', MINIMUM_VISIT_TIME: 'advanced.minimum-visit-time', MINIMUM_VISITS: 'advanced.minimum-visits', - MINIMUM_PERCENTAGE: 'advanced.minimum-percentage', AUTO_SUGGEST_SITES: 'advanced.auto-suggest-sites', // Sync settings SYNC_ENABLED: 'sync.enabled', diff --git a/js/state/syncUtil.js b/js/state/syncUtil.js index 9c0bd8d57db..dda9167b2b9 100644 --- a/js/state/syncUtil.js +++ b/js/state/syncUtil.js @@ -38,7 +38,8 @@ const siteSettingDefaults = { httpsEverywhere: true, fingerprintingProtection: false, ledgerPayments: true, - ledgerPaymentsShown: true + ledgerPaymentsShown: true, + ledgerPinPercentage: 0 } // Whitelist of valid browser-laptop site fields. In browser-laptop, site diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 076b1f13c95..88e997475ca 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -911,6 +911,15 @@ const handleAppAction = (action) => { } }) break + case appConstants.APP_CHANGE_LEDGER_PINNED_PERCENTAGES: + Object.keys(action.publishers).map((item) => { + const pattern = `https?://${item}` + let newSiteSettings = siteSettings.mergeSiteSetting(appState.get('siteSettings'), pattern, 'ledgerPinPercentage', action.publishers[item].pinPercentage) + const syncObject = siteUtil.setObjectId(newSiteSettings.get(pattern)) + newSiteSettings = newSiteSettings.set(pattern, syncObject) + appState = appState.set('siteSettings', newSiteSettings) + }) + break default: } diff --git a/less/sortableTable.less b/less/sortableTable.less index fa32313e2e9..e312b73f22a 100644 --- a/less/sortableTable.less +++ b/less/sortableTable.less @@ -45,11 +45,11 @@ table.sortableTable { float: right; } - &.sort-up:after { + &[aria-sort="ascending"]:after { content: "\f077"; } - &.sort-down:after { + &[aria-sort="descending"]:after { content: "\f078"; } } @@ -111,11 +111,11 @@ table.sortableTable { } &:not(.sort) { - th.sort-up:after { + th[aria-sort="ascending"]:after { content: ''; } - th.sort-down:after { + th[aria-sort="descending"]after { content: ''; } } diff --git a/package.json b/package.json index 2444696350a..afee1bd7f42 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "spellchecker": "brave/node-spellchecker", "string.prototype.endswith": "^0.2.0", "string.prototype.startswith": "^0.2.0", - "tablesort": "4.0.1", + "tablesort": "5.0.0", "tldjs": "1.6.2", "tracking-protection": "1.1.x", "underscore": "1.8.3", diff --git a/test/about/ledgerTableTest.js b/test/about/ledgerTableTest.js new file mode 100644 index 00000000000..4414f08d6d0 --- /dev/null +++ b/test/about/ledgerTableTest.js @@ -0,0 +1,268 @@ +/* global describe, it, beforeEach */ + +const Brave = require('../lib/brave') +const {urlInput, addFundsButton, paymentsWelcomePage, paymentsTab, walletSwitch} = require('../lib/selectors') + +const ledgerAPIWaitTimeout = 20000 +const prefsUrl = 'about:preferences' +const sites = [ + 'http://example.com/', + 'https://www.eff.org/' +] +const sites2 = [ + 'http://example.com/', + 'https://www.eff.org/', + 'https://brianbondy.com/', + 'https://clifton.io/' +] +const showAllLedger = '[data-l10n-id="showAll"]' +const firstTable = '[data-tbody-index="0"]' +const firstTableFirstRow = `${firstTable} [data-row-index="0"]` +const firstTableSecondRow = `${firstTable} [data-row-index="1"]` +const firstTableThirdRow = `${firstTable} [data-row-index="2"]` +const secondTable = '[data-tbody-index="1"]' +const secondTableFirstRow = `${secondTable} [data-row-index="0"]` +const secondTableSecondRow = `${secondTable} [data-row-index="1"]` +const secondTableThirdRow = `${secondTable} [data-row-index="2"]` +const secondTableForthRow = `${secondTable} [data-row-index="3"]` + +function * setup (client) { + yield client + .waitForUrl(Brave.newTabUrl) + .waitForBrowserWindow() + .waitForVisible(urlInput) +} + +function * before (client, siteList) { + yield client + .tabByIndex(0) + .loadUrl(prefsUrl) + .waitForVisible(paymentsTab) + .click(paymentsTab) + .waitForVisible(paymentsWelcomePage) + .waitForVisible(walletSwitch) + .click(walletSwitch) + .waitForEnabled(addFundsButton, ledgerAPIWaitTimeout) + + for (let site of siteList) { + yield client + .tabByIndex(0) + .loadUrl(site) + .windowByUrl(Brave.browserWindowUrl) + .waitForSiteEntry(site, false) + .tabByUrl(site) + } + + yield client + .tabByIndex(0) + .loadUrl(prefsUrl) + .waitForVisible(paymentsTab) + .click(paymentsTab) + .waitForVisible('[data-l10n-id="publisher"]') +} + +function findBiggestPercentage (synopsis) { + return synopsis.sortBy((publisher) => publisher.get('percentage')).get(0) +} + +describe('Ledger table', function () { + describe('2 publishers', function () { + Brave.beforeEach(this) + beforeEach(function * () { + yield setup(this.app.client) + yield before(this.app.client, sites) + }) + + it('check if all sites are on the unpinned by default', function * () { + yield this.app.client + .tabByIndex(0) + .click(showAllLedger) + .waitForElementCount(`${secondTable} tr`, sites.length) + }) + + it('pin publisher', function * () { + let topPublisher + + yield this.app.client + .tabByIndex(0) + .click(`${secondTableFirstRow} [data-test-pinned="false"]`) + .waitForVisible(`${firstTableFirstRow} [data-test-pinned="true"]`) + .windowByUrl(Brave.browserWindowUrl) + .waitUntilSynopsis(function (synopsis) { + topPublisher = findBiggestPercentage(synopsis) + return true + }) + .tabByIndex(0) + .waitUntil(function () { + return this.getText(`${firstTableFirstRow} [data-test-id="siteName"]`).then((value) => { + return value === topPublisher.get('site') + }) + }, 5000) + .waitUntil(function () { + return this.getValue(`${firstTableFirstRow} [data-test-id="pinnedInput"]`).then((value) => { + return Number(value) === topPublisher.get('pinPercentage') + }) + }, 5000) + }) + + it('pin publisher and change percentage', function * () { + yield this.app.client + .tabByIndex(0) + .click(`${secondTableFirstRow} [data-test-pinned="false"]`) + .waitForVisible(`${firstTableFirstRow} [data-test-pinned="true"]`) + .click(`${firstTableFirstRow} [data-test-id="pinnedInput"]`) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '40']) + .click(showAllLedger) + .waitForTextValue(`${secondTableSecondRow} [data-test-id="percentageValue"]`, '60') + }) + + it('pin publisher and change percentage over 100', function * () { + yield this.app.client + .tabByIndex(0) + .click(`${secondTableFirstRow} [data-test-pinned="false"]`) + .waitForVisible(`${firstTableFirstRow} [data-test-pinned="true"]`) + .click(`${firstTableFirstRow} [data-test-id="pinnedInput"]`) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '150']) + .click(showAllLedger) + .waitForInputText(`${firstTableFirstRow} [data-test-id="pinnedInput"]`, '100') + .waitForTextValue(`${secondTableSecondRow} [data-test-id="percentageValue"]`, '0') + }) + + it('pin excluded publisher', function * () { + let topPublisher + + yield this.app.client + .tabByIndex(0) + .click(showAllLedger) + .click(`${secondTableFirstRow} .switchBackground`) + .waitForVisible(`${secondTableSecondRow} [data-switch-status="false"]`) + .click(`${secondTableSecondRow} [data-test-pinned="false"]`) + .waitForVisible(`${firstTableFirstRow} [data-test-pinned="true"]`) + .windowByUrl(Brave.browserWindowUrl) + .waitUntilSynopsis(function (synopsis) { + topPublisher = findBiggestPercentage(synopsis) + return true + }) + .tabByIndex(0) + .waitUntil(function () { + return this.getText(`${firstTableFirstRow} [data-test-id="siteName"]`).then((value) => { + return value === topPublisher.get('site') + }) + }, 5000) + .waitForVisible(`${firstTableFirstRow} [data-switch-status="true"]`) + }) + }) + + describe('4 publishers', function () { + Brave.beforeEach(this) + + beforeEach(function * () { + yield setup(this.app.client) + yield before(this.app.client, sites2) + }) + + it('check if all sites are on the unpinned list', function * () { + yield this.app.client + .tabByIndex(0) + .click(showAllLedger) + .waitForElementCount(`${secondTable} tr`, sites2.length) + }) + + it('pin 3 publishers', function * () { + yield this.app.client + .tabByIndex(0) + .click(showAllLedger) + .click(`${secondTableFirstRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 1) + .click(`${secondTableSecondRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 2) + .click(`${secondTableThirdRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 3) + .waitForElementCount(`${secondTable} tr`, sites2.length - 3) + }) + + it('pin 3 publishers and check unpinned value', function * () { + let pinnedSum = 0 + yield this.app.client + .tabByIndex(0) + .click(showAllLedger) + .click(`${secondTableFirstRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 1) + .click(`${secondTableSecondRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 2) + .click(`${secondTableThirdRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 3) + .windowByUrl(Brave.browserWindowUrl) + .waitUntilSynopsis(function (synopsis) { + pinnedSum = synopsis.reduce((total, publisher) => { + if (publisher.get('pinPercentage') !== undefined) { + return total + publisher.get('pinPercentage') + } + + return total + }, 0) + return true + }) + .tabByIndex(0) + .waitUntil(function () { + return this.getText(`${secondTableForthRow} [data-test-id="percentageValue"]`).then((value) => { + return Number(value) === (100 - pinnedSum) + }) + }, 5000) + }) + + it('pin 3 publishers custom value and check unpinned value', function * () { + yield this.app.client + .tabByIndex(0) + .click(showAllLedger) + .click(`${secondTableFirstRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 1) + .click(`${secondTableSecondRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 2) + .click(`${secondTableThirdRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 3) + .click(`${firstTableFirstRow} [data-test-id="pinnedInput"]`) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '40']) + .waitForInputText(`${firstTableFirstRow} [data-test-id="pinnedInput"]`, '40') + .click(`${firstTableSecondRow} [data-test-id="pinnedInput"]`) + .pause(100) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '30']) + .waitForInputText(`${firstTableSecondRow} [data-test-id="pinnedInput"]`, '30') + .click(`${firstTableThirdRow} [data-test-id="pinnedInput"]`) + .pause(100) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '20', Brave.keys.ENTER]) + .waitForInputText(`${firstTableThirdRow} [data-test-id="pinnedInput"]`, '20') + .waitForTextValue(`${secondTableForthRow} [data-test-id="percentageValue"]`, '10') + }) + + it('pin 3 publishers over 100 value and check unpinned value', function * () { + yield this.app.client + .tabByIndex(0) + .click(showAllLedger) + .click(`${secondTableFirstRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 1) + .click(`${secondTableSecondRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 2) + .click(`${secondTableThirdRow} [data-test-pinned="false"]`) + .waitForElementCount(`${firstTable} tr`, 3) + .click(`${firstTableFirstRow} [data-test-id="pinnedInput"]`) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '40']) + .waitForInputText(`${firstTableFirstRow} [data-test-id="pinnedInput"]`, '40') + .click(`${firstTableSecondRow} [data-test-id="pinnedInput"]`) + .pause(100) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '30']) + .waitForInputText(`${firstTableSecondRow} [data-test-id="pinnedInput"]`, '30') + .click(`${firstTableThirdRow} [data-test-id="pinnedInput"]`) + .pause(100) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '20', Brave.keys.ENTER]) + .waitForInputText(`${firstTableThirdRow} [data-test-id="pinnedInput"]`, '20') + .click(`${firstTableThirdRow} [data-test-id="pinnedInput"]`) + .pause(100) + .keys([Brave.keys.DELETE, Brave.keys.DELETE, '150', Brave.keys.ENTER]) + .waitForInputText(`${firstTableFirstRow} [data-test-id="pinnedInput"]`, '19') + .waitForInputText(`${firstTableSecondRow} [data-test-id="pinnedInput"]`, '13') + .waitForInputText(`${firstTableThirdRow} [data-test-id="pinnedInput"]`, '68') + .waitForTextValue(`${secondTableForthRow} [data-test-id="percentageValue"]`, '0') + }) + }) +}) diff --git a/test/lib/brave.js b/test/lib/brave.js index 581b5f69e8b..9c9084d4942 100644 --- a/test/lib/brave.js +++ b/test/lib/brave.js @@ -1,6 +1,7 @@ /* globals devTools */ var Application = require('spectron').Application var chai = require('chai') +const Immutable = require('immutable') const {activeWebview, navigator, titleBar, urlInput} = require('./selectors') require('./coMocha') @@ -327,6 +328,7 @@ var exports = { logVerbose('waitForTabCount(' + tabCount + ')') return this.waitUntil(function () { return this.getTabCount().then((count) => { + logVerbose('waitForTabCount("' + tabCount + '") => ' + count) return count === tabCount }) }, 5000, null, 100) @@ -774,6 +776,20 @@ var exports = { this.app.client.addCommand('translations', function () { return this.ipcSendRendererSync('translations') }) + + // get synopsis from the store + this.app.client.addCommand('waitUntilSynopsis', function (cb) { + return this.waitUntil(function () { + return this.getAppState().then((val) => { + val = Immutable.fromJS(val) + let synopsis = val.getIn(['value', 'publisherInfo', 'synopsis']) + if (synopsis !== undefined) { + return cb(synopsis) + } + return false + }) + }, 5000, null, 100) + }) }, startApp: function () { diff --git a/test/unit/about/preferencesTest.js b/test/unit/about/preferencesTest.js index 5db6213fab7..06d077747b8 100644 --- a/test/unit/about/preferencesTest.js +++ b/test/unit/about/preferencesTest.js @@ -37,6 +37,8 @@ describe('Preferences component', function () { mockery.registerMock('../../../extensions/brave/img/ledger/icon_history.svg') mockery.registerMock('../../../../extensions/brave/img/ledger/verified_green_icon.svg') mockery.registerMock('../../../../extensions/brave/img/ledger/verified_white_icon.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/icon_remove.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/icon_pin.svg') mockery.registerMock('../../../../extensions/brave/img/private_internet_access.png') mockery.registerMock('../../../../extensions/brave/img/private_internet_access_2x.png') mockery.registerMock('../../../../extensions/brave/img/bitgo.png') diff --git a/test/unit/app/renderer/components/ledgerTableTest.js b/test/unit/app/renderer/components/ledgerTableTest.js new file mode 100644 index 00000000000..6fd4c3f6029 --- /dev/null +++ b/test/unit/app/renderer/components/ledgerTableTest.js @@ -0,0 +1,544 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* global describe, before, after, it */ +const mockery = require('mockery') +const {mount} = require('enzyme') +const assert = require('assert') +const Immutable = require('immutable') +const fakeElectron = require('../../../lib/fakeElectron') +const fakeSettings = require('../../../lib/fakeSettings') +let LedgerTable +require('../../../braveUnit') + +const fivePublishers = { + siteSettings: Immutable.Map([ + [ + 'https?://times.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 10 + }) + ], + [ + 'https?://cnn.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 15 + }) + ], + [ + 'https?://brianbondy.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0 + }) + ], + [ + 'https?://github.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0 + }) + ], + [ + 'https?://clifton.io', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0 + }) + ] + ]), + synopsis: Immutable.List([ + Immutable.Map({ + site: 'times.com', + verified: false, + views: 2, + pinPercentage: 10, + percentage: 10, + secondsSpent: 60, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.6513249998816057, + publisherURL: 'http://times.com', + duration: 59963, + faviconURL: '' + }), + Immutable.Map({ + site: 'cnn.com', + verified: false, + views: 1, + pinPercentage: 15, + percentage: 15, + secondsSpent: 52, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.468416104362607, + publisherURL: 'http://cnn.com', + duration: 52143, + faviconURL: '' + }), + Immutable.Map({ + site: 'brianbondy.com', + verified: true, + views: 1, + pinPercentage: 0, + percentage: 34, + secondsSpent: 38, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 2.290627169652012, + publisherURL: 'https://brianbondy.com', + duration: 37688, + faviconURL: '' + }), + Immutable.Map({ + site: 'github.com', + verified: false, + views: 1, + pinPercentage: 0, + percentage: 22, + secondsSpent: 18, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 1.4855477833585369, + publisherURL: 'https://github.com', + duration: 18462, + faviconURL: '' + }), + Immutable.Map({ + site: 'clifton.io', + verified: false, + views: 1, + pinPercentage: 0, + percentage: 19, + secondsSpent: 15, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 1.3011662888251045, + publisherURL: 'https://clifton.io', + duration: 14971, + faviconURL: '' + }) + ]) +} + +describe('LedgerTable component', function () { + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('../../../../js/settings', fakeSettings) + fakeSettings.mockReturnValue = false + window.chrome = fakeElectron + mockery.registerMock('../../../../extensions/brave/img/ledger/verified_green_icon.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/verified_white_icon.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/icon_remove.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/icon_pin.svg') + LedgerTable = require('../../../../../app/renderer/components/preferences/payment/ledgerTable') + }) + + after(function () { + mockery.disable() + }) + + it('only non pinned tabs', function () { + const siteSettings = Immutable.Map([ + [ + 'https?://times.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0 + }) + ] + ]) + + const synopsis = Immutable.List([ + Immutable.Map({ + site: 'times.com', + verified: false, + views: 2, + pinPercentage: 0, + secondsSpent: 60, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.6513249998816057, + publisherURL: 'http://times.com', + duration: 59963, + faviconURL: '' + }) + ]) + + const wrapper = mount( + + ) + assert.equal(wrapper.find('[data-tbody-index="0"] [data-test-id="siteName"]').length, 0, '0 pinned') + assert.equal(wrapper.find('[data-tbody-index="1"] [data-test-id="siteName"]').length, 1, '1 unpinned') + }) + + it('two pinned tabs, 3 unpinned (show all button, 1 hidden unpinned)', function () { + const wrapper = mount( + + ) + assert.equal(wrapper.find('[data-tbody-index="0"] [data-test-id="siteName"]').length, 2, '2 pinned') + assert.equal(wrapper.find('[data-tbody-index="1"] [data-test-id="siteName"]').length, 2, '2 unpinned, 1 hidden') + assert.equal(wrapper.find('[data-l10n-id="showAll"]').length, 1, 'show all button visible') + }) + + it('two pinned tabs, 3 unpinned (hide less button, all visible)', function () { + const wrapper = mount( + + ) + assert.equal(wrapper.find('[data-tbody-index="0"] [data-test-id="siteName"]').length, 2, '2 pinned') + assert.equal(wrapper.find('[data-tbody-index="1"] [data-test-id="siteName"]').length, 3, '3 unpinned') + assert.equal(wrapper.find('[data-l10n-id="showAll"]').length, 0, 'show all button hidden') + }) + + it('two pinned tabs, 1 unpinned tab (show all button is not necessary', function () { + const siteSettings = Immutable.Map([ + [ + 'https?://times.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 10 + }) + ], + [ + 'https?://cnn.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 15 + }) + ], + [ + 'https?://brianbondy.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0 + }) + ] + ]) + + const synopsis = Immutable.List([ + Immutable.Map({ + site: 'times.com', + verified: false, + views: 2, + pinPercentage: 10, + percentage: 10, + secondsSpent: 60, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.6513249998816057, + publisherURL: 'http://times.com', + duration: 59963, + faviconURL: '' + }), + Immutable.Map({ + site: 'cnn.com', + verified: false, + views: 1, + pinPercentage: 15, + percentage: 15, + secondsSpent: 52, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.468416104362607, + publisherURL: 'http://cnn.com', + duration: 52143, + faviconURL: '' + }), + Immutable.Map({ + site: 'brianbondy.com', + verified: true, + views: 1, + pinPercentage: 0, + percentage: 34, + secondsSpent: 38, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 2.290627169652012, + publisherURL: 'https://brianbondy.com', + duration: 37688, + faviconURL: '' + }) + ]) + + const wrapper = mount( + + ) + assert.equal(wrapper.find('[data-tbody-index="0"] [data-test-id="siteName"]').length, 2, '2 pinned') + assert.equal(wrapper.find('[data-tbody-index="1"] [data-test-id="siteName"]').length, 1, '1 unpinned') + assert.equal(wrapper.find('[data-l10n-id="showAll"]').length, 0, 'show all button hidden') + }) + + it('two pinned tabs, no un pinned (there shouldn\'t be any show all button', function () { + const siteSettings = Immutable.Map([ + [ + 'https?://times.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 10 + }) + ], + [ + 'https?://cnn.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 15 + }) + ] + ]) + + const synopsis = Immutable.List([ + Immutable.Map({ + site: 'times.com', + verified: false, + views: 2, + pinPercentage: 10, + percentage: 10, + secondsSpent: 60, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.6513249998816057, + publisherURL: 'http://times.com', + duration: 59963, + faviconURL: '' + }), + Immutable.Map({ + site: 'cnn.com', + verified: false, + views: 1, + pinPercentage: 15, + percentage: 15, + secondsSpent: 52, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.468416104362607, + publisherURL: 'http://cnn.com', + duration: 52143, + faviconURL: '' + }) + ]) + + const wrapper = mount( + + ) + assert.equal(wrapper.find('[data-tbody-index="0"] [data-test-id="siteName"]').length, 2, '2 pinned') + assert.equal(wrapper.find('[data-tbody-index="1"] [data-test-id="siteName"]').length, 0, '0 unpinned') + assert.equal(wrapper.find('[data-l10n-id="showAll"]').length, 0, 'show all button hidden') + }) + + it('pinned tabs should have exclude disabled', function () { + const siteSettings = Immutable.Map([ + [ + 'https?://times.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 42 + }) + ] + ]) + + const synopsis = Immutable.List([ + Immutable.Map({ + site: 'times.com', + verified: false, + views: 2, + pinPercentage: 42, + percentage: 42, + secondsSpent: 60, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.6513249998816057, + publisherURL: 'http://times.com', + duration: 59963, + faviconURL: '' + }) + ]) + + const wrapper = mount( + + ) + assert.equal(wrapper.find('[data-tbody-index="0"] [data-td-index="2"] .disabled').length, 1, 'exclude disabled') + }) + + it('two pinned tabs (1 banned), 3 unpinned (1 banned)', function () { + const siteSettings = Immutable.Map([ + [ + 'https?://times.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 10 + }) + ], + [ + 'https?://cnn.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 15, + ledgerPaymentsShown: false + }) + ], + [ + 'https?://brianbondy.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0 + }) + ], + [ + 'https?://github.com', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0 + }) + ], + [ + 'https?://clifton.io', + Immutable.Map({ + ledgerPayments: true, + ledgerPinPercentage: 0, + ledgerPaymentsShown: false + }) + ] + ]) + + const synopsis = Immutable.List([ + Immutable.Map({ + site: 'times.com', + verified: false, + views: 2, + pinPercentage: 10, + percentage: 10, + secondsSpent: 60, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.6513249998816057, + publisherURL: 'http://times.com', + duration: 59963, + faviconURL: '' + }), + Immutable.Map({ + site: 'cnn.com', + verified: false, + views: 1, + pinPercentage: 15, + percentage: 15, + secondsSpent: 52, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 3.468416104362607, + publisherURL: 'http://cnn.com', + duration: 52143, + faviconURL: '' + }), + Immutable.Map({ + site: 'brianbondy.com', + verified: true, + views: 1, + pinPercentage: 0, + percentage: 34, + secondsSpent: 38, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 2.290627169652012, + publisherURL: 'https://brianbondy.com', + duration: 37688, + faviconURL: '' + }), + Immutable.Map({ + site: 'github.com', + verified: false, + views: 1, + pinPercentage: 0, + percentage: 22, + secondsSpent: 18, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 1.4855477833585369, + publisherURL: 'https://github.com', + duration: 18462, + faviconURL: '' + }), + Immutable.Map({ + site: 'clifton.io', + verified: false, + views: 1, + pinPercentage: 0, + percentage: 19, + secondsSpent: 15, + minutesSpent: 0, + hoursSpent: 0, + daysSpent: 0, + score: 1.3011662888251045, + publisherURL: 'https://clifton.io', + duration: 14971, + faviconURL: '' + }) + ]) + + const wrapper = mount( + + ) + assert.equal(wrapper.find('[data-tbody-index="0"] [data-test-id="siteName"]').length, 1, '1 pinned') + assert.equal(wrapper.find('[data-tbody-index="1"] [data-test-id="siteName"]').length, 2, '2 unpinned') + }) +}) diff --git a/test/unit/app/renderer/paymentsTabTest.js b/test/unit/app/renderer/paymentsTabTest.js index c9da506ae4b..0de8483e88c 100644 --- a/test/unit/app/renderer/paymentsTabTest.js +++ b/test/unit/app/renderer/paymentsTabTest.js @@ -39,6 +39,8 @@ describe('PaymentsTab component', function () { mockery.registerMock('../../../extensions/brave/img/ledger/icon_history.svg') mockery.registerMock('../../../../extensions/brave/img/ledger/verified_green_icon.svg') mockery.registerMock('../../../../extensions/brave/img/ledger/verified_white_icon.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/icon_remove.svg') + mockery.registerMock('../../../../extensions/brave/img/ledger/icon_pin.svg') mockery.registerMock('../../../../extensions/brave/img/private_internet_access.png') mockery.registerMock('../../../../extensions/brave/img/private_internet_access_2x.png') mockery.registerMock('../../../../extensions/brave/img/bitgo.png') From 6098ebcb07b1f783a5b9cb0e0da1519bae66d6bf Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Wed, 22 Mar 2017 12:30:17 +0100 Subject: [PATCH 2/5] Fixes --- .../preferences/payment/ledgerTable.js | 11 ++-- js/components/sortableTable.js | 7 +-- test/about/historyTest.js | 2 +- test/about/ledgerPanelAdvancedPanelTest.js | 1 + test/about/ledgerPanelTest.js | 4 +- test/about/ledgerTableTest.js | 16 ++---- test/lib/selectors.js | 1 + .../renderer/components/ledgerTableTest.js | 54 +++++++++---------- 8 files changed, 40 insertions(+), 56 deletions(-) diff --git a/app/renderer/components/preferences/payment/ledgerTable.js b/app/renderer/components/preferences/payment/ledgerTable.js index 9fcc004630a..5a9aabec221 100644 --- a/app/renderer/components/preferences/payment/ledgerTable.js +++ b/app/renderer/components/preferences/payment/ledgerTable.js @@ -246,7 +246,7 @@ class LedgerTable extends ImmutableComponent { const totalUnPinnedRows = unPinnedRows.size const hideLower = getSetting(settings.HIDE_LOWER_SITES, this.props.settings) - if (hideLower && totalUnPinnedRows > 2) { + if (hideLower && totalUnPinnedRows > 10) { let sumUnPinned = 0 let threshold = 90 const limit = 0.9 // show only 90th of publishers @@ -301,7 +301,7 @@ class LedgerTable extends ImmutableComponent { ]} /> { - totalUnPinnedRows > 1 || (totalUnPinnedRows !== unPinnedRows.size && hideLower) + (totalUnPinnedRows !== unPinnedRows.size && hideLower) ?