mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-29 14:16:36 +00:00
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:
parent
ecd5d5bf5c
commit
85676a2e94
@ -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
|
||||
|
||||
|
56
lib/gui/pages/main/controllers/drive-selection.js
Normal file
56
lib/gui/pages/main/controllers/drive-selection.js
Normal 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');
|
||||
};
|
||||
|
||||
};
|
127
lib/gui/pages/main/controllers/flash.js
Normal file
127
lib/gui/pages/main/controllers/flash.js
Normal 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}%`;
|
||||
};
|
||||
|
||||
};
|
105
lib/gui/pages/main/controllers/image-selection.js
Normal file
105
lib/gui/pages/main/controllers/image-selection.js
Normal 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');
|
||||
};
|
||||
|
||||
};
|
@ -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();
|
||||
};
|
||||
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -1,22 +1,22 @@
|
||||
<div class="row around-xs">
|
||||
<div class="col-xs">
|
||||
<div class="box text-center" os-dropzone="main.selectImage($file)">
|
||||
<div class="col-xs" ng-controller="ImageSelectionController as image">
|
||||
<div class="box text-center" os-dropzone="image.selectImage($file)">
|
||||
<svg-icon class="center-block" path="{{ main.selection.getImageLogo() || '../../../assets/image.svg' }}"></svg-icon>
|
||||
<span class="icon-caption">SELECT IMAGE</span>
|
||||
<span class="badge space-top-medium">1</span>
|
||||
|
||||
<div class="space-vertical-large">
|
||||
<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">
|
||||
{{ ::main.formats.getAllExtensions().slice(0, 3).join(', ') }}, and
|
||||
{{ ::image.mainSupportedExtensions.join(', ') }}, and
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<button class="btn btn-link step-tooltip"
|
||||
@ -26,61 +26,61 @@
|
||||
})">SHOW IN FULL</button>
|
||||
|
||||
<button class="btn btn-link step-footer"
|
||||
ng-click="main.reselectImage()"
|
||||
ng-click="image.reselectImage()"
|
||||
ng-hide="main.state.isFlashing()">Change</button>
|
||||
</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="step-border-left" ng-disabled="!main.selection.hasImage()"></div>
|
||||
<div class="step-border-right" ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()"></div>
|
||||
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()"></div>
|
||||
<div class="step-border-right" ng-disabled="main.shouldFlashStateBeDisabled()"></div>
|
||||
|
||||
<svg-icon class="center-block"
|
||||
path="../../../assets/drive.svg"
|
||||
ng-disabled="!main.selection.hasImage()"></svg-icon>
|
||||
ng-disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
|
||||
<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 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"
|
||||
ng-disabled="!main.selection.hasImage()"
|
||||
ng-click="main.openDriveSelector()">Select drive</button>
|
||||
ng-disabled="main.shouldDriveStepBeDisabled()"
|
||||
ng-click="drive.openDriveSelector()">Select drive</button>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div ng-show="main.selection.hasDrive()">
|
||||
<div ng-class="{
|
||||
soft: !main.selection.hasImage()
|
||||
soft: main.shouldDriveStepBeDisabled()
|
||||
}">{{ main.selection.getDrive().name }} - {{ main.selection.getDrive().size | gigabyte | number:1 }} GB</div>
|
||||
<button class="btn btn-link step-footer"
|
||||
ng-click="main.reselectDrive()"
|
||||
ng-click="drive.reselectDrive()"
|
||||
ng-hide="main.state.isFlashing()">Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs">
|
||||
<div class="col-xs" ng-controller="FlashController as flash">
|
||||
<div class="box text-center">
|
||||
<svg-icon class="center-block"
|
||||
path="../../../assets/flash.svg"
|
||||
ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()"></svg-icon>
|
||||
ng-disabled="main.shouldFlashStateBeDisabled()"></svg-icon>
|
||||
<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">
|
||||
<progress-button class="btn-brick"
|
||||
@ -88,9 +88,9 @@
|
||||
striped="{{ main.state.getFlashState().type == 'check' }}"
|
||||
ng-attr-active="{{ main.state.isFlashing() }}"
|
||||
ng-show="main.state.wasLastFlashSuccessful()"
|
||||
ng-click="main.flash(main.selection.getImagePath(), main.selection.getDrive())"
|
||||
ng-disabled="!main.selection.hasImage() || !main.selection.hasDrive()">
|
||||
<span ng-bind="main.getProgressButtonLabel()"></span>
|
||||
ng-click="flash.flashImageToDrive(main.selection.getImagePath(), main.selection.getDrive())"
|
||||
ng-disabled="main.shouldFlashStateBeDisabled()">
|
||||
<span ng-bind="flash.getProgressButtonLabel()"></span>
|
||||
</progress-button>
|
||||
|
||||
<div class="alert-ribbon alert-warning" ng-class="{ 'alert-ribbon--open': !main.state.wasLastFlashSuccessful() }">
|
||||
|
378
tests/gui/pages/main.spec.js
Normal file
378
tests/gui/pages/main.spec.js
Normal 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...');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user