This repository has been archived by the owner on May 29, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 106
/
mocha.js
337 lines (284 loc) · 9.62 KB
/
mocha.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/*
* grunt
* https://github.com/cowboy/grunt
*
* Copyright (c) 2012 "Cowboy" Ben Alman
* Licensed under the MIT license.
* http://benalman.com/about/license/
*
* Mocha task
* Copyright (c) 2012 Kelly Miyashiro
* Licensed under the MIT license.
* http://benalman.com/about/license/
*/
'use strict';
// Nodejs libs.
var _ = require('lodash');
var util = require('util');
var path = require('path');
var EventEmitter = require('events').EventEmitter;
var reporters = require('mocha').reporters;
// Helpers
var helpers = require('../support/mocha-helpers');
module.exports = function(grunt) {
// External lib.
var phantomjs = require('grunt-lib-phantomjs').init(grunt);
var reporter;
// Growl is optional
var growl;
try {
growl = require('growl');
} catch(e) {
growl = function(){};
grunt.verbose.write('Growl not found, \'npm install growl\' for Growl support');
}
// Get an asset file, local to the root of the project.
var asset = path.join.bind(null, __dirname, '..');
// Manage runners listening to phantomjs
var phantomjsEventManager = (function() {
var listeners = {};
var suites = [];
// Hook on Phantomjs Mocha reporter events.
phantomjs.on('mocha.*', function(test) {
var name, fullTitle, slow, err;
var evt = this.event.replace('mocha.', '');
if (evt === 'end') {
phantomjs.halt();
}
// Expand test values (and façace the Mocha test object)
if (test) {
fullTitle = test.fullTitle;
test.fullTitle = function() {
return fullTitle;
};
slow = this.slow;
test.slow = function() {
return slow;
};
test.parent = suites[suites.length - 1] || null;
test.isPending = function () { return test.pending; };
err = test.err;
}
if (evt === 'suite') {
suites.push(test);
} else if (evt === 'suite end') {
suites.pop(test);
}
// Trigger events for each runner listening
for (name in listeners) {
listeners[name].emit.call(listeners[name], evt, test, err);
}
});
return {
add: function(name, runner) {
listeners[name] = runner;
},
remove: function(name) {
delete listeners[name];
}
};
}());
// Built-in error handlers.
phantomjs.on('fail.load', function(url) {
phantomjs.halt();
grunt.verbose.write('Running PhantomJS...').or.write('...');
grunt.log.error();
grunt.warn('PhantomJS unable to load "' + url + '" URI.', 90);
});
phantomjs.on('fail.timeout', function() {
phantomjs.halt();
grunt.log.writeln();
grunt.warn('PhantomJS timed out, possibly due to a missing Mocha run() call.', 90);
});
// Debugging messages.
phantomjs.on('debug', grunt.log.debug.bind(grunt.log, 'phantomjs'));
// ==========================================================================
// TASKS
// ==========================================================================
grunt.registerMultiTask('mocha', 'Run Mocha unit tests in a headless PhantomJS instance.', function() {
var dest = this.data.dest;
// Merge task-specific and/or target-specific options with these defaults.
var options = this.options({
// Output console.log calls
log: false,
// Mocha reporter
reporter: 'Dot',
// Default PhantomJS timeout.
timeout: 5000,
// Mocha-PhantomJS bridge file to be injected.
inject: asset('phantomjs/bridge.js'),
// Main PhantomJS script file
phantomScript: asset('phantomjs/main.js'),
// Explicit non-file URLs to test.
urls: [],
// Fail with grunt.warn on first test failure
bail: false,
// Log script errors as grunt errors
logErrors: false,
// Growl notification when tests pass.
growlOnSuccess: true,
// Run tests, set to false if you would rather call `mocha.run` yourself
// due to async loading of your assets.
run: true,
reporterOptions: { output: dest },
});
// Output console messages if log == true
if (options.log) {
phantomjs.removeAllListeners(['console']);
phantomjs.on('console', grunt.log.writeln);
} else {
phantomjs.off('console', grunt.log.writeln);
}
// Output errors on script errors
if (options.logErrors) {
phantomjs.on('error.*', function(error, stack) {
var formattedStack = _.map(stack, function(frame) {
return " at " + (frame.function ? frame.function : "undefined") + " (" + frame.file + ":" + frame.line + ")";
}).join("\n");
grunt.fail.warn(error + "\n" + formattedStack, 3);
});
}
var optsStr = JSON.stringify(options, null, ' ');
grunt.verbose.writeln('Options: ' + optsStr);
// Clean Phantomjs options to prevent any conflicts
var PhantomjsOptions = _.omit(options, 'reporter', 'urls', 'log', 'bail');
var phantomOptsStr = JSON.stringify(PhantomjsOptions, null, ' ');
grunt.verbose.writeln('Phantom options: ' + phantomOptsStr);
// Combine any specified URLs with src files.
var urls = options.urls.concat(_.compact(this.filesSrc));
// Remember all stats from all tests
var testStats = [];
// This task is asynchronous.
var done = this.async();
// Hijack console.log to capture reporter output
var output = [];
var consoleLog = console.log;
// Only hijack if we really need to
// Some "good" reporters like XUnit accept an `output` option
// and we pass `dest` there but there's no good way to detect
// if a reporter supports this so stub out console.log just in case.
if (dest) {
if (grunt.file.isFile(dest)) {
grunt.file.delete(dest);
}
console.log = function() {
consoleLog.apply(console, arguments);
output.push(util.format.apply(util, arguments));
};
}
// Process each filepath in-order.
grunt.util.async.forEachSeries(urls, function(url, next) {
grunt.log.writeln('Testing: ' + url);
// create a new mocha runner façade
var runner = new EventEmitter();
phantomjsEventManager.add(url, runner);
// Clear runner event listener when test is over
runner.on('end', function() {
phantomjsEventManager.remove(url);
});
// Set Mocha reporter
var Reporter = null;
if (reporters[options.reporter]) {
Reporter = reporters[options.reporter];
} else {
// Resolve external reporter module
var externalReporter;
try {
externalReporter = require.resolve(options.reporter);
} catch (e) {
// Resolve to local path
externalReporter = path.resolve(options.reporter);
}
if (externalReporter) {
try {
Reporter = require(externalReporter);
}
catch (e) { }
}
}
if (Reporter === null) {
grunt.fatal('Specified reporter is unknown or unresolvable: ' + options.reporter);
}
reporter = new Reporter(runner, options);
// Launch PhantomJS.
phantomjs.spawn(url, {
// Exit code to use if PhantomJS fails in an uncatchable way.
failCode: 90,
// Additional PhantomJS options.
options: PhantomjsOptions,
// Do stuff when done.
done: function(err) {
var stats = runner.stats;
testStats.push(stats);
if (err) {
// Show Growl notice
// @TODO: Get an example of this
// growl('PhantomJS Error!');
// If there was a PhantomJS error, abort the series.
grunt.fatal(err);
done(false);
} else {
// If failures, show growl notice
if (stats.failures > 0) {
var reduced = helpers.reduceStats([stats]);
var failMsg = reduced.failures + '/' + reduced.tests +
' tests failed (' + reduced.duration + 's)';
// Show Growl notice, if avail
growl(failMsg, {
image: asset('growl/error.png'),
title: 'Failure in ' + grunt.task.current.target,
priority: 3
});
// Bail tests if bail option is true
if (options.bail) grunt.warn(failMsg);
}
// Process next file/url
next();
}
}
});
},
// All tests have been run.
function() {
if (dest) {
// Restore console.log to original and write the output
console.log = consoleLog;
if (!grunt.file.exists(dest)) {
// Write only if our reporter ignored our `output` option
grunt.file.write(dest, output.join('\n'));
}
}
var stats = helpers.reduceStats(testStats);
if (stats.failures === 0) {
var okMsg = stats.tests + ' passed!' + ' (' + stats.duration + 's)';
if (options.growlOnSuccess) {
growl(okMsg, {
image: asset('growl/ok.png'),
title: okMsg,
priority: 3
});
}
grunt.log.ok(okMsg);
// Async test pass
done(true);
} else {
var failMsg = stats.failures + '/' + stats.tests + ' tests failed (' +
stats.duration + 's)';
// Show Growl notice, if avail
growl(failMsg, {
image: asset('growl/error.png'),
title: failMsg,
priority: 3
});
// Bail tests if bail option is true
if (options.bail) {
grunt.warn(failMsg);
} else {
grunt.log.error(failMsg);
}
// Async test fail
done(false);
}
});
});
};