summaryrefslogtreecommitdiffstats
path: root/testing/marionette/driver.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/driver.js')
-rw-r--r--testing/marionette/driver.js4016
1 files changed, 4016 insertions, 0 deletions
diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js
new file mode 100644
index 0000000000..5589d30b5f
--- /dev/null
+++ b/testing/marionette/driver.js
@@ -0,0 +1,4016 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+/* global XPCNativeWrapper */
+
+const EXPORTED_SYMBOLS = ["GeckoDriver"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ OS: "resource://gre/modules/osfile.jsm",
+
+ accessibility: "chrome://marionette/content/accessibility.js",
+ Addon: "chrome://marionette/content/addon.js",
+ allowAllCerts: "chrome://marionette/content/cert.js",
+ assert: "chrome://marionette/content/assert.js",
+ atom: "chrome://marionette/content/atom.js",
+ browser: "chrome://marionette/content/browser.js",
+ Capabilities: "chrome://marionette/content/capabilities.js",
+ capture: "chrome://marionette/content/capture.js",
+ ChromeWebElement: "chrome://marionette/content/element.js",
+ clearElementIdCache:
+ "chrome://marionette/content/actors/MarionetteCommandsParent.jsm",
+ clearActionInputState:
+ "chrome://marionette/content/actors/MarionetteCommandsChild.jsm",
+ Context: "chrome://marionette/content/browser.js",
+ cookie: "chrome://marionette/content/cookie.js",
+ DebounceCallback: "chrome://marionette/content/sync.js",
+ element: "chrome://marionette/content/element.js",
+ error: "chrome://marionette/content/error.js",
+ evaluate: "chrome://marionette/content/evaluate.js",
+ getMarionetteCommandsActorProxy:
+ "chrome://marionette/content/actors/MarionetteCommandsParent.jsm",
+ IdlePromise: "chrome://marionette/content/sync.js",
+ interaction: "chrome://marionette/content/interaction.js",
+ l10n: "chrome://marionette/content/l10n.js",
+ legacyaction: "chrome://marionette/content/legacyaction.js",
+ Log: "chrome://marionette/content/log.js",
+ MarionettePrefs: "chrome://marionette/content/prefs.js",
+ modal: "chrome://marionette/content/modal.js",
+ navigate: "chrome://marionette/content/navigate.js",
+ PollPromise: "chrome://marionette/content/sync.js",
+ pprint: "chrome://marionette/content/format.js",
+ print: "chrome://marionette/content/print.js",
+ proxy: "chrome://marionette/content/proxy.js",
+ reftest: "chrome://marionette/content/reftest.js",
+ registerCommandsActor:
+ "chrome://marionette/content/actors/MarionetteCommandsParent.jsm",
+ registerEventsActor:
+ "chrome://marionette/content/actors/MarionetteEventsParent.jsm",
+ Sandboxes: "chrome://marionette/content/evaluate.js",
+ TimedPromise: "chrome://marionette/content/sync.js",
+ Timeouts: "chrome://marionette/content/capabilities.js",
+ UnhandledPromptBehavior: "chrome://marionette/content/capabilities.js",
+ unregisterCommandsActor:
+ "chrome://marionette/content/actors/MarionetteCommandsParent.jsm",
+ unregisterEventsActor:
+ "chrome://marionette/content/actors/MarionetteEventsParent.jsm",
+ waitForEvent: "chrome://marionette/content/sync.js",
+ waitForLoadEvent: "chrome://marionette/content/sync.js",
+ waitForObserverTopic: "chrome://marionette/content/sync.js",
+ WebElement: "chrome://marionette/content/element.js",
+ WebElementEventTarget: "chrome://marionette/content/dom.js",
+ WindowState: "chrome://marionette/content/browser.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+const APP_ID_FIREFOX = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+const APP_ID_THUNDERBIRD = "{3550f703-e582-4d05-9a08-453d09bdfdc6}";
+
+const FRAME_SCRIPT = "chrome://marionette/content/listener.js";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const SUPPORTED_STRATEGIES = new Set([
+ element.Strategy.ClassName,
+ element.Strategy.Selector,
+ element.Strategy.ID,
+ element.Strategy.Name,
+ element.Strategy.LinkText,
+ element.Strategy.PartialLinkText,
+ element.Strategy.TagName,
+ element.Strategy.XPath,
+]);
+
+// Timeout used to abort fullscreen, maximize, and minimize
+// commands if no window manager is present.
+const TIMEOUT_NO_WINDOW_MANAGER = 5000;
+
+const globalMessageManager = Services.mm;
+
+/**
+ * The Marionette WebDriver services provides a standard conforming
+ * implementation of the W3C WebDriver specification.
+ *
+ * @see {@link https://w3c.github.io/webdriver/webdriver-spec.html}
+ * @namespace driver
+ */
+
+/**
+ * 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 <var>cmd</var>'s
+ * documentation refers to the contents of the <code>cmd.parameter</code>
+ * object.
+ *
+ * @class GeckoDriver
+ *
+ * @param {MarionetteServer} server
+ * The instance of Marionette server.
+ */
+this.GeckoDriver = function(server) {
+ this.appId = Services.appinfo.ID;
+ this.appName = Services.appinfo.name.toLowerCase();
+ this._server = server;
+
+ this.sessionID = null;
+ this.browsers = {};
+
+ // Maps permanentKey to browsing context id: WeakMap.<Object, number>
+ this._browserIds = new WeakMap();
+
+ // points to current browser
+ this.curBrowser = null;
+ // top-most chrome window
+ this.mainFrame = null;
+
+ // current browsing contexts for chrome and content
+ this.chromeBrowsingContext = null;
+ this.contentBrowsingContext = null;
+
+ // Use content context by default
+ this.context = Context.Content;
+
+ this.sandboxes = new Sandboxes(() => this.getCurrentWindow());
+ this.legacyactions = new legacyaction.Chain();
+
+ this.capabilities = new Capabilities();
+
+ this.mm = globalMessageManager;
+ if (!MarionettePrefs.useActors) {
+ this.listener = proxy.toListener(
+ this.sendAsync.bind(this),
+ () => this.curBrowser
+ );
+ }
+
+ // used for modal dialogs or tab modal alerts
+ this.dialog = null;
+ this.dialogObserver = null;
+};
+
+Object.defineProperty(GeckoDriver.prototype, "a11yChecks", {
+ get() {
+ return this.capabilities.get("moz:accessibilityChecks");
+ },
+});
+
+/**
+ * The current context decides if commands are executed in chrome- or
+ * content space.
+ */
+Object.defineProperty(GeckoDriver.prototype, "context", {
+ get() {
+ return this._context;
+ },
+
+ set(context) {
+ this._context = Context.fromString(context);
+ },
+});
+
+/**
+ * Returns the current URL of the ChromeWindow or content browser,
+ * depending on context.
+ *
+ * @return {URL}
+ * Read-only property containing the currently loaded URL.
+ */
+Object.defineProperty(GeckoDriver.prototype, "currentURL", {
+ get() {
+ const browsingContext = this.getBrowsingContext({ top: true });
+ return new URL(browsingContext.currentWindowGlobal.documentURI.spec);
+ },
+});
+
+/**
+ * Returns the title of the ChromeWindow or content browser,
+ * depending on context.
+ *
+ * @return {string}
+ * Read-only property containing the title of the loaded URL.
+ */
+Object.defineProperty(GeckoDriver.prototype, "title", {
+ get() {
+ const browsingContext = this.getBrowsingContext({ top: true });
+ return browsingContext.currentWindowGlobal.documentTitle;
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "proxy", {
+ get() {
+ return this.capabilities.get("proxy");
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "secureTLS", {
+ get() {
+ return !this.capabilities.get("acceptInsecureCerts");
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "timeouts", {
+ get() {
+ return this.capabilities.get("timeouts");
+ },
+
+ set(newTimeouts) {
+ this.capabilities.set("timeouts", newTimeouts);
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "windows", {
+ get() {
+ return Services.wm.getEnumerator(null);
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "windowType", {
+ get() {
+ return this.curBrowser.window.document.documentElement.getAttribute(
+ "windowtype"
+ );
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "windowHandles", {
+ get() {
+ let hs = [];
+
+ for (let win of this.windows) {
+ let tabBrowser = browser.getTabBrowser(win);
+
+ // Only return handles for browser windows
+ if (tabBrowser && tabBrowser.tabs) {
+ for (let tab of tabBrowser.tabs) {
+ let winId = this.getIdForBrowser(browser.getBrowserForTab(tab));
+ if (winId !== null) {
+ hs.push(winId);
+ }
+ }
+ }
+ }
+
+ return hs;
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "chromeWindowHandles", {
+ get() {
+ let hs = [];
+
+ for (let win of this.windows) {
+ hs.push(getWindowId(win));
+ }
+
+ return hs;
+ },
+});
+
+GeckoDriver.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+]);
+
+GeckoDriver.prototype.init = function() {
+ if (MarionettePrefs.useActors) {
+ // When using JSWindowActors, we are not relying on framescript events
+ return;
+ }
+
+ this.mm.addMessageListener("Marionette:ListenersAttached", this);
+ this.mm.addMessageListener("Marionette:Register", this);
+ this.mm.addMessageListener("Marionette:switchedToFrame", this);
+ this.mm.addMessageListener("Marionette:NavigationEvent", this);
+ this.mm.addMessageListener("Marionette:Unloaded", this, true);
+};
+
+GeckoDriver.prototype.uninit = function() {
+ if (MarionettePrefs.useActors) {
+ return;
+ }
+
+ this.mm.removeMessageListener("Marionette:ListenersAttached", this);
+ this.mm.removeMessageListener("Marionette:Register", this);
+ this.mm.removeMessageListener("Marionette:switchedToFrame", this);
+ this.mm.removeMessageListener("Marionette:NavigationEvent", this);
+ this.mm.removeMessageListener("Marionette:Unloaded", this);
+};
+
+/**
+ * Callback used to observe the creation of new modal or tab modal dialogs
+ * during the session's lifetime.
+ */
+GeckoDriver.prototype.handleModalDialog = function(action, dialog, win) {
+ // Only care about modals of the currently selected window.
+ if (win !== this.curBrowser.window) {
+ return;
+ }
+
+ if (action === modal.ACTION_OPENED) {
+ this.dialog = new modal.Dialog(() => this.curBrowser, dialog);
+ } else if (action === modal.ACTION_CLOSED) {
+ this.dialog = null;
+ }
+};
+
+/**
+ * Get the current visible URL.
+ *
+ * Can be removed once WindowGlobal supports visibleURL (bug 1664881).
+ */
+GeckoDriver.prototype._getCurrentURL = async function() {
+ let url;
+
+ if (MarionettePrefs.useActors) {
+ url = await this.getActor({ top: true }).getCurrentUrl();
+ return new URL(url);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ const browsingContext = this.getBrowsingContext({ top: true });
+ url = browsingContext.window.location.href;
+ break;
+ case Context.Content:
+ url = await this.listener.getCurrentUrl();
+ break;
+ }
+
+ return new URL(url);
+};
+
+/**
+ * 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 target message handler <tt>Marionette:SUFFIX</tt>.
+ * @param {Object=} data
+ * Data that must be serialisable using {@link evaluate.toJSON}.
+ * @param {number=} commandID
+ * Optional command ID to ensure synchronisity.
+ *
+ * @throws {JavaScriptError}
+ * If <var>data</var> could not be marshaled.
+ * @throws {NoSuchWindowError}
+ * If there is no current target frame.
+ */
+GeckoDriver.prototype.sendAsync = function(name, data, commandID) {
+ let payload = evaluate.toJSON(data, this.seenEls);
+
+ if (payload === null) {
+ payload = {};
+ }
+
+ // TODO(ato): When proxy.AsyncMessageChannel
+ // is used for all chrome <-> content communication
+ // this can be removed.
+ if (commandID) {
+ payload.commandID = commandID;
+ }
+
+ if (this.curBrowser.curFrameId) {
+ let target = `Marionette:${name}`;
+ this.curBrowser.messageManager.sendAsyncMessage(target, payload);
+ } else {
+ throw new error.NoSuchWindowError(
+ "No such content frame; perhaps the listener was not registered?"
+ );
+ }
+};
+
+/**
+ * Get the current "MarionetteCommands" parent actor.
+ *
+ * @param {Object} options
+ * @param {boolean=} options.top
+ * If set to true use the window's top-level browsing context for the actor,
+ * otherwise the one from the currently selected frame. Defaults to false.
+ *
+ * @returns {MarionetteCommandsParent}
+ * The parent actor.
+ */
+GeckoDriver.prototype.getActor = function(options = {}) {
+ return getMarionetteCommandsActorProxy(() =>
+ this.getBrowsingContext(options)
+ );
+};
+
+/**
+ * Get the selected BrowsingContext for the current context.
+ *
+ * @param {Object} options
+ * @param {Context=} options.context
+ * Context (content or chrome) for which to retrieve the browsing context.
+ * Defaults to the current one.
+ * @param {boolean=} options.parent
+ * If set to true return the window's parent browsing context,
+ * otherwise the one from the currently selected frame. Defaults to false.
+ * @param {boolean=} options.top
+ * If set to true return the window's top-level browsing context,
+ * otherwise the one from the currently selected frame. Defaults to false.
+ *
+ * @return {BrowsingContext}
+ * The browsing context.
+ */
+GeckoDriver.prototype.getBrowsingContext = function(options = {}) {
+ const { context = this.context, parent = false, top = false } = options;
+
+ let browsingContext = null;
+ if (context === Context.Chrome) {
+ browsingContext = this.chromeBrowsingContext;
+ } else {
+ browsingContext = this.contentBrowsingContext;
+ }
+
+ if (browsingContext && parent) {
+ browsingContext = browsingContext.parent;
+ }
+
+ if (browsingContext && top) {
+ browsingContext = browsingContext.top;
+ }
+
+ return browsingContext;
+};
+
+/**
+ * Get the currently selected window.
+ *
+ * It will return the outer {@link ChromeWindow} previously selected by
+ * window handle through {@link #switchToWindow}, or the first window that
+ * was registered.
+ *
+ * @param {Object} options
+ * @param {Context=} options.context
+ * Optional name of the context to use for finding the window.
+ * It will be required if a command always needs a specific context,
+ * whether which context is currently set. Defaults to the current
+ * context.
+ *
+ * @return {ChromeWindow}
+ * The current top-level browsing context.
+ */
+GeckoDriver.prototype.getCurrentWindow = function(options = {}) {
+ const { context = this.context } = options;
+
+ let win = null;
+ switch (context) {
+ case Context.Chrome:
+ if (this.curBrowser) {
+ win = this.curBrowser.window;
+ }
+ break;
+
+ case Context.Content:
+ if (this.curBrowser && this.curBrowser.contentBrowser) {
+ win = this.curBrowser.window;
+ }
+ break;
+ }
+
+ return win;
+};
+
+GeckoDriver.prototype.isReftestBrowser = function(element) {
+ return (
+ this._reftest &&
+ element &&
+ element.tagName === "xul:browser" &&
+ element.parentElement &&
+ element.parentElement.id === "reftest"
+ );
+};
+
+GeckoDriver.prototype.addFrameCloseListener = function(action) {
+ let win = this.getCurrentWindow();
+ this.mozBrowserClose = e => {
+ if (e.target.id == this.oopFrameId) {
+ win.removeEventListener("mozbrowserclose", this.mozBrowserClose, true);
+ throw new error.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 {ChromeWindow} 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 context = new browser.Context(win, this);
+ let winId = getWindowId(win);
+
+ this.browsers[winId] = context;
+ this.curBrowser = this.browsers[winId];
+};
+
+/**
+ * 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 {ChromeWindow} 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(window, isNewSession = false) {
+ this.mainFrame = window;
+
+ this.addBrowser(window);
+ this.whenBrowserStarted(window, isNewSession);
+};
+
+/**
+ * Callback invoked after a new session has been started in a browser.
+ * Loads the Marionette frame script into the browser if needed.
+ *
+ * @param {ChromeWindow} window
+ * 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(window, isNewSession) {
+ // Do not load the framescript when actors are used.
+ if (MarionettePrefs.useActors) {
+ return;
+ }
+
+ let mm = 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 (!MarionettePrefs.contentListener || !isNewSession) {
+ // load listener into the remote frame
+ // and any applicable new frames
+ // opened after this call
+ mm.loadFrameScript(FRAME_SCRIPT, true);
+ MarionettePrefs.contentListener = true;
+ }
+ } else {
+ logger.error("Unable to load content frame script");
+ }
+};
+
+/**
+ * Recursively get all labeled text.
+ *
+ * @param {Element} 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.
+ *
+ * @param {xul:browser} browserElement
+ */
+GeckoDriver.prototype.registerBrowser = function(browserElement) {
+ // We want to ignore frames that are XUL browsers that aren't in the "main"
+ // tabbrowser, but accept things on Fennec (which doesn't have a
+ // xul:tabbrowser), and accept HTML iframes (because tests depend on it),
+ // as well as XUL frames. Ideally this should be cleaned up and we should
+ // keep track of browsers a different way.
+ if (
+ this.appId != APP_ID_FIREFOX ||
+ browserElement.namespaceURI != XUL_NS ||
+ browserElement.nodeName != "browser" ||
+ browserElement.getTabBrowser()
+ ) {
+ this.curBrowser.register(browserElement);
+ }
+};
+
+GeckoDriver.prototype.registerPromise = function() {
+ const li = "Marionette:Register";
+
+ return new Promise(resolve => {
+ let cb = ({ json, target }) => {
+ this.registerBrowser(target);
+
+ if (this.curBrowser.frameRegsPending > 0) {
+ this.curBrowser.frameRegsPending--;
+ }
+
+ if (this.curBrowser.frameRegsPending === 0) {
+ this.mm.removeMessageListener(li, cb);
+ resolve();
+ }
+
+ return { frameId: json.frameId };
+ };
+ this.mm.addMessageListener(li, cb);
+ });
+};
+
+GeckoDriver.prototype.listeningPromise = function() {
+ const li = "Marionette:ListenersAttached";
+
+ return new Promise(resolve => {
+ let cb = msg => {
+ if (msg.json.frameId === this.curBrowser.curFrameId) {
+ this.mm.removeMessageListener(li, cb);
+ resolve(msg.json.frameId);
+ }
+ };
+ this.mm.addMessageListener(li, cb);
+ });
+};
+
+/**
+ * Create a new WebDriver session.
+ *
+ * It is expected that the caller performs the necessary checks on
+ * the requested capabilities to be WebDriver conforming. The WebDriver
+ * service offered by Marionette does not match or negotiate capabilities
+ * beyond type- and bounds checks.
+ *
+ * <h3>Capabilities</h3>
+ *
+ * <dl>
+ * <dt><code>pageLoadStrategy</code> (string)
+ * <dd>The page load strategy to use for the current session. Must be
+ * one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>".
+ *
+ * <dt><code>acceptInsecureCerts</code> (boolean)
+ * <dd>Indicates whether untrusted and self-signed TLS certificates
+ * are implicitly trusted on navigation for the duration of the session.
+ *
+ * <dt><code>timeouts</code> (Timeouts object)
+ * <dd>Describes the timeouts imposed on certian session operations.
+ *
+ * <dt><code>proxy</code> (Proxy object)
+ * <dd>Defines the proxy configuration.
+ *
+ * <dt><code>moz:accessibilityChecks</code> (boolean)
+ * <dd>Run a11y checks when clicking elements.
+ *
+ * <dt><code>moz:useNonSpecCompliantPointerOrigin</code> (boolean)
+ * <dd>Use the not WebDriver conforming calculation of the pointer origin
+ * when the origin is an element, and the element center point is used.
+ *
+ * <dt><code>moz:webdriverClick</code> (boolean)
+ * <dd>Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
+ * </dl>
+ *
+ * <h4>Timeouts object</h4>
+ *
+ * <dl>
+ * <dt><code>script</code> (number)
+ * <dd>Determines when to interrupt a script that is being evaluates.
+ *
+ * <dt><code>pageLoad</code> (number)
+ * <dd>Provides the timeout limit used to interrupt navigation of the
+ * browsing context.
+ *
+ * <dt><code>implicit</code> (number)
+ * <dd>Gives the timeout of when to abort when locating an element.
+ * </dl>
+ *
+ * <h4>Proxy object</h4>
+ *
+ * <dl>
+ * <dt><code>proxyType</code> (string)
+ * <dd>Indicates the type of proxy configuration. Must be one
+ * of "<tt>pac</tt>", "<tt>direct</tt>", "<tt>autodetect</tt>",
+ * "<tt>system</tt>", or "<tt>manual</tt>".
+ *
+ * <dt><code>proxyAutoconfigUrl</code> (string)
+ * <dd>Defines the URL for a proxy auto-config file if
+ * <code>proxyType</code> is equal to "<tt>pac</tt>".
+ *
+ * <dt><code>ftpProxy</code> (string)
+ * <dd>Defines the proxy host for FTP traffic when the
+ * <code>proxyType</code> is "<tt>manual</tt>".
+ *
+ * <dt><code>httpProxy</code> (string)
+ * <dd>Defines the proxy host for HTTP traffic when the
+ * <code>proxyType</code> is "<tt>manual</tt>".
+ *
+ * <dt><code>noProxy</code> (string)
+ * <dd>Lists the adress for which the proxy should be bypassed when
+ * the <code>proxyType</code> is "<tt>manual</tt>". Must be a JSON
+ * List containing any number of any of domains, IPv4 addresses, or IPv6
+ * addresses.
+ *
+ * <dt><code>sslProxy</code> (string)
+ * <dd>Defines the proxy host for encrypted TLS traffic when the
+ * <code>proxyType</code> is "<tt>manual</tt>".
+ *
+ * <dt><code>socksProxy</code> (string)
+ * <dd>Defines the proxy host for a SOCKS proxy traffic when the
+ * <code>proxyType</code> is "<tt>manual</tt>".
+ *
+ * <dt><code>socksVersion</code> (string)
+ * <dd>Defines the SOCKS proxy version when the <code>proxyType</code> is
+ * "<tt>manual</tt>". It must be any integer between 0 and 255
+ * inclusive.
+ * </dl>
+ *
+ * <h3>Example</h3>
+ *
+ * Input:
+ *
+ * <pre><code>
+ * {"capabilities": {"acceptInsecureCerts": true}}
+ * </code></pre>
+ *
+ * @param {string=} sessionId
+ * Normally a unique ID is given to a new session, however this can
+ * be overriden by providing this field.
+ * @param {Object.<string, *>=} capabilities
+ * JSON Object containing any of the recognised capabilities listed
+ * above.
+ *
+ * @return {Object}
+ * Session ID and capabilities offered by the WebDriver service.
+ *
+ * @throws {SessionNotCreatedError}
+ * If, for whatever reason, a session could not be created.
+ */
+GeckoDriver.prototype.newSession = async function(cmd) {
+ if (this.sessionID) {
+ throw new error.SessionNotCreatedError("Maximum number of active sessions");
+ }
+ this.sessionID = WebElement.generateUUID();
+
+ try {
+ this.capabilities = Capabilities.fromJSON(cmd.parameters);
+
+ if (!this.secureTLS) {
+ logger.warn("TLS certificate errors will be ignored for this session");
+ allowAllCerts.enable();
+ }
+
+ if (this.proxy.init()) {
+ logger.info("Proxy settings initialised: " + JSON.stringify(this.proxy));
+ }
+ } catch (e) {
+ throw new error.SessionNotCreatedError(e);
+ }
+
+ // 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 waitForWindow = function() {
+ let windowTypes;
+ switch (this.appId) {
+ case APP_ID_THUNDERBIRD:
+ windowTypes = ["mail:3pane"];
+ break;
+ default:
+ // We assume that an app either has GeckoView windows, or
+ // Firefox/Fennec windows, but not both.
+ windowTypes = ["navigator:browser", "navigator:geckoview"];
+ break;
+ }
+ let win;
+ for (let windowType of windowTypes) {
+ win = Services.wm.getMostRecentWindow(windowType);
+ if (win) {
+ break;
+ }
+ }
+ 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 {
+ if (MarionettePrefs.clickToStart) {
+ Services.prompt.alert(
+ win,
+ "",
+ "Click to start execution of marionette tests"
+ );
+ }
+ this.startBrowser(win, true);
+ }
+ };
+
+ let registerBrowsers;
+ let browserListening;
+
+ if (MarionettePrefs.useActors) {
+ registerCommandsActor();
+ registerEventsActor();
+ } else {
+ registerBrowsers = this.registerPromise();
+ browserListening = this.listeningPromise();
+ }
+
+ if (!MarionettePrefs.contentListener) {
+ waitForWindow.call(this);
+ } else if (this.appId != APP_ID_FIREFOX && this.curBrowser === null) {
+ // if there is a content listener, then we just wake it up
+ let win = this.getCurrentWindow();
+ this.addBrowser(win);
+ this.whenBrowserStarted(win, false);
+ } else {
+ throw new error.WebDriverError("Session already running");
+ }
+
+ if (MarionettePrefs.useActors) {
+ for (let win of this.windows) {
+ const tabBrowser = browser.getTabBrowser(win);
+
+ if (tabBrowser) {
+ for (const tab of tabBrowser.tabs) {
+ const contentBrowser = browser.getBrowserForTab(tab);
+ this.registerBrowser(contentBrowser);
+ }
+ }
+ }
+ } else {
+ await registerBrowsers;
+ await browserListening;
+ }
+
+ if (this.mainFrame) {
+ this.chromeBrowsingContext = this.mainFrame.browsingContext;
+ this.mainFrame.focus();
+ }
+
+ if (this.curBrowser.tab) {
+ this.contentBrowsingContext = this.curBrowser.contentBrowser.browsingContext;
+ this.curBrowser.contentBrowser.focus();
+ }
+
+ // Setup observer for modal dialogs
+ this.dialogObserver = new modal.DialogObserver(this);
+ this.dialogObserver.add(this.handleModalDialog.bind(this));
+
+ Services.obs.addObserver(this, "browsing-context-attached");
+
+ // Check if there is already an open dialog for the selected browser window.
+ this.dialog = modal.findModalDialogs(this.curBrowser);
+
+ return {
+ sessionId: this.sessionID,
+ capabilities: this.capabilities,
+ };
+};
+
+GeckoDriver.prototype.observe = function(subject, topic, data) {
+ switch (topic) {
+ case "browsing-context-attached":
+ // For cross-group navigations the complete browsing context tree of a tab
+ // gets replaced. An indication for that is when the newly attached
+ // browsing context has the same browserId as the currently selected
+ // content browsing context, and doesn't have a parent.
+ //
+ // Also the current content browsing context gets only updated when it's
+ // the top-level one to not automatically switch away from the currently
+ // selected frame.
+ if (
+ subject.browserId == this.contentBrowsingContext?.browserId &&
+ !subject.parent &&
+ !this.contentBrowsingContext?.parent
+ ) {
+ logger.trace(
+ "Remoteness change detected. Set new top-level browsing context " +
+ `to ${subject.id}`
+ );
+ this.contentBrowsingContext = subject;
+ if (MarionettePrefs.useActors) {
+ // When using the framescript, the new browsing context created after
+ // a remoteness change will self-register. With JSWindowActors, we
+ // manually update the stored browsing context id.
+ // Switching to browserId instead of browsingContext.id would make
+ // this call unnecessary. See Bug 1681973.
+ this.updateIdForBrowser(this.curBrowser.contentBrowser, subject.id);
+ }
+ }
+ break;
+ }
+};
+
+/**
+ * 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() {
+ return { capabilities: this.capabilities };
+};
+
+/**
+ * Sets the context of the subsequent commands.
+ *
+ * All subsequent requests to commands that in some way involve
+ * interaction with a browsing context will target the chosen browsing
+ * context.
+ *
+ * @param {string} value
+ * Name of the context to be switched to. Must be one of "chrome" or
+ * "content".
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>value</var> is not a string.
+ * @throws {WebDriverError}
+ * If <var>value</var> is not a valid browsing context.
+ */
+GeckoDriver.prototype.setContext = function(cmd) {
+ let value = assert.string(cmd.parameters.value);
+
+ this.context = value;
+};
+
+/**
+ * Gets the context type that is Marionette's current target for
+ * browsing context scoped commands.
+ *
+ * You may choose a context through the {@link #setContext} command.
+ *
+ * The default browsing context is {@link Context.Content}.
+ *
+ * @return {Context}
+ * Current context.
+ */
+GeckoDriver.prototype.getContext = function() {
+ return this.context;
+};
+
+/**
+ * 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 <var>sandboxName</var> 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</code>.
+ * The array items must be serialisable to the WebDriver protocol.
+ * @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
+ * <var>newSandbox</var> 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.
+ *
+ * @return {(string|boolean|number|object|WebElement)}
+ * Return value from the script, or null which signifies either the
+ * JavaScript notion of null or undefined.
+ *
+ * @throws {JavaScriptError}
+ * If an {@link Error} was thrown whilst evaluating the script.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {ScriptTimeoutError}
+ * If the script was interrupted due to reaching the session's
+ * script timeout.
+ */
+GeckoDriver.prototype.executeScript = async function(cmd) {
+ let { script, args } = cmd.parameters;
+ let opts = {
+ script: cmd.parameters.script,
+ args: cmd.parameters.args,
+ sandboxName: cmd.parameters.sandbox,
+ newSandbox: cmd.parameters.newSandbox,
+ file: cmd.parameters.filename,
+ line: cmd.parameters.line,
+ };
+
+ return { value: await this.execute_(script, args, 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 <var>arguments</var>
+ * list passed to the function scope of the script. It can be retrieved
+ * as such:
+ *
+ * <pre><code>
+ * let callback = arguments[arguments.length - 1];
+ * callback("foo");
+ * // "foo" is returned
+ * </code></pre>
+ *
+ * It is important to note that if the <var>sandboxName</var> 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</code>.
+ * The array items must be serialisable to the WebDriver protocol.
+ * @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
+ * <var>newSandbox</var> 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.
+ *
+ * @return {(string|boolean|number|object|WebElement)}
+ * Return value from the script, or null which signifies either the
+ * JavaScript notion of null or undefined.
+ *
+ * @throws {JavaScriptError}
+ * If an Error was thrown whilst evaluating the script.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {ScriptTimeoutError}
+ * If the script was interrupted due to reaching the session's
+ * script timeout.
+ */
+GeckoDriver.prototype.executeAsyncScript = async function(cmd) {
+ let { script, args } = cmd.parameters;
+ let opts = {
+ script: cmd.parameters.script,
+ args: cmd.parameters.args,
+ sandboxName: cmd.parameters.sandbox,
+ newSandbox: cmd.parameters.newSandbox,
+ file: cmd.parameters.filename,
+ line: cmd.parameters.line,
+ async: true,
+ };
+
+ return { value: await this.execute_(script, args, opts) };
+};
+
+GeckoDriver.prototype.execute_ = async function(
+ script,
+ args = [],
+ {
+ sandboxName = null,
+ newSandbox = false,
+ file = "",
+ line = 0,
+ async = false,
+ } = {}
+) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ assert.string(script, pprint`Expected "script" to be a string: ${script}`);
+ assert.array(args, pprint`Expected script args to be an array: ${args}`);
+ if (sandboxName !== null) {
+ assert.string(
+ sandboxName,
+ pprint`Expected sandbox name to be a string: ${sandboxName}`
+ );
+ }
+ assert.boolean(
+ newSandbox,
+ pprint`Expected newSandbox to be boolean: ${newSandbox}`
+ );
+ assert.string(file, pprint`Expected file to be a string: ${file}`);
+ assert.number(line, pprint`Expected line to be a number: ${line}`);
+
+ let opts = {
+ timeout: this.timeouts.script,
+ sandboxName,
+ newSandbox,
+ file,
+ line,
+ async,
+ };
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().executeScript(script, args, opts);
+ }
+
+ let res, els;
+
+ switch (this.context) {
+ case Context.Chrome:
+ let sb = this.sandboxes.get(sandboxName, newSandbox);
+ let wargs = evaluate.fromJSON(args, this.curBrowser.seenEls, sb.window);
+ res = await evaluate.sandbox(sb, script, wargs, opts);
+ els = this.curBrowser.seenEls;
+ break;
+
+ case Context.Content:
+ // evaluate in content with lasting side-effects
+ opts.useSandbox = !!sandboxName;
+ res = await this.listener.executeScript(script, args, opts);
+ break;
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+
+ return evaluate.toJSON(res, els);
+};
+
+/**
+ * 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.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.navigateTo = async function(cmd) {
+ assert.content(this.context);
+ const browsingContext = assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ let validURL;
+ try {
+ validURL = new URL(cmd.parameters.url);
+ } catch (e) {
+ throw new error.InvalidArgumentError(`Malformed URL: ${e.message}`);
+ }
+
+ // Switch to the top-level browsing context before navigating
+ if (MarionettePrefs.useActors) {
+ this.contentBrowsingContext = browsingContext;
+ } else {
+ await this.listener.switchToFrame();
+ }
+
+ const loadEventExpected = navigate.isLoadEventExpected(
+ await this._getCurrentURL(),
+ {
+ future: validURL,
+ }
+ );
+
+ await navigate.waitForNavigationCompleted(
+ this,
+ () => {
+ navigate.navigateTo(browsingContext, validURL);
+ },
+ { loadEventExpected }
+ );
+
+ this.curBrowser.contentBrowser.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.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getCurrentUrl = async function() {
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ const url = await this._getCurrentURL();
+ return url.href;
+};
+
+/**
+ * Gets the current title of the window.
+ *
+ * @return {string}
+ * Document title of the top-level browsing context.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getTitle = async function() {
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ return this.title;
+};
+
+/**
+ * Gets the current type of the window.
+ *
+ * @return {string}
+ * Type of window
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.getWindowType = function() {
+ assert.open(this.getBrowsingContext({ top: true }));
+
+ return this.windowType;
+};
+
+/**
+ * Gets the page source of the content document.
+ *
+ * @return {string}
+ * String serialisation of the DOM of the current browsing context's
+ * active document.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getPageSource = async function() {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getPageSource();
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ const win = this.getCurrentWindow();
+ const s = new win.XMLSerializer();
+ return s.serializeToString(win.document);
+
+ case Context.Content:
+ return this.listener.getPageSource();
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Cause the browser to traverse one step backward in the joint history
+ * of the current browsing context.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.goBack = async function() {
+ assert.content(this.context);
+ const browsingContext = assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ // If there is no history, just return
+ if (!browsingContext.embedderElement?.canGoBack) {
+ return;
+ }
+
+ await navigate.waitForNavigationCompleted(this, () => {
+ browsingContext.goBack();
+ });
+};
+
+/**
+ * Cause the browser to traverse one step forward in the joint history
+ * of the current browsing context.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.goForward = async function() {
+ assert.content(this.context);
+ const browsingContext = assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ // If there is no history, just return
+ if (!browsingContext.embedderElement?.canGoForward) {
+ return;
+ }
+
+ await navigate.waitForNavigationCompleted(this, () => {
+ browsingContext.goForward();
+ });
+};
+
+/**
+ * Causes the browser to reload the page in current top-level browsing
+ * context.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.refresh = async function() {
+ assert.content(this.context);
+ const browsingContext = assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ // Switch to the top-level browsing context before navigating
+ if (MarionettePrefs.useActors) {
+ this.contentBrowsingContext = browsingContext;
+ } else {
+ await this.listener.switchToFrame();
+ }
+
+ await navigate.waitForNavigationCompleted(this, () => {
+ navigate.refresh(browsingContext);
+ });
+};
+
+/**
+ * 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.browsingContext.id;
+ if (winId) {
+ 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.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.getWindowHandle = function() {
+ const browsingContext = assert.open(
+ this.getBrowsingContext({
+ context: Context.Content,
+ top: true,
+ })
+ );
+
+ return browsingContext.id.toString();
+};
+
+/**
+ * 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() {
+ return this.windowHandles.map(String);
+};
+
+/**
+ * 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.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnknownError}
+ * Internal browsing context reference not found
+ */
+GeckoDriver.prototype.getChromeWindowHandle = function() {
+ const browsingContext = assert.open(
+ this.getBrowsingContext({
+ context: Context.Chrome,
+ top: true,
+ })
+ );
+
+ return browsingContext.id.toString();
+};
+
+/**
+ * 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() {
+ return this.chromeWindowHandles.map(String);
+};
+
+/**
+ * Get the current position and 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.
+ *
+ * @return {Object.<string, number>}
+ * Object with |x| and |y| coordinates, and |width| and |height|
+ * of browser window.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getWindowRect = async function() {
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ return this.curBrowser.rect;
+};
+
+/**
+ * Set the window position and size of the browser on the operating
+ * system window manager.
+ *
+ * The supplied `width` and `height` values refer to the window `outerWidth`
+ * and `outerHeight` values, which include browser chrome and OS-level
+ * window borders.
+ *
+ * @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.
+ * @param {number} width
+ * Width to resize the window to.
+ * @param {number} height
+ * Height to resize the window to.
+ *
+ * @return {Object.<string, number>}
+ * Object with `x` and `y` coordinates and `width` and `height`
+ * dimensions.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not applicable to application.
+ */
+GeckoDriver.prototype.setWindowRect = async function(cmd) {
+ assert.firefox();
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ let { x, y, width, height } = cmd.parameters;
+
+ const win = this.getCurrentWindow();
+ switch (WindowState.from(win.windowState)) {
+ case WindowState.Fullscreen:
+ await exitFullscreen(win);
+ break;
+
+ case WindowState.Maximized:
+ case WindowState.Minimized:
+ await restoreWindow(win);
+ break;
+ }
+
+ if (width != null && height != null) {
+ assert.positiveInteger(height);
+ assert.positiveInteger(width);
+
+ if (win.outerWidth != width || win.outerHeight != height) {
+ win.resizeTo(width, height);
+ await new IdlePromise(win);
+ }
+ }
+
+ if (x != null && y != null) {
+ assert.integer(x);
+ assert.integer(y);
+
+ if (win.screenX != x || win.screenY != y) {
+ win.moveTo(x, y);
+ await new IdlePromise(win);
+ }
+ }
+
+ return this.curBrowser.rect;
+};
+
+/**
+ * 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} handle
+ * Handle of the window to switch to.
+ * @param {boolean=} focus
+ * A boolean value which determines whether to focus
+ * the window. Defaults to true.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.switchToWindow = async function(cmd) {
+ const { focus = true, handle } = cmd.parameters;
+
+ assert.string(
+ handle,
+ pprint`Expected "handle" to be a string, got ${handle}`
+ );
+ assert.boolean(focus, pprint`Expected "focus" to be a boolean, got ${focus}`);
+
+ const id = parseInt(handle);
+ const found = this.findWindow(this.windows, (win, winId) => id == winId);
+
+ let selected = false;
+ if (found) {
+ try {
+ await this.setWindowHandle(found, focus);
+ selected = true;
+ } catch (e) {
+ logger.error(e);
+ }
+ }
+
+ if (!selected) {
+ throw new error.NoSuchWindowError(`Unable to locate window: ${handle}`);
+ }
+};
+
+/**
+ * Find a specific window according to some filter function.
+ *
+ * @param {Iterable.<Window>} winIterable
+ * Iterable that emits Window objects.
+ * @param {function(Window, number): boolean} filter
+ * A callback function taking two arguments; the window and
+ * the outerId of the window, and returning a boolean indicating
+ * whether the window is the target.
+ *
+ * @return {Object}
+ * A window handle object containing the window and some
+ * associated metadata.
+ */
+GeckoDriver.prototype.findWindow = function(winIterable, filter) {
+ for (const win of winIterable) {
+ const browsingContext = win.docShell.browsingContext;
+ const tabBrowser = browser.getTabBrowser(win);
+
+ // In case the wanted window is a chrome window, we are done.
+ if (filter(win, browsingContext.id)) {
+ return { win, id: browsingContext.id, hasTabBrowser: !!tabBrowser };
+
+ // Otherwise check if the chrome window has a tab browser, and that it
+ // contains a tab with the wanted window handle.
+ } else if (tabBrowser && tabBrowser.tabs) {
+ for (let i = 0; i < tabBrowser.tabs.length; ++i) {
+ let contentBrowser = browser.getBrowserForTab(tabBrowser.tabs[i]);
+ let contentWindowId = this.getIdForBrowser(contentBrowser);
+
+ if (filter(win, contentWindowId)) {
+ return {
+ win,
+ id: browsingContext.id,
+ hasTabBrowser: true,
+ tabIndex: i,
+ };
+ }
+ }
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Switch the marionette window to a given window. If the browser in
+ * the window is unregistered, register that browser and wait for
+ * the registration is complete. If |focus| is true then set the focus
+ * on the window.
+ *
+ * @param {Object} winProperties
+ * Object containing window properties such as returned from
+ * GeckoDriver#findWindow
+ * @param {boolean=} focus
+ * A boolean value which determines whether to focus the window.
+ * Defaults to true.
+ */
+GeckoDriver.prototype.setWindowHandle = async function(
+ winProperties,
+ focus = true
+) {
+ if (!(winProperties.id 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 (!MarionettePrefs.useActors && winProperties.hasTabBrowser) {
+ registerBrowsers = this.registerPromise();
+ browserListening = this.listeningPromise();
+ }
+
+ this.startBrowser(winProperties.win, false /* isNewSession */);
+
+ this.chromeBrowsingContext = this.mainFrame.browsingContext;
+
+ if (!winProperties.hasTabBrowser) {
+ this.contentBrowsingContext = null;
+ } else if (MarionettePrefs.useActors) {
+ const tabBrowser = browser.getTabBrowser(winProperties.win);
+
+ // For chrome windows such as a reftest window, `getTabBrowser` is not
+ // a tabbrowser, it is the content browser which should be used here.
+ const contentBrowser = tabBrowser.tabs
+ ? tabBrowser.selectedBrowser
+ : tabBrowser;
+
+ this.contentBrowsingContext = contentBrowser.browsingContext;
+ this.registerBrowser(contentBrowser);
+ } else {
+ await registerBrowsers;
+ const id = await browserListening;
+ this.contentBrowsingContext = BrowsingContext.get(id);
+ }
+ } else {
+ // Otherwise switch to the known chrome window
+ this.curBrowser = this.browsers[winProperties.id];
+ this.mainFrame = this.curBrowser.window;
+
+ // Activate the tab if it's a content window.
+ let tab = null;
+ if (winProperties.hasTabBrowser) {
+ tab = await this.curBrowser.switchToTab(
+ winProperties.tabIndex,
+ winProperties.win,
+ focus
+ );
+ }
+
+ this.chromeBrowsingContext = this.mainFrame.browsingContext;
+ this.contentBrowsingContext = tab?.linkedBrowser.browsingContext;
+ }
+
+ if (focus) {
+ await this.curBrowser.focusWindow();
+ }
+};
+
+/**
+ * Set the current browsing context for future commands to the parent
+ * of the current browsing context.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.switchToParentFrame = async function() {
+ let browsingContext = this.getBrowsingContext();
+ if (browsingContext && !browsingContext.parent) {
+ return;
+ }
+
+ browsingContext = assert.open(browsingContext?.parent);
+
+ if (MarionettePrefs.useActors) {
+ this.contentBrowsingContext = browsingContext;
+ return;
+ }
+
+ await this.listener.switchToParentFrame();
+};
+
+/**
+ * Switch to a given frame within the current window.
+ *
+ * @param {(string|Object)=} element
+ * A web element reference of the frame or its element id.
+ * @param {number=} id
+ * The index of the frame to switch to.
+ * If both element and id are not defined, switch to top-level frame.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.switchToFrame = async function(cmd) {
+ const { element: el, id } = cmd.parameters;
+
+ if (typeof id == "number") {
+ assert.unsignedShort(id, `Expected id to be unsigned short, got ${id}`);
+ }
+
+ const top = id == null && el == null;
+ assert.open(this.getBrowsingContext({ top }));
+ await this._handleUserPrompts();
+
+ // Bug 1495063: Elements should be passed as WebElement reference
+ let byFrame;
+ if (typeof el == "string") {
+ byFrame = WebElement.fromUUID(el, this.context);
+ } else if (el) {
+ byFrame = WebElement.fromJSON(el);
+ }
+
+ if (MarionettePrefs.useActors) {
+ const { browsingContext } = await this.getActor({ top }).switchToFrame(
+ byFrame || id
+ );
+
+ this.contentBrowsingContext = browsingContext;
+ return;
+ }
+
+ const checkLoad = function(win) {
+ const otherErrorsExpr = /about:.+(error)|(blocked)\?/;
+
+ return new PollPromise(resolve => {
+ if (win.document.readyState == "complete") {
+ resolve();
+ } else if (win.document.readyState == "interactive") {
+ let documentURI = win.document.documentURI;
+ if (documentURI.startsWith("about:certerror")) {
+ throw new error.InsecureCertificateError();
+ } else if (otherErrorsExpr.exec(documentURI)) {
+ throw new error.UnknownError("Reached error page: " + documentURI);
+ }
+ }
+ });
+ };
+
+ if (this.context == Context.Chrome) {
+ const childContexts = this.getBrowsingContext().children;
+
+ let browsingContext;
+ if (id == null && !byFrame) {
+ browsingContext = this.getBrowsingContext({ top: true });
+ } else if (typeof id == "number") {
+ if (id < 0 || id >= childContexts.length) {
+ throw new error.NoSuchFrameError(
+ `Unable to locate frame with index: ${id}`
+ );
+ }
+ browsingContext = childContexts[id];
+ } else {
+ const wantedFrame = this.curBrowser.seenEls.get(byFrame);
+ const context = childContexts.find(context => {
+ return context.embedderElement === wantedFrame;
+ });
+ if (!context) {
+ throw new error.NoSuchFrameError(
+ `Unable to locate frame for element: ${byFrame}`
+ );
+ }
+ browsingContext = context;
+ }
+
+ this.contentBrowsingContext = browsingContext;
+
+ const frameWindow = browsingContext.window;
+ await checkLoad(frameWindow);
+ } else if (this.context == Context.Content) {
+ cmd.commandID = cmd.id;
+ await this.listener.switchToFrame(cmd.parameters);
+ }
+};
+
+GeckoDriver.prototype.getTimeouts = function() {
+ 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) {
+ // merge with existing timeouts
+ let merged = Object.assign(this.timeouts.toJSON(), cmd.parameters);
+ this.timeouts = Timeouts.fromJSON(merged);
+};
+
+/** Single tap. */
+GeckoDriver.prototype.singleTap = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+
+ let { id, x, y } = cmd.parameters;
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ await this.getActor().singleTap(webEl, x, y, this.capabilities);
+ return;
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ throw new error.UnsupportedOperationError(
+ "Command 'singleTap' is not yet available in chrome context"
+ );
+
+ case Context.Content:
+ await this.listener.singleTap(webEl, x, y, this.capabilities);
+ 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 {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not yet available in current context.
+ */
+GeckoDriver.prototype.performActions = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ const actions = cmd.parameters.actions;
+
+ if (MarionettePrefs.useActors) {
+ await this.getActor().performActions(actions, this.capabilities);
+ return;
+ }
+
+ assert.content(
+ this.context,
+ "Command 'performActions' is not yet available in chrome context"
+ );
+
+ await this.listener.performActions({ actions }, this.capabilities);
+};
+
+/**
+ * Release all the keys and pointer buttons that are currently depressed.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.releaseActions = async function() {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ if (MarionettePrefs.useActors) {
+ await this.getActor().releaseActions();
+ return;
+ }
+
+ assert.content(
+ this.context,
+ "Command 'releaseActions' is not yet available in chrome context"
+ );
+ await this.listener.releaseActions();
+};
+
+/**
+ * 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.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.findElement = async function(cmd) {
+ const { element: el, using, value } = cmd.parameters;
+
+ if (!SUPPORTED_STRATEGIES.has(using)) {
+ throw new error.InvalidSelectorError(`Strategy not supported: ${using}`);
+ }
+
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let startNode;
+ if (typeof el != "undefined") {
+ startNode = WebElement.fromUUID(el, this.context);
+ }
+
+ let opts = {
+ startNode,
+ timeout: this.timeouts.implicit,
+ all: false,
+ };
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().findElement(using, value, opts);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let container = { frame: this.getCurrentWindow() };
+ if (opts.startNode) {
+ opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
+ }
+ let el = await element.find(container, using, value, opts);
+ return this.curBrowser.seenEls.add(el);
+
+ case Context.Content:
+ return this.listener.findElementContent(using, value, opts);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * 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.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ */
+GeckoDriver.prototype.findElements = async function(cmd) {
+ const { element: el, using, value } = cmd.parameters;
+
+ if (!SUPPORTED_STRATEGIES.has(using)) {
+ throw new error.InvalidSelectorError(`Strategy not supported: ${using}`);
+ }
+
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let startNode;
+ if (typeof el != "undefined") {
+ startNode = WebElement.fromUUID(el, this.context);
+ }
+
+ let opts = {
+ startNode,
+ timeout: this.timeouts.implicit,
+ all: true,
+ };
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().findElements(using, value, opts);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let container = { frame: this.getCurrentWindow() };
+ if (startNode) {
+ opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
+ }
+ let els = await element.find(container, using, value, opts);
+ return this.curBrowser.seenEls.addAll(els);
+
+ case Context.Content:
+ return this.listener.findElementsContent(using, value, opts);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Return the active element in the document.
+ *
+ * @return {WebElement}
+ * Active element of the current browsing context's document
+ * element, if the document element is non-null.
+ *
+ * @throws {NoSuchElementError}
+ * If the document does not have an active element, i.e. if
+ * its document element has been deleted.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.getActiveElement = async function() {
+ assert.content(this.context);
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getActiveElement();
+ }
+ return this.listener.getActiveElement();
+};
+
+/**
+ * Send click event to element.
+ *
+ * @param {string} id
+ * Reference ID to the element that will be clicked.
+ *
+ * @throws {InvalidArgumentError}
+ * If element <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.clickElement = async function(cmd) {
+ const browsingContext = assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ const actor = this.getActor();
+
+ const loadEventExpected = navigate.isLoadEventExpected(
+ await this._getCurrentURL(),
+ {
+ browsingContext,
+ target: await actor.getElementAttribute(webEl, "target"),
+ }
+ );
+
+ await navigate.waitForNavigationCompleted(
+ this,
+ () => actor.clickElement(webEl, this.capabilities),
+ {
+ loadEventExpected,
+ // The click might trigger a navigation, so don't count on it.
+ requireBeforeUnload: false,
+ }
+ );
+ return;
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let el = this.curBrowser.seenEls.get(webEl);
+ await interaction.clickElement(el, this.a11yChecks);
+ break;
+
+ case Context.Content:
+ const loadEventExpected = navigate.isLoadEventExpected(
+ await this._getCurrentURL(),
+ {
+ browsingContext,
+ target: this.listener.getElementAttribute(webEl, "target"),
+ }
+ );
+
+ await navigate.waitForNavigationCompleted(
+ this,
+ () => this.listener.clickElement(webEl, this.capabilities),
+ {
+ loadEventExpected,
+ // The click might trigger a navigation, so don't count on it.
+ requireBeforeUnload: false,
+ }
+ );
+ break;
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * 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.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> or <var>name</var> are not strings.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getElementAttribute = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ const id = assert.string(cmd.parameters.id);
+ const name = assert.string(cmd.parameters.name);
+ const webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getElementAttribute(webEl, name);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let el = this.curBrowser.seenEls.get(webEl);
+ return el.getAttribute(name);
+
+ case Context.Content:
+ return this.listener.getElementAttribute(webEl, name);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * 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.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> or <var>name</var> are not strings.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getElementProperty = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ const id = assert.string(cmd.parameters.id);
+ const name = assert.string(cmd.parameters.name);
+ const webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getElementProperty(webEl, name);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let el = this.curBrowser.seenEls.get(webEl);
+ return evaluate.toJSON(el[name], this.curBrowser.seenEls);
+
+ case Context.Content:
+ return this.listener.getElementProperty(webEl, name);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * 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.
+ *
+ * @return {string}
+ * Element's text "as rendered".
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getElementText = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getElementText(webEl);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ // for chrome, we look at text nodes, and any node with a "label" field
+ let el = this.curBrowser.seenEls.get(webEl);
+ let lines = [];
+ this.getVisibleText(el, lines);
+ return lines.join("\n");
+
+ case Context.Content:
+ return this.listener.getElementText(webEl);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Get the tag name of the element.
+ *
+ * @param {string} id
+ * Reference ID to the element that will be inspected.
+ *
+ * @return {string}
+ * Local tag name of element.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getElementTagName = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getElementTagName(webEl);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let el = this.curBrowser.seenEls.get(webEl);
+ return el.tagName.toLowerCase();
+
+ case Context.Content:
+ return this.listener.getElementTagName(webEl);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Check if element is displayed.
+ *
+ * @param {string} id
+ * Reference ID to the element that will be inspected.
+ *
+ * @return {boolean}
+ * True if displayed, false otherwise.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.isElementDisplayed = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().isElementDisplayed(webEl, this.capabilities);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let el = this.curBrowser.seenEls.get(webEl);
+ return interaction.isElementDisplayed(el, this.a11yChecks);
+
+ case Context.Content:
+ return this.listener.isElementDisplayed(webEl, this.capabilities);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * 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.
+ *
+ * @return {string}
+ * Value of |propertyName|.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> or <var>propertyName</var> are not strings.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getElementValueOfCssProperty = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let prop = assert.string(cmd.parameters.propertyName);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getElementValueOfCssProperty(webEl, prop);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ const win = this.getCurrentWindow();
+ const el = this.curBrowser.seenEls.get(webEl);
+ const style = win.document.defaultView.getComputedStyle(el);
+ return style.getPropertyValue(prop);
+
+ case Context.Content:
+ return this.listener.getElementValueOfCssProperty(webEl, prop);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Check if element is enabled.
+ *
+ * @param {string} id
+ * Reference ID to the element that will be checked.
+ *
+ * @return {boolean}
+ * True if enabled, false if disabled.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.isElementEnabled = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().isElementEnabled(webEl, this.capabilities);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ // Selenium atom doesn't quite work here
+ let el = this.curBrowser.seenEls.get(webEl);
+ return interaction.isElementEnabled(el, this.a11yChecks);
+
+ case Context.Content:
+ return this.listener.isElementEnabled(webEl, this.capabilities);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Check if element is selected.
+ *
+ * @param {string} id
+ * Reference ID to the element that will be checked.
+ *
+ * @return {boolean}
+ * True if selected, false if unselected.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.isElementSelected = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().isElementSelected(webEl, this.capabilities);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ // Selenium atom doesn't quite work here
+ let el = this.curBrowser.seenEls.get(webEl);
+ return interaction.isElementSelected(el, this.a11yChecks);
+
+ case Context.Content:
+ return this.listener.isElementSelected(webEl, this.capabilities);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.getElementRect = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().getElementRect(webEl);
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ const win = this.getCurrentWindow();
+ const el = this.curBrowser.seenEls.get(webEl);
+ const rect = el.getBoundingClientRect();
+ return {
+ x: rect.x + win.pageXOffset,
+ y: rect.y + win.pageYOffset,
+ width: rect.width,
+ height: rect.height,
+ };
+
+ case Context.Content:
+ return this.listener.getElementRect(webEl);
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Send key presses to element after focusing on it.
+ *
+ * @param {string} id
+ * Reference ID to the element that will be checked.
+ * @param {string} text
+ * Value to send to the element.
+ *
+ * @throws {InvalidArgumentError}
+ * If `id` or `text` are not strings.
+ * @throws {NoSuchElementError}
+ * If element represented by reference `id` is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.sendKeysToElement = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let text = assert.string(cmd.parameters.text);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ await this.getActor().sendKeysToElement(webEl, text, this.capabilities);
+ return;
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ let el = this.curBrowser.seenEls.get(webEl);
+ await interaction.sendKeysToElement(el, text, {
+ accessibilityChecks: this.a11yChecks,
+ });
+ break;
+
+ case Context.Content:
+ await this.listener.sendKeysToElement(webEl, text, this.capabilities);
+ break;
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Clear the text of an element.
+ *
+ * @param {string} id
+ * Reference ID to the element that will be cleared.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ * If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.clearElement = async function(cmd) {
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let id = assert.string(cmd.parameters.id);
+ let webEl = WebElement.fromUUID(id, this.context);
+
+ if (MarionettePrefs.useActors) {
+ await this.getActor().clearElement(webEl);
+ return;
+ }
+
+ switch (this.context) {
+ case Context.Chrome:
+ // the selenium atom doesn't work here
+ let el = this.curBrowser.seenEls.get(webEl);
+ if (el.nodeName == "input" && el.type == "text") {
+ el.value = "";
+ } else if (el.nodeName == "checkbox") {
+ el.checked = false;
+ }
+ break;
+
+ case Context.Content:
+ await this.listener.clearElement(webEl);
+ break;
+
+ default:
+ throw new TypeError(`Unknown context: ${this.context}`);
+ }
+};
+
+/**
+ * Add a single cookie to the cookie store associated with the active
+ * document's address.
+ *
+ * @param {Map.<string, (string|number|boolean)> cookie
+ * Cookie object.
+ *
+ * @throws {InvalidCookieDomainError}
+ * If <var>cookie</var> is for a different domain than the active
+ * document's host.
+ * @throws {NoSuchWindowError}
+ * Bbrowsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.addCookie = async function(cmd) {
+ assert.content(this.context);
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let { protocol, hostname } = await this._getCurrentURL();
+
+ const networkSchemes = ["ftp:", "http:", "https:"];
+ if (!networkSchemes.includes(protocol)) {
+ throw new error.InvalidCookieDomainError("Document is cookie-averse");
+ }
+
+ let newCookie = cookie.fromJSON(cmd.parameters.cookie);
+
+ cookie.add(newCookie, { restrictToHost: hostname, protocol });
+};
+
+/**
+ * Get all the cookies for the current domain.
+ *
+ * This is the equivalent of calling <code>document.cookie</code> and
+ * parsing the result.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.getCookies = async function() {
+ assert.content(this.context);
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let { hostname, pathname } = await this._getCurrentURL();
+ return [...cookie.iter(hostname, pathname)];
+};
+
+/**
+ * Delete all cookies that are visible to a document.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.deleteAllCookies = async function() {
+ assert.content(this.context);
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let { hostname, pathname } = await this._getCurrentURL();
+ for (let toDelete of cookie.iter(hostname, pathname)) {
+ cookie.remove(toDelete);
+ }
+};
+
+/**
+ * Delete a cookie by name.
+ *
+ * @throws {NoSuchWindowError}
+ * Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available in current context.
+ */
+GeckoDriver.prototype.deleteCookie = async function(cmd) {
+ assert.content(this.context);
+ assert.open(this.getBrowsingContext());
+ await this._handleUserPrompts();
+
+ let { hostname, pathname } = await this._getCurrentURL();
+ let name = assert.string(cmd.parameters.name);
+ for (let c of cookie.iter(hostname, pathname)) {
+ if (c.name === name) {
+ cookie.remove(c);
+ }
+ }
+};
+
+/**
+ * Open a new top-level browsing context.
+ *
+ * @param {string=} type
+ * Optional type of the new top-level browsing context. Can be one of
+ * `tab` or `window`. Defaults to `tab`.
+ * @param {boolean=} focus
+ * Optional flag if the new top-level browsing context should be opened
+ * in foreground (focused) or background (not focused). Defaults to false.
+ * @param {boolean=} private
+ * Optional flag, which gets only evaluated for type `window`. True if the
+ * new top-level browsing context should be a private window.
+ * Defaults to false.
+ *
+ * @return {Object.<string, string>}
+ * Handle and type of the new browsing context.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.newWindow = async function(cmd) {
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ let focus = false;
+ if (typeof cmd.parameters.focus != "undefined") {
+ focus = assert.boolean(
+ cmd.parameters.focus,
+ pprint`Expected "focus" to be a boolean, got ${cmd.parameters.focus}`
+ );
+ }
+
+ let isPrivate = false;
+ if (typeof cmd.parameters.private != "undefined") {
+ isPrivate = assert.boolean(
+ cmd.parameters.private,
+ pprint`Expected "private" to be a boolean, got ${cmd.parameters.private}`
+ );
+ }
+
+ let type;
+ if (typeof cmd.parameters.type != "undefined") {
+ type = assert.string(
+ cmd.parameters.type,
+ pprint`Expected "type" to be a string, got ${cmd.parameters.type}`
+ );
+ }
+
+ // If an invalid or no type has been specified default to a tab.
+ if (typeof type == "undefined" || !["tab", "window"].includes(type)) {
+ type = "tab";
+ }
+
+ let contentBrowser;
+
+ let onBrowserContentLoaded;
+ if (MarionettePrefs.useActors) {
+ // Actors need the new window to be loaded to safely execute queries.
+ // Wait until a load event is dispatched for the new browsing context.
+ onBrowserContentLoaded = waitForLoadEvent(
+ "pageshow",
+ () => contentBrowser?.browsingContext
+ );
+ }
+
+ switch (type) {
+ case "window":
+ let win = await this.curBrowser.openBrowserWindow(focus, isPrivate);
+ contentBrowser = browser.getTabBrowser(win).selectedBrowser;
+ break;
+
+ default:
+ // To not fail if a new type gets added in the future, make opening
+ // a new tab the default action.
+ let tab = await this.curBrowser.openTab(focus);
+ contentBrowser = browser.getBrowserForTab(tab);
+ }
+
+ await onBrowserContentLoaded;
+
+ // Even with the framescript registered, the browser might not be known to
+ // the parent process yet. Wait until it is available.
+ // TODO: Fix by using `Browser:Init` or equivalent on bug 1311041
+ let windowId = await new PollPromise((resolve, reject) => {
+ let id = this.getIdForBrowser(contentBrowser);
+ this.windowHandles.includes(id) ? resolve(id) : reject();
+ });
+
+ return { handle: windowId.toString(), type };
+};
+
+/**
+ * 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.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ */
+GeckoDriver.prototype.close = async function() {
+ assert.open(this.getBrowsingContext({ context: Context.Content, top: true }));
+ await this._handleUserPrompts();
+
+ let nwins = 0;
+
+ for (let win of this.windows) {
+ // For browser windows count the tabs. Otherwise take the window itself.
+ let tabbrowser = browser.getTabBrowser(win);
+ if (tabbrowser && tabbrowser.tabs) {
+ nwins += tabbrowser.tabs.length;
+ } else {
+ nwins += 1;
+ }
+ }
+
+ // If there is only one 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 [];
+ }
+
+ await this.curBrowser.closeTab();
+ this.contentBrowsingContext = null;
+
+ return this.windowHandles.map(String);
+};
+
+/**
+ * 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.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.closeChromeWindow = async function() {
+ assert.firefox();
+ assert.open(this.getBrowsingContext({ context: Context.Chrome, top: true }));
+
+ let nwins = 0;
+
+ // eslint-disable-next-line
+ for (let _ of this.windows) {
+ nwins++;
+ }
+
+ // If there is only one 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 [];
+ }
+
+ await this.curBrowser.closeWindow();
+ this.chromeBrowsingContext = null;
+ this.contentBrowsingContext = null;
+
+ return this.chromeWindowHandles.map(String);
+};
+
+/** Delete Marionette session. */
+GeckoDriver.prototype.deleteSession = function() {
+ if (MarionettePrefs.useActors) {
+ clearActionInputState();
+ clearElementIdCache();
+
+ unregisterCommandsActor();
+ unregisterEventsActor();
+ } else if (this.curBrowser !== null) {
+ // frame scripts can be safely reused
+ MarionettePrefs.contentListener = false;
+
+ globalMessageManager.broadcastAsyncMessage("Marionette:Session:Delete");
+ globalMessageManager.broadcastAsyncMessage("Marionette:Deregister");
+
+ for (let win of this.windows) {
+ if (win.messageManager) {
+ win.messageManager.removeDelayedFrameScript(FRAME_SCRIPT);
+ } else {
+ logger.error(
+ `Could not remove listener from page ${win.location.href}`
+ );
+ }
+ }
+ }
+
+ // reset to the top-most frame, and clear browsing context references
+ this.mainFrame = null;
+ this.chromeBrowsingContext = null;
+ this.contentBrowsingContext = null;
+
+ if (this.dialogObserver) {
+ this.dialogObserver.cleanup();
+ this.dialogObserver = null;
+ }
+
+ try {
+ Services.obs.removeObserver(this, "browsing-context-attached");
+ } catch (e) {}
+
+ this.sandboxes.clear();
+ allowAllCerts.disable();
+
+ this.sessionID = null;
+ this.capabilities = new Capabilities();
+};
+
+/**
+ * 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 |id| 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 {boolean=} full
+ * True to take a screenshot of the entire document element. Is only
+ * considered if <var>id</var> is not defined. Defaults to true.
+ * @param {boolean=} hash
+ * True if the user requests a hash of the image data. Defaults to false.
+ * @param {boolean=} scroll
+ * Scroll to element if |id| is provided. Defaults to true.
+ *
+ * @return {string}
+ * If <var>hash</var> is false, PNG image encoded as Base64 encoded
+ * string. If <var>hash</var> is true, hex digest of the SHA-256
+ * hash of the Base64 encoded string.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.takeScreenshot = async function(cmd) {
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ let { id, full, hash, scroll } = cmd.parameters;
+ let format = hash ? capture.Format.Hash : capture.Format.Base64;
+
+ full = typeof full == "undefined" ? true : full;
+ scroll = typeof scroll == "undefined" ? true : scroll;
+
+ let webEl = id ? WebElement.fromUUID(id, this.context) : null;
+
+ // Only consider full screenshot if no element has been specified
+ full = webEl ? false : full;
+
+ if (MarionettePrefs.useActors) {
+ return this.getActor().takeScreenshot(webEl, format, full, scroll);
+ }
+
+ const win = this.getCurrentWindow();
+
+ let rect;
+ switch (this.context) {
+ case Context.Chrome:
+ if (id) {
+ let el = this.curBrowser.seenEls.get(webEl, win);
+ rect = el.getBoundingClientRect();
+ } else if (full) {
+ const docEl = win.document.documentElement;
+ rect = new DOMRect(0, 0, docEl.scrollWidth, docEl.scrollHeight);
+ } else {
+ // viewport
+ rect = new win.DOMRect(
+ win.pageXOffset,
+ win.pageYOffset,
+ win.innerWidth,
+ win.innerHeight
+ );
+ }
+ break;
+
+ case Context.Content:
+ rect = await this.listener.getScreenshotRect({ el: webEl, full, scroll });
+ break;
+ }
+
+ // If no element has been specified use the top-level browsing context.
+ // Otherwise use the browsing context from the currently selected frame.
+ const browsingContext = this.getBrowsingContext({ top: !webEl });
+
+ let canvas = await capture.canvas(
+ win,
+ browsingContext,
+ rect.x,
+ rect.y,
+ rect.width,
+ rect.height
+ );
+
+ switch (format) {
+ case capture.Format.Hash:
+ return capture.toHash(canvas);
+
+ case capture.Format.Base64:
+ return capture.toBase64(canvas);
+ }
+
+ throw new TypeError(`Unknown context: ${this.context}`);
+};
+
+/**
+ * Get the current browser orientation.
+ *
+ * Will return one of the valid primary orientation values
+ * portrait-primary, landscape-primary, portrait-secondary, or
+ * landscape-secondary.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.getScreenOrientation = function() {
+ assert.fennec();
+ assert.open(this.getBrowsingContext({ top: true }));
+
+ const win = this.getCurrentWindow();
+
+ return win.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".
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.setScreenOrientation = function(cmd) {
+ assert.fennec();
+ assert.open(this.getBrowsingContext({ top: true }));
+
+ 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 error.InvalidArgumentError(`Unknown screen orientation: ${or}`);
+ }
+
+ const win = this.getCurrentWindow();
+ if (!win.screen.mozLockOrientation(mozOr)) {
+ throw new error.WebDriverError(`Unable to set screen orientation: ${or}`);
+ }
+};
+
+/**
+ * Synchronously minimizes the user agent window as if the user pressed
+ * the minimize button.
+ *
+ * No action is taken if the window is already minimized.
+ *
+ * Not supported on Fennec.
+ *
+ * @return {Object.<string, number>}
+ * Window rect and window state.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available for current application.
+ */
+GeckoDriver.prototype.minimizeWindow = async function() {
+ assert.firefox();
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ const win = this.getCurrentWindow();
+ switch (WindowState.from(win.windowState)) {
+ case WindowState.Fullscreen:
+ await exitFullscreen(win);
+ break;
+
+ case WindowState.Maximized:
+ await restoreWindow(win);
+ break;
+ }
+
+ if (WindowState.from(win.windowState) != WindowState.Minimized) {
+ let cb;
+ let observer = new WebElementEventTarget(this.curBrowser.messageManager);
+ // Use a timed promise to abort if no window manager is present
+ await new TimedPromise(
+ resolve => {
+ cb = new DebounceCallback(resolve);
+ observer.addEventListener("visibilitychange", cb);
+ win.minimize();
+ },
+ { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER }
+ );
+ observer.removeEventListener("visibilitychange", cb);
+ await new IdlePromise(win);
+ }
+
+ return this.curBrowser.rect;
+};
+
+/**
+ * Synchronously maximizes the user agent window as if the user pressed
+ * the maximize button.
+ *
+ * No action is taken if the window is already maximized.
+ *
+ * Not supported on Fennec.
+ *
+ * @return {Object.<string, number>}
+ * Window rect.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available for current application.
+ */
+GeckoDriver.prototype.maximizeWindow = async function() {
+ assert.firefox();
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ const win = this.getCurrentWindow();
+ switch (WindowState.from(win.windowState)) {
+ case WindowState.Fullscreen:
+ await exitFullscreen(win);
+ break;
+
+ case WindowState.Minimized:
+ await restoreWindow(win);
+ break;
+ }
+
+ if (WindowState.from(win.windowState) != WindowState.Maximized) {
+ let cb;
+ // Use a timed promise to abort if no window manager is present
+ await new TimedPromise(
+ resolve => {
+ cb = new DebounceCallback(resolve);
+ win.addEventListener("sizemodechange", cb);
+ win.maximize();
+ },
+ { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER }
+ );
+ win.removeEventListener("sizemodechange", cb);
+ await new IdlePromise(win);
+ }
+
+ return this.curBrowser.rect;
+};
+
+/**
+ * Synchronously sets the user agent window to full screen as if the user
+ * had done "View > Enter Full Screen".
+ *
+ * No action is taken if the window is already in full screen mode.
+ *
+ * Not supported on Fennec.
+ *
+ * @return {Map.<string, number>}
+ * Window rect.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ * A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ * Not available for current application.
+ */
+GeckoDriver.prototype.fullscreenWindow = async function() {
+ assert.firefox();
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ const win = this.getCurrentWindow();
+ switch (WindowState.from(win.windowState)) {
+ case WindowState.Maximized:
+ case WindowState.Minimized:
+ await restoreWindow(win);
+ break;
+ }
+
+ if (WindowState.from(win.windowState) != WindowState.Fullscreen) {
+ let cb;
+ // Use a timed promise to abort if no window manager is present
+ await new TimedPromise(
+ resolve => {
+ cb = new DebounceCallback(resolve);
+ win.addEventListener("sizemodechange", cb);
+ win.fullScreen = true;
+ },
+ { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER }
+ );
+ win.removeEventListener("sizemodechange", cb);
+ }
+ await new IdlePromise(win);
+
+ return this.curBrowser.rect;
+};
+
+/**
+ * Dismisses a currently displayed tab modal, or returns no such alert if
+ * no modal is displayed.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.dismissDialog = async function() {
+ assert.open(this.getBrowsingContext({ top: true }));
+ this._checkIfAlertIsPresent();
+
+ const win = this.getCurrentWindow();
+ const dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
+
+ const { button0, button1 } = this.dialog.ui;
+ (button1 ? button1 : button0).click();
+
+ await dialogClosed;
+ await new IdlePromise(win);
+};
+
+/**
+ * Accepts a currently displayed tab modal, or returns no such alert if
+ * no modal is displayed.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.acceptDialog = async function() {
+ assert.open(this.getBrowsingContext({ top: true }));
+ this._checkIfAlertIsPresent();
+
+ const win = this.getCurrentWindow();
+ const dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
+
+ const { button0 } = this.dialog.ui;
+ button0.click();
+
+ await dialogClosed;
+ await new IdlePromise(win);
+};
+
+/**
+ * Returns the message shown in a currently displayed modal, or returns
+ * a no such alert error if no modal is currently displayed.
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.getTextFromDialog = function() {
+ assert.open(this.getBrowsingContext({ top: true }));
+ this._checkIfAlertIsPresent();
+
+ return this.dialog.ui.infoBody.textContent;
+};
+
+/**
+ * Set the user prompt's value field.
+ *
+ * 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.
+ *
+ * @param {string} text
+ * Input to the user prompt's value field.
+ *
+ * @throws {ElementNotInteractableError}
+ * If the current user prompt is an alert or confirm.
+ * @throws {NoSuchAlertError}
+ * If there is no current user prompt.
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ * @throws {UnsupportedOperationError}
+ * If the current user prompt is something other than an alert,
+ * confirm, or a prompt.
+ */
+GeckoDriver.prototype.sendKeysToDialog = async function(cmd) {
+ assert.open(this.getBrowsingContext({ top: true }));
+ this._checkIfAlertIsPresent();
+
+ let text = assert.string(cmd.parameters.text);
+ let promptType = this.dialog.args.promptType;
+
+ switch (promptType) {
+ case "alert":
+ case "confirm":
+ throw new error.ElementNotInteractableError(
+ `User prompt of type ${promptType} is not interactable`
+ );
+ case "prompt":
+ break;
+ default:
+ await this.dismissDialog();
+ throw new error.UnsupportedOperationError(
+ `User prompt of type ${promptType} is not supported`
+ );
+ }
+
+ // see toolkit/components/prompts/content/commonDialog.js
+ let { loginTextbox } = this.dialog.ui;
+ loginTextbox.value = text;
+};
+
+GeckoDriver.prototype._checkIfAlertIsPresent = function() {
+ if (!this.dialog || !this.dialog.ui) {
+ throw new error.NoSuchAlertError();
+ }
+};
+
+GeckoDriver.prototype._handleUserPrompts = async function() {
+ if (!this.dialog || !this.dialog.ui) {
+ return;
+ }
+
+ let { textContent } = this.dialog.ui.infoBody;
+
+ let behavior = this.capabilities.get("unhandledPromptBehavior");
+ switch (behavior) {
+ case UnhandledPromptBehavior.Accept:
+ await this.acceptDialog();
+ break;
+
+ case UnhandledPromptBehavior.AcceptAndNotify:
+ await this.acceptDialog();
+ throw new error.UnexpectedAlertOpenError(
+ `Accepted user prompt dialog: ${textContent}`
+ );
+
+ case UnhandledPromptBehavior.Dismiss:
+ await this.dismissDialog();
+ break;
+
+ case UnhandledPromptBehavior.DismissAndNotify:
+ await this.dismissDialog();
+ throw new error.UnexpectedAlertOpenError(
+ `Dismissed user prompt dialog: ${textContent}`
+ );
+
+ case UnhandledPromptBehavior.Ignore:
+ throw new error.UnexpectedAlertOpenError(
+ "Encountered unhandled user prompt dialog"
+ );
+
+ default:
+ throw new TypeError(`Unknown unhandledPromptBehavior "${behavior}"`);
+ }
+};
+
+/**
+ * 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) {
+ assert.boolean(cmd.parameters.value);
+ this._server.acceptConnections = cmd.parameters.value;
+};
+
+/**
+ * Quits the application with the provided flags.
+ *
+ * Marionette will stop accepting new connections before ending the
+ * current session, and finally attempting to quit the application.
+ *
+ * Optional {@link nsIAppStartup} flags may be provided as
+ * an array of masks, and these will be combined by ORing
+ * them with a bitmask. The available masks are defined in
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIAppStartup.
+ *
+ * Crucially, only one of the *Quit flags can be specified. The |eRestart|
+ * flag may be bit-wise combined with one of the *Quit flags to cause
+ * the application to restart after it quits.
+ *
+ * @param {Array.<string>=} flags
+ * Constant name of masks to pass to |Services.startup.quit|.
+ * If empty or undefined, |nsIAppStartup.eAttemptQuit| is used.
+ *
+ * @return {string}
+ * Explaining the reason why the application quit. This can be
+ * in response to a normal shutdown or restart, yielding "shutdown"
+ * or "restart", respectively.
+ *
+ * @throws {InvalidArgumentError}
+ * If <var>flags</var> contains unknown or incompatible flags,
+ * for example multiple Quit flags.
+ */
+GeckoDriver.prototype.quit = async function(cmd) {
+ const quits = ["eConsiderQuit", "eAttemptQuit", "eForceQuit"];
+
+ let flags = [];
+ if (typeof cmd.parameters.flags != "undefined") {
+ flags = assert.array(cmd.parameters.flags);
+ }
+
+ let quitSeen;
+ let mode = 0;
+ if (flags.length > 0) {
+ for (let k of flags) {
+ assert.in(k, Ci.nsIAppStartup);
+
+ if (quits.includes(k)) {
+ if (quitSeen) {
+ throw new error.InvalidArgumentError(
+ `${k} cannot be combined with ${quitSeen}`
+ );
+ }
+ quitSeen = k;
+ }
+
+ mode |= Ci.nsIAppStartup[k];
+ }
+ } else {
+ mode = Ci.nsIAppStartup.eAttemptQuit;
+ }
+
+ this._server.acceptConnections = false;
+ this.deleteSession();
+
+ // delay response until the application is about to quit
+ let quitApplication = waitForObserverTopic("quit-application");
+ Services.startup.quit(mode);
+
+ return { cause: (await quitApplication).data };
+};
+
+GeckoDriver.prototype.installAddon = function(cmd) {
+ assert.desktop();
+
+ let path = cmd.parameters.path;
+ let temp = cmd.parameters.temporary || false;
+ if (
+ typeof path == "undefined" ||
+ typeof path != "string" ||
+ typeof temp != "boolean"
+ ) {
+ throw new error.InvalidArgumentError();
+ }
+
+ return Addon.install(path, temp);
+};
+
+GeckoDriver.prototype.uninstallAddon = function(cmd) {
+ assert.firefox();
+
+ let id = cmd.parameters.id;
+ if (typeof id == "undefined" || typeof id != "string") {
+ throw new error.InvalidArgumentError();
+ }
+
+ return Addon.uninstall(id);
+};
+
+/** Receives all messages from content messageManager. */
+/* eslint-disable consistent-return */
+GeckoDriver.prototype.receiveMessage = function(message) {
+ switch (message.name) {
+ case "Marionette:switchedToFrame":
+ this.contentBrowsingContext = BrowsingContext.get(
+ message.json.browsingContextId
+ );
+ break;
+
+ case "Marionette:Register":
+ this.registerBrowser(message.target);
+ return { frameId: message.json.frameId };
+
+ case "Marionette:ListenersAttached":
+ if (message.json.frameId === this.curBrowser.curFrameId) {
+ const browsingContext = BrowsingContext.get(message.json.frameId);
+
+ // If the framescript for the current content browsing context
+ // has been re-attached due to a remoteness change (the browserId is
+ // always persistent) then track the new browsing context.
+ if (
+ browsingContext.browserId == this.contentBrowsingContext?.browserId
+ ) {
+ logger.trace(
+ "Detected remoteness change. New browsing context: " +
+ browsingContext.id
+ );
+ this.contentBrowsingContext = browsingContext;
+ }
+ }
+ break;
+ }
+};
+/* eslint-enable consistent-return */
+
+/**
+ * Retrieve the localized string for the specified entity id.
+ *
+ * Example:
+ * localizeEntity(["chrome://branding/locale/brand.dtd"], "brandShortName")
+ *
+ * @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) {
+ let { urls, id } = cmd.parameters;
+
+ if (!Array.isArray(urls)) {
+ throw new error.InvalidArgumentError(
+ "Value of `urls` should be of type 'Array'"
+ );
+ }
+ if (typeof id != "string") {
+ throw new error.InvalidArgumentError(
+ "Value of `id` should be of type 'string'"
+ );
+ }
+
+ return 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) {
+ let { urls, id } = cmd.parameters;
+
+ if (!Array.isArray(urls)) {
+ throw new error.InvalidArgumentError(
+ "Value of `urls` should be of type 'Array'"
+ );
+ }
+ if (typeof id != "string") {
+ throw new error.InvalidArgumentError(
+ "Value of `id` should be of type 'string'"
+ );
+ }
+
+ return l10n.localizeProperty(urls, id);
+};
+
+/**
+ * Initialize the reftest mode
+ */
+GeckoDriver.prototype.setupReftest = async function(cmd) {
+ if (this._reftest) {
+ throw new error.UnsupportedOperationError(
+ "Called reftest:setup with a reftest session already active"
+ );
+ }
+
+ let {
+ urlCount = {},
+ screenshot = "unexpected",
+ isPrint = false,
+ } = cmd.parameters;
+ if (!["always", "fail", "unexpected"].includes(screenshot)) {
+ throw new error.InvalidArgumentError(
+ "Value of `screenshot` should be 'always', 'fail' or 'unexpected'"
+ );
+ }
+
+ this._reftest = new reftest.Runner(this);
+ this._reftest.setup(urlCount, screenshot, isPrint);
+};
+
+/** Run a reftest. */
+GeckoDriver.prototype.runReftest = async function(cmd) {
+ let {
+ test,
+ references,
+ expected,
+ timeout,
+ width,
+ height,
+ pageRanges,
+ } = cmd.parameters;
+
+ if (!this._reftest) {
+ throw new error.UnsupportedOperationError(
+ "Called reftest:run before reftest:start"
+ );
+ }
+
+ assert.string(test);
+ assert.string(expected);
+ assert.array(references);
+
+ return {
+ value: await this._reftest.run(
+ test,
+ references,
+ expected,
+ timeout,
+ pageRanges,
+ width,
+ height
+ ),
+ };
+};
+
+/**
+ * End a reftest run.
+ *
+ * Closes the reftest window (without changing the current window handle),
+ * and removes cached canvases.
+ */
+GeckoDriver.prototype.teardownReftest = function() {
+ if (!this._reftest) {
+ throw new error.UnsupportedOperationError(
+ "Called reftest:teardown before reftest:start"
+ );
+ }
+
+ this._reftest.teardown();
+ this._reftest = null;
+};
+
+/**
+ * Print page as PDF.
+ *
+ * @param {boolean=} landscape
+ * Paper orientation. Defaults to false.
+ * @param {number=} margin.bottom
+ * Bottom margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {number=} margin.left
+ * Left margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {number=} margin.right
+ * Right margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {number=} margin.top
+ * Top margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {string=} pageRanges (not supported)
+ * Paper ranges to print, e.g., '1-5, 8, 11-13'.
+ * Defaults to the empty string, which means print all pages.
+ * @param {number=} page.height
+ * Paper height in cm. Defaults to US letter height (11 inches / 27.94cm)
+ * @param {number=} page.width
+ * Paper width in cm. Defaults to US letter width (8.5 inches / 21.59cm)
+ * @param {boolean=} shrinkToFit
+ * Whether or not to override page size as defined by CSS.
+ * Defaults to true, in which case the content will be scaled
+ * to fit the paper size.
+ * @param {boolean=} printBackground
+ * Print background graphics. Defaults to false.
+ * @param {number=} scale
+ * Scale of the webpage rendering. Defaults to 1.
+ *
+ * @return {string}
+ * Base64 encoded PDF representing printed document
+ *
+ * @throws {NoSuchWindowError}
+ * Top-level browsing context has been discarded.
+ */
+GeckoDriver.prototype.print = async function(cmd) {
+ assert.content(this.context);
+ assert.open(this.getBrowsingContext({ top: true }));
+ await this._handleUserPrompts();
+
+ const settings = print.addDefaultSettings(cmd.parameters);
+ for (let prop of ["top", "bottom", "left", "right"]) {
+ assert.positiveNumber(
+ settings.margin[prop],
+ pprint`margin.${prop} is not a positive number`
+ );
+ }
+ for (let prop of ["width", "height"]) {
+ assert.positiveNumber(
+ settings.page[prop],
+ pprint`page.${prop} is not a positive number`
+ );
+ }
+ assert.positiveNumber(
+ settings.scale,
+ `scale ${settings.scale} is not a positive number`
+ );
+ assert.that(
+ s => s >= print.minScaleValue && settings.scale <= print.maxScaleValue,
+ `scale ${settings.scale} is outside the range ${print.minScaleValue}-${print.maxScaleValue}`
+ )(settings.scale);
+ assert.boolean(settings.shrinkToFit);
+ assert.boolean(settings.landscape);
+ assert.boolean(settings.printBackground);
+
+ const linkedBrowser = this.curBrowser.tab.linkedBrowser;
+ const filePath = await print.printToFile(
+ linkedBrowser,
+ linkedBrowser.outerWindowID,
+ settings
+ );
+
+ // return all data as a base64 encoded string
+ let bytes;
+ const file = await OS.File.open(filePath);
+ try {
+ bytes = await file.read();
+ } finally {
+ file.close();
+ await OS.File.remove(filePath);
+ }
+
+ // Each UCS2 character has an upper byte of 0 and a lower byte matching
+ // the binary data
+ return {
+ value: btoa(String.fromCharCode.apply(null, bytes)),
+ };
+};
+
+GeckoDriver.prototype.commands = {
+ // Marionette service
+ "Marionette:AcceptConnections": GeckoDriver.prototype.acceptConnections,
+ "Marionette:GetContext": GeckoDriver.prototype.getContext,
+ "Marionette:GetScreenOrientation": GeckoDriver.prototype.getScreenOrientation,
+ "Marionette:GetWindowType": GeckoDriver.prototype.getWindowType,
+ "Marionette:Quit": GeckoDriver.prototype.quit,
+ "Marionette:SetContext": GeckoDriver.prototype.setContext,
+ "Marionette:SetScreenOrientation": GeckoDriver.prototype.setScreenOrientation,
+ "Marionette:SingleTap": GeckoDriver.prototype.singleTap,
+
+ // Addon service
+ "Addon:Install": GeckoDriver.prototype.installAddon,
+ "Addon:Uninstall": GeckoDriver.prototype.uninstallAddon,
+
+ // L10n service
+ "L10n:LocalizeEntity": GeckoDriver.prototype.localizeEntity,
+ "L10n:LocalizeProperty": GeckoDriver.prototype.localizeProperty,
+
+ // Reftest service
+ "reftest:setup": GeckoDriver.prototype.setupReftest,
+ "reftest:run": GeckoDriver.prototype.runReftest,
+ "reftest:teardown": GeckoDriver.prototype.teardownReftest,
+
+ // WebDriver service
+ "WebDriver:AcceptAlert": GeckoDriver.prototype.acceptDialog,
+ "WebDriver:AcceptDialog": GeckoDriver.prototype.acceptDialog, // deprecated, but used in geckodriver (see also bug 1495063)
+ "WebDriver:AddCookie": GeckoDriver.prototype.addCookie,
+ "WebDriver:Back": GeckoDriver.prototype.goBack,
+ "WebDriver:CloseChromeWindow": GeckoDriver.prototype.closeChromeWindow,
+ "WebDriver:CloseWindow": GeckoDriver.prototype.close,
+ "WebDriver:DeleteAllCookies": GeckoDriver.prototype.deleteAllCookies,
+ "WebDriver:DeleteCookie": GeckoDriver.prototype.deleteCookie,
+ "WebDriver:DeleteSession": GeckoDriver.prototype.deleteSession,
+ "WebDriver:DismissAlert": GeckoDriver.prototype.dismissDialog,
+ "WebDriver:ElementClear": GeckoDriver.prototype.clearElement,
+ "WebDriver:ElementClick": GeckoDriver.prototype.clickElement,
+ "WebDriver:ElementSendKeys": GeckoDriver.prototype.sendKeysToElement,
+ "WebDriver:ExecuteAsyncScript": GeckoDriver.prototype.executeAsyncScript,
+ "WebDriver:ExecuteScript": GeckoDriver.prototype.executeScript,
+ "WebDriver:FindElement": GeckoDriver.prototype.findElement,
+ "WebDriver:FindElements": GeckoDriver.prototype.findElements,
+ "WebDriver:Forward": GeckoDriver.prototype.goForward,
+ "WebDriver:FullscreenWindow": GeckoDriver.prototype.fullscreenWindow,
+ "WebDriver:GetActiveElement": GeckoDriver.prototype.getActiveElement,
+ "WebDriver:GetAlertText": GeckoDriver.prototype.getTextFromDialog,
+ "WebDriver:GetCapabilities": GeckoDriver.prototype.getSessionCapabilities,
+ "WebDriver:GetChromeWindowHandle":
+ GeckoDriver.prototype.getChromeWindowHandle,
+ "WebDriver:GetChromeWindowHandles":
+ GeckoDriver.prototype.getChromeWindowHandles,
+ "WebDriver:GetCookies": GeckoDriver.prototype.getCookies,
+ "WebDriver:GetCurrentChromeWindowHandle":
+ GeckoDriver.prototype.getChromeWindowHandle,
+ "WebDriver:GetCurrentURL": GeckoDriver.prototype.getCurrentUrl,
+ "WebDriver:GetElementAttribute": GeckoDriver.prototype.getElementAttribute,
+ "WebDriver:GetElementCSSValue":
+ GeckoDriver.prototype.getElementValueOfCssProperty,
+ "WebDriver:GetElementProperty": GeckoDriver.prototype.getElementProperty,
+ "WebDriver:GetElementRect": GeckoDriver.prototype.getElementRect,
+ "WebDriver:GetElementTagName": GeckoDriver.prototype.getElementTagName,
+ "WebDriver:GetElementText": GeckoDriver.prototype.getElementText,
+ "WebDriver:GetPageSource": GeckoDriver.prototype.getPageSource,
+ "WebDriver:GetTimeouts": GeckoDriver.prototype.getTimeouts,
+ "WebDriver:GetTitle": GeckoDriver.prototype.getTitle,
+ "WebDriver:GetWindowHandle": GeckoDriver.prototype.getWindowHandle,
+ "WebDriver:GetWindowHandles": GeckoDriver.prototype.getWindowHandles,
+ "WebDriver:GetWindowRect": GeckoDriver.prototype.getWindowRect,
+ "WebDriver:IsElementDisplayed": GeckoDriver.prototype.isElementDisplayed,
+ "WebDriver:IsElementEnabled": GeckoDriver.prototype.isElementEnabled,
+ "WebDriver:IsElementSelected": GeckoDriver.prototype.isElementSelected,
+ "WebDriver:MinimizeWindow": GeckoDriver.prototype.minimizeWindow,
+ "WebDriver:MaximizeWindow": GeckoDriver.prototype.maximizeWindow,
+ "WebDriver:Navigate": GeckoDriver.prototype.navigateTo,
+ "WebDriver:NewSession": GeckoDriver.prototype.newSession,
+ "WebDriver:NewWindow": GeckoDriver.prototype.newWindow,
+ "WebDriver:PerformActions": GeckoDriver.prototype.performActions,
+ "WebDriver:Print": GeckoDriver.prototype.print,
+ "WebDriver:Refresh": GeckoDriver.prototype.refresh,
+ "WebDriver:ReleaseActions": GeckoDriver.prototype.releaseActions,
+ "WebDriver:SendAlertText": GeckoDriver.prototype.sendKeysToDialog,
+ "WebDriver:SetTimeouts": GeckoDriver.prototype.setTimeouts,
+ "WebDriver:SetWindowRect": GeckoDriver.prototype.setWindowRect,
+ "WebDriver:SwitchToFrame": GeckoDriver.prototype.switchToFrame,
+ "WebDriver:SwitchToParentFrame": GeckoDriver.prototype.switchToParentFrame,
+ "WebDriver:SwitchToWindow": GeckoDriver.prototype.switchToWindow,
+ "WebDriver:TakeScreenshot": GeckoDriver.prototype.takeScreenshot,
+};
+
+function getWindowId(win) {
+ return win.docShell.browsingContext.id;
+}
+
+async function exitFullscreen(win) {
+ let cb;
+ // Use a timed promise to abort if no window manager is present
+ await new TimedPromise(
+ resolve => {
+ cb = new DebounceCallback(resolve);
+ win.addEventListener("sizemodechange", cb);
+ win.fullScreen = false;
+ },
+ { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER }
+ );
+ win.removeEventListener("sizemodechange", cb);
+}
+
+async function restoreWindow(win) {
+ win.restore();
+ // Use a poll promise to abort if no window manager is present
+ await new PollPromise(
+ (resolve, reject) => {
+ if (WindowState.from(win.windowState) == WindowState.Normal) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ { timeout: TIMEOUT_NO_WINDOW_MANAGER }
+ );
+}