diff options
Diffstat (limited to 'testing/marionette/driver.js')
-rw-r--r-- | testing/marionette/driver.js | 2907 |
1 files changed, 2907 insertions, 0 deletions
diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js new file mode 100644 index 000000000..7ef72d3bb --- /dev/null +++ b/testing/marionette/driver.js @@ -0,0 +1,2907 @@ +/* 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"; + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader); + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter( + this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2"); + +Cu.import("chrome://marionette/content/accessibility.js"); +Cu.import("chrome://marionette/content/addon.js"); +Cu.import("chrome://marionette/content/assert.js"); +Cu.import("chrome://marionette/content/atom.js"); +Cu.import("chrome://marionette/content/browser.js"); +Cu.import("chrome://marionette/content/capture.js"); +Cu.import("chrome://marionette/content/cert.js"); +Cu.import("chrome://marionette/content/element.js"); +Cu.import("chrome://marionette/content/error.js"); +Cu.import("chrome://marionette/content/evaluate.js"); +Cu.import("chrome://marionette/content/event.js"); +Cu.import("chrome://marionette/content/interaction.js"); +Cu.import("chrome://marionette/content/l10n.js"); +Cu.import("chrome://marionette/content/legacyaction.js"); +Cu.import("chrome://marionette/content/logging.js"); +Cu.import("chrome://marionette/content/modal.js"); +Cu.import("chrome://marionette/content/proxy.js"); +Cu.import("chrome://marionette/content/session.js"); +Cu.import("chrome://marionette/content/simpletest.js"); + +this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"]; + +var FRAME_SCRIPT = "chrome://marionette/content/listener.js"; +const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished"; +const CLICK_TO_START_PREF = "marionette.debugging.clicktostart"; +const CONTENT_LISTENER_PREF = "marionette.contentListener"; + +const SUPPORTED_STRATEGIES = new Set([ + element.Strategy.ClassName, + element.Strategy.Selector, + element.Strategy.ID, + element.Strategy.TagName, + element.Strategy.XPath, + element.Strategy.Anon, + element.Strategy.AnonAttribute, +]); + +const logger = Log.repository.getLogger("Marionette"); +const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageBroadcaster); + +// This is used to prevent newSession from returning before the telephony +// API's are ready; see bug 792647. This assumes that marionette-server.js +// will be loaded before the 'system-message-listener-ready' message +// is fired. If this stops being true, this approach will have to change. +var systemMessageListenerReady = false; +Services.obs.addObserver(function() { + systemMessageListenerReady = true; +}, "system-message-listener-ready", false); + +// This is used on desktop to prevent newSession from returning before a page +// load initiated by the Firefox command line has completed. +var delayedBrowserStarted = false; +Services.obs.addObserver(function () { + delayedBrowserStarted = true; +}, BROWSER_STARTUP_FINISHED, false); + +this.Context = { + CHROME: "chrome", + CONTENT: "content", +}; + +this.Context.fromString = function (s) { + s = s.toUpperCase(); + if (s in this) { + return this[s]; + } + return null; +}; + +/** + * Implements (parts of) the W3C WebDriver protocol. GeckoDriver lives + * in chrome space and mediates calls to the message listener of the current + * browsing context's content frame message listener via ListenerProxy. + * + * Throughout this prototype, functions with the argument {@code cmd}'s + * documentation refers to the contents of the {@code cmd.parameters} + * object. + * + * @param {string} appName + * Description of the product, for example "B2G" or "Firefox". + * @param {MarionetteServer} server + * The instance of Marionette server. + */ +this.GeckoDriver = function (appName, server) { + this.appName = appName; + this._server = server; + + this.sessionId = null; + this.wins = new browser.Windows(); + this.browsers = {}; + // points to current browser + this.curBrowser = null; + // topmost chrome frame + this.mainFrame = null; + // chrome iframe that currently has focus + this.curFrame = null; + this.mainContentFrameId = null; + this.mozBrowserClose = null; + this.currentFrameElement = null; + // frame ID of the current remote frame, used for mozbrowserclose events + this.oopFrameId = null; + this.observing = null; + this._browserIds = new WeakMap(); + + // The curent context decides if commands should affect chrome- or + // content space. + this.context = Context.CONTENT; + + this.importedScripts = new evaluate.ScriptStorageService( + [Context.CHROME, Context.CONTENT]); + this.sandboxes = new Sandboxes(() => this.getCurrentWindow()); + this.legacyactions = new legacyaction.Chain(); + + this.timer = null; + this.inactivityTimer = null; + + this.marionetteLog = new logging.ContentLogger(); + this.testName = null; + + this.capabilities = new session.Capabilities(); + + this.mm = globalMessageManager; + this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this)); + + // always keep weak reference to current dialogue + this.dialog = null; + let handleDialog = (subject, topic) => { + let winr; + if (topic == modal.COMMON_DIALOG_LOADED) { + winr = Cu.getWeakReference(subject); + } + this.dialog = new modal.Dialog(() => this.curBrowser, winr); + }; + modal.addHandler(handleDialog); +}; + +Object.defineProperty(GeckoDriver.prototype, "a11yChecks", { + get: function () { + return this.capabilities.get("moz:accessibilityChecks"); + } +}); + +Object.defineProperty(GeckoDriver.prototype, "proxy", { + get: function () { + return this.capabilities.get("proxy"); + } +}); + +Object.defineProperty(GeckoDriver.prototype, "secureTLS", { + get: function () { + return !this.capabilities.get("acceptInsecureCerts"); + } +}); + +Object.defineProperty(GeckoDriver.prototype, "timeouts", { + get: function () { + return this.capabilities.get("timeouts"); + }, + + set: function (newTimeouts) { + this.capabilities.set("timeouts", newTimeouts); + }, +}); + +Object.defineProperty(GeckoDriver.prototype, "windowHandles", { + get: function () { + let hs = []; + let winEn = Services.wm.getEnumerator(null); + + while (winEn.hasMoreElements()) { + let win = winEn.getNext(); + let tabBrowser = browser.getTabBrowser(win); + + if (tabBrowser) { + tabBrowser.tabs.forEach(tab => { + let winId = this.getIdForBrowser(browser.getBrowserForTab(tab)); + if (winId !== null) { + hs.push(winId); + } + }); + } else { + // For other chrome windows beside the browser window, only add the window itself. + hs.push(getOuterWindowId(win)); + } + } + + return hs; + }, +}); + +Object.defineProperty(GeckoDriver.prototype, "chromeWindowHandles", { + get : function () { + let hs = []; + let winEn = Services.wm.getEnumerator(null); + + while (winEn.hasMoreElements()) { + hs.push(getOuterWindowId(winEn.getNext())); + } + + return hs; + }, +}); + +GeckoDriver.prototype.QueryInterface = XPCOMUtils.generateQI([ + Ci.nsIMessageListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference, +]); + +/** + * Switches to the global ChromeMessageBroadcaster, potentially replacing + * a frame-specific ChromeMessageSender. Has no effect if the global + * ChromeMessageBroadcaster is already in use. If this replaces a + * frame-specific ChromeMessageSender, it removes the message listeners + * from that sender, and then puts the corresponding frame script "to + * sleep", which removes most of the message listeners from it as well. + */ +GeckoDriver.prototype.switchToGlobalMessageManager = function() { + if (this.curBrowser && this.curBrowser.frameManager.currentRemoteFrame !== null) { + this.curBrowser.frameManager.removeMessageManagerListeners(this.mm); + this.sendAsync("sleepSession"); + this.curBrowser.frameManager.currentRemoteFrame = null; + } + this.mm = globalMessageManager; +}; + +/** + * Helper method to send async messages to the content listener. + * Correct usage is to pass in the name of a function in listener.js, + * a serialisable object, and optionally the current command's ID + * when not using the modern dispatching technique. + * + * @param {string} name + * Suffix of the targetted message listener + * ({@code Marionette:<suffix>}). + * @param {Object=} msg + * Optional JSON serialisable object to send to the listener. + * @param {number=} commandID + * Optional command ID to ensure synchronisity. + */ +GeckoDriver.prototype.sendAsync = function (name, data, commandID) { + name = "Marionette:" + name; + let payload = copy(data); + + // TODO(ato): When proxy.AsyncMessageChannel + // is used for all chrome <-> content communication + // this can be removed. + if (commandID) { + payload.command_id = commandID; + } + + if (!this.curBrowser.frameManager.currentRemoteFrame) { + this.broadcastDelayedAsyncMessage_(name, payload); + } else { + this.sendTargettedAsyncMessage_(name, payload); + } +}; + +GeckoDriver.prototype.broadcastDelayedAsyncMessage_ = function (name, payload) { + this.curBrowser.executeWhenReady(() => { + if (this.curBrowser.curFrameId) { + const target = name + this.curBrowser.curFrameId; + this.mm.broadcastAsyncMessage(target, payload); + } else { + throw new NoSuchWindowError( + "No such content frame; perhaps the listener was not registered?"); + } + }); +}; + +GeckoDriver.prototype.sendTargettedAsyncMessage_ = function (name, payload) { + const curRemoteFrame = this.curBrowser.frameManager.currentRemoteFrame; + const target = name + curRemoteFrame.targetFrameId; + + try { + this.mm.sendAsyncMessage(target, payload); + } catch (e) { + switch (e.result) { + case Cr.NS_ERROR_FAILURE: + case Cr.NS_ERROR_NOT_INITIALIZED: + throw new NoSuchWindowError(); + + default: + throw new WebDriverError(e); + } + } +}; + +/** + * Gets the current active window. + * + * @return {nsIDOMWindow} + */ +GeckoDriver.prototype.getCurrentWindow = function() { + let typ = null; + if (this.curFrame === null) { + if (this.curBrowser === null) { + if (this.context == Context.CONTENT) { + typ = "navigator:browser"; + } + return Services.wm.getMostRecentWindow(typ); + } else { + return this.curBrowser.window; + } + } else { + return this.curFrame; + } +}; + +GeckoDriver.prototype.addFrameCloseListener = function (action) { + let win = this.getCurrentWindow(); + this.mozBrowserClose = e => { + if (e.target.id == this.oopFrameId) { + win.removeEventListener("mozbrowserclose", this.mozBrowserClose, true); + this.switchToGlobalMessageManager(); + throw new NoSuchWindowError("The window closed during action: " + action); + } + }; + win.addEventListener("mozbrowserclose", this.mozBrowserClose, true); +}; + +/** + * Create a new browsing context for window and add to known browsers. + * + * @param {nsIDOMWindow} win + * Window for which we will create a browsing context. + * + * @return {string} + * Returns the unique server-assigned ID of the window. + */ +GeckoDriver.prototype.addBrowser = function (win) { + let bc = new browser.Context(win, this); + let winId = getOuterWindowId(win); + + this.browsers[winId] = bc; + this.curBrowser = this.browsers[winId]; + if (!this.wins.has(winId)) { + // add this to seenItems so we can guarantee + // the user will get winId as this window's id + this.wins.set(winId, win); + } +}; + +/** + * Registers a new browser, win, with Marionette. + * + * If we have not seen the browser content window before, the listener + * frame script will be loaded into it. If isNewSession is true, we will + * switch focus to the start frame when it registers. + * + * @param {nsIDOMWindow} win + * Window whose browser we need to access. + * @param {boolean=false} isNewSession + * True if this is the first time we're talking to this browser. + */ +GeckoDriver.prototype.startBrowser = function (win, isNewSession = false) { + this.mainFrame = win; + this.curFrame = null; + this.addBrowser(win); + this.curBrowser.isNewSession = isNewSession; + this.curBrowser.startSession(isNewSession, win, this.whenBrowserStarted.bind(this)); +}; + +/** + * Callback invoked after a new session has been started in a browser. + * Loads the Marionette frame script into the browser if needed. + * + * @param {nsIDOMWindow} win + * Window whose browser we need to access. + * @param {boolean} isNewSession + * True if this is the first time we're talking to this browser. + */ +GeckoDriver.prototype.whenBrowserStarted = function (win, isNewSession) { + let mm = win.window.messageManager; + if (mm) { + if (!isNewSession) { + // Loading the frame script corresponds to a situation we need to + // return to the server. If the messageManager is a message broadcaster + // with no children, we don't have a hope of coming back from this call, + // so send the ack here. Otherwise, make a note of how many child scripts + // will be loaded so we known when it's safe to return. + // Child managers may not have child scripts yet (e.g. socialapi), only + // count child managers that have children, but only count the top level + // children as they are the ones that we expect a response from. + if (mm.childCount !== 0) { + this.curBrowser.frameRegsPending = 0; + for (let i = 0; i < mm.childCount; i++) { + if (mm.getChildAt(i).childCount !== 0) { + this.curBrowser.frameRegsPending += 1; + } + } + } + } + + if (!Preferences.get(CONTENT_LISTENER_PREF) || !isNewSession) { + // load listener into the remote frame + // and any applicable new frames + // opened after this call + mm.loadFrameScript(FRAME_SCRIPT, true); + Preferences.set(CONTENT_LISTENER_PREF, true); + } + } else { + logger.error( + `Could not load listener into content for page ${win.location.href}`); + } +}; + +/** + * Recursively get all labeled text. + * + * @param {nsIDOMElement} el + * The parent element. + * @param {Array.<string>} lines + * Array that holds the text lines. + */ +GeckoDriver.prototype.getVisibleText = function (el, lines) { + try { + if (atom.isElementDisplayed(el, this.getCurrentWindow())) { + if (el.value) { + lines.push(el.value); + } + for (let child in el.childNodes) { + this.getVisibleText(el.childNodes[child], lines); + } + } + } catch (e) { + if (el.nodeName == "#text") { + lines.push(el.textContent); + } + } +}; + +/** + * Handles registration of new content listener browsers. Depending on + * their type they are either accepted or ignored. + */ +GeckoDriver.prototype.registerBrowser = function (id, be) { + let nullPrevious = this.curBrowser.curFrameId === null; + let listenerWindow = Services.wm.getOuterWindowWithId(id); + + // go in here if we're already in a remote frame + if (this.curBrowser.frameManager.currentRemoteFrame !== null && + (!listenerWindow || this.mm == this.curBrowser.frameManager + .currentRemoteFrame.messageManager.get())) { + // The outerWindowID from an OOP frame will not be meaningful to + // the parent process here, since each process maintains its own + // independent window list. So, it will either be null (!listenerWindow) + // if we're already in a remote frame, or it will point to some + // random window, which will hopefully cause an href mismatch. + // Currently this only happens in B2G for OOP frames registered in + // Marionette:switchToFrame, so we'll acknowledge the switchToFrame + // message here. + // + // TODO: Should have a better way of determining that this message + // is from a remote frame. + this.curBrowser.frameManager.currentRemoteFrame.targetFrameId = + this.generateFrameId(id); + } + + let reg = {}; + // this will be sent to tell the content process if it is the main content + let mainContent = this.curBrowser.mainContentId === null; + if (be.getAttribute("type") != "content") { + // curBrowser holds all the registered frames in knownFrames + let uid = this.generateFrameId(id); + reg.id = uid; + reg.remotenessChange = this.curBrowser.register(uid, be); + } + + // set to true if we updated mainContentId + mainContent = mainContent && this.curBrowser.mainContentId !== null; + if (mainContent) { + this.mainContentFrameId = this.curBrowser.curFrameId; + } + + this.wins.set(reg.id, listenerWindow); + if (nullPrevious && (this.curBrowser.curFrameId !== null)) { + this.sendAsync( + "newSession", + this.capabilities.toJSON(), + this.newSessionCommandId); + if (this.curBrowser.isNewSession) { + this.newSessionCommandId = null; + } + } + + return [reg, mainContent, this.capabilities.toJSON()]; +}; + +GeckoDriver.prototype.registerPromise = function () { + const li = "Marionette:register"; + + return new Promise(resolve => { + let cb = msg => { + let wid = msg.json.value; + let be = msg.target; + let rv = this.registerBrowser(wid, be); + + if (this.curBrowser.frameRegsPending > 0) { + this.curBrowser.frameRegsPending--; + } + + if (this.curBrowser.frameRegsPending === 0) { + this.mm.removeMessageListener(li, cb); + resolve(); + } + + // this is a sync message and listeners expect the ID back + return rv; + }; + this.mm.addMessageListener(li, cb); + }); +}; + +GeckoDriver.prototype.listeningPromise = function () { + const li = "Marionette:listenersAttached"; + return new Promise(resolve => { + let cb = () => { + this.mm.removeMessageListener(li, cb); + resolve(); + }; + this.mm.addMessageListener(li, cb); + }); +}; + +/** Create a new session. */ +GeckoDriver.prototype.newSession = function* (cmd, resp) { + if (this.sessionId) { + throw new SessionNotCreatedError("Maximum number of active sessions"); + } + + this.sessionId = cmd.parameters.sessionId || + cmd.parameters.session_id || + element.generateUUID(); + this.newSessionCommandId = cmd.id; + + try { + this.capabilities = session.Capabilities.fromJSON( + cmd.parameters.capabilities, {merge: true}); + logger.config("Matched capabilities: " + + JSON.stringify(this.capabilities)); + } catch (e) { + throw new SessionNotCreatedError(e); + } + + if (!this.secureTLS) { + logger.warn("TLS certificate errors will be ignored for this session"); + let acceptAllCerts = new cert.InsecureSweepingOverride(); + cert.installOverride(acceptAllCerts); + } + + if (this.proxy.init()) { + logger.info("Proxy settings initialised: " + JSON.stringify(this.proxy)); + } + + // If we are testing accessibility with marionette, start a11y service in + // chrome first. This will ensure that we do not have any content-only + // services hanging around. + if (this.a11yChecks && accessibility.service) { + logger.info("Preemptively starting accessibility service in Chrome"); + } + + let registerBrowsers = this.registerPromise(); + let browserListening = this.listeningPromise(); + + let waitForWindow = function() { + let win = this.getCurrentWindow(); + if (!win) { + // if the window isn't even created, just poll wait for it + let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + checkTimer.initWithCallback(waitForWindow.bind(this), 100, + Ci.nsITimer.TYPE_ONE_SHOT); + } else if (win.document.readyState != "complete") { + // otherwise, wait for it to be fully loaded before proceeding + let listener = ev => { + // ensure that we proceed, on the top level document load event + // (not an iframe one...) + if (ev.target != win.document) { + return; + } + win.removeEventListener("load", listener); + waitForWindow.call(this); + }; + win.addEventListener("load", listener, true); + } else { + let clickToStart = Preferences.get(CLICK_TO_START_PREF); + if (clickToStart && (this.appName != "B2G")) { + let pService = Cc["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Ci.nsIPromptService); + pService.alert(win, "", "Click to start execution of marionette tests"); + } + this.startBrowser(win, true); + } + }; + + let runSessionStart = function() { + if (!Preferences.get(CONTENT_LISTENER_PREF)) { + waitForWindow.call(this); + } else if (this.appName != "Firefox" && this.curBrowser === null) { + // if there is a content listener, then we just wake it up + this.addBrowser(this.getCurrentWindow()); + this.curBrowser.startSession(this.whenBrowserStarted.bind(this)); + this.mm.broadcastAsyncMessage("Marionette:restart", {}); + } else { + throw new WebDriverError("Session already running"); + } + this.switchToGlobalMessageManager(); + }; + + if (!delayedBrowserStarted && this.appName != "B2G") { + let self = this; + Services.obs.addObserver(function onStart() { + Services.obs.removeObserver(onStart, BROWSER_STARTUP_FINISHED); + runSessionStart.call(self); + }, BROWSER_STARTUP_FINISHED, false); + } else { + runSessionStart.call(this); + } + + yield registerBrowsers; + yield browserListening; + + if (this.curBrowser.tab) { + browser.getBrowserForTab(this.curBrowser.tab).focus(); + } + + return { + sessionId: this.sessionId, + capabilities: this.capabilities, + }; +}; + +/** + * Send the current session's capabilities to the client. + * + * Capabilities informs the client of which WebDriver features are + * supported by Firefox and Marionette. They are immutable for the + * length of the session. + * + * The return value is an immutable map of string keys + * ("capabilities") to values, which may be of types boolean, + * numerical or string. + */ +GeckoDriver.prototype.getSessionCapabilities = function (cmd, resp) { + resp.body.capabilities = this.capabilities; +}; + +/** + * Log message. Accepts user defined log-level. + * + * @param {string} value + * Log message. + * @param {string} level + * Arbitrary log level. + */ +GeckoDriver.prototype.log = function (cmd, resp) { + // if level is null, we want to use ContentLogger#send's default + this.marionetteLog.log( + cmd.parameters.value, + cmd.parameters.level || undefined); +}; + +/** Return all logged messages. */ +GeckoDriver.prototype.getLogs = function (cmd, resp) { + resp.body = this.marionetteLog.get(); +}; + +/** + * Sets the context of the subsequent commands to be either "chrome" or + * "content". + * + * @param {string} value + * Name of the context to be switched to. Must be one of "chrome" or + * "content". + */ +GeckoDriver.prototype.setContext = function (cmd, resp) { + let val = cmd.parameters.value; + let ctx = Context.fromString(val); + if (ctx === null) { + throw new WebDriverError(`Invalid context: ${val}`); + } + this.context = ctx; +}; + +/** Gets the context of the server, either "chrome" or "content". */ +GeckoDriver.prototype.getContext = function (cmd, resp) { + resp.body.value = this.context.toString(); +}; + +/** + * Executes a JavaScript function in the context of the current browsing + * context, if in content space, or in chrome space otherwise, and returns + * the return value of the function. + * + * It is important to note that if the {@code sandboxName} parameter + * is left undefined, the script will be evaluated in a mutable sandbox, + * causing any change it makes on the global state of the document to have + * lasting side-effects. + * + * @param {string} script + * Script to evaluate as a function body. + * @param {Array.<(string|boolean|number|object|WebElement)>} args + * Arguments exposed to the script in {@code arguments}. The array + * items must be serialisable to the WebDriver protocol. + * @param {number} scriptTimeout + * Duration in milliseconds of when to interrupt and abort the + * script evaluation. + * @param {string=} sandbox + * Name of the sandbox to evaluate the script in. The sandbox is + * cached for later re-use on the same Window object if + * {@code newSandbox} is false. If he parameter is undefined, + * the script is evaluated in a mutable sandbox. If the parameter + * is "system", it will be evaluted in a sandbox with elevated system + * privileges, equivalent to chrome space. + * @param {boolean=} newSandbox + * Forces the script to be evaluated in a fresh sandbox. Note that if + * it is undefined, the script will normally be evaluted in a fresh + * sandbox. + * @param {string=} filename + * Filename of the client's program where this script is evaluated. + * @param {number=} line + * Line in the client's program where this script is evaluated. + * @param {boolean=} debug_script + * Attach an {@code onerror} event handler on the Window object. + * It does not differentiate content errors from chrome errors. + * @param {boolean=} directInject + * Evaluate the script without wrapping it in a function. + * + * @return {(string|boolean|number|object|WebElement)} + * Return value from the script, or null which signifies either the + * JavaScript notion of null or undefined. + * + * @throws ScriptTimeoutError + * If the script was interrupted due to reaching the {@code + * scriptTimeout} or default timeout. + * @throws JavaScriptError + * If an Error was thrown whilst evaluating the script. + */ +GeckoDriver.prototype.executeScript = function*(cmd, resp) { + let {script, args, scriptTimeout} = cmd.parameters; + scriptTimeout = scriptTimeout || this.timeouts.script; + + let opts = { + sandboxName: cmd.parameters.sandbox, + newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") || + cmd.parameters.newSandbox, + filename: cmd.parameters.filename, + line: cmd.parameters.line, + debug: cmd.parameters.debug_script, + }; + + resp.body.value = yield this.execute_(script, args, scriptTimeout, opts); +}; + +/** + * Executes a JavaScript function in the context of the current browsing + * context, if in content space, or in chrome space otherwise, and returns + * the object passed to the callback. + * + * The callback is always the last argument to the {@code arguments} + * list passed to the function scope of the script. It can be retrieved + * as such: + * + * let callback = arguments[arguments.length - 1]; + * callback("foo"); + * // "foo" is returned + * + * It is important to note that if the {@code sandboxName} parameter + * is left undefined, the script will be evaluated in a mutable sandbox, + * causing any change it makes on the global state of the document to have + * lasting side-effects. + * + * @param {string} script + * Script to evaluate as a function body. + * @param {Array.<(string|boolean|number|object|WebElement)>} args + * Arguments exposed to the script in {@code arguments}. The array + * items must be serialisable to the WebDriver protocol. + * @param {number} scriptTimeout + * Duration in milliseconds of when to interrupt and abort the + * script evaluation. + * @param {string=} sandbox + * Name of the sandbox to evaluate the script in. The sandbox is + * cached for later re-use on the same Window object if + * {@code newSandbox} is false. If the parameter is undefined, + * the script is evaluated in a mutable sandbox. If the parameter + * is "system", it will be evaluted in a sandbox with elevated system + * privileges, equivalent to chrome space. + * @param {boolean=} newSandbox + * Forces the script to be evaluated in a fresh sandbox. Note that if + * it is undefined, the script will normally be evaluted in a fresh + * sandbox. + * @param {string=} filename + * Filename of the client's program where this script is evaluated. + * @param {number=} line + * Line in the client's program where this script is evaluated. + * @param {boolean=} debug_script + * Attach an {@code onerror} event handler on the Window object. + * It does not differentiate content errors from chrome errors. + * @param {boolean=} directInject + * Evaluate the script without wrapping it in a function. + * + * @return {(string|boolean|number|object|WebElement)} + * Return value from the script, or null which signifies either the + * JavaScript notion of null or undefined. + * + * @throws ScriptTimeoutError + * If the script was interrupted due to reaching the {@code + * scriptTimeout} or default timeout. + * @throws JavaScriptError + * If an Error was thrown whilst evaluating the script. + */ +GeckoDriver.prototype.executeAsyncScript = function* (cmd, resp) { + let {script, args, scriptTimeout} = cmd.parameters; + scriptTimeout = scriptTimeout || this.timeouts.script; + + let opts = { + sandboxName: cmd.parameters.sandbox, + newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") || + cmd.parameters.newSandbox, + filename: cmd.parameters.filename, + line: cmd.parameters.line, + debug: cmd.parameters.debug_script, + async: true, + }; + + resp.body.value = yield this.execute_(script, args, scriptTimeout, opts); +}; + +GeckoDriver.prototype.execute_ = function (script, args, timeout, opts = {}) { + switch (this.context) { + case Context.CONTENT: + // evaluate in content with lasting side-effects + if (!opts.sandboxName) { + return this.listener.execute(script, args, timeout, opts); + + // evaluate in content with sandbox + } else { + return this.listener.executeInSandbox(script, args, timeout, opts); + } + + case Context.CHROME: + let sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox); + if (opts.sandboxName) { + sb = sandbox.augment(sb, new logging.Adapter(this.marionetteLog)); + sb = sandbox.augment(sb, {global: sb}); + } + + opts.timeout = timeout; + script = this.importedScripts.for(Context.CHROME).concat(script); + let wargs = element.fromJson(args, this.curBrowser.seenEls, sb.window); + let evaluatePromise = evaluate.sandbox(sb, script, wargs, opts); + return evaluatePromise.then(res => element.toJson(res, this.curBrowser.seenEls)); + } +}; + +/** + * Execute pure JavaScript. Used to execute simpletest harness tests, + * which are like mochitests only injected using Marionette. + * + * Scripts are expected to call the {@code finish} global when done. + */ +GeckoDriver.prototype.executeJSScript = function* (cmd, resp) { + let {script, args, scriptTimeout} = cmd.parameters; + scriptTimeout = scriptTimeout || this.timeouts.script; + + let opts = { + filename: cmd.parameters.filename, + line: cmd.parameters.line, + async: cmd.parameters.async, + }; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let wargs = element.fromJson(args, this.curBrowser.seenEls, win); + let harness = new simpletest.Harness( + win, + Context.CHROME, + this.marionetteLog, + scriptTimeout, + function() {}, + this.testName); + + let sb = sandbox.createSimpleTest(win, harness); + // TODO(ato): Not sure this is needed: + sb = sandbox.augment(sb, new logging.Adapter(this.marionetteLog)); + + let res = yield evaluate.sandbox(sb, script, wargs, opts); + resp.body.value = element.toJson(res, this.curBrowser.seenEls); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.executeSimpleTest(script, args, scriptTimeout, opts); + break; + } +}; + +/** + * Navigate to given URL. + * + * Navigates the current browsing context to the given URL and waits for + * the document to load or the session's page timeout duration to elapse + * before returning. + * + * The command will return with a failure if there is an error loading + * the document or the URL is blocked. This can occur if it fails to + * reach host, the URL is malformed, or if there is a certificate issue + * to name some examples. + * + * The document is considered successfully loaded when the + * DOMContentLoaded event on the frame element associated with the + * current window triggers and document.readyState is "complete". + * + * In chrome context it will change the current window's location to + * the supplied URL and wait until document.readyState equals "complete" + * or the page timeout duration has elapsed. + * + * @param {string} url + * URL to navigate to. + */ +GeckoDriver.prototype.get = function*(cmd, resp) { + assert.content(this.context); + + let url = cmd.parameters.url; + + let get = this.listener.get({url: url, pageTimeout: this.timeouts.pageLoad}); + + // If a remoteness update interrupts our page load, this will never return + // We need to re-issue this request to correctly poll for readyState and + // send errors. + this.curBrowser.pendingCommands.push(() => { + let parameters = { + // TODO(ato): Bug 1242595 + command_id: this.listener.activeMessageId, + pageTimeout: this.timeouts.pageLoad, + startTime: new Date().getTime(), + }; + this.mm.broadcastAsyncMessage( + "Marionette:pollForReadyState" + this.curBrowser.curFrameId, + parameters); + }); + + yield get; + browser.getBrowserForTab(this.curBrowser.tab).focus(); +}; + +/** + * Get a string representing the current URL. + * + * On Desktop this returns a string representation of the URL of the + * current top level browsing context. This is equivalent to + * document.location.href. + * + * When in the context of the chrome, this returns the canonical URL + * of the current resource. + */ +GeckoDriver.prototype.getCurrentUrl = function (cmd) { + switch (this.context) { + case Context.CHROME: + return this.getCurrentWindow().location.href; + + case Context.CONTENT: + let isB2G = this.appName == "B2G"; + return this.listener.getCurrentUrl(isB2G); + } +}; + +/** Gets the current title of the window. */ +GeckoDriver.prototype.getTitle = function* (cmd, resp) { + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + resp.body.value = win.document.documentElement.getAttribute("title"); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.getTitle(); + break; + } +}; + +/** Gets the current type of the window. */ +GeckoDriver.prototype.getWindowType = function (cmd, resp) { + let win = this.getCurrentWindow(); + resp.body.value = win.document.documentElement.getAttribute("windowtype"); +}; + +/** Gets the page source of the content document. */ +GeckoDriver.prototype.getPageSource = function* (cmd, resp) { + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let s = new win.XMLSerializer(); + resp.body.value = s.serializeToString(win.document); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.getPageSource(); + break; + } +}; + +/** + * Cause the browser to traverse one step backward in the joint history + * of the current browsing context. + */ +GeckoDriver.prototype.goBack = function* (cmd, resp) { + assert.content(this.context); + + if (!this.curBrowser.tab) { + // Navigation does not work for non-browser windows + return; + } + + let contentBrowser = browser.getBrowserForTab(this.curBrowser.tab) + if (!contentBrowser.webNavigation.canGoBack) { + return; + } + + let currentURL = yield this.listener.getCurrentUrl(); + let goBack = this.listener.goBack({pageTimeout: this.timeouts.pageLoad}); + + // If a remoteness update interrupts our page load, this will never return + // We need to re-issue this request to correctly poll for readyState and + // send errors. + this.curBrowser.pendingCommands.push(() => { + let parameters = { + // TODO(ato): Bug 1242595 + command_id: this.listener.activeMessageId, + lastSeenURL: currentURL, + pageTimeout: this.timeouts.pageLoad, + startTime: new Date().getTime(), + }; + this.mm.broadcastAsyncMessage( + // TODO: combine with + // "Marionette:pollForReadyState" + this.curBrowser.curFrameId, + "Marionette:pollForReadyState" + this.curBrowser.curFrameId, + parameters); + }); + + yield goBack; +}; + +/** + * Cause the browser to traverse one step forward in the joint history + * of the current browsing context. + */ +GeckoDriver.prototype.goForward = function* (cmd, resp) { + assert.content(this.context); + + if (!this.curBrowser.tab) { + // Navigation does not work for non-browser windows + return; + } + + let contentBrowser = browser.getBrowserForTab(this.curBrowser.tab) + if (!contentBrowser.webNavigation.canGoForward) { + return; + } + + let currentURL = yield this.listener.getCurrentUrl(); + let goForward = this.listener.goForward({pageTimeout: this.timeouts.pageLoad}); + + // If a remoteness update interrupts our page load, this will never return + // We need to re-issue this request to correctly poll for readyState and + // send errors. + this.curBrowser.pendingCommands.push(() => { + let parameters = { + // TODO(ato): Bug 1242595 + command_id: this.listener.activeMessageId, + lastSeenURL: currentURL, + pageTimeout: this.timeouts.pageLoad, + startTime: new Date().getTime(), + }; + this.mm.broadcastAsyncMessage( + // TODO: combine with + // "Marionette:pollForReadyState" + this.curBrowser.curFrameId, + "Marionette:pollForReadyState" + this.curBrowser.curFrameId, + parameters); + }); + + yield goForward; +}; + +/** Refresh the page. */ +GeckoDriver.prototype.refresh = function*(cmd, resp) { + assert.content(this.context); + + yield this.listener.refresh(); +}; + +/** + * Forces an update for the given browser's id. + */ +GeckoDriver.prototype.updateIdForBrowser = function (browser, newId) { + this._browserIds.set(browser.permanentKey, newId); +}; + +/** + * Retrieves a listener id for the given xul browser element. In case + * the browser is not known, an attempt is made to retrieve the id from + * a CPOW, and null is returned if this fails. + */ +GeckoDriver.prototype.getIdForBrowser = function (browser) { + if (browser === null) { + return null; + } + let permKey = browser.permanentKey; + if (this._browserIds.has(permKey)) { + return this._browserIds.get(permKey); + } + + let winId = browser.outerWindowID; + if (winId) { + winId = winId.toString(); + this._browserIds.set(permKey, winId); + return winId; + } + return null; +}, + +/** + * Get the current window's handle. On desktop this typically corresponds + * to the currently selected tab. + * + * Return an opaque server-assigned identifier to this window that + * uniquely identifies it within this Marionette instance. This can + * be used to switch to this window at a later point. + * + * @return {string} + * Unique window handle. + */ +GeckoDriver.prototype.getWindowHandle = function (cmd, resp) { + // curFrameId always holds the current tab. + if (this.curBrowser.curFrameId) { + resp.body.value = this.curBrowser.curFrameId; + return; + } + + for (let i in this.browsers) { + if (this.curBrowser == this.browsers[i]) { + resp.body.value = i; + return; + } + } +}; + +/** + * Get a list of top-level browsing contexts. On desktop this typically + * corresponds to the set of open tabs for browser windows, or the window itself + * for non-browser chrome windows. + * + * Each window handle is assigned by the server and is guaranteed unique, + * however the return array does not have a specified ordering. + * + * @return {Array.<string>} + * Unique window handles. + */ +GeckoDriver.prototype.getWindowHandles = function (cmd, resp) { + return this.windowHandles; +} + +/** + * Get the current window's handle. This corresponds to a window that + * may itself contain tabs. + * + * Return an opaque server-assigned identifier to this window that + * uniquely identifies it within this Marionette instance. This can + * be used to switch to this window at a later point. + * + * @return {string} + * Unique window handle. + */ +GeckoDriver.prototype.getChromeWindowHandle = function (cmd, resp) { + for (let i in this.browsers) { + if (this.curBrowser == this.browsers[i]) { + resp.body.value = i; + return; + } + } +}; + +/** + * Returns identifiers for each open chrome window for tests interested in + * managing a set of chrome windows and tabs separately. + * + * @return {Array.<string>} + * Unique window handles. + */ +GeckoDriver.prototype.getChromeWindowHandles = function (cmd, resp) { + return this.chromeWindowHandles; +} + +/** + * Get the current window position. + * + * @return {Object.<string, number>} + * Object with |x| and |y| coordinates. + */ +GeckoDriver.prototype.getWindowPosition = function (cmd, resp) { + return this.curBrowser.position; +}; + +/** + * Set the window position of the browser on the OS Window Manager + * + * @param {number} x + * X coordinate of the top/left of the window that it will be + * moved to. + * @param {number} y + * Y coordinate of the top/left of the window that it will be + * moved to. + * + * @return {Object.<string, number>} + * Object with |x| and |y| coordinates. + */ +GeckoDriver.prototype.setWindowPosition = function (cmd, resp) { + assert.firefox() + + let {x, y} = cmd.parameters; + assert.positiveInteger(x); + assert.positiveInteger(y); + + let win = this.getCurrentWindow(); + win.moveTo(x, y); + + return this.curBrowser.position; +}; + +/** + * Switch current top-level browsing context by name or server-assigned ID. + * Searches for windows by name, then ID. Content windows take precedence. + * + * @param {string} name + * Target name or ID of the window to switch to. + * @param {boolean=} focus + * A boolean value which determines whether to focus + * the window. Defaults to true. + */ +GeckoDriver.prototype.switchToWindow = function* (cmd, resp) { + let switchTo = cmd.parameters.name; + let focus = (cmd.parameters.focus !== undefined) ? cmd.parameters.focus : true; + let found; + + let byNameOrId = function (name, windowId) { + return switchTo === name || switchTo === windowId; + }; + + let winEn = Services.wm.getEnumerator(null); + while (winEn.hasMoreElements()) { + let win = winEn.getNext(); + let outerId = getOuterWindowId(win); + let tabBrowser = browser.getTabBrowser(win); + + if (byNameOrId(win.name, outerId)) { + // In case the wanted window is a chrome window, we are done. + found = {win: win, outerId: outerId, hasTabBrowser: !!tabBrowser}; + break; + + } else if (tabBrowser) { + // Otherwise check if the chrome window has a tab browser, and that it + // contains a tab with the wanted window handle. + for (let i = 0; i < tabBrowser.tabs.length; ++i) { + let contentBrowser = browser.getBrowserForTab(tabBrowser.tabs[i]); + let contentWindowId = this.getIdForBrowser(contentBrowser); + + if (byNameOrId(win.name, contentWindowId)) { + found = { + win: win, + outerId: outerId, + hasTabBrowser: true, + tabIndex: i, + }; + break; + } + } + } + } + + if (found) { + if (!(found.outerId in this.browsers)) { + // Initialise Marionette if the current chrome window has not been seen + // before. Also register the initial tab, if one exists. + let registerBrowsers, browserListening; + + if (found.hasTabBrowser) { + registerBrowsers = this.registerPromise(); + browserListening = this.listeningPromise(); + } + + this.startBrowser(found.win, false /* isNewSession */); + + if (registerBrowsers && browserListening) { + yield registerBrowsers; + yield browserListening; + } + + } else { + // Otherwise switch to the known chrome window, and activate the tab + // if it's a content browser. + this.curBrowser = this.browsers[found.outerId]; + + if ("tabIndex" in found) { + this.curBrowser.switchToTab(found.tabIndex, found.win, focus); + } + } + } else { + throw new NoSuchWindowError(`Unable to locate window: ${switchTo}`); + } +}; + +GeckoDriver.prototype.getActiveFrame = function (cmd, resp) { + switch (this.context) { + case Context.CHROME: + // no frame means top-level + resp.body.value = null; + if (this.curFrame) { + let elRef = this.curBrowser.seenEls + .add(this.curFrame.frameElement); + let el = element.makeWebElement(elRef); + resp.body.value = el; + } + break; + + case Context.CONTENT: + resp.body.value = null; + if (this.currentFrameElement !== null) { + let el = element.makeWebElement(this.currentFrameElement); + resp.body.value = el; + } + break; + } +}; + +GeckoDriver.prototype.switchToParentFrame = function*(cmd, resp) { + let res = yield this.listener.switchToParentFrame(); +}; + +/** + * Switch to a given frame within the current window. + * + * @param {Object} element + * A web element reference to the element to switch to. + * @param {(string|number)} id + * If element is not defined, then this holds either the id, name, + * or index of the frame to switch to. + */ +GeckoDriver.prototype.switchToFrame = function* (cmd, resp) { + let {id, element, focus} = cmd.parameters; + + const otherErrorsExpr = /about:.+(error)|(blocked)\?/; + const checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let curWindow = this.getCurrentWindow(); + + let checkLoad = function() { + let win = this.getCurrentWindow(); + if (win.document.readyState == "complete") { + return; + } else if (win.document.readyState == "interactive") { + let baseURI = win.document.baseURI; + if (baseURI.startsWith("about:certerror")) { + throw new InsecureCertificateError(); + } else if (otherErrorsExpr.exec(win.document.baseURI)) { + throw new UnknownError("Error loading page"); + } + } + + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + }; + + if (this.context == Context.CHROME) { + let foundFrame = null; + + // just focus + if (typeof id == "undefined" && typeof element == "undefined") { + this.curFrame = null; + if (focus) { + this.mainFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + + // by element + if (this.curBrowser.seenEls.has(element)) { + // HTMLIFrameElement + let wantedFrame = this.curBrowser.seenEls.get(element, {frame: curWindow}); + // Deal with an embedded xul:browser case + if (wantedFrame.tagName == "xul:browser" || wantedFrame.tagName == "browser") { + curWindow = wantedFrame.contentWindow; + this.curFrame = curWindow; + if (focus) { + this.curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + + // Check if the frame is XBL anonymous + let parent = curWindow.document.getBindingParent(wantedFrame); + // Shadow nodes also show up in getAnonymousNodes, we should ignore them. + if (parent && !(parent.shadowRoot && parent.shadowRoot.contains(wantedFrame))) { + let anonNodes = [...curWindow.document.getAnonymousNodes(parent) || []]; + if (anonNodes.length > 0) { + let el = wantedFrame; + while (el) { + if (anonNodes.indexOf(el) > -1) { + curWindow = wantedFrame.contentWindow; + this.curFrame = curWindow; + if (focus) { + this.curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + el = el.parentNode; + } + } + } + + // else, assume iframe + let frames = curWindow.document.getElementsByTagName("iframe"); + let numFrames = frames.length; + for (let i = 0; i < numFrames; i++) { + if (new XPCNativeWrapper(frames[i]) == new XPCNativeWrapper(wantedFrame)) { + curWindow = frames[i].contentWindow; + this.curFrame = curWindow; + if (focus) { + this.curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + } + } + + switch (typeof id) { + case "string" : + let foundById = null; + let frames = curWindow.document.getElementsByTagName("iframe"); + let numFrames = frames.length; + for (let i = 0; i < numFrames; i++) { + //give precedence to name + let frame = frames[i]; + if (frame.getAttribute("name") == id) { + foundFrame = i; + curWindow = frame.contentWindow; + break; + } else if (foundById === null && frame.id == id) { + foundById = i; + } + } + if (foundFrame === null && foundById !== null) { + foundFrame = foundById; + curWindow = frames[foundById].contentWindow; + } + break; + case "number": + if (typeof curWindow.frames[id] != "undefined") { + foundFrame = id; + curWindow = curWindow.frames[foundFrame].frameElement.contentWindow; + } + break; + } + + if (foundFrame !== null) { + this.curFrame = curWindow; + if (focus) { + this.curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + } else { + throw new NoSuchFrameError(`Unable to locate frame: ${id}`); + } + + } else if (this.context == Context.CONTENT) { + if (!id && !element && + this.curBrowser.frameManager.currentRemoteFrame !== null) { + // We're currently using a ChromeMessageSender for a remote frame, so this + // request indicates we need to switch back to the top-level (parent) frame. + // We'll first switch to the parent's (global) ChromeMessageBroadcaster, so + // we send the message to the right listener. + this.switchToGlobalMessageManager(); + } + cmd.command_id = cmd.id; + + let res = yield this.listener.switchToFrame(cmd.parameters); + if (res) { + let {win: winId, frame: frameId} = res; + this.mm = this.curBrowser.frameManager.getFrameMM(winId, frameId); + + let registerBrowsers = this.registerPromise(); + let browserListening = this.listeningPromise(); + + this.oopFrameId = + this.curBrowser.frameManager.switchToFrame(winId, frameId); + + yield registerBrowsers; + yield browserListening; + } + } +}; + +GeckoDriver.prototype.getTimeouts = function (cmd, resp) { + return this.timeouts; +}; + +/** + * Set timeout for page loading, searching, and scripts. + * + * @param {Object.<string, number>} + * Dictionary of timeout types and their new value, where all timeout + * types are optional. + * + * @throws {InvalidArgumentError} + * If timeout type key is unknown, or the value provided with it is + * not an integer. + */ +GeckoDriver.prototype.setTimeouts = function (cmd, resp) { + // backwards compatibility with old API + // that accepted a dictionary {type: <string>, ms: <number>} + let json = {}; + if (typeof cmd.parameters == "object" && + "type" in cmd.parameters && + "ms" in cmd.parameters) { + logger.warn("Using deprecated data structure for setting timeouts"); + json = {[cmd.parameters.type]: parseInt(cmd.parameters.ms)}; + } else { + json = cmd.parameters; + } + + // merge with existing timeouts + let merged = Object.assign(this.timeouts.toJSON(), json); + this.timeouts = session.Timeouts.fromJSON(merged); +}; + +/** Single tap. */ +GeckoDriver.prototype.singleTap = function*(cmd, resp) { + let {id, x, y} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + throw new UnsupportedOperationError( + "Command 'singleTap' is not yet available in chrome context"); + + case Context.CONTENT: + this.addFrameCloseListener("tap"); + yield this.listener.singleTap(id, x, y); + break; + } +}; + +/** + * Perform a series of grouped actions at the specified points in time. + * + * @param {Array.<?>} actions + * Array of objects that each represent an action sequence. + * + * @throws {UnsupportedOperationError} + * If the command is made in chrome context. + */ +GeckoDriver.prototype.performActions = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new UnsupportedOperationError( + "Command 'performActions' is not yet available in chrome context"); + + case Context.CONTENT: + return this.listener.performActions({"actions": cmd.parameters.actions}); + } +}; + +/** + * Release all the keys and pointer buttons that are currently depressed. + */ +GeckoDriver.prototype.releaseActions = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new UnsupportedOperationError( + "Command 'releaseActions' is not yet available in chrome context"); + + case Context.CONTENT: + return this.listener.releaseActions(); + } +}; + +/** + * An action chain. + * + * @param {Object} value + * A nested array where the inner array represents each event, + * and the outer array represents a collection of events. + * + * @return {number} + * Last touch ID. + */ +GeckoDriver.prototype.actionChain = function*(cmd, resp) { + let {chain, nextId} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + // be conservative until this has a use case and is established + // to work as expected in Fennec + assert.firefox() + + let win = this.getCurrentWindow(); + resp.body.value = yield this.legacyactions.dispatchActions( + chain, nextId, {frame: win}, this.curBrowser.seenEls); + break; + + case Context.CONTENT: + this.addFrameCloseListener("action chain"); + resp.body.value = yield this.listener.actionChain(chain, nextId); + break; + } +}; + +/** + * A multi-action chain. + * + * @param {Object} value + * A nested array where the inner array represents eache vent, + * the middle array represents a collection of events for each + * finger, and the outer array represents all fingers. + */ +GeckoDriver.prototype.multiAction = function*(cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new UnsupportedOperationError( + "Command 'multiAction' is not yet available in chrome context"); + + case Context.CONTENT: + this.addFrameCloseListener("multi action chain"); + yield this.listener.multiAction(cmd.parameters.value, cmd.parameters.max_length); + break; + } +}; + +/** + * Find an element using the indicated search strategy. + * + * @param {string} using + * Indicates which search method to use. + * @param {string} value + * Value the client is looking for. + */ +GeckoDriver.prototype.findElement = function*(cmd, resp) { + let strategy = cmd.parameters.using; + let expr = cmd.parameters.value; + let opts = { + startNode: cmd.parameters.element, + timeout: this.timeouts.implicit, + all: false, + }; + + switch (this.context) { + case Context.CHROME: + if (!SUPPORTED_STRATEGIES.has(strategy)) { + throw new InvalidSelectorError(`Strategy not supported: ${strategy}`); + } + + let container = {frame: this.getCurrentWindow()}; + if (opts.startNode) { + opts.startNode = this.curBrowser.seenEls.get(opts.startNode, container); + } + let el = yield element.find(container, strategy, expr, opts); + let elRef = this.curBrowser.seenEls.add(el); + let webEl = element.makeWebElement(elRef); + + resp.body.value = webEl; + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.findElementContent( + strategy, + expr, + opts); + break; + } +}; + +/** + * Find elements using the indicated search strategy. + * + * @param {string} using + * Indicates which search method to use. + * @param {string} value + * Value the client is looking for. + */ +GeckoDriver.prototype.findElements = function*(cmd, resp) { + let strategy = cmd.parameters.using; + let expr = cmd.parameters.value; + let opts = { + startNode: cmd.parameters.element, + timeout: this.timeouts.implicit, + all: true, + }; + + switch (this.context) { + case Context.CHROME: + if (!SUPPORTED_STRATEGIES.has(strategy)) { + throw new InvalidSelectorError(`Strategy not supported: ${strategy}`); + } + + let container = {frame: this.getCurrentWindow()}; + if (opts.startNode) { + opts.startNode = this.curBrowser.seenEls.get(opts.startNode, container); + } + let els = yield element.find(container, strategy, expr, opts); + + let elRefs = this.curBrowser.seenEls.addAll(els); + let webEls = elRefs.map(element.makeWebElement); + resp.body = webEls; + break; + + case Context.CONTENT: + resp.body = yield this.listener.findElementsContent( + cmd.parameters.using, + cmd.parameters.value, + opts); + break; + } +}; + +/** Return the active element on the page. */ +GeckoDriver.prototype.getActiveElement = function*(cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new UnsupportedOperationError( + "Command 'getActiveElement' is not yet available in chrome context"); + + case Context.CONTENT: + resp.body.value = yield this.listener.getActiveElement(); + break; + } +}; + +/** + * Send click event to element. + * + * @param {string} id + * Reference ID to the element that will be clicked. + */ +GeckoDriver.prototype.clickElement = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + yield interaction.clickElement(el, this.a11yChecks); + break; + + case Context.CONTENT: + // We need to protect against the click causing an OOP frame to close. + // This fires the mozbrowserclose event when it closes so we need to + // listen for it and then just send an error back. The person making the + // call should be aware something isnt right and handle accordingly + this.addFrameCloseListener("click"); + yield this.listener.clickElement(id); + break; + } +}; + +/** + * Get a given attribute of an element. + * + * @param {string} id + * Web element reference ID to the element that will be inspected. + * @param {string} name + * Name of the attribute which value to retrieve. + * + * @return {string} + * Value of the attribute. + */ +GeckoDriver.prototype.getElementAttribute = function*(cmd, resp) { + let {id, name} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + + resp.body.value = el.getAttribute(name); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.getElementAttribute(id, name); + break; + } +}; + +/** + * Returns the value of a property associated with given element. + * + * @param {string} id + * Web element reference ID to the element that will be inspected. + * @param {string} name + * Name of the property which value to retrieve. + * + * @return {string} + * Value of the property. + */ +GeckoDriver.prototype.getElementProperty = function*(cmd, resp) { + let {id, name} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + resp.body.value = el[name]; + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.getElementProperty(id, name); + break; + } +}; + +/** + * Get the text of an element, if any. Includes the text of all child + * elements. + * + * @param {string} id + * Reference ID to the element that will be inspected. + */ +GeckoDriver.prototype.getElementText = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // for chrome, we look at text nodes, and any node with a "label" field + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + let lines = []; + this.getVisibleText(el, lines); + resp.body.value = lines.join("\n"); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.getElementText(id); + break; + } +}; + +/** + * Get the tag name of the element. + * + * @param {string} id + * Reference ID to the element that will be inspected. + */ +GeckoDriver.prototype.getElementTagName = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + resp.body.value = el.tagName.toLowerCase(); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.getElementTagName(id); + break; + } +}; + +/** + * Check if element is displayed. + * + * @param {string} id + * Reference ID to the element that will be inspected. + */ +GeckoDriver.prototype.isElementDisplayed = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + resp.body.value = yield interaction.isElementDisplayed( + el, this.a11yChecks); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.isElementDisplayed(id); + break; + } +}; + +/** + * Return the property of the computed style of an element. + * + * @param {string} id + * Reference ID to the element that will be checked. + * @param {string} propertyName + * CSS rule that is being requested. + */ +GeckoDriver.prototype.getElementValueOfCssProperty = function*(cmd, resp) { + let {id, propertyName: prop} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + let sty = win.document.defaultView.getComputedStyle(el, null); + resp.body.value = sty.getPropertyValue(prop); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.getElementValueOfCssProperty(id, prop); + break; + } +}; + +/** + * Check if element is enabled. + * + * @param {string} id + * Reference ID to the element that will be checked. + */ +GeckoDriver.prototype.isElementEnabled = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // Selenium atom doesn't quite work here + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + resp.body.value = yield interaction.isElementEnabled( + el, this.a11yChecks); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.isElementEnabled(id); + break; + } +}, + +/** + * Check if element is selected. + * + * @param {string} id + * Reference ID to the element that will be checked. + */ +GeckoDriver.prototype.isElementSelected = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // Selenium atom doesn't quite work here + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + resp.body.value = yield interaction.isElementSelected( + el, this.a11yChecks); + break; + + case Context.CONTENT: + resp.body.value = yield this.listener.isElementSelected(id); + break; + } +}; + +GeckoDriver.prototype.getElementRect = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + let rect = el.getBoundingClientRect(); + resp.body = { + x: rect.x + win.pageXOffset, + y: rect.y + win.pageYOffset, + width: rect.width, + height: rect.height + }; + break; + + case Context.CONTENT: + resp.body = yield this.listener.getElementRect(id); + break; + } +}; + +/** + * Send key presses to element after focusing on it. + * + * @param {string} id + * Reference ID to the element that will be checked. + * @param {string} value + * Value to send to the element. + */ +GeckoDriver.prototype.sendKeysToElement = function*(cmd, resp) { + let {id, value} = cmd.parameters; + assert.defined(value, `Expected character sequence: ${value}`); + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + yield interaction.sendKeysToElement( + el, value, true, this.a11yChecks); + break; + + case Context.CONTENT: + yield this.listener.sendKeysToElement(id, value); + break; + } +}; + +/** Sets the test name. The test name is used for logging purposes. */ +GeckoDriver.prototype.setTestName = function*(cmd, resp) { + let val = cmd.parameters.value; + this.testName = val; + yield this.listener.setTestName({value: val}); +}; + +/** + * Clear the text of an element. + * + * @param {string} id + * Reference ID to the element that will be cleared. + */ +GeckoDriver.prototype.clearElement = function*(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // the selenium atom doesn't work here + let win = this.getCurrentWindow(); + let el = this.curBrowser.seenEls.get(id, {frame: win}); + if (el.nodeName == "textbox") { + el.value = ""; + } else if (el.nodeName == "checkbox") { + el.checked = false; + } + break; + + case Context.CONTENT: + yield this.listener.clearElement(id); + break; + } +}; + +/** + * Switch to shadow root of the given host element. + * + * @param {string} id element id. + */ +GeckoDriver.prototype.switchToShadowRoot = function*(cmd, resp) { + assert.content(this.context) + + let id; + if (cmd.parameters) { id = cmd.parameters.id; } + yield this.listener.switchToShadowRoot(id); +}; + +/** Add a cookie to the document. */ +GeckoDriver.prototype.addCookie = function*(cmd, resp) { + assert.content(this.context) + + let cb = msg => { + this.mm.removeMessageListener("Marionette:addCookie", cb); + let cookie = msg.json; + Services.cookies.add( + cookie.domain, + cookie.path, + cookie.name, + cookie.value, + cookie.secure, + cookie.httpOnly, + cookie.session, + cookie.expiry, + {}); // originAttributes + return true; + }; + + this.mm.addMessageListener("Marionette:addCookie", cb); + yield this.listener.addCookie(cmd.parameters.cookie); +}; + +/** + * Get all the cookies for the current domain. + * + * This is the equivalent of calling {@code document.cookie} and parsing + * the result. + */ +GeckoDriver.prototype.getCookies = function*(cmd, resp) { + assert.content(this.context) + + resp.body = yield this.listener.getCookies(); +}; + +/** Delete all cookies that are visible to a document. */ +GeckoDriver.prototype.deleteAllCookies = function*(cmd, resp) { + assert.content(this.context) + + let cb = msg => { + let cookie = msg.json; + cookieManager.remove( + cookie.host, + cookie.name, + cookie.path, + false, + cookie.originAttributes); + return true; + }; + + this.mm.addMessageListener("Marionette:deleteCookie", cb); + yield this.listener.deleteAllCookies(); + this.mm.removeMessageListener("Marionette:deleteCookie", cb); +}; + +/** Delete a cookie by name. */ +GeckoDriver.prototype.deleteCookie = function*(cmd, resp) { + assert.content(this.context) + + let cb = msg => { + this.mm.removeMessageListener("Marionette:deleteCookie", cb); + let cookie = msg.json; + cookieManager.remove( + cookie.host, + cookie.name, + cookie.path, + false, + cookie.originAttributes); + return true; + }; + + this.mm.addMessageListener("Marionette:deleteCookie", cb); + yield this.listener.deleteCookie(cmd.parameters.name); +}; + +/** + * Close the currently selected tab/window. + * + * With multiple open tabs present the currently selected tab will be closed. + * Otherwise the window itself will be closed. If it is the last window + * currently open, the window will not be closed to prevent a shutdown of the + * application. Instead the returned list of window handles is empty. + * + * @return {Array.<string>} + * Unique window handles of remaining windows. + */ +GeckoDriver.prototype.close = function (cmd, resp) { + let nwins = 0; + let winEn = Services.wm.getEnumerator(null); + + while (winEn.hasMoreElements()) { + let win = winEn.getNext(); + + // For browser windows count the tabs. Otherwise take the window itself. + let tabbrowser = browser.getTabBrowser(win); + if (tabbrowser) { + nwins += tabbrowser.tabs.length; + } else { + nwins++; + } + } + + // If there is only 1 window left, do not close it. Instead return a faked + // empty array of window handles. This will instruct geckodriver to terminate + // the application. + if (nwins == 1) { + return []; + } + + if (this.mm != globalMessageManager) { + this.mm.removeDelayedFrameScript(FRAME_SCRIPT); + } + + return this.curBrowser.closeTab().then(() => this.windowHandles); +}; + +/** + * Close the currently selected chrome window. + * + * If it is the last window currently open, the chrome window will not be + * closed to prevent a shutdown of the application. Instead the returned + * list of chrome window handles is empty. + * + * @return {Array.<string>} + * Unique chrome window handles of remaining chrome windows. + */ +GeckoDriver.prototype.closeChromeWindow = function (cmd, resp) { + assert.firefox(); + + // Get the total number of windows + let nwins = 0; + let winEn = Services.wm.getEnumerator(null); + + while (winEn.hasMoreElements()) { + nwins++; + winEn.getNext(); + } + + // If there is only 1 window left, do not close it. Instead return a faked + // empty array of window handles. This will instruct geckodriver to terminate + // the application. + if (nwins == 1) { + return []; + } + + // reset frame to the top-most frame + this.curFrame = null; + + if (this.mm != globalMessageManager) { + this.mm.removeDelayedFrameScript(FRAME_SCRIPT); + } + + return this.curBrowser.closeWindow().then(() => this.chromeWindowHandles); +}; + +/** Delete Marionette session. */ +GeckoDriver.prototype.deleteSession = function (cmd, resp) { + if (this.curBrowser !== null) { + // frame scripts can be safely reused + Preferences.set(CONTENT_LISTENER_PREF, false); + + // delete session in each frame in each browser + for (let win in this.browsers) { + let browser = this.browsers[win]; + for (let i in browser.knownFrames) { + globalMessageManager.broadcastAsyncMessage( + "Marionette:deleteSession" + browser.knownFrames[i], {}); + } + } + + let winEn = Services.wm.getEnumerator(null); + while (winEn.hasMoreElements()) { + let win = winEn.getNext(); + if (win.messageManager) { + win.messageManager.removeDelayedFrameScript(FRAME_SCRIPT); + } else { + logger.error( + `Could not remove listener from page ${win.location.href}`); + } + } + + this.curBrowser.frameManager.removeMessageManagerListeners( + globalMessageManager); + } + + this.switchToGlobalMessageManager(); + + // reset frame to the top-most frame + this.curFrame = null; + if (this.mainFrame) { + try { + this.mainFrame.focus(); + } catch (e) { + this.mainFrame = null; + } + } + + if (this.observing !== null) { + for (let topic in this.observing) { + Services.obs.removeObserver(this.observing[topic], topic); + } + this.observing = null; + } + + this.sandboxes.clear(); + cert.uninstallOverride(); + + this.sessionId = null; + this.capabilities = new session.Capabilities(); +}; + +/** Returns the current status of the Application Cache. */ +GeckoDriver.prototype.getAppCacheStatus = function* (cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new UnsupportedOperationError( + "Command 'getAppCacheStatus' is not yet available in chrome context"); + + case Context.CONTENT: + resp.body.value = yield this.listener.getAppCacheStatus(); + break; + } +}; + +/** + * Import script to the JS evaluation runtime. + * + * Imported scripts are exposed in the contexts of all subsequent + * calls to {@code executeScript}, {@code executeAsyncScript}, and + * {@code executeJSScript} by prepending them to the evaluated script. + * + * Scripts can be cleared with the {@code clearImportedScripts} command. + * + * @param {string} script + * Script to include. If the script is byte-by-byte equal to an + * existing imported script, it is not imported. + */ +GeckoDriver.prototype.importScript = function*(cmd, resp) { + let script = cmd.parameters.script; + this.importedScripts.for(this.context).add(script); +}; + +/** + * Clear all scripts that are imported into the JS evaluation runtime. + * + * Scripts can be imported using the {@code importScript} command. + */ +GeckoDriver.prototype.clearImportedScripts = function*(cmd, resp) { + this.importedScripts.for(this.context).clear(); +}; + +/** + * Takes a screenshot of a web element, current frame, or viewport. + * + * The screen capture is returned as a lossless PNG image encoded as + * a base 64 string. + * + * If called in the content context, the <code>id</code> argument is not null + * and refers to a present and visible web element's ID, the capture area + * will be limited to the bounding box of that element. Otherwise, the + * capture area will be the bounding box of the current frame. + * + * If called in the chrome context, the screenshot will always represent the + * entire viewport. + * + * @param {string=} id + * Optional web element reference to take a screenshot of. + * If undefined, a screenshot will be taken of the document element. + * @param {Array.<string>=} highlights + * List of web elements to highlight. + * @param {boolean} full + * True to take a screenshot of the entire document element. Is not + * considered if {@code id} is not defined. Defaults to true. + * @param {boolean=} hash + * True if the user requests a hash of the image data. + * @param {boolean=} scroll + * Scroll to element if |id| is provided. If undefined, it will + * scroll to the element. + * + * @return {string} + * If {@code hash} is false, PNG image encoded as base64 encoded string. If + * 'hash' is True, hex digest of the SHA-256 hash of the base64 encoded + * string. + */ +GeckoDriver.prototype.takeScreenshot = function (cmd, resp) { + let {id, highlights, full, hash, scroll} = cmd.parameters; + highlights = highlights || []; + let format = hash ? capture.Format.Hash : capture.Format.Base64; + + switch (this.context) { + case Context.CHROME: + let container = {frame: this.getCurrentWindow().document.defaultView}; + if (!container.frame) { + throw new NoSuchWindowError("Unable to locate window"); + } + + let highlightEls = highlights.map( + ref => this.curBrowser.seenEls.get(ref, container)); + + // viewport + let canvas; + if (!id && !full) { + canvas = capture.viewport(container.frame, highlightEls); + + // element or full document element + } else { + let node; + if (id) { + node = this.curBrowser.seenEls.get(id, container); + } else { + node = container.frame.document.documentElement; + } + + canvas = capture.element(node, highlightEls); + } + + switch (format) { + case capture.Format.Hash: + return capture.toHash(canvas); + + case capture.Format.Base64: + return capture.toBase64(canvas); + } + break; + + case Context.CONTENT: + return this.listener.takeScreenshot(format, cmd.parameters); + } +}; + +/** + * Get the current browser orientation. + * + * Will return one of the valid primary orientation values + * portrait-primary, landscape-primary, portrait-secondary, or + * landscape-secondary. + */ +GeckoDriver.prototype.getScreenOrientation = function (cmd, resp) { + assert.fennec(); + + resp.body.value = this.getCurrentWindow().screen.mozOrientation; +}; + +/** + * Set the current browser orientation. + * + * The supplied orientation should be given as one of the valid + * orientation values. If the orientation is unknown, an error will + * be raised. + * + * Valid orientations are "portrait" and "landscape", which fall + * back to "portrait-primary" and "landscape-primary" respectively, + * and "portrait-secondary" as well as "landscape-secondary". + */ +GeckoDriver.prototype.setScreenOrientation = function (cmd, resp) { + assert.fennec(); + + const ors = [ + "portrait", "landscape", + "portrait-primary", "landscape-primary", + "portrait-secondary", "landscape-secondary", + ]; + + let or = String(cmd.parameters.orientation); + assert.string(or); + let mozOr = or.toLowerCase(); + if (!ors.includes(mozOr)) { + throw new InvalidArgumentError(`Unknown screen orientation: ${or}`); + } + + let win = this.getCurrentWindow(); + if (!win.screen.mozLockOrientation(mozOr)) { + throw new WebDriverError(`Unable to set screen orientation: ${or}`); + } +}; + +/** + * Get the size of the browser window currently in focus. + * + * Will return the current browser window size in pixels. Refers to + * window outerWidth and outerHeight values, which include scroll bars, + * title bars, etc. + */ +GeckoDriver.prototype.getWindowSize = function (cmd, resp) { + let win = this.getCurrentWindow(); + resp.body.width = win.outerWidth; + resp.body.height = win.outerHeight; +}; + +/** + * Set the size of the browser window currently in focus. + * + * Not supported on B2G. The supplied width and height values refer to + * the window outerWidth and outerHeight values, which include scroll + * bars, title bars, etc. + */ +GeckoDriver.prototype.setWindowSize = function (cmd, resp) { + assert.firefox() + + let {width, height} = cmd.parameters; + let win = this.getCurrentWindow(); + win.resizeTo(width, height); + this.getWindowSize(cmd, resp); +}; + +/** + * Maximizes the user agent window as if the user pressed the maximise + * button. + * + * Not Supported on B2G or Fennec. + */ +GeckoDriver.prototype.maximizeWindow = function (cmd, resp) { + assert.firefox() + + let win = this.getCurrentWindow(); + win.maximize() +}; + +/** + * Dismisses a currently displayed tab modal, or returns no such alert if + * no modal is displayed. + */ +GeckoDriver.prototype.dismissDialog = function (cmd, resp) { + this._checkIfAlertIsPresent(); + + let {button0, button1} = this.dialog.ui; + (button1 ? button1 : button0).click(); + this.dialog = null; +}; + +/** + * Accepts a currently displayed tab modal, or returns no such alert if + * no modal is displayed. + */ +GeckoDriver.prototype.acceptDialog = function (cmd, resp) { + this._checkIfAlertIsPresent(); + + let {button0} = this.dialog.ui; + button0.click(); + this.dialog = null; +}; + +/** + * Returns the message shown in a currently displayed modal, or returns a no such + * alert error if no modal is currently displayed. + */ +GeckoDriver.prototype.getTextFromDialog = function (cmd, resp) { + this._checkIfAlertIsPresent(); + + let {infoBody} = this.dialog.ui; + resp.body.value = infoBody.textContent; +}; + +/** + * Sends keys to the input field of a currently displayed modal, or + * returns a no such alert error if no modal is currently displayed. If + * a tab modal is currently displayed but has no means for text input, + * an element not visible error is returned. + */ +GeckoDriver.prototype.sendKeysToDialog = function (cmd, resp) { + this._checkIfAlertIsPresent(); + + // see toolkit/components/prompts/content/commonDialog.js + let {loginContainer, loginTextbox} = this.dialog.ui; + if (loginContainer.hidden) { + throw new ElementNotInteractableError( + "This prompt does not accept text input"); + } + + let win = this.dialog.window ? this.dialog.window : this.getCurrentWindow(); + event.sendKeysToElement( + cmd.parameters.value, + loginTextbox, + {ignoreVisibility: true}, + win); +}; + +GeckoDriver.prototype._checkIfAlertIsPresent = function() { + if (!this.dialog || !this.dialog.ui) { + throw new NoAlertOpenError( + "No tab modal was open when attempting to get the dialog text"); + } +}; + +/** + * Enables or disables accepting new socket connections. + * + * By calling this method with `false` the server will not accept any further + * connections, but existing connections will not be forcible closed. Use `true` + * to re-enable accepting connections. + * + * Please note that when closing the connection via the client you can end-up in + * a non-recoverable state if it hasn't been enabled before. + * + * This method is used for custom in application shutdowns via marionette.quit() + * or marionette.restart(), like File -> Quit. + * + * @param {boolean} state + * True if the server should accept new socket connections. + */ +GeckoDriver.prototype.acceptConnections = function (cmd, resp) { + assert.boolean(cmd.parameters.value); + this._server.acceptConnections = cmd.parameters.value; +} + +/** + * Quits Firefox with the provided flags and tears down the current + * session. + */ +GeckoDriver.prototype.quitApplication = function (cmd, resp) { + assert.firefox("Bug 1298921 - In app initiated quit not yet available beside Firefox") + + let flags = Ci.nsIAppStartup.eAttemptQuit; + for (let k of cmd.parameters.flags || []) { + flags |= Ci.nsIAppStartup[k]; + } + + this._server.acceptConnections = false; + resp.send(); + + this.deleteSession(); + Services.startup.quit(flags); +}; + +GeckoDriver.prototype.installAddon = function (cmd, resp) { + assert.firefox() + + let path = cmd.parameters.path; + let temp = cmd.parameters.temporary || false; + if (typeof path == "undefined" || typeof path != "string" || + typeof temp != "boolean") { + throw InvalidArgumentError(); + } + + return addon.install(path, temp); +}; + +GeckoDriver.prototype.uninstallAddon = function (cmd, resp) { + assert.firefox() + + let id = cmd.parameters.id; + if (typeof id == "undefined" || typeof id != "string") { + throw new InvalidArgumentError(); + } + + return addon.uninstall(id); +}; + +/** + * Helper function to convert an outerWindowID into a UID that Marionette + * tracks. + */ +GeckoDriver.prototype.generateFrameId = function (id) { + let uid = id + (this.appName == "B2G" ? "-b2g" : ""); + return uid; +}; + +/** Receives all messages from content messageManager. */ +GeckoDriver.prototype.receiveMessage = function (message) { + switch (message.name) { + case "Marionette:ok": + case "Marionette:done": + case "Marionette:error": + // check if we need to remove the mozbrowserclose listener + if (this.mozBrowserClose !== null) { + let win = this.getCurrentWindow(); + win.removeEventListener("mozbrowserclose", this.mozBrowserClose, true); + this.mozBrowserClose = null; + } + break; + + case "Marionette:log": + // log server-side messages + logger.info(message.json.message); + break; + + case "Marionette:shareData": + // log messages from tests + if (message.json.log) { + this.marionetteLog.addAll(message.json.log); + } + break; + + case "Marionette:switchToModalOrigin": + this.curBrowser.frameManager.switchToModalOrigin(message); + this.mm = this.curBrowser.frameManager + .currentRemoteFrame.messageManager.get(); + break; + + case "Marionette:switchedToFrame": + if (message.json.restorePrevious) { + this.currentFrameElement = this.previousFrameElement; + } else { + // we don't arbitrarily save previousFrameElement, since + // we allow frame switching after modals appear, which would + // override this value and we'd lose our reference + if (message.json.storePrevious) { + this.previousFrameElement = this.currentFrameElement; + } + this.currentFrameElement = message.json.frameValue; + } + break; + + case "Marionette:getVisibleCookies": + let [currentPath, host] = message.json; + let isForCurrentPath = path => currentPath.indexOf(path) != -1; + let results = []; + + let en = cookieManager.getCookiesFromHost(host, {}); + while (en.hasMoreElements()) { + let cookie = en.getNext().QueryInterface(Ci.nsICookie2); + // take the hostname and progressively shorten + let hostname = host; + do { + if ((cookie.host == "." + hostname || cookie.host == hostname) && + isForCurrentPath(cookie.path)) { + results.push({ + "name": cookie.name, + "value": cookie.value, + "path": cookie.path, + "host": cookie.host, + "secure": cookie.isSecure, + "expiry": cookie.expires, + "httpOnly": cookie.isHttpOnly, + "originAttributes": cookie.originAttributes + }); + break; + } + hostname = hostname.replace(/^.*?\./, ""); + } while (hostname.indexOf(".") != -1); + } + return results; + + case "Marionette:emitTouchEvent": + globalMessageManager.broadcastAsyncMessage( + "MarionetteMainListener:emitTouchEvent", message.json); + break; + + case "Marionette:register": + let wid = message.json.value; + let be = message.target; + let rv = this.registerBrowser(wid, be); + return rv; + + case "Marionette:listenersAttached": + if (message.json.listenerId === this.curBrowser.curFrameId) { + // If remoteness gets updated we need to call newSession. In the case + // of desktop this just sets up a small amount of state that doesn't + // change over the course of a session. + this.sendAsync("newSession", this.capabilities); + this.curBrowser.flushPendingCommands(); + } + break; + } +}; + +GeckoDriver.prototype.responseCompleted = function () { + if (this.curBrowser !== null) { + this.curBrowser.pendingCommands = []; + } +}; + +/** + * Retrieve the localized string for the specified entity id. + * + * Example: + * localizeEntity(["chrome://global/locale/about.dtd"], "about.version") + * + * @param {Array.<string>} urls + * Array of .dtd URLs. + * @param {string} id + * The ID of the entity to retrieve the localized string for. + * + * @return {string} + * The localized string for the requested entity. + */ +GeckoDriver.prototype.localizeEntity = function (cmd, resp) { + let {urls, id} = cmd.parameters; + + if (!Array.isArray(urls)) { + throw new InvalidArgumentError("Value of `urls` should be of type 'Array'"); + } + if (typeof id != "string") { + throw new InvalidArgumentError("Value of `id` should be of type 'string'"); + } + + resp.body.value = l10n.localizeEntity(urls, id); +} + +/** + * Retrieve the localized string for the specified property id. + * + * Example: + * localizeProperty(["chrome://global/locale/findbar.properties"], "FastFind") + * + * @param {Array.<string>} urls + * Array of .properties URLs. + * @param {string} id + * The ID of the property to retrieve the localized string for. + * + * @return {string} + * The localized string for the requested property. + */ +GeckoDriver.prototype.localizeProperty = function (cmd, resp) { + let {urls, id} = cmd.parameters; + + if (!Array.isArray(urls)) { + throw new InvalidArgumentError("Value of `urls` should be of type 'Array'"); + } + if (typeof id != "string") { + throw new InvalidArgumentError("Value of `id` should be of type 'string'"); + } + + resp.body.value = l10n.localizeProperty(urls, id); +} + +GeckoDriver.prototype.commands = { + "getMarionetteID": GeckoDriver.prototype.getMarionetteID, + "sayHello": GeckoDriver.prototype.sayHello, + "newSession": GeckoDriver.prototype.newSession, + "getSessionCapabilities": GeckoDriver.prototype.getSessionCapabilities, + "log": GeckoDriver.prototype.log, + "getLogs": GeckoDriver.prototype.getLogs, + "setContext": GeckoDriver.prototype.setContext, + "getContext": GeckoDriver.prototype.getContext, + "executeScript": GeckoDriver.prototype.executeScript, + "getTimeouts": GeckoDriver.prototype.getTimeouts, + "timeouts": GeckoDriver.prototype.setTimeouts, // deprecated until Firefox 55 + "setTimeouts": GeckoDriver.prototype.setTimeouts, + "singleTap": GeckoDriver.prototype.singleTap, + "performActions": GeckoDriver.prototype.performActions, + "releaseActions": GeckoDriver.prototype.releaseActions, + "actionChain": GeckoDriver.prototype.actionChain, // deprecated + "multiAction": GeckoDriver.prototype.multiAction, // deprecated + "executeAsyncScript": GeckoDriver.prototype.executeAsyncScript, + "executeJSScript": GeckoDriver.prototype.executeJSScript, + "findElement": GeckoDriver.prototype.findElement, + "findElements": GeckoDriver.prototype.findElements, + "clickElement": GeckoDriver.prototype.clickElement, + "getElementAttribute": GeckoDriver.prototype.getElementAttribute, + "getElementProperty": GeckoDriver.prototype.getElementProperty, + "getElementText": GeckoDriver.prototype.getElementText, + "getElementTagName": GeckoDriver.prototype.getElementTagName, + "isElementDisplayed": GeckoDriver.prototype.isElementDisplayed, + "getElementValueOfCssProperty": GeckoDriver.prototype.getElementValueOfCssProperty, + "getElementRect": GeckoDriver.prototype.getElementRect, + "isElementEnabled": GeckoDriver.prototype.isElementEnabled, + "isElementSelected": GeckoDriver.prototype.isElementSelected, + "sendKeysToElement": GeckoDriver.prototype.sendKeysToElement, + "clearElement": GeckoDriver.prototype.clearElement, + "getTitle": GeckoDriver.prototype.getTitle, + "getWindowType": GeckoDriver.prototype.getWindowType, + "getPageSource": GeckoDriver.prototype.getPageSource, + "get": GeckoDriver.prototype.get, + "getCurrentUrl": GeckoDriver.prototype.getCurrentUrl, + "goBack": GeckoDriver.prototype.goBack, + "goForward": GeckoDriver.prototype.goForward, + "refresh": GeckoDriver.prototype.refresh, + "getWindowHandle": GeckoDriver.prototype.getWindowHandle, + "getChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle, + "getCurrentChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle, + "getWindowHandles": GeckoDriver.prototype.getWindowHandles, + "getChromeWindowHandles": GeckoDriver.prototype.getChromeWindowHandles, + "getWindowPosition": GeckoDriver.prototype.getWindowPosition, + "setWindowPosition": GeckoDriver.prototype.setWindowPosition, + "getActiveFrame": GeckoDriver.prototype.getActiveFrame, + "switchToFrame": GeckoDriver.prototype.switchToFrame, + "switchToParentFrame": GeckoDriver.prototype.switchToParentFrame, + "switchToWindow": GeckoDriver.prototype.switchToWindow, + "switchToShadowRoot": GeckoDriver.prototype.switchToShadowRoot, + "deleteSession": GeckoDriver.prototype.deleteSession, + "importScript": GeckoDriver.prototype.importScript, + "clearImportedScripts": GeckoDriver.prototype.clearImportedScripts, + "getAppCacheStatus": GeckoDriver.prototype.getAppCacheStatus, + "close": GeckoDriver.prototype.close, + "closeChromeWindow": GeckoDriver.prototype.closeChromeWindow, + "setTestName": GeckoDriver.prototype.setTestName, + "takeScreenshot": GeckoDriver.prototype.takeScreenshot, + "addCookie": GeckoDriver.prototype.addCookie, + "getCookies": GeckoDriver.prototype.getCookies, + "deleteAllCookies": GeckoDriver.prototype.deleteAllCookies, + "deleteCookie": GeckoDriver.prototype.deleteCookie, + "getActiveElement": GeckoDriver.prototype.getActiveElement, + "getScreenOrientation": GeckoDriver.prototype.getScreenOrientation, + "setScreenOrientation": GeckoDriver.prototype.setScreenOrientation, + "getWindowSize": GeckoDriver.prototype.getWindowSize, + "setWindowSize": GeckoDriver.prototype.setWindowSize, + "maximizeWindow": GeckoDriver.prototype.maximizeWindow, + "dismissDialog": GeckoDriver.prototype.dismissDialog, + "acceptDialog": GeckoDriver.prototype.acceptDialog, + "getTextFromDialog": GeckoDriver.prototype.getTextFromDialog, + "sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog, + "acceptConnections": GeckoDriver.prototype.acceptConnections, + "quitApplication": GeckoDriver.prototype.quitApplication, + + "localization:l10n:localizeEntity": GeckoDriver.prototype.localizeEntity, + "localization:l10n:localizeProperty": GeckoDriver.prototype.localizeProperty, + + "addon:install": GeckoDriver.prototype.installAddon, + "addon:uninstall": GeckoDriver.prototype.uninstallAddon, +}; + +function copy (obj) { + if (Array.isArray(obj)) { + return obj.slice(); + } else if (typeof obj == "object") { + return Object.assign({}, obj); + } + return obj; +} + +/** + * Get the outer window ID for the specified window. + * + * @param {nsIDOMWindow} win + * Window whose browser we need to access. + * + * @return {string} + * Returns the unique window ID. + */ +function getOuterWindowId(win) { + let id = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + + return id.toString(); +} |