diff --git a/app/browser/electronDownloadItem.js b/app/browser/electronDownloadItem.js new file mode 100644 index 00000000000..01b6721b875 --- /dev/null +++ b/app/browser/electronDownloadItem.js @@ -0,0 +1,27 @@ +/* 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 downloadStates = require('../../js/constants/downloadStates') + +/** + * Maps downloadId to an electron download-item + */ +const downloadMap = {} + +module.exports.updateElectronDownloadItem = (downloadId, item, state) => { + if (state === downloadStates.INTERRUPTED || state === downloadStates.CANCELLED || state === downloadStates.COMPLETED) { + delete downloadMap[downloadId] + } else { + downloadMap[downloadId] = item + } +} + +module.exports.cancelDownload = (downloadId) => + downloadMap[downloadId] && downloadMap[downloadId].cancel() + +module.exports.pauseDownload = (downloadId) => + downloadMap[downloadId] && downloadMap[downloadId].pause() + +module.exports.resumeDownload = (downloadId) => + downloadMap[downloadId] && downloadMap[downloadId].resume() diff --git a/app/browser/reducers/downloadsReducer.js b/app/browser/reducers/downloadsReducer.js new file mode 100644 index 00000000000..02547ab1200 --- /dev/null +++ b/app/browser/reducers/downloadsReducer.js @@ -0,0 +1,96 @@ +/* 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/. */ + +'use strict' + +const appConstants = require('../../../js/constants/appConstants') +const downloadStates = require('../../../js/constants/downloadStates') +const {clipboard, BrowserWindow, shell} = require('electron') +const fs = require('fs') +const path = require('path') +const {cancelDownload, pauseDownload, resumeDownload} = require('../electronDownloadItem') +const {CANCEL, PAUSE, RESUME} = require('../../common/constants/electronDownloadItemActions') + +const downloadsReducer = (state, action) => { + const download = action.downloadId ? state.getIn(['downloads', action.downloadId]) : undefined + if (!download && + ![appConstants.APP_MERGE_DOWNLOAD_DETAIL, + appConstants.APP_CLEAR_COMPLETED_DOWNLOADS].includes(action.actionType)) { + return state + } + switch (action.actionType) { + case appConstants.APP_DOWNLOAD_REVEALED: + fs.exists(download.get('savePath'), (exists) => { + if (exists) { + shell.showItemInFolder(download.get('savePath')) + } else { + shell.openItem(path.dirname(download.get('savePath'))) + } + }) + break + case appConstants.APP_DOWNLOAD_OPENED: + fs.exists(download.get('savePath'), (exists) => { + if (exists) { + shell.openItem(download.get('savePath')) + } else { + shell.beep() + } + }) + break + case appConstants.APP_DOWNLOAD_ACTION_PERFORMED: + switch (action.downloadAction) { + case CANCEL: + // It's important to update state before the cancel since it'll remove the reference + state = state.setIn(['downloads', action.downloadId, 'state'], downloadStates.CANCELLED) + cancelDownload(action.downloadId) + break + case PAUSE: + pauseDownload(action.downloadId) + state = state.setIn(['downloads', action.downloadId, 'state'], downloadStates.PAUSED) + break + case RESUME: + resumeDownload(action.downloadId) + state = state.setIn(['downloads', action.downloadId, 'state'], downloadStates.IN_PROGRESS) + break + } + break + case appConstants.APP_DOWNLOAD_COPIED_TO_CLIPBOARD: + clipboard.writeText(download.get('url')) + break + case appConstants.APP_DOWNLOAD_DELETED: + shell.moveItemToTrash(download.get('savePath')) + state = state.deleteIn(['downloads', action.downloadId]) + break + case appConstants.APP_DOWNLOAD_CLEARED: + state = state.deleteIn(['downloads', action.downloadId]) + break + case appConstants.APP_DOWNLOAD_REDOWNLOADED: + const win = BrowserWindow.getFocusedWindow() + if (win) { + win.webContents.downloadURL(download.get('url')) + state = state.deleteIn(['downloads', action.downloadId]) + } else { + shell.beep() + } + break + case appConstants.APP_MERGE_DOWNLOAD_DETAIL: + if (action.downloadDetail) { + state = state.mergeIn(['downloads', action.downloadId], action.downloadDetail) + } else { + state = state.deleteIn(['downloads', action.downloadId]) + } + break + case appConstants.APP_CLEAR_COMPLETED_DOWNLOADS: + if (state.get('downloads')) { + const downloads = state.get('downloads') + .filter((download) => + ![downloadStates.COMPLETED, downloadStates.INTERRUPTED, downloadStates.CANCELLED].includes(download.get('state'))) + state = state.set('downloads', downloads) + } + break + } + return state +} + +module.exports = downloadsReducer diff --git a/js/constants/downloadActions.js b/app/common/constants/electronDownloadItemActions.js similarity index 59% rename from js/constants/downloadActions.js rename to app/common/constants/electronDownloadItemActions.js index 3c662abbe61..48af3c48785 100644 --- a/js/constants/downloadActions.js +++ b/app/common/constants/electronDownloadItemActions.js @@ -2,13 +2,13 @@ * 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 mapValuesByKeys = require('../lib/functional').mapValuesByKeys +const mapValuesByKeys = require('../../../js/lib/functional').mapValuesByKeys const _ = null -const downloadActions = { +const electronDownloadItemActions = { PAUSE: _, RESUME: _, CANCEL: _ } -module.exports = mapValuesByKeys(downloadActions) +module.exports = mapValuesByKeys(electronDownloadItemActions) diff --git a/app/filtering.js b/app/filtering.js index cdb6df8f2d5..77054d25b1a 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -12,7 +12,6 @@ const webContents = electron.webContents const appActions = require('../js/actions/appActions') const appConfig = require('../js/constants/appConfig') const downloadStates = require('../js/constants/downloadStates') -const downloadActions = require('../js/constants/downloadActions') const urlParse = require('url').parse const getBaseDomain = require('../js/lib/baseDomain').getBaseDomain const getSetting = require('../js/settings').getSetting @@ -31,6 +30,7 @@ const uuid = require('node-uuid') const path = require('path') const getOrigin = require('../js/state/siteUtil').getOrigin const {adBlockResourceName} = require('./adBlock') +const {updateElectronDownloadItem} = require('./browser/electronDownloadItem') let appStore = null @@ -46,11 +46,6 @@ const pdfjsOrigin = `chrome-extension://${config.PDFJSExtensionId}` // Third party domains that require a valid referer to work const refererExceptions = ['use.typekit.net', 'cloud.typography.com'] -/** - * Maps downloadId to an electron download-item - */ -const downloadMap = {} - /** * Maps partition name to the session object */ @@ -467,11 +462,7 @@ module.exports.isThirdPartyHost = (baseContextHost, testHost) => { } function updateDownloadState (downloadId, item, state) { - if (state === downloadStates.INTERRUPTED || state === downloadStates.CANCELLED || state === downloadStates.COMPLETED) { - delete downloadMap[downloadId] - } else { - downloadMap[downloadId] = item - } + updateElectronDownloadItem(downloadId, item, state) if (!item) { appActions.mergeDownloadDetail(downloadId, { state: downloadStates.INTERRUPTED }) @@ -602,29 +593,6 @@ module.exports.init = (state, action, store) => { e.returnValue = true return e.returnValue }) - ipcMain.on(messages.DOWNLOAD_ACTION, (e, downloadId, action) => { - const item = downloadMap[downloadId] - switch (action) { - case downloadActions.CANCEL: - updateDownloadState(downloadId, item, downloadStates.CANCELLED) - if (item) { - item.cancel() - } - break - case downloadActions.PAUSE: - if (item) { - item.pause() - } - updateDownloadState(downloadId, item, downloadStates.PAUSED) - break - case downloadActions.RESUME: - if (item) { - item.resume() - } - updateDownloadState(downloadId, item, downloadStates.IN_PROGRESS) - break - } - }) ipcMain.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex, persist) => { if (permissionCallbacks[message]) { permissionCallbacks[message](buttonIndex, persist) diff --git a/app/index.js b/app/index.js index e0d925a2c41..c9abcdece2a 100644 --- a/app/index.js +++ b/app/index.js @@ -52,7 +52,6 @@ const Importer = require('./importer') const messages = require('../js/constants/messages') const appConfig = require('../js/constants/appConfig') const appActions = require('../js/actions/appActions') -const downloadActions = require('../js/actions/downloadActions') const SessionStore = require('./sessionStore') const AppStore = require('../js/stores/appStore') const PackageLoader = require('./package-loader') @@ -490,10 +489,6 @@ app.on('ready', () => { electron.clipboard.writeText(text) }) - ipcMain.on(messages.OPEN_DOWNLOAD_PATH, (e, download) => { - downloadActions.openDownloadPath(Immutable.fromJS(download)) - }) - ipcMain.on(messages.CERT_ERROR_ACCEPTED, (event, url) => { try { let host = urlParse(url).host diff --git a/docs/appActions.md b/docs/appActions.md index 8f52d4397e2..744628cb03c 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -541,6 +541,85 @@ Dispatch a message to copy data URL to clipboard +### shuttingDown() + +Dispatches a message when the app is shutting down. + + + +### downloadRevealed(downloadId) + +Dispatches a message when a download is being revealed. +Typically this will open the download directory in finder / explorer and select the icon. + +**Parameters** + +**downloadId**: `string`, ID of the download being revealed + + + +### downloadOpened(downloadId) + +Dispatches a message when a download is being opened. + +**Parameters** + +**downloadId**: `string`, ID of the download being opened + + + +### downloadActionPerformed(downloadId, downloadAction) + +Dispatches a message when an electron download action is being performed (pause, resume, cancel) + +**Parameters** + +**downloadId**: `string`, ID of the download item the action is being performed to + +**downloadAction**: `string`, the action to perform from constants/electronDownloadItemActions.js + + + +### downloadCopiedToClipboard(downloadId) + +Dispatches a message when a download URL is being copied to the clipboard + +**Parameters** + +**downloadId**: `string`, ID of the download item being copied to the clipboard + + + +### downloadDeleted(downloadId) + +Dispatches a message when a download is being deleted + +**Parameters** + +**downloadId**: `string`, ID of the download item being deleted + + + +### downloadCleared(downloadId) + +Dispatches a message when a download is being cleared + +**Parameters** + +**downloadId**: `string`, ID of the download item being cleared + + + +### downloadRedownloaded(downloadId) + +Dispatches a message when a download is being redownloaded + +**Parameters** + +**downloadId**: `string`, ID of the download item being redownloaded + + + * * * diff --git a/js/about/aboutActions.js b/js/about/aboutActions.js index 4a5e6bab9cf..9e871f0e1d9 100644 --- a/js/about/aboutActions.js +++ b/js/about/aboutActions.js @@ -154,8 +154,11 @@ const aboutActions = { }) }, - openDownloadPath: function (download) { - ipc.send(messages.OPEN_DOWNLOAD_PATH, download.toJS()) + downloadRevealed: function (downloadId) { + aboutActions.dispatchAction({ + actionType: appConstants.APP_DOWNLOAD_REVEALED, + downloadId + }) }, decryptPassword: function (encryptedPassword, authTag, iv, id) { diff --git a/js/about/downloads.js b/js/about/downloads.js index 5d4b1a3577f..ce8fc001681 100644 --- a/js/about/downloads.js +++ b/js/about/downloads.js @@ -29,7 +29,7 @@ class DownloadItem extends ImmutableComponent { className='listItem' onContextMenu={aboutActions.contextMenu.bind(this, contextMenuDownload, 'download')} data-context-menu-disable - onDoubleClick={aboutActions.openDownloadPath.bind(this, this.props.download)}> + onDoubleClick={aboutActions.downloadRevealed.bind(this, this.props.downloadId)}> {
{this.props.download.get('filename')}
diff --git a/js/actions/appActions.js b/js/actions/appActions.js index fc7ddee7b45..b1aa8df48c7 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -4,7 +4,7 @@ 'use strict' const AppDispatcher = require('../dispatcher/appDispatcher') -const AppConstants = require('../constants/appConstants') +const appConstants = require('../constants/appConstants') const appActions = { /** @@ -15,7 +15,7 @@ const appActions = { */ setState: function (appState) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_STATE, + actionType: appConstants.APP_SET_STATE, appState }) }, @@ -29,7 +29,7 @@ const appActions = { */ newWindow: function (frameOpts, browserOpts, restoredState, cb) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_NEW_WINDOW, + actionType: appConstants.APP_NEW_WINDOW, frameOpts, browserOpts, restoredState, @@ -39,28 +39,28 @@ const appActions = { closeWindow: function (windowId) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CLOSE_WINDOW, + actionType: appConstants.APP_CLOSE_WINDOW, windowId }) }, windowClosed: function (windowValue) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_WINDOW_CLOSED, + actionType: appConstants.APP_WINDOW_CLOSED, windowValue }) }, windowCreated: function (windowValue) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_WINDOW_CREATED, + actionType: appConstants.APP_WINDOW_CREATED, windowValue }) }, windowUpdated: function (windowValue) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_WINDOW_UPDATED, + actionType: appConstants.APP_WINDOW_UPDATED, windowValue }) }, @@ -71,7 +71,7 @@ const appActions = { */ newTab: function (frameProps) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_NEW_TAB, + actionType: appConstants.APP_NEW_TAB, frameProps }) }, @@ -82,7 +82,7 @@ const appActions = { */ tabCreated: function (tabValue) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_TAB_CREATED, + actionType: appConstants.APP_TAB_CREATED, tabValue }) }, @@ -93,7 +93,7 @@ const appActions = { */ tabUpdated: function (tabValue) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_TAB_UPDATED, + actionType: appConstants.APP_TAB_UPDATED, tabValue }) }, @@ -104,7 +104,7 @@ const appActions = { */ tabClosed: function (tabValue) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_TAB_CLOSED, + actionType: appConstants.APP_TAB_CLOSED, tabValue }) }, @@ -119,7 +119,7 @@ const appActions = { */ addSite: function (siteDetail, tag, originalSiteDetail, destinationDetail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_ADD_SITE, + actionType: appConstants.APP_ADD_SITE, siteDetail, tag, originalSiteDetail, @@ -132,7 +132,7 @@ const appActions = { */ clearHistory: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CLEAR_HISTORY + actionType: appConstants.APP_CLEAR_HISTORY }) }, @@ -143,7 +143,7 @@ const appActions = { */ removeSite: function (siteDetail, tag) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_REMOVE_SITE, + actionType: appConstants.APP_REMOVE_SITE, siteDetail, tag }) @@ -160,7 +160,7 @@ const appActions = { */ moveSite: function (sourceDetail, destinationDetail, prepend, destinationIsParent) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_MOVE_SITE, + actionType: appConstants.APP_MOVE_SITE, sourceDetail, destinationDetail, prepend, @@ -176,7 +176,7 @@ const appActions = { */ mergeDownloadDetail: function (downloadId, downloadDetail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_MERGE_DOWNLOAD_DETAIL, + actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL, downloadId, downloadDetail }) @@ -187,7 +187,7 @@ const appActions = { */ clearCompletedDownloads: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CLEAR_COMPLETED_DOWNLOADS + actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS }) }, @@ -196,7 +196,7 @@ const appActions = { */ ledgerRecoverySucceeded: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_LEDGER_RECOVERY_SUCCEEDED + actionType: appConstants.APP_LEDGER_RECOVERY_SUCCEEDED }) }, @@ -205,7 +205,7 @@ const appActions = { */ ledgerRecoveryFailed: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_LEDGER_RECOVERY_FAILED + actionType: appConstants.APP_LEDGER_RECOVERY_FAILED }) }, @@ -216,7 +216,7 @@ const appActions = { */ defaultWindowParamsChanged: function (size, position) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_DEFAULT_WINDOW_PARAMS_CHANGED, + actionType: appConstants.APP_DEFAULT_WINDOW_PARAMS_CHANGED, size, position }) @@ -231,7 +231,7 @@ const appActions = { */ setResourceETag: function (resourceName, etag) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_DATA_FILE_ETAG, + actionType: appConstants.APP_SET_DATA_FILE_ETAG, resourceName, etag }) @@ -244,7 +244,7 @@ const appActions = { */ setResourceLastCheck: function (resourceName, lastCheckVersion, lastCheckDate) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_DATA_FILE_LAST_CHECK, + actionType: appConstants.APP_SET_DATA_FILE_LAST_CHECK, resourceName, lastCheckVersion, lastCheckDate @@ -258,7 +258,7 @@ const appActions = { */ setResourceEnabled: function (resourceName, enabled) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_RESOURCE_ENABLED, + actionType: appConstants.APP_SET_RESOURCE_ENABLED, resourceName, enabled }) @@ -270,7 +270,7 @@ const appActions = { */ resourceReady: function (resourceName) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_RESOURCE_READY, + actionType: appConstants.APP_RESOURCE_READY, resourceName }) }, @@ -282,7 +282,7 @@ const appActions = { */ addResourceCount: function (resourceName, count) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_ADD_RESOURCE_COUNT, + actionType: appConstants.APP_ADD_RESOURCE_COUNT, resourceName, count }) @@ -294,7 +294,7 @@ const appActions = { */ setUpdateLastCheck: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_UPDATE_LAST_CHECK + actionType: appConstants.APP_UPDATE_LAST_CHECK }) }, @@ -306,7 +306,7 @@ const appActions = { */ setUpdateStatus: function (status, verbose, metadata) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_UPDATE_STATUS, + actionType: appConstants.APP_SET_UPDATE_STATUS, status, verbose, metadata @@ -319,7 +319,7 @@ const appActions = { */ savePassword: function (passwordDetail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_ADD_PASSWORD, + actionType: appConstants.APP_ADD_PASSWORD, passwordDetail }) }, @@ -330,7 +330,7 @@ const appActions = { */ deletePassword: function (passwordDetail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_REMOVE_PASSWORD, + actionType: appConstants.APP_REMOVE_PASSWORD, passwordDetail }) }, @@ -340,7 +340,7 @@ const appActions = { */ clearPasswords: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CLEAR_PASSWORDS + actionType: appConstants.APP_CLEAR_PASSWORDS }) }, @@ -351,7 +351,7 @@ const appActions = { */ changeSetting: function (key, value) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CHANGE_SETTING, + actionType: appConstants.APP_CHANGE_SETTING, key, value }) @@ -367,7 +367,7 @@ const appActions = { */ changeSiteSetting: function (hostPattern, key, value, temp) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CHANGE_SITE_SETTING, + actionType: appConstants.APP_CHANGE_SITE_SETTING, hostPattern, key, value, @@ -384,7 +384,7 @@ const appActions = { */ removeSiteSetting: function (hostPattern, key, temp) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_REMOVE_SITE_SETTING, + actionType: appConstants.APP_REMOVE_SITE_SETTING, hostPattern, key, temporary: temp || false @@ -397,7 +397,7 @@ const appActions = { */ updateLedgerInfo: function (ledgerInfo) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_UPDATE_LEDGER_INFO, + actionType: appConstants.APP_UPDATE_LEDGER_INFO, ledgerInfo }) }, @@ -408,7 +408,7 @@ const appActions = { */ updatePublisherInfo: function (publisherInfo) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_UPDATE_PUBLISHER_INFO, + actionType: appConstants.APP_UPDATE_PUBLISHER_INFO, publisherInfo }) }, @@ -419,7 +419,7 @@ const appActions = { */ showMessageBox: function (detail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SHOW_MESSAGE_BOX, + actionType: appConstants.APP_SHOW_MESSAGE_BOX, detail }) }, @@ -430,7 +430,7 @@ const appActions = { */ hideMessageBox: function (message) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_HIDE_MESSAGE_BOX, + actionType: appConstants.APP_HIDE_MESSAGE_BOX, message }) }, @@ -441,7 +441,7 @@ const appActions = { */ clearMessageBoxes: function (origin) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CLEAR_MESSAGE_BOXES, + actionType: appConstants.APP_CLEAR_MESSAGE_BOXES, origin }) }, @@ -453,7 +453,7 @@ const appActions = { */ addWord: function (word, learn) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_ADD_WORD, + actionType: appConstants.APP_ADD_WORD, word, learn }) @@ -465,7 +465,7 @@ const appActions = { */ setDictionary: function (locale) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_DICTIONARY, + actionType: appConstants.APP_SET_DICTIONARY, locale }) }, @@ -477,7 +477,7 @@ const appActions = { */ setLoginRequiredDetail: function (tabId, detail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_LOGIN_REQUIRED_DETAIL, + actionType: appConstants.APP_SET_LOGIN_REQUIRED_DETAIL, tabId, detail }) @@ -485,7 +485,7 @@ const appActions = { setLoginResponseDetail: function (tabId, detail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_LOGIN_RESPONSE_DETAIL, + actionType: appConstants.APP_SET_LOGIN_RESPONSE_DETAIL, tabId, detail }) @@ -497,7 +497,7 @@ const appActions = { */ clearAppData: function (clearDataDetail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_CLEAR_DATA, + actionType: appConstants.APP_CLEAR_DATA, clearDataDetail }) }, @@ -508,7 +508,7 @@ const appActions = { */ importBrowserData: function (selected) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_IMPORT_BROWSER_DATA, + actionType: appConstants.APP_IMPORT_BROWSER_DATA, selected }) }, @@ -520,7 +520,7 @@ const appActions = { */ addAutofillAddress: function (detail, originalDetail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_ADD_AUTOFILL_ADDRESS, + actionType: appConstants.APP_ADD_AUTOFILL_ADDRESS, detail, originalDetail }) @@ -532,7 +532,7 @@ const appActions = { */ removeAutofillAddress: function (detail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_REMOVE_AUTOFILL_ADDRESS, + actionType: appConstants.APP_REMOVE_AUTOFILL_ADDRESS, detail }) }, @@ -544,7 +544,7 @@ const appActions = { */ addAutofillCreditCard: function (detail, originalDetail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_ADD_AUTOFILL_CREDIT_CARD, + actionType: appConstants.APP_ADD_AUTOFILL_CREDIT_CARD, detail, originalDetail }) @@ -556,7 +556,7 @@ const appActions = { */ removeAutofillCreditCard: function (detail) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_REMOVE_AUTOFILL_CREDIT_CARD, + actionType: appConstants.APP_REMOVE_AUTOFILL_CREDIT_CARD, detail }) }, @@ -568,7 +568,7 @@ const appActions = { */ autofillDataChanged: function (addressGuids, creditCardGuids) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_AUTOFILL_DATA_CHANGED, + actionType: appConstants.APP_AUTOFILL_DATA_CHANGED, addressGuids, creditCardGuids }) @@ -582,7 +582,7 @@ const appActions = { */ windowBlurred: function (windowId) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_WINDOW_BLURRED, + actionType: appConstants.APP_WINDOW_BLURRED, windowId: windowId }) }, @@ -593,7 +593,7 @@ const appActions = { */ setMenubarTemplate: function (menubarTemplate) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SET_MENUBAR_TEMPLATE, + actionType: appConstants.APP_SET_MENUBAR_TEMPLATE, menubarTemplate }) }, @@ -604,7 +604,7 @@ const appActions = { */ networkConnected: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_NETWORK_CONNECTED + actionType: appConstants.APP_NETWORK_CONNECTED }) }, @@ -613,7 +613,7 @@ const appActions = { */ networkDisconnected: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_NETWORK_DISCONNECTED + actionType: appConstants.APP_NETWORK_DISCONNECTED }) }, @@ -624,7 +624,7 @@ const appActions = { */ defaultBrowserUpdated: function (useBrave) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_DEFAULT_BROWSER_UPDATED, + actionType: appConstants.APP_DEFAULT_BROWSER_UPDATED, useBrave }) }, @@ -634,7 +634,7 @@ const appActions = { */ defaultBrowserCheckComplete: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_DEFAULT_BROWSER_CHECK_COMPLETE + actionType: appConstants.APP_DEFAULT_BROWSER_CHECK_COMPLETE }) }, @@ -643,7 +643,7 @@ const appActions = { */ populateHistory: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_POPULATE_HISTORY + actionType: appConstants.APP_POPULATE_HISTORY }) }, @@ -652,16 +652,99 @@ const appActions = { **/ dataURLCopied: function (dataURL, html, text) { AppDispatcher.dispatch({ - actionType: AppConstants.APP_DATA_URL_COPIED, + actionType: appConstants.APP_DATA_URL_COPIED, dataURL, html, text }) }, + /** + * Dispatches a message when the app is shutting down. + */ shuttingDown: function () { AppDispatcher.dispatch({ - actionType: AppConstants.APP_SHUTTING_DOWN + actionType: appConstants.APP_SHUTTING_DOWN + }) + }, + + /** + * Dispatches a message when a download is being revealed. + * Typically this will open the download directory in finder / explorer and select the icon. + * @param {string} downloadId - ID of the download being revealed + */ + downloadRevealed: function (downloadId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_DOWNLOAD_REVEALED, + downloadId + }) + }, + + /** + * Dispatches a message when a download is being opened. + * @param {string} downloadId - ID of the download being opened + */ + downloadOpened: function (downloadId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_DOWNLOAD_OPENED, + downloadId + }) + }, + + /** + * Dispatches a message when an electron download action is being performed (pause, resume, cancel) + * @param {string} downloadId - ID of the download item the action is being performed to + * @param {string} downloadAction - the action to perform from constants/electronDownloadItemActions.js + */ + downloadActionPerformed: function (downloadId, downloadAction) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_DOWNLOAD_ACTION_PERFORMED, + downloadId, + downloadAction + }) + }, + + /** + * Dispatches a message when a download URL is being copied to the clipboard + * @param {string} downloadId - ID of the download item being copied to the clipboard + */ + downloadCopiedToClipboard: function (downloadId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_DOWNLOAD_COPIED_TO_CLIPBOARD, + downloadId + }) + }, + + /** + * Dispatches a message when a download is being deleted + * @param {string} downloadId - ID of the download item being deleted + */ + downloadDeleted: function (downloadId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_DOWNLOAD_DELETED, + downloadId + }) + }, + + /** + * Dispatches a message when a download is being cleared + * @param {string} downloadId - ID of the download item being cleared + */ + downloadCleared: function (downloadId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_DOWNLOAD_CLEARED, + downloadId + }) + }, + + /** + * Dispatches a message when a download is being redownloaded + * @param {string} downloadId - ID of the download item being redownloaded + */ + downloadRedownloaded: function (downloadId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_DOWNLOAD_REDOWNLOADED, + downloadId }) } } diff --git a/js/actions/downloadActions.js b/js/actions/downloadActions.js deleted file mode 100644 index d22a8bb4bef..00000000000 --- a/js/actions/downloadActions.js +++ /dev/null @@ -1,85 +0,0 @@ -/* 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/. */ - -'use strict' - -const electron = require('electron') - -let shell, ipc, clipboard, getCurrentWebContents -if (process.type === 'browser') { - shell = electron.shell - ipc = electron.ipcRenderer - clipboard = electron.clipboard - getCurrentWebContents = electron.getCurrentWebContents -} else { - shell = electron.remote.shell - ipc = electron.ipcRenderer - clipboard = electron.remote.clipboard - getCurrentWebContents = electron.remote.getCurrentWebContents -} - -const appDownloadActions = require('../constants/downloadActions') -const appActions = require('../actions/appActions') -const messages = require('../constants/messages') -// const fs = require('fs') -// const path = require('path') - -/** - * Creates an action function for the specified app download action - * @param {string} appDownloadAction - The ID of the app action to send - */ -const appActionForDownload = (appDownloadAction) => (downloadId) => - ipc.send(messages.DOWNLOAD_ACTION, downloadId, appDownloadAction) - -const downloadActions = { - cancelDownload: appActionForDownload(appDownloadActions.CANCEL), - pauseDownload: appActionForDownload(appDownloadActions.PAUSE), - resumeDownload: appActionForDownload(appDownloadActions.RESUME), - copyLinkToClipboard: function (download) { - clipboard.writeText(download.get('url')) - // disabling notificiations from the main window until we have a - // better way to do it - // void new window.Notification(locale.translation('urlCopied')) - }, - openDownloadPath: function (download) { - // fs.exists(download.get('savePath'), (exists) => { - // if (exists) { - // shell.openItem(download.get('savePath')) - // } else { - // shell.beep() - // } - // }) - }, - locateShellPath: function (download) { - // fs.exists(download.get('savePath'), (exists) => { - // if (exists) { - // shell.showItemInFolder(download.get('savePath')) - // } else { - // shell.openItem(path.dirname(download.get('savePath'))) - // } - // }) - }, - hideDownloadsToolbar: function () { - if (process.type === 'renderer') { - const windowActions = require('../actions/windowActions') - windowActions.setDownloadsToolbarVisible(false) - } - }, - deleteDownload: function (downloads, download, downloadId) { - shell.moveItemToTrash(download.get('savePath')) - downloadActions.clearDownload(downloads, downloadId) - }, - clearDownload: function (downloads, downloadId) { - if (downloads && downloads.size === 1) { - downloadActions.hideDownloadsToolbar() - } - appActions.mergeDownloadDetail(downloadId) - }, - redownloadURL: function (download, downloadId) { - getCurrentWebContents().downloadURL(download.get('url')) - downloadActions.clearDownload(undefined, downloadId) - } -} - -module.exports = downloadActions diff --git a/js/components/downloadsBar.js b/js/components/downloadsBar.js index 3123163eb57..56899af1100 100644 --- a/js/components/downloadsBar.js +++ b/js/components/downloadsBar.js @@ -7,11 +7,46 @@ const ImmutableComponent = require('./immutableComponent') const Button = require('./button') const contextMenus = require('../contextMenus') const downloadStates = require('../constants/downloadStates') -const downloadActions = require('../actions/downloadActions') +const {PAUSE, RESUME, CANCEL} = require('../../app/common/constants/electronDownloadItemActions') +const appActions = require('../actions/appActions') +const windowActions = require('../actions/windowActions') const downloadUtil = require('../state/downloadUtil') const cx = require('../lib/classSet') class DownloadItem extends ImmutableComponent { + constructor () { + super() + this.onRevealDownload = this.onRevealDownload.bind(this) + this.onOpenDownload = this.onOpenDownload.bind(this) + this.onPauseDownload = this.onDownloadActionPerformed.bind(this, PAUSE) + this.onResumeDownload = this.onDownloadActionPerformed.bind(this, RESUME) + this.onCancelDownload = this.onDownloadActionPerformed.bind(this, CANCEL) + this.onClearDownload = this.onClearDownload.bind(this) + this.onDeleteDownload = this.onDeleteDownload.bind(this) + this.onRedownload = this.onRedownload.bind(this) + this.onCopyLinkToClipboard = this.onCopyLinkToClipboard.bind(this) + } + onRevealDownload () { + appActions.downloadRevealed(this.props.downloadId) + } + onOpenDownload () { + appActions.downloadOpened(this.props.downloadId) + } + onClearDownload () { + appActions.downloadCleared(this.props.downloadId) + } + onDeleteDownload () { + appActions.downloadDeleted(this.props.downloadId) + } + onDownloadActionPerformed (downloadAction) { + appActions.downloadActionPerformed(this.props.downloadId, downloadAction) + } + onCopyLinkToClipboard () { + appActions.downloadCopiedToClipboard(this.props.downloadId) + } + onRedownload () { + appActions.downloadRedownloaded(this.props.downloadId) + } get isInterrupted () { return this.props.download.get('state') === downloadStates.INTERRUPTED } @@ -39,7 +74,7 @@ class DownloadItem extends ImmutableComponent { } return { downloadUtil.shouldAllowPause(this.props.download) - ?
@@ -112,6 +147,13 @@ class DownloadItem extends ImmutableComponent { } class DownloadsBar extends ImmutableComponent { + constructor () { + super() + this.onHideDownloadsToolbar = this.onHideDownloadsToolbar.bind(this) + } + onHideDownloadsToolbar () { + windowActions.setDownloadsToolbarVisible(false) + } render () { const downloadItemWidth = Number.parseInt(window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--download-item-width'), 10) const downloadItemMargin = Number.parseInt(window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--download-item-margin'), 10) @@ -135,7 +177,7 @@ class DownloadsBar extends ImmutableComponent {
} diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 65979266f61..60d3d2e10a0 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -73,7 +73,14 @@ const AppConstants = { APP_POPULATE_HISTORY: _, APP_RENDER_URL_TO_PDF: _, APP_DATA_URL_COPIED: _, - APP_SHUTTING_DOWN: _ + APP_SHUTTING_DOWN: _, + APP_DOWNLOAD_REVEALED: _, + APP_DOWNLOAD_OPENED: _, + APP_DOWNLOAD_ACTION_PERFORMED: _, + APP_DOWNLOAD_COPIED_TO_CLIPBOARD: _, + APP_DOWNLOAD_DELETED: _, + APP_DOWNLOAD_CLEARED: _, + APP_DOWNLOAD_REDOWNLOADED: _ } module.exports = mapValuesByKeys(AppConstants) diff --git a/js/constants/messages.js b/js/constants/messages.js index 7430b4fcca0..e5265dc1a67 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -112,7 +112,6 @@ const messages = { NEWTAB_DATA_UPDATED: _, VERSION_INFORMATION_UPDATED: _, // About pages from contentScript - OPEN_DOWNLOAD_PATH: _, RELOAD_URL: _, DISPATCH_ACTION: _, CHECK_FLASH_INSTALLED: _, diff --git a/js/contextMenus.js b/js/contextMenus.js index e44bc026187..318a0ea43a1 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -12,9 +12,9 @@ const windowStore = require('./stores/windowStore') const windowActions = require('./actions/windowActions') const webviewActions = require('./actions/webviewActions') const bookmarkActions = require('./actions/bookmarkActions') -const downloadActions = require('./actions/downloadActions') const appActions = require('./actions/appActions') const siteTags = require('./constants/siteTags') +const electronDownloadItemActions = require('../app/common/constants/electronDownloadItemActions') const dragTypes = require('./constants/dragTypes') const siteUtil = require('./state/siteUtil') const downloadUtil = require('./state/downloadUtil') @@ -145,60 +145,59 @@ function downloadsToolbarTemplateInit (downloadId, downloadItem) { const template = [] if (downloadItem) { - const downloads = appStoreRenderer.state.get('downloads') if (downloadUtil.shouldAllowPause(downloadItem)) { template.push({ label: locale.translation('downloadItemPause'), - click: downloadActions.pauseDownload.bind(null, downloadId) + click: appActions.downloadActionPerformed.bind(null, downloadId, electronDownloadItemActions.PAUSE) }) } if (downloadUtil.shouldAllowResume(downloadItem)) { template.push({ label: locale.translation('downloadItemResume'), - click: downloadActions.resumeDownload.bind(null, downloadId) + click: appActions.downloadActionPerformed.bind(null, downloadId, electronDownloadItemActions.RESUME) }) } if (downloadUtil.shouldAllowCancel(downloadItem)) { template.push({ label: locale.translation('downloadItemCancel'), - click: downloadActions.cancelDownload.bind(null, downloadId) + click: appActions.downloadActionPerformed.bind(null, downloadId, electronDownloadItemActions.CANCEL) }) } if (downloadUtil.shouldAllowRedownload(downloadItem)) { template.push({ label: locale.translation('downloadItemRedownload'), - click: downloadActions.redownloadURL.bind(null, downloadItem, downloadId) + click: appActions.downloadRedownloaded.bind(null, downloadId) }) } if (downloadUtil.shouldAllowCopyLink(downloadItem)) { template.push({ label: locale.translation('downloadItemCopyLink'), - click: downloadActions.copyLinkToClipboard.bind(null, downloadItem) + click: appActions.downloadCopiedToClipboard.bind(null, downloadId) }) } if (downloadUtil.shouldAllowOpenDownloadLocation(downloadItem)) { template.push({ label: locale.translation('downloadItemPath'), - click: downloadActions.locateShellPath.bind(null, downloadItem) + click: appActions.downloadRevealed.bind(null, downloadId) }) } if (downloadUtil.shouldAllowDelete(downloadItem)) { template.push({ label: locale.translation('downloadItemDelete'), - click: downloadActions.deleteDownload.bind(null, downloads, downloadItem, downloadId) + click: appActions.downloadDeleted.bind(null, downloadId) }) } if (downloadUtil.shouldAllowRemoveFromList(downloadItem)) { template.push({ label: locale.translation('downloadItemClear'), - click: downloadActions.clearDownload.bind(null, downloads, downloadId) + click: appActions.downloadCleared.bind(null, downloadId) }) } } diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 43ecd182fef..eda76ff7d50 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -17,7 +17,6 @@ const app = electron.app const ipcMain = electron.ipcMain const messages = require('../constants/messages') const UpdateStatus = require('../constants/updateStatus') -const downloadStates = require('../constants/downloadStates') const BrowserWindow = electron.BrowserWindow const LocalShortcuts = require('../../app/localShortcuts') const appActions = require('../actions/appActions') @@ -36,6 +35,7 @@ const Filtering = require('../../app/filtering') const basicAuth = require('../../app/browser/basicAuth') const tabs = require('../../app/browser/tabs') const windows = require('../../app/browser/windows') +const downloadsReducer = require('../../app/browser/reducers/downloadsReducer') // state helpers const basicAuthState = require('../../app/common/state/basicAuthState') @@ -361,6 +361,8 @@ const handleAppAction = (action) => { const ledger = require('../../app/ledger') + appState = downloadsReducer(appState, action) + switch (action.actionType) { case AppConstants.APP_SET_STATE: appState = action.appState @@ -498,21 +500,6 @@ const handleAppAction = (action) => { case AppConstants.APP_MOVE_SITE: appState = appState.set('sites', siteUtil.moveSite(appState.get('sites'), action.sourceDetail, action.destinationDetail, action.prepend, action.destinationIsParent, false)) break - case AppConstants.APP_MERGE_DOWNLOAD_DETAIL: - if (action.downloadDetail) { - appState = appState.mergeIn(['downloads', action.downloadId], action.downloadDetail) - } else { - appState = appState.deleteIn(['downloads', action.downloadId]) - } - break - case AppConstants.APP_CLEAR_COMPLETED_DOWNLOADS: - if (appState.get('downloads')) { - const downloads = appState.get('downloads') - .filter((download) => - ![downloadStates.COMPLETED, downloadStates.INTERRUPTED, downloadStates.CANCELLED].includes(download.get('state'))) - appState = appState.set('downloads', downloads) - } - break case AppConstants.APP_CLEAR_HISTORY: appState = appState.set('sites', siteUtil.clearHistory(appState.get('sites'))) appState = aboutNewTabState.setSites(appState, action) diff --git a/package.json b/package.json index dc0058c58fc..3490d3bb0d5 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "nsp": "^2.2.0", "pre-commit": "brave/pre-commit", "react-addons-perf": "^15.2.1", + "sinon": "^1.17.6", "spectron": "brave/spectron#chromium54", "sqlite3": "^3.1.1", "standard": "8.1.0", diff --git a/test/unit/app/browser/reducers/downloadsReducerTest.js b/test/unit/app/browser/reducers/downloadsReducerTest.js new file mode 100644 index 00000000000..1248b26cc63 --- /dev/null +++ b/test/unit/app/browser/reducers/downloadsReducerTest.js @@ -0,0 +1,237 @@ +/* global describe, it, before, after, beforeEach */ +const mockery = require('mockery') +const sinon = require('sinon') +const Immutable = require('immutable') +const process = require('process') +const assert = require('assert') +const uuid = require('uuid') +const path = require('path') +const fakeElectron = require('../../../lib/fakeElectron') + +const appConstants = require('../../../../../js/constants/appConstants') +const {PENDING, IN_PROGRESS, RESUMING, PAUSED, COMPLETED, CANCELLED, INTERRUPTED} = require('../../../../../js/constants/downloadStates') +const {CANCEL, PAUSE, RESUME} = require('../../../../../app/common/constants/electronDownloadItemActions') +require('../../../braveUnit') + +const downloadId = (state, i = 0) => Object.keys(state.get('downloads').toJS())[i] +const downloadUrl = 'http://www.bradrichter.com/mostHatedPrimes.txt' +const savePath = path.join(require('os').tmpdir(), 'mostHatedPrimes.txt') +const oneDownloadWithState = (state) => Immutable.fromJS({ + downloads: { + [uuid.v4()]: { + startTime: new Date().getTime(), + filename: 'mostHatedPrimes.txt', + savePath, + url: downloadUrl, + totalBytes: 104729, + receivedBytes: 96931, + state + } + } +}) + +describe('downloadsReducer', function () { + let downloadsReducer + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + downloadsReducer = require('../../../../../app/browser/reducers/downloadsReducer') + }) + + beforeEach(function () { + }) + + after(function () { + mockery.disable() + }) + + it('returns original state for unhandled actions', function () { + const oldState = oneDownloadWithState(IN_PROGRESS) + const newState = downloadsReducer(oldState, {actionType: uuid.v4()}) + assert.deepEqual(newState.toJS(), oldState.toJS()) + }) + + describe('APP_DOWNLOAD_REVEALED', function () { + it('Reveals file for paths that does not exist exist', function (cb) { + sinon.stub(fakeElectron.shell, 'showItemInFolder', (path) => { + fakeElectron.shell.showItemInFolder.restore() + assert.equal(path, process.cwd()) + cb() + }) + let oldState = oneDownloadWithState(IN_PROGRESS) + oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], process.cwd()) + downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REVEALED, downloadId: downloadId(oldState)}) + }) + it('Reveals file for paths that does not exist exist', function (cb) { + const saveDir = path.dirname(savePath) + sinon.stub(fakeElectron.shell, 'openItem', (path) => { + fakeElectron.shell.openItem.restore() + assert.equal(path, saveDir) + cb() + }) + const oldState = oneDownloadWithState(IN_PROGRESS) + downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REVEALED, downloadId: downloadId(oldState)}) + }) + }) + + describe('APP_DOWNLOAD_OPENED', function () { + it('Opens a downloaded file', function (cb) { + sinon.stub(fakeElectron.shell, 'openItem', (path) => { + fakeElectron.shell.openItem.restore() + assert.equal(path, process.cwd()) + cb() + }) + let oldState = oneDownloadWithState(IN_PROGRESS) + oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], process.cwd()) + downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_OPENED, downloadId: downloadId(oldState)}) + }) + it('Beeps when a downloaded file is trying to be opened', function (cb) { + sinon.stub(fakeElectron.shell, 'beep', () => { + fakeElectron.shell.beep.restore() + cb() + }) + const oldState = oneDownloadWithState(IN_PROGRESS) + downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_OPENED, downloadId: downloadId(oldState)}) + }) + }) + + describe('APP_DOWNLOAD_ACTION_PERFORMED', function () { + it('CANCEL causes CANCELLED state', function () { + const oldState = oneDownloadWithState(IN_PROGRESS) + const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_ACTION_PERFORMED, downloadId: downloadId(oldState), downloadAction: CANCEL}) + assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), CANCELLED) + }) + it('PAUSE causes PAUSED state', function () { + const oldState = oneDownloadWithState(IN_PROGRESS) + const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_ACTION_PERFORMED, downloadId: downloadId(oldState), downloadAction: PAUSE}) + assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), PAUSED) + }) + it('RESUME causes an IN_PROGRESS state', function () { + const oldState = oneDownloadWithState(PAUSED) + const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_ACTION_PERFORMED, downloadId: downloadId(oldState), downloadAction: RESUME}) + assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), IN_PROGRESS) + }) + }) + + describe('APP_DOWNLOAD_COPIED_TO_CLIPBOARD', function () { + it('copies the download URL to the clipboard', function () { + const spy = sinon.spy(fakeElectron.clipboard, 'writeText') + spy.withArgs(downloadUrl) + const oldState = oneDownloadWithState(IN_PROGRESS) + downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_COPIED_TO_CLIPBOARD, downloadId: downloadId(oldState)}) + assert(spy.withArgs(downloadUrl).calledOnce) + }) + }) + + describe('APP_DOWNLOAD_DELETED', function () { + it('deletes a downloadId that exists', function (cb) { + let oldState = oneDownloadWithState(IN_PROGRESS) + const existingPath = process.cwd() + oldState = oldState.setIn(['downloads', downloadId(oldState), 'savePath'], existingPath) + sinon.stub(fakeElectron.shell, 'moveItemToTrash', (path) => { + assert.equal(path, existingPath) + fakeElectron.shell.moveItemToTrash.restore() + cb() + }) + const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: downloadId(oldState)}) + assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0) + }) + it('does nothing for a downloadId that does not exist', function () { + const oldState = oneDownloadWithState(IN_PROGRESS) + const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: uuid.v4()}) + assert.deepEqual(newState.toJS(), oldState.toJS()) + }) + }) + + describe('APP_DOWNLOAD_CLEARED', function () { + it('clears download item', function () { + const oldState = oneDownloadWithState(IN_PROGRESS) + const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: downloadId(oldState)}) + assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0) + }) + it('does nothing for a downloadId that does not exist', function () { + const oldState = oneDownloadWithState(IN_PROGRESS) + const newState = downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_DELETED, downloadId: uuid.v4()}) + assert.deepEqual(newState.toJS(), oldState.toJS()) + }) + }) + + describe('APP_DOWNLOAD_REDOWNLOADED', function () { + it('should redownload the same URL', function (cb) { + const win = { + webContents: { + downloadURL: function () { + } + } + } + sinon.stub(win.webContents, 'downloadURL', (redownloadUrl) => { + assert.equal(redownloadUrl, downloadUrl) + cb() + }) + sinon.stub(fakeElectron.BrowserWindow, 'getFocusedWindow', (path) => { + return win + }) + const oldState = oneDownloadWithState(CANCELLED) + downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REDOWNLOADED, downloadId: downloadId(oldState)}) + }) + }) + + describe('APP_MERGE_DOWNLOAD_DETAIL', function () { + it('should update downloads', function () { + const oldState = oneDownloadWithState(PENDING) + const newState = downloadsReducer(oldState, + { + actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL, + downloadId: downloadId(oldState), + downloadDetail: {state: COMPLETED} + } + ) + assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), COMPLETED) + }) + it('should not update for invalid download Ids', function () { + const oldState = oneDownloadWithState(PENDING) + const newState = downloadsReducer(oldState, + { + actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL, + downloadId: uuid.v4(), + downloadDetail: {state: COMPLETED} + } + ) + assert.equal(newState.getIn(['downloads', downloadId(oldState), 'state']), PENDING) + }) + it('should add new download IDs as needed', function () { + const oldState = oneDownloadWithState(PENDING) + const downloadId = uuid.v4() + const newState = downloadsReducer(oldState, + { + actionType: appConstants.APP_MERGE_DOWNLOAD_DETAIL, + downloadId, + downloadDetail: {state: COMPLETED} + } + ) + assert.equal(newState.getIn(['downloads', downloadId, 'state']), COMPLETED) + assert.equal(Object.keys(newState.get('downloads').toJS()).length, 2) + }) + }) + + describe('APP_CLEAR_COMPLETED_DOWNLOADS', function () { + it('should clear completed downloads', function () { + const states = [COMPLETED, CANCELLED, INTERRUPTED] + states.forEach((state) => { + const newState = downloadsReducer(oneDownloadWithState(state), {actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS}) + assert.equal(Object.keys(newState.get('downloads').toJS()).length, 0) + }) + }) + it('should not clear downloads when they are still in progress', function () { + const states = [PENDING, IN_PROGRESS, RESUMING, PAUSED] + states.forEach((state) => { + const newState = downloadsReducer(oneDownloadWithState(state), {actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS}) + assert.equal(Object.keys(newState.get('downloads').toJS()).length, 1) + }) + }) + }) +}) diff --git a/test/unit/lib/fakeElectron.js b/test/unit/lib/fakeElectron.js index 3db52fc9458..2e4d444f6c6 100644 --- a/test/unit/lib/fakeElectron.js +++ b/test/unit/lib/fakeElectron.js @@ -1,4 +1,7 @@ const fakeElectron = { + BrowserWindow: { + getFocusedWindow: function () {} + }, ipcMain: { on: function () { } }, @@ -11,6 +14,21 @@ const fakeElectron = { app: { on: function () { } + }, + clipboard: { + writeText: function () { + } + }, + shell: { + showItemInFolder: function () { + }, + openItem: function () { + }, + beep: function () { + }, + moveItemToTrash: function () { + } } } + module.exports = fakeElectron diff --git a/tools/lib/ignoredPaths.js b/tools/lib/ignoredPaths.js index d1c751995bf..abefdc84a4b 100644 --- a/tools/lib/ignoredPaths.js +++ b/tools/lib/ignoredPaths.js @@ -36,6 +36,7 @@ module.exports = [ '.github', 'jsdoc', 'docs', + 'sinon', 'electron-download', 'electron-rebuild', 'electron-packager',