Commit cb7c4b36 authored by Henrik Skupin's avatar Henrik Skupin
Browse files

Bug 1822466 - [marionette] Move WebElement and ShadowRoot...

Bug 1822466 - [marionette] Move WebElement and ShadowRoot serialization/deserialization helpers to Marionette command client actor. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D177491
parent 7d0e610e
Loading
Loading
Loading
Loading
+157 −6
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
  Log: "chrome://remote/content/shared/Log.sys.mjs",
  sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs",
  Sandboxes: "chrome://remote/content/marionette/evaluate.sys.mjs",
  WebReference: "chrome://remote/content/marionette/element.sys.mjs",
});

XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
@@ -58,6 +59,10 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
    return this._legacyactions;
  }

  get #nodeCache() {
    return this.#processActor.getNodeCache();
  }

  actorCreated() {
    lazy.logger.trace(
      `[${this.browsingContext.id}] MarionetteCommands actor created ` +
@@ -82,11 +87,12 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
      let waitForNextTick = false;

      const { name, data: serializedData } = msg;
      const data = lazy.json.deserialize(
        serializedData,
        this.#processActor.getNodeCache(),
        this.contentWindow
      );
      const data = lazy.json.deserialize({
        value: serializedData,
        win: this.contentWindow,
        getKnownElement: this.#getKnownElement.bind(this),
        getKnownShadowRoot: this.#getKnownShadowRoot.bind(this),
      });

      switch (name) {
        case "MarionetteCommandsParent:clearElement":
@@ -184,7 +190,10 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
      }

      return {
        data: lazy.json.clone(result, this.#processActor.getNodeCache()),
        data: lazy.json.clone({
          value: result,
          getOrCreateNodeReference: this.#getOrCreateNodeReference.bind(this),
        }),
      };
    } catch (e) {
      // Always wrap errors as WebDriverError
@@ -605,4 +614,146 @@ export class MarionetteCommandsChild extends JSWindowActorChild {

    return { browsingContextId: browsingContext.id };
  }

  // Private methods

  /**
   * Resolve element from specified web reference identifier.
   *
   * @param {BrowsingContext} browsingContext
   *     The browsing context to retrieve the element from.
   * @param {string} nodeId
   *     The WebReference uuid for a DOM element.
   *
   * @returns {Element}
   *     The DOM element that the identifier was generated for.
   *
   * @throws {NoSuchElementError}
   *     If the element doesn't exist in the current browsing context.
   * @throws {StaleElementReferenceError}
   *     If the element has gone stale, indicating its node document is no
   *     longer the active document or it is no longer attached to the DOM.
   */
  #getKnownElement(browsingContext, nodeId) {
    if (!this.#isNodeReferenceKnown(browsingContext, nodeId)) {
      throw new lazy.error.NoSuchElementError(
        `The element with the reference ${nodeId} is not known in the current browsing context`
      );
    }

    const node = this.#nodeCache.getNode(browsingContext, nodeId);

    // Ensure the node is of the correct Node type.
    if (node !== null && !lazy.element.isElement(node)) {
      throw new lazy.error.NoSuchElementError(
        `The element with the reference ${nodeId} is not of type HTMLElement`
      );
    }

    // If null, which may be the case if the element has been unwrapped from a
    // weak reference, it is always considered stale.
    if (node === null || lazy.element.isStale(node)) {
      throw new lazy.error.StaleElementReferenceError(
        `The element with the reference ${nodeId} ` +
          "is stale; either its node document is not the active document, " +
          "or it is no longer connected to the DOM"
      );
    }

    return node;
  }

  /**
   * Resolve ShadowRoot from specified web reference identifier.
   *
   * @param {BrowsingContext} browsingContext
   *     The browsing context to retrieve the shadow root from.
   * @param {string} nodeId
   *     The WebReference uuid for a ShadowRoot.
   *
   * @returns {ShadowRoot}
   *     The ShadowRoot that the identifier was generated for.
   *
   * @throws {NoSuchShadowRootError}
   *     If the ShadowRoot doesn't exist in the current browsing context.
   * @throws {DetachedShadowRootError}
   *     If the ShadowRoot is detached, indicating its node document is no
   *     longer the active document or it is no longer attached to the DOM.
   */
  #getKnownShadowRoot(browsingContext, nodeId) {
    if (!this.#isNodeReferenceKnown(browsingContext, nodeId)) {
      throw new lazy.error.NoSuchShadowRootError(
        `The shadow root with the reference ${nodeId} is not known in the current browsing context`
      );
    }

    const node = this.#nodeCache.getNode(browsingContext, nodeId);

    // Ensure the node is of the correct Node type.
    if (node !== null && !lazy.element.isShadowRoot(node)) {
      throw new lazy.error.NoSuchShadowRootError(
        `The shadow root with the reference ${nodeId} is not of type ShadowRoot`
      );
    }

    // If null, which may be the case if the element has been unwrapped from a
    // weak reference, it is always considered stale.
    if (node === null || lazy.element.isDetached(node)) {
      throw new lazy.error.DetachedShadowRootError(
        `The shadow root with the reference ${nodeId} ` +
          "is detached; either its node document is not the active document, " +
          "or it is no longer connected to the DOM"
      );
    }

    return node;
  }

  /**
   * Returns the WebReference for the given node.
   *
   * Hereby it tries to find a known node reference for that node in the
   * node cache, and returns it. Otherwise it creates a new reference and
   * adds it to the cache.
   *
   * @param {Node} node
   *     The node to create or get a WebReference for.
   *
   * @returns {WebReference} The web reference for the node.
   */
  #getOrCreateNodeReference(node) {
    const nodeRef = this.#nodeCache.getOrCreateNodeReference(node);
    return lazy.WebReference.from(node, nodeRef);
  }

  /**
   * Determines if the node reference is known for the given browsing context.
   *
   * For WebDriver classic only nodes from the same browsing context are
   * allowed to be accessed.
   *
   * @param {BrowsingContext} browsingContext
   *     The browsing context the element has to be part of.
   * @param {ElementIdentifier} nodeId
   *     The WebElement reference identifier for a DOM element.
   *
   * @returns {boolean}
   *     True if the element is known in the given browsing context.
   */
  #isNodeReferenceKnown(browsingContext, nodeId) {
    const nodeDetails = this.#nodeCache.getReferenceDetails(nodeId);
    if (nodeDetails === null) {
      return false;
    }

    if (nodeDetails.isTopBrowsingContext) {
      // As long as Navigables are not available any cross-group navigation will
      // cause a swap of the current top-level browsing context. The only unique
      // identifier in such a case is the browser id the top-level browsing
      // context actually lives in.
      return nodeDetails.browserId === browsingContext.browserId;
    }

    return nodeDetails.browsingContextId === browsingContext.id;
  }
}
+0 −129
Original line number Diff line number Diff line
@@ -467,102 +467,6 @@ element.findClosest = function(startNode, selector) {
  return null;
};

