/* 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, {
actions: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
Addon: "chrome://remote/content/shared/Addon.sys.mjs",
AnimationFramePromise: "chrome://remote/content/shared/Sync.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",
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/shared/Permissions.sys.mjs",
pprint: "chrome://remote/content/shared/Format.sys.mjs",
print: "chrome://remote/content/shared/PDF.sys.mjs",
PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
PromptHandlers:
"chrome://remote/content/shared/webdriver/UserPromptHandler.sys.mjs",
PromptListener:
"chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
PromptTypes:
"chrome://remote/content/shared/webdriver/UserPromptHandler.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",
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/shared/WindowManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
);
ChromeUtils.defineLazyGetter(lazy, "prefAsyncEventsEnabled", () =>
Services.prefs.getBoolPref("remote.events.async.enabled", false)
);
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
*/
class ActionsHelper {
#actionsOptions;
#driver;
constructor(driver) {
this.#driver = driver;
// Options for actions to pass through performActions and releaseActions.
this.#actionsOptions = {
// Callbacks as defined in the WebDriver specification.
getElementOrigin: this.getElementOrigin.bind(this),
isElementOrigin: this.isElementOrigin.bind(this),
// Custom callbacks.
assertInViewPort: this.assertInViewPort.bind(this),
dispatchEvent: this.dispatchEvent.bind(this),
getClientRects: this.getClientRects.bind(this),
getInViewCentrePoint: this.getInViewCentrePoint.bind(this),
};
}
get actionsOptions() {
return this.#actionsOptions;
}
#getActor(browsingContext) {
return lazy.getMarionetteCommandsActorProxy(() => browsingContext);
}
/**
* Assert that the target coordinates are within the visible viewport.
*
* @param {Array.<number>} target
* Coordinates [x, y] of the target relative to the viewport.
* @param {BrowsingContext} browsingContext
* The browsing context to dispatch the event to.
*
* @returns {Promise<undefined>}
* Promise that rejects, if the coordinates are not within
* the visible viewport.
*
* @throws {MoveTargetOutOfBoundsError}
* If target is outside the viewport.
*/
assertInViewPort(target, browsingContext) {
return this.#getActor(browsingContext).assertInViewPort(target);
}
/**
* Dispatch an event.
*
* @param {string} eventName
* Name of the event to be dispatched.
* @param {BrowsingContext} browsingContext
* The browsing context to dispatch the event to.
* @param {object} details
* Details of the event to be dispatched.
*
* @returns {Promise}
* Promise that resolves once the event is dispatched.
*/
dispatchEvent(eventName, browsingContext, details) {
return this.#getActor(browsingContext).dispatchEvent(eventName, details);
}
/**
* Finalize an action command.
*
* @param {BrowsingContext} browsingContext
* The browsing context to dispatch the event to.
*
* @returns {Promise}
* Promise that resolves when the finalization is done.
*/
finalizeAction(browsingContext) {
return this.#getActor(browsingContext).finalizeAction();
}
/**
* Retrieves the WebElement reference of the origin.
*
* @param {ElementOrigin} origin
* Reference to the element origin of the action.
* @param {BrowsingContext} _browsingContext
* Not used by Marionette.
*
* @returns {WebElement}
* The WebElement reference.
*/
getElementOrigin(origin, _browsingContext) {
return origin;
}
/**
* Retrieve the list of client rects for the element.
*
* @param {WebElement} element
* The web element reference to retrieve the rects from.
* @param {BrowsingContext} browsingContext
* The browsing context to dispatch the event to.
*
* @returns {Promise<Array<Map.<string, number>>>}
* Promise that resolves to a list of DOMRect-like objects.
*/
getClientRects(element, browsingContext) {
return this.#getActor(browsingContext).executeScript(
"return arguments[0].getClientRects()",
[element]
);
}
/**
* Retrieve the in-view center point for the rect and visible viewport.
*
* @param {DOMRect} rect
* Size and position of the rectangle to check.
* @param {BrowsingContext} browsingContext
* The browsing context to dispatch the event to.
*
* @returns {Promise<Map.<string, number>>}
* X and Y coordinates that denotes the in-view centre point of
* `rect`.
*/
getInViewCentrePoint(rect, browsingContext) {
return this.#getActor(browsingContext).getInViewCentrePoint(rect);
}
/**
* Retrieves the action's input state.
*
* @param {BrowsingContext} browsingContext
* The Browsing Context to retrieve the input state for.
*
* @returns {Actions.InputState}
* The action's input state.
*/
getInputState(browsingContext) {
// Bug 1821460: Fetch top-level browsing context.
let inputState = this.#driver._inputStates.get(browsingContext);
if (inputState === undefined) {
inputState = new lazy.actions.State();
this.#driver._inputStates.set(browsingContext, inputState);
}
return inputState;
}
/**
* Checks if the given object is a valid element origin.
*
* @param {object} origin
* The object to check.
*
* @returns {boolean}
* True, if it's a WebElement.
*/
isElementOrigin(origin) {
return lazy.WebElement.Identifier in origin;
}
/**
* Resets the action's input state.
*
* @param {BrowsingContext} browsingContext
* The Browsing Context to reset the input state for.
*/
resetInputState(browsingContext) {
// Bug 1821460: Fetch top-level browsing context.
if (this.#driver._inputStates.has(browsingContext)) {
this.#driver._inputStates.delete(browsingContext);
}
}
}
/**
* 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 a WebDriver HTTP session
this._sessionConfigFlags = new Set([lazy.WebDriverSession.SESSION_FLAG_HTTP]);
// 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;
// Browsing context => input state.
// Bug 1821460: Move to WebDriver Session and share with Remote Agent.
this._inputStates = new WeakMap();
this._actionsHelper = new ActionsHelper(this);
}
/**
* 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" && !this.currentSession?.bidi) {
// Only implicitly accept the prompt when its not a BiDi session.
lazy.logger.trace(`Implicitly accepted "beforeunload" prompt`);
this.dialog.accept();
return;
}
if (!this._isShuttingDown) {
this.getActor().notifyDialogOpened(this.dialog);
}
};
/**
* Get the current URL.
*
* @param {object} options
* @param {boolean=} options.top
* If set to true return the window's top-level URL,
* otherwise the one from the currently selected frame. Defaults to true.
* @see
https://w3c.github.io/webdriver/#get-current-url
*/
GeckoDriver.prototype._getCurrentURL = function (options = {}) {
if (options.top === undefined) {
options.top = true;
}
const browsingContext = this.getBrowsingContext(options);
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 {Record<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 (lazy.RemoteAgent.webDriverBiDi) {
// If the WebDriver BiDi protocol is active always use the Remote Agent
// to handle the WebDriver session.
await lazy.RemoteAgent.webDriverBiDi.createSession(
capabilities,
this._sessionConfigFlags
);
} else {
// If it's not the case then Marionette itself needs to handle it, and
// has to nullify the "webSocketUrl" capability.
this._currentSession = new lazy.WebDriverSession(
capabilities,
this._sessionConfigFlags
);
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) {
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,
lazy.pprint`Expected "value" to be a string, got ${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, got ${script}`
);
lazy.assert.array(
args,
lazy.pprint`Expected "args" to be an array, got ${args}`
);
if (sandboxName !== null) {
lazy.assert.string(
sandboxName,
lazy.pprint`Expected "sandboxName" to be a string, got ${sandboxName}`
);
}
lazy.assert.boolean(
newSandbox,
lazy.pprint`Expected "newSandbox" to be boolean, got ${newSandbox}`
);
lazy.assert.string(
file,
lazy.pprint`Expected "file" to be a string, got ${file}`
);
lazy.assert.number(
line,
lazy.pprint`Expected "line" to be a number, got ${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?.canGoBackIgnoringUserInteraction) {
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 {Record<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 {Record<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,
lazy.pprint`Expected "x" to be an integer value, got ${x}`
);
}
if (y !== null) {
lazy.assert.integer(
y,
lazy.pprint`Expected "y" to be an integer value, got ${y}`
);
}
if (height !== null) {
lazy.assert.positiveInteger(
height,
lazy.pprint`Expected "height" to be a positive integer value, got ${height}`
);
}
if (width !== null) {
lazy.assert.positiveInteger(
width,
lazy.pprint`Expected "width" to be a positive integer value, got ${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;
}
// Wayland doesn't support getting the window position.
if (
!lazy.AppInfo.isWayland &&
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);
}
// Wayland doesn't support setting the window position.
if (lazy.AppInfo.isWayland !== "wayland" && 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,
lazy.pprint`Expected "id" to be an 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;
}
// If the current context changed during the switchToFrame call, attempt to
// call switchToFrame again until the browsing context remains stable.
// See
https://bugzilla.mozilla.org/show_bug.cgi?id=1786640#c11
let browsingContext;
for (let i = 0; i < 5; i++) {
const currentBrowsingContext = this.currentSession.contentBrowsingContext;
({ browsingContext } = await this.getActor({ top }).switchToFrame(
byFrame || id
));
if (currentBrowsingContext == this.currentSession.contentBrowsingContext) {
break;
}
}
this.currentSession.contentBrowsingContext = browsingContext;
};
GeckoDriver.prototype.getTimeouts = function () {
return this.currentSession.timeouts;
};
/**
* Set timeout for page loading, searching, and scripts.
*
* @param {object} cmd
* @param {Record<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) {
const { actions } = cmd.parameters;
const browsingContext = lazy.assert.open(this.getBrowsingContext());
await this._handleUserPrompts();
if (!lazy.prefAsyncEventsEnabled) {
// Bug 1920959: Remove if we no longer need to dispatch in content.
await this.getActor().performActions(actions);
return;
}
// Bug 1821460: Fetch top-level browsing context.
const inputState = this._actionsHelper.getInputState(browsingContext);
const actionsOptions = {
...this._actionsHelper.actionsOptions,
context: browsingContext,
};
const actionChain = await lazy.actions.Chain.fromJSON(
inputState,
actions,
actionsOptions
);
// Enqueue to serialize access to input state.
await inputState.enqueueAction(() =>
actionChain.dispatch(inputState, actionsOptions)
);
// Process async follow-up tasks in content before the reply is sent.
await this._actionsHelper.finalizeAction(browsingContext);
};
/**
* 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 () {
const browsingContext = lazy.assert.open(this.getBrowsingContext());
await this._handleUserPrompts();
if (!lazy.prefAsyncEventsEnabled) {
// Bug 1920959: Remove if we no longer need to dispatch in content.
await this.getActor().releaseActions();
return;
}
// Bug 1821460: Fetch top-level browsing context.
const inputState = this._actionsHelper.getInputState(browsingContext);
const actionsOptions = {
...this._actionsHelper.actionsOptions,
context: browsingContext,
};
// Enqueue to serialize access to input state.
await inputState.enqueueAction(() => {
const undoActions = inputState.inputCancelList.reverse();
return undoActions.dispatch(inputState, actionsOptions);
});
this._actionsHelper.resetInputState(browsingContext);
// Process async follow-up tasks in content before the reply is sent.
await this._actionsHelper.finalizeAction(browsingContext);
};
/**
* 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,
lazy.pprint`Expected "id" to be a string, got ${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,
--> --------------------
--> maximum size reached
--> --------------------