/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ /* vim:set ts=2 sw=2 sts=2 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); // Note: duplicated in browser-test.js . See also bug 1820150. function isErrorOrException(err) { // It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)` // but we don't, so do it ourselves: if (!err) { return false; } if (err instanceof Ci.nsIException) { return true; } try { let glob = SpecialPowers.Cu.getGlobalForObject(err); return err instanceof glob.Error; } catch { // getGlobalForObject can be upset if it doesn't get passed an object. // Just do a standard instanceof check using this global and cross fingers: } return err instanceof Error; } // 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 /