diff options
Diffstat (limited to 'testing/marionette/browser.js')
-rw-r--r-- | testing/marionette/browser.js | 532 |
1 files changed, 532 insertions, 0 deletions
diff --git a/testing/marionette/browser.js b/testing/marionette/browser.js new file mode 100644 index 0000000000..60b402e395 --- /dev/null +++ b/testing/marionette/browser.js @@ -0,0 +1,532 @@ +/* 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/. */ + +"use strict"; +/* global frame */ + +const EXPORTED_SYMBOLS = ["browser", "Context", "WindowState"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + element: "chrome://marionette/content/element.js", + error: "chrome://marionette/content/error.js", + Log: "chrome://marionette/content/log.js", + MessageManagerDestroyedPromise: "chrome://marionette/content/sync.js", + waitForEvent: "chrome://marionette/content/sync.js", + waitForObserverTopic: "chrome://marionette/content/sync.js", + WebElementEventTarget: "chrome://marionette/content/dom.js", +}); + +XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get()); + +/** @namespace */ +this.browser = {}; + +/** + * Variations of Marionette contexts. + * + * Choosing a context through the <tt>Marionette:SetContext</tt> + * command directs all subsequent browsing context scoped commands + * to that context. + */ +class Context { + /** + * Gets the correct context from a string. + * + * @param {string} s + * Context string serialisation. + * + * @return {Context} + * Context. + * + * @throws {TypeError} + * If <var>s</var> 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"; +this.Context = Context; + +// GeckoView shim for Desktop's gBrowser +class MobileTabBrowser { + constructor(window) { + this.window = window; + } + + get tabs() { + return [this.window.tab]; + } + + get selectedTab() { + return this.window.tab; + } + + set selectedTab(tab) { + if (tab != this.selectedTab) { + throw new Error("GeckoView only supports a single tab"); + } + + // Synthesize a custom TabSelect event to indicate that a tab has been + // selected even when we don't change it. + const event = this.window.CustomEvent("TabSelect", { + bubbles: true, + cancelable: false, + detail: { + previousTab: this.selectedTab, + }, + }); + this.window.document.dispatchEvent(event); + } + + get selectedBrowser() { + return this.selectedTab.linkedBrowser; + } +} + +/** + * Get the <code><xul:browser></code> for the specified tab. + * + * @param {Tab} tab + * The tab whose browser needs to be returned. + * + * @return {Browser} + * The linked browser for the tab or null if no browser can be found. + */ +browser.getBrowserForTab = function(tab) { + if (tab && "linkedBrowser" in tab) { + return tab.linkedBrowser; + } + + return null; +}; + +/** + * Return the tab browser for the specified chrome window. + * + * @param {ChromeWindow} win + * Window whose <code>tabbrowser</code> needs to be accessed. + * + * @return {Tab} + * Tab browser or null if it's not a browser window. + */ +browser.getTabBrowser = function(window) { + // GeckoView + if (Services.androidBridge) { + return new MobileTabBrowser(window); + // Firefox + } else if ("gBrowser" in window) { + return window.gBrowser; + // Thunderbird + } else if (window.document.getElementById("tabmail")) { + return window.document.getElementById("tabmail"); + } + + return null; +}; + +/** + * Creates a browsing context wrapper. + * + * Browsing contexts handle interactions with the browser, according to + * the current environment. + */ +browser.Context = class { + /** + * @param {ChromeWindow} win + * 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 <xul:tabbrowser> (not <xul:browser>!) + // and MobileTabBrowser in GeckoView. + this.tabBrowser = browser.getTabBrowser(this.window); + + // Used to set curFrameId upon new session + this.newSession = true; + + this.seenEls = new element.Store(); + + // 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; + + this.frameRegsPending = 0; + + this.getIdForBrowser = driver.getIdForBrowser.bind(driver); + this.updateIdForBrowser = driver.updateIdForBrowser.bind(driver); + } + + /** + * 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 browser.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 <code><xul:browser></code>, + * has been detached. + * + * @return {boolean} + * True if browsing context has been discarded, false otherwise. + */ + get closed() { + return this.contentBrowser === null; + } + + /** + * The current frame ID is managed per browser element on desktop in + * case the ID needs to be refreshed. The currently selected window is + * identified by a tab. + */ + get curFrameId() { + let rv = null; + if (this.tab || this.driver.isReftestBrowser(this.contentBrowser)) { + rv = this.getIdForBrowser(this.contentBrowser); + } + return rv; + } + + /** + * Gets the position and dimensions of the top-level browsing context. + * + * @return {Map.<string, number>} + * 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. + * + * @return {Promise} + * A promise which is resolved when the current window has been closed. + */ + async closeWindow() { + const destroyed = waitForObserverTopic("xul-window-destroyed", { + checkFn: () => this.window && this.window.closed, + }); + + this.window.close(); + + return destroyed; + } + + /** + * Focus the current window. + * + * @return {Promise} + * A promise which is resolved when the current window has been focused. + */ + async focusWindow() { + if (Services.focus.activeWindow != this.window) { + let activated = waitForEvent(this.window, "activate"); + let focused = waitForEvent(this.window, "focus", { capture: true }); + + this.window.focus(); + + await Promise.all([activated, focused]); + } + } + + /** + * Open a new browser window. + * + * @return {Promise} + * A promise resolving to the newly created chrome window. + */ + async openBrowserWindow(focus = false, isPrivate = false) { + switch (this.driver.appName) { + case "firefox": + // Open new browser window, and wait until it is fully loaded. + // Also wait for the window to be focused and activated to prevent a + // race condition when promptly focusing to the original window again. + const win = this.window.OpenBrowserWindow({ private: isPrivate }); + + const activated = waitForEvent(win, "activate"); + const focused = waitForEvent(win, "focus", { capture: true }); + const startup = waitForObserverTopic( + "browser-delayed-startup-finished", + { + checkFn: subject => subject == win, + } + ); + + win.focus(); + await Promise.all([activated, focused, startup]); + + // The new window shouldn't get focused. As such set the + // focus back to the opening window. + if (!focus) { + await this.focusWindow(); + } + + return win; + + default: + throw new error.UnsupportedOperationError( + `openWindow() not supported in ${this.driver.appName}` + ); + } + } + + /** + * Close the current tab. + * + * @return {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. + */ + 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. + if ( + !this.tabBrowser || + !this.tabBrowser.tabs || + this.tabBrowser.tabs.length === 1 || + !this.tab + ) { + return this.closeWindow(); + } + + let destroyed = new MessageManagerDestroyedPromise(this.messageManager); + let tabClosed; + + switch (this.driver.appName) { + case "firefox": + tabClosed = waitForEvent(this.tab, "TabClose"); + this.tabBrowser.removeTab(this.tab); + break; + + default: + throw new error.UnsupportedOperationError( + `closeTab() not supported in ${this.driver.appName}` + ); + } + + return Promise.all([destroyed, tabClosed]); + } + + /** + * Open a new tab in the currently selected chrome window. + */ + async openTab(focus = false) { + let tab = null; + + switch (this.driver.appName) { + case "firefox": + const opened = waitForEvent(this.window, "TabOpen"); + this.window.BrowserOpenTab(); + 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) { + this.tabBrowser.selectedTab = this.tab; + } + + break; + + default: + throw new error.UnsupportedOperationError( + `openTab() not supported in ${this.driver.appName}` + ); + } + + 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. + * + * @return {Tab} + * The selected tab. + * + * @throws UnsupportedOperationError + * If tab handling for the current application isn't supported. + */ + async switchToTab(index, window = undefined, focus = true) { + let currentTab = this.tabBrowser.selectedTab; + + if (window) { + this.window = window; + this.tabBrowser = browser.getTabBrowser(this.window); + } + + if (!this.tabBrowser) { + return null; + } + + if (typeof index == "undefined") { + this.tab = this.tabBrowser.selectedTab; + } else { + this.tab = this.tabBrowser.tabs[index]; + } + + if (focus && this.tab != currentTab) { + const tabSelected = waitForEvent(this.window, "TabSelect"); + this.tabBrowser.selectedTab = this.tab; + await tabSelected; + } + + // TODO(ato): Currently tied to curBrowser, but should be moved to + // WebElement when introduced by https://bugzil.la/1400256. + this.eventObserver = new WebElementEventTarget(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. + * + * @param {xul:browser} target + * The <xul:browser> that was the target of the originating message. + */ + register(target) { + 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(); + } + + if (target === this.contentBrowser) { + // Note that browsing contexts can be swapped during navigation in which + // case this id would no longer match the target. See Bug 1680479. + const uid = target.browsingContext.id; + this.updateIdForBrowser(this.contentBrowser, uid); + } + } +}; + +/** + * Marionette representation of the {@link ChromeWindow} window state. + * + * @enum {string} + */ +const WindowState = { + Maximized: "maximized", + Minimized: "minimized", + Normal: "normal", + Fullscreen: "fullscreen", + + /** + * Converts {@link nsIDOMChromeWindow.windowState} to WindowState. + * + * @param {number} windowState + * Attribute from {@link nsIDOMChromeWindow.windowState}. + * + * @return {WindowState} + * JSON representation. + * + * @throws {TypeError} + * If <var>windowState</var> 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}`); + } + }, +}; +this.WindowState = WindowState; |