Bug 1752140 [wpt PR 32556] - battery status: Remove battery-insecure-context.html...
[gecko.git] / remote / shared / WindowManager.jsm
blob177ccc5bcad4b305c893d7f2f3a376e7752a8d26
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/. */
5 "use strict";
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",
22 });
24 /**
25  * Provides helpers to interact with Window objects.
26  *
27  * @class WindowManager
28  */
29 class WindowManager {
30   constructor() {
31     // Maps ChromeWindow to uuid: WeakMap.<Object, string>
32     this._chromeWindowHandles = new WeakMap();
33   }
35   get chromeWindowHandles() {
36     const chromeWindowHandles = [];
38     for (const win of this.windows) {
39       chromeWindowHandles.push(this.getIdForWindow(win));
40     }
42     return chromeWindowHandles;
43   }
45   get windows() {
46     return Services.wm.getEnumerator(null);
47   }
49   /**
50    * Find a specific window matching the provided window handle.
51    *
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`.
55    *
56    * @return {Object} A window properties object,
57    *     @see :js:func:`GeckoDriver#getWindowProperties`
58    */
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);
65       }
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 });
77           }
78         }
79       }
80     }
82     return null;
83   }
85   /**
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
88    * Content Window.
89    *
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
95    *     tabBrowser.
96    * @property {Number} tabIndex - Optional, the index of the specific tab
97    *     within the window.
98    */
100   /**
101    * Returns a WindowProperties object, that can be used with :js:func:`GeckoDriver#setWindowHandle`.
102    *
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.
109    */
110   getWindowProperties(win, options = {}) {
111     if (!(win instanceof Window)) {
112       throw new TypeError("Invalid argument, expected a Window object");
113     }
115     return {
116       win,
117       id: this.getIdForWindow(win),
118       hasTabBrowser: !!TabManager.getTabBrowser(win),
119       tabIndex: options.tabIndex,
120     };
121   }
123   /**
124    * Retrieves an id for the given chrome window. The id is a dynamically
125    * generated uuid associated with the window object.
126    *
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.
130    */
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));
135     }
136     return this._chromeWindowHandles.get(win);
137   }
139   /**
140    * Close the specified window.
141    *
142    * @param {window} win
143    *     The window to close.
144    * @return {Promise}
145    *     A promise which is resolved when the current window has been closed.
146    */
147   async closeWindow(win) {
148     const destroyed = waitForObserverTopic("xul-window-destroyed", {
149       checkFn: () => win && win.closed,
150     });
152     win.close();
154     return destroyed;
155   }
157   /**
158    * Focus the specified window.
159    *
160    * @param {window} win
161    *     The window to focus.
162    * @return {Promise}
163    *     A promise which is resolved when the window has been focused.
164    */
165   async focusWindow(win) {
166     if (Services.focus.activeWindow != win) {
167       let activated = waitForEvent(win, "activate");
168       let focused = waitForEvent(win, "focus", { capture: true });
170       win.focus();
172       await Promise.all([activated, focused]);
173     }
174   }
176   /**
177    * Open a new browser window.
178    *
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.
185    * @return {Promise}
186    *     A promise resolving to the newly created chrome window.
187    */
188   async openBrowserWindow(openerWindow, focus = false, isPrivate = false) {
189     switch (AppInfo.name) {
190       case "Firefox":
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",
200           {
201             checkFn: subject => subject == win,
202           }
203         );
205         win.focus();
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.
210         if (!focus) {
211           await this.focusWindow(openerWindow);
212         }
214         return win;
216       default:
217         throw new error.UnsupportedOperationError(
218           `openWindow() not supported in ${AppInfo.name}`
219         );
220     }
221   }
223   /**
224    * Wait until the initial application window has been opened and loaded.
225    *
226    * @return {Promise<WindowProxy>}
227    *     A promise that resolved to the application window.
228    */
229   waitForInitialApplicationWindow() {
230     return new TimedPromise(
231       resolve => {
232         const waitForWindow = () => {
233           let windowTypes;
234           if (AppInfo.isThunderbird) {
235             windowTypes = ["mail:3pane"];
236           } else {
237             // We assume that an app either has GeckoView windows, or
238             // Firefox/Fennec windows, but not both.
239             windowTypes = ["navigator:browser", "navigator:geckoview"];
240           }
242           let win;
243           for (const windowType of windowTypes) {
244             win = Services.wm.getMostRecentWindow(windowType);
245             if (win) {
246               break;
247             }
248           }
250           if (!win) {
251             // if the window isn't even created, just poll wait for it
252             let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(
253               Ci.nsITimer
254             );
255             checkTimer.initWithCallback(
256               waitForWindow,
257               100,
258               Ci.nsITimer.TYPE_ONE_SHOT
259             );
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) {
266                 return;
267               }
268               win.removeEventListener("load", listener);
269               waitForWindow();
270             };
271             win.addEventListener("load", listener, true);
272           } else {
273             resolve(win);
274           }
275         };
277         waitForWindow();
278       },
279       {
280         errorMessage: "No applicable application windows found",
281       }
282     );
283   }
286 // Expose a shared singleton.
287 const windowManager = new WindowManager();