Skip to content

Instantly share code, notes, and snippets.

@iongion
Created December 5, 2014 10:15
Show Gist options
  • Save iongion/f23edaa708eafc240a8b to your computer and use it in GitHub Desktop.
Save iongion/f23edaa708eafc240a8b to your computer and use it in GitHub Desktop.
phantom-qunit-junit-runner.js
/**
* 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