diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index b1050db6ef47..d3f5e1eb9e8e 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -13,6 +13,7 @@ _Released 6/4/2024 (PENDING)_ **Bugfixes:** +- Fixed an issue where Cypress did not detect visible elements with width or height in rem as visible. Fixes [#29224](https://github.com/cypress-io/cypress/issues/29093) and [#28638](https://github.com/cypress-io/cypress/issues/28638). - Fixed a situation where the Launchpad would hang if the project config had not been loaded when the Launchpad first queries the current project. Fixes [#29486](https://github.com/cypress-io/cypress/issues/29486). - Pre-emptively fix behavior with Chrome for when `unload` events are forcefully deprecated by using `pagehide` as a proxy. Fixes [#29241](https://github.com/cypress-io/cypress/issues/29241). diff --git a/packages/driver/cypress/e2e/dom/visibility.cy.ts b/packages/driver/cypress/e2e/dom/visibility.cy.ts index 5dc7495afc30..e2c352430973 100644 --- a/packages/driver/cypress/e2e/dom/visibility.cy.ts +++ b/packages/driver/cypress/e2e/dom/visibility.cy.ts @@ -247,7 +247,14 @@ describe('src/cypress/dom/visibility', () => { this.$parentNoWidth = add(`\
- parent width: 0 + parent width: 0 +
+
`) + + this.$parentOfDivNoWidth = add(`\ +
+
+
parent width: 0
`) @@ -368,7 +375,7 @@ describe('src/cypress/dom/visibility', () => { this.$elOutOfParentBoundsAbove = add(`\
- position: absolute, out of bounds above + position: absolute, out of bounds above
\ `) @@ -733,8 +740,13 @@ describe('src/cypress/dom/visibility', () => { }) it('is hidden if parent has overflow: hidden and no width', function () { - expect(this.$parentNoWidth.find('span')).to.be.hidden - expect(this.$parentNoWidth.find('span')).to.not.be.visible + expect(this.$parentNoWidth.find('#parentNoWidth')).to.be.hidden + expect(this.$parentNoWidth.find('#parentNoWidth')).to.not.be.visible + }) + + it('is hidden if parent has overflow: hidden and no width', function () { + expect(this.$parentOfDivNoWidth.find('#parentOfDivNoWidth')).to.be.hidden + expect(this.$parentOfDivNoWidth.find('#parentOfDivNoWidth')).to.not.be.visible }) it('is hidden if parent has overflow: hidden and no height', function () { @@ -833,7 +845,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('#outOfParentBoundsAbove'), `expected '' to be 'hidden'`).to.be.hidden }) it('is hidden when parent overflow hidden and out of bounds below', function () { diff --git a/packages/driver/cypress/e2e/issues/29093.cy.js b/packages/driver/cypress/e2e/issues/29093.cy.js new file mode 100644 index 000000000000..7f62c965f0d1 --- /dev/null +++ b/packages/driver/cypress/e2e/issues/29093.cy.js @@ -0,0 +1,12 @@ +// https://github.com/cypress-io/cypress/issues/29093 +describe('issue 29093', () => { + before(() => { + cy + .viewport('macbook-16') + .visit('/fixtures/issue-29093.html') + }) + + it('can click selection when rem width used', () => { + cy.get('#sidebar-left > section').click() + }) +}) diff --git a/packages/driver/cypress/fixtures/issue-29093.html b/packages/driver/cypress/fixtures/issue-29093.html new file mode 100644 index 000000000000..9a09bcaca713 --- /dev/null +++ b/packages/driver/cypress/fixtures/issue-29093.html @@ -0,0 +1,37 @@ + + + + + + 29093 repro + + + + +
+ +
+ + \ No newline at end of file diff --git a/packages/driver/src/dom/visibility.ts b/packages/driver/src/dom/visibility.ts index 2a92fae2b963..bcc1e541bfa5 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' } @@ -189,6 +201,10 @@ const elHasDisplayNone = ($el) => { return $el.css('display') === 'none' } +const elHasDisplayContents = ($el) => { + return $el.css('display') === 'contents' +} + const elHasDisplayInline = ($el) => { return $el.css('display') === 'inline' } @@ -219,13 +235,24 @@ const canClipContent = function ($el, $ancestor) { return false } + // fix for 29093 + if (elHasDisplayContents($ancestor)) { + return false + } + // 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 +335,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 +343,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 +380,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 +408,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 +428,7 @@ const parentHasNoOffsetWidthOrHeightAndOverflowHidden = function ($el) { } // continue walking - return parentHasNoOffsetWidthOrHeightAndOverflowHidden(getParent($el)) + return parentHasNoClientWidthOrHeightAndOverflowHidden(getParent($el)) } const parentHasDisplayNone = function ($el) { @@ -463,8 +499,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 +549,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.` } diff --git a/packages/launchpad/package.json b/packages/launchpad/package.json index 8feebc04a179..2ddc6a4b55e5 100644 --- a/packages/launchpad/package.json +++ b/packages/launchpad/package.json @@ -57,6 +57,7 @@ "gravatar": "1.8.0", "javascript-time-ago": "2.3.8", "markdown-it": "13.0.1", + "mocha": "10.4.0", "rollup-plugin-polyfill-node": "^0.7.0", "type-fest": "^2.3.4", "unplugin-vue-components": "0.24.1", diff --git a/packages/launchpad/src/components/code/FileRow.cy.tsx b/packages/launchpad/src/components/code/FileRow.cy.tsx index 660cb11180f6..8640adff41d4 100644 --- a/packages/launchpad/src/components/code/FileRow.cy.tsx +++ b/packages/launchpad/src/components/code/FileRow.cy.tsx @@ -117,10 +117,10 @@ describe('FileRow', () => { cy.contains(changesRequiredDescription).should('be.visible') cy.get('pre').should('have.length', 2) - cy.get('.shiki').should('be.visible') + cy.get('div.rounded > div.text-left.cursor-text').should('be.visible') cy.contains('cypress/integration/command.js').click() - cy.get('.shiki').should('not.be.visible') + cy.get('div.rounded > div.text-left.cursor-text').should('not.exist') }) it('responds nice to small screens', { viewportWidth: 500 }, () => {