UNPKG

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