/** * Module dependencies. */ var tty = require('tty') , diff = require('diff') , ms = require('../ms'); /** * Save timer references to avoid Sinon interfering (see GH-237). */ var Date = global.Date , setTimeout = global.setTimeout , setInterval = global.setInterval , clearTimeout = global.clearTimeout , clearInterval = global.clearInterval; /** * Check if both stdio streams are associated with a tty. */ var isatty = tty.isatty(1) && tty.isatty(2); /** * Expose `Base`. */ exports = module.exports = Base; /** * Enable coloring by default. */ exports.useColors = isatty; /** * Default color map. */ exports.colors = { 'pass': 90 , 'fail': 31 , 'bright pass': 92 , 'bright fail': 91 , 'bright yellow': 93 , 'pending': 36 , 'suite': 0 , 'error title': 0 , 'error message': 31 , 'error stack': 90 , 'checkmark': 32 , 'fast': 90 , 'medium': 33 , 'slow': 31 , 'green': 32 , 'light': 90 , 'diff gutter': 90 , 'diff added': 42 , 'diff removed': 41 }; /** * Default symbol map. */ exports.symbols = { ok: '✓', err: '✖', dot: '․' }; // With node.js on Windows: use symbols available in terminal default fonts if ('win32' == process.platform) { exports.symbols.ok = '\u221A'; exports.symbols.err = '\u00D7'; exports.symbols.dot = '.'; } /** * Color `str` with the given `type`, * allowing colors to be disabled, * as well as user-defined color * schemes. * * @param {String} type * @param {String} str * @return {String} * @api private */ var color = exports.color = function(type, str) { if (!exports.useColors) return str; return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; }; /** * Expose term window size, with some * defaults for when stderr is not a tty. */ exports.window = { width: isatty ? process.stdout.getWindowSize ? process.stdout.getWindowSize(1)[0] : tty.getWindowSize()[1] : 75 }; /** * Expose some basic cursor interactions * that are common among reporters. */ exports.cursor = { hide: function(){ process.stdout.write('\u001b[?25l'); }, show: function(){ process.stdout.write('\u001b[?25h'); }, deleteLine: function(){ process.stdout.write('\u001b[2K'); }, beginningOfLine: function(){ process.stdout.write('\u001b[0G'); }, CR: function(){ exports.cursor.deleteLine(); exports.cursor.beginningOfLine(); } }; /** * Outut the given `failures` as a list. * * @param {Array} failures * @api public */ exports.list = function(failures){ console.error(); failures.forEach(function(test, i){ // format var fmt = color('error title', ' %s) %s:\n') + color('error message', ' %s') + color('error stack', '\n%s\n'); // msg var err = test.err , message = err.message || '' , stack = err.stack || message , index = stack.indexOf(message) + message.length , msg = stack.slice(0, index) , actual = err.actual , expected = err.expected , escape = true; // uncaught if (err.uncaught) { msg = 'Uncaught ' + msg; } // explicitly show diff if (err.showDiff && sameType(actual, expected)) { escape = false; err.actual = actual = stringify(actual); err.expected = expected = stringify(expected); } // actual / expected diff if ('string' == typeof actual && 'string' == typeof expected) { msg = errorDiff(err, 'Words', escape); // linenos var lines = msg.split('\n'); if (lines.length > 4) { var width = String(lines.length).length; msg = lines.map(function(str, i){ return pad(++i, width) + ' |' + ' ' + str; }).join('\n'); } // legend msg = '\n' + color('diff removed', 'actual') + ' ' + color('diff added', 'expected') + '\n\n' + msg + '\n'; // indent msg = msg.replace(/^/gm, ' '); fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); } // indent stack trace without msg stack = stack.slice(index ? index + 1 : index) .replace(/^/gm, ' '); console.error(fmt, (i + 1), test.fullTitle(), msg, stack); }); }; /** * Initialize a new `Base` reporter. * * All other reporters generally * inherit from this reporter, providing * stats such as test duration, number * of tests passed / failed etc. * * @param {Runner} runner * @api public */ function Base(runner) { var self = this , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } , failures = this.failures = []; if (!runner) return; this.runner = runner; runner.stats = stats; runner.on('start', function(){ stats.start = new Date; }); runner.on('suite', function(suite){ stats.suites = stats.suites || 0; suite.root || stats.suites++; }); runner.on('test end', function(test){ stats.tests = stats.tests || 0; stats.tests++; }); runner.on('pass', function(test){ stats.passes = stats.passes || 0; var medium = test.slow() / 2; test.speed = test.duration > test.slow() ? 'slow' : test.duration > medium ? 'medium' : 'fast'; stats.passes++; }); runner.on('fail', function(test, err){ stats.failures = stats.failures || 0; stats.failures++; test.err = err; failures.push(test); }); runner.on('end', function(){ stats.end = new Date; stats.duration = new Date - stats.start; }); runner.on('pending', function(){ stats.pending++; }); } /** * Output common epilogue used by many of * the bundled reporters. * * @api public */ Base.prototype.epilogue = function(){ var stats = this.stats; var tests; var fmt; console.log(); // passes fmt = color('bright pass', ' ') + color('green', ' %d passing') + color('light', ' (%s)'); console.log(fmt, stats.passes || 0, ms(stats.duration)); // pending if (stats.pending) { fmt = color('pending', ' ') + color('pending', ' %d pending'); console.log(fmt, stats.pending); } // failures if (stats.failures) { fmt = color('fail', ' %d failing'); console.error(fmt, stats.failures); Base.list(this.failures); console.error(); } console.log(); }; /** * Pad the given `str` to `len`. * * @param {String} str * @param {String} len * @return {String} * @api private */ function pad(str, len) { str = String(str); return Array(len - str.length + 1).join(' ') + str; } /** * Return a character diff for `err`. * * @param {Error} err * @return {String} * @api private */ function errorDiff(err, type, escape) { return diff['diff' + type](err.actual, err.expected).map(function(str){ if (escape) { str.value = str.value .replace(/\t/g, '') .replace(/\r/g, '') .replace(/\n/g, '\n'); } if (str.added) return colorLines('diff added', str.value); if (str.removed) return colorLines('diff removed', str.value); return str.value; }).join(''); } /** * Color lines for `str`, using the color `name`. * * @param {String} name * @param {String} str * @return {String} * @api private */ function colorLines(name, str) { return str.split('\n').map(function(str){ return color(name, str); }).join('\n'); } /** * Stringify `obj`. * * @param {Mixed} obj * @return {String} * @api private */ function stringify(obj) { if (obj instanceof RegExp) return obj.toString(); return JSON.stringify(obj, null, 2); } /** * Check that a / b have the same type. * * @param {Object} a * @param {Object} b * @return {Boolean} * @api private */ function sameType(a, b) { a = Object.prototype.toString.call(a); b = Object.prototype.toString.call(b); return a == b; }