1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const EXPORTED_SYMBOLS = ["windowManager"];
9 const { XPCOMUtils } = ChromeUtils.import(
10 "resource://gre/modules/XPCOMUtils.jsm"
13 XPCOMUtils.defineLazyModuleGetters(this, {
14 Services: "resource://gre/modules/Services.jsm",
16 AppInfo: "chrome://remote/content/marionette/appinfo.js",
17 error: "chrome://remote/content/shared/webdriver/Errors.jsm",
18 TabManager: "chrome://remote/content/shared/TabManager.jsm",
19 TimedPromise: "chrome://remote/content/marionette/sync.js",
20 waitForEvent: "chrome://remote/content/marionette/sync.js",
21 waitForObserverTopic: "chrome://remote/content/marionette/sync.js",
25 * Provides helpers to interact with Window objects.
27 * @class WindowManager
31 // Maps ChromeWindow to uuid: WeakMap.<Object, string>
32 this._chromeWindowHandles = new WeakMap();
35 get chromeWindowHandles() {
36 const chromeWindowHandles = [];
38 for (const win of this.windows) {
39 chromeWindowHandles.push(this.getIdForWindow(win));
42 return chromeWindowHandles;
46 return Services.wm.getEnumerator(null);
50 * Find a specific window matching the provided window handle.
52 * @param {String} handle
53 * The unique handle of either a chrome window or a content browser, as
54 * returned by :js:func:`#getIdForBrowser` or :js:func:`#getIdForWindow`.
56 * @return {Object} A window properties object,
57 * @see :js:func:`GeckoDriver#getWindowProperties`
59 findWindowByHandle(handle) {
60 for (const win of this.windows) {
61 // In case the wanted window is a chrome window, we are done.
62 const chromeWindowId = this.getIdForWindow(win);
63 if (chromeWindowId == handle) {
64 return this.getWindowProperties(win);
67 // Otherwise check if the chrome window has a tab browser, and that it
68 // contains a tab with the wanted window handle.
69 const tabBrowser = TabManager.getTabBrowser(win);
70 if (tabBrowser && tabBrowser.tabs) {
71 for (let i = 0; i < tabBrowser.tabs.length; ++i) {
72 let contentBrowser = TabManager.getBrowserForTab(tabBrowser.tabs[i]);
73 let contentWindowId = TabManager.getIdForBrowser(contentBrowser);
75 if (contentWindowId == handle) {
76 return this.getWindowProperties(win, { tabIndex: i });
86 * A set of properties describing a window and that should allow to uniquely
87 * identify it. The described window can either be a Chrome Window or a
90 * @typedef {Object} WindowProperties
91 * @property {Window} win - The Chrome Window containing the window.
92 * When describing a Chrome Window, this is the window itself.
93 * @property {String} id - The unique id of the containing Chrome Window.
94 * @property {Boolean} hasTabBrowser - `true` if the Chrome Window has a
96 * @property {Number} tabIndex - Optional, the index of the specific tab
101 * Returns a WindowProperties object, that can be used with :js:func:`GeckoDriver#setWindowHandle`.
103 * @param {Window} win
104 * The Chrome Window for which we want to create a properties object.
105 * @param {Object} options
106 * @param {Number} options.tabIndex
107 * Tab index of a specific Content Window in the specified Chrome Window.
108 * @return {WindowProperties} A window properties object.
110 getWindowProperties(win, options = {}) {
111 if (!(win instanceof Window)) {
112 throw new TypeError("Invalid argument, expected a Window object");
117 id: this.getIdForWindow(win),
118 hasTabBrowser: !!TabManager.getTabBrowser(win),
119 tabIndex: options.tabIndex,
124 * Retrieves an id for the given chrome window. The id is a dynamically
125 * generated uuid associated with the window object.
127 * @param {window} win
128 * The window object for which we want to retrieve the id.
129 * @return {String} The unique id for this chrome window.
131 getIdForWindow(win) {
132 if (!this._chromeWindowHandles.has(win)) {
133 const uuid = Services.uuid.generateUUID().toString();
134 this._chromeWindowHandles.set(win, uuid.substring(1, uuid.length - 1));
136 return this._chromeWindowHandles.get(win);
140 * Close the specified window.
142 * @param {window} win
143 * The window to close.
145 * A promise which is resolved when the current window has been closed.
147 async closeWindow(win) {
148 const destroyed = waitForObserverTopic("xul-window-destroyed", {
149 checkFn: () => win && win.closed,
158 * Focus the specified window.
160 * @param {window} win
161 * The window to focus.
163 * A promise which is resolved when the window has been focused.
165 async focusWindow(win) {
166 if (Services.focus.activeWindow != win) {
167 let activated = waitForEvent(win, "activate");
168 let focused = waitForEvent(win, "focus", { capture: true });
172 await Promise.all([activated, focused]);
177 * Open a new browser window.
179 * @param {window} openerWindow
180 * The window from which the new window should be opened.
181 * @param {Boolean} [focus=false]
182 * If true, the opened window will receive the focus.
183 * @param {Boolean} [isPrivate=false]
184 * If true, the opened window will be a private window.
186 * A promise resolving to the newly created chrome window.
188 async openBrowserWindow(openerWindow, focus = false, isPrivate = false) {
189 switch (AppInfo.name) {
191 // Open new browser window, and wait until it is fully loaded.
192 // Also wait for the window to be focused and activated to prevent a
193 // race condition when promptly focusing to the original window again.
194 const win = openerWindow.OpenBrowserWindow({ private: isPrivate });
196 const activated = waitForEvent(win, "activate");
197 const focused = waitForEvent(win, "focus", { capture: true });
198 const startup = waitForObserverTopic(
199 "browser-delayed-startup-finished",
201 checkFn: subject => subject == win,
206 await Promise.all([activated, focused, startup]);
208 // The new window shouldn't get focused. As such set the
209 // focus back to the opening window.
211 await this.focusWindow(openerWindow);
217 throw new error.UnsupportedOperationError(
218 `openWindow() not supported in ${AppInfo.name}`
224 * Wait until the initial application window has been opened and loaded.
226 * @return {Promise<WindowProxy>}
227 * A promise that resolved to the application window.
229 waitForInitialApplicationWindow() {
230 return new TimedPromise(
232 const waitForWindow = () => {
234 if (AppInfo.isThunderbird) {
235 windowTypes = ["mail:3pane"];
237 // We assume that an app either has GeckoView windows, or
238 // Firefox/Fennec windows, but not both.
239 windowTypes = ["navigator:browser", "navigator:geckoview"];
243 for (const windowType of windowTypes) {
244 win = Services.wm.getMostRecentWindow(windowType);
251 // if the window isn't even created, just poll wait for it
252 let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(
255 checkTimer.initWithCallback(
258 Ci.nsITimer.TYPE_ONE_SHOT
260 } else if (win.document.readyState != "complete") {
261 // otherwise, wait for it to be fully loaded before proceeding
262 let listener = ev => {
263 // ensure that we proceed, on the top level document load event
264 // (not an iframe one...)
265 if (ev.target != win.document) {
268 win.removeEventListener("load", listener);
271 win.addEventListener("load", listener, true);
280 errorMessage: "No applicable application windows found",
286 // Expose a shared singleton.
287 const windowManager = new WindowManager();