Skip to content

Instantly share code, notes, and snippets.

@gseguin
Created May 22, 2012 22:05
Show Gist options
  • Save gseguin/2771964 to your computer and use it in GitHub Desktop.
Save gseguin/2771964 to your computer and use it in GitHub Desktop.
PhantomJS QUnit test runner with multiple output
/*
* QUnit Qt+WebKit powered headless test runner using Phantomjs
*
* Phantomjs installation: http://code.google.com/p/phantomjs/wiki/BuildInstructions
*
* Run with:
* phantomjs phantomjs-qunit-runner.js [url-of-your-qunit-testsuite]
*
* E.g.
* phantomjs phantomjs-qunit-runner.js http://localhost/qunit/test
*/
/**
* Wait until the test condition is true or a timeout occurs. Useful for waiting
* on a server response or for a ui change (fadeIn, etc.) to occur.
*
* @param testFx javascript condition that evaluates to a boolean,
* it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
* as a callback function.
* @param onReady what to do when testFx condition is fulfilled,
* it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
* as a callback function.
* @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
*/
function waitFor(testFx, onReady, timeOutMillis) {
var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timout is 3s
start = new Date().getTime(),
condition = false,
interval = setInterval(function() {
if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
// If not time-out yet and condition not yet fulfilled
condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
} else {
if(!condition) {
// If condition still not fulfilled (timeout but condition is 'false')
console.log("'waitFor()' timeout");
phantom.exit(1);
} else {
// Condition fulfilled (timeout and/or condition is 'true')
typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
clearInterval(interval); //< Stop this interval
}
}
}, 100); //< repeat check every 250ms
};
function consoleLogging() {
var module;
QUnit.moduleStart = function(context) {
module = context.name;
}
var current_test_assertions = [];
QUnit.testDone = function(result) {
var name = module + ": " + result.name;
if (result.failed) {
console.log("\u001B[31m✖ " + name);
for (var i = 0; i < current_test_assertions.length; i++) {
console.log(" " + current_test_assertions[i]);
}
console.log("\u001B[39m");
}
current_test_assertions = [];
};
QUnit.log = function(details) {
if (details.result) {
return;
}
var response = details.message || "";
if (details.expected) {
if (response.length > 0) {
response += ", ";
}
response += "expected: " + details.expected + ", but was: " + details.actual;
}
current_test_assertions.push("Failed assertion: " + response);
};
QUnit.done = function(result) {
console.log("Took " + result.runtime + "ms to run " + result.total + " tests. \u001B[32m✔ " + result.passed + "\u001B[39m \u001B[31m✖ " + result.failed + "\u001B[39m ");
};
}
function junitLogging() {
var module,
moduleStart,
testStart,
testCases = [],
current_test_assertions = [];
QUnit.begin = function() {
// That does not work when invoked in PhantomJS
}
QUnit.moduleStart = function(context) {
moduleStart = new Date();
module = context.name;
testCases = [];
}
QUnit.moduleDone = function(context) {
// context = { name, failed, passed, total }
var xml = ']]>\t<testsuite name="'+context.name+'" errors="0" failures="'+context.failed+'" tests="'+context.total+'" time="'+(new Date() - moduleStart)/1000+'"';
if(testCases.length) {
xml += '>\n<![CDATA[';
for (var i=0, l=testCases.length; i<l; i++) {
xml += testCases[i];
}
xml += ']]>\n\t</testsuite>\n<![CDATA[';
} else {
xml += '/>\n<![CDATA[';
}
console.log(xml);
}
QUnit.testStart = function() {
testStart = new Date();
}
QUnit.testDone = function(result) {
// result = { name, failed, passed, total }
var xml = ']]>\n\t\t<testcase classname="'+module+'" name="'+result.name+'" time="'+(new Date() - testStart)/1000+'"';
if (result.failed) {
xml += '>\n<![CDATA[';
for (var i = 0; i < current_test_assertions.length; i++) {
xml += "\t\t\t" + current_test_assertions[i];
}
xml += ']]>\t\t</testcase>\n<![CDATA[';
} else {
xml += '/>\n<![CDATA[';
}
current_test_assertions = [];
testCases.push(xml);
};
QUnit.log = function(details) {
function xmlEncode(message) {
return message.replace(/\&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/\'/g,'&apos;').replace(/\"/g,'&quot;');
}
//details = { result , actual, expected, message }
if (details.result) {
return;
}
var message = details.message || "";
if (details.expected) {
if (message.length > 0) {
message += ", ";
}
message += "expected: " + details.expected + ", but was: " + details.actual;
}
var xml = ']]>\n<failure type="failed" message="' + xmlEncode(message) + '"/>\n<![CDATA[';
current_test_assertions.push(xml);
};
QUnit.done = function(result) {
console.log(']]></testsuites>');
console.log = console.error = console.debug = function () {};
};
}
var args, url,
out = "console",
orientation = "portrait",
noQUnitAutoStart = false,
page = new WebPage();
if (phantom.args.length === 0 || phantom.args.length > 3) {
console.log('Usage: phantomjs-test-runner.js [-j|--junit] [-s] <some URL>');
phantom.exit();
} else {
args = phantom.args.slice();
url = args.pop();
while (args.length) {
var arg = args.pop().toLowerCase();
switch (arg) {
case "-j":
case "--junit":
out = "junit";
break;
case "-l":
case "--landscape":
orientation = "landscape";
break;
case "-p":
case "--portrait":
orientation = "portrait";
break;
case '-s':
noQUnitAutoStart = true;
break;
default:
}
}
}
if (noQUnitAutoStart) {
if(url.indexOf("?") == -1) {
url += "?";
} else {
url += "&";
}
url += "noautostart=true";
}
// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this")
page.onConsoleMessage = function(msg) {
console.log(msg);
};
page.viewportSize = (orientation === "portrait") ? { width: 768, height: 1024 } : { width: 1024, height: 768 };
if ( out === "junit" ) {
console.log('<?xml version="1.0" encoding="UTF-8"?>\n');
console.log('<testsuites name="testsuites"><![CDATA[');
}
page.open(url, function(status){
if (status !== "success") {
console.log("Unable to access network");
phantom.exit( 1 );
} else {
page.evaluate(this[out+"Logging"]);
if(noQUnitAutoStart) {
page.evaluate(function() {
console.log("starting qunit from test runner.");
QUnit.start();
});
}
waitFor(function(){
return page.evaluate(function(){
var el = document.getElementById('qunit-testresult');
if (el && el.innerText.match('completed')) {
return true;
}
return false;
});
}, function(){
var failedNum = page.evaluate(function(){
var el = document.getElementById('qunit-testresult');
try {
return el.getElementsByClassName('failed')[0].innerHTML;
} catch (e) { }
return 10000;
});
phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0);
}, 180000);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment