diff options
Diffstat (limited to 'testing/marionette/browser.js')
-rw-r--r-- | testing/marionette/browser.js | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/testing/marionette/browser.js b/testing/marionette/browser.js new file mode 100644 index 000000000..c6f9a2338 --- /dev/null +++ b/testing/marionette/browser.js @@ -0,0 +1,436 @@ +/* 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"; + +const {utils: Cu} = Components; + +Cu.import("chrome://marionette/content/element.js"); +Cu.import("chrome://marionette/content/error.js"); +Cu.import("chrome://marionette/content/frame.js"); + +this.EXPORTED_SYMBOLS = ["browser"]; + +this.browser = {}; + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + +/** + * Get the <xul:browser> for the specified tab. + * + * @param {<xul:tab>} tab + * The tab whose browser needs to be returned. + * + * @return {<xul:browser>} + * The linked browser for the tab or null if no browser can be found. + */ +browser.getBrowserForTab = function (tab) { + if ("browser" in tab) { + // Fennec + return tab.browser; + + } else if ("linkedBrowser" in tab) { + // Firefox + return tab.linkedBrowser; + + } else { + return null; + } +}; + +/** + * Return the tab browser for the specified chrome window. + * + * @param {nsIDOMWindow} win + * The window whose tabbrowser needs to be accessed. + * + * @return {<xul:tabbrowser>} + * Tab browser or null if it's not a browser window. + */ +browser.getTabBrowser = function (win) { + if ("BrowserApp" in win) { + // Fennec + return win.BrowserApp; + + } else if ("gBrowser" in win) { + // Firefox + return win.gBrowser; + + } else { + return null; + } +}; + +/** + * Creates a browsing context wrapper. + * + * Browsing contexts handle interactions with the browser, according to + * the current environment (desktop, B2G, Fennec, &c). + * + * @param {nsIDOMWindow} win + * The window whose browser needs to be accessed. + * @param {GeckoDriver} driver + * Reference to the driver the browser is attached to. + */ +browser.Context = class { + + /** + * @param {<xul:browser>} win + * Frame that is expected to contain the view of the web document. + * @param {GeckoDriver} driver + * Reference to driver instance. + */ + constructor(win, driver) { + this.window = win; + this.driver = driver; + + // In Firefox this is <xul:tabbrowser> (not <xul:browser>!) + // and BrowserApp in Fennec + this.tabBrowser = browser.getTabBrowser(win); + + this.knownFrames = []; + + // Used in B2G to identify the homescreen content page + this.mainContentId = null; + + // 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.pendingCommands = []; + + // We should have one frame.Manager per browser.Context so that we + // can handle modals in each <xul:browser>. + this.frameManager = new frame.Manager(driver); + this.frameRegsPending = 0; + + // register all message listeners + this.frameManager.addMessageManagerListeners(driver.mm); + this.getIdForBrowser = driver.getIdForBrowser.bind(driver); + this.updateIdForBrowser = driver.updateIdForBrowser.bind(driver); + this._curFrameId = null; + this._browserWasRemote = null; + this._hasRemotenessChange = false; + } + + /** + * 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.driver.appName == "B2G") { + rv = this._curFrameId; + } else if (this.tab) { + rv = this.getIdForBrowser(browser.getBrowserForTab(this.tab)); + } + return rv; + } + + set curFrameId(id) { + if (this.driver.appName != "Firefox") { + this._curFrameId = id; + } + } + + /** + * Retrieves the current tabmodal UI object. According to the browser + * associated with the currently selected tab. + */ + getTabModalUI() { + let br = browser.getBrowserForTab(this.tab); + if (!br.hasAttribute("tabmodalPromptShowing")) { + return null; + } + + // The modal is a direct sibling of the browser element. + // See tabbrowser.xml's getTabModalPromptBox. + let modals = br.parentNode.getElementsByTagNameNS( + XUL_NS, "tabmodalprompt"); + return modals[0].ui; + } + + /** + * Close the current window. + * + * @return {Promise} + * A promise which is resolved when the current window has been closed. + */ + closeWindow() { + return new Promise(resolve => { + this.window.addEventListener("unload", ev => { + resolve(); + }, {once: true}); + this.window.close(); + }); + } + + /** Called when we start a session with this browser. */ + startSession(newSession, win, callback) { + callback(win, newSession); + } + + /** + * 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.length === 1 || !this.tab) { + return this.closeWindow(); + } + + return new Promise((resolve, reject) => { + if (this.tabBrowser.closeTab) { + // Fennec + this.tabBrowser.deck.addEventListener("TabClose", ev => { + resolve(); + }, {once: true}); + this.tabBrowser.closeTab(this.tab); + + } else if (this.tabBrowser.removeTab) { + // Firefox + this.tab.addEventListener("TabClose", ev => { + resolve(); + }, {once: true}); + this.tabBrowser.removeTab(this.tab); + + } else { + reject(new UnsupportedOperationError( + `closeTab() not supported in ${this.driver.appName}`)); + } + }); + } + + /** + * Opens a tab with given URI. + * + * @param {string} uri + * URI to open. + */ + addTab(uri) { + return this.tabBrowser.addTab(uri, true); + } + + /** + * Set the current tab and update remoteness tracking if a tabbrowser is available. + * + * @param {number=} index + * Tab index to switch to. If the parameter is undefined, + * the currently selected tab will be used. + * @param {nsIDOMWindow=} win + * Switch to this window before selecting the tab. + * @param {boolean=} focus + * A boolean value which determins whether to focus + * the window. Defaults to true. + * + * @throws UnsupportedOperationError + * If tab handling for the current application isn't supported. + */ + switchToTab(index, win, focus = true) { + if (win) { + this.window = win; + this.tabBrowser = browser.getTabBrowser(win); + } + + if (!this.tabBrowser) { + return; + } + + if (typeof index == "undefined") { + this.tab = this.tabBrowser.selectedTab; + } else { + this.tab = this.tabBrowser.tabs[index]; + + if (focus) { + if (this.tabBrowser.selectTab) { + // Fennec + this.tabBrowser.selectTab(this.tab); + + } else if ("selectedTab" in this.tabBrowser) { + // Firefox + this.tabBrowser.selectedTab = this.tab; + + } else { + throw new UnsupportedOperationError("switchToTab() not supported"); + } + } + } + + if (this.driver.appName == "Firefox") { + this._browserWasRemote = browser.getBrowserForTab(this.tab).isRemoteBrowser; + this._hasRemotenessChange = false; + } + } + + /** + * 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 {string} uid + * Frame uid for use by Marionette. + * @param the XUL <browser> that was the target of the originating message. + */ + register(uid, target) { + let remotenessChange = this.hasRemotenessChange(); + if (this.curFrameId === null || remotenessChange) { + if (this.tabBrowser) { + // 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 == browser.getBrowserForTab(this.tab)) { + this.updateIdForBrowser(browser.getBrowserForTab(this.tab), uid); + this.mainContentId = uid; + } + } else { + this._curFrameId = uid; + this.mainContentId = uid; + } + } + + // used to delete sessions + this.knownFrames.push(uid); + return remotenessChange; + } + + /** + * When navigating between pages results in changing a browser's + * process, we need to take measures not to lose contact with a listener + * script. This function does the necessary bookkeeping. + */ + hasRemotenessChange() { + // None of these checks are relevant on b2g or if we don't have a tab yet, + // and may not apply on Fennec. + if (this.driver.appName != "Firefox" || + this.tab === null || + browser.getBrowserForTab(this.tab) === null) { + return false; + } + + if (this._hasRemotenessChange) { + return true; + } + + let currentIsRemote = browser.getBrowserForTab(this.tab).isRemoteBrowser; + this._hasRemotenessChange = this._browserWasRemote !== currentIsRemote; + this._browserWasRemote = currentIsRemote; + return this._hasRemotenessChange; + } + + /** + * Flushes any pending commands queued when a remoteness change is being + * processed and mark this remotenessUpdate as complete. + */ + flushPendingCommands() { + if (!this._hasRemotenessChange) { + return; + } + + this._hasRemotenessChange = false; + this.pendingCommands.forEach(cb => cb()); + this.pendingCommands = []; + } + + /** + * This function intercepts commands interacting with content and queues + * or executes them as needed. + * + * No commands interacting with content are safe to process until + * the new listener script is loaded and registers itself. + * This occurs when a command whose effect is asynchronous (such + * as goBack) results in a remoteness change and new commands + * are subsequently posted to the server. + */ + executeWhenReady(cb) { + if (this.hasRemotenessChange()) { + this.pendingCommands.push(cb); + } else { + cb(); + } + } + + /** + * Returns the position of the OS window. + */ + get position() { + return { + x: this.window.screenX, + y: this.window.screenY, + }; + } + +}; + +/** + * The window storage is used to save outer window IDs mapped to weak + * references of Window objects. + * + * Usage: + * + * let wins = new browser.Windows(); + * wins.set(browser.outerWindowID, window); + * + * ... + * + * let win = wins.get(browser.outerWindowID); + * + */ +browser.Windows = class extends Map { + + /** + * Save a weak reference to the Window object. + * + * @param {string} id + * Outer window ID. + * @param {Window} win + * Window object to save. + * + * @return {browser.Windows} + * Instance of self. + */ + set(id, win) { + let wref = Cu.getWeakReference(win); + super.set(id, wref); + return this; + } + + /** + * Get the window object stored by provided |id|. + * + * @param {string} id + * Outer window ID. + * + * @return {Window} + * Saved window object, or |undefined| if no window is stored by + * provided |id|. + */ + get(id) { + let wref = super.get(id); + if (wref) { + return wref.get(); + } + } + +}; |