summaryrefslogtreecommitdiffstats
path: root/remote/marionette/actors
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /remote/marionette/actors
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/marionette/actors')
-rw-r--r--remote/marionette/actors/MarionetteCommandsChild.sys.mjs579
-rw-r--r--remote/marionette/actors/MarionetteCommandsParent.sys.mjs369
-rw-r--r--remote/marionette/actors/MarionetteEventsChild.sys.mjs84
-rw-r--r--remote/marionette/actors/MarionetteEventsParent.sys.mjs115
-rw-r--r--remote/marionette/actors/MarionetteReftestChild.sys.mjs236
-rw-r--r--remote/marionette/actors/MarionetteReftestParent.sys.mjs85
6 files changed, 1468 insertions, 0 deletions
diff --git a/remote/marionette/actors/MarionetteCommandsChild.sys.mjs b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs
new file mode 100644
index 0000000000..e805d6f9ef
--- /dev/null
+++ b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs
@@ -0,0 +1,579 @@
+/* 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/. */
+
+/* eslint-disable no-restricted-globals */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ action: "chrome://remote/content/marionette/action.sys.mjs",
+ atom: "chrome://remote/content/marionette/atom.sys.mjs",
+ element: "chrome://remote/content/marionette/element.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs",
+ event: "chrome://remote/content/marionette/event.sys.mjs",
+ interaction: "chrome://remote/content/marionette/interaction.sys.mjs",
+ json: "chrome://remote/content/marionette/json.sys.mjs",
+ legacyaction: "chrome://remote/content/marionette/legacyaction.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs",
+ Sandboxes: "chrome://remote/content/marionette/evaluate.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+export class MarionetteCommandsChild extends JSWindowActorChild {
+ #processActor;
+
+ constructor() {
+ super();
+
+ this.#processActor = ChromeUtils.domProcessChild.getActor(
+ "WebDriverProcessData"
+ );
+
+ // sandbox storage and name of the current sandbox
+ this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView);
+ // State of the input actions. This is specific to contexts and sessions
+ this.actionState = null;
+ }
+
+ get innerWindowId() {
+ return this.manager.innerWindowId;
+ }
+
+ /**
+ * Lazy getter to create a legacyaction Chain instance for touch events.
+ */
+ get legacyactions() {
+ if (!this._legacyactions) {
+ this._legacyactions = new lazy.legacyaction.Chain();
+ }
+
+ return this._legacyactions;
+ }
+
+ actorCreated() {
+ lazy.logger.trace(
+ `[${this.browsingContext.id}] MarionetteCommands actor created ` +
+ `for window id ${this.innerWindowId}`
+ );
+ }
+
+ didDestroy() {
+ lazy.logger.trace(
+ `[${this.browsingContext.id}] MarionetteCommands actor destroyed ` +
+ `for window id ${this.innerWindowId}`
+ );
+ }
+
+ async receiveMessage(msg) {
+ if (!this.contentWindow) {
+ throw new DOMException("Actor is no longer active", "InactiveActor");
+ }
+
+ try {
+ let result;
+ let waitForNextTick = false;
+
+ const { name, data: serializedData } = msg;
+ const data = lazy.json.deserialize(
+ serializedData,
+ this.#processActor.getNodeCache(),
+ this.contentWindow
+ );
+
+ switch (name) {
+ case "MarionetteCommandsParent:clearElement":
+ this.clearElement(data);
+ waitForNextTick = true;
+ break;
+ case "MarionetteCommandsParent:clickElement":
+ result = await this.clickElement(data);
+ waitForNextTick = true;
+ break;
+ case "MarionetteCommandsParent:executeScript":
+ result = await this.executeScript(data);
+ waitForNextTick = true;
+ break;
+ case "MarionetteCommandsParent:findElement":
+ result = await this.findElement(data);
+ break;
+ case "MarionetteCommandsParent:findElements":
+ result = await this.findElements(data);
+ break;
+ case "MarionetteCommandsParent:getActiveElement":
+ result = await this.getActiveElement();
+ break;
+ case "MarionetteCommandsParent:getElementAttribute":
+ result = await this.getElementAttribute(data);
+ break;
+ case "MarionetteCommandsParent:getElementProperty":
+ result = await this.getElementProperty(data);
+ break;
+ case "MarionetteCommandsParent:getElementRect":
+ result = await this.getElementRect(data);
+ break;
+ case "MarionetteCommandsParent:getElementTagName":
+ result = await this.getElementTagName(data);
+ break;
+ case "MarionetteCommandsParent:getElementText":
+ result = await this.getElementText(data);
+ break;
+ case "MarionetteCommandsParent:getElementValueOfCssProperty":
+ result = await this.getElementValueOfCssProperty(data);
+ break;
+ case "MarionetteCommandsParent:getPageSource":
+ result = await this.getPageSource();
+ break;
+ case "MarionetteCommandsParent:getScreenshotRect":
+ result = await this.getScreenshotRect(data);
+ break;
+ case "MarionetteCommandsParent:getShadowRoot":
+ result = await this.getShadowRoot(data);
+ break;
+ case "MarionetteCommandsParent:isElementDisplayed":
+ result = await this.isElementDisplayed(data);
+ break;
+ case "MarionetteCommandsParent:isElementEnabled":
+ result = await this.isElementEnabled(data);
+ break;
+ case "MarionetteCommandsParent:isElementSelected":
+ result = await this.isElementSelected(data);
+ break;
+ case "MarionetteCommandsParent:performActions":
+ result = await this.performActions(data);
+ waitForNextTick = true;
+ break;
+ case "MarionetteCommandsParent:releaseActions":
+ result = await this.releaseActions();
+ break;
+ case "MarionetteCommandsParent:sendKeysToElement":
+ result = await this.sendKeysToElement(data);
+ waitForNextTick = true;
+ break;
+ case "MarionetteCommandsParent:singleTap":
+ result = await this.singleTap(data);
+ waitForNextTick = true;
+ break;
+ case "MarionetteCommandsParent:switchToFrame":
+ result = await this.switchToFrame(data);
+ waitForNextTick = true;
+ break;
+ case "MarionetteCommandsParent:switchToParentFrame":
+ result = await this.switchToParentFrame();
+ waitForNextTick = true;
+ break;
+ }
+
+ // Inform the content process that the command has completed. It allows
+ // it to process async follow-up tasks before the reply is sent.
+ if (waitForNextTick) {
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ }
+
+ return {
+ data: lazy.json.clone(result, this.#processActor.getNodeCache()),
+ };
+ } catch (e) {
+ // Always wrap errors as WebDriverError
+ return { error: lazy.error.wrap(e).toJSON() };
+ }
+ }
+
+ // Implementation of WebDriver commands
+
+ /** Clear the text of an element.
+ *
+ * @param {Object} options
+ * @param {Element} options.elem
+ */
+ clearElement(options = {}) {
+ const { elem } = options;
+
+ lazy.interaction.clearElement(elem);
+ }
+
+ /**
+ * Click an element.
+ */
+ async clickElement(options = {}) {
+ const { capabilities, elem } = options;
+
+ return lazy.interaction.clickElement(
+ elem,
+ capabilities["moz:accessibilityChecks"],
+ capabilities["moz:webdriverClick"]
+ );
+ }
+
+ /**
+ * Executes a JavaScript function.
+ */
+ async executeScript(options = {}) {
+ const { args, opts = {}, script } = options;
+
+ let sb;
+ if (opts.sandboxName) {
+ sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox);
+ } else {
+ sb = lazy.sandbox.createMutable(this.document.defaultView);
+ }
+
+ return lazy.evaluate.sandbox(sb, script, args, opts);
+ }
+
+ /**
+ * Find an element in the current browsing context's document using the
+ * given search strategy.
+ *
+ * @param {Object} options
+ * @param {Object} options.opts
+ * @param {Element} opts.startNode
+ * @param {string} opts.strategy
+ * @param {string} opts.selector
+ *
+ */
+ async findElement(options = {}) {
+ const { strategy, selector, opts } = options;
+
+ opts.all = false;
+
+ const container = { frame: this.document.defaultView };
+ return lazy.element.find(container, strategy, selector, opts);
+ }
+
+ /**
+ * Find elements in the current browsing context's document using the
+ * given search strategy.
+ *
+ * @param {Object} options
+ * @param {Object} options.opts
+ * @param {Element} opts.startNode
+ * @param {string} opts.strategy
+ * @param {string} opts.selector
+ *
+ */
+ async findElements(options = {}) {
+ const { strategy, selector, opts } = options;
+
+ opts.all = true;
+
+ const container = { frame: this.document.defaultView };
+ return lazy.element.find(container, strategy, selector, opts);
+ }
+
+ /**
+ * Return the active element in the document.
+ */
+ async getActiveElement() {
+ let elem = this.document.activeElement;
+ if (!elem) {
+ throw new lazy.error.NoSuchElementError();
+ }
+
+ return elem;
+ }
+
+ /**
+ * Get the value of an attribute for the given element.
+ */
+ async getElementAttribute(options = {}) {
+ const { name, elem } = options;
+
+ if (lazy.element.isBooleanAttribute(elem, name)) {
+ if (elem.hasAttribute(name)) {
+ return "true";
+ }
+ return null;
+ }
+ return elem.getAttribute(name);
+ }
+
+ /**
+ * Get the value of a property for the given element.
+ */
+ async getElementProperty(options = {}) {
+ const { name, elem } = options;
+
+ // Waive Xrays to get unfiltered access to the untrusted element.
+ const el = Cu.waiveXrays(elem);
+ return typeof el[name] != "undefined" ? el[name] : null;
+ }
+
+ /**
+ * Get the position and dimensions of the element.
+ */
+ async getElementRect(options = {}) {
+ const { elem } = options;
+
+ const rect = elem.getBoundingClientRect();
+ return {
+ x: rect.x + this.document.defaultView.pageXOffset,
+ y: rect.y + this.document.defaultView.pageYOffset,
+ width: rect.width,
+ height: rect.height,
+ };
+ }
+
+ /**
+ * Get the tagName for the given element.
+ */
+ async getElementTagName(options = {}) {
+ const { elem } = options;
+
+ return elem.tagName.toLowerCase();
+ }
+
+ /**
+ * Get the text content for the given element.
+ */
+ async getElementText(options = {}) {
+ const { elem } = options;
+
+ try {
+ return lazy.atom.getElementText(elem, this.document.defaultView);
+ } catch (e) {
+ lazy.logger.warn(`Atom getElementText failed: "${e.message}"`);
+
+ // Fallback in case the atom implementation is broken.
+ // As known so far this only happens for XML documents (bug 1794099).
+ return elem.textContent;
+ }
+ }
+
+ /**
+ * Get the value of a css property for the given element.
+ */
+ async getElementValueOfCssProperty(options = {}) {
+ const { name, elem } = options;
+
+ const style = this.document.defaultView.getComputedStyle(elem);
+ return style.getPropertyValue(name);
+ }
+
+ /**
+ * Get the source of the current browsing context's document.
+ */
+ async getPageSource() {
+ return this.document.documentElement.outerHTML;
+ }
+
+ /**
+ * Returns the rect of the element to screenshot.
+ *
+ * Because the screen capture takes place in the parent process the dimensions
+ * for the screenshot have to be determined in the appropriate child process.
+ *
+ * Also it takes care of scrolling an element into view if requested.
+ *
+ * @param {Object} options
+ * @param {Element} options.elem
+ * Optional element to take a screenshot of.
+ * @param {boolean=} options.full
+ * True to take a screenshot of the entire document element.
+ * Defaults to true.
+ * @param {boolean=} options.scroll
+ * When <var>elem</var> is given, scroll it into view.
+ * Defaults to true.
+ *
+ * @return {DOMRect}
+ * The area to take a snapshot from.
+ */
+ async getScreenshotRect(options = {}) {
+ const { elem, full = true, scroll = true } = options;
+ const win = elem
+ ? this.document.defaultView
+ : this.browsingContext.top.window;
+
+ let rect;
+
+ if (elem) {
+ if (scroll) {
+ lazy.element.scrollIntoView(elem);
+ }
+ rect = this.getElementRect({ elem });
+ } else if (full) {
+ const docEl = win.document.documentElement;
+ rect = new DOMRect(0, 0, docEl.scrollWidth, docEl.scrollHeight);
+ } else {
+ // viewport
+ rect = new DOMRect(
+ win.pageXOffset,
+ win.pageYOffset,
+ win.innerWidth,
+ win.innerHeight
+ );
+ }
+
+ return rect;
+ }
+
+ /**
+ * Return the shadowRoot attached to an element
+ */
+ async getShadowRoot(options = {}) {
+ const { elem } = options;
+
+ return lazy.element.getShadowRoot(elem);
+ }
+
+ /**
+ * Determine the element displayedness of the given web element.
+ */
+ async isElementDisplayed(options = {}) {
+ const { capabilities, elem } = options;
+
+ return lazy.interaction.isElementDisplayed(
+ elem,
+ capabilities["moz:accessibilityChecks"]
+ );
+ }
+
+ /**
+ * Check if element is enabled.
+ */
+ async isElementEnabled(options = {}) {
+ const { capabilities, elem } = options;
+
+ return lazy.interaction.isElementEnabled(
+ elem,
+ capabilities["moz:accessibilityChecks"]
+ );
+ }
+
+ /**
+ * Determine whether the referenced element is selected or not.
+ */
+ async isElementSelected(options = {}) {
+ const { capabilities, elem } = options;
+
+ return lazy.interaction.isElementSelected(
+ elem,
+ capabilities["moz:accessibilityChecks"]
+ );
+ }
+
+ /**
+ * Perform a series of grouped actions at the specified points in time.
+ *
+ * @param {Object} options
+ * @param {Object} options.actions
+ * Array of objects with each representing an action sequence.
+ * @param {Object} options.capabilities
+ * Object with a list of WebDriver session capabilities.
+ */
+ async performActions(options = {}) {
+ const { actions, capabilities } = options;
+ if (this.actionState === null) {
+ this.actionState = new lazy.action.State({
+ specCompatPointerOrigin: !capabilities[
+ "moz:useNonSpecCompliantPointerOrigin"
+ ],
+ });
+ }
+ let actionChain = lazy.action.Chain.fromJSON(this.actionState, actions);
+
+ await actionChain.dispatch(this.actionState, this.document.defaultView);
+ }
+
+ /**
+ * The release actions command is used to release all the keys and pointer
+ * buttons that are currently depressed. This causes events to be fired
+ * as if the state was released by an explicit series of actions. It also
+ * clears all the internal state of the virtual devices.
+ */
+ async releaseActions() {
+ if (this.actionState === null) {
+ return;
+ }
+ this.actionState.inputsToCancel.reverse();
+ await this.actionState.inputsToCancel.dispatch(
+ this.actionState,
+ this.document.defaultView
+ );
+ this.actionState = null;
+ lazy.event.DoubleClickTracker.resetClick();
+ }
+
+ /*
+ * Send key presses to element after focusing on it.
+ */
+ async sendKeysToElement(options = {}) {
+ const { capabilities, elem, text } = options;
+
+ const opts = {
+ strictFileInteractability: capabilities.strictFileInteractability,
+ accessibilityChecks: capabilities["moz:accessibilityChecks"],
+ webdriverClick: capabilities["moz:webdriverClick"],
+ };
+
+ return lazy.interaction.sendKeysToElement(elem, text, opts);
+ }
+
+ /**
+ * Perform a single tap.
+ */
+ async singleTap(options = {}) {
+ const { capabilities, elem, x, y } = options;
+ return this.legacyactions.singleTap(elem, x, y, capabilities);
+ }
+
+ /**
+ * Switch to the specified frame.
+ *
+ * @param {Object=} options
+ * @param {(number|Element)=} options.id
+ * If it's a number treat it as the index for all the existing frames.
+ * If it's an Element switch to this specific frame.
+ * If not specified or `null` switch to the top-level browsing context.
+ */
+ async switchToFrame(options = {}) {
+ const { id } = options;
+
+ const childContexts = this.browsingContext.children;
+ let browsingContext;
+
+ if (id == null) {
+ browsingContext = this.browsingContext.top;
+ } else if (typeof id == "number") {
+ if (id < 0 || id >= childContexts.length) {
+ throw new lazy.error.NoSuchFrameError(
+ `Unable to locate frame with index: ${id}`
+ );
+ }
+ browsingContext = childContexts[id];
+ } else {
+ const context = childContexts.find(context => {
+ return context.embedderElement === id;
+ });
+ if (!context) {
+ throw new lazy.error.NoSuchFrameError(
+ `Unable to locate frame for element: ${id}`
+ );
+ }
+ browsingContext = context;
+ }
+
+ // For in-process iframes the window global is lazy-loaded for optimization
+ // reasons. As such force the currentWindowGlobal to be created so we always
+ // have a window (bug 1691348).
+ browsingContext.window;
+
+ return { browsingContextId: browsingContext.id };
+ }
+
+ /**
+ * Switch to the parent frame.
+ */
+ async switchToParentFrame() {
+ const browsingContext = this.browsingContext.parent || this.browsingContext;
+
+ return { browsingContextId: browsingContext.id };
+ }
+}
diff --git a/remote/marionette/actors/MarionetteCommandsParent.sys.mjs b/remote/marionette/actors/MarionetteCommandsParent.sys.mjs
new file mode 100644
index 0000000000..fed8a4e7e4
--- /dev/null
+++ b/remote/marionette/actors/MarionetteCommandsParent.sys.mjs
@@ -0,0 +1,369 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ capture: "chrome://remote/content/shared/Capture.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+export class MarionetteCommandsParent extends JSWindowActorParent {
+ actorCreated() {
+ this._resolveDialogOpened = null;
+ }
+
+ dialogOpenedPromise() {
+ return new Promise(resolve => {
+ this._resolveDialogOpened = resolve;
+ });
+ }
+
+ async sendQuery(name, data) {
+ // return early if a dialog is opened
+ const result = await Promise.race([
+ super.sendQuery(name, data),
+ this.dialogOpenedPromise(),
+ ]).finally(() => {
+ this._resolveDialogOpened = null;
+ });
+
+ if ("error" in result) {
+ throw lazy.error.WebDriverError.fromJSON(result.error);
+ } else {
+ return result.data;
+ }
+ }
+
+ notifyDialogOpened() {
+ if (this._resolveDialogOpened) {
+ this._resolveDialogOpened({ 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,
+ 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 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, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:performActions", {
+ actions,
+ capabilities: capabilities.toJSON(),
+ });
+ }
+
+ async releaseActions() {
+ return this.sendQuery("MarionetteCommandsParent:releaseActions");
+ }
+
+ async singleTap(webEl, x, y, capabilities) {
+ return this.sendQuery("MarionetteCommandsParent:singleTap", {
+ capabilities: capabilities.toJSON(),
+ 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 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",
+ "singleTap",
+ ];
+
+ 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.
+ */
+export function registerCommandsActor() {
+ 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;
+ }
+ }
+}
+
+export function unregisterCommandsActor() {
+ ChromeUtils.unregisterWindowActor("MarionetteCommands");
+}
diff --git a/remote/marionette/actors/MarionetteEventsChild.sys.mjs b/remote/marionette/actors/MarionetteEventsChild.sys.mjs
new file mode 100644
index 0000000000..2cf5afac65
--- /dev/null
+++ b/remote/marionette/actors/MarionetteEventsChild.sys.mjs
@@ -0,0 +1,84 @@
+/* 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/. */
+
+/* eslint-disable no-restricted-globals */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ event: "chrome://remote/content/marionette/event.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+export class MarionetteEventsChild extends JSWindowActorChild {
+ get innerWindowId() {
+ return this.manager.innerWindowId;
+ }
+
+ actorCreated() {
+ // Prevent the logger from being created if the current log level
+ // isn't set to 'trace'. This is important for a faster content process
+ // creation when Marionette is running.
+ if (lazy.Log.isTraceLevel) {
+ lazy.logger.trace(
+ `[${this.browsingContext.id}] MarionetteEvents actor created ` +
+ `for window id ${this.innerWindowId}`
+ );
+ }
+ }
+
+ handleEvent({ target, type }) {
+ if (!Services.cpmm.sharedData.get("MARIONETTE_EVENTS_ENABLED")) {
+ // The parent process will set MARIONETTE_EVENTS_ENABLED to false when
+ // the Marionette session ends to avoid unnecessary inter process
+ // communications
+ return;
+ }
+
+ // Ignore invalid combinations of load events and document's readyState.
+ if (
+ (type === "DOMContentLoaded" && target.readyState != "interactive") ||
+ (type === "pageshow" && target.readyState != "complete")
+ ) {
+ lazy.logger.warn(
+ `Ignoring event '${type}' because document has an invalid ` +
+ `readyState of '${target.readyState}'.`
+ );
+ return;
+ }
+
+ switch (type) {
+ case "beforeunload":
+ case "DOMContentLoaded":
+ case "hashchange":
+ case "pagehide":
+ case "pageshow":
+ case "popstate":
+ this.sendAsyncMessage("MarionetteEventsChild:PageLoadEvent", {
+ browsingContext: this.browsingContext,
+ documentURI: target.documentURI,
+ readyState: target.readyState,
+ type,
+ windowId: this.innerWindowId,
+ });
+ break;
+
+ // Listen for click event to indicate one click has happened, so actions
+ // code can send dblclick event
+ case "click":
+ lazy.event.DoubleClickTracker.setClick();
+ break;
+ case "dblclick":
+ case "unload":
+ lazy.event.DoubleClickTracker.resetClick();
+ break;
+ }
+ }
+}
diff --git a/remote/marionette/actors/MarionetteEventsParent.sys.mjs b/remote/marionette/actors/MarionetteEventsParent.sys.mjs
new file mode 100644
index 0000000000..4211f99e59
--- /dev/null
+++ b/remote/marionette/actors/MarionetteEventsParent.sys.mjs
@@ -0,0 +1,115 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
+
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+// Singleton to allow forwarding events to registered listeners.
+export const EventDispatcher = {
+ init() {
+ lazy.EventEmitter.decorate(this);
+ },
+};
+
+EventDispatcher.init();
+
+export class MarionetteEventsParent extends JSWindowActorParent {
+ async receiveMessage(msg) {
+ const { name, data } = msg;
+
+ let rv;
+ switch (name) {
+ case "MarionetteEventsChild:PageLoadEvent":
+ EventDispatcher.emit("page-load", data);
+ break;
+ }
+
+ return rv;
+ }
+}
+
+// Flag to check if the MarionetteEvents actors have already been registed.
+let eventsActorRegistered = false;
+
+/**
+ * Register Events actors to listen for page load events via EventDispatcher.
+ */
+function registerEventsActor() {
+ if (eventsActorRegistered) {
+ return;
+ }
+
+ try {
+ // Register the JSWindowActor pair for events as used by Marionette
+ ChromeUtils.registerWindowActor("MarionetteEvents", {
+ kind: "JSWindowActor",
+ parent: {
+ esModuleURI:
+ "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs",
+ },
+ child: {
+ esModuleURI:
+ "chrome://remote/content/marionette/actors/MarionetteEventsChild.sys.mjs",
+ events: {
+ beforeunload: { capture: true },
+ DOMContentLoaded: { mozSystemGroup: true },
+ hashchange: { mozSystemGroup: true },
+ pagehide: { mozSystemGroup: true },
+ pageshow: { mozSystemGroup: true },
+ // popstate doesn't bubble, as such use capturing phase
+ popstate: { capture: true, mozSystemGroup: true },
+
+ click: {},
+ dblclick: {},
+ unload: { capture: true, createActor: false },
+ },
+ },
+
+ allFrames: true,
+ includeChrome: true,
+ });
+
+ eventsActorRegistered = true;
+ } catch (e) {
+ if (e.name === "NotSupportedError") {
+ lazy.logger.warn(`MarionetteEvents actor is already registered!`);
+ } else {
+ throw e;
+ }
+ }
+}
+
+/**
+ * Enable MarionetteEvents actors to start forwarding page load events from the
+ * child actor to the parent actor. Register the MarionetteEvents actor if necessary.
+ */
+export function enableEventsActor() {
+ // sharedData is replicated across processes and will be checked by
+ // MarionetteEventsChild before forward events to the parent actor.
+ Services.ppmm.sharedData.set("MARIONETTE_EVENTS_ENABLED", true);
+ // Request to immediately flush the data to the content processes to avoid races.
+ Services.ppmm.sharedData.flush();
+
+ registerEventsActor();
+}
+
+/**
+ * Disable MarionetteEvents actors to stop forwarding page load events from the
+ * child actor to the parent actor.
+ */
+export function disableEventsActor() {
+ Services.ppmm.sharedData.set("MARIONETTE_EVENTS_ENABLED", false);
+ Services.ppmm.sharedData.flush();
+}
diff --git a/remote/marionette/actors/MarionetteReftestChild.sys.mjs b/remote/marionette/actors/MarionetteReftestChild.sys.mjs
new file mode 100644
index 0000000000..e1a9918af2
--- /dev/null
+++ b/remote/marionette/actors/MarionetteReftestChild.sys.mjs
@@ -0,0 +1,236 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+/**
+ * Child JSWindowActor to handle navigation for reftests relying on marionette.
+ */
+export class MarionetteReftestChild extends JSWindowActorChild {
+ constructor() {
+ super();
+
+ // This promise will resolve with the URL recorded in the "load" event
+ // handler. This URL will not be impacted by any hash modification that
+ // might be performed by the test script.
+ // The harness should be loaded before loading any test page, so the actors
+ // should be registered before the "load" event is received for a test page.
+ this._loadedURLPromise = new Promise(
+ r => (this._resolveLoadedURLPromise = r)
+ );
+ }
+
+ handleEvent(event) {
+ if (event.type == "load") {
+ const url = event.target.location.href;
+ lazy.logger.debug(`Handle load event with URL ${url}`);
+ this._resolveLoadedURLPromise(url);
+ }
+ }
+
+ actorCreated() {
+ lazy.logger.trace(
+ `[${this.browsingContext.id}] Reftest actor created ` +
+ `for window id ${this.manager.innerWindowId}`
+ );
+ }
+
+ async receiveMessage(msg) {
+ const { name, data } = msg;
+
+ let result;
+ switch (name) {
+ case "MarionetteReftestParent:flushRendering":
+ result = await this.flushRendering(data);
+ break;
+ case "MarionetteReftestParent:reftestWait":
+ result = await this.reftestWait(data);
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Wait for a reftest page to be ready for screenshots:
+ * - wait for the loadedURL to be available (see handleEvent)
+ * - check if the URL matches the expected URL
+ * - if present, wait for the "reftest-wait" classname to be removed from the
+ * document element
+ *
+ * @param {Object} options
+ * @param {String} options.url
+ * The expected test page URL
+ * @param {Boolean} options.useRemote
+ * True when using e10s
+ * @return {Boolean}
+ * Returns true when the correct page is loaded and ready for
+ * screenshots. Returns false if the page loaded bug does not have the
+ * expected URL.
+ */
+ async reftestWait(options = {}) {
+ const { url, useRemote } = options;
+ const loadedURL = await this._loadedURLPromise;
+ if (loadedURL !== url) {
+ lazy.logger.debug(
+ `Window URL does not match the expected URL "${loadedURL}" !== "${url}"`
+ );
+ return false;
+ }
+
+ const documentElement = this.document.documentElement;
+ const hasReftestWait = documentElement.classList.contains("reftest-wait");
+
+ lazy.logger.debug("Waiting for event loop to spin");
+ await new Promise(resolve =>
+ this.document.defaultView.setTimeout(resolve, 0)
+ );
+
+ await this.paintComplete({ useRemote, ignoreThrottledAnimations: true });
+
+ if (hasReftestWait) {
+ const event = new this.document.defaultView.Event("TestRendered", {
+ bubbles: true,
+ });
+ documentElement.dispatchEvent(event);
+ lazy.logger.info("Emitted TestRendered event");
+ await this.reftestWaitRemoved();
+ await this.paintComplete({ useRemote, ignoreThrottledAnimations: false });
+ }
+ if (
+ this.document.defaultView.innerWidth < documentElement.scrollWidth ||
+ this.document.defaultView.innerHeight < documentElement.scrollHeight
+ ) {
+ lazy.logger.warn(
+ `${url} overflows viewport (width: ${documentElement.scrollWidth}, height: ${documentElement.scrollHeight})`
+ );
+ }
+ return true;
+ }
+
+ paintComplete({ useRemote, ignoreThrottledAnimations }) {
+ lazy.logger.debug("Waiting for rendering");
+ let windowUtils = this.document.defaultView.windowUtils;
+ return new Promise(resolve => {
+ let maybeResolve = () => {
+ this.flushRendering({ ignoreThrottledAnimations });
+ if (useRemote) {
+ // Flush display (paint)
+ lazy.logger.debug("Force update of layer tree");
+ windowUtils.updateLayerTree();
+ }
+
+ if (windowUtils.isMozAfterPaintPending) {
+ lazy.logger.debug("isMozAfterPaintPending: true");
+ this.document.defaultView.addEventListener(
+ "MozAfterPaint",
+ maybeResolve,
+ {
+ once: true,
+ }
+ );
+ } else {
+ // resolve at the start of the next frame in case of leftover paints
+ lazy.logger.debug("isMozAfterPaintPending: false");
+ this.document.defaultView.requestAnimationFrame(() => {
+ this.document.defaultView.requestAnimationFrame(resolve);
+ });
+ }
+ };
+ maybeResolve();
+ });
+ }
+
+ reftestWaitRemoved() {
+ lazy.logger.debug("Waiting for reftest-wait removal");
+ return new Promise(resolve => {
+ const documentElement = this.document.documentElement;
+ let observer = new this.document.defaultView.MutationObserver(() => {
+ if (!documentElement.classList.contains("reftest-wait")) {
+ observer.disconnect();
+ lazy.logger.debug("reftest-wait removed");
+ this.document.defaultView.setTimeout(resolve, 0);
+ }
+ });
+ if (documentElement.classList.contains("reftest-wait")) {
+ observer.observe(documentElement, { attributes: true });
+ } else {
+ this.document.defaultView.setTimeout(resolve, 0);
+ }
+ });
+ }
+
+ /**
+ * Ensure layout is flushed in each frame
+ *
+ * @param {Object} options
+ * @param {Boolean} options.ignoreThrottledAnimations Don't flush
+ * the layout of throttled animations. We can end up in a
+ * situation where flushing a throttled animation causes
+ * mozAfterPaint events even when all rendering we care about
+ * should have ceased. See
+ * https://searchfox.org/mozilla-central/rev/d58860eb739af613774c942c3bb61754123e449b/layout/tools/reftest/reftest-content.js#723-729
+ * for more detail.
+ */
+ flushRendering(options = {}) {
+ let { ignoreThrottledAnimations } = options;
+ lazy.logger.debug(
+ `flushRendering ignoreThrottledAnimations:${ignoreThrottledAnimations}`
+ );
+ let anyPendingPaintsGeneratedInDescendants = false;
+
+ let windowUtils = this.document.defaultView.windowUtils;
+
+ function flushWindow(win) {
+ let utils = win.windowUtils;
+ let afterPaintWasPending = utils.isMozAfterPaintPending;
+
+ let root = win.document.documentElement;
+ if (root) {
+ try {
+ if (ignoreThrottledAnimations) {
+ utils.flushLayoutWithoutThrottledAnimations();
+ } else {
+ root.getBoundingClientRect();
+ }
+ } catch (e) {
+ lazy.logger.error("flushWindow failed", e);
+ }
+ }
+
+ if (!afterPaintWasPending && utils.isMozAfterPaintPending) {
+ anyPendingPaintsGeneratedInDescendants = true;
+ }
+
+ for (let i = 0; i < win.frames.length; ++i) {
+ // Skip remote frames, flushRendering will be called on their individual
+ // MarionetteReftest actor via _recursiveFlushRendering performed from
+ // the topmost MarionetteReftest actor.
+ if (!Cu.isRemoteProxy(win.frames[i])) {
+ flushWindow(win.frames[i]);
+ }
+ }
+ }
+ flushWindow(this.document.defaultView);
+
+ if (
+ anyPendingPaintsGeneratedInDescendants &&
+ !windowUtils.isMozAfterPaintPending
+ ) {
+ lazy.logger.error(
+ "Descendant frame generated a MozAfterPaint event, " +
+ "but the root document doesn't have one!"
+ );
+ }
+ }
+}
diff --git a/remote/marionette/actors/MarionetteReftestParent.sys.mjs b/remote/marionette/actors/MarionetteReftestParent.sys.mjs
new file mode 100644
index 0000000000..f6d79f04d3
--- /dev/null
+++ b/remote/marionette/actors/MarionetteReftestParent.sys.mjs
@@ -0,0 +1,85 @@
+/* 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/. */
+
+/**
+ * Parent JSWindowActor to handle navigation for reftests relying on marionette.
+ */
+export class MarionetteReftestParent extends JSWindowActorParent {
+ /**
+ * Wait for the expected URL to be loaded.
+ *
+ * @param {String} url
+ * The expected url.
+ * @param {Boolean} useRemote
+ * True if tests are running with e10s.
+ * @return {Boolean} true if the page is fully loaded with the expected url,
+ * false otherwise.
+ */
+ async reftestWait(url, useRemote) {
+ try {
+ const isCorrectUrl = await this.sendQuery(
+ "MarionetteReftestParent:reftestWait",
+ {
+ url,
+ useRemote,
+ }
+ );
+
+ if (isCorrectUrl) {
+ // Trigger flush rendering for all remote frames.
+ await this._flushRenderingInSubtree({
+ ignoreThrottledAnimations: false,
+ });
+ }
+
+ return isCorrectUrl;
+ } catch (e) {
+ if (e.name === "AbortError") {
+ // If the query is aborted, the window global is being destroyed, most
+ // likely because a navigation happened.
+ return false;
+ }
+
+ // Other errors should not be swallowed.
+ throw e;
+ }
+ }
+
+ /**
+ * Call flushRendering on all browsing contexts in the subtree.
+ * Each actor will flush rendering in all the same process frames.
+ */
+ async _flushRenderingInSubtree({ ignoreThrottledAnimations }) {
+ const browsingContext = this.manager.browsingContext;
+ const contexts = browsingContext.getAllBrowsingContextsInSubtree();
+
+ await Promise.all(
+ contexts.map(async context => {
+ if (context === browsingContext) {
+ // Skip the top browsing context, for which flushRendering is
+ // already performed via the initial reftestWait call.
+ return;
+ }
+
+ const windowGlobal = context.currentWindowGlobal;
+ if (!windowGlobal) {
+ // Bail out if there is no window attached to the current context.
+ return;
+ }
+
+ if (!windowGlobal.isProcessRoot) {
+ // Bail out if this window global is not a process root.
+ // MarionetteReftestChild::flushRendering will flush all same process
+ // frames, so we only need to call flushRendering on process roots.
+ return;
+ }
+
+ const reftestActor = windowGlobal.getActor("MarionetteReftest");
+ await reftestActor.sendQuery("MarionetteReftestParent:flushRendering", {
+ ignoreThrottledAnimations,
+ });
+ })
+ );
+ }
+}