Created
December 5, 2014 10:15
-
-
Save iongion/f23edaa708eafc240a8b to your computer and use it in GitHub Desktop.
phantom-qunit-junit-runner.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* XMLWriter - XML generator for Javascript, based on .NET's XMLTextWriter. | |
* Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com | |
* Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) | |
* Date: 3/12/2008 | |
* @version 1.0.0 | |
* @author Ariel Flesler | |
* http://flesler.blogspot.com/2008/03/xmlwriter-for-javascript.html | |
*/ | |
function XMLWriter( encoding, version ){ | |
if( encoding ) | |
this.encoding = encoding; | |
if( version ) | |
this.version = version; | |
}; | |
(function(){ | |
XMLWriter.prototype = { | |
encoding:'UTF-8',// what is the encoding | |
version:'1.0', //what xml version to use | |
formatting: 'indented', //how to format the output (indented/none) ? | |
indentChar:'\t', //char to use for indent | |
indentation: 1, //how many indentChar to add per level | |
newLine: '\n', //character to separate nodes when formatting | |
//start a new document, cleanup if we are reusing | |
writeStartDocument:function( standalone ){ | |
this.close();//cleanup | |
this.stack = [ ]; | |
this.standalone = standalone; | |
}, | |
//get back to the root | |
writeEndDocument:function(){ | |
this.active = this.root; | |
this.stack = [ ]; | |
}, | |
//set the text of the doctype | |
writeDocType:function( dt ){ | |
this.doctype = dt; | |
}, | |
//start a new node with this name, and an optional namespace | |
writeStartElement:function( name, ns ){ | |
if( ns )//namespace | |
name = ns + ':' + name; | |
var node = { n:name, a:{ }, c: [ ] };//(n)ame, (a)ttributes, (c)hildren | |
if( this.active ){ | |
this.active.c.push(node); | |
this.stack.push(this.active); | |
}else | |
this.root = node; | |
this.active = node; | |
}, | |
//go up one node, if we are in the root, ignore it | |
writeEndElement:function(){ | |
this.active = this.stack.pop() || this.root; | |
}, | |
//add an attribute to the active node | |
writeAttributeString:function( name, value ){ | |
if( this.active ) | |
this.active.a[name] = value; | |
}, | |
//add a text node to the active node | |
writeString:function( text ){ | |
if( this.active ) | |
this.active.c.push(text); | |
}, | |
//shortcut, open an element, write the text and close | |
writeElementString:function( name, text, ns ){ | |
this.writeStartElement( name, ns ); | |
this.writeString( text ); | |
this.writeEndElement(); | |
}, | |
//add a text node wrapped with CDATA | |
writeCDATA:function( text ){ | |
this.writeString( '<![CDATA[' + text + ']]>' ); | |
}, | |
//add a text node wrapped in a comment | |
writeComment:function( text ){ | |
this.writeString('<!-- ' + text + ' -->'); | |
}, | |
//generate the xml string, you can skip closing the last nodes | |
flush:function(){ | |
if( this.stack && this.stack[0] )//ensure it's closed | |
this.writeEndDocument(); | |
var | |
chr = '', indent = '', num = this.indentation, | |
formatting = this.formatting.toLowerCase() == 'indented', | |
buffer = '<?xml version="'+this.version+'" encoding="'+this.encoding+'"'; | |
if( this.standalone !== undefined ) | |
buffer += ' standalone="'+!!this.standalone+'"'; | |
buffer += ' ?>'; | |
buffer = [buffer]; | |
if( this.doctype && this.root ) | |
buffer.push('<!DOCTYPE '+ this.root.n + ' ' + this.doctype+'>'); | |
if( formatting ){ | |
while( num-- ) | |
chr += this.indentChar; | |
} | |
if( this.root )//skip if no element was added | |
format( this.root, indent, chr, buffer ); | |
return buffer.join( formatting ? this.newLine : '' ); | |
}, | |
//cleanup, don't use again without calling startDocument | |
close:function(){ | |
if( this.root ) | |
clean( this.root ); | |
this.active = this.root = this.stack = null; | |
}, | |
getDocument: typeof window !== 'undefined' && window.ActiveXObject | |
? function(){ //MSIE | |
var doc = new ActiveXObject('Microsoft.XMLDOM'); | |
doc.async = false; | |
doc.loadXML(this.flush()); | |
return doc; | |
} | |
: function(){// Mozilla, Firefox, Opera, etc. | |
return (new DOMParser()).parseFromString(this.flush(), 'text/xml'); | |
} | |
}; | |
//utility, you don't need it | |
function clean( node ){ | |
var l = node.c.length; | |
while( l-- ){ | |
if( typeof node.c[l] == 'object' ) | |
clean( node.c[l] ); | |
} | |
node.n = node.a = node.c = null; | |
}; | |
//utility, you don't need it | |
function format( node, indent, chr, buffer ){ | |
var | |
xml = indent + '<' + node.n, | |
nc = node.c.length, | |
attr, child, i = 0; | |
for( attr in node.a ) | |
xml += ' ' + attr + '="' + node.a[attr] + '"'; | |
xml += nc ? '>' : ' />'; | |
buffer.push( xml ); | |
if( nc ){ | |
do{ | |
child = node.c[i++]; | |
if( typeof child == 'string' ){ | |
if( nc == 1 )//single text node | |
return buffer.push( buffer.pop() + child + '</'+node.n+'>' ); | |
else //regular text node | |
buffer.push( indent+chr+child ); | |
}else if( typeof child == 'object' ) //element node | |
format(child, indent+chr, chr, buffer); | |
}while( i < nc ); | |
buffer.push( indent + '</'+node.n+'>' ); | |
} | |
}; | |
})(); | |
(function() { | |
function prettyPrint(obj) { | |
var pstring = Object.prototype.toString.call(obj) | |
switch (pstring) { | |
case '[object Number]': | |
return '' + obj | |
break; | |
case '[object String]': | |
return obj | |
break; | |
case '[object Boolean]': | |
return '' + obj ? 'true' : 'false' | |
break; | |
case '[object Date]': | |
return '' + obj.format('yyyy-MM-dd h:mm:ss.S') | |
break; | |
case '[object Function]': | |
return '' + obj.format('yyyy-MM-dd h:mm:ss.S') | |
break; | |
default: | |
return JSON.stringify(obj) | |
} | |
} | |
// extend date object | |
Date.prototype.format = function(format) //author: meizz | |
{ | |
var o = { | |
"M+" : this.getMonth()+1, //month | |
"d+" : this.getDate(), //day | |
"h+" : this.getHours(), //hour | |
"m+" : this.getMinutes(), //minute | |
"s+" : this.getSeconds(), //second | |
"q+" : Math.floor((this.getMonth()+3)/3), //quarter | |
"S" : this.getMilliseconds() //millisecond | |
} | |
if(/(y+)/.test(format)) format=format.replace(RegExp.$1, | |
(this.getFullYear()+"").substr(4 - RegExp.$1.length)); | |
for(var k in o)if(new RegExp("("+ k +")").test(format)) | |
format = format.replace(RegExp.$1, | |
RegExp.$1.length==1 ? o[k] : | |
("00"+ o[k]).substr((""+ o[k]).length)); | |
return format; | |
} | |
// actual setup of environment | |
var console = typeof console === 'undefined' ? {} : console | |
function getEnvironment() { | |
return typeof phantom !== 'undefined' ? "phantom" : "node" | |
} | |
function getCmdArgs() { | |
var args = {}, env = getEnvironment() | |
if (["phantom", "node"].indexOf(env) != -1) { | |
var source = env == "phantom" ? phantom.args : process.argv.splice(2) | |
for (var i=0;i<source.length;i++) { | |
var kva = source[i].split("="), | |
k = kva[0].split('--')[1], | |
v = kva[1] | |
args[k] = v | |
} | |
} else { | |
throw "Unknown environment" | |
} | |
return args | |
} | |
function parseCmdArgs(ARGS) { | |
var configuration = {}, cargs = getCmdArgs() | |
for (var a in ARGS) { | |
var elv = cargs[a] == null ? ARGS[a]["default"] : cargs[a] | |
switch (ARGS[a]["type"]) { | |
case "string": elv = String(elv); break; | |
case "number": elv = Number(elv); break; | |
case "boolean": elv = ([1, "y", "yes", "true"].indexOf(elv) != -1 ? true : false); break; | |
default: break; | |
} | |
configuration[a] = elv | |
} | |
return configuration | |
} | |
function getUsage(args) { | |
var configuration = ["Usage(all values should be typed as json encoded strings):"] | |
for(var a in args) { | |
var el = args[a] | |
configuration.push("\t- " + a + " = " + el.help + (el["default"] != null ? (" (default: " + JSON.stringify(el["default"]) + ")") : "")) | |
} | |
return configuration.join("\n") | |
} | |
function getCleanURL(url) { | |
if (url.indexOf('http') == 0 || url.indexOf("file://") == 0) { | |
return url | |
} | |
return "file://" + path.resolve(url).replace(/\\/g, "/").replace(":/", "|/") | |
} | |
function generateLog(ostate, output, onDone) { | |
var fileout = configuration.output != null, | |
fileGenerate = function(output, destination) { | |
var fs = require('fs') | |
fs.write(destination, output, 'w') | |
if (onDone) { | |
onDone() | |
} | |
} | |
// console.dump(ostate) | |
switch (configuration.format) { | |
case 'console': | |
// should add fileout support for console | |
break; | |
case 'junit': | |
var xw = new XMLWriter('UTF-8', '1.0') | |
xw.writeStartDocument() | |
xw.formatting = 'indented' | |
xw.indentChar = ' ' | |
xw.indentation = 2 | |
xw.writeStartElement('testsuites') | |
xw.writeAttributeString('name', 'QUnit') | |
for (var tskey in ostate.collect) { | |
var suite = ostate.collect[tskey] | |
// console.dump(suite) | |
// generate test suite details | |
xw.writeStartElement('testsuite') | |
xw.writeAttributeString('name', suite.name) | |
xw.writeAttributeString('tests', suite.total) | |
xw.writeAttributeString('passed', suite.passed) | |
xw.writeAttributeString('failures', suite.failed) | |
xw.writeAttributeString('errors', suite.total - suite.failed - suite.passed) | |
xw.writeAttributeString('time', suite.duration) | |
// generate test suite test details | |
for (var tckey in suite.tests) { | |
var tcase = suite.tests[tckey] | |
xw.writeStartElement('testcase') | |
xw.writeAttributeString('classname', suite.name) | |
xw.writeAttributeString('name', tcase.name) | |
xw.writeAttributeString('time', tcase.duration) | |
// check if there is anything outputed by console dumps | |
var okey = 'all-module-' + suite.name + '-test-' + tcase.name, | |
stdout = [], | |
stderr = [] | |
if (output[okey] != null) { | |
for (var ostidx in output[okey]) { | |
var ostel = output[okey][ostidx] | |
if (ostel.destination == 'stdout') { | |
stdout.push(ostel.buffer) | |
} | |
if (ostel.destination == 'stderr') { | |
stderr.push(ostel.buffer) | |
} | |
} | |
} | |
// loop over failures and add them to the dom | |
// <failure type="sikuli-generic" message="sikuli-generic error"> | |
for (var f=0;f<tcase.asserts['failure'].length;f++) { | |
var failure = tcase.asserts['failure'][f] | |
xw.writeStartElement('failure') | |
xw.writeAttributeString('type', 'assert') | |
xw.writeAttributeString('message', failure.message) | |
xw.writeCDATA(failure.source) | |
xw.writeEndElement() // <- </failure> | |
} | |
// asserts always go to stdout | |
for (var s=0;s<tcase.asserts['success'].length;s++) { | |
var success = tcase.asserts['success'][s] | |
stdout.push(success.message) | |
} | |
// write system output | |
xw.writeStartElement('system-out') | |
xw.writeCDATA(stdout.join("\n")) | |
xw.writeEndElement() // <- </system-out> | |
// write system output | |
xw.writeStartElement('system-err') | |
xw.writeCDATA(stderr.join("\n")) | |
xw.writeEndElement() // <- </system-out> | |
xw.writeEndElement() // <- </testcase> | |
} | |
xw.writeEndElement() // <- </testsuite> | |
} | |
xw.writeEndElement() // <- </testsuites> | |
xw.writeEndDocument() | |
var output = (new XMLSerializer()).serializeToString(xw.getDocument()) | |
if (fileout) { | |
fileGenerate(output, configuration.output, onDone) | |
} else { | |
console.dump(output) | |
if (onDone) onDone() | |
} | |
break; | |
default: | |
console.dump('unknonw format ' + format) | |
if (onDone) onDone() | |
break; | |
} | |
} | |
// main | |
try { | |
var ARGS = { | |
"url": { "default": "index.html", "type": "string", "help": "QUnit capable test url" }, | |
"format": { "default": "console", "choices": ["console", "junit"], "type": "string", "help": "The test output format. [console, junit]]" }, | |
"output": { "default": null, "type": "none", "help": "Destination for output buffer" }, | |
"noglobals": { "default": false, "type": "boolean", "help": "Invoke QUnit with the noglobals setting" }, | |
"notrycatch": { "default": false, "type": "boolean", "help": "Invoke QUnit with the notrycatch setting" } | |
} | |
var configuration = parseCmdArgs(ARGS), | |
url = getCleanURL(configuration["url"]), | |
isPhantom = getEnvironment() == "phantom", | |
isNode = getEnvironment() == "node", | |
coutput = {"before": [], "current": []}, | |
currentModule = null, | |
currentTest = null, | |
runtime = { | |
"modules": { | |
} | |
} | |
if (isPhantom) { | |
var system = require('system') | |
levels = ['log', 'info', 'debug', 'warn', 'error', 'warning', 'notice'], | |
ostate = { | |
current: { module: null, test: null }, | |
collect: {} | |
}, | |
ensureModuleInited = function(module) { | |
if (ostate.collect[module] == null) { | |
ostate.collect[module] = { | |
'start': new Date(), | |
'done': null, | |
'duration': 0, | |
'failed': 0, | |
'passed': 0, | |
'total': 0, | |
'type': 'module', | |
'name': module, | |
'tests': {} | |
} | |
} | |
return ostate.collect[module] | |
}, | |
ensureTestsInited = function(test, module) { | |
var mdesc = ensureModuleInited(module) | |
if (mdesc.tests[test] == null) { | |
mdesc.tests[test] ={ | |
'start': new Date(), | |
'done': null, | |
'duration': 0, | |
'asserts': { | |
'success': [], | |
'failure': [] | |
}, | |
'type': 'test', | |
'name': test | |
} | |
} | |
return mdesc.tests[test] | |
}, | |
pushToLog = function(buffer, destination) { | |
var keys = ['all'] | |
if (ostate.current.module != null) { | |
keys.push('module-'+ostate.current.module) | |
} | |
if (ostate.current.test != null) { | |
keys.push('test-'+ostate.current.test) | |
} | |
var key = keys.join('-') | |
if (output[key] == null) { | |
output[key] = [] | |
} | |
output[key].push({buffer: buffer, destination: destination}) | |
}, | |
output = {} | |
for (var lidx in levels) { | |
var level = levels[lidx] | |
console[level] = (function(level) { | |
return function() { | |
var destination = level == 'error' ? 'stderr' : 'stdout' | |
var args = Array.prototype.slice.call(arguments, 0) | |
var obuf = args.length == 1 ? prettyPrint(args[0]): prettyPrint(args) | |
var parts = [ | |
'['+((new Date()).format('yyyy-MM-dd h:mm:ss.S'))+']', | |
'[console.'+level+']', | |
' ' + obuf | |
] | |
var buffer = parts.join('') | |
// collect output | |
pushToLog(obuf, destination) | |
system[destination].writeLine(buffer) | |
} | |
})(level) | |
} | |
console.dump = function() { | |
var args = Array.prototype.slice.call(arguments, 0) | |
var obuf = args.length == 1 ? prettyPrint(args[0]): prettyPrint(args) | |
system.stdout.writeLine(obuf) | |
} | |
// qunit test reactors | |
var reactors = { | |
// each assertion goes through log? | |
log: function(payload) { | |
var ainfo = payload.details, | |
message = ainfo.message, | |
source = ainfo.source == null ? null : ainfo.source | |
ostate.collect[ainfo.module].tests[ainfo.name].asserts[ainfo.result ? 'success':'failure'].push({ | |
message: message, | |
source: source | |
}) | |
if (configuration.format == 'console' && configuration.output == null) { | |
system[ainfo.result?'stdout':'stderr'].writeLine((ainfo.result ? 'SUCCESS' : 'FAILURE') + ' - ' + ainfo.module + '.' + ainfo.name + ' - ' + message) | |
} | |
}, | |
begin: function(payload) { | |
console.dump('begin', payload) | |
}, | |
done: function(payload) { | |
console.dump('end', payload) | |
generateLog(ostate, output, function() { | |
phantom.exit(0) | |
}) | |
}, | |
moduleStart: function(payload) { | |
console.dump('moduleStart', payload) | |
var minfo = payload.details | |
// collect each test case into module relative dictionary | |
ensureModuleInited(minfo.name) | |
// for external loggings or outputs | |
ostate.current.module = minfo.name | |
}, | |
moduleDone: function(payload) { | |
console.dump('moduleDone', payload) | |
var minfo = payload.details | |
ostate.collect[minfo.name].done = new Date() | |
ostate.collect[minfo.name].failed = minfo.failed | |
ostate.collect[minfo.name].passed = minfo.passed | |
ostate.collect[minfo.name].total = minfo.total | |
ostate.collect[minfo.name].duration = 0 | |
// compute the duration | |
for (var tk in ostate.collect[minfo.name].tests) { | |
var test = ostate.collect[minfo.name].tests[tk] | |
ostate.collect[minfo.name].duration += test.duration | |
} | |
// reset the module | |
ostate.current.module = null | |
}, | |
testStart: function(payload) { | |
console.dump('testStart', payload) | |
var tinfo = payload.details | |
ensureTestsInited(tinfo.name, tinfo.module) | |
// for external loggings or outputs | |
ostate.current.test = tinfo.name | |
}, | |
testDone: function(payload) { | |
console.dump('testDone', payload) | |
var tinfo = payload.details | |
ostate.collect[tinfo.module].tests[tinfo.name].done = new Date() | |
ostate.collect[tinfo.module].tests[tinfo.name].duration = tinfo.duration | |
// reset the test | |
ostate.current.test = null | |
} | |
} | |
var page = require('webpage').create(), | |
step = "current", | |
testsTotal=0, testsDuration=0, testsFailed=0, testsPassed=0 | |
page.onConsoleMessage = function() { | |
// cannot make difference between log,error,info,warn,notice so all is sent to log | |
var args = Array.prototype.slice.call(arguments, 0) | |
if (coutput[step] == null) coutput[step] = [] | |
coutput[step].push(args) | |
console.log.apply(console, args) | |
} | |
page.onError = function(msg, trace) { | |
console.dump('error on page', msg, trace) | |
phantom.exit(1) | |
} | |
page.onInitialized = function() { | |
page.evaluate(function() { | |
window.document.addEventListener('DOMContentLoaded', function() { | |
// Setup Error Handling | |
var qunit_error = window.onerror; | |
window.onerror = function ( error, filePath, linerNr ) { | |
qunit_error(error, filePath, linerNr); | |
if (typeof window.callPhantom === 'function') { | |
window.callPhantom({ | |
'name': 'Window.error', | |
'error': error, | |
'filePath': filePath, | |
'linerNr': linerNr | |
}) | |
} | |
} | |
var callback = function(name) { | |
return function(details) { | |
if (typeof window.callPhantom === 'function') { | |
window.callPhantom({ | |
'name': 'QUnit.' + name, | |
'details': details | |
}) | |
} | |
} | |
} | |
var i, callbacks = [ | |
'begin', 'done', 'log', | |
'moduleStart', 'moduleDone', | |
'testStart', 'testDone' | |
]; | |
for (i=0; i<callbacks.length;i+=1) { | |
QUnit[callbacks[i]](callback(callbacks[i])) | |
} | |
}, false) | |
}) | |
} | |
page.onCallback = function(message) { | |
if (message) { | |
// console.dump(message.name, message) | |
if (message.name === 'Window.error') { | |
console.dump("completed with error: " + JSON.stringify(message)) | |
phantom.exit(1) | |
} else { | |
var msgs = message.name.split(".") | |
if (msgs[0] === "QUnit") { | |
if (reactors[msgs[1]] != null) { | |
reactors[msgs[1]](message) | |
} | |
} else { | |
console.dump('unknown message', message) | |
} | |
} | |
} | |
} | |
page.open(url, function(status) { | |
if (status !== 'success') { | |
console.dump('Unable to access network: ' + status) | |
phantom.exit(1) | |
} else { | |
// Cannot do this verification with the 'DOMContentLoaded' handler because it | |
// will be too late to attach it if a page does not have any script tags. | |
var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }) | |
if (qunitMissing) { | |
console.dump('The `QUnit` object is not present on this page.') | |
phantom.exit(1) | |
} | |
} | |
}) | |
} | |
} catch (e) { | |
console.dump("Error("+e+"), see usage help bellow") | |
console.dump(getUsage(ARGS)) | |
if (getEnvironment() == "phantom") { | |
phantom.exit(1) | |
} else if (getEnvironment() == "node") { | |
process.exit(1) | |
} else { | |
throw "Unknown environment" | |
} | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment