summaryrefslogtreecommitdiffstats
path: root/testing/marionette/actors/MarionetteCommandsParent.jsm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /testing/marionette/actors/MarionetteCommandsParent.jsm
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/marionette/actors/MarionetteCommandsParent.jsm')
-rw-r--r--testing/marionette/actors/MarionetteCommandsParent.jsm396
1 files changed, 396 insertions, 0 deletions
diff --git a/testing/marionette/actors/MarionetteCommandsParent.jsm b/testing/marionette/actors/MarionetteCommandsParent.jsm
new file mode 100644
index 0000000000..f7c1990ef5
--- /dev/null
+++ b/testing/marionette/actors/MarionetteCommandsParent.jsm
@@ -0,0 +1,396 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+("use strict");
+
+const EXPORTED_SYMBOLS = [
+ "clearElementIdCache",
+ "getMarionetteCommandsActorProxy",
+ "MarionetteCommandsParent",
+ "registerCommandsActor",
+ "unregisterCommandsActor",
+];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ capture: "chrome://marionette/content/capture.js",
+ element: "chrome://marionette/content/element.js",
+ error: "chrome://marionette/content/error.js",
+ evaluate: "chrome://marionette/content/evaluate.js",
+ Log: "chrome://marionette/content/log.js",
+ modal: "chrome://marionette/content/modal.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
+XPCOMUtils.defineLazyGetter(this, "elementIdCache", () => {
+ return new element.ReferenceStore();
+});
+
+class MarionetteCommandsParent extends JSWindowActorParent {
+ actorCreated() {
+ this._resolveDialogOpened = null;
+
+ this.dialogObserver = new modal.DialogObserver();
+ this.dialogObserver.add(this.onDialog.bind(this));
+
+ this.topWindow = this.browsingContext.top.embedderElement?.ownerGlobal;
+ this.topWindow?.addEventListener("TabClose", _onTabClose);
+ }
+
+ dialogOpenedPromise() {
+ return new Promise(resolve => {
+ this._resolveDialogOpened = resolve;
+ });
+ }
+
+ async sendQuery(name, data) {
+ const serializedData = evaluate.toJSON(data, elementIdCache);
+
+ // return early if a dialog is opened
+ const result = await Promise.race([
+ super.sendQuery(name, serializedData),
+ this.dialogOpenedPromise(),
+ ]).finally(() => {
+ this._resolveDialogOpened = null;
+ });
+
+ if ("error" in result) {
+ throw error.WebDriverError.fromJSON(result.error);
+ } else {
+ return evaluate.fromJSON(result.data, elementIdCache);
+ }
+ }
+
+ didDestroy() {
+ this.dialogObserver.remove(this.onDialog);
+ this.dialogObserver.unregister();
+
+ this.topWindow?.removeEventListener("TabClose", _onTabClose);
+ }
+
+ onDialog(action, dialogRef, win) {
+ if (
+ this._resolveDialogOpened &&
+ action == "opened" &&
+ win == this.browsingContext.topChromeWindow
+ ) {
+ this._resolveDialogOpened({ data: null });
+ }
+ }
+
+ // Proxying methods for WebDriver commands
+ // TODO: Maybe using a proxy class instead similar to proxy.js
+
+ clearElement(webEl) {
+ return this.sendQuery("MarionetteCommandsParent:clearElement", {
+ elem: webEl,
+ });
+ }
+
+ clickElement(webEl, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:clickElement", {
+ elem: webEl,
+ capabilities,
+ });
+ }
+
+ async executeScript(script, args, opts) {
+ return this.sendQuery("MarionetteCommandsParent:executeScript", {
+ script,
+ 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 getActiveElement() {
+ return this.sendQuery("MarionetteCommandsParent:getActiveElement");
+ }
+
+ async getCurrentUrl() {
+ return this.sendQuery("MarionetteCommandsParent:getCurrentUrl");
+ }
+
+ 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,
+ elem: webEl,
+ });
+ }
+
+ async isElementEnabled(webEl, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:isElementEnabled", {
+ capabilities,
+ elem: webEl,
+ });
+ }
+
+ async isElementSelected(webEl, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:isElementSelected", {
+ capabilities,
+ elem: webEl,
+ });
+ }
+
+ async sendKeysToElement(webEl, text, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:sendKeysToElement", {
+ capabilities,
+ elem: webEl,
+ text,
+ });
+ }
+
+ async performActions(actions, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:performActions", {
+ actions,
+ capabilities,
+ });
+ }
+
+ async releaseActions() {
+ return this.sendQuery("MarionetteCommandsParent:releaseActions");
+ }
+
+ async singleTap(webEl, x, y, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:singleTap", {
+ capabilities,
+ elem: webEl,
+ x,
+ y,
+ });
+ }
+
+ 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 capture.canvas(
+ browsingContext.topChromeWindow,
+ browsingContext,
+ rect.x,
+ rect.y,
+ rect.width,
+ rect.height
+ );
+
+ switch (format) {
+ case capture.Format.Hash:
+ return capture.toHash(canvas);
+
+ case capture.Format.Base64:
+ return capture.toBase64(canvas);
+
+ default:
+ throw new TypeError(`Invalid capture format: ${format}`);
+ }
+ }
+}
+
+/**
+ * Clear all the entries from the element id cache.
+ */
+function clearElementIdCache() {
+ elementIdCache.clear();
+}
+
+function _onTabClose(event) {
+ elementIdCache.clear(event.target.linkedBrowser.browsingContext);
+}
+
+/**
+ * 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.
+ */
+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",
+ "singleTap",
+ ];
+
+ return new Proxy(
+ {},
+ {
+ get(target, methodName) {
+ return async (...args) => {
+ let attempts = 0;
+ while (true) {
+ try {
+ // TODO: Scenarios where the window/tab got closed and
+ // currentWindowGlobal is null will be handled in Bug 1662808.
+ const actor = browsingContextFn().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)) {
+ return null;
+ }
+
+ if (++attempts > MAX_ATTEMPTS) {
+ const browsingContextId = browsingContextFn()?.id;
+ logger.trace(
+ `[${browsingContextId}] Querying "${methodName} "` +
+ `reached the limit of retry attempts (${MAX_ATTEMPTS})`
+ );
+ throw e;
+ }
+
+ logger.trace(`Retrying "${methodName}", attempt: ${attempts}`);
+ }
+ }
+ };
+ },
+ }
+ );
+}
+
+/**
+ * Register the MarionetteCommands actor that holds all the commands.
+ */
+function registerCommandsActor() {
+ try {
+ ChromeUtils.registerWindowActor("MarionetteCommands", {
+ kind: "JSWindowActor",
+ parent: {
+ moduleURI:
+ "chrome://marionette/content/actors/MarionetteCommandsParent.jsm",
+ },
+ child: {
+ moduleURI:
+ "chrome://marionette/content/actors/MarionetteCommandsChild.jsm",
+ },
+
+ allFrames: true,
+ includeChrome: true,
+ });
+ } catch (e) {
+ if (e.name === "NotSupportedError") {
+ logger.warn(`MarionetteCommands actor is already registered!`);
+ } else {
+ throw e;
+ }
+ }
+}
+
+function unregisterCommandsActor() {
+ ChromeUtils.unregisterWindowActor("MarionetteCommands");
+}