browser/growl.js

'use strict';

/**
 * Web Notifications module.
 * @module Growl
 */

/**
 * Save timer references to avoid Sinon interfering (see GH-237).
 */
var Date = global.Date;
var setTimeout = global.setTimeout;
var EVENT_RUN_END = require('../runner').constants.EVENT_RUN_END;

/**
 * Checks if browser notification support exists.
 *
 * @public
 * @see {@link https://caniuse.com/#feat=notifications|Browser support (notifications)}
 * @see {@link https://caniuse.com/#feat=promises|Browser support (promises)}
 * @see {@link Mocha#growl}
 * @see {@link Mocha#isGrowlCapable}
 * @return {boolean} whether browser notification support exists
 */
exports.isCapable = function() {
  var hasNotificationSupport = 'Notification' in window;
  var hasPromiseSupport = typeof Promise === 'function';
  return process.browser && hasNotificationSupport && hasPromiseSupport;
};

/**
 * Implements browser notifications as a pseudo-reporter.
 *
 * @public
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/notification|Notification API}
 * @see {@link https://developers.google.com/web/fundamentals/push-notifications/display-a-notification|Displaying a Notification}
 * @see {@link Growl#isPermitted}
 * @see {@link Mocha#_growl}
 * @param {Runner} runner - Runner instance.
 */
exports.notify = function(runner) {
  var promise = isPermitted();

  /**
   * Attempt notification.
   */
  var sendNotification = function() {
    // If user hasn't responded yet... "No notification for you!" (Seinfeld)
    Promise.race([promise, Promise.resolve(undefined)])
      .then(canNotify)
      .then(function() {
        display(runner);
      })
      .catch(notPermitted);
  };

  runner.once(EVENT_RUN_END, sendNotification);
};

/**
 * Checks if browser notification is permitted by user.
 *
 * @private
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Notification/permission|Notification.permission}
 * @see {@link Mocha#growl}
 * @see {@link Mocha#isGrowlPermitted}
 * @returns {Promise<boolean>} promise determining if browser notification
 *     permissible when fulfilled.
 */
function isPermitted() {
  var permitted = {
    granted: function allow() {
      return Promise.resolve(true);
    },
    denied: function deny() {
      return Promise.resolve(false);
    },
    default: function ask() {
      return Notification.requestPermission().then(function(permission) {
        return permission === 'granted';
      });
    }
  };

  return permitted[Notification.permission]();
}

/**
 * @summary
 * Determines if notification should proceed.
 *
 * @description
 * Notification shall <strong>not</strong> proceed unless `value` is true.
 *
 * `value` will equal one of:
 * <ul>
 *   <li><code>true</code> (from `isPermitted`)</li>
 *   <li><code>false</code> (from `isPermitted`)</li>
 *   <li><code>undefined</code> (from `Promise.race`)</li>
 * </ul>
 *
 * @private
 * @param {boolean|undefined} value - Determines if notification permissible.
 * @returns {Promise<undefined>} Notification can proceed
 */
function canNotify(value) {
  if (!value) {
    var why = value === false ? 'blocked' : 'unacknowledged';
    var reason = 'not permitted by user (' + why + ')';
    return Promise.reject(new Error(reason));
  }
  return Promise.resolve();
}

/**
 * Displays the notification.
 *
 * @private
 * @param {Runner} runner - Runner instance.
 */
function display(runner) {
  var stats = runner.stats;
  var symbol = {
    cross: '\u274C',
    tick: '\u2705'
  };
  var logo = require('../../package').notifyLogo;
  var _message;
  var message;
  var title;

  if (stats.failures) {
    _message = stats.failures + ' of ' + stats.tests + ' tests failed';
    message = symbol.cross + ' ' + _message;
    title = 'Failed';
  } else {
    _message = stats.passes + ' tests passed in ' + stats.duration + 'ms';
    message = symbol.tick + ' ' + _message;
    title = 'Passed';
  }

  // Send notification
  var options = {
    badge: logo,
    body: message,
    dir: 'ltr',
    icon: logo,
    lang: 'en-US',
    name: 'mocha',
    requireInteraction: false,
    timestamp: Date.now()
  };
  var notification = new Notification(title, options);

  // Autoclose after brief delay (makes various browsers act same)
  var FORCE_DURATION = 4000;
  setTimeout(notification.close.bind(notification), FORCE_DURATION);
}

/**
 * As notifications are tangential to our purpose, just log the error.
 *
 * @private
 * @param {Error} err - Why notification didn't happen.
 */
function notPermitted(err) {
  console.error('notification error:', err.message);
}