Skip to content

Instantly share code, notes, and snippets.

@geowarin
Last active August 29, 2015 14:12
Show Gist options
  • Save geowarin/d86c7ff39ac43ee730a7 to your computer and use it in GitHub Desktop.
Save geowarin/d86c7ff39ac43ee730a7 to your computer and use it in GitHub Desktop.
Blanket and mocha reporters to enable generation of a html report when doing in browser testing with phantomjs
/**
* Author: Geoffroy Warin (http://geowarin.github.io)
*
* Reporter for blanket. Outputs the coverage html to the console.
* This is basically a copy of https://github.com/alex-seville/blanket/blob/master/src/qunit/reporter.js
* with a few adaptations
*/
(function (){
var reporter = function(coverage){
var cssSytle = "#blanket-main {margin:2px;background:#EEE;color:#333;clear:both;font-family:'Helvetica Neue Light', 'HelveticaNeue-Light', 'Helvetica Neue', Calibri, Helvetica, Arial, sans-serif; font-size:17px;} #blanket-main a {color:#333;text-decoration:none;} #blanket-main a:hover {text-decoration:underline;} .blanket {margin:0;padding:5px;clear:both;border-bottom: 1px solid #FFFFFF;} .bl-error {color:red;}.bl-success {color:#5E7D00;} .bl-file{width:auto;} .bl-cl{float:left;} .blanket div.rs {margin-left:50px; width:150px; float:right} .bl-nb {padding-right:10px;} #blanket-main a.bl-logo {color: #EB1764;cursor: pointer;font-weight: bold;text-decoration: none} .bl-source{ overflow-x:scroll; background-color: #FFFFFF; border: 1px solid #CBCBCB; color: #363636; margin: 25px 20px; width: 80%;} .bl-source div{white-space: pre;font-family: monospace;} .bl-source > div > span:first-child{background-color: #EAEAEA;color: #949494;display: inline-block;padding: 0 10px;text-align: center;width: 30px;} .bl-source .miss{background-color:#e6c3c7} .bl-source span.branchWarning{color:#000;background-color:yellow;} .bl-source span.branchOkay{color:#000;background-color:transparent;}",
successRate = 60,
fileNumber = 0,
headerContent,
hasBranchTracking = Object.keys(coverage.files).some(function(elem){
return typeof coverage.files[elem].branchData !== 'undefined';
}),
bodyContent = "<div id='blanket-main'><div class='blanket bl-title'><div class='bl-cl bl-file'><a href='http://alex-seville.github.com/blanket/' target='_blank' class='bl-logo'>Blanket.js</a> results</div><div class='bl-cl rs'>Coverage (%)</div><div class='bl-cl rs'>Covered/Total Smts.</div>"+(hasBranchTracking ? "<div class='bl-cl rs'>Covered/Total Branches</div>":"")+"<div style='clear:both;'></div></div>",
fileTemplate = "<div class='blanket {{statusclass}}'><div class='bl-cl bl-file'><span class='bl-nb'>{{fileNumber}}.</span><a href='javascript:blanket_toggleSource(\"file-{{fileNumber}}\")'>{{file}}</a></div><div class='bl-cl rs'>{{percentage}} %</div><div class='bl-cl rs'>{{numberCovered}}/{{totalSmts}}</div>"+( hasBranchTracking ? "<div class='bl-cl rs'>{{passedBranches}}/{{totalBranches}}</div>" : "" )+"<div id='file-{{fileNumber}}' class='bl-source' style='display:none;'>{{source}}</div><div style='clear:both;'></div></div>";
grandTotalTemplate = "<div class='blanket grand-total {{statusclass}}'><div class='bl-cl'>{{rowTitle}}</div><div class='bl-cl rs'>{{percentage}} %</div><div class='bl-cl rs'>{{numberCovered}}/{{totalSmts}}</div>"+( hasBranchTracking ? "<div class='bl-cl rs'>{{passedBranches}}/{{totalBranches}}</div>" : "" ) + "<div style='clear:both;'></div></div>";
function blanket_toggleSource(id) {
var element = document.getElementById(id);
if(element.style.display === 'block') {
element.style.display = 'none';
} else {
element.style.display = 'block';
}
}
var percentage = function(number, total) {
return (Math.round(((number/total) * 100)*100)/100);
};
var writeTag = function (type, str) {
console.log('<' + type + '>' + str + '</' + type + '>');
};
function escapeInvalidXmlChars(str) {
return str.replace(/\&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/\>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/\'/g, "&apos;");
}
function isBranchFollowed(data,bool){
var mode = bool ? 0 : 1;
if (typeof data === 'undefined' ||
typeof data === null ||
typeof data[mode] === 'undefined'){
return false;
}
return data[mode].length > 0;
}
var branchStack = [];
function branchReport(colsIndex,src,cols,offset,lineNum){
var newsrc="";
var postfix="";
if (branchStack.length > 0){
newsrc += "<span class='" + (isBranchFollowed(branchStack[0][1],branchStack[0][1].consequent === branchStack[0][0]) ? 'branchOkay' : 'branchWarning') + "'>";
if (branchStack[0][0].end.line === lineNum){
newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + "</span>";
src = src.slice(branchStack[0][0].end.column);
branchStack.shift();
if (branchStack.length > 0){
newsrc += "<span class='" + (isBranchFollowed(branchStack[0][1],false) ? 'branchOkay' : 'branchWarning') + "'>";
if (branchStack[0][0].end.line === lineNum){
newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + "</span>";
src = src.slice(branchStack[0][0].end.column);
branchStack.shift();
if (!cols){
return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols};
}
}
else if (!cols){
return {src: newsrc + escapeInvalidXmlChars(src) + "</span>",cols:cols};
}
else{
postfix = "</span>";
}
}else if (!cols){
return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols};
}
}else if(!cols){
return {src: newsrc + escapeInvalidXmlChars(src) + "</span>",cols:cols};
}else{
postfix = "</span>";
}
}
var thisline = cols[colsIndex];
//consequent
var cons = thisline.consequent;
if (cons.start.line > lineNum){
branchStack.unshift([thisline.alternate,thisline]);
branchStack.unshift([cons,thisline]);
src = escapeInvalidXmlChars(src);
}else{
var style = "<span class='" + (isBranchFollowed(thisline,true) ? 'branchOkay' : 'branchWarning') + "'>";
newsrc += escapeInvalidXmlChars(src.slice(0,cons.start.column-offset)) + style;
if (cols.length > colsIndex+1 &&
cols[colsIndex+1].consequent.start.line === lineNum &&
cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].consequent.end.column-offset)
{
var res = branchReport(colsIndex+1,src.slice(cons.start.column-offset,cons.end.column-offset),cols,cons.start.column-offset,lineNum);
newsrc += res.src;
cols = res.cols;
cols[colsIndex+1] = cols[colsIndex+2];
cols.length--;
}else{
newsrc += escapeInvalidXmlChars(src.slice(cons.start.column-offset,cons.end.column-offset));
}
newsrc += "</span>";
var alt = thisline.alternate;
if (alt.start.line > lineNum){
newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset));
branchStack.unshift([alt,thisline]);
}else{
newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset,alt.start.column-offset));
style = "<span class='" + (isBranchFollowed(thisline,false) ? 'branchOkay' : 'branchWarning') + "'>";
newsrc += style;
if (cols.length > colsIndex+1 &&
cols[colsIndex+1].consequent.start.line === lineNum &&
cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].alternate.end.column-offset)
{
var res2 = branchReport(colsIndex+1,src.slice(alt.start.column-offset,alt.end.column-offset),cols,alt.start.column-offset,lineNum);
newsrc += res2.src;
cols = res2.cols;
cols[colsIndex+1] = cols[colsIndex+2];
cols.length--;
}else{
newsrc += escapeInvalidXmlChars(src.slice(alt.start.column-offset,alt.end.column-offset));
}
newsrc += "</span>";
newsrc += escapeInvalidXmlChars(src.slice(alt.end.column-offset));
src = newsrc;
}
}
return {src:src+postfix, cols:cols};
}
var isUndefined = function(item){
return typeof item !== 'undefined';
};
var files = coverage.files;
var totals = {
totalSmts: 0,
numberOfFilesCovered: 0,
passedBranches: 0,
totalBranches: 0,
moduleTotalStatements : {},
moduleTotalCoveredStatements : {},
moduleTotalBranches : {},
moduleTotalCoveredBranches : {}
};
// check if a data-cover-modulepattern was provided for per-module coverage reporting
var modulePattern = _blanket.options("modulePattern");
var modulePatternRegex = ( modulePattern ? new RegExp(modulePattern) : null );
for(var file in files)
{
fileNumber++;
var statsForFile = files[file],
totalSmts = 0,
numberOfFilesCovered = 0,
code = [],
i;
var end = [];
for(i = 0; i < statsForFile.source.length; i +=1){
var src = statsForFile.source[i];
if (branchStack.length > 0 ||
typeof statsForFile.branchData !== 'undefined')
{
if (typeof statsForFile.branchData[i+1] !== 'undefined')
{
var cols = statsForFile.branchData[i+1].filter(isUndefined);
var colsIndex=0;
src = branchReport(colsIndex,src,cols,0,i+1).src;
}else if (branchStack.length){
src = branchReport(0,src,null,0,i+1).src;
}else{
src = escapeInvalidXmlChars(src);
}
}else{
src = escapeInvalidXmlChars(src);
}
var lineClass="";
if(statsForFile[i+1]) {
numberOfFilesCovered += 1;
totalSmts += 1;
lineClass = 'hit';
}else{
if(statsForFile[i+1] === 0){
totalSmts++;
lineClass = 'miss';
}
}
code[i + 1] = "<div class='"+lineClass+"'><span class=''>"+(i + 1)+"</span>"+src+"</div>";
}
totals.totalSmts += totalSmts;
totals.numberOfFilesCovered += numberOfFilesCovered;
var totalBranches=0;
var passedBranches=0;
if (typeof statsForFile.branchData !== 'undefined'){
for(var j=0;j<statsForFile.branchData.length;j++){
if (typeof statsForFile.branchData[j] !== 'undefined'){
for(var k=0;k<statsForFile.branchData[j].length;k++){
if (typeof statsForFile.branchData[j][k] !== 'undefined'){
totalBranches++;
if (typeof statsForFile.branchData[j][k][0] !== 'undefined' &&
statsForFile.branchData[j][k][0].length > 0 &&
typeof statsForFile.branchData[j][k][1] !== 'undefined' &&
statsForFile.branchData[j][k][1].length > 0){
passedBranches++;
}
}
}
}
}
}
totals.passedBranches += passedBranches;
totals.totalBranches += totalBranches;
// if "data-cover-modulepattern" was provided,
// track totals per module name as well as globally
if (modulePatternRegex) {
var moduleName = file.match(modulePatternRegex)[1];
if(!totals.moduleTotalStatements.hasOwnProperty(moduleName)) {
totals.moduleTotalStatements[moduleName] = 0;
totals.moduleTotalCoveredStatements[moduleName] = 0;
}
totals.moduleTotalStatements[moduleName] += totalSmts;
totals.moduleTotalCoveredStatements[moduleName] += numberOfFilesCovered;
if(!totals.moduleTotalBranches.hasOwnProperty(moduleName)) {
totals.moduleTotalBranches[moduleName] = 0;
totals.moduleTotalCoveredBranches[moduleName] = 0;
}
totals.moduleTotalBranches[moduleName] += totalBranches;
totals.moduleTotalCoveredBranches[moduleName] += passedBranches;
}
var result = percentage(numberOfFilesCovered, totalSmts);
var output = fileTemplate.replace("{{file}}", file)
.replace("{{percentage}}",result)
.replace("{{numberCovered}}", numberOfFilesCovered)
.replace(/\{\{fileNumber\}\}/g, fileNumber)
.replace("{{totalSmts}}", totalSmts)
.replace("{{totalBranches}}", totalBranches)
.replace("{{passedBranches}}", passedBranches)
.replace("{{source}}", code.join(" "));
if(result < successRate)
{
output = output.replace("{{statusclass}}", "bl-error");
} else {
output = output.replace("{{statusclass}}", "bl-success");
}
bodyContent += output;
}
// create temporary function for use by the global totals reporter,
// as well as the per-module totals reporter
var createAggregateTotal = function(numSt, numCov, numBranch, numCovBr, moduleName) {
var totalPercent = percentage(numCov, numSt);
var statusClass = totalPercent < successRate ? "bl-error" : "bl-success";
var rowTitle = ( moduleName ? "Total for module: " + moduleName : "Global total" );
var totalsOutput = grandTotalTemplate.replace("{{rowTitle}}", rowTitle)
.replace("{{percentage}}", totalPercent)
.replace("{{numberCovered}}", numCov)
.replace("{{totalSmts}}", numSt)
.replace("{{passedBranches}}", numCovBr)
.replace("{{totalBranches}}", numBranch)
.replace("{{statusclass}}", statusClass);
bodyContent += totalsOutput;
};
// if "data-cover-modulepattern" was provided,
// output the per-module totals alongside the global totals
if (modulePatternRegex) {
for (var thisModuleName in totals.moduleTotalStatements) {
if (totals.moduleTotalStatements.hasOwnProperty(thisModuleName)) {
var moduleTotalSt = totals.moduleTotalStatements[thisModuleName];
var moduleTotalCovSt = totals.moduleTotalCoveredStatements[thisModuleName];
var moduleTotalBr = totals.moduleTotalBranches[thisModuleName];
var moduleTotalCovBr = totals.moduleTotalCoveredBranches[thisModuleName];
createAggregateTotal(moduleTotalSt, moduleTotalCovSt, moduleTotalBr, moduleTotalCovBr, thisModuleName);
}
}
}
createAggregateTotal(totals.totalSmts, totals.numberOfFilesCovered, totals.totalBranches, totals.passedBranches, null);
bodyContent += "</div>"; //closing main
console.log('<!DOCTYPE html>');
console.log('<html>');
console.log('<head>');
writeTag('style', cssSytle);
var scriptText = blanket_toggleSource.toString().replace('function ' + blanket_toggleSource.name, 'function blanket_toggleSource');
writeTag('script', scriptText);
console.log('</head>');
console.log('<body>');
writeTag('div', bodyContent);
console.log('</body>');
console.log('</html>');
};
blanket.customReporter= function(coverageData){
reporter(coverageData);
};
})();
var gulp = require('gulp'),
inject = require('gulp-inject'),
bowerFiles = require('main-bower-files'),
mochaPhantomJS = require('gulp-mocha-phantomjs'),
rename = require('gulp-rename'),
preprocess = require('gulp-preprocess'),
argv = require('yargs').argv,
gutil = require('gulp-util');
var coverage = !!argv.coverage; // true if --coverage flag is used
/**
* Process test index to interpret the conditions and move it
* to the test/ directory
*/
gulp.task('processIndex', function () {
return gulp.src('build/templates/testIndex.html')
.pipe(preprocess({context: {coverage: coverage}}))
.pipe(rename('index.html'))
.pipe(gulp.dest('test'));
});
/**
* Inject resources into test index.
* Please note the file should be in its final position for relative paths to work.
*/
gulp.task('injectIntoIndex', ['processIndex'], function () {
return gulp.src('test/index.html')
.pipe(inject(
gulp.src(bowerFiles({includeDev: true}), {read: false}),
{name: 'bower', relative: true}
))
.pipe(inject(
gulp.src(['src/js/**/*.js'], {read: false}),
{name: 'all', relative: true}
))
.pipe(inject(
gulp.src(['test/suites/**/*.js'], {read: false}),
{name: 'suites', relative: true}
))
.pipe(gulp.dest('test'));
});
/**
* Launch mocha phantomjs to run the tests.
* By default, all output is redirected to the console and the spec reporter is used.
* In coverage mode, output will be redirected to a file and a custom mocha reporter will be used.
*/
gulp.task('test', ['injectIntoIndex'], function () {
var options = !coverage ? {} : {
reporter: 'build/report/mochaBlanketAdapter.js',
dump: 'test/coverage.html'
};
return gulp.src('test/index.html', {read: false})
.on('data', processWinPath)
.pipe(mochaPhantomJS(options))
.on('end', function() {
if (coverage)
gutil.log('Generated coverage report to', gutil.colors.cyan(options.dump));
});
});
var processWinPath = function (file) {
var path = require('path');
if (process.platform === 'win32') {
file.path = path.relative('.', file.path);
}
};
var Base = require('./base')
, cursor = Base.cursor
, color = Base.color;
exports = module.exports = BlanketReporter;
/**
* Author: Geoffroy Warin (http://geowarin.github.io)
*
* Adapter reporter to wire blanket from mocha.
* Essentially taken from https://github.com/alex-seville/blanket/blob/master/src/adapters/mocha-blanket.js
*/
function BlanketReporter(runner) {
Base.call(this, runner);
runner.on('start', function() {
blanket.setupCoverage();
});
runner.on('end', function() {
blanket.onTestsDone();
});
runner.on('suite', function() {
blanket.onModuleStart();
});
runner.on('test', function() {
blanket.onTestStart();
});
runner.on('test end', function(test) {
blanket.onTestDone(test.parent.tests.length, test.state === 'passed');
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment