'use strict'; /* eslint-env browser */ /** * @module HTML */ /** * Module dependencies. */ var Base = require('./base'); var utils = require('../utils'); var Progress = require('../browser/progress'); var escapeRe = require('escape-string-regexp'); var constants = require('../runner').constants; var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN; var EVENT_SUITE_END = constants.EVENT_SUITE_END; var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; var escape = utils.escape; /** * Save timer references to avoid Sinon interfering (see GH-237). */ var Date = global.Date; /** * Expose `HTML`. */ exports = module.exports = HTML; /** * Stats template. */ var statsTemplate = ''; var playIcon = '‣'; /** * Constructs a new `HTML` reporter instance. * * @public * @class * @memberof Mocha.reporters * @extends Mocha.reporters.Base * @param {Runner} runner - Instance triggers reporter actions. * @param {Object} [options] - runner options */ function HTML(runner, options) { Base.call(this, runner, options); var self = this; var stats = this.stats; var stat = fragment(statsTemplate); var items = stat.getElementsByTagName('li'); var passes = items[1].getElementsByTagName('em')[0]; var passesLink = items[1].getElementsByTagName('a')[0]; var failures = items[2].getElementsByTagName('em')[0]; var failuresLink = items[2].getElementsByTagName('a')[0]; var duration = items[3].getElementsByTagName('em')[0]; var canvas = stat.getElementsByTagName('canvas')[0]; var report = fragment(''); var stack = [report]; var progress; var ctx; var root = document.getElementById('mocha'); if (canvas.getContext) { var ratio = window.devicePixelRatio || 1; canvas.style.width = canvas.width; canvas.style.height = canvas.height; canvas.width *= ratio; canvas.height *= ratio; ctx = canvas.getContext('2d'); ctx.scale(ratio, ratio); progress = new Progress(); } if (!root) { return error('#mocha div missing, add it to your document'); } // pass toggle on(passesLink, 'click', function(evt) { evt.preventDefault(); unhide(); var name = /pass/.test(report.className) ? '' : ' pass'; report.className = report.className.replace(/fail|pass/g, '') + name; if (report.className.trim()) { hideSuitesWithout('test pass'); } }); // failure toggle on(failuresLink, 'click', function(evt) { evt.preventDefault(); unhide(); var name = /fail/.test(report.className) ? '' : ' fail'; report.className = report.className.replace(/fail|pass/g, '') + name; if (report.className.trim()) { hideSuitesWithout('test fail'); } }); root.appendChild(stat); root.appendChild(report); if (progress) { progress.size(40); } runner.on(EVENT_SUITE_BEGIN, function(suite) { if (suite.root) { return; } // suite var url = self.suiteURL(suite); var el = fragment( '
  • %s

  • ', url, escape(suite.title) ); // container stack[0].appendChild(el); stack.unshift(document.createElement('ul')); el.appendChild(stack[0]); }); runner.on(EVENT_SUITE_END, function(suite) { if (suite.root) { updateStats(); return; } stack.shift(); }); runner.on(EVENT_TEST_PASS, function(test) { var url = self.testURL(test); var markup = '
  • %e%ems ' + '' + playIcon + '

  • '; var el = fragment(markup, test.speed, test.title, test.duration, url); self.addCodeToggle(el, test.body); appendToStack(el); updateStats(); }); runner.on(EVENT_TEST_FAIL, function(test) { var el = fragment( '
  • %e ' + playIcon + '

  • ', test.title, self.testURL(test) ); var stackString; // Note: Includes leading newline var message = test.err.toString(); // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we // check for the result of the stringifying. if (message === '[object Error]') { message = test.err.message; } if (test.err.stack) { var indexOfMessage = test.err.stack.indexOf(test.err.message); if (indexOfMessage === -1) { stackString = test.err.stack; } else { stackString = test.err.stack.substr( test.err.message.length + indexOfMessage ); } } else if (test.err.sourceURL && test.err.line !== undefined) { // Safari doesn't give you a stack. Let's at least provide a source line. stackString = '\n(' + test.err.sourceURL + ':' + test.err.line + ')'; } stackString = stackString || ''; if (test.err.htmlMessage && stackString) { el.appendChild( fragment( '
    %s\n
    %e
    ', test.err.htmlMessage, stackString ) ); } else if (test.err.htmlMessage) { el.appendChild( fragment('
    %s
    ', test.err.htmlMessage) ); } else { el.appendChild( fragment('
    %e%e
    ', message, stackString) ); } self.addCodeToggle(el, test.body); appendToStack(el); updateStats(); }); runner.on(EVENT_TEST_PENDING, function(test) { var el = fragment( '
  • %e

  • ', test.title ); appendToStack(el); updateStats(); }); function appendToStack(el) { // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. if (stack[0]) { stack[0].appendChild(el); } } function updateStats() { // TODO: add to stats var percent = ((stats.tests / runner.total) * 100) | 0; if (progress) { progress.update(percent).draw(ctx); } // update stats var ms = new Date() - stats.start; text(passes, stats.passes); text(failures, stats.failures); text(duration, (ms / 1000).toFixed(2)); } } /** * Makes a URL, preserving querystring ("search") parameters. * * @param {string} s * @return {string} A new URL. */ function makeUrl(s) { var search = window.location.search; // Remove previous grep query parameter if present if (search) { search = search.replace(/[?&]grep=[^&\s]*/g, '').replace(/^&/, '?'); } return ( window.location.pathname + (search ? search + '&' : '?') + 'grep=' + encodeURIComponent(escapeRe(s)) ); } /** * Provide suite URL. * * @param {Object} [suite] */ HTML.prototype.suiteURL = function(suite) { return makeUrl(suite.fullTitle()); }; /** * Provide test URL. * * @param {Object} [test] */ HTML.prototype.testURL = function(test) { return makeUrl(test.fullTitle()); }; /** * Adds code toggle functionality for the provided test's list element. * * @param {HTMLLIElement} el * @param {string} contents */ HTML.prototype.addCodeToggle = function(el, contents) { var h2 = el.getElementsByTagName('h2')[0]; on(h2, 'click', function() { pre.style.display = pre.style.display === 'none' ? 'block' : 'none'; }); var pre = fragment('
    %e
    ', utils.clean(contents)); el.appendChild(pre); pre.style.display = 'none'; }; /** * Display error `msg`. * * @param {string} msg */ function error(msg) { document.body.appendChild(fragment('
    %s
    ', msg)); } /** * Return a DOM fragment from `html`. * * @param {string} html */ function fragment(html) { var args = arguments; var div = document.createElement('div'); var i = 1; div.innerHTML = html.replace(/%([se])/g, function(_, type) { switch (type) { case 's': return String(args[i++]); case 'e': return escape(args[i++]); // no default } }); return div.firstChild; } /** * Check for suites that do not have elements * with `classname`, and hide them. * * @param {text} classname */ function hideSuitesWithout(classname) { var suites = document.getElementsByClassName('suite'); for (var i = 0; i < suites.length; i++) { var els = suites[i].getElementsByClassName(classname); if (!els.length) { suites[i].className += ' hidden'; } } } /** * Unhide .hidden suites. */ function unhide() { var els = document.getElementsByClassName('suite hidden'); while (els.length > 0) { els[0].className = els[0].className.replace('suite hidden', 'suite'); } } /** * Set an element's text contents. * * @param {HTMLElement} el * @param {string} contents */ function text(el, contents) { if (el.textContent) { el.textContent = contents; } else { el.innerText = contents; } } /** * Listen on `event` with callback `fn`. */ function on(el, event, fn) { if (el.addEventListener) { el.addEventListener(event, fn, false); } else { el.attachEvent('on' + event, fn); } } HTML.browserOnly = true;