diff options
Diffstat (limited to '')
-rw-r--r-- | testing/mochitest/tests/SimpleTest/SimpleTest.js | 2189 |
1 files changed, 2189 insertions, 0 deletions
diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js new file mode 100644 index 0000000000..12c8b3f6ea --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -0,0 +1,2189 @@ +/* -*- js-indent-level: 4; tab-width: 4; indent-tabs-mode: nil -*- */ +/* vim:set ts=4 sw=4 sts=4 et: */ + +// Generally gTestPath should be set by the harness. +/* global gTestPath */ + +/** + * SimpleTest framework object. + * @class + */ +var SimpleTest = {}; +var parentRunner = null; + +// Using a try/catch rather than SpecialPowers.Cu.isRemoteProxy() because +// it doesn't cover the case where an iframe is xorigin but fission is +// not enabled. +let isSameOrigin = function(w) { + try { + w.top.TestRunner; + } catch (e) { + if (e instanceof DOMException) { + return false; + } + } + return true; +}; +let isXOrigin = !isSameOrigin(window); + +// 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 || (window.arguments && window.arguments[0].SimpleTest)); +try { + var isPrimaryTestWindow = + (isXOrigin && parent != window && parent == top) || + (!isXOrigin && (!!parent.TestRunner || isSingleTestRun)); +} catch (e) { + dump( + "TEST-UNEXPECTED-FAIL, Exception caught: " + + e.message + + ", at: " + + e.fileName + + " (" + + e.lineNumber + + "), location: " + + window.location.href + + "\n" + ); +} + +let xOriginRunner = { + init(harnessWindow) { + this.harnessWindow = harnessWindow; + let url = new URL(document.URL); + this.testFile = url.pathname; + this.showTestReport = url.searchParams.get("showTestReport") == "true"; + this.expected = url.searchParams.get("expected"); + }, + callHarnessMethod(applyOn, command, ...params) { + // Message handled by xOriginTestRunnerHandler in TestRunner.js + this.harnessWindow.postMessage( + { + harnessType: "SimpleTest", + applyOn, + command, + params, + }, + "*" + ); + }, + getParameterInfo() { + let url = new URL(document.URL); + return { + currentTestURL: url.searchParams.get("currentTestURL"), + testRoot: url.searchParams.get("testRoot"), + }; + }, + addFailedTest(test) { + this.callHarnessMethod("runner", "addFailedTest", test); + }, + expectAssertions(min, max) { + this.callHarnessMethod("runner", "expectAssertions", min, max); + }, + expectChildProcessCrash() { + this.callHarnessMethod("runner", "expectChildProcessCrash"); + }, + requestLongerTimeout(factor) { + this.callHarnessMethod("runner", "requestLongerTimeout", factor); + }, + _lastAssertionCount: 0, + testFinished(tests) { + var newAssertionCount = SpecialPowers.assertionCount(); + var numAsserts = newAssertionCount - this._lastAssertionCount; + this._lastAssertionCount = newAssertionCount; + this.callHarnessMethod("runner", "addAssertionCount", numAsserts); + this.callHarnessMethod("runner", "testFinished", tests); + }, + structuredLogger: { + info(msg) { + xOriginRunner.callHarnessMethod("logger", "structuredLogger.info", msg); + }, + warning(msg) { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.warning", + msg + ); + }, + error(msg) { + xOriginRunner.callHarnessMethod("logger", "structuredLogger.error", msg); + }, + activateBuffering() { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.activateBuffering" + ); + }, + deactivateBuffering() { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.deactivateBuffering" + ); + }, + testStatus(url, subtest, status, expected, diagnostic, stack) { + xOriginRunner.callHarnessMethod( + "logger", + "structuredLogger.testStatus", + url, + subtest, + status, + expected, + diagnostic, + stack + ); + }, + }, +}; + +// 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.xhtml and we need SpecialPowers in the iframe, and also for tests +// like test_focus.xhtml 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 || + (!isXOrigin && + w.arguments && + SpecialPowers.wrap(Window).isInstance(w.arguments[0]) && + w.arguments[0]); + } + + var w = ancestor(window); + while (w && !parentRunner) { + isXOrigin = !isSameOrigin(w); + + if (isXOrigin) { + if (w.parent != w) { + w = w.top; + } + xOriginRunner.init(w); + parentRunner = xOriginRunner; + } + + if (!parentRunner) { + parentRunner = w.TestRunner; + if (!parentRunner && w.wrappedJSObject) { + parentRunner = w.wrappedJSObject.TestRunner; + } + } + w = ancestor(w); + } + + if (parentRunner) { + SimpleTest.harnessParameters = parentRunner.getParameterInfo(); + } +})(); + +/* Helper functions pulled out of various MochiKit modules */ +if (typeof repr == "undefined") { + this.repr = function repr(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 != repr) { + 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) {} + var ostring; + try { + if (o === 0) { + ostring = 1 / o > 0 ? "+0" : "-0"; + } else if (typeof o === "string") { + ostring = JSON.stringify(o); + } else if (Array.isArray(o)) { + ostring = "[" + o.map(val => repr(val)).join(", ") + "]"; + } else { + 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 (let i = 1; i < arguments.length; i++) { + args.push(arguments[i]); + } + return function() { + if (arguments.length) { + for (let 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 callStackHandler() { + var callStack = callStackHandler.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); + if (typeof style == "undefined" || style === null) { + return undefined; + } + + var selectorCase = cssProperty.replace(/([A-Z])/g, "-$1").toLowerCase(); + + return style.getPropertyValue(selectorCase); + }; +} + +SimpleTest._tests = []; +SimpleTest._stopOnLoad = true; +SimpleTest._cleanupFunctions = []; +SimpleTest._timeoutFunctions = []; +SimpleTest._inChaosMode = false; +// When using failure pattern file to filter unexpected issues, +// SimpleTest.expected would be an array of [pattern, expected count], +// and SimpleTest.num_failed would be an array of actual counts which +// has the same length as SimpleTest.expected. +SimpleTest.expected = "pass"; +SimpleTest.num_failed = 0; + +SpecialPowers.setAsDefaultAssertHandler(); + +function usesFailurePatterns() { + return Array.isArray(SimpleTest.expected); +} + +/** + * Checks whether there is any failure pattern matches the given error + * message, and if found, bumps the counter of the failure pattern. + * + * @return {boolean} Whether a matched failure pattern is found. + */ +function recordIfMatchesFailurePattern(name, diag) { + let index = SimpleTest.expected.findIndex(([pat, count]) => { + return ( + pat == null || + (typeof name == "string" && name.includes(pat)) || + (typeof diag == "string" && diag.includes(pat)) + ); + }); + if (index >= 0) { + SimpleTest.num_failed[index]++; + return true; + } + return false; +} + +SimpleTest.setExpected = function() { + if (!parentRunner) { + return; + } + if (!Array.isArray(parentRunner.expected)) { + SimpleTest.expected = parentRunner.expected; + } else { + // Assertions are checked by the runner. + SimpleTest.expected = parentRunner.expected.filter( + ([pat]) => pat != "ASSERTION" + ); + SimpleTest.num_failed = new Array(SimpleTest.expected.length); + SimpleTest.num_failed.fill(0); + } +}; +SimpleTest.setExpected(); + +/** + * Something like assert. + **/ +SimpleTest.ok = function(condition, name) { + if (arguments.length > 2) { + const diag = "Too many arguments passed to `ok(condition, name)`"; + SimpleTest.record(false, name, diag); + } else { + SimpleTest.record(condition, name); + } +}; + +SimpleTest.record = function(condition, name, diag, stack, expected) { + var test = { result: !!condition, name, diag }; + let successInfo; + let failureInfo; + if (SimpleTest.expected == "fail") { + if (!test.result) { + SimpleTest.num_failed++; + test.result = true; + } + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else if (!test.result && usesFailurePatterns()) { + if (recordIfMatchesFailurePattern(name, diag)) { + test.result = true; + // Add a mark for unexpected failures suppressed by failure pattern. + name = "[suppressed] " + name; + } + successInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } else if (expected == "fail") { + successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else { + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } + + if (condition) { + stack = null; + } else if (!stack) { + stack = new Error().stack + .replace(/^(.*@)http:\/\/mochi.test:8888\/tests\//gm, " $1") + .split("\n"); + stack.splice(0, 1); + stack = stack.join("\n"); + } + SimpleTest._logResult(test, successInfo, failureInfo, stack); + SimpleTest._tests.push(test); +}; + +/** + * Roughly equivalent to ok(Object.is(a, b), name) + **/ +SimpleTest.is = function(a, b, name) { + // Be lazy and use Object.is til we want to test a browser without it. + var pass = Object.is(a, b); + var diag = pass ? "" : "got " + repr(a) + ", expected " + repr(b); + SimpleTest.record(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.record(pass, name, diag); +}; + +SimpleTest.isnot = function(a, b, name) { + var pass = !Object.is(a, b); + var diag = pass ? "" : "didn't expect " + repr(a) + ", but got it"; + SimpleTest.record(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, diag, todo: true }; + if ( + test.result && + usesFailurePatterns() && + recordIfMatchesFailurePattern(name, diag) + ) { + // Flipping the result to false so we don't get unexpected result. There + // is no perfect way here. A known failure can trigger unexpected pass, + // in which case, tagging it as KNOWN-FAIL probably makes more sense than + // marking it PASS. + test.result = false; + // Add a mark for unexpected failures suppressed by failure pattern. + name = "[suppressed] " + name; + } + var successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + var failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + SimpleTest._logResult(test, successInfo, failureInfo); + 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 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 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 ( + (SimpleTest.harnessParameters && + SimpleTest.harnessParameters.currentTestURL) || + (parentRunner && parentRunner.currentTestURL) || + (typeof gTestPath == "string" && gTestPath) || + "unknown test url" + ); +}; + +SimpleTest._forceLogMessageOutput = false; + +/** + * Force all test messages to be displayed. Only applies for the current test. + */ +SimpleTest.requestCompleteLog = function() { + if (!parentRunner || SimpleTest._forceLogMessageOutput) { + return; + } + + parentRunner.structuredLogger.deactivateBuffering(); + SimpleTest._forceLogMessageOutput = true; + + SimpleTest.registerCleanupFunction(function() { + parentRunner.structuredLogger.activateBuffering(); + SimpleTest._forceLogMessageOutput = false; + }); +}; + +SimpleTest._logResult = function(test, passInfo, failInfo, stack) { + var url = SimpleTest._getCurrentTestURL(); + var result = test.result ? passInfo : failInfo; + var diagnostic = test.diag || null; + // BUGFIX : coercing test.name to a string, because some a11y tests pass an xpconnect object + var subtest = test.name ? String(test.name) : null; + var isError = !test.result == !test.todo; + + if (parentRunner) { + if (!result.status || !result.expected) { + if (diagnostic) { + parentRunner.structuredLogger.info(diagnostic); + } + return; + } + + if (isError) { + parentRunner.addFailedTest(url); + } + + parentRunner.structuredLogger.testStatus( + url, + subtest, + result.status, + result.expected, + diagnostic, + stack + ); + } else if (typeof dump === "function") { + var diagMessage = test.name + (test.diag ? " - " + test.diag : ""); + var debugMsg = [result.message, url, diagMessage].join(" | "); + dump(debugMsg + "\n"); + } else { + // Non-Mozilla browser? Just do nothing. + } +}; + +SimpleTest.info = function(name, message) { + var log = message ? name + " | " + message : name; + if (parentRunner) { + parentRunner.structuredLogger.info(log); + } else { + dump(log + "\n"); + } +}; + +/** + * 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 = Object.is(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 = !Object.is(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 < SimpleTest._tests.length; d++) { + results.push(tallyAndCreateDiv(SimpleTest._tests[d])); + } + + var summary_class = + // eslint-disable-next-line no-nested-ternary + failed != 0 ? "some_fail" : passed == 0 ? "todo_only" : "all_pass"; + + var div1 = createEl("div", { class: "tests_report" }); + var div2 = createEl("div", { class: "tests_summary " + summary_class }); + var div3 = createEl("div", { class: "tests_passed" }, "Passed: " + passed); + var div4 = createEl("div", { class: "tests_failed" }, "Failed: " + failed); + var div5 = createEl("div", { class: "tests_todo" }, "Todo: " + todo); + div2.appendChild(div3); + div2.appendChild(div4); + div2.appendChild(div5); + div1.appendChild(div2); + for (var t = 0; t < results.length; t++) { + //iterate in order + div1.appendChild(results[t]); + } + return div1; +}; + +/** + * Toggle element visibility + **/ +SimpleTest.toggle = function(el) { + if (computedStyle(el, "display") == "block") { + el.style.display = "none"; + } else { + el.style.display = "block"; + } +}; + +/** + * Toggle visibility for divs with a specific class. + **/ +SimpleTest.toggleByClass = function(cls, evt) { + var children = document.getElementsByTagName("div"); + var elements = []; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var clsName = child.className; + if (!clsName) { + continue; + } + var classNames = clsName.split(" "); + for (var j = 0; j < classNames.length; j++) { + if (classNames[j] == cls) { + elements.push(child); + break; + } + } + } + for (var t = 0; t < elements.length; t++) { + //TODO: again, for-in loop over elems seems to break this + SimpleTest.toggle(elements[t]); + } + if (evt) { + evt.preventDefault(); + } +}; + +/** + * Shows the report in the browser + **/ +SimpleTest.showReport = function() { + var togglePassed = createEl("a", { href: "#" }, "Toggle passed checks"); + var toggleFailed = createEl("a", { href: "#" }, "Toggle failed checks"); + var toggleTodo = createEl("a", { href: "#" }, "Toggle todo checks"); + togglePassed.onclick = partial(SimpleTest.toggleByClass, "test_ok"); + toggleFailed.onclick = partial(SimpleTest.toggleByClass, "test_not_ok"); + toggleTodo.onclick = partial(SimpleTest.toggleByClass, "test_todo"); + var body = document.body; // Handles HTML documents + if (!body) { + // Do the XML thing. + body = document.getElementsByTagNameNS( + "http://www.w3.org/1999/xhtml", + "body" + )[0]; + } + var firstChild = body.childNodes[0]; + var addNode; + if (firstChild) { + addNode = function(el) { + body.insertBefore(el, firstChild); + }; + } else { + addNode = function(el) { + body.appendChild(el); + }; + } + addNode(togglePassed); + addNode(createEl("span", null, " ")); + addNode(toggleFailed); + addNode(createEl("span", null, " ")); + addNode(toggleTodo); + addNode(SimpleTest.report()); + // Add a separator from the test content. + addNode(createEl("hr")); +}; + +/** + * Tells SimpleTest to don't finish the test when the document is loaded, + * useful for asynchronous tests. + * + * When SimpleTest.waitForExplicitFinish is called, + * explicit SimpleTest.finish() is required. + **/ +SimpleTest.waitForExplicitFinish = function() { + SimpleTest._stopOnLoad = false; +}; + +/** + * Multiply the timeout the parent runner uses for this test by the + * given factor. + * + * For example, in a test that may take a long time to complete, using + * "SimpleTest.requestLongerTimeout(5)" will give it 5 times as long to + * finish. + * + * @param {Number} factor + * The multiplication factor to use on the timeout for this test. + */ +SimpleTest.requestLongerTimeout = function(factor) { + if (parentRunner) { + parentRunner.requestLongerTimeout(factor); + } else { + dump( + "[SimpleTest.requestLongerTimeout()] ignoring request, maybe you meant to call the global `requestLongerTimeout` instead?\n" + ); + } +}; + +/** + * Note that the given range of assertions is to be expected. When + * this function is not called, 0 assertions are expected. When only + * one argument is given, that number of assertions are expected. + * + * A test where we expect to have assertions (which should largely be a + * transitional mechanism to get assertion counts down from our current + * situation) can call the SimpleTest.expectAssertions() function, with + * either one or two arguments: one argument gives an exact number + * expected, and two arguments give a range. For example, a test might do + * one of the following: + * + * @example + * + * // Currently triggers two assertions (bug NNNNNN). + * SimpleTest.expectAssertions(2); + * + * // Currently triggers one assertion on Mac (bug NNNNNN). + * if (navigator.platform.indexOf("Mac") == 0) { + * SimpleTest.expectAssertions(1); + * } + * + * // Currently triggers two assertions on all platforms (bug NNNNNN), + * // but intermittently triggers two additional assertions (bug NNNNNN) + * // on Windows. + * if (navigator.platform.indexOf("Win") == 0) { + * SimpleTest.expectAssertions(2, 4); + * } else { + * SimpleTest.expectAssertions(2); + * } + * + * // Intermittently triggers up to three assertions (bug NNNNNN). + * SimpleTest.expectAssertions(0, 3); + */ +SimpleTest.expectAssertions = function(min, max) { + if (parentRunner) { + parentRunner.expectAssertions(min, max); + } +}; + +SimpleTest._flakyTimeoutIsOK = false; +SimpleTest._originalSetTimeout = window.setTimeout; +window.setTimeout = function SimpleTest_setTimeoutShim() { + // Don't break tests that are loaded without a parent runner. + if (parentRunner) { + // Right now, we only enable these checks for mochitest-plain. + switch (SimpleTest.harnessParameters.testRoot) { + case "browser": + case "chrome": + case "a11y": + break; + default: + if ( + !SimpleTest._alreadyFinished && + arguments.length > 1 && + arguments[1] > 0 + ) { + if (SimpleTest._flakyTimeoutIsOK) { + SimpleTest.todo( + false, + "The author of the test has indicated that flaky timeouts are expected. Reason: " + + SimpleTest._flakyTimeoutReason + ); + } else { + SimpleTest.ok( + false, + "Test attempted to use a flaky timeout value " + arguments[1] + ); + } + } + } + } + return SimpleTest._originalSetTimeout.apply(window, arguments); +}; + +/** + * Request the framework to allow usage of setTimeout(func, timeout) + * where ``timeout > 0``. This is required to note that the author of + * the test is aware of the inherent flakiness in the test caused by + * that, and asserts that there is no way around using the magic timeout + * value number for some reason. + * + * Use of this function is **STRONGLY** discouraged. Think twice before + * using it. Such magic timeout values could result in intermittent + * failures in your test, and are almost never necessary! + * + * @param {String} reason + * A string representation of the reason why the test needs timeouts. + * + */ +SimpleTest.requestFlakyTimeout = function(reason) { + SimpleTest.is(typeof reason, "string", "A valid string reason is expected"); + SimpleTest.isnot(reason, "", "Reason cannot be empty"); + SimpleTest._flakyTimeoutIsOK = true; + SimpleTest._flakyTimeoutReason = reason; +}; + +/** + * If the page is not yet loaded, waits for the load event. If the page is + * not yet focused, focuses and waits for the window to be focused. + * If the current page is 'about:blank', then the page is assumed to not + * yet be loaded. Pass true for expectBlankPage to not make this assumption + * if you expect a blank page to be present. + * + * The target object should be specified if it is different than 'window'. The + * actual focused window may be a descendant window of aObject. + * + * @param {Window|browser|BrowsingContext} [aObject] + * Optional object to be focused, and may be any of: + * window - a window object to focus + * browser - a <browser>/<iframe> element. The top-level window + * within the frame will be focused. + * browsing context - a browsing context containing a window to focus + * If not specified, defaults to the global 'window'. + * @param {boolean} [expectBlankPage=false] + * True if targetWindow.location is 'about:blank'. + * @param {boolean} [aBlurSubframe=false] + * If true, and a subframe within the window to focus is focused, blur + * it so that the specified window or browsing context will receive + * focus events. + * + * @returns The browsing context that was focused. + */ +SimpleTest.promiseFocus = async function( + aObject, + aExpectBlankPage = false, + aBlurSubframe = false +) { + let browser; + let browsingContext; + let windowToFocus; + + if (!aObject) { + aObject = window; + } + + async function waitForEvent(aTarget, aEventName) { + return new Promise(resolve => { + aTarget.addEventListener(aEventName, resolve, { + capture: true, + once: true, + }); + }); + } + + if (SpecialPowers.wrap(Window).isInstance(aObject)) { + windowToFocus = aObject; + + let isBlank = windowToFocus.location.href == "about:blank"; + if ( + aExpectBlankPage != isBlank || + windowToFocus.document.readyState != "complete" + ) { + info("must wait for load"); + await waitForEvent(windowToFocus, "load"); + } + } else { + if (SpecialPowers.wrap(Element).isInstance(aObject)) { + // assume this is a browser/iframe element + browsingContext = aObject.browsingContext; + } else { + browsingContext = aObject; + } + + browser = + browsingContext == aObject ? aObject.top.embedderElement : aObject; + windowToFocus = browser.ownerGlobal; + } + + if (!windowToFocus.document.hasFocus()) { + info("must wait for focus"); + let focusPromise = waitForEvent(windowToFocus.document, "focus"); + SpecialPowers.focus(windowToFocus); + await focusPromise; + } + + if (browser) { + if (windowToFocus.document.activeElement != browser) { + browser.focus(); + } + + info("must wait for focus in content"); + + // Make sure that the child process thinks it is focused as well. + await SpecialPowers.ensureFocus(browsingContext, aBlurSubframe); + } else { + if (aBlurSubframe) { + SpecialPowers.clearFocus(windowToFocus); + } + + browsingContext = windowToFocus.browsingContext; + } + + // Some tests rely on this delay, likely expecting layout or paint to occur. + await new Promise(resolve => { + SimpleTest.executeSoon(resolve); + }); + + return browsingContext; +}; + +/** + * Version of promiseFocus that uses a callback. For compatibility, + * the callback is passed one argument, the window that was focused. + * If the focused window is not in the same process, null is supplied. + */ +SimpleTest.waitForFocus = function(callback, aObject, expectBlankPage) { + SimpleTest.promiseFocus(aObject, expectBlankPage).then(focusedBC => { + callback(focusedBC?.window); + }); +}; +/* eslint-enable mozilla/use-services */ + +SimpleTest.stripLinebreaksAndWhitespaceAfterTags = function(aString) { + return aString.replace(/(>\s*(\r\n|\n|\r)*\s*)/gm, ">"); +}; + +/* + * `navigator.platform` should include this, when the platform is Windows. + */ +const kPlatformWindows = "Win"; + +/* + * See `SimpleTest.waitForClipboard`. + */ +const kTextHtmlPrefixClipboardDataWindows = + "<html><body>\n<!--StartFragment-->"; + +/* + * See `SimpleTest.waitForClipboard`. + */ +const kTextHtmlSuffixClipboardDataWindows = + "<!--EndFragment-->\n</body>\n</html>"; + +/* + * Polls the clipboard waiting for the expected value. A known value different than + * the expected value is put on the clipboard first (and also polled for) so we + * can be sure the value we get isn't just the expected value because it was already + * on the clipboard. This only uses the global clipboard and only for text/unicode + * values. + * + * @param {String|Function} aExpectedStringOrValidatorFn + * The string value that is expected to be on the clipboard, or a + * validator function getting expected clipboard data and returning a bool. + * If you specify string value, line breakers in clipboard are treated + * as LineFeed. Therefore, you cannot include CarriageReturn to the + * string. + * If you specify string value and expect "text/html" data, this wraps + * the expected value with `kTextHtmlPrefixClipboardDataWindows` and + * `kTextHtmlSuffixClipboardDataWindows` only when it runs on Windows + * because they are appended only by nsDataObj.cpp for Windows. + * https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842 + * Therefore, you can specify selected (copied) HTML data simply on any + * platforms. + * @param {Function} aSetupFn + * A function responsible for setting the clipboard to the expected value, + * called after the known value setting succeeds. + * @param {Function} aSuccessFn + * A function called when the expected value is found on the clipboard. + * @param {Function} aFailureFn + * A function called if the expected value isn't found on the clipboard + * within 5s. It can also be called if the known value can't be found. + * @param {String} [aFlavor="text/unicode"] + * The flavor to look for. + * @param {Number} [aTimeout=5000] + * The timeout (in milliseconds) to wait for a clipboard change. + * @param {boolean} [aExpectFailure=false] + * If true, fail if the clipboard contents are modified within the timeout + * interval defined by aTimeout. When aExpectFailure is true, the argument + * aExpectedStringOrValidatorFn must be null, as it won't be used. + * @param {boolean} [aDontInitializeClipboardIfExpectFailure=false] + * If aExpectFailure and this is set to true, this does NOT initialize + * clipboard with random data before running aSetupFn. + */ +SimpleTest.waitForClipboard = function( + aExpectedStringOrValidatorFn, + aSetupFn, + aSuccessFn, + aFailureFn, + aFlavor, + aTimeout, + aExpectFailure, + aDontInitializeClipboardIfExpectFailure +) { + let promise = SimpleTest.promiseClipboardChange( + aExpectedStringOrValidatorFn, + aSetupFn, + aFlavor, + aTimeout, + aExpectFailure, + aDontInitializeClipboardIfExpectFailure + ); + promise.then(aSuccessFn).catch(aFailureFn); +}; + +/** + * Promise-oriented version of waitForClipboard. + */ +SimpleTest.promiseClipboardChange = async function( + aExpectedStringOrValidatorFn, + aSetupFn, + aFlavor, + aTimeout, + aExpectFailure, + aDontInitializeClipboardIfExpectFailure +) { + let requestedFlavor = aFlavor || "text/unicode"; + + // The known value we put on the clipboard before running aSetupFn + let initialVal = "waitForClipboard-known-value-" + Math.random(); + let preExpectedVal = initialVal; + + let inputValidatorFn; + if (aExpectFailure) { + // If we expect failure, the aExpectedStringOrValidatorFn should be null + if (aExpectedStringOrValidatorFn !== null) { + SimpleTest.ok( + false, + "When expecting failure, aExpectedStringOrValidatorFn must be null" + ); + } + + inputValidatorFn = function(aData) { + return aData != initialVal; + }; + // Build a default validator function for common string input. + } else if (typeof aExpectedStringOrValidatorFn == "string") { + if (aExpectedStringOrValidatorFn.includes("\r")) { + throw new Error( + "Use function instead of string to compare raw line breakers in clipboard" + ); + } + if (requestedFlavor === "text/html" && navigator.platform.includes("Win")) { + inputValidatorFn = function(aData) { + return ( + aData.replace(/\r\n?/g, "\n") === + kTextHtmlPrefixClipboardDataWindows + + aExpectedStringOrValidatorFn + + kTextHtmlSuffixClipboardDataWindows + ); + }; + } else { + inputValidatorFn = function(aData) { + return aData.replace(/\r\n?/g, "\n") === aExpectedStringOrValidatorFn; + }; + } + } else { + inputValidatorFn = aExpectedStringOrValidatorFn; + } + + let maxPolls = aTimeout ? aTimeout / 100 : 50; + + async function putAndVerify(operationFn, validatorFn, flavor, expectFailure) { + await operationFn(); + + let data; + for (let i = 0; i < maxPolls; i++) { + data = SpecialPowers.getClipboardData(flavor); + if (validatorFn(data)) { + // Don't show the success message when waiting for preExpectedVal + if (preExpectedVal) { + preExpectedVal = null; + } else { + SimpleTest.ok( + !expectFailure, + "Clipboard has the given value: '" + data + "'" + ); + } + + return data; + } + + // Wait 100ms and check again. + await new Promise(resolve => { + SimpleTest._originalSetTimeout.apply(window, [resolve, 100]); + }); + } + + let errorMsg = `Timed out while polling clipboard for ${ + preExpectedVal ? "initialized" : "requested" + } data, got: ${data}`; + SimpleTest.ok(expectFailure, errorMsg); + if (!expectFailure) { + throw new Error(errorMsg); + } + return data; + } + + if (!aExpectFailure || !aDontInitializeClipboardIfExpectFailure) { + // First we wait for a known value different from the expected one. + SimpleTest.info(`Initializing clipboard with "${preExpectedVal}"...`); + await putAndVerify( + function() { + SpecialPowers.clipboardCopyString(preExpectedVal); + }, + function(aData) { + return aData == preExpectedVal; + }, + "text/unicode", + false + ); + + SimpleTest.info( + "Succeeded initializing clipboard, start requested things..." + ); + } else { + preExpectedVal = null; + } + + return putAndVerify( + aSetupFn, + inputValidatorFn, + requestedFlavor, + aExpectFailure + ); +}; + +/** + * Wait for a condition for a while (actually up to 3s here). + * + * @param {Function} aCond + * A function returns the result of the condition + * @param {Function} aCallback + * A function called after the condition is passed or timeout. + * @param {String} aErrorMsg + * The message displayed when the condition failed to pass + * before timeout. + */ +SimpleTest.waitForCondition = function(aCond, aCallback, aErrorMsg) { + this.promiseWaitForCondition(aCond, aErrorMsg).then(() => aCallback()); +}; +SimpleTest.promiseWaitForCondition = async function(aCond, aErrorMsg) { + for (let tries = 0; tries < 30; ++tries) { + // Wait 100ms between checks. + await new Promise(resolve => { + SimpleTest._originalSetTimeout.apply(window, [resolve, 100]); + }); + + let conditionPassed; + try { + conditionPassed = await aCond(); + } catch (e) { + ok(false, `${e}\n${e.stack}`); + conditionPassed = false; + } + if (conditionPassed) { + return; + } + } + ok(false, aErrorMsg); +}; + +/** + * Executes a function shortly after the call, but lets the caller continue + * working (or finish). + * + * @param {Function} aFunc + * Function to execute soon. + */ +SimpleTest.executeSoon = function(aFunc) { + if ("SpecialPowers" in window) { + return SpecialPowers.executeSoon(aFunc, window); + } + setTimeout(aFunc, 0); + return null; // Avoid warning. +}; + +/** + * Register a cleanup/teardown function (which may be async) to run after all + * tasks have finished, before running the next test. If async (or the function + * returns a promise), the framework will wait for the promise/async function + * to resolve. + * + * @param {Function} aFunc + * The cleanup/teardown function to run. + */ +SimpleTest.registerCleanupFunction = function(aFunc) { + SimpleTest._cleanupFunctions.push(aFunc); +}; + +SimpleTest.registerTimeoutFunction = function(aFunc) { + SimpleTest._timeoutFunctions.push(aFunc); +}; + +SimpleTest.testInChaosMode = function() { + if (SimpleTest._inChaosMode) { + // It's already enabled for this test, don't enter twice + return; + } + SpecialPowers.DOMWindowUtils.enterChaosMode(); + SimpleTest._inChaosMode = true; + // increase timeout here as chaosmode is very slow (i.e. 10x) + // doing 20x as this overwrites anything the tests set + SimpleTest.requestLongerTimeout(20); +}; + +SimpleTest.timeout = async function() { + for (const func of SimpleTest._timeoutFunctions) { + await func(); + } + SimpleTest._timeoutFunctions = []; +}; + +SimpleTest.finishWithFailure = function(msg) { + SimpleTest.ok(false, msg); + SimpleTest.finish(); +}; + +/** + * Finishes the tests. This is automatically called, except when + * SimpleTest.waitForExplicitFinish() has been invoked. + **/ +SimpleTest.finish = function() { + if (SimpleTest._alreadyFinished) { + var err = + "TEST-UNEXPECTED-FAIL | SimpleTest | this test already called finish!"; + if (parentRunner) { + parentRunner.structuredLogger.error(err); + } else { + dump(err + "\n"); + } + } + + if (SimpleTest.expected == "fail" && SimpleTest.num_failed <= 0) { + let msg = "We expected at least one failure"; + let test = { + result: false, + name: "fail-if condition in manifest", + diag: msg, + }; + let successInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + let failureInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + SimpleTest._logResult(test, successInfo, failureInfo); + SimpleTest._tests.push(test); + } else if (usesFailurePatterns()) { + SimpleTest.expected.forEach(([pat, expected_count], i) => { + let count = SimpleTest.num_failed[i]; + let diag; + if (expected_count === null && count == 0) { + diag = "expected some failures but got none"; + } else if (expected_count !== null && expected_count != count) { + diag = `expected ${expected_count} failures but got ${count}`; + } else { + return; + } + let name = pat + ? `failure pattern \`${pat}\` in this test` + : "failures in this test"; + let test = { result: false, name, diag }; + let successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + let failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + SimpleTest._logResult(test, successInfo, failureInfo); + SimpleTest._tests.push(test); + }); + } + + SimpleTest._timeoutFunctions = []; + + SimpleTest.testsLength = SimpleTest._tests.length; + + SimpleTest._alreadyFinished = true; + + if (SimpleTest._inChaosMode) { + SpecialPowers.DOMWindowUtils.leaveChaosMode(); + SimpleTest._inChaosMode = false; + } + + var afterCleanup = async function() { + SpecialPowers.removeFiles(); + + 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._tests.length) { + 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.)" + ); + } + + let workers = await SpecialPowers.registeredServiceWorkers(); + let promise = null; + if (SimpleTest._expectingRegisteredServiceWorker) { + if (workers.length === 0) { + SimpleTest.ok( + false, + "This test is expected to leave a service worker registered" + ); + } + } else if (workers.length) { + let FULL_PROFILE_WORKERS_TO_IGNORE = []; + if (parentRunner.conditionedProfile) { + // Full profile has service workers in the profile, without clearing the profile + // service workers will be leftover, in all my testing youtube is the only one. + FULL_PROFILE_WORKERS_TO_IGNORE = ["https://www.youtube.com/sw.js"]; + } else { + SimpleTest.ok( + false, + "This test left a service worker registered without cleaning it up" + ); + } + + for (let worker of workers) { + if (FULL_PROFILE_WORKERS_TO_IGNORE.includes(worker.scriptSpec)) { + continue; + } + SimpleTest.ok( + false, + `Left over worker: ${worker.scriptSpec} (scope: ${worker.scope})` + ); + } + promise = SpecialPowers.removeAllServiceWorkerData(); + } + + // If we want to wait for removeAllServiceWorkerData to finish, above, + // there's a small chance that spinning the event loop could cause + // SpecialPowers and SimpleTest to go away (e.g. if the test did + // document.open). promise being non-null should be rare (a test would + // have had to already fail by leaving a service worker around), so + // limit the chances of the async wait happening to that case. + function finish() { + if (parentRunner) { + /* We're running in an iframe, and the parent has a TestRunner */ + parentRunner.testFinished(SimpleTest._tests); + } + + if (!parentRunner || parentRunner.showTestReport) { + SpecialPowers.flushPermissions(function() { + SpecialPowers.flushPrefEnv(function() { + SimpleTest.showReport(); + }); + }); + } + } + + if (promise) { + promise.then(finish); + } else { + finish(); + } + }; + + var executeCleanupFunction = function() { + var func = SimpleTest._cleanupFunctions.pop(); + + if (!func) { + afterCleanup(); + return; + } + + var ret; + try { + ret = func(); + } catch (ex) { + SimpleTest.ok(false, "Cleanup function threw exception: " + ex); + } + + if (ret && ret.constructor.name == "Promise") { + ret.then(executeCleanupFunction, ex => + SimpleTest.ok(false, "Cleanup promise rejected: " + ex) + ); + } else { + executeCleanupFunction(); + } + }; + + executeCleanupFunction(); + + SpecialPowers.notifyObservers(null, "test-complete"); +}; + +/** + * 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: 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 (var 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) { + let 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; + var assertionLabel = JSON.stringify(msgs); + function listener(msg) { + if (msg.message === "SENTINEL" && !msg.isScriptError) { + is( + counter, + msgs.length, + "monitorConsole | number of messages " + assertionLabel + ); + SimpleTest.executeSoon(continuation); + return; + } + for (let 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; +}; + +/** + * Indicates to the test framework that this test is expected to leave a + * service worker registered when it finishes. + */ +SimpleTest.expectRegisteredServiceWorker = function() { + SimpleTest._expectingRegisteredServiceWorker = true; +}; + +/** + * 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._expectingRegisteredServiceWorker = 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._deepCheck = function(e1, e2, stack, seen) { + var ok = false; + if (Object.is(e1, e2)) { + // Handles identical primitives and references. + ok = true; + } else if ( + typeof e1 != "object" || + typeof e2 != "object" || + e1 === null || + e2 === null + ) { + // If either argument is a primitive or function, don't consider the arguments the same. + ok = false; + } else if (e1 == SimpleTest.DNE || e2 == SimpleTest.DNE) { + ok = false; + } else if (SimpleTest.isa(e1, "Array") && SimpleTest.isa(e2, "Array")) { + ok = SimpleTest._eqArray(e1, e2, stack, seen); + } else { + ok = SimpleTest._eqAssoc(e1, e2, stack, seen); + } + 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 = Math.max(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 ? a1[i] : SimpleTest.DNE; + var e2 = i < a2.length ? a2[i] : SimpleTest.DNE; + 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 (let 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; + // eslint-disable-next-line no-unused-vars + for (let i in o1) { + o1Size++; + } + var o2Size = 0; + // eslint-disable-next-line no-unused-vars + for (let i in o2) { + o2Size++; + } + var bigger = o1Size > o2Size ? o1 : o2; + for (let i in bigger) { + var e1 = i in o1 ? o1[i] : SimpleTest.DNE; + var e2 = i in o2 ? o2[i] : SimpleTest.DNE; + 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 (let i = 0; i < stack.length; i++) { + var entry = stack[i]; + var type = entry.type; + var idx = entry.idx; + if (idx != null) { + if (type == "Array") { + // 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 (let i = 0; i < vals.length; i++) { + var val = vals[i]; + if (val === SimpleTest.DNE) { + val = "Does not exist"; + } else { + val = repr(val); + } + out += vars[i] + " = " + val + SimpleTest.LF; + } + + return " " + out; +}; + +SimpleTest.isDeeply = function(it, as, name) { + var stack = [{ vals: [it, as] }]; + var seen = []; + if (SimpleTest._deepCheck(it, as, stack, seen)) { + SimpleTest.record(true, name); + } else { + SimpleTest.record(false, name, SimpleTest._formatStack(stack)); + } +}; + +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 record = SimpleTest.record; +var is = SimpleTest.is; +var isfuzzy = SimpleTest.isfuzzy; +var isnot = SimpleTest.isnot; +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, + columnNumber, + originalException +) { + // 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 "; + try { + error += originalException.stack; + } catch (e) { + // At least use the url+line+column we were given + error += url + ":" + lineNumber + ":" + columnNumber; + } + if (!SimpleTest._ignoringAllUncaughtExceptions) { + // Don't log if SimpleTest.finish() is already called, it would cause failures + if (!SimpleTest._alreadyFinished) { + SimpleTest.record(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 && !SimpleTest._alreadyFinished) { + // Need to finish() manually here, yet let the test actually end first. + SimpleTest.executeSoon(SimpleTest.finish); + } +}; + +// Lifted from dom/media/test/manifest.js +// Make sure to not touch navigator in here, since we want to push prefs that +// will affect the APIs it exposes, but the set of exposed APIs is determined +// when Navigator.prototype is created. So if we touch navigator before pushing +// the prefs, the APIs it exposes will not take those prefs into account. We +// work around this by using a navigator object from a different global for our +// UA string testing. +var gAndroidSdk = null; +function getAndroidSdk() { + if (gAndroidSdk === null) { + var iframe = document.documentElement.appendChild( + document.createElement("iframe") + ); + iframe.style.display = "none"; + var nav = iframe.contentWindow.navigator; + if ( + !nav.userAgent.includes("Mobile") && + !nav.userAgent.includes("Tablet") + ) { + gAndroidSdk = -1; + } else { + // See nsSystemInfo.cpp, the getProperty('version') returns different value + // on each platforms, so we need to distinguish the android platform. + var versionString = nav.userAgent.includes("Android") + ? "version" + : "sdk_version"; + gAndroidSdk = SpecialPowers.Services.sysinfo.getProperty(versionString); + } + document.documentElement.removeChild(iframe); + } + return gAndroidSdk; +} + +// add_task(generatorFunction): +// Call `add_task(generatorFunction)` for each separate +// asynchronous task in a mochitest. Tasks are run consecutively. +// Before the first task, `SimpleTest.waitForExplicitFinish()` +// will be called automatically, and after the last task, +// `SimpleTest.finish()` will be called. +var add_task = (function() { + // The list of tasks to run. + var task_list = []; + var run_only_this_task = null; + + function isGenerator(value) { + return ( + value && typeof value === "object" && typeof value.next === "function" + ); + } + + // The "add_task" function + return function(generatorFunction, options = { isSetup: false }) { + if (task_list.length === 0) { + // This is the first time add_task has been called. + // First, confirm that SimpleTest is available. + if (!SimpleTest) { + throw new Error("SimpleTest not available."); + } + // Don't stop tests until asynchronous tasks are finished. + SimpleTest.waitForExplicitFinish(); + // Because the client is using add_task for this set of tests, + // we need to spawn a "master task" that calls each task in succesion. + // Use setTimeout to ensure the master task runs after the client + // script finishes. + setTimeout(function nextTick() { + // If we are in a HTML document, we should wait for the document + // to be fully loaded. + // These checks ensure that we are in an HTML document without + // throwing TypeError; also I am told that readyState in XUL documents + // are totally bogus so we don't try to do this there. + if ( + typeof window !== "undefined" && + typeof HTMLDocument !== "undefined" && + window.document instanceof HTMLDocument && + window.document.readyState !== "complete" + ) { + setTimeout(nextTick); + return; + } + + (async () => { + // Allow for a task to be skipped; we need only use the structured logger + // for this, whilst deactivating log buffering to ensure that messages + // are always printed to stdout. + function skipTask(name) { + let logger = parentRunner && parentRunner.structuredLogger; + if (!logger) { + info("add_task | Skipping test " + name); + return; + } + logger.deactivateBuffering(); + logger.testStatus(SimpleTest._getCurrentTestURL(), name, "SKIP"); + logger.warning("add_task | Skipping test " + name); + logger.activateBuffering(); + } + + // We stop the entire test file at the first exception because this + // may mean that the state of subsequent tests may be corrupt. + try { + for (var task of task_list) { + var name = task.name || ""; + if ( + task.__skipMe || + (run_only_this_task && task != run_only_this_task) + ) { + skipTask(name); + continue; + } + const taskInfo = action => + info( + `${ + task.isSetup ? "add_setup" : "add_task" + } | ${action} ${name}` + ); + taskInfo("Entering"); + let result = await task(); + if (isGenerator(result)) { + ok(false, "Task returned a generator"); + } + taskInfo("Leaving"); + } + } catch (ex) { + try { + let serializedEx; + if (ex instanceof Error) { + serializedEx = `${ex}`; + } else { + serializedEx = JSON.stringify(ex); + } + + SimpleTest.record( + false, + serializedEx, + "Should not throw any errors", + ex.stack + ); + } catch (ex2) { + SimpleTest.record( + false, + "(The exception cannot be converted to string.)", + "Should not throw any errors", + ex.stack + ); + } + } + // All tasks are finished. + SimpleTest.finish(); + })(); + }); + } + generatorFunction.skip = () => (generatorFunction.__skipMe = true); + generatorFunction.only = () => (run_only_this_task = generatorFunction); + // Add the task to the list of tasks to run after + // the main thread is finished. + if (options.isSetup) { + generatorFunction.isSetup = true; + let lastSetupIndex = task_list.findLastIndex(t => t.isSetup) + 1; + task_list.splice(lastSetupIndex, 0, generatorFunction); + } else { + task_list.push(generatorFunction); + } + return generatorFunction; + }; +})(); + +// Like add_task, but setup tasks are executed first. +function add_setup(generatorFunction) { + return add_task(generatorFunction, { isSetup: true }); +} + +// Request complete log when using failure patterns so that failure info +// from infra can be useful. +if (usesFailurePatterns()) { + SimpleTest.requestCompleteLog(); +} + +addEventListener("message", async event => { + if (event.data == "SimpleTest:timeout") { + await SimpleTest.timeout(); + SimpleTest.finish(); + } +}); |