browser.js (21890B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 // NOTE: If you're adding new test harness functionality to this file -- first, 7 // should you at all? Most stuff is better in specific tests, or in 8 // nested shell.js/browser.js. Second, can you instead add it to 9 // shell.js? Our goal is to unify these two files for readability, and 10 // the plan is to empty out this file into that one over time. Third, 11 // supposing you must add to this file, please add it to this IIFE for 12 // better modularity/resilience against tests that must do particularly 13 // bizarre things that might break the harness. 14 15 (function initializeUtilityExports(global, parent) { 16 "use strict"; 17 18 /********************************************************************** 19 * CACHED PRIMORDIAL FUNCTIONALITY (before a test might overwrite it) * 20 **********************************************************************/ 21 22 var Error = global.Error; 23 var String = global.String; 24 var GlobalEval = global.eval; 25 var ReflectApply = global.Reflect.apply; 26 var FunctionToString = global.Function.prototype.toString; 27 var ObjectDefineProperty = global.Object.defineProperty; 28 29 // BEWARE: ObjectGetOwnPropertyDescriptor is only safe to use if its result 30 // is inspected using own-property-examining functionality. Directly 31 // accessing properties on a returned descriptor without first 32 // verifying the property's existence can invoke user-modifiable 33 // behavior. 34 var ObjectGetOwnPropertyDescriptor = global.Object.getOwnPropertyDescriptor; 35 36 var {get: ArrayBufferByteLength} = 37 ObjectGetOwnPropertyDescriptor(global.ArrayBuffer.prototype, "byteLength"); 38 39 var Worker = global.Worker; 40 var Blob = global.Blob; 41 var URL = global.URL; 42 43 var document = global.document; 44 var documentDocumentElement = global.document.documentElement; 45 var DocumentCreateElement = global.document.createElement; 46 47 var DocumentPrototypeAllGetter = ObjectGetOwnPropertyDescriptor(global.Document.prototype, "all").get; 48 var EventTargetPrototypeAddEventListener = global.EventTarget.prototype.addEventListener; 49 var HTMLElementPrototypeStyleSetter = 50 ObjectGetOwnPropertyDescriptor(global.HTMLElement.prototype, "style").set; 51 var HTMLIFramePrototypeContentWindowGetter = 52 ObjectGetOwnPropertyDescriptor(global.HTMLIFrameElement.prototype, "contentWindow").get; 53 var HTMLScriptElementTextSetter = 54 ObjectGetOwnPropertyDescriptor(global.HTMLScriptElement.prototype, "text").set; 55 var NodePrototypeAppendChild = global.Node.prototype.appendChild; 56 var NodePrototypeRemoveChild = global.Node.prototype.removeChild; 57 var {get: WindowOnErrorGetter, set: WindowOnErrorSetter} = 58 ObjectGetOwnPropertyDescriptor(global, "onerror"); 59 var WorkerPrototypePostMessage = Worker.prototype.postMessage; 60 var URLCreateObjectURL = URL.createObjectURL; 61 62 // List of saved window.onerror handlers. 63 var savedGlobalOnError = []; 64 65 // Set |newOnError| as the current window.onerror handler. 66 function setGlobalOnError(newOnError) { 67 var currentOnError = ReflectApply(WindowOnErrorGetter, global, []); 68 ArrayPush(savedGlobalOnError, currentOnError); 69 ReflectApply(WindowOnErrorSetter, global, [newOnError]); 70 } 71 72 // Restore the previous window.onerror handler. 73 function restoreGlobalOnError() { 74 var previousOnError = ArrayPop(savedGlobalOnError); 75 ReflectApply(WindowOnErrorSetter, global, [previousOnError]); 76 } 77 78 /**************************** 79 * GENERAL HELPER FUNCTIONS * 80 ****************************/ 81 82 function ArrayPush(array, value) { 83 ReflectApply(ObjectDefineProperty, null, [ 84 array, array.length, 85 {__proto__: null, value, writable: true, enumerable: true, configurable: true} 86 ]); 87 } 88 89 function ArrayPop(array) { 90 if (array.length) { 91 var item = array[array.length - 1]; 92 array.length -= 1; 93 return item; 94 } 95 } 96 97 function AppendChild(elt, kid) { 98 ReflectApply(NodePrototypeAppendChild, elt, [kid]); 99 } 100 101 function CreateElement(name) { 102 return ReflectApply(DocumentCreateElement, document, [name]); 103 } 104 105 function RemoveChild(elt, kid) { 106 ReflectApply(NodePrototypeRemoveChild, elt, [kid]); 107 } 108 109 function CreateWorker(script) { 110 var blob = new Blob([script], {__proto__: null, type: "text/javascript"}); 111 return new Worker(URLCreateObjectURL(blob)); 112 } 113 114 /**************************** 115 * UTILITY FUNCTION EXPORTS * 116 ****************************/ 117 118 var evaluate = global.evaluate; 119 if (typeof evaluate !== "function") { 120 // Shim in "evaluate". 121 evaluate = function evaluate(code) { 122 if (typeof code !== "string") 123 throw Error("Expected string argument for evaluate()"); 124 125 return GlobalEval(code); 126 }; 127 128 global.evaluate = evaluate; 129 } 130 131 var evaluateScript = global.evaluateScript; 132 if (typeof evaluateScript !== "function") { 133 evaluateScript = function evaluateScript(code) { 134 code = String(code); 135 var script = CreateElement("script"); 136 137 // Temporarily install a new onerror handler to catch script errors. 138 var hasUncaughtError = false; 139 var uncaughtError; 140 141 setGlobalOnError(function(messageOrEvent, source, lineno, colno, error) { 142 hasUncaughtError = true; 143 uncaughtError = error; 144 return true; 145 }); 146 ReflectApply(HTMLScriptElementTextSetter, script, [code]); 147 AppendChild(documentDocumentElement, script); 148 RemoveChild(documentDocumentElement, script); 149 restoreGlobalOnError(); 150 151 if (hasUncaughtError) 152 throw uncaughtError; 153 }; 154 155 global.evaluateScript = evaluateScript; 156 } 157 158 var newGlobal = global.newGlobal; 159 if (typeof newGlobal !== "function") { 160 // Reuse the parent's newGlobal to ensure iframes can be added to the DOM. 161 newGlobal = parent ? parent.newGlobal : function newGlobal() { 162 var iframe = CreateElement("iframe"); 163 AppendChild(documentDocumentElement, iframe); 164 var win = 165 ReflectApply(HTMLIFramePrototypeContentWindowGetter, iframe, []); 166 167 // Removing the iframe breaks evaluateScript() and detachArrayBuffer(). 168 ReflectApply(HTMLElementPrototypeStyleSetter, iframe, ["display:none"]); 169 170 // Create utility functions in the new global object. 171 var initFunction = ReflectApply(FunctionToString, initializeUtilityExports, []); 172 win.Function("parent", initFunction + "; initializeUtilityExports(this, parent);")(global); 173 174 return win; 175 }; 176 177 global.newGlobal = newGlobal; 178 } 179 180 var detachArrayBuffer = global.detachArrayBuffer; 181 if (typeof detachArrayBuffer !== "function") { 182 var worker = null; 183 detachArrayBuffer = function detachArrayBuffer(arrayBuffer) { 184 if (worker === null) { 185 worker = CreateWorker("/* black hole */"); 186 } 187 try { 188 ReflectApply(WorkerPrototypePostMessage, worker, ["detach", [arrayBuffer]]); 189 } catch (e) { 190 // postMessage throws an error if the array buffer was already detached. 191 // Test for this condition by checking if the byte length is zero. 192 if (ReflectApply(ArrayBufferByteLength, arrayBuffer, []) !== 0) { 193 throw e; 194 } 195 } 196 }; 197 198 global.detachArrayBuffer = detachArrayBuffer; 199 } 200 201 var createIsHTMLDDA = global.createIsHTMLDDA; 202 if (typeof createIsHTMLDDA !== "function") { 203 createIsHTMLDDA = function() { 204 return ReflectApply(DocumentPrototypeAllGetter, document, []); 205 }; 206 207 global.createIsHTMLDDA = createIsHTMLDDA; 208 } 209 })(this); 210 211 (function(global) { 212 "use strict"; 213 214 /********************************************************************** 215 * CACHED PRIMORDIAL FUNCTIONALITY (before a test might overwrite it) * 216 **********************************************************************/ 217 218 var undefined; // sigh 219 220 var Error = global.Error; 221 var Number = global.Number; 222 var Object = global.Object; 223 var String = global.String; 224 225 var decodeURIComponent = global.decodeURIComponent; 226 var ReflectApply = global.Reflect.apply; 227 var ObjectDefineProperty = Object.defineProperty; 228 var ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; 229 var ObjectPrototypeIsPrototypeOf = Object.prototype.isPrototypeOf; 230 231 // BEWARE: ObjectGetOwnPropertyDescriptor is only safe to use if its result 232 // is inspected using own-property-examining functionality. Directly 233 // accessing properties on a returned descriptor without first 234 // verifying the property's existence can invoke user-modifiable 235 // behavior. 236 var ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; 237 238 var window = global.window; 239 var document = global.document; 240 var documentDocumentElement = document.documentElement; 241 var DocumentCreateElement = document.createElement; 242 var ElementSetClassName = 243 ObjectGetOwnPropertyDescriptor(global.Element.prototype, "className").set; 244 var NodePrototypeAppendChild = global.Node.prototype.appendChild; 245 var NodePrototypeTextContentSetter = 246 ObjectGetOwnPropertyDescriptor(global.Node.prototype, "textContent").set; 247 var setTimeout = global.setTimeout; 248 249 // Saved harness functions. 250 var dump = global.dump; 251 var gczeal = global.gczeal; 252 var print = global.print; 253 var reportFailure = global.reportFailure; 254 var TestCase = global.TestCase; 255 256 var SpecialPowers = global.SpecialPowers; 257 var SpecialPowersCu = SpecialPowers.Cu; 258 var SpecialPowersForceGC = SpecialPowers.forceGC; 259 var TestingFunctions = SpecialPowers.Cu.getJSTestingFunctions(); 260 var ClearKeptObjects = TestingFunctions.clearKeptObjects; 261 262 // Cached DOM nodes used by the test harness itself. (We assume the test 263 // doesn't misbehave in a way that actively interferes with what the test 264 // harness runner observes, e.g. navigating the page to a different location. 265 // Short of running every test in a worker -- which has its own problems -- 266 // there's no way to isolate a test from the page to that extent.) 267 var printOutputContainer = 268 global.document.getElementById("jsreftest-print-output-container"); 269 270 /**************************** 271 * GENERAL HELPER FUNCTIONS * 272 ****************************/ 273 274 function ArrayPush(array, value) { 275 ReflectApply(ObjectDefineProperty, null, [ 276 array, array.length, 277 {__proto__: null, value, writable: true, enumerable: true, configurable: true} 278 ]); 279 } 280 281 function ArrayPop(array) { 282 if (array.length) { 283 var item = array[array.length - 1]; 284 array.length -= 1; 285 return item; 286 } 287 } 288 289 function HasOwnProperty(object, property) { 290 return ReflectApply(ObjectPrototypeHasOwnProperty, object, [property]); 291 } 292 293 function AppendChild(elt, kid) { 294 ReflectApply(NodePrototypeAppendChild, elt, [kid]); 295 } 296 297 function CreateElement(name) { 298 return ReflectApply(DocumentCreateElement, document, [name]); 299 } 300 301 function SetTextContent(element, text) { 302 ReflectApply(NodePrototypeTextContentSetter, element, [text]); 303 } 304 305 // Object containing the set options. 306 var currentOptions; 307 308 // browser.js version of shell.js' |shellOptionsClear| function. 309 function browserOptionsClear() { 310 for (var optionName in currentOptions) { 311 delete currentOptions[optionName]; 312 SpecialPowersCu[optionName] = false; 313 } 314 } 315 316 // This function is *only* used by shell.js's for-browsers |print()| function! 317 // It's only defined/exported here because it needs CreateElement and friends, 318 // only defined here, and we're not yet ready to move them to shell.js. 319 function AddPrintOutput(s) { 320 var msgDiv = CreateElement("div"); 321 SetTextContent(msgDiv, s); 322 AppendChild(printOutputContainer, msgDiv); 323 } 324 global.AddPrintOutput = AddPrintOutput; 325 326 /************************************************************************* 327 * HARNESS-CENTRIC EXPORTS (we should generally work to eliminate these) * 328 *************************************************************************/ 329 330 // This overwrites shell.js's version that merely prints the given string. 331 function writeHeaderToLog(string) { 332 string = String(string); 333 334 // First dump to the console. 335 dump(string + "\n"); 336 337 // Then output to the page. 338 var h2 = CreateElement("h2"); 339 SetTextContent(h2, string); 340 AppendChild(printOutputContainer, h2); 341 } 342 global.writeHeaderToLog = writeHeaderToLog; 343 344 /************************* 345 * GLOBAL ERROR HANDLING * 346 *************************/ 347 348 // Possible values: 349 // - "Unknown" if no error is expected, 350 // - "error" if no specific error type is expected, 351 // - otherwise the error name, e.g. "TypeError" or "RangeError". 352 var expectedError; 353 354 window.onerror = function (msg, page, line, column, error) { 355 // Unset all options even when the test finished with an error. 356 browserOptionsClear(); 357 358 if (DESCRIPTION === undefined) { 359 DESCRIPTION = "Unknown"; 360 } 361 362 var actual = "error"; 363 var expected = expectedError; 364 if (expected !== "error" && expected !== "Unknown") { 365 // Check the error type when an actual Error object is available. 366 // NB: The |error| parameter of the onerror handler is not required to 367 // be an Error instance. 368 if (ReflectApply(ObjectPrototypeIsPrototypeOf, Error.prototype, [error])) { 369 actual = error.constructor.name; 370 } else { 371 expected = "error"; 372 } 373 } 374 375 var reason = `${page}:${line}: ${msg}`; 376 new TestCase(DESCRIPTION, expected, actual, reason); 377 378 reportFailure(msg); 379 }; 380 381 /********************************************** 382 * BROWSER IMPLEMENTATION FOR SHELL FUNCTIONS * 383 **********************************************/ 384 385 function gc() { 386 try { 387 SpecialPowersForceGC(); 388 } catch (ex) { 389 print("gc: " + ex); 390 } 391 } 392 global.gc = gc; 393 394 global.clearKeptObjects = ClearKeptObjects; 395 396 function options(aOptionName) { 397 // return value of options() is a comma delimited list 398 // of the previously set values 399 400 var value = ""; 401 for (var optionName in currentOptions) { 402 if (value) 403 value += ","; 404 value += optionName; 405 } 406 407 if (aOptionName) { 408 if (!HasOwnProperty(SpecialPowersCu, aOptionName)) { 409 // This test is trying to flip an unsupported option, so it's 410 // likely no longer testing what it was supposed to. Fail it 411 // hard. 412 throw "Unsupported JSContext option '" + aOptionName + "'"; 413 } 414 415 if (aOptionName in currentOptions) { 416 // option is set, toggle it to unset 417 delete currentOptions[aOptionName]; 418 SpecialPowersCu[aOptionName] = false; 419 } else { 420 // option is not set, toggle it to set 421 currentOptions[aOptionName] = true; 422 SpecialPowersCu[aOptionName] = true; 423 } 424 } 425 426 return value; 427 } 428 global.options = options; 429 430 /**************************************** 431 * HARNESS SETUP AND TEARDOWN FUNCTIONS * 432 ****************************************/ 433 434 function jsTestDriverBrowserInit() { 435 // Initialize with an empty set, because we just turned off all options. 436 currentOptions = Object.create(null); 437 438 if (document.location.search.indexOf("?") !== 0) { 439 // not called with a query string 440 return; 441 } 442 443 var properties = Object.create(null); 444 var fields = document.location.search.slice(1).split(";"); 445 for (var i = 0; i < fields.length; i++) { 446 var propertycaptures = /^([^=]+)=(.*)$/.exec(fields[i]); 447 if (propertycaptures === null) { 448 properties[fields[i]] = true; 449 } else { 450 properties[propertycaptures[1]] = decodeURIComponent(propertycaptures[2]); 451 } 452 } 453 454 // The test path may contain \ separators for the path. 455 // Bug 1877606: use / consistently 456 properties.test = properties.test.replace(/\\/g, "/"); 457 458 global.gTestPath = properties.test; 459 460 var testpathparts = properties.test.split("/"); 461 if (testpathparts.length < 2) { 462 // must have at least suitepath/testcase.js 463 return; 464 } 465 466 var testFileName = testpathparts[testpathparts.length - 1]; 467 468 if (testFileName.endsWith("-n.js")) { 469 // Negative test without a specific error type. 470 expectedError = "error"; 471 } else if (properties.error) { 472 // Negative test which expects a specific error type. 473 expectedError = properties.error; 474 } else { 475 // No error is expected. 476 expectedError = "Unknown"; 477 } 478 479 if (properties.gczeal) { 480 gczeal(Number(properties.gczeal)); 481 } 482 483 // Display the test path in the title. 484 document.title = properties.test; 485 486 // Output script tags for shell.js, then browser.js, at each level of the 487 // test path hierarchy. 488 var prepath = ""; 489 var scripts = []; 490 for (var i = 0; i < testpathparts.length - 1; i++) { 491 prepath += testpathparts[i] + "/"; 492 493 if (properties["test262-raw"]) { 494 // Skip running test harness files (shell.js and browser.js) if the 495 // test has the raw flag. 496 continue; 497 } 498 499 scripts.push({src: prepath + "shell.js", module: false}); 500 scripts.push({src: prepath + "browser.js", module: false}); 501 } 502 503 // Output the test script itself. 504 var moduleTest = !!properties.module; 505 scripts.push({src: prepath + testFileName, module: moduleTest}); 506 507 // Finally output the driver-end script to advance to the next test. 508 scripts.push({src: "js-test-driver-end.js", module: false}); 509 510 if (properties.async) { 511 gDelayTestDriverEnd = true; 512 } 513 514 if (!moduleTest) { 515 for (var i = 0; i < scripts.length; i++) { 516 var src = scripts[i].src; 517 document.write(`<script src="${src}" charset="utf-8"><\/script>`); 518 } 519 } else { 520 // Modules are loaded asynchronously by default, but for the test harness 521 // we need to execute all scripts and modules one after the other. 522 523 // Appends the next script element to the DOM. 524 function appendScript(index) { 525 var script = scriptElements[index]; 526 scriptElements[index] = null; 527 if (script !== null) { 528 ReflectApply(NodePrototypeAppendChild, documentDocumentElement, [script]); 529 } 530 } 531 532 // Create all script elements upfront, so we don't need to worry about 533 // modified built-ins. 534 var scriptElements = []; 535 for (var i = 0; i < scripts.length; i++) { 536 var spec = scripts[i]; 537 538 var script = document.createElement("script"); 539 script.charset = "utf-8"; 540 if (spec.module) { 541 script.type = "module"; 542 } 543 script.src = spec.src; 544 545 let nextScriptIndex = i + 1; 546 if (nextScriptIndex < scripts.length) { 547 var callNextAppend = () => appendScript(nextScriptIndex); 548 script.addEventListener("load", callNextAppend, {once: true}); 549 script.addEventListener("error", callNextAppend, {once: true}); 550 } 551 552 scriptElements[i] = script; 553 } 554 555 // Append the first script. 556 appendScript(0); 557 } 558 } 559 560 global.gDelayTestDriverEnd = false; 561 562 function jsTestDriverEnd() { 563 // gDelayTestDriverEnd is used to delay collection of the test result and 564 // signal to Spider so that tests can continue to run after page load has 565 // fired. They are responsible for setting gDelayTestDriverEnd = true then 566 // when completed, setting gDelayTestDriverEnd = false then calling 567 // jsTestDriverEnd() 568 569 if (gDelayTestDriverEnd) { 570 return; 571 } 572 573 window.onerror = null; 574 575 // Unset all options when the test has finished. 576 browserOptionsClear(); 577 578 if (window.opener && window.opener.runNextTest) { 579 if (window.opener.reportCallBack) { 580 window.opener.reportCallBack(window.opener.gWindow); 581 } 582 583 setTimeout("window.opener.runNextTest()", 250); 584 } else { 585 // tell reftest the test is complete. 586 ReflectApply(ElementSetClassName, documentDocumentElement, [""]); 587 // tell Spider page is complete 588 gPageCompleted = true; 589 } 590 } 591 global.jsTestDriverEnd = jsTestDriverEnd; 592 593 /*************************************************************************** 594 * DIALOG CLOSER, PRESUMABLY TO CLOSE SLOW SCRIPT DIALOGS AND OTHER POPUPS * 595 ***************************************************************************/ 596 597 // dialog closer from http://bclary.com/projects/spider/spider/chrome/content/spider/dialog-closer.js 598 599 // Use an array to handle the case where multiple dialogs appear at one time. 600 var dialogCloserSubjects = []; 601 var dialogCloser = SpecialPowers 602 .Cc["@mozilla.org/embedcomp/window-watcher;1"] 603 .getService(SpecialPowers.Ci.nsIWindowWatcher); 604 var dialogCloserObserver = { 605 observe(subject, topic, data) { 606 if (topic === "domwindowopened" && subject.isChromeWindow) { 607 ArrayPush(dialogCloserSubjects, subject); 608 609 // Timeout of 0 needed when running under reftest framework. 610 subject.setTimeout(closeDialog, 0); 611 } 612 } 613 }; 614 615 function closeDialog() { 616 while (dialogCloserSubjects.length > 0) { 617 var subject = ArrayPop(dialogCloserSubjects); 618 subject.close(); 619 } 620 } 621 622 function unregisterDialogCloser() { 623 gczeal(0); 624 625 if (!dialogCloserObserver || !dialogCloser) { 626 return; 627 } 628 629 dialogCloser.unregisterNotification(dialogCloserObserver); 630 631 dialogCloserObserver = null; 632 dialogCloser = null; 633 } 634 635 dialogCloser.registerNotification(dialogCloserObserver); 636 window.addEventListener("unload", unregisterDialogCloser, true); 637 638 /******************************************* 639 * RUN ONCE CODE TO SETUP ADDITIONAL STATE * 640 *******************************************/ 641 642 jsTestDriverBrowserInit(); 643 644 })(this); 645 646 var gPageCompleted;