| // 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 = " "; |
| 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); |
| } |
| })(); |