diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 2a3fbee031f6..b0921401fe31 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -46,6 +46,7 @@ in this [GitHub issue](https://github.com/cypress-io/cypress/issues/30447). Addr - Elements with `display: contents` will no longer use box model calculations for visibility, and correctly show as visible when it is visible. Fixed in [#29680](https://github.com/cypress-io/cypress/pull/29680). Fixes [#29605](https://github.com/cypress-io/cypress/issues/29605). - The CSS pseudo-class `:dir()` is now supported when testing in Electron. Addresses [#29766](https://github.com/cypress-io/cypress/issues/29766). +- Fixed a visibility issue when the element is positioned `static` or `relative` and the element's offset parent is positioned `absolute`, a descendent of the ancestor, and has no clippable overflow. Fixed in [#29689](https://github.com/cypress-io/cypress/pull/29689). Fixes [#28638](https://github.com/cypress-io/cypress/issues/28638). - Fixed a visibility issue for elements with `textContent` but without a width or height. Fixed in [#29688](https://github.com/cypress-io/cypress/pull/29688). Fixes [#29687](https://github.com/cypress-io/cypress/issues/29687). - Elements whose parent elements has `overflow: clip` and no height/width will now correctly show as hidden. Fixed in [#29778](https://github.com/cypress-io/cypress/pull/29778). Fixes [#23852](https://github.com/cypress-io/cypress/issues/23852). @@ -96,7 +97,6 @@ _Released 11/5/2024_ - Updated `mobx` from `5.15.4` to `6.13.5` and `mobx-react` from `6.1.8` to `9.1.1`. Addresses [#30509](https://github.com/cypress-io/cypress/issues/30509). - Updated `@cypress/request` from `3.0.4` to `3.0.6`. Addressed in [#30488](https://github.com/cypress-io/cypress/pull/30488). - ## 13.15.1 _Released 10/24/2024_ diff --git a/packages/driver/cypress/e2e/dom/visibility.cy.ts b/packages/driver/cypress/e2e/dom/visibility.cy.ts index 78ae396ac175..b27c17ddd294 100644 --- a/packages/driver/cypress/e2e/dom/visibility.cy.ts +++ b/packages/driver/cypress/e2e/dom/visibility.cy.ts @@ -176,7 +176,7 @@ describe('src/cypress/dom/visibility', () => { context('hidden/visible overrides', () => { beforeEach(function () { // ensure all tests run against a scrollable window - const scrollThisIntoView = add('
Should be in view
') + const scrollThisIntoView = add('
Should be in view
') this.$visHidden = add('') this.$parentVisHidden = add('') @@ -997,6 +997,56 @@ describe('src/cypress/dom/visibility', () => { it('is visible when parent is relatively positioned out of bounds but el is relatively positioned back in bounds', function () { expect(this.$parentOutOfBoundsButElInBounds.find('span')).to.be.visible }) + + it('is visible when element is statically positioned and parent element is absolutely positioned and ancestor has overflow hidden', function () { + const add = (el) => { + return $(el).appendTo(cy.$$('body')) + } + + cy.$$('body').empty() + + const el = add(` +
+
+
+ +
+
+
+ `) + + expect(el.find('#visible-button')).to.be.visible + }) + + it('is visible when element is relatively positioned and parent element is absolutely positioned and ancestor has overflow auto', function () { + const add = (el) => { + return $(el).appendTo(cy.$$('body')) + } + + cy.$$('body').empty() + + const el = add(` +
+
+
+
+

Example

+
+ +
+
+
+
+
+ `) + + expect(el.find('#visible-button')).to.be.visible + }) }) describe('css clip-path', () => { diff --git a/packages/driver/cypress/e2e/dom/visibility_shadow_dom.cy.ts b/packages/driver/cypress/e2e/dom/visibility_shadow_dom.cy.ts index 18e353ee1691..974a909d57de 100644 --- a/packages/driver/cypress/e2e/dom/visibility_shadow_dom.cy.ts +++ b/packages/driver/cypress/e2e/dom/visibility_shadow_dom.cy.ts @@ -3,7 +3,7 @@ export {} // make typescript see this as a module const { $ } = Cypress describe('src/cypress/dom/visibility - shadow dom', () => { - let add + let add: (el: string, shadowEl: string, rootIdentifier: string) => JQuery beforeEach(() => { cy.visit('/fixtures/empty.html').then((win) => { @@ -11,7 +11,6 @@ describe('src/cypress/dom/visibility - shadow dom', () => { constructor () { super() - // @ts-ignore this.attachShadow({ mode: 'open' }) this.style.display = 'block' } @@ -20,8 +19,7 @@ describe('src/cypress/dom/visibility - shadow dom', () => { add = (el, shadowEl, rootIdentifier) => { const $el = $(el).appendTo(cy.$$('body')) - // @ts-ignore - $(shadowEl).appendTo(cy.$$(rootIdentifier)[0].shadowRoot) + $(shadowEl).appendTo(cy.$$(rootIdentifier)[0].shadowRoot!) return $el } @@ -574,6 +572,48 @@ describe('src/cypress/dom/visibility - shadow dom', () => { cy.wrap($outsideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('be.visible') cy.wrap($outsideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') }) + + it('is visible when element is statically positioned and parent element is absolutely positioned and ancestor has overflow hidden', function () { + const el = add( + `
+
+ +
+
`, + `
+ +
`, + '#shadow', + ) + + cy.wrap(el).find('#visible-button', { includeShadowDom: true }).should('be.visible') + cy.wrap(el).find('#visible-button', { includeShadowDom: true }).should('not.be.hidden') + }) + + it('is visible when element is relatively positioned and parent element is absolutely positioned and ancestor has overflow auto', function () { + const el = add( + `
+
+
+
+

Example

+ +
+
+
+
`, + `
+ +
`, + '#shadow', + ) + + cy.wrap(el).find('#visible-button', { includeShadowDom: true }).should('be.visible') + cy.wrap(el).find('#visible-button', { includeShadowDom: true }).should('not.be.hidden') + }) }) describe('css transform', () => { diff --git a/packages/driver/src/dom/coordinates.ts b/packages/driver/src/dom/coordinates.ts index 976f6665e9ef..670e8da325d7 100644 --- a/packages/driver/src/dom/coordinates.ts +++ b/packages/driver/src/dom/coordinates.ts @@ -3,7 +3,7 @@ import $window from './window' import $elements from './elements' import $jquery from './jquery' -const getElementAtPointFromViewport = (doc, x, y) => { +const getElementAtPointFromViewport = (doc: Document, x: number, y: number) => { return $elements.elementFromPoint(doc, x, y) } diff --git a/packages/driver/src/dom/elements/find.ts b/packages/driver/src/dom/elements/find.ts index 16f95fa2017a..caceed6d7295 100644 --- a/packages/driver/src/dom/elements/find.ts +++ b/packages/driver/src/dom/elements/find.ts @@ -171,11 +171,11 @@ export const elementFromPoint = (doc, x, y): HTMLElement => { * By DOM Hierarchy * Compares two elements to see what their relationship is */ -export const isAncestor = ($el, $maybeAncestor) => { +export const isAncestor = ($el: JQuery, $maybeAncestor: JQuery) => { return $jquery.wrap(getAllParents($el[0])).index($maybeAncestor) >= 0 } -export const isChild = ($el, $maybeChild) => { +export const isChild = ($el: JQuery, $maybeChild: JQuery) => { let children = $el.children() if (children.length && children[0].nodeName === 'SHADOW-ROOT') { @@ -185,7 +185,7 @@ export const isChild = ($el, $maybeChild) => { return children.index($maybeChild) >= 0 } -export const isDescendent = ($el1, $el2) => { +export const isDescendent = ($el1: JQuery, $el2?: JQuery) => { if (!$el2) { return false } @@ -328,7 +328,7 @@ export const getContainsSelector = (text, filter = '', options: { return selectors.join() } -export const getInputFromLabel = ($el) => { +export const getInputFromLabel = ($el: JQuery) => { if (!$el.is('label')) { return $([]) } diff --git a/packages/driver/src/dom/visibility.ts b/packages/driver/src/dom/visibility.ts index a887981fa897..f9373dc6d75a 100644 --- a/packages/driver/src/dom/visibility.ts +++ b/packages/driver/src/dom/visibility.ts @@ -206,11 +206,15 @@ const elHasOverflowHidden = function ($el) { return cssOverflow.includes('hidden') } -const elHasPositionRelative = ($el) => { +const elHasPositionRelative = ($el: JQuery) => { return $el.css('position') === 'relative' } -const elHasPositionAbsolute = ($el) => { +const elHasPositionStatic = ($el: JQuery) => { + return $el.css('position') == null || $el.css('position') === 'static' +} + +const elHasPositionAbsolute = ($el: JQuery) => { return $el.css('position') === 'absolute' } @@ -220,13 +224,12 @@ const elHasClippableOverflow = function ($el) { OVERFLOW_PROPS.includes($el.css('overflow-x')) } -const canClipContent = function ($el, $ancestor) { +const canClipContent = function ($el: JQuery, $ancestor: JQuery) { // can't clip without overflow properties if (!elHasClippableOverflow($ancestor)) { return false } - // fix for 29605 - display: contents if (elHasDisplayContents($ancestor)) { return false } @@ -248,11 +251,21 @@ const canClipContent = function ($el, $ancestor) { // even if ancestors' overflow is clippable, if the element's offset parent // is a child of the ancestor, the ancestor will not clip the element - // unless the ancestor has position absolute + // unless the ancestor has a position that is not absolute if (elHasPositionAbsolute($offsetParent) && isChild($ancestor, $offsetParent)) { return false } + // even if ancestors' overflow is clippable, + // if the element is position static or relative, + // and the element's offset parent is positioned absolute, a descendent of the ancestor, and has no clippable overflow, + // then the ancestor will not clip the element + if ((elHasPositionStatic($el) || elHasPositionRelative($el)) + && elHasPositionAbsolute($offsetParent) && isDescendent($ancestor, $offsetParent) && !elHasClippableOverflow($offsetParent) + ) { + return false + } + return true } @@ -266,8 +279,7 @@ export const isW3CFocusable = (el) => { return isFocusable(wrap(el)) && isW3CRendered(el) } -// @ts-ignore -const elAtCenterPoint = function ($el) { +const elAtCenterPoint = function ($el: JQuery) { const doc = $document.getDocumentFromElement($el.get(0)) const elProps = $coordinates.getElementPositioning($el) @@ -278,6 +290,8 @@ const elAtCenterPoint = function ($el) { if (el) { return $jquery.wrap(el) } + + return undefined } const elDescendentsHavePositionFixedOrAbsolute = function ($parent, $child) { @@ -298,7 +312,7 @@ const elHasVisibleChild = function ($el) { }) } -const elIsNotElementFromPoint = function ($el) { +const elIsNotElementFromPoint = function ($el: JQuery) { // 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 @@ -333,7 +347,6 @@ const elIsOutOfBoundsOfAncestorsOverflow = function ($el: JQuery, $ancestor return false } - // fix for 29605 - display: contents if (elHasDisplayContents($el)) { return false } @@ -400,8 +413,7 @@ const elIsHiddenByAncestors = function ($el, checkOpacity, $origEl = $el) { } if (elHasOverflowHidden($parent) && !elHasDisplayContents($parent) && elHasNoEffectiveWidthOrHeight($parent)) { - // if any of the elements between the parent and origEl - // have fixed or position absolute + // if any of the elements between the parent and origEl have fixed or position absolute return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl) } @@ -564,7 +576,6 @@ export const getReasonIsHidden = function ($el, options = { checkOpacity: true } 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.` } - // nested else --___________-- if (elOrAncestorIsFixedOrSticky($el)) { if (elIsNotElementFromPoint($el)) { // show the long element here