/* 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/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", EventPromise: "chrome://remote/content/shared/Sync.sys.mjs", MessageManagerDestroyedPromise: "chrome://remote/content/marionette/sync.sys.mjs", TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", }); /** @namespace */ export const browser = {}; /** * Variations of Marionette contexts. * * Choosing a context through the Marionette:SetContext * command directs all subsequent browsing context scoped commands * to that context. * * @class Marionette.Context */ export class Context { /** * Gets the correct context from a string. * * @param {string} s * Context string serialisation. * * @returns {Context} * Context. * * @throws {TypeError} * If s is not a context. */ static fromString(s) { switch (s) { case "chrome": return Context.Chrome; case "content": return Context.Content; default: throw new TypeError(`Unknown context: ${s}`); } } } Context.Chrome = "chrome"; Context.Content = "content"; /** * Creates a browsing context wrapper. * * Browsing contexts handle interactions with the browser, according to * the current environment. */ browser.Context = class { /** * @param {ChromeWindow} window * ChromeWindow that contains the top-level browsing context. * @param {GeckoDriver} driver * Reference to driver instance. */ constructor(window, driver) { this.window = window; this.driver = driver; // In Firefox this is (not !) // and MobileTabBrowser in GeckoView. this.tabBrowser = lazy.TabManager.getTabBrowser(this.window); // Used to set curFrameId upon new session this.newSession = true; // A reference to the tab corresponding to the current window handle, // if any. Specifically, this.tab refers to the last tab that Marionette // switched to in this browser window. Note that this may not equal the // currently selected tab. For example, if Marionette switches to tab // A, and then clicks on a button that opens a new tab B in the same // browser window, this.tab will still point to tab A, despite tab B // being the currently selected tab. this.tab = null; } /** * Returns the content browser for the currently selected tab. * If there is no tab selected, null will be returned. */ get contentBrowser() { if (this.tab) { return lazy.TabManager.getBrowserForTab(this.tab); } else if ( this.tabBrowser && this.driver.isReftestBrowser(this.tabBrowser) ) { return this.tabBrowser; } return null; } get messageManager() { if (this.contentBrowser) { return this.contentBrowser.messageManager; } return null; } /** * Checks if the browsing context has been discarded. * * The browsing context will have been discarded if the content * browser, represented by the <xul:browser>, * has been detached. * * @returns {boolean} * True if browsing context has been discarded, false otherwise. */ get closed() { return this.contentBrowser === null; } /** * Gets the position and dimensions of the top-level browsing context. * * @returns {Map.} * Object with |x|, |y|, |width|, and |height| properties. */ get rect() { return { x: this.window.screenX, y: this.window.screenY, width: this.window.outerWidth, height: this.window.outerHeight, }; } /** * Retrieves the current tabmodal UI object. According to the browser * associated with the currently selected tab. */ getTabModal() { let br = this.contentBrowser; if (!br.hasAttribute("tabmodalPromptShowing")) { return null; } // The modal is a direct sibling of the browser element. // See tabbrowser.xml's getTabModalPromptBox. let modalElements = br.parentNode.getElementsByTagName("tabmodalprompt"); return br.tabModalPromptBox.getPrompt(modalElements[0]); } /** * Close the current window. * * @returns {Promise} * A promise which is resolved when the current window has been closed. */ async closeWindow() { return lazy.windowManager.closeWindow(this.window); } /** * Focus the current window. * * @returns {Promise} * A promise which is resolved when the current window has been focused. */ async focusWindow() { await lazy.windowManager.focusWindow(this.window); // Also focus the currently selected tab if present. this.contentBrowser?.focus(); } /** * Open a new browser window. * * @returns {Promise} * A promise resolving to the newly created chrome window. */ openBrowserWindow(focus = false, isPrivate = false) { return lazy.windowManager.openBrowserWindow({ openerWindow: this.window, focus, isPrivate, }); } /** * Close the current tab. * * @returns {Promise} * A promise which is resolved when the current tab has been closed. * * @throws UnsupportedOperationError * If tab handling for the current application isn't supported. */ async closeTab() { // If the current window is not a browser then close it directly. Do the // same if only one remaining tab is open, or no tab selected at all. // // Note: For GeckoView there will always be a single tab only. But for // consistency with other platforms a specific condition has been added // below as well even it's not really used. if ( !this.tabBrowser || !this.tabBrowser.tabs || this.tabBrowser.tabs.length === 1 || !this.tab ) { return this.closeWindow(); } let destroyed = new lazy.MessageManagerDestroyedPromise( this.messageManager ); let tabClosed; if (lazy.AppInfo.isAndroid) { await lazy.TabManager.removeTab(this.tab); } else if (lazy.AppInfo.isFirefox) { tabClosed = new lazy.EventPromise(this.tab, "TabClose"); await this.tabBrowser.removeTab(this.tab); } else { throw new lazy.error.UnsupportedOperationError( `closeTab() not supported for ${lazy.AppInfo.name}` ); } return Promise.all([destroyed, tabClosed]); } /** * Open a new tab in the currently selected chrome window. */ async openTab(focus = false) { let tab = null; // Bug 1795841 - For Firefox the TabManager cannot be used yet. As such // handle opening a tab differently for Android. if (lazy.AppInfo.isAndroid) { tab = await lazy.TabManager.addTab({ focus, window: this.window }); } else if (lazy.AppInfo.isFirefox) { const opened = new lazy.EventPromise(this.window, "TabOpen"); this.window.BrowserOpenTab({ url: "about:blank" }); await opened; tab = this.tabBrowser.selectedTab; // The new tab is always selected by default. If focus is not wanted, // the previously tab needs to be selected again. if (!focus) { await lazy.TabManager.selectTab(this.tab); } } else { throw new lazy.error.UnsupportedOperationError( `openTab() not supported for ${lazy.AppInfo.name}` ); } return tab; } /** * Set the current tab. * * @param {number=} index * Tab index to switch to. If the parameter is undefined, * the currently selected tab will be used. * @param {ChromeWindow=} window * Switch to this window before selecting the tab. * @param {boolean=} focus * A boolean value which determins whether to focus * the window. Defaults to true. * * @returns {Tab} * The selected tab. * * @throws UnsupportedOperationError * If tab handling for the current application isn't supported. */ async switchToTab(index, window = undefined, focus = true) { if (window) { this.window = window; this.tabBrowser = lazy.TabManager.getTabBrowser(this.window); } if (!this.tabBrowser || this.driver.isReftestBrowser(this.tabBrowser)) { return null; } if (typeof index == "undefined") { this.tab = this.tabBrowser.selectedTab; } else { this.tab = this.tabBrowser.tabs[index]; } if (focus) { await lazy.TabManager.selectTab(this.tab); } // By accessing the content browser's message manager a new browsing // context is created for browserless tabs, which is needed to successfully // run the WebDriver's is browsing context open step. This is temporary // until we find a better solution on bug 1812258. this.messageManager; return this.tab; } /** * Registers a new frame, and sets its current frame id to this frame * if it is not already assigned, and if a) we already have a session * or b) we're starting a new session and it is the right start frame. */ register() { if (!this.tabBrowser) { return; } // If we're setting up a new session on Firefox, we only process the // registration for this frame if it belongs to the current tab. if (!this.tab) { this.switchToTab(); } } }; /** * Marionette representation of the {@link ChromeWindow} window state. * * @enum {string} */ export const WindowState = { Maximized: "maximized", Minimized: "minimized", Normal: "normal", Fullscreen: "fullscreen", /** * Converts {@link Window.windowState} to WindowState. * * @param {number} windowState * Attribute from {@link Window.windowState}. * * @returns {WindowState} * JSON representation. * * @throws {TypeError} * If windowState was unknown. */ from(windowState) { switch (windowState) { case 1: return WindowState.Maximized; case 2: return WindowState.Minimized; case 3: return WindowState.Normal; case 4: return WindowState.Fullscreen; default: throw new TypeError(`Unknown window state: ${windowState}`); } }, };