diff --git a/lib/gui/app.js b/lib/gui/app.js index a10a8fab..e3f2ce35 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -179,6 +179,10 @@ app.run(() => { app.run(() => { store.subscribe(() => { + if (!flashState.isFlashing()) { + return + } + const currentFlashState = flashState.getFlashState() // There is usually a short time period between the `isFlashing()` @@ -188,17 +192,15 @@ app.run(() => { // // We use the presence of `.eta` to determine that the actual // writing started. - if (!flashState.isFlashing() || !currentFlashState.eta) { - return + if (!currentFlashState.eta) { + analytics.logDebug( + `Progress (${currentFlashState.type}): ` + + `${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` + + `(eta ${currentFlashState.eta}s)` + ) } - analytics.logDebug([ - `Progress (${currentFlashState.type}):`, - `${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s`, - `(eta ${currentFlashState.eta}s)` - ].join(' ')) - - windowProgress.set(currentFlashState.percentage) + windowProgress.set(currentFlashState) }) }) diff --git a/lib/gui/modules/progress-status.js b/lib/gui/modules/progress-status.js new file mode 100644 index 00000000..bdba120a --- /dev/null +++ b/lib/gui/modules/progress-status.js @@ -0,0 +1,63 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const settings = require('../models/settings') +const utils = require('../../shared/utils') + +/** + * @summary Make the progress status subtitle string + * + * @param {Object} state - flashing metadata + * + * @returns {String} + * + * @example + * const status = progressStatus.fromFlashState({ + * type: 'write', + * percentage: 55, + * speed: 2049 + * }) + * + * console.log(status) + * // '55% Flashing' + */ +exports.fromFlashState = (state) => { + const isChecking = state.type === 'check' + const shouldValidate = settings.get('validateWriteOnSuccess') + const shouldUnmount = settings.get('unmountOnSuccess') + + if (state.percentage === utils.PERCENTAGE_MINIMUM && !state.speed) { + if (isChecking) { + return 'Validating...' + } + + return 'Starting...' + } else if (state.percentage === utils.PERCENTAGE_MAXIMUM) { + if ((isChecking || !shouldValidate) && shouldUnmount) { + return 'Unmounting...' + } + + return 'Finishing...' + } else if (state.type === 'write') { + return `${state.percentage}% Flashing` + } else if (state.type === 'check') { + return `${state.percentage}% Validating` + } + + throw new Error(`Invalid state: ${JSON.stringify(state)}`) +} diff --git a/lib/gui/os/window-progress.js b/lib/gui/os/window-progress.js index d601f42e..1cd8494a 100644 --- a/lib/gui/os/window-progress.js +++ b/lib/gui/os/window-progress.js @@ -18,6 +18,43 @@ const electron = require('electron') const utils = require('../../shared/utils') +const progressStatus = require('../modules/progress-status') + +/** + * @summary The title of the main window upon program launch + * @type {String} + * @private + * @constant + */ +const INITIAL_TITLE = document.title + +/** + * @summary Make the full window status title + * @private + * + * @param {Object} state - flash state object + * + * @returns {String} + * + * @example + * const title = getWindowTitle({ + * type: 'write', + * percentage: 55, + * speed: 2049 + * }); + * + * console.log(title); + * // 'Etcher \u2013 55% Flashing' + */ +const getWindowTitle = (state) => { + if (state) { + const subtitle = progressStatus.fromFlashState(state) + const DASH_UNICODE_CHAR = '\u2013' + return `${INITIAL_TITLE} ${DASH_UNICODE_CHAR} ${subtitle}` + } + + return INITIAL_TITLE +} /** * @summary A reference to the current renderer Electron window @@ -37,13 +74,18 @@ exports.currentWindow = electron.remote.getCurrentWindow() * @description * Show progress inline in operating system task bar * - * @param {Number} percentage - percentage + * @param {Number} state - flash state object * * @example - * windowProgress.set(85); + * windowProgress.set({ + * type: 'write', + * percentage: 55, + * speed: 2049 + * }) */ -exports.set = (percentage) => { - exports.currentWindow.setProgressBar(utils.percentageToFloat(percentage)) +exports.set = (state) => { + exports.currentWindow.setProgressBar(utils.percentageToFloat(state.percentage)) + exports.currentWindow.setTitle(getWindowTitle(state)) } /** @@ -59,4 +101,5 @@ exports.clear = () => { const ELECTRON_PROGRESS_BAR_RESET_VALUE = -1 exports.currentWindow.setProgressBar(ELECTRON_PROGRESS_BAR_RESET_VALUE) + exports.currentWindow.setTitle(getWindowTitle(null)) } diff --git a/lib/gui/pages/main/controllers/flash.js b/lib/gui/pages/main/controllers/flash.js index d0eb9832..751e619e 100644 --- a/lib/gui/pages/main/controllers/flash.js +++ b/lib/gui/pages/main/controllers/flash.js @@ -17,10 +17,9 @@ 'use strict' const messages = require('../../../../shared/messages') -const settings = require('../../../models/settings') const flashState = require('../../../../shared/models/flash-state') const driveScanner = require('../../../modules/drive-scanner') -const utils = require('../../../../shared/utils') +const progressStatus = require('../../../modules/progress-status') const notification = require('../../../os/notification') const exceptionReporter = require('../../../modules/exception-reporter') const path = require('path') @@ -119,23 +118,10 @@ module.exports = function ( * const label = FlashController.getProgressButtonLabel(); */ this.getProgressButtonLabel = () => { - const currentFlashState = flashState.getFlashState() - const isChecking = currentFlashState.type === 'check' - if (!flashState.isFlashing()) { return 'Flash!' - } else if (currentFlashState.percentage === utils.PERCENTAGE_MINIMUM && !currentFlashState.speed) { - return 'Starting...' - } else if (currentFlashState.percentage === utils.PERCENTAGE_MAXIMUM) { - if (isChecking && settings.get('unmountOnSuccess')) { - return 'Unmounting...' - } - - return 'Finishing...' - } else if (isChecking) { - return `${currentFlashState.percentage}% Validating...` } - return `${currentFlashState.percentage}%` + return progressStatus.fromFlashState(flashState.getFlashState()) } } diff --git a/tests/gui/modules/progress-status.spec.js b/tests/gui/modules/progress-status.spec.js new file mode 100644 index 00000000..3ca719d7 --- /dev/null +++ b/tests/gui/modules/progress-status.spec.js @@ -0,0 +1,104 @@ +'use strict' + +const m = require('mochainon') +const settings = require('../../../lib/gui/models/settings') +const progressStatus = require('../../../lib/gui/modules/progress-status') + +describe('Browser: progressStatus', function () { + describe('.fromFlashState()', function () { + beforeEach(function () { + this.state = { + type: 'write', + percentage: 0, + eta: 15, + speed: 100000000000000 + } + + settings.set('unmountOnSuccess', true) + settings.set('validateWriteOnSuccess', true) + }) + + it('should report 0% if percentage == 0 but speed != 0', function () { + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('0% Flashing') + }) + + it('should handle percentage == 0, type == write, unmountOnSuccess', function () { + this.state.speed = 0 + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...') + }) + + it('should handle percentage == 0, type == write, !unmountOnSuccess', function () { + this.state.speed = 0 + settings.set('unmountOnSuccess', false) + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...') + }) + + it('should handle percentage == 0, type == check, unmountOnSuccess', function () { + this.state.speed = 0 + this.state.type = 'check' + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...') + }) + + it('should handle percentage == 0, type == check, !unmountOnSuccess', function () { + this.state.speed = 0 + this.state.type = 'check' + settings.set('unmountOnSuccess', false) + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...') + }) + + it('should handle percentage == 50, type == write, unmountOnSuccess', function () { + this.state.percentage = 50 + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing') + }) + + it('should handle percentage == 50, type == write, !unmountOnSuccess', function () { + this.state.percentage = 50 + settings.set('unmountOnSuccess', false) + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing') + }) + + it('should handle percentage == 50, type == check, unmountOnSuccess', function () { + this.state.type = 'check' + this.state.percentage = 50 + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating') + }) + + it('should handle percentage == 50, type == check, !unmountOnSuccess', function () { + this.state.type = 'check' + this.state.percentage = 50 + settings.set('unmountOnSuccess', false) + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating') + }) + + it('should handle percentage == 100, type == write, unmountOnSuccess, validateWriteOnSuccess', function () { + this.state.percentage = 100 + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...') + }) + + it('should handle percentage == 100, type == write, unmountOnSuccess, !validateWriteOnSuccess', function () { + this.state.percentage = 100 + settings.set('validateWriteOnSuccess', false) + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...') + }) + + it('should handle percentage == 100, type == write, !unmountOnSuccess, !validateWriteOnSuccess', function () { + this.state.percentage = 100 + settings.set('unmountOnSuccess', false) + settings.set('validateWriteOnSuccess', false) + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...') + }) + + it('should handle percentage == 100, type == check, unmountOnSuccess', function () { + this.state.type = 'check' + this.state.percentage = 100 + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...') + }) + + it('should handle percentage == 100, type == check, !unmountOnSuccess', function () { + this.state.type = 'check' + this.state.percentage = 100 + settings.set('unmountOnSuccess', false) + m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...') + }) + }) +}) diff --git a/tests/gui/os/window-progress.spec.js b/tests/gui/os/window-progress.spec.js index e8fcb60d..ae3d8814 100644 --- a/tests/gui/os/window-progress.spec.js +++ b/tests/gui/os/window-progress.spec.js @@ -24,39 +24,80 @@ describe('Browser: WindowProgress', function () { describe('given a stubbed current window', function () { beforeEach(function () { this.setProgressBarSpy = m.sinon.spy() + this.setTitleSpy = m.sinon.spy() windowProgress.currentWindow = { - setProgressBar: this.setProgressBarSpy + setProgressBar: this.setProgressBarSpy, + setTitle: this.setTitleSpy + } + + this.state = { + percentage: 85, + speed: 100, + type: 'write' } }) describe('.set()', function () { it('should translate 0-100 percentages to 0-1 ranges', function () { - windowProgress.set(85) + windowProgress.set(this.state) m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(0.85) }) it('should set 0 given 0', function () { - windowProgress.set(0) + this.state.percentage = 0 + windowProgress.set(this.state) m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(0) }) it('should set 1 given 100', function () { - windowProgress.set(100) + this.state.percentage = 100 + windowProgress.set(this.state) m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(1) }) it('should throw if given a percentage higher than 100', function () { + this.state.percentage = 101 + const state = this.state m.chai.expect(function () { - windowProgress.set(101) + windowProgress.set(state) }).to.throw('Invalid percentage: 101') }) it('should throw if given a percentage less than 0', function () { + this.state.percentage = -1 + const state = this.state m.chai.expect(function () { - windowProgress.set(-1) + windowProgress.set(state) }).to.throw('Invalid percentage: -1') }) + + it('should set the flashing title', function () { + this.state.type = 'write' + windowProgress.set(this.state) + m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Flashing') + }) + + it('should set the validating title', function () { + this.state.type = 'check' + windowProgress.set(this.state) + m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Validating') + }) + + it('should set the starting title', function () { + this.state.type = 'write' + this.state.percentage = 0 + this.state.speed = 0 + windowProgress.set(this.state) + m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 Starting...') + }) + + it('should set the finishing title', function () { + this.state.type = 'write' + this.state.percentage = 100 + windowProgress.set(this.state) + m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 Finishing...') + }) }) describe('.clear()', function () { @@ -64,6 +105,11 @@ describe('Browser: WindowProgress', function () { windowProgress.clear() m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(-1) }) + + it('should clear the window title', function () { + windowProgress.clear() + m.chai.expect(this.setTitleSpy).to.have.been.calledWith('') + }) }) }) }) diff --git a/tests/gui/pages/main.spec.js b/tests/gui/pages/main.spec.js index 583278c8..bdab3b95 100644 --- a/tests/gui/pages/main.spec.js +++ b/tests/gui/pages/main.spec.js @@ -21,7 +21,6 @@ const _ = require('lodash') const path = require('path') const supportedFormats = require('../../../lib/shared/supported-formats') const angular = require('angular') -const settings = require('../../../lib/gui/models/settings') const flashState = require('../../../lib/shared/models/flash-state') const availableDrives = require('../../../lib/shared/models/available-drives') const selectionState = require('../../../lib/shared/models/selection-state') @@ -226,231 +225,19 @@ describe('Browser: MainPage', function () { m.chai.expect(controller.getProgressButtonLabel()).to.equal('Flash!') }) - describe('given there is a flash in progress', function () { - beforeEach(function () { - flashState.setFlashingFlag() + it('should display the flashing progress', function () { + const controller = $controller('FlashController', { + $scope: {} }) - it('should report 0% if percentage == 0 but speed != 0', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'write', - percentage: 0, - eta: 15, - speed: 100000000000000 - }) - - return settings.set('unmountOnSuccess', true).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('0%') - }) - }) - - it('should handle percentage == 0, type = write, unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'write', - percentage: 0, - eta: 15, - speed: 0 - }) - - return settings.set('unmountOnSuccess', true).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...') - }) - }) - - it('should handle percentage == 0, type = write, !unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'write', - percentage: 0, - eta: 15, - speed: 0 - }) - - return settings.set('unmountOnSuccess', false).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...') - }) - }) - - it('should handle percentage == 0, type = check, unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'check', - percentage: 0, - eta: 15, - speed: 0 - }) - - return settings.set('unmountOnSuccess', true).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...') - }) - }) - - it('should handle percentage == 0, type = check, !unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'check', - percentage: 0, - eta: 15, - speed: 0 - }) - - return settings.set('unmountOnSuccess', false).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...') - }) - }) - - it('should handle percentage == 50, type = write, unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'write', - percentage: 50, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', true).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('50%') - }) - }) - - it('should handle percentage == 50, type = write, !unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'write', - percentage: 50, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', false).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('50%') - }) - }) - - it('should handle percentage == 50, type = check, unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'check', - percentage: 50, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', true).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('50% Validating...') - }) - }) - - it('should handle percentage == 50, type = check, !unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'check', - percentage: 50, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', false).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('50% Validating...') - }) - }) - - it('should handle percentage == 100, type = write, unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'write', - percentage: 100, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', true).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...') - }) - }) - - it('should handle percentage == 100, type = write, !unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'write', - percentage: 100, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', false).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...') - }) - }) - - it('should handle percentage == 100, type = check, unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'check', - percentage: 100, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', true).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Unmounting...') - }) - }) - - it('should handle percentage == 100, type = check, !unmountOnSuccess', function () { - const controller = $controller('FlashController', { - $scope: {} - }) - - flashState.setProgressState({ - type: 'check', - percentage: 100, - eta: 15, - speed: 1000 - }) - - return settings.set('unmountOnSuccess', false).then(() => { - m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...') - }) + flashState.setFlashingFlag() + flashState.setProgressState({ + type: 'write', + percentage: 85, + eta: 15, + speed: 1000 }) + m.chai.expect(controller.getProgressButtonLabel()).to.equal('85% Flashing') }) }) })