WIP: calculate whether an element is covered using the center, not top left [skip ci]

- provide a reason a fixed position element is hidden
- format error messages
- cleanup visibility algorithm when dealing with fixed position
- add tests to ensure new reason element is hidden is displayed
- return topCenter and leftCenter values
This commit is contained in:
Brian Mann
2017-09-15 21:19:54 -04:00
parent 3d8489369b
commit 66c90c6c24
7 changed files with 193 additions and 53 deletions
@@ -39,11 +39,14 @@ getFixedOrStickyEl = ($el) ->
ensureElIsNotCovered = (cy, win, $el, fromViewport, options, log, onScroll) ->
$elAtCoords = null
getElementAtPointFromViewport = (fromViewport) ->
if elAtCoords = $dom.getElementAtPointFromViewport(win.document, fromViewport.leftCenter, fromViewport.topCenter)
$elAtCoords = $dom.wrap(elAtCoords)
ensureDescendents = (fromViewport) ->
## figure out the deepest element we are about to interact
## with at these coordinates
if elAtCoords = $dom.getElementAtPointFromViewport(win.document, fromViewport.left, fromViewport.top)
$elAtCoords = $dom.wrap(elAtCoords)
$elAtCoords = getElementAtPointFromViewport(fromViewport)
cy.ensureDescendents($el, $elAtCoords, log)
@@ -149,9 +152,7 @@ ensureElIsNotCovered = (cy, win, $el, fromViewport, options, log, onScroll) ->
## we failed here, but before scrolling the next container
## we need to first verify that the element covering up
## is the same one as before our scroll
if elAtCoords = $dom.getElementAtPointFromViewport(win.document, fromViewport.left, fromViewport.top)
$elAtCoords = $dom.wrap(elAtCoords)
if $elAtCoords = getElementAtPointFromViewport(fromViewport)
## get the fixed element again
$fixed = getFixedOrStickyEl($elAtCoords)
@@ -139,19 +139,25 @@ module.exports = {
dom:
animating: """
#{cmd('{{cmd}}')} could not be issued because this element is currently animating:\n
{{node}}\n
#{cmd('{{cmd}}')} could not be issued because this element is currently animating:
{{node}}
You can fix this problem by:
- Passing {force: true} which disables all error checking
- Passing {waitForAnimations: false} which disables waiting on animations
- Passing {animationDistanceThreshold: 20} which decreases the sensitivity\n
- Passing {animationDistanceThreshold: 20} which decreases the sensitivity
https://on.cypress.io/element-is-animating
"""
animation_check_failed: "Not enough coord points provided to calculate distance."
center_hidden: """
#{cmd('{{cmd}}')} failed because the center of this element is hidden from view:\n
{{node}}\n
Fix this problem, or use {force: true} to disable error checking.\n
#{cmd('{{cmd}}')} failed because the center of this element is hidden from view:
{{node}}
Fix this problem, or use {force: true} to disable error checking.
https://on.cypress.io/element-cannot-be-interacted-with
"""
covered: (obj) ->
@@ -169,9 +175,12 @@ module.exports = {
https://on.cypress.io/element-cannot-be-interacted-with
"""
disabled: """
#{cmd('{{cmd}}')} failed because this element is disabled:\n
{{node}}\n
Fix this problem, or use {force: true} to disable error checking.\n
#{cmd('{{cmd}}')} failed because this element is disabled:
{{node}}
Fix this problem, or use {force: true} to disable error checking.
https://on.cypress.io/element-cannot-be-interacted-with
"""
invalid_position_argument: "Invalid position argument: '{{position}}'. Position may only be {{validPositions}}."
@@ -180,10 +189,14 @@ module.exports = {
{{node}}\n
"""
not_visible: """
#{cmd('{{cmd}}')} failed because this element is not visible:\n
{{node}}\n
{{reason}}\n
Fix this problem, or use {force: true} to disable error checking.\n
#{cmd('{{cmd}}')} failed because this element is not visible:
{{node}}
{{reason}}
Fix this problem, or use {force: true} to disable error checking.
https://on.cypress.io/element-cannot-be-interacted-with
"""
+33 -12
View File
@@ -12,6 +12,11 @@ getElementPositioning = ($el) ->
## are relative to the top left of the viewport
rect = el.getBoundingClientRect()
center = getCenterCoordinates(rect)
topCenter = center.top
leftCenter = center.left
return {
scrollTop: el.scrollTop
scrollLeft: el.scrollLeft
@@ -22,10 +27,14 @@ getElementPositioning = ($el) ->
left: rect.left
right: rect.right
bottom: rect.bottom
topCenter
leftCenter
}
fromWindow: {
top: rect.top + win.pageYOffset
left: rect.left + win.pageXOffset
topCenter: topCenter + win.pageYOffset
leftCenter: leftCenter + win.pageXOffset
}
}
@@ -127,22 +136,34 @@ getElementCoordinatesByPosition = ($el, position = "center") ->
fn = calculations[fnName]
normalizeFromViewport = fn({
width
height
top: fromViewport.top
left: fromViewport.left
})
normalizeFromWindow = fn({
width
height
top: fromWindow.top
left: fromWindow.left
})
fromViewport.top = normalizeFromViewport.top
fromViewport.left = normalizeFromViewport.left
fromWindow.top = normalizeFromWindow.top
fromWindow.left = normalizeFromWindow.left
## return an object with both sets
## of normalized coordinates for both
## the window and the viewport
return {
fromViewport: fn({
width
height
top: fromViewport.top
left: fromViewport.left
}),
fromWindow: fn({
width
height
top: fromWindow.top
left: fromWindow.left
})
width
height
fromViewport
fromWindow
}
calculations = {
+48 -21
View File
@@ -43,7 +43,15 @@ isHidden = (el, name) ->
## to see if its hidden by a parent
elIsHiddenByAncestors($el) or
elIsOutOfBoundsOfAncestorsOverflow($el)
## 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
@@ -89,6 +97,20 @@ canClipContent = ($el, $ancestor) ->
return true
elIsFixed = ($el) ->
if $stickyOrFixedEl = $elements.getFirstFixedOrStickyPositionParent($el)
$stickyOrFixedEl.css("position") is "fixed"
elAtPoint = ($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
@@ -99,14 +121,11 @@ elDescendentsHavePositionFixedOrAbsolute = ($parent, $child) ->
fixedOrAbsoluteRe.test $jquery.wrap(el).css("position")
elIsNotElementFromPoint = ($el) ->
elProps = $coordinates.getElementPositioning($el)
{top, left} = elProps.fromViewport
doc = $document.getDocumentFromElement($el.get(0))
if el = $coordinates.getElementAtPointFromViewport(doc, left, top)
$elAtPoint = $jquery.wrap(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 = elAtPoint($el)
## if the element at point is not a descendent
## of our $el then we know it's being covered or its
@@ -114,14 +133,6 @@ elIsNotElementFromPoint = ($el) ->
return not $elements.isDescendent($el, $elAtPoint)
elIsOutOfBoundsOfAncestorsOverflow = ($el, $ancestor) ->
if $stickyOrFixedEl = $elements.getFirstFixedOrStickyPositionParent($el)
if $stickyOrFixedEl.css("position") is "fixed"
## 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
return elIsNotElementFromPoint($stickyOrFixedEl)
$ancestor ?= $el.parent()
## no ancestor, not out of bounds!
@@ -227,10 +238,12 @@ getReasonIsHidden = ($el) ->
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)
@@ -239,19 +252,33 @@ getReasonIsHidden = ($el) ->
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."
when elIsOutOfBoundsOfAncestorsOverflow($el)
"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'"
else
"Cypress could not determine why this element '#{node}' is not visible."
## nested else --___________--
if elIsFixed($el)
if elIsNotElementFromPoint($el)
## show the long element here
covered = $elements.stringify(elAtPoint($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
@@ -1027,6 +1027,42 @@ describe "src/cy/commands/actions/clicking", ->
cy.get("#button-covered-in-span").click()
it "throws when element is fixed position and being covered", (done) ->
$btn = $("<button>button covered</button>")
.attr("id", "button-covered-in-span")
.css({position: "fixed", left: 0, top: 0})
.prependTo(cy.$$("body"))
$span = $("<span>span on button</span>")
.css({position: "fixed", left: 0, top: 0, padding: 20, display: "inline-block", backgroundColor: "yellow", zIndex: 10})
.prependTo(cy.$$("body"))
cy.on "fail", (err) =>
lastLog = @lastLog
## get + click logs
expect(@logs.length).eq(2)
expect(lastLog.get("error")).to.eq(err)
## there should still be 2 snapshots on error (before + after)
expect(lastLog.get("snapshots").length).to.eq(2)
expect(lastLog.get("snapshots")[0]).to.be.an("object")
expect(lastLog.get("snapshots")[0].name).to.eq("before")
expect(lastLog.get("snapshots")[1]).to.be.an("object")
expect(lastLog.get("snapshots")[1].name).to.eq("after")
expect(err.message).to.include "cy.click() failed because this element is not visible:"
expect(err.message).to.include ">button ...</button>"
expect(err.message).to.include "'<button#button-covered-in-span>' is not visible because it has CSS property: 'position: fixed' and its being covered"
expect(err.message).to.include ">span on...</span>"
console = lastLog.invoke("consoleProps")
expect(console["Tried to Click"]).to.be.undefined
expect(console["But its Covered By"]).to.be.undefined
done()
cy.get("#button-covered-in-span").click()
it "throws when element is hidden and theres no element specifically covering it", (done) ->
## i cant come up with a way to easily make getElementAtCoordinates
## return null so we are just forcing it to return null to simulate
@@ -418,8 +418,8 @@ describe "src/cypress/dom", ->
expect(@$descendantInPosFixed.find("#descendantInPosFixed")).not.to.be.hidden
it "is hidden if position: fixed and covered up", ->
expect(@$coveredUpPosFixed).to.be.hidden
expect(@$coveredUpPosFixed).not.to.be.visible
expect(@$coveredUpPosFixed.find("#coveredUpPosFixed")).to.be.hidden
expect(@$coveredUpPosFixed.find("#coveredUpPosFixed")).not.to.be.visible
it "is hidden if position: fixed and off screent", ->
expect(@$offScreenPosFixed).to.be.hidden
@@ -517,5 +517,12 @@ describe "src/cypress/dom", ->
it "element sits outside boundaries of parent with overflow clipping", ->
@reasonIs @$elOutOfParentBoundsToRight.find("span"), "This element '<span>' 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\'"
it "element is fixed and being covered", ->
@reasonIs @$coveredUpPosFixed.find("#coveredUpPosFixed"), """
This element '<div#coveredUpPosFixed>' is not visible because it has CSS property: 'position: fixed' and its being covered by another element:
<div style="position: fixed; bottom: 0; left: 0">on top</div>
"""
it "cannot determine why element is not visible", ->
@reasonIs @$btnOpacity, "Cypress could not determine why this element '<button>' is not visible."
@@ -15,6 +15,41 @@ describe "src/dom/coordinates", ->
@$button = $("<button style='position: absolute; top: 25px; left: 50px; width: 100px; line-height: 50px; padding: 10px; margin: 10px; border: 10px solid black'>foo</button>")
.appendTo(cy.$$("body"))
context ".getElementPositioning", ->
## this is necessary so that document.elementFromPoint
## does not miss elements
it "returns the leftCenter and topCenter normalized", ->
win = Cypress.dom.getWindowByElement(@$button.get(0))
pageYOffset = Object.getOwnPropertyDescriptor(win, "pageYOffset")
pageXOffset = Object.getOwnPropertyDescriptor(win, "pageXOffset")
Object.defineProperty(win, "pageYOffset", {
value: 10
})
Object.defineProperty(win, "pageXOffset", {
value: 20
})
cy.stub(@$button.get(0), "getBoundingClientRect").returns({
top: 100.9
left: 60.9
width: 50
height: 40
})
{ fromViewport, fromWindow } = Cypress.dom.getElementPositioning(@$button)
expect(fromViewport.topCenter).to.eq(120)
expect(fromViewport.leftCenter).to.eq(85)
expect(fromWindow.topCenter).to.eq(130)
expect(fromWindow.leftCenter).to.eq(105)
Object.defineProperty(win, "pageYOffset", pageYOffset)
Object.defineProperty(win, "pageXOffset", pageXOffset)
context ".normalizeCoords", ->
it "rounds down x and y values to object", ->
expect(Cypress.dom.normalizeCoords(5.96, 7.68)).to.deep.eq({left: 5, top: 7})