Implement update notifier modal (#406)

Auto-update functionality is not ready for usage. As a workaround to
prevent users staying with older versions, we now check for updates at
startup, and if the user is not running the latest version, we present a
modal informing the user of the availiblity of a new version, and
provide a call to action to open the Etcher website in his web browser.

Extra features:

- The user can skip the update, and tell the program to delay the
notification for 7 days.

Misc changes:

- Center modal with flexbox, to allow more flexibility on the modal height.
interacting with the S3 server.
- Implement `ManifestBindService`, which now serves as a backend for the
`manifest-bind` directive to allow the directive's functionality to be
re-used by other services.
- Namespace checkbox styles that are specific to the settings page.

Fixes: https://github.com/resin-io/etcher/issues/396
Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
This commit is contained in:
Juan Cruz Viotti 2016-05-12 13:11:30 -04:00
parent 662c589ab9
commit a4e2639c00
20 changed files with 971 additions and 125 deletions

View File

@ -1254,7 +1254,7 @@ mark,
.text-right, .section-header {
text-align: right; }
.text-center, .alert, .alert-ribbon, .section-footer {
.text-center, .alert, .alert-ribbon, .update-notifier-modal-body__content, .section-footer {
text-align: center; }
.text-justify {
@ -5909,9 +5909,6 @@ html {
position: initial;
margin-right: 2px; }
.checkbox input[type="checkbox"]:not(:checked) + * {
color: #ddd; }
.modal-backdrop.in {
opacity: 0; }
@ -6116,6 +6113,184 @@ button.btn:focus, button.progress-button:focus {
background-color: #d9534f;
border-color: #d9534f; }
/*
* 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.
*/
.modal-content {
display: flex;
flex-direction: column; }
.modal-header {
display: flex;
align-items: baseline;
text-transform: uppercase;
font-size: 11px; }
.modal-title {
font-size: inherit;
flex-grow: 1; }
.modal-header {
color: #b3b3b3;
padding: 11px 20px;
flex-grow: 0; }
.modal-body {
flex-grow: 1;
color: #666;
padding: 0 20px;
max-height: 250px;
overflow: auto; }
.modal-content {
height: 320px; }
.modal-body .list-group-item {
display: flex;
align-items: center;
border-left: none;
border-right: none;
border-radius: 0;
border-color: #eee;
padding: 12px 0; }
.modal-body .list-group-item > .tick {
font-size: 11px; }
.modal-body .list-group-item-heading {
font-size: 13px; }
.modal-body .list-group-item-text {
line-height: 1;
font-size: 11px;
color: #aaa; }
.modal-body .list-group-item :first-child {
flex-grow: 1; }
.modal-body .list-group-item:first-child {
border-top: none; }
.modal-open {
padding-right: 0 !important; }
.modal-fat-and-short {
width: 400px;
margin-top: -10px; }
.modal-fat-and-short .modal-content {
height: 245px; }
.modal-footer {
flex-grow: 0;
border: 0; }
.modal .btn-primary[disabled], .modal [disabled].progress-button--primary {
background-color: #d5d5d5; }
.modal {
display: flex !important;
justify-content: center;
align-items: center; }
.modal-dialog {
margin: 0;
position: initial; }
/*
* 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.
*/
.alert-ribbon {
width: 60%;
position: fixed;
left: 0;
right: 0;
margin: 0 auto;
border-top-left-radius: 0;
border-top-right-radius: 0;
top: -100%;
transition: top 0.5s; }
.alert-ribbon > .glyphicon:first-child, .alert-ribbon > .tick:first-child {
margin-right: 5px; }
.alert-ribbon > .glyphicon:last-child, .alert-ribbon > .tick:last-child {
margin-left: 5px; }
.alert-ribbon .btn-link {
padding: 0;
font-size: inherit;
vertical-align: baseline;
border-radius: 0;
border-bottom: 1px solid; }
.alert-ribbon.alert-warning .btn-link {
border-color: #f7dbc3;
color: #fff; }
.alert-ribbon.alert-warning .btn-link:hover {
color: #e6e6e6;
border-color: #f2c096; }
.alert-ribbon--open {
top: 0; }
/*
* 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.
*/
.update-notifier-modal-body {
padding: 30px 35px; }
.update-notifier-modal-body__content {
padding-bottom: 15px;
margin-bottom: 25px;
border-bottom: 1px solid #eeeeee; }
.update-notifier-modal-body__title {
margin-bottom: 15px; }
.update-notifier-modal-body__menu {
display: flex;
justify-content: center; }
.update-notifier-modal-body__menu > .btn, .update-notifier-modal-body__menu > .progress-button {
flex-grow: 1;
width: 0; }
.update-notifier-modal-body__menu > .btn + .btn, .update-notifier-modal-body__menu > .progress-button + .btn, .update-notifier-modal-body__menu > .btn + .progress-button, .update-notifier-modal-body__menu > .progress-button + .progress-button {
margin-left: 10px; }
.update-notifier-modal-body .checkbox {
color: #959595;
font-size: 11px; }
/*
* Copyright 2016 Resin.io
*
@ -6231,114 +6406,8 @@ button.btn:focus, button.progress-button:focus {
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.modal-content {
display: flex;
flex-direction: column; }
.modal-header {
display: flex;
align-items: baseline;
text-transform: uppercase;
font-size: 11px; }
.modal-title {
font-size: inherit;
flex-grow: 1; }
.modal-header {
color: #b3b3b3;
padding: 11px 20px;
flex-grow: 0; }
.modal-body {
flex-grow: 1;
color: #666;
padding: 0 20px;
max-height: 250px;
overflow: auto; }
.modal-content {
height: 320px; }
.modal-body .list-group-item {
display: flex;
align-items: center;
border-left: none;
border-right: none;
border-radius: 0;
border-color: #eee;
padding: 12px 0; }
.modal-body .list-group-item > .tick {
font-size: 11px; }
.modal-body .list-group-item-heading {
font-size: 13px; }
.modal-body .list-group-item-text {
line-height: 1;
font-size: 11px;
color: #aaa; }
.modal-body .list-group-item :first-child {
flex-grow: 1; }
.modal-body .list-group-item:first-child {
border-top: none; }
.modal-open {
padding-right: 0 !important; }
.modal-footer {
flex-grow: 0;
border: 0; }
.modal .btn-primary[disabled], .modal [disabled].progress-button--primary {
background-color: #d5d5d5; }
/*
* 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.
*/
.alert-ribbon {
width: 60%;
position: fixed;
left: 0;
right: 0;
margin: 0 auto;
border-top-left-radius: 0;
border-top-right-radius: 0;
top: -100%;
transition: top 0.5s; }
.alert-ribbon > .glyphicon:first-child, .alert-ribbon > .tick:first-child {
margin-right: 5px; }
.alert-ribbon > .glyphicon:last-child, .alert-ribbon > .tick:last-child {
margin-left: 5px; }
.alert-ribbon .btn-link {
padding: 0;
font-size: inherit;
vertical-align: baseline;
border-radius: 0;
border-bottom: 1px solid; }
.alert-ribbon.alert-warning .btn-link {
border-color: #f7dbc3;
color: #fff; }
.alert-ribbon.alert-warning .btn-link:hover {
color: #e6e6e6;
border-color: #f2c096; }
.alert-ribbon--open {
top: 0; }
.page-settings .checkbox input[type="checkbox"]:not(:checked) + * {
color: #ddd; }
.icon-caption {
margin-top: 10px; }

View File

@ -40,6 +40,7 @@ const app = angular.module('Etcher', [
require('./components/progress-button/progress-button'),
require('./components/drive-selector/drive-selector'),
require('./components/svg-icon/svg-icon'),
require('./components/update-notifier/update-notifier'),
// Pages
require('./pages/finish/finish'),
@ -85,6 +86,7 @@ app.controller('AppController', function(
ImageWriterService,
AnalyticsService,
DriveSelectorService,
UpdateNotifierService,
OSWindowProgressService,
OSNotificationService,
OSDialogService
@ -96,6 +98,24 @@ app.controller('AppController', function(
this.settings = SettingsModel.data;
this.success = true;
if (UpdateNotifierService.shouldCheckForUpdates()) {
AnalyticsService.logEvent('Checking for updates');
UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) {
// In case the internet connection is not good and checking the
// latest published version takes too long, only show notify
// the user about the new version if he didn't start the flash
// process (e.g: selected an image), otherwise such interruption
// might be annoying.
if (!isLatestVersion && !SelectionStateModel.hasImage()) {
AnalyticsService.logEvent('Notifying update');
UpdateNotifierService.notify();
}
});
}
// This catches the case where the user enters
// the settings screen when a flash finished
// and goes back to the main screen with the back button.

View File

@ -0,0 +1,47 @@
/*
* 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($uibModalInstance, SettingsModel) {
// We update this value in this controller since its the only place
// where we can be sure the modal was really presented to the user.
// If the controller is instantiated, means the modal was shown.
// Compare that to `UpdateNotifierService.notify()`, which could
// have been called, but the modal could have failed to be shown.
SettingsModel.data.lastUpdateNotify = Date.now();
/**
* @summary Settings data
* @type Object
* @public
*/
this.settings = SettingsModel.data;
/**
* @summary Close the modal
* @function
* @public
*
* @example
* UpdateNotifierController.closeModal();
*/
this.closeModal = function() {
return $uibModalInstance.dismiss();
};
};

View File

@ -0,0 +1,70 @@
/*
* 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');
const semver = require('semver');
const xml = require('xml2js');
module.exports = function($q, $http, UPDATE_NOTIFIER_URL) {
/**
* @summary Get the latest published Etcher version
* @function
* @public
*
* @description
* This function performs its job by querying the publicily accessible
* S3 bucket where we store the builds and uses the `node-semver` module
* to determine which is the latest one.
*
* @fulfil {String} - latest version
* @returns {Promise}
*
* @example
* UpdateNotifierS3Service.getLatestVersion().then(function(latestVersion) {
* console.log('The latest version is: ' + latestVersion);
* });
*/
this.getLatestVersion = function() {
return $http.get(UPDATE_NOTIFIER_URL).then(function(response) {
return $q(function(resolve, reject) {
xml.parseString(response.data, function(error, result) {
if (error) {
return reject(error);
}
const bucketEntries = result.ListBucketResult.Contents;
return resolve(_.reduce(bucketEntries, function(latest, entry) {
const version = _.chain(entry.Key)
.first()
.split('/')
.nth(1)
.value();
return semver.gt(version, latest) ? version : latest;
// This is a good accumulator default value since
// every version is semantically greater than this.
}, '0.0.0'));
});
});
});
};
};

View File

@ -0,0 +1,90 @@
/*
* 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 semver = require('semver');
module.exports = function($uibModal, UPDATE_NOTIFIER_SLEEP_TIME, ManifestBindService, UpdateNotifierS3Service, SettingsModel) {
/**
* @summary Check if the current version is the latest version
* @function
* @public
*
* @fulfil {Boolean} - is latest version
* @returns {Promise}
*
* @example
* UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) {
* if (!isLatestVersion) {
* console.log('There is an update available');
* }
* });
*/
this.isLatestVersion = function() {
return UpdateNotifierS3Service.getLatestVersion().then(function(version) {
return semver.gte(ManifestBindService.get('version'), version);
});
};
/**
* @summary Determine if its time to check for updates
* @function
* @public
*
* @returns {Boolean} should check for updates
*
* @example
* if (UpdateNotifierService.shouldCheckForUpdates()) {
* console.log('We should check for updates!');
* }
*/
this.shouldCheckForUpdates = function() {
const lastUpdateNotify = SettingsModel.data.lastUpdateNotify;
if (!SettingsModel.data.sleepUpdateCheck || !lastUpdateNotify) {
return true;
}
if (lastUpdateNotify - Date.now() > UPDATE_NOTIFIER_SLEEP_TIME) {
SettingsModel.data.sleepUpdateCheck = false;
return true;
}
return false;
};
/**
* @summary Open the update notifier widget
* @function
* @public
*
* @returns {Promise}
*
* @example
* UpdateNotifierService.notify();
*/
this.notify = function() {
return $uibModal.open({
animation: true,
templateUrl: './components/update-notifier/templates/update-notifier-modal.tpl.html',
controller: 'UpdateNotifierController as modal',
size: 'fat-and-short'
}).result;
};
};

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.
*/
.update-notifier-modal-body {
padding: 30px 35px;
}
.update-notifier-modal-body__content {
@extend .text-center;
padding-bottom: 15px;
margin-bottom: 25px;
border-bottom: 1px solid $gray-lighter;
}
.update-notifier-modal-body__title {
margin-bottom: 15px;
}
.update-notifier-modal-body__menu {
display: flex;
justify-content: center;
> .btn {
flex-grow: 1;
// This causes flex children buttons to be
// equally resized independently of the
// button text length
width: 0;
}
> .btn + .btn {
margin-left: 10px;
}
}
.update-notifier-modal-body .checkbox {
color: lighten($gray, 25%);
font-size: 11px;
}

View File

@ -0,0 +1,20 @@
<div class="modal-body update-notifier-modal-body">
<div class="update-notifier-modal-body__content">
<h4 class="update-notifier-modal-body__title">New Update Available!</h4>
<p>A new version of Etcher is available for download</p>
</div>
<div class="update-notifier-modal-body__menu">
<button class="btn btn-primary"
os-open-external="http://etcher.io">DOWNLOAD</button>
<button class="btn btn-default"
ng-click="modal.closeModal()">SKIP</button>
</div>
<div class="checkbox text-right">
<label>
<input type="checkbox" ng-model="modal.settings.sleepUpdateCheck">
<span>Remind me again in 7 days</span>
</label>
</div>
</div>

View File

@ -0,0 +1,38 @@
/*
* 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 Etcher.Components.UpdateNotifier
*/
const angular = require('angular');
const MODULE_NAME = 'Etcher.Components.UpdateNotifier';
const UpdateNotifier = angular.module(MODULE_NAME, [
require('angular-ui-bootstrap'),
require('../../models/settings'),
require('../../utils/manifest-bind/manifest-bind'),
require('../../os/open-external/open-external')
]);
UpdateNotifier.constant('UPDATE_NOTIFIER_URL', 'https://resin-production-downloads.s3.amazonaws.com');
UpdateNotifier.constant('UPDATE_NOTIFIER_SLEEP_TIME', 7 * 24 * 60 * 60 * 100);
UpdateNotifier.controller('UpdateNotifierController', require('./controllers/update-notifier'));
UpdateNotifier.service('UpdateNotifierService', require('./services/update-notifier'));
UpdateNotifier.service('UpdateNotifierS3Service', require('./services/update-notifier-s3'));
module.exports = MODULE_NAME;

View File

@ -37,7 +37,8 @@ SettingsModel.service('SettingsModel', function($localStorage) {
this.data = $localStorage.$default({
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true
validateWriteOnSuccess: true,
sleepUpdateCheck: false
});
});

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
.page-settings .checkbox input[type="checkbox"]:not(:checked) + * {
color: $gray-light;
}

View File

@ -1,4 +1,4 @@
<div class="text-left">
<div class="page-settings text-left">
<h1 class="space-bottom-large">Settings</h1>
<div class="checkbox">

View File

@ -93,6 +93,18 @@
padding-right: 0 !important;
}
.modal-fat-and-short {
width: 400px;
// Move it a bit to the top for
// aesthetic reasons
margin-top: -10px;
.modal-content {
height: 245px;
}
}
.modal-footer {
flex-grow: 0;
border: 0;
@ -101,3 +113,18 @@
.modal .btn-primary[disabled] {
background-color: darken($gray-lighter, 10%);
}
// Center the modal using Flexbox so we can
// freely use any height.
.modal {
display: flex !important;
justify-content: center;
align-items: center;
}
.modal-dialog {
margin: 0;
position: initial;
}

View File

@ -41,10 +41,12 @@ $alert-padding: 13px;
@import "./components/caption";
@import "./components/button";
@import "./components/tick";
@import "../components/progress-button/styles/progress-button";
@import "../components/svg-icon/styles/svg-icon";
@import "./components/modal";
@import "./components/alert-ribbon";
@import "../components/update-notifier/styles/update-notifier";
@import "../components/progress-button/styles/progress-button";
@import "../components/svg-icon/styles/svg-icon";
@import "../pages/settings/styles/settings";
.icon-caption {
@extend .caption;

View File

@ -28,10 +28,6 @@ html {
margin-right: 2px;
}
.checkbox input[type="checkbox"]:not(:checked) + * {
color: $gray-light;
}
// Disable modal opacity
.modal-backdrop.in {
opacity: 0;

View File

@ -16,9 +16,6 @@
'use strict';
const _ = require('lodash');
const packageJSON = require('../../../../../package.json');
/**
* @summary ManifestBind directive
* @function
@ -28,17 +25,18 @@ const packageJSON = require('../../../../../package.json');
* This directive provides an attribute to bind the current
* element value to a property in `package.json`.
*
* @param {Object} ManifestBindService - ManifestBindService
* @returns {Object} directive
*
* @example
* <span manifest-bind="version"></button>
*/
module.exports = function() {
module.exports = function(ManifestBindService) {
return {
restrict: 'A',
scope: false,
link: function(scope, element, attributes) {
const value = _.get(packageJSON, attributes.manifestBind);
const value = ManifestBindService.get(attributes.manifestBind);
if (!value) {
throw new Error('ManifestBind: Unknown property `' + attributes.manifestBind + '`');

View File

@ -27,6 +27,7 @@
const angular = require('angular');
const MODULE_NAME = 'Etcher.Utils.ManifestBind';
const ManifestBind = angular.module(MODULE_NAME, []);
ManifestBind.service('ManifestBindService', require('./services/manifest-bind'));
ManifestBind.directive('manifestBind', require('./directives/manifest-bind'));
module.exports = MODULE_NAME;

View File

@ -0,0 +1,39 @@
/*
* 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');
const packageJSON = require('../../../../../package.json');
module.exports = function() {
/**
* @summary Get a package.json property
* @function
* @public
*
* @param {String} attribute - attribute
* @returns {*} property value
*
* @example
* const version = ManifestBindService.get('version');
*/
this.get = function(attribute) {
return _.get(packageJSON, attribute);
};
};

View File

@ -65,10 +65,12 @@
"resin-cli-errors": "^1.2.0",
"resin-cli-form": "^1.4.1",
"resin-cli-visuals": "^1.2.8",
"semver": "^5.1.0",
"sudo-prompt": "^3.1.0",
"trackjs": "^2.1.16",
"umount": "^1.1.3",
"username": "^2.1.0",
"xml2js": "^0.4.16",
"yargs": "^4.6.0"
},
"devDependencies": {

View File

@ -0,0 +1,325 @@
'use strict';
const m = require('mochainon');
const angular = require('angular');
require('angular-mocks');
describe('Browser: UpdateNotifier', function() {
beforeEach(angular.mock.module(
require('../../../lib/gui/components/update-notifier/update-notifier')
));
describe('UpdateNotifierService', function() {
describe('.shouldCheckForUpdates()', function() {
let UpdateNotifierService;
let SettingsModel;
let UPDATE_NOTIFIER_SLEEP_TIME;
beforeEach(angular.mock.inject(function(_UpdateNotifierService_, _SettingsModel_, _UPDATE_NOTIFIER_SLEEP_TIME_) {
UpdateNotifierService = _UpdateNotifierService_;
SettingsModel = _SettingsModel_;
UPDATE_NOTIFIER_SLEEP_TIME = _UPDATE_NOTIFIER_SLEEP_TIME_;
}));
describe('given the `sleepUpdateCheck` is disabled', function() {
beforeEach(function() {
SettingsModel.data.sleepUpdateCheck = false;
});
it('should return true', function() {
const result = UpdateNotifierService.shouldCheckForUpdates();
m.chai.expect(result).to.be.true;
});
});
describe('given the `sleepUpdateCheck` is enabled', function() {
beforeEach(function() {
SettingsModel.data.sleepUpdateCheck = true;
});
describe('given the `lastUpdateNotify` was never updated', function() {
beforeEach(function() {
SettingsModel.data.lastUpdateNotify = undefined;
});
it('should return true', function() {
const result = UpdateNotifierService.shouldCheckForUpdates();
m.chai.expect(result).to.be.true;
});
});
describe('given the `lastUpdateNotify` was very recently updated', function() {
beforeEach(function() {
SettingsModel.data.lastUpdateNotify = Date.now() + 1000;
});
it('should return false', function() {
const result = UpdateNotifierService.shouldCheckForUpdates();
m.chai.expect(result).to.be.false;
});
});
describe('given the `lastUpdateNotify` was updated long ago', function() {
beforeEach(function() {
SettingsModel.data.lastUpdateNotify = Date.now() + UPDATE_NOTIFIER_SLEEP_TIME + 1;
});
it('should return true', function() {
const result = UpdateNotifierService.shouldCheckForUpdates();
m.chai.expect(result).to.be.true;
});
it('should unset the `sleepUpdateCheck` setting', function() {
m.chai.expect(SettingsModel.data.sleepUpdateCheck).to.be.true;
UpdateNotifierService.shouldCheckForUpdates();
m.chai.expect(SettingsModel.data.sleepUpdateCheck).to.be.false;
});
});
});
});
describe('.isLatestVersion()', function() {
describe('given the latest version is equal to the current version', function() {
let $q;
let $rootScope;
let UpdateNotifierService;
let ManifestBindService;
beforeEach(function() {
angular.mock.module(function($provide) {
$provide.value('UpdateNotifierS3Service', {
getLatestVersion: function() {
return $q.resolve(ManifestBindService.get('version'));
}
});
});
});
beforeEach(angular.mock.inject(function(_$q_, _$rootScope_, _UpdateNotifierService_, _ManifestBindService_) {
$q = _$q_;
$rootScope = _$rootScope_;
UpdateNotifierService = _UpdateNotifierService_;
ManifestBindService = _ManifestBindService_;
}));
it('should resolve true', function() {
let result = null;
UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) {
result = isLatestVersion;
});
$rootScope.$apply();
m.chai.expect(result).to.be.true;
});
});
describe('given the latest version is greater than the current version', function() {
let $q;
let $rootScope;
let UpdateNotifierService;
beforeEach(function() {
angular.mock.module(function($provide) {
$provide.value('UpdateNotifierS3Service', {
getLatestVersion: function() {
return $q.resolve('99999.9.9');
}
});
});
});
beforeEach(angular.mock.inject(function(_$q_, _$rootScope_, _UpdateNotifierService_) {
$q = _$q_;
$rootScope = _$rootScope_;
UpdateNotifierService = _UpdateNotifierService_;
}));
it('should resolve false', function() {
let result = null;
UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) {
result = isLatestVersion;
});
$rootScope.$apply();
m.chai.expect(result).to.be.false;
});
});
describe('given the latest version is less than the current version', function() {
let $q;
let $rootScope;
let UpdateNotifierService;
beforeEach(function() {
angular.mock.module(function($provide) {
$provide.value('UpdateNotifierS3Service', {
getLatestVersion: function() {
return $q.resolve('0.0.0');
}
});
});
});
beforeEach(angular.mock.inject(function(_$q_, _$rootScope_, _UpdateNotifierService_) {
$q = _$q_;
$rootScope = _$rootScope_;
UpdateNotifierService = _UpdateNotifierService_;
}));
it('should resolve true', function() {
let result = null;
UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) {
result = isLatestVersion;
});
$rootScope.$apply();
m.chai.expect(result).to.be.true;
});
});
});
});
describe('UpdateNotifierS3Service', function() {
let UpdateNotifierS3Service;
let $rootScope;
beforeEach(angular.mock.inject(function(_$rootScope_, _UpdateNotifierS3Service_) {
$rootScope = _$rootScope_;
UpdateNotifierS3Service = _UpdateNotifierS3Service_;
}));
describe('given a mocked S3 XML response', function() {
let $httpBackend;
let UPDATE_NOTIFIER_URL;
beforeEach(angular.mock.inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
UPDATE_NOTIFIER_URL = $injector.get('UPDATE_NOTIFIER_URL');
$httpBackend.whenGET(UPDATE_NOTIFIER_URL).respond(`
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>resin-production-downloads</Name>
<Prefix/>
<Marker/>
<MaxKeys>1000</MaxKeys>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>etcher/1.0.0-beta.0/Etcher-darwin-x64.dmg</Key>
<LastModified>2016-03-10T17:34:21.000Z</LastModified>
<ETag>"5a715255aa25686688bf1e23bc1d3fc6"</ETag>
<Size>46109720</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.1/Etcher-darwin-x64.dmg</Key>
<LastModified>2016-04-08T20:12:03.000Z</LastModified>
<ETag>"cc1d6d9d53385e3edd099416fcd894c1"</ETag>
<Size>47071474</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.2/Etcher-darwin-x64.dmg</Key>
<LastModified>2016-04-08T19:03:18.000Z</LastModified>
<ETag>"5f1849f7781197ce2ee6129c16bcd498"</ETag>
<Size>48650090</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.3/Etcher-darwin-x64.dmg</Key>
<LastModified>2016-04-18T01:32:09.000Z</LastModified>
<ETag>"c173895886f44d115c66e7206ce3dff8"</ETag>
<Size>50585335</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.3/Etcher-darwin-x64.zip</Key>
<LastModified>2016-04-18T01:42:37.000Z</LastModified>
<ETag>"e9f6e957e65373b232530215d98df141"</ETag>
<Size>129327442</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.4/Etcher-darwin-x64.dmg</Key>
<LastModified>2016-04-22T17:29:49.000Z</LastModified>
<ETag>"bccb0024c58747a9b7516cbdfc5a7ecb"</ETag>
<Size>55240852</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.4/Etcher-darwin-x64.zip</Key>
<LastModified>2016-04-22T17:43:27.000Z</LastModified>
<ETag>"c93e26e68b3c4f2b7e8e88e6befc8e64"</ETag>
<Size>135443284</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.5/Etcher-darwin-x64.dmg</Key>
<LastModified>2016-05-04T08:27:11.000Z</LastModified>
<ETag>"fb596bfdb8bbaf09807b5fc4a940ce14"</ETag>
<Size>77757305</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>etcher/1.0.0-beta.5/Etcher-darwin-x64.zip</Key>
<LastModified>2016-05-04T08:39:56.000Z</LastModified>
<ETag>"3f11c1b6f06644f9ceb2aea4b1947fdf"</ETag>
<Size>157933876</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
</ListBucketResult>
`);
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should resolve the latest version', function() {
$httpBackend.expectGET(UPDATE_NOTIFIER_URL);
let latestVersion = null;
UpdateNotifierS3Service.getLatestVersion().then(function(result) {
latestVersion = result;
});
$rootScope.$apply();
$httpBackend.flush();
m.chai.expect(latestVersion).to.equal('1.0.0-beta.5');
});
});
});
});

View File

@ -35,6 +35,31 @@ describe('Browser: ManifestBind', function() {
$rootScope = _$rootScope_;
}));
describe('ManifestBindService', function() {
let ManifestBindService;
beforeEach(angular.mock.inject(function(_ManifestBindService_) {
ManifestBindService = _ManifestBindService_;
}));
it('should be able to fetch top level properties', function() {
const value = ManifestBindService.get('version');
m.chai.expect(value).to.equal(packageJSON.version);
});
it('should be able to fetch nested properties', function() {
const value = ManifestBindService.get('repository.type');
m.chai.expect(value).to.equal(packageJSON.repository.type);
});
it('should return undefined if the property does not exist', function() {
const value = ManifestBindService.get('foo.bar');
m.chai.expect(value).to.be.undefined;
});
});
describe('manifestBind', function() {
it('should bind to top level properties', function() {