1 // Next three methods are primarily for IE5, which is missing them
2 if (!Array.prototype.push) {
3 Array.prototype.push = function() {
4 for (var i = 0; i < arguments.length; i++){
5 this[this.length] = arguments[i];
11 if (!Array.prototype.shift) {
12 Array.prototype.shift = function() {
13 if (this.length > 0) {
14 var firstItem = this[0];
15 for (var i = 0; i < this.length - 1; i++) {
16 this[i] = this[i + 1];
18 this.length = this.length - 1;
24 if (!Function.prototype.apply) {
25 Function.prototype.apply = function(obj, args) {
26 var methodName = "__apply__";
27 if (typeof obj[methodName] != "undefined") {
28 methodName += (String(Math.random())).substr(2);
30 obj[methodName] = this;
32 var argsStrings = new Array(args.length);
33 for (var i = 0; i < args.length; i++) {
34 argsStrings[i] = "args[" + i + "]";
36 var script = "obj." + methodName + "(" + argsStrings.join(",") + ")";
37 var returnValue = eval(script);
38 delete obj[methodName];
43 /* -------------------------------------------------------------------------- */
45 var xn = new Object();
51 var getListenersPropertyName = function(eventName) {
52 return "__listeners__" + eventName;
55 var addEventListener = function(node, eventName, listener, useCapture) {
56 useCapture = Boolean(useCapture);
57 if (node.addEventListener) {
58 node.addEventListener(eventName, listener, useCapture);
59 } else if (node.attachEvent) {
60 node.attachEvent("on" + eventName, listener);
62 var propertyName = getListenersPropertyName(eventName);
63 if (!node[propertyName]) {
64 node[propertyName] = new Array();
67 node["on" + eventName] = function(evt) {
68 evt = module.getEvent(evt);
69 var listenersPropertyName = getListenersPropertyName(eventName);
71 // Clone the array of listeners to leave the original untouched
72 var listeners = cloneArray(this[listenersPropertyName]);
75 // Call each listener in turn
76 while (currentListener = listeners.shift()) {
77 currentListener.call(this, evt);
81 node[propertyName].push(listener);
86 var cloneArray = function(arr) {
88 for (var i = 0; i < arr.length; i++) {
89 clonedArray[i] = arr[i];
94 var isFunction = function(f) {
95 if (!f){ return false; }
96 return (f instanceof Function || typeof f == "function");
101 function array_contains(arr, val) {
102 for (var i = 0, len = arr.length; i < len; i++) {
103 if (arr[i] === val) {
110 function addClass(el, cssClass) {
111 if (!hasClass(el, cssClass)) {
113 el.className += " " + cssClass;
115 el.className = cssClass;
120 function hasClass(el, cssClass) {
122 var classNames = el.className.split(" ");
123 return array_contains(classNames, cssClass);
128 function removeClass(el, cssClass) {
129 if (hasClass(el, cssClass)) {
130 // Rebuild the className property
131 var existingClasses = el.className.split(" ");
133 for (var i = 0; i < existingClasses.length; i++) {
134 if (existingClasses[i] != cssClass) {
135 newClasses[newClasses.length] = existingClasses[i];
138 el.className = newClasses.join(" ");
142 function replaceClass(el, newCssClass, oldCssClass) {
143 removeClass(el, oldCssClass);
144 addClass(el, newCssClass);
147 function getExceptionStringRep(ex) {
149 var exStr = "Exception: ";
152 } else if (ex.description) {
153 exStr += ex.description;
156 exStr += " on line number " + ex.lineNumber;
159 exStr += " in file " + ex.fileName;
167 /* ---------------------------------------------------------------------- */
169 /* Configure the test logger try to use FireBug */
171 if (window["console"] && typeof console.log == "function") {
173 if (xn.test.enableTestDebug) {
174 console.log.apply(console, arguments);
178 if (xn.test.enableTestDebug) {
179 console.error.apply(console, arguments);
186 /* Set up something to report to */
188 var initialized = false;
190 var progressBarContainer, progressBar, overallSummaryText;
191 var currentTest = null;
193 var totalTestCount = 0;
194 var currentTestIndex = 0;
195 var testFailed = false;
196 var testsPassedCount = 0;
199 var log4javascriptEnabled = false;
201 var nextSuiteIndex = 0;
203 function runNextSuite() {
204 if (nextSuiteIndex < suites.length) {
205 suites[nextSuiteIndex++].run();
209 var init = function() {
210 if (initialized) { return true; }
212 container = document.createElement("div");
214 // Create the overall progress bar
215 progressBarContainer = container.appendChild(document.createElement("div"));
216 progressBarContainer.className = "xn_test_progressbar_container xn_test_overallprogressbar_container";
217 progressBar = progressBarContainer.appendChild(document.createElement("div"));
218 progressBar.className = "success";
220 document.body.appendChild(container);
222 var h1 = progressBar.appendChild(document.createElement("h1"));
223 overallSummaryText = h1.appendChild(document.createTextNode(""));
228 log4javascriptEnabled = !!log4javascript && xn.test.enable_log4javascript;
230 function TestLogAppender() {}
232 if (log4javascriptEnabled) {
233 TestLogAppender.prototype = new log4javascript.Appender();
234 TestLogAppender.prototype.layout = new log4javascript.PatternLayout("%d{HH:mm:ss,SSS} %-5p %m");
235 TestLogAppender.prototype.append = function(loggingEvent) {
236 var formattedMessage = this.getLayout().format(loggingEvent);
237 if (this.getLayout().ignoresThrowable()) {
238 formattedMessage += loggingEvent.getThrowableStrRep();
240 currentTest.addLogMessage(formattedMessage);
243 var appender = new TestLogAppender();
244 appender.setThreshold(log4javascript.Level.ALL);
245 log4javascript.getRootLogger().addAppender(appender);
246 log4javascript.getRootLogger().setLevel(log4javascript.Level.ALL);
249 startTime = new Date();
251 // First, build each suite
252 for (var i = 0; i < suites.length; i++) {
254 totalTestCount += suites[i].tests.length;
257 // Now run each suite
261 function updateProgressBar() {
262 progressBar.style.width = "" + parseInt(100 * (currentTestIndex) / totalTestCount) + "%";
263 var s = (totalTestCount === 1) ? "" : "s";
264 var timeTaken = new Date().getTime() - startTime.getTime();
265 overallSummaryText.nodeValue = "" + testsPassedCount + " of " + totalTestCount + " test" + s + " passed in " + timeTaken + "ms";
268 addEventListener(window, "load", init);
270 /* ---------------------------------------------------------------------- */
273 var Suite = function(name, callback, hideSuccessful) {
275 this.callback = callback;
276 this.hideSuccessful = hideSuccessful;
280 this.expanded = true;
284 Suite.prototype.test = function(name, callback, setUp, tearDown) {
285 this.log("adding a test named " + name)
286 var t = new Test(name, callback, this, setUp, tearDown);
290 Suite.prototype.build = function() {
291 // Build the elements used by the suite
293 this.testFailed = false;
294 this.container = document.createElement("div");
295 this.container.className = "xn_test_suite_container";
297 var heading = document.createElement("h2");
298 this.expander = document.createElement("span");
299 this.expander.className = "xn_test_expander";
300 this.expander.onclick = function() {
301 if (suite.expanded) {
307 heading.appendChild(this.expander);
309 this.headingTextNode = document.createTextNode(this.name);
310 heading.appendChild(this.headingTextNode);
311 this.container.appendChild(heading);
313 this.reportContainer = document.createElement("dl");
314 this.container.appendChild(this.reportContainer);
316 this.progressBarContainer = document.createElement("div");
317 this.progressBarContainer.className = "xn_test_progressbar_container";
318 this.progressBar = document.createElement("div");
319 this.progressBar.className = "success";
320 this.progressBar.innerHTML = " ";
321 this.progressBarContainer.appendChild(this.progressBar);
322 this.reportContainer.appendChild(this.progressBarContainer);
326 container.appendChild(this.container);
328 // invoke callback to build the tests
329 this.callback.apply(this, [this]);
332 Suite.prototype.run = function() {
333 this.log("running suite '%s'", this.name)
334 this.startTime = new Date();
336 // now run the first test
337 this._currentIndex = 0;
341 Suite.prototype.updateProgressBar = function() {
342 // Update progress bar
343 this.progressBar.style.width = "" + parseInt(100 * (this._currentIndex) / this.tests.length) + "%";
344 //log(this._currentIndex + ", " + this.tests.length + ", " + progressBar.style.width + ", " + progressBar.className);
347 Suite.prototype.expand = function() {
348 this.expander.innerHTML = "-";
349 replaceClass(this.reportContainer, "xn_test_expanded", "xn_test_collapsed");
350 this.expanded = true;
353 Suite.prototype.collapse = function() {
354 this.expander.innerHTML = "+";
355 replaceClass(this.reportContainer, "xn_test_collapsed", "xn_test_expanded");
356 this.expanded = false;
359 Suite.prototype.finish = function(timeTaken) {
360 var newClass = this.testFailed ? "xn_test_suite_failure" : "xn_test_suite_success";
361 var oldClass = this.testFailed ? "xn_test_suite_success" : "xn_test_suite_failure";
362 replaceClass(this.container, newClass, oldClass);
364 this.headingTextNode.nodeValue += " (" + timeTaken + "ms)";
366 if (this.hideSuccessful && !this.testFailed) {
373 * Works recursively with external state (the next index)
374 * so that we can handle async tests differently
376 Suite.prototype.runNextTest = function() {
377 if (this._currentIndex == this.tests.length) {
379 var timeTaken = new Date().getTime() - this.startTime.getTime();
381 this.finish(timeTaken);
386 var t = this.tests[this._currentIndex++];
389 if (isFunction(suite.setUp)) {
390 suite.setUp.apply(suite, [t]);
392 if (isFunction(t.setUp)) {
393 t.setUp.apply(t, [t]);
398 function afterTest() {
399 if (isFunction(suite.tearDown)) {
400 suite.tearDown.apply(suite, [t]);
402 if (isFunction(t.tearDown)) {
403 t.tearDown.apply(t, [t]);
405 suite.log("finished test [%s]", t.name);
407 suite.updateProgressBar();
412 t.whenFinished = afterTest;
414 setTimeout(afterTest, 1);
418 Suite.prototype.reportSuccess = function() {
421 /* ---------------------------------------------------------------------- */
425 var Test = function(name, callback, suite, setUp, tearDown) {
427 this.callback = callback;
430 this.tearDown = tearDown;
433 this.assertCount = 0;
434 this.logMessages = [];
435 this.logExpanded = false;
439 * Default success reporter, please override
441 Test.prototype.reportSuccess = function(name, timeTaken) {
442 /* default success reporting handler */
443 this.reportHeading = document.createElement("dt");
444 var text = this.name + " passed in " + timeTaken + "ms";
446 this.reportHeading.appendChild(document.createTextNode(text));
448 this.reportHeading.className = "success";
449 var dd = document.createElement("dd");
450 dd.className = "success";
452 this.suite.reportContainer.appendChild(this.reportHeading);
453 this.suite.reportContainer.appendChild(dd);
454 this.createLogReport();
458 * Cause the test to immediately fail
460 Test.prototype.reportFailure = function(name, msg, ex) {
461 this.suite.testFailed = true;
462 this.suite.progressBar.className = "failure";
463 progressBar.className = "failure";
464 this.reportHeading = document.createElement("dt");
465 this.reportHeading.className = "failure";
466 var text = document.createTextNode(this.name);
467 this.reportHeading.appendChild(text);
469 var dd = document.createElement("dd");
470 dd.appendChild(document.createTextNode(msg));
471 dd.className = "failure";
473 this.suite.reportContainer.appendChild(this.reportHeading);
474 this.suite.reportContainer.appendChild(dd);
475 if (ex && ex.stack) {
476 var stackTraceContainer = this.suite.reportContainer.appendChild(document.createElement("code"));
477 stackTraceContainer.className = "xn_test_stacktrace";
478 stackTraceContainer.innerHTML = ex.stack.replace(/\r/g, "\n").replace(/\n{1,2}/g, "<br />");
480 this.createLogReport();
483 Test.prototype.createLogReport = function() {
484 if (this.logMessages.length > 0) {
485 this.reportHeading.appendChild(document.createTextNode(" ("));
486 var logToggler = this.reportHeading.appendChild(document.createElement("a"));
487 logToggler.href = "#";
488 logToggler.innerHTML = "show log";
491 logToggler.onclick = function() {
492 if (test.logExpanded) {
493 test.hideLogReport();
494 this.innerHTML = "show log";
495 test.logExpanded = false;
497 test.showLogReport();
498 this.innerHTML = "hide log";
499 test.logExpanded = true;
504 this.reportHeading.appendChild(document.createTextNode(")"));
507 this.logReport = this.suite.reportContainer.appendChild(document.createElement("pre"));
508 this.logReport.style.display = "none";
509 this.logReport.className = "xn_test_log_report";
511 for (var i = 0, len = this.logMessages.length; i < len; i++) {
512 logMessageDiv = this.logReport.appendChild(document.createElement("div"));
513 logMessageDiv.appendChild(document.createTextNode(this.logMessages[i]));
518 Test.prototype.showLogReport = function() {
519 this.logReport.style.display = "inline-block";
522 Test.prototype.hideLogReport = function() {
523 this.logReport.style.display = "none";
526 Test.prototype.async = function(timeout, callback) {
527 timeout = timeout || 250;
529 var timedOutFunc = function() {
530 if (!self.completed) {
531 var message = (typeof callback === "undefined") ?
532 "Asynchronous test timed out" : callback(self);
536 var timer = setTimeout(function () { timedOutFunc.apply(self, []); }, timeout)
543 Test.prototype._run = function() {
544 this.log("starting test [%s]", this.name);
545 this.startTime = new Date();
549 if (!this.completed && !this.isAsync) {
553 this.log("test [%s] threw exception [%s]", this.name, e);
554 var s = (this.assertCount === 1) ? "" : "s";
555 this.fail("Exception thrown after " + this.assertCount + " successful assertion" + s + ": " + getExceptionStringRep(e), e);
560 * Cause the test to immediately succeed
562 Test.prototype.succeed = function() {
563 if (this.completed) { return false; }
564 // this.log("test [%s] succeeded", this.name);
565 this.completed = true;
566 var timeTaken = new Date().getTime() - this.startTime.getTime();
568 this.reportSuccess(this.name, timeTaken);
569 if (this.whenFinished) {
574 Test.prototype.fail = function(msg, ex) {
575 if (typeof msg != "string") {
576 msg = getExceptionStringRep(msg);
578 if (this.completed) { return false; }
579 this.completed = true;
580 // this.log("test [%s] failed", this.name);
581 this.reportFailure(this.name, msg, ex);
582 if (this.whenFinished) {
587 Test.prototype.addLogMessage = function(logMessage) {
588 this.logMessages.push(logMessage);
592 var displayStringForValue = function(obj) {
595 } else if (typeof obj === "undefined") {
598 return obj.toString();
601 var assert = function(args, expectedArgsCount, testFunction, defaultComment) {
603 var comment = defaultComment;
607 if (args.length == expectedArgsCount) {
608 for (i = 0; i < args.length; i++) {
611 } else if (args.length == expectedArgsCount + 1) {
613 for (i = 1; i < args.length; i++) {
614 values[i - 1] = args[i];
617 throw new Error("Invalid number of arguments passed to assert function");
619 success = testFunction(values);
621 var regex = /\{([0-9]+)\}/;
622 while (regex.test(comment)) {
623 comment = comment.replace(regex, displayStringForValue(values[parseInt(RegExp.$1)]));
625 this.fail("Test failed on assertion " + this.assertCount + ": " + comment);
629 var testNull = function(values) {
630 return (values[0] === null);
633 Test.prototype.assertNull = function() {
634 assert.apply(this, [arguments, 1, testNull, "Expected to be null but was {0}"]);
637 var testNotNull = function(values) {
638 return (values[0] !== null);
641 Test.prototype.assertNotNull = function() {
642 assert.apply(this, [arguments, 1, testNotNull, "Expected not to be null but was {0}"]);
645 var testBoolean = function(values) {
646 return (Boolean(values[0]));
649 Test.prototype.assert = function() {
650 assert.apply(this, [arguments, 1, testBoolean, "Expected not to be equivalent to false"]);
653 var testTrue = function(values) {
654 return (values[0] === true);
657 Test.prototype.assertTrue = function() {
658 assert.apply(this, [arguments, 1, testTrue, "Expected to be true but was {0}"]);
661 Test.prototype.assert = function() {
662 assert.apply(this, [arguments, 1, testTrue, "Expected to be true but was {0}"]);
665 var testFalse = function(values) {
666 return (values[0] === false);
669 Test.prototype.assertFalse = function() {
670 assert.apply(this, [arguments, 1, testFalse, "Expected to be false but was {0}"]);
673 var testEquivalent = function(values) {
674 return (values[0] === values[1]);
677 Test.prototype.assertEquivalent = function() {
678 assert.apply(this, [arguments, 2, testEquivalent, "Expected to be equal but values were {0} and {1}"]);
681 var testNotEquivalent = function(values) {
682 return (values[0] !== values[1]);
685 Test.prototype.assertNotEquivalent = function() {
686 assert.apply(this, [arguments, 2, testNotEquivalent, "Expected to be not equal but values were {0} and {1}"]);
689 var testEquals = function(values) {
690 return (values[0] == values[1]);
693 Test.prototype.assertEquals = function() {
694 assert.apply(this, [arguments, 2, testEquals, "Expected to be equal but values were {0} and {1}"]);
697 var testNotEquals = function(values) {
698 return (values[0] != values[1]);
701 Test.prototype.assertNotEquals = function() {
702 assert.apply(this, [arguments, 2, testNotEquals, "Expected to be not equal but values were {0} and {1}"]);
705 var testRegexMatches = function(values) {
706 return (values[0].test(values[1]));
709 Test.prototype.assertRegexMatches = function() {
710 assert.apply(this, [arguments, 2, testRegexMatches, "Expected regex {0} to match value {1} but it didn't"]);
713 Test.prototype.assertError = function(f, errorType) {
716 this.fail("Expected error to be thrown");
718 if (errorType && (!(e instanceof errorType))) {
719 this.fail("Expected error of type " + errorType + " to be thrown but error thrown was " + e);
725 * Execute a synchronous test
727 xn.test = function(name, callback) {
728 xn.test.suite("Anonymous", function(s) {
729 s.test(name, callback);
734 * Create a test suite with a given name
736 xn.test.suite = function(name, callback, hideSuccessful) {
737 var s = new Suite(name, callback, hideSuccessful);