Skip to content

Commit

Permalink
feat(Growl): Check notification support _before_ use!
Browse files Browse the repository at this point in the history
Added (minorly brittle) means to verify prerequisite software
is installed. Migrated Growl support code to its own file. This
implementation's checks are required to enable Growl; failure
will write notice to `stderr`. Other modifications based on
discussion from Chad Rickman's PR mochajs#3311. This also checks for
errors from Growl callback.

Fixes mochajs#3111

Signed-off-by: Paul Roebuck <plroebuck@users.noreply.github.com>
  • Loading branch information
plroebuck committed Nov 1, 2018
1 parent 2344119 commit e2204c1
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 45 deletions.
153 changes: 153 additions & 0 deletions lib/growl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use strict';
/**
* @module Growl
*/

/**
* Module dependencies.
*/
var fs = require('fs');
var os = require('os');
var path = require('path');

/**
* @summary
* Checks if Growl support seems likely.
*
* @description
* Glosses over the distinction between an unsupported platform
* and one that lacks prerequisite software installations.
* Autofails when run in browser.
*
* @public
* @see {@link https://github.com/tj/node-growl/blob/master/README.md|Prerequisite Installs}
* @see {@link Mocha#growl}
* @return {boolean} whether Growl support can be expected
*/
exports.isCapable = function() {
return !process.browser && which(getSupportBinaries()) !== '';
};

/**
* Implements desktop notifications as a pseudo-reporter.
*
* @private
* @see {@link Mocha#_growl}
* @param {Object} runner - Runner instance.
*/
exports.notify = function(runner) {
var sendNotification = function() {
var growl = require('growl');
var stats = runner.stats;
var msg;
var options;

if (stats.failures) {
msg = stats.failures + ' of ' + runner.total + ' tests failed';
options = {
name: 'mocha',
title: 'Failed',
image: image('error')
};
} else {
msg = stats.passes + ' tests passed in ' + stats.duration + 'ms';
options = {
name: 'mocha',
title: 'Passed',
image: image('ok')
};
}
growl(msg, options, onCompletion);
};

/**
* Callback for result of attempted Growl notification.
*
* @callback Growl~growlCB
* @param {*} err - Error object, or <code>null</code> if successful.
* @param {string} stdout - <code>stdout</code> from notification delivery
* process.
* @param {string} stderr - <code>stderr</code> from notification delivery
* process. It will include timestamp and executed command with arguments.
*/

function onCompletion(err, stdout, stderr) {
if (err) {
if (err.code === 'ENOENT') {
console.error(
'Growl notification support not installed... listener removed!'
);
runner.removeListener('end', sendNotification);
} else {
console.error('growl: ', err.message);
}
}
}

// :TBD: should this be `.once`?
runner.on('end', sendNotification);
};

/**
* Returns Growl image `name` path.
*
* @private
* @param {string} name - Basename of associated Growl image.
* @return {string} Pathname to Growl image
*/
function image(name) {
return path.join(__dirname, '..', 'assets', 'growl', name + '.png');
}

/**
* @summary
* Locate a binary in the user's `PATH`.
*
* @description
* Takes a list of command names and searches the path for each executable
* file that would be run had these commands actually been invoked.
*
* @private
* @param {string[]} binaries - Names of binary files to search for.
* @return {string} absolute path of first binary found, or empty string if none
*/
function which(binaries) {
var paths = process.env.PATH.split(path.delimiter);
var exists = fs.existsSync || path.existsSync;

for (var n = 0, blen = binaries.length; n < blen; n++) {
var binary = binaries[n];
var loc;
for (var i = 0, plen = paths.length; i < plen; i++) {
loc = path.join(paths[i], binary);
if (exists(loc)) {
return loc;
}
}
}
return '';
}

/**
* @summary
* Get platform-specific Growl support binaries.
*
* @description
* Somewhat brittle dependency on `growl` package implementation, but it
* rarely changes.
*
* @private
* @see {@link https://github.com/tj/node-growl/blob/master/lib/growl.js#L28-L126|setupCmd}
* @return {string[]} names of Growl support binaries
*/
function getSupportBinaries() {
switch (os.type()) {
case 'Darwin':
return ['terminal-notifier', 'growlnotify'];
case 'Linux':
return ['notify-send', 'growl'];
case 'Windows_NT':
return ['growlnotify'];
}
return [];
}
79 changes: 39 additions & 40 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
var escapeRe = require('escape-string-regexp');
var path = require('path');
var builtinReporters = require('./reporters');
var growl = require('./growl');
var utils = require('./utils');

