refactor(GUI): main controller (#623)

The main page controller contained a lot of undocumented and untested
logic. As a first step towards cleaning up the whole thing, this PR
introduces the following changes:

- Implement `ImageSelectionController`, `DriveSelectionController`, and
  `FlashController` as children of `MainController`. Each of them is
  used by the appropriate main page "steps", and contains logic specific
  to them. The `MainController` hosts functionality that applies to the
  page as a whole.

- Add JSDoc annotations fo every controller function/property.

- Unit test several controller functions.

- Simplify template logic.

The "GUI fifty-thousand foot view" section in ARCHITECTURE.md has been
removed since there is no longer a single place where you can see all
the interactions between components.

Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
This commit is contained in:
Juan Cruz Viotti 2016-08-05 14:40:51 -04:00 committed by GitHub
parent ecd5d5bf5c
commit 85676a2e94
8 changed files with 736 additions and 198 deletions

View File

@ -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 - A `--robot` option, which causes the Etcher CLI to output state in a way that
can be easily machine-parsed. 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 Summary
------- -------
@ -106,8 +92,6 @@ be documented instead!
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328 [lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
[etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write [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 [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 [cli-dir]: https://github.com/resin-io/etcher/tree/master/lib/cli
[gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui [gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui

View File

@ -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');
};
};

View File

@ -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}%`;
};
};

View File

@ -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');
};
};

View File

@ -16,136 +16,34 @@
'use strict'; 'use strict';
const _ = require('lodash');
module.exports = function( module.exports = function(
$state,
DriveScannerService,
SelectionStateModel, SelectionStateModel,
DrivesModel,
FlashStateModel, FlashStateModel,
SettingsModel, SettingsModel,
SupportedFormatsModel,
DrivesModel,
ImageWriterService,
AnalyticsService, AnalyticsService,
ErrorService,
DriveSelectorService,
TooltipModalService, TooltipModalService,
OSWindowProgressService,
OSNotificationService,
OSDialogService,
OSOpenExternalService OSOpenExternalService
) { ) {
this.formats = SupportedFormatsModel; // Expose several modules to the template for convenience
this.selection = SelectionStateModel; this.selection = SelectionStateModel;
this.drives = DrivesModel; this.drives = DrivesModel;
this.state = FlashStateModel; this.state = FlashStateModel;
this.settings = SettingsModel; this.settings = SettingsModel;
this.external = OSOpenExternalService;
this.tooltipModal = TooltipModalService; this.tooltipModal = TooltipModalService;
this.getProgressButtonLabel = () => { /**
const flashState = this.state.getFlashState(); * @summary Restart after failure
* @function
if (!this.state.isFlashing()) { * @public
return 'Flash!'; *
} * @example
* MainController.restartAfterFailure();
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');
};
this.restartAfterFailure = () => { this.restartAfterFailure = () => {
this.selection.clear({ SelectionStateModel.clear({
preserveImage: true preserveImage: true
}); });
@ -153,49 +51,36 @@ module.exports = function(
AnalyticsService.logEvent('Restart after failure'); 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; * @summary Determine if the flash step should be disabled
} * @function
* @public
// Stop scanning drives when flashing *
// otherwise Windows throws EPERM * @returns {Boolean} whether the flash step should be disabled
DriveScannerService.stop(); *
* @example
AnalyticsService.logEvent('Flash', { * if (MainController.shouldFlashStateBeDisabled()) {
image: image, * console.log('The flash step should be disabled');
device: drive.device * }
}); */
this.shouldFlashStateBeDisabled = () => {
return ImageWriterService.flash(image, drive).then(() => { return this.shouldDriveStepBeDisabled() || !SelectionStateModel.hasDrive();
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();
});
}; };
}; };

View File

@ -55,6 +55,9 @@ const MainPage = angular.module(MODULE_NAME, [
]); ]);
MainPage.controller('MainController', require('./controllers/main')); 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) => { MainPage.config(($stateProvider) => {
$stateProvider $stateProvider

View File

@ -1,22 +1,22 @@
<div class="row around-xs"> <div class="row around-xs">
<div class="col-xs"> <div class="col-xs" ng-controller="ImageSelectionController as image">
<div class="box text-center" os-dropzone="main.selectImage($file)"> <div class="box text-center" os-dropzone="image.selectImage($file)">
<svg-icon class="center-block" path="{{ main.selection.getImageLogo() || '../../../assets/image.svg' }}"></svg-icon> <svg-icon class="center-block" path="{{ main.selection.getImageLogo() || '../../../assets/image.svg' }}"></svg-icon>
<span class="icon-caption">SELECT IMAGE</span> <span class="icon-caption">SELECT IMAGE</span>
<span class="badge space-top-medium">1</span> <span class="badge space-top-medium">1</span>
<div class="space-vertical-large"> <div class="space-vertical-large">
<div ng-hide="main.selection.hasImage()"> <div ng-hide="main.selection.hasImage()">
<button class="btn btn-primary btn-brick" ng-click="main.openImageSelector()">Select image</button> <button class="btn btn-primary btn-brick" ng-click="image.openImageSelector()">Select image</button>
<p class="step-footer"> <p class="step-footer">
{{ ::main.formats.getAllExtensions().slice(0, 3).join(', ') }}, and {{ ::image.mainSupportedExtensions.join(', ') }}, and
<span class="step-footer-underline" <span class="step-footer-underline"
uib-tooltip="{{ main.formats.getAllExtensions().slice(3).join(', ') }}">many more</span> uib-tooltip="{{ image.extraSupportedExtensions.join(', ') }}">many more</span>
</p> </p>
</div> </div>
<div ng-if="main.selection.hasImage()"> <div ng-if="main.selection.hasImage()">
<div ng-click="main.openImageUrl()" <div ng-click="main.external.open(main.selection.getImageUrl())"
ng-bind="main.selection.getImageName() || main.selection.getImagePath() | basename | middleEllipses:25"></div> ng-bind="main.selection.getImageName() || main.selection.getImagePath() | basename | middleEllipses:25"></div>
<button class="btn btn-link step-tooltip" <button class="btn btn-link step-tooltip"
@ -26,61 +26,61 @@
})">SHOW IN FULL</button> })">SHOW IN FULL</button>
<button class="btn btn-link step-footer" <button class="btn btn-link step-footer"
ng-click="main.reselectImage()" ng-click="image.reselectImage()"
ng-hide="main.state.isFlashing()">Change</button> ng-hide="main.state.isFlashing()">Change</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xs"> <div class="col-xs" ng-controller="DriveSelectionController as drive">
<div class="box text-center relative"> <div class="box text-center relative">
<div class="step-border-left" ng-disabled="!main.selection.hasImage()"></div> <div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()"></div>
<div class="step-border-right" ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()"></div> <div class="step-border-right" ng-disabled="main.shouldFlashStateBeDisabled()"></div>
<svg-icon class="center-block" <svg-icon class="center-block"
path="../../../assets/drive.svg" path="../../../assets/drive.svg"
ng-disabled="!main.selection.hasImage()"></svg-icon> ng-disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
<span class="icon-caption" <span class="icon-caption"
ng-disabled="!main.selection.hasImage()">SELECT DRIVE</span> ng-disabled="main.shouldDriveStepBeDisabled()">SELECT DRIVE</span>
<span class="badge space-top-medium" ng-disabled="!main.selection.hasImage()">2</span> <span class="badge space-top-medium" ng-disabled="main.shouldDriveStepBeDisabled()">2</span>
<div class="space-vertical-large"> <div class="space-vertical-large">
<div ng-hide="main.selection.hasDrive()"> <div ng-hide="main.selection.hasDrive()">
<div ng-show="main.drives.hasAvailableDrives() || !main.selection.hasImage()"> <div ng-show="main.drives.hasAvailableDrives() || main.shouldDriveStepBeDisabled()">
<button class="btn btn-primary btn-brick" <button class="btn btn-primary btn-brick"
ng-disabled="!main.selection.hasImage()" ng-disabled="main.shouldDriveStepBeDisabled()"
ng-click="main.openDriveSelector()">Select drive</button> ng-click="drive.openDriveSelector()">Select drive</button>
</div> </div>
<div ng-hide="main.drives.hasAvailableDrives() || !main.selection.hasImage()"> <div ng-hide="main.drives.hasAvailableDrives() || main.shouldDriveStepBeDisabled()">
<button class="btn btn-danger btn-brick">Connect a drive</button> <button class="btn btn-danger btn-brick">Connect a drive</button>
</div> </div>
</div> </div>
<div ng-show="main.selection.hasDrive()"> <div ng-show="main.selection.hasDrive()">
<div ng-class="{ <div ng-class="{
soft: !main.selection.hasImage() soft: main.shouldDriveStepBeDisabled()
}">{{ main.selection.getDrive().name }} - {{ main.selection.getDrive().size | gigabyte | number:1 }} GB</div> }">{{ main.selection.getDrive().name }} - {{ main.selection.getDrive().size | gigabyte | number:1 }} GB</div>
<button class="btn btn-link step-footer" <button class="btn btn-link step-footer"
ng-click="main.reselectDrive()" ng-click="drive.reselectDrive()"
ng-hide="main.state.isFlashing()">Change</button> ng-hide="main.state.isFlashing()">Change</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xs"> <div class="col-xs" ng-controller="FlashController as flash">
<div class="box text-center"> <div class="box text-center">
<svg-icon class="center-block" <svg-icon class="center-block"
path="../../../assets/flash.svg" path="../../../assets/flash.svg"
ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()"></svg-icon> ng-disabled="main.shouldFlashStateBeDisabled()"></svg-icon>
<span class="icon-caption" <span class="icon-caption"
ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()">FLASH IMAGE</span> ng-disabled="main.shouldFlashStateBeDisabled()">FLASH IMAGE</span>
<span class="badge space-top-medium" ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()">3</span> <span class="badge space-top-medium" ng-disabled="main.shouldFlashStateBeDisabled()">3</span>
<div class="space-vertical-large"> <div class="space-vertical-large">
<progress-button class="btn-brick" <progress-button class="btn-brick"
@ -88,9 +88,9 @@
striped="{{ main.state.getFlashState().type == 'check' }}" striped="{{ main.state.getFlashState().type == 'check' }}"
ng-attr-active="{{ main.state.isFlashing() }}" ng-attr-active="{{ main.state.isFlashing() }}"
ng-show="main.state.wasLastFlashSuccessful()" ng-show="main.state.wasLastFlashSuccessful()"
ng-click="main.flash(main.selection.getImagePath(), main.selection.getDrive())" ng-click="flash.flashImageToDrive(main.selection.getImagePath(), main.selection.getDrive())"
ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()"> ng-disabled="main.shouldFlashStateBeDisabled()">
<span ng-bind="main.getProgressButtonLabel()"></span> <span ng-bind="flash.getProgressButtonLabel()"></span>
</progress-button> </progress-button>
<div class="alert-ribbon alert-warning" ng-class="{ 'alert-ribbon--open': !main.state.wasLastFlashSuccessful() }"> <div class="alert-ribbon alert-warning" ng-class="{ 'alert-ribbon--open': !main.state.wasLastFlashSuccessful() }">

View File

@ -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...');
});
});
});
});
});