diff --git a/.eslintrc.yml b/.eslintrc.yml index affaa103be..e8e97312fa 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -9,9 +9,6 @@ rules: # Possible Errors - comma-dangle: - - error - - never no-cond-assign: - error no-console: @@ -59,6 +56,8 @@ rules: - error no-sparse-arrays: - error + no-template-curly-in-string: + - error no-unexpected-multiline: - error no-unreachable: @@ -93,8 +92,12 @@ rules: - error block-scoped-var: - error + class-methods-use-this: + - error complexity: - off + consistent-return: + - error curly: - error default-case: @@ -136,6 +139,8 @@ rules: - error no-floating-decimal: - error + no-global-assign: + - error no-implicit-coercion: - error no-implicit-globals: @@ -150,6 +155,8 @@ rules: - error no-loop-func: - error + no-magic-numbers: + - error no-multi-spaces: - error no-multi-str: @@ -166,12 +173,19 @@ rules: - error no-octal-escape: - error + no-param-reassign: + - error no-proto: - error no-redeclare: - error + no-restricted-properties: + - error + - property: __proto__ no-return-assign: - error + no-return-await: + - error no-script-url: - error no-self-assign: @@ -184,6 +198,8 @@ rules: - error no-unmodified-loop-condition: - error + no-unused-expressions: + - error no-unused-labels: - error no-useless-call: @@ -216,12 +232,18 @@ rules: # Variables + init-declarations: + - error + - always no-catch-shadow: - error no-delete-var: - error no-label-var: - error + no-restricted-globals: + - error + - event no-shadow: - error no-shadow-restricted-names: @@ -230,6 +252,8 @@ rules: - error no-undef-init: - error + no-undefined: + - error no-unused-vars: - error no-use-before-define: @@ -268,6 +292,13 @@ rules: - 1tbs camelcase: - error + capitalized-comments: + - error + - always + - ignoreConsecutiveComments: true + comma-dangle: + - error + - never comma-spacing: - error - before: false @@ -283,6 +314,12 @@ rules: - self eol-last: - error + func-call-spacing: + - error + - never + func-name-matching: + - error + - always func-names: - error - never @@ -291,6 +328,11 @@ rules: - expression id-blacklist: - error + id-length: + - error + - min: 2 + exceptions: + - "_" indent: - error - 2 @@ -304,6 +346,12 @@ rules: - error - before: true after: true + line-comment-position: + - error + - position: above + linebreak-style: + - error + - unix lines-around-comment: - error - beforeBlockComment: true @@ -316,6 +364,9 @@ rules: allowObjectEnd: false allowArrayStart: true allowArrayEnd: false + lines-around-directive: + - error + - always max-len: - error - code: 130 @@ -323,13 +374,20 @@ rules: ignoreComments: false ignoreTrailingComments: false ignoreUrls: true + max-params: + - off max-statements-per-line: - error - max: 1 + multiline-ternary: + - error + - never new-cap: - error new-parens: - error + newline-per-chained-call: + - off no-array-constructor: - error no-bitwise: @@ -338,10 +396,14 @@ rules: - error no-inline-comments: - error + no-lonely-if: + - error no-mixed-operators: - error no-mixed-spaces-and-tabs: - error + no-multi-assign: + - error no-multiple-empty-lines: - error - max: 1 @@ -355,8 +417,14 @@ rules: - error no-plusplus: - error + no-restricted-syntax: + - error + - WithStatement + - ForInStatement no-spaced-func: - error + no-tabs: + - error no-trailing-spaces: - error no-underscore-dangle: @@ -374,6 +442,9 @@ rules: - always object-property-newline: - error + one-var-declaration-per-line: + - error + - always one-var: - error - never @@ -383,6 +454,9 @@ rules: operator-linebreak: - error - before + padded-blocks: + - error + - classes: always quote-props: - error - as-needed @@ -395,6 +469,7 @@ rules: FunctionDeclaration: true ClassDeclaration: true MethodDefinition: true + ArrowFunctionExpression: true semi: - error - always @@ -412,9 +487,16 @@ rules: - never space-infix-ops: - error + space-unary-ops: + - error + - words: true + nonwords: false spaced-comment: - error - always + template-tag-spacing: + - error + - always unicode-bom: - error @@ -458,12 +540,24 @@ rules: - error no-var: - error + object-shorthand: + - error + - always prefer-const: - error prefer-reflect: - error prefer-spread: - error + prefer-numeric-literals: + - error + prefer-rest-params: + - error + prefer-template: + - error + prefer-arrow-callback: + - error + - allowNamedFunctions: false require-yield: - error rest-spread-spacing: @@ -471,6 +565,8 @@ rules: template-curly-spacing: - error - never + symbol-description: + - error yield-star-spacing: - error - before: true diff --git a/lib/child-writer/cli.js b/lib/child-writer/cli.js index aff0f93f27..8c544fac81 100644 --- a/lib/child-writer/cli.js +++ b/lib/child-writer/cli.js @@ -47,7 +47,8 @@ exports.getBooleanArgumentForm = (argumentName, value) => { return '--no-'; } - if (_.size(argumentName) === 1) { + const SHORT_OPTION_LENGTH = 1; + if (_.size(argumentName) === SHORT_OPTION_LENGTH) { return '-'; } diff --git a/lib/child-writer/index.js b/lib/child-writer/index.js index bfa82d45ac..2b3482014c 100644 --- a/lib/child-writer/index.js +++ b/lib/child-writer/index.js @@ -61,7 +61,7 @@ exports.write = (image, drive, options) => { const argv = cli.getArguments({ entryPoint: rendererUtils.getApplicationEntryPoint(), - image: image, + image, device: drive.device, validateWriteOnSuccess: options.validateWriteOnSuccess, unmountOnSuccess: options.unmountOnSuccess @@ -77,6 +77,14 @@ exports.write = (image, drive, options) => { ipc.config.silent = true; ipc.serve(); + /** + * @summary Safely terminate the IPC server + * @function + * @private + * + * @example + * terminateServer(); + */ const terminateServer = () => { // Turns out we need to destroy all sockets for @@ -90,6 +98,16 @@ exports.write = (image, drive, options) => { ipc.server.stop(); }; + /** + * @summary Emit an error to the client + * @function + * @private + * + * @param {Error} error - error + * + * @example + * emitError(new Error('foo bar')); + */ const emitError = (error) => { terminateServer(); emitter.emit('error', error); @@ -97,7 +115,7 @@ exports.write = (image, drive, options) => { ipc.server.on('error', emitError); ipc.server.on('message', (data) => { - let message; + let message = null; try { message = robot.parseMessage(data); @@ -112,7 +130,7 @@ exports.write = (image, drive, options) => { return emitError(robot.recomposeErrorMessage(message)); } - emitter.emit(robot.getCommand(message), robot.getData(message)); + return emitter.emit(robot.getCommand(message), robot.getData(message)); }); ipc.server.on('start', () => { @@ -144,9 +162,13 @@ exports.write = (image, drive, options) => { }); } - if (code !== EXIT_CODES.SUCCESS && code !== EXIT_CODES.VALIDATION_ERROR) { - return emitError(new Error(`Child process exited with error code: ${code}`)); + // We shouldn't emit the `done` event manually here + // since the writer process will take care of it. + if (code === EXIT_CODES.SUCCESS || code === EXIT_CODES.VALIDATION_ERROR) { + return null; } + + return emitError(new Error(`Child process exited with error code: ${code}`)); }); }); diff --git a/lib/child-writer/renderer-utils.js b/lib/child-writer/renderer-utils.js index 050034f9fc..dfa1e56f94 100644 --- a/lib/child-writer/renderer-utils.js +++ b/lib/child-writer/renderer-utils.js @@ -40,9 +40,12 @@ exports.getApplicationEntryPoint = () => { return path.join(process.resourcesPath, 'app.asar'); } + const ENTRY_POINT_ARGV_INDEX = 1; + const relativeEntryPoint = electron.remote.process.argv[ENTRY_POINT_ARGV_INDEX]; + // On GNU/Linux, `pkexec` resolves relative paths // from `/root`, therefore we pass an absolute path, // in order to be on the safe side. - return path.join(CONSTANTS.PROJECT_ROOT, electron.remote.process.argv[1]); + return path.join(CONSTANTS.PROJECT_ROOT, relativeEntryPoint); }; diff --git a/lib/child-writer/writer-proxy.js b/lib/child-writer/writer-proxy.js index 6377721d71..426c2e095a 100644 --- a/lib/child-writer/writer-proxy.js +++ b/lib/child-writer/writer-proxy.js @@ -37,8 +37,32 @@ const packageJSON = require('../../package.json'); // and `stderr` to the parent process using IPC communication, // taking care of the writer elevation as needed. -const EXECUTABLE = process.argv[0]; -const ETCHER_ARGUMENTS = process.argv.slice(2); +/** + * @summary The Etcher executable file path + * @constant + * @private + * @type {String} + */ +const executable = _.first(process.argv); + +/** + * @summary The first index that represents an actual option argument + * @constant + * @private + * @type {Number} + * + * @description + * The first arguments are usually the program executable itself, etc. + */ +const OPTIONS_INDEX_START = 2; + +/** + * @summary The list of Etcher argument options + * @constant + * @private + * @type {String[]} + */ +const etcherArguments = process.argv.slice(OPTIONS_INDEX_START); return isElevated().then((elevated) => { @@ -153,7 +177,7 @@ return isElevated().then((elevated) => { ipc.of[process.env.IPC_SERVER_ID].on('error', reject); ipc.of[process.env.IPC_SERVER_ID].on('connect', () => { - const child = childProcess.spawn(EXECUTABLE, ETCHER_ARGUMENTS, { + const child = childProcess.spawn(executable, etcherArguments, { env: { // The CLI might call operating system utilities (like `diskutil`), @@ -169,6 +193,18 @@ return isElevated().then((elevated) => { child.on('error', reject); child.on('close', resolve); + /** + * @summary Emit an object message to the IPC server + * @function + * @private + * + * @param {Buffer} data - json message data + * + * @example + * emitMessage(Buffer.from(JSON.stringify({ + * foo: 'bar' + * }))); + */ const emitMessage = (data) => { // Output from stdout/stderr coming from the CLI might be buffered, diff --git a/lib/cli/errors.js b/lib/cli/errors.js index c791e7fc4b..c336281f04 100644 --- a/lib/cli/errors.js +++ b/lib/cli/errors.js @@ -108,7 +108,7 @@ exports.getErrorMessage = (error) => { }); if (error.description) { - return message + '\n\n' + error.description; + return `${message}\n\n${error.description}`; } return message; diff --git a/lib/cli/etcher.js b/lib/cli/etcher.js index f823b0593a..876d731d76 100644 --- a/lib/cli/etcher.js +++ b/lib/cli/etcher.js @@ -29,6 +29,9 @@ const robot = require('../shared/robot'); const messages = require('../shared/messages'); const EXIT_CODES = require('../shared/exit-codes'); +const ARGV_IMAGE_PATH_INDEX = 0; +const imagePath = options._[ARGV_IMAGE_PATH_INDEX]; + isElevated().then((elevated) => { if (!elevated) { throw new Error(messages.error.elevationRequired()); @@ -50,10 +53,10 @@ isElevated().then((elevated) => { override: { drive: options.drive, - // If `options.yes` is `false`, pass `undefined`, + // If `options.yes` is `false`, pass `null`, // otherwise the question will not be asked because // `false` is a defined value. - yes: robot.isEnabled(process.env) || options.yes || undefined + yes: robot.isEnabled(process.env) || options.yes || null } }); @@ -76,7 +79,7 @@ isElevated().then((elevated) => { throw new Error(`Drive not found: ${answers.drive}`); } - return writer.writeImage(options._[0], selectedDrive, { + return writer.writeImage(imagePath, selectedDrive, { unmountOnSuccess: options.unmount, validateWriteOnSuccess: options.check }, (state) => { @@ -109,6 +112,7 @@ isElevated().then((elevated) => { console.log(`Checksum: ${results.sourceChecksum}`); } + return Bluebird.resolve(); }).then(() => { process.exit(EXIT_CODES.SUCCESS); }); @@ -121,6 +125,7 @@ isElevated().then((elevated) => { } errors.print(error); + return Bluebird.resolve(); }).then(() => { if (error.code === 'EVALIDATION') { process.exit(EXIT_CODES.VALIDATION_ERROR); diff --git a/lib/cli/options.js b/lib/cli/options.js index cd9016d377..85af5cd59e 100644 --- a/lib/cli/options.js +++ b/lib/cli/options.js @@ -24,6 +24,33 @@ const robot = require('../shared/robot'); const EXIT_CODES = require('../shared/exit-codes'); const packageJSON = require('../../package.json'); +/** + * @summary The minimum required number of CLI arguments + * @constant + * @private + * @type {Number} + */ +const MINIMUM_NUMBER_OF_ARGUMENTS = 1; + +/** + * @summary The index of the image argument + * @constant + * @private + * @type {Number} + */ +const IMAGE_PATH_ARGV_INDEX = 0; + +/** + * @summary The first index that represents an actual option argument + * @constant + * @private + * @type {Number} + * + * @description + * The first arguments are usually the program executable itself, etc. + */ +const OPTIONS_INDEX_START = 2; + /** * @summary Parsed CLI options and arguments * @type {Object} @@ -34,7 +61,7 @@ module.exports = yargs // Don't wrap at all .wrap(null) - .demand(1, 'Missing image') + .demand(MINIMUM_NUMBER_OF_ARGUMENTS, 'Missing image') // Usage help .usage('Usage: $0 [options] ') @@ -42,7 +69,7 @@ module.exports = yargs 'Exit codes:', _.map(EXIT_CODES, (value, key) => { const reason = _.map(_.split(key, '_'), _.capitalize).join(' '); - return ' ' + value + ' - ' + reason; + return ` ${value} - ${reason}`; }).join('\n'), '', 'If you need help, don\'t hesitate in contacting us at:', @@ -64,7 +91,7 @@ module.exports = yargs .version(_.constant(packageJSON.version)) // Error reporting - .fail(function(message, error) { + .fail((message, error) => { if (robot.isEnabled(process.env)) { robot.printError(error || message); } else { @@ -72,12 +99,12 @@ module.exports = yargs errors.print(error || message); } - process.exit(1); + process.exit(EXIT_CODES.GENERAL_ERROR); }) // Assert that image exists .check((argv) => { - fs.accessSync(argv._[0]); + fs.accessSync(argv._[IMAGE_PATH_ARGV_INDEX]); return true; }) @@ -123,4 +150,4 @@ module.exports = yargs default: true } }) - .parse(process.argv.slice(2)); + .parse(process.argv.slice(OPTIONS_INDEX_START)); diff --git a/lib/cli/writer.js b/lib/cli/writer.js index 1539c1027e..8697754379 100644 --- a/lib/cli/writer.js +++ b/lib/cli/writer.js @@ -59,7 +59,7 @@ exports.writeImage = (imagePath, drive, options, onProgress) => { // Unmounting a drive in Windows means we can't write to it anymore if (os.platform() === 'win32') { - return; + return Bluebird.resolve(); } return unmount.unmountDrive(drive); @@ -96,7 +96,7 @@ exports.writeImage = (imagePath, drive, options, onProgress) => { return fs.closeAsync(driveFileDescriptor).then(() => { if (!options.unmountOnSuccess) { - return; + return Bluebird.resolve(); } return unmount.unmountDrive(drive); diff --git a/lib/gui/app.js b/lib/gui/app.js index cabc0514f8..e5da33c920 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -24,6 +24,7 @@ var angular = require('angular'); const electron = require('electron'); +const Bluebird = require('bluebird'); const EXIT_CODES = require('../shared/exit-codes'); const messages = require('../shared/messages'); @@ -98,6 +99,8 @@ app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateMo AnalyticsService.logEvent('Notifying update'); return UpdateNotifierService.notify(); } + + return Bluebird.resolve(); }).catch(ErrorService.reportException); } @@ -158,7 +161,7 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe // Don't open any more popups popupExists = true; - return OSDialogService.showWarning({ + OSDialogService.showWarning({ confirmationLabel: 'Yes, quit', rejectionLabel: 'Cancel', title: 'Are you sure you want to close Etcher?', diff --git a/lib/gui/components/drive-selector/controllers/drive-selector.js b/lib/gui/components/drive-selector/controllers/drive-selector.js index ba447f9e28..42b3cf919d 100644 --- a/lib/gui/components/drive-selector/controllers/drive-selector.js +++ b/lib/gui/components/drive-selector/controllers/drive-selector.js @@ -82,7 +82,7 @@ module.exports = function( description: [ messages.warning.unrecommendedDriveSize({ image: SelectionStateModel.getImage(), - drive: drive + drive }), 'Are you sure you want to continue?' ].join(' ') diff --git a/lib/gui/components/modal/services/modal.js b/lib/gui/components/modal/services/modal.js index 5452607d00..31ceeabc87 100644 --- a/lib/gui/components/modal/services/modal.js +++ b/lib/gui/components/modal/services/modal.js @@ -59,18 +59,12 @@ module.exports = function($uibModal, $q) { .then(resolve) .catch((error) => { - // Bootstrap doesn't 'resolve' these but cancels the dialog; - // therefore call 'resolve' here applied to 'false'. + // Bootstrap doesn't 'resolve' these but cancels the dialog if (error === 'escape key press' || error === 'backdrop click') { - resolve(); - - // For some annoying reason, UI Bootstrap Modal rejects - // the result reason if the user clicks on the backdrop - // (e.g: the area surrounding the modal). - } else if (error !== 'backdrop click') { - return reject(error); + return resolve(); } + return reject(error); }); }) }; diff --git a/lib/gui/components/update-notifier/controllers/update-notifier.js b/lib/gui/components/update-notifier/controllers/update-notifier.js index 39425b6aac..80cb26d2b2 100644 --- a/lib/gui/components/update-notifier/controllers/update-notifier.js +++ b/lib/gui/components/update-notifier/controllers/update-notifier.js @@ -16,7 +16,7 @@ 'use strict'; -module.exports = function($uibModalInstance, SettingsModel, options) { +module.exports = function($uibModalInstance, SettingsModel, UPDATE_NOTIFIER_SLEEP_DAYS, options) { // 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. @@ -25,6 +25,14 @@ module.exports = function($uibModalInstance, SettingsModel, options) { // have been called, but the modal could have failed to be shown. SettingsModel.set('lastUpdateNotify', Date.now()); + /** + * @summary The number of days the update notified can be put to sleep + * @constant + * @public + * @type {Number} + */ + this.sleepDays = UPDATE_NOTIFIER_SLEEP_DAYS; + /** * @summary Settings model * @type {Object} diff --git a/lib/gui/components/update-notifier/services/update-notifier.js b/lib/gui/components/update-notifier/services/update-notifier.js index ffafe4c543..38416e296b 100644 --- a/lib/gui/components/update-notifier/services/update-notifier.js +++ b/lib/gui/components/update-notifier/services/update-notifier.js @@ -19,8 +19,9 @@ const _ = require('lodash'); const semver = require('semver'); const etcherLatestVersion = require('etcher-latest-version'); +const units = require('../../../../shared/units'); -module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, ManifestBindService, SettingsModel) { +module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_DAYS, ManifestBindService, SettingsModel) { /** * @summary The current application version @@ -57,10 +58,12 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M }, (error, latestVersion) => { if (error) { - // The error status equals -1 if the request couldn't - // be made successfully, for example, because of a - // timeout on an unstable network connection. - if (error.status === -1) { + // The error status equals this number if the request + // couldn't be made successfuly, for example, because + // of a timeout on an unstable network connection. + const ERROR_CODE_UNSUCCESSFUL_REQUEST = -1; + + if (error.status === ERROR_CODE_UNSUCCESSFUL_REQUEST) { return resolve(CURRENT_VERSION); } @@ -114,7 +117,7 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M return true; } - if (lastUpdateNotify - Date.now() > UPDATE_NOTIFIER_SLEEP_TIME) { + if (lastUpdateNotify - Date.now() > units.daysToMilliseconds(UPDATE_NOTIFIER_SLEEP_DAYS)) { SettingsModel.set('sleepUpdateCheck', false); return true; } @@ -140,7 +143,7 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M size: 'update-notifier', resolve: { options: _.constant({ - version: version + version }) } }).result; diff --git a/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html b/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html index 0560bd3bd4..d3f587842b 100644 --- a/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html +++ b/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html @@ -11,7 +11,7 @@ - Remind me again in 7 days + Remind me again in {{::modal.sleepDays}} days diff --git a/lib/gui/components/update-notifier/update-notifier.js b/lib/gui/components/update-notifier/update-notifier.js index 9b37d9735d..f11b485632 100644 --- a/lib/gui/components/update-notifier/update-notifier.js +++ b/lib/gui/components/update-notifier/update-notifier.js @@ -29,7 +29,15 @@ const UpdateNotifier = angular.module(MODULE_NAME, [ require('../../os/open-external/open-external') ]); -UpdateNotifier.constant('UPDATE_NOTIFIER_SLEEP_TIME', 7 * 24 * 60 * 60 * 100); +/** + * @summary The number of days the update notifier can be put to sleep + * @constant + * @private + * @type {Number} + */ +const UPDATE_NOTIFIER_SLEEP_DAYS = 7; + +UpdateNotifier.constant('UPDATE_NOTIFIER_SLEEP_DAYS', UPDATE_NOTIFIER_SLEEP_DAYS); UpdateNotifier.controller('UpdateNotifierController', require('./controllers/update-notifier')); UpdateNotifier.service('UpdateNotifierService', require('./services/update-notifier')); diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js index 78daddb65c..81e94b74bc 100644 --- a/lib/gui/etcher.js +++ b/lib/gui/etcher.js @@ -41,12 +41,13 @@ electron.app.on('ready', () => { // Prevent flash of white when starting the application // https://github.com/atom/electron/issues/2172 mainWindow.webContents.on('did-finish-load', () => { + const WEBVIEW_LOAD_TIMEOUT_MS = 100; // The flash of white is still present for a very short // while after the WebView reports it finished loading setTimeout(() => { mainWindow.show(); - }, 100); + }, WEBVIEW_LOAD_TIMEOUT_MS); }); diff --git a/lib/gui/models/drives.js b/lib/gui/models/drives.js index 2608a51a56..0bd9ecbaa4 100644 --- a/lib/gui/models/drives.js +++ b/lib/gui/models/drives.js @@ -64,19 +64,36 @@ Drives.service('DrivesModel', function() { }); }; - // This workaround is needed to avoid AngularJS from getting - // caught in an infinite digest loop when using `ngRepeat` - // over a function that returns a mutable version of an - // ImmutableJS object. - // - // The problem is that every time you call `myImmutableObject.toJS()` - // you will get a new object, whose reference is different from - // the one you previously got, even if the data is exactly the same. + /** + * @summary Memoize ImmutableJS list reference + * @function + * @private + * + * @description + * This workaround is needed to avoid AngularJS from getting + * caught in an infinite digest loop when using `ngRepeat` + * over a function that returns a mutable version of an + * ImmutableJS object. + * + * The problem is that every time you call `myImmutableObject.toJS()` + * you will get a new object, whose reference is different from + * the one you previously got, even if the data is exactly the same. + * + * @param {Function} func - function that returns an ImmutableJS list + * @returns {Function} memoized function + * + * @example + * const getList = () => { + * return Store.getState().toJS().myList; + * }; + * + * const memoizedFunction = memoizeImmutableListReference(getList); + */ const memoizeImmutableListReference = (func) => { let previous = []; - return () => { - const list = Reflect.apply(func, this, arguments); + return (...args) => { + const list = Reflect.apply(func, this, args); if (!_.isEqual(list, previous)) { previous = list; diff --git a/lib/gui/models/flash-state.js b/lib/gui/models/flash-state.js index 56f8106b1b..5ecd2bcaa3 100644 --- a/lib/gui/models/flash-state.js +++ b/lib/gui/models/flash-state.js @@ -134,9 +134,12 @@ FlashState.service('FlashStateModel', function() { if (_.isNumber(state.speed) && !_.isNaN(state.speed)) { // Preserve only two decimal places - return Math.floor(units.bytesToMegabytes(state.speed) * 100) / 100; + const PRECISION = 2; + return _.round(units.bytesToMegabytes(state.speed), PRECISION); } + + return null; }) } }); diff --git a/lib/gui/models/selection-state.js b/lib/gui/models/selection-state.js index dd1adc0174..e1fdea9d12 100644 --- a/lib/gui/models/selection-state.js +++ b/lib/gui/models/selection-state.js @@ -286,9 +286,7 @@ SelectionStateModel.service('SelectionStateModel', function(DrivesModel) { * @example * SelectionStateModel.clear({ preserveImage: true }); */ - this.clear = (options) => { - options = options || {}; - + this.clear = (options = {}) => { if (!options.preserveImage) { Store.dispatch({ type: Store.Actions.REMOVE_IMAGE diff --git a/lib/gui/models/store.js b/lib/gui/models/store.js index 0dd8c574e0..72c691685e 100644 --- a/lib/gui/models/store.js +++ b/lib/gui/models/store.js @@ -75,9 +75,21 @@ const ACTIONS = _.fromPairs(_.map([ return [ message, message ]; })); -const storeReducer = (state, action) => { - state = state || DEFAULT_STATE; - +/** + * @summary The redux store reducer + * @function + * @private + * + * @param {Object} state - application state + * @param {Object} action - dispatched action + * @returns {Object} new application state + * + * @example + * const newState = storeReducer(DEFAULT_STATE, { + * type: ACTIONS.REMOVE_DRIVE + * }); + */ +const storeReducer = (state = DEFAULT_STATE, action) => { switch (action.type) { case ACTIONS.SET_AVAILABLE_DRIVES: { @@ -91,7 +103,9 @@ const storeReducer = (state, action) => { const newState = state.set('availableDrives', Immutable.fromJS(action.data)); - if (action.data.length === 1) { + const AUTOSELECT_DRIVE_COUNT = 1; + const numberOfDrives = action.data.length; + if (numberOfDrives === AUTOSELECT_DRIVE_COUNT) { const drive = _.first(action.data); @@ -150,7 +164,7 @@ const storeReducer = (state, action) => { throw new Error(`Invalid state percentage: ${action.data.percentage}`); } - if (!action.data.eta && action.data.eta !== 0) { + if (_.isNil(action.data.eta)) { throw new Error('Missing state eta'); } @@ -352,9 +366,9 @@ module.exports = _.merge(redux.createStore( // In the first run, there will be no information // to deserialize. In this case, we avoid merging, // otherwise we will be basically erasing the property - // we aim the keep serialising the in future. + // we aim to keep serialising the in future. if (!subset) { - return; + return state; } // Blindly setting the state to the deserialised subset diff --git a/lib/gui/models/supported-formats.js b/lib/gui/models/supported-formats.js index 8ac695b364..062355e517 100644 --- a/lib/gui/models/supported-formats.js +++ b/lib/gui/models/supported-formats.js @@ -115,7 +115,7 @@ SupportedFormats.service('SupportedFormatsModel', function() { * } */ this.isSupportedImage = (imagePath) => { - const extension = path.extname(imagePath).slice(1).toLowerCase(); + const extension = _.replace(path.extname(imagePath), '.', '').toLowerCase(); if (_.some([ _.includes(this.getNonCompressedExtensions(), extension), diff --git a/lib/gui/modules/analytics.js b/lib/gui/modules/analytics.js index b84eec30b2..5dc28f9546 100644 --- a/lib/gui/modules/analytics.js +++ b/lib/gui/modules/analytics.js @@ -93,13 +93,13 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting * AnalyticsService.log('Hello World'); */ this.logDebug = (message) => { - message = new Date() + ' ' + message; + const debugMessage = `${new Date()} ${message}`; if (SettingsModel.get('errorReporting') && isRunningInAsar()) { - $window.trackJs.console.debug(message); + $window.trackJs.console.debug(debugMessage); } - $log.debug(message); + $log.debug(debugMessage); }; /** @@ -119,7 +119,6 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting * }); */ this.logEvent = (message, data) => { - if (SettingsModel.get('errorReporting') && isRunningInAsar()) { // Clone data before passing it to `mixpanel.track` @@ -129,11 +128,15 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting } - if (data) { - message += ` (${JSON.stringify(data)})`; - } + const debugMessage = _.attempt(() => { + if (data) { + return `${message} (${JSON.stringify(data)})`; + } + + return message; + }); - this.logDebug(message); + this.logDebug(debugMessage); }; /** diff --git a/lib/gui/modules/drive-scanner.js b/lib/gui/modules/drive-scanner.js index e9a12809a1..386e542fe9 100644 --- a/lib/gui/modules/drive-scanner.js +++ b/lib/gui/modules/drive-scanner.js @@ -34,16 +34,20 @@ const driveScanner = angular.module(MODULE_NAME, [ driveScanner.factory('DriveScannerService', (SettingsModel) => { const DRIVE_SCANNER_INTERVAL_MS = 2000; + const DRIVE_SCANNER_FIRST_SCAN_DELAY_MS = 0; const emitter = new EventEmitter(); - const availableDrives = Rx.Observable.timer(0, DRIVE_SCANNER_INTERVAL_MS) + const availableDrives = Rx.Observable.timer( + DRIVE_SCANNER_FIRST_SCAN_DELAY_MS, + DRIVE_SCANNER_INTERVAL_MS + ) .flatMap(() => { return Rx.Observable.fromNodeCallback(drivelist.list)(); }) - .map((drives) => { - // Calculate an appropriate "display name" - drives = _.map(drives, (drive) => { + // Build human friendly "description" + .map((drives) => { + return _.map(drives, (drive) => { drive.name = drive.device; if (os.platform() === 'win32' && !_.isEmpty(drive.mountpoints)) { @@ -52,7 +56,9 @@ driveScanner.factory('DriveScannerService', (SettingsModel) => { return drive; }); + }) + .map((drives) => { if (SettingsModel.get('unsafeMode')) { return drives; } diff --git a/lib/gui/os/dialog/services/dialog.js b/lib/gui/os/dialog/services/dialog.js index 56fde26b35..d1887bf659 100644 --- a/lib/gui/os/dialog/services/dialog.js +++ b/lib/gui/os/dialog/services/dialog.js @@ -78,7 +78,7 @@ module.exports = function($q, SupportedFormatsModel) { return resolve(); } - imageStream.getImageMetadata(imagePath).then((metadata) => { + return imageStream.getImageMetadata(imagePath).then((metadata) => { metadata.path = imagePath; metadata.size = metadata.size.final.value; return resolve(metadata); @@ -95,8 +95,8 @@ module.exports = function($q, SupportedFormatsModel) { * @param {Object} options - options * @param {String} options.title - dialog title * @param {String} options.description - dialog description - * @param {String} options.confirmationLabel - confirmation label - * @param {String} options.rejectionLabel - rejection label + * @param {String} [options.confirmationLabel="OK"] - confirmation label + * @param {String} [options.rejectionLabel="Cancel"] - rejection label * @fulfil {Boolean} - whether the dialog was confirmed or not * @returns {Promise}; * @@ -113,20 +113,30 @@ module.exports = function($q, SupportedFormatsModel) { * }); */ this.showWarning = (options) => { + _.defaults(options, { + confirmationLabel: 'OK', + rejectionLabel: 'Cancel' + }); + + const BUTTONS = [ + options.confirmationLabel, + options.rejectionLabel + ]; + + const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, options.confirmationLabel); + const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel); + return $q((resolve) => { electron.remote.dialog.showMessageBox(currentWindow, { type: 'warning', - buttons: [ - options.confirmationLabel, - options.rejectionLabel - ], - defaultId: 1, - cancelId: 1, + buttons: BUTTONS, + defaultId: BUTTON_REJECTION_INDEX, + cancelId: BUTTON_REJECTION_INDEX, title: 'Attention', message: options.title, detail: options.description }, (response) => { - return resolve(response === 0); + return resolve(response === BUTTON_CONFIRMATION_INDEX); }); }); }; @@ -149,19 +159,19 @@ module.exports = function($q, SupportedFormatsModel) { * OSDialogService.showError('Foo Bar', 'An error happened!'); */ this.showError = (error, description) => { - error = error || {}; + const errorObject = error || {}; // Try to get as most information as possible about the error // rather than falling back to generic messages right away. const title = _.attempt(() => { - if (_.isString(error)) { - return error; + if (_.isString(errorObject)) { + return errorObject; } - return error.message || error.code || 'An error ocurred'; + return errorObject.message || errorObject.code || 'An error ocurred'; }); - const message = description || error.stack || JSON.stringify(error) || ''; + const message = description || errorObject.stack || JSON.stringify(errorObject) || ''; // Ensure the parameters are strings to prevent the following // types of obscure errors: diff --git a/lib/gui/os/dropzone/directives/dropzone.js b/lib/gui/os/dropzone/directives/dropzone.js index 85cfca2b20..fa1a531cf9 100644 --- a/lib/gui/os/dropzone/directives/dropzone.js +++ b/lib/gui/os/dropzone/directives/dropzone.js @@ -40,8 +40,8 @@ module.exports = ($timeout) => { scope: { osDropzone: '&' }, - link: (scope, element) => { - const domElement = element[0]; + link: (scope, $element) => { + const domElement = _.first($element); // See https://github.com/electron/electron/blob/master/docs/api/file-object.md @@ -52,7 +52,7 @@ module.exports = ($timeout) => { domElement.ondrop = (event) => { event.preventDefault(); - const filename = event.dataTransfer.files[0].path; + const filename = _.first(event.dataTransfer.files).path; // Safely bring this to the word of Angular $timeout(() => { diff --git a/lib/gui/os/notification/services/notification.js b/lib/gui/os/notification/services/notification.js index 2b67770682..1686f7fa78 100644 --- a/lib/gui/os/notification/services/notification.js +++ b/lib/gui/os/notification/services/notification.js @@ -48,7 +48,7 @@ module.exports = function() { } return new Notification(title, { - body: body + body }); }; diff --git a/lib/gui/os/open-external/open-external.js b/lib/gui/os/open-external/open-external.js index ddce865caa..023882c33c 100644 --- a/lib/gui/os/open-external/open-external.js +++ b/lib/gui/os/open-external/open-external.js @@ -28,7 +28,7 @@ const OSOpenExternal = angular.module(MODULE_NAME, []); OSOpenExternal.service('OSOpenExternalService', require('./services/open-external')); OSOpenExternal.directive('osOpenExternal', require('./directives/open-external')); -OSOpenExternal.run(function(OSOpenExternalService) { +OSOpenExternal.run((OSOpenExternalService) => { document.addEventListener('click', (event) => { const target = event.target; if (target.tagName === 'A' && angular.isDefined(target.href)) { diff --git a/lib/gui/os/window-progress/services/window-progress.js b/lib/gui/os/window-progress/services/window-progress.js index 43436cb783..034cac2db6 100644 --- a/lib/gui/os/window-progress/services/window-progress.js +++ b/lib/gui/os/window-progress/services/window-progress.js @@ -26,12 +26,9 @@ module.exports = function() { * @protected * * @description - * Since electron only has one renderer view, we can assume the - * current window is the one with id == 1. - * * We expose this property to `this` for testability purposes. */ - this.currentWindow = electron.remote.BrowserWindow.fromId(1); + this.currentWindow = electron.remote.getCurrentWindow(); /** * @summary Set operating system window progress @@ -47,11 +44,14 @@ module.exports = function() { * OSWindowProgressService.set(85); */ this.set = (percentage) => { - if (percentage > 100 || percentage < 0) { + const PERCENTAGE_MINIMUM = 0; + const PERCENTAGE_MAXIMUM = 100; + + if (percentage > PERCENTAGE_MAXIMUM || percentage < PERCENTAGE_MINIMUM) { throw new Error(`Invalid window progress percentage: ${percentage}`); } - this.currentWindow.setProgressBar(percentage / 100); + this.currentWindow.setProgressBar(percentage / PERCENTAGE_MAXIMUM); }; /** @@ -65,8 +65,9 @@ module.exports = function() { this.clear = () => { // Passing 0 or null/undefined doesn't work. - this.currentWindow.setProgressBar(-1); + const ELECTRON_PROGRESS_BAR_RESET_VALUE = -1; + this.currentWindow.setProgressBar(ELECTRON_PROGRESS_BAR_RESET_VALUE); }; }; diff --git a/lib/gui/pages/main/controllers/flash.js b/lib/gui/pages/main/controllers/flash.js index a9cfb30a8e..ba8fc7c9f4 100644 --- a/lib/gui/pages/main/controllers/flash.js +++ b/lib/gui/pages/main/controllers/flash.js @@ -58,7 +58,7 @@ module.exports = function( DriveScannerService.stop(); AnalyticsService.logEvent('Flash', { - image: image, + image, device: drive.device }); @@ -106,22 +106,20 @@ module.exports = function( this.getProgressButtonLabel = () => { const flashState = FlashStateModel.getFlashState(); const isChecking = flashState.type === 'check'; + const PERCENTAGE_MINIMUM = 0; + const PERCENTAGE_MAXIMUM = 100; if (!FlashStateModel.isFlashing()) { return 'Flash!'; - } - - if (flashState.percentage === 0 && !flashState.speed) { + } else if (flashState.percentage === PERCENTAGE_MINIMUM && !flashState.speed) { return 'Starting...'; - } else if (flashState.percentage === 100) { + } else if (flashState.percentage === PERCENTAGE_MAXIMUM) { if (isChecking && SettingsModel.get('unmountOnSuccess')) { return 'Unmounting...'; } return 'Finishing...'; - } - - if (isChecking) { + } else if (isChecking) { return `${flashState.percentage}% Validating...`; } diff --git a/lib/gui/pages/main/controllers/image-selection.js b/lib/gui/pages/main/controllers/image-selection.js index 06d84e4802..ba7b950a86 100644 --- a/lib/gui/pages/main/controllers/image-selection.js +++ b/lib/gui/pages/main/controllers/image-selection.js @@ -66,7 +66,7 @@ module.exports = function( this.selectImage = (image) => { if (!SupportedFormatsModel.isSupportedImage(image.path)) { OSDialogService.showError('Invalid image', messages.error.invalidImage({ - image: image + image })); AnalyticsService.logEvent('Invalid image', image); @@ -100,9 +100,8 @@ module.exports = function( image.logo = Boolean(image.logo); image.bmap = Boolean(image.bmap); - AnalyticsService.logEvent('Select image', image); + return AnalyticsService.logEvent('Select image', image); }).catch(ErrorService.reportException); - }; /** diff --git a/lib/gui/pages/settings/controllers/settings.js b/lib/gui/pages/settings/controllers/settings.js index a46fb9ee64..ba2ce0d26f 100644 --- a/lib/gui/pages/settings/controllers/settings.js +++ b/lib/gui/pages/settings/controllers/settings.js @@ -18,7 +18,7 @@ const os = require('os'); -module.exports = function(WarningModalService, SettingsModel) { +module.exports = function(WarningModalService, SettingsModel, ErrorService) { /** * @summary Client platform @@ -78,12 +78,12 @@ module.exports = function(WarningModalService, SettingsModel) { // Keep the checkbox unchecked until the user confirms this.currentData[name] = false; - WarningModalService.display(options).then((userAccepted) => { + return WarningModalService.display(options).then((userAccepted) => { if (userAccepted) { this.model.set(name, true); this.refreshSettings(); } - }); + }).catch(ErrorService.reportException); }; }; diff --git a/lib/gui/pages/settings/settings.js b/lib/gui/pages/settings/settings.js index 0f97b3cdd5..58ae934681 100644 --- a/lib/gui/pages/settings/settings.js +++ b/lib/gui/pages/settings/settings.js @@ -25,7 +25,8 @@ const MODULE_NAME = 'Etcher.Pages.Settings'; const SettingsPage = angular.module(MODULE_NAME, [ require('angular-ui-router'), require('../../components/warning-modal/warning-modal'), - require('../../models/settings') + require('../../models/settings'), + require('../../modules/error') ]); SettingsPage.controller('SettingsController', require('./controllers/settings')); diff --git a/lib/gui/utils/manifest-bind/directives/manifest-bind.js b/lib/gui/utils/manifest-bind/directives/manifest-bind.js index 181e515695..0d0fc4a60b 100644 --- a/lib/gui/utils/manifest-bind/directives/manifest-bind.js +++ b/lib/gui/utils/manifest-bind/directives/manifest-bind.js @@ -39,7 +39,7 @@ module.exports = (ManifestBindService) => { const value = ManifestBindService.get(attributes.manifestBind); if (!value) { - throw new Error('ManifestBind: Unknown property `' + attributes.manifestBind + '`'); + throw new Error(`ManifestBind: Unknown property \`${attributes.manifestBind}\``); } element.html(value); diff --git a/lib/gui/utils/path/filters/basename.js b/lib/gui/utils/path/filters/basename.js index 7058d24294..823c15e209 100644 --- a/lib/gui/utils/path/filters/basename.js +++ b/lib/gui/utils/path/filters/basename.js @@ -33,7 +33,7 @@ module.exports = () => { */ return (input) => { if (!input) { - return; + return ''; } return path.basename(input); diff --git a/lib/image-stream/archive-hooks/zip.js b/lib/image-stream/archive-hooks/zip.js index 9598254c71..205a38cdd8 100644 --- a/lib/image-stream/archive-hooks/zip.js +++ b/lib/image-stream/archive-hooks/zip.js @@ -48,9 +48,11 @@ exports.getEntries = (archive) => { zip.on('error', reject); zip.on('ready', () => { + const EMPTY_ENTRY_SIZE = 0; + return resolve(_.chain(zip.entries()) .omitBy((entry) => { - return entry.size === 0; + return entry.size === EMPTY_ENTRY_SIZE; }) .map((metadata) => { return { @@ -99,7 +101,7 @@ exports.extractFile = (archive, entries, file) => { return zipfile.readEntry(); } - zipfile.openReadStream(entry, (error, readStream) => { + return zipfile.openReadStream(entry, (error, readStream) => { if (error) { return reject(error); } diff --git a/lib/image-stream/archive.js b/lib/image-stream/archive.js index a09bb4310f..a3ac4c77a6 100644 --- a/lib/image-stream/archive.js +++ b/lib/image-stream/archive.js @@ -170,11 +170,12 @@ exports.extractImage = (archive, hooks) => { return hooks.getEntries(archive).then((entries) => { const imageEntries = _.filter(entries, (entry) => { - const extension = path.extname(entry.name).slice(1); + const extension = _.replace(path.extname(entry.name), '.', '').toLowerCase(); return _.includes(IMAGE_EXTENSIONS, extension); }); - if (imageEntries.length !== 1) { + const VALID_NUMBER_OF_IMAGE_ENTRIES = 1; + if (imageEntries.length !== VALID_NUMBER_OF_IMAGE_ENTRIES) { const error = new Error('Invalid archive image'); error.description = 'The archive image should contain one and only one top image file.'; error.report = false; diff --git a/lib/image-stream/utils.js b/lib/image-stream/utils.js index fef1d62d89..afd58dbbd2 100644 --- a/lib/image-stream/utils.js +++ b/lib/image-stream/utils.js @@ -33,9 +33,11 @@ const archiveType = require('archive-type'); */ exports.getArchiveMimeType = (file) => { - // archive-type only needs the first 261 bytes + // `archive-type` only needs the first 261 bytes // See https://github.com/kevva/archive-type - const chunk = readChunk.sync(file, 0, 261); + const MAGIC_NUMBER_BUFFER_START = 0; + const MAGIC_NUMBER_BUFFER_END = 261; + const chunk = readChunk.sync(file, MAGIC_NUMBER_BUFFER_START, MAGIC_NUMBER_BUFFER_END); return _.get(archiveType(chunk), 'mime', 'application/octet-stream'); }; diff --git a/lib/shared/drive-constraints.js b/lib/shared/drive-constraints.js index 20fa6a5816..47c4ed1aeb 100644 --- a/lib/shared/drive-constraints.js +++ b/lib/shared/drive-constraints.js @@ -19,6 +19,14 @@ const _ = require('lodash'); const pathIsInside = require('path-is-inside'); +/** + * @summary The default unknown size for things such as images and drives + * @constant + * @private + * @type {Number} + */ +const UNKNOWN_SIZE = 0; + /** * @summary Check if a drive is locked * @function @@ -134,7 +142,7 @@ exports.isSourceDrive = (drive, image) => { * } */ exports.isDriveLargeEnough = (drive, image) => { - return _.get(drive, 'size', 0) >= _.get(image, 'size', 0); + return _.get(drive, 'size', UNKNOWN_SIZE) >= _.get(image, 'size', UNKNOWN_SIZE); }; /** @@ -198,5 +206,5 @@ exports.isDriveValid = (drive, image) => { * } */ exports.isDriveSizeRecommended = (drive, image) => { - return _.get(drive, 'size', 0) >= _.get(image, 'recommendedDriveSize', 0); + return _.get(drive, 'size', UNKNOWN_SIZE) >= _.get(image, 'recommendedDriveSize', UNKNOWN_SIZE); }; diff --git a/lib/shared/robot/index.js b/lib/shared/robot/index.js index fe09121ba3..7143405077 100644 --- a/lib/shared/robot/index.js +++ b/lib/shared/robot/index.js @@ -60,7 +60,7 @@ exports.buildMessage = (title, data = {}) => { return JSON.stringify({ command: title, - data: data + data }); }; @@ -83,7 +83,7 @@ exports.buildMessage = (title, data = {}) => { * > } */ exports.parseMessage = (string) => { - let output; + let output = null; try { output = JSON.parse(string); @@ -124,15 +124,13 @@ exports.parseMessage = (string) => { * > true */ exports.buildErrorMessage = (error) => { - if (_.isString(error)) { - error = new Error(error); - } + const errorObject = _.isString(error) ? new Error(error) : error; return exports.buildMessage('error', { - message: error.message, - description: error.description, - stacktrace: error.stack, - code: error.code + message: errorObject.message, + description: errorObject.description, + stacktrace: errorObject.stack, + code: errorObject.code }); }; diff --git a/lib/shared/units.js b/lib/shared/units.js index eeeae62442..12a2dae933 100644 --- a/lib/shared/units.js +++ b/lib/shared/units.js @@ -16,6 +16,39 @@ 'use strict'; +/** + * @summary Gigabyte to byte ratio + * @constant + * @private + * @type {Number} + * + * @description + * 1 GB = 1e+9 B + */ +const GIGABYTE_TO_BYTE_RATIO = 1e+9; + +/** + * @summary Megabyte to byte ratio + * @constant + * @private + * @type {Number} + * + * @description + * 1 MB = 1e+6 B + */ +const MEGABYTE_TO_BYTE_RATIO = 1e+6; + +/** + * @summary Milliseconds in a day + * @constant + * @private + * @type {Number} + * + * @description + * From 24 * 60 * 60 * 1000 + */ +const MILLISECONDS_IN_A_DAY = 86400000; + /** * @summary Convert bytes to gigabytes * @function @@ -28,7 +61,7 @@ * const result = units.bytesToGigabytes(7801405440); */ exports.bytesToGigabytes = (bytes) => { - return bytes / 1e+9; + return bytes / GIGABYTE_TO_BYTE_RATIO; }; /** @@ -43,5 +76,20 @@ exports.bytesToGigabytes = (bytes) => { * const result = units.bytesToMegabytes(7801405440); */ exports.bytesToMegabytes = (bytes) => { - return bytes / 1e+6; + return bytes / MEGABYTE_TO_BYTE_RATIO; +}; + +/** + * @summary Convert days to milliseconds + * @function + * @public + * + * @param {Number} days - days + * @returns {Number} milliseconds + * + * @example + * const result = units.daysToMilliseconds(2); + */ +exports.daysToMilliseconds = (days) => { + return days * MILLISECONDS_IN_A_DAY; }; diff --git a/package.json b/package.json index 799b617456..fb770b3391 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "electron-mocha": "^3.1.1", "electron-packager": "^7.0.1", "electron-prebuilt": "1.4.4", - "eslint": "^2.13.1", + "eslint": "^3.16.1", "file-exists": "^1.0.0", "html-angular-validate": "^0.1.9", "jsonfile": "^2.3.1", diff --git a/scripts/clean-shrinkwrap.js b/scripts/clean-shrinkwrap.js index 1a08486a42..aed02fc7b0 100644 --- a/scripts/clean-shrinkwrap.js +++ b/scripts/clean-shrinkwrap.js @@ -21,6 +21,7 @@ const jsonfile = require('jsonfile'); const childProcess = require('child_process'); const packageJSON = require('../package.json'); const shrinkwrapIgnore = _.union(packageJSON.shrinkwrapIgnore, _.keys(packageJSON.optionalDependencies)); +const EXIT_CODES = require('../lib/shared/exit-codes'); const SHRINKWRAP_PATH = path.join(__dirname, '..', 'npm-shrinkwrap.json'); try { @@ -29,7 +30,7 @@ try { })); } catch (error) { console.error(error.stderr.toString()); - process.exit(1); + process.exit(EXIT_CODES.GENERAL_ERROR); } const shrinkwrapContents = jsonfile.readFileSync(SHRINKWRAP_PATH); diff --git a/scripts/html-lint.js b/scripts/html-lint.js index e6ddc06d6f..df8ebbb3a2 100644 --- a/scripts/html-lint.js +++ b/scripts/html-lint.js @@ -14,6 +14,7 @@ const chalk = require('chalk'); const path = require('path'); const _ = require('lodash'); const angularValidate = require('html-angular-validate'); +const EXIT_CODES = require('../lib/shared/exit-codes'); const PROJECT_ROOT = path.join(__dirname, '..'); const FILENAME = path.relative(PROJECT_ROOT, __filename); @@ -45,16 +46,14 @@ angularValidate.validate( reportCheckstylePath: null } ).then((result) => { - - // console.log(result); - _.each(result.failed, (failure) => { // The module has a typo in the "numbers" property console.error(chalk.red(`${failure.numerrs} errors at ${path.relative(PROJECT_ROOT, failure.filepath)}`)); _.each(failure.errors, (error) => { - console.error(' ' + chalk.yellow(`[${error.line}:${error.col}]`) + ` ${error.msg}`); + const errorPosition = `[${error.line}:${error.col}]`; + console.error(` ${chalk.yellow(errorPosition)} ${error.msg}`); if (/^Attribute (.*) not allowed on/.test(error.msg)) { console.error(chalk.dim(` If this is a valid directive attribute, add it to the whitelist at ${FILENAME}`)); @@ -71,16 +70,17 @@ angularValidate.validate( } if (!result.allpassed) { + const EXIT_TIMEOUT_MS = 500; // Add a small timeout, otherwise the scripts exits // before every string was printed on the screen. setTimeout(() => { - process.exit(1); - }, 500); + process.exit(EXIT_CODES.GENERAL_ERROR); + }, EXIT_TIMEOUT_MS); } }, (error) => { console.error(error); - process.exit(1); + process.exit(EXIT_CODES.GENERAL_ERROR); }); diff --git a/scripts/packageignore.js b/scripts/packageignore.js index 5c49ac7d0a..7117c8df3d 100644 --- a/scripts/packageignore.js +++ b/scripts/packageignore.js @@ -22,19 +22,19 @@ console.log(_.flatten([ packageJSON.packageIgnore, // Development dependencies - _.map(_.keys(packageJSON.devDependencies), function(dependency) { + _.map(_.keys(packageJSON.devDependencies), (dependency) => { return path.join('node_modules', dependency); }), // Top level hidden files - _.map(_.filter(topLevelFiles, function(file) { + _.map(_.filter(topLevelFiles, (file) => { return _.startsWith(file, '.'); - }), function(file) { - return '\\' + file; + }), (file) => { + return `\\${file}`; }), // Top level markdown files - _.filter(topLevelFiles, function(file) { + _.filter(topLevelFiles, (file) => { return _.endsWith(file, '.md'); }) diff --git a/tests/.eslintrc.yml b/tests/.eslintrc.yml new file mode 100644 index 0000000000..78399fd7d0 --- /dev/null +++ b/tests/.eslintrc.yml @@ -0,0 +1,19 @@ +rules: + require-jsdoc: + - off + no-undefined: + - off + init-declarations: + - off + no-unused-expressions: + - off + prefer-arrow-callback: + - off + no-magic-numbers: + - off + id-length: + - error + - min: 2 + exceptions: + - "_" + - "m" diff --git a/tests/gui/components/svg-icon.spec.js b/tests/gui/components/svg-icon.spec.js index 7752737baa..fbd706ad66 100644 --- a/tests/gui/components/svg-icon.spec.js +++ b/tests/gui/components/svg-icon.spec.js @@ -39,7 +39,7 @@ describe('Browser: SVGIcon', function() { // Injecting XML as HTML causes the XML header to be commented out. // Modify here to ease assertions later on. - iconContents[0] = ''; + iconContents[0] = ``; iconContents = iconContents.join('\n'); const element = $compile(`Resin.io`)($rootScope); diff --git a/tests/gui/components/update-notifier.spec.js b/tests/gui/components/update-notifier.spec.js index 692d9e175b..4a8b2afb1b 100644 --- a/tests/gui/components/update-notifier.spec.js +++ b/tests/gui/components/update-notifier.spec.js @@ -2,6 +2,7 @@ const m = require('mochainon'); const angular = require('angular'); +const units = require('../../../lib/shared/units'); require('angular-mocks'); describe('Browser: UpdateNotifier', function() { @@ -16,12 +17,12 @@ describe('Browser: UpdateNotifier', function() { let UpdateNotifierService; let SettingsModel; - let UPDATE_NOTIFIER_SLEEP_TIME; + let UPDATE_NOTIFIER_SLEEP_DAYS; - beforeEach(angular.mock.inject(function(_UpdateNotifierService_, _SettingsModel_, _UPDATE_NOTIFIER_SLEEP_TIME_) { + beforeEach(angular.mock.inject(function(_UpdateNotifierService_, _SettingsModel_, _UPDATE_NOTIFIER_SLEEP_DAYS_) { UpdateNotifierService = _UpdateNotifierService_; SettingsModel = _SettingsModel_; - UPDATE_NOTIFIER_SLEEP_TIME = _UPDATE_NOTIFIER_SLEEP_TIME_; + UPDATE_NOTIFIER_SLEEP_DAYS = _UPDATE_NOTIFIER_SLEEP_DAYS_; })); describe('given the `sleepUpdateCheck` is disabled', function() { @@ -72,7 +73,8 @@ describe('Browser: UpdateNotifier', function() { describe('given the `lastUpdateNotify` was updated long ago', function() { beforeEach(function() { - SettingsModel.set('lastUpdateNotify', Date.now() + UPDATE_NOTIFIER_SLEEP_TIME + 1000); + const SLEEP_MS = units.daysToMilliseconds(UPDATE_NOTIFIER_SLEEP_DAYS); + SettingsModel.set('lastUpdateNotify', Date.now() + SLEEP_MS + 1000); }); it('should return true', function() { diff --git a/tests/gui/models/settings.spec.js b/tests/gui/models/settings.spec.js index c85476b735..9df79dd8c0 100644 --- a/tests/gui/models/settings.spec.js +++ b/tests/gui/models/settings.spec.js @@ -65,7 +65,7 @@ describe('Browser: SettingsModel', function() { const keyUnderTest = _.first(SUPPORTED_KEYS); m.chai.expect(function() { SettingsModel.set(keyUnderTest, { - x: 1 + setting: 1 }); }).to.throw('Invalid setting value: [object Object]'); }); diff --git a/tests/gui/models/supported-formats.spec.js b/tests/gui/models/supported-formats.spec.js index 0e56f872c3..57e99dc50f 100644 --- a/tests/gui/models/supported-formats.spec.js +++ b/tests/gui/models/supported-formats.spec.js @@ -73,35 +73,35 @@ describe('Browser: SupportedFormats', function() { it('should return true if the extension is included in .getAllExtensions()', function() { const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); - const imagePath = '/path/to/foo.' + nonCompressedExtension; + const imagePath = `/path/to/foo.${nonCompressedExtension}`; const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); m.chai.expect(isSupported).to.be.true; }); it('should ignore casing when determining extension validity', function() { const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); - const imagePath = '/path/to/foo.' + nonCompressedExtension.toUpperCase(); + const imagePath = `/path/to/foo.${nonCompressedExtension.toUpperCase()}`; const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); m.chai.expect(isSupported).to.be.true; }); it('should not consider an extension before a non compressed extension', function() { const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); - const imagePath = '/path/to/foo.1234.' + nonCompressedExtension; + const imagePath = `/path/to/foo.1234.${nonCompressedExtension}`; const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); m.chai.expect(isSupported).to.be.true; }); it('should return true if the extension is supported and the file name includes dots', function() { const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); - const imagePath = '/path/to/foo.1.2.3-bar.' + nonCompressedExtension; + const imagePath = `/path/to/foo.1.2.3-bar.${nonCompressedExtension}`; const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); m.chai.expect(isSupported).to.be.true; }); it('should return true if the extension is only a supported archive extension', function() { const archiveExtension = _.first(SupportedFormatsModel.getArchiveExtensions()); - const imagePath = '/path/to/foo.' + archiveExtension; + const imagePath = `/path/to/foo.${archiveExtension}`; const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); m.chai.expect(isSupported).to.be.true; }); @@ -109,14 +109,14 @@ describe('Browser: SupportedFormats', function() { it('should return true if the extension is a supported one plus a supported compressed extensions', function() { const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions()); - const imagePath = '/path/to/foo.' + nonCompressedExtension + '.' + compressedExtension; + const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}`; const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); m.chai.expect(isSupported).to.be.true; }); it('should return false if the extension is an unsupported one plus a supported compressed extensions', function() { const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions()); - const imagePath = '/path/to/foo.jpg.' + compressedExtension; + const imagePath = `/path/to/foo.jpg.${compressedExtension}`; const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); m.chai.expect(isSupported).to.be.false; }); diff --git a/tests/gui/utils/path.spec.js b/tests/gui/utils/path.spec.js index bceeeb58c8..1b003442ae 100644 --- a/tests/gui/utils/path.spec.js +++ b/tests/gui/utils/path.spec.js @@ -19,8 +19,8 @@ describe('Browser: Path', function() { basenameFilter = _basenameFilter_; })); - it('should return undefined if no input', function() { - m.chai.expect(basenameFilter()).to.be.undefined; + it('should return an empty string if no input', function() { + m.chai.expect(basenameFilter()).to.equal(''); }); it('should return the basename', function() { diff --git a/tests/image-stream/tester.js b/tests/image-stream/tester.js index 07ee063c2f..0ab33469bb 100644 --- a/tests/image-stream/tester.js +++ b/tests/image-stream/tester.js @@ -39,6 +39,8 @@ const deleteIfExists = (file) => { if (fileExists(file)) { return fs.unlinkAsync(file); } + + return Bluebird.resolve(); }); }; @@ -63,7 +65,7 @@ exports.extractFromFilePath = function(file, image) { results.size.original === fs.statSync(file).size, results.size.original === fs.statSync(image).size ])) { - throw new Error('Invalid size: ' + results.size.original); + throw new Error(`Invalid size: ${results.size.original}`); } const stream = results.stream