BrowserTestUtils.sys.mjs (98766B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* 6 * This module implements a number of utilities useful for browser tests. 7 * 8 * All asynchronous helper methods should return promises, rather than being 9 * callback based. 10 */ 11 12 // This file uses ContentTask & frame scripts, where these are available. 13 /* global ContentTaskUtils */ 14 15 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 16 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 17 import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; 18 19 const lazy = {}; 20 21 ChromeUtils.defineESModuleGetters(lazy, { 22 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 23 ContentTask: "resource://testing-common/ContentTask.sys.mjs", 24 }); 25 26 XPCOMUtils.defineLazyServiceGetters(lazy, { 27 ProtocolProxyService: [ 28 "@mozilla.org/network/protocol-proxy-service;1", 29 Ci.nsIProtocolProxyService, 30 ], 31 }); 32 33 let gListenerId = 0; 34 35 const DISABLE_CONTENT_PROCESS_REUSE_PREF = "dom.ipc.disableContentProcessReuse"; 36 37 const kAboutPageRegistrationContentScript = 38 "chrome://mochikit/content/tests/BrowserTestUtils/content-about-page-utils.js"; 39 40 /** 41 * Create and register the BrowserTestUtils and ContentEventListener window 42 * actors. 43 */ 44 function registerActors() { 45 ChromeUtils.registerWindowActor("BrowserTestUtils", { 46 parent: { 47 esModuleURI: "resource://testing-common/BrowserTestUtilsParent.sys.mjs", 48 }, 49 child: { 50 esModuleURI: "resource://testing-common/BrowserTestUtilsChild.sys.mjs", 51 events: { 52 DOMContentLoaded: { capture: true }, 53 load: { capture: true }, 54 }, 55 }, 56 allFrames: true, 57 includeChrome: true, 58 }); 59 60 ChromeUtils.registerWindowActor("ContentEventListener", { 61 parent: { 62 esModuleURI: 63 "resource://testing-common/ContentEventListenerParent.sys.mjs", 64 }, 65 child: { 66 esModuleURI: 67 "resource://testing-common/ContentEventListenerChild.sys.mjs", 68 events: { 69 // We need to see the creation of all new windows, in case they have 70 // a browsing context we are interested in. 71 DOMWindowCreated: { capture: true }, 72 }, 73 }, 74 allFrames: true, 75 }); 76 } 77 78 registerActors(); 79 80 /** 81 * BrowserTestUtils provides useful test utilities for working with the browser 82 * in browser mochitests. Most common operations (opening, closing and switching 83 * between tabs and windows, loading URLs, waiting for events in the parent or 84 * content process, clicking things in the content process, registering about 85 * pages, etc.) have dedicated helpers on this object. 86 * 87 * @class 88 */ 89 export var BrowserTestUtils = { 90 // We define the function separately, rather than using an arrow function 91 // inline due to https://github.com/jsdoc/jsdoc/issues/2143. 92 /** 93 * @template T 94 * @typedef {Function} withNewTabTaskFn 95 * @param {MozBrowser} browser 96 * @returns {Promise<T> | T} 97 */ 98 99 /** 100 * Loads a page in a new tab, executes a Task and closes the tab. 101 * 102 * @template T 103 * @param {object | string} options 104 * If this is a string it is the url to open and will be opened in the 105 * currently active browser window. 106 * @param {tabbrowser} [options.gBrowser] 107 * A reference to the ``tabbrowser`` element where the new tab should 108 * be opened, 109 * @param {string} options.url 110 * The URL of the page to load. 111 * @param {withNewTabTaskFn<T>} taskFn 112 * Async function representing that will be executed while 113 * the tab is loaded. The first argument passed to the function is a 114 * reference to the browser object for the new tab. 115 * 116 * @return {Promise<T>} Resolves to the value that is returned from taskFn. 117 * @rejects Any exception from taskFn is propagated. 118 */ 119 async withNewTab(options, taskFn) { 120 if (typeof options == "string") { 121 options = { 122 gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser, 123 url: options, 124 }; 125 } 126 let tab = await BrowserTestUtils.openNewForegroundTab(options); 127 let originalWindow = tab.ownerGlobal; 128 let result; 129 try { 130 result = await taskFn(tab.linkedBrowser); 131 } finally { 132 let finalWindow = tab.ownerGlobal; 133 if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) { 134 // taskFn may resolve within a tick after opening a new tab. 135 // We shouldn't remove the newly opened tab in the same tick. 136 // Wait for the next tick here. 137 await TestUtils.waitForTick(); 138 BrowserTestUtils.removeTab(tab); 139 } else { 140 Services.console.logStringMessage( 141 "BrowserTestUtils.withNewTab: Tab was already closed before " + 142 "removeTab would have been called" 143 ); 144 } 145 } 146 147 return Promise.resolve(result); 148 }, 149 150 /** 151 * Opens a new tab in the foreground. 152 * 153 * This function takes an options object (which is preferred) or actual 154 * parameters. The names of the options must correspond to the names below. 155 * gBrowser is required and all other options are optional. 156 * 157 * @param {tabbrowser} gBrowser 158 * The tabbrowser to open the tab new in. 159 * @param {string|function} opening (or url) 160 * May be either a string URL to load in the tab, or a function that 161 * will be called to open a foreground tab. Defaults to "about:blank". 162 * @param {boolean} waitForLoad 163 * True to wait for the page in the new tab to load. Defaults to true. 164 * @param {boolean} waitForStateStop 165 * True to wait for the web progress listener to send STATE_STOP for the 166 * document in the tab. Defaults to false. 167 * @param {boolean} forceNewProcess 168 * True to force the new tab to load in a new process. Defaults to 169 * false. 170 * 171 * @return {Promise} 172 * Resolves when the tab is ready and loaded as necessary. 173 */ 174 openNewForegroundTab(tabbrowser, ...args) { 175 let startTime = ChromeUtils.now(); 176 let options; 177 if ( 178 tabbrowser.ownerGlobal && 179 tabbrowser === tabbrowser.ownerGlobal.gBrowser 180 ) { 181 // tabbrowser is a tabbrowser, read the rest of the arguments from args. 182 let [ 183 opening = "about:blank", 184 waitForLoad = true, 185 waitForStateStop = false, 186 forceNewProcess = false, 187 ] = args; 188 189 options = { opening, waitForLoad, waitForStateStop, forceNewProcess }; 190 } else { 191 if ("url" in tabbrowser && !("opening" in tabbrowser)) { 192 tabbrowser.opening = tabbrowser.url; 193 } 194 195 let { 196 opening = "about:blank", 197 waitForLoad = true, 198 waitForStateStop = false, 199 forceNewProcess = false, 200 } = tabbrowser; 201 202 tabbrowser = tabbrowser.gBrowser; 203 options = { opening, waitForLoad, waitForStateStop, forceNewProcess }; 204 } 205 206 let { 207 opening: opening, 208 waitForLoad: aWaitForLoad, 209 waitForStateStop: aWaitForStateStop, 210 } = options; 211 212 let promises, tab; 213 try { 214 // If we're asked to force a new process, set the pref to disable process 215 // re-use while we insert this new tab. 216 if (options.forceNewProcess) { 217 Services.ppmm.releaseCachedProcesses(); 218 Services.prefs.setBoolPref(DISABLE_CONTENT_PROCESS_REUSE_PREF, true); 219 } 220 221 promises = [ 222 BrowserTestUtils.switchTab(tabbrowser, function () { 223 if (typeof opening == "function") { 224 opening(); 225 tab = tabbrowser.selectedTab; 226 } else { 227 tabbrowser.selectedTab = tab = BrowserTestUtils.addTab( 228 tabbrowser, 229 opening 230 ); 231 } 232 }), 233 ]; 234 235 if (aWaitForLoad) { 236 // accept any load, including about:blank 237 promises.push( 238 BrowserTestUtils.browserLoaded(tab.linkedBrowser, { 239 wantLoad: () => true, 240 }) 241 ); 242 } 243 if (aWaitForStateStop) { 244 promises.push(BrowserTestUtils.browserStopped(tab.linkedBrowser)); 245 } 246 } finally { 247 // Clear the pref once we're done, if needed. 248 if (options.forceNewProcess) { 249 Services.prefs.clearUserPref(DISABLE_CONTENT_PROCESS_REUSE_PREF); 250 } 251 } 252 return Promise.all(promises).then(() => { 253 let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild; 254 ChromeUtils.addProfilerMarker( 255 "BrowserTestUtils", 256 { startTime, category: "Test", innerWindowId }, 257 "openNewForegroundTab" 258 ); 259 return tab; 260 }); 261 }, 262 263 showOnlyTheseTabs(tabbrowser, tabs) { 264 for (let tab of tabs) { 265 tabbrowser.showTab(tab); 266 } 267 for (let tab of tabbrowser.tabs) { 268 if (!tabs.includes(tab)) { 269 tabbrowser.hideTab(tab); 270 } 271 } 272 }, 273 274 /** 275 * Checks if a DOM element is hidden. 276 * 277 * @param {Element} element 278 * The element which is to be checked. 279 * 280 * @return {boolean} 281 */ 282 isHidden(element) { 283 if ( 284 element.nodeType == Node.DOCUMENT_FRAGMENT_NODE && 285 element.containingShadowRoot == element 286 ) { 287 return BrowserTestUtils.isHidden(element.getRootNode().host); 288 } 289 290 let win = element.ownerGlobal; 291 let style = win.getComputedStyle(element); 292 if (style.display == "none") { 293 return true; 294 } 295 if (style.visibility != "visible") { 296 return true; 297 } 298 if (win.XULPopupElement.isInstance(element)) { 299 return ["hiding", "closed"].includes(element.state); 300 } 301 302 // Hiding a parent element will hide all its children 303 if (element.parentNode != element.ownerDocument) { 304 return BrowserTestUtils.isHidden(element.parentNode); 305 } 306 307 return false; 308 }, 309 310 /** 311 * Checks if a DOM element is visible. 312 * 313 * @param {Element} element 314 * The element which is to be checked. 315 * 316 * @return {boolean} 317 */ 318 isVisible(element) { 319 if ( 320 element.nodeType == Node.DOCUMENT_FRAGMENT_NODE && 321 element.containingShadowRoot == element 322 ) { 323 return BrowserTestUtils.isVisible(element.getRootNode().host); 324 } 325 326 let win = element.ownerGlobal; 327 let style = win.getComputedStyle(element); 328 if (style.display == "none") { 329 return false; 330 } 331 if (style.visibility != "visible") { 332 return false; 333 } 334 if (win.XULPopupElement.isInstance(element) && element.state != "open") { 335 return false; 336 } 337 338 // Hiding a parent element will hide all its children 339 if (element.parentNode != element.ownerDocument) { 340 return BrowserTestUtils.isVisible(element.parentNode); 341 } 342 343 return true; 344 }, 345 346 /** 347 * If the argument is a browsingContext, return it. If the 348 * argument is a browser/frame, returns the browsing context for it. 349 */ 350 getBrowsingContextFrom(browser) { 351 if (Element.isInstance(browser)) { 352 return browser.browsingContext; 353 } 354 355 return browser; 356 }, 357 358 /** 359 * Switches to a tab and resolves when it is ready. 360 * 361 * @param {tabbrowser} tabbrowser 362 * The tabbrowser. 363 * @param {tab} tab 364 * Either a tab element to switch to or a function to perform the switch. 365 * 366 * @return {Promise} 367 * Resolves when the tab has been switched to. 368 */ 369 switchTab(tabbrowser, tab) { 370 let startTime = ChromeUtils.now(); 371 let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild; 372 373 // Some tests depend on the delay and TabSwitched only fires if the browser is visible. 374 // Bug 1977993 tracks always dispatching TabSwitched. 375 let switchEvent = 376 Services.prefs.getBoolPref("test.wait300msAfterTabSwitch", false) || 377 tabbrowser.ownerDocument.hidden 378 ? "TabSwitchDone" 379 : "TabSwitched"; 380 381 let promise = new Promise(resolve => { 382 tabbrowser.addEventListener( 383 switchEvent, 384 function () { 385 TestUtils.executeSoon(() => { 386 ChromeUtils.addProfilerMarker( 387 "BrowserTestUtils", 388 { category: "Test", startTime, innerWindowId }, 389 "switchTab" 390 ); 391 resolve(tabbrowser.selectedTab); 392 }); 393 }, 394 { once: true } 395 ); 396 }); 397 398 if (typeof tab == "function") { 399 tab(); 400 } else { 401 tabbrowser.selectedTab = tab; 402 } 403 return promise; 404 }, 405 406 /** 407 * Waits for an ongoing page load in a browser window to complete. By default 408 * about:blank loads are ignored. 409 * 410 * This can be used in conjunction with any synchronous method for starting a 411 * load, like the "addTab" method on "tabbrowser", and must be called before 412 * yielding control to the event loop. 413 * 414 * Note that calling this after multiple successive load operations can be racy, 415 * so ``wantLoad`` should be specified in these cases. The same holds if we're 416 * interested in about:blank to load. 417 * 418 * This function works by listening for custom load events on ``browser``. These 419 * are sent by a BrowserTestUtils window actor in response to "load" and 420 * "DOMContentLoaded" content events. 421 * 422 * @param {xul:browser} browser 423 * A xul:browser. 424 * @param {object} options 425 * @param {boolean} [options.includeSubFrames = false] 426 * A boolean indicating if loads from subframes should be included. 427 * @param {string|function} [options.wantLoad] 428 * If a function, takes a URL and returns true if that's the load we're 429 * interested in. If a string, gives the URL of the load we're interested 430 * in. If not present, the first non-about:blank load resolves the promise. 431 * @param {boolean} [options.maybeErrorPage = false] 432 * If true, this uses DOMContentLoaded event instead of load event. 433 * Also wantLoad will be called with visible URL, instead of 434 * 'about:neterror?...' for error page. 435 * 436 * @return {Promise} 437 * Resovles when a load event is triggered for the browser. 438 */ 439 browserLoaded(browser, ...args) { 440 const options = 441 args.length && typeof args[0] === "object" 442 ? args[0] 443 : { 444 includeSubFrames: args[0] ?? false, 445 wantLoad: args[1] ?? null, 446 maybeErrorPage: args[2] ?? false, 447 }; 448 const { 449 includeSubFrames = false, 450 wantLoad = null, 451 maybeErrorPage = false, 452 } = options; 453 let startTime = ChromeUtils.now(); 454 let { innerWindowId } = browser.ownerGlobal.windowGlobalChild; 455 456 // Passing a url as second argument is a common mistake we should prevent. 457 if (includeSubFrames && typeof includeSubFrames != "boolean") { 458 throw new Error( 459 "The second argument to browserLoaded should be a boolean." 460 ); 461 } 462 463 // Consumers may pass gBrowser instead of a browser, so adjust for that. 464 if ("selectedBrowser" in browser) { 465 browser = browser.selectedBrowser; 466 } 467 468 // If browser belongs to tabbrowser-tab, ensure it has been 469 // inserted into the document. 470 let tabbrowser = browser.ownerGlobal.gBrowser; 471 if (tabbrowser && tabbrowser.getTabForBrowser) { 472 let tab = tabbrowser.getTabForBrowser(browser); 473 if (tab) { 474 tabbrowser._insertBrowser(tab); 475 } 476 } 477 478 function isWanted(url) { 479 if (!wantLoad) { 480 return !url.startsWith("about:blank"); 481 } else if (typeof wantLoad == "function") { 482 return wantLoad(url); 483 } 484 485 // HTTPS-First (Bug 1704453) TODO: In case we are waiting 486 // for an http:// URL to be loaded and https-first is enabled, 487 // then we also return true in case the backend upgraded 488 // the load to https://. 489 if ( 490 BrowserTestUtils._httpsFirstEnabled && 491 typeof wantLoad == "string" && 492 wantLoad.startsWith("http://") 493 ) { 494 let wantLoadHttps = wantLoad.replace("http://", "https://"); 495 if (wantLoadHttps == url) { 496 return true; 497 } 498 } 499 500 // It's a string. 501 return wantLoad == url; 502 } 503 504 // Error pages are loaded slightly differently, so listen for the 505 // DOMContentLoaded event for those instead. 506 let loadEvent = maybeErrorPage ? "DOMContentLoaded" : "load"; 507 let eventName = `BrowserTestUtils:ContentEvent:${loadEvent}`; 508 509 return new Promise((resolve, reject) => { 510 function listener(event) { 511 switch (event.type) { 512 case eventName: { 513 let { browsingContext, internalURL, visibleURL } = event.detail; 514 515 // Sometimes we arrive here without an internalURL. If that's the 516 // case, just keep waiting until we get one. 517 if (!internalURL) { 518 return; 519 } 520 521 // Ignore subframes if we only care about the top-level load. 522 let subframe = browsingContext !== browsingContext.top; 523 if (subframe && !includeSubFrames) { 524 return; 525 } 526 527 // See testing/mochitest/BrowserTestUtils/content/BrowserTestUtilsChild.sys.mjs 528 // for the difference between visibleURL and internalURL. 529 if (!isWanted(maybeErrorPage ? visibleURL : internalURL)) { 530 return; 531 } 532 533 ChromeUtils.addProfilerMarker( 534 "BrowserTestUtils", 535 { startTime, category: "Test", innerWindowId }, 536 "browserLoaded: " + internalURL 537 ); 538 resolve(internalURL); 539 break; 540 } 541 542 case "unload": 543 reject( 544 new Error( 545 "The window unloaded while we were waiting for the browser to load - this should never happen." 546 ) 547 ); 548 break; 549 550 default: 551 return; 552 } 553 554 browser.removeEventListener(eventName, listener, true); 555 browser.ownerGlobal.removeEventListener("unload", listener); 556 } 557 558 browser.addEventListener(eventName, listener, true); 559 browser.ownerGlobal.addEventListener("unload", listener); 560 }); 561 }, 562 563 /** 564 * Waits for the selected browser to load in a new window. This 565 * is most useful when you've got a window that might not have 566 * loaded its DOM yet, and where you can't easily use browserLoaded 567 * on gBrowser.selectedBrowser since gBrowser doesn't yet exist. 568 * 569 * @param {xul:window} window 570 * A newly opened window for which we're waiting for the 571 * first browser load. 572 * @param {boolean} aboutBlank [optional] 573 * If false, about:blank loads are ignored and we continue 574 * to wait. 575 * @param {function|null} checkFn [optional] 576 * If checkFn(browser) returns false, the load is ignored 577 * and we continue to wait. 578 * 579 * @return {Promise<Event>} 580 * Resolves to the fired load event. 581 */ 582 firstBrowserLoaded(win, aboutBlank = true, checkFn = null) { 583 return this.waitForEvent( 584 win, 585 "BrowserTestUtils:ContentEvent:load", 586 true, 587 event => { 588 if (checkFn) { 589 return checkFn(event.target); 590 } 591 return ( 592 win.gBrowser.selectedBrowser.currentURI.spec !== "about:blank" || 593 aboutBlank 594 ); 595 } 596 ); 597 }, 598 599 _webProgressListeners: new Set(), 600 601 _contentEventListenerSharedState: new Map(), 602 603 _contentEventListeners: new Map(), 604 605 /** 606 * Waits for the web progress listener associated with this tab to fire a 607 * state change that matches checkFn for the toplevel document. 608 * 609 * @param {xul:browser} browser 610 * A xul:browser. 611 * @param {string} expectedURI (optional) 612 * A specific URL to check the channel load against 613 * @param {Function} checkFn 614 * If checkFn(aStateFlags, aStatus) returns false, the state change 615 * is ignored and we continue to wait. 616 * 617 * @return {Promise<void>} 618 * Resolves when the desired state change reaches the tab's progress listener. 619 */ 620 waitForBrowserStateChange(browser, expectedURI, checkFn) { 621 return new Promise(resolve => { 622 let wpl = { 623 onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { 624 dump( 625 "Saw state " + 626 aStateFlags.toString(16) + 627 " and status " + 628 aStatus.toString(16) + 629 "\n" 630 ); 631 if (checkFn(aStateFlags, aStatus) && aWebProgress.isTopLevel) { 632 let chan = aRequest.QueryInterface(Ci.nsIChannel); 633 dump( 634 "Browser got expected state change " + 635 chan.originalURI.spec + 636 "\n" 637 ); 638 if (!expectedURI || chan.originalURI.spec == expectedURI) { 639 browser.removeProgressListener(wpl); 640 BrowserTestUtils._webProgressListeners.delete(wpl); 641 resolve(); 642 } 643 } 644 }, 645 onSecurityChange() {}, 646 onStatusChange() {}, 647 onLocationChange() {}, 648 onContentBlockingEvent() {}, 649 QueryInterface: ChromeUtils.generateQI([ 650 "nsIWebProgressListener", 651 "nsIWebProgressListener2", 652 "nsISupportsWeakReference", 653 ]), 654 }; 655 browser.addProgressListener(wpl); 656 this._webProgressListeners.add(wpl); 657 dump( 658 "Waiting for browser state change" + 659 (expectedURI ? " of " + expectedURI : "") + 660 "\n" 661 ); 662 }); 663 }, 664 665 /** 666 * Waits for the web progress listener associated with this tab to fire a 667 * STATE_STOP for the toplevel document. 668 * 669 * @param {xul:browser} browser 670 * A xul:browser. 671 * @param {string} expectedURI (optional) 672 * A specific URL to check the channel load against 673 * @param {boolean} checkAborts (optional, defaults to false) 674 * Whether NS_BINDING_ABORTED stops 'count' as 'real' stops 675 * (e.g. caused by the stop button or equivalent APIs) 676 * 677 * @return {Promise<void>} 678 * Resolves when STATE_STOP reaches the tab's progress listener. 679 */ 680 browserStopped(browser, expectedURI, checkAborts = false) { 681 let testFn = function (aStateFlags, aStatus) { 682 return ( 683 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && 684 aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && 685 (checkAborts || aStatus != Cr.NS_BINDING_ABORTED) 686 ); 687 }; 688 dump( 689 "Waiting for browser load" + 690 (expectedURI ? " of " + expectedURI : "") + 691 "\n" 692 ); 693 return BrowserTestUtils.waitForBrowserStateChange( 694 browser, 695 expectedURI, 696 testFn 697 ); 698 }, 699 700 /** 701 * Waits for the web progress listener associated with this tab to fire a 702 * STATE_START for the toplevel document. 703 * 704 * @param {xul:browser} browser 705 * A xul:browser. 706 * @param {string} expectedURI (optional) 707 * A specific URL to check the channel load against 708 * 709 * @return {Promise<void>} 710 * Resolves when STATE_START reaches the tab's progress listener 711 */ 712 browserStarted(browser, expectedURI) { 713 let testFn = function (aStateFlags) { 714 return ( 715 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && 716 aStateFlags & Ci.nsIWebProgressListener.STATE_START 717 ); 718 }; 719 dump( 720 "Waiting for browser to start load" + 721 (expectedURI ? " of " + expectedURI : "") + 722 "\n" 723 ); 724 return BrowserTestUtils.waitForBrowserStateChange( 725 browser, 726 expectedURI, 727 testFn 728 ); 729 }, 730 731 /** 732 * Waits for a tab to open and load a given URL. 733 * 734 * By default, the method doesn't wait for the tab contents to load. 735 * 736 * @param {tabbrowser} tabbrowser 737 * The tabbrowser to look for the next new tab in. 738 * @param {string|function} [wantLoad = null] 739 * If a function, takes a URL and returns true if that's the load we're 740 * interested in. If a string, gives the URL of the load we're interested 741 * in. If not present, the first non-about:blank load is used. 742 * @param {boolean} [waitForLoad = false] 743 * True to wait for the page in the new tab to load. Defaults to false. 744 * @param {boolean} [waitForAnyTab = false] 745 * True to wait for the url to be loaded in any new tab, not just the next 746 * one opened. 747 * @param {boolean} [maybeErrorPage = false] 748 * See ``browserLoaded`` function. 749 * 750 * @return {Promise} 751 * Resolves with the {xul:tab} when a tab is opened and its location changes 752 * to the given URL and optionally that browser has loaded. 753 * 754 * NB: this method will not work if you open a new tab with e.g. BrowserCommands.openTab 755 * and the tab does not load a URL, because no onLocationChange will fire. 756 */ 757 waitForNewTab( 758 tabbrowser, 759 wantLoad = null, 760 waitForLoad = false, 761 waitForAnyTab = false, 762 maybeErrorPage = false 763 ) { 764 let urlMatches; 765 if (wantLoad && typeof wantLoad == "function") { 766 urlMatches = wantLoad; 767 } else if (wantLoad) { 768 urlMatches = urlToMatch => urlToMatch == wantLoad; 769 } else { 770 urlMatches = urlToMatch => urlToMatch != "about:blank"; 771 } 772 return new Promise(resolve => { 773 tabbrowser.tabContainer.addEventListener( 774 "TabOpen", 775 function tabOpenListener(openEvent) { 776 if (!waitForAnyTab) { 777 tabbrowser.tabContainer.removeEventListener( 778 "TabOpen", 779 tabOpenListener 780 ); 781 } 782 let newTab = openEvent.target; 783 if (wantLoad == "about:blank") { 784 TestUtils.executeSoon(() => resolve(newTab)); 785 return; 786 } 787 let newBrowser = newTab.linkedBrowser; 788 let result; 789 if (waitForLoad) { 790 // If waiting for load, resolve with promise for that, which when load 791 // completes resolves to the new tab. 792 result = BrowserTestUtils.browserLoaded(newBrowser, { 793 includeSubFrames: false, 794 wantLoad: urlMatches, 795 maybeErrorPage, 796 }).then(() => newTab); 797 } else { 798 // If not waiting for load, just resolve with the new tab. 799 result = newTab; 800 } 801 802 let progressListener = { 803 onLocationChange(aBrowser) { 804 // Only interested in location changes on our browser. 805 if (aBrowser != newBrowser) { 806 return; 807 } 808 809 // Check that new location is the URL we want. 810 if (!urlMatches(aBrowser.currentURI.spec)) { 811 return; 812 } 813 if (waitForAnyTab) { 814 tabbrowser.tabContainer.removeEventListener( 815 "TabOpen", 816 tabOpenListener 817 ); 818 } 819 tabbrowser.removeTabsProgressListener(progressListener); 820 TestUtils.executeSoon(() => resolve(result)); 821 }, 822 }; 823 tabbrowser.addTabsProgressListener(progressListener); 824 } 825 ); 826 }); 827 }, 828 829 /** 830 * Waits for onLocationChange. 831 * 832 * @param {tabbrowser} tabbrowser 833 * The tabbrowser to wait for the location change on. 834 * @param {string} [url] 835 * The string URL to look for. The URL must match the URL in the 836 * location bar exactly. 837 * @return {Promise<{webProgress: nsIWebProgress, request: nsIRequest, flags: number}>} 838 */ 839 waitForLocationChange(tabbrowser, url) { 840 return new Promise(resolve => { 841 let progressListener = { 842 onLocationChange(browser, webProgress, request, newURI, flags) { 843 if ( 844 (url && newURI.spec != url) || 845 (!url && newURI.spec == "about:blank") 846 ) { 847 return; 848 } 849 850 tabbrowser.removeTabsProgressListener(progressListener); 851 resolve({ webProgress, request, flags }); 852 }, 853 }; 854 tabbrowser.addTabsProgressListener(progressListener); 855 }); 856 }, 857 858 /** 859 * Waits for the next browser window to open and be fully loaded. 860 * 861 * @param {object} aParams 862 * @param {string} [aParams.url] 863 * The url to await being loaded. If unset this may or may not wait for 864 * any page to be loaded, according to the waitForAnyURLLoaded param. 865 * @param {bool} [aParams.waitForAnyURLLoaded] When `url` is unset, this 866 * controls whether to wait for any initial URL to be loaded. 867 * Defaults to false, that means the initial browser may or may not 868 * have finished loading its first page when this resolves. 869 * When `url` is set, this is ignored, thus the load is always awaited for. 870 * @param {bool} [aParams.anyWindow] 871 * @param {bool} [aParams.maybeErrorPage] 872 * See ``browserLoaded`` function. 873 * @return {Promise} 874 * A Promise which resolves the next time that a DOM window 875 * opens and the delayed startup observer notification fires. 876 */ 877 waitForNewWindow(aParams = {}) { 878 let { 879 url = null, 880 anyWindow = false, 881 maybeErrorPage = false, 882 waitForAnyURLLoaded = false, 883 } = aParams; 884 885 if (anyWindow && !url) { 886 throw new Error("url should be specified if anyWindow is true"); 887 } 888 889 return new Promise((resolve, reject) => { 890 let observe = async (win, topic) => { 891 if (topic != "domwindowopened") { 892 return; 893 } 894 895 try { 896 if (!anyWindow) { 897 Services.ww.unregisterNotification(observe); 898 } 899 900 // Add these event listeners now since they may fire before the 901 // DOMContentLoaded event down below. 902 let promises = [ 903 this.waitForEvent(win, "focus", true), 904 this.waitForEvent(win, "activate"), 905 ]; 906 907 if (url || waitForAnyURLLoaded) { 908 await this.waitForEvent(win, "DOMContentLoaded"); 909 910 if (win.document.documentURI != AppConstants.BROWSER_CHROME_URL) { 911 return; 912 } 913 } 914 915 if (!(win.gBrowserInit && win.gBrowserInit.delayedStartupFinished)) { 916 promises.push( 917 TestUtils.topicObserved( 918 "browser-delayed-startup-finished", 919 subject => subject == win 920 ) 921 ); 922 } 923 924 if (url || waitForAnyURLLoaded) { 925 let loadPromise = this.browserLoaded(win.gBrowser.selectedBrowser, { 926 includeSubFrames: false, 927 wantLoad: waitForAnyURLLoaded ? null : url, 928 maybeErrorPage, 929 }); 930 promises.push(loadPromise); 931 } 932 933 await Promise.all(promises); 934 935 if (anyWindow) { 936 Services.ww.unregisterNotification(observe); 937 } 938 resolve(win); 939 } catch (err) { 940 // We failed to wait for the load in this URI. This is only an error 941 // if `anyWindow` is not set, as if it is we can just wait for another 942 // window. 943 if (!anyWindow) { 944 reject(err); 945 } 946 } 947 }; 948 Services.ww.registerNotification(observe); 949 }); 950 }, 951 952 /** 953 * Starts the load of a new URI in the given browser, triggered by the system 954 * principal. 955 * Note this won't want for the load to be complete. For that you may either 956 * use BrowserTestUtils.browserLoaded(), BrowserTestUtils.waitForErrorPage(), 957 * or make your own handler. 958 * 959 * @param {xul:browser} browser 960 * A xul:browser. 961 * @param {string} uri 962 * The URI to load. 963 * @param {number} loadFlags [optional] 964 * Load flags to pass to nsIWebNavigation.loadURI. 965 */ 966 startLoadingURIString(browser, uri, loadFlags) { 967 browser.fixupAndLoadURIString(uri, { 968 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 969 loadFlags, 970 }); 971 }, 972 973 /** 974 * Loads a given URI in the specified tab and waits for the load to complete. 975 * 976 * @param {object} options 977 * @param {xul:browser} options.browser 978 * The browser to load the URI into. 979 * @param {string} options.uriString 980 * The string URI to load. 981 * @param {string} [options.finalURI] 982 * The expected final URI to wait for, e.g. if the load is automatically 983 * redirected. 984 */ 985 loadURIString({ browser, uriString, finalURI = uriString }) { 986 this.startLoadingURIString(browser, uriString); 987 return this.browserLoaded(browser, { wantLoad: finalURI }); 988 }, 989 990 /** 991 * Maybe create a preloaded browser and ensure it's finished loading. 992 * 993 * @param gBrowser (<xul:tabbrowser>) 994 * The tabbrowser in which to preload a browser. 995 */ 996 async maybeCreatePreloadedBrowser(gBrowser) { 997 let win = gBrowser.ownerGlobal; 998 win.NewTabPagePreloading.maybeCreatePreloadedBrowser(win); 999 1000 // We cannot use the regular BrowserTestUtils helper for waiting here, since that 1001 // would try to insert the preloaded browser, which would only break things. 1002 await lazy.ContentTask.spawn(gBrowser.preloadedBrowser, [], async () => { 1003 await ContentTaskUtils.waitForCondition(() => { 1004 return ( 1005 this.content.document && 1006 this.content.document.readyState == "complete" 1007 ); 1008 }); 1009 }); 1010 }, 1011 1012 /** 1013 * @param win (optional) 1014 * The window we should wait to have "domwindowopened" sent through 1015 * the observer service for. If this is not supplied, we'll just 1016 * resolve when the first "domwindowopened" notification is seen. 1017 * @param {function} checkFn [optional] 1018 * Called with the nsIDOMWindow object as argument, should return true 1019 * if the event is the expected one, or false if it should be ignored 1020 * and observing should continue. If not specified, the first window 1021 * resolves the returned promise. 1022 * @return {Promise} 1023 * A Promise which resolves when a "domwindowopened" notification 1024 * has been fired by the window watcher. 1025 */ 1026 domWindowOpened(win, checkFn) { 1027 return new Promise(resolve => { 1028 async function observer(subject, topic) { 1029 if (topic == "domwindowopened" && (!win || subject === win)) { 1030 let observedWindow = subject; 1031 if (checkFn && !(await checkFn(observedWindow))) { 1032 return; 1033 } 1034 Services.ww.unregisterNotification(observer); 1035 resolve(observedWindow); 1036 } 1037 } 1038 Services.ww.registerNotification(observer); 1039 }); 1040 }, 1041 1042 /** 1043 * @param win (optional) 1044 * The window we should wait to have "domwindowopened" sent through 1045 * the observer service for. If this is not supplied, we'll just 1046 * resolve when the first "domwindowopened" notification is seen. 1047 * The promise will be resolved once the new window's document has been 1048 * loaded. 1049 * 1050 * @param {function} checkFn (optional) 1051 * Called with the nsIDOMWindow object as argument, should return true 1052 * if the event is the expected one, or false if it should be ignored 1053 * and observing should continue. If not specified, the first window 1054 * resolves the returned promise. 1055 * 1056 * @return {Promise} 1057 * A Promise which resolves when a "domwindowopened" notification 1058 * has been fired by the window watcher. 1059 */ 1060 domWindowOpenedAndLoaded(win, checkFn) { 1061 return this.domWindowOpened(win, async observedWin => { 1062 await this.waitForEvent(observedWin, "load"); 1063 if (checkFn && !(await checkFn(observedWin))) { 1064 return false; 1065 } 1066 return true; 1067 }); 1068 }, 1069 1070 /** 1071 * @param win (optional) 1072 * The window we should wait to have "domwindowclosed" sent through 1073 * the observer service for. If this is not supplied, we'll just 1074 * resolve when the first "domwindowclosed" notification is seen. 1075 * @return {Promise} 1076 * A Promise which resolves when a "domwindowclosed" notification 1077 * has been fired by the window watcher. 1078 */ 1079 domWindowClosed(win) { 1080 return new Promise(resolve => { 1081 function observer(subject, topic) { 1082 if (topic == "domwindowclosed" && (!win || subject === win)) { 1083 Services.ww.unregisterNotification(observer); 1084 resolve(subject); 1085 } 1086 } 1087 Services.ww.registerNotification(observer); 1088 }); 1089 }, 1090 1091 /** 1092 * Open a new browser window from an existing one. 1093 * This relies on OpenBrowserWindow in browser.js, and waits for the window 1094 * to be completely loaded before resolving. 1095 * 1096 * @param {object} [options] 1097 * Options to pass to OpenBrowserWindow. Additionally, supports: 1098 * @param {bool} [options.waitForTabURL] 1099 * Forces the initial browserLoaded check to wait for the tab to 1100 * load the given URL (instead of about:blank) 1101 * 1102 * @return {Promise} 1103 * Resolves with the new window once it is loaded. 1104 */ 1105 async openNewBrowserWindow(options = {}) { 1106 let startTime = ChromeUtils.now(); 1107 1108 let openerWindow = lazy.BrowserWindowTracker.getTopWindow({ 1109 private: false, 1110 }); 1111 let win = lazy.BrowserWindowTracker.openWindow({ 1112 openerWindow, 1113 ...options, 1114 }); 1115 1116 let promises = [ 1117 this.waitForEvent(win, "focus", true), 1118 this.waitForEvent(win, "activate"), 1119 ]; 1120 1121 // Wait for browser-delayed-startup-finished notification, it indicates 1122 // that the window has loaded completely and is ready to be used for 1123 // testing. 1124 promises.push( 1125 TestUtils.topicObserved( 1126 "browser-delayed-startup-finished", 1127 subject => subject == win 1128 ).then(() => win) 1129 ); 1130 1131 promises.push( 1132 this.firstBrowserLoaded(win, !options.waitForTabURL, browser => { 1133 return ( 1134 !options.waitForTabURL || 1135 options.waitForTabURL == browser.currentURI.spec 1136 ); 1137 }) 1138 ); 1139 1140 await Promise.all(promises); 1141 ChromeUtils.addProfilerMarker( 1142 "BrowserTestUtils", 1143 { startTime, category: "Test" }, 1144 "openNewBrowserWindow" 1145 ); 1146 1147 return win; 1148 }, 1149 1150 /** 1151 * Closes a window. 1152 * 1153 * @param {Window} win 1154 * A window to close. 1155 * 1156 * @return {Promise} 1157 * Resolves when the provided window has been closed. For browser 1158 * windows, the Promise will also wait until all final SessionStore 1159 * messages have been sent up from all browser tabs. 1160 */ 1161 closeWindow(win) { 1162 let closedPromise = BrowserTestUtils.windowClosed(win); 1163 win.close(); 1164 return closedPromise; 1165 }, 1166 1167 /** 1168 * Returns a Promise that resolves when a window has finished closing. 1169 * 1170 * @param {Window} win 1171 * The closing window. 1172 * 1173 * @return {Promise} 1174 * Resolves when the provided window has been fully closed. For 1175 * browser windows, the Promise will also wait until all final 1176 * SessionStore messages have been sent up from all browser tabs. 1177 */ 1178 windowClosed(win) { 1179 let domWinClosedPromise = BrowserTestUtils.domWindowClosed(win); 1180 let promises = [domWinClosedPromise]; 1181 let winType = win.document.documentElement.getAttribute("windowtype"); 1182 let flushTopic = "sessionstore-browser-shutdown-flush"; 1183 1184 if (winType == "navigator:browser") { 1185 let finalMsgsPromise = new Promise(resolve => { 1186 let browserSet = new Set(win.gBrowser.browsers); 1187 // Ensure all browsers have been inserted or we won't get 1188 // messages back from them. 1189 browserSet.forEach(browser => { 1190 win.gBrowser._insertBrowser(win.gBrowser.getTabForBrowser(browser)); 1191 }); 1192 1193 let observer = subject => { 1194 if (browserSet.has(subject)) { 1195 browserSet.delete(subject); 1196 } 1197 if (!browserSet.size) { 1198 Services.obs.removeObserver(observer, flushTopic); 1199 // Give the TabStateFlusher a chance to react to this final 1200 // update and for the TabStateFlusher.flushWindow promise 1201 // to resolve before we resolve. 1202 TestUtils.executeSoon(resolve); 1203 } 1204 }; 1205 1206 Services.obs.addObserver(observer, flushTopic); 1207 }); 1208 1209 promises.push(finalMsgsPromise); 1210 } 1211 1212 return Promise.all(promises); 1213 }, 1214 1215 /** 1216 * Returns a Promise that resolves once the SessionStore information for the 1217 * given tab is updated and all listeners are called. 1218 * 1219 * @param {xul:tab} tab 1220 * The tab that will be removed. 1221 * @returns {Promise<void>} 1222 * Resolves when the SessionStore information is updated. 1223 */ 1224 waitForSessionStoreUpdate(tab) { 1225 let browser = tab.linkedBrowser; 1226 return TestUtils.topicObserved( 1227 "sessionstore-browser-shutdown-flush", 1228 s => s === browser 1229 ); 1230 }, 1231 1232 /** 1233 * @returns {Promise<void>} 1234 * Resolves when the locale has been changed. 1235 */ 1236 enableRtlLocale() { 1237 let localeChanged = TestUtils.topicObserved("intl:app-locales-changed"); 1238 Services.prefs.setStringPref("intl.l10n.pseudo", "bidi"); 1239 return localeChanged; 1240 }, 1241 1242 /** 1243 * @returns {Promise<void>} 1244 * Resolves when the locale has been changed. 1245 */ 1246 disableRtlLocale() { 1247 let localeChanged = TestUtils.topicObserved("intl:app-locales-changed"); 1248 Services.prefs.setStringPref("intl.l10n.pseudo", ""); 1249 return localeChanged; 1250 }, 1251 1252 /** 1253 * Waits for an event to be fired on a specified element. 1254 * 1255 * @example 1256 * 1257 * let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName"); 1258 * // Do some processing here that will cause the event to be fired 1259 * // ... 1260 * // Now wait until the Promise is fulfilled 1261 * let receivedEvent = await promiseEvent; 1262 * 1263 * @example 1264 * // The promise resolution/rejection handler for the returned promise is 1265 * // guaranteed not to be called until the next event tick after the event 1266 * // listener gets called, so that all other event listeners for the element 1267 * // are executed before the handler is executed. 1268 * 1269 * let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName"); 1270 * // Same event tick here. 1271 * await promiseEvent; 1272 * // Next event tick here. 1273 * 1274 * @example 1275 * // If some code, such like adding yet another event listener, needs to be 1276 * // executed in the same event tick, use raw addEventListener instead and 1277 * // place the code inside the event listener. 1278 * 1279 * element.addEventListener("load", () => { 1280 * // Add yet another event listener in the same event tick as the load 1281 * // event listener. 1282 * p = BrowserTestUtils.waitForEvent(element, "ready"); 1283 * }, { once: true }); 1284 * 1285 * @param {Element} subject 1286 * The element that should receive the event. 1287 * @param {string} eventName 1288 * Name of the event to listen to. 1289 * @param {bool} [capture] 1290 * True to use a capturing listener. 1291 * @param {function} [checkFn] 1292 * Called with the Event object as argument, should return true if the 1293 * event is the expected one, or false if it should be ignored and 1294 * listening should continue. If not specified, the first event with 1295 * the specified name resolves the returned promise. 1296 * @param {bool} [wantsUntrusted=false] 1297 * True to receive synthetic events dispatched by web content. 1298 * 1299 * Note: Because this function is intended for testing, any error in checkFn 1300 * will cause the returned promise to be rejected instead of waiting for 1301 * the next event, since this is probably a bug in the test. 1302 * 1303 * @returns {Promise<Event>} 1304 */ 1305 waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted) { 1306 let startTime = ChromeUtils.now(); 1307 let innerWindowId = subject.ownerGlobal?.windowGlobalChild.innerWindowId; 1308 1309 return new Promise((resolve, reject) => { 1310 let removed = false; 1311 function listener(event) { 1312 function cleanup() { 1313 removed = true; 1314 // Avoid keeping references to objects after the promise resolves. 1315 subject = null; 1316 checkFn = null; 1317 } 1318 try { 1319 if (checkFn && !checkFn(event)) { 1320 return; 1321 } 1322 subject.removeEventListener(eventName, listener, capture); 1323 cleanup(); 1324 TestUtils.executeSoon(() => { 1325 ChromeUtils.addProfilerMarker( 1326 "BrowserTestUtils", 1327 { startTime, category: "Test", innerWindowId }, 1328 "waitForEvent: " + eventName 1329 ); 1330 resolve(event); 1331 }); 1332 } catch (ex) { 1333 try { 1334 subject.removeEventListener(eventName, listener, capture); 1335 } catch (ex2) { 1336 // Maybe the provided object does not support removeEventListener. 1337 } 1338 cleanup(); 1339 TestUtils.executeSoon(() => reject(ex)); 1340 } 1341 } 1342 1343 subject.addEventListener(eventName, listener, capture, wantsUntrusted); 1344 1345 TestUtils.promiseTestFinished?.then(() => { 1346 if (removed) { 1347 return; 1348 } 1349 1350 subject.removeEventListener(eventName, listener, capture); 1351 let text = eventName + " listener"; 1352 if (subject.id) { 1353 text += ` on #${subject.id}`; 1354 } 1355 text += " not removed before the end of test"; 1356 reject(text); 1357 ChromeUtils.addProfilerMarker( 1358 "BrowserTestUtils", 1359 { startTime, category: "Test", innerWindowId }, 1360 "waitForEvent: " + text 1361 ); 1362 }); 1363 }); 1364 }, 1365 1366 /** 1367 * Like waitForEvent, but adds the event listener to the message manager 1368 * global for browser. 1369 * 1370 * @param {string} eventName 1371 * Name of the event to listen to. 1372 * @param {bool} capture [optional] 1373 * Whether to use a capturing listener. 1374 * @param {function} checkFn [optional] 1375 * Called with the Event object as argument, should return true if the 1376 * event is the expected one, or false if it should be ignored and 1377 * listening should continue. If not specified, the first event with 1378 * the specified name resolves the returned promise. 1379 * @param {bool} wantUntrusted [optional] 1380 * Whether to accept untrusted events 1381 * 1382 * Note: As of bug 1588193, this function no longer rejects the returned 1383 * promise in the case of a checkFn error. Instead, since checkFn is now 1384 * called through eval in the content process, the error is thrown in 1385 * the listener created by ContentEventListenerChild. Work to improve 1386 * error handling (eg. to reject the promise as before and to preserve 1387 * the filename/stack) is being tracked in bug 1593811. 1388 * 1389 * @returns {Promise<string>} 1390 * Resolves with the event name. 1391 */ 1392 waitForContentEvent( 1393 browser, 1394 eventName, 1395 capture = false, 1396 checkFn, 1397 wantUntrusted = false 1398 ) { 1399 return new Promise(resolve => { 1400 let removeEventListener = this.addContentEventListener( 1401 browser, 1402 eventName, 1403 () => { 1404 removeEventListener(); 1405 resolve(eventName); 1406 }, 1407 { capture, wantUntrusted }, 1408 checkFn 1409 ); 1410 }); 1411 }, 1412 1413 /** 1414 * Like waitForEvent, but acts on a popup. It ensures the popup is not already 1415 * in the expected state. 1416 * 1417 * @param {Element} popup 1418 * The popup element that should receive the event. 1419 * @param {string} eventSuffix 1420 * The event suffix expected to be received, one of "shown" or "hidden". 1421 * @returns {Promise} 1422 */ 1423 waitForPopupEvent(popup, eventSuffix) { 1424 let endState = { shown: "open", hidden: "closed" }[eventSuffix]; 1425 1426 if (popup.state == endState) { 1427 return Promise.resolve(); 1428 } 1429 return this.waitForEvent(popup, "popup" + eventSuffix); 1430 }, 1431 1432 /** 1433 * Waits for the select popup to be shown. This is needed because the select 1434 * dropdown is created lazily. 1435 * 1436 * @param {Window} win 1437 * A window to expect the popup in. 1438 * 1439 * @return {Promise} 1440 * Resolves when the popup has been fully opened. The resolution value 1441 * is the select popup. 1442 */ 1443 async waitForSelectPopupShown(win) { 1444 let getMenulist = () => 1445 win.document.getElementById("ContentSelectDropdown"); 1446 let menulist = getMenulist(); 1447 if (!menulist) { 1448 await this.waitForMutationCondition( 1449 win.document, 1450 { childList: true, subtree: true }, 1451 getMenulist 1452 ); 1453 menulist = getMenulist(); 1454 if (menulist.menupopup.state == "open") { 1455 return menulist.menupopup; 1456 } 1457 } 1458 await this.waitForEvent(menulist.menupopup, "popupshown"); 1459 return menulist.menupopup; 1460 }, 1461 1462 /** 1463 * Waits for the datetime picker popup to be shown. 1464 * 1465 * @param {Window} win 1466 * A window to expect the popup in. 1467 * 1468 * @return {Promise} 1469 * Resolves when the popup has been fully opened. The resolution value 1470 * is the select popup. 1471 */ 1472 async waitForDateTimePickerPanelShown(win) { 1473 let getPanel = () => win.document.getElementById("DateTimePickerPanel"); 1474 let panel = getPanel(); 1475 let ensureReady = async () => { 1476 let frame = panel.querySelector("#DateTimePickerPanelPopupFrame"); 1477 let isValidUrl = () => { 1478 return ( 1479 frame.browsingContext?.currentURI?.spec == 1480 "chrome://global/content/datetimepicker.xhtml" 1481 ); 1482 }; 1483 1484 // Ensure it's loaded. 1485 if (!isValidUrl() || frame.contentDocument.readyState != "complete") { 1486 await new Promise(resolve => { 1487 frame.addEventListener( 1488 "load", 1489 function listener() { 1490 if (isValidUrl()) { 1491 frame.removeEventListener("load", listener, { capture: true }); 1492 resolve(); 1493 } 1494 }, 1495 { capture: true } 1496 ); 1497 }); 1498 } 1499 1500 // Ensure it's ready. 1501 if (!frame.contentWindow.PICKER_READY) { 1502 await new Promise(resolve => { 1503 frame.contentDocument.addEventListener("PickerReady", resolve, { 1504 once: true, 1505 }); 1506 }); 1507 } 1508 // And that l10n mutations are flushed. 1509 // FIXME(bug 1828721): We should ideally localize everything before 1510 // showing the panel. 1511 if (frame.contentDocument.hasPendingL10nMutations) { 1512 await new Promise(resolve => { 1513 frame.contentDocument.addEventListener( 1514 "L10nMutationsFinished", 1515 resolve, 1516 { 1517 once: true, 1518 } 1519 ); 1520 }); 1521 } 1522 }; 1523 1524 if (!panel) { 1525 await this.waitForMutationCondition( 1526 win.document, 1527 { childList: true, subtree: true }, 1528 getPanel 1529 ); 1530 panel = getPanel(); 1531 if (panel.state == "open") { 1532 await ensureReady(); 1533 return panel; 1534 } 1535 } 1536 await this.waitForEvent(panel, "popupshown"); 1537 await ensureReady(); 1538 return panel; 1539 }, 1540 1541 /** 1542 * Adds a content event listener on the given browser 1543 * element. Similar to waitForContentEvent, but the listener will 1544 * fire until it is removed. A callable object is returned that, 1545 * when called, removes the event listener. Note that this function 1546 * works even if the browser's frameloader is swapped. 1547 * 1548 * @param {xul:browser} browser 1549 * The browser element to listen for events in. 1550 * @param {string} eventName 1551 * Name of the event to listen to. 1552 * @param {function} listener 1553 * Function to call in parent process when event fires. 1554 * Not passed any arguments. 1555 * @param {object} listenerOptions [optional] 1556 * Options to pass to the event listener. 1557 * @param {function} checkFn [optional] 1558 * Called with the Event object as argument, should return true if the 1559 * event is the expected one, or false if it should be ignored and 1560 * listening should continue. If not specified, the first event with 1561 * the specified name resolves the returned promise. This is called 1562 * within the content process and can have no closure environment. 1563 * 1564 * @returns function 1565 * If called, the return value will remove the event listener. 1566 */ 1567 addContentEventListener( 1568 browser, 1569 eventName, 1570 listener, 1571 listenerOptions = {}, 1572 checkFn 1573 ) { 1574 let id = gListenerId++; 1575 let contentEventListeners = this._contentEventListeners; 1576 contentEventListeners.set(id, { 1577 listener, 1578 browserId: browser.browserId, 1579 }); 1580 1581 let eventListenerState = this._contentEventListenerSharedState; 1582 eventListenerState.set(id, { 1583 eventName, 1584 listenerOptions, 1585 checkFnSource: checkFn ? checkFn.toSource() : "", 1586 }); 1587 1588 Services.ppmm.sharedData.set( 1589 "BrowserTestUtils:ContentEventListener", 1590 eventListenerState 1591 ); 1592 Services.ppmm.sharedData.flush(); 1593 1594 let unregisterFunction = function () { 1595 if (!eventListenerState.has(id)) { 1596 return; 1597 } 1598 eventListenerState.delete(id); 1599 contentEventListeners.delete(id); 1600 Services.ppmm.sharedData.set( 1601 "BrowserTestUtils:ContentEventListener", 1602 eventListenerState 1603 ); 1604 Services.ppmm.sharedData.flush(); 1605 }; 1606 return unregisterFunction; 1607 }, 1608 1609 /** 1610 * This is an internal method to be invoked by 1611 * BrowserTestUtilsParent.sys.mjs when a content event we were listening for 1612 * happens. 1613 * 1614 * @private 1615 */ 1616 _receivedContentEventListener(listenerId, browserId) { 1617 let listenerData = this._contentEventListeners.get(listenerId); 1618 if (!listenerData) { 1619 return; 1620 } 1621 if (listenerData.browserId != browserId) { 1622 return; 1623 } 1624 listenerData.listener(); 1625 }, 1626 1627 /** 1628 * This is an internal method that cleans up any state from content event 1629 * listeners. 1630 * 1631 * @private 1632 */ 1633 _cleanupContentEventListeners() { 1634 this._contentEventListeners.clear(); 1635 1636 if (this._contentEventListenerSharedState.size != 0) { 1637 this._contentEventListenerSharedState.clear(); 1638 Services.ppmm.sharedData.set( 1639 "BrowserTestUtils:ContentEventListener", 1640 this._contentEventListenerSharedState 1641 ); 1642 Services.ppmm.sharedData.flush(); 1643 } 1644 1645 if (this._contentEventListenerActorRegistered) { 1646 this._contentEventListenerActorRegistered = false; 1647 ChromeUtils.unregisterWindowActor("ContentEventListener"); 1648 } 1649 }, 1650 1651 observe(subject, topic) { 1652 switch (topic) { 1653 case "test-complete": 1654 this._cleanupContentEventListeners(); 1655 break; 1656 } 1657 }, 1658 1659 /** 1660 * Wait until DOM mutations cause the condition expressed in checkFn 1661 * to pass. 1662 * 1663 * Intended as an easy-to-use alternative to waitForCondition. 1664 * 1665 * @param {Element} target The target in which to observe mutations. 1666 * @param {object} options The options to pass to MutationObserver.observe(); 1667 * @param {function} checkFn Function that returns true when it wants the promise to be 1668 * resolved. 1669 */ 1670 waitForMutationCondition(target, options, checkFn) { 1671 if (checkFn()) { 1672 return Promise.resolve(); 1673 } 1674 return new Promise(resolve => { 1675 let obs = new target.ownerGlobal.MutationObserver(function () { 1676 if (checkFn()) { 1677 obs.disconnect(); 1678 resolve(); 1679 } 1680 }); 1681 obs.observe(target, options); 1682 }); 1683 }, 1684 1685 /** 1686 * Like browserLoaded, but waits for an error page to appear. 1687 * 1688 * @param {xul:browser} browser 1689 * A xul:browser. 1690 * 1691 * @return {Promise<string>} 1692 * Resolves when an error page has been loaded in the browser, with the name 1693 * of the event. 1694 */ 1695 waitForErrorPage(browser) { 1696 return this.waitForContentEvent( 1697 browser, 1698 "AboutNetErrorLoad", 1699 false, 1700 null, 1701 true 1702 ); 1703 }, 1704 1705 /** 1706 * Waits for the next top-level document load in the current browser. The URI 1707 * of the document is compared against expectedURL. The load is then stopped 1708 * before it actually starts. 1709 * 1710 * @param {string} expectedURL 1711 * The URL of the document that is expected to load. 1712 * @param {object} browser 1713 * The browser to wait for. 1714 * @param {function} checkFn (optional) 1715 * Function to run on the channel before stopping it. 1716 * @returns {Promise} 1717 */ 1718 waitForDocLoadAndStopIt(expectedURL, browser, checkFn) { 1719 let isHttp = url => /^https?:/.test(url); 1720 1721 return new Promise(resolve => { 1722 // Redirect non-http URIs to http://mochi.test:8888/, so we can still 1723 // use http-on-before-connect to listen for loads. Since we're 1724 // aborting the load as early as possible, it doesn't matter whether the 1725 // server handles it sensibly or not. However, this also means that this 1726 // helper shouldn't be used to load local URIs (about pages, chrome:// 1727 // URIs, etc). 1728 let proxyFilter; 1729 if (!isHttp(expectedURL)) { 1730 proxyFilter = { 1731 proxyInfo: lazy.ProtocolProxyService.newProxyInfo( 1732 "http", 1733 "mochi.test", 1734 8888, 1735 "", 1736 "", 1737 0, 1738 4096, 1739 null 1740 ), 1741 1742 applyFilter(channel, defaultProxyInfo, callback) { 1743 callback.onProxyFilterResult( 1744 isHttp(channel.URI.spec) ? defaultProxyInfo : this.proxyInfo 1745 ); 1746 }, 1747 }; 1748 1749 lazy.ProtocolProxyService.registerChannelFilter(proxyFilter, 0); 1750 } 1751 1752 function observer(chan) { 1753 chan.QueryInterface(Ci.nsIHttpChannel); 1754 if (!chan.originalURI || chan.originalURI.spec !== expectedURL) { 1755 return; 1756 } 1757 if (checkFn && !checkFn(chan)) { 1758 return; 1759 } 1760 1761 // TODO: We should check that the channel's BrowsingContext matches 1762 // the browser's. See bug 1587114. 1763 1764 try { 1765 chan.cancel(Cr.NS_BINDING_ABORTED); 1766 } finally { 1767 if (proxyFilter) { 1768 lazy.ProtocolProxyService.unregisterChannelFilter(proxyFilter); 1769 } 1770 Services.obs.removeObserver(observer, "http-on-before-connect"); 1771 resolve(); 1772 } 1773 } 1774 1775 Services.obs.addObserver(observer, "http-on-before-connect"); 1776 }); 1777 }, 1778 1779 /** 1780 * Versions of EventUtils.sys.mjs synthesizeMouse functions that synthesize a 1781 * mouse event in a child process and return promises that resolve when the 1782 * event has fired and completed. Instead of a window, a browser or 1783 * browsing context is required to be passed to this function. 1784 * 1785 * @param target 1786 * One of the following: 1787 * - a selector string that identifies the element to target. The syntax is as 1788 * for querySelector. 1789 * - a function to be run in the content process that returns the element to 1790 * target 1791 * - null, in which case the offset is from the content document's edge. 1792 * @param {integer} offsetX 1793 * x offset from target's left bounding edge 1794 * @param {integer} offsetY 1795 * y offset from target's top bounding edge 1796 * @param {object} event object 1797 * Additional arguments, similar to the EventUtils.sys.mjs version 1798 * @param {BrowserContext|MozFrameLoaderOwner} browsingContext 1799 * Browsing context or browser element, must not be null 1800 * @param {boolean} handlingUserInput 1801 * Whether the synthesize should be perfomed while simulating 1802 * user interaction (making windowUtils.isHandlingUserInput be true). 1803 * 1804 * @returns {Promise<boolean>} 1805 * Resolves to true if the mouse event was cancelled. 1806 */ 1807 synthesizeMouse( 1808 target, 1809 offsetX, 1810 offsetY, 1811 event, 1812 browsingContext, 1813 handlingUserInput 1814 ) { 1815 let targetFn = null; 1816 if (typeof target == "function") { 1817 targetFn = target.toString(); 1818 target = null; 1819 } else if (typeof target != "string" && !Array.isArray(target)) { 1820 target = null; 1821 } 1822 1823 browsingContext = this.getBrowsingContextFrom(browsingContext); 1824 return this.sendQuery(browsingContext, "Test:SynthesizeMouse", { 1825 target, 1826 targetFn, 1827 x: offsetX, 1828 y: offsetY, 1829 event, 1830 handlingUserInput, 1831 }); 1832 }, 1833 1834 /** 1835 * Versions of EventUtils.sys.mjs synthesizeTouch functions that synthesize a 1836 * touch event in a child process and return promises that resolve when the 1837 * event has fired and completed. Instead of a window, a browser or 1838 * browsing context is required to be passed to this function. 1839 * 1840 * @param target 1841 * One of the following: 1842 * - a selector string that identifies the element to target. The syntax is as 1843 * for querySelector. 1844 * - a function to be run in the content process that returns the element to 1845 * target 1846 * - null, in which case the offset is from the content document's edge. 1847 * @param {integer} offsetX 1848 * x offset from target's left bounding edge 1849 * @param {integer} offsetY 1850 * y offset from target's top bounding edge 1851 * @param {object} event object 1852 * Additional arguments, similar to the EventUtils.sys.mjs version 1853 * @param {BrowserContext|MozFrameLoaderOwner} browsingContext 1854 * Browsing context or browser element, must not be null 1855 * 1856 * @returns {Promise<boolean>} 1857 * Resolves to true if the touch event was cancelled. 1858 */ 1859 synthesizeTouch(target, offsetX, offsetY, event, browsingContext) { 1860 let targetFn = null; 1861 if (typeof target == "function") { 1862 targetFn = target.toString(); 1863 target = null; 1864 } else if (typeof target != "string" && !Array.isArray(target)) { 1865 target = null; 1866 } 1867 1868 browsingContext = this.getBrowsingContextFrom(browsingContext); 1869 return this.sendQuery(browsingContext, "Test:SynthesizeTouch", { 1870 target, 1871 targetFn, 1872 x: offsetX, 1873 y: offsetY, 1874 event, 1875 }); 1876 }, 1877 1878 /** 1879 * Wait for a message to be fired from a particular message manager 1880 * 1881 * @param {nsIMessageManager} messageManager 1882 * The message manager that should be used. 1883 * @param {string} message 1884 * The message we're waiting for. 1885 * @param {Function} checkFn (optional) 1886 * Optional function to invoke to check the message. 1887 */ 1888 waitForMessage(messageManager, message, checkFn) { 1889 return new Promise(resolve => { 1890 messageManager.addMessageListener(message, function onMessage(msg) { 1891 if (!checkFn || checkFn(msg)) { 1892 messageManager.removeMessageListener(message, onMessage); 1893 resolve(msg.data); 1894 } 1895 }); 1896 }); 1897 }, 1898 1899 /** 1900 * Version of synthesizeMouse that uses the center of the target as the mouse 1901 * location. Arguments and the return value are the same. 1902 */ 1903 synthesizeMouseAtCenter(target, event, browsingContext) { 1904 // Use a flag to indicate to center rather than having a separate message. 1905 event.centered = true; 1906 return BrowserTestUtils.synthesizeMouse( 1907 target, 1908 0, 1909 0, 1910 event, 1911 browsingContext 1912 ); 1913 }, 1914 1915 /** 1916 * Version of synthesizeMouse that uses a client point within the child 1917 * window instead of a target as the offset. Otherwise, the arguments and 1918 * return value are the same as synthesizeMouse. 1919 */ 1920 synthesizeMouseAtPoint(offsetX, offsetY, event, browsingContext) { 1921 return BrowserTestUtils.synthesizeMouse( 1922 null, 1923 offsetX, 1924 offsetY, 1925 event, 1926 browsingContext 1927 ); 1928 }, 1929 1930 /** 1931 * Removes the given tab from its parent tabbrowser. 1932 * This method doesn't SessionStore etc. 1933 * 1934 * @param (tab) tab 1935 * The tab to remove. 1936 * @param (Object) options 1937 * Extra options to pass to tabbrowser's removeTab method. 1938 */ 1939 removeTab(tab, options = {}) { 1940 tab.ownerGlobal.gBrowser.removeTab(tab, options); 1941 }, 1942 1943 /** 1944 * Returns a Promise that resolves once the tab starts closing. 1945 * 1946 * @param {tab} tab 1947 * The tab that will be removed. 1948 * @returns {Promise<Event>} 1949 * Resolves with the event when the tab starts closing. 1950 */ 1951 waitForTabClosing(tab) { 1952 return this.waitForEvent(tab, "TabClose"); 1953 }, 1954 1955 /** 1956 * 1957 * @param {tab} tab 1958 * The tab that will be reloaded. 1959 * @param {object} [options] 1960 * Options for the reload. 1961 * @param {boolean} options.includeSubFrames = false [optional] 1962 * A boolean indicating if loads from subframes should be included 1963 * when waiting for the frame to reload. 1964 * @param {boolean} options.bypassCache = false [optional] 1965 * A boolean indicating if loads should bypass the cache. 1966 * If bypassCache is true, this skips some steps that normally happen 1967 * when a user reloads a tab. 1968 * @returns {Promise} 1969 * Resolves when the tab finishes reloading. 1970 */ 1971 reloadTab(tab, options = {}) { 1972 const finished = BrowserTestUtils.browserLoaded(tab.linkedBrowser, { 1973 includeSubFrames: !!options.includeSubFrames, 1974 }); 1975 if (options.bypassCache) { 1976 tab.linkedBrowser.reloadWithFlags( 1977 Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE 1978 ); 1979 } else { 1980 tab.ownerGlobal.gBrowser.reloadTab(tab); 1981 } 1982 return finished; 1983 }, 1984 1985 /** 1986 * Create enough tabs to cause a tab overflow in the given window. 1987 * 1988 * @param {Function|null} registerCleanupFunction 1989 * The test framework doesn't keep its cleanup stuff anywhere accessible, 1990 * so the first argument is a reference to your cleanup registration 1991 * function, allowing us to clean up after you if necessary. This can be 1992 * null if you are using a temporary window for the test. 1993 * @param {Window} win 1994 * The window where the tabs need to be overflowed. 1995 * @param {object} params [optional] 1996 * Parameters object for BrowserTestUtils.overflowTabs. 1997 * overflowAtStart: bool 1998 * Determines whether the new tabs are added at the beginning of the 1999 * URL bar or at the end of it. 2000 * overflowTabFactor: 3 | 1.1 2001 * Factor that helps in determining the tab count for overflow. 2002 */ 2003 async overflowTabs(registerCleanupFunction, win, params = {}) { 2004 if (!params.hasOwnProperty("overflowAtStart")) { 2005 params.overflowAtStart = true; 2006 } 2007 if (!params.hasOwnProperty("overflowTabFactor")) { 2008 params.overflowTabFactor = 1.1; 2009 } 2010 let { gBrowser } = win; 2011 let overflowDirection = gBrowser.tabContainer.verticalMode 2012 ? "height" 2013 : "width"; 2014 let tabIndex = params.overflowAtStart ? 0 : undefined; 2015 let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox; 2016 if (arrowScrollbox.hasAttribute("overflowing")) { 2017 return; 2018 } 2019 let promises = []; 2020 promises.push( 2021 BrowserTestUtils.waitForEvent( 2022 arrowScrollbox, 2023 "overflow", 2024 false, 2025 e => e.target == arrowScrollbox 2026 ) 2027 ); 2028 const originalSmoothScroll = arrowScrollbox.smoothScroll; 2029 arrowScrollbox.smoothScroll = false; 2030 if (registerCleanupFunction) { 2031 registerCleanupFunction(() => { 2032 arrowScrollbox.smoothScroll = originalSmoothScroll; 2033 }); 2034 } 2035 2036 let size = ele => ele.getBoundingClientRect()[overflowDirection]; 2037 let tabMinSize = gBrowser.tabContainer.verticalMode 2038 ? size(gBrowser.selectedTab) 2039 : parseInt(win.getComputedStyle(gBrowser.selectedTab).minWidth); 2040 let tabCountForOverflow = Math.ceil( 2041 (size(arrowScrollbox) / tabMinSize) * params.overflowTabFactor 2042 ); 2043 while (gBrowser.tabs.length < tabCountForOverflow) { 2044 promises.push( 2045 BrowserTestUtils.addTab(gBrowser, "about:blank", { 2046 skipAnimation: true, 2047 tabIndex, 2048 }) 2049 ); 2050 } 2051 await Promise.all(promises); 2052 }, 2053 2054 /** 2055 * Crashes a remote frame tab and cleans up the generated minidumps. 2056 * Resolves with the data from the .extra file (the crash annotations). 2057 * 2058 * @param (Browser) browser 2059 * A remote <xul:browser> element. Must not be null. 2060 * @param (bool) shouldShowTabCrashPage 2061 * True if it is expected that the tab crashed page will be shown 2062 * for this browser. If so, the Promise will only resolve once the 2063 * tab crash page has loaded. 2064 * @param (bool) shouldClearMinidumps 2065 * True if the minidumps left behind by the crash should be removed. 2066 * @param (BrowsingContext) browsingContext 2067 * The context where the frame leaves. Default to 2068 * top level context if not supplied. 2069 * @param (object?) options 2070 * An object with any of the following fields: 2071 * crashType: "CRASH_INVALID_POINTER_DEREF" | "CRASH_OOM" | "CRASH_SYSCALL" 2072 * The type of crash. If unspecified, default to "CRASH_INVALID_POINTER_DEREF" 2073 * asyncCrash: bool 2074 * If specified and `true`, cause the crash asynchronously. 2075 * 2076 * @returns (Promise) 2077 * An Object with key-value pairs representing the data from the crash 2078 * report's extra file (if applicable). 2079 */ 2080 async crashFrame( 2081 browser, 2082 shouldShowTabCrashPage = true, 2083 shouldClearMinidumps = true, 2084 browsingContext, 2085 options = {} 2086 ) { 2087 let extra = {}; 2088 2089 if (!browser.isRemoteBrowser) { 2090 throw new Error("<xul:browser> needs to be remote in order to crash"); 2091 } 2092 2093 /** 2094 * Returns the directory where crash dumps are stored. 2095 * 2096 * @return nsIFile 2097 */ 2098 function getMinidumpDirectory() { 2099 let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); 2100 dir.append("minidumps"); 2101 return dir; 2102 } 2103 2104 /** 2105 * Removes a file from a directory. This is a no-op if the file does not 2106 * exist. 2107 * 2108 * @param directory 2109 * The nsIFile representing the directory to remove from. 2110 * @param filename 2111 * A string for the file to remove from the directory. 2112 */ 2113 function removeFile(directory, filename) { 2114 let file = directory.clone(); 2115 file.append(filename); 2116 if (file.exists()) { 2117 file.remove(false); 2118 } 2119 } 2120 2121 let expectedPromises = []; 2122 2123 let crashCleanupPromise = new Promise((resolve, reject) => { 2124 let observer = (subject, topic) => { 2125 if (topic != "ipc:content-shutdown") { 2126 reject("Received incorrect observer topic: " + topic); 2127 return; 2128 } 2129 if (!(subject instanceof Ci.nsIPropertyBag2)) { 2130 reject("Subject did not implement nsIPropertyBag2"); 2131 return; 2132 } 2133 // we might see this called as the process terminates due to previous tests. 2134 // We are only looking for "abnormal" exits... 2135 if (!subject.hasKey("abnormal")) { 2136 dump( 2137 "\nThis is a normal termination and isn't the one we are looking for...\n" 2138 ); 2139 return; 2140 } 2141 2142 Services.obs.removeObserver(observer, "ipc:content-shutdown"); 2143 2144 let dumpID; 2145 if (AppConstants.MOZ_CRASHREPORTER) { 2146 dumpID = subject.getPropertyAsAString("dumpID"); 2147 if (!dumpID) { 2148 reject( 2149 "dumpID was not present despite crash reporting being enabled" 2150 ); 2151 return; 2152 } 2153 } 2154 2155 let removalPromise = Promise.resolve(); 2156 2157 if (dumpID) { 2158 removalPromise = Services.crashmanager 2159 .ensureCrashIsPresent(dumpID) 2160 .then(async () => { 2161 let minidumpDirectory = getMinidumpDirectory(); 2162 let extrafile = minidumpDirectory.clone(); 2163 extrafile.append(dumpID + ".extra"); 2164 if (extrafile.exists()) { 2165 if (AppConstants.MOZ_CRASHREPORTER) { 2166 extra = await IOUtils.readJSON(extrafile.path); 2167 } else { 2168 dump( 2169 "\nCrashReporter not enabled - will not return any extra data\n" 2170 ); 2171 } 2172 } else { 2173 dump(`\nNo .extra file for dumpID: ${dumpID}\n`); 2174 } 2175 2176 if (shouldClearMinidumps) { 2177 removeFile(minidumpDirectory, dumpID + ".dmp"); 2178 removeFile(minidumpDirectory, dumpID + ".extra"); 2179 } 2180 }); 2181 } 2182 2183 removalPromise.then(() => { 2184 dump("\nCrash cleaned up\n"); 2185 // There might be other ipc:content-shutdown handlers that need to 2186 // run before we want to continue, so we'll resolve on the next tick 2187 // of the event loop. 2188 TestUtils.executeSoon(() => resolve()); 2189 }); 2190 }; 2191 2192 Services.obs.addObserver(observer, "ipc:content-shutdown"); 2193 }); 2194 2195 expectedPromises.push(crashCleanupPromise); 2196 2197 if (shouldShowTabCrashPage) { 2198 expectedPromises.push( 2199 new Promise(resolve => { 2200 browser.addEventListener( 2201 "AboutTabCrashedReady", 2202 function onCrash() { 2203 browser.removeEventListener("AboutTabCrashedReady", onCrash); 2204 dump("\nabout:tabcrashed loaded and ready\n"); 2205 resolve(); 2206 }, 2207 false, 2208 true 2209 ); 2210 }) 2211 ); 2212 } 2213 2214 // Trigger crash by sending a message to BrowserTestUtils actor. 2215 this.sendAsyncMessage( 2216 browsingContext || browser.browsingContext, 2217 "BrowserTestUtils:CrashFrame", 2218 { 2219 crashType: options.crashType || "", 2220 asyncCrash: options.asyncCrash || false, 2221 } 2222 ); 2223 2224 await Promise.all(expectedPromises); 2225 2226 if (shouldShowTabCrashPage) { 2227 let gBrowser = browser.ownerGlobal.gBrowser; 2228 let tab = gBrowser.getTabForBrowser(browser); 2229 if (tab.getAttribute("crashed") != "true") { 2230 throw new Error("Tab should be marked as crashed"); 2231 } 2232 } 2233 2234 return extra; 2235 }, 2236 2237 /** 2238 * Attempts to simulate a launch fail by crashing a browser, but 2239 * stripping the browser of its childID so that the TabCrashHandler 2240 * thinks it was a launch fail. 2241 * 2242 * @param browser (<xul:browser>) 2243 * The browser to simulate a content process launch failure on. 2244 * @return {Promise<void>} 2245 * Resolves when the TabCrashHandler should be done handling the 2246 * simulated crash. 2247 */ 2248 simulateProcessLaunchFail(browser, dueToBuildIDMismatch = false) { 2249 const NORMAL_CRASH_TOPIC = "ipc:content-shutdown"; 2250 2251 Object.defineProperty(browser.frameLoader, "childID", { 2252 get: () => 0, 2253 }); 2254 2255 let sawNormalCrash = false; 2256 let observer = () => { 2257 sawNormalCrash = true; 2258 }; 2259 2260 Services.obs.addObserver(observer, NORMAL_CRASH_TOPIC); 2261 2262 Services.obs.notifyObservers( 2263 browser.frameLoader, 2264 "oop-frameloader-crashed" 2265 ); 2266 2267 let eventType = dueToBuildIDMismatch 2268 ? "oop-browser-buildid-mismatch" 2269 : "oop-browser-crashed"; 2270 2271 let event = new browser.ownerGlobal.CustomEvent(eventType, { 2272 bubbles: true, 2273 }); 2274 event.isTopFrame = true; 2275 browser.dispatchEvent(event); 2276 2277 Services.obs.removeObserver(observer, NORMAL_CRASH_TOPIC); 2278 2279 if (sawNormalCrash) { 2280 throw new Error(`Unexpectedly saw ${NORMAL_CRASH_TOPIC}`); 2281 } 2282 2283 return new Promise(resolve => TestUtils.executeSoon(resolve)); 2284 }, 2285 2286 /** 2287 * Returns a promise that is resolved when element gains attribute (or, 2288 * optionally, when it is set to value). 2289 * 2290 * @param {string} attr 2291 * The attribute to wait for 2292 * @param {Element} element 2293 * The element which should gain the attribute 2294 * @param {string} value (optional) 2295 * Optional, the value the attribute should have. 2296 * 2297 * @returns {Promise} 2298 */ 2299 waitForAttribute(attr, element, value) { 2300 let MutationObserver = element.ownerGlobal.MutationObserver; 2301 return new Promise(resolve => { 2302 let mut = new MutationObserver(() => { 2303 if ( 2304 (!value && element.hasAttribute(attr)) || 2305 (value && element.getAttribute(attr) === value) 2306 ) { 2307 resolve(); 2308 mut.disconnect(); 2309 } 2310 }); 2311 2312 mut.observe(element, { attributeFilter: [attr] }); 2313 }); 2314 }, 2315 2316 /** 2317 * Returns a promise that is resolved when element loses an attribute. 2318 * 2319 * @param {string} attr 2320 * The attribute to wait for 2321 * @param {Element} element 2322 * The element which should lose the attribute 2323 * 2324 * @returns {Promise} 2325 */ 2326 waitForAttributeRemoval(attr, element) { 2327 if (!element.hasAttribute(attr)) { 2328 return Promise.resolve(); 2329 } 2330 2331 let MutationObserver = element.ownerGlobal.MutationObserver; 2332 return new Promise(resolve => { 2333 dump("Waiting for removal\n"); 2334 let mut = new MutationObserver(() => { 2335 if (!element.hasAttribute(attr)) { 2336 resolve(); 2337 mut.disconnect(); 2338 } 2339 }); 2340 2341 mut.observe(element, { attributeFilter: [attr] }); 2342 }); 2343 }, 2344 2345 /** 2346 * Version of EventUtils' `sendChar` function; it will synthesize a keypress 2347 * event in a child process and returns a Promise that will resolve when the 2348 * event was fired. Instead of a Window, a Browser or Browsing Context 2349 * is required to be passed to this function. 2350 * 2351 * @param {string} char 2352 * A character for the keypress event that is sent to the browser. 2353 * @param {BrowserContext|MozFrameLoaderOwner} browsingContext 2354 * Browsing context or browser element, must not be null 2355 * 2356 * @returns {Promise<boolean>} 2357 * Resolves to true if the keypress event was synthesized. 2358 */ 2359 sendChar(char, browsingContext) { 2360 browsingContext = this.getBrowsingContextFrom(browsingContext); 2361 return this.sendQuery(browsingContext, "Test:SendChar", { char }); 2362 }, 2363 2364 /** 2365 * Version of EventUtils' `synthesizeKey` function; it will synthesize a key 2366 * event in a child process and returns a Promise that will resolve when the 2367 * event was fired. Instead of a Window, a Browser or Browsing Context 2368 * is required to be passed to this function. 2369 * 2370 * @param {string} key 2371 * See the documentation available for EventUtils#synthesizeKey. 2372 * @param {object} event 2373 * See the documentation available for EventUtils#synthesizeKey. 2374 * @param {BrowserContext|MozFrameLoaderOwner} browsingContext 2375 * Browsing context or browser element, must not be null 2376 * 2377 * @returns {Promise} 2378 */ 2379 synthesizeKey(key, event, browsingContext) { 2380 browsingContext = this.getBrowsingContextFrom(browsingContext); 2381 return this.sendQuery(browsingContext, "Test:SynthesizeKey", { 2382 key, 2383 event, 2384 }); 2385 }, 2386 2387 /** 2388 * Version of EventUtils' `synthesizeComposition` function; it will synthesize 2389 * a composition event in a child process and returns a Promise that will 2390 * resolve when the event was fired. Instead of a Window, a Browser or 2391 * Browsing Context is required to be passed to this function. 2392 * 2393 * @param {object} event 2394 * See the documentation available for EventUtils#synthesizeComposition. 2395 * @param {BrowserContext|MozFrameLoaderOwner} browsingContext 2396 * Browsing context or browser element, must not be null 2397 * 2398 * @returns {Promise<boolean>} 2399 * Resolves to false if the composition event could not be synthesized. 2400 */ 2401 synthesizeComposition(event, browsingContext) { 2402 browsingContext = this.getBrowsingContextFrom(browsingContext); 2403 return this.sendQuery(browsingContext, "Test:SynthesizeComposition", { 2404 event, 2405 }); 2406 }, 2407 2408 /** 2409 * Version of EventUtils' `synthesizeCompositionChange` function; it will 2410 * synthesize a compositionchange event in a child process and returns a 2411 * Promise that will resolve when the event was fired. Instead of a Window, a 2412 * Browser or Browsing Context object is required to be passed to this function. 2413 * 2414 * @param {object} event 2415 * See the documentation available for EventUtils#synthesizeCompositionChange. 2416 * @param {BrowserContext|MozFrameLoaderOwner} browsingContext 2417 * Browsing context or browser element, must not be null 2418 * 2419 * @returns {Promise} 2420 */ 2421 synthesizeCompositionChange(event, browsingContext) { 2422 browsingContext = this.getBrowsingContextFrom(browsingContext); 2423 return this.sendQuery(browsingContext, "Test:SynthesizeCompositionChange", { 2424 event, 2425 }); 2426 }, 2427 2428 // TODO: Fix consumers and remove me. 2429 waitForCondition: TestUtils.waitForCondition, 2430 2431 /** 2432 * Waits for a <xul:notification> with a particular value to appear 2433 * for the <xul:notificationbox> of the passed in browser. 2434 * 2435 * @param {xul:tabbrowser} tabbrowser 2436 * The gBrowser that hosts the browser that should show 2437 * the notification. For most tests, this will probably be 2438 * gBrowser. 2439 * @param {xul:browser} browser 2440 * The browser that should be showing the notification. 2441 * @param {string} notificationValue 2442 * The "value" of the notification, which is often used as 2443 * a unique identifier. Example: "plugin-crashed". 2444 * 2445 * @return {Promise} 2446 * Resolves to the <xul:notification> that is being shown. 2447 */ 2448 waitForNotificationBar(tabbrowser, browser, notificationValue) { 2449 let notificationBox = tabbrowser.getNotificationBox(browser); 2450 return this.waitForNotificationInNotificationBox( 2451 notificationBox, 2452 notificationValue 2453 ); 2454 }, 2455 2456 /** 2457 * Waits for a <xul:notification> with a particular value to appear 2458 * in the global <xul:notificationbox> of the given browser window. 2459 * 2460 * @param {Window} win 2461 * The browser window in whose global notificationbox the 2462 * notification is expected to appear. 2463 * @param {string} notificationValue 2464 * The "value" of the notification, which is often used as 2465 * a unique identifier. Example: "captive-portal-detected". 2466 * 2467 * @return {Promise} 2468 * Resolves to the <xul:notification> that is being shown. 2469 */ 2470 waitForGlobalNotificationBar(win, notificationValue) { 2471 return this.waitForNotificationInNotificationBox( 2472 win.gNotificationBox, 2473 notificationValue 2474 ); 2475 }, 2476 2477 waitForNotificationInNotificationBox(notificationBox, notificationValue) { 2478 return new Promise(resolve => { 2479 let check = event => { 2480 return event.target.getAttribute("value") == notificationValue; 2481 }; 2482 2483 BrowserTestUtils.waitForEvent( 2484 notificationBox.stack, 2485 "AlertActive", 2486 false, 2487 check 2488 ).then(event => { 2489 // The originalTarget of the AlertActive on a notificationbox 2490 // will be the notification itself. 2491 resolve(event.originalTarget); 2492 }); 2493 }); 2494 }, 2495 2496 /** 2497 * Waits for CSS transitions to complete for an element. Tracks any 2498 * transitions that start after this function is called and resolves once all 2499 * started transitions complete. 2500 * 2501 * @param {Element} element 2502 * The element that will transition. 2503 * @param {number} timeout 2504 * The maximum time to wait in milliseconds. Defaults to 5 seconds. 2505 * @return {Promise} 2506 * Resolves when transitions complete or rejects if the timeout is hit. 2507 */ 2508 waitForTransition(element, timeout = 5000) { 2509 return new Promise((resolve, reject) => { 2510 let cleanup = () => { 2511 element.removeEventListener("transitionrun", listener); 2512 element.removeEventListener("transitionend", listener); 2513 }; 2514 2515 let timer = element.ownerGlobal.setTimeout(() => { 2516 cleanup(); 2517 reject(); 2518 }, timeout); 2519 2520 let transitionCount = 0; 2521 2522 let listener = event => { 2523 if (event.type == "transitionrun") { 2524 transitionCount++; 2525 } else { 2526 transitionCount--; 2527 if (transitionCount == 0) { 2528 cleanup(); 2529 element.ownerGlobal.clearTimeout(timer); 2530 resolve(); 2531 } 2532 } 2533 }; 2534 2535 element.addEventListener("transitionrun", listener); 2536 element.addEventListener("transitionend", listener); 2537 element.addEventListener("transitioncancel", listener); 2538 }); 2539 }, 2540 2541 _knownAboutPages: new Set(), 2542 _loadedAboutContentScript: false, 2543 2544 /** 2545 * Registers an about: page with particular flags in both the parent 2546 * and any content processes. Returns a promise that resolves when 2547 * registration is complete. 2548 * 2549 * @param {Function} registerCleanupFunction 2550 * The test framework doesn't keep its cleanup stuff anywhere accessible, 2551 * so the first argument is a reference to your cleanup registration 2552 * function, allowing us to clean up after you if necessary. 2553 * @param {string} aboutModule 2554 * The name of the about page. 2555 * @param {string} pageURI 2556 * The URI the about: page should point to. 2557 * @param {number} flags 2558 * The nsIAboutModule flags to use for registration. 2559 * 2560 * @returns {Promise} 2561 * Promise that resolves when registration has finished. 2562 */ 2563 registerAboutPage(registerCleanupFunction, aboutModule, pageURI, flags) { 2564 // Return a promise that resolves when registration finished. 2565 const kRegistrationMsgId = 2566 "browser-test-utils:about-registration:registered"; 2567 let rv = this.waitForMessage(Services.ppmm, kRegistrationMsgId, msg => { 2568 return msg.data == aboutModule; 2569 }); 2570 // Load a script that registers our page, then send it a message to execute the registration. 2571 if (!this._loadedAboutContentScript) { 2572 Services.ppmm.loadProcessScript( 2573 kAboutPageRegistrationContentScript, 2574 true 2575 ); 2576 this._loadedAboutContentScript = true; 2577 registerCleanupFunction(this._removeAboutPageRegistrations.bind(this)); 2578 } 2579 Services.ppmm.broadcastAsyncMessage( 2580 "browser-test-utils:about-registration:register", 2581 { aboutModule, pageURI, flags } 2582 ); 2583 return rv.then(() => { 2584 this._knownAboutPages.add(aboutModule); 2585 }); 2586 }, 2587 2588 unregisterAboutPage(aboutModule) { 2589 if (!this._knownAboutPages.has(aboutModule)) { 2590 return Promise.reject( 2591 new Error("We don't think this about page exists!") 2592 ); 2593 } 2594 const kUnregistrationMsgId = 2595 "browser-test-utils:about-registration:unregistered"; 2596 let rv = this.waitForMessage(Services.ppmm, kUnregistrationMsgId, msg => { 2597 return msg.data == aboutModule; 2598 }); 2599 Services.ppmm.broadcastAsyncMessage( 2600 "browser-test-utils:about-registration:unregister", 2601 aboutModule 2602 ); 2603 return rv.then(() => this._knownAboutPages.delete(aboutModule)); 2604 }, 2605 2606 async _removeAboutPageRegistrations() { 2607 for (let aboutModule of this._knownAboutPages) { 2608 await this.unregisterAboutPage(aboutModule); 2609 } 2610 Services.ppmm.removeDelayedProcessScript( 2611 kAboutPageRegistrationContentScript 2612 ); 2613 }, 2614 2615 /** 2616 * Waits for the dialog to open, and clicks the specified button. 2617 * 2618 * @param {string} buttonNameOrElementID 2619 * The name of the button ("accept", "cancel", etc) or element ID to 2620 * click. 2621 * @param {string} uri 2622 * The URI of the dialog to wait for. Defaults to the common dialog. 2623 * @return {Promise} 2624 * A Promise which resolves when a "domwindowopened" notification 2625 * for a dialog has been fired by the window watcher and the 2626 * specified button is clicked. 2627 */ 2628 async promiseAlertDialogOpen( 2629 buttonNameOrElementID, 2630 uri = "chrome://global/content/commonDialog.xhtml", 2631 options = { callback: null, isSubDialog: false } 2632 ) { 2633 let win; 2634 if (uri == "chrome://global/content/commonDialog.xhtml") { 2635 [win] = await TestUtils.topicObserved("common-dialog-loaded"); 2636 } else if (options.isSubDialog) { 2637 for (let attempts = 0; attempts < 3; attempts++) { 2638 [win] = await TestUtils.topicObserved("subdialog-loaded"); 2639 if (uri === undefined || uri === null || uri === "") { 2640 break; 2641 } 2642 if (win.document.documentURI === uri) { 2643 break; 2644 } 2645 } 2646 } else { 2647 // The test listens for the "load" event which guarantees that the alert 2648 // class has already been added (it is added when "DOMContentLoaded" is 2649 // fired). 2650 win = await this.domWindowOpenedAndLoaded(null, win => { 2651 return win.document.documentURI === uri; 2652 }); 2653 } 2654 2655 if (options.callback) { 2656 await options.callback(win); 2657 return win; 2658 } 2659 2660 if (buttonNameOrElementID) { 2661 let dialog = win.document.querySelector("dialog"); 2662 let element = 2663 dialog.getButton(buttonNameOrElementID) || 2664 win.document.getElementById(buttonNameOrElementID); 2665 element.click(); 2666 } 2667 2668 return win; 2669 }, 2670 2671 /** 2672 * Wait for the containing dialog with the id `window-modal-dialog` to become 2673 * empty and close. 2674 * 2675 * @param {HTMLDialogElement} dialog 2676 * The dialog to wait on. 2677 * @return {Promise} 2678 * Resolves once the the dialog has closed 2679 */ 2680 async waitForDialogClose(dialog) { 2681 return this.waitForEvent(dialog, "close").then(() => { 2682 return this.waitForMutationCondition( 2683 dialog, 2684 { childList: true, attributes: true }, 2685 () => !dialog.hasChildNodes() && !dialog.open 2686 ); 2687 }); 2688 }, 2689 2690 /** 2691 * Waits for the dialog to open, and clicks the specified button, and waits 2692 * for the dialog to close. 2693 * 2694 * @param {string} buttonNameOrElementID 2695 * The name of the button ("accept", "cancel", etc) or element ID to 2696 * click. 2697 * @param {string} uri 2698 * The URI of the dialog to wait for. Defaults to the common dialog. 2699 * 2700 * @return {Promise} 2701 * A Promise which resolves when a "domwindowopened" notification 2702 * for a dialog has been fired by the window watcher and the 2703 * specified button is clicked, and the dialog has been fully closed. 2704 */ 2705 async promiseAlertDialog( 2706 buttonNameOrElementID, 2707 uri = "chrome://global/content/commonDialog.xhtml", 2708 options = { callback: null, isSubDialog: false } 2709 ) { 2710 let win = await this.promiseAlertDialogOpen( 2711 buttonNameOrElementID, 2712 uri, 2713 options 2714 ); 2715 if (!win.docShell.browsingContext.embedderElement) { 2716 return this.windowClosed(win); 2717 } 2718 const dialog = win.top.document.getElementById("window-modal-dialog"); 2719 return this.waitForDialogClose(dialog); 2720 }, 2721 2722 /** 2723 * Opens a tab with a given uri and params object. If the params object is not set 2724 * or the params parameter does not include a triggeringPrincipal then this function 2725 * provides a params object using the systemPrincipal as the default triggeringPrincipal. 2726 * 2727 * @param {xul:tabbrowser} tabbrowser 2728 * The gBrowser object to open the tab with. 2729 * @param {string} uri 2730 * The URI to open in the new tab. 2731 * @param {object} params [optional] 2732 * Parameters object for gBrowser.addTab. 2733 * @param {function} beforeLoadFunc [optional] 2734 * A function to run after that xul:browser has been created but before the URL is 2735 * loaded. Can spawn a content task in the tab, for example. 2736 */ 2737 addTab(tabbrowser, uri, params = {}, beforeLoadFunc = null) { 2738 if (!params.triggeringPrincipal) { 2739 params.triggeringPrincipal = 2740 Services.scriptSecurityManager.getSystemPrincipal(); 2741 } 2742 if (beforeLoadFunc) { 2743 let window = tabbrowser.ownerGlobal; 2744 window.addEventListener( 2745 "TabOpen", 2746 function (e) { 2747 beforeLoadFunc(e.target); 2748 }, 2749 { once: true } 2750 ); 2751 } 2752 return tabbrowser.addTab(uri, params); 2753 }, 2754 2755 /** 2756 * There are two ways to listen for observers in a content process: 2757 * 1. Call contentTopicObserved which will watch for an observer notification 2758 * in a content process to occur, and will return a promise which resolves 2759 * when that notification occurs. 2760 * 2. Enclose calls to contentTopicObserved inside a pair of calls to 2761 * startObservingTopics and stopObservingTopics. Usually this pair will be 2762 * placed at the start and end of a test or set of tests. Any observer 2763 * notification that happens between the start and stop that doesn't match 2764 * any explicitly expected by using contentTopicObserved will cause 2765 * stopObservingTopics to reject with an error. 2766 * For example: 2767 * 2768 * await BrowserTestUtils.startObservingTopics(bc, ["a", "b", "c"]); 2769 * await BrowserTestUtils contentTopicObserved(bc, "a", 2); 2770 * await BrowserTestUtils.stopObservingTopics(bc, ["a", "b", "c"]); 2771 * 2772 * This will expect two "a" notifications to occur, but will fail if more 2773 * than two occur, or if any "b" or "c" notifications occur. 2774 * 2775 * Note that this function doesn't handle adding a listener for the same topic 2776 * more than once. To do that, use the aCount argument. 2777 * 2778 * @param aBrowsingContext 2779 * The browsing context associated with the content process to listen to. 2780 * @param {string} aTopic 2781 * Observer topic to listen to. May be null to listen to any topic. 2782 * @param {number} aCount 2783 * Number of such matching topics to listen to, defaults to 1. A match 2784 * occurs when the topic and filter function match. 2785 * @param {function} aFilterFn 2786 * Function to be evaluated in the content process which should 2787 * return true if the notification matches. This function is passed 2788 * the same arguments as nsIObserver.observe(). May be null to 2789 * always match. 2790 * @returns {Promise} resolves when the notification occurs. 2791 */ 2792 contentTopicObserved(aBrowsingContext, aTopic, aCount = 1, aFilterFn = null) { 2793 return this.sendQuery(aBrowsingContext, "BrowserTestUtils:ObserveTopic", { 2794 topic: aTopic, 2795 count: aCount, 2796 filterFunctionSource: aFilterFn ? aFilterFn.toSource() : null, 2797 }); 2798 }, 2799 2800 /** 2801 * Starts observing a list of topics in a content process. Use contentTopicObserved 2802 * to allow an observer notification. Any other observer notification that occurs that 2803 * matches one of the specified topics will cause the promise to reject. 2804 * 2805 * Calling this function more than once adds additional topics to be observed without 2806 * replacing the existing ones. 2807 * 2808 * @param {BrowsingContext} aBrowsingContext 2809 * The browsing context associated with the content process to listen to. 2810 * @param {string[]} aTopics array of observer topics 2811 * @returns {Promise} resolves when the listeners have been added. 2812 */ 2813 startObservingTopics(aBrowsingContext, aTopics) { 2814 return this.sendQuery( 2815 aBrowsingContext, 2816 "BrowserTestUtils:StartObservingTopics", 2817 { 2818 topics: aTopics, 2819 } 2820 ); 2821 }, 2822 2823 /** 2824 * Stop listening to a set of observer topics. 2825 * 2826 * @param {BrowsingContext} aBrowsingContext 2827 * The browsing context associated with the content process to listen to. 2828 * @param {string[]} aTopics array of observer topics. If empty, then all 2829 * current topics being listened to are removed. 2830 * @returns {Promise} promise that fails if an unexpected observer occurs. 2831 */ 2832 stopObservingTopics(aBrowsingContext, aTopics) { 2833 return this.sendQuery( 2834 aBrowsingContext, 2835 "BrowserTestUtils:StopObservingTopics", 2836 { 2837 topics: aTopics, 2838 } 2839 ); 2840 }, 2841 2842 /** 2843 * Sends a message to a specific BrowserTestUtils window actor. 2844 * 2845 * @param {BrowsingContext} aBrowsingContext 2846 * The browsing context where the actor lives. 2847 * @param {string} aMessageName 2848 * Name of the message to be sent to the actor. 2849 * @param {object} aMessageData 2850 * Extra information to pass to the actor. 2851 */ 2852 async sendAsyncMessage(aBrowsingContext, aMessageName, aMessageData) { 2853 if (!aBrowsingContext.currentWindowGlobal) { 2854 await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal); 2855 } 2856 2857 let actor = 2858 aBrowsingContext.currentWindowGlobal.getActor("BrowserTestUtils"); 2859 actor.sendAsyncMessage(aMessageName, aMessageData); 2860 }, 2861 2862 /** 2863 * Sends a query to a specific BrowserTestUtils window actor. 2864 * 2865 * @param {BrowsingContext} aBrowsingContext 2866 * The browsing context where the actor lives. 2867 * @param {string} aMessageName 2868 * Name of the message to be sent to the actor. 2869 * @param {object} aMessageData 2870 * Extra information to pass to the actor. 2871 */ 2872 async sendQuery(aBrowsingContext, aMessageName, aMessageData) { 2873 let startTime = ChromeUtils.now(); 2874 if (!aBrowsingContext.currentWindowGlobal) { 2875 await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal); 2876 } 2877 2878 let actor = 2879 aBrowsingContext.currentWindowGlobal.getActor("BrowserTestUtils"); 2880 return actor.sendQuery(aMessageName, aMessageData).then(val => { 2881 ChromeUtils.addProfilerMarker( 2882 "BrowserTestUtils", 2883 { startTime, category: "Test" }, 2884 aMessageName 2885 ); 2886 return val; 2887 }); 2888 }, 2889 2890 /** 2891 * A helper function for this test that returns a Promise that resolves 2892 * once the migration wizard appears. 2893 * 2894 * @param {DOMWindow} window 2895 * The top-level window that the about:preferences tab is likely to open 2896 * in if the new migration wizard is enabled. 2897 * @returns {Promise<Element>} 2898 * Resolves to the opened about:preferences tab with the migration wizard 2899 * running and loaded in it. 2900 */ 2901 async waitForMigrationWizard(window) { 2902 let wizardReady = this.waitForEvent(window, "MigrationWizard:Ready"); 2903 let wizardTab = await this.waitForNewTab(window.gBrowser, url => { 2904 return url.startsWith("about:preferences"); 2905 }); 2906 await wizardReady; 2907 2908 return wizardTab; 2909 }, 2910 2911 /** 2912 * When calling this function, the window will be hidden from various APIs, 2913 * so that they won't be able to find it. 2914 * 2915 * This makes it possible to hide the main window to test some behaviors when 2916 * it doesn't exist, e.g. when only private or non-browser windows exist. 2917 * 2918 * @param {ChromeWindow} window The window to be concealed. 2919 * @param {object} options 2920 * @param {AbortSignal} options.signal 2921 * Unconceals the window when the signal aborts. 2922 */ 2923 concealWindow(window, { signal }) { 2924 let oldWinType = window.document.documentElement.getAttribute("windowtype"); 2925 // Check if we've already done this to allow calling multiple times: 2926 if (oldWinType != "navigator:testrunner") { 2927 // Make the main test window not count as a browser window any longer 2928 window.document.documentElement.setAttribute( 2929 "windowtype", 2930 "navigator:testrunner" 2931 ); 2932 lazy.BrowserWindowTracker.untrackForTestsOnly(window); 2933 2934 signal.addEventListener("abort", () => { 2935 lazy.BrowserWindowTracker.track(window); 2936 window.document.documentElement.setAttribute("windowtype", oldWinType); 2937 }); 2938 } 2939 }, 2940 }; 2941 2942 XPCOMUtils.defineLazyPreferenceGetter( 2943 BrowserTestUtils, 2944 "_httpsFirstEnabled", 2945 "dom.security.https_first", 2946 false 2947 ); 2948 2949 Services.obs.addObserver(BrowserTestUtils, "test-complete");