chore: revise ESLint built-in configuration (#1149)

There are a lot of new rules since the last time I revised the ESLint
rules documentation.

I've updated the main `.eslintrc.yml` to include some newer additions,
plus I added another ESLint configuration file inside `tests`, so we can
add some stricted rules to the production code while relaxing them for
the test suite (due to the fact that Mocha is not very ES6 friendly and
Angular tests require a bit of dark magic to setup).

This is a summary of the most important changes:

- Disallow "magic numbers"

These should now be extracted to constants, which forces us to think of
a good name for them, and thus make the code more self-documenting (I
had to Google up the meaning of some existing magic numbers, so I guess
this will be great for readability purposes).

- Require consistent `return` statements

Some functions relied on JavaScript relaxed casting mechanism to work,
which now have explicit return values. This flag also helped me detect
some promises that were not being returned, and therefore risked not
being caught by the exception handlers in case of errors.

- Disallow redefining function arguments

Immutability makes functions easier to reason about.

- Enforce JavaScript string templates instead of string concatenation

We were heavily mixing boths across the codebase.

There are some extra rules that I tweaked, however most of codebase
changes in this commit are related to the rules mentioned above.

Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
Juan Cruz Viotti 2017-03-07 23:46:44 -04:00 committed by GitHub
parent c93f528f96
commit 6c8bc117ab
52 changed files with 514 additions and 174 deletions

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -108,7 +108,7 @@ exports.getErrorMessage = (error) => {
});
if (error.description) {
return message + '\n\n' + error.description;
return `${message}\n\n${error.description}`;
}
return message;

View File

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

View File

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

View File

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

View File

@ -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?',

View File

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

View File

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

View File

@ -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}

View File

@ -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;

View File

@ -11,7 +11,7 @@
<input type="checkbox"
ng-model="modal.sleepUpdateCheck"
ng-change="modal.settings.set('sleepUpdateCheck', modal.sleepUpdateCheck)">
<span>Remind me again in 7 days</span>
<span>Remind me again in {{::modal.sleepDays}} days</span>
</label>
</div>
</div>

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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)})`;
}
this.logDebug(message);
return message;
});
this.logDebug(debugMessage);
};
/**

View File

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

View File

@ -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:

View File

@ -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(() => {

View File

@ -48,7 +48,7 @@ module.exports = function() {
}
return new Notification(title, {
body: body
body
});
};

View File

@ -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)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ module.exports = () => {
*/
return (input) => {
if (!input) {
return;
return '';
}
return path.basename(input);

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

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

19
tests/.eslintrc.yml Normal file
View File

@ -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"

View File

@ -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].slice(1, iconContents[0].length - 1) + '-->';
iconContents[0] = `<!--${iconContents[0].slice(1, iconContents[0].length - 1)}-->`;
iconContents = iconContents.join('\n');
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);

View File

@ -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() {

View File

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

View File

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

View File

@ -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() {

View File

@ -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