David Wheeler - Test.Simple-0.29
Name
Test.Harness.Browser - Run TAP standard JavaScript test scripts with statistics in a Browser
Synopsis
<html>
<head>
<script type="text/javascript" src="JSAN.js"></script>
</head>
<body>
<script type="text/javascript">
new JSAN("../lib").use("Test.Harness.Browser");
new Test.Harness.Browser('JSAN.js').runTests(
'async.js',
'bad_plan.html',
'buffer.js',
'builder.html'
);
</script>
</body>
</html>
or non-JSAN approach (JavaScript test files will not work without JSAN)
<html>
<head>
<script type="text/javascript" src="Test/Harness.js"></script>
<script type="text/javascript" src="Test/Harness/Browser.js"></script>
</head>
<body>
<script type="text/javascript">
Test.Harness.Browser.runTests(
'async.html',
'bad_plan.html',
'buffer.html',
'builder.html'
);
</script>
</body>
</html>
Description
STOP! If all you want to do is write a test script, consider using Test.Simple. Test.Harness is the module that reads the output from Test.Simple, Test.More and other modules based on Test.Builder. You don't need to know about Test.Harness to use those modules.
Test.Harness.Browser runs JavaScript tests in a browser and expects to get the results from the TestResults attribute of the Test.Builder object constructed by each test script. These results conform to a format called TAP, the Test Anything Protocol. It is defined in http://search.cpan.org/dist/Test-Harness/lib/Test/Harness/TAP.pod. See Test.Harness for details on the output.
Class Methods
Test.Harness.Browser.runTests('testone.js', 'testtwo.html');
Constructs a new Test.Harness.Browser object and calls its runTests() instance method, passing all arguments along.
Constructors
var harness = new Test.Harness.Browser('script.js', 'another.js');
Constructs a new Test.Harness.Browser object. If your tests will be pure JavaScript files (that is, ending in .js, pass in a list of dependency scripts to be loaded before each script. A particularly handy one to load is JSAN.js (http://www.openjsan.org/go?l=JSAN). This library will allow you to dymamically load whatever other libraries you need from each test script, like so:
var jsan = new JSAN('../lib');
jsan.use('Test.More');
jsan.use('HTTP.Query');
plan({tests: 1});
var q = new HTTP.Query;
isaOK(q, HTTP.Query);
In fact, this is the approach that Test.Simple's own tests take. Check 'em out!
Instance Methods
- runTests
-
harness.runTests('testone.js', 'testtwo.html');This method runs all the given test files and divines whether they passed or failed based on the contents of the
TestResultsattribute of their globalTest.Builder.Testobject. It prints out each individual test that failed along with a summary report and a how long it all took. When all tests have been run, a diagnostic message will be output. See Test.Harness for details on the output.For .js files, be sure to pass to the Test.Harness.Browser constructor a list of required scripts to be loaded before the test is loaded and executed.
GET Options
An HTML file that uses Test.Harness.Browser will automatically process the GET its arguments, and these can be used to affect the behavior of the harness.
- verbose
-
index.html?verbose=1
Set the
verboseoption to a true value to have all of the output of all of the tests in the harness output to the browser window. By default, only failing tests display their output. - file
-
index.html?file=foo.html;file=bar.html
Set the
fileoption to override the list of files passed to runTest(). This option may be specified multiple times, and each file specified will be passed to runTest().
Bugs
Safari (and maybe KHTML?) has a number of bugs that affect how Test.Harness.Browser works. The most obvious is that it cannot run tests on a local disk. The harness only works in Safari if the tests are served by a Web server. The WebKit team is aware of the issue; expect it to be fixed in a future version.
Other Safari bugs I reported while writing this module:
- iFrame Doesn't seem to Respect a local "file://" src
- Function.toString() Doesn't Stringify Constructors as Attributes
-
Some tests are skipped in tests/create.html, tests/harness.html, and tests/more.html to work around this bug.
- WebKit JavaScript Does not Properly Support Circular References
-
One test is skipped in tests/circular_data.html to work around this bug.
- iFrames Appear to be Cached
- iFrames set to display:none are Missing from frames array
-
So the iframe used to run tests isn't hidden in Safari. Instead, it is set to "height: 0; widht: 0".
- Add Support for the watch() method of Object
-
This would just be nice to have, so that we wouldn't have to set timeouts to check for test completion.
See Also
Test.Harness, the base class for this class.
Test.Simple and Test.More, modules with which to write tests.
Authors
David Wheeler <david@kineticode.com>.
Copyright
Copyright 2005 by David Wheeler <david@kineticode.com>
This program is free software; you can redistribute it and/or modify it under the terms of the Perl Artistic License or the GNU GPL.
See http://www.perl.com/perl/misc/Artistic.html and http://www.gnu.org/copyleft/gpl.html.
// $Id$
/*global JSAN, Test, ActiveXObject */
if (typeof JSAN != 'undefined') JSAN.use('Test.Harness');
else {
if (typeof Test == 'undefined') Test = {};
if (!Test.Harness) Test.Harness = {};
}
if (window.parent != window &&
location.href.replace(/[?#].+/, "") == parent.location.href.replace(/[?#].+/, ""))
{
// We're in a test iframe. Set up the necessary parts and load the
// test script with XMLHttpRequest (the Safari and Opera xml-hack).
var __MY = {};
// Build fake T.H.B so original script from this file doesn't throw
// exception. This is a bit of a hack...
Test.Harness.Browser = function() {
this.runTests = function() {};
this.encoding = function () { return this };
};
// Create the test script element.
__MY.pre = document.createElement("pre");
__MY.pre.id = "test";
if (window.parent.Test.Harness.Browser._encoding) {
// Set all scripts to use the appropriate encoding.
__MY.scripts = document.getElementsByTagName('script');
for (var j = 0; j < __MY.scripts.length; j++) {
__MY.scripts[j].charset =
window.parent.Test.Harness.Browser._encoding;
}
}
// XXX replace with a script element at some point? Safari is due to
// have this working soon (not sure about IE or Opera):
// http://bugs.webkit.org/show_bug.cgi?id=3748
__MY.inc = window.parent.Test.Harness.Browser.includes;
__MY.req = typeof XMLHttpRequest != 'undefined'
? new XMLHttpRequest()
: new ActiveXObject("Microsoft.XMLHTTP");
for (var k = 0; k < __MY.inc.length; k++) {
__MY.req.open("GET", __MY.inc[k], false);
__MY.req.send(null);
var stat = __MY.req.status;
// OK Not Modified IE Cached Safari cached
if (stat == 200 || stat == 304 || stat == 0 || stat == null) {
eval(__MY.req.responseText);
} else {
throw new Error(
"Unable to load " + __MY.inc[k]
+ ': Status ' + __MY.req.status
);
}
}
// IE 6 SP 2 doesn't seem to run the onload() event, so we force the
// issue.
Test.Builder._finish(Test);
// XXX Opera throws a DOM exception here, but I don't know what to do
// about that.
__MY.body = document.body
|| document.getElementsByTagName("body")[0].appendChild(__MY.pre);
if (__MY.body) __MY.body.appendChild(__MY.pre);
else if (document.appendChild) document.appendChild(__MY.pre);
} else {
// Create the harness and run the tests.
Test.Harness.Browser = function () {
this.includes = Test.Harness.Browser.includes = [];
Array.prototype.push.apply(Test.Harness.Browser.includes, arguments);
this.includes.push('');
};
Test.Harness.Browser.VERSION = '0.29';
Test.Harness.Browser.runTests = function () {
var harness = new Test.Harness.Browser();
harness.runTests.apply(harness, arguments);
};
Test.Harness.Browser.prototype = new Test.Harness();
Test.Harness.Browser.prototype.interval = 100;
Test.Harness.Browser.prototype._setupFrame = function () {
// Setup the iFrame to run the tests.
var node = document.getElementById('buffer');
if (node) return node.contentWindow || frames.buffer;
node = document.createElement("iframe");
node.setAttribute( 'id', 'buffer' );
node.setAttribute( 'name', 'buffer' );
node.style.visibility = 'hidden';
// http://www.quirksmode.org/bugreports/archives/2005/02/hidden_iframes.html
node.style.height = '1';
node.style.width = '1';
document.body.appendChild(node);
return node.contentWindow || frames.buffer;
};
Test.Harness.Browser.prototype._setupOutput = function () {
// Setup the pre element for test output.
var node = document.createElement('pre');
node.setAttribute('id', 'output');
document.body.appendChild(node);
fixoutput = function(node) {
// Trailing space added and replaced to work around yet another
// Safari bug.
node.innerHTML = node.innerHTML.replace(
/ ?(\w[\w\.\-]+?\w)(?=\.\.\.)/m,
(/MSIE/.test(navigator.userAgent) ? '<br>' : '') +
'<a href="$1">$1</a>'
) + ' ';
};
return {
pass: function (msg) {
node.appendChild(document.createTextNode(msg));
window.scrollTo(0, document.body.offsetHeight
|| document.body.scrollHeight);
fixoutput(node);
},
fail: function (msg) {
var red = document.createElement('span');
red.setAttribute('style', 'color: red; font-weight: bold');
node.appendChild(red);
red.appendChild(document.createTextNode(msg));
window.scrollTo(0, document.body.offsetHeight
|| document.body.scrollHeight);
}
};
};
Test.Harness.Browser.prototype._setupSummary = function () {
// Setup the div for the summary.
var node = document.createElement("pre");
node.setAttribute("id", "summary");
document.body.appendChild(node);
return function (msg) {
node.appendChild(document.createTextNode(msg));
window.scrollTo(0, document.body.offsetHeight
|| document.body.scrollHeight);
};
};
Test.Harness.Browser.prototype.runTests = function () {
Test.Harness.Browser._encoding = this.encoding();
var files = this.args.file
? typeof this.args.file == 'string' ? [this.args.file] : this.args.file
: arguments;
if (!files.length) return;
var outfiles = this.outFileNames(files);
var buffer = this._setupFrame();
var harness = this;
var ti = 0;
var start;
var output = this._setupOutput();
var summaryOutput = this._setupSummary();
// These depend on how we're watching for a test to finish.
var finish = function () {}, runNext = function () {};
// This function handles most of the work of outputting results and
// running the next test, if there is one.
var runner = function () {
harness.outputResults(
buffer.Test.Builder.Test,
files[ti],
output,
harness.args
);
if (files[++ti]) {
output.pass(
outfiles[ti]
+ (harness.args.verbose ? Test.Harness.LF : '')
);
harness.runTest(files[ti], buffer);
runNext();
} else {
harness.outputSummary(summaryOutput, new Date() - start);
finish();
}
};
// XXX Support IE's propertychange event?
// http://msdn.microsoft.com/workshop/author/dhtml/reference/events/onpropertychange.asp
if (Object.watch) {
// We can use the cool watch method, and avoid setting timeouts!
// We just need to unwatch() when all tests are finished.
finish = function () { Test.Harness.unwatch('Done') };
Test.Harness.watch('Done', function (attr, prev, next) {
if (next < buffer.Test.Builder.Instances.length) return next;
runner();
return 0;
});
} else {
// Damn. We have to set timeouts. :-(
var pkg;
var wait = function () {
// Check Test.Harness.Done. If it's non-zero, then we know
// that the buffer is fully loaded, because it has incremented
// Test.Harness.Done. Grrr.. IE 6 SP 2 seems to delete
// buffer.Test after all the tests have finished running, but
// before this code executes for the correct number of
// completed tests. So we cache it in a variable outside of
// the function on previous calls to the function.
if (!pkg) pkg = buffer.Test;
if (Test.Harness.Done > 0
&& Test.Harness.Done >= pkg.Builder.Instances.length)
{
Test.Harness.Done = 0;
// Avoid race condition by resetting the instances, too. I
// have no idea why this might remain set from a previous
// test, but such can be the case in IE 6 SP 2.
pkg.Builder.Instances = [];
runner();
} else {
window.setTimeout(wait, harness.interval);
}
};
// We'll just have to set a timeout for the next test.
runNext = function () {
window.setTimeout(wait, harness.interval);
};
window.setTimeout(wait, this.interval);
}
// Now start the first test.
output.pass(outfiles[ti] + (this.args.verbose ? Test.Harness.LF : ''));
start = new Date();
this.runTest(files[ti], buffer);
};
Test.Harness.Browser.prototype.runTest = function (file, buffer) {
var fileType = /\.html$/.test(file) ? 'html'
: /\.js$/.test(file) ? 'js'
: this.defaultTestType
if ( fileType == 'html' ) {
buffer.location.replace(file);
}
else if ( fileType == 'js' ) {
if (/MSIE|Safari|Opera|Konqueror/.test(navigator.userAgent)) {
// These browsers have problems with the DOM solution, though
// I'm not sure why. It simply doesn't work in Safari, and
// Opera considers its handling of buffer.document to be a
// security violation. Theoretically, we should be able to get
// it working with all browsers and get rid of the XML hack,
// but it will require more expertise than I possess. In the
// meantime, we have to live with it.
this.includes[this.includes.length-1] = file;
buffer.location.replace(location.pathname + "?xml-hack=1");
return;
}
// Use the DOM (document.write() won't work) to create a new
// document with script elements for all of the JavaScript scrips
// we want to run.
var doc = buffer.document;
doc.open("text/html");
doc.close();
// Set up a function to do the DOM insertion and run the test.
var harn = this;
var doit = function () {
var el;
// XXX Opera chokes on this line. It thinks that using the doc
// element like this is a security violation, never mind that
// we were the ones who actually created it. Whatever!
var body = doc.body || doc.getElementsByTagName("body")[0];
var head = doc.getElementsByTagName("head")[0];
// Safari seems to be headless at this point.
if (!head) {
head = doc.createElement('head');
doc.appendChild(head);
}
// Add script elements for all includes.
for (var i = 0; i < harn.includes.length - 1; i++) {
el = doc.createElement("script");
el.setAttribute("src", harn.includes[i]);
head.appendChild(el);
}
// Create the pre and script element for the test file.
var pre = doc.createElement("pre");
pre.id = "test";
el = doc.createElement("script");
el.type = "text/javascript";
if (harn.encoding()) el.charset = harn.encoding();
// XXX This doesn't work in Safari right now. See
// http://bugs.webkit.org/show_bug.cgi?id=3748
el.src = file;
pre.appendChild(el);
// Create a script element to finish the tests.
el = doc.createElement("script");
el.type = "text/javascript";
var text = "window.onload(null, Test)";
// IE doesn't let script elements have children.
if (null != el.canHaveChildren) el.text = text;
// But most other browsers do.
else el.appendChild(document.createTextNode(text));
pre.appendChild(el);
// IE 6 SP 2 Requires getting the body element again.
body = doc.body || doc.getElementsByTagName("body")[0];
body.appendChild(pre);
};
// If we have a body, just do it. Otherwise, do it when
// the document loads.
if (doc.body) doit(); else buffer.onload = doit;
/* Let's just assume that if it's not .html, it's JavaScript.
} else {
// Who are you, man??
alert("I don't know what kind of file '" + file + "' is");
*/
}
};
Test.Harness.Browser.prototype.args = {};
var pairs = location.search.substring(1).split(/[;&]/);
for (var i = 0; i < pairs.length; i++) {
var parts = pairs[i].split('=');
if (parts[0] == null) continue;
var key = unescape(parts[0]), val = unescape(parts[1]);
if (Test.Harness.Browser.prototype.args[key] == null) {
Test.Harness.Browser.prototype.args[key] = unescape(val);
} else {
if (typeof Test.Harness.Browser.prototype.args[key] == 'string') {
Test.Harness.Browser.prototype.args[key] =
[Test.Harness.Browser.prototype.args[key], unescape(val)];
} else {
Test.Harness.Browser.prototype.args[key].push(unescape(val));
}
}
}
Test.Harness.Browser.prototype.formatFailures = function (fn) {
// XXX Switch to DOM?
var failedStr = "Failed Test";
var middleStr = " Total Fail Failed ";
var listStr = "List of Failed";
var table =
'<style>table {padding: 0; border-collapse: collapse; }'
+ 'tr { height: 2em; }'
+ 'th { background: lightgrey; }'
+ 'td, th { padding: 2px 5px; text-align: left; border: solid #000000 1px;}'
+ '.odd { background: #e8e8cd }'
+ '</style>'
+ '<table style="padding: 0"><tr><th>Failed Test</th><th>Total</th>'
+ '<th>Fail</th><th>Failed</th></tr>';
for (var i = 0; i < this.failures.length; i++) {
var track = this.failures[i];
var style = i % 2 ? 'even' : 'odd';
table += '<tr class="' + style + '"><td>' + track.fn + '</td>'
+ '<td>' + track.total + '</td>'
+ '<td>' + (track.total - track.ok) + '</td>'
+ '<td>' + this._failList(track.failList) + '</td></tr>';
};
table += '</table>' + Test.Harness.LF;
var node = document.getElementById('summary');
node.innerHTML += table;
window.scrollTo(0, document.body.offsetHeight
|| document.body.scrollHeight);
};
}