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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* 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";
import { ComponentUtils } from "resource://gre/modules/ComponentUtils.sys.mjs";
import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
XPCOMUtils.defineLazyServiceGetters(lazy, {
ProtocolProxyService: [
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService",
],
});
const PROCESSSELECTOR_CONTRACTID = "@mozilla.org/ipc/processselector;1";
const OUR_PROCESSSELECTOR_CID = Components.ID(
"{f9746211-3d53-4465-9aeb-ca0d96de0253}"
);
const EXISTING_JSID = Cc[PROCESSSELECTOR_CONTRACTID];
const DEFAULT_PROCESSSELECTOR_CID = EXISTING_JSID
? Components.ID(EXISTING_JSID.number)
: null;
let gListenerId = 0;
// A process selector that always asks for a new process.
function NewProcessSelector() {}
NewProcessSelector.prototype = {
classID: OUR_PROCESSSELECTOR_CID,
QueryInterface: ChromeUtils.generateQI(["nsIContentProcessProvider"]),
provideProcess() {
return Ci.nsIContentProcessProvider.NEW_PROCESS;
},
};
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
let selectorFactory =
ComponentUtils.generateSingletonFactory(NewProcessSelector);
registrar.registerFactory(OUR_PROCESSSELECTOR_CID, "", null, selectorFactory);
const kAboutPageRegistrationContentScript =
/**
* Create and register the BrowserTestUtils and ContentEventListener window
* actors.
*/
function registerActors() {
ChromeUtils.registerWindowActor("BrowserTestUtils", {
},
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, replace the normal process
// selector with one that always asks for a new process.
// If DEFAULT_PROCESSSELECTOR_CID is null, we're in non-e10s mode and we
// should skip this.
if (options.forceNewProcess && DEFAULT_PROCESSSELECTOR_CID) {
Services.ppmm.releaseCachedProcesses();
registrar.registerFactory(
OUR_PROCESSSELECTOR_CID,
"",
PROCESSSELECTOR_CONTRACTID,
null
);
}
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 {
// Restore the original process selector, if needed.
if (options.forceNewProcess && DEFAULT_PROCESSSELECTOR_CID) {
registrar.registerFactory(
DEFAULT_PROCESSSELECTOR_CID,
"",
PROCESSSELECTOR_CONTRACTID,
null
);
}
}
return Promise.all(promises).then(() => {
let { innerWindowId } = tabbrowser.ownerGlobal.windowGlobalChild;
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ startTime, category: "Test", innerWindowId },
"openNewForegroundTab"
);
return 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);
}
// HTTPS-First (Bug 1704453) TODO: In case we are waiting
// 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 When onLocationChange fires.
*/
waitForLocationChange(tabbrowser, url) {
return new Promise(resolve => {
let progressListener = {
onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI) {
if (
(url && aLocationURI.spec != url) ||
(!url && aLocationURI.spec == "about:blank")
) {
return;
}
tabbrowser.removeTabsProgressListener(progressListener);
resolve();
},
};
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
*
* @note As of bug 1588193, this function no longer rejects the returned
* 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
* the filename/stack) is being tracked in bug 1593811.
*
* @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 ==
frame.browsingContext?.currentURI?.spec ==
);
};
// 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.
// FIXME(bug 1828721): We should ideally localize everything before
// 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 => {
// Redirect non-http URIs to http://mochi.test:8888/, so we can still
// 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,
4096,
null
),
applyFilter(channel, defaultProxyInfo, callback) {
callback.onProxyFilterResult(
isHttp(channel.URI.spec) ? defaultProxyInfo : this.proxyInfo
);
},
};
lazy.ProtocolProxyService.registerChannelFilter(proxyFilter, 0);
}
function observer(chan) {
chan.QueryInterface(Ci.nsIHttpChannel);
if (!chan.originalURI || chan.originalURI.spec !== expectedURL) {
return;
}
if (checkFn && !checkFn(chan)) {
return;
}
// TODO: We should check that the channel's BrowsingContext matches
// the browser's. See bug 1587114.
try {
chan.cancel(Cr.NS_BINDING_ABORTED);
} finally {
if (proxyFilter) {
lazy.ProtocolProxyService.unregisterChannelFilter(proxyFilter);
}
Services.obs.removeObserver(observer, "http-on-before-connect");
resolve();
}
}
Services.obs.addObserver(observer, "http-on-before-connect");
});
},
/**
* Versions of EventUtils.sys.mjs synthesizeMouse functions that synthesize a
* mouse event in a child process and return promises that resolve when the
* event has fired and completed. Instead of a window, a browser or
* browsing context is required to be passed to this function.
*
* @param target
* One of the following:
* - a selector string that identifies the element to target. The syntax is as
* for querySelector.
* - a function to be run in the content process that returns the element to
* target
* - null, in which case the offset is from the content document's edge.
* @param {integer} offsetX
* x offset from target's left bounding edge
* @param {integer} offsetY
* y offset from target's top bounding edge
* @param {Object} event object
* Additional arguments, similar to the EventUtils.sys.mjs version
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
* Browsing context or browser element, must not be null
* @param {boolean} handlingUserInput
* Whether the synthesize should be perfomed while simulating
* user interaction (making windowUtils.isHandlingUserInput be true).
*
* @returns {Promise}
* @resolves True if the mouse event was cancelled.
*/
synthesizeMouse(
target,
offsetX,
offsetY,
event,
browsingContext,
handlingUserInput
) {
let targetFn = null;
if (typeof target == "function") {
targetFn = target.toString();
target = null;
} else if (typeof target != "string" && !Array.isArray(target)) {
target = null;
}
browsingContext = this.getBrowsingContextFrom(browsingContext);
return this.sendQuery(browsingContext, "Test:SynthesizeMouse", {
target,
targetFn,
x: offsetX,
y: offsetY,
event,
handlingUserInput,
});
},
/**
* Versions of EventUtils.sys.mjs synthesizeTouch functions that synthesize a
* touch event in a child process and return promises that resolve when the
* event has fired and completed. Instead of a window, a browser or
* browsing context is required to be passed to this function.
*
* @param target
* One of the following:
* - a selector string that identifies the element to target. The syntax is as
* for querySelector.
* - a function to be run in the content process that returns the element to
* target
* - null, in which case the offset is from the content document's edge.
* @param {integer} offsetX
* x offset from target's left bounding edge
* @param {integer} offsetY
* y offset from target's top bounding edge
* @param {Object} event object
* Additional arguments, similar to the EventUtils.sys.mjs version
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
* Browsing context or browser element, must not be null
*
* @returns {Promise}
* @resolves True if the touch event was cancelled.
*/
synthesizeTouch(target, offsetX, offsetY, event, browsingContext) {
let targetFn = null;
if (typeof target == "function") {
targetFn = target.toString();
target = null;
} else if (typeof target != "string" && !Array.isArray(target)) {
target = null;
}
browsingContext = this.getBrowsingContextFrom(browsingContext);
return this.sendQuery(browsingContext, "Test:SynthesizeTouch", {
target,
targetFn,
x: offsetX,
y: offsetY,
event,
});
},
/**
* Wait for a message to be fired from a particular message manager
*
* @param {nsIMessageManager} messageManager
* The message manager that should be used.
* @param {String} message
* The message we're waiting for.
* @param {Function} checkFn (optional)
* Optional function to invoke to check the message.
*/
waitForMessage(messageManager, message, checkFn) {
return new Promise(resolve => {
messageManager.addMessageListener(message, function onMessage(msg) {
if (!checkFn || checkFn(msg)) {
messageManager.removeMessageListener(message, onMessage);
resolve(msg.data);
}
});
});
},
/**
* Version of synthesizeMouse that uses the center of the target as the mouse
* location. Arguments and the return value are the same.
*/
synthesizeMouseAtCenter(target, event, browsingContext) {
// Use a flag to indicate to center rather than having a separate message.
event.centered = true;
return BrowserTestUtils.synthesizeMouse(
target,
0,
0,
event,
browsingContext
);
},
/**
* Version of synthesizeMouse that uses a client point within the child
* window instead of a target as the offset. Otherwise, the arguments and
* return value are the same as synthesizeMouse.
*/
synthesizeMouseAtPoint(offsetX, offsetY, event, browsingContext) {
return BrowserTestUtils.synthesizeMouse(
null,
offsetX,
offsetY,
event,
browsingContext
);
},
/**
* Removes the given tab from its parent tabbrowser.
* This method doesn't SessionStore etc.
*
* @param (tab) tab
* The tab to remove.
* @param (Object) options
* Extra options to pass to tabbrowser's removeTab method.
*/
removeTab(tab, options = {}) {
tab.ownerGlobal.gBrowser.removeTab(tab, options);
},
/**
* Returns a Promise that resolves once the tab starts closing.
*
* @param (tab) tab
* The tab that will be removed.
* @returns (Promise)
* @resolves When the tab starts closing. Does not get passed a value.
*/
waitForTabClosing(tab) {
return this.waitForEvent(tab, "TabClose");
},
/**
*
* @param {tab} tab
* The tab that will be reloaded.
* @param {Boolean} [includeSubFrames = false]
* A boolean indicating if loads from subframes should be included
* when waiting for the frame to reload.
* @returns {Promise}
* @resolves When the tab finishes reloading.
*/
reloadTab(tab, includeSubFrames = false) {
const finished = BrowserTestUtils.browserLoaded(
tab.linkedBrowser,
includeSubFrames
);
tab.ownerGlobal.gBrowser.reloadTab(tab);
return finished;
},
/**
* Create enough tabs to cause a tab overflow in the given window.
* @param {Function} registerCleanupFunction
* The test framework doesn't keep its cleanup stuff anywhere accessible,
* so the first argument is a reference to your cleanup registration
* function, allowing us to clean up after you if necessary.
* @param {Window} win
* The window where the tabs need to be overflowed.
* @param {object} params [optional]
* Parameters object for BrowserTestUtils.overflowTabs.
* overflowAtStart: bool
* Determines whether the new tabs are added at the beginning of the
* URL bar or at the end of it.
* overflowTabFactor: 3 | 1.1
* Factor that helps in determining the tab count for overflow.
*/
async overflowTabs(registerCleanupFunction, win, params = {}) {
if (!params.hasOwnProperty("overflowAtStart")) {
params.overflowAtStart = true;
}
if (!params.hasOwnProperty("overflowTabFactor")) {
params.overflowTabFactor = 1.1;
}
let index = params.overflowAtStart ? 0 : undefined;
let { gBrowser } = win;
let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
const originalSmoothScroll = arrowScrollbox.smoothScroll;
arrowScrollbox.smoothScroll = false;
registerCleanupFunction(() => {
arrowScrollbox.smoothScroll = originalSmoothScroll;
});
let width = ele => ele.getBoundingClientRect().width;
let tabMinWidth = parseInt(
win.getComputedStyle(gBrowser.selectedTab).minWidth
);
let tabCountForOverflow = Math.ceil(
(width(arrowScrollbox) / tabMinWidth) * params.overflowTabFactor
);
while (gBrowser.tabs.length < tabCountForOverflow) {
BrowserTestUtils.addTab(gBrowser, "about:blank", {
skipAnimation: true,
index,
});
}
},
/**
* Crashes a remote frame tab and cleans up the generated minidumps.
* Resolves with the data from the .extra file (the crash annotations).
*
* @param (Browser) browser
* A remote <xul:browser> element. Must not be null.
* @param (bool) shouldShowTabCrashPage
* True if it is expected that the tab crashed page will be shown
* for this browser. If so, the Promise will only resolve once the
* tab crash page has loaded.
* @param (bool) shouldClearMinidumps
* True if the minidumps left behind by the crash should be removed.
* @param (BrowsingContext) browsingContext
* The context where the frame leaves. Default to
* top level context if not supplied.
* @param (object?) options
* An object with any of the following fields:
* crashType: "CRASH_INVALID_POINTER_DEREF" | "CRASH_OOM"
* The type of crash. If unspecified, default to "CRASH_INVALID_POINTER_DEREF"
* asyncCrash: bool
* If specified and `true`, cause the crash asynchronously.
*
* @returns (Promise)
* @resolves An Object with key-value pairs representing the data from the
* crash report's extra file (if applicable).
*/
async crashFrame(
browser,
shouldShowTabCrashPage = true,
shouldClearMinidumps = true,
browsingContext,
options = {}
) {
let extra = {};
if (!browser.isRemoteBrowser) {
throw new Error("<xul:browser> needs to be remote in order to crash");
}
/**
* Returns the directory where crash dumps are stored.
*
* @return nsIFile
*/
function getMinidumpDirectory() {
let dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
dir.append("minidumps");
return dir;
}
/**
* Removes a file from a directory. This is a no-op if the file does not
* exist.
*
* @param directory
* The nsIFile representing the directory to remove from.
* @param filename
* A string for the file to remove from the directory.
*/
function removeFile(directory, filename) {
let file = directory.clone();
file.append(filename);
if (file.exists()) {
file.remove(false);
}
}
let expectedPromises = [];
let crashCleanupPromise = new Promise((resolve, reject) => {
let observer = (subject, topic) => {
if (topic != "ipc:content-shutdown") {
reject("Received incorrect observer topic: " + topic);
return;
}
if (!(subject instanceof Ci.nsIPropertyBag2)) {
reject("Subject did not implement nsIPropertyBag2");
return;
}
// we might see this called as the process terminates due to previous tests.
// We are only looking for "abnormal" exits...
if (!subject.hasKey("abnormal")) {
dump(
"\nThis is a normal termination and isn't the one we are looking for...\n"
);
return;
}
Services.obs.removeObserver(observer, "ipc:content-shutdown");
let dumpID;
if (AppConstants.MOZ_CRASHREPORTER) {
dumpID = subject.getPropertyAsAString("dumpID");
if (!dumpID) {
reject(
"dumpID was not present despite crash reporting being enabled"
);
return;
}
}
let removalPromise = Promise.resolve();
if (dumpID) {
removalPromise = Services.crashmanager
.ensureCrashIsPresent(dumpID)
.then(async () => {
let minidumpDirectory = getMinidumpDirectory();
let extrafile = minidumpDirectory.clone();
extrafile.append(dumpID + ".extra");
if (extrafile.exists()) {
if (AppConstants.MOZ_CRASHREPORTER) {
extra = await IOUtils.readJSON(extrafile.path);
} else {
dump(
"\nCrashReporter not enabled - will not return any extra data\n"
);
}
} else {
dump(`\nNo .extra file for dumpID: ${dumpID}\n`);
}
if (shouldClearMinidumps) {
removeFile(minidumpDirectory, dumpID + ".dmp");
removeFile(minidumpDirectory, dumpID + ".extra");
}
});
}
removalPromise.then(() => {
dump("\nCrash cleaned up\n");
// There might be other ipc:content-shutdown handlers that need to
// run before we want to continue, so we'll resolve on the next tick
// of the event loop.
TestUtils.executeSoon(() => resolve());
});
};
Services.obs.addObserver(observer, "ipc:content-shutdown");
});
expectedPromises.push(crashCleanupPromise);
if (shouldShowTabCrashPage) {
expectedPromises.push(
new Promise(resolve => {
browser.addEventListener(
"AboutTabCrashedReady",
function onCrash() {
browser.removeEventListener("AboutTabCrashedReady", onCrash);
dump("\nabout:tabcrashed loaded and ready\n");
resolve();
},
false,
true
);
})
);
}
// Trigger crash by sending a message to BrowserTestUtils actor.
this.sendAsyncMessage(
browsingContext || browser.browsingContext,
"BrowserTestUtils:CrashFrame",
{
crashType: options.crashType || "",
asyncCrash: options.asyncCrash || false,
}
);
await Promise.all(expectedPromises);
if (shouldShowTabCrashPage) {
let gBrowser = browser.ownerGlobal.gBrowser;
let tab = gBrowser.getTabForBrowser(browser);
if (tab.getAttribute("crashed") != "true") {
throw new Error("Tab should be marked as crashed");
}
}
return extra;
},
/**
* Attempts to simulate a launch fail by crashing a browser, but
* stripping the browser of its childID so that the TabCrashHandler
* thinks it was a launch fail.
*
* @param browser (<xul:browser>)
* The browser to simulate a content process launch failure on.
* @return Promise
* @resolves undefined
* Resolves when the TabCrashHandler should be done handling the
* simulated crash.
*/
simulateProcessLaunchFail(browser, dueToBuildIDMismatch = false) {
const NORMAL_CRASH_TOPIC = "ipc:content-shutdown";
Object.defineProperty(browser.frameLoader, "childID", {
get: () => 0,
});
let sawNormalCrash = false;
let observer = () => {
sawNormalCrash = true;
};
Services.obs.addObserver(observer, NORMAL_CRASH_TOPIC);
Services.obs.notifyObservers(
browser.frameLoader,
"oop-frameloader-crashed"
);
let eventType = dueToBuildIDMismatch
? "oop-browser-buildid-mismatch"
: "oop-browser-crashed";
let event = new browser.ownerGlobal.CustomEvent(eventType, {
bubbles: true,
});
event.isTopFrame = true;
browser.dispatchEvent(event);
Services.obs.removeObserver(observer, NORMAL_CRASH_TOPIC);
if (sawNormalCrash) {
throw new Error(`Unexpectedly saw ${NORMAL_CRASH_TOPIC}`);
}
return new Promise(resolve => TestUtils.executeSoon(resolve));
},
/**
* Returns a promise that is resolved when element gains attribute (or,
* optionally, when it is set to value).
* @param {String} attr
* The attribute to wait for
* @param {Element} element
* The element which should gain the attribute
* @param {String} value (optional)
* Optional, the value the attribute should have.
*
* @returns {Promise}
*/
waitForAttribute(attr, element, value) {
let MutationObserver = element.ownerGlobal.MutationObserver;
return new Promise(resolve => {
let mut = new MutationObserver(() => {
if (
(!value && element.hasAttribute(attr)) ||
(value && element.getAttribute(attr) === value)
) {
resolve();
mut.disconnect();
}
});
mut.observe(element, { attributeFilter: [attr] });
});
},
/**
* Returns a promise that is resolved when element loses an attribute.
* @param {String} attr
* The attribute to wait for
* @param {Element} element
* The element which should lose the attribute
*
* @returns {Promise}
*/
waitForAttributeRemoval(attr, element) {
if (!element.hasAttribute(attr)) {
return Promise.resolve();
}
let MutationObserver = element.ownerGlobal.MutationObserver;
return new Promise(resolve => {
dump("Waiting for removal\n");
let mut = new MutationObserver(() => {
if (!element.hasAttribute(attr)) {
resolve();
mut.disconnect();
}
});
mut.observe(element, { attributeFilter: [attr] });
});
},
/**
* Version of EventUtils' `sendChar` function; it will synthesize a keypress
* event in a child process and returns a Promise that will resolve when the
* event was fired. Instead of a Window, a Browser or Browsing Context
* is required to be passed to this function.
*
* @param {String} char
* A character for the keypress event that is sent to the browser.
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
* Browsing context or browser element, must not be null
*
* @returns {Promise}
* @resolves True if the keypress event was synthesized.
*/
sendChar(char, browsingContext) {
browsingContext = this.getBrowsingContextFrom(browsingContext);
return this.sendQuery(browsingContext, "Test:SendChar", { char });
},
/**
* Version of EventUtils' `synthesizeKey` function; it will synthesize a key
* event in a child process and returns a Promise that will resolve when the
* event was fired. Instead of a Window, a Browser or Browsing Context
* is required to be passed to this function.
*
* @param {String} key
* See the documentation available for EventUtils#synthesizeKey.
* @param {Object} event
* See the documentation available for EventUtils#synthesizeKey.
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
* Browsing context or browser element, must not be null
*
* @returns {Promise}
*/
synthesizeKey(key, event, browsingContext) {
browsingContext = this.getBrowsingContextFrom(browsingContext);
return this.sendQuery(browsingContext, "Test:SynthesizeKey", {
key,
event,
});
},
/**
* Version of EventUtils' `synthesizeComposition` function; it will synthesize
* a composition event in a child process and returns a Promise that will
* resolve when the event was fired. Instead of a Window, a Browser or
* Browsing Context is required to be passed to this function.
*
* @param {Object} event
* See the documentation available for EventUtils#synthesizeComposition.
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
* Browsing context or browser element, must not be null
*
* @returns {Promise}
* @resolves False if the composition event could not be synthesized.
*/
synthesizeComposition(event, browsingContext) {
browsingContext = this.getBrowsingContextFrom(browsingContext);
return this.sendQuery(browsingContext, "Test:SynthesizeComposition", {
event,
});
},
/**
* Version of EventUtils' `synthesizeCompositionChange` function; it will
* synthesize a compositionchange event in a child process and returns a
* Promise that will resolve when the event was fired. Instead of a Window, a
* Browser or Browsing Context object is required to be passed to this function.
*
* @param {Object} event
* See the documentation available for EventUtils#synthesizeCompositionChange.
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
* Browsing context or browser element, must not be null
*
* @returns {Promise}
*/
synthesizeCompositionChange(event, browsingContext) {
browsingContext = this.getBrowsingContextFrom(browsingContext);
return this.sendQuery(browsingContext, "Test:SynthesizeCompositionChange", {
event,
});
},
// TODO: Fix consumers and remove me.
waitForCondition: TestUtils.waitForCondition,
/**
* Waits for a <xul:notification> with a particular value to appear
* for the <xul:notificationbox> of the passed in browser.
*
* @param {xul:tabbrowser} tabbrowser
* The gBrowser that hosts the browser that should show
* the notification. For most tests, this will probably be
* gBrowser.
* @param {xul:browser} browser
* The browser that should be showing the notification.
* @param {String} notificationValue
* The "value" of the notification, which is often used as
* a unique identifier. Example: "plugin-crashed".
*
* @return {Promise}
* Resolves to the <xul:notification> that is being shown.
*/
waitForNotificationBar(tabbrowser, browser, notificationValue) {
let notificationBox = tabbrowser.getNotificationBox(browser);
return this.waitForNotificationInNotificationBox(
notificationBox,
notificationValue
);
},
/**
* Waits for a <xul:notification> with a particular value to appear
* in the global <xul:notificationbox> of the given browser window.
*
* @param {Window} win
* The browser window in whose global notificationbox the
* notification is expected to appear.
* @param {String} notificationValue
* The "value" of the notification, which is often used as
* a unique identifier. Example: "captive-portal-detected".
*
* @return {Promise}
* Resolves to the <xul:notification> that is being shown.
*/
waitForGlobalNotificationBar(win, notificationValue) {
return this.waitForNotificationInNotificationBox(
win.gNotificationBox,
notificationValue
);
},
waitForNotificationInNotificationBox(notificationBox, notificationValue) {
return new Promise(resolve => {
let check = event => {
return event.target.getAttribute("value") == notificationValue;
};
BrowserTestUtils.waitForEvent(
notificationBox.stack,
"AlertActive",
false,
check
).then(event => {
// The originalTarget of the AlertActive on a notificationbox
// will be the notification itself.
resolve(event.originalTarget);
});
});
},
/**
* Waits for CSS transitions to complete for an element. Tracks any
* transitions that start after this function is called and resolves once all
* started transitions complete.
*
* @param {Element} element
* The element that will transition.
* @param {Number} timeout
* The maximum time to wait in milliseconds. Defaults to 5 seconds.
* @return {Promise}
* Resolves when transitions complete or rejects if the timeout is hit.
*/
waitForTransition(element, timeout = 5000) {
return new Promise((resolve, reject) => {
let cleanup = () => {
element.removeEventListener("transitionrun", listener);
element.removeEventListener("transitionend", listener);
};
let timer = element.ownerGlobal.setTimeout(() => {
cleanup();
reject();
}, timeout);
let transitionCount = 0;
let listener = event => {
if (event.type == "transitionrun") {
transitionCount++;
} else {
transitionCount--;
if (transitionCount == 0) {
cleanup();
element.ownerGlobal.clearTimeout(timer);
resolve();
}
}
};
element.addEventListener("transitionrun", listener);
element.addEventListener("transitionend", listener);
element.addEventListener("transitioncancel", listener);
});
},
_knownAboutPages: new Set(),
_loadedAboutContentScript: false,
/**
* Registers an about: page with particular flags in both the parent
* and any content processes. Returns a promise that resolves when
* registration is complete.
*
* @param {Function} registerCleanupFunction
* The test framework doesn't keep its cleanup stuff anywhere accessible,
* so the first argument is a reference to your cleanup registration
* function, allowing us to clean up after you if necessary.
* @param {String} aboutModule
* The name of the about page.
* @param {String} pageURI
* The URI the about: page should point to.
* @param {Number} flags
* The nsIAboutModule flags to use for registration.
*
* @returns {Promise}
* Promise that resolves when registration has finished.
*/
registerAboutPage(registerCleanupFunction, aboutModule, pageURI, flags) {
// Return a promise that resolves when registration finished.
const kRegistrationMsgId =
"browser-test-utils:about-registration:registered";
let rv = this.waitForMessage(Services.ppmm, kRegistrationMsgId, msg => {
return msg.data == aboutModule;
});
// Load a script that registers our page, then send it a message to execute the registration.
if (!this._loadedAboutContentScript) {
Services.ppmm.loadProcessScript(
kAboutPageRegistrationContentScript,
true
);
this._loadedAboutContentScript = true;
registerCleanupFunction(this._removeAboutPageRegistrations.bind(this));
}
Services.ppmm.broadcastAsyncMessage(
"browser-test-utils:about-registration:register",
{ aboutModule, pageURI, flags }
);
return rv.then(() => {
this._knownAboutPages.add(aboutModule);
});
},
unregisterAboutPage(aboutModule) {
if (!this._knownAboutPages.has(aboutModule)) {
return Promise.reject(
new Error("We don't think this about page exists!")
);
}
const kUnregistrationMsgId =
"browser-test-utils:about-registration:unregistered";
let rv = this.waitForMessage(Services.ppmm, kUnregistrationMsgId, msg => {
return msg.data == aboutModule;
});
Services.ppmm.broadcastAsyncMessage(
"browser-test-utils:about-registration:unregister",
aboutModule
);
return rv.then(() => this._knownAboutPages.delete(aboutModule));
},
async _removeAboutPageRegistrations() {
for (let aboutModule of this._knownAboutPages) {
await this.unregisterAboutPage(aboutModule);
}
Services.ppmm.removeDelayedProcessScript(
kAboutPageRegistrationContentScript
);
},
/**
* Waits for the dialog to open, and clicks the specified button.
*
* @param {string} buttonNameOrElementID
* The name of the button ("accept", "cancel", etc) or element ID to
* click.
* @param {string} uri
* The URI of the dialog to wait for. Defaults to the common dialog.
* @return {Promise}
* A Promise which resolves when a "domwindowopened" notification
* for a dialog has been fired by the window watcher and the
* specified button is clicked.
*/
async promiseAlertDialogOpen(
buttonNameOrElementID,
options = { callback: null, isSubDialog: false }
) {
let win;
[win] = await TestUtils.topicObserved("common-dialog-loaded");
} else if (options.isSubDialog) {
for (let attempts = 0; attempts < 3; attempts++) {
[win] = await TestUtils.topicObserved("subdialog-loaded");
if (uri === undefined || uri === null || uri === "") {
break;
}
if (win.document.documentURI === uri) {
break;
}
}
} else {
// The test listens for the "load" event which guarantees that the alert
// class has already been added (it is added when "DOMContentLoaded" is
// fired).
win = await this.domWindowOpenedAndLoaded(null, win => {
return win.document.documentURI === uri;
});
}
if (options.callback) {
await options.callback(win);
return win;
}
if (buttonNameOrElementID) {
let dialog = win.document.querySelector("dialog");
let element =
dialog.getButton(buttonNameOrElementID) ||
win.document.getElementById(buttonNameOrElementID);
element.click();
}
return win;
},
/**
* Wait for the containing dialog with the id `window-modal-dialog` to become
* empty and close.
*
* @param {HTMLDialogElement} dialog
* The dialog to wait on.
* @return {Promise}
* Resolves once the the dialog has closed
*/
async waitForDialogClose(dialog) {
return this.waitForEvent(dialog, "close").then(() => {
return this.waitForMutationCondition(
dialog,
{ childList: true, attributes: true },
() => !dialog.hasChildNodes() && !dialog.open
);
});
},
/**
* Waits for the dialog to open, and clicks the specified button, and waits
* for the dialog to close.
*
* @param {string} buttonNameOrElementID
* The name of the button ("accept", "cancel", etc) or element ID to
* click.
* @param {string} uri
* The URI of the dialog to wait for. Defaults to the common dialog.
*
* @return {Promise}
* A Promise which resolves when a "domwindowopened" notification
* for a dialog has been fired by the window watcher and the
* specified button is clicked, and the dialog has been fully closed.
*/
async promiseAlertDialog(
buttonNameOrElementID,
options = { callback: null, isSubDialog: false }
) {
let win = await this.promiseAlertDialogOpen(
buttonNameOrElementID,
uri,
options
);
if (!win.docShell.browsingContext.embedderElement) {
return this.windowClosed(win);
}
const dialog = win.top.document.getElementById("window-modal-dialog");
return this.waitForDialogClose(dialog);
},
/**
* Opens a tab with a given uri and params object. If the params object is not set
* or the params parameter does not include a triggeringPrincipal then this function
* provides a params object using the systemPrincipal as the default triggeringPrincipal.
*
* @param {xul:tabbrowser} tabbrowser
* The gBrowser object to open the tab with.
* @param {string} uri
* The URI to open in the new tab.
* @param {object} params [optional]
* Parameters object for gBrowser.addTab.
* @param {function} beforeLoadFunc [optional]
* A function to run after that xul:browser has been created but before the URL is
* loaded. Can spawn a content task in the tab, for example.
*/
addTab(tabbrowser, uri, params = {}, beforeLoadFunc = null) {
if (!params.triggeringPrincipal) {
params.triggeringPrincipal =
Services.scriptSecurityManager.getSystemPrincipal();
}
if (!params.allowInheritPrincipal) {
params.allowInheritPrincipal = true;
}
if (beforeLoadFunc) {
let window = tabbrowser.ownerGlobal;
window.addEventListener(
"TabOpen",
function (e) {
beforeLoadFunc(e.target);
},
{ once: true }
);
}
return tabbrowser.addTab(uri, params);
},
/**
* There are two ways to listen for observers in a content process:
* 1. Call contentTopicObserved which will watch for an observer notification
* in a content process to occur, and will return a promise which resolves
* when that notification occurs.
* 2. Enclose calls to contentTopicObserved inside a pair of calls to
* startObservingTopics and stopObservingTopics. Usually this pair will be
* placed at the start and end of a test or set of tests. Any observer
* notification that happens between the start and stop that doesn't match
* any explicitly expected by using contentTopicObserved will cause
* stopObservingTopics to reject with an error.
* For example:
*
* await BrowserTestUtils.startObservingTopics(bc, ["a", "b", "c"]);
* await BrowserTestUtils contentTopicObserved(bc, "a", 2);
* await BrowserTestUtils.stopObservingTopics(bc, ["a", "b", "c"]);
*
* This will expect two "a" notifications to occur, but will fail if more
* than two occur, or if any "b" or "c" notifications occur.
*
* Note that this function doesn't handle adding a listener for the same topic
* more than once. To do that, use the aCount argument.
*
* @param aBrowsingContext
* The browsing context associated with the content process to listen to.
* @param {string} aTopic
* Observer topic to listen to. May be null to listen to any topic.
* @param {number} aCount
* Number of such matching topics to listen to, defaults to 1. A match
* occurs when the topic and filter function match.
* @param {function} aFilterFn
* Function to be evaluated in the content process which should
* return true if the notification matches. This function is passed
* the same arguments as nsIObserver.observe(). May be null to
* always match.
* @returns {Promise} resolves when the notification occurs.
*/
contentTopicObserved(aBrowsingContext, aTopic, aCount = 1, aFilterFn = null) {
return this.sendQuery(aBrowsingContext, "BrowserTestUtils:ObserveTopic", {
topic: aTopic,
count: aCount,
filterFunctionSource: aFilterFn ? aFilterFn.toSource() : null,
});
},
/**
* Starts observing a list of topics in a content process. Use contentTopicObserved
* to allow an observer notification. Any other observer notification that occurs that
* matches one of the specified topics will cause the promise to reject.
*
* Calling this function more than once adds additional topics to be observed without
* replacing the existing ones.
*
* @param {BrowsingContext} aBrowsingContext
* The browsing context associated with the content process to listen to.
* @param {String[]} aTopics array of observer topics
* @returns {Promise} resolves when the listeners have been added.
*/
startObservingTopics(aBrowsingContext, aTopics) {
return this.sendQuery(
aBrowsingContext,
"BrowserTestUtils:StartObservingTopics",
{
topics: aTopics,
}
);
},
/**
* Stop listening to a set of observer topics.
*
* @param {BrowsingContext} aBrowsingContext
* The browsing context associated with the content process to listen to.
* @param {String[]} aTopics array of observer topics. If empty, then all
* current topics being listened to are removed.
* @returns {Promise} promise that fails if an unexpected observer occurs.
*/
stopObservingTopics(aBrowsingContext, aTopics) {
return this.sendQuery(
aBrowsingContext,
"BrowserTestUtils:StopObservingTopics",
{
topics: aTopics,
}
);
},
/**
* Sends a message to a specific BrowserTestUtils window actor.
* @param {BrowsingContext} aBrowsingContext
* The browsing context where the actor lives.
* @param {string} aMessageName
* Name of the message to be sent to the actor.
* @param {object} aMessageData
* Extra information to pass to the actor.
*/
async sendAsyncMessage(aBrowsingContext, aMessageName, aMessageData) {
if (!aBrowsingContext.currentWindowGlobal) {
await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal);
}
let actor =
aBrowsingContext.currentWindowGlobal.getActor("BrowserTestUtils");
actor.sendAsyncMessage(aMessageName, aMessageData);
},
/**
* Sends a query to a specific BrowserTestUtils window actor.
* @param {BrowsingContext} aBrowsingContext
* The browsing context where the actor lives.
* @param {string} aMessageName
* Name of the message to be sent to the actor.
* @param {object} aMessageData
* Extra information to pass to the actor.
*/
async sendQuery(aBrowsingContext, aMessageName, aMessageData) {
let startTime = Cu.now();
if (!aBrowsingContext.currentWindowGlobal) {
await this.waitForCondition(() => aBrowsingContext.currentWindowGlobal);
}
let actor =
aBrowsingContext.currentWindowGlobal.getActor("BrowserTestUtils");
return actor.sendQuery(aMessageName, aMessageData).then(val => {
ChromeUtils.addProfilerMarker(
"BrowserTestUtils",
{ startTime, category: "Test" },
aMessageName
);
return val;
});
},
/**
* A helper function for this test that returns a Promise that resolves
* once the migration wizard appears.
*
* @param {DOMWindow} window
* The top-level window that the about:preferences tab is likely to open
* in if the new migration wizard is enabled.
* @returns {Promise<Element>}
* Resolves to the opened about:preferences tab with the migration wizard
* running and loaded in it.
*/
async waitForMigrationWizard(window) {
let wizardReady = this.waitForEvent(window, "MigrationWizard:Ready");
let wizardTab = await this.waitForNewTab(window.gBrowser, url => {
return url.startsWith("about:preferences");
});
await wizardReady;
return wizardTab;
},
};
XPCOMUtils.defineLazyPreferenceGetter(
BrowserTestUtils,
"_httpsFirstEnabled",
"dom.security.https_first",
false
);
Services.obs.addObserver(BrowserTestUtils, "test-complete");