diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index de12ae239f90..a4e03f6ab1e3 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -1,4 +1,12 @@
+## 14.01.1
+
+_Released 10/31/2024 (PENDING)_
+
+**Bugfixes:**
+
+- Fixed an issue where "isVisible" is incorrectly assessed for the absolutely positioned elements if the ancestor has overflow and static position [#29689](https://github.com/cypress-io/cypress/pull/29689).
+
## 13.15.0
_Released 9/25/2024_
diff --git a/packages/app/cypress/e2e/specs_list_e2e.cy.ts b/packages/app/cypress/e2e/specs_list_e2e.cy.ts
index 03cfd8fbe936..4999f3cba6b2 100644
--- a/packages/app/cypress/e2e/specs_list_e2e.cy.ts
+++ b/packages/app/cypress/e2e/specs_list_e2e.cy.ts
@@ -120,7 +120,7 @@ describe('App: Spec List (E2E)', () => {
cy.findAllByTestId('spec-item-link').contains('dom-content.spec.js').click()
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
- cy.findByText('Your tests are loading...').should('not.be.visible')
+ cy.get('.runnable-loading-title', { timeout: 4000 }).should('not.be.exist')
cy.get('body').type('f')
cy.get('[data-selected-spec="true"]').contains('dom-content.spec.js')
diff --git a/packages/driver/cypress/e2e/dom/visibility.cy.ts b/packages/driver/cypress/e2e/dom/visibility.cy.ts
index fc747350b5c3..a195ad8cd839 100644
--- a/packages/driver/cypress/e2e/dom/visibility.cy.ts
+++ b/packages/driver/cypress/e2e/dom/visibility.cy.ts
@@ -382,8 +382,8 @@ describe('src/cypress/dom/visibility', () => {
`)
this.$elOutOfParentBoundsAbove = add(`\
-
- position: absolute, out of bounds above
+
+ position: absolute, out of bounds above
\
`)
@@ -863,7 +863,7 @@ describe('src/cypress/dom/visibility', () => {
})
it('is hidden when parent overflow hidden and out of bounds above', function () {
- expect(this.$elOutOfParentBoundsAbove.find('span')).to.be.hidden
+ expect(this.$elOutOfParentBoundsAbove.find('span#elOutOfParentBoundsAbove')).to.be.hidden
})
it('is hidden when parent overflow hidden and out of bounds below', function () {
@@ -1200,10 +1200,7 @@ describe('src/cypress/dom/visibility', () => {
})
it('element is fixed and being covered', function () {
- this.reasonIs(this.$coveredUpPosFixed.find('#coveredUpPosFixed'), `\
-This element \`
\` is not visible because it has CSS property: \`position: fixed\` and it's being covered by another element:
-
-\`
on top
\``)
+ this.reasonIs(this.$coveredUpPosFixed.find('#coveredUpPosFixed'), `\This element \`
\` is not visible because it has CSS property: \`position: fixed\` and it's being covered by another element:\n\n\`
+
\ No newline at end of file
diff --git a/packages/driver/src/dom/visibility.ts b/packages/driver/src/dom/visibility.ts
index 2a92fae2b963..b1ff4ebe84bc 100644
--- a/packages/driver/src/dom/visibility.ts
+++ b/packages/driver/src/dom/visibility.ts
@@ -65,7 +65,7 @@ const isStrictlyHidden = (el, methodName = 'isStrictlyHidden()', options = { che
}
// in Cypress-land we consider the element hidden if
- // either its offsetHeight or offsetWidth is 0 because
+ // either its clientHeight or clientWidth is 0 because
// it is impossible for the user to interact with this element
if (elHasNoEffectiveWidthOrHeight($el)) {
// https://github.com/cypress-io/cypress/issues/6183
@@ -121,7 +121,7 @@ const elHasNoEffectiveWidthOrHeight = ($el) => {
// Is the element's CSS width OR height, including any borders,
// padding, and vertical scrollbars (if rendered) less than 0?
//
- // elOffsetWidth:
+ // elClientWidth:
// If the element is hidden (for example, by setting style.display
// on the element or one of its ancestors to "none"), then 0 is returned.
@@ -129,16 +129,22 @@ const elHasNoEffectiveWidthOrHeight = ($el) => {
// For HTML elements, SVG elements that do not render anything themselves,
// display:none elements, and generally any elements that are not directly rendered,
// an empty list is returned.
-
const el = $el[0]
- const style = getComputedStyle(el)
- const transform = style.getPropertyValue('transform')
- const width = elOffsetWidth($el)
- const height = elOffsetHeight($el)
- const overflowHidden = elHasOverflowHidden($el)
+ let transform
+
+ if ($el[0].style.transform) {
+ const style = getComputedStyle(el)
+
+ transform = style.getPropertyValue('transform')
+ } else {
+ transform = 'none'
+ }
+
+ const width = elClientWidth($el)
+ const height = elClientHeight($el)
return isZeroLengthAndTransformNone(width, height, transform) ||
- isZeroLengthAndOverflowHidden(width, height, overflowHidden) ||
+ isZeroLengthAndOverflowHidden(width, height, elHasOverflowHidden($el)) ||
(el.getClientRects().length <= 0)
}
@@ -146,7 +152,7 @@ const isZeroLengthAndTransformNone = (width, height, transform) => {
// From https://github.com/cypress-io/cypress/issues/5974,
// we learned that when an element has non-'none' transform style value like "translate(0, 0)",
// it is visible even with `height: 0` or `width: 0`.
- // That's why we're checking `transform === 'none'` together with elOffsetWidth/Height.
+ // That's why we're checking `transform === 'none'` together with elClientWidth/Height.
return (width <= 0 && transform === 'none') ||
(height <= 0 && transform === 'none')
@@ -157,22 +163,28 @@ const isZeroLengthAndOverflowHidden = (width, height, overflowHidden) => {
(height <= 0 && overflowHidden)
}
-const elHasNoOffsetWidthOrHeight = ($el) => {
- return (elOffsetWidth($el) <= 0) || (elOffsetHeight($el) <= 0)
+const elHasNoClientWidthOrHeight = ($el) => {
+ return (elClientWidth($el) <= 0) || (elClientHeight($el) <= 0)
}
-const elOffsetWidth = ($el) => {
- return $el[0].offsetWidth
+const elementBoundingRect = ($el) => $el[0].getBoundingClientRect()
+
+const elClientHeight = ($el) => {
+ return elementBoundingRect($el).height
}
-const elOffsetHeight = ($el) => {
- return $el[0].offsetHeight
+const elClientWidth = ($el) => {
+ return elementBoundingRect($el).width
}
const elHasVisibilityHiddenOrCollapse = ($el) => {
return elHasVisibilityHidden($el) || elHasVisibilityCollapse($el)
}
+const elHasVisibilityVisible = ($el) => {
+ return $el.css('visibility') === 'visible'
+}
+
const elHasVisibilityHidden = ($el) => {
return $el.css('visibility') === 'hidden'
}
@@ -221,11 +233,17 @@ const canClipContent = function ($el, $ancestor) {
// the closest parent with position relative, absolute, or fixed
const $offsetParent = $el.offsetParent()
+ const isClosestAncsestor = isAncestor($ancestor, $offsetParent)
+
+ // fix for 28638 - when element postion is relative and it's parent absolute
+ if (elHasPositionRelative($el) && isClosestAncsestor && elHasPositionAbsolute($ancestor)) {
+ return false
+ }
// even if ancestors' overflow is clippable, if the element's offset parent
// is a parent of the ancestor, the ancestor will not clip the element
// unless the element is position relative
- if (!elHasPositionRelative($el) && isAncestor($ancestor, $offsetParent)) {
+ if (!elHasPositionRelative($el) && isClosestAncsestor) {
return false
}
@@ -308,7 +326,7 @@ const elIsNotElementFromPoint = function ($el) {
return true
}
-const elIsOutOfBoundsOfAncestorsOverflow = function ($el, $ancestor = getParent($el)) {
+const elIsOutOfBoundsOfAncestorsOverflow = function ($el: JQuery, $ancestor = getParent($el)) {
// no ancestor, not out of bounds!
// if we've reached the top parent, which is not a normal DOM el
// then we're in bounds all the way up, return false
@@ -316,27 +334,32 @@ const elIsOutOfBoundsOfAncestorsOverflow = function ($el, $ancestor = getParent(
return false
}
+ if (elHasPositionRelative($el) && elHasPositionAbsolute($ancestor)) {
+ return false
+ }
+
if (canClipContent($el, $ancestor)) {
- const elProps = $coordinates.getElementPositioning($el)
- const ancestorProps = $coordinates.getElementPositioning($ancestor)
+ const ancestorProps = $ancestor.get(0).getBoundingClientRect()
if (elHasPositionAbsolute($el) && (ancestorProps.width === 0 || ancestorProps.height === 0)) {
return elIsOutOfBoundsOfAncestorsOverflow($el, getParent($ancestor))
}
+ const elProps = $el.get(0).getBoundingClientRect()
+
// target el is out of bounds
if (
// target el is to the right of the ancestor's visible area
- (elProps.fromElWindow.left >= (ancestorProps.width + ancestorProps.fromElWindow.left)) ||
+ (elProps.left >= (ancestorProps.width + ancestorProps.left)) ||
// target el is to the left of the ancestor's visible area
- ((elProps.fromElWindow.left + elProps.width) <= ancestorProps.fromElWindow.left) ||
+ ((elProps.left + elProps.width) <= ancestorProps.left) ||
// target el is under the ancestor's visible area
- (elProps.fromElWindow.top >= (ancestorProps.height + ancestorProps.fromElWindow.top)) ||
+ (elProps.top >= (ancestorProps.height + ancestorProps.top)) ||
// target el is above the ancestor's visible area
- ((elProps.fromElWindow.top + elProps.height) <= ancestorProps.fromElWindow.top)
+ ((elProps.top + elProps.height) <= ancestorProps.top)
) {
return true
}
@@ -348,7 +371,7 @@ const elIsOutOfBoundsOfAncestorsOverflow = function ($el, $ancestor = getParent(
const elIsHiddenByAncestors = function ($el, checkOpacity, $origEl = $el) {
// walk up to each parent until we reach the body
// if any parent has opacity: 0
- // or has an effective offsetHeight of 0
+ // or has an effective clientHeight of 0
// and its set overflow: hidden then our child element
// is effectively hidden
// -----UNLESS------
@@ -376,11 +399,15 @@ const elIsHiddenByAncestors = function ($el, checkOpacity, $origEl = $el) {
return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl)
}
+ if (elHasVisibilityVisible($parent)) {
+ return false
+ }
+
// continue to recursively walk up the chain until we reach body or html
return elIsHiddenByAncestors($parent, checkOpacity, $origEl)
}
-const parentHasNoOffsetWidthOrHeightAndOverflowHidden = function ($el) {
+const parentHasNoClientWidthOrHeightAndOverflowHidden = function ($el) {
// if we've walked all the way up to body or html then return false
if (isUndefinedOrHTMLBodyDoc($el)) {
return false
@@ -392,7 +419,7 @@ const parentHasNoOffsetWidthOrHeightAndOverflowHidden = function ($el) {
}
// continue walking
- return parentHasNoOffsetWidthOrHeightAndOverflowHidden(getParent($el))
+ return parentHasNoClientWidthOrHeightAndOverflowHidden(getParent($el))
}
const parentHasDisplayNone = function ($el) {
@@ -463,8 +490,8 @@ export const getReasonIsHidden = function ($el, options = { checkOpacity: true }
// either being covered or there is no el
const node = stringifyElement($el, 'short')
- let width = elOffsetWidth($el)
- let height = elOffsetHeight($el)
+ let width = elClientWidth($el)
+ let height = elClientHeight($el)
let $parent
let parentNode
@@ -513,24 +540,24 @@ export const getReasonIsHidden = function ($el, options = { checkOpacity: true }
return `This element \`${node}\` is not visible because its parent \`${parentNode}\` has CSS property: \`opacity: 0\``
}
- if (elHasNoOffsetWidthOrHeight($el)) {
- return `This element \`${node}\` is not visible because it has an effective width and height of: \`${width} x ${height}\` pixels.`
- }
-
const transformResult = $transform.detectVisibility($el)
if (transformResult === 'transformed') {
return `This element \`${node}\` is not visible because it is hidden by transform.`
}
+ if (elHasNoClientWidthOrHeight($el)) {
+ return `This element \`${node}\` is not visible because it has an effective width and height of: \`${width} x ${height}\` pixels.`
+ }
+
if (transformResult === 'backface') {
return `This element \`${node}\` is not visible because it is rotated and its backface is hidden.`
}
- if ($parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden(getParent($el))) {
+ if ($parent = parentHasNoClientWidthOrHeightAndOverflowHidden(getParent($el))) {
parentNode = stringifyElement($parent, 'short')
- width = elOffsetWidth($parent)
- height = elOffsetHeight($parent)
+ width = elClientWidth($parent)
+ height = elClientHeight($parent)
return `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.`
}