diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /remote/marionette/driver.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | remote/marionette/driver.sys.mjs | 3575 |
1 files changed, 3575 insertions, 0 deletions
diff --git a/remote/marionette/driver.sys.mjs b/remote/marionette/driver.sys.mjs new file mode 100644 index 0000000000..154d2cde83 --- /dev/null +++ b/remote/marionette/driver.sys.mjs @@ -0,0 +1,3575 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Addon: "chrome://remote/content/marionette/addon.sys.mjs", + AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", + assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + browser: "chrome://remote/content/marionette/browser.sys.mjs", + capture: "chrome://remote/content/shared/Capture.sys.mjs", + Context: "chrome://remote/content/marionette/browser.sys.mjs", + cookie: "chrome://remote/content/marionette/cookie.sys.mjs", + DebounceCallback: "chrome://remote/content/marionette/sync.sys.mjs", + disableEventsActor: + "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs", + dom: "chrome://remote/content/shared/DOM.sys.mjs", + enableEventsActor: + "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", + EventPromise: "chrome://remote/content/shared/Sync.sys.mjs", + getMarionetteCommandsActorProxy: + "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs", + IdlePromise: "chrome://remote/content/marionette/sync.sys.mjs", + l10n: "chrome://remote/content/marionette/l10n.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", + Marionette: "chrome://remote/content/components/Marionette.sys.mjs", + MarionettePrefs: "chrome://remote/content/marionette/prefs.sys.mjs", + modal: "chrome://remote/content/shared/Prompt.sys.mjs", + navigate: "chrome://remote/content/marionette/navigate.sys.mjs", + permissions: "chrome://remote/content/marionette/permissions.sys.mjs", + pprint: "chrome://remote/content/shared/Format.sys.mjs", + print: "chrome://remote/content/shared/PDF.sys.mjs", + PromptListener: + "chrome://remote/content/shared/listeners/PromptListener.sys.mjs", + quit: "chrome://remote/content/shared/Browser.sys.mjs", + reftest: "chrome://remote/content/marionette/reftest.sys.mjs", + registerCommandsActor: + "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs", + RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs", + ShadowRoot: "chrome://remote/content/marionette/web-reference.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", + TimedPromise: "chrome://remote/content/marionette/sync.sys.mjs", + Timeouts: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs", + UnhandledPromptBehavior: + "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs", + unregisterCommandsActor: + "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs", + waitForInitialNavigationCompleted: + "chrome://remote/content/shared/Navigate.sys.mjs", + webauthn: "chrome://remote/content/marionette/webauthn.sys.mjs", + WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs", + WebElement: "chrome://remote/content/marionette/web-reference.sys.mjs", + windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", + WindowState: "chrome://remote/content/marionette/browser.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +ChromeUtils.defineLazyGetter( + lazy, + "supportedStrategies", + () => + new Set([ + lazy.dom.Strategy.ClassName, + lazy.dom.Strategy.Selector, + lazy.dom.Strategy.ID, + lazy.dom.Strategy.Name, + lazy.dom.Strategy.LinkText, + lazy.dom.Strategy.PartialLinkText, + lazy.dom.Strategy.TagName, + lazy.dom.Strategy.XPath, + ]) +); + +// Timeout used to abort fullscreen, maximize, and minimize +// commands if no window manager is present. +const TIMEOUT_NO_WINDOW_MANAGER = 5000; + +// Observer topic to wait for until the browser window is ready. +const TOPIC_BROWSER_READY = "browser-delayed-startup-finished"; +// Observer topic to perform clean up when application quit is requested. +const TOPIC_QUIT_APPLICATION_REQUESTED = "quit-application-requested"; + +/** + * 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 current browsing context's actor. + * + * 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. + */ +export function GeckoDriver(server) { + this._server = server; + + // WebDriver Session + this._currentSession = null; + + // Flag to indicate that the application is shutting down + this._isShuttingDown = false; + + this.browsers = {}; + + // points to current browser + this.curBrowser = null; + // top-most chrome window + this.mainFrame = null; + + // Use content context by default + this.context = lazy.Context.Content; + + // used for modal dialogs + this.dialog = null; + this.promptListener = null; +} + +/** + * 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 = lazy.Context.fromString(context); + }, +}); + +/** + * The current WebDriver Session. + */ +Object.defineProperty(GeckoDriver.prototype, "currentSession", { + get() { + if (lazy.RemoteAgent.webDriverBiDi) { + return lazy.RemoteAgent.webDriverBiDi.session; + } + + return this._currentSession; + }, +}); + +/** + * Returns the current URL of the ChromeWindow or content browser, + * depending on context. + * + * @returns {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. + * + * @returns {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, "windowType", { + get() { + return this.curBrowser.window.document.documentElement.getAttribute( + "windowtype" + ); + }, +}); + +GeckoDriver.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", +]); + +/** + * Callback used to observe the closing of modal dialogs + * during the session's lifetime. + */ +GeckoDriver.prototype.handleClosedModalDialog = function () { + this.dialog = null; +}; + +/** + * Callback used to observe the creation of new modal dialogs + * during the session's lifetime. + */ +GeckoDriver.prototype.handleOpenModalDialog = function (eventName, data) { + this.dialog = data.prompt; + + if (this.dialog.promptType === "beforeunload") { + lazy.logger.trace(`Implicitly accepted "beforeunload" prompt`); + this.dialog.accept(); + return; + } + + if (!this._isShuttingDown) { + this.getActor().notifyDialogOpened(this.dialog); + } +}; + +/** + * Get the current visible URL. + */ +GeckoDriver.prototype._getCurrentURL = function () { + const browsingContext = this.getBrowsingContext({ top: true }); + return new URL(browsingContext.currentURI.spec); +}; + +/** + * 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 lazy.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. + * + * @returns {BrowsingContext} + * The browsing context, or `null` if none is available + */ +GeckoDriver.prototype.getBrowsingContext = function (options = {}) { + const { context = this.context, parent = false, top = false } = options; + + let browsingContext = null; + if (context === lazy.Context.Chrome) { + browsingContext = this.currentSession?.chromeBrowsingContext; + } else { + browsingContext = this.currentSession?.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. + * + * @returns {ChromeWindow} + * The current top-level browsing context. + */ +GeckoDriver.prototype.getCurrentWindow = function (options = {}) { + const { context = this.context } = options; + + let win = null; + switch (context) { + case lazy.Context.Chrome: + if (this.curBrowser) { + win = this.curBrowser.window; + } + break; + + case lazy.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" + ); +}; + +/** + * Create a new browsing context for window and add to known browsers. + * + * @param {ChromeWindow} win + * Window for which we will create a browsing context. + * + * @returns {string} + * Returns the unique server-assigned ID of the window. + */ +GeckoDriver.prototype.addBrowser = function (win) { + let context = new lazy.browser.Context(win, this); + let winId = lazy.windowManager.getIdForWindow(win); + + this.browsers[winId] = context; + this.curBrowser = this.browsers[winId]; +}; + +/** + * Handles registration of new content browsers. Depending on + * their type they are either accepted or ignored. + * + * @param {XULBrowser} 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 ( + !lazy.AppInfo.isFirefox || + browserElement.namespaceURI != XUL_NS || + browserElement.nodeName != "browser" || + browserElement.getTabBrowser() + ) { + this.curBrowser.register(browserElement); + } +}; + +/** + * Create a new WebDriver session. + * + * @param {object} cmd + * @param {Object<string, *>=} cmd.parameters + * JSON Object containing any of the recognised capabilities as listed + * on the `WebDriverSession` class. + * + * @returns {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.currentSession) { + throw new lazy.error.SessionNotCreatedError( + "Maximum number of active sessions" + ); + } + + const { parameters: capabilities } = cmd; + + try { + // If the WebDriver BiDi protocol is active always use the Remote Agent + // to handle the WebDriver session. If it's not the case then Marionette + // itself needs to handle it, and has to nullify the "webSocketUrl" + // capability. + if (lazy.RemoteAgent.webDriverBiDi) { + await lazy.RemoteAgent.webDriverBiDi.createSession(capabilities); + } else { + this._currentSession = new lazy.WebDriverSession(capabilities); + this._currentSession.capabilities.delete("webSocketUrl"); + } + + // Don't wait for the initial window when Marionette is in windowless mode + if (!this.currentSession.capabilities.get("moz:windowless")) { + // Creating a WebDriver session too early can cause issues with + // clients in not being able to find any available window handle. + // Also when closing the application while it's still starting up can + // cause shutdown hangs. As such Marionette will return a new session + // once the initial application window has finished initializing. + lazy.logger.debug(`Waiting for initial application window`); + await lazy.Marionette.browserStartupFinished; + + const appWin = + await lazy.windowManager.waitForInitialApplicationWindowLoaded(); + + if (lazy.MarionettePrefs.clickToStart) { + Services.prompt.alert( + appWin, + "", + "Click to start execution of marionette tests" + ); + } + + this.addBrowser(appWin); + this.mainFrame = appWin; + + // Setup observer for modal dialogs + this.promptListener = new lazy.PromptListener(() => this.curBrowser); + this.promptListener.on("closed", this.handleClosedModalDialog.bind(this)); + this.promptListener.on("opened", this.handleOpenModalDialog.bind(this)); + this.promptListener.startListening(); + + for (let win of lazy.windowManager.windows) { + this.registerWindow(win, { registerBrowsers: true }); + } + + if (this.mainFrame) { + this.currentSession.chromeBrowsingContext = + this.mainFrame.browsingContext; + this.mainFrame.focus(); + } + + if (this.curBrowser.tab) { + const browsingContext = this.curBrowser.contentBrowser.browsingContext; + this.currentSession.contentBrowsingContext = browsingContext; + + // Bug 1838381 - Only use a longer unload timeout for desktop, because + // on Android only the initial document is loaded, and loading a + // specific page during startup doesn't succeed. + const options = {}; + if (!lazy.AppInfo.isAndroid) { + options.unloadTimeout = 5000; + } + + await lazy.waitForInitialNavigationCompleted( + browsingContext.webProgress, + options + ); + + this.curBrowser.contentBrowser.focus(); + } + + // Check if there is already an open dialog for the selected browser window. + this.dialog = lazy.modal.findPrompt(this.curBrowser); + } + + lazy.registerCommandsActor(this.currentSession.id); + lazy.enableEventsActor(); + + Services.obs.addObserver(this, TOPIC_BROWSER_READY); + } catch (e) { + throw new lazy.error.SessionNotCreatedError(e); + } + + return { + sessionId: this.currentSession.id, + capabilities: this.currentSession.capabilities, + }; +}; + +/** + * Start observing the specified window. + * + * @param {ChromeWindow} win + * Chrome window to register event listeners for. + * @param {object=} options + * @param {boolean=} options.registerBrowsers + * If true, register all content browsers of found tabs. Defaults to false. + */ +GeckoDriver.prototype.registerWindow = function (win, options = {}) { + const { registerBrowsers = false } = options; + const tabBrowser = lazy.TabManager.getTabBrowser(win); + + if (registerBrowsers && tabBrowser) { + for (const tab of tabBrowser.tabs) { + const contentBrowser = lazy.TabManager.getBrowserForTab(tab); + this.registerBrowser(contentBrowser); + } + } + + // Listen for any kind of top-level process switch + tabBrowser?.addEventListener("XULFrameLoaderCreated", this); +}; + +/** + * Stop observing the specified window. + * + * @param {ChromeWindow} win + * Chrome window to unregister event listeners for. + */ +GeckoDriver.prototype.stopObservingWindow = function (win) { + const tabBrowser = lazy.TabManager.getTabBrowser(win); + + tabBrowser?.removeEventListener("XULFrameLoaderCreated", this); +}; + +GeckoDriver.prototype.handleEvent = function ({ target, type }) { + switch (type) { + case "XULFrameLoaderCreated": + if (target === this.curBrowser.contentBrowser) { + lazy.logger.trace( + "Remoteness change detected. Set new top-level browsing context " + + `to ${target.browsingContext.id}` + ); + + this.currentSession.contentBrowsingContext = target.browsingContext; + } + break; + } +}; + +GeckoDriver.prototype.observe = async function (subject, topic, data) { + switch (topic) { + case TOPIC_BROWSER_READY: + this.registerWindow(subject); + break; + + case TOPIC_QUIT_APPLICATION_REQUESTED: + // Run Marionette specific cleanup steps before allowing + // the application to shutdown + await this._server.setAcceptConnections(false); + this.deleteSession(); + 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.currentSession.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 {object} cmd + * @param {string} cmd.parameters.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 = lazy.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}. + * + * @returns {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 {object} cmd + * @param {string} cmd.parameters.script + * Script to evaluate as a function body. + * @param {Array.<(string|boolean|number|object|WebReference)>} cmd.parameters.args + * Arguments exposed to the script in <code>arguments</code>. + * The array items must be serialisable to the WebDriver protocol. + * @param {string=} cmd.parameters.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=} cmd.parameters.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=} cmd.parameters.filename + * Filename of the client's program where this script is evaluated. + * @param {number=} cmd.parameters.line + * Line in the client's program where this script is evaluated. + * + * @returns {(string|boolean|number|object|WebReference)} + * 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 {NoSuchElementError} + * If an element that was passed as part of <var>args</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {ScriptTimeoutError} + * If the script was interrupted due to reaching the session's + * script timeout. + * @throws {StaleElementReferenceError} + * If an element that was passed as part of <var>args</var> or that is + * returned as result has gone stale. + */ +GeckoDriver.prototype.executeScript = 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 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 {object} cmd + * @param {string} cmd.parameters.script + * Script to evaluate as a function body. + * @param {Array.<(string|boolean|number|object|WebReference)>} cmd.parameters.args + * Arguments exposed to the script in <code>arguments</code>. + * The array items must be serialisable to the WebDriver protocol. + * @param {string=} cmd.parameters.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=} cmd.parameters.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=} cmd.parameters.filename + * Filename of the client's program where this script is evaluated. + * @param {number=} cmd.parameters.line + * Line in the client's program where this script is evaluated. + * + * @returns {(string|boolean|number|object|WebReference)} + * 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 {NoSuchElementError} + * If an element that was passed as part of <var>args</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {ScriptTimeoutError} + * If the script was interrupted due to reaching the session's + * script timeout. + * @throws {StaleElementReferenceError} + * If an element that was passed as part of <var>args</var> or that is + * returned as result has gone stale. + */ +GeckoDriver.prototype.executeAsyncScript = 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 this.execute_(script, args, opts); +}; + +GeckoDriver.prototype.execute_ = async function ( + script, + args = [], + { + sandboxName = null, + newSandbox = false, + file = "", + line = 0, + async = false, + } = {} +) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + lazy.assert.string( + script, + lazy.pprint`Expected "script" to be a string: ${script}` + ); + lazy.assert.array( + args, + lazy.pprint`Expected script args to be an array: ${args}` + ); + if (sandboxName !== null) { + lazy.assert.string( + sandboxName, + lazy.pprint`Expected sandbox name to be a string: ${sandboxName}` + ); + } + lazy.assert.boolean( + newSandbox, + lazy.pprint`Expected newSandbox to be boolean: ${newSandbox}` + ); + lazy.assert.string(file, lazy.pprint`Expected file to be a string: ${file}`); + lazy.assert.number(line, lazy.pprint`Expected line to be a number: ${line}`); + + let opts = { + timeout: this.currentSession.timeouts.script, + sandboxName, + newSandbox, + file, + line, + async, + }; + + return this.getActor().executeScript(script, args, opts); +}; + +/** + * 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 {object} cmd + * @param {string} cmd.parameters.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) { + lazy.assert.content(this.context); + const browsingContext = lazy.assert.open( + this.getBrowsingContext({ top: true }) + ); + await this._handleUserPrompts(); + + let validURL; + try { + validURL = new URL(cmd.parameters.url); + } catch (e) { + throw new lazy.error.InvalidArgumentError(`Malformed URL: ${e.message}`); + } + + // Switch to the top-level browsing context before navigating + this.currentSession.contentBrowsingContext = browsingContext; + + const loadEventExpected = lazy.navigate.isLoadEventExpected( + this._getCurrentURL(), + { + future: validURL, + } + ); + + await lazy.navigate.waitForNavigationCompleted( + this, + () => { + lazy.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 () { + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + return this._getCurrentURL().href; +}; + +/** + * Gets the current title of the window. + * + * @returns {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 () { + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + return this.title; +}; + +/** + * Gets the current type of the window. + * + * @returns {string} + * Type of window + * + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + */ +GeckoDriver.prototype.getWindowType = function () { + lazy.assert.open(this.getBrowsingContext({ top: true })); + + return this.windowType; +}; + +/** + * Gets the page source of the content document. + * + * @returns {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 () { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + return this.getActor().getPageSource(); +}; + +/** + * 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 () { + lazy.assert.content(this.context); + const browsingContext = lazy.assert.open( + this.getBrowsingContext({ top: true }) + ); + await this._handleUserPrompts(); + + // If there is no history, just return + if (!browsingContext.embedderElement?.canGoBack) { + return; + } + + await lazy.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 () { + lazy.assert.content(this.context); + const browsingContext = lazy.assert.open( + this.getBrowsingContext({ top: true }) + ); + await this._handleUserPrompts(); + + // If there is no history, just return + if (!browsingContext.embedderElement?.canGoForward) { + return; + } + + await lazy.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 () { + lazy.assert.content(this.context); + const browsingContext = lazy.assert.open( + this.getBrowsingContext({ top: true }) + ); + await this._handleUserPrompts(); + + // Switch to the top-level browsing context before navigating + this.currentSession.contentBrowsingContext = browsingContext; + + await lazy.navigate.waitForNavigationCompleted(this, () => { + lazy.navigate.refresh(browsingContext); + }); +}; + +/** + * Get the current window's handle. On desktop this typically corresponds + * to the currently selected tab. + * + * For chrome scope it returns the window identifier for the current chrome + * window for tests interested in managing the chrome window and tab separately. + * + * 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. + * + * @returns {string} + * Unique window handle. + * + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + */ +GeckoDriver.prototype.getWindowHandle = function () { + lazy.assert.open(this.getBrowsingContext({ top: true })); + + if (this.context == lazy.Context.Chrome) { + return lazy.windowManager.getIdForWindow(this.curBrowser.window); + } + return lazy.TabManager.getIdForBrowser(this.curBrowser.contentBrowser); +}; + +/** + * 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. + * + * For chrome scope it returns identifiers for each open chrome window for + * tests interested in managing a set of chrome windows and tabs separately. + * + * Each window handle is assigned by the server and is guaranteed unique, + * however the return array does not have a specified ordering. + * + * @returns {Array.<string>} + * Unique window handles. + */ +GeckoDriver.prototype.getWindowHandles = function () { + if (this.context == lazy.Context.Chrome) { + return lazy.windowManager.chromeWindowHandles.map(String); + } + return lazy.TabManager.allBrowserUniqueIds.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. + * + * @returns {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 () { + lazy.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 {object} cmd + * @param {number} cmd.parameters.x + * X coordinate of the top/left of the window that it will be + * moved to. + * @param {number} cmd.parameters.y + * Y coordinate of the top/left of the window that it will be + * moved to. + * @param {number} cmd.parameters.width + * Width to resize the window to. + * @param {number} cmd.parameters.height + * Height to resize the window to. + * + * @returns {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) { + lazy.assert.desktop(); + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + const { x = null, y = null, width = null, height = null } = cmd.parameters; + if (x !== null) { + lazy.assert.integer(x); + } + if (y !== null) { + lazy.assert.integer(y); + } + if (height !== null) { + lazy.assert.positiveInteger(height); + } + if (width !== null) { + lazy.assert.positiveInteger(width); + } + + const win = this.getCurrentWindow(); + switch (lazy.WindowState.from(win.windowState)) { + case lazy.WindowState.Fullscreen: + await exitFullscreen(win); + break; + + case lazy.WindowState.Maximized: + case lazy.WindowState.Minimized: + await restoreWindow(win); + break; + } + + function geometryMatches() { + if ( + width !== null && + height !== null && + (win.outerWidth !== width || win.outerHeight !== height) + ) { + return false; + } + if (x !== null && y !== null && (win.screenX !== x || win.screenY !== y)) { + return false; + } + lazy.logger.trace(`Requested window geometry matches`); + return true; + } + + if (!geometryMatches()) { + // There might be more than one resize or MozUpdateWindowPos event due + // to previous geometry changes, such as from restoreWindow(), so + // wait longer if window geometry does not match. + const options = { checkFn: geometryMatches, timeout: 500 }; + const promises = []; + if (width !== null && height !== null) { + promises.push(new lazy.EventPromise(win, "resize", options)); + win.resizeTo(width, height); + } + if (x !== null && y !== null) { + promises.push( + new lazy.EventPromise(win.windowRoot, "MozUpdateWindowPos", options) + ); + win.moveTo(x, y); + } + try { + await Promise.race(promises); + } catch (e) { + if (e instanceof lazy.error.TimeoutError) { + // The operating system might not honor the move or resize, in which + // case assume that geometry will have been adjusted "as close as + // possible" to that requested. There may be no event received if the + // geometry is already as close as possible. + } else { + throw e; + } + } + } + + 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 {object} cmd + * @param {string} cmd.parameters.handle + * Handle of the window to switch to. + * @param {boolean=} cmd.parameters.focus + * A boolean value which determines whether to focus + * the window. Defaults to true. + * + * @throws {InvalidArgumentError} + * If <var>handle</var> is not a string or <var>focus</var> not a boolean. + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + */ +GeckoDriver.prototype.switchToWindow = async function (cmd) { + const { focus = true, handle } = cmd.parameters; + + lazy.assert.string( + handle, + lazy.pprint`Expected "handle" to be a string, got ${handle}` + ); + lazy.assert.boolean( + focus, + lazy.pprint`Expected "focus" to be a boolean, got ${focus}` + ); + + const found = lazy.windowManager.findWindowByHandle(handle); + + let selected = false; + if (found) { + try { + await this.setWindowHandle(found, focus); + selected = true; + } catch (e) { + lazy.logger.error(e); + } + } + + if (!selected) { + throw new lazy.error.NoSuchWindowError( + `Unable to locate window: ${handle}` + ); + } +}; + +/** + * 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 + * :js:func:`GeckoDriver#getWindowProperties` + * @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. + this.addBrowser(winProperties.win); + this.mainFrame = winProperties.win; + + this.currentSession.chromeBrowsingContext = this.mainFrame.browsingContext; + + if (!winProperties.hasTabBrowser) { + this.currentSession.contentBrowsingContext = null; + } else { + const tabBrowser = lazy.TabManager.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.currentSession.contentBrowsingContext = + contentBrowser.browsingContext; + this.registerBrowser(contentBrowser); + } + } 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.currentSession.chromeBrowsingContext = this.mainFrame.browsingContext; + this.currentSession.contentBrowsingContext = + tab?.linkedBrowser.browsingContext; + } + + // Check for an existing dialog for the new window + this.dialog = lazy.modal.findPrompt(this.curBrowser); + + // If there is an open window modal dialog the underlying chrome window + // cannot be focused. + if (focus && !this.dialog?.isWindowModal) { + 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 = lazy.assert.open(browsingContext?.parent); + + this.currentSession.contentBrowsingContext = browsingContext; +}; + +/** + * Switch to a given frame within the current window. + * + * @param {object} cmd + * @param {(string | object)=} cmd.parameters.element + * A web element reference of the frame or its element id. + * @param {number=} cmd.parameters.id + * The index of the frame to switch to. + * If both element and id are not defined, switch to top-level frame. + * + * @throws {NoSuchElementError} + * If element represented by reference <var>element</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {StaleElementReferenceError} + * If element represented by reference <var>element</var> has gone stale. + * @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") { + lazy.assert.unsignedShort( + id, + `Expected id to be unsigned short, got ${id}` + ); + } + + const top = id == null && el == null; + lazy.assert.open(this.getBrowsingContext({ top })); + await this._handleUserPrompts(); + + // Bug 1495063: Elements should be passed as WebReference reference + let byFrame; + if (typeof el == "string") { + byFrame = lazy.WebElement.fromUUID(el).toJSON(); + } else if (el) { + byFrame = el; + } + + const { browsingContext } = await this.getActor({ top }).switchToFrame( + byFrame || id + ); + + this.currentSession.contentBrowsingContext = browsingContext; +}; + +GeckoDriver.prototype.getTimeouts = function () { + return this.currentSession.timeouts; +}; + +/** + * Set timeout for page loading, searching, and scripts. + * + * @param {object} cmd + * @param {Object<string, number>} cmd.parameters + * 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.currentSession.timeouts.toJSON(), + cmd.parameters + ); + + this.currentSession.timeouts = lazy.Timeouts.fromJSON(merged); +}; + +/** + * Perform a series of grouped actions at the specified points in time. + * + * @param {object} cmd + * @param {Array<?>} cmd.parameters.actions + * Array of objects that each represent an action sequence. + * + * @throws {NoSuchElementError} + * If an element that is used as part of the action chain is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {StaleElementReferenceError} + * If an element that is used as part of the action chain has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + * @throws {UnsupportedOperationError} + * Not yet available in current context. + */ +GeckoDriver.prototype.performActions = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + const actions = cmd.parameters.actions; + await this.getActor().performActions(actions); +}; + +/** + * 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 () { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + await this.getActor().releaseActions(); +}; + +/** + * Find an element using the indicated search strategy. + * + * @param {object} cmd + * @param {string=} cmd.parameters.element + * Web element reference ID to the element that will be used as start node. + * @param {string} cmd.parameters.using + * Indicates which search method to use. + * @param {string} cmd.parameters.value + * Value the client is looking for. + * + * @returns {WebElement} + * Return the found element. + * + * @throws {NoSuchElementError} + * If element represented by reference <var>element</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {StaleElementReferenceError} + * If element represented by reference <var>element</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.findElement = async function (cmd) { + const { element: el, using, value } = cmd.parameters; + + if (!lazy.supportedStrategies.has(using)) { + throw new lazy.error.InvalidSelectorError( + `Strategy not supported: ${using}` + ); + } + + lazy.assert.defined(value); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let startNode; + if (typeof el != "undefined") { + startNode = lazy.WebElement.fromUUID(el).toJSON(); + } + + let opts = { + startNode, + timeout: this.currentSession.timeouts.implicit, + all: false, + }; + + return this.getActor().findElement(using, value, opts); +}; + +/** + * Find an element within shadow root using the indicated search strategy. + * + * @param {object} cmd + * @param {string} cmd.parameters.shadowRoot + * Shadow root reference ID. + * @param {string} cmd.parameters.using + * Indicates which search method to use. + * @param {string} cmd.parameters.value + * Value the client is looking for. + * + * @returns {WebElement} + * Return the found element. + * + * @throws {DetachedShadowRootError} + * If shadow root represented by reference <var>id</var> is + * no longer attached to the DOM. + * @throws {NoSuchElementError} + * If the element which is looked for with <var>value</var> was + * not found. + * @throws {NoSuchShadowRoot} + * If shadow root represented by reference <var>shadowRoot</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.findElementFromShadowRoot = async function (cmd) { + const { shadowRoot, using, value } = cmd.parameters; + + if (!lazy.supportedStrategies.has(using)) { + throw new lazy.error.InvalidSelectorError( + `Strategy not supported: ${using}` + ); + } + + lazy.assert.defined(value); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + const opts = { + all: false, + startNode: lazy.ShadowRoot.fromUUID(shadowRoot).toJSON(), + timeout: this.currentSession.timeouts.implicit, + }; + + return this.getActor().findElement(using, value, opts); +}; + +/** + * Find elements using the indicated search strategy. + * + * @param {object} cmd + * @param {string=} cmd.parameters.element + * Web element reference ID to the element that will be used as start node. + * @param {string} cmd.parameters.using + * Indicates which search method to use. + * @param {string} cmd.parameters.value + * Value the client is looking for. + * + * @returns {Array<WebElement>} + * Return the array of found elements. + * + * @throws {NoSuchElementError} + * If element represented by reference <var>element</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {StaleElementReferenceError} + * If element represented by reference <var>element</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.findElements = async function (cmd) { + const { element: el, using, value } = cmd.parameters; + + if (!lazy.supportedStrategies.has(using)) { + throw new lazy.error.InvalidSelectorError( + `Strategy not supported: ${using}` + ); + } + + lazy.assert.defined(value); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let startNode; + if (typeof el != "undefined") { + startNode = lazy.WebElement.fromUUID(el).toJSON(); + } + + let opts = { + startNode, + timeout: this.currentSession.timeouts.implicit, + all: true, + }; + + return this.getActor().findElements(using, value, opts); +}; + +/** + * Find elements within shadow root using the indicated search strategy. + * + * @param {object} cmd + * @param {string} cmd.parameters.shadowRoot + * Shadow root reference ID. + * @param {string} cmd.parameters.using + * Indicates which search method to use. + * @param {string} cmd.parameters.value + * Value the client is looking for. + * + * @returns {Array<WebElement>} + * Return the array of found elements. + * + * @throws {DetachedShadowRootError} + * If shadow root represented by reference <var>id</var> is + * no longer attached to the DOM. + * @throws {NoSuchShadowRoot} + * If shadow root represented by reference <var>shadowRoot</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.findElementsFromShadowRoot = async function (cmd) { + const { shadowRoot, using, value } = cmd.parameters; + + if (!lazy.supportedStrategies.has(using)) { + throw new lazy.error.InvalidSelectorError( + `Strategy not supported: ${using}` + ); + } + + lazy.assert.defined(value); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + const opts = { + all: true, + startNode: lazy.ShadowRoot.fromUUID(shadowRoot).toJSON(), + timeout: this.currentSession.timeouts.implicit, + }; + + return this.getActor().findElements(using, value, opts); +}; + +/** + * Return the shadow root of an element in the document. + * + * @param {object} cmd + * @param {id} cmd.parameters.id + * A web element id reference. + * @returns {ShadowRoot} + * ShadowRoot of the element. + * + * @throws {InvalidArgumentError} + * If element <var>id</var> is not a string. + * @throws {NoSuchElementError} + * If element represented by reference <var>id</var> is unknown. + * @throws {NoSuchShadowRoot} + * Element does not have a shadow root attached. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + * @throws {UnsupportedOperationError} + * Not available in chrome current context. + */ +GeckoDriver.prototype.getShadowRoot = async function (cmd) { + // Bug 1743541: Add support for chrome scope. + lazy.assert.content(this.context); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string( + cmd.parameters.id, + lazy.pprint`Expected "id" to be a string, got ${cmd.parameters.id}` + ); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getShadowRoot(webEl); +}; + +/** + * Return the active element in the document. + * + * @returns {WebReference} + * 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 chrome context. + */ +GeckoDriver.prototype.getActiveElement = async function () { + lazy.assert.content(this.context); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + return this.getActor().getActiveElement(); +}; + +/** + * Send click event to element. + * + * @param {object} cmd + * @param {string} cmd.parameters.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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.clickElement = async function (cmd) { + const browsingContext = lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + const actor = this.getActor(); + + const loadEventExpected = lazy.navigate.isLoadEventExpected( + this._getCurrentURL(), + { + browsingContext, + target: await actor.getElementAttribute(webEl, "target"), + } + ); + + await lazy.navigate.waitForNavigationCompleted( + this, + () => actor.clickElement(webEl, this.currentSession.capabilities), + { + loadEventExpected, + // The click might trigger a navigation, so don't count on it. + requireBeforeUnload: false, + } + ); +}; + +/** + * Get a given attribute of an element. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Web element reference ID to the element that will be inspected. + * @param {string} cmd.parameters.name + * Name of the attribute which value to retrieve. + * + * @returns {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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.getElementAttribute = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + const id = lazy.assert.string(cmd.parameters.id); + const name = lazy.assert.string(cmd.parameters.name); + const webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getElementAttribute(webEl, name); +}; + +/** + * Returns the value of a property associated with given element. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Web element reference ID to the element that will be inspected. + * @param {string} cmd.parameters.name + * Name of the property which value to retrieve. + * + * @returns {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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.getElementProperty = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + const id = lazy.assert.string(cmd.parameters.id); + const name = lazy.assert.string(cmd.parameters.name); + const webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getElementProperty(webEl, name); +}; + +/** + * Get the text of an element, if any. Includes the text of all child + * elements. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Reference ID to the element that will be inspected. + * + * @returns {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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.getElementText = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getElementText(webEl); +}; + +/** + * Get the tag name of the element. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Reference ID to the element that will be inspected. + * + * @returns {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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.getElementTagName = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getElementTagName(webEl); +}; + +/** + * Check if element is displayed. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Reference ID to the element that will be inspected. + * + * @returns {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) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().isElementDisplayed( + webEl, + this.currentSession.capabilities + ); +}; + +/** + * Return the property of the computed style of an element. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Reference ID to the element that will be checked. + * @param {string} cmd.parameters.propertyName + * CSS rule that is being requested. + * + * @returns {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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.getElementValueOfCssProperty = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let prop = lazy.assert.string(cmd.parameters.propertyName); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getElementValueOfCssProperty(webEl, prop); +}; + +/** + * Check if element is enabled. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Reference ID to the element that will be checked. + * + * @returns {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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.isElementEnabled = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().isElementEnabled( + webEl, + this.currentSession.capabilities + ); +}; + +/** + * Check if element is selected. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Reference ID to the element that will be checked. + * + * @returns {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) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().isElementSelected( + webEl, + this.currentSession.capabilities + ); +}; + +/** + * @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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.getElementRect = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getElementRect(webEl); +}; + +/** + * Send key presses to element after focusing on it. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Reference ID to the element that will be checked. + * @param {string} cmd.parameters.text + * Value to send to the element. + * + * @throws {InvalidArgumentError} + * If <var>id</var> or <var>text</var> are not strings. + * @throws {NoSuchElementError} + * If element represented by reference <var>id</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.sendKeysToElement = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let text = lazy.assert.string(cmd.parameters.text); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().sendKeysToElement( + webEl, + text, + this.currentSession.capabilities + ); +}; + +/** + * Clear the text of an element. + * + * @param {object} cmd + * @param {string} cmd.parameters.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 {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.clearElement = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + await this.getActor().clearElement(webEl); +}; + +/** + * Add a single cookie to the cookie store associated with the active + * document's address. + * + * @param {object} cmd + * @param {Map.<string, (string|number|boolean)>} cmd.parameters.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) { + lazy.assert.content(this.context); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let { protocol, hostname } = this._getCurrentURL(); + + const networkSchemes = ["http:", "https:"]; + if (!networkSchemes.includes(protocol)) { + throw new lazy.error.InvalidCookieDomainError("Document is cookie-averse"); + } + + let newCookie = lazy.cookie.fromJSON(cmd.parameters.cookie); + + lazy.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 () { + lazy.assert.content(this.context); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let { hostname, pathname } = this._getCurrentURL(); + return [...lazy.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 () { + lazy.assert.content(this.context); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let { hostname, pathname } = this._getCurrentURL(); + for (let toDelete of lazy.cookie.iter(hostname, pathname)) { + lazy.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) { + lazy.assert.content(this.context); + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let { hostname, pathname } = this._getCurrentURL(); + let name = lazy.assert.string(cmd.parameters.name); + for (let c of lazy.cookie.iter(hostname, pathname)) { + if (c.name === name) { + lazy.cookie.remove(c); + } + } +}; + +/** + * Open a new top-level browsing context. + * + * @param {object} cmd + * @param {string=} cmd.parameters.type + * Optional type of the new top-level browsing context. Can be one of + * `tab` or `window`. Defaults to `tab`. + * @param {boolean=} cmd.parameters.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=} cmd.parameters.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. + * + * @returns {Object<string, string>} + * Handle and type of the new browsing context. + * + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + */ +GeckoDriver.prototype.newWindow = async function (cmd) { + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + let focus = false; + if (typeof cmd.parameters.focus != "undefined") { + focus = lazy.assert.boolean( + cmd.parameters.focus, + lazy.pprint`Expected "focus" to be a boolean, got ${cmd.parameters.focus}` + ); + } + + let isPrivate = false; + if (typeof cmd.parameters.private != "undefined") { + isPrivate = lazy.assert.boolean( + cmd.parameters.private, + lazy.pprint`Expected "private" to be a boolean, got ${cmd.parameters.private}` + ); + } + + let type; + if (typeof cmd.parameters.type != "undefined") { + type = lazy.assert.string( + cmd.parameters.type, + lazy.pprint`Expected "type" to be a string, got ${cmd.parameters.type}` + ); + } + + // If an invalid or no type has been specified default to a tab. + // On Android always use a new tab instead because the application has a + // single window only. + if ( + typeof type == "undefined" || + !["tab", "window"].includes(type) || + lazy.AppInfo.isAndroid + ) { + type = "tab"; + } + + let contentBrowser; + + switch (type) { + case "window": + let win = await this.curBrowser.openBrowserWindow(focus, isPrivate); + contentBrowser = lazy.TabManager.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 = lazy.TabManager.getBrowserForTab(tab); + } + + // Actors need the new window to be loaded to safely execute queries. + // Wait until the initial page load has been finished. + await lazy.waitForInitialNavigationCompleted( + contentBrowser.browsingContext.webProgress, + { + unloadTimeout: 5000, + } + ); + + const id = lazy.TabManager.getIdForBrowser(contentBrowser); + + return { handle: id.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. + * + * @returns {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 () { + lazy.assert.open( + this.getBrowsingContext({ context: lazy.Context.Content, top: true }) + ); + await this._handleUserPrompts(); + + // If there is only one window left, do not close unless windowless mode is + // enabled. Instead return a faked empty array of window handles. + // This will instruct geckodriver to terminate the application. + if ( + lazy.TabManager.getTabCount() === 1 && + !this.currentSession.capabilities.get("moz:windowless") + ) { + return []; + } + + await this.curBrowser.closeTab(); + this.currentSession.contentBrowsingContext = null; + + return lazy.TabManager.allBrowserUniqueIds.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. + * + * @returns {Array.<string>} + * Unique chrome window handles of remaining chrome windows. + * + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + */ +GeckoDriver.prototype.closeChromeWindow = async function () { + lazy.assert.desktop(); + lazy.assert.open( + this.getBrowsingContext({ context: lazy.Context.Chrome, top: true }) + ); + + let nwins = 0; + + // eslint-disable-next-line + for (let _ of lazy.windowManager.windows) { + nwins++; + } + + // If there is only one window left, do not close unless windowless mode is + // enabled. Instead return a faked empty array of window handles. + // This will instruct geckodriver to terminate the application. + if (nwins == 1 && !this.currentSession.capabilities.get("moz:windowless")) { + return []; + } + + await this.curBrowser.closeWindow(); + this.currentSession.chromeBrowsingContext = null; + this.currentSession.contentBrowsingContext = null; + + return lazy.windowManager.chromeWindowHandles.map(String); +}; + +/** Delete Marionette session. */ +GeckoDriver.prototype.deleteSession = function () { + if (!this.currentSession) { + return; + } + + for (let win of lazy.windowManager.windows) { + this.stopObservingWindow(win); + } + + // reset to the top-most frame + this.mainFrame = null; + + if (!this._isShuttingDown && this.promptListener) { + // Do not stop the prompt listener when quitting the browser to + // allow us to also accept beforeunload prompts during shutdown. + this.promptListener.stopListening(); + this.promptListener = null; + } + + try { + Services.obs.removeObserver(this, TOPIC_BROWSER_READY); + } catch (e) { + lazy.logger.debug(`Failed to remove observer "${TOPIC_BROWSER_READY}"`); + } + + // Always unregister actors after all other observers + // and listeners have been removed. + lazy.unregisterCommandsActor(); + // MarionetteEvents actors are only disabled to avoid IPC errors if there are + // in flight events being forwarded from the content process to the parent + // process. + lazy.disableEventsActor(); + + if (lazy.RemoteAgent.webDriverBiDi) { + lazy.RemoteAgent.webDriverBiDi.deleteSession(); + } else { + this.currentSession.destroy(); + this._currentSession = null; + } +}; + +/** + * 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 {object} cmd + * @param {string=} cmd.parameters.id + * Optional web element reference to take a screenshot of. + * If undefined, a screenshot will be taken of the document element. + * @param {boolean=} cmd.parameters.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=} cmd.parameters.hash + * True if the user requests a hash of the image data. Defaults to false. + * @param {boolean=} cmd.parameters.scroll + * Scroll to element if |id| is provided. Defaults to true. + * + * @returns {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 {NoSuchElementError} + * If element represented by reference <var>id</var> is unknown. + * @throws {NoSuchWindowError} + * Browsing context has been discarded. + * @throws {StaleElementReferenceError} + * If element represented by reference <var>id</var> has gone stale. + */ +GeckoDriver.prototype.takeScreenshot = async function (cmd) { + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + let { id, full, hash, scroll } = cmd.parameters; + let format = hash ? lazy.capture.Format.Hash : lazy.capture.Format.Base64; + + full = typeof full == "undefined" ? true : full; + scroll = typeof scroll == "undefined" ? true : scroll; + + let webEl = id ? lazy.WebElement.fromUUID(id).toJSON() : null; + + // Only consider full screenshot if no element has been specified + full = webEl ? false : full; + + return this.getActor().takeScreenshot(webEl, format, full, scroll); +}; + +/** + * 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 () { + lazy.assert.mobile(); + lazy.assert.open(this.getBrowsingContext({ top: true })); + + const win = this.getCurrentWindow(); + + return win.screen.orientation.type; +}; + +/** + * 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 = async function (cmd) { + lazy.assert.mobile(); + lazy.assert.open(this.getBrowsingContext({ top: true })); + + const ors = [ + "portrait", + "landscape", + "portrait-primary", + "landscape-primary", + "portrait-secondary", + "landscape-secondary", + ]; + + let or = String(cmd.parameters.orientation); + lazy.assert.string(or); + let mozOr = or.toLowerCase(); + if (!ors.includes(mozOr)) { + throw new lazy.error.InvalidArgumentError( + `Unknown screen orientation: ${or}` + ); + } + + const win = this.getCurrentWindow(); + + try { + await win.screen.orientation.lock(mozOr); + } catch (e) { + throw new lazy.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. + * + * @returns {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 () { + lazy.assert.desktop(); + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + const win = this.getCurrentWindow(); + switch (lazy.WindowState.from(win.windowState)) { + case lazy.WindowState.Fullscreen: + await exitFullscreen(win); + break; + + case lazy.WindowState.Maximized: + await restoreWindow(win); + break; + } + + if (lazy.WindowState.from(win.windowState) != lazy.WindowState.Minimized) { + let cb; + // Use a timed promise to abort if no window manager is present + await new lazy.TimedPromise( + resolve => { + cb = new lazy.DebounceCallback(resolve); + win.addEventListener("sizemodechange", cb); + win.minimize(); + }, + { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER } + ); + win.removeEventListener("sizemodechange", cb); + await new lazy.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. + * + * @returns {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 () { + lazy.assert.desktop(); + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + const win = this.getCurrentWindow(); + switch (lazy.WindowState.from(win.windowState)) { + case lazy.WindowState.Fullscreen: + await exitFullscreen(win); + break; + + case lazy.WindowState.Minimized: + await restoreWindow(win); + break; + } + + if (lazy.WindowState.from(win.windowState) != lazy.WindowState.Maximized) { + let cb; + // Use a timed promise to abort if no window manager is present + await new lazy.TimedPromise( + resolve => { + cb = new lazy.DebounceCallback(resolve); + win.addEventListener("sizemodechange", cb); + win.maximize(); + }, + { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER } + ); + win.removeEventListener("sizemodechange", cb); + await new lazy.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. + * + * @returns {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 () { + lazy.assert.desktop(); + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + const win = this.getCurrentWindow(); + switch (lazy.WindowState.from(win.windowState)) { + case lazy.WindowState.Maximized: + case lazy.WindowState.Minimized: + await restoreWindow(win); + break; + } + + if (lazy.WindowState.from(win.windowState) != lazy.WindowState.Fullscreen) { + let cb; + // Use a timed promise to abort if no window manager is present + await new lazy.TimedPromise( + resolve => { + cb = new lazy.DebounceCallback(resolve); + win.addEventListener("sizemodechange", cb); + win.fullScreen = true; + }, + { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER } + ); + win.removeEventListener("sizemodechange", cb); + } + await new lazy.IdlePromise(win); + + return this.curBrowser.rect; +}; + +/** + * Dismisses a currently displayed modal dialogs, or returns no such alert if + * no modal is displayed. + * + * @throws {NoSuchAlertError} + * If there is no current user prompt. + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + */ +GeckoDriver.prototype.dismissDialog = async function () { + lazy.assert.open(this.getBrowsingContext({ top: true })); + this._checkIfAlertIsPresent(); + + const dialogClosed = this.promptListener.dialogClosed(); + this.dialog.dismiss(); + await dialogClosed; + + const win = this.getCurrentWindow(); + await new lazy.IdlePromise(win); +}; + +/** + * Accepts a currently displayed dialog modal, or returns no such alert if + * no modal is displayed. + * + * @throws {NoSuchAlertError} + * If there is no current user prompt. + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + */ +GeckoDriver.prototype.acceptDialog = async function () { + lazy.assert.open(this.getBrowsingContext({ top: true })); + this._checkIfAlertIsPresent(); + + const dialogClosed = this.promptListener.dialogClosed(); + this.dialog.accept(); + await dialogClosed; + + const win = this.getCurrentWindow(); + await new lazy.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 {NoSuchAlertError} + * If there is no current user prompt. + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + */ +GeckoDriver.prototype.getTextFromDialog = async function () { + lazy.assert.open(this.getBrowsingContext({ top: true })); + this._checkIfAlertIsPresent(); + const text = await this.dialog.getText(); + return text; +}; + +/** + * 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 modal dialog is currently displayed but has no means for text input, + * an element not visible error is returned. + * + * @param {object} cmd + * @param {string} cmd.parameters.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) { + lazy.assert.open(this.getBrowsingContext({ top: true })); + this._checkIfAlertIsPresent(); + + let text = lazy.assert.string(cmd.parameters.text); + let promptType = this.dialog.args.promptType; + + switch (promptType) { + case "alert": + case "confirm": + throw new lazy.error.ElementNotInteractableError( + `User prompt of type ${promptType} is not interactable` + ); + case "prompt": + break; + default: + await this.dismissDialog(); + throw new lazy.error.UnsupportedOperationError( + `User prompt of type ${promptType} is not supported` + ); + } + this.dialog.text = text; +}; + +GeckoDriver.prototype._checkIfAlertIsPresent = function () { + if (!this.dialog || !this.dialog.isOpen) { + throw new lazy.error.NoSuchAlertError(); + } +}; + +GeckoDriver.prototype._handleUserPrompts = async function () { + if (!this.dialog || !this.dialog.isOpen) { + return; + } + + if (this.dialog.promptType == "beforeunload") { + // Wait until the "beforeunload" prompt has been accepted. + await this.promptListener.dialogClosed(); + return; + } + + const textContent = await this.dialog.getText(); + + const behavior = this.currentSession.unhandledPromptBehavior; + switch (behavior) { + case lazy.UnhandledPromptBehavior.Accept: + await this.acceptDialog(); + break; + + case lazy.UnhandledPromptBehavior.AcceptAndNotify: + await this.acceptDialog(); + throw new lazy.error.UnexpectedAlertOpenError( + `Accepted user prompt dialog: ${textContent}` + ); + + case lazy.UnhandledPromptBehavior.Dismiss: + await this.dismissDialog(); + break; + + case lazy.UnhandledPromptBehavior.DismissAndNotify: + await this.dismissDialog(); + throw new lazy.error.UnexpectedAlertOpenError( + `Dismissed user prompt dialog: ${textContent}` + ); + + case lazy.UnhandledPromptBehavior.Ignore: + throw new lazy.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 {object} cmd + * @param {boolean} cmd.parameters.value + * True if the server should accept new socket connections. + */ +GeckoDriver.prototype.acceptConnections = async function (cmd) { + lazy.assert.boolean(cmd.parameters.value); + await this._server.setAcceptConnections(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 {object} cmd + * @param {Array.<string>=} cmd.parameters.flags + * Constant name of masks to pass to |Services.startup.quit|. + * If empty or undefined, |nsIAppStartup.eAttemptQuit| is used. + * @param {boolean=} cmd.parameters.safeMode + * Optional flag to indicate that the application has to + * be restarted in safe mode. + * + * @returns {Object<string,boolean>} + * Dictionary containing information that explains the shutdown reason. + * The value for `cause` contains the shutdown kind like "shutdown" or + * "restart", while `forced` will indicate if it was a normal or forced + * shutdown of the application. "in_app" is always set to indicate that + * it is a shutdown triggered from within the application. + * + * @throws {InvalidArgumentError} + * If <var>flags</var> contains unknown or incompatible flags, + * for example multiple Quit flags. + */ +GeckoDriver.prototype.quit = async function (cmd) { + const { flags = [], safeMode = false } = cmd.parameters; + + lazy.assert.array(flags, `Expected "flags" to be an array`); + lazy.assert.boolean(safeMode, `Expected "safeMode" to be a boolean`); + + if (safeMode && !flags.includes("eRestart")) { + throw new lazy.error.InvalidArgumentError( + `"safeMode" only works with restart flag` + ); + } + + // Register handler to run Marionette specific shutdown code. + Services.obs.addObserver(this, TOPIC_QUIT_APPLICATION_REQUESTED); + + let quitApplicationResponse; + try { + this._isShuttingDown = true; + quitApplicationResponse = await lazy.quit( + flags, + safeMode, + this.currentSession.capabilities.get("moz:windowless") + ); + } catch (e) { + this._isShuttingDown = false; + if (e instanceof TypeError) { + throw new lazy.error.InvalidArgumentError(e.message); + } + throw new lazy.error.UnsupportedOperationError(e.message); + } finally { + Services.obs.removeObserver(this, TOPIC_QUIT_APPLICATION_REQUESTED); + } + + return quitApplicationResponse; +}; + +GeckoDriver.prototype.installAddon = function (cmd) { + lazy.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 lazy.error.InvalidArgumentError(); + } + + return lazy.Addon.install(path, temp); +}; + +GeckoDriver.prototype.uninstallAddon = function (cmd) { + lazy.assert.desktop(); + + let id = cmd.parameters.id; + if (typeof id == "undefined" || typeof id != "string") { + throw new lazy.error.InvalidArgumentError(); + } + + return lazy.Addon.uninstall(id); +}; + +/** + * Retrieve the localized string for the specified entity id. + * + * Example: + * localizeEntity(["chrome://branding/locale/brand.dtd"], "brandShortName") + * + * @param {object} cmd + * @param {Array.<string>} cmd.parameters.urls + * Array of .dtd URLs. + * @param {string} cmd.parameters.id + * The ID of the entity to retrieve the localized string for. + * + * @returns {string} + * The localized string for the requested entity. + */ +GeckoDriver.prototype.localizeEntity = function (cmd) { + let { urls, id } = cmd.parameters; + + if (!Array.isArray(urls)) { + throw new lazy.error.InvalidArgumentError( + "Value of `urls` should be of type 'Array'" + ); + } + if (typeof id != "string") { + throw new lazy.error.InvalidArgumentError( + "Value of `id` should be of type 'string'" + ); + } + + return lazy.l10n.localizeEntity(urls, id); +}; + +/** + * Retrieve the localized string for the specified property id. + * + * Example: + * + * localizeProperty( + * ["chrome://global/locale/findbar.properties"], "FastFind"); + * + * @param {object} cmd + * @param {Array.<string>} cmd.parameters.urls + * Array of .properties URLs. + * @param {string} cmd.parameters.id + * The ID of the property to retrieve the localized string for. + * + * @returns {string} + * The localized string for the requested property. + */ +GeckoDriver.prototype.localizeProperty = function (cmd) { + let { urls, id } = cmd.parameters; + + if (!Array.isArray(urls)) { + throw new lazy.error.InvalidArgumentError( + "Value of `urls` should be of type 'Array'" + ); + } + if (typeof id != "string") { + throw new lazy.error.InvalidArgumentError( + "Value of `id` should be of type 'string'" + ); + } + + return lazy.l10n.localizeProperty(urls, id); +}; + +/** + * Initialize the reftest mode + */ +GeckoDriver.prototype.setupReftest = async function (cmd) { + if (this._reftest) { + throw new lazy.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 lazy.error.InvalidArgumentError( + "Value of `screenshot` should be 'always', 'fail' or 'unexpected'" + ); + } + + this._reftest = new lazy.reftest.Runner(this); + this._reftest.setup(urlCount, screenshot, isPrint); +}; + +/** Run a reftest. */ +GeckoDriver.prototype.runReftest = function (cmd) { + let { test, references, expected, timeout, width, height, pageRanges } = + cmd.parameters; + + if (!this._reftest) { + throw new lazy.error.UnsupportedOperationError( + "Called reftest:run before reftest:start" + ); + } + + lazy.assert.string(test); + lazy.assert.string(expected); + lazy.assert.array(references); + + return 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 lazy.error.UnsupportedOperationError( + "Called reftest:teardown before reftest:start" + ); + } + + this._reftest.teardown(); + this._reftest = null; +}; + +/** + * Print page as PDF. + * + * @param {object} cmd + * @param {boolean=} cmd.parameters.background + * Whether or not to print background colors and images. + * Defaults to false, which prints without background graphics. + * @param {number=} cmd.parameters.margin.bottom + * Bottom margin in cm. Defaults to 1cm (~0.4 inches). + * @param {number=} cmd.parameters.margin.left + * Left margin in cm. Defaults to 1cm (~0.4 inches). + * @param {number=} cmd.parameters.margin.right + * Right margin in cm. Defaults to 1cm (~0.4 inches). + * @param {number=} cmd.parameters.margin.top + * Top margin in cm. Defaults to 1cm (~0.4 inches). + * @param {('landscape'|'portrait')=} cmd.parameters.options.orientation + * Paper orientation. Defaults to 'portrait'. + * @param {Array.<string|number>=} cmd.parameters.pageRanges + * Paper ranges to print, e.g., ['1-5', 8, '11-13']. + * Defaults to the empty array, which means print all pages. + * @param {number=} cmd.parameters.page.height + * Paper height in cm. Defaults to US letter height (27.94cm / 11 inches) + * @param {number=} cmd.parameters.page.width + * Paper width in cm. Defaults to US letter width (21.59cm / 8.5 inches) + * @param {number=} cmd.parameters.scale + * Scale of the webpage rendering. Defaults to 1.0. + * @param {boolean=} cmd.parameters.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. + * + * @returns {string} + * Base64 encoded PDF representing printed document + * + * @throws {NoSuchWindowError} + * Top-level browsing context has been discarded. + * @throws {UnexpectedAlertOpenError} + * A modal dialog is open, blocking this operation. + * @throws {UnsupportedOperationError} + * Not available in chrome context. + */ +GeckoDriver.prototype.print = async function (cmd) { + lazy.assert.content(this.context); + lazy.assert.open(this.getBrowsingContext({ top: true })); + await this._handleUserPrompts(); + + const settings = lazy.print.addDefaultSettings(cmd.parameters); + for (const prop of ["top", "bottom", "left", "right"]) { + lazy.assert.positiveNumber( + settings.margin[prop], + lazy.pprint`margin.${prop} is not a positive number` + ); + } + for (const prop of ["width", "height"]) { + lazy.assert.positiveNumber( + settings.page[prop], + lazy.pprint`page.${prop} is not a positive number` + ); + } + lazy.assert.positiveNumber( + settings.scale, + `scale ${settings.scale} is not a positive number` + ); + lazy.assert.that( + s => + s >= lazy.print.minScaleValue && + settings.scale <= lazy.print.maxScaleValue, + `scale ${settings.scale} is outside the range ${lazy.print.minScaleValue}-${lazy.print.maxScaleValue}` + )(settings.scale); + lazy.assert.boolean(settings.shrinkToFit); + lazy.assert.that( + orientation => lazy.print.defaults.orientationValue.includes(orientation), + `orientation ${ + settings.orientation + } doesn't match allowed values "${lazy.print.defaults.orientationValue.join( + "/" + )}"` + )(settings.orientation); + lazy.assert.boolean(settings.background); + lazy.assert.array(settings.pageRanges); + + const browsingContext = this.curBrowser.tab.linkedBrowser.browsingContext; + const printSettings = await lazy.print.getPrintSettings(settings); + const binaryString = await lazy.print.printToBinaryString( + browsingContext, + printSettings + ); + + return btoa(binaryString); +}; + +GeckoDriver.prototype.addVirtualAuthenticator = function (cmd) { + const { + protocol, + transport, + hasResidentKey, + hasUserVerification, + isUserConsenting, + isUserVerified, + } = cmd.parameters; + + lazy.assert.string( + protocol, + "addVirtualAuthenticator: protocol must be a string" + ); + lazy.assert.string( + transport, + "addVirtualAuthenticator: transport must be a string" + ); + lazy.assert.boolean( + hasResidentKey, + "addVirtualAuthenticator: hasResidentKey must be a boolean" + ); + lazy.assert.boolean( + hasUserVerification, + "addVirtualAuthenticator: hasUserVerification must be a boolean" + ); + lazy.assert.boolean( + isUserConsenting, + "addVirtualAuthenticator: isUserConsenting must be a boolean" + ); + lazy.assert.boolean( + isUserVerified, + "addVirtualAuthenticator: isUserVerified must be a boolean" + ); + + return lazy.webauthn.addVirtualAuthenticator( + protocol, + transport, + hasResidentKey, + hasUserVerification, + isUserConsenting, + isUserVerified + ); +}; + +GeckoDriver.prototype.removeVirtualAuthenticator = function (cmd) { + const { authenticatorId } = cmd.parameters; + + lazy.assert.positiveInteger( + authenticatorId, + "removeVirtualAuthenticator: authenticatorId must be a positiveInteger" + ); + + lazy.webauthn.removeVirtualAuthenticator(authenticatorId); +}; + +GeckoDriver.prototype.addCredential = function (cmd) { + const { + authenticatorId, + credentialId, + isResidentCredential, + rpId, + privateKey, + userHandle, + signCount, + } = cmd.parameters; + + lazy.assert.positiveInteger( + authenticatorId, + "addCredential: authenticatorId must be a positiveInteger" + ); + lazy.assert.string( + credentialId, + "addCredential: credentialId must be a string" + ); + lazy.assert.boolean( + isResidentCredential, + "addCredential: isResidentCredential must be a boolean" + ); + lazy.assert.string(rpId, "addCredential: rpId must be a string"); + lazy.assert.string(privateKey, "addCredential: privateKey must be a string"); + if (userHandle) { + lazy.assert.string( + userHandle, + "addCredential: userHandle must be a string if present" + ); + } + lazy.assert.number(signCount, "addCredential: signCount must be a number"); + + lazy.webauthn.addCredential( + authenticatorId, + credentialId, + isResidentCredential, + rpId, + privateKey, + userHandle, + signCount + ); +}; + +GeckoDriver.prototype.getCredentials = function (cmd) { + const { authenticatorId } = cmd.parameters; + + lazy.assert.positiveInteger( + authenticatorId, + "getCredentials: authenticatorId must be a positiveInteger" + ); + + return lazy.webauthn.getCredentials(authenticatorId); +}; + +GeckoDriver.prototype.removeCredential = function (cmd) { + const { authenticatorId, credentialId } = cmd.parameters; + + lazy.assert.positiveInteger( + authenticatorId, + "removeCredential: authenticatorId must be a positiveInteger" + ); + lazy.assert.string( + credentialId, + "removeCredential: credentialId must be a string" + ); + + lazy.webauthn.removeCredential(authenticatorId, credentialId); +}; + +GeckoDriver.prototype.removeAllCredentials = function (cmd) { + const { authenticatorId } = cmd.parameters; + + lazy.assert.positiveInteger( + authenticatorId, + "removeAllCredentials: authenticatorId must be a positiveInteger" + ); + + lazy.webauthn.removeAllCredentials(authenticatorId); +}; + +GeckoDriver.prototype.setUserVerified = function (cmd) { + const { authenticatorId, isUserVerified } = cmd.parameters; + + lazy.assert.positiveInteger( + authenticatorId, + "setUserVerified: authenticatorId must be a positiveInteger" + ); + lazy.assert.boolean( + isUserVerified, + "setUserVerified: isUserVerified must be a boolean" + ); + + lazy.webauthn.setUserVerified(authenticatorId, isUserVerified); +}; + +GeckoDriver.prototype.setPermission = async function (cmd) { + const { descriptor, state, oneRealm = false } = cmd.parameters; + const browsingContext = lazy.assert.open(this.getBrowsingContext()); + + // XXX: WPT should not have these but currently they do and we pass testing pref to + // pass them, see bug 1875837. + if ( + ["clipboard-read", "clipboard-write"].includes(descriptor.name) && + state === "granted" + ) { + if ( + Services.prefs.getBoolPref("dom.events.testing.asyncClipboard", false) + ) { + // Okay, do nothing. The clipboard module will work without permission. + return; + } + throw new lazy.error.UnsupportedOperationError( + "setPermission: expected dom.events.testing.asyncClipboard to be set" + ); + } + + // XXX: We currently depend on camera/microphone tests throwing UnsupportedOperationError, + // the fix is ongoing in bug 1609427. + if (["camera", "microphone"].includes(descriptor.name)) { + throw new lazy.error.UnsupportedOperationError( + "setPermission: camera and microphone permissions are currently unsupported" + ); + } + + // XXX: Allowing this permission causes timing related Android crash, see also bug 1878741 + if (descriptor.name === "notifications") { + if (Services.prefs.getBoolPref("notification.prompt.testing", false)) { + // Okay, do nothing. The notifications module will work without permission. + return; + } + throw new lazy.error.UnsupportedOperationError( + "setPermission: expected notification.prompt.testing to be set" + ); + } + + let params; + try { + params = + await this.curBrowser.window.navigator.permissions.parseSetParameters({ + descriptor, + state, + }); + } catch (err) { + throw new lazy.error.InvalidArgumentError(`setPermission: ${err.message}`); + } + + lazy.assert.boolean(oneRealm); + + lazy.permissions.set(params.type, params.state, oneRealm, browsingContext); +}; + +/** + * Determines the Accessibility label for this element. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Web element reference ID to the element for which the accessibility label + * will be returned. + * + * @returns {string} + * The Accessibility label for this element + */ +GeckoDriver.prototype.getComputedLabel = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + + return this.getActor().getComputedLabel(webEl); +}; + +/** + * Determines the Accessibility role for this element. + * + * @param {object} cmd + * @param {string} cmd.parameters.id + * Web element reference ID to the element for which the accessibility role + * will be returned. + * + * @returns {string} + * The Accessibility role for this element + */ +GeckoDriver.prototype.getComputedRole = async function (cmd) { + lazy.assert.open(this.getBrowsingContext()); + await this._handleUserPrompts(); + + let id = lazy.assert.string(cmd.parameters.id); + let webEl = lazy.WebElement.fromUUID(id).toJSON(); + return this.getActor().getComputedRole(webEl); +}; + +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, + + // 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, + // deprecated, no longer used since the geckodriver 0.30.0 release + "WebDriver:AcceptDialog": GeckoDriver.prototype.acceptDialog, + "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:FindElementFromShadowRoot": + GeckoDriver.prototype.findElementFromShadowRoot, + "WebDriver:FindElements": GeckoDriver.prototype.findElements, + "WebDriver:FindElementsFromShadowRoot": + GeckoDriver.prototype.findElementsFromShadowRoot, + "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:GetComputedLabel": GeckoDriver.prototype.getComputedLabel, + "WebDriver:GetComputedRole": GeckoDriver.prototype.getComputedRole, + "WebDriver:GetCookies": GeckoDriver.prototype.getCookies, + "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:GetShadowRoot": GeckoDriver.prototype.getShadowRoot, + "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:SetPermission": GeckoDriver.prototype.setPermission, + "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, + + // WebAuthn + "WebAuthn:AddVirtualAuthenticator": + GeckoDriver.prototype.addVirtualAuthenticator, + "WebAuthn:RemoveVirtualAuthenticator": + GeckoDriver.prototype.removeVirtualAuthenticator, + "WebAuthn:AddCredential": GeckoDriver.prototype.addCredential, + "WebAuthn:GetCredentials": GeckoDriver.prototype.getCredentials, + "WebAuthn:RemoveCredential": GeckoDriver.prototype.removeCredential, + "WebAuthn:RemoveAllCredentials": GeckoDriver.prototype.removeAllCredentials, + "WebAuthn:SetUserVerified": GeckoDriver.prototype.setUserVerified, +}; + +async function exitFullscreen(win) { + let cb; + // Use a timed promise to abort if no window manager is present + await new lazy.TimedPromise( + resolve => { + cb = new lazy.DebounceCallback(resolve); + win.addEventListener("sizemodechange", cb); + win.fullScreen = false; + }, + { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER } + ); + win.removeEventListener("sizemodechange", cb); + await new lazy.IdlePromise(win); +} + +async function restoreWindow(win) { + let cb; + if (lazy.WindowState.from(win.windowState) == lazy.WindowState.Normal) { + return; + } + // Use a timed promise to abort if no window manager is present + await new lazy.TimedPromise( + resolve => { + cb = new lazy.DebounceCallback(resolve); + win.addEventListener("sizemodechange", cb); + win.restore(); + }, + { throws: null, timeout: TIMEOUT_NO_WINDOW_MANAGER } + ); + win.removeEventListener("sizemodechange", cb); + await new lazy.IdlePromise(win); +} |