I had the need to run a bunch of QUnit test pages and get a page level visual with the ability to drill down into a page to examine the tests without hacking them to fit in a runner or adding/changing script references in each and every one.
A need is like an itch and I like to code, not scratch, so here is a little QUnit addon that you can safely tack on to the tail of your deployed qunit.js to enable a runner and to unobtrusively hook the referenced pages. You do not have to touch the test pages and they will run as usual standalone.
See it in action here


qunit-runner.js - add to qunit.js
/*
// <copyright project "Salient.QualityControl" file="qunit.runner.js" company="Sky Sanders">
// This source is a Public Domain Dedication.
// http://salientqc.codeplex.com
// Attribution is appreciated.
// </copyright>
*/
/* this file can safely be added to the tail of your qunit.js to simplify deployment */
(function(window) {
var runner = function(tests, sequential, done) {
this.failures = 0;
this.total = 0;
this.currentIndex = 0;
this.sequential = sequential;
this.tests = tests;
this.done = done || this.done;
var that = this;
$(document).ready(function() {
$(".runner-test-page-header").live("click", function() { $(this).next(".runner-test-page-frame").slideToggle(100); });
that.runPage();
});
}
runner.prototype.nextPage = function() {
if (this.currentIndex + 1 < this.tests.length) {
this.currentIndex++;
this.runPage();
}
}
runner.prototype.runPage = function() {
// update page counter
$("#qunit-runner-userAgent").html('<span class="runner-test-page-counter">Test Page '
+ (this.currentIndex + 1) + " of " + this.tests.length + "</span><br/>" + window.navigator.userAgent);
var test = this.tests[this.currentIndex];
test.title = test.title || test.page;
// load the test page in an iframe
test.header = $('<p class="runner-test-page-header">' + test.title + "</p>").addClass("passed")[0];
$("#runner-test-page-container").append(test.header);
test.frame = $('<iframe class="runner-test-page-frame" src="' + test.page + '" width="100%" height="200"></iframe>')[0];
$("#runner-test-page-container").append(test.frame);
if (!this.sequential) {
this.nextPage();
}
}
runner.prototype.pageProgress = function(frame, failures, total, testName, isDone) {
var that = this;
$.grep(this.tests, function(test, index) {
if (test.frame === frame) {
$(test.header).removeClass("passed").addClass(failures > 0 ? "failed" : "passed")
.html(test.title + " " + testName
+ (total == 0 ? "" : " " + failures + " failed, " + total + " total"));
if (isDone) {
test.complete = true;
that.failures += failures;
that.total += total;
// are all pages finished?
if ($.grep(that.tests, function(test, index) { return !test.complete; }).length == 0) {
$("#qunit-banner").addClass(that.failures > 0 ? "qunit-fail" : "qunit-pass");
that.done(that.failures, that.total);
}
else {
if (that.sequential) {
that.nextPage();
}
}
}
}
});
}
// if you need to be notified the runner is finished..
runner.prototype.done = function(failures, total) {
}
QUnit.run = function(tests, sequential, done) {
/// <param name="tests" type="Array"></param>
/// <param name="sequential" type="Boolean"></param>
/// <param name="done" type="Function">Function(failures, total) will be called when all tests complete.</param>
if (window.__qunit_runner) {
throw new Error("One runner per page please.");
}
window.__qunit_runner = new runner(tests, sequential, done);
}
// runner test page hooks - if this page has a runner as parent
// then set up the metric callbacks
if (top.__qunit_runner) {
var runner = top.__qunit_runner;
QUnit.done = function(failures, total) {
runner.pageProgress(window.frameElement, failures, total, "done", true);
};
QUnit.testStart = function(name) {
runner.pageProgress(window.frameElement, 0, 0, name + " started");
}
QUnit.testDone = function(name, failures, total) {
runner.pageProgress(window.frameElement, failures, total, name);
}
}
})(this);
qunit-runner.css - deploy this to runner
/*
// <copyright project "Salient.QualityControl" file="qunit.runner.css" company="Sky Sanders">
// This source is a Public Domain Dedication.
// http://salientqc.codeplex.com
// Attribution is appreciated.
// </copyright>
*/
/* this file can be added to qunit.css to simplify deployment */
.runner-test-page-counter
{
font-size: 110%;
}
.runner-test-page-header
{
position: relative;
font-family: "Helvetica Neue Light" , "HelveticaNeue-Light" , "Helvetica Neue" , Calibri, Helvetica, Arial;
list-style-position: inside;
font-size: small;
padding: 5px 10px;
cursor: pointer;
margin: 1px;
}
.runner-test-page-header.failed
{
background-color: #EE5757;
color: #000000;
}
.runner-test-page-header.passed
{
background-color: #D2E0E6;
color: #528CE0;
}
.runner-test-page-frame
{
background-color: #F4F4F8;
display: none;
}
h2#qunit-runner-userAgent
{
font-family: "Helvetica Neue Light" , "HelveticaNeue-Light" , "Helvetica Neue" , Calibri, Helvetica, Arial;
background-color: #2b81af;
margin: 0;
padding: 0;
color: #fff;
font-size: small;
padding: 0.5em 0 0.5em 2.5em;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
Usage - assuming the runner is rolled up into qunit.js
runner.htm
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<link href="scripts/qunit.css" rel="stylesheet" type="text/css" />
<link href="scripts/qunit.runner.css" rel="stylesheet" type="text/css" />
<script src="scripts/jquery-1.4.1.js" type="text/javascript"></script>
<script src="scripts/qunit.js" type="text/javascript"></script>
<script type="text/javascript">
var tests = [
{ page: "TestPage01.htm", title: "Test Page 01 - click me to view test page" },
{ page: "TestPage02.htm" },
{ page: "TestPage01.htm" }
];
var sequential = true; // false to run tests simultaneously
var runnerDone = function(failures, total) {
// get notified when runner completes
}
QUnit.run(tests, sequential, runnerDone);
</script>
</head>
<body>
<div>
<h1 id="qunit-header">TEST RUNNER</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-runner-userAgent"></h2>
<div id="runner-test-page-container"></div>
</div>
</body>
</html>
TODO: I can see adding some more instrumentation if I get interested in it again, maybe a total footer.