summaryrefslogtreecommitdiffstats
path: root/remote/marionette/actors/MarionetteCommandsParent.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /remote/marionette/actors/MarionetteCommandsParent.sys.mjs
parentInitial commit. (diff)
downloadfirefox-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 'remote/marionette/actors/MarionetteCommandsParent.sys.mjs')
-rw-r--r--remote/marionette/actors/MarionetteCommandsParent.sys.mjs436
1 files changed, 436 insertions, 0 deletions
diff --git a/remote/marionette/actors/MarionetteCommandsParent.sys.mjs b/remote/marionette/actors/MarionetteCommandsParent.sys.mjs
new file mode 100644
index 0000000000..970c927eab
--- /dev/null
+++ b/remote/marionette/actors/MarionetteCommandsParent.sys.mjs
@@ -0,0 +1,436 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ capture: "chrome://remote/content/shared/Capture.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ getSeenNodesForBrowsingContext:
+ "chrome://remote/content/shared/webdriver/Session.sys.mjs",
+ json: "chrome://remote/content/marionette/json.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+// Because Marionette supports a single session only we store its id
+// globally so that the parent actor can access it.
+let webDriverSessionId = null;
+
+export class MarionetteCommandsParent extends JSWindowActorParent {
+ #deferredDialogOpened;
+
+ actorCreated() {
+ this.#deferredDialogOpened = null;
+ }
+
+ async sendQuery(name, serializedValue) {
+ const seenNodes = lazy.getSeenNodesForBrowsingContext(
+ webDriverSessionId,
+ this.manager.browsingContext
+ );
+
+ // return early if a dialog is opened
+ this.#deferredDialogOpened = Promise.withResolvers();
+ let {
+ error,
+ seenNodeIds,
+ serializedValue: serializedResult,
+ hasSerializedWindows,
+ } = await Promise.race([
+ super.sendQuery(name, serializedValue),
+ this.#deferredDialogOpened.promise,
+ ]).finally(() => {
+ this.#deferredDialogOpened = null;
+ });
+
+ if (error) {
+ const err = lazy.error.WebDriverError.fromJSON(error);
+ this.#handleError(err, seenNodes);
+ }
+
+ // Update seen nodes for serialized element and shadow root nodes.
+ seenNodeIds?.forEach(nodeId => seenNodes.add(nodeId));
+
+ if (hasSerializedWindows) {
+ // The serialized data contains WebWindow references that need to be
+ // converted to unique identifiers.
+ serializedResult = lazy.json.mapToNavigableIds(serializedResult);
+ }
+
+ return serializedResult;
+ }
+
+ /**
+ * Handle WebDriver error and replace error type if necessary.
+ *
+ * @param {WebDriverError} error
+ * The WebDriver error to handle.
+ * @param {Set<string>} seenNodes
+ * List of node ids already seen in this navigable.
+ *
+ * @throws {WebDriverError}
+ * The original or replaced WebDriver error.
+ */
+ #handleError(error, seenNodes) {
+ // If an element hasn't been found during deserialization check if it
+ // may be a stale reference.
+ if (
+ error instanceof lazy.error.NoSuchElementError &&
+ error.data.elementId !== undefined &&
+ seenNodes.has(error.data.elementId)
+ ) {
+ throw new lazy.error.StaleElementReferenceError(error);
+ }
+
+ // If a shadow root hasn't been found during deserialization check if it
+ // may be a detached reference.
+ if (
+ error instanceof lazy.error.NoSuchShadowRootError &&
+ error.data.shadowId !== undefined &&
+ seenNodes.has(error.data.shadowId)
+ ) {
+ throw new lazy.error.DetachedShadowRootError(error);
+ }
+
+ throw error;
+ }
+
+ notifyDialogOpened() {
+ if (this.#deferredDialogOpened) {
+ this.#deferredDialogOpened.resolve({ data: null });
+ }
+ }
+
+ // Proxying methods for WebDriver commands
+
+ clearElement(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:clearElement", {
+ elem: webEl,
+ });
+ }
+
+ clickElement(webEl, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:clickElement", {
+ elem: webEl,
+ capabilities: capabilities.toJSON(),
+ });
+ }
+
+ async executeScript(script, args, opts) {
+ return this.sendQuery("MarionetteCommandsParent:executeScript", {
+ script,
+ args: lazy.json.mapFromNavigableIds(args),
+ opts,
+ });
+ }
+
+ findElement(strategy, selector, opts) {
+ return this.sendQuery("MarionetteCommandsParent:findElement", {
+ strategy,
+ selector,
+ opts,
+ });
+ }
+
+ findElements(strategy, selector, opts) {
+ return this.sendQuery("MarionetteCommandsParent:findElements", {
+ strategy,
+ selector,
+ opts,
+ });
+ }
+
+ async getShadowRoot(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:getShadowRoot", {
+ elem: webEl,
+ });
+ }
+
+ async getActiveElement() {
+ return this.sendQuery("MarionetteCommandsParent:getActiveElement");
+ }
+
+ async getComputedLabel(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:getComputedLabel", {
+ elem: webEl,
+ });
+ }
+
+ async getComputedRole(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:getComputedRole", {
+ elem: webEl,
+ });
+ }
+
+ async getElementAttribute(webEl, name) {
+ return this.sendQuery("MarionetteCommandsParent:getElementAttribute", {
+ elem: webEl,
+ name,
+ });
+ }
+
+ async getElementProperty(webEl, name) {
+ return this.sendQuery("MarionetteCommandsParent:getElementProperty", {
+ elem: webEl,
+ name,
+ });
+ }
+
+ async getElementRect(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:getElementRect", {
+ elem: webEl,
+ });
+ }
+
+ async getElementTagName(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:getElementTagName", {
+ elem: webEl,
+ });
+ }
+
+ async getElementText(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:getElementText", {
+ elem: webEl,
+ });
+ }
+
+ async getElementValueOfCssProperty(webEl, name) {
+ return this.sendQuery(
+ "MarionetteCommandsParent:getElementValueOfCssProperty",
+ {
+ elem: webEl,
+ name,
+ }
+ );
+ }
+
+ async getPageSource() {
+ return this.sendQuery("MarionetteCommandsParent:getPageSource");
+ }
+
+ async isElementDisplayed(webEl, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:isElementDisplayed", {
+ capabilities: capabilities.toJSON(),
+ elem: webEl,
+ });
+ }
+
+ async isElementEnabled(webEl, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:isElementEnabled", {
+ capabilities: capabilities.toJSON(),
+ elem: webEl,
+ });
+ }
+
+ async isElementSelected(webEl, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:isElementSelected", {
+ capabilities: capabilities.toJSON(),
+ elem: webEl,
+ });
+ }
+
+ async sendKeysToElement(webEl, text, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:sendKeysToElement", {
+ capabilities: capabilities.toJSON(),
+ elem: webEl,
+ text,
+ });
+ }
+
+ async performActions(actions) {
+ return this.sendQuery("MarionetteCommandsParent:performActions", {
+ actions,
+ });
+ }
+
+ async releaseActions() {
+ return this.sendQuery("MarionetteCommandsParent:releaseActions");
+ }
+
+ async switchToFrame(id) {
+ const { browsingContextId } = await this.sendQuery(
+ "MarionetteCommandsParent:switchToFrame",
+ { id }
+ );
+
+ return {
+ browsingContext: BrowsingContext.get(browsingContextId),
+ };
+ }
+
+ async switchToParentFrame() {
+ const { browsingContextId } = await this.sendQuery(
+ "MarionetteCommandsParent:switchToParentFrame"
+ );
+
+ return {
+ browsingContext: BrowsingContext.get(browsingContextId),
+ };
+ }
+
+ async takeScreenshot(webEl, format, full, scroll) {
+ const rect = await this.sendQuery(
+ "MarionetteCommandsParent:getScreenshotRect",
+ {
+ elem: webEl,
+ full,
+ scroll,
+ }
+ );
+
+ // If no element has been specified use the top-level browsing context.
+ // Otherwise use the browsing context from the currently selected frame.
+ const browsingContext = webEl
+ ? this.browsingContext
+ : this.browsingContext.top;
+
+ let canvas = await lazy.capture.canvas(
+ browsingContext.topChromeWindow,
+ browsingContext,
+ rect.x,
+ rect.y,
+ rect.width,
+ rect.height
+ );
+
+ switch (format) {
+ case lazy.capture.Format.Hash:
+ return lazy.capture.toHash(canvas);
+
+ case lazy.capture.Format.Base64:
+ return lazy.capture.toBase64(canvas);
+
+ default:
+ throw new TypeError(`Invalid capture format: ${format}`);
+ }
+ }
+}
+
+/**
+ * Proxy that will dynamically create MarionetteCommands actors for a dynamically
+ * provided browsing context until the method can be fully executed by the
+ * JSWindowActor pair.
+ *
+ * @param {function(): BrowsingContext} browsingContextFn
+ * A function that returns the reference to the browsing context for which
+ * the query should run.
+ */
+export function getMarionetteCommandsActorProxy(browsingContextFn) {
+ const MAX_ATTEMPTS = 10;
+
+ /**
+ * Methods which modify the content page cannot be retried safely.
+ * See Bug 1673345.
+ */
+ const NO_RETRY_METHODS = [
+ "clickElement",
+ "executeScript",
+ "performActions",
+ "releaseActions",
+ "sendKeysToElement",
+ ];
+
+ return new Proxy(
+ {},
+ {
+ get(target, methodName) {
+ return async (...args) => {
+ let attempts = 0;
+ while (true) {
+ try {
+ const browsingContext = browsingContextFn();
+ if (!browsingContext) {
+ throw new DOMException(
+ "No BrowsingContext found",
+ "NoBrowsingContext"
+ );
+ }
+
+ // TODO: Scenarios where the window/tab got closed and
+ // currentWindowGlobal is null will be handled in Bug 1662808.
+ const actor =
+ browsingContext.currentWindowGlobal.getActor(
+ "MarionetteCommands"
+ );
+
+ const result = await actor[methodName](...args);
+ return result;
+ } catch (e) {
+ if (!["AbortError", "InactiveActor"].includes(e.name)) {
+ // Only retry when the JSWindowActor pair gets destroyed, or
+ // gets inactive eg. when the page is moved into bfcache.
+ throw e;
+ }
+
+ if (NO_RETRY_METHODS.includes(methodName)) {
+ const browsingContextId = browsingContextFn()?.id;
+ lazy.logger.trace(
+ `[${browsingContextId}] Querying "${methodName}" failed with` +
+ ` ${e.name}, returning "null" as fallback`
+ );
+ return null;
+ }
+
+ if (++attempts > MAX_ATTEMPTS) {
+ const browsingContextId = browsingContextFn()?.id;
+ lazy.logger.trace(
+ `[${browsingContextId}] Querying "${methodName} "` +
+ `reached the limit of retry attempts (${MAX_ATTEMPTS})`
+ );
+ throw e;
+ }
+
+ lazy.logger.trace(
+ `Retrying "${methodName}", attempt: ${attempts}`
+ );
+ }
+ }
+ };
+ },
+ }
+ );
+}
+
+/**
+ * Register the MarionetteCommands actor that holds all the commands.
+ *
+ * @param {string} sessionId
+ * The id of the current WebDriver session.
+ */
+export function registerCommandsActor(sessionId) {
+ try {
+ ChromeUtils.registerWindowActor("MarionetteCommands", {
+ kind: "JSWindowActor",
+ parent: {
+ esModuleURI:
+ "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs",
+ },
+ child: {
+ esModuleURI:
+ "chrome://remote/content/marionette/actors/MarionetteCommandsChild.sys.mjs",
+ },
+
+ allFrames: true,
+ includeChrome: true,
+ });
+ } catch (e) {
+ if (e.name === "NotSupportedError") {
+ lazy.logger.warn(`MarionetteCommands actor is already registered!`);
+ } else {
+ throw e;
+ }
+ }
+
+ webDriverSessionId = sessionId;
+}
+
+export function unregisterCommandsActor() {
+ webDriverSessionId = null;
+
+ ChromeUtils.unregisterWindowActor("MarionetteCommands");
+}