/**
 * Resolve element from specified web reference identifier.
 *
 * @param {BrowsingContext} browsingContext
 *     The browsing context to retrieve the element from.
 * @param {string} nodeId
 *     The WebReference uuid for a DOM element.
 * @param {NodeCache} nodeCache
 *     Node cache that holds already seen WebElement and ShadowRoot references.
 *
 * @returns {Element}
 *     The DOM element that the identifier was generated for.
 *
 * @throws {NoSuchElementError}
 *     If the element doesn't exist in the current browsing context.
 * @throws {StaleElementReferenceError}
 *     If the element has gone stale, indicating its node document is no
 *     longer the active document or it is no longer attached to the DOM.
 */
element.getKnownElement = function(browsingContext, nodeId, nodeCache) {
  if (!element.isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
    throw new lazy.error.NoSuchElementError(
      `The element with the reference ${nodeId} is not known in the current browsing context`
    );
  }

  const node = nodeCache.getNode(browsingContext, nodeId);

  // Ensure the node is of the correct Node type.
  if (node !== null && !element.isElement(node)) {
    throw new lazy.error.NoSuchElementError(
      `The element with the reference ${nodeId} is not of type HTMLElement`
    );
  }

  // If null, which may be the case if the element has been unwrapped from a
  // weak reference, it is always considered stale.
  if (node === null || element.isStale(node)) {
    throw new lazy.error.StaleElementReferenceError(
      `The element with the reference ${nodeId} ` +
        "is stale; either its node document is not the active document, " +
        "or it is no longer connected to the DOM"
    );
  }

  return node;
};

