chromium/chrome/test/chromedriver/js/get_element_location.js

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Constructs new error to be thrown with given code and message.
 *
 * @param {string} message Message reported to user.
 * @param {StatusCode} code StatusCode for error.
 * @return {!Error} Error object that can be thrown.
 */
function newError(message, code) {
  const error = new Error(message);
  error.code = code;
  return error;
}

/**
 * Get the root node for the given element, jumping up through any ShadowRoots
 * if they are found.
 *
 * @param {Node} node The node to find the root of
 * @return {Node} The root node
 */
function getNodeRootThroughAnyShadows(node) {
  // Fetch the root node for the current node.
  let root = node.getRootNode()

  // Keep jumping to the root node for the attachment host of any ShadowRoot.
  while (root.host) {
    root = root.host.getRootNode()
  }

  return root;
}

/**
 * Check whether the specified node is attached to the DOM, either directly or
 * via any attached ShadowRoot.
 *
 * @param {Node} node The node to test
 * @return {boolean} Whether the node is attached to the DOM.
 */
function isNodeReachable(node) {
  const nodeRoot = getNodeRootThroughAnyShadows(node);

  // Check whether the root is the Document or Proxy node.
  return (nodeRoot == document.documentElement.parentNode);
}

function getFirstNonZeroWidthHeightRect(rects) {
  for (const rect of rects) {
    if (rect.height > 0 && rect.width > 0) {
      return rect;
    }
  }
  return rects[0];
}

function getParentRect(element) {
  var parent = element.parentElement;
  var parentRect = getFirstNonZeroWidthHeightRect(parent.getClientRects());
  return parentRect;
}

function getInViewPoint(element) {
  var rectangles = element.getClientRects();
  if (rectangles.length === 0) {
    return false;
  }

  var rect = getFirstNonZeroWidthHeightRect(rectangles);
  var left = Math.max(0, rect.left);
  var right = Math.min(window.innerWidth, rect.right);
  var top = Math.max(0, rect.top);
  var bottom = Math.min(window.innerHeight, rect.bottom);

  // Find the view boundary of the element by checking itself and all of its
  // ancestor's boundary.
  while (element.parentElement != null &&
         element.parentElement != document.body &&
         element.parentElement.getClientRects().length > 0) {
    var parentStyle = window.getComputedStyle(element.parentElement);
    var overflow = parentStyle.getPropertyValue("overflow");
    var overflowX = parentStyle.getPropertyValue("overflow-x");
    var overflowY = parentStyle.getPropertyValue("overflow-y");
    var parentRect = getParentRect(element);
    // Only consider about overflow cases when the parent area overlaps with
    // the element's area.
    if (parentRect.right > left && parentRect.bottom > top &&
        right > parentRect.left && bottom > parentRect.top) {
      if (overflow == "auto" || overflow == "scroll" || overflow == "hidden") {
        left = Math.max(left, parentRect.left);
        right = Math.min(right, parentRect.right);
        top = Math.max(top, parentRect.top);
        bottom = Math.min(bottom, parentRect.bottom);
      } else {
        if (overflowX == "auto" || overflowX == "scroll" ||
            overflowX == "hidden") {
          left = Math.max(left, parentRect.left);
          right = Math.min(right, parentRect.right);
        }
        if (overflowY == "auto" || overflowY == "scroll" ||
            overflowY == "hidden") {
          top = Math.max(top, parentRect.top);
          bottom = Math.min(bottom, parentRect.bottom);
        }
      }
    }
    element = element.parentElement;
  }

  var x = 0.5 * (left + right);
  var y = 0.5 * (top + bottom);
  return [x, y, left, top];
}

function rootNodeIncludes(element, elementPoint) {
  if (!element)
    return false;
  let rootNode = element.getRootNode();
  if (rootNode.elementsFromPoint(elementPoint[0], elementPoint[1])
      .includes(element)) {
    if (rootNode == document)
      return true;
    return rootNodeIncludes(rootNode.host, elementPoint);
  }
  return false;
}

function inView(element) {
  var elementPoint = getInViewPoint(element);
  if (!elementPoint ||
      elementPoint[0] <= 0 || elementPoint[1] <= 0 ||
      elementPoint[0] >= window.innerWidth ||
      elementPoint[1] >= window.innerHeight ||
      !rootNodeIncludes(element, elementPoint)) {
    return false;
  }

  return true;
}

function getElementLocation(element, center) {
  // Check that node type is element.
  if (element.nodeType != 1)
    throw new Error(element + ' is not an element');

  if (!isNodeReachable(element)) {
    // errorCode 10: StaleElementException
    throw newError('element is not attached to the page document', 10);
  }

  if (!inView(element)) {
    element.scrollIntoView({behavior: "instant",
                            block: "end",
                            inline: "nearest"});
  }

  var clientRects = element.getClientRects();
  if (clientRects.length === 0) {
    // errorCode 60: ElementNotInteractableException
    throw newError(element + ' has no size and location', 60);
  }

  var elementPoint = getInViewPoint(element);
  if (center) {
    return {
        'x': elementPoint[0],
        'y': elementPoint[1]
    };
  } else {
    return {
        'x': elementPoint[2],
        'y': elementPoint[3]
    };
  }
}