teamcapybara/capybara

View on GitHub
lib/capybara/selenium/atoms/src/isDisplayed.js

Summary

Maintainability
D
1 day
Test Coverage
(function(){
  var OverflowState = {
    NONE: "none",
    HIDDEN: "hidden",
    SCROLL: "scroll"
  };

  function isShown_(elem, ignoreOpacity, parentsDisplayedFn) {
    // By convention, BODY element is always shown: BODY represents the document
    // and even if there's nothing rendered in there, user can always see there's
    // the document.
    var elemTagName = elem.tagName.toUpperCase();
    if (elemTagName == "BODY") {
      return true;
    }

    // Option or optgroup is shown if enclosing select is shown (ignoring the
    // select's opacity).
    if ((elemTagName == "OPTION") ||
        (elemTagName == "OPTGROUP")) {
      var select = getAncestor(elem, function(e) {
        return e.tagName.toUpperCase() == "SELECT";
      });
      return !!select && isShown_(select, true, parentsDisplayedFn);
    }

    // Image map elements are shown if image that uses it is shown, and
    // the area of the element is positive.
    var imageMap = maybeFindImageMap_(elem);
    if (imageMap) {
      return !!imageMap.image &&
             imageMap.rect.width > 0 && imageMap.rect.height > 0 &&
             isShown_(imageMap.image, ignoreOpacity, parentsDisplayedFn);
    }

    // Any hidden input is not shown.
    if ((elemTagName == "INPUT") && (elem.type.toLowerCase() == "hidden")) {
      return false;
    }

    // Any NOSCRIPT element is not shown.
    if (elemTagName == "NOSCRIPT") {
      return false;
    }

    // Any element with hidden/collapsed visibility is not shown.
    var visibility = window.getComputedStyle(elem)["visibility"];
    if (visibility == "collapse" || visibility == "hidden") {
      return false;
    }

    if (!parentsDisplayedFn(elem)) {
      return false;
    }

    // Any transparent element is not shown.
    if (!ignoreOpacity && getOpacity(elem) == 0) {
      return false;
    }

    // Any element without positive size dimensions is not shown.
    function positiveSize(e) {
      var rect = getClientRect(e);
      if (rect.height > 0 && rect.width > 0) {
        return true;
      }

      // A vertical or horizontal SVG Path element will report zero width or
      // height but is "shown" if it has a positive stroke-width.
      if ((e.tagName.toUpperCase() == "PATH") && (rect.height > 0 || rect.width > 0)) {
        var strokeWidth = window.getComputedStyle(e)["stroke-width"];
        return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
      }

      // Zero-sized elements should still be considered to have positive size
      // if they have a child element or text node with positive size, unless
      // the element has an 'overflow' style of "hidden".
      return window.getComputedStyle(e)["overflow"] != "hidden" &&
        Array.prototype.slice.call(e.childNodes).some(function(n) {
          return (n.nodeType == Node.TEXT_NODE) ||
                 ((n.nodeType == Node.ELEMENT_NODE) && positiveSize(n));
          });
    }

    if (!positiveSize(elem)) {
      return false;
    }

    // Elements that are hidden by overflow are not shown.
    function hiddenByOverflow(e) {
      return getOverflowState(e) == OverflowState.HIDDEN &&
          Array.prototype.slice.call(e.childNodes).every(function(n) {
            return (n.nodeType != Node.ELEMENT_NODE) || hiddenByOverflow(n) ||
                   !positiveSize(n);
          });
    }
    return !hiddenByOverflow(elem);
  }

  function getClientRegion(elem) {
    var region = getClientRect(elem);
    return { left: region.left,
             right: region.left + region.width,
             top: region.top,
             bottom: region.top + region.height };
  }

  function getParentElement(node) {
    return node.parentElement
  }

  function getOverflowState(elem) {
    var region = getClientRegion(elem);
    var ownerDoc = elem.ownerDocument;
    var htmlElem = ownerDoc.documentElement;
    var bodyElem = ownerDoc.body;
    var htmlOverflowStyle = window.getComputedStyle(htmlElem)["overflow"];
    var treatAsFixedPosition;

    // Return the closest ancestor that the given element may overflow.
    function getOverflowParent(e) {
      function canBeOverflowed(container) {
        // The HTML element can always be overflowed.
        if (container == htmlElem) {
          return true;
        }
        var containerStyle = window.getComputedStyle(container);
        // An element cannot overflow an element with an inline or contents display style.
        var containerDisplay = containerStyle["display"];
        if ((containerDisplay.indexOf("inline") == 0) ||
            (containerDisplay == "contents")) {
          return false;
        }
        // An absolute-positioned element cannot overflow a static-positioned one.
        if ((position == "absolute") && (containerStyle["position"] == "static")) {
          return false;
        }
        return true;
      }

      var position = window.getComputedStyle(e)["position"];
      if (position == "fixed") {
        treatAsFixedPosition = true;
        // Fixed-position element may only overflow the viewport.
        return e == htmlElem ? null : htmlElem;
      } else {
        var parent = getParentElement(e);
        while (parent && !canBeOverflowed(parent)) {
          parent = getParentElement(parent);
        }
        return parent;
      }
    };

    // Return the x and y overflow styles for the given element.
    function getOverflowStyles(e) {
      // When the <html> element has an overflow style of 'visible', it assumes
      // the overflow style of the body, and the body is really overflow:visible.
      var overflowElem = e;
      if (htmlOverflowStyle == "visible") {
        // Note: bodyElem will be null/undefined in SVG documents.
        if (e == htmlElem && bodyElem) {
          overflowElem = bodyElem;
        } else if (e == bodyElem) {
          return {x: "visible", y: "visible"};
        }
      }
      var overflowElemStyle = window.getComputedStyle(overflowElem);
      var overflow = {
        x: overflowElemStyle["overflow-x"],
        y: overflowElemStyle["overflow-y"]
      };
      // The <html> element cannot have a genuine 'visible' overflow style,
      // because the viewport can't expand; 'visible' is really 'auto'.
      if (e == htmlElem) {
        overflow.x = overflow.x == "visible" ? "auto" : overflow.x;
        overflow.y = overflow.y == "visible" ? "auto" : overflow.y;
      }
      return overflow;
    };

    // Returns the scroll offset of the given element.
    function getScroll(e) {
      if (e == htmlElem) {
        return { x: window.scrollX, y: window.scrollY }
      }
      return { x: e.scrollLeft, y: e.scrollTop }
    }

    // Check if the element overflows any ancestor element.
    for (var container = getOverflowParent(elem);
         !!container;
         container = getOverflowParent(container)) {
      var containerOverflow = getOverflowStyles(container);

      // If the container has overflow:visible, the element cannot overflow it.
      if (containerOverflow.x == "visible" && containerOverflow.y == "visible") {
        continue;
      }

      var containerRect = getClientRect(container);

      // Zero-sized containers without overflow:visible hide all descendants.
      if (containerRect.width == 0 || containerRect.height == 0) {
        return OverflowState.HIDDEN;
      }

      // Check "underflow": if an element is to the left or above the container
      var underflowsX = region.right < containerRect.left;
      var underflowsY = region.bottom < containerRect.top;
      if ((underflowsX && containerOverflow.x == "hidden") ||
          (underflowsY && containerOverflow.y == "hidden")) {
        return OverflowState.HIDDEN;
      } else if ((underflowsX && containerOverflow.x != "visible") ||
                 (underflowsY && containerOverflow.y != "visible")) {
        // When the element is positioned to the left or above a container, we
        // have to distinguish between the element being completely outside the
        // container and merely scrolled out of view within the container.
        var containerScroll = getScroll(container);
        var unscrollableX = region.right < containerRect.left - containerScroll.x;
        var unscrollableY = region.bottom < containerRect.top - containerScroll.y;
        if ((unscrollableX && containerOverflow.x != "visible") ||
            (unscrollableY && containerOverflow.x != "visible")) {
          return OverflowState.HIDDEN;
        }
        var containerState = getOverflowState(container);
        return containerState == OverflowState.HIDDEN ?
            OverflowState.HIDDEN : OverflowState.SCROLL;
      }

      // Check "overflow": if an element is to the right or below a container
      var overflowsX = region.left >= containerRect.left + containerRect.width;
      var overflowsY = region.top >= containerRect.top + containerRect.height;
      if ((overflowsX && containerOverflow.x == "hidden") ||
          (overflowsY && containerOverflow.y == "hidden")) {
        return OverflowState.HIDDEN;
      } else if ((overflowsX && containerOverflow.x != "visible") ||
                 (overflowsY && containerOverflow.y != "visible")) {
        // If the element has fixed position and falls outside the scrollable area
        // of the document, then it is hidden.
        if (treatAsFixedPosition) {
          var docScroll = getScroll(container);
          if ((region.left >= htmlElem.scrollWidth - docScroll.x) ||
              (region.right >= htmlElem.scrollHeight - docScroll.y)) {
            return OverflowState.HIDDEN;
          }
        }
        // If the element can be scrolled into view of the parent, it has a scroll
        // state; unless the parent itself is entirely hidden by overflow, in
        // which it is also hidden by overflow.
        var containerState = getOverflowState(container);
        return containerState == OverflowState.HIDDEN ?
            OverflowState.HIDDEN : OverflowState.SCROLL;
      }
    }

    // Does not overflow any ancestor.
    return OverflowState.NONE;
  }

  function getViewportSize(win) {
    var el = win.document.documentElement;
    return { width: el.clientWidth, height: el.clientHeight };
  }

  function rect_(x, y, w, h){
    return { left: x, top: y, width: w, height: h };
  }

  function getClientRect(elem) {
    var imageMap = maybeFindImageMap_(elem);
    if (imageMap) {
      return imageMap.rect;
    } else if (elem.tagName.toUpperCase() == "HTML") {
      // Define the client rect of the <html> element to be the viewport.
      var doc = elem.ownerDocument;
      // TODO: Is this too simplified???
      var viewportSize = getViewportSize(window);
      return rect_(0, 0, viewportSize.width, viewportSize.height);
    } else {
      var nativeRect;
      try {
        nativeRect = elem.getBoundingClientRect();
      } catch (e) {
        return rect_(0, 0, 0, 0);
      }

      return rect_(nativeRect.left, nativeRect.top,
                   nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top);
    }
  }

  function getOpacity(elem) {
    // By default the element is opaque.
    var elemOpacity = 1;

    var opacityStyle = window.getComputedStyle(elem)["opacity"];
    if (opacityStyle) {
      elemOpacity = Number(opacityStyle);
    }

    // Let's apply the parent opacity to the element.
    var parentElement = getParentElement(elem);
    if (parentElement && parentElement.nodeType == Node.ELEMENT_NODE) {
      elemOpacity = elemOpacity * getOpacity(parentElement);
    }
    return elemOpacity;
  }

  function getAreaRelativeRect_(area) {
    var shape = area.shape.toLowerCase();
    var coords = area.coords.split(",");
    if (shape == "rect" && coords.length == 4) {
      var x = coords[0], y = coords[1];
      return rect_(x, y, coords[2] - x, coords[3] - y);
    } else if (shape == "circle" && coords.length == 3) {
      var centerX = coords[0], centerY = coords[1], radius = coords[2];
      return rect_(centerX - radius, centerY - radius, 2 * radius, 2 * radius);
    } else if (shape == "poly" && coords.length > 2) {
      var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY;
      for (var i = 2; i + 1 < coords.length; i += 2) {
        minX = Math.min(minX, coords[i]);
        maxX = Math.max(maxX, coords[i]);
        minY = Math.min(minY, coords[i + 1]);
        maxY = Math.max(maxY, coords[i + 1]);
      }
      return rect_(minX, minY, maxX - minX, maxY - minY);
    }
    return rect_(0, 0, 0, 0);
  }

  function maybeFindImageMap_(elem) {
    // If not a <map> or <area>, return null indicating so.
    var elemTagName = elem.tagName.toUpperCase();
    var isMap = elemTagName == "MAP";
    if (!isMap && (elemTagName != "AREA")) {
      return null;
    }

    // Get the <map> associated with this element, or null if none.
    var map = isMap ? elem :
        ((getParentElement(elem).tagName.toUpperCase() == "MAP") ?
            getParentElement(elem) : null);

    var image = null, rect = null;
    if (map && map.name) {
      var mapDoc = map.ownerDocument;

      image = mapDoc.querySelector("*[usemap='#" + map.name + "']");

      if (image) {
        rect = getClientRect(image);
        if (!isMap && elem.shape.toLowerCase() != "default") {
          // Shift and crop the relative area rectangle to the map.
          var relRect = getAreaRelativeRect_(elem);
          var relX = Math.min(Math.max(relRect.left, 0), rect.width);
          var relY = Math.min(Math.max(relRect.top, 0), rect.height);
          var w = Math.min(relRect.width, rect.width - relX);
          var h = Math.min(relRect.height, rect.height - relY);
          rect = rect_(relX + rect.left, relY + rect.top, w, h);
        }
      }
    }

    return {image: image, rect: rect || rect_(0, 0, 0, 0)};
  }

  function getAncestor(element, matcher) {
    if (element) {
      element = getParentElement(element);
    }
    while (element) {
      if (matcher(element)) {
        return element;
      }
      element = getParentElement(element);
    }
    // Reached the root of the DOM without a match
    return null;
  }


  function isElement(node, opt_tagName) {
    // because we call this with deprecated tags such as SHADOW
    if (opt_tagName && (typeof opt_tagName !== "string")) {
      opt_tagName = opt_tagName.toString();
    }
    return !!node && node.nodeType == Node.ELEMENT_NODE &&
        (!opt_tagName || node.tagName.toUpperCase() == opt_tagName);
  }

  function getParentNodeInComposedDom(node) {
    var /**@type {Node}*/ parent = node.parentNode;

    // Shadow DOM v1
    if (parent && parent.shadowRoot && node.assignedSlot !== undefined) {
      // Can be null on purpose, meaning it has no parent as
      // it hasn't yet been slotted
      return node.assignedSlot ? node.assignedSlot.parentNode : null;
    }

    // Shadow DOM V0 (deprecated)
    if (node.getDestinationInsertionPoints) {
      var destinations = node.getDestinationInsertionPoints();
      if (destinations.length > 0) {
        return destinations[destinations.length - 1];
      }
    }

    return parent;
  }

  return function isShown(elem, opt_ignoreOpacity) {
    /**
     * Determines whether an element or its parents have `display: none` set
     * @param {!Node} e the element
     * @return {boolean}
     */
    function displayed(e) {
      if (window.getComputedStyle(e)["display"] == "none"){
        return false;
      }

      var parent = getParentNodeInComposedDom(e);

      if ((typeof ShadowRoot === "function") && (parent instanceof ShadowRoot)) {
        if (parent.host.shadowRoot !== parent) {
          // There is a younger shadow root, which will take precedence over
          // the shadow this element is in, thus this element won't be
          // displayed.
          return false;
        } else {
          parent = parent.host;
        }
      }

      if (parent && (parent.nodeType == Node.DOCUMENT_NODE ||
          parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) {
        return true;
      }

      // Child of DETAILS element is not shown unless the DETAILS element is open
      // or the child is a SUMMARY element.
      if (parent && parent.tagName && (parent.tagName.toUpperCase() == "DETAILS") &&
          !parent.open && !(e.tagName == "SUMMARY")) {
        return false;
      }

      return parent && displayed(parent);
    }

    return isShown_(elem, !!opt_ignoreOpacity, displayed);
  };
})()