base.js 10 KB

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