/* 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); 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) { if (typeof tab == "function") { tab(); } else { tabbrowser.selectedTab = tab; } //XXX this is esr38, we do not have e10s, this is never async, we do not pass go // and do not collect, err, I mean, we continue without waiting for an event: return new Promise(resolve => { TestUtils.executeSoon(() => resolve(tabbrowser.selectedTab)); }); }, /** * 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(); } }); }); }, /** * @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 {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, checkFn) { return new Promise((resolve, reject) => { subject.addEventListener(eventName, function listener(event) { try { if (checkFn && !checkFn(event)) { return; } subject.removeEventListener(eventName, listener); resolve(event); } catch (ex) { try { subject.removeEventListener(eventName, listener); } catch (ex2) { // Maybe the provided object does not support removeEventListener. } reject(ex); } }); }); }, /** * 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); }, };