Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/*
* This module implements a number of utilities useful for browser tests.
*
* All asynchronous helper methods should return promises, rather than being
* callback based.
*/
// This file uses ContentTask & frame scripts, where these are available.
/* global ContentTaskUtils */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetters(lazy, {
ProtocolProxyService: [
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService",
],
});
let gListenerId = 0;
const DISABLE_CONTENT_PROCESS_REUSE_PREF = "dom.ipc.disableContentProcessReuse";
const kAboutPageRegistrationContentScript =
/**
* Create and register the BrowserTestUtils and ContentEventListener window
* actors.
*/
function registerActors() {
ChromeUtils.registerWindowActor("BrowserTestUtils", {
parent: {
},
child: {
events: {
DOMContentLoaded: { capture: true },
load: { capture: true },
},
},
allFrames: true,
includeChrome: true,
});
ChromeUtils.registerWindowActor("ContentEventListener", {
},
child: {
esModuleURI:
events: {
// We need to see the creation of all new windows, in case they have
// a browsing context we are interested in.
DOMWindowCreated: { capture: true },
},
},
allFrames: true,
});
}
registerActors();
/**
* BrowserTestUtils provides useful test utilities for working with the browser
* in browser mochitests. Most common operations (opening, closing and switching
* between tabs and windows, loading URLs, waiting for events in the parent or
* content process, clicking things in the content process, registering about
* pages, etc.) have dedicated helpers on this object.
*
* @class
*/
export var BrowserTestUtils = {
/**
* Loads a page in a new tab, executes a Task and closes the tab.
*
* @param {Object|String} options
* If this is a string it is the url to open and will be opened in the
* currently active browser window.
* @param {tabbrowser} [options.gBrowser
* A reference to the ``tabbrowser`` element where the new tab should
* be opened,
* @param {string} options.url
* The URL of the page to load.
* @param {Function} taskFn
* Async function representing that will be executed while
* the tab is loaded. The first argument passed to the function is a
* reference to the browser object for the new tab.
*
* @return {Any} Returns the value that is returned from taskFn.
* @resolves When the tab has been closed.
* @rejects Any exception from taskFn is propagated.
*/
async withNewTab(options, taskFn) {
if (typeof options == "string") {
options = {
gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
url: options,
};
}
let tab = await BrowserTestUtils.openNewForegroundTab(options);
let originalWindow = tab.ownerGlobal;
let result;
try {
result = await taskFn(tab.linkedBrowser);
} finally {
let finalWindow = tab.ownerGlobal;
if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) {
// taskFn may resolve within a tick after opening a new tab.
// We shouldn't remove the newly opened tab in the same tick.
// Wait for the next tick here.
await TestUtils.waitForTick();
BrowserTestUtils.removeTab(tab);
} else {
Services.console.logStringMessage(
"BrowserTestUtils.withNewTab: Tab was already closed before " +
"removeTab would have been called"
);
}
}
return Promise.resolve(result);
},
/**
* Opens a new tab in the foreground.
*
* This function takes an options object (which is preferred) or actual
* parameters. The names of the options must correspond to the names below.
* gBrowser is required and all other options are optional.
*
* @param {tabbrowser} gBrowser
* The tabbrowser to open the tab new in.
* @param {string} opening (or url)
* May be either a string URL to load in the tab, or a function that
* will be called to open a foreground tab. Defaults to "about:blank".
* @param {boolean} waitForLoad
* True to wait for the page in the new tab to load. Defaults to true.
* @param {boolean} waitForStateStop
* True to wait for the web progress listener to send STATE_STOP for the
* document in the tab. Defaults to false.
* @param {boolean} forceNewProcess
* True to force the new tab to load in a new process. Defaults to
* false.
*
* @return {Promise}
* Resolves when the tab is ready and loaded as necessary.
* @resolves The new tab.
*/
openNewForegroundTab(tabbrowser, ...args) {
let startTime = Cu.now();
let options;
if (
tabbrowser.ownerGlobal &&
tabbrowser === tabbrowser.ownerGlobal.gBrowser
) {
// tabbrowser is a tabbrowser, read the rest of the arguments from args.
let [
opening = "about:blank",
waitForLoad = true,
waitForStateStop = false,
forceNewProcess = false,
] = args;
options = { opening, waitForLoad, waitForStateStop, forceNewProcess };
} else {
if ("url" in tabbrowser && !("opening" in tabbrowser)) {
tabbrowser.opening = tabbrowser.url;
}
let {
opening = "about:blank",
waitForLoad = true,
waitForStateStop = false,
forceNewProcess = false,
} = tabbrowser;
tabbrowser = tabbrowser.gBrowser;
options = { opening, waitForLoad, waitForStateStop, forceNewProcess };
}
let {
opening: opening,
waitForLoad: aWaitForLoad,
waitForStateStop: aWaitForStateStop,
} = options;
let promises, tab;
try {
// If we're asked to force a new process, set the pref to disable process
// re-use while we insert this new tab.
if (options.forceNewProcess) {
Services.ppmm.releaseCachedProcesses();
Services.prefs.setBoolPref(DISABLE_CONTENT_PROCESS_REUSE_PREF, true);
}
promises = [
BrowserTestUtils.switchTab(tabbrowser, function () {
if (typeof opening == "function") {
opening();
tab = tabbrowser.selectedTab;
} else {
tabbrowser.selectedTab = tab = BrowserTestUtils.addTab(
tabbrowser,
opening
);
}
}),
];
if (aWaitForLoad) {
promises.push(BrowserTestUtils.browserLoaded(tab.linkedBrowser));
}
if (aWaitForStateStop) {
promises.push(BrowserTestUtils.browserStopped(tab.linkedBrowser));
}
} finally {
// Clear the pref once we're done, if needed.
if (options.forceNewProcess) {
Services.prefs.clearUserPref(DISABLE_CONTENT_PROCESS_REUSE_PREF);
}
}
return Promise.all(promises).then(() => {
let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild;
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ startTime, category: "Test", innerWindowId },
"openNewForegroundTab"
);
return tab;
});
},
showOnlyTheseTabs(tabbrowser, tabs) {
for (let tab of tabs) {
tabbrowser.showTab(tab);
}
for (let tab of tabbrowser.tabs) {
if (!tabs.includes(tab)) {
tabbrowser.hideTab(tab);
}
}
},
/**
* Checks if a DOM element is hidden.
*
* @param {Element} element
* The element which is to be checked.
*
* @return {boolean}
*/
isHidden(element) {
if (
element.nodeType == Node.DOCUMENT_FRAGMENT_NODE &&
element.containingShadowRoot == element
) {
return BrowserTestUtils.isHidden(element.getRootNode().host);
}
let win = element.ownerGlobal;
let style = win.getComputedStyle(element);
if (style.display == "none") {
return true;
}
if (style.visibility != "visible") {
return true;
}
if (win.XULPopupElement.isInstance(element)) {
return ["hiding", "closed"].includes(element.state);
}
// Hiding a parent element will hide all its children
if (element.parentNode != element.ownerDocument) {
return BrowserTestUtils.isHidden(element.parentNode);
}
return false;
},
/**
* Checks if a DOM element is visible.
*
* @param {Element} element
* The element which is to be checked.
*
* @return {boolean}
*/
isVisible(element) {
if (
element.nodeType == Node.DOCUMENT_FRAGMENT_NODE &&
element.containingShadowRoot == element
) {
return BrowserTestUtils.isVisible(element.getRootNode().host);
}
let win = element.ownerGlobal;
let style = win.getComputedStyle(element);
if (style.display == "none") {
return false;
}
if (style.visibility != "visible") {
return false;
}
if (win.XULPopupElement.isInstance(element) && element.state != "open") {
return false;
}
// Hiding a parent element will hide all its children
if (element.parentNode != element.ownerDocument) {
return BrowserTestUtils.isVisible(element.parentNode);
}
return true;
},
/**
* If the argument is a browsingContext, return it. If the
* argument is a browser/frame, returns the browsing context for it.
*/
getBrowsingContextFrom(browser) {
if (Element.isInstance(browser)) {
return browser.browsingContext;
}
return browser;
},
/**
* Switches to a tab and resolves when it is ready.
*
* @param {tabbrowser} tabbrowser
* The tabbrowser.
* @param {tab} tab
* Either a tab element to switch to or a function to perform the switch.
*
* @return {Promise}
* Resolves when the tab has been switched to.
* @resolves The tab switched to.
*/
switchTab(tabbrowser, tab) {
let startTime = Cu.now();
let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild;
let promise = new Promise(resolve => {
tabbrowser.addEventListener(
"TabSwitchDone",
function () {
TestUtils.executeSoon(() => {
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ category: "Test", startTime, innerWindowId },
"switchTab"
);
resolve(tabbrowser.selectedTab);
});
},
{ once: true }
);
});
if (typeof tab == "function") {
tab();
} else {
tabbrowser.selectedTab = tab;
}
return promise;
},
/**
* Waits for an ongoing page load in a browser window to complete.
*
* This can be used in conjunction with any synchronous method for starting a
* load, like the "addTab" method on "tabbrowser", and must be called before
* yielding control to the event loop. Note that calling this after multiple
* successive load operations can be racy, so ``wantLoad`` should be specified
* in these cases.
*
* This function works by listening for custom load events on ``browser``. These
* are sent by a BrowserTestUtils window actor in response to "load" and
* "DOMContentLoaded" content events.
*
* @param {xul:browser} browser
* A xul:browser.
* @param {Boolean} [includeSubFrames = false]
* A boolean indicating if loads from subframes should be included.
* @param {string|function} [wantLoad = null]
* If a function, takes a URL and returns true if that's the load we're
* interested in. If a string, gives the URL of the load we're interested
* in. If not present, the first load resolves the promise.
* @param {boolean} [maybeErrorPage = false]
* If true, this uses DOMContentLoaded event instead of load event.
* Also wantLoad will be called with visible URL, instead of
* 'about:neterror?...' for error page.
*
* @return {Promise}
* @resolves When a load event is triggered for the browser.
*/
browserLoaded(
browser,
includeSubFrames = false,
wantLoad = null,
maybeErrorPage = false
) {
let startTime = Cu.now();
let { innerWindowId } = browser.ownerGlobal.windowGlobalChild;
// Passing a url as second argument is a common mistake we should prevent.
if (includeSubFrames && typeof includeSubFrames != "boolean") {
throw new Error(
"The second argument to browserLoaded should be a boolean."
);
}
// Consumers may pass gBrowser instead of a browser, so adjust for that.
if ("selectedBrowser" in browser) {
browser = browser.selectedBrowser;
}
// If browser belongs to tabbrowser-tab, ensure it has been
// inserted into the document.
let tabbrowser = browser.ownerGlobal.gBrowser;
if (tabbrowser && tabbrowser.getTabForBrowser) {
let tab = tabbrowser.getTabForBrowser(browser);
if (tab) {
tabbrowser._insertBrowser(tab);
}
}
function isWanted(url) {
if (!wantLoad) {
return true;
} else if (typeof wantLoad == "function") {
return wantLoad(url);
}
// for an http:// URL to be loaded and https-first is enabled,
// then we also return true in case the backend upgraded
// the load to https://.
if (
BrowserTestUtils._httpsFirstEnabled &&
typeof wantLoad == "string" &&
wantLoad.startsWith("http://")
) {
let wantLoadHttps = wantLoad.replace("http://", "https://");
if (wantLoadHttps == url) {
return true;
}
}
// It's a string.
return wantLoad == url;
}
// Error pages are loaded slightly differently, so listen for the
// DOMContentLoaded event for those instead.
let loadEvent = maybeErrorPage ? "DOMContentLoaded" : "load";
let eventName = `BrowserTestUtils:ContentEvent:${loadEvent}`;
return new Promise((resolve, reject) => {
function listener(event) {
switch (event.type) {
case eventName: {
let { browsingContext, internalURL, visibleURL } = event.detail;
// Sometimes we arrive here without an internalURL. If that's the
// case, just keep waiting until we get one.
if (!internalURL) {
return;
}
// Ignore subframes if we only care about the top-level load.
let subframe = browsingContext !== browsingContext.top;
if (subframe && !includeSubFrames) {
return;
}
// See testing/mochitest/BrowserTestUtils/content/BrowserTestUtilsChild.sys.mjs
// for the difference between visibleURL and internalURL.
if (!isWanted(maybeErrorPage ? visibleURL : internalURL)) {
return;
}
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ startTime, category: "Test", innerWindowId },
"browserLoaded: " + internalURL
);
resolve(internalURL);
break;
}
case "unload":
reject(
new Error(
"The window unloaded while we were waiting for the browser to load - this should never happen."
)
);
break;
default:
return;
}
browser.removeEventListener(eventName, listener, true);
browser.ownerGlobal.removeEventListener("unload", listener);
}
browser.addEventListener(eventName, listener, true);
browser.ownerGlobal.addEventListener("unload", listener);
});
},
/**
* Waits for the selected browser to load in a new window. This
* is most useful when you've got a window that might not have
* loaded its DOM yet, and where you can't easily use browserLoaded
* on gBrowser.selectedBrowser since gBrowser doesn't yet exist.
*
* @param {xul:window} window
* A newly opened window for which we're waiting for the
* first browser load.
* @param {Boolean} aboutBlank [optional]
* If false, about:blank loads are ignored and we continue
* to wait.
* @param {function|null} checkFn [optional]
* If checkFn(browser) returns false, the load is ignored
* and we continue to wait.
*
* @return {Promise}
* @resolves Once the selected browser fires its load event.
*/
firstBrowserLoaded(win, aboutBlank = true, checkFn = null) {
return this.waitForEvent(
win,
"BrowserTestUtils:ContentEvent:load",
true,
event => {
if (checkFn) {
return checkFn(event.target);
}
return (
win.gBrowser.selectedBrowser.currentURI.spec !== "about:blank" ||
aboutBlank
);
}
);
},
_webProgressListeners: new Set(),
_contentEventListenerSharedState: new Map(),
_contentEventListeners: new Map(),
/**
* Waits for the web progress listener associated with this tab to fire a
* state change that matches checkFn for the toplevel document.
*
* @param {xul:browser} browser
* A xul:browser.
* @param {String} expectedURI (optional)
* A specific URL to check the channel load against
* @param {Function} checkFn
* If checkFn(aStateFlags, aStatus) returns false, the state change
* is ignored and we continue to wait.
*
* @return {Promise}
* @resolves When the desired state change reaches the tab's progress listener
*/
waitForBrowserStateChange(browser, expectedURI, checkFn) {
return new Promise(resolve => {
let wpl = {
onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
dump(
"Saw state " +
aStateFlags.toString(16) +
" and status " +
aStatus.toString(16) +
"\n"
);
if (checkFn(aStateFlags, aStatus) && aWebProgress.isTopLevel) {
let chan = aRequest.QueryInterface(Ci.nsIChannel);
dump(
"Browser got expected state change " +
chan.originalURI.spec +
"\n"
);
if (!expectedURI || chan.originalURI.spec == expectedURI) {
browser.removeProgressListener(wpl);
BrowserTestUtils._webProgressListeners.delete(wpl);
resolve();
}
}
},
onSecurityChange() {},
onStatusChange() {},
onLocationChange() {},
onContentBlockingEvent() {},
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsIWebProgressListener2",
"nsISupportsWeakReference",
]),
};
browser.addProgressListener(wpl);
this._webProgressListeners.add(wpl);
dump(
"Waiting for browser state change" +
(expectedURI ? " of " + expectedURI : "") +
"\n"
);
});
},
/**
* Waits for the web progress listener associated with this tab to fire a
* STATE_STOP for the toplevel document.
*
* @param {xul:browser} browser
* A xul:browser.
* @param {String} expectedURI (optional)
* A specific URL to check the channel load against
* @param {Boolean} checkAborts (optional, defaults to false)
* Whether NS_BINDING_ABORTED stops 'count' as 'real' stops
* (e.g. caused by the stop button or equivalent APIs)
*
* @return {Promise}
* @resolves When STATE_STOP reaches the tab's progress listener
*/
browserStopped(browser, expectedURI, checkAborts = false) {
let testFn = function (aStateFlags, aStatus) {
return (
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
(checkAborts || aStatus != Cr.NS_BINDING_ABORTED)
);
};
dump(
"Waiting for browser load" +
(expectedURI ? " of " + expectedURI : "") +
"\n"
);
return BrowserTestUtils.waitForBrowserStateChange(
browser,
expectedURI,
testFn
);
},
/**
* Waits for the web progress listener associated with this tab to fire a
* STATE_START for the toplevel document.
*
* @param {xul:browser} browser
* A xul:browser.
* @param {String} expectedURI (optional)
* A specific URL to check the channel load against
*
* @return {Promise}
* @resolves When STATE_START reaches the tab's progress listener
*/
browserStarted(browser, expectedURI) {
let testFn = function (aStateFlags) {
return (
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_START
);
};
dump(
"Waiting for browser to start load" +
(expectedURI ? " of " + expectedURI : "") +
"\n"
);
return BrowserTestUtils.waitForBrowserStateChange(
browser,
expectedURI,
testFn
);
},
/**
* Waits for a tab to open and load a given URL.
*
* By default, the method doesn't wait for the tab contents to load.
*
* @param {tabbrowser} tabbrowser
* The tabbrowser to look for the next new tab in.
* @param {string|function} [wantLoad = null]
* If a function, takes a URL and returns true if that's the load we're
* interested in. If a string, gives the URL of the load we're interested
* in. If not present, the first non-about:blank load is used.
* @param {boolean} [waitForLoad = false]
* True to wait for the page in the new tab to load. Defaults to false.
* @param {boolean} [waitForAnyTab = false]
* True to wait for the url to be loaded in any new tab, not just the next
* one opened.
* @param {boolean} [maybeErrorPage = false]
* See ``browserLoaded`` function.
*
* @return {Promise}
* @resolves With the {xul:tab} when a tab is opened and its location changes
* to the given URL and optionally that browser has loaded.
*
* NB: this method will not work if you open a new tab with e.g. BrowserCommands.openTab
* and the tab does not load a URL, because no onLocationChange will fire.
*/
waitForNewTab(
tabbrowser,
wantLoad = null,
waitForLoad = false,
waitForAnyTab = false,
maybeErrorPage = false
) {
let urlMatches;
if (wantLoad && typeof wantLoad == "function") {
urlMatches = wantLoad;
} else if (wantLoad) {
urlMatches = urlToMatch => urlToMatch == wantLoad;
} else {
urlMatches = urlToMatch => urlToMatch != "about:blank";
}
return new Promise(resolve => {
tabbrowser.tabContainer.addEventListener(
"TabOpen",
function tabOpenListener(openEvent) {
if (!waitForAnyTab) {
tabbrowser.tabContainer.removeEventListener(
"TabOpen",
tabOpenListener
);
}
let newTab = openEvent.target;
let newBrowser = newTab.linkedBrowser;
let result;
if (waitForLoad) {
// If waiting for load, resolve with promise for that, which when load
// completes resolves to the new tab.
result = BrowserTestUtils.browserLoaded(
newBrowser,
false,
urlMatches,
maybeErrorPage
).then(() => newTab);
} else {
// If not waiting for load, just resolve with the new tab.
result = newTab;
}
let progressListener = {
onLocationChange(aBrowser) {
// Only interested in location changes on our browser.
if (aBrowser != newBrowser) {
return;
}
// Check that new location is the URL we want.
if (!urlMatches(aBrowser.currentURI.spec)) {
return;
}
if (waitForAnyTab) {
tabbrowser.tabContainer.removeEventListener(
"TabOpen",
tabOpenListener
);
}
tabbrowser.removeTabsProgressListener(progressListener);
TestUtils.executeSoon(() => resolve(result));
},
};
tabbrowser.addTabsProgressListener(progressListener);
}
);
});
},
/**
* Waits for onLocationChange.
*
* @param {tabbrowser} tabbrowser
* The tabbrowser to wait for the location change on.
* @param {string} [url]
* The string URL to look for. The URL must match the URL in the
* location bar exactly.
* @return {Promise}
* @resolves {webProgress, request, flags} When onLocationChange fires.
*/
waitForLocationChange(tabbrowser, url) {
return new Promise(resolve => {
let progressListener = {
onLocationChange(browser, webProgress, request, newURI, flags) {
if (
(url && newURI.spec != url) ||
(!url && newURI.spec == "about:blank")
) {
return;
}
tabbrowser.removeTabsProgressListener(progressListener);
resolve({ webProgress, request, flags });
},
};
tabbrowser.addTabsProgressListener(progressListener);
});
},
/**
* Waits for the next browser window to open and be fully loaded.
*
* @param {Object} aParams
* @param {string} [aParams.url]
* The url to await being loaded. If unset this may or may not wait for
* any page to be loaded, according to the waitForAnyURLLoaded param.
* @param {bool} [aParams.waitForAnyURLLoaded] When `url` is unset, this
* controls whether to wait for any initial URL to be loaded.
* Defaults to false, that means the initial browser may or may not
* have finished loading its first page when this resolves.
* When `url` is set, this is ignored, thus the load is always awaited for.
* @param {bool} [aParams.anyWindow]
* @param {bool} [aParams.maybeErrorPage]
* See ``browserLoaded`` function.
* @return {Promise}
* A Promise which resolves the next time that a DOM window
* opens and the delayed startup observer notification fires.
*/
waitForNewWindow(aParams = {}) {
let {
url = null,
anyWindow = false,
maybeErrorPage = false,
waitForAnyURLLoaded = false,
} = aParams;
if (anyWindow && !url) {
throw new Error("url should be specified if anyWindow is true");
}
return new Promise((resolve, reject) => {
let observe = async (win, topic) => {
if (topic != "domwindowopened") {
return;
}
try {
if (!anyWindow) {
Services.ww.unregisterNotification(observe);
}
// Add these event listeners now since they may fire before the
// DOMContentLoaded event down below.
let promises = [
this.waitForEvent(win, "focus", true),
this.waitForEvent(win, "activate"),
];
if (url || waitForAnyURLLoaded) {
await this.waitForEvent(win, "DOMContentLoaded");
if (win.document.documentURI != AppConstants.BROWSER_CHROME_URL) {
return;
}
}
promises.push(
TestUtils.topicObserved(
"browser-delayed-startup-finished",
subject => subject == win
)
);
if (url || waitForAnyURLLoaded) {
let loadPromise = this.browserLoaded(
win.gBrowser.selectedBrowser,
false,
waitForAnyURLLoaded ? null : url,
maybeErrorPage
);
promises.push(loadPromise);
}
await Promise.all(promises);
if (anyWindow) {
Services.ww.unregisterNotification(observe);
}
resolve(win);
} catch (err) {
// We failed to wait for the load in this URI. This is only an error
// if `anyWindow` is not set, as if it is we can just wait for another
// window.
if (!anyWindow) {
reject(err);
}
}
};
Services.ww.registerNotification(observe);
});
},
/**
* Starts the load of a new URI in the given browser, triggered by the system
* principal.
* Note this won't want for the load to be complete. For that you may either
* use BrowserTestUtils.browserLoaded(), BrowserTestUtils.waitForErrorPage(),
* or make your own handler.
*
* @param {xul:browser} browser
* A xul:browser.
* @param {string} uri
* The URI to load.
*/
startLoadingURIString(browser, uri) {
browser.fixupAndLoadURIString(uri, {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
},
/**
* Maybe create a preloaded browser and ensure it's finished loading.
*
* @param gBrowser (<xul:tabbrowser>)
* The tabbrowser in which to preload a browser.
*/
async maybeCreatePreloadedBrowser(gBrowser) {
let win = gBrowser.ownerGlobal;
win.NewTabPagePreloading.maybeCreatePreloadedBrowser(win);
// We cannot use the regular BrowserTestUtils helper for waiting here, since that
// would try to insert the preloaded browser, which would only break things.
await lazy.ContentTask.spawn(gBrowser.preloadedBrowser, [], async () => {
await ContentTaskUtils.waitForCondition(() => {
return (
this.content.document &&
this.content.document.readyState == "complete"
);
});
});
},
/**
* @param win (optional)
* The window we should wait to have "domwindowopened" sent through
* the observer service for. If this is not supplied, we'll just
* resolve when the first "domwindowopened" notification is seen.
* @param {function} checkFn [optional]
* Called with the nsIDOMWindow object as argument, should return true
* if the event is the expected one, or false if it should be ignored
* and observing should continue. If not specified, the first window
* resolves the returned promise.
* @return {Promise}
* A Promise which resolves when a "domwindowopened" notification
* has been fired by the window watcher.
*/
domWindowOpened(win, checkFn) {
return new Promise(resolve => {
async function observer(subject, topic) {
if (topic == "domwindowopened" && (!win || subject === win)) {
let observedWindow = subject;
if (checkFn && !(await checkFn(observedWindow))) {
return;
}
Services.ww.unregisterNotification(observer);
resolve(observedWindow);
}
}
Services.ww.registerNotification(observer);
});
},
/**
* @param win (optional)
* The window we should wait to have "domwindowopened" sent through
* the observer service for. If this is not supplied, we'll just
* resolve when the first "domwindowopened" notification is seen.
* The promise will be resolved once the new window's document has been
* loaded.
*
* @param {function} checkFn (optional)
* Called with the nsIDOMWindow object as argument, should return true
* if the event is the expected one, or false if it should be ignored
* and observing should continue. If not specified, the first window
* resolves the returned promise.
*
* @return {Promise}
* A Promise which resolves when a "domwindowopened" notification
* has been fired by the window watcher.
*/
domWindowOpenedAndLoaded(win, checkFn) {
return this.domWindowOpened(win, async observedWin => {
await this.waitForEvent(observedWin, "load");
if (checkFn && !(await checkFn(observedWin))) {
return false;
}
return true;
});
},
/**
* @param win (optional)
* The window we should wait to have "domwindowclosed" sent through
* the observer service for. If this is not supplied, we'll just
* resolve when the first "domwindowclosed" notification is seen.
* @return {Promise}
* A Promise which resolves when a "domwindowclosed" notification
* has been fired by the window watcher.
*/
domWindowClosed(win) {
return new Promise(resolve => {
function observer(subject, topic) {
if (topic == "domwindowclosed" && (!win || subject === win)) {
Services.ww.unregisterNotification(observer);
resolve(subject);
}
}
Services.ww.registerNotification(observer);
});
},
/**
* Open a new browser window from an existing one.
* This relies on OpenBrowserWindow in browser.js, and waits for the window
* to be completely loaded before resolving.
*
* @param {Object} options
* Options to pass to OpenBrowserWindow. Additionally, supports:
* @param {bool} options.waitForTabURL
* Forces the initial browserLoaded check to wait for the tab to
* load the given URL (instead of about:blank)
*
* @return {Promise}
* Resolves with the new window once it is loaded.
*/
async openNewBrowserWindow(options = {}) {
let startTime = Cu.now();
let currentWin = lazy.BrowserWindowTracker.getTopWindow({ private: false });
if (!currentWin) {
throw new Error(
"Can't open a new browser window from this helper if no non-private window is open."
);
}
let win = currentWin.OpenBrowserWindow(options);
let promises = [
this.waitForEvent(win, "focus", true),
this.waitForEvent(win, "activate"),
];
// Wait for browser-delayed-startup-finished notification, it indicates
// that the window has loaded completely and is ready to be used for
// testing.
promises.push(
TestUtils.topicObserved(
"browser-delayed-startup-finished",
subject => subject == win
).then(() => win)
);
promises.push(
this.firstBrowserLoaded(win, !options.waitForTabURL, browser => {
return (
!options.waitForTabURL ||
options.waitForTabURL == browser.currentURI.spec
);
})
);
await Promise.all(promises);
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ startTime, category: "Test" },
"openNewBrowserWindow"
);
return win;
},
/**
* Closes a window.
*
* @param {Window} win
* A window to close.
*
* @return {Promise}
* Resolves when the provided window has been closed. For browser
* windows, the Promise will also wait until all final SessionStore
* messages have been sent up from all browser tabs.
*/
closeWindow(win) {
let closedPromise = BrowserTestUtils.windowClosed(win);
win.close();
return closedPromise;
},
/**
* Returns a Promise that resolves when a window has finished closing.
*
* @param {Window} win
* The closing window.
*
* @return {Promise}
* Resolves when the provided window has been fully closed. For
* browser windows, the Promise will also wait until all final
* SessionStore messages have been sent up from all browser tabs.
*/
windowClosed(win) {
let domWinClosedPromise = BrowserTestUtils.domWindowClosed(win);
let promises = [domWinClosedPromise];
let winType = win.document.documentElement.getAttribute("windowtype");
let flushTopic = "sessionstore-browser-shutdown-flush";
if (winType == "navigator:browser") {
let finalMsgsPromise = new Promise(resolve => {
let browserSet = new Set(win.gBrowser.browsers);
// Ensure all browsers have been inserted or we won't get
// messages back from them.
browserSet.forEach(browser => {
win.gBrowser._insertBrowser(win.gBrowser.getTabForBrowser(browser));
});
let observer = subject => {
if (browserSet.has(subject)) {
browserSet.delete(subject);
}
if (!browserSet.size) {
Services.obs.removeObserver(observer, flushTopic);
// Give the TabStateFlusher a chance to react to this final
// update and for the TabStateFlusher.flushWindow promise
// to resolve before we resolve.
TestUtils.executeSoon(resolve);
}
};
Services.obs.addObserver(observer, flushTopic);
});
promises.push(finalMsgsPromise);
}
return Promise.all(promises);
},
/**
* Returns a Promise that resolves once the SessionStore information for the
* given tab is updated and all listeners are called.
*
* @param {xul:tab} tab
* The tab that will be removed.
* @returns {Promise}
* @resolves When the SessionStore information is updated.
*/
waitForSessionStoreUpdate(tab) {
return new Promise(resolve => {
let browser = tab.linkedBrowser;
let flushTopic = "sessionstore-browser-shutdown-flush";
let observer = subject => {
if (subject === browser) {
Services.obs.removeObserver(observer, flushTopic);
// Wait for the next event tick to make sure other listeners are
// called.
TestUtils.executeSoon(() => resolve());
}
};
Services.obs.addObserver(observer, flushTopic);
});
},
/**
* Waits for an event to be fired on a specified element.
*
* @example
*
* let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName");
* // Do some processing here that will cause the event to be fired
* // ...
* // Now wait until the Promise is fulfilled
* let receivedEvent = await promiseEvent;
*
* @example
* // The promise resolution/rejection handler for the returned promise is
* // guaranteed not to be called until the next event tick after the event
* // listener gets called, so that all other event listeners for the element
* // are executed before the handler is executed.
*
* let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName");
* // Same event tick here.
* await promiseEvent;
* // Next event tick here.
*
* @example
* // If some code, such like adding yet another event listener, needs to be
* // executed in the same event tick, use raw addEventListener instead and
* // place the code inside the event listener.
*
* element.addEventListener("load", () => {
* // Add yet another event listener in the same event tick as the load
* // event listener.
* p = BrowserTestUtils.waitForEvent(element, "ready");
* }, { once: true });
*
* @param {Element} subject
* The element that should receive the event.
* @param {string} eventName
* Name of the event to listen to.
* @param {bool} [capture]
* True to use a capturing listener.
* @param {function} [checkFn]
* Called with the Event object as argument, should return true if the
* event is the expected one, or false if it should be ignored and
* listening should continue. If not specified, the first event with
* the specified name resolves the returned promise.
* @param {bool} [wantsUntrusted=false]
* True to receive synthetic events dispatched by web content.
*
* @note Because this function is intended for testing, any error in checkFn
* will cause the returned promise to be rejected instead of waiting for
* the next event, since this is probably a bug in the test.
*
* @returns {Promise}
* @resolves The Event object.
*/
waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted) {
let startTime = Cu.now();
let innerWindowId = subject.ownerGlobal?.windowGlobalChild.innerWindowId;
return new Promise((resolve, reject) => {
let removed = false;
function listener(event) {
function cleanup() {
removed = true;
// Avoid keeping references to objects after the promise resolves.
subject = null;
checkFn = null;
}
try {
if (checkFn && !checkFn(event)) {
return;
}
subject.removeEventListener(eventName, listener, capture);
cleanup();
TestUtils.executeSoon(() => {
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ startTime, category: "Test", innerWindowId },
"waitForEvent: " + eventName
);
resolve(event);
});
} catch (ex) {
try {
subject.removeEventListener(eventName, listener, capture);
} catch (ex2) {
// Maybe the provided object does not support removeEventListener.
}
cleanup();
TestUtils.executeSoon(() => reject(ex));
}
}
subject.addEventListener(eventName, listener, capture, wantsUntrusted);
TestUtils.promiseTestFinished?.then(() => {
if (removed) {
return;
}
subject.removeEventListener(eventName, listener, capture);
let text = eventName + " listener";
if (subject.id) {
text += ` on #${subject.id}`;
}
text += " not removed before the end of test";
reject(text);
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ startTime, category: "Test", innerWindowId },
"waitForEvent: " + text
);
});
});
},
/**
* Like waitForEvent, but adds the event listener to the message manager
* global for browser.
*
* @param {string} eventName
* Name of the event to listen to.
* @param {bool} capture [optional]
* Whether to use a capturing listener.
* @param {function} checkFn [optional]
* Called with the Event object as argument, should return true if the
* event is the expected one, or false if it should be ignored and
* listening should continue. If not specified, the first event with
* the specified name resolves the returned promise.
* @param {bool} wantUntrusted [optional]
* Whether to accept untrusted events
*
* promise in the case of a checkFn error. Instead, since checkFn is now
* called through eval in the content process, the error is thrown in
* the listener created by ContentEventListenerChild. Work to improve
* error handling (eg. to reject the promise as before and to preserve
*
* @returns {Promise}
*/
waitForContentEvent(
browser,
eventName,
capture = false,
checkFn,
wantUntrusted = false
) {
return new Promise(resolve => {
let removeEventListener = this.addContentEventListener(
browser,
eventName,
() => {
removeEventListener();
resolve(eventName);
},
{ capture, wantUntrusted },
checkFn
);
});
},
/**
* Like waitForEvent, but acts on a popup. It ensures the popup is not already
* in the expected state.
*
* @param {Element} popup
* The popup element that should receive the event.
* @param {string} eventSuffix
* The event suffix expected to be received, one of "shown" or "hidden".
* @returns {Promise}
*/
waitForPopupEvent(popup, eventSuffix) {
let endState = { shown: "open", hidden: "closed" }[eventSuffix];
if (popup.state == endState) {
return Promise.resolve();
}
return this.waitForEvent(popup, "popup" + eventSuffix);
},
/**
* Waits for the select popup to be shown. This is needed because the select
* dropdown is created lazily.
*
* @param {Window} win
* A window to expect the popup in.
*
* @return {Promise}
* Resolves when the popup has been fully opened. The resolution value
* is the select popup.
*/
async waitForSelectPopupShown(win) {
let getMenulist = () =>
win.document.getElementById("ContentSelectDropdown");
let menulist = getMenulist();
if (!menulist) {
await this.waitForMutationCondition(
win.document,
{ childList: true, subtree: true },
getMenulist
);
menulist = getMenulist();
if (menulist.menupopup.state == "open") {
return menulist.menupopup;
}
}
await this.waitForEvent(menulist.menupopup, "popupshown");
return menulist.menupopup;
},
/**
* Waits for the datetime picker popup to be shown.
*
* @param {Window} win
* A window to expect the popup in.
*
* @return {Promise}
* Resolves when the popup has been fully opened. The resolution value
* is the select popup.
*/
async waitForDateTimePickerPanelShown(win) {
let getPanel = () => win.document.getElementById("DateTimePickerPanel");
let panel = getPanel();
let ensureReady = async () => {
let frame = panel.querySelector("#dateTimePopupFrame");
let isValidUrl = () => {
return (
frame.browsingContext?.currentURI?.spec ==
"chrome://global/content/datepicker.xhtml" ||
frame.browsingContext?.currentURI?.spec ==
"chrome://global/content/timepicker.xhtml"
);
};
// Ensure it's loaded.
if (!isValidUrl() || frame.contentDocument.readyState != "complete") {
await new Promise(resolve => {
frame.addEventListener(
"load",
function listener() {
if (isValidUrl()) {
frame.removeEventListener("load", listener, { capture: true });
resolve();
}
},
{ capture: true }
);
});
}
// Ensure it's ready.
if (!frame.contentWindow.PICKER_READY) {
await new Promise(resolve => {
frame.contentDocument.addEventListener("PickerReady", resolve, {
once: true,
});
});
}
// And that l10n mutations are flushed.
// showing the panel.
if (frame.contentDocument.hasPendingL10nMutations) {
await new Promise(resolve => {
frame.contentDocument.addEventListener(
"L10nMutationsFinished",
resolve,
{
once: true,
}
);
});
}
};
if (!panel) {
await this.waitForMutationCondition(
win.document,
{ childList: true, subtree: true },
getPanel
);
panel = getPanel();
if (panel.state == "open") {
await ensureReady();
return panel;
}
}
await this.waitForEvent(panel, "popupshown");
await ensureReady();
return panel;
},
/**
* Adds a content event listener on the given browser
* element. Similar to waitForContentEvent, but the listener will
* fire until it is removed. A callable object is returned that,
* when called, removes the event listener. Note that this function
* works even if the browser's frameloader is swapped.
*
* @param {xul:browser} browser
* The browser element to listen for events in.
* @param {string} eventName
* Name of the event to listen to.
* @param {function} listener
* Function to call in parent process when event fires.
* Not passed any arguments.
* @param {object} listenerOptions [optional]
* Options to pass to the event listener.
* @param {function} checkFn [optional]
* Called with the Event object as argument, should return true if the
* event is the expected one, or false if it should be ignored and
* listening should continue. If not specified, the first event with
* the specified name resolves the returned promise. This is called
* within the content process and can have no closure environment.
*
* @returns function
* If called, the return value will remove the event listener.
*/
addContentEventListener(
browser,
eventName,
listener,
listenerOptions = {},
checkFn
) {
let id = gListenerId++;
let contentEventListeners = this._contentEventListeners;
contentEventListeners.set(id, {
listener,
browserId: browser.browserId,
});
let eventListenerState = this._contentEventListenerSharedState;
eventListenerState.set(id, {
eventName,
listenerOptions,
checkFnSource: checkFn ? checkFn.toSource() : "",
});
Services.ppmm.sharedData.set(
"BrowserTestUtils:ContentEventListener",
eventListenerState
);
Services.ppmm.sharedData.flush();
let unregisterFunction = function () {
if (!eventListenerState.has(id)) {
return;
}
eventListenerState.delete(id);
contentEventListeners.delete(id);
Services.ppmm.sharedData.set(
"BrowserTestUtils:ContentEventListener",
eventListenerState
);
Services.ppmm.sharedData.flush();
};
return unregisterFunction;
},
/**
* This is an internal method to be invoked by
* BrowserTestUtilsParent.sys.mjs when a content event we were listening for
* happens.
*
* @private
*/
_receivedContentEventListener(listenerId, browserId) {
let listenerData = this._contentEventListeners.get(listenerId);
if (!listenerData) {
return;
}
if (listenerData.browserId != browserId) {
return;
}
listenerData.listener();
},
/**
* This is an internal method that cleans up any state from content event
* listeners.
*
* @private
*/
_cleanupContentEventListeners() {
this._contentEventListeners.clear();
if (this._contentEventListenerSharedState.size != 0) {
this._contentEventListenerSharedState.clear();
Services.ppmm.sharedData.set(
"BrowserTestUtils:ContentEventListener",
this._contentEventListenerSharedState
);
Services.ppmm.sharedData.flush();
}
if (this._contentEventListenerActorRegistered) {
this._contentEventListenerActorRegistered = false;
ChromeUtils.unregisterWindowActor("ContentEventListener");
}
},
observe(subject, topic) {
switch (topic) {
case "test-complete":
this._cleanupContentEventListeners();
break;
}
},
/**
* Wait until DOM mutations cause the condition expressed in checkFn
* to pass.
*
* Intended as an easy-to-use alternative to waitForCondition.
*
* @param {Element} target The target in which to observe mutations.
* @param {Object} options The options to pass to MutationObserver.observe();
* @param {function} checkFn Function that returns true when it wants the promise to be
* resolved.
*/
waitForMutationCondition(target, options, checkFn) {
if (checkFn()) {
return Promise.resolve();
}
return new Promise(resolve => {
let obs = new target.ownerGlobal.MutationObserver(function () {
if (checkFn()) {
obs.disconnect();
resolve();
}
});
obs.observe(target, options);
});
},
/**
* Like browserLoaded, but waits for an error page to appear.
*
* @param {xul:browser} browser
* A xul:browser.
*
* @return {Promise}
* @resolves When an error page has been loaded in the browser.
*/
waitForErrorPage(browser) {
return this.waitForContentEvent(
browser,
"AboutNetErrorLoad",
false,
null,
true
);
},
/**
* Waits for the next top-level document load in the current browser. The URI
* of the document is compared against expectedURL. The load is then stopped
* before it actually starts.
*
* @param {string} expectedURL
* The URL of the document that is expected to load.
* @param {object} browser
* The browser to wait for.
* @param {function} checkFn (optional)
* Function to run on the channel before stopping it.
* @returns {Promise}
*/
waitForDocLoadAndStopIt(expectedURL, browser, checkFn) {
let isHttp = url => /^https?:/.test(url);
return new Promise(resolve => {
// use http-on-before-connect to listen for loads. Since we're
// aborting the load as early as possible, it doesn't matter whether the
// server handles it sensibly or not. However, this also means that this
// helper shouldn't be used to load local URIs (about pages, chrome://
// URIs, etc).
let proxyFilter;
if (!isHttp(expectedURL)) {
proxyFilter = {
proxyInfo: lazy.ProtocolProxyService.newProxyInfo(
"http",
"mochi.test",
8888,
"",
"",
0,