diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8a1b680a..94ea63aa 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -80,20 +80,6 @@ contains certain features to ease communication: - A `--robot` option, which causes the Etcher CLI to output state in a way that can be easily machine-parsed. -GUI fifty-thousand foot view ----------------------------- - -Given the event oriented nature of desktop applications, it can be hard to -follow what's going on without getting deep in the details. - -To mitigate this, we try to encapsulate functionality with nice and -straightforward interfaces as AngularJS modules, and provide a single place -where all the modules are tied together. - -Therefore, if you want to get a rough idea of how the GUI works, the perfect -place to start is [main controller][maincontroller] and the [main -view][mainview], and diving into specific modules depending on your interests. - Summary ------- @@ -106,8 +92,6 @@ be documented instead! [lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328 [etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write [exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/src/exit-codes.js -[maincontroller]: https://github.com/resin-io/etcher/blob/master/lib/gui/pages/main/controllers/main.js -[mainview]: https://github.com/resin-io/etcher/blob/master/lib/gui/pages/main/templates/main.tpl.html [cli-dir]: https://github.com/resin-io/etcher/tree/master/lib/cli [gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui diff --git a/lib/gui/pages/main/controllers/drive-selection.js b/lib/gui/pages/main/controllers/drive-selection.js new file mode 100644 index 00000000..4da4112b --- /dev/null +++ b/lib/gui/pages/main/controllers/drive-selection.js @@ -0,0 +1,56 @@ +/* + * Copyright 2016 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'; + +module.exports = function(SelectionStateModel, AnalyticsService, ErrorService, DriveSelectorService) { + + /** + * @summary Open drive selector + * @function + * @public + * + * @example + * DriveSelectionController.openDriveSelector(); + */ + this.openDriveSelector = () => { + DriveSelectorService.open().then((drive) => { + if (!drive) { + return; + } + + SelectionStateModel.setDrive(drive.device); + + AnalyticsService.logEvent('Select drive', { + device: drive.device + }); + }).catch(ErrorService.reportException); + }; + + /** + * @summary Reselect a drive + * @function + * @public + * + * @example + * DriveSelectionController.reselectDrive(); + */ + this.reselectDrive = () => { + this.openDriveSelector(); + AnalyticsService.logEvent('Reselect drive'); + }; + +}; diff --git a/lib/gui/pages/main/controllers/flash.js b/lib/gui/pages/main/controllers/flash.js new file mode 100644 index 00000000..a17b27df --- /dev/null +++ b/lib/gui/pages/main/controllers/flash.js @@ -0,0 +1,127 @@ +/* + * Copyright 2016 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'; + +module.exports = function( + $state, + FlashStateModel, + SettingsModel, + DriveScannerService, + ImageWriterService, + AnalyticsService, + ErrorService, + OSNotificationService, + OSWindowProgressService +) { + + /** + * @summary Flash image to a drive + * @function + * @public + * + * @param {String} image - image path + * @param {Object} drive - drive + * + * @example + * FlashController.flashImageToDrive('rpi.img', { + * device: '/dev/disk2', + * description: 'Foo', + * size: 99999, + * mountpoint: '/mnt/foo', + * system: false + * }); + */ + this.flashImageToDrive = (image, drive) => { + if (FlashStateModel.isFlashing()) { + return; + } + + // Stop scanning drives when flashing + // otherwise Windows throws EPERM + DriveScannerService.stop(); + + AnalyticsService.logEvent('Flash', { + image: image, + device: drive.device + }); + + ImageWriterService.flash(image, drive).then(() => { + if (FlashStateModel.wasLastFlashCancelled()) { + return; + } + + if (FlashStateModel.wasLastFlashSuccessful()) { + OSNotificationService.send('Success!', 'Your flash is complete'); + AnalyticsService.logEvent('Done'); + $state.go('success'); + } else { + OSNotificationService.send('Oops!', 'Looks like your flash has failed'); + AnalyticsService.logEvent('Validation error'); + } + }) + .catch((error) => { + + if (error.type === 'check') { + AnalyticsService.logEvent('Validation error'); + } else { + AnalyticsService.logEvent('Flash error'); + } + + ErrorService.reportException(error); + }) + .finally(() => { + OSWindowProgressService.clear(); + DriveScannerService.start(); + }); + }; + + /** + * @summary Get progress button label + * @function + * @public + * + * @returns {String} progress button label + * + * @example + * const label = FlashController.getProgressButtonLabel(); + */ + this.getProgressButtonLabel = () => { + const flashState = FlashStateModel.getFlashState(); + const isChecking = flashState.type === 'check'; + + if (!FlashStateModel.isFlashing()) { + return 'Flash!'; + } + + if (flashState.percentage === 0) { + return 'Starting...'; + } else if (flashState.percentage === 100) { + if (isChecking && SettingsModel.get('unmountOnSuccess')) { + return 'Unmounting...'; + } + + return 'Finishing...'; + } + + if (isChecking) { + return `${flashState.percentage}% Validating...`; + } + + return `${flashState.percentage}%`; + }; + +}; diff --git a/lib/gui/pages/main/controllers/image-selection.js b/lib/gui/pages/main/controllers/image-selection.js new file mode 100644 index 00000000..fab0f0d0 --- /dev/null +++ b/lib/gui/pages/main/controllers/image-selection.js @@ -0,0 +1,105 @@ +/* + * Copyright 2016 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 _ = require('lodash'); + +module.exports = function(SupportedFormatsModel, SelectionStateModel, AnalyticsService, ErrorService, OSDialogService) { + + /** + * @summary Main supported extensions + * @constant + * @type {String[]} + * @public + */ + this.mainSupportedExtensions = _.slice(SupportedFormatsModel.getAllExtensions(), 0, 3); + + /** + * @summary Extra supported extensions + * @constant + * @type {String[]} + * @public + */ + this.extraSupportedExtensions = _.difference( + SupportedFormatsModel.getAllExtensions(), + this.mainSupportedExtensions + ); + + /** + * @summary Select image + * @function + * @public + * + * @param {Object} image - image + * + * @example + * OSDialogService.selectImage() + * .then(ImageSelectionController.selectImage); + */ + this.selectImage = (image) => { + if (!SupportedFormatsModel.isSupportedImage(image.path)) { + OSDialogService.showError('Invalid image', `${image.path} is not a supported image type.`); + AnalyticsService.logEvent('Invalid image', image); + return; + } + + SelectionStateModel.setImage(image); + AnalyticsService.logEvent('Select image', _.omit(image, 'logo')); + }; + + /** + * @summary Open image selector + * @function + * @public + * + * @example + * ImageSelectionController.openImageSelector(); + */ + this.openImageSelector = () => { + OSDialogService.selectImage().then((image) => { + + // Avoid analytics and selection state changes + // if no file was resolved from the dialog. + if (!image) { + return; + } + + this.selectImage(image); + }).catch(ErrorService.reportException); + }; + + /** + * @summary Reselect image + * @function + * @public + * + * @example + * ImageSelectionController.reselectImage(); + */ + this.reselectImage = () => { + + // Reselecting an image automatically + // de-selects the current drive, if any. + // This is made so the user effectively + // "returns" to the first step. + SelectionStateModel.clear(); + + this.openImageSelector(); + AnalyticsService.logEvent('Reselect image'); + }; + +}; diff --git a/lib/gui/pages/main/controllers/main.js b/lib/gui/pages/main/controllers/main.js index cf7876b8..cef2cd6b 100644 --- a/lib/gui/pages/main/controllers/main.js +++ b/lib/gui/pages/main/controllers/main.js @@ -16,136 +16,34 @@ 'use strict'; -const _ = require('lodash'); - module.exports = function( - $state, - DriveScannerService, SelectionStateModel, + DrivesModel, FlashStateModel, SettingsModel, - SupportedFormatsModel, - DrivesModel, - ImageWriterService, AnalyticsService, - ErrorService, - DriveSelectorService, TooltipModalService, - OSWindowProgressService, - OSNotificationService, - OSDialogService, OSOpenExternalService ) { - this.formats = SupportedFormatsModel; + // Expose several modules to the template for convenience this.selection = SelectionStateModel; this.drives = DrivesModel; this.state = FlashStateModel; this.settings = SettingsModel; + this.external = OSOpenExternalService; this.tooltipModal = TooltipModalService; - this.getProgressButtonLabel = () => { - const flashState = this.state.getFlashState(); - - if (!this.state.isFlashing()) { - return 'Flash!'; - } - - if (flashState.percentage === 100) { - if (flashState.type === 'check' && this.settings.get('unmountOnSuccess')) { - return 'Unmounting...'; - } - - return 'Finishing...'; - } - - if (flashState.percentage === 0) { - return 'Starting...'; - } - - if (flashState.type === 'check') { - return `${flashState.percentage}% Validating...`; - } - - return `${flashState.percentage}%`; - }; - - this.selectImage = (image) => { - if (!SupportedFormatsModel.isSupportedImage(image.path)) { - OSDialogService.showError('Invalid image', `${image.path} is not a supported image type.`); - AnalyticsService.logEvent('Invalid image', image); - return; - } - - this.selection.setImage(image); - AnalyticsService.logEvent('Select image', _.omit(image, 'logo')); - }; - - this.openImageUrl = () => { - const imageUrl = this.selection.getImageUrl(); - - if (imageUrl) { - OSOpenExternalService.open(imageUrl); - } - }; - - this.openImageSelector = () => { - return OSDialogService.selectImage().then((image) => { - - // Avoid analytics and selection state changes - // if no file was resolved from the dialog. - if (!image) { - return; - } - - this.selectImage(image); - }).catch(ErrorService.reportException); - }; - - this.selectDrive = (drive) => { - if (!drive) { - return; - } - - this.selection.setDrive(drive.device); - - AnalyticsService.logEvent('Select drive', { - device: drive.device - }); - }; - - this.openDriveSelector = () => { - DriveSelectorService.open() - .then(this.selectDrive) - .catch(ErrorService.reportException); - }; - - this.reselectImage = () => { - if (FlashStateModel.isFlashing()) { - return; - } - - // Reselecting an image automatically - // de-selects the current drive, if any. - // This is made so the user effectively - // "returns" to the first step. - this.selection.clear(); - - this.openImageSelector(); - AnalyticsService.logEvent('Reselect image'); - }; - - this.reselectDrive = () => { - if (FlashStateModel.isFlashing()) { - return; - } - - this.openDriveSelector(); - AnalyticsService.logEvent('Reselect drive'); - }; - + /** + * @summary Restart after failure + * @function + * @public + * + * @example + * MainController.restartAfterFailure(); + */ this.restartAfterFailure = () => { - this.selection.clear({ + SelectionStateModel.clear({ preserveImage: true }); @@ -153,49 +51,36 @@ module.exports = function( AnalyticsService.logEvent('Restart after failure'); }; - this.flash = (image, drive) => { + /** + * @summary Determine if the drive step should be disabled + * @function + * @public + * + * @returns {Boolean} whether the drive step should be disabled + * + * @example + * if (MainController.shouldDriveStepBeDisabled()) { + * console.log('The drive step should be disabled'); + * } + */ + this.shouldDriveStepBeDisabled = () => { + return !SelectionStateModel.hasImage(); + }; - if (FlashStateModel.isFlashing()) { - return; - } - - // Stop scanning drives when flashing - // otherwise Windows throws EPERM - DriveScannerService.stop(); - - AnalyticsService.logEvent('Flash', { - image: image, - device: drive.device - }); - - return ImageWriterService.flash(image, drive).then(() => { - if (FlashStateModel.wasLastFlashCancelled()) { - return; - } - - if (FlashStateModel.wasLastFlashSuccessful()) { - OSNotificationService.send('Success!', 'Your flash is complete'); - AnalyticsService.logEvent('Done'); - $state.go('success'); - } else { - OSNotificationService.send('Oops!', 'Looks like your flash has failed'); - AnalyticsService.logEvent('Validation error'); - } - }) - .catch((error) => { - - if (error.type === 'check') { - AnalyticsService.logEvent('Validation error'); - } else { - AnalyticsService.logEvent('Flash error'); - } - - ErrorService.reportException(error); - }) - .finally(() => { - OSWindowProgressService.clear(); - DriveScannerService.start(); - }); + /** + * @summary Determine if the flash step should be disabled + * @function + * @public + * + * @returns {Boolean} whether the flash step should be disabled + * + * @example + * if (MainController.shouldFlashStateBeDisabled()) { + * console.log('The flash step should be disabled'); + * } + */ + this.shouldFlashStateBeDisabled = () => { + return this.shouldDriveStepBeDisabled() || !SelectionStateModel.hasDrive(); }; }; diff --git a/lib/gui/pages/main/main.js b/lib/gui/pages/main/main.js index e819fbca..995b7686 100644 --- a/lib/gui/pages/main/main.js +++ b/lib/gui/pages/main/main.js @@ -55,6 +55,9 @@ const MainPage = angular.module(MODULE_NAME, [ ]); MainPage.controller('MainController', require('./controllers/main')); +MainPage.controller('ImageSelectionController', require('./controllers/image-selection')); +MainPage.controller('DriveSelectionController', require('./controllers/drive-selection')); +MainPage.controller('FlashController', require('./controllers/flash')); MainPage.config(($stateProvider) => { $stateProvider diff --git a/lib/gui/pages/main/templates/main.tpl.html b/lib/gui/pages/main/templates/main.tpl.html index 2ba9ee20..9276a030 100644 --- a/lib/gui/pages/main/templates/main.tpl.html +++ b/lib/gui/pages/main/templates/main.tpl.html @@ -1,22 +1,22 @@
-
-
+
+
SELECT IMAGE 1
- +
-
-
+
-
-
+
+
+ ng-disabled="main.shouldDriveStepBeDisabled()"> SELECT DRIVE + ng-disabled="main.shouldDriveStepBeDisabled()">SELECT DRIVE - 2 + 2
-
+
+ ng-disabled="main.shouldDriveStepBeDisabled()" + ng-click="drive.openDriveSelector()">Select drive
-
+
{{ main.selection.getDrive().name }} - {{ main.selection.getDrive().size | gigabyte | number:1 }} GB
-
+
+ ng-disabled="main.shouldFlashStateBeDisabled()"> FLASH IMAGE + ng-disabled="main.shouldFlashStateBeDisabled()">FLASH IMAGE - 3 + 3
- + ng-click="flash.flashImageToDrive(main.selection.getImagePath(), main.selection.getDrive())" + ng-disabled="main.shouldFlashStateBeDisabled()"> +
diff --git a/tests/gui/pages/main.spec.js b/tests/gui/pages/main.spec.js new file mode 100644 index 00000000..eaef2934 --- /dev/null +++ b/tests/gui/pages/main.spec.js @@ -0,0 +1,378 @@ +'use strict'; + +const m = require('mochainon'); +const angular = require('angular'); +require('angular-mocks'); + +describe('Browser: MainPage', function() { + + beforeEach(angular.mock.module( + require('../../../lib/gui/pages/main/main') + )); + + describe('MainController', function() { + + let $controller; + let SelectionStateModel; + let DrivesModel; + + beforeEach(angular.mock.inject(function(_$controller_, _SelectionStateModel_, _DrivesModel_) { + $controller = _$controller_; + SelectionStateModel = _SelectionStateModel_; + DrivesModel = _DrivesModel_; + })); + + describe('.shouldDriveStepBeDisabled()', function() { + + it('should return true if there is no image', function() { + const controller = $controller('MainController', { + $scope: {} + }); + + SelectionStateModel.clear(); + + m.chai.expect(controller.shouldDriveStepBeDisabled()).to.be.true; + }); + + it('should return false if there is an image', function() { + const controller = $controller('MainController', { + $scope: {} + }); + + SelectionStateModel.setImage({ + path: 'rpi.img', + size: 99999 + }); + + m.chai.expect(controller.shouldDriveStepBeDisabled()).to.be.false; + }); + + }); + + describe('.shouldFlashStateBeDisabled()', function() { + + it('should return true if there is no selected drive nor image', function() { + const controller = $controller('MainController', { + $scope: {} + }); + + SelectionStateModel.clear(); + + m.chai.expect(controller.shouldFlashStateBeDisabled()).to.be.true; + }); + + it('should return true if there is a selected image but no drive', function() { + const controller = $controller('MainController', { + $scope: {} + }); + + SelectionStateModel.clear(); + SelectionStateModel.setImage({ + path: 'rpi.img', + size: 99999 + }); + + m.chai.expect(controller.shouldFlashStateBeDisabled()).to.be.true; + }); + + it('should return true if there is a selected drive but no image', function() { + const controller = $controller('MainController', { + $scope: {} + }); + + DrivesModel.setDrives([ + { + device: '/dev/disk2', + description: 'Foo', + size: 99999, + mountpoint: '/mnt/foo', + system: false + } + ]); + + SelectionStateModel.clear(); + SelectionStateModel.setDrive('/dev/disk2'); + + m.chai.expect(controller.shouldFlashStateBeDisabled()).to.be.true; + }); + + it('should return false if there is a selected drive and a selected image', function() { + const controller = $controller('MainController', { + $scope: {} + }); + + DrivesModel.setDrives([ + { + device: '/dev/disk2', + description: 'Foo', + size: 99999, + mountpoint: '/mnt/foo', + system: false + } + ]); + + SelectionStateModel.clear(); + SelectionStateModel.setDrive('/dev/disk2'); + + SelectionStateModel.setImage({ + path: 'rpi.img', + size: 99999 + }); + + m.chai.expect(controller.shouldFlashStateBeDisabled()).to.be.false; + }); + + }); + + }); + + describe('ImageSelectionController', function() { + + let $controller; + let SupportedFormatsModel; + + beforeEach(angular.mock.inject(function(_$controller_, _SupportedFormatsModel_) { + $controller = _$controller_; + SupportedFormatsModel = _SupportedFormatsModel_; + })); + + it('should contain all available extensions in mainSupportedExtensions and extraSupportedExtensions', function() { + const $scope = {}; + const controller = $controller('ImageSelectionController', { + $scope + }); + + const extensions = controller.mainSupportedExtensions.concat(controller.extraSupportedExtensions); + m.chai.expect(extensions).to.deep.equal(SupportedFormatsModel.getAllExtensions()); + }); + + }); + + describe('FlashController', function() { + + let $controller; + let FlashStateModel; + let SettingsModel; + + beforeEach(angular.mock.inject(function(_$controller_, _FlashStateModel_, _SettingsModel_) { + $controller = _$controller_; + FlashStateModel = _FlashStateModel_; + SettingsModel = _SettingsModel_; + })); + + describe('.getProgressButtonLabel()', function() { + + it('should return "Flash!" given a clean state', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.resetState(); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Flash!'); + }); + + describe('given there is a flash in progress', function() { + + beforeEach(function() { + FlashStateModel.setFlashingFlag(); + }); + + it('should handle percentage == 0, type = write, unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'write', + percentage: 0, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', true); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...'); + }); + + it('should handle percentage == 0, type = write, !unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'write', + percentage: 0, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', false); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...'); + }); + + it('should handle percentage == 0, type = check, unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'check', + percentage: 0, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', true); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...'); + }); + + it('should handle percentage == 0, type = check, !unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'check', + percentage: 0, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', false); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Starting...'); + }); + + it('should handle percentage == 50, type = write, unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'write', + percentage: 50, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', true); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('50%'); + }); + + it('should handle percentage == 50, type = write, !unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'write', + percentage: 50, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', false); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('50%'); + }); + + it('should handle percentage == 50, type = check, unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'check', + percentage: 50, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', true); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('50% Validating...'); + }); + + it('should handle percentage == 50, type = check, !unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'check', + percentage: 50, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', false); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('50% Validating...'); + }); + + it('should handle percentage == 100, type = write, unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'write', + percentage: 100, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', true); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...'); + }); + + it('should handle percentage == 100, type = write, !unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'write', + percentage: 100, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', false); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...'); + }); + + it('should handle percentage == 100, type = check, unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'check', + percentage: 100, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', true); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Unmounting...'); + }); + + it('should handle percentage == 100, type = check, !unmountOnSuccess', function() { + const controller = $controller('FlashController', { + $scope: {} + }); + + FlashStateModel.setProgressState({ + type: 'check', + percentage: 100, + eta: 15, + speed: 1000 + }); + + SettingsModel.set('unmountOnSuccess', false); + m.chai.expect(controller.getProgressButtonLabel()).to.equal('Finishing...'); + }); + + }); + + }); + + }); + +});