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