docshell_helpers.js (23852B)
1 if (!window.opener && window.arguments) { 2 window.opener = window.arguments[0]; 3 } 4 /** 5 * Import common SimpleTest methods so that they're usable in this window. 6 */ 7 /* globals SimpleTest, is, isnot, ok, onerror, todo, todo_is, todo_isnot */ 8 var imports = [ 9 "SimpleTest", 10 "is", 11 "isnot", 12 "ok", 13 "onerror", 14 "todo", 15 "todo_is", 16 "todo_isnot", 17 ]; 18 for (var name of imports) { 19 window[name] = window.opener.wrappedJSObject[name]; 20 } 21 const { BrowserTestUtils } = ChromeUtils.importESModule( 22 "resource://testing-common/BrowserTestUtils.sys.mjs" 23 ); 24 25 const ACTOR_MODULE_URI = 26 "chrome://mochitests/content/chrome/docshell/test/chrome/DocShellHelpers.sys.mjs"; 27 const { DocShellHelpersParent } = ChromeUtils.importESModule(ACTOR_MODULE_URI); 28 // Some functions assume chrome-harness.js has been loaded. 29 /* import-globals-from ../../../testing/mochitest/chrome-harness.js */ 30 31 /** 32 * Define global constants and variables. 33 */ 34 const NAV_NONE = 0; 35 const NAV_BACK = 1; 36 const NAV_FORWARD = 2; 37 const NAV_GOTOINDEX = 3; 38 const NAV_URI = 4; 39 const NAV_RELOAD = 5; 40 41 var gExpectedEvents; // an array of events which are expected to 42 // be triggered by this navigation 43 var gUnexpectedEvents; // an array of event names which are NOT expected 44 // to be triggered by this navigation 45 var gFinalEvent; // true if the last expected event has fired 46 var gUrisNotInBFCache = []; // an array of uri's which shouldn't be stored 47 // in the bfcache 48 var gNavType = NAV_NONE; // defines the most recent navigation type 49 // executed by doPageNavigation 50 var gOrigMaxTotalViewers = undefined; // original value of max_total_viewers, // to be restored at end of test 51 52 var gExtractedPath = null; // used to cache file path for extracting files from a .jar file 53 54 /** 55 * The doPageNavigation() function performs page navigations asynchronously, 56 * listens for specified events, and compares actual events with a list of 57 * expected events. When all expected events have occurred, an optional 58 * callback can be notified. The parameter passed to this function is an 59 * object with the following properties: 60 * 61 * uri: if !undefined, the browser will navigate to this uri 62 * 63 * back: if true, the browser will execute goBack() 64 * 65 * forward: if true, the browser will execute goForward() 66 * 67 * gotoIndex: if a number, the browser will execute gotoIndex() with 68 * the number as index 69 * 70 * reload: if true, the browser will execute reload() 71 * 72 * eventsToListenFor: an array containing one or more of the following event 73 * types to listen for: "pageshow", "pagehide", "onload", 74 * "onunload". If this property is undefined, only a 75 * single "pageshow" events will be listened for. If this 76 * property is explicitly empty, [], then no events will 77 * be listened for. 78 * 79 * expectedEvents: an array of one or more expectedEvent objects, 80 * corresponding to the events which are expected to be 81 * fired for this navigation. Each object has the 82 * following properties: 83 * 84 * type: one of the event type strings 85 * title (optional): the title of the window the 86 * event belongs to 87 * persisted (optional): the event's expected 88 * .persisted attribute 89 * 90 * This function will verify that events with the 91 * specified properties are fired in the same order as 92 * specified in the array. If .title or .persisted 93 * properties for an expectedEvent are undefined, those 94 * properties will not be verified for that particular 95 * event. 96 * 97 * This property is ignored if eventsToListenFor is 98 * undefined or []. 99 * 100 * preventBFCache: if true, an RTCPeerConnection will be added to the loaded 101 * page to prevent it from being bfcached. This property 102 * has no effect when eventsToListenFor is []. 103 * 104 * onNavComplete: a callback which is notified after all expected events 105 * have occurred, or after a timeout has elapsed. This 106 * callback is not notified if eventsToListenFor is []. 107 * onGlobalCreation: a callback which is notified when a DOMWindow is created 108 * (implemented by observing 109 * "content-document-global-created") 110 * 111 * There must be an expectedEvent object for each event of the types in 112 * eventsToListenFor which is triggered by this navigation. For example, if 113 * eventsToListenFor = [ "pagehide", "pageshow" ], then expectedEvents 114 * must contain an object for each pagehide and pageshow event which occurs as 115 * a result of this navigation. 116 */ 117 // eslint-disable-next-line complexity 118 function doPageNavigation(params) { 119 // Parse the parameters. 120 let back = params.back ? params.back : false; 121 let forward = params.forward ? params.forward : false; 122 let gotoIndex = params.gotoIndex ? params.gotoIndex : false; 123 let reload = params.reload ? params.reload : false; 124 let uri = params.uri ? params.uri : false; 125 let eventsToListenFor = 126 typeof params.eventsToListenFor != "undefined" 127 ? params.eventsToListenFor 128 : ["pageshow"]; 129 gExpectedEvents = 130 typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length 131 ? undefined 132 : params.expectedEvents; 133 gUnexpectedEvents = 134 typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length 135 ? undefined 136 : params.unexpectedEvents; 137 let preventBFCache = 138 typeof [params.preventBFCache] == "undefined" 139 ? false 140 : params.preventBFCache; 141 let waitOnly = 142 typeof params.waitForEventsOnly == "boolean" && params.waitForEventsOnly; 143 144 // Do some sanity checking on arguments. 145 let navigation = ["back", "forward", "gotoIndex", "reload", "uri"].filter(k => 146 params.hasOwnProperty(k) 147 ); 148 if (navigation.length > 1) { 149 throw new Error(`Can't specify both ${navigation[0]} and ${navigation[1]}`); 150 } else if (!navigation.length && !waitOnly) { 151 throw new Error( 152 "Must specify back or forward or gotoIndex or reload or uri" 153 ); 154 } 155 if (params.onNavComplete && !eventsToListenFor.length) { 156 throw new Error("Can't use onNavComplete when eventsToListenFor == []"); 157 } 158 if (params.preventBFCache && !eventsToListenFor.length) { 159 throw new Error("Can't use preventBFCache when eventsToListenFor == []"); 160 } 161 if (params.preventBFCache && waitOnly) { 162 throw new Error("Can't prevent bfcaching when only waiting for events"); 163 } 164 if (waitOnly && typeof params.onNavComplete == "undefined") { 165 throw new Error( 166 "Must specify onNavComplete when specifying waitForEventsOnly" 167 ); 168 } 169 if (waitOnly && navigation.length) { 170 throw new Error( 171 "Can't specify a navigation type when using waitForEventsOnly" 172 ); 173 } 174 for (let anEventType of eventsToListenFor) { 175 let eventFound = false; 176 if (anEventType == "pageshow" && !gExpectedEvents) { 177 eventFound = true; 178 } 179 if (gExpectedEvents) { 180 for (let anExpectedEvent of gExpectedEvents) { 181 if (anExpectedEvent.type == anEventType) { 182 eventFound = true; 183 } 184 } 185 } 186 if (gUnexpectedEvents) { 187 for (let anExpectedEventType of gUnexpectedEvents) { 188 if (anExpectedEventType == anEventType) { 189 eventFound = true; 190 } 191 } 192 } 193 if (!eventFound) { 194 throw new Error( 195 `Event type ${anEventType} is specified in ` + 196 "eventsToListenFor, but not in expectedEvents" 197 ); 198 } 199 } 200 201 // If the test explicitly sets .eventsToListenFor to [], don't wait for any 202 // events. 203 gFinalEvent = !eventsToListenFor.length; 204 205 // Add observers as needed. 206 let observers = new Map(); 207 if (params.hasOwnProperty("onGlobalCreation")) { 208 observers.set("content-document-global-created", params.onGlobalCreation); 209 } 210 211 // Add an event listener for each type of event in the .eventsToListenFor 212 // property of the input parameters, and add an observer for all the topics 213 // in the observers map. 214 let cleanup; 215 let useActor = TestWindow.getBrowser().isRemoteBrowser; 216 if (useActor) { 217 ChromeUtils.registerWindowActor("DocShellHelpers", { 218 parent: { 219 esModuleURI: ACTOR_MODULE_URI, 220 }, 221 child: { 222 esModuleURI: ACTOR_MODULE_URI, 223 events: { 224 pageshow: { createActor: true, capture: true }, 225 pagehide: { createActor: true, capture: true }, 226 load: { createActor: true, capture: true }, 227 unload: { createActor: true, capture: true }, 228 visibilitychange: { createActor: true, capture: true }, 229 }, 230 observers: observers.keys(), 231 }, 232 allFrames: true, 233 // We avoid messages from system addons event pages here, as 234 // the tests test_bug321671.xhtml and test_bug690056.xhtml do 235 // not expect those events, and so will intermittently fail. 236 // They require messages from "browsers", "test", and "" to pass. 237 // See bug 1784831 and bug 1883434 for more context. 238 messageManagerGroups: ["browsers", "test", ""], 239 }); 240 DocShellHelpersParent.eventsToListenFor = eventsToListenFor; 241 DocShellHelpersParent.observers = observers; 242 243 cleanup = () => { 244 DocShellHelpersParent.eventsToListenFor = null; 245 DocShellHelpersParent.observers = null; 246 ChromeUtils.unregisterWindowActor("DocShellHelpers"); 247 }; 248 } else { 249 for (let eventType of eventsToListenFor) { 250 dump("TEST: registering a listener for " + eventType + " events\n"); 251 TestWindow.getBrowser().addEventListener( 252 eventType, 253 pageEventListener, 254 true 255 ); 256 } 257 if (observers.size > 0) { 258 let observer = (_, topic) => { 259 observers.get(topic).call(); 260 }; 261 for (let topic of observers.keys()) { 262 Services.obs.addObserver(observer, topic); 263 } 264 265 // We only need to do cleanup for the observer, the event listeners will 266 // go away with the window. 267 cleanup = () => { 268 for (let topic of observers.keys()) { 269 Services.obs.removeObserver(observer, topic); 270 } 271 }; 272 } 273 } 274 275 if (cleanup) { 276 // Register a cleanup function on domwindowclosed, to avoid contaminating 277 // other tests if we bail out early because of an error. 278 Services.ww.registerNotification(function windowClosed(subject, topic) { 279 if (topic == "domwindowclosed" && subject == window) { 280 Services.ww.unregisterNotification(windowClosed); 281 cleanup(); 282 } 283 }); 284 } 285 286 // Perform the specified navigation. 287 if (back) { 288 gNavType = NAV_BACK; 289 TestWindow.getBrowser().goBack(); 290 } else if (forward) { 291 gNavType = NAV_FORWARD; 292 TestWindow.getBrowser().goForward(); 293 } else if (typeof gotoIndex == "number") { 294 gNavType = NAV_GOTOINDEX; 295 TestWindow.getBrowser().gotoIndex(gotoIndex); 296 } else if (uri) { 297 gNavType = NAV_URI; 298 BrowserTestUtils.startLoadingURIString(TestWindow.getBrowser(), uri); 299 } else if (reload) { 300 gNavType = NAV_RELOAD; 301 TestWindow.getBrowser().reload(); 302 } else if (waitOnly) { 303 gNavType = NAV_NONE; 304 } else { 305 throw new Error("No valid navigation type passed to doPageNavigation!"); 306 } 307 308 // If we're listening for events and there is an .onNavComplete callback, 309 // wait for all events to occur, and then call doPageNavigation_complete(). 310 if (eventsToListenFor.length && params.onNavComplete) { 311 waitForTrue( 312 function () { 313 return gFinalEvent; 314 }, 315 function () { 316 doPageNavigation_complete( 317 eventsToListenFor, 318 params.onNavComplete, 319 preventBFCache, 320 useActor, 321 cleanup 322 ); 323 } 324 ); 325 } else if (cleanup) { 326 cleanup(); 327 } 328 } 329 330 /** 331 * Finish doPageNavigation(), by removing event listeners, adding an unload 332 * handler if appropriate, and calling the onNavComplete callback. This 333 * function is called after all the expected events for this navigation have 334 * occurred. 335 */ 336 function doPageNavigation_complete( 337 eventsToListenFor, 338 onNavComplete, 339 preventBFCache, 340 useActor, 341 cleanup 342 ) { 343 if (useActor) { 344 if (preventBFCache) { 345 let actor = 346 TestWindow.getBrowser().browsingContext.currentWindowGlobal.getActor( 347 "DocShellHelpers" 348 ); 349 actor.sendAsyncMessage("docshell_helpers:preventBFCache"); 350 } 351 } else { 352 // Unregister our event listeners. 353 dump("TEST: removing event listeners\n"); 354 for (let eventType of eventsToListenFor) { 355 TestWindow.getBrowser().removeEventListener( 356 eventType, 357 pageEventListener, 358 true 359 ); 360 } 361 362 // If the .preventBFCache property was set, add an RTCPeerConnection to 363 // prevent the page from being bfcached. 364 if (preventBFCache) { 365 let win = TestWindow.getWindow(); 366 win.blockBFCache = new win.RTCPeerConnection(); 367 } 368 } 369 370 if (cleanup) { 371 cleanup(); 372 } 373 374 let uri = TestWindow.getBrowser().currentURI.spec; 375 if (preventBFCache) { 376 // Save the current uri in an array of uri's which shouldn't be 377 // stored in the bfcache, for later verification. 378 if (!(uri in gUrisNotInBFCache)) { 379 gUrisNotInBFCache.push(uri); 380 } 381 } else if (gNavType == NAV_URI) { 382 // If we're navigating to a uri and .preventBFCache was not 383 // specified, splice it out of gUrisNotInBFCache if it's there. 384 gUrisNotInBFCache.forEach(function (element, index, array) { 385 if (element == uri) { 386 array.splice(index, 1); 387 } 388 }, this); 389 } 390 391 // Notify the callback now that we're done. 392 onNavComplete.call(); 393 } 394 395 function promisePageNavigation(params) { 396 if (params.hasOwnProperty("onNavComplete")) { 397 throw new Error( 398 "Can't use a onNavComplete completion callback with promisePageNavigation." 399 ); 400 } 401 return new Promise(resolve => { 402 params.onNavComplete = resolve; 403 doPageNavigation(params); 404 }); 405 } 406 407 /** 408 * Allows a test to wait for page navigation events, and notify a 409 * callback when they've all been received. This works exactly the 410 * same as doPageNavigation(), except that no navigation is initiated. 411 */ 412 function waitForPageEvents(params) { 413 params.waitForEventsOnly = true; 414 doPageNavigation(params); 415 } 416 417 function promisePageEvents(params) { 418 if (params.hasOwnProperty("onNavComplete")) { 419 throw new Error( 420 "Can't use a onNavComplete completion callback with promisePageEvents." 421 ); 422 } 423 return new Promise(resolve => { 424 params.waitForEventsOnly = true; 425 params.onNavComplete = resolve; 426 doPageNavigation(params); 427 }); 428 } 429 430 /** 431 * The event listener which listens for expectedEvents. 432 */ 433 function pageEventListener( 434 event, 435 originalTargetIsHTMLDocument = HTMLDocument.isInstance(event.originalTarget) 436 ) { 437 try { 438 dump( 439 "TEST: eventListener received a " + 440 event.type + 441 " event for page " + 442 event.originalTarget.title + 443 ", persisted=" + 444 event.persisted + 445 "\n" 446 ); 447 } catch (e) { 448 // Ignore any exception. 449 } 450 451 // If this page shouldn't be in the bfcache because it was previously 452 // loaded with .preventBFCache, make sure that its pageshow event 453 // has .persisted = false, even if the test doesn't explicitly test 454 // for .persisted. 455 if ( 456 event.type == "pageshow" && 457 (gNavType == NAV_BACK || 458 gNavType == NAV_FORWARD || 459 gNavType == NAV_GOTOINDEX) 460 ) { 461 let uri = TestWindow.getBrowser().currentURI.spec; 462 if (uri in gUrisNotInBFCache) { 463 ok( 464 !event.persisted, 465 "pageshow event has .persisted = false, even " + 466 "though it was loaded with .preventBFCache previously\n" 467 ); 468 } 469 } 470 471 if (typeof gUnexpectedEvents != "undefined") { 472 is( 473 gUnexpectedEvents.indexOf(event.type), 474 -1, 475 "Should not get unexpected event " + event.type 476 ); 477 } 478 479 // If no expected events were specified, mark the final event as having been 480 // triggered when a pageshow event is fired; this will allow 481 // doPageNavigation() to return. 482 if (typeof gExpectedEvents == "undefined" && event.type == "pageshow") { 483 waitForNextPaint(function () { 484 gFinalEvent = true; 485 }); 486 return; 487 } 488 489 // If there are explicitly no expected events, but we receive one, it's an 490 // error. 491 if (!gExpectedEvents.length) { 492 ok(false, "Unexpected event (" + event.type + ") occurred"); 493 return; 494 } 495 496 // Grab the next expected event, and compare its attributes against the 497 // actual event. 498 let expected = gExpectedEvents.shift(); 499 500 is( 501 event.type, 502 expected.type, 503 "A " + 504 expected.type + 505 " event was expected, but a " + 506 event.type + 507 " event occurred" 508 ); 509 510 if (typeof expected.title != "undefined") { 511 ok( 512 originalTargetIsHTMLDocument, 513 "originalTarget for last " + event.type + " event not an HTMLDocument" 514 ); 515 is( 516 event.originalTarget.title, 517 expected.title, 518 "A " + 519 event.type + 520 " event was expected for page " + 521 expected.title + 522 ", but was fired for page " + 523 event.originalTarget.title 524 ); 525 } 526 527 if (typeof expected.persisted != "undefined") { 528 is( 529 event.persisted, 530 expected.persisted, 531 "The persisted property of the " + 532 event.type + 533 " event on page " + 534 event.originalTarget.location + 535 " had an unexpected value" 536 ); 537 } 538 539 if ("visibilityState" in expected) { 540 is( 541 event.originalTarget.visibilityState, 542 expected.visibilityState, 543 "The visibilityState property of the document on page " + 544 event.originalTarget.location + 545 " had an unexpected value" 546 ); 547 } 548 549 if ("hidden" in expected) { 550 is( 551 event.originalTarget.hidden, 552 expected.hidden, 553 "The hidden property of the document on page " + 554 event.originalTarget.location + 555 " had an unexpected value" 556 ); 557 } 558 559 // If we're out of expected events, let doPageNavigation() return. 560 if (!gExpectedEvents.length) { 561 waitForNextPaint(function () { 562 gFinalEvent = true; 563 }); 564 } 565 } 566 567 DocShellHelpersParent.eventListener = pageEventListener; 568 569 /** 570 * End a test. 571 */ 572 function finish() { 573 // Work around bug 467960. 574 let historyPurged; 575 if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { 576 let history = TestWindow.getBrowser().browsingContext?.sessionHistory; 577 history.purgeHistory(history.count); 578 historyPurged = Promise.resolve(); 579 } else { 580 historyPurged = SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { 581 let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory 582 .legacySHistory; 583 history.purgeHistory(history.count); 584 }); 585 } 586 587 // If the test changed the value of max_total_viewers via a call to 588 // enableBFCache(), then restore it now. 589 if (typeof gOrigMaxTotalViewers != "undefined") { 590 Services.prefs.setIntPref( 591 "browser.sessionhistory.max_total_viewers", 592 gOrigMaxTotalViewers 593 ); 594 } 595 596 // Close the test window and signal the framework that the test is done. 597 let opener = window.opener; 598 let SimpleTest = opener.wrappedJSObject.SimpleTest; 599 600 // Wait for the window to be closed before finishing the test 601 Services.ww.registerNotification(function observer(subject, topic) { 602 if (topic == "domwindowclosed") { 603 Services.ww.unregisterNotification(observer); 604 SimpleTest.waitForFocus(SimpleTest.finish, opener); 605 } 606 }); 607 608 historyPurged.then(_ => { 609 window.close(); 610 }); 611 } 612 613 /** 614 * Helper function which waits until another function returns true, or until a 615 * timeout occurs, and then notifies a callback. 616 * 617 * Parameters: 618 * 619 * fn: a function which is evaluated repeatedly, and when it turns true, 620 * the onWaitComplete callback is notified. 621 * 622 * onWaitComplete: a callback which will be notified when fn() returns 623 * true, or when a timeout occurs. 624 * 625 * timeout: a timeout, in seconds or ms, after which waitForTrue() will 626 * fail an assertion and then return, even if the fn function never 627 * returns true. If timeout is undefined, waitForTrue() will never 628 * time out. 629 */ 630 function waitForTrue(fn, onWaitComplete, timeout) { 631 promiseTrue(fn, timeout).then(() => { 632 onWaitComplete.call(); 633 }); 634 } 635 636 function promiseTrue(fn, timeout) { 637 if (typeof timeout != "undefined") { 638 // If timeoutWait is less than 500, assume it represents seconds, and 639 // convert to ms. 640 if (timeout < 500) { 641 timeout *= 1000; 642 } 643 } 644 645 // Loop until the test function returns true, or until a timeout occurs, 646 // if a timeout is defined. 647 let intervalid, timeoutid; 648 let condition = new Promise(resolve => { 649 intervalid = setInterval(async () => { 650 if (await fn.call()) { 651 resolve(); 652 } 653 }, 20); 654 }); 655 if (typeof timeout != "undefined") { 656 condition = Promise.race([ 657 condition, 658 new Promise((_, reject) => { 659 timeoutid = setTimeout(() => { 660 reject(); 661 }, timeout); 662 }), 663 ]); 664 } 665 return condition 666 .finally(() => { 667 clearInterval(intervalid); 668 }) 669 .then(() => { 670 clearTimeout(timeoutid); 671 }); 672 } 673 674 function waitForNextPaint(cb) { 675 requestAnimationFrame(_ => requestAnimationFrame(cb)); 676 } 677 678 function promiseNextPaint() { 679 return new Promise(resolve => { 680 waitForNextPaint(resolve); 681 }); 682 } 683 684 /** 685 * Enable or disable the bfcache. 686 * 687 * Parameters: 688 * 689 * enable: if true, set max_total_viewers to -1 (the default); if false, set 690 * to 0 (disabled), if a number, set it to that specific number 691 */ 692 function enableBFCache(enable) { 693 // If this is the first time the test called enableBFCache(), 694 // store the original value of max_total_viewers, so it can 695 // be restored at the end of the test. 696 if (typeof gOrigMaxTotalViewers == "undefined") { 697 gOrigMaxTotalViewers = Services.prefs.getIntPref( 698 "browser.sessionhistory.max_total_viewers" 699 ); 700 } 701 702 if (typeof enable == "boolean") { 703 if (enable) { 704 Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", -1); 705 } else { 706 Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0); 707 } 708 } else if (typeof enable == "number") { 709 Services.prefs.setIntPref( 710 "browser.sessionhistory.max_total_viewers", 711 enable 712 ); 713 } 714 } 715 716 /* 717 * get http root for local tests. Use a single extractJarToTmp instead of 718 * extracting for each test. 719 * Returns a file://path if we have a .jar file 720 */ 721 function getHttpRoot() { 722 var location = window.location.href; 723 location = getRootDirectory(location); 724 var jar = getJar(location); 725 if (jar != null) { 726 if (gExtractedPath == null) { 727 var resolved = extractJarToTmp(jar); 728 gExtractedPath = resolved.path; 729 } 730 } else { 731 return null; 732 } 733 return "file://" + gExtractedPath + "/"; 734 } 735 736 /** 737 * Returns the full HTTP url for a file in the mochitest docshell test 738 * directory. 739 */ 740 function getHttpUrl(filename) { 741 var root = getHttpRoot(); 742 if (root == null) { 743 root = "http://mochi.test:8888/chrome/docshell/test/chrome/"; 744 } 745 return root + filename; 746 } 747 748 /** 749 * A convenience object with methods that return the current test window, 750 * browser, and document. 751 */ 752 var TestWindow = {}; 753 TestWindow.getWindow = function () { 754 return document.getElementById("content").contentWindow; 755 }; 756 TestWindow.getBrowser = function () { 757 return document.getElementById("content"); 758 }; 759 TestWindow.getDocument = function () { 760 return document.getElementById("content").contentDocument; 761 };