chromium/chrome/test/chromedriver/js/get_element_region.js

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

// Return the portion of the element that should be made visible.
// Based on the WebDriver spec, this function only considers the first rectangle
// returned by element.getClientRects function.
// * When the rectangle is already partially visible in the enclosing viewport,
//   return the portion that is currently visible. According to WebDriver spec,
//   no scrolling should be done to bring more of the element into view.
// * When the rectangle is completely outside of the enclosing viewport,
//   return the entire rectangle, as WebDriver spec requires us to scroll the
//   entire rectangle into view. (However, scrolling is NOT the responsibility
//   of this function.)
//
// The returned value is an object with the following properties about the
// region mentioned above: left, top, height, width. Note that left and top are
// relative to the upper-left corner of the element's bounding client rect (as
// returned by element.getBoundingClientRect).
function getElementRegion(element) {
  // Check that node type is element.
  if (element.nodeType != 1)
    throw new Error(element + ' is not an element');

  // We try 2 methods to determine element region. Try the first client rect,
  // and then the bounding client rect.
  // SVG is one case that doesn't have a first client rect.
  const clientRects = element.getClientRects();

  // Determines if region is partially in viewport, returning visible region
  // if so. If not, returns null. If fully visible, returns original region.
  function getVisibleSubregion(region) {
    // Given two regions, determines if any intersection occurs.
    // Overlapping edges are not considered intersections.
    function getIntersectingSubregion(region1, region2) {
      if (!(Math.round(region2.right)  <= Math.round(region1.left)   ||
            Math.round(region2.left)   >= Math.round(region1.right)  ||
            Math.round(region2.top)    >= Math.round(region1.bottom) ||
            Math.round(region2.bottom) <= Math.round(region1.top))) {
        // Determines region of intersection.
        // If region2 contains region1, returns region1.
        // If region1 contains region2, returns region2.
        return {
          'left': Math.max(region1.left, region2.left),
          'right': Math.min(region1.right, region2.right),
          'bottom': Math.min(region1.bottom, region2.bottom),
          'top': Math.max(region1.top, region2.top)
        };
      }
      return null;
    }
    const visualViewport = window.visualViewport;
    // We need to disregard any scrollbars therefore instead of innerSize
    // of the window we should use the viewport size.
    // This size can be affected (scaled) by user's pinch.
    // We need to undo this scaling because client rects are calculated
    // relatively to the original unscaled viewport.
    const viewport = new DOMRect(0, 0,
      visualViewport.width * visualViewport.scale,
      visualViewport.height * visualViewport.scale
    );
    return getIntersectingSubregion(viewport, region);
  }

  let boundingRect = null;
  let clientRect = null;
  // Element area of a map has same first ClientRect and BoundingClientRect
  // after blink roll at chromium commit position 290738 which includes blink
  // revision 180610. Thus handle area as a special case.
  if (clientRects.length == 0 || element.tagName.toLowerCase() == 'area') {
    // Area clicking is technically not supported by W3C standard but is a
    // desired feature. Returns region containing the area instead of subregion
    // so that whole area is visible and always clicked correctly.
    if (element.tagName.toLowerCase() == 'area') {
      const coords = element.coords.split(',');
      if (element.shape.toLowerCase() == 'rect') {
        if (coords.length != 4)
          throw new Error('failed to detect the region of the area');
        const leftX = Number(coords[0]);
        const topY = Number(coords[1]);
        const rightX = Number(coords[2]);
        const bottomY = Number(coords[3]);
        return {
            'left': leftX,
            'top': topY,
            'width': rightX - leftX,
            'height': bottomY - topY
        };
      } else if (element.shape.toLowerCase() == 'circle') {
        if (coords.length != 3)
          throw new Error('failed to detect the region of the area');
        const centerX = Number(coords[0]);
        const centerY = Number(coords[1]);
        const radius = Number(coords[2]);
        return {
            'left': Math.max(0, centerX - radius),
            'top': Math.max(0, centerY - radius),
            'width': radius * 2,
            'height': radius * 2
        };
      } else if (element.shape.toLowerCase() == 'poly') {
        if (coords.length < 2)
          throw new Error('failed to detect the region of the area');
        let minX = Number(coords[0]);
        let minY = Number(coords[1]);
        let maxX = minX;
        let maxY = minY;
        for (i = 2; i < coords.length; i += 2) {
          const x = Number(coords[i]);
          const y = Number(coords[i + 1]);
          minX = Math.min(minX, x);
          minY = Math.min(minY, y);
          maxX = Math.max(maxX, x);
          maxY = Math.max(maxY, y);
        }
        return {
            'left': minX,
            'top': minY,
            'width': maxX - minX,
            'height': maxY - minY
        };
      } else {
        throw new Error('shape=' + element.shape + ' is not supported');
      }
    } else {
      clientRect = boundingRect = element.getBoundingClientRect();
    }
  } else {
    boundingRect = element.getBoundingClientRect();
    clientRect = clientRects[0];
    for (let i = 0; i < clientRects.length; i++) {
      if (clientRects[i].height != 0 && clientRects[i].width != 0) {
        clientRect = clientRects[i];
        break;
      }
    }
  }
  const visiblePortion = getVisibleSubregion(clientRect) || clientRect;
  // Returned region is relative to boundingRect's left,top.
  return {
    'left': visiblePortion.left - boundingRect.left,
    'top': visiblePortion.top - boundingRect.top,
    'height': visiblePortion.bottom - visiblePortion.top,
    'width': visiblePortion.right - visiblePortion.left
  };
}