| /** |
| * Module dependencies. |
| */ |
| |
| var tty = require('tty') |
| , diff = require('diff') |
| , ms = require('../ms') |
| , utils = require('../utils') |
| , supportsColor = require('color-support')() |
| |
| /** |
| * 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); |
| |
| /** |
| * Expose `Base`. |
| */ |
| |
| exports = module.exports = Base; |
| |
| /** |
| * Enable coloring by default, except in the browser interface. |
| */ |
| |
| exports.useColors = process.env |
| ? (supportsColor || (process.env.TAP_COLORS !== undefined)) |
| : false; |
| |
| if (exports.useColors && +process.env.TAP_COLORS === 0) |
| exports.useColors = false |
| |
| /** |
| * Inline diffs instead of +/- |
| */ |
| |
| exports.inlineDiffs = false; |
| |
| /** |
| * Default color map. |
| */ |
| |
| exports.colors = { |
| 'pass': 90 |
| , 'fail': 31 |
| , 'bright pass': 92 |
| , 'bright fail': 91 |
| , 'bright yellow': 93 |
| , 'pending': 35 |
| , 'skip': 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 String(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(){ |
| isatty && process.stdout.write('\u001b[?25l'); |
| }, |
| |
| show: function(){ |
| isatty && process.stdout.write('\u001b[?25h'); |
| }, |
| |
| deleteLine: function(){ |
| isatty && process.stdout.write('\u001b[2K'); |
| }, |
| |
| beginningOfLine: function(){ |
| isatty && process.stdout.write('\u001b[0G'); |
| }, |
| |
| CR: function(){ |
| if (isatty) { |
| exports.cursor.deleteLine(); |
| exports.cursor.beginningOfLine(); |
| } else { |
| process.stdout.write('\r'); |
| } |
| } |
| }; |
| |
| /** |
| * Outut the given `failures` as a list. |
| * |
| * @param {Array} failures |
| * @api public |
| */ |
| |
| exports.list = function(failures){ |
| console.log(); |
| 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 |
| |
| var 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)) { |
| |
| if ('string' !== typeof actual) { |
| escape = false; |
| err.actual = actual = utils.stringify(actual); |
| err.expected = expected = utils.stringify(expected); |
| } |
| |
| fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); |
| var match = message.match(/^([^:]+): expected/); |
| msg = '\n ' + color('error message', match ? match[1] : msg); |
| |
| if (exports.inlineDiffs) { |
| msg += inlineDiff(err, escape); |
| } else { |
| msg += unifiedDiff(err, escape); |
| } |
| } |
| |
| // indent stack trace without msg |
| stack = utils.stackTraceFilter()(stack.slice(index ? index + 1 : index) |
| .replace(/^/gm, ' ')); |
| |
| console.log(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; |
| if (!stats.duration) |
| stats.duration = stats.end - 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.log(fmt, stats.failures); |
| |
| Base.list(this.failures); |
| } |
| }; |
| |
| /** |
| * 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; |
| } |
| |
| |
| /** |
| * Returns an inline diff between 2 strings with coloured ANSI output |
| * |
| * @param {Error} Error with actual/expected |
| * @return {String} Diff |
| * @api private |
| */ |
| |
| function inlineDiff(err, escape) { |
| var msg = errorDiff(err, 'WordsWithSpace', 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, ' '); |
| return msg; |
| } |
| |
| /** |
| * Returns a unified diff between 2 strings |
| * |
| * @param {Error} Error with actual/expected |
| * @return {String} Diff |
| * @api private |
| */ |
| |
| function unifiedDiff(err, escape) { |
| var indent = ' '; |
| function cleanUp(line) { |
| if (escape) { |
| line = escapeInvisibles(line); |
| } |
| if (line[0] === '+') return indent + colorLines('diff added', line); |
| if (line[0] === '-') return indent + colorLines('diff removed', line); |
| if (line.match(/\@\@/)) return null; |
| if (line.match(/\\ No newline/)) return null; |
| else return indent + line; |
| } |
| function notBlank(line) { |
| return line != null; |
| } |
| var msg = diff.createPatch('string', err.actual, err.expected); |
| var lines = msg.split('\n').splice(4); |
| return '\n ' |
| + colorLines('diff added', '+ expected') + ' ' |
| + colorLines('diff removed', '- actual') |
| + '\n\n' |
| + lines.map(cleanUp).filter(notBlank).join('\n'); |
| } |
| |
| /** |
| * Return a character diff for `err`. |
| * |
| * @param {Error} err |
| * @return {String} |
| * @api private |
| */ |
| |
| function errorDiff(err, type, escape) { |
| var actual = escape ? escapeInvisibles(err.actual) : err.actual; |
| var expected = escape ? escapeInvisibles(err.expected) : err.expected; |
| return diff['diff' + type](actual, expected).map(function(str){ |
| if (str.added) return colorLines('diff added', str.value); |
| if (str.removed) return colorLines('diff removed', str.value); |
| return str.value; |
| }).join(''); |
| } |
| |
| /** |
| * Returns a string with all invisible characters in plain text |
| * |
| * @param {String} line |
| * @return {String} |
| * @api private |
| */ |
| function escapeInvisibles(line) { |
| return line.replace(/\t/g, '<tab>') |
| .replace(/\r/g, '<CR>') |
| .replace(/\n/g, '<LF>\n'); |
| } |
| |
| /** |
| * 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'); |
| } |
| |
| /** |
| * 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; |
| } |