diff -r 000000000000 -r 6474c204b198 testing/mochitest/tests/SimpleTest/SimpleTest.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1356 @@ +/* -*- js-indent-level: 4; tab-width: 4; indent-tabs-mode: nil -*- */ +/* vim:set ts=4 sw=4 sts=4 et: */ +/** + * SimpleTest, a partial Test.Simple/Test.More API compatible test library. + * + * Why? + * + * Test.Simple doesn't work on IE < 6. + * TODO: + * * Support the Test.Simple API used by MochiKit, to be able to test MochiKit + * itself against IE 5.5 + * + * NOTE: Pay attention to cross-browser compatibility in this file. For + * instance, do not use const or JS > 1.5 features which are not yet + * implemented everywhere. + * +**/ + +var SimpleTest = { }; +var parentRunner = null; + +// In normal test runs, the window that has a TestRunner in its parent is +// the primary window. In single test runs, if there is no parent and there +// is no opener then it is the primary window. +var isSingleTestRun = (parent == window && !opener) +var isPrimaryTestWindow = !!parent.TestRunner || isSingleTestRun; + +// Finds the TestRunner for this test run and the SpecialPowers object (in +// case it is not defined) from a parent/opener window. +// +// Finding the SpecialPowers object is needed when we have ChromePowers in +// harness.xul and we need SpecialPowers in the iframe, and also for tests +// like test_focus.xul where we open a window which opens another window which +// includes SimpleTest.js. +(function() { + function ancestor(w) { + return w.parent != w ? w.parent : w.opener; + } + + var w = ancestor(window); + while (w && (!parentRunner || !window.SpecialPowers)) { + if (!parentRunner) { + parentRunner = w.TestRunner; + if (!parentRunner && w.wrappedJSObject) { + parentRunner = w.wrappedJSObject.TestRunner; + } + } + if (!window.SpecialPowers) { + window.SpecialPowers = w.SpecialPowers; + } + w = ancestor(w); + } +})(); + +/* Helper functions pulled out of various MochiKit modules */ +if (typeof(repr) == 'undefined') { + this.repr = function(o) { + if (typeof(o) == "undefined") { + return "undefined"; + } else if (o === null) { + return "null"; + } + try { + if (typeof(o.__repr__) == 'function') { + return o.__repr__(); + } else if (typeof(o.repr) == 'function' && o.repr != arguments.callee) { + return o.repr(); + } + } catch (e) { + } + try { + if (typeof(o.NAME) == 'string' && ( + o.toString == Function.prototype.toString || + o.toString == Object.prototype.toString + )) { + return o.NAME; + } + } catch (e) { + } + try { + var ostring = (o + ""); + } catch (e) { + return "[" + typeof(o) + "]"; + } + if (typeof(o) == "function") { + o = ostring.replace(/^\s+/, ""); + var idx = o.indexOf("{"); + if (idx != -1) { + o = o.substr(0, idx) + "{...}"; + } + } + return ostring; + }; +} + +/* This returns a function that applies the previously given parameters. + * This is used by SimpleTest.showReport + */ +if (typeof(partial) == 'undefined') { + this.partial = function(func) { + var args = []; + for (var i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + return function() { + if (arguments.length > 0) { + for (var i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + } + func(args); + }; + }; +} + +if (typeof(getElement) == 'undefined') { + this.getElement = function(id) { + return ((typeof(id) == "string") ? + document.getElementById(id) : id); + }; + this.$ = this.getElement; +} + +SimpleTest._newCallStack = function(path) { + var rval = function () { + var callStack = arguments.callee.callStack; + for (var i = 0; i < callStack.length; i++) { + if (callStack[i].apply(this, arguments) === false) { + break; + } + } + try { + this[path] = null; + } catch (e) { + // pass + } + }; + rval.callStack = []; + return rval; +}; + +if (typeof(addLoadEvent) == 'undefined') { + this.addLoadEvent = function(func) { + var existing = window["onload"]; + var regfunc = existing; + if (!(typeof(existing) == 'function' + && typeof(existing.callStack) == "object" + && existing.callStack !== null)) { + regfunc = SimpleTest._newCallStack("onload"); + if (typeof(existing) == 'function') { + regfunc.callStack.push(existing); + } + window["onload"] = regfunc; + } + regfunc.callStack.push(func); + }; +} + +function createEl(type, attrs, html) { + //use createElementNS so the xul/xhtml tests have no issues + var el; + if (!document.body) { + el = document.createElementNS("http://www.w3.org/1999/xhtml", type); + } + else { + el = document.createElement(type); + } + if (attrs !== null && attrs !== undefined) { + for (var k in attrs) { + el.setAttribute(k, attrs[k]); + } + } + if (html !== null && html !== undefined) { + el.appendChild(document.createTextNode(html)); + } + return el; +} + +/* lots of tests use this as a helper to get css properties */ +if (typeof(computedStyle) == 'undefined') { + this.computedStyle = function(elem, cssProperty) { + elem = getElement(elem); + if (elem.currentStyle) { + return elem.currentStyle[cssProperty]; + } + if (typeof(document.defaultView) == 'undefined' || document === null) { + return undefined; + } + var style = document.defaultView.getComputedStyle(elem, null); + if (typeof(style) == 'undefined' || style === null) { + return undefined; + } + + var selectorCase = cssProperty.replace(/([A-Z])/g, '-$1' + ).toLowerCase(); + + return style.getPropertyValue(selectorCase); + }; +} + +/** + * Check for OOP test plugin +**/ +SimpleTest.testPluginIsOOP = function () { + var testPluginIsOOP = false; + if (navigator.platform.indexOf("Mac") == 0) { + if (SpecialPowers.XPCOMABI.match(/x86-/)) { + try { + testPluginIsOOP = SpecialPowers.getBoolPref("dom.ipc.plugins.enabled.i386.test.plugin"); + } catch (e) { + testPluginIsOOP = SpecialPowers.getBoolPref("dom.ipc.plugins.enabled.i386"); + } + } + else if (SpecialPowers.XPCOMABI.match(/x86_64-/)) { + try { + testPluginIsOOP = SpecialPowers.getBoolPref("dom.ipc.plugins.enabled.x86_64.test.plugin"); + } catch (e) { + testPluginIsOOP = SpecialPowers.getBoolPref("dom.ipc.plugins.enabled.x86_64"); + } + } + } + else { + testPluginIsOOP = SpecialPowers.getBoolPref("dom.ipc.plugins.enabled"); + } + + return testPluginIsOOP; +}; + +SimpleTest._tests = []; +SimpleTest._stopOnLoad = true; +SimpleTest._cleanupFunctions = []; + +/** + * Something like assert. +**/ +SimpleTest.ok = function (condition, name, diag) { + var test = {'result': !!condition, 'name': name, 'diag': diag}; + SimpleTest._logResult(test, "TEST-PASS", "TEST-UNEXPECTED-FAIL"); + SimpleTest._tests.push(test); +}; + +/** + * Roughly equivalent to ok(a==b, name) +**/ +SimpleTest.is = function (a, b, name) { + var pass = (a == b); + var diag = pass ? "" : "got " + repr(a) + ", expected " + repr(b) + SimpleTest.ok(pass, name, diag); +}; + +SimpleTest.isfuzzy = function (a, b, epsilon, name) { + var pass = (a > b - epsilon) && (a < b + epsilon); + var diag = pass ? "" : "got " + repr(a) + ", expected " + repr(b) + " epsilon: +/- " + repr(epsilon) + SimpleTest.ok(pass, name, diag); +}; + +SimpleTest.isnot = function (a, b, name) { + var pass = (a != b); + var diag = pass ? "" : "didn't expect " + repr(a) + ", but got it"; + SimpleTest.ok(pass, name, diag); +}; + +/** + * Roughly equivalent to ok(a===b, name) +**/ +SimpleTest.ise = function (a, b, name) { + var pass = (a === b); + var diag = pass ? "" : "got " + repr(a) + ", strictly expected " + repr(b) + SimpleTest.ok(pass, name, diag); +}; + +/** + * Check that the function call throws an exception. + */ +SimpleTest.doesThrow = function(fn, name) { + var gotException = false; + try { + fn(); + } catch (ex) { gotException = true; } + ok(gotException, name); +}; + +// --------------- Test.Builder/Test.More todo() ----------------- + +SimpleTest.todo = function(condition, name, diag) { + var test = {'result': !!condition, 'name': name, 'diag': diag, todo: true}; + SimpleTest._logResult(test, "TEST-UNEXPECTED-PASS", "TEST-KNOWN-FAIL"); + SimpleTest._tests.push(test); +}; + +/* + * Returns the absolute URL to a test data file from where tests + * are served. i.e. the file doesn't necessarely exists where tests + * are executed. + * (For b2g and android, mochitest are executed on the device, while + * all mochitest html (and others) files are served from the test runner + * slave) + */ +SimpleTest.getTestFileURL = function(path) { + var lastSlashIdx = path.lastIndexOf("/") + 1; + var filename = path.substr(lastSlashIdx); + var location = window.location; + // Remove mochitest html file name from the path + var remotePath = location.pathname.replace(/\/[^\/]+?$/,""); + var url = location.origin + + remotePath + "/" + path; + return url; +}; + +SimpleTest._getCurrentTestURL = function() { + return parentRunner && parentRunner.currentTestURL || + typeof gTestPath == "string" && gTestPath || + "unknown test url"; +}; + +SimpleTest._forceLogMessageOutput = parentRunner && !parentRunner.quiet; + +/** + * Force all test messages to be displayed. Only applies for the current test. + */ +SimpleTest.requestCompleteLog = function() { + if (SimpleTest._forceLogMessageOutput) + return; + + SimpleTest._forceLogMessageOutput = true; + SimpleTest.registerCleanupFunction(function() { + SimpleTest._forceLogMessageOutput = false; + }); +}; + +/** + * A circular buffer, managed by _logResult. We explicitly manage the + * circularness of the buffer, rather than resorting to .shift()/.push() + * because explicit management is much faster. + */ +SimpleTest._bufferedMessages = []; +SimpleTest._logResult = (function () { + var bufferingThreshold = 100; + var outputIndex = 0; + + function logResult(test, passString, failString) { + var url = SimpleTest._getCurrentTestURL(); + var resultString = test.result ? passString : failString; + var diagnostic = test.name + (test.diag ? " - " + test.diag : ""); + var msg = [resultString, url, diagnostic].join(" | "); + var isError = !test.result == !test.todo; + + // Due to JavaScript's name lookup rules, it is important that + // the second parameter here be named identically to the isError + // variable declared above. + function dumpMessage(msg, isError) { + if (parentRunner) { + if (isError) { + parentRunner.addFailedTest(url); + parentRunner.error(msg); + } else { + parentRunner.log(msg); + } + } else if (typeof dump === "function") { + dump(msg + "\n"); + } else { + // Non-Mozilla browser? Just do nothing. + } + } + + // Detect when SimpleTest.reset() has been called, so we can + // reset outputIndex. We store outputIndex as local state to + // avoid adding even more state to SimpleTest. + if (SimpleTest._bufferedMessages.length == 0) { + outputIndex = 0; + } + + // We want to eliminate mundane TEST-PASS/TEST-KNOWN-FAIL + // output, since some tests produce tens of thousands of of such + // messages. These messages can consume a lot of memory to + // generate and take a significant amount of time to output. + // However, the reality is that TEST-PASS messages can also be + // used as a form of logging via constructs like: + // + // SimpleTest.ok(true, "some informative message"); + // + // And eliding the logging can be very confusing when trying to + // debug test failures. + // + // Hence the compromise adopted here: We buffer messages up to + // some limit and dump the buffer when a test failure happens. + // This behavior ought to provide enough context for developers + // looking to understand where in the test things failed. + if (isError) { + // Display this message and all the messages we have buffered. + if (SimpleTest._bufferedMessages.length > 0) { + dumpMessage("TEST-INFO | dumping last " + SimpleTest._bufferedMessages.length + " message(s)"); + dumpMessage("TEST-INFO | if you need more context, please use SimpleTest.requestCompleteLog() in your test"); + + function dumpBufferedMessage(m) { + dumpMessage(m, false); + } + // The latest message is just before outputIndex. + // The earliest message is located at outputIndex. + var earliest = SimpleTest._bufferedMessages.slice(outputIndex); + var latest = SimpleTest._bufferedMessages.slice(0, outputIndex); + earliest.map(dumpBufferedMessage); + latest.map(dumpBufferedMessage); + + SimpleTest._bufferedMessages = []; + } + + dumpMessage(msg); + return; + } + + var runningSingleTest = ((parentRunner && + parentRunner._urls.length == 1) || + isSingleTestRun); + var shouldLogImmediately = (runningSingleTest || + SimpleTest._forceLogMessageOutput); + + if (!shouldLogImmediately) { + // Buffer the message for possible later output. + if (SimpleTest._bufferedMessages.length >= bufferingThreshold) { + if (outputIndex >= bufferingThreshold) { + outputIndex = 0; + } + SimpleTest._bufferedMessages[outputIndex] = msg; + outputIndex++; + } else { + SimpleTest._bufferedMessages.push(msg); + } + return; + } + + dumpMessage(msg); + } + + return logResult; +})(); + +SimpleTest.info = function(name, message) { + SimpleTest._logResult({result:true, name:name, diag:message}, "TEST-INFO"); +}; + +/** + * Copies of is and isnot with the call to ok replaced by a call to todo. +**/ + +SimpleTest.todo_is = function (a, b, name) { + var pass = (a == b); + var diag = pass ? repr(a) + " should equal " + repr(b) + : "got " + repr(a) + ", expected " + repr(b); + SimpleTest.todo(pass, name, diag); +}; + +SimpleTest.todo_isnot = function (a, b, name) { + var pass = (a != b); + var diag = pass ? repr(a) + " should not equal " + repr(b) + : "didn't expect " + repr(a) + ", but got it"; + SimpleTest.todo(pass, name, diag); +}; + + +/** + * Makes a test report, returns it as a DIV element. +**/ +SimpleTest.report = function () { + var passed = 0; + var failed = 0; + var todo = 0; + + var tallyAndCreateDiv = function (test) { + var cls, msg, div; + var diag = test.diag ? " - " + test.diag : ""; + if (test.todo && !test.result) { + todo++; + cls = "test_todo"; + msg = "todo | " + test.name + diag; + } else if (test.result && !test.todo) { + passed++; + cls = "test_ok"; + msg = "passed | " + test.name + diag; + } else { + failed++; + cls = "test_not_ok"; + msg = "failed | " + test.name + diag; + } + div = createEl('div', {'class': cls}, msg); + return div; + }; + var results = []; + for (var d=0; d 50) { + // Log the failure. + SimpleTest.ok(false, "Timed out while polling clipboard for pasted data."); + reset(); + failureFn(); + return; + } + + var data = SpecialPowers.getClipboardData(flavor); + + if (validatorFn(data)) { + // Don't show the success message when waiting for preExpectedVal + if (preExpectedVal) + preExpectedVal = null; + else + SimpleTest.ok(true, "Clipboard has the correct value"); + reset(); + successFn(); + } else { + setTimeout(function() { return wait(validatorFn, successFn, failureFn, flavor); }, 100); + } + } + + // First we wait for a known value different from the expected one. + var preExpectedVal = SimpleTest._waitForClipboardMonotonicCounter + + "-waitForClipboard-known-value"; + SpecialPowers.clipboardCopyString(preExpectedVal); + wait(function(aData) { return aData == preExpectedVal; }, + function() { + // Call the original setup fn + aSetupFn(); + wait(inputValidatorFn, aSuccessFn, aFailureFn, requestedFlavor); + }, aFailureFn, "text/unicode"); +} + +/** + * Executes a function shortly after the call, but lets the caller continue + * working (or finish). + */ +SimpleTest.executeSoon = function(aFunc) { + if ("SpecialPowers" in window) { + return SpecialPowers.executeSoon(aFunc, window); + } + setTimeout(aFunc, 0); + return null; // Avoid warning. +}; + +SimpleTest.registerCleanupFunction = function(aFunc) { + SimpleTest._cleanupFunctions.push(aFunc); +}; + +/** + * Finishes the tests. This is automatically called, except when + * SimpleTest.waitForExplicitFinish() has been invoked. +**/ +SimpleTest.finish = function() { + var Task = SpecialPowers.Cu.import("resource://gre/modules/Task.jsm").Task; + + if (SimpleTest._alreadyFinished) { + SimpleTest.ok(false, "[SimpleTest.finish()] this test already called finish!"); + } + + SimpleTest._alreadyFinished = true; + + Task.spawn(function*() { + // Execute all of our cleanup functions. + var func; + while ((func = SimpleTest._cleanupFunctions.pop())) { + try { + yield func(); + } + catch (ex) { + SimpleTest.ok(false, "Cleanup function threw exception: " + ex); + } + } + + if (SpecialPowers.DOMWindowUtils.isTestControllingRefreshes) { + SimpleTest.ok(false, "test left refresh driver under test control"); + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + } + if (SimpleTest._expectingUncaughtException) { + SimpleTest.ok(false, "expectUncaughtException was called but no uncaught exception was detected!"); + } + if (SimpleTest._pendingWaitForFocusCount != 0) { + SimpleTest.is(SimpleTest._pendingWaitForFocusCount, 0, + "[SimpleTest.finish()] waitForFocus() was called a " + + "different number of times from the number of " + + "callbacks run. Maybe the test terminated " + + "prematurely -- be sure to use " + + "SimpleTest.waitForExplicitFinish()."); + } + if (SimpleTest._tests.length == 0) { + SimpleTest.ok(false, "[SimpleTest.finish()] No checks actually run. " + + "(You need to call ok(), is(), or similar " + + "functions at least once. Make sure you use " + + "SimpleTest.waitForExplicitFinish() if you need " + + "it.)"); + } + + if (parentRunner) { + /* We're running in an iframe, and the parent has a TestRunner */ + parentRunner.testFinished(SimpleTest._tests); + } else { + SpecialPowers.flushAllAppsLaunchable(); + SpecialPowers.flushPermissions(function () { + SpecialPowers.flushPrefEnv(function() { + SimpleTest.showReport(); + }); + }); + } + }); +}; + +/** + * Monitor console output from now until endMonitorConsole is called. + * + * Expect to receive all console messages described by the elements of + * |msgs|, an array, in the order listed in |msgs|; each element is an + * object which may have any number of the following properties: + * message, errorMessage, sourceName, sourceLine, category: + * string or regexp + * lineNumber, columnNumber: number + * isScriptError, isWarning, isException, isStrict: boolean + * Strings, numbers, and booleans must compare equal to the named + * property of the Nth console message. Regexps must match. Any + * fields present in the message but not in the pattern object are ignored. + * + * In addition to the above properties, elements in |msgs| may have a |forbid| + * boolean property. When |forbid| is true, a failure is logged each time a + * matching message is received. + * + * If |forbidUnexpectedMsgs| is true, then the messages received in the console + * must exactly match the non-forbidden messages in |msgs|; for each received + * message not described by the next element in |msgs|, a failure is logged. If + * false, then other non-forbidden messages are ignored, but all expected + * messages must still be received. + * + * After endMonitorConsole is called, |continuation| will be called + * asynchronously. (Normally, you will want to pass |SimpleTest.finish| here.) + * + * It is incorrect to use this function in a test which has not called + * SimpleTest.waitForExplicitFinish. + */ +SimpleTest.monitorConsole = function (continuation, msgs, forbidUnexpectedMsgs) { + if (SimpleTest._stopOnLoad) { + ok(false, "Console monitoring requires use of waitForExplicitFinish."); + } + + function msgMatches(msg, pat) { + for (k in pat) { + if (!(k in msg)) { + return false; + } + if (pat[k] instanceof RegExp && typeof(msg[k]) === 'string') { + if (!pat[k].test(msg[k])) { + return false; + } + } else if (msg[k] !== pat[k]) { + return false; + } + } + return true; + } + + var forbiddenMsgs = []; + var i = 0; + while (i < msgs.length) { + var pat = msgs[i]; + if ("forbid" in pat) { + var forbid = pat.forbid; + delete pat.forbid; + if (forbid) { + forbiddenMsgs.push(pat); + msgs.splice(i, 1); + continue; + } + } + i++; + } + + var counter = 0; + function listener(msg) { + if (msg.message === "SENTINEL" && !msg.isScriptError) { + is(counter, msgs.length, "monitorConsole | number of messages"); + SimpleTest.executeSoon(continuation); + return; + } + for (var pat of forbiddenMsgs) { + if (msgMatches(msg, pat)) { + ok(false, "monitorConsole | observed forbidden message " + + JSON.stringify(msg)); + return; + } + } + if (counter >= msgs.length) { + var str = "monitorConsole | extra message | " + JSON.stringify(msg); + if (forbidUnexpectedMsgs) { + ok(false, str); + } else { + info(str); + } + return; + } + var matches = msgMatches(msg, msgs[counter]); + if (forbidUnexpectedMsgs) { + ok(matches, "monitorConsole | [" + counter + "] must match " + + JSON.stringify(msg)); + } else { + info("monitorConsole | [" + counter + "] " + + (matches ? "matched " : "did not match ") + JSON.stringify(msg)); + } + if (matches) + counter++; + } + SpecialPowers.registerConsoleListener(listener); +}; + +/** + * Stop monitoring console output. + */ +SimpleTest.endMonitorConsole = function () { + SpecialPowers.postConsoleSentinel(); +}; + +/** + * Run |testfn| synchronously, and monitor its console output. + * + * |msgs| is handled as described above for monitorConsole. + * + * After |testfn| returns, console monitoring will stop, and + * |continuation| will be called asynchronously. + */ +SimpleTest.expectConsoleMessages = function (testfn, msgs, continuation) { + SimpleTest.monitorConsole(continuation, msgs); + testfn(); + SimpleTest.executeSoon(SimpleTest.endMonitorConsole); +}; + +/** + * Wrapper around |expectConsoleMessages| for the case where the test has + * only one |testfn| to run. + */ +SimpleTest.runTestExpectingConsoleMessages = function(testfn, msgs) { + SimpleTest.waitForExplicitFinish(); + SimpleTest.expectConsoleMessages(testfn, msgs, SimpleTest.finish); +}; + +/** + * Indicates to the test framework that the current test expects one or + * more crashes (from plugins or IPC documents), and that the minidumps from + * those crashes should be removed. + */ +SimpleTest.expectChildProcessCrash = function () { + if (parentRunner) { + parentRunner.expectChildProcessCrash(); + } +}; + +/** + * Indicates to the test framework that the next uncaught exception during + * the test is expected, and should not cause a test failure. + */ +SimpleTest.expectUncaughtException = function (aExpecting) { + SimpleTest._expectingUncaughtException = aExpecting === void 0 || !!aExpecting; +}; + +/** + * Returns whether the test has indicated that it expects an uncaught exception + * to occur. + */ +SimpleTest.isExpectingUncaughtException = function () { + return SimpleTest._expectingUncaughtException; +}; + +/** + * Indicates to the test framework that all of the uncaught exceptions + * during the test are known problems that should be fixed in the future, + * but which should not cause the test to fail currently. + */ +SimpleTest.ignoreAllUncaughtExceptions = function (aIgnoring) { + SimpleTest._ignoringAllUncaughtExceptions = aIgnoring === void 0 || !!aIgnoring; +}; + +/** + * Returns whether the test has indicated that all uncaught exceptions should be + * ignored. + */ +SimpleTest.isIgnoringAllUncaughtExceptions = function () { + return SimpleTest._ignoringAllUncaughtExceptions; +}; + +/** + * Resets any state this SimpleTest object has. This is important for + * browser chrome mochitests, which reuse the same SimpleTest object + * across a run. + */ +SimpleTest.reset = function () { + SimpleTest._ignoringAllUncaughtExceptions = false; + SimpleTest._expectingUncaughtException = false; + SimpleTest._bufferedMessages = []; +}; + +if (isPrimaryTestWindow) { + addLoadEvent(function() { + if (SimpleTest._stopOnLoad) { + SimpleTest.finish(); + } + }); +} + +// --------------- Test.Builder/Test.More isDeeply() ----------------- + + +SimpleTest.DNE = {dne: 'Does not exist'}; +SimpleTest.LF = "\r\n"; +SimpleTest._isRef = function (object) { + var type = typeof(object); + return type == 'object' || type == 'function'; +}; + + +SimpleTest._deepCheck = function (e1, e2, stack, seen) { + var ok = false; + // Either they're both references or both not. + var sameRef = !(!SimpleTest._isRef(e1) ^ !SimpleTest._isRef(e2)); + if (e1 == null && e2 == null) { + ok = true; + } else if (e1 != null ^ e2 != null) { + ok = false; + } else if (e1 == SimpleTest.DNE ^ e2 == SimpleTest.DNE) { + ok = false; + } else if (sameRef && e1 == e2) { + // Handles primitives and any variables that reference the same + // object, including functions. + ok = true; + } else if (SimpleTest.isa(e1, 'Array') && SimpleTest.isa(e2, 'Array')) { + ok = SimpleTest._eqArray(e1, e2, stack, seen); + } else if (typeof e1 == "object" && typeof e2 == "object") { + ok = SimpleTest._eqAssoc(e1, e2, stack, seen); + } else if (typeof e1 == "number" && typeof e2 == "number" + && isNaN(e1) && isNaN(e2)) { + ok = true; + } else { + // If we get here, they're not the same (function references must + // always simply reference the same function). + stack.push({ vals: [e1, e2] }); + ok = false; + } + return ok; +}; + +SimpleTest._eqArray = function (a1, a2, stack, seen) { + // Return if they're the same object. + if (a1 == a2) return true; + + // JavaScript objects have no unique identifiers, so we have to store + // references to them all in an array, and then compare the references + // directly. It's slow, but probably won't be much of an issue in + // practice. Start by making a local copy of the array to as to avoid + // confusing a reference seen more than once (such as [a, a]) for a + // circular reference. + for (var j = 0; j < seen.length; j++) { + if (seen[j][0] == a1) { + return seen[j][1] == a2; + } + } + + // If we get here, we haven't seen a1 before, so store it with reference + // to a2. + seen.push([ a1, a2 ]); + + var ok = true; + // Only examines enumerable attributes. Only works for numeric arrays! + // Associative arrays return 0. So call _eqAssoc() for them, instead. + var max = a1.length > a2.length ? a1.length : a2.length; + if (max == 0) return SimpleTest._eqAssoc(a1, a2, stack, seen); + for (var i = 0; i < max; i++) { + var e1 = i > a1.length - 1 ? SimpleTest.DNE : a1[i]; + var e2 = i > a2.length - 1 ? SimpleTest.DNE : a2[i]; + stack.push({ type: 'Array', idx: i, vals: [e1, e2] }); + ok = SimpleTest._deepCheck(e1, e2, stack, seen); + if (ok) { + stack.pop(); + } else { + break; + } + } + return ok; +}; + +SimpleTest._eqAssoc = function (o1, o2, stack, seen) { + // Return if they're the same object. + if (o1 == o2) return true; + + // JavaScript objects have no unique identifiers, so we have to store + // references to them all in an array, and then compare the references + // directly. It's slow, but probably won't be much of an issue in + // practice. Start by making a local copy of the array to as to avoid + // confusing a reference seen more than once (such as [a, a]) for a + // circular reference. + seen = seen.slice(0); + for (var j = 0; j < seen.length; j++) { + if (seen[j][0] == o1) { + return seen[j][1] == o2; + } + } + + // If we get here, we haven't seen o1 before, so store it with reference + // to o2. + seen.push([ o1, o2 ]); + + // They should be of the same class. + + var ok = true; + // Only examines enumerable attributes. + var o1Size = 0; for (var i in o1) o1Size++; + var o2Size = 0; for (var i in o2) o2Size++; + var bigger = o1Size > o2Size ? o1 : o2; + for (var i in bigger) { + var e1 = o1[i] == undefined ? SimpleTest.DNE : o1[i]; + var e2 = o2[i] == undefined ? SimpleTest.DNE : o2[i]; + stack.push({ type: 'Object', idx: i, vals: [e1, e2] }); + ok = SimpleTest._deepCheck(e1, e2, stack, seen) + if (ok) { + stack.pop(); + } else { + break; + } + } + return ok; +}; + +SimpleTest._formatStack = function (stack) { + var variable = '$Foo'; + for (var i = 0; i < stack.length; i++) { + var entry = stack[i]; + var type = entry['type']; + var idx = entry['idx']; + if (idx != null) { + if (/^\d+$/.test(idx)) { + // Numeric array index. + variable += '[' + idx + ']'; + } else { + // Associative array index. + idx = idx.replace("'", "\\'"); + variable += "['" + idx + "']"; + } + } + } + + var vals = stack[stack.length-1]['vals'].slice(0, 2); + var vars = [ + variable.replace('$Foo', 'got'), + variable.replace('$Foo', 'expected') + ]; + + var out = "Structures begin differing at:" + SimpleTest.LF; + for (var i = 0; i < vals.length; i++) { + var val = vals[i]; + if (val == null) { + val = 'undefined'; + } else { + val == SimpleTest.DNE ? "Does not exist" : "'" + val + "'"; + } + } + + out += vars[0] + ' = ' + vals[0] + SimpleTest.LF; + out += vars[1] + ' = ' + vals[1] + SimpleTest.LF; + + return ' ' + out; +}; + + +SimpleTest.isDeeply = function (it, as, name) { + var ok; + // ^ is the XOR operator. + if (SimpleTest._isRef(it) ^ SimpleTest._isRef(as)) { + // One's a reference, one isn't. + ok = false; + } else if (!SimpleTest._isRef(it) && !SimpleTest._isRef(as)) { + // Neither is an object. + ok = SimpleTest.is(it, as, name); + } else { + // We have two objects. Do a deep comparison. + var stack = [], seen = []; + if ( SimpleTest._deepCheck(it, as, stack, seen)) { + ok = SimpleTest.ok(true, name); + } else { + ok = SimpleTest.ok(false, name, SimpleTest._formatStack(stack)); + } + } + return ok; +}; + +SimpleTest.typeOf = function (object) { + var c = Object.prototype.toString.apply(object); + var name = c.substring(8, c.length - 1); + if (name != 'Object') return name; + // It may be a non-core class. Try to extract the class name from + // the constructor function. This may not work in all implementations. + if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) { + return RegExp.$1; + } + // No idea. :-( + return name; +}; + +SimpleTest.isa = function (object, clas) { + return SimpleTest.typeOf(object) == clas; +}; + +// Global symbols: +var ok = SimpleTest.ok; +var is = SimpleTest.is; +var isfuzzy = SimpleTest.isfuzzy; +var isnot = SimpleTest.isnot; +var ise = SimpleTest.ise; +var todo = SimpleTest.todo; +var todo_is = SimpleTest.todo_is; +var todo_isnot = SimpleTest.todo_isnot; +var isDeeply = SimpleTest.isDeeply; +var info = SimpleTest.info; + +var gOldOnError = window.onerror; +window.onerror = function simpletestOnerror(errorMsg, url, lineNumber) { + // Log the message. + // XXX Chrome mochitests sometimes trigger this window.onerror handler, + // but there are a number of uncaught JS exceptions from those tests. + // For now, for tests that self identify as having unintentional uncaught + // exceptions, just dump it so that the error is visible but doesn't cause + // a test failure. See bug 652494. + var isExpected = !!SimpleTest._expectingUncaughtException; + var message = (isExpected ? "expected " : "") + "uncaught exception"; + var error = errorMsg + " at " + url + ":" + lineNumber; + if (!SimpleTest._ignoringAllUncaughtExceptions) { + SimpleTest.ok(isExpected, message, error); + SimpleTest._expectingUncaughtException = false; + } else { + SimpleTest.todo(false, message + ": " + error); + } + // There is no Components.stack.caller to log. (See bug 511888.) + + // Call previous handler. + if (gOldOnError) { + try { + // Ignore return value: always run default handler. + gOldOnError(errorMsg, url, lineNumber); + } catch (e) { + // Log the error. + SimpleTest.info("Exception thrown by gOldOnError(): " + e); + // Log its stack. + if (e.stack) { + SimpleTest.info("JavaScript error stack:\n" + e.stack); + } + } + } + + if (!SimpleTest._stopOnLoad && !isExpected) { + // Need to finish() manually here, yet let the test actually end first. + SimpleTest.executeSoon(SimpleTest.finish); + } +};