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