зеркало из https://github.com/mozilla/gecko-dev.git
1800 строки
64 KiB
JavaScript
1800 строки
64 KiB
JavaScript
/* 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 addEventListener, removeEventListener, sendAsyncMessage,
|
|
addMessageListener, removeMessageListener, privateNoteIntentionalCrash */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"BrowserTestUtils",
|
|
];
|
|
|
|
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
ChromeUtils.import("resource://testing-common/TestUtils.jsm");
|
|
ChromeUtils.import("resource://testing-common/ContentTask.jsm");
|
|
|
|
Services
|
|
.mm
|
|
.loadFrameScript(
|
|
"chrome://mochikit/content/tests/BrowserTestUtils/content-utils.js", true);
|
|
|
|
ChromeUtils.defineModuleGetter(this, "E10SUtils",
|
|
"resource://gre/modules/E10SUtils.jsm");
|
|
|
|
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([Ci.nsIContentProcessProvider]),
|
|
|
|
provideProcess() {
|
|
return Ci.nsIContentProcessProvider.NEW_PROCESS;
|
|
}
|
|
};
|
|
|
|
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
|
let selectorFactory = XPCOMUtils._getFactory(NewProcessSelector);
|
|
registrar.registerFactory(OUR_PROCESSSELECTOR_CID, "", null, selectorFactory);
|
|
|
|
// For now, we'll allow tests to use CPOWs in this module for
|
|
// some cases.
|
|
Cu.permitCPOWsInScope(this);
|
|
|
|
var gSendCharCount = 0;
|
|
var gSynthesizeKeyCount = 0;
|
|
var gSynthesizeCompositionCount = 0;
|
|
var gSynthesizeCompositionChangeCount = 0;
|
|
|
|
const kAboutPageRegistrationContentScript =
|
|
"chrome://mochikit/content/tests/BrowserTestUtils/content-about-page-utils.js";
|
|
|
|
var BrowserTestUtils = {
|
|
/**
|
|
* Loads a page in a new tab, executes a Task and closes the tab.
|
|
*
|
|
* @param options
|
|
* An object or string.
|
|
* If this is a string it is the url to open and will be opened in the
|
|
* currently active browser window.
|
|
* If an object it should have the following properties:
|
|
* {
|
|
* gBrowser:
|
|
* Reference to the "tabbrowser" element where the new tab should
|
|
* be opened.
|
|
* url:
|
|
* String with the URL of the page to load.
|
|
* }
|
|
* @param taskFn
|
|
* Generator function representing a Task 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 {} 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 = await taskFn(tab.linkedBrowser);
|
|
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 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) {
|
|
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(() => tab);
|
|
},
|
|
|
|
/**
|
|
* Checks if a DOM element is hidden.
|
|
*
|
|
* @param {Element} element
|
|
* The element which is to be checked.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
is_hidden(element) {
|
|
var style = element.ownerGlobal.getComputedStyle(element);
|
|
if (style.display == "none")
|
|
return true;
|
|
if (style.visibility != "visible")
|
|
return true;
|
|
if (style.display == "-moz-popup")
|
|
return ["hiding", "closed"].includes(element.state);
|
|
|
|
// Hiding a parent element will hide all its children
|
|
if (element.parentNode != element.ownerDocument)
|
|
return BrowserTestUtils.is_hidden(element.parentNode);
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Checks if a DOM element is visible.
|
|
*
|
|
* @param {Element} element
|
|
* The element which is to be checked.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
is_visible(element) {
|
|
var style = element.ownerGlobal.getComputedStyle(element);
|
|
if (style.display == "none")
|
|
return false;
|
|
if (style.visibility != "visible")
|
|
return false;
|
|
if (style.display == "-moz-popup" && element.state != "open")
|
|
return false;
|
|
|
|
// Hiding a parent element will hide all its children
|
|
if (element.parentNode != element.ownerDocument)
|
|
return BrowserTestUtils.is_visible(element.parentNode);
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* 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 promise = new Promise(resolve => {
|
|
tabbrowser.addEventListener("TabSwitchDone", function() {
|
|
TestUtils.executeSoon(() => 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. This is guaranteed to work because the
|
|
* way we're listening for the load is in the content-utils.js frame script,
|
|
* and then sending an async message up, so we can't miss the message.
|
|
*
|
|
* @param {xul:browser} browser
|
|
* A xul:browser.
|
|
* @param {Boolean} includeSubFrames
|
|
* A boolean indicating if loads from subframes should be included.
|
|
* @param {optional string or function} wantLoad
|
|
* 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 {optional boolean} maybeErrorPage
|
|
* 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) {
|
|
// Passing a url as second argument is a common mistake we should prevent.
|
|
if (includeSubFrames && typeof includeSubFrames != "boolean") {
|
|
throw ("The second argument to browserLoaded should be a boolean.");
|
|
}
|
|
|
|
// If browser belongs to tabbrowser-tab, ensure it has been
|
|
// inserted into the document.
|
|
let tabbrowser = browser.ownerGlobal.gBrowser;
|
|
if (tabbrowser && tabbrowser.getTabForBrowser) {
|
|
tabbrowser._insertBrowser(tabbrowser.getTabForBrowser(browser));
|
|
}
|
|
|
|
function isWanted(url) {
|
|
if (!wantLoad) {
|
|
return true;
|
|
} else if (typeof(wantLoad) == "function") {
|
|
return wantLoad(url);
|
|
}
|
|
// It's a string.
|
|
return wantLoad == url;
|
|
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
let mm = browser.ownerGlobal.messageManager;
|
|
let eventName = maybeErrorPage
|
|
? "browser-test-utils:DOMContentLoadedEvent"
|
|
: "browser-test-utils:loadEvent";
|
|
mm.addMessageListener(eventName, function onLoad(msg) {
|
|
// See testing/mochitest/BrowserTestUtils/content/content-utils.js for
|
|
// the difference between visibleURL and internalURL.
|
|
if (msg.target == browser && (!msg.data.subframe || includeSubFrames) &&
|
|
isWanted(maybeErrorPage
|
|
? msg.data.visibleURL : msg.data.internalURL)) {
|
|
mm.removeMessageListener(eventName, onLoad);
|
|
resolve(msg.data.internalURL);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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 or 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) {
|
|
let mm = win.messageManager;
|
|
return this.waitForMessage(mm, "browser-test-utils:loadEvent", (msg) => {
|
|
if (checkFn) {
|
|
return checkFn(msg.target);
|
|
}
|
|
|
|
let selectedBrowser = win.gBrowser.selectedBrowser;
|
|
return msg.target == selectedBrowser &&
|
|
(aboutBlank || selectedBrowser.currentURI.spec != "about:blank");
|
|
});
|
|
},
|
|
|
|
_webProgressListeners: new Set(),
|
|
|
|
/**
|
|
* 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) {
|
|
return new Promise(resolve => {
|
|
let wpl = {
|
|
onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
|
|
dump("Saw state " + aStateFlags.toString(16) + " and status " + aStatus.toString(16) + "\n");
|
|
if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
|
|
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
|
|
(checkAborts || aStatus != Cr.NS_BINDING_ABORTED) &&
|
|
aWebProgress.isTopLevel) {
|
|
let chan = aRequest.QueryInterface(Ci.nsIChannel);
|
|
dump("Browser loaded " + chan.originalURI.spec + "\n");
|
|
if (!expectedURI || chan.originalURI.spec == expectedURI) {
|
|
browser.removeProgressListener(wpl);
|
|
BrowserTestUtils._webProgressListeners.delete(wpl);
|
|
resolve();
|
|
}
|
|
}
|
|
},
|
|
onSecurityChange() {},
|
|
onStatusChange() {},
|
|
onLocationChange() {},
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
Ci.nsIWebProgressListener,
|
|
Ci.nsIWebProgressListener2,
|
|
Ci.nsISupportsWeakReference,
|
|
]),
|
|
};
|
|
browser.addProgressListener(wpl);
|
|
this._webProgressListeners.add(wpl);
|
|
dump("Waiting for browser load" + (expectedURI ? (" of " + expectedURI) : "") + "\n");
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Waits for a tab to open and load a given URL.
|
|
*
|
|
* 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} url
|
|
* A string URL to look for in the new tab. If null, allows any non-blank URL.
|
|
* @param {boolean} waitForLoad
|
|
* True to wait for the page in the new tab to load. Defaults to false.
|
|
* @param {boolean} waitForAnyTab
|
|
* True to wait for the url to be loaded in any new tab, not just the next
|
|
* one opened.
|
|
*
|
|
* @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. BrowserOpenTab
|
|
* and the tab does not load a URL, because no onLocationChange will fire.
|
|
*/
|
|
waitForNewTab(tabbrowser, url, waitForLoad = false, waitForAnyTab = false) {
|
|
let urlMatches = url ? (urlToMatch) => urlToMatch == url
|
|
: (urlToMatch) => urlToMatch != "about:blank";
|
|
return new Promise((resolve, reject) => {
|
|
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)
|
|
.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, reject) => {
|
|
let progressListener = {
|
|
onLocationChange(aBrowser) {
|
|
if ((url && aBrowser.currentURI.spec != url) ||
|
|
(!url && aBrowser.currentURI.spec == "about:blank")) {
|
|
return;
|
|
}
|
|
|
|
tabbrowser.removeTabsProgressListener(progressListener);
|
|
resolve();
|
|
},
|
|
};
|
|
tabbrowser.addTabsProgressListener(progressListener);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Waits for the next browser window to open and be fully loaded.
|
|
*
|
|
* @param aParams
|
|
* {
|
|
* url: A string (optional). If set, we will wait until the initial
|
|
* browser in the new window has loaded a particular page.
|
|
* If unset, the initial browser may or may not have finished
|
|
* loading its first page when the resulting Promise resolves.
|
|
* anyWindow: True to wait for the url to be loaded in any new
|
|
* window, not just the next one opened.
|
|
* 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,
|
|
} = aParams;
|
|
|
|
if (anyWindow && !url) {
|
|
throw new Error("url should be specified if anyWindow is true");
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
let observe = async (win, topic, data) => {
|
|
if (topic != "domwindowopened") {
|
|
return;
|
|
}
|
|
|
|
if (!anyWindow) {
|
|
Services.ww.unregisterNotification(observe);
|
|
}
|
|
|
|
if (url) {
|
|
await this.waitForEvent(win, "DOMContentLoaded");
|
|
|
|
if (win.document.documentURI != AppConstants.BROWSER_CHROME_URL) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let promises = [
|
|
TestUtils.topicObserved("browser-delayed-startup-finished",
|
|
subject => subject == win),
|
|
];
|
|
|
|
if (url) {
|
|
let browser = win.gBrowser.selectedBrowser;
|
|
|
|
// Retrieve the given browser's current process type.
|
|
let process =
|
|
browser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
|
|
: Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
|
if (win.gMultiProcessBrowser &&
|
|
!E10SUtils.canLoadURIInProcess(url, process)) {
|
|
await this.waitForEvent(browser, "XULFrameLoaderCreated");
|
|
}
|
|
|
|
let loadPromise = this.browserLoaded(browser, false, url, maybeErrorPage);
|
|
promises.push(loadPromise);
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
if (anyWindow) {
|
|
Services.ww.unregisterNotification(observe);
|
|
}
|
|
resolve(win);
|
|
};
|
|
Services.ww.registerNotification(observe);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Loads a new URI in the given browser and waits until we really started
|
|
* loading. In e10s browser.loadURI() can be an asynchronous operation due
|
|
* to having to switch the browser's remoteness and keep its shistory data.
|
|
*
|
|
* @param {xul:browser} browser
|
|
* A xul:browser.
|
|
* @param {string} uri
|
|
* The URI to load.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When we started loading the given URI.
|
|
*/
|
|
async loadURI(browser, uri) {
|
|
// Load the new URI.
|
|
browser.loadURI(uri);
|
|
|
|
// Nothing to do in non-e10s mode.
|
|
if (!browser.ownerGlobal.gMultiProcessBrowser) {
|
|
return;
|
|
}
|
|
|
|
// Retrieve the given browser's current process type.
|
|
let process = browser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
|
|
: Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
|
|
|
// If the new URI can't load in the browser's current process then we
|
|
// should wait for the new frameLoader to be created. This will happen
|
|
// asynchronously when the browser's remoteness changes.
|
|
if (!E10SUtils.canLoadURIInProcess(uri, process)) {
|
|
await this.waitForEvent(browser, "XULFrameLoaderCreated");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @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, data) {
|
|
if (topic == "domwindowopened" && (!win || subject === win)) {
|
|
let observedWindow = subject.QueryInterface(Ci.nsIDOMWindow);
|
|
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 "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, data) {
|
|
if (topic == "domwindowclosed" && (!win || subject === win)) {
|
|
Services.ww.unregisterNotification(observer);
|
|
resolve(subject.QueryInterface(Ci.nsIDOMWindow));
|
|
}
|
|
}
|
|
Services.ww.registerNotification(observer);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @param {Object} options
|
|
* {
|
|
* private: A boolean indicating if the window should be
|
|
* private
|
|
* remote: A boolean indicating if the window should run
|
|
* remote browser tabs or not. If omitted, the window
|
|
* will choose the profile default state.
|
|
* width: Desired width of window
|
|
* height: Desired height of window
|
|
* }
|
|
* @return {Promise}
|
|
* Resolves with the new window once it is loaded.
|
|
*/
|
|
async openNewBrowserWindow(options = {}) {
|
|
let argString = Cc["@mozilla.org/supports-string;1"].
|
|
createInstance(Ci.nsISupportsString);
|
|
argString.data = "";
|
|
let features = "chrome,dialog=no,all";
|
|
let opener = null;
|
|
|
|
if (options.opener) {
|
|
opener = options.opener;
|
|
}
|
|
|
|
if (options.private) {
|
|
features += ",private";
|
|
}
|
|
|
|
if (options.width) {
|
|
features += ",width=" + options.width;
|
|
}
|
|
if (options.height) {
|
|
features += ",height=" + options.height;
|
|
}
|
|
|
|
if (options.left) {
|
|
features += ",left=" + options.left;
|
|
}
|
|
|
|
if (options.top) {
|
|
features += ",top=" + options.top;
|
|
}
|
|
|
|
if (options.hasOwnProperty("remote")) {
|
|
let remoteState = options.remote ? "remote" : "non-remote";
|
|
features += `,${remoteState}`;
|
|
}
|
|
|
|
if (options.url) {
|
|
argString.data = options.url;
|
|
}
|
|
|
|
let win = Services.ww.openWindow(
|
|
opener, AppConstants.BROWSER_CHROME_URL, "_blank",
|
|
features, argString);
|
|
|
|
// Wait for browser-delayed-startup-finished notification, it indicates
|
|
// that the window has loaded completely and is ready to be used for
|
|
// testing.
|
|
let startupPromise =
|
|
TestUtils.topicObserved("browser-delayed-startup-finished",
|
|
subject => subject == win).then(() => win);
|
|
|
|
let loadPromise = this.firstBrowserLoaded(win);
|
|
|
|
await startupPromise;
|
|
await loadPromise;
|
|
|
|
return win;
|
|
},
|
|
|
|
/**
|
|
* Closes a window.
|
|
*
|
|
* @param {Window}
|
|
* 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}
|
|
* 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");
|
|
|
|
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 mm = win.getGroupMessageManager("browsers");
|
|
|
|
mm.addMessageListener("SessionStore:update", function onMessage(msg) {
|
|
if (browserSet.has(msg.target) && msg.data.isFinal) {
|
|
browserSet.delete(msg.target);
|
|
if (!browserSet.size) {
|
|
mm.removeMessageListener("SessionStore:update", onMessage);
|
|
// 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);
|
|
}
|
|
}
|
|
}, true);
|
|
});
|
|
|
|
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 (tab) tab
|
|
* The tab that will be removed.
|
|
* @returns (Promise)
|
|
* @resolves When the SessionStore information is updated.
|
|
*/
|
|
waitForSessionStoreUpdate(tab) {
|
|
return new Promise(resolve => {
|
|
let {messageManager: mm, frameLoader} = tab.linkedBrowser;
|
|
mm.addMessageListener("SessionStore:update", function onMessage(msg) {
|
|
if (msg.targetFrameLoader == frameLoader && msg.data.isFinal) {
|
|
mm.removeMessageListener("SessionStore:update", onMessage);
|
|
// Wait for the next event tick to make sure other listeners are
|
|
// called.
|
|
TestUtils.executeSoon(() => resolve());
|
|
}
|
|
}, true);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Waits for an event to be fired on a specified element.
|
|
*
|
|
* Usage:
|
|
* 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;
|
|
*
|
|
* 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.
|
|
*
|
|
* 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 [optional]
|
|
* True 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} wantsUntrusted [optional]
|
|
* 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) {
|
|
return new Promise((resolve, reject) => {
|
|
subject.addEventListener(eventName, function listener(event) {
|
|
try {
|
|
if (checkFn && !checkFn(event)) {
|
|
return;
|
|
}
|
|
subject.removeEventListener(eventName, listener, capture);
|
|
TestUtils.executeSoon(() => resolve(event));
|
|
} catch (ex) {
|
|
try {
|
|
subject.removeEventListener(eventName, listener, capture);
|
|
} catch (ex2) {
|
|
// Maybe the provided object does not support removeEventListener.
|
|
}
|
|
TestUtils.executeSoon(() => reject(ex));
|
|
}
|
|
}, capture, wantsUntrusted);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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} wantsUntrusted [optional]
|
|
* Whether to accept untrusted events
|
|
*
|
|
* @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}
|
|
*/
|
|
waitForContentEvent(browser, eventName, capture = false, checkFn, wantsUntrusted = false) {
|
|
let parameters = {
|
|
eventName,
|
|
capture,
|
|
checkFnSource: checkFn ? checkFn.toSource() : null,
|
|
wantsUntrusted,
|
|
};
|
|
/* eslint-disable no-eval */
|
|
return ContentTask.spawn(browser, parameters,
|
|
function({ eventName, capture, checkFnSource, wantsUntrusted }) {
|
|
let checkFn;
|
|
if (checkFnSource) {
|
|
checkFn = eval(`(() => (${checkFnSource}))()`);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
addEventListener(eventName, function listener(event) {
|
|
let completion = resolve;
|
|
try {
|
|
if (checkFn && !checkFn(event)) {
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
completion = () => reject(e);
|
|
}
|
|
removeEventListener(eventName, listener, capture);
|
|
completion();
|
|
}, capture, wantsUntrusted);
|
|
});
|
|
});
|
|
/* eslint-enable no-eval */
|
|
},
|
|
|
|
/**
|
|
* 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 {bool} useCapture [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. This is called
|
|
* within the content process and can have no closure environment.
|
|
* @param {bool} wantsUntrusted [optional]
|
|
* Whether to accept untrusted events
|
|
* @param {bool} autoremove [optional]
|
|
* Whether the listener should be removed when |browser| is removed
|
|
* from the DOM. Note that, if this flag is true, it won't be possible
|
|
* to listen for events after a frameloader swap.
|
|
*
|
|
* @returns function
|
|
* If called, the return value will remove the event listener.
|
|
*/
|
|
addContentEventListener(browser,
|
|
eventName,
|
|
listener,
|
|
useCapture = false,
|
|
checkFn,
|
|
wantsUntrusted = false,
|
|
autoremove = true) {
|
|
let id = gListenerId++;
|
|
let checkFnSource = checkFn ? encodeURIComponent(escape(checkFn.toSource())) : "";
|
|
|
|
// To correctly handle frameloader swaps, we load a frame script
|
|
// into all tabs but ignore messages from the ones not related to
|
|
// |browser|.
|
|
|
|
/* eslint-disable no-eval */
|
|
function frameScript(id, eventName, useCapture, checkFnSource, wantsUntrusted) {
|
|
let checkFn;
|
|
if (checkFnSource) {
|
|
checkFn = eval(`(() => (${unescape(checkFnSource)}))()`);
|
|
}
|
|
|
|
function listener(event) {
|
|
if (checkFn && !checkFn(event)) {
|
|
return;
|
|
}
|
|
sendAsyncMessage("ContentEventListener:Run", id);
|
|
}
|
|
function removeListener(msg) {
|
|
if (msg.data == id) {
|
|
removeMessageListener("ContentEventListener:Remove", removeListener);
|
|
removeEventListener(eventName, listener, useCapture, wantsUntrusted);
|
|
}
|
|
}
|
|
addMessageListener("ContentEventListener:Remove", removeListener);
|
|
addEventListener(eventName, listener, useCapture, wantsUntrusted);
|
|
}
|
|
/* eslint-enable no-eval */
|
|
|
|
let frameScriptSource =
|
|
`data:,(${frameScript.toString()})(${id}, "${eventName}", ${uneval(useCapture)}, "${checkFnSource}", ${wantsUntrusted})`;
|
|
|
|
let mm = Services.mm;
|
|
|
|
function runListener(msg) {
|
|
if (msg.data == id && msg.target == browser) {
|
|
listener();
|
|
}
|
|
}
|
|
mm.addMessageListener("ContentEventListener:Run", runListener);
|
|
|
|
let needCleanup = true;
|
|
|
|
let unregisterFunction = function() {
|
|
if (!needCleanup) {
|
|
return;
|
|
}
|
|
needCleanup = false;
|
|
mm.removeMessageListener("ContentEventListener:Run", runListener);
|
|
mm.broadcastAsyncMessage("ContentEventListener:Remove", id);
|
|
mm.removeDelayedFrameScript(frameScriptSource);
|
|
if (autoremove) {
|
|
Services.obs.removeObserver(cleanupObserver, "message-manager-close");
|
|
}
|
|
};
|
|
|
|
function cleanupObserver(subject, topic, data) {
|
|
if (subject == browser.messageManager) {
|
|
unregisterFunction();
|
|
}
|
|
}
|
|
if (autoremove) {
|
|
Services.obs.addObserver(cleanupObserver, "message-manager-close");
|
|
}
|
|
|
|
mm.loadFrameScript(frameScriptSource, true);
|
|
|
|
return unregisterFunction;
|
|
},
|
|
|
|
/**
|
|
* Like browserLoaded, but waits for an error page to appear.
|
|
* This explicitly deals with cases where the browser is not currently remote and a
|
|
* remoteness switch will occur before the error page is loaded, which is tricky
|
|
* because error pages don't fire 'regular' load events that we can rely on.
|
|
*
|
|
* @param {xul:browser} browser
|
|
* A xul:browser.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When an error page has been loaded in the browser.
|
|
*/
|
|
waitForErrorPage(browser) {
|
|
let waitForLoad = () =>
|
|
this.waitForContentEvent(browser, "AboutNetErrorLoad", false, null, true);
|
|
|
|
let win = browser.ownerGlobal;
|
|
let tab = win.gBrowser.getTabForBrowser(browser);
|
|
if (!tab || browser.isRemoteBrowser || !win.gMultiProcessBrowser) {
|
|
return waitForLoad();
|
|
}
|
|
|
|
// We're going to switch remoteness when loading an error page. We need to be
|
|
// quite careful in order to make sure we're adding the listener in time to
|
|
// get this event:
|
|
return new Promise((resolve, reject) => {
|
|
tab.addEventListener("TabRemotenessChange", function() {
|
|
waitForLoad().then(resolve, reject);
|
|
}, {once: true});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Versions of EventUtils.jsm 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 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.
|
|
* - An array of selector strings. Each selector after the first
|
|
* selects for an element in the iframe specified by the previous
|
|
* selector.
|
|
* - a CPOW element (for easier test-conversion).
|
|
* - 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.jsm version
|
|
* @param {Browser} browser
|
|
* Browser element, must not be null
|
|
*
|
|
* @returns {Promise}
|
|
* @resolves True if the mouse event was cancelled.
|
|
*/
|
|
synthesizeMouse(target, offsetX, offsetY, event, browser) {
|
|
return new Promise((resolve, reject) => {
|
|
let mm = browser.messageManager;
|
|
mm.addMessageListener("Test:SynthesizeMouseDone", function mouseMsg(message) {
|
|
mm.removeMessageListener("Test:SynthesizeMouseDone", mouseMsg);
|
|
if (message.data.hasOwnProperty("defaultPrevented")) {
|
|
resolve(message.data.defaultPrevented);
|
|
} else {
|
|
reject(new Error(message.data.error));
|
|
}
|
|
});
|
|
|
|
let cpowObject = null;
|
|
let targetFn = null;
|
|
if (typeof target == "function") {
|
|
targetFn = target.toString();
|
|
target = null;
|
|
} else if (typeof target != "string" && !Array.isArray(target)) {
|
|
cpowObject = target;
|
|
target = null;
|
|
}
|
|
|
|
mm.sendAsyncMessage("Test:SynthesizeMouse",
|
|
{target, targetFn, x: offsetX, y: offsetY, event},
|
|
{object: cpowObject});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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, browser) {
|
|
// Use a flag to indicate to center rather than having a separate message.
|
|
event.centered = true;
|
|
return BrowserTestUtils.synthesizeMouse(target, 0, 0, event, browser);
|
|
},
|
|
|
|
/**
|
|
* 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, browser) {
|
|
return BrowserTestUtils.synthesizeMouse(null, offsetX, offsetY, event, browser);
|
|
},
|
|
|
|
/**
|
|
* 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");
|
|
},
|
|
|
|
/**
|
|
* Crashes a remote browser 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.
|
|
*
|
|
* @returns (Promise)
|
|
* @resolves An Object with key-value pairs representing the data from the
|
|
* crash report's extra file (if applicable).
|
|
*/
|
|
async crashBrowser(browser, shouldShowTabCrashPage = true,
|
|
shouldClearMinidumps = true) {
|
|
let extra = {};
|
|
let KeyValueParser = {};
|
|
if (AppConstants.MOZ_CRASHREPORTER) {
|
|
ChromeUtils.import("resource://gre/modules/KeyValueParser.jsm", KeyValueParser);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// This frame script is injected into the remote browser, and used to
|
|
// intentionally crash the tab. We crash by using js-ctypes and dereferencing
|
|
// a bad pointer. The crash should happen immediately upon loading this
|
|
// frame script.
|
|
let frame_script = () => {
|
|
ChromeUtils.import("resource://gre/modules/ctypes.jsm");
|
|
|
|
let dies = function() {
|
|
privateNoteIntentionalCrash();
|
|
let zero = new ctypes.intptr_t(8);
|
|
let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
|
|
badptr.contents;
|
|
};
|
|
|
|
dump("\nEt tu, Brute?\n");
|
|
dies();
|
|
};
|
|
|
|
let expectedPromises = [];
|
|
|
|
let crashCleanupPromise = new Promise((resolve, reject) => {
|
|
let observer = (subject, topic, data) => {
|
|
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;
|
|
}
|
|
|
|
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(() => {
|
|
let minidumpDirectory = getMinidumpDirectory();
|
|
let extrafile = minidumpDirectory.clone();
|
|
extrafile.append(dumpID + ".extra");
|
|
if (extrafile.exists()) {
|
|
dump(`\nNo .extra file for dumpID: ${dumpID}\n`);
|
|
if (AppConstants.MOZ_CRASHREPORTER) {
|
|
extra = KeyValueParser.parseKeyValuePairsFromFile(extrafile);
|
|
} else {
|
|
dump("\nCrashReporter not enabled - will not return any extra data\n");
|
|
}
|
|
}
|
|
|
|
if (shouldClearMinidumps) {
|
|
removeFile(minidumpDirectory, dumpID + ".dmp");
|
|
removeFile(minidumpDirectory, dumpID + ".extra");
|
|
}
|
|
});
|
|
}
|
|
|
|
removalPromise.then(() => {
|
|
Services.obs.removeObserver(observer, "ipc:content-shutdown");
|
|
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, reject) => {
|
|
browser.addEventListener("AboutTabCrashedReady", function onCrash() {
|
|
browser.removeEventListener("AboutTabCrashedReady", onCrash);
|
|
dump("\nabout:tabcrashed loaded and ready\n");
|
|
resolve();
|
|
}, false, true);
|
|
}));
|
|
}
|
|
|
|
// This frame script will crash the remote browser as soon as it is
|
|
// evaluated.
|
|
let mm = browser.messageManager;
|
|
mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", 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;
|
|
},
|
|
|
|
/**
|
|
* 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(mutations => {
|
|
if ((!value && element.getAttribute(attr)) ||
|
|
(value && element.getAttribute(attr) === value)) {
|
|
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 object is required to be
|
|
* passed to this function.
|
|
*
|
|
* @param {String} char
|
|
* A character for the keypress event that is sent to the browser.
|
|
* @param {Browser} browser
|
|
* Browser element, must not be null.
|
|
*
|
|
* @returns {Promise}
|
|
* @resolves True if the keypress event was synthesized.
|
|
*/
|
|
sendChar(char, browser) {
|
|
return new Promise(resolve => {
|
|
let seq = ++gSendCharCount;
|
|
let mm = browser.messageManager;
|
|
|
|
mm.addMessageListener("Test:SendCharDone", function charMsg(message) {
|
|
if (message.data.seq != seq)
|
|
return;
|
|
|
|
mm.removeMessageListener("Test:SendCharDone", charMsg);
|
|
resolve(message.data.result);
|
|
});
|
|
|
|
mm.sendAsyncMessage("Test:SendChar", {
|
|
char,
|
|
seq
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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 object 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 {Browser} browser
|
|
* Browser element, must not be null.
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
synthesizeKey(key, event, browser) {
|
|
return new Promise(resolve => {
|
|
let seq = ++gSynthesizeKeyCount;
|
|
let mm = browser.messageManager;
|
|
|
|
mm.addMessageListener("Test:SynthesizeKeyDone", function keyMsg(message) {
|
|
if (message.data.seq != seq)
|
|
return;
|
|
|
|
mm.removeMessageListener("Test:SynthesizeKeyDone", keyMsg);
|
|
resolve();
|
|
});
|
|
|
|
mm.sendAsyncMessage("Test:SynthesizeKey", { key, event, seq });
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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 object is
|
|
* required to be passed to this function.
|
|
*
|
|
* @param {Object} event
|
|
* See the documentation available for EventUtils#synthesizeComposition.
|
|
* @param {Browser} browser
|
|
* Browser element, must not be null.
|
|
*
|
|
* @returns {Promise}
|
|
* @resolves False if the composition event could not be synthesized.
|
|
*/
|
|
synthesizeComposition(event, browser) {
|
|
return new Promise(resolve => {
|
|
let seq = ++gSynthesizeCompositionCount;
|
|
let mm = browser.messageManager;
|
|
|
|
mm.addMessageListener("Test:SynthesizeCompositionDone", function compMsg(message) {
|
|
if (message.data.seq != seq)
|
|
return;
|
|
|
|
mm.removeMessageListener("Test:SynthesizeCompositionDone", compMsg);
|
|
resolve(message.data.result);
|
|
});
|
|
|
|
mm.sendAsyncMessage("Test:SynthesizeComposition", { event, seq });
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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 object is required to be passed to this function.
|
|
*
|
|
* @param {Object} event
|
|
* See the documentation available for EventUtils#synthesizeCompositionChange.
|
|
* @param {Browser} browser
|
|
* Browser element, must not be null.
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
synthesizeCompositionChange(event, browser) {
|
|
return new Promise(resolve => {
|
|
let seq = ++gSynthesizeCompositionChangeCount;
|
|
let mm = browser.messageManager;
|
|
|
|
mm.addMessageListener("Test:SynthesizeCompositionChangeDone", function compMsg(message) {
|
|
if (message.data.seq != seq)
|
|
return;
|
|
|
|
mm.removeMessageListener("Test:SynthesizeCompositionChangeDone", compMsg);
|
|
resolve();
|
|
});
|
|
|
|
mm.sendAsyncMessage("Test:SynthesizeCompositionChange", { event, seq });
|
|
});
|
|
},
|
|
|
|
// 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 tabbrowser (<xul:tabbrowser>)
|
|
* The gBrowser that hosts the browser that should show
|
|
* the notification. For most tests, this will probably be
|
|
* gBrowser.
|
|
* @param browser (<xul:browser>)
|
|
* The browser that should be showing the notification.
|
|
* @param notificationValue (string)
|
|
* 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 win (<xul:window>)
|
|
* The browser window in whose global notificationbox the
|
|
* notification is expected to appear.
|
|
* @param notificationValue (string)
|
|
* 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) {
|
|
let notificationBox =
|
|
win.document.getElementById("high-priority-global-notificationbox");
|
|
return this.waitForNotificationInNotificationBox(notificationBox,
|
|
notificationValue);
|
|
},
|
|
|
|
waitForNotificationInNotificationBox(notificationBox, notificationValue) {
|
|
return new Promise((resolve) => {
|
|
let check = (event) => {
|
|
return event.target.value == notificationValue;
|
|
};
|
|
|
|
BrowserTestUtils.waitForEvent(notificationBox, "AlertActive",
|
|
false, check).then((event) => {
|
|
// The originalTarget of the AlertActive on a notificationbox
|
|
// will be the notification itself.
|
|
resolve(event.originalTarget);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns a Promise that will resolve once MozAfterPaint
|
|
* has been fired in the content of a browser.
|
|
*
|
|
* @param browser (<xul:browser>)
|
|
* The browser for which we're waiting for the MozAfterPaint
|
|
* event to occur in.
|
|
* @returns Promise
|
|
*/
|
|
contentPainted(browser) {
|
|
return ContentTask.spawn(browser, null, async function() {
|
|
return new Promise((resolve) => {
|
|
addEventListener("MozAfterPaint", function onPaint() {
|
|
removeEventListener("MozAfterPaint", onPaint);
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
_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 registerCleanupFunction (Function)
|
|
* 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 aboutModule (String)
|
|
* The name of the about page.
|
|
* @param pageURI (String)
|
|
* The URI the about: page should point to.
|
|
* @param flags (Number)
|
|
* The nsIAboutModule flags to use for registration.
|
|
* @returns 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} buttonAction
|
|
* The ID of the button to click ("accept", "cancel", etc).
|
|
* @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(buttonAction,
|
|
uri = "chrome://global/content/commonDialog.xul",
|
|
func) {
|
|
let win = await this.domWindowOpened(null, async win => {
|
|
// The test listens for the "load" event which guarantees that the alert
|
|
// class has already been added (it is added when "DOMContentLoaded" is
|
|
// fired).
|
|
await this.waitForEvent(win, "load");
|
|
|
|
return win.document.documentURI === uri;
|
|
});
|
|
|
|
if (func) {
|
|
await func(win);
|
|
return win;
|
|
}
|
|
|
|
let doc = win.document.documentElement;
|
|
doc.getButton(buttonAction).click();
|
|
|
|
return win;
|
|
},
|
|
|
|
/**
|
|
* Waits for the dialog to open, and clicks the specified button, and waits
|
|
* for the dialog to close.
|
|
*
|
|
* @param {string} buttonAction
|
|
* The ID of the button to click ("accept", "cancel", etc).
|
|
* @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(buttonAction,
|
|
uri = "chrome://global/content/commonDialog.xul",
|
|
func) {
|
|
let win = await this.promiseAlertDialogOpen(buttonAction, uri, func);
|
|
return this.windowClosed(win);
|
|
},
|
|
|
|
/**
|
|
* 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 triggeringPricnipal 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);
|
|
}
|
|
};
|