Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #29605 for display: contents parent visibility with correct width check #29680

Open
wants to merge 31 commits into
base: release/14.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
61e8cfe
forst working version
senpl Jun 14, 2024
dc0991c
Merge branch 'develop' into issue-29605v2
senpl Jun 14, 2024
0ce64cb
remove visibilityVisible as non essencial
senpl Jun 14, 2024
8158429
Merge branch 'issue-29605v2' of https://github.com/senpl/cypress into…
senpl Jun 14, 2024
7b10b97
Merge branch 'develop' into issue-29605v2
jennifer-shehane Jun 18, 2024
a654985
Merge branch 'develop' into issue-29605v2
jennifer-shehane Jun 26, 2024
eb2a3f9
changelog for pipeline fix
senpl Jun 27, 2024
4ecb344
Merge branch 'develop' into issue-29605v2
senpl Jun 27, 2024
3dec565
fix only for firefox. It works in other browsers without this change
senpl Jun 28, 2024
093cd2c
Merge branch 'issue-29605v2' of https://github.com/senpl/cypress into…
senpl Jun 28, 2024
386b837
Merge branch 'develop' into issue-29605v2
senpl Jun 28, 2024
53bd9c1
Merge branch 'develop' into issue-29605v2
senpl Jul 1, 2024
0534c6d
Merge branch 'develop' into issue-29605v2
senpl Jul 2, 2024
eb3126f
Merge branch 'develop' into issue-29605v2
senpl Jul 3, 2024
a8abbb1
changelog update for pipeline
senpl Jul 3, 2024
2f6e02e
pipeline fix
senpl Jul 3, 2024
6e6d5c5
Merge branch 'develop' into issue-29605v2
senpl Jul 8, 2024
bb043a0
Merge branch 'develop' into issue-29605v2
senpl Jul 9, 2024
65ace77
return of previous spacing
senpl Jul 9, 2024
68050be
Merge branch 'issue-29605v2' of https://github.com/senpl/cypress into…
senpl Jul 9, 2024
d826d28
Merge branch 'develop' into issue-29605v2
senpl Jul 12, 2024
64e0dcd
Merge branch 'develop' into issue-29605v2
senpl Jul 25, 2024
26493c3
Update CHANGELOG.md
senpl Jul 25, 2024
e6da2e1
Merge branch 'develop' into issue-29605v2
senpl Jul 26, 2024
4db745f
Merge branch 'develop' into issue-29605v2
senpl Aug 7, 2024
f940487
Merge branch 'develop' into issue-29605v2
senpl Aug 8, 2024
0b04205
Merge branch 'develop' into issue-29605v2
senpl Aug 16, 2024
a3fe83e
Merge branch 'develop' into issue-29605v2
senpl Sep 30, 2024
4772063
changelog without unnecessery changes
senpl Sep 30, 2024
01a1609
Merge branch 'release/14.0.0' into issue-29605v2
senpl Oct 1, 2024
8c37855
Merge branch 'release/14.0.0' into issue-29605v2
senpl Oct 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ _Released 7/02/2024 (PENDING)_

