base.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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 milliseconds = require('ms');
  11. var utils = require('../utils');
  12. var supportsColor = process.browser ? null : require('supports-color');
  13. var constants = require('../runner').constants;
  14. var EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
  15. var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL;
  16. /**
  17. * Expose `Base`.
  18. */
  19. exports = module.exports = Base;
  20. /**
  21. * Check if both stdio streams are associated with a tty.
  22. */
  23. var isatty = tty.isatty(1) && tty.isatty(2);
  24. /**
  25. * Enable coloring by default, except in the browser interface.
  26. */
  27. exports.useColors =
  28. !process.browser &&
  29. (supportsColor.stdout || process.env.MOCHA_COLORS !== undefined);
  30. /**
  31. * Inline diffs instead of +/-
  32. */
  33. exports.inlineDiffs = false;
  34. /**
  35. * Default color map.
  36. */
  37. exports.colors = {
  38. pass: 90,
  39. fail: 31,
  40. 'bright pass': 92,
  41. 'bright fail': 91,
  42. 'bright yellow': 93,
  43. pending: 36,
  44. suite: 0,
  45. 'error title': 0,
  46. 'error message': 31,
  47. 'error stack': 90,
  48. checkmark: 32,
  49. fast: 90,
  50. medium: 33,
  51. slow: 31,
  52. green: 32,
  53. light: 90,
  54. 'diff gutter': 90,
  55. 'diff added': 32,
  56. 'diff removed': 31
  57. };
  58. /**
  59. * Default symbol map.
  60. */
  61. exports.symbols = {
  62. ok: '✓',
  63. err: '✖',
  64. dot: '․',
  65. comma: ',',
  66. bang: '!'
  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. * @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. function showDiff(err) {
  128. return (
  129. err &&
  130. err.showDiff !== false &&
  131. sameType(err.actual, err.expected) &&
  132. err.expected !== undefined
  133. );
  134. }
  135. function stringifyDiffObjs(err) {
  136. if (!utils.isString(err.actual) || !utils.isString(err.expected)) {
  137. err.actual = utils.stringify(err.actual);
  138. err.expected = utils.stringify(err.expected);
  139. }
  140. }
  141. /**
  142. * Returns a diff between 2 strings with coloured ANSI output.
  143. *
  144. * The diff will be either inline or unified dependant on the value
  145. * of `Base.inlineDiff`.
  146. *
  147. * @param {string} actual
  148. * @param {string} expected
  149. * @return {string} Diff
  150. */
  151. var generateDiff = (exports.generateDiff = function(actual, expected) {
  152. return exports.inlineDiffs
  153. ? inlineDiff(actual, expected)
  154. : unifiedDiff(actual, expected);
  155. });
  156. /**
  157. * Output the given `failures` as a list.
  158. *
  159. * @public
  160. * @memberof Mocha.reporters.Base
  161. * @variation 1
  162. * @param {Array} failures
  163. */
  164. exports.list = function(failures) {
  165. console.log();
  166. failures.forEach(function(test, i) {
  167. // format
  168. var fmt =
  169. color('error title', ' %s) %s:\n') +
  170. color('error message', ' %s') +
  171. color('error stack', '\n%s\n');
  172. // msg
  173. var msg;
  174. var err = test.err;
  175. var message;
  176. if (err.message && typeof err.message.toString === 'function') {
  177. message = err.message + '';
  178. } else if (typeof err.inspect === 'function') {
  179. message = err.inspect() + '';
  180. } else {
  181. message = '';
  182. }
  183. var stack = err.stack || message;
  184. var index = message ? stack.indexOf(message) : -1;
  185. if (index === -1) {
  186. msg = message;
  187. } else {
  188. index += message.length;
  189. msg = stack.slice(0, index);
  190. // remove msg from stack
  191. stack = stack.slice(index + 1);
  192. }
  193. // uncaught
  194. if (err.uncaught) {
  195. msg = 'Uncaught ' + msg;
  196. }
  197. // explicitly show diff
  198. if (!exports.hideDiff && showDiff(err)) {
  199. stringifyDiffObjs(err);
  200. fmt =
  201. color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n');
  202. var match = message.match(/^([^:]+): expected/);
  203. msg = '\n ' + color('error message', match ? match[1] : msg);
  204. msg += generateDiff(err.actual, err.expected);
  205. }
  206. // indent stack trace
  207. stack = stack.replace(/^/gm, ' ');
  208. // indented test title
  209. var testTitle = '';
  210. test.titlePath().forEach(function(str, index) {
  211. if (index !== 0) {
  212. testTitle += '\n ';
  213. }
  214. for (var i = 0; i < index; i++) {
  215. testTitle += ' ';
  216. }
  217. testTitle += str;
  218. });
  219. console.log(fmt, i + 1, testTitle, msg, stack);
  220. });
  221. };
  222. /**
  223. * Initialize a new `Base` reporter.
  224. *
  225. * All other reporters generally
  226. * inherit from this reporter.
  227. *
  228. * @memberof Mocha.reporters
  229. * @public
  230. * @class
  231. * @param {Runner} runner
  232. */
  233. function Base(runner) {
  234. var failures = (this.failures = []);
  235. if (!runner) {
  236. throw new TypeError('Missing runner argument');
  237. }
  238. this.stats = runner.stats; // assigned so Reporters keep a closer reference
  239. this.runner = runner;
  240. runner.on(EVENT_TEST_PASS, function(test) {
  241. if (test.duration > test.slow()) {
  242. test.speed = 'slow';
  243. } else if (test.duration > test.slow() / 2) {
  244. test.speed = 'medium';
  245. } else {
  246. test.speed = 'fast';
  247. }
  248. });
  249. runner.on(EVENT_TEST_FAIL, function(test, err) {
  250. if (showDiff(err)) {
  251. stringifyDiffObjs(err);
  252. }
  253. test.err = err;
  254. failures.push(test);
  255. });
  256. }
  257. /**
  258. * Output common epilogue used by many of
  259. * the bundled reporters.
  260. *
  261. * @memberof Mocha.reporters.Base
  262. * @public
  263. */
  264. Base.prototype.epilogue = function() {
  265. var stats = this.stats;
  266. var fmt;
  267. console.log();
  268. // passes
  269. fmt =
  270. color('bright pass', ' ') +
  271. color('green', ' %d passing') +
  272. color('light', ' (%s)');
  273. console.log(fmt, stats.passes || 0, milliseconds(stats.duration));
  274. // pending
  275. if (stats.pending) {
  276. fmt = color('pending', ' ') + color('pending', ' %d pending');
  277. console.log(fmt, stats.pending);
  278. }
  279. // failures
  280. if (stats.failures) {
  281. fmt = color('fail', ' %d failing');
  282. console.log(fmt, stats.failures);
  283. Base.list(this.failures);
  284. console.log();
  285. }
  286. console.log();
  287. };
  288. /**
  289. * Pad the given `str` to `len`.
  290. *
  291. * @private
  292. * @param {string} str
  293. * @param {string} len
  294. * @return {string}
  295. */
  296. function pad(str, len) {
  297. str = String(str);
  298. return Array(len - str.length + 1).join(' ') + str;
  299. }
  300. /**
  301. * Returns an inline diff between 2 strings with coloured ANSI output.
  302. *
  303. * @private
  304. * @param {String} actual
  305. * @param {String} expected
  306. * @return {string} Diff
  307. */
  308. function inlineDiff(actual, expected) {
  309. var msg = errorDiff(actual, expected);
  310. // linenos
  311. var lines = msg.split('\n');
  312. if (lines.length > 4) {
  313. var width = String(lines.length).length;
  314. msg = lines
  315. .map(function(str, i) {
  316. return pad(++i, width) + ' |' + ' ' + str;
  317. })
  318. .join('\n');
  319. }
  320. // legend
  321. msg =
  322. '\n' +
  323. color('diff removed', 'actual') +
  324. ' ' +
  325. color('diff added', 'expected') +
  326. '\n\n' +
  327. msg +
  328. '\n';
  329. // indent
  330. msg = msg.replace(/^/gm, ' ');
  331. return msg;
  332. }
  333. /**
  334. * Returns a unified diff between two strings with coloured ANSI output.
  335. *
  336. * @private
  337. * @param {String} actual
  338. * @param {String} expected
  339. * @return {string} The diff.
  340. */
  341. function unifiedDiff(actual, expected) {
  342. var indent = ' ';
  343. function cleanUp(line) {
  344. if (line[0] === '+') {
  345. return indent + colorLines('diff added', line);
  346. }
  347. if (line[0] === '-') {
  348. return indent + colorLines('diff removed', line);
  349. }
  350. if (line.match(/@@/)) {
  351. return '--';
  352. }
  353. if (line.match(/\\ No newline/)) {
  354. return null;
  355. }
  356. return indent + line;
  357. }
  358. function notBlank(line) {
  359. return typeof line !== 'undefined' && line !== null;
  360. }
  361. var msg = diff.createPatch('string', actual, expected);
  362. var lines = msg.split('\n').splice(5);
  363. return (
  364. '\n ' +
  365. colorLines('diff added', '+ expected') +
  366. ' ' +
  367. colorLines('diff removed', '- actual') +
  368. '\n\n' +
  369. lines
  370. .map(cleanUp)
  371. .filter(notBlank)
  372. .join('\n')
  373. );
  374. }
  375. /**
  376. * Return a character diff for `err`.
  377. *
  378. * @private
  379. * @param {String} actual
  380. * @param {String} expected
  381. * @return {string} the diff
  382. */
  383. function errorDiff(actual, expected) {
  384. return diff
  385. .diffWordsWithSpace(actual, expected)
  386. .map(function(str) {
  387. if (str.added) {
  388. return colorLines('diff added', str.value);
  389. }
  390. if (str.removed) {
  391. return colorLines('diff removed', str.value);
  392. }
  393. return str.value;
  394. })
  395. .join('');
  396. }
  397. /**
  398. * Color lines for `str`, using the color `name`.
  399. *
  400. * @private
  401. * @param {string} name
  402. * @param {string} str
  403. * @return {string}
  404. */
  405. function colorLines(name, str) {
  406. return str
  407. .split('\n')
  408. .map(function(str) {
  409. return color(name, str);
  410. })
  411. .join('\n');
  412. }
  413. /**
  414. * Object#toString reference.
  415. */
  416. var objToString = Object.prototype.toString;
  417. /**
  418. * Check that a / b have the same type.
  419. *
  420. * @private
  421. * @param {Object} a
  422. * @param {Object} b
  423. * @return {boolean}
  424. */
  425. function sameType(a, b) {
  426. return objToString.call(a) === objToString.call(b);
  427. }
  428. Base.abstract = true;