UNPKG

21.5 kBJavaScriptView Raw
1'use strict';
2
3var chalk = require('chalk');
4var symbols = require('log-symbols');
5var stripAnsi = require('strip-ansi');
6
7/**
8 * The MochaReporter.
9 *
10 * @param {!object} baseReporterDecorator The karma base reporter.
11 * @param {!Function} formatError The karma function to format an error.
12 * @param {!object} config The karma config.
13 * @constructor
14 */
15var MochaReporter = function (baseReporterDecorator, formatError, config) {
16 // extend the base reporter
17 baseReporterDecorator(this);
18
19 var self = this;
20 var firstRun = true;
21 var isRunCompleted = false;
22 var internalPrefix = '$%$';
23
24 /**
25 * Returns the text repeated n times.
26 *
27 * @param {!string} text The text.
28 * @param {!number} n The number of times the string should be repeated.
29 * @returns {string}
30 */
31 function repeatString(text, n) {
32 var res = [];
33 var i;
34
35 for (i = 0; i < n; i++) {
36 res.push(text);
37 }
38
39 return res.join('');
40 }
41
42 config.mochaReporter = config.mochaReporter || {};
43
44 var outputMode = config.mochaReporter.output || 'full';
45 var ignoreSkipped = config.mochaReporter.ignoreSkipped || false;
46 var divider = config.mochaReporter.hasOwnProperty('divider') ? config.mochaReporter.divider : '=';
47 divider = repeatString(divider || '', process.stdout.columns || 80);
48
49 // disable chalk when colors is set to false
50 chalk.enabled = config.colors !== false;
51
52 // set color functions
53 config.mochaReporter.colors = config.mochaReporter.colors || {};
54
55 // set symbol functions
56 config.mochaReporter.symbols = config.mochaReporter.symbols || {};
57
58 // set diff output
59 config.mochaReporter.showDiff = config.mochaReporter.showDiff || false;
60
61 // print first successful result
62 config.mochaReporter.printFirstSuccess = config.mochaReporter.printFirstSuccess || false;
63
64 var colors = {
65 success: {
66 symbol: config.mochaReporter.symbols.success || stripAnsi(symbols.success),
67 print: chalk[config.mochaReporter.colors.success] || chalk.green
68 },
69 info: {
70 symbol: config.mochaReporter.symbols.info || stripAnsi(symbols.info),
71 print: chalk[config.mochaReporter.colors.info] || chalk.grey
72 },
73 warning: {
74 symbol: config.mochaReporter.symbols.warning || stripAnsi(symbols.warning),
75 print: chalk[config.mochaReporter.colors.warning] || chalk.yellow
76 },
77 error: {
78 symbol: config.mochaReporter.symbols.error || stripAnsi(symbols.error),
79 print: chalk[config.mochaReporter.colors.error] || chalk.red
80 }
81 };
82
83 // init max number of log lines
84 config.mochaReporter.maxLogLines = config.mochaReporter.maxLogLines || 999;
85
86 if (isNaN(config.mochaReporter.maxLogLines)) {
87 self.write(colors.warning.print('Option "config.mochaReporter.maxLogLines" must be of type number. Default value 999 is used!'));
88 config.mochaReporter.maxLogLines = 999;
89 }
90
91 // check if mocha is installed when showDiff is enabled
92 if (config.mochaReporter.showDiff) {
93 try {
94 var mocha = require('mocha');
95 var diff = require('diff');
96 } catch (e) {
97 self.write(colors.error.print('Error loading module mocha!\nYou have enabled diff output. That only works with karma-mocha and mocha installed!\nRun the following command in your command line:\n npm install karma-mocha mocha diff\n'));
98 return;
99 }
100 }
101
102 function getLogSymbol(color) {
103 return chalk.enabled ? color.print(color.symbol) : stripAnsi(color.symbol);
104 }
105
106 /**
107 * Returns a unified diff between two strings.
108 *
109 * @param {Error} err with actual/expected
110 * @return {string} The diff.
111 */
112 function unifiedDiff(err) {
113 var indent = ' ';
114
115 function cleanUp(line) {
116 if (line[0] === '+') {
117 return indent + colors.success.print(line);
118 }
119 if (line[0] === '-') {
120 return indent + colors.error.print(line);
121 }
122 if (line.match(/\@\@/)) {
123 return null;
124 }
125 if (line.match(/\\ No newline/)) {
126 return null;
127 }
128 return indent + line;
129 }
130
131 function notBlank(line) {
132 return line !== null;
133 }
134
135 var msg = diff.createPatch('string', err.actual, err.expected);
136 var lines = msg.split('\n').splice(4);
137 return '\n ' +
138 colors.success.print('+ expected') + ' ' +
139 colors.error.print('- actual') +
140 '\n\n' +
141 lines.map(cleanUp).filter(notBlank).join('\n');
142 }
143
144 /**
145 * Return a character diff for `err`.
146 *
147 * @param {Error} err
148 * @param {string} type
149 * @return {string}
150 */
151 function errorDiff(err, type) {
152 var actual = err.actual;
153 var expected = err.expected;
154 return diff['diff' + type](actual, expected).map(function (str) {
155 if (str.added) {
156 return colors.success.print(str.value);
157 }
158 if (str.removed) {
159 return colors.error.print(str.value);
160 }
161 return str.value;
162 }).join('');
163 }
164
165 /**
166 * Pad the given `str` to `len`.
167 *
168 * @param {string} str
169 * @param {string} len
170 * @return {string}
171 */
172 function pad(str, len) {
173 str = String(str);
174 return Array(len - str.length + 1).join(' ') + str;
175 }
176
177 /**
178 * Returns an inline diff between 2 strings with coloured ANSI output
179 *
180 * @param {Error} err with actual/expected
181 * @return {string} Diff
182 */
183 function inlineDiff(err) {
184 var msg = errorDiff(err, 'WordsWithSpace');
185
186 // linenos
187 var lines = msg.split('\n');
188 if (lines.length > 4) {
189 var width = String(lines.length).length;
190 msg = lines.map(function (str, i) {
191 return pad(++i, width) + ' |' + ' ' + str;
192 }).join('\n');
193 }
194
195 // legend
196 msg = '\n' +
197 colors.success.print('expected') +
198 ' ' +
199 colors.error.print('actual') +
200 '\n\n' +
201 msg +
202 '\n';
203
204 // indent
205 msg = msg.replace(/^/gm, ' ');
206 return msg;
207 }
208
209 /**
210 * Returns a formatted time interval
211 *
212 * @param {!number} time The time.
213 * @returns {string}
214 */
215 function formatTimeInterval(time) {
216 var mins = Math.floor(time / 60000);
217 var secs = (time - mins * 60000) / 1000;
218 var str = secs + (secs === 1 ? ' sec' : ' secs');
219
220 if (mins) {
221 str = mins + (mins === 1 ? ' min ' : ' mins ') + str;
222 }
223
224 return str;
225 }
226
227 /**
228 * Checks if all items are completed
229 *
230 * @param {object} items The item objects
231 * @returns {boolean}
232 */
233 function allChildItemsAreCompleted(items) {
234 var item;
235 var isCompleted = true;
236
237 Object.keys(items).forEach(function (key) {
238 item = items[key];
239
240 if (item.type === 'it') {
241 isCompleted = isCompleted && item.isCompleted;
242 } else if (item.items) {
243 // recursive check of child items
244 isCompleted = isCompleted && allChildItemsAreCompleted(item.items);
245 }
246 });
247
248 return isCompleted;
249 }
250
251 /**
252 * Prints a single item
253 *
254 * @param {!object} item The item to print
255 * @param {number} depth The depth
256 */
257 function printItem(item, depth) {
258 // only print to output once
259 if (item.name && !item.printed && (!item.skipped || !ignoreSkipped)) {
260 // only print it block when it was ran through all browsers
261 if (item.type === 'it' && !item.isCompleted) {
262 return;
263 }
264
265 // indent
266 var line = repeatString(' ', depth) + item.name.replace(internalPrefix, '');
267
268 // it block
269 if (item.type === 'it') {
270 if (item.skipped) {
271 // print skipped tests info
272 line = colors.info.print(stripAnsi(line) + ' (skipped)');
273 } else {
274 // set color to success or error
275 line = item.success ? colors.success.print(line) : colors.error.print(line);
276 }
277 } else {
278 // print name of a suite block in bold
279 line = chalk.bold(line);
280 }
281
282 // use write method of baseReporter
283 self.write(line + '\n');
284
285 // set item as printed
286 item.printed = true;
287 }
288 }
289
290 /**
291 * Writes the test results to the output
292 *
293 * @param {!object} suite The test suite
294 * @param {number=} depth The indention.
295 */
296 function print(suite, depth) {
297 var keys = Object.keys(suite);
298 var length = keys.length;
299 var i, item;
300
301 for (i = 0; i < length; i++) {
302 item = suite[keys[i]];
303
304 // start of a new suite
305 if (item.isRoot) {
306 depth = 1;
307 }
308
309 if (item.items) {
310 var allChildItemsCompleted = allChildItemsAreCompleted(item.items);
311
312 if (allChildItemsCompleted) {
313 // print current item because all children are completed
314 printItem(item, depth);
315
316 // print all child items
317 print(item.items, depth + 1);
318 }
319 } else {
320 // print current item which has no children
321 printItem(item, depth);
322 }
323 }
324 }
325
326 /**
327 * Writes the failed test to the output
328 *
329 * @param {!object} suite The test suite
330 * @param {number=} depth The indention.
331 */
332 function printFailures(suite, depth) {
333 var keys = Object.keys(suite);
334 var length = keys.length;
335 var i, item;
336
337 for (i = 0; i < length; i++) {
338 item = suite[keys[i]];
339
340 // start of a new suite
341 if (item.isRoot) {
342 depth = 1;
343 }
344
345 // only print to output when test failed
346 if (item.name && !item.success && !item.skipped) {
347 // indent
348 var line = repeatString(' ', depth) + item.name.replace(internalPrefix, '');
349
350 // it block
351 if (item.type === 'it') {
352 // make item name error
353 line = colors.error.print(line) + '\n';
354
355 // add all browser in which the test failed with color warning
356 for (var bi = 0; bi < item.failed.length; bi++) {
357 var browserName = item.failed[bi];
358 line += repeatString(' ', depth + 1) + chalk.italic(colors.warning.print(browserName)) + '\n';
359 }
360
361 // add the error log in error color
362 item.log = item.log || [];
363 var log = item.log.length ? item.log[0].split('\n') : [];
364 var linesToLog = config.mochaReporter.maxLogLines;
365 var ii = 0;
366
367 // set number of lines to output
368 if (log.length < linesToLog) {
369 linesToLog = log.length;
370 }
371
372 // print diff
373 if (config.mochaReporter.showDiff && item.assertionErrors && item.assertionErrors[0]) {
374 var errorMessage = log.splice(0, 1)[0];
375
376 // print error message before diff
377 line += colors.error.print(repeatString(' ', depth) + errorMessage + '\n');
378
379 var expected = item.assertionErrors[0].expected;
380 var actual = item.assertionErrors[0].actual;
381 var utils = mocha.utils;
382 var err = {
383 actual: actual,
384 expected: expected
385 };
386
387 if (String(err.actual).match(/^".*"$/) && String(err.expected).match(/^".*"$/)) {
388 try {
389 err.actual = JSON.parse(err.actual);
390 err.expected = JSON.parse(err.expected);
391 } catch (e) { }
392 }
393
394 // ensure that actual and expected are strings
395 if (!(utils.isString(actual) && utils.isString(expected))) {
396 err.actual = utils.stringify(actual);
397 err.expected = utils.stringify(expected);
398 }
399
400 // create diff
401 var diff = config.mochaReporter.showDiff === 'inline' ? inlineDiff(err) : unifiedDiff(err);
402
403 line += diff + '\n';
404
405 // print formatted stack trace after diff
406 for (ii; ii < linesToLog; ii++) {
407 line += colors.error.print(formatError(log[ii]));
408 }
409 } else {
410 for (ii; ii < linesToLog; ii++) {
411 line += colors.error.print(formatError(log[ii], repeatString(' ', depth)));
412 }
413 }
414 }
415
416 // use write method of baseReporter
417 self.write(line + '\n');
418 }
419
420 if (item.items) {
421 // print all child items
422 printFailures(item.items, depth + 1);
423 }
424 }
425 }
426
427 /**
428 * Returns a singularized or plularized noun for "test" based on test count
429 *
430 * @param {!Number} testCount
431 * @returns {String}
432 */
433 function getTestNounFor(testCount) {
434 if (testCount === 1) {
435 return 'test';
436 }
437 return 'tests';
438 }
439
440 /**
441 * Called each time a test is completed in a given browser.
442 *
443 * @param {!object} browser The current browser.
444 * @param {!object} result The result of the test.
445 */
446 function specComplete(browser, result) {
447 // complete path of the test
448 var path = [].concat(result.suite, result.description);
449 var maxDepth = path.length - 1;
450
451 path.reduce(function (suite, description, depth) {
452 // add prefix to description to prevent errors when the description is a reserved name (e.g. 'toString' or 'hasOwnProperty')
453 description = internalPrefix + description;
454
455 var item;
456
457 if (suite.hasOwnProperty(description) && suite[description].type === 'it' && self.numberOfBrowsers === 1) {
458 item = {};
459 description += ' ';
460 } else {
461 item = suite[description] || {};
462 }
463
464 suite[description] = item;
465
466 item.name = description;
467 item.isRoot = depth === 0;
468 item.type = 'describe';
469 item.skipped = result.skipped;
470 item.success = (item.success === undefined ? true : item.success) && result.success;
471
472 // set item success to true when item is skipped
473 if (item.skipped) {
474 item.success = true;
475 }
476
477 // it block
478 if (depth === maxDepth) {
479 item.type = 'it';
480 item.count = item.count || 0;
481 item.count++;
482 item.failed = item.failed || [];
483 item.success = result.success && item.success;
484 item.name = (item.success ? getLogSymbol(colors.success) : getLogSymbol(colors.error)) + ' ' + item.name;
485 item.skipped = result.skipped;
486 item.visited = item.visited || [];
487 item.visited.push(browser.name);
488 self.netTime += result.time;
489
490 if (result.skipped) {
491 self.numberOfSkippedTests++;
492 }
493
494 if (result.success === false) {
495 // add browser to failed browsers array
496 item.failed.push(browser.name);
497
498 // add error log
499 item.log = result.log;
500
501 // add assertion errors if available (currently in karma-mocha)
502 item.assertionErrors = result.assertionErrors;
503 }
504
505 if (config.reportSlowerThan && result.time > config.reportSlowerThan) {
506 // add slow report warning
507 item.name += colors.warning.print((' (slow: ' + formatTimeInterval(result.time) + ')'));
508 self.numberOfSlowTests++;
509 }
510
511 if (item.count === self.numberOfBrowsers || config.mochaReporter.printFirstSuccess) {
512 item.isCompleted = true;
513
514 // print results to output when test was ran through all browsers
515 if (outputMode !== 'minimal') {
516 print(self.allResults, depth);
517 }
518 }
519 } else {
520 item.items = item.items || {};
521 }
522
523 return item.items;
524 }, self.allResults);
525 }
526
527 self.specSuccess = specComplete;
528 self.specSkipped = specComplete;
529 self.specFailure = specComplete;
530
531 self.onSpecComplete = function (browser, result) {
532 specComplete(browser, result);
533 };
534
535 self.onRunStart = function () {
536 if (!firstRun && divider) {
537 self.write('\n' + chalk.bold(divider) + '\n');
538 }
539 firstRun = false;
540 isRunCompleted = false;
541
542 self.write('\n' + chalk.underline.bold('START:') + '\n');
543 self._browsers = [];
544 self.allResults = {};
545 self.totalTime = 0;
546 self.netTime = 0;
547 self.numberOfSlowTests = 0;
548 self.numberOfSkippedTests = 0;
549 self.numberOfBrowsers = (config.browsers || []).length || 1;
550 };
551
552 self.onBrowserStart = function (browser) {
553 self._browsers.push(browser);
554 };
555
556 self.onRunComplete = function (browsers, results) {
557 browsers.forEach(function (browser) {
558 self.totalTime += browser.lastResult.totalTime;
559 });
560
561 // print extra error message for some special cases, e.g. when having the error "Some of your tests did a full page reload!" the onRunComplete() method is called twice
562 if (results.error && isRunCompleted) {
563 self.write('\n');
564 self.write(getLogSymbol(colors.error) + colors.error.print(' Error while running the tests! Exit code: ' + results.exitCode));
565 self.write('\n\n');
566 return;
567 }
568
569 isRunCompleted = true;
570
571 self.write('\n' + colors.success.print('Finished in ' + formatTimeInterval(self.totalTime) + ' / ' +
572 formatTimeInterval(self.netTime) + ' @ ' + new Date().toTimeString()));
573 self.write('\n\n');
574
575 if (browsers.length > 0 && !results.disconnected) {
576 self.write(chalk.underline.bold('SUMMARY:') + '\n');
577 self.write(colors.success.print(getLogSymbol(colors.success) + ' ' + results.success + ' ' + getTestNounFor(results.success) + ' completed'));
578 self.write('\n');
579
580 if (self.numberOfSkippedTests > 0) {
581 self.write(colors.info.print(getLogSymbol(colors.info) + ' ' + self.numberOfSkippedTests + ' ' + getTestNounFor(self.numberOfSkippedTests) + ' skipped'));
582 self.write('\n');
583 }
584
585 if (self.numberOfSlowTests > 0) {
586 self.write(colors.warning.print(getLogSymbol(colors.warning) + ' ' + self.numberOfSlowTests + ' ' + getTestNounFor(self.numberOfSlowTests) + ' slow'));
587 self.write('\n');
588 }
589
590 if (results.failed) {
591 self.write(colors.error.print(getLogSymbol(colors.error) + ' ' + results.failed + ' ' + getTestNounFor(results.failed) + ' failed'));
592 self.write('\n');
593
594 if (outputMode !== 'noFailures') {
595 self.write('\n' + chalk.underline.bold('FAILED TESTS:') + '\n');
596 printFailures(self.allResults);
597 }
598 }
599 }
600
601 if (outputMode === 'autowatch') {
602 outputMode = 'minimal';
603 }
604 };
605};
606
607// inject karma runner baseReporter and config
608MochaReporter.$inject = ['baseReporterDecorator', 'formatError', 'config'];
609
610// PUBLISH DI MODULE
611module.exports = {
612 'reporter:mocha': ['type', MochaReporter]
613};