| 'use strict'; |
| /** |
| * @module Base |
| */ |
| /** |
| * Module dependencies. |
| */ |
| |
| var tty = require('tty'); |
| var diff = require('diff'); |
| var milliseconds = require('ms'); |
| var utils = require('../utils'); |
| var supportsColor = process.browser ? null : require('supports-color'); |
| var constants = require('../runner').constants; |
| var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; |
| var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; |
| |
| /** |
| * Expose `Base`. |
| */ |
| |
| exports = module.exports = Base; |
| |
| /** |
| * Check if both stdio streams are associated with a tty. |
| */ |
| |
| var isatty = tty.isatty(1) && tty.isatty(2); |
| |
| /** |
| * Save log references to avoid tests interfering (see GH-3604). |
| */ |
| var consoleLog = console.log; |
| |
| /** |
| * Enable coloring by default, except in the browser interface. |
| */ |
| |
| exports.useColors = |
| !process.browser && |
| (supportsColor.stdout || process.env.MOCHA_COLORS !== undefined); |
| |
| /** |
| * 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: 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': 32, |
| 'diff removed': 31 |
| }; |
| |
| /** |
| * Default symbol map. |
| */ |
| |
| exports.symbols = { |
| ok: '✓', |
| err: '✖', |
| dot: '․', |
| comma: ',', |
| bang: '!' |
| }; |
| |
| // With node.js on Windows: use symbols available in terminal default fonts |
| if (process.platform === 'win32') { |
| 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. |
| * |
| * @private |
| * @param {string} type |
| * @param {string} str |
| * @return {string} |
| */ |
| 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: 75 |
| }; |
| |
| if (isatty) { |
| exports.window.width = process.stdout.getWindowSize |
| ? process.stdout.getWindowSize(1)[0] |
| : tty.getWindowSize()[1]; |
| } |
| |
| /** |
| * 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'); |
| } |
| } |
| }; |
| |
| function showDiff(err) { |
| return ( |
| err && |
| err.showDiff !== false && |
| sameType(err.actual, err.expected) && |
| err.expected !== undefined |
| ); |
| } |
| |
| function stringifyDiffObjs(err) { |
| if (!utils.isString(err.actual) || !utils.isString(err.expected)) { |
| err.actual = utils.stringify(err.actual); |
| err.expected = utils.stringify(err.expected); |
| } |
| } |
| |
| /** |
| * Returns a diff between 2 strings with coloured ANSI output. |
| * |
| * @description |
| * The diff will be either inline or unified dependent on the value |
| * of `Base.inlineDiff`. |
| * |
| * @param {string} actual |
| * @param {string} expected |
| * @return {string} Diff |
| */ |
| var generateDiff = (exports.generateDiff = function(actual, expected) { |
| return exports.inlineDiffs |
| ? inlineDiff(actual, expected) |
| : unifiedDiff(actual, expected); |
| }); |
| |
| /** |
| * Outputs the given `failures` as a list. |
| * |
| * @public |
| * @memberof Mocha.reporters.Base |
| * @variation 1 |
| * @param {Object[]} failures - Each is Test instance with corresponding |
| * Error property |
| */ |
| exports.list = function(failures) { |
| Base.consoleLog(); |
| 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 msg; |
| var err = test.err; |
| var message; |
| if (err.message && typeof err.message.toString === 'function') { |
| message = err.message + ''; |
| } else if (typeof err.inspect === 'function') { |
| message = err.inspect() + ''; |
| } else { |
| message = ''; |
| } |
| var stack = err.stack || message; |
| var index = message ? stack.indexOf(message) : -1; |
| |
| if (index === -1) { |
| msg = message; |
| } else { |
| index += message.length; |
| msg = stack.slice(0, index); |
| // remove msg from stack |
| stack = stack.slice(index + 1); |
| } |
| |
| // uncaught |
| if (err.uncaught) { |
| msg = 'Uncaught ' + msg; |
| } |
| // explicitly show diff |
| if (!exports.hideDiff && showDiff(err)) { |
| stringifyDiffObjs(err); |
| 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); |
| |
| msg += generateDiff(err.actual, err.expected); |
| } |
| |
| // indent stack trace |
| stack = stack.replace(/^/gm, ' '); |
| |
| // indented test title |
| var testTitle = ''; |
| test.titlePath().forEach(function(str, index) { |
| if (index !== 0) { |
| testTitle += '\n '; |
| } |
| for (var i = 0; i < index; i++) { |
| testTitle += ' '; |
| } |
| testTitle += str; |
| }); |
| |
| Base.consoleLog(fmt, i + 1, testTitle, msg, stack); |
| }); |
| }; |
| |
| /** |
| * Constructs a new `Base` reporter instance. |
| * |
| * @description |
| * All other reporters generally inherit from this reporter. |
| * |
| * @public |
| * @class |
| * @memberof Mocha.reporters |
| * @param {Runner} runner - Instance triggers reporter actions. |
| * @param {Object} [options] - runner options |
| */ |
| function Base(runner, options) { |
| var failures = (this.failures = []); |
| |
| if (!runner) { |
| throw new TypeError('Missing runner argument'); |
| } |
| this.options = options || {}; |
| this.runner = runner; |
| this.stats = runner.stats; // assigned so Reporters keep a closer reference |
| |
| runner.on(EVENT_TEST_PASS, function(test) { |
| if (test.duration > test.slow()) { |
| test.speed = 'slow'; |
| } else if (test.duration > test.slow() / 2) { |
| test.speed = 'medium'; |
| } else { |
| test.speed = 'fast'; |
| } |
| }); |
| |
| runner.on(EVENT_TEST_FAIL, function(test, err) { |
| if (showDiff(err)) { |
| stringifyDiffObjs(err); |
| } |
| test.err = err; |
| failures.push(test); |
| }); |
| } |
| |
| /** |
| * Outputs common epilogue used by many of the bundled reporters. |
| * |
| * @public |
| * @memberof Mocha.reporters.Base |
| */ |
| Base.prototype.epilogue = function() { |
| var stats = this.stats; |
| var fmt; |
| |
| Base.consoleLog(); |
| |
| // passes |
| fmt = |
| color('bright pass', ' ') + |
| color('green', ' %d passing') + |
| color('light', ' (%s)'); |
| |
| Base.consoleLog(fmt, stats.passes || 0, milliseconds(stats.duration)); |
| |
| // pending |
| if (stats.pending) { |
| fmt = color('pending', ' ') + color('pending', ' %d pending'); |
| |
| Base.consoleLog(fmt, stats.pending); |
| } |
| |
| // failures |
| if (stats.failures) { |
| fmt = color('fail', ' %d failing'); |
| |
| Base.consoleLog(fmt, stats.failures); |
| |
| Base.list(this.failures); |
| Base.consoleLog(); |
| } |
| |
| Base.consoleLog(); |
| }; |
| |
| /** |
| * Pads the given `str` to `len`. |
| * |
| * @private |
| * @param {string} str |
| * @param {string} len |
| * @return {string} |
| */ |
| function pad(str, len) { |
| str = String(str); |
| return Array(len - str.length + 1).join(' ') + str; |
| } |
| |
| /** |
| * Returns inline diff between 2 strings with coloured ANSI output. |
| * |
| * @private |
| * @param {String} actual |
| * @param {String} expected |
| * @return {string} Diff |
| */ |
| function inlineDiff(actual, expected) { |
| var msg = errorDiff(actual, expected); |
| |
| // 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 unified diff between two strings with coloured ANSI output. |
| * |
| * @private |
| * @param {String} actual |
| * @param {String} expected |
| * @return {string} The diff. |
| */ |
| function unifiedDiff(actual, expected) { |
| var indent = ' '; |
| function cleanUp(line) { |
| if (line[0] === '+') { |
| return indent + colorLines('diff added', line); |
| } |
| if (line[0] === '-') { |
| return indent + colorLines('diff removed', line); |
| } |
| if (line.match(/@@/)) { |
| return '--'; |
| } |
| if (line.match(/\\ No newline/)) { |
| return null; |
| } |
| return indent + line; |
| } |
| function notBlank(line) { |
| return typeof line !== 'undefined' && line !== null; |
| } |
| var msg = diff.createPatch('string', actual, expected); |
| var lines = msg.split('\n').splice(5); |
| return ( |
| '\n ' + |
| colorLines('diff added', '+ expected') + |
| ' ' + |
| colorLines('diff removed', '- actual') + |
| '\n\n' + |
| lines |
| .map(cleanUp) |
| .filter(notBlank) |
| .join('\n') |
| ); |
| } |
| |
| /** |
| * Returns character diff for `err`. |
| * |
| * @private |
| * @param {String} actual |
| * @param {String} expected |
| * @return {string} the diff |
| */ |
| function errorDiff(actual, expected) { |
| return diff |
| .diffWordsWithSpace(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(''); |
| } |
| |
| /** |
| * Colors lines for `str`, using the color `name`. |
| * |
| * @private |
| * @param {string} name |
| * @param {string} str |
| * @return {string} |
| */ |
| function colorLines(name, str) { |
| return str |
| .split('\n') |
| .map(function(str) { |
| return color(name, str); |
| }) |
| .join('\n'); |
| } |
| |
| /** |
| * Object#toString reference. |
| */ |
| var objToString = Object.prototype.toString; |
| |
| /** |
| * Checks that a / b have the same type. |
| * |
| * @private |
| * @param {Object} a |
| * @param {Object} b |
| * @return {boolean} |
| */ |
| function sameType(a, b) { |
| return objToString.call(a) === objToString.call(b); |
| } |
| |
| Base.consoleLog = consoleLog; |
| |
| Base.abstract = true; |