html.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. 'use strict';
  2. /* eslint-env browser */
  3. /**
  4. * @module HTML
  5. */
  6. /**
  7. * Module dependencies.
  8. */
  9. var Base = require('./base');
  10. var utils = require('../utils');
  11. var Progress = require('../browser/progress');
  12. var escapeRe = require('escape-string-regexp');
  13. var escape = utils.escape;
  14. /**
  15. * Save timer references to avoid Sinon interfering (see GH-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. * Expose `HTML`.
  26. */
  27. exports = module.exports = HTML;
  28. /**
  29. * Stats template.
  30. */
  31. var statsTemplate =
  32. '<ul id="mocha-stats">' +
  33. '<li class="progress"><canvas width="40" height="40"></canvas></li>' +
  34. '<li class="passes"><a href="javascript:void(0);">passes:</a> <em>0</em></li>' +
  35. '<li class="failures"><a href="javascript:void(0);">failures:</a> <em>0</em></li>' +
  36. '<li class="duration">duration: <em>0</em>s</li>' +
  37. '</ul>';
  38. var playIcon = '&#x2023;';
  39. /**
  40. * Initialize a new `HTML` reporter.
  41. *
  42. * @public
  43. * @class
  44. * @memberof Mocha.reporters
  45. * @extends Mocha.reporters.Base
  46. * @api public
  47. * @param {Runner} runner
  48. */
  49. function HTML(runner) {
  50. Base.call(this, runner);
  51. var self = this;
  52. var stats = this.stats;
  53. var stat = fragment(statsTemplate);
  54. var items = stat.getElementsByTagName('li');
  55. var passes = items[1].getElementsByTagName('em')[0];
  56. var passesLink = items[1].getElementsByTagName('a')[0];
  57. var failures = items[2].getElementsByTagName('em')[0];
  58. var failuresLink = items[2].getElementsByTagName('a')[0];
  59. var duration = items[3].getElementsByTagName('em')[0];
  60. var canvas = stat.getElementsByTagName('canvas')[0];
  61. var report = fragment('<ul id="mocha-report"></ul>');
  62. var stack = [report];
  63. var progress;
  64. var ctx;
  65. var root = document.getElementById('mocha');
  66. if (canvas.getContext) {
  67. var ratio = window.devicePixelRatio || 1;
  68. canvas.style.width = canvas.width;
  69. canvas.style.height = canvas.height;
  70. canvas.width *= ratio;
  71. canvas.height *= ratio;
  72. ctx = canvas.getContext('2d');
  73. ctx.scale(ratio, ratio);
  74. progress = new Progress();
  75. }
  76. if (!root) {
  77. return error('#mocha div missing, add it to your document');
  78. }
  79. // pass toggle
  80. on(passesLink, 'click', function(evt) {
  81. evt.preventDefault();
  82. unhide();
  83. var name = /pass/.test(report.className) ? '' : ' pass';
  84. report.className = report.className.replace(/fail|pass/g, '') + name;
  85. if (report.className.trim()) {
  86. hideSuitesWithout('test pass');
  87. }
  88. });
  89. // failure toggle
  90. on(failuresLink, 'click', function(evt) {
  91. evt.preventDefault();
  92. unhide();
  93. var name = /fail/.test(report.className) ? '' : ' fail';
  94. report.className = report.className.replace(/fail|pass/g, '') + name;
  95. if (report.className.trim()) {
  96. hideSuitesWithout('test fail');
  97. }
  98. });
  99. root.appendChild(stat);
  100. root.appendChild(report);
  101. if (progress) {
  102. progress.size(40);
  103. }
  104. runner.on('suite', function(suite) {
  105. if (suite.root) {
  106. return;
  107. }
  108. // suite
  109. var url = self.suiteURL(suite);
  110. var el = fragment(
  111. '<li class="suite"><h1><a href="%s">%s</a></h1></li>',
  112. url,
  113. escape(suite.title)
  114. );
  115. // container
  116. stack[0].appendChild(el);
  117. stack.unshift(document.createElement('ul'));
  118. el.appendChild(stack[0]);
  119. });
  120. runner.on('suite end', function(suite) {
  121. if (suite.root) {
  122. updateStats();
  123. return;
  124. }
  125. stack.shift();
  126. });
  127. runner.on('pass', function(test) {
  128. var url = self.testURL(test);
  129. var markup =
  130. '<li class="test pass %e"><h2>%e<span class="duration">%ems</span> ' +
  131. '<a href="%s" class="replay">' +
  132. playIcon +
  133. '</a></h2></li>';
  134. var el = fragment(markup, test.speed, test.title, test.duration, url);
  135. self.addCodeToggle(el, test.body);
  136. appendToStack(el);
  137. updateStats();
  138. });
  139. runner.on('fail', function(test) {
  140. var el = fragment(
  141. '<li class="test fail"><h2>%e <a href="%e" class="replay">' +
  142. playIcon +
  143. '</a></h2></li>',
  144. test.title,
  145. self.testURL(test)
  146. );
  147. var stackString; // Note: Includes leading newline
  148. var message = test.err.toString();
  149. // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
  150. // check for the result of the stringifying.
  151. if (message === '[object Error]') {
  152. message = test.err.message;
  153. }
  154. if (test.err.stack) {
  155. var indexOfMessage = test.err.stack.indexOf(test.err.message);
  156. if (indexOfMessage === -1) {
  157. stackString = test.err.stack;
  158. } else {
  159. stackString = test.err.stack.substr(
  160. test.err.message.length + indexOfMessage
  161. );
  162. }
  163. } else if (test.err.sourceURL && test.err.line !== undefined) {
  164. // Safari doesn't give you a stack. Let's at least provide a source line.
  165. stackString = '\n(' + test.err.sourceURL + ':' + test.err.line + ')';
  166. }
  167. stackString = stackString || '';
  168. if (test.err.htmlMessage && stackString) {
  169. el.appendChild(
  170. fragment(
  171. '<div class="html-error">%s\n<pre class="error">%e</pre></div>',
  172. test.err.htmlMessage,
  173. stackString
  174. )
  175. );
  176. } else if (test.err.htmlMessage) {
  177. el.appendChild(
  178. fragment('<div class="html-error">%s</div>', test.err.htmlMessage)
  179. );
  180. } else {
  181. el.appendChild(
  182. fragment('<pre class="error">%e%e</pre>', message, stackString)
  183. );
  184. }
  185. self.addCodeToggle(el, test.body);
  186. appendToStack(el);
  187. updateStats();
  188. });
  189. runner.on('pending', function(test) {
  190. var el = fragment(
  191. '<li class="test pass pending"><h2>%e</h2></li>',
  192. test.title
  193. );
  194. appendToStack(el);
  195. updateStats();
  196. });
  197. function appendToStack(el) {
  198. // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack.
  199. if (stack[0]) {
  200. stack[0].appendChild(el);
  201. }
  202. }
  203. function updateStats() {
  204. // TODO: add to stats
  205. var percent = (stats.tests / runner.total * 100) | 0;
  206. if (progress) {
  207. progress.update(percent).draw(ctx);
  208. }
  209. // update stats
  210. var ms = new Date() - stats.start;
  211. text(passes, stats.passes);
  212. text(failures, stats.failures);
  213. text(duration, (ms / 1000).toFixed(2));
  214. }
  215. }
  216. /**
  217. * Makes a URL, preserving querystring ("search") parameters.
  218. *
  219. * @param {string} s
  220. * @return {string} A new URL.
  221. */
  222. function makeUrl(s) {
  223. var search = window.location.search;
  224. // Remove previous grep query parameter if present
  225. if (search) {
  226. search = search.replace(/[?&]grep=[^&\s]*/g, '').replace(/^&/, '?');
  227. }
  228. return (
  229. window.location.pathname +
  230. (search ? search + '&' : '?') +
  231. 'grep=' +
  232. encodeURIComponent(escapeRe(s))
  233. );
  234. }
  235. /**
  236. * Provide suite URL.
  237. *
  238. * @param {Object} [suite]
  239. */
  240. HTML.prototype.suiteURL = function(suite) {
  241. return makeUrl(suite.fullTitle());
  242. };
  243. /**
  244. * Provide test URL.
  245. *
  246. * @param {Object} [test]
  247. */
  248. HTML.prototype.testURL = function(test) {
  249. return makeUrl(test.fullTitle());
  250. };
  251. /**
  252. * Adds code toggle functionality for the provided test's list element.
  253. *
  254. * @param {HTMLLIElement} el
  255. * @param {string} contents
  256. */
  257. HTML.prototype.addCodeToggle = function(el, contents) {
  258. var h2 = el.getElementsByTagName('h2')[0];
  259. on(h2, 'click', function() {
  260. pre.style.display = pre.style.display === 'none' ? 'block' : 'none';
  261. });
  262. var pre = fragment('<pre><code>%e</code></pre>', utils.clean(contents));
  263. el.appendChild(pre);
  264. pre.style.display = 'none';
  265. };
  266. /**
  267. * Display error `msg`.
  268. *
  269. * @param {string} msg
  270. */
  271. function error(msg) {
  272. document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg));
  273. }
  274. /**
  275. * Return a DOM fragment from `html`.
  276. *
  277. * @param {string} html
  278. */
  279. function fragment(html) {
  280. var args = arguments;
  281. var div = document.createElement('div');
  282. var i = 1;
  283. div.innerHTML = html.replace(/%([se])/g, function(_, type) {
  284. switch (type) {
  285. case 's':
  286. return String(args[i++]);
  287. case 'e':
  288. return escape(args[i++]);
  289. // no default
  290. }
  291. });
  292. return div.firstChild;
  293. }
  294. /**
  295. * Check for suites that do not have elements
  296. * with `classname`, and hide them.
  297. *
  298. * @param {text} classname
  299. */
  300. function hideSuitesWithout(classname) {
  301. var suites = document.getElementsByClassName('suite');
  302. for (var i = 0; i < suites.length; i++) {
  303. var els = suites[i].getElementsByClassName(classname);
  304. if (!els.length) {
  305. suites[i].className += ' hidden';
  306. }
  307. }
  308. }
  309. /**
  310. * Unhide .hidden suites.
  311. */
  312. function unhide() {
  313. var els = document.getElementsByClassName('suite hidden');
  314. for (var i = 0; i < els.length; ++i) {
  315. els[i].className = els[i].className.replace('suite hidden', 'suite');
  316. }
  317. }
  318. /**
  319. * Set an element's text contents.
  320. *
  321. * @param {HTMLElement} el
  322. * @param {string} contents
  323. */
  324. function text(el, contents) {
  325. if (el.textContent) {
  326. el.textContent = contents;
  327. } else {
  328. el.innerText = contents;
  329. }
  330. }
  331. /**
  332. * Listen on `event` with callback `fn`.
  333. */
  334. function on(el, event, fn) {
  335. if (el.addEventListener) {
  336. el.addEventListener(event, fn, false);
  337. } else {
  338. el.attachEvent('on' + event, fn);
  339. }
  340. }