blob: 1b8f475e9842b927b520fa6b913659df5dff299d [file] [log] [blame]
// Next three methods are primarily for IE5, which is missing them
if (!Array.prototype.push) {
Array.prototype.push = function() {
for (var i = 0; i < arguments.length; i++){
this[this.length] = arguments[i];
}
return this.length;
};
}
if (!Array.prototype.shift) {
Array.prototype.shift = function() {
if (this.length > 0) {
var firstItem = this[0];
for (var i = 0; i < this.length - 1; i++) {
this[i] = this[i + 1];
}
this.length = this.length - 1;
return firstItem;
}
};
}
if (!Function.prototype.apply) {
Function.prototype.apply = function(obj, args) {
var methodName = "__apply__";
if (typeof obj[methodName] != "undefined") {
methodName += (String(Math.random())).substr(2);
}
obj[methodName] = this;
var argsStrings = new Array(args.length);
for (var i = 0; i < args.length; i++) {
argsStrings[i] = "args[" + i + "]";
}
var script = "obj." + methodName + "(" + argsStrings.join(",") + ")";
var returnValue = eval(script);
delete obj[methodName];
return returnValue;
};
}
/* -------------------------------------------------------------------------- */
var xn = new Object();
(function() {
// Utility functions
// Event listeners
var getListenersPropertyName = function(eventName) {
return "__listeners__" + eventName;
};
var addEventListener = function(node, eventName, listener, useCapture) {
useCapture = Boolean(useCapture);
if (node.addEventListener) {
node.addEventListener(eventName, listener, useCapture);
} else if (node.attachEvent) {
node.attachEvent("on" + eventName, listener);
} else {
var propertyName = getListenersPropertyName(eventName);
if (!node[propertyName]) {
node[propertyName] = new Array();
// Set event handler
node["on" + eventName] = function(evt) {
evt = module.getEvent(evt);
var listenersPropertyName = getListenersPropertyName(eventName);
// Clone the array of listeners to leave the original untouched
var listeners = cloneArray(this[listenersPropertyName]);
var currentListener;
// Call each listener in turn
while (currentListener = listeners.shift()) {
currentListener.call(this, evt);
}
};
}
node[propertyName].push(listener);
}
};
// Clones an array
var cloneArray = function(arr) {
var clonedArray = [];
for (var i = 0; i < arr.length; i++) {
clonedArray[i] = arr[i];
}
return clonedArray;
}
var isFunction = function(f) {
if (!f){ return false; }
return (f instanceof Function || typeof f == "function");
};
// CSS Utilities
function array_contains(arr, val) {
for (var i = 0, len = arr.length; i < len; i++) {
if (arr[i] === val) {
return true;
}
}
return false;
}
function addClass(el, cssClass) {
if (!hasClass(el, cssClass)) {
if (el.className) {
el.className += " " + cssClass;
} else {
el.className = cssClass;
}
}
}
function hasClass(el, cssClass) {
if (el.className) {
var classNames = el.className.split(" ");
return array_contains(classNames, cssClass);
}
return false;
}
function removeClass(el, cssClass) {
if (hasClass(el, cssClass)) {
// Rebuild the className property
var existingClasses = el.className.split(" ");
var newClasses = [];
for (var i = 0; i < existingClasses.length; i++) {
if (existingClasses[i] != cssClass) {
newClasses[newClasses.length] = existingClasses[i];
}
}
el.className = newClasses.join(" ");
}
}
function replaceClass(el, newCssClass, oldCssClass) {
removeClass(el, oldCssClass);
addClass(el, newCssClass);
}
function getExceptionStringRep(ex) {
if (ex) {
var exStr = "Exception: ";
if (ex.message) {
exStr += ex.message;
} else if (ex.description) {
exStr += ex.description;
}
if (ex.lineNumber) {
exStr += " on line number " + ex.lineNumber;
}
if (ex.fileName) {
exStr += " in file " + ex.fileName;
}
return exStr;
}
return null;
}
/* ---------------------------------------------------------------------- */
/* Configure the test logger try to use FireBug */
var log, error;
if (window["console"] && typeof console.log == "function") {
log = function() {
if (xn.test.enableTestDebug) {
console.log.apply(console, arguments);
}
};
error = function() {
if (xn.test.enableTestDebug) {
console.error.apply(console, arguments);
}
};
} else {
log = function() {};
}
/* Set up something to report to */
var initialized = false;
var container;
var progressBarContainer, progressBar, overallSummaryText;
var currentTest = null;
var suites = [];
var totalTestCount = 0;
var currentTestIndex = 0;
var testFailed = false;
var testsPassedCount = 0;
var startTime;
var log4javascriptEnabled = false;
var nextSuiteIndex = 0;
function runNextSuite() {
if (nextSuiteIndex < suites.length) {
suites[nextSuiteIndex++].run();
}
}
var init = function() {
if (initialized) { return true; }
container = document.createElement("div");
// Create the overall progress bar
progressBarContainer = container.appendChild(document.createElement("div"));
progressBarContainer.className = "xn_test_progressbar_container xn_test_overallprogressbar_container";
progressBar = progressBarContainer.appendChild(document.createElement("div"));
progressBar.className = "success";
document.body.appendChild(container);
var h1 = progressBar.appendChild(document.createElement("h1"));
overallSummaryText = h1.appendChild(document.createTextNode(""));
initialized = true;
// Set up logging
log4javascriptEnabled = !!log4javascript && xn.test.enable_log4javascript;
function TestLogAppender() {}
if (log4javascriptEnabled) {
TestLogAppender.prototype = new log4javascript.Appender();
TestLogAppender.prototype.layout = new log4javascript.PatternLayout("%d{HH:mm:ss,SSS} %-5p %m");
TestLogAppender.prototype.append = function(loggingEvent) {
var formattedMessage = this.getLayout().format(loggingEvent);
if (this.getLayout().ignoresThrowable()) {
formattedMessage += loggingEvent.getThrowableStrRep();
}
currentTest.addLogMessage(formattedMessage);
};
var appender = new TestLogAppender();
appender.setThreshold(log4javascript.Level.ALL);
log4javascript.getRootLogger().addAppender(appender);
log4javascript.getRootLogger().setLevel(log4javascript.Level.ALL);
}
startTime = new Date();
// First, build each suite
for (var i = 0; i < suites.length; i++) {
suites[i].build();
totalTestCount += suites[i].tests.length;
}
// Now run each suite
runNextSuite();
};
function updateProgressBar() {
progressBar.style.width = "" + parseInt(100 * (currentTestIndex) / totalTestCount) + "%";
var s = (totalTestCount === 1) ? "" : "s";
var timeTaken = new Date().getTime() - startTime.getTime();
overallSummaryText.nodeValue = "" + testsPassedCount + " of " + totalTestCount + " test" + s + " passed in " + timeTaken + "ms";
}
addEventListener(window, "load", init);
/* ---------------------------------------------------------------------- */
/* Test Suite */
var Suite = function(name, callback, hideSuccessful) {
this.name = name;
this.callback = callback;
this.hideSuccessful = hideSuccessful;
this.tests = [];
this.log = log;
this.error = error;
this.expanded = true;
suites.push(this);
}
Suite.prototype.test = function(name, callback, setUp, tearDown) {
this.log("adding a test named " + name)
var t = new Test(name, callback, this, setUp, tearDown);
this.tests.push(t);
};
Suite.prototype.build = function() {
// Build the elements used by the suite
var suite = this;
this.testFailed = false;
this.container = document.createElement("div");
this.container.className = "xn_test_suite_container";
var heading = document.createElement("h2");
this.expander = document.createElement("span");
this.expander.className = "xn_test_expander";
this.expander.onclick = function() {
if (suite.expanded) {
suite.collapse();
} else {
suite.expand();
}
};
heading.appendChild(this.expander);
this.headingTextNode = document.createTextNode(this.name);
heading.appendChild(this.headingTextNode);
this.container.appendChild(heading);
this.reportContainer = document.createElement("dl");
this.container.appendChild(this.reportContainer);
this.progressBarContainer = document.createElement("div");
this.progressBarContainer.className = "xn_test_progressbar_container";
this.progressBar = document.createElement("div");
this.progressBar.className = "success";
this.progressBar.innerHTML = "&nbsp;";
this.progressBarContainer.appendChild(this.progressBar);
this.reportContainer.appendChild(this.progressBarContainer);
this.expand();
container.appendChild(this.container);
// invoke callback to build the tests
this.callback.apply(this, [this]);
};
Suite.prototype.run = function() {
this.log("running suite '%s'", this.name)
this.startTime = new Date();
// now run the first test
this._currentIndex = 0;
this.runNextTest();
};
Suite.prototype.updateProgressBar = function() {
// Update progress bar
this.progressBar.style.width = "" + parseInt(100 * (this._currentIndex) / this.tests.length) + "%";
//log(this._currentIndex + ", " + this.tests.length + ", " + progressBar.style.width + ", " + progressBar.className);
};
Suite.prototype.expand = function() {
this.expander.innerHTML = "-";
replaceClass(this.reportContainer, "xn_test_expanded", "xn_test_collapsed");
this.expanded = true;
};
Suite.prototype.collapse = function() {
this.expander.innerHTML = "+";
replaceClass(this.reportContainer, "xn_test_collapsed", "xn_test_expanded");
this.expanded = false;
};
Suite.prototype.finish = function(timeTaken) {
var newClass = this.testFailed ? "xn_test_suite_failure" : "xn_test_suite_success";
var oldClass = this.testFailed ? "xn_test_suite_success" : "xn_test_suite_failure";
replaceClass(this.container, newClass, oldClass);
this.headingTextNode.nodeValue += " (" + timeTaken + "ms)";
if (this.hideSuccessful && !this.testFailed) {
this.collapse();
}
runNextSuite();
};
/**
* Works recursively with external state (the next index)
* so that we can handle async tests differently
*/
Suite.prototype.runNextTest = function() {
if (this._currentIndex == this.tests.length) {
// finished!
var timeTaken = new Date().getTime() - this.startTime.getTime();
this.finish(timeTaken);
return;
}
var suite = this;
var t = this.tests[this._currentIndex++];
currentTestIndex++;
if (isFunction(suite.setUp)) {
suite.setUp.apply(suite, [t]);
}
if (isFunction(t.setUp)) {
t.setUp.apply(t, [t]);
}
t._run();
function afterTest() {
if (isFunction(suite.tearDown)) {
suite.tearDown.apply(suite, [t]);
}
if (isFunction(t.tearDown)) {
t.tearDown.apply(t, [t]);
}
suite.log("finished test [%s]", t.name);
updateProgressBar();
suite.updateProgressBar();
suite.runNextTest();
}
if (t.isAsync) {
t.whenFinished = afterTest;
} else {
setTimeout(afterTest, 1);
}
};
Suite.prototype.reportSuccess = function() {
};
/* ---------------------------------------------------------------------- */
/**
* Create a new test
*/
var Test = function(name, callback, suite, setUp, tearDown) {
this.name = name;
this.callback = callback;
this.suite = suite;
this.setUp = setUp;
this.tearDown = tearDown;
this.log = log;
this.error = error;
this.assertCount = 0;
this.logMessages = [];
this.logExpanded = false;
};
/**
* Default success reporter, please override
*/
Test.prototype.reportSuccess = function(name, timeTaken) {
/* default success reporting handler */
this.reportHeading = document.createElement("dt");
var text = this.name + " passed in " + timeTaken + "ms";
this.reportHeading.appendChild(document.createTextNode(text));
this.reportHeading.className = "success";
var dd = document.createElement("dd");
dd.className = "success";
this.suite.reportContainer.appendChild(this.reportHeading);
this.suite.reportContainer.appendChild(dd);
this.createLogReport();
};
/**
* Cause the test to immediately fail
*/
Test.prototype.reportFailure = function(name, msg, ex) {
this.suite.testFailed = true;
this.suite.progressBar.className = "failure";
progressBar.className = "failure";
this.reportHeading = document.createElement("dt");
this.reportHeading.className = "failure";
var text = document.createTextNode(this.name);
this.reportHeading.appendChild(text);
var dd = document.createElement("dd");
dd.appendChild(document.createTextNode(msg));
dd.className = "failure";
this.suite.reportContainer.appendChild(this.reportHeading);
this.suite.reportContainer.appendChild(dd);
if (ex && ex.stack) {
var stackTraceContainer = this.suite.reportContainer.appendChild(document.createElement("code"));
stackTraceContainer.className = "xn_test_stacktrace";
stackTraceContainer.innerHTML = ex.stack.replace(/\r/g, "\n").replace(/\n{1,2}/g, "<br />");
}
this.createLogReport();
};
Test.prototype.createLogReport = function() {
if (this.logMessages.length > 0) {
this.reportHeading.appendChild(document.createTextNode(" ("));
var logToggler = this.reportHeading.appendChild(document.createElement("a"));
logToggler.href = "#";
logToggler.innerHTML = "show log";
var test = this;
logToggler.onclick = function() {
if (test.logExpanded) {
test.hideLogReport();
this.innerHTML = "show log";
test.logExpanded = false;
} else {
test.showLogReport();
this.innerHTML = "hide log";
test.logExpanded = true;
}
return false;
};
this.reportHeading.appendChild(document.createTextNode(")"));
// Create log report
this.logReport = this.suite.reportContainer.appendChild(document.createElement("pre"));
this.logReport.style.display = "none";
this.logReport.className = "xn_test_log_report";
var logMessageDiv;
for (var i = 0, len = this.logMessages.length; i < len; i++) {
logMessageDiv = this.logReport.appendChild(document.createElement("div"));
logMessageDiv.appendChild(document.createTextNode(this.logMessages[i]));
}
}
};
Test.prototype.showLogReport = function() {
this.logReport.style.display = "inline-block";
};
Test.prototype.hideLogReport = function() {
this.logReport.style.display = "none";
};
Test.prototype.async = function(timeout, callback) {
timeout = timeout || 250;
var self = this;
var timedOutFunc = function() {
if (!self.completed) {
var message = (typeof callback === "undefined") ?
"Asynchronous test timed out" : callback(self);
self.fail(message);
}
}
var timer = setTimeout(function () { timedOutFunc.apply(self, []); }, timeout)
this.isAsync = true;
};
/**
* Run the test
*/
Test.prototype._run = function() {
this.log("starting test [%s]", this.name);
this.startTime = new Date();
currentTest = this;
try {
this.callback(this);
if (!this.completed && !this.isAsync) {
this.succeed();
}
} catch (e) {
this.log("test [%s] threw exception [%s]", this.name, e);
var s = (this.assertCount === 1) ? "" : "s";
this.fail("Exception thrown after " + this.assertCount + " successful assertion" + s + ": " + getExceptionStringRep(e), e);
}
};
/**
* Cause the test to immediately succeed
*/
Test.prototype.succeed = function() {
if (this.completed) { return false; }
// this.log("test [%s] succeeded", this.name);
this.completed = true;
var timeTaken = new Date().getTime() - this.startTime.getTime();
testsPassedCount++;
this.reportSuccess(this.name, timeTaken);
if (this.whenFinished) {
this.whenFinished();
}
};
Test.prototype.fail = function(msg, ex) {
if (typeof msg != "string") {
msg = getExceptionStringRep(msg);
}
if (this.completed) { return false; }
this.completed = true;
// this.log("test [%s] failed", this.name);
this.reportFailure(this.name, msg, ex);
if (this.whenFinished) {
this.whenFinished();
}
};
Test.prototype.addLogMessage = function(logMessage) {
this.logMessages.push(logMessage);
};
/* assertions */
var displayStringForValue = function(obj) {
if (obj === null) {
return "null";
} else if (typeof obj === "undefined") {
return "undefined";
}
return obj.toString();
};
var assert = function(args, expectedArgsCount, testFunction, defaultComment) {
this.assertCount++;
var comment = defaultComment;
var i;
var success;
var values = [];
if (args.length == expectedArgsCount) {
for (i = 0; i < args.length; i++) {
values[i] = args[i];
}
} else if (args.length == expectedArgsCount + 1) {
comment = args[0];
for (i = 1; i < args.length; i++) {
values[i - 1] = args[i];
}
} else {
throw new Error("Invalid number of arguments passed to assert function");
}
success = testFunction(values);
if (!success) {
var regex = /\{([0-9]+)\}/;
while (regex.test(comment)) {
comment = comment.replace(regex, displayStringForValue(values[parseInt(RegExp.$1)]));
}
this.fail("Test failed on assertion " + this.assertCount + ": " + comment);
}
};
var testNull = function(values) {
return (values[0] === null);
};
Test.prototype.assertNull = function() {
assert.apply(this, [arguments, 1, testNull, "Expected to be null but was {0}"]);
}
var testNotNull = function(values) {
return (values[0] !== null);
};
Test.prototype.assertNotNull = function() {
assert.apply(this, [arguments, 1, testNotNull, "Expected not to be null but was {0}"]);
}
var testBoolean = function(values) {
return (Boolean(values[0]));
};
Test.prototype.assert = function() {
assert.apply(this, [arguments, 1, testBoolean, "Expected not to be equivalent to false"]);
};
var testTrue = function(values) {
return (values[0] === true);
};
Test.prototype.assertTrue = function() {
assert.apply(this, [arguments, 1, testTrue, "Expected to be true but was {0}"]);
};
Test.prototype.assert = function() {
assert.apply(this, [arguments, 1, testTrue, "Expected to be true but was {0}"]);
};
var testFalse = function(values) {
return (values[0] === false);
};
Test.prototype.assertFalse = function() {
assert.apply(this, [arguments, 1, testFalse, "Expected to be false but was {0}"]);
}
var testEquivalent = function(values) {
return (values[0] === values[1]);
};
Test.prototype.assertEquivalent = function() {
assert.apply(this, [arguments, 2, testEquivalent, "Expected to be equal but values were {0} and {1}"]);
}
var testNotEquivalent = function(values) {
return (values[0] !== values[1]);
};
Test.prototype.assertNotEquivalent = function() {
assert.apply(this, [arguments, 2, testNotEquivalent, "Expected to be not equal but values were {0} and {1}"]);
}
var testEquals = function(values) {
return (values[0] == values[1]);
};
Test.prototype.assertEquals = function() {
assert.apply(this, [arguments, 2, testEquals, "Expected to be equal but values were {0} and {1}"]);
}
var testNotEquals = function(values) {
return (values[0] != values[1]);
};
Test.prototype.assertNotEquals = function() {
assert.apply(this, [arguments, 2, testNotEquals, "Expected to be not equal but values were {0} and {1}"]);
}
var testRegexMatches = function(values) {
return (values[0].test(values[1]));
};
Test.prototype.assertRegexMatches = function() {
assert.apply(this, [arguments, 2, testRegexMatches, "Expected regex {0} to match value {1} but it didn't"]);
}
Test.prototype.assertError = function(f, errorType) {
try {
f();
this.fail("Expected error to be thrown");
} catch (e) {
if (errorType && (!(e instanceof errorType))) {
this.fail("Expected error of type " + errorType + " to be thrown but error thrown was " + e);
}
}
};
/**
* Execute a synchronous test
*/
xn.test = function(name, callback) {
xn.test.suite("Anonymous", function(s) {
s.test(name, callback);
});
}
/**
* Create a test suite with a given name
*/
xn.test.suite = function(name, callback, hideSuccessful) {
var s = new Suite(name, callback, hideSuccessful);
}
})();