/**
 * Resolve ShadowRoot from specified web reference identifier.
 *
 * @param {BrowsingContext} browsingContext
 *     The browsing context to retrieve the shadow root from.
 * @param {string} nodeId
 *     The WebReference uuid for a ShadowRoot.
 * @param {NodeCache} nodeCache
 *     Node cache that holds already seen WebElement and ShadowRoot references.
 *
 * @returns {ShadowRoot}
 *     The ShadowRoot that the identifier was generated for.
 *
 * @throws {NoSuchShadowRootError}
 *     If the ShadowRoot doesn't exist in the current browsing context.
 * @throws {DetachedShadowRootError}
 *     If the ShadowRoot is detached, indicating its node document is no
 *     longer the active document or it is no longer attached to the DOM.
 */
element.getKnownShadowRoot = function(browsingContext, nodeId, nodeCache) {
  if (!element.isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
    throw new lazy.error.NoSuchShadowRootError(
      `The shadow root with the reference ${nodeId} is not known in the current browsing context`
    );
  }

  const node = nodeCache.getNode(browsingContext, nodeId);

  // Ensure the node is of the correct Node type.
  if (node !== null && !element.isShadowRoot(node)) {
    throw new lazy.error.NoSuchShadowRootError(
      `The shadow root with the reference ${nodeId} is not of type ShadowRoot`
    );
  }

  // If null, which may be the case if the element has been unwrapped from a
  // weak reference, it is always considered stale.
  if (node === null || element.isDetached(node)) {
    throw new lazy.error.DetachedShadowRootError(
      `The shadow root with the reference ${nodeId} ` +
        "is detached; either its node document is not the active document, " +
        "or it is no longer connected to the DOM"
    );
  }

  return node;
};

/**
 * Determines if <var>obj<var> is an HTML or JS collection.
 *
@@ -608,39 +512,6 @@ element.isDetached = function(shadowRoot) {
  );
};

/**
 * Determines if the node reference is known for the given browsing context.
 *
 * For WebDriver classic only nodes from the same browsing context are
 * allowed to be accessed.
 *
 * @param {BrowsingContext} browsingContext
 *     The browsing context the element has to be part of.
 * @param {ElementIdentifier} nodeId
 *     The WebElement reference identifier for a DOM element.
 * @param {NodeCache} nodeCache
 *     Node cache that holds already seen node references.
 *
 * @returns {boolean}
 *     True if the element is known in the given browsing context.
 */
element.isNodeReferenceKnown = function(browsingContext, nodeId, nodeCache) {
  const nodeDetails = nodeCache.getReferenceDetails(nodeId);
  if (nodeDetails === null) {
    return false;
  }

  if (nodeDetails.isTopBrowsingContext) {
    // As long as Navigables are not available any cross-group navigation will
    // cause a swap of the current top-level browsing context. The only unique
    // identifier in such a case is the browser id the top-level browsing
    // context actually lives in.
    return nodeDetails.browserId === browsingContext.browserId;
  }

  return nodeDetails.browsingContextId === browsingContext.id;
};

/**
 * Determines if <var>el</var> is stale.
 *
+26 −23
Original line number Diff line number Diff line
@@ -90,10 +90,12 @@ function cloneObject(value, seen, cloneAlgorithm) {
 *
 * - If a cyclic references is detected a JavaScriptError is thrown.
 *
 * @param {object} value
 * @param {object} options
 * @param {object} options.value
 *     Object to be cloned.
 * @param {NodeCache} nodeCache
 *     Node cache that holds already seen WebElement and ShadowRoot references.
 * @param {Function} options.getOrCreateNodeReference
 *     Callback that tries to use a known node reference from the node cache,
 *     or creates a new one if not present yet.
 *
 * @returns {object}
 *     Same object as provided by `value` with the WebDriver specific
@@ -105,7 +107,9 @@ function cloneObject(value, seen, cloneAlgorithm) {
 *     If the element has gone stale, indicating it is no longer
 *     attached to the DOM.
 */
