Web Automation: upstream safaridriver's JavaScript atom implementations
[WebKit.git] / Source / WebKit2 / UIProcess / Automation / atoms / ElementDisplayed.js
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 function isShown(element) {
27     "use strict";
28
29     function nodeIsElement(node) {
30         if (!node)
31             return false;
32
33         switch (node.nodeType) {
34         case Node.ELEMENT_NODE:
35         case Node.DOCUMENT_NODE:
36         case Node.DOCUMENT_FRAGMENT_NODE:
37             return true;
38
39         default:
40             return false;
41         }
42     }
43
44     function parentElementForElement(element) {
45         if (!element)
46             return null;
47
48         return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement);
49     }
50
51     function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) {
52         for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode)
53             if (predicate(node))
54                 return node;
55
56         return null;
57     }
58
59     function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) {
60         for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element))
61             if (predicate(element))
62                 return element;
63
64         return null;
65     }
66
67     function cascadedStylePropertyForElement(element, property) {
68         if (!element || !property)
69             return null;
70
71         let computedStyle = window.getComputedStyle(element);
72         let computedStyleProperty = computedStyle.getPropertyValue(property);
73         if (computedStyleProperty && computedStyleProperty !== "inherit")
74             return computedStyleProperty;
75
76         // Ideally getPropertyValue would return the 'used' or 'actual' value, but
77         // it doesn't for legacy reasons. So we need to do our own poor man's cascade.
78         // Fall back to the first non-'inherit' value found in an ancestor.
79         // In any case, getPropertyValue will not return 'initial'.
80
81         // FIXME: will this incorrectly inherit non-inheritable CSS properties?
82         // I think all important non-inheritable properties (width, height, etc.)
83         // for our purposes here are specially resolved, so this may not be an issue.
84         // Specification is here: https://drafts.csswg.org/cssom/#resolved-values
85         let parentElement = parentElementForElement(element);
86         return cascadedStylePropertyForElement(parentElement, property);
87     }
88
89     function elementSubtreeHasNonZeroDimensions(element) {
90         let boundingBox = element.getBoundingClientRect();
91         if (boundingBox.width > 0 && boundingBox.height > 0)
92             return true;
93
94         // Paths can have a zero width or height. Treat them as shown if the stroke width is positive.
95         if (element.tagName.toUpperCase() === "PATH" && boundingBox.width + boundingBox.height > 0) {
96             let strokeWidth = cascadedStylePropertyForElement(element, "stroke-width");
97             return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
98         }
99
100         let cascadedOverflow = cascadedStylePropertyForElement(element, "overflow");
101         if (cascadedOverflow === "hidden")
102             return false;
103
104         // If the container's overflow is not hidden and it has zero size, consider the
105         // container to have non-zero dimensions if a child node has non-zero dimensions.
106         return Array.from(element.childNodes).some((childNode) => {
107             if (childNode.nodeType === Node.TEXT_NODE)
108                 return true;
109
110             if (nodeIsElement(childNode))
111                 return elementSubtreeHasNonZeroDimensions(childNode);
112
113             return false;
114         });
115     }
116
117     function elementOverflowsContainer(element) {
118         let cascadedOverflow = cascadedStylePropertyForElement(element, "overflow");
119         if (cascadedOverflow !== "hidden")
120             return false;
121
122         // FIXME: this needs to take into account the scroll position of the element,
123         // the display modes of it and its ancestors, and the container it overflows.
124         // See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases.
125         return true;
126     }
127
128     function isElementSubtreeHiddenByOverflow(element) {
129         if (!element)
130             return false;
131
132         if (!elementOverflowsContainer(element))
133             return false;
134
135         // This element's subtree is hidden by overflow if all child subtrees are as well.
136         return Array.from(element.childNodes).every((childNode) => {
137             // Returns true if the child node is overflowed or otherwise hidden.
138             // Base case: not an element, has zero size, scrolled out, or doesn't overflow container.
139             if (!nodeIsElement(childNode))
140                 return true;
141
142             if (!elementSubtreeHasNonZeroDimensions(childNode))
143                 return true;
144
145             // Recurse.
146             return isElementSubtreeHiddenByOverflow(childNode);
147         });
148     }
149
150     // This is a partial reimplementation of Selenium's "element is displayed" algorithm.
151     // When the W3C specification's algorithm stabilizes, we should implement that.
152
153     if (!(element instanceof Element))
154         throw new Error("Cannot check the displayedness of a non-Element argument.");
155
156     // If this command is misdirected to the wrong document, treat it as not shown.
157     if (!document.contains(element))
158         return false;
159
160     // Special cases for specific tag names.
161     switch (element.tagName.toUpperCase()) {
162     case "BODY":
163         return true;
164
165     case "SCRIPT":
166     case "NOSCRIPT":
167         return false;
168
169     case "OPTGROUP":
170     case "OPTION":
171         // Option/optgroup are considered shown if the containing <select> is shown.
172         let enclosingSelectElement = enclosingNodeOrSelfMatchingPredicate(element, (e) => e.tagName.toUpperCase() === "SELECT");
173         return isShown(enclosingSelectElement);
174
175     case "INPUT":
176         // <input type="hidden"> is considered not shown.
177         if (element.type === "hidden")
178             return false;
179         break;
180
181     case "MAP":
182         // FIXME: Selenium has special handling for <map> elements. We don't do anything now.
183
184     default:
185         break;
186     }
187
188     if (cascadedStylePropertyForElement(element, "visibility") !== "visible")
189         return false;
190
191     let hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
192         return Number(cascadedStylePropertyForElement(e, "opacity")) === 0;
193     });
194     let hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
195         return cascadedStylePropertyForElement(e, "display") === "none";
196     });
197     if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone)
198         return false;
199
200     if (!elementSubtreeHasNonZeroDimensions(element))
201         return false;
202
203     if (isElementSubtreeHiddenByOverflow(element))
204         return false;
205
206     return true;
207 }