blob: a6ec9f3b5db7b5ed1116692becdbbcc682bc2fdc [file] [log] [blame]
/*
* Copyright (C) 2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
function isShown(element) {
"use strict";
function nodeIsElement(node) {
if (!node)
return false;
switch (node.nodeType) {
case Node.ELEMENT_NODE:
case Node.DOCUMENT_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
return true;
default:
return false;
}
}
function parentElementForElement(element) {
if (!element)
return null;
return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement);
}
function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) {
for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode)
if (predicate(node))
return node;
return null;
}
function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) {
for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element))
if (predicate(element))
return element;
return null;
}
function cascadedStylePropertyForElement(element, property) {
if (!element || !property)
return null;
let computedStyle = window.getComputedStyle(element);
let computedStyleProperty = computedStyle.getPropertyValue(property);
if (computedStyleProperty && computedStyleProperty !== "inherit")
return computedStyleProperty;
// Ideally getPropertyValue would return the 'used' or 'actual' value, but
// it doesn't for legacy reasons. So we need to do our own poor man's cascade.
// Fall back to the first non-'inherit' value found in an ancestor.
// In any case, getPropertyValue will not return 'initial'.
// FIXME: will this incorrectly inherit non-inheritable CSS properties?
// I think all important non-inheritable properties (width, height, etc.)
// for our purposes here are specially resolved, so this may not be an issue.
// Specification is here: https://drafts.csswg.org/cssom/#resolved-values
let parentElement = parentElementForElement(element);
return cascadedStylePropertyForElement(parentElement, property);
}
function elementSubtreeHasNonZeroDimensions(element) {
let boundingBox = element.getBoundingClientRect();
if (boundingBox.width > 0 && boundingBox.height > 0)
return true;
// Paths can have a zero width or height. Treat them as shown if the stroke width is positive.
if (element.tagName.toUpperCase() === "PATH" && boundingBox.width + boundingBox.height > 0) {
let strokeWidth = cascadedStylePropertyForElement(element, "stroke-width");
return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
}
let cascadedOverflow = cascadedStylePropertyForElement(element, "overflow");
if (cascadedOverflow === "hidden")
return false;
// If the container's overflow is not hidden and it has zero size, consider the
// container to have non-zero dimensions if a child node has non-zero dimensions.
return Array.from(element.childNodes).some((childNode) => {
if (childNode.nodeType === Node.TEXT_NODE)
return true;
if (nodeIsElement(childNode))
return elementSubtreeHasNonZeroDimensions(childNode);
return false;
});
}
function elementOverflowsContainer(element) {
let cascadedOverflow = cascadedStylePropertyForElement(element, "overflow");
if (cascadedOverflow !== "hidden")
return false;
// FIXME: this needs to take into account the scroll position of the element,
// the display modes of it and its ancestors, and the container it overflows.
// See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases.
return true;
}
function isElementSubtreeHiddenByOverflow(element) {
if (!element)
return false;
if (!elementOverflowsContainer(element))
return false;
if (!element.childNodes.length)
return false;
// This element's subtree is hidden by overflow if all child subtrees are as well.
return Array.from(element.childNodes).every((childNode) => {
// Returns true if the child node is overflowed or otherwise hidden.
// Base case: not an element, has zero size, scrolled out, or doesn't overflow container.
if (!nodeIsElement(childNode))
return true;
if (!elementSubtreeHasNonZeroDimensions(childNode))
return true;
// Recurse.
return isElementSubtreeHiddenByOverflow(childNode);
});
}
// This is a partial reimplementation of Selenium's "element is displayed" algorithm.
// When the W3C specification's algorithm stabilizes, we should implement that.
if (!(element instanceof Element))
throw new Error("Cannot check the displayedness of a non-Element argument.");
// If this command is misdirected to the wrong document, treat it as not shown.
if (!document.contains(element))
return false;
// Special cases for specific tag names.
switch (element.tagName.toUpperCase()) {
case "BODY":
return true;
case "SCRIPT":
case "NOSCRIPT":
return false;
case "OPTGROUP":
case "OPTION":
// Option/optgroup are considered shown if the containing <select> is shown.
let enclosingSelectElement = enclosingNodeOrSelfMatchingPredicate(element, (e) => e.tagName.toUpperCase() === "SELECT");
return isShown(enclosingSelectElement);
case "INPUT":
// <input type="hidden"> is considered not shown.
if (element.type === "hidden")
return false;
break;
case "MAP":
// FIXME: Selenium has special handling for <map> elements. We don't do anything now.
default:
break;
}
if (cascadedStylePropertyForElement(element, "visibility") !== "visible")
return false;
let hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
return Number(cascadedStylePropertyForElement(e, "opacity")) === 0;
});
let hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
return cascadedStylePropertyForElement(e, "display") === "none";
});
if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone)
return false;
if (!elementSubtreeHasNonZeroDimensions(element))
return false;
if (isElementSubtreeHiddenByOverflow(element))
return false;
return true;
}