json.clone = function(value, nodeCache) {
json.clone = function(options) {
  const { getOrCreateNodeReference, value } = options;

  function cloneJSON(value, seen) {
    if (seen === undefined) {
      seen = new Set();
@@ -143,8 +147,7 @@ json.clone = function(value, nodeCache) {
        );
      }

      const nodeRef = nodeCache.getOrCreateNodeReference(value);
      return lazy.WebReference.from(value, nodeRef).toJSON();
      return getOrCreateNodeReference(value).toJSON();
    }

    if (isNode && lazy.element.isShadowRoot(value)) {
@@ -157,8 +160,7 @@ json.clone = function(value, nodeCache) {
        );
      }

      const nodeRef = nodeCache.getOrCreateNodeReference(value);
      return lazy.WebReference.from(value, nodeRef).toJSON();
      return getOrCreateNodeReference(value).toJSON();
    }

    if (typeof value.toJSON == "function") {
@@ -183,23 +185,32 @@ json.clone = function(value, nodeCache) {
/**
 * Deserialize an arbitrary object.
 *
 * @param {object} value
 * @param {object} options
 * @param {object} options.value
 *     Arbitrary object.
 * @param {NodeCache} nodeCache
 *     Node cache that holds already seen WebElement and ShadowRoot references.
 * @param {WindowProxy} win
 * @param {WindowProxy} options.win
 *     Current window.
 * @param {Function} options.getKnownElement
 *     Callback that will try to resolve a WebElement reference to an Element node.
 * @param {Function} options.getKnownShadowRoot
 *     Callback that will try to resolve a ShadowRoot reference to a ShadowRoot node.
 *
 * @returns {object}
 *     Same object as provided by `value` with the WebDriver specific
 *     references replaced with real JavaScript objects.
 *
 * @throws {DetachedShadowRootError}
 *     If the ShadowRoot is detached, indicating it is no longer attached to the DOM.
 * @throws {NoSuchElementError}
 *     If the WebElement reference has not been seen before.
 * @throws {NoSuchShadowRootError}
 *     If the ShadowRoot reference has not been seen before.
 * @throws {StaleElementReferenceError}
 *     If the element is stale, indicating it is no longer attached to the DOM.
 */
json.deserialize = function(value, nodeCache, win) {
json.deserialize = function(options) {
  const { value, win, getKnownElement, getKnownShadowRoot } = options;

  function deserializeJSON(value, seen) {
    if (seen === undefined) {
      seen = new Set();
@@ -222,19 +233,11 @@ json.deserialize = function(value, nodeCache, win) {
          const webRef = lazy.WebReference.fromJSON(value);

          if (webRef instanceof lazy.ShadowRoot) {
            return lazy.element.getKnownShadowRoot(
              win.browsingContext,
              webRef.uuid,
              nodeCache
            );
            return getKnownShadowRoot(win.browsingContext, webRef.uuid);
          }

          if (webRef instanceof lazy.WebElement) {
            return lazy.element.getKnownElement(
              win.browsingContext,
              webRef.uuid,
              nodeCache
            );
            return getKnownElement(win.browsingContext, webRef.uuid);
          }

          // WebFrame and WebWindow not supported yet
+0 −144
Original line number Diff line number Diff line
@@ -12,13 +12,6 @@ const {
} = ChromeUtils.importESModule(
  "chrome://remote/content/marionette/element.sys.mjs"
);
const { NodeCache } = ChromeUtils.importESModule(
  "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
);

const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
  Ci.nsIMemoryReporterManager
);

class MockElement {
  constructor(tagName, attrs = {}) {
@@ -103,7 +96,6 @@ function setupTest() {

  return {
    browser,
    nodeCache: new NodeCache(),
    childEl,
    divEl,
    iframeEl,
@@ -455,142 +447,6 @@ add_task(function test_coordinates() {
  );
});

add_task(function test_isNodeReferenceKnown() {
  const { browser, nodeCache, childEl, iframeEl, videoEl } = setupTest();

  // Unknown node reference
  ok(!element.isNodeReferenceKnown(browser.browsingContext, "foo", nodeCache));

  // Known node reference
  const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
  ok(
    element.isNodeReferenceKnown(browser.browsingContext, videoElRef, nodeCache)
  );

  // Different top-level browsing context
  const browser2 = Services.appShell.createWindowlessBrowser(false);
  ok(
    !element.isNodeReferenceKnown(
      browser2.browsingContext,
      videoElRef,
      nodeCache
    )
  );

  // Different child browsing context
  const childElRef = nodeCache.getOrCreateNodeReference(childEl);
  const childBrowsingContext = iframeEl.contentWindow.browsingContext;
  ok(element.isNodeReferenceKnown(childBrowsingContext, childElRef, nodeCache));

  const iframeEl2 = browser2.document.createElement("iframe");
  browser2.document.body.appendChild(iframeEl2);
  const childBrowsingContext2 = iframeEl2.contentWindow.browsingContext;
  ok(
    !element.isNodeReferenceKnown(childBrowsingContext2, childElRef, nodeCache)
  );
});

add_task(function test_getKnownElement() {
  const { browser, nodeCache, shadowRoot, videoEl } = setupTest();

  // Unknown element reference
  Assert.throws(() => {
    element.getKnownElement(browser.browsingContext, "foo", nodeCache);
  }, /NoSuchElementError/);

  // With a ShadowRoot reference
  const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
  Assert.throws(() => {
    element.getKnownElement(browser.browsingContext, shadowRootRef, nodeCache);
  }, /NoSuchElementError/);

  // Deleted element (eg. garbage collected)
  let detachedEl = browser.document.createElement("div");
  const detachedElRef = nodeCache.getOrCreateNodeReference(detachedEl);

  // ... not connected to the DOM
  Assert.throws(() => {
    element.getKnownElement(browser.browsingContext, detachedElRef, nodeCache);
  }, /StaleElementReferenceError/);

  // ... element garbage collected
  detachedEl = null;
  MemoryReporter.minimizeMemoryUsage(() => {
    Assert.throws(() => {
      element.getKnownElement(
        browser.browsingContext,
        detachedElRef,
        nodeCache
      );
    }, /StaleElementReferenceError/);
  });

  // Known element reference
  const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);
  equal(
    element.getKnownElement(browser.browsingContext, videoElRef, nodeCache),
    videoEl
  );
});

add_task(function test_getKnownShadowRoot() {
  const { browser, nodeCache, shadowRoot, videoEl } = setupTest();

  const videoElRef = nodeCache.getOrCreateNodeReference(videoEl);

  // Unknown ShadowRoot reference
  Assert.throws(() => {
    element.getKnownShadowRoot(browser.browsingContext, "foo", nodeCache);
  }, /NoSuchShadowRootError/);

  // With a HTMLElement reference
  Assert.throws(() => {
    element.getKnownShadowRoot(browser.browsingContext, videoElRef, nodeCache);
  }, /NoSuchShadowRootError/);

  // Known ShadowRoot reference
  const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot);
  equal(
    element.getKnownShadowRoot(
      browser.browsingContext,
      shadowRootRef,
      nodeCache
    ),
    shadowRoot
  );

  // Detached ShadowRoot host
  let el = browser.document.createElement("div");
  let detachedShadowRoot = el.attachShadow({ mode: "open" });
  detachedShadowRoot.innerHTML = "<input></input>";

  const detachedShadowRootRef = nodeCache.getOrCreateNodeReference(
    detachedShadowRoot
  );

  // ... not connected to the DOM
  Assert.throws(() => {
    element.getKnownShadowRoot(
      browser.browsingContext,
      detachedShadowRootRef,
      nodeCache
    );
  }, /DetachedShadowRootError/);

  // ... host and shadow root garbage collected
  el = null;
  detachedShadowRoot = null;
  MemoryReporter.minimizeMemoryUsage(() => {
    Assert.throws(() => {
      element.getKnownShadowRoot(
        browser.browsingContext,
        detachedShadowRootRef,
        nodeCache
      );
    }, /DetachedShadowRootError/);
  });
});

add_task(function test_isDetached() {
  const { childEl, iframeEl } = setupTest();

+98 −41

File changed.

Preview size limit exceeded, changes collapsed.