/* 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. */ "use strict"; this.EXPORTED_SYMBOLS = [ "BrowserTestUtils", ]; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Timer.jsm"); Cu.import("resource://testing-common/TestUtils.jsm"); Cc["@mozilla.org/globalmessagemanager;1"] .getService(Ci.nsIMessageListenerManager) .loadFrameScript( "chrome://mochikit/content/tests/BrowserTestUtils/content-utils.js", true); XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", "resource:///modules/E10SUtils.jsm"); this.BrowserTestUtils = { /** * Loads a page in a new tab, executes a Task and closes the tab. * * @param options * An object with 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 {Promise} * @resolves When the tab has been closed. * @rejects Any exception from taskFn is propagated. */ withNewTab: Task.async(function* (options, taskFn) { let tab = yield BrowserTestUtils.openNewForegroundTab(options.gBrowser, options.url); yield taskFn(tab.linkedBrowser); options.gBrowser.removeTab(tab); }), /** * Opens a new tab in the foreground. * * @param {tabbrowser} tabbrowser * The tabbrowser to open the tab new in. * @param {string} opening * 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. * * @return {Promise} * Resolves when the tab is ready and loaded as necessary. * @resolves The new tab. */ openNewForegroundTab(tabbrowser, opening = "about:blank", aWaitForLoad = true) { let tab; let promises = [ BrowserTestUtils.switchTab(tabbrowser, function () { if (typeof opening == "function") { opening(); tab = tabbrowser.selectedTab; } else { tabbrowser.selectedTab = tab = tabbrowser.addTab(opening); } }) ]; if (aWaitForLoad) { promises.push(BrowserTestUtils.browserLoaded(tab.linkedBrowser)); } return Promise.all(promises).then(() => tab); }, /** * 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 onSwitch() { tabbrowser.removeEventListener("TabSwitchDone", onSwitch); TestUtils.executeSoon(() => resolve(tabbrowser.selectedTab)); }); }); 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. * * @return {Promise} * @resolves When a load event is triggered for the browser. */ browserLoaded(browser, includeSubFrames=false) { return new Promise(resolve => { let mm = browser.ownerDocument.defaultView.messageManager; mm.addMessageListener("browser-test-utils:loadEvent", function onLoad(msg) { if (msg.target == browser && (!msg.data.subframe || includeSubFrames)) { mm.removeMessageListener("browser-test-utils:loadEvent", onLoad); resolve(); } }); }); }, /** * Waits for the next 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. * * @return {Promise} * @resolves With the {xul:tab} when a tab is opened and its location changes to the given URL. */ waitForNewTab(tabbrowser, url) { return new Promise((resolve, reject) => { tabbrowser.tabContainer.addEventListener("TabOpen", function onTabOpen(openEvent) { tabbrowser.tabContainer.removeEventListener("TabOpen", onTabOpen); let progressListener = { onLocationChange(aBrowser) { if (aBrowser != openEvent.target.linkedBrowser || aBrowser.currentURI.spec != url) { return; } tabbrowser.removeTabsProgressListener(progressListener); resolve(openEvent.target); }, }; tabbrowser.addTabsProgressListener(progressListener); }); }); }, /** * 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. */ loadURI: Task.async(function* (browser, uri) { // Load the new URI. browser.loadURI(uri); // Nothing to do in non-e10s mode. if (!browser.ownerDocument.defaultView.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)) { yield this.waitForEvent(browser, "XULFrameLoaderCreated"); } }), /** * @return {Promise} * A Promise which resolves when a "domwindowopened" notification * has been fired by the window watcher. */ domWindowOpened() { return new Promise(resolve => { function observer(subject, topic, data) { if (topic != "domwindowopened") { return; } 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. * } * @return {Promise} * Resolves with the new window once it is loaded. */ openNewBrowserWindow(options={}) { let argString = Cc["@mozilla.org/supports-string;1"]. createInstance(Ci.nsISupportsString); argString.data = ""; let features = "chrome,dialog=no,all"; if (options.private) { features += ",private"; } if (options.hasOwnProperty("remote")) { let remoteState = options.remote ? "remote" : "non-remote"; features += `,${remoteState}`; } let win = Services.ww.openWindow( null, Services.prefs.getCharPref("browser.chromeURL"), "_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. return TestUtils.topicObserved("browser-delayed-startup-finished", subject => subject == win).then(() => win); }, /** * Closes a window. * * @param {Window} * A window to close. * * @return {Promise} * Resolves when the provided window has been closed. */ closeWindow(win) { return new Promise(resolve => { function observer(subject, topic, data) { if (topic == "domwindowclosed" && subject === win) { Services.ww.unregisterNotification(observer); resolve(); } } Services.ww.registerNotification(observer); win.close(); }); }, /** * Waits for an event to be fired on a specified element. * * Usage: * let promiseEvent = BrowserTestUtil.waitForEvent(element, "eventName"); * // Do some processing here that will cause the event to be fired * // ... * // Now yield until the Promise is fulfilled * let receivedEvent = yield promiseEvent; * * @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. * * @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) { return new Promise((resolve, reject) => { subject.addEventListener(eventName, function listener(event) { try { if (checkFn && !checkFn(event)) { return; } subject.removeEventListener(eventName, listener, capture); resolve(event); } catch (ex) { try { subject.removeEventListener(eventName, listener, capture); } catch (ex2) { // Maybe the provided object does not support removeEventListener. } reject(ex); } }, capture); }); }, /** * 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 {string} target * A selector that identifies the element to target. The syntax is as * for querySelector. This may also be a CPOW element for easier * test-conversion. If this is null, then 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 => { let mm = browser.messageManager; mm.addMessageListener("Test:SynthesizeMouseDone", function mouseMsg(message) { mm.removeMessageListener("Test:SynthesizeMouseDone", mouseMsg); resolve(message.data.defaultPrevented); }); let cpowObject = null; if (typeof target != "string") { cpowObject = target; target = null; } mm.sendAsyncMessage("Test:SynthesizeMouse", {target, target, x: offsetX, y: offsetY, event: event}, {object: cpowObject}); }); }, /** * 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 and * waits until its final message has reached the parent. */ removeTab(tab, options = {}) { let dontRemove = options && options.dontRemove; 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); resolve(); } }, true); if (!dontRemove && !tab.closing) { tab.ownerDocument.defaultView.gBrowser.removeTab(tab); } }); } };