base.js 11 KB

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