mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-29 19:41:16 -05:00
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:
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user