Backed out 2 changesets (bug 1855992) for causing talos failures @ mozilla::net:...
[gecko.git] / remote / shared / WindowManager.sys.mjs
blob350483a55c5445665589c663c129c2dbc7c4fd2d
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 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
9   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
10   EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
11   generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
12   TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
13   TimedPromise: "chrome://remote/content/marionette/sync.sys.mjs",
14   waitForObserverTopic: "chrome://remote/content/marionette/sync.sys.mjs",
15 });
17 /**
18  * Provides helpers to interact with Window objects.
19  *
20  * @class WindowManager
21  */
22 class WindowManager {
23   constructor() {
24     // Maps ChromeWindow to uuid: WeakMap.<Object, string>
25     this._chromeWindowHandles = new WeakMap();
26   }
28   get chromeWindowHandles() {
29     const chromeWindowHandles = [];
31     for (const win of this.windows) {
32       chromeWindowHandles.push(this.getIdForWindow(win));
33     }
35     return chromeWindowHandles;
36   }
38   get windows() {
39     return Services.wm.getEnumerator(null);
40   }
42   /**
43    * Find a specific window matching the provided window handle.
44    *
45    * @param {string} handle
46    *     The unique handle of either a chrome window or a content browser, as
47    *     returned by :js:func:`#getIdForBrowser` or :js:func:`#getIdForWindow`.
48    *
49    * @returns {object} A window properties object,
50    *     @see :js:func:`GeckoDriver#getWindowProperties`
51    */
52   findWindowByHandle(handle) {
53     for (const win of this.windows) {
54       // In case the wanted window is a chrome window, we are done.
55       const chromeWindowId = this.getIdForWindow(win);
56       if (chromeWindowId == handle) {
57         return this.getWindowProperties(win);
58       }
60       // Otherwise check if the chrome window has a tab browser, and that it
61       // contains a tab with the wanted window handle.
62       const tabBrowser = lazy.TabManager.getTabBrowser(win);
63       if (tabBrowser && tabBrowser.tabs) {
64         for (let i = 0; i < tabBrowser.tabs.length; ++i) {
65           let contentBrowser = lazy.TabManager.getBrowserForTab(
66             tabBrowser.tabs[i]
67           );
68           let contentWindowId = lazy.TabManager.getIdForBrowser(contentBrowser);
70           if (contentWindowId == handle) {
71             return this.getWindowProperties(win, { tabIndex: i });
72           }
73         }
74       }
75     }
77     return null;
78   }
80   /**
81    * A set of properties describing a window and that should allow to uniquely
82    * identify it. The described window can either be a Chrome Window or a
83    * Content Window.
84    *
85    * @typedef {object} WindowProperties
86    * @property {Window} win - The Chrome Window containing the window.
87    *     When describing a Chrome Window, this is the window itself.
88    * @property {string} id - The unique id of the containing Chrome Window.
89    * @property {boolean} hasTabBrowser - `true` if the Chrome Window has a
90    *     tabBrowser.
91    * @property {number} tabIndex - Optional, the index of the specific tab
92    *     within the window.
93    */
95   /**
96    * Returns a WindowProperties object, that can be used with :js:func:`GeckoDriver#setWindowHandle`.
97    *
98    * @param {Window} win
99    *     The Chrome Window for which we want to create a properties object.
100    * @param {object} options
101    * @param {number} options.tabIndex
102    *     Tab index of a specific Content Window in the specified Chrome Window.
103    * @returns {WindowProperties} A window properties object.
104    */
105   getWindowProperties(win, options = {}) {
106     if (!Window.isInstance(win)) {
107       throw new TypeError("Invalid argument, expected a Window object");
108     }
110     return {
111       win,
112       id: this.getIdForWindow(win),
113       hasTabBrowser: !!lazy.TabManager.getTabBrowser(win),
114       tabIndex: options.tabIndex,
115     };
116   }
118   /**
119    * Retrieves an id for the given chrome window. The id is a dynamically
120    * generated uuid associated with the window object.
121    *
122    * @param {window} win
123    *     The window object for which we want to retrieve the id.
124    * @returns {string} The unique id for this chrome window.
125    */
126   getIdForWindow(win) {
127     if (!this._chromeWindowHandles.has(win)) {
128       this._chromeWindowHandles.set(win, lazy.generateUUID());
129     }
130     return this._chromeWindowHandles.get(win);
131   }
133   /**
134    * Close the specified window.
135    *
136    * @param {window} win
137    *     The window to close.
138    * @returns {Promise}
139    *     A promise which is resolved when the current window has been closed.
140    */
141   async closeWindow(win) {
142     const destroyed = lazy.waitForObserverTopic("xul-window-destroyed", {
143       checkFn: () => win && win.closed,
144     });
146     win.close();
148     return destroyed;
149   }
151   /**
152    * Focus the specified window.
153    *
154    * @param {window} win
155    *     The window to focus.
156    * @returns {Promise}
157    *     A promise which is resolved when the window has been focused.
158    */
159   async focusWindow(win) {
160     if (Services.focus.activeWindow != win) {
161       let activated = new lazy.EventPromise(win, "activate");
162       let focused = new lazy.EventPromise(win, "focus", { capture: true });
164       win.focus();
166       await Promise.all([activated, focused]);
167     }
168   }
170   /**
171    * Open a new browser window.
172    *
173    * @param {object=} options
174    * @param {boolean=} options.focus
175    *     If true, the opened window will receive the focus. Defaults to false.
176    * @param {boolean=} options.isPrivate
177    *     If true, the opened window will be a private window. Defaults to false.
178    * @param {ChromeWindow=} options.openerWindow
179    *     Use this window as the opener of the new window. Defaults to the
180    *     topmost window.
181    * @returns {Promise}
182    *     A promise resolving to the newly created chrome window.
183    */
184   async openBrowserWindow(options = {}) {
185     let { focus = false, isPrivate = false, openerWindow = null } = options;
187     switch (lazy.AppInfo.name) {
188       case "Firefox":
189         if (openerWindow === null) {
190           // If no opener was provided, fallback to the topmost window.
191           openerWindow = Services.wm.getMostRecentBrowserWindow();
192         }
194         if (!openerWindow) {
195           throw new lazy.error.UnsupportedOperationError(
196             `openWindow() could not find a valid opener window`
197           );
198         }
200         // Open new browser window, and wait until it is fully loaded.
201         // Also wait for the window to be focused and activated to prevent a
202         // race condition when promptly focusing to the original window again.
203         const win = openerWindow.OpenBrowserWindow({ private: isPrivate });
205         const activated = new lazy.EventPromise(win, "activate");
206         const focused = new lazy.EventPromise(win, "focus", { capture: true });
207         const startup = lazy.waitForObserverTopic(
208           "browser-delayed-startup-finished",
209           {
210             checkFn: subject => subject == win,
211           }
212         );
214         // TODO: Both for WebDriver BiDi and classic, opening a new window
215         // should not run the focus steps. When focus is false we should avoid
216         // focusing the new window completely. See Bug 1766329
217         win.focus();
219         await Promise.all([activated, focused, startup]);
221         if (focus) {
222           // Focus the currently selected tab.
223           const browser = lazy.TabManager.getTabBrowser(win).selectedBrowser;
224           browser.focus();
225         } else {
226           // If the new window shouldn't get focused, set the
227           // focus back to the opening window.
228           await this.focusWindow(openerWindow);
229         }
231         return win;
233       default:
234         throw new lazy.error.UnsupportedOperationError(
235           `openWindow() not supported in ${lazy.AppInfo.name}`
236         );
237     }
238   }
240   /**
241    * Wait until the initial application window has been opened and loaded.
242    *
243    * @returns {Promise<WindowProxy>}
244    *     A promise that resolved to the application window.
245    */
246   waitForInitialApplicationWindowLoaded() {
247     return new lazy.TimedPromise(
248       async resolve => {
249         // This call includes a fallback to "mail3:pane" as well.
250         const win = Services.wm.getMostRecentBrowserWindow();
252         const windowLoaded = lazy.waitForObserverTopic(
253           "browser-delayed-startup-finished",
254           {
255             checkFn: subject => (win !== null ? subject == win : true),
256           }
257         );
259         // The current window has already been finished loading.
260         if (win && win.document.readyState == "complete") {
261           resolve(win);
262           return;
263         }
265         // Wait for the next browser/mail window to open and finished loading.
266         const { subject } = await windowLoaded;
267         resolve(subject);
268       },
269       {
270         errorMessage: "No applicable application window found",
271       }
272     );
273   }
276 // Expose a shared singleton.
277 export const windowManager = new WindowManager();