/
visibility.coffee
289 lines (215 loc) · 9.85 KB
/
visibility.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
_ = require("lodash")
$jquery = require("./jquery")
$document = require("./document")
$elements = require("./elements")
$coordinates = require("./coordinates")
fixedOrAbsoluteRe = /(fixed|absolute)/
OVERFLOW_PROPS = ["hidden", "scroll", "auto"]
## WARNING:
## developer beware. visibility is a sink hole
## that leads to sheer madness. you should
## avoid this file before its too late.
isVisible = (el) ->
not isHidden(el, "isVisible()")
## TODO: we should prob update dom
## to be passed in $utils as a dependency
## because of circular references
isHidden = (el, name) ->
if not $elements.isElement(el)
name ?= "isHidden()"
throw new Error("Cypress.dom.#{name} must be passed a basic DOM element.")
$el = $jquery.wrap(el)
## in Cypress-land we consider the element hidden if
## either its offsetHeight or offsetWidth is 0 because
## it is impossible for the user to interact with this element
## offsetHeight / offsetWidth includes the ef
elHasNoEffectiveWidthOrHeight($el) or
## additionally if the effective visibility of the element
## is hidden (which includes any parent nodes) then the user
## cannot interact with this element and thus it is hidden
elHasVisibilityHidden($el) or
## we do some calculations taking into account the parents
## to see if its hidden by a parent
elIsHiddenByAncestors($el) or
## if this is a fixed element check if its covered
(
if elIsFixed($el)
elIsNotElementFromPoint($el)
else
## else check if el is outside the bounds
## of its ancestors overflow
elIsOutOfBoundsOfAncestorsOverflow($el)
)
elHasNoEffectiveWidthOrHeight = ($el) ->
elOffsetWidth($el) <= 0 or elOffsetHeight($el) <= 0 or $el[0].getClientRects().length <= 0
elHasNoOffsetWidthOrHeight = ($el) ->
elOffsetWidth($el) <= 0 or elOffsetHeight($el) <= 0
elOffsetWidth = ($el) ->
$el[0].offsetWidth
elOffsetHeight = ($el) ->
$el[0].offsetHeight
elHasVisibilityHidden = ($el) ->
$el.css("visibility") is "hidden"
elHasDisplayNone = ($el) ->
$el.css("display") is "none"
elHasOverflowHidden = ($el) ->
"hidden" in [$el.css("overflow"), $el.css("overflow-y"), $el.css("overflow-x")]
elHasPositionRelative = ($el) ->
$el.css("position") is "relative"
elHasClippableOverflow = ($el) ->
$el.css("overflow") in OVERFLOW_PROPS or
$el.css("overflow-y") in OVERFLOW_PROPS or
$el.css("overflow-x") in OVERFLOW_PROPS
canClipContent = ($el, $ancestor) ->
## can't clip without clippable overflow
if not elHasClippableOverflow($ancestor)
return false
$offsetParent = $jquery.wrap($el[0].offsetParent)
## even if overflow is clippable, if an ancestor of the ancestor is the
## element's offset parent, the ancestor will not clip the element
## unless the element is position relative
if not elHasPositionRelative($el) and $elements.isAncestor($ancestor, $offsetParent)
return false
return true
elIsFixed = ($el) ->
if $stickyOrFixedEl = $elements.getFirstFixedOrStickyPositionParent($el)
$stickyOrFixedEl.css("position") is "fixed"
elAtCenterPoint = ($el) ->
elProps = $coordinates.getElementPositioning($el)
{ topCenter, leftCenter } = elProps.fromViewport
doc = $document.getDocumentFromElement($el.get(0))
if el = $coordinates.getElementAtPointFromViewport(doc, leftCenter, topCenter)
$jquery.wrap(el)
elDescendentsHavePositionFixedOrAbsolute = ($parent, $child) ->
## create an array of all elements between $parent and $child
## including child but excluding parent
## and check if these have position fixed|absolute
$els = $child.parentsUntil($parent).add($child)
_.some $els.get(), (el) ->
fixedOrAbsoluteRe.test $jquery.wrap(el).css("position")
elIsNotElementFromPoint = ($el) ->
## if we have a fixed position element that means
## it is fixed 'relative' to the viewport which means
## it MUST be available with elementFromPoint because
## that is also relative to the viewport
$elAtPoint = elAtCenterPoint($el)
## if the element at point is not a descendent
## of our $el then we know it's being covered or its
## not visible
return not $elements.isDescendent($el, $elAtPoint)
elIsOutOfBoundsOfAncestorsOverflow = ($el, $ancestor) ->
$ancestor ?= $el.parent()
## no ancestor, not out of bounds!
return false if not $ancestor
## if we've reached the top parent, which is document
## then we're in bounds all the way up, return false
return false if $ancestor.is("body,html") or $document.isDocument($ancestor)
elProps = $coordinates.getElementPositioning($el)
if canClipContent($el, $ancestor)
ancestorProps = $coordinates.getElementPositioning($ancestor)
## target el is out of bounds
return true if (
## target el is to the right of the ancestor's visible area
elProps.fromWindow.left > (ancestorProps.width + ancestorProps.fromWindow.left) or
## target el is to the left of the ancestor's visible area
(elProps.fromWindow.left + elProps.width) < ancestorProps.fromWindow.left or
## target el is under the ancestor's visible area
elProps.fromWindow.top > (ancestorProps.height + ancestorProps.fromWindow.top) or
## target el is above the ancestor's visible area
(elProps.fromWindow.top + elProps.height) < ancestorProps.fromWindow.top
)
elIsOutOfBoundsOfAncestorsOverflow($el, $ancestor.parent())
elIsHiddenByAncestors = ($el, $origEl) ->
## store the original $el
$origEl ?= $el
## walk up to each parent until we reach the body
## if any parent has an effective offsetHeight of 0
## and its set overflow: hidden then our child element
## is effectively hidden
## -----UNLESS------
## the parent or a descendent has position: absolute|fixed
$parent = $el.parent()
## stop if we've reached the body or html
## in case there is no body
## or if parent is the document which can
## happen if we already have an <html> element
return false if $parent.is("body,html") or $document.isDocument($parent)
if elHasOverflowHidden($parent) and elHasNoEffectiveWidthOrHeight($parent)
## if any of the elements between the parent and origEl
## have fixed or position absolute
return not elDescendentsHavePositionFixedOrAbsolute($parent, $origEl)
## continue to recursively walk up the chain until we reach body or html
elIsHiddenByAncestors($parent, $origEl)
parentHasNoOffsetWidthOrHeightAndOverflowHidden = ($el) ->
## if we've walked all the way up to body or html then return false
return false if not $el.length or $el.is("body,html")
## if we have overflow hidden and no effective width or height
if elHasOverflowHidden($el) and elHasNoEffectiveWidthOrHeight($el)
return $el
else
## continue walking
return parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
parentHasDisplayNone = ($el) ->
## if we have no $el or we've walked all the way up to document
## then return false
return false if not $el.length or $document.isDocument($el)
## if we have display none then return the $el
if elHasDisplayNone($el)
return $el
else
## continue walking
return parentHasDisplayNone($el.parent())
parentHasVisibilityNone = ($el) ->
## if we've walked all the way up to document then return false
return false if not $el.length or $document.isDocument($el)
## if we have display none then return the $el
if elHasVisibilityHidden($el)
return $el
else
## continue walking
return parentHasVisibilityNone($el.parent())
getReasonIsHidden = ($el) ->
## TODO: need to add in the reason an element
## is hidden when its fixed position and its
## either being covered or there is no el
node = $elements.stringify($el, "short")
## returns the reason in human terms why an element is considered not visible
switch
when elHasDisplayNone($el)
"This element '#{node}' is not visible because it has CSS property: 'display: none'"
when $parent = parentHasDisplayNone($el.parent())
parentNode = $elements.stringify($parent, "short")
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'"
when $parent = parentHasVisibilityNone($el.parent())
parentNode = $elements.stringify($parent, "short")
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'visibility: hidden'"
when elHasVisibilityHidden($el)
"This element '#{node}' is not visible because it has CSS property: 'visibility: hidden'"
when elHasNoOffsetWidthOrHeight($el)
width = elOffsetWidth($el)
height = elOffsetHeight($el)
"This element '#{node}' is not visible because it has an effective width and height of: '#{width} x #{height}' pixels."
when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
parentNode = $elements.stringify($parent, "short")
width = elOffsetWidth($parent)
height = elOffsetHeight($parent)
"This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels."
else
## nested else --___________--
if elIsFixed($el)
if elIsNotElementFromPoint($el)
## show the long element here
covered = $elements.stringify(elAtCenterPoint($el))
return """
This element '#{node}' is not visible because it has CSS property: 'position: fixed' and its being covered by another element:
#{covered}
"""
else
if elIsOutOfBoundsOfAncestorsOverflow($el)
return "This element '#{node}' is not visible because its content is being clipped by one of its parent elements, which has a CSS property of overflow: 'hidden', 'scroll' or 'auto'"
return "Cypress could not determine why this element '#{node}' is not visible."
module.exports = {
isVisible
isHidden
getReasonIsHidden
}