base.js 10 KB

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