html.js 9.2 KB

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