blob: 7b46b8e1585d2364c9f8bc5b5dde91ba42961d04 [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001'use strict';
2
3/**
4 * Web Notifications module.
5 * @module Growl
6 */
7
8/**
9 * Save timer references to avoid Sinon interfering (see GH-237).
10 */
11var Date = global.Date;
12var setTimeout = global.setTimeout;
13var EVENT_RUN_END = require('../runner').constants.EVENT_RUN_END;
Peter Marshall0b95ea12020-07-02 18:50:04 +020014var isBrowser = require('../utils').isBrowser;
Yang Guo4fd355c2019-09-19 10:59:03 +020015
16/**
17 * Checks if browser notification support exists.
18 *
19 * @public
20 * @see {@link https://caniuse.com/#feat=notifications|Browser support (notifications)}
21 * @see {@link https://caniuse.com/#feat=promises|Browser support (promises)}
22 * @see {@link Mocha#growl}
23 * @see {@link Mocha#isGrowlCapable}
24 * @return {boolean} whether browser notification support exists
25 */
26exports.isCapable = function() {
27 var hasNotificationSupport = 'Notification' in window;
28 var hasPromiseSupport = typeof Promise === 'function';
Peter Marshall0b95ea12020-07-02 18:50:04 +020029 return isBrowser() && hasNotificationSupport && hasPromiseSupport;
Yang Guo4fd355c2019-09-19 10:59:03 +020030};
31
32/**
33 * Implements browser notifications as a pseudo-reporter.
34 *
35 * @public
36 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/notification|Notification API}
37 * @see {@link https://developers.google.com/web/fundamentals/push-notifications/display-a-notification|Displaying a Notification}
38 * @see {@link Growl#isPermitted}
39 * @see {@link Mocha#_growl}
40 * @param {Runner} runner - Runner instance.
41 */
42exports.notify = function(runner) {
43 var promise = isPermitted();
44
45 /**
46 * Attempt notification.
47 */
48 var sendNotification = function() {
49 // If user hasn't responded yet... "No notification for you!" (Seinfeld)
50 Promise.race([promise, Promise.resolve(undefined)])
51 .then(canNotify)
52 .then(function() {
53 display(runner);
54 })
55 .catch(notPermitted);
56 };
57
58 runner.once(EVENT_RUN_END, sendNotification);
59};
60
61/**
62 * Checks if browser notification is permitted by user.
63 *
64 * @private
65 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Notification/permission|Notification.permission}
66 * @see {@link Mocha#growl}
67 * @see {@link Mocha#isGrowlPermitted}
68 * @returns {Promise<boolean>} promise determining if browser notification
69 * permissible when fulfilled.
70 */
71function isPermitted() {
72 var permitted = {
73 granted: function allow() {
74 return Promise.resolve(true);
75 },
76 denied: function deny() {
77 return Promise.resolve(false);
78 },
79 default: function ask() {
80 return Notification.requestPermission().then(function(permission) {
81 return permission === 'granted';
82 });
83 }
84 };
85
86 return permitted[Notification.permission]();
87}
88
89/**
90 * @summary
91 * Determines if notification should proceed.
92 *
93 * @description
94 * Notification shall <strong>not</strong> proceed unless `value` is true.
95 *
96 * `value` will equal one of:
97 * <ul>
98 * <li><code>true</code> (from `isPermitted`)</li>
99 * <li><code>false</code> (from `isPermitted`)</li>
100 * <li><code>undefined</code> (from `Promise.race`)</li>
101 * </ul>
102 *
103 * @private
104 * @param {boolean|undefined} value - Determines if notification permissible.
105 * @returns {Promise<undefined>} Notification can proceed
106 */
107function canNotify(value) {
108 if (!value) {
109 var why = value === false ? 'blocked' : 'unacknowledged';
110 var reason = 'not permitted by user (' + why + ')';
111 return Promise.reject(new Error(reason));
112 }
113 return Promise.resolve();
114}
115
116/**
117 * Displays the notification.
118 *
119 * @private
120 * @param {Runner} runner - Runner instance.
121 */
122function display(runner) {
123 var stats = runner.stats;
124 var symbol = {
125 cross: '\u274C',
126 tick: '\u2705'
127 };
Tim van der Lippe6d109a92021-02-16 16:00:32 +0000128 var logo = require('../../package.json').notifyLogo;
Yang Guo4fd355c2019-09-19 10:59:03 +0200129 var _message;
130 var message;
131 var title;
132
133 if (stats.failures) {
134 _message = stats.failures + ' of ' + stats.tests + ' tests failed';
135 message = symbol.cross + ' ' + _message;
136 title = 'Failed';
137 } else {
138 _message = stats.passes + ' tests passed in ' + stats.duration + 'ms';
139 message = symbol.tick + ' ' + _message;
140 title = 'Passed';
141 }
142
143 // Send notification
144 var options = {
145 badge: logo,
146 body: message,
147 dir: 'ltr',
148 icon: logo,
149 lang: 'en-US',
150 name: 'mocha',
151 requireInteraction: false,
152 timestamp: Date.now()
153 };
154 var notification = new Notification(title, options);
155
156 // Autoclose after brief delay (makes various browsers act same)
157 var FORCE_DURATION = 4000;
158 setTimeout(notification.close.bind(notification), FORCE_DURATION);
159}
160
161/**
162 * As notifications are tangential to our purpose, just log the error.
163 *
164 * @private
165 * @param {Error} err - Why notification didn't happen.
166 */
167function notPermitted(err) {
168 console.error('notification error:', err.message);
169}