html.js 8.8 KB

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