summaryrefslogtreecommitdiffstats
path: root/remote/marionette/actors/MarionetteCommandsChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/marionette/actors/MarionetteCommandsChild.sys.mjs')
-rw-r--r--remote/marionette/actors/MarionetteCommandsChild.sys.mjs611
1 files changed, 611 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..b51e758cae
--- /dev/null
+++ b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs
@@ -0,0 +1,611 @@
+/* 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, {
+ accessibility: "chrome://remote/content/marionette/accessibility.sys.mjs",
+ action: "chrome://remote/content/shared/webdriver/Actions.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",
+ 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:getComputedLabel":
+ result = await this.getComputedLabel(data);
+ break;
+ case "MarionetteCommandsParent:getComputedRole":
+ result = await this.getComputedRole(data);
+ 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 {string} options.strategy
+ * @param {string} options.selector
+ * @param {object} options.opts
+ * @param {Element} options.opts.startNode
+ *
+ */
+ 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 {string} options.strategy
+ * @param {string} options.selector
+ * @param {object} options.opts
+ * @param {Element} options.opts.startNode
+ *
+ */
+ 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;
+ }
+
+ /**
+ * Return the accessible label for a given element.
+ */
+ async getComputedLabel(options = {}) {
+ const { elem } = options;
+
+ const accessible = await lazy.accessibility.getAccessible(elem);
+ if (!accessible) {
+ return null;
+ }
+
+ return accessible.name;
+ }
+
+ /**
+ * Return the accessible role for a given element.
+ */
+ async getComputedRole(options = {}) {
+ const { elem } = options;
+
+ const accessible = await lazy.accessibility.getAccessible(elem);
+ if (!accessible) {
+ // If it's not in the a11y tree, it's probably presentational.
+ return "none";
+ }
+
+ return accessible.computedARIARole;
+ }
+
+ /**
+ * 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.
+ *
+ * @returns {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);
+ // Terminate the current wheel transaction if there is one. Wheel
+ // transactions should not live longer than a single action chain.
+ ChromeUtils.endWheelTransaction();
+ }
+
+ /**
+ * 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;
+ }
+ await this.actionState.release(this.document.defaultView);
+ this.actionState = null;
+ }
+
+ /*
+ * 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 };
+ }
+}