diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/tests/browser.js | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | js/src/tests/browser.js | 653 |
1 files changed, 653 insertions, 0 deletions
diff --git a/js/src/tests/browser.js b/js/src/tests/browser.js new file mode 100644 index 0000000000..2c0dd5db8c --- /dev/null +++ b/js/src/tests/browser.js @@ -0,0 +1,653 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// NOTE: If you're adding new test harness functionality to this file -- first, +// should you at all? Most stuff is better in specific tests, or in +// nested shell.js/browser.js. Second, can you instead add it to +// shell.js? Our goal is to unify these two files for readability, and +// the plan is to empty out this file into that one over time. Third, +// supposing you must add to this file, please add it to this IIFE for +// better modularity/resilience against tests that must do particularly +// bizarre things that might break the harness. + +(function initializeUtilityExports(global, parent) { + "use strict"; + + /********************************************************************** + * CACHED PRIMORDIAL FUNCTIONALITY (before a test might overwrite it) * + **********************************************************************/ + + var Error = global.Error; + var String = global.String; + var GlobalEval = global.eval; + var ReflectApply = global.Reflect.apply; + var FunctionToString = global.Function.prototype.toString; + var ObjectDefineProperty = global.Object.defineProperty; + + // BEWARE: ObjectGetOwnPropertyDescriptor is only safe to use if its result + // is inspected using own-property-examining functionality. Directly + // accessing properties on a returned descriptor without first + // verifying the property's existence can invoke user-modifiable + // behavior. + var ObjectGetOwnPropertyDescriptor = global.Object.getOwnPropertyDescriptor; + + var Worker = global.Worker; + var Blob = global.Blob; + var URL = global.URL; + + var document = global.document; + var documentAll = global.document.all; + var documentDocumentElement = global.document.documentElement; + var DocumentCreateElement = global.document.createElement; + + var EventTargetPrototypeAddEventListener = global.EventTarget.prototype.addEventListener; + var HTMLElementPrototypeStyleSetter = + ObjectGetOwnPropertyDescriptor(global.HTMLElement.prototype, "style").set; + var HTMLIFramePrototypeContentWindowGetter = + ObjectGetOwnPropertyDescriptor(global.HTMLIFrameElement.prototype, "contentWindow").get; + var HTMLScriptElementTextSetter = + ObjectGetOwnPropertyDescriptor(global.HTMLScriptElement.prototype, "text").set; + var NodePrototypeAppendChild = global.Node.prototype.appendChild; + var NodePrototypeRemoveChild = global.Node.prototype.removeChild; + var {get: WindowOnErrorGetter, set: WindowOnErrorSetter} = + ObjectGetOwnPropertyDescriptor(global, "onerror"); + var WorkerPrototypePostMessage = Worker.prototype.postMessage; + var URLCreateObjectURL = URL.createObjectURL; + + // List of saved window.onerror handlers. + var savedGlobalOnError = []; + + // Set |newOnError| as the current window.onerror handler. + function setGlobalOnError(newOnError) { + var currentOnError = ReflectApply(WindowOnErrorGetter, global, []); + ArrayPush(savedGlobalOnError, currentOnError); + ReflectApply(WindowOnErrorSetter, global, [newOnError]); + } + + // Restore the previous window.onerror handler. + function restoreGlobalOnError() { + var previousOnError = ArrayPop(savedGlobalOnError); + ReflectApply(WindowOnErrorSetter, global, [previousOnError]); + } + + /**************************** + * GENERAL HELPER FUNCTIONS * + ****************************/ + + function ArrayPush(array, value) { + ReflectApply(ObjectDefineProperty, null, [ + array, array.length, + {__proto__: null, value, writable: true, enumerable: true, configurable: true} + ]); + } + + function ArrayPop(array) { + if (array.length) { + var item = array[array.length - 1]; + array.length -= 1; + return item; + } + } + + function AppendChild(elt, kid) { + ReflectApply(NodePrototypeAppendChild, elt, [kid]); + } + + function CreateElement(name) { + return ReflectApply(DocumentCreateElement, document, [name]); + } + + function RemoveChild(elt, kid) { + ReflectApply(NodePrototypeRemoveChild, elt, [kid]); + } + + function CreateWorker(script) { + var blob = new Blob([script], {__proto__: null, type: "text/javascript"}); + return new Worker(URLCreateObjectURL(blob)); + } + + /**************************** + * UTILITY FUNCTION EXPORTS * + ****************************/ + + var evaluate = global.evaluate; + if (typeof evaluate !== "function") { + // Shim in "evaluate". + evaluate = function evaluate(code) { + if (typeof code !== "string") + throw Error("Expected string argument for evaluate()"); + + return GlobalEval(code); + }; + + global.evaluate = evaluate; + } + + var evaluateScript = global.evaluateScript; + if (typeof evaluateScript !== "function") { + evaluateScript = function evaluateScript(code) { + code = String(code); + var script = CreateElement("script"); + + // Temporarily install a new onerror handler to catch script errors. + var hasUncaughtError = false; + var uncaughtError; + var eventOptions = {__proto__: null, once: true}; + ReflectApply(EventTargetPrototypeAddEventListener, script, [ + "beforescriptexecute", function() { + setGlobalOnError(function(messageOrEvent, source, lineno, colno, error) { + hasUncaughtError = true; + uncaughtError = error; + return true; + }); + }, eventOptions + ]); + ReflectApply(EventTargetPrototypeAddEventListener, script, [ + "afterscriptexecute", function() { + restoreGlobalOnError(); + }, eventOptions + ]); + + ReflectApply(HTMLScriptElementTextSetter, script, [code]); + AppendChild(documentDocumentElement, script); + RemoveChild(documentDocumentElement, script); + + if (hasUncaughtError) + throw uncaughtError; + }; + + global.evaluateScript = evaluateScript; + } + + var newGlobal = global.newGlobal; + if (typeof newGlobal !== "function") { + // Reuse the parent's newGlobal to ensure iframes can be added to the DOM. + newGlobal = parent ? parent.newGlobal : function newGlobal() { + var iframe = CreateElement("iframe"); + AppendChild(documentDocumentElement, iframe); + var win = + ReflectApply(HTMLIFramePrototypeContentWindowGetter, iframe, []); + + // Removing the iframe breaks evaluateScript() and detachArrayBuffer(). + ReflectApply(HTMLElementPrototypeStyleSetter, iframe, ["display:none"]); + + // Create utility functions in the new global object. + var initFunction = ReflectApply(FunctionToString, initializeUtilityExports, []); + win.Function("parent", initFunction + "; initializeUtilityExports(this, parent);")(global); + + return win; + }; + + global.newGlobal = newGlobal; + } + + var detachArrayBuffer = global.detachArrayBuffer; + if (typeof detachArrayBuffer !== "function") { + var worker = null; + detachArrayBuffer = function detachArrayBuffer(arrayBuffer) { + if (worker === null) { + worker = CreateWorker("/* black hole */"); + } + ReflectApply(WorkerPrototypePostMessage, worker, ["detach", [arrayBuffer]]); + }; + + global.detachArrayBuffer = detachArrayBuffer; + } + + var createIsHTMLDDA = global.createIsHTMLDDA; + if (typeof createIsHTMLDDA !== "function") { + createIsHTMLDDA = function() { + return documentAll; + }; + + global.createIsHTMLDDA = createIsHTMLDDA; + } +})(this); + +(function(global) { + "use strict"; + + /********************************************************************** + * CACHED PRIMORDIAL FUNCTIONALITY (before a test might overwrite it) * + **********************************************************************/ + + var undefined; // sigh + + var Error = global.Error; + var Number = global.Number; + var Object = global.Object; + var String = global.String; + + var decodeURIComponent = global.decodeURIComponent; + var ReflectApply = global.Reflect.apply; + var ObjectDefineProperty = Object.defineProperty; + var ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; + var ObjectPrototypeIsPrototypeOf = Object.prototype.isPrototypeOf; + + // BEWARE: ObjectGetOwnPropertyDescriptor is only safe to use if its result + // is inspected using own-property-examining functionality. Directly + // accessing properties on a returned descriptor without first + // verifying the property's existence can invoke user-modifiable + // behavior. + var ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + var window = global.window; + var document = global.document; + var documentDocumentElement = document.documentElement; + var DocumentCreateElement = document.createElement; + var ElementSetClassName = + ObjectGetOwnPropertyDescriptor(global.Element.prototype, "className").set; + var NodePrototypeAppendChild = global.Node.prototype.appendChild; + var NodePrototypeTextContentSetter = + ObjectGetOwnPropertyDescriptor(global.Node.prototype, "textContent").set; + var setTimeout = global.setTimeout; + + // Saved harness functions. + var dump = global.dump; + var gczeal = global.gczeal; + var print = global.print; + var reportFailure = global.reportFailure; + var TestCase = global.TestCase; + + var SpecialPowers = global.SpecialPowers; + var SpecialPowersCu = SpecialPowers.Cu; + var SpecialPowersForceGC = SpecialPowers.forceGC; + var TestingFunctions = SpecialPowers.Cu.getJSTestingFunctions(); + var ClearKeptObjects = TestingFunctions.clearKeptObjects; + + // Cached DOM nodes used by the test harness itself. (We assume the test + // doesn't misbehave in a way that actively interferes with what the test + // harness runner observes, e.g. navigating the page to a different location. + // Short of running every test in a worker -- which has its own problems -- + // there's no way to isolate a test from the page to that extent.) + var printOutputContainer = + global.document.getElementById("jsreftest-print-output-container"); + + /**************************** + * GENERAL HELPER FUNCTIONS * + ****************************/ + + function ArrayPush(array, value) { + ReflectApply(ObjectDefineProperty, null, [ + array, array.length, + {__proto__: null, value, writable: true, enumerable: true, configurable: true} + ]); + } + + function ArrayPop(array) { + if (array.length) { + var item = array[array.length - 1]; + array.length -= 1; + return item; + } + } + + function HasOwnProperty(object, property) { + return ReflectApply(ObjectPrototypeHasOwnProperty, object, [property]); + } + + function AppendChild(elt, kid) { + ReflectApply(NodePrototypeAppendChild, elt, [kid]); + } + + function CreateElement(name) { + return ReflectApply(DocumentCreateElement, document, [name]); + } + + function SetTextContent(element, text) { + ReflectApply(NodePrototypeTextContentSetter, element, [text]); + } + + // Object containing the set options. + var currentOptions; + + // browser.js version of shell.js' |shellOptionsClear| function. + function browserOptionsClear() { + for (var optionName in currentOptions) { + delete currentOptions[optionName]; + SpecialPowersCu[optionName] = false; + } + } + + // This function is *only* used by shell.js's for-browsers |print()| function! + // It's only defined/exported here because it needs CreateElement and friends, + // only defined here, and we're not yet ready to move them to shell.js. + function AddPrintOutput(s) { + var msgDiv = CreateElement("div"); + SetTextContent(msgDiv, s); + AppendChild(printOutputContainer, msgDiv); + } + global.AddPrintOutput = AddPrintOutput; + + /************************************************************************* + * HARNESS-CENTRIC EXPORTS (we should generally work to eliminate these) * + *************************************************************************/ + + // This overwrites shell.js's version that merely prints the given string. + function writeHeaderToLog(string) { + string = String(string); + + // First dump to the console. + dump(string + "\n"); + + // Then output to the page. + var h2 = CreateElement("h2"); + SetTextContent(h2, string); + AppendChild(printOutputContainer, h2); + } + global.writeHeaderToLog = writeHeaderToLog; + + /************************* + * GLOBAL ERROR HANDLING * + *************************/ + + // Possible values: + // - "Unknown" if no error is expected, + // - "error" if no specific error type is expected, + // - otherwise the error name, e.g. "TypeError" or "RangeError". + var expectedError; + + window.onerror = function (msg, page, line, column, error) { + // Unset all options even when the test finished with an error. + browserOptionsClear(); + + if (DESCRIPTION === undefined) { + DESCRIPTION = "Unknown"; + } + + var actual = "error"; + var expected = expectedError; + if (expected !== "error" && expected !== "Unknown") { + // Check the error type when an actual Error object is available. + // NB: The |error| parameter of the onerror handler is not required to + // be an Error instance. + if (ReflectApply(ObjectPrototypeIsPrototypeOf, Error.prototype, [error])) { + actual = error.constructor.name; + } else { + expected = "error"; + } + } + + var reason = `${page}:${line}: ${msg}`; + new TestCase(DESCRIPTION, expected, actual, reason); + + reportFailure(msg); + }; + + /********************************************** + * BROWSER IMPLEMENTATION FOR SHELL FUNCTIONS * + **********************************************/ + + function gc() { + try { + SpecialPowersForceGC(); + } catch (ex) { + print("gc: " + ex); + } + } + global.gc = gc; + + global.clearKeptObjects = ClearKeptObjects; + + function options(aOptionName) { + // return value of options() is a comma delimited list + // of the previously set values + + var value = ""; + for (var optionName in currentOptions) { + if (value) + value += ","; + value += optionName; + } + + if (aOptionName) { + if (!HasOwnProperty(SpecialPowersCu, aOptionName)) { + // This test is trying to flip an unsupported option, so it's + // likely no longer testing what it was supposed to. Fail it + // hard. + throw "Unsupported JSContext option '" + aOptionName + "'"; + } + + if (aOptionName in currentOptions) { + // option is set, toggle it to unset + delete currentOptions[aOptionName]; + SpecialPowersCu[aOptionName] = false; + } else { + // option is not set, toggle it to set + currentOptions[aOptionName] = true; + SpecialPowersCu[aOptionName] = true; + } + } + + return value; + } + global.options = options; + + /**************************************** + * HARNESS SETUP AND TEARDOWN FUNCTIONS * + ****************************************/ + + function jsTestDriverBrowserInit() { + // Unset all options before running any test code, cf. the call to + // |shellOptionsClear| in shell.js' set-up code. + for (var optionName of ["strict_mode"]) { + if (!HasOwnProperty(SpecialPowersCu, optionName)) + throw "options is out of sync with Components.utils"; + + // Option is set, toggle it to unset. (Reading an option is a cheap + // operation, but setting is relatively expensive, so only assign if + // necessary.) + if (SpecialPowersCu[optionName]) + SpecialPowersCu[optionName] = false; + } + + // Initialize with an empty set, because we just turned off all options. + currentOptions = Object.create(null); + + if (document.location.search.indexOf("?") !== 0) { + // not called with a query string + return; + } + + var properties = Object.create(null); + var fields = document.location.search.slice(1).split(";"); + for (var i = 0; i < fields.length; i++) { + var propertycaptures = /^([^=]+)=(.*)$/.exec(fields[i]); + if (propertycaptures === null) { + properties[fields[i]] = true; + } else { + properties[propertycaptures[1]] = decodeURIComponent(propertycaptures[2]); + } + } + + global.gTestPath = properties.test; + + var testpathparts = properties.test.split("/"); + if (testpathparts.length < 2) { + // must have at least suitepath/testcase.js + return; + } + + var testFileName = testpathparts[testpathparts.length - 1]; + + if (testFileName.endsWith("-n.js")) { + // Negative test without a specific error type. + expectedError = "error"; + } else if (properties.error) { + // Negative test which expects a specific error type. + expectedError = properties.error; + } else { + // No error is expected. + expectedError = "Unknown"; + } + + if (properties.gczeal) { + gczeal(Number(properties.gczeal)); + } + + // Display the test path in the title. + document.title = properties.test; + + // Output script tags for shell.js, then browser.js, at each level of the + // test path hierarchy. + var prepath = ""; + var scripts = []; + for (var i = 0; i < testpathparts.length - 1; i++) { + prepath += testpathparts[i] + "/"; + + scripts.push({src: prepath + "shell.js", module: false}); + scripts.push({src: prepath + "browser.js", module: false}); + } + + // Output the test script itself. + var moduleTest = !!properties.module; + scripts.push({src: prepath + testFileName, module: moduleTest}); + + // Finally output the driver-end script to advance to the next test. + scripts.push({src: "js-test-driver-end.js", module: false}); + + if (properties.async) { + gDelayTestDriverEnd = true; + } + + if (!moduleTest) { + for (var i = 0; i < scripts.length; i++) { + var src = scripts[i].src; + document.write(`<script src="${src}" charset="utf-8"><\/script>`); + } + } else { + // Modules are loaded asynchronously by default, but for the test harness + // we need to execute all scripts and modules one after the other. + + // Appends the next script element to the DOM. + function appendScript(index) { + var script = scriptElements[index]; + scriptElements[index] = null; + if (script !== null) { + ReflectApply(NodePrototypeAppendChild, documentDocumentElement, [script]); + } + } + + // Create all script elements upfront, so we don't need to worry about + // modified built-ins. + var scriptElements = []; + for (var i = 0; i < scripts.length; i++) { + var spec = scripts[i]; + + var script = document.createElement("script"); + script.charset = "utf-8"; + if (spec.module) { + script.type = "module"; + } + script.src = spec.src; + + let nextScriptIndex = i + 1; + if (nextScriptIndex < scripts.length) { + var callNextAppend = () => appendScript(nextScriptIndex); + script.addEventListener("afterscriptexecute", callNextAppend, {once: true}); + + // Module scripts don't fire the "afterscriptexecute" event when there + // was an error, instead the "error" event is emitted. So listen for + // both events when creating module scripts. + if (spec.module) { + script.addEventListener("error", callNextAppend, {once: true}); + } + } + + scriptElements[i] = script; + } + + // Append the first script. + appendScript(0); + } + } + + global.gDelayTestDriverEnd = false; + + function jsTestDriverEnd() { + // gDelayTestDriverEnd is used to delay collection of the test result and + // signal to Spider so that tests can continue to run after page load has + // fired. They are responsible for setting gDelayTestDriverEnd = true then + // when completed, setting gDelayTestDriverEnd = false then calling + // jsTestDriverEnd() + + if (gDelayTestDriverEnd) { + return; + } + + window.onerror = null; + + // Unset all options when the test has finished. + browserOptionsClear(); + + if (window.opener && window.opener.runNextTest) { + if (window.opener.reportCallBack) { + window.opener.reportCallBack(window.opener.gWindow); + } + + setTimeout("window.opener.runNextTest()", 250); + } else { + // tell reftest the test is complete. + ReflectApply(ElementSetClassName, documentDocumentElement, [""]); + // tell Spider page is complete + gPageCompleted = true; + } + } + global.jsTestDriverEnd = jsTestDriverEnd; + + /*************************************************************************** + * DIALOG CLOSER, PRESUMABLY TO CLOSE SLOW SCRIPT DIALOGS AND OTHER POPUPS * + ***************************************************************************/ + + // dialog closer from http://bclary.com/projects/spider/spider/chrome/content/spider/dialog-closer.js + + // Use an array to handle the case where multiple dialogs appear at one time. + var dialogCloserSubjects = []; + var dialogCloser = SpecialPowers + .Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(SpecialPowers.Ci.nsIWindowWatcher); + var dialogCloserObserver = { + observe(subject, topic, data) { + if (topic === "domwindowopened" && subject.isChromeWindow) { + ArrayPush(dialogCloserSubjects, subject); + + // Timeout of 0 needed when running under reftest framework. + subject.setTimeout(closeDialog, 0); + } + } + }; + + function closeDialog() { + while (dialogCloserSubjects.length > 0) { + var subject = ArrayPop(dialogCloserSubjects); + subject.close(); + } + } + + function unregisterDialogCloser() { + gczeal(0); + + if (!dialogCloserObserver || !dialogCloser) { + return; + } + + dialogCloser.unregisterNotification(dialogCloserObserver); + + dialogCloserObserver = null; + dialogCloser = null; + } + + dialogCloser.registerNotification(dialogCloserObserver); + window.addEventListener("unload", unregisterDialogCloser, true); + + /******************************************* + * RUN ONCE CODE TO SETUP ADDITIONAL STATE * + *******************************************/ + + jsTestDriverBrowserInit(); + +})(this); + +var gPageCompleted; |