- Fixed an issue where Chrome launch instances would not recreate the browser CRI client correctly after recovering from an unexpected browser closure. Fixes [#27657](https://github.com/cypress-io/cypress/issues/27657). Fixed in [#29663](https://github.com/cypress-io/cypress/pull/29663).
- Fixed an issue where Firefox 129 (Firefox Nightly) would not launch with Cypress. Fixes [#29713](https://github.com/cypress-io/cypress/issues/29713). Fixed in [#29720](https://github.com/cypress-io/cypress/pull/29720).
- Fixed an issue where Cypress incorrectly uses the box model of elements that have display:contents when determining visibility of child elements. Fixes [#29605](https://github.com/cypress-io/cypress/issues/29605).

**Dependency Updates:**

Expand Down
13 changes: 13 additions & 0 deletions packages/driver/cypress/e2e/issues/29605.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// https://github.com/cypress-io/cypress/issues/29093
describe('issue #29605', () => {
before(() => {
cy
.viewport('macbook-16')
.visit('/fixtures/issue-29605.html')
})

it('can click selection when display: contents width used', () => {
// cy.
cy.get('#child').should('be.visible')
})
})
33 changes: 33 additions & 0 deletions packages/driver/cypress/fixtures/issue-29605.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en" class="dark">

<head>
<meta charset="utf-8" />
<title>29093 repro</title>
<style id="compiled-css" type="text/css">
.big {
font-size: 128px;
}

.contents {
display: contents
}

.overflow-hidden {
overflow: hidden
}

@media (min-width: 1024px) {
.lg\:grid {
display: grid
}
}
</style>
</head>

<body>
<div id="parent" style="overflow: hidden; display: contents;">
<div id="child" style="height:200px; width: 200px; background: #f00"></div>
</div>
</body>
</html>
101 changes: 63 additions & 38 deletions packages/driver/src/dom/visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -121,32 +121,38 @@ 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.

// $el[0].getClientRects().length:
// For HTML <area> 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)
}

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')
Expand All @@ -157,17 +163,15 @@ 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 elOffsetHeight = ($el) => {
return $el[0].offsetHeight
}
const elClientHeight = ($el) => elementBoundingRect($el).height

const elClientWidth = ($el) => elementBoundingRect($el).width

const elHasVisibilityHiddenOrCollapse = ($el) => {
return elHasVisibilityHidden($el) || elHasVisibilityCollapse($el)
Expand All @@ -185,6 +189,10 @@ const elHasOpacityZero = ($el) => {
return $el.css('opacity') === '0'
}

const elHasDisplayContents = ($el) => {
return $el.css('display') === 'contents'
}

const elHasDisplayNone = ($el) => {
return $el.css('display') === 'none'
}
Expand Down Expand Up @@ -219,6 +227,11 @@ const canClipContent = function ($el, $ancestor) {
return false
}

// fix for 29605 - display: contents
if (elHasDisplayContents($ancestor)) {
return false
}

// the closest parent with position relative, absolute, or fixed
const $offsetParent = $el.offsetParent()

Expand Down Expand Up @@ -308,35 +321,41 @@ const elIsNotElementFromPoint = function ($el) {
return true
}

const elIsOutOfBoundsOfAncestorsOverflow = function ($el, $ancestor = getParent($el)) {
const elIsOutOfBoundsOfAncestorsOverflow = function ($el: JQuery<any>, $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
if (isUndefinedOrHTMLBodyDoc($ancestor)) {
return false
}

// fix for 29605 - display: contents
if (elHasDisplayContents($el)) {
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
}
Expand All @@ -348,7 +367,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------
Expand All @@ -363,14 +382,20 @@ const elIsHiddenByAncestors = function ($el, checkOpacity, $origEl = $el) {
return false
}

if (elHasDisplayContents($el)) {
let $parent = getParent($el)

return elIsHiddenByAncestors($parent, checkOpacity, $parent)
}

// a child can never have a computed opacity
// greater than that of its parent
// so if the parent has an opacity of 0, so does the child
if (elHasOpacityZero($parent) && checkOpacity) {
return true
}

if (elHasOverflowHidden($parent) && elHasNoEffectiveWidthOrHeight($parent)) {
if (elHasOverflowHidden($parent) && !elHasDisplayContents($parent) && elHasNoEffectiveWidthOrHeight($parent)) {
// if any of the elements between the parent and origEl
// have fixed or position absolute
return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl)
Expand All @@ -380,7 +405,7 @@ const elIsHiddenByAncestors = function ($el, checkOpacity, $origEl = $el) {
return elIsHiddenByAncestors($parent, checkOpacity, $origEl)
}

const parentHasNoOffsetWidthOrHeightAndOverflowHidden = function ($el) {
const parentHasNoClientWidthOrHeightAndOverflowHidden = function ($el: JQuery<HTMLElement>) {
// if we've walked all the way up to body or html then return false
if (isUndefinedOrHTMLBodyDoc($el)) {
return false
Expand All @@ -392,7 +417,7 @@ const parentHasNoOffsetWidthOrHeightAndOverflowHidden = function ($el) {
}

// continue walking
return parentHasNoOffsetWidthOrHeightAndOverflowHidden(getParent($el))
return parentHasNoClientWidthOrHeightAndOverflowHidden(getParent($el))
}

const parentHasDisplayNone = function ($el) {
Expand Down Expand Up @@ -463,8 +488,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

Expand Down Expand Up @@ -513,24 +538,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)
let width = elClientWidth($parent)
let 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.`
}
Expand Down