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