/**
* Module dependencies.
*/
var Base = require('./base')
, utils = require('../utils')
, Progress = require('../browser/progress')
, escape = utils.escape;
/**
* Expose `Doc`.
*/
exports = module.exports = HTML;
/**
* Stats template.
*/
var statsTemplate = '
'
+ ''
+ '- passes: 0
'
+ '- failures: 0
'
+ '- duration: 0s
'
+ '
';
/**
* Initialize a new `Doc` reporter.
*
* @param {Runner} runner
* @api public
*/
function HTML(runner) {
Base.call(this, runner);
var self = this
, stats = this.stats
, total = runner.total
, root = document.getElementById('mocha')
, stat = fragment(statsTemplate)
, items = stat.getElementsByTagName('li')
, passes = items[1].getElementsByTagName('em')[0]
, failures = items[2].getElementsByTagName('em')[0]
, duration = items[3].getElementsByTagName('em')[0]
, canvas = stat.getElementsByTagName('canvas')[0]
, stack = [root]
, progress
, ctx
if (canvas.getContext) {
ctx = canvas.getContext('2d');
progress = new Progress;
}
if (!root) return error('#mocha div missing, add it to your document');
root.appendChild(stat);
if (progress) progress.size(40);
runner.on('suite', function(suite){
if (suite.root) return;
// suite
var el = fragment('%s
', suite.title);
// container
stack[0].appendChild(el);
stack.unshift(document.createElement('div'));
el.appendChild(stack[0]);
});
runner.on('suite end', function(suite){
if (suite.root) return;
stack.shift();
});
runner.on('fail', function(test, err){
if ('hook' == test.type || err.uncaught) runner.emit('test end', test);
});
runner.on('test end', function(test){
// TODO: add to stats
var percent = stats.tests / 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));
// test
if ('passed' == test.state) {
var el = fragment('%e
', test.title);
} else if (test.pending) {
var el = fragment('%e
', test.title);
} else {
var el = fragment('%e
', test.title);
var str = test.err.stack || test.err.toString();
// FF / Opera do not add the message
if (!~str.indexOf(test.err.message)) {
str = test.err.message + '\n' + str;
}
// <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
// check for the result of the stringifying.
if ('[object Error]' == str) str = test.err.message;
// Safari doesn't give you a stack. Let's at least provide a source line.
if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) {
str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")";
}
el.appendChild(fragment('%e
', str));
}
// toggle code
var h2 = el.getElementsByTagName('h2')[0];
on(h2, 'click', function(){
pre.style.display = 'none' == pre.style.display
? 'block'
: 'none';
});
// code
// TODO: defer
if (!test.pending) {
var pre = fragment('%e
', clean(test.fn.toString()));
el.appendChild(pre);
pre.style.display = 'none';
}
stack[0].appendChild(el);
});
}
/**
* Display error `msg`.
*/
function error(msg) {
document.body.appendChild(fragment('%s
', msg));
}
/**
* Return a DOM fragment from `html`.
*/
function fragment(html) {
var args = arguments
, div = document.createElement('div')
, i = 1;
div.innerHTML = html.replace(/%([se])/g, function(_, type){
switch (type) {
case 's': return String(args[i++]);
case 'e': return escape(args[i++]);
}
});
return div.firstChild;
}
/**
* Set `el` text to `str`.
*/
function text(el, str) {
if (el.textContent) {
el.textContent = str;
} else {
el.innerText = str;
}
}
/**
* 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);
}
}
/**
* Strip the function definition from `str`,
* and re-indent for pre whitespace.
*/
function clean(str) {
str = str
.replace(/^function *\(.*\) *{/, '')
.replace(/\s+\}$/, '');
var spaces = str.match(/^\n?( *)/)[1].length
, re = new RegExp('^ {' + spaces + '}', 'gm');
str = str
.replace(re, '')
.replace(/^\s+/, '');
return str;
}