exports = module.exports = Mocha;
Expand Down Expand Up @@ -49,17 +50,6 @@ exports.Suite = require('./suite');
exports.Hook = require('./hook');
exports.Test = require('./test');

/**
* Returns Growl image `name` path.
*
* @private
* @param {string} name - Basename of associated Growl image.
* @return {string} Pathname to Growl image
*/
function image(name) {
return path.join(__dirname, '..', 'assets', 'growl', name + '.png');
}

/**
* Constructs a new Mocha instance with `options`.
*
Expand Down Expand Up @@ -104,6 +94,9 @@ function Mocha(options) {
this.suite = new exports.Suite('', new exports.Context());
this.ui(options.ui);
this.bail(options.bail);
if (options.growl) {
this.growl();
}
this.reporter(options.reporter, options.reporterOptions);
if (typeof options.timeout !== 'undefined' && options.timeout !== null) {
this.timeout(options.timeout);
Expand Down Expand Up @@ -290,32 +283,6 @@ Mocha.prototype.loadFiles = function(fn) {
fn && fn();
};

/**
* Implements desktop notifications using a pseudo-reporter.
*
* @private
* @see {@link Mocha#growl}
* @param {Object} runner - Runner instance.
* @param {Object} reporter - Reporter instance.
*/
Mocha.prototype._growl = function(runner, reporter) {
var notify = require('growl');

runner.on('end', function() {
var stats = reporter.stats;
if (stats.failures) {
var msg = stats.failures + ' of ' + runner.total + ' tests failed';
notify(msg, {name: 'mocha', title: 'Failed', image: image('error')});
} else {
notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', {
name: 'mocha',
title: 'Passed',
image: image('ok')
});
}
});
};

/**
* Sets `grep` filter after escaping RegExp special characters.
*
Expand Down Expand Up @@ -440,18 +407,50 @@ Mocha.prototype.fullTrace = function() {
};

/**
* Enables desktop notification support.
* @summary
* Enables desktop notification support if prerequisite software installed.
*
* @public
* @see {@link Mocha#isGrowlCapable}
* @see {@link Mocha#_growl}
* @return {Mocha} this
* @chainable
*/
Mocha.prototype.growl = function() {
this.options.growl = true;
this.options.growl = this.isGrowlCapable();
if (!this.options.growl) {
var detail = process.browser
? 'Growl notification support not available in browser... cannot enable!'
: 'Growl notification support not installed... cannot enable!';
console.error(detail);
}
return this;
};

/**
* @summary
* Determines if Growl support seems likely.
*
* @description
* <strong>Not available when run in browser.</strong>
*
* @private
* @see {@link Growl#isCapable}
* @see {@link Mocha#growl}
* @return {boolean} whether Growl support can be expected
*/
Mocha.prototype.isGrowlCapable = growl.isCapable;

/**
* Implements desktop notifications using a pseudo-reporter.
*
* @private
* @see {@link Mocha#growl}
* @see {@link Growl#notify}
* @param {Object} runner - Runner instance.
*/
Mocha.prototype._growl = growl.notify;

/**
* Specifies whitelist of variable names to be expected in global scope.
*
Expand Down Expand Up @@ -721,7 +720,7 @@ Mocha.prototype.run = function(fn) {
runner.globals(options.globals);
}
if (options.growl) {
this._growl(runner, reporter);
this._growl(runner);
}
if (options.useColors !== undefined) {
exports.reporters.Base.useColors = options.useColors;
Expand Down
24 changes: 19 additions & 5 deletions test/unit/mocha.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,26 @@ describe('Mocha', function() {
});

describe('.growl()', function() {
it('should set the growl option to true', function() {
var mocha = new Mocha(blankOpts);
mocha.growl();
expect(mocha.options.growl, 'to be', true);
context('if Growl capable', function() {
it('should set the growl option to true', function() {
var mocha = new Mocha(blankOpts);
mocha.isGrowlCapable = function() {
return true;
};
mocha.growl();
expect(mocha.options.growl, 'to be', true);
});
});
context('if not Growl capable', function() {
it('should set the growl option to false', function() {
var mocha = new Mocha(blankOpts);
mocha.isGrowlCapable = function() {
return false;
};
mocha.growl();
expect(mocha.options.growl, 'to be', false);
});
});

it('should be chainable', function() {
var mocha = new Mocha(blankOpts);
expect(mocha.growl(), 'to be', mocha);
Expand Down

0 comments on commit e2204c1

Please sign in to comment.