UNPKG

11.5 kBJavaScriptView Raw
1'use strict';
2/**
3 * @module Base
4 */
5/**
6 * Module dependencies.
7 */
8
9var diff = require('diff');
10var milliseconds = require('ms');
11var utils = require('../utils');
12var supportsColor = require('supports-color');
13var constants = require('../runner').constants;
14var EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
15var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL;
16
17const isBrowser = utils.isBrowser();
18
19function getBrowserWindowSize() {
20 if ('innerHeight' in global) {
21 return [global.innerHeight, global.innerWidth];
22 }
23 // In a Web Worker, the DOM Window is not available.
24 return [640, 480];
25}
26
27/**
28 * Expose `Base`.
29 */
30
31exports = module.exports = Base;
32
33/**
34 * Check if both stdio streams are associated with a tty.
35 */
36
37var isatty = isBrowser || (process.stdout.isTTY && process.stderr.isTTY);
38
39/**
40 * Save log references to avoid tests interfering (see GH-3604).
41 */
42var consoleLog = console.log;
43
44/**
45 * Enable coloring by default, except in the browser interface.
46 */
47
48exports.useColors =
49 !isBrowser &&
50 (supportsColor.stdout || process.env.MOCHA_COLORS !== undefined);
51
52/**
53 * Inline diffs instead of +/-
54 */
55
56exports.inlineDiffs = false;
57
58/**
59 * Default color map.
60 */
61
62exports.colors = {
63 pass: 90,
64 fail: 31,
65 'bright pass': 92,
66 'bright fail': 91,
67 'bright yellow': 93,
68 pending: 36,
69 suite: 0,
70 'error title': 0,
71 'error message': 31,
72 'error stack': 90,
73 checkmark: 32,
74 fast: 90,
75 medium: 33,
76 slow: 31,
77 green: 32,
78 light: 90,
79 'diff gutter': 90,
80 'diff added': 32,
81 'diff removed': 31,
82 'diff added inline': '30;42',
83 'diff removed inline': '30;41'
84};
85
86/**
87 * Default symbol map.
88 */
89
90exports.symbols = {
91 ok: '✓',
92 err: '✖',
93 dot: '․',
94 comma: ',',
95 bang: '!'
96};
97
98// With node.js on Windows: use symbols available in terminal default fonts
99if (process.platform === 'win32') {
100 exports.symbols.ok = '\u221A';
101 exports.symbols.err = '\u00D7';
102 exports.symbols.dot = '.';
103}
104
105/**
106 * Color `str` with the given `type`,
107 * allowing colors to be disabled,
108 * as well as user-defined color
109 * schemes.
110 *
111 * @private
112 * @param {string} type
113 * @param {string} str
114 * @return {string}
115 */
116var color = (exports.color = function(type, str) {
117 if (!exports.useColors) {
118 return String(str);
119 }
120 return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m';
121});
122
123/**
124 * Expose term window size, with some defaults for when stderr is not a tty.
125 */
126
127exports.window = {
128 width: 75
129};
130
131if (isatty) {
132 if (isBrowser) {
133 exports.window.width = getBrowserWindowSize()[1];
134 } else {
135 exports.window.width = process.stdout.getWindowSize(1)[0];
136 }
137}
138
139/**
140 * Expose some basic cursor interactions that are common among reporters.
141 */
142
143exports.cursor = {
144 hide: function() {
145 isatty && process.stdout.write('\u001b[?25l');
146 },
147
148 show: function() {
149 isatty && process.stdout.write('\u001b[?25h');
150 },
151
152 deleteLine: function() {
153 isatty && process.stdout.write('\u001b[2K');
154 },
155
156 beginningOfLine: function() {
157 isatty && process.stdout.write('\u001b[0G');
158 },
159
160 CR: function() {
161 if (isatty) {
162 exports.cursor.deleteLine();
163 exports.cursor.beginningOfLine();
164 } else {
165 process.stdout.write('\r');
166 }
167 }
168};
169
170var showDiff = (exports.showDiff = function(err) {
171 return (
172 err &&
173 err.showDiff !== false &&
174 sameType(err.actual, err.expected) &&
175 err.expected !== undefined
176 );
177});
178
179function stringifyDiffObjs(err) {
180 if (!utils.isString(err.actual) || !utils.isString(err.expected)) {
181 err.actual = utils.stringify(err.actual);
182 err.expected = utils.stringify(err.expected);
183 }
184}
185
186/**
187 * Returns a diff between 2 strings with coloured ANSI output.
188 *
189 * @description
190 * The diff will be either inline or unified dependent on the value
191 * of `Base.inlineDiff`.
192 *
193 * @param {string} actual
194 * @param {string} expected
195 * @return {string} Diff
196 */
197var generateDiff = (exports.generateDiff = function(actual, expected) {
198 try {
199 return exports.inlineDiffs
200 ? inlineDiff(actual, expected)
201 : unifiedDiff(actual, expected);
202 } catch (err) {
203 var msg =
204 '\n ' +
205 color('diff added', '+ expected') +
206 ' ' +
207 color('diff removed', '- actual: failed to generate Mocha diff') +
208 '\n';
209 return msg;
210 }
211});
212
213/**
214 * Outputs the given `failures` as a list.
215 *
216 * @public
217 * @memberof Mocha.reporters.Base
218 * @variation 1
219 * @param {Object[]} failures - Each is Test instance with corresponding
220 * Error property
221 */
222exports.list = function(failures) {
223 var multipleErr, multipleTest;
224 Base.consoleLog();
225 failures.forEach(function(test, i) {
226 // format
227 var fmt =
228 color('error title', ' %s) %s:\n') +
229 color('error message', ' %s') +
230 color('error stack', '\n%s\n');
231
232 // msg
233 var msg;
234 var err;
235 if (test.err && test.err.multiple) {
236 if (multipleTest !== test) {
237 multipleTest = test;
238 multipleErr = [test.err].concat(test.err.multiple);
239 }
240 err = multipleErr.shift();
241 } else {
242 err = test.err;
243 }
244 var message;
245 if (err.message && typeof err.message.toString === 'function') {
246 message = err.message + '';
247 } else if (typeof err.inspect === 'function') {
248 message = err.inspect() + '';
249 } else {
250 message = '';
251 }
252 var stack = err.stack || message;
253 var index = message ? stack.indexOf(message) : -1;
254
255 if (index === -1) {
256 msg = message;
257 } else {
258 index += message.length;
259 msg = stack.slice(0, index);
260 // remove msg from stack
261 stack = stack.slice(index + 1);
262 }
263
264 // uncaught
265 if (err.uncaught) {
266 msg = 'Uncaught ' + msg;
267 }
268 // explicitly show diff
269 if (!exports.hideDiff && showDiff(err)) {
270 stringifyDiffObjs(err);
271 fmt =
272 color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n');
273 var match = message.match(/^([^:]+): expected/);
274 msg = '\n ' + color('error message', match ? match[1] : msg);
275
276 msg += generateDiff(err.actual, err.expected);
277 }
278
279 // indent stack trace
280 stack = stack.replace(/^/gm, ' ');
281
282 // indented test title
283 var testTitle = '';
284 test.titlePath().forEach(function(str, index) {
285 if (index !== 0) {
286 testTitle += '\n ';
287 }
288 for (var i = 0; i < index; i++) {
289 testTitle += ' ';
290 }
291 testTitle += str;
292 });
293
294 Base.consoleLog(fmt, i + 1, testTitle, msg, stack);
295 });
296};
297
298/**
299 * Constructs a new `Base` reporter instance.
300 *
301 * @description
302 * All other reporters generally inherit from this reporter.
303 *
304 * @public
305 * @class
306 * @memberof Mocha.reporters
307 * @param {Runner} runner - Instance triggers reporter actions.
308 * @param {Object} [options] - runner options
309 */
310function Base(runner, options) {
311 var failures = (this.failures = []);
312
313 if (!runner) {
314 throw new TypeError('Missing runner argument');
315 }
316 this.options = options || {};
317 this.runner = runner;
318 this.stats = runner.stats; // assigned so Reporters keep a closer reference
319
320 runner.on(EVENT_TEST_PASS, function(test) {
321 if (test.duration > test.slow()) {
322 test.speed = 'slow';
323 } else if (test.duration > test.slow() / 2) {
324 test.speed = 'medium';
325 } else {
326 test.speed = 'fast';
327 }
328 });
329
330 runner.on(EVENT_TEST_FAIL, function(test, err) {
331 if (showDiff(err)) {
332 stringifyDiffObjs(err);
333 }
334 // more than one error per test
335 if (test.err && err instanceof Error) {
336 test.err.multiple = (test.err.multiple || []).concat(err);
337 } else {
338 test.err = err;
339 }
340 failures.push(test);
341 });
342}
343
344/**
345 * Outputs common epilogue used by many of the bundled reporters.
346 *
347 * @public
348 * @memberof Mocha.reporters
349 */
350Base.prototype.epilogue = function() {
351 var stats = this.stats;
352 var fmt;
353
354 Base.consoleLog();
355
356 // passes
357 fmt =
358 color('bright pass', ' ') +
359 color('green', ' %d passing') +
360 color('light', ' (%s)');
361
362 Base.consoleLog(fmt, stats.passes || 0, milliseconds(stats.duration));
363
364 // pending
365 if (stats.pending) {
366 fmt = color('pending', ' ') + color('pending', ' %d pending');
367
368 Base.consoleLog(fmt, stats.pending);
369 }
370
371 // failures
372 if (stats.failures) {
373 fmt = color('fail', ' %d failing');
374
375 Base.consoleLog(fmt, stats.failures);
376
377 Base.list(this.failures);
378 Base.consoleLog();
379 }
380
381 Base.consoleLog();
382};
383
384/**
385 * Pads the given `str` to `len`.
386 *
387 * @private
388 * @param {string} str
389 * @param {string} len
390 * @return {string}
391 */
392function pad(str, len) {
393 str = String(str);
394 return Array(len - str.length + 1).join(' ') + str;
395}
396
397/**
398 * Returns inline diff between 2 strings with coloured ANSI output.
399 *
400 * @private
401 * @param {String} actual
402 * @param {String} expected
403 * @return {string} Diff
404 */
405function inlineDiff(actual, expected) {
406 var msg = errorDiff(actual, expected);
407
408 // linenos
409 var lines = msg.split('\n');
410 if (lines.length > 4) {
411 var width = String(lines.length).length;
412 msg = lines
413 .map(function(str, i) {
414 return pad(++i, width) + ' |' + ' ' + str;
415 })
416 .join('\n');
417 }
418
419 // legend
420 msg =
421 '\n' +
422 color('diff removed inline', 'actual') +
423 ' ' +
424 color('diff added inline', 'expected') +
425 '\n\n' +
426 msg +
427 '\n';
428
429 // indent
430 msg = msg.replace(/^/gm, ' ');
431 return msg;
432}
433
434/**
435 * Returns unified diff between two strings with coloured ANSI output.
436 *
437 * @private
438 * @param {String} actual
439 * @param {String} expected
440 * @return {string} The diff.
441 */
442function unifiedDiff(actual, expected) {
443 var indent = ' ';
444 function cleanUp(line) {
445 if (line[0] === '+') {
446 return indent + colorLines('diff added', line);
447 }
448 if (line[0] === '-') {
449 return indent + colorLines('diff removed', line);
450 }
451 if (line.match(/@@/)) {
452 return '--';
453 }
454 if (line.match(/\\ No newline/)) {
455 return null;
456 }
457 return indent + line;
458 }
459 function notBlank(line) {
460 return typeof line !== 'undefined' && line !== null;
461 }
462 var msg = diff.createPatch('string', actual, expected);
463 var lines = msg.split('\n').splice(5);
464 return (
465 '\n ' +
466 colorLines('diff added', '+ expected') +
467 ' ' +
468 colorLines('diff removed', '- actual') +
469 '\n\n' +
470 lines
471 .map(cleanUp)
472 .filter(notBlank)
473 .join('\n')
474 );
475}
476
477/**
478 * Returns character diff for `err`.
479 *
480 * @private
481 * @param {String} actual
482 * @param {String} expected
483 * @return {string} the diff
484 */
485function errorDiff(actual, expected) {
486 return diff
487 .diffWordsWithSpace(actual, expected)
488 .map(function(str) {
489 if (str.added) {
490 return colorLines('diff added inline', str.value);
491 }
492 if (str.removed) {
493 return colorLines('diff removed inline', str.value);
494 }
495 return str.value;
496 })
497 .join('');
498}
499
500/**
501 * Colors lines for `str`, using the color `name`.
502 *
503 * @private
504 * @param {string} name
505 * @param {string} str
506 * @return {string}
507 */
508function colorLines(name, str) {
509 return str
510 .split('\n')
511 .map(function(str) {
512 return color(name, str);
513 })
514 .join('\n');
515}
516
517/**
518 * Object#toString reference.
519 */
520var objToString = Object.prototype.toString;
521
522/**
523 * Checks that a / b have the same type.
524 *
525 * @private
526 * @param {Object} a
527 * @param {Object} b
528 * @return {boolean}
529 */
530function sameType(a, b) {
531 return objToString.call(a) === objToString.call(b);
532}
533
534Base.consoleLog = consoleLog;
535
536Base.abstract = true;