diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 5eaf04b31f14..fc3953d5f6b4 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -30,9 +30,7 @@ mainBuildFilters: &mainBuildFilters - /^release\/\d+\.\d+\.\d+$/ # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - 'update-v8-snapshot-cache-on-develop' - - 'chore/update_vue_test_utils' - - 'publish-binary' - - 'chore/fix_windows_kitchensink' + - 'mschile/issue-30922' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -43,8 +41,7 @@ macWorkflowFilters: &darwin-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'chore/update_vue_test_utils', << pipeline.git.branch >> ] - - equal: [ 'cacie/fix-hook-test-stack-analysis', << pipeline.git.branch >> ] + - equal: [ 'mschile/issue-30922', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -55,8 +52,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'chore/update_binary_branch', << pipeline.git.branch >> ] - - equal: [ 'cacie/fix-hook-test-stack-analysis', << pipeline.git.branch >> ] + - equal: [ 'mschile/issue-30922', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -79,8 +75,7 @@ windowsWorkflowFilters: &windows-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'ryanm/chore/electron-33-upgrade', << pipeline.git.branch >> ] - - equal: [ 'chore/fix_windows_kitchensink', << pipeline.git.branch >> ] + - equal: [ 'mschile/issue-30922', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -156,7 +151,7 @@ commands: name: Set environment variable to determine whether or not to persist artifacts command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "chore/update_vue_test_utils" && "$CIRCLE_BRANCH" != chore/fix_windows_kitchensink ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "mschile/issue-30922" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" # You must run `setup_should_persist_artifacts` command and be using bash before running this command diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 73d348db324e..7a16a2fc946c 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -6,6 +6,9 @@ _Released 1/28/2025 (PENDING)_ **Bugfixes:** - Fixed an issue where Cypress would incorrectly navigate to `about:blank` when test isolation was disabled and the last test would fail and then retry. Fixes [#28527](https://github.com/cypress-io/cypress/issues/28527). +- Fixed a regression introduced in [`14.0.0`](https://docs.cypress.io/guides/references/changelog#14-0-0) where an element would not return the correct visibility if its offset parent was within the clipping element. Fixes [#30922](https://github.com/cypress-io/cypress/issues/30922). +- Fixed a regression introduced in [`14.0.0`](https://docs.cypress.io/guides/references/changelog#14-0-0) where the incorrect visiblity would be returned when either `overflow-x` or `overflow-y` was visible but the other one was clipping. Fixed in [#30934](https://github.com/cypress-io/cypress/pull/30934). +- Fixed an issue where an `option` element would not return the correct visibility if its parent element has a clipping overflow. Fixed in [#30934](https://github.com/cypress-io/cypress/pull/30934). - Fixed an issue where non-HTMLElement(s) may fail during assertions. Fixes [#30944](https://github.com/cypress-io/cypress/issues/30944) **Misc:** diff --git a/packages/driver/cypress/e2e/dom/visibility.cy.ts b/packages/driver/cypress/e2e/dom/visibility.cy.ts index 50f7a86f7708..43c7e31bbbfb 100644 --- a/packages/driver/cypress/e2e/dom/visibility.cy.ts +++ b/packages/driver/cypress/e2e/dom/visibility.cy.ts @@ -6,7 +6,7 @@ describe('src/cypress/dom/visibility', () => { return $(el).appendTo(cy.$$('body')) } - const reasonIs = ($el, str) => { + const reasonIs = ($el: JQuery, str: string) => { expect(dom.getReasonIsHidden($el)).to.eq(str) } @@ -995,10 +995,6 @@ describe('src/cypress/dom/visibility', () => { }) 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(` @@ -1015,10 +1011,6 @@ describe('src/cypress/dom/visibility', () => { }) 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(` @@ -1043,6 +1035,185 @@ describe('src/cypress/dom/visibility', () => { expect(el.find('#visible-button')).to.be.visible }) + + it('is hidden when parent element is absolutely position and offset parent is a decendent of the ancestor', function () { + cy.$$('body').empty() + + add(` +
+
+
+
+ test test-1 +
+
+ test test-2 +
+
+
+
+ `) + + cy.contains('test-2').should('not.be.visible') + cy.contains('test-1').should('be.visible') + }) + + it('is hidden when element is an option and the parent has overflow clip', function () { + cy.$$('body').empty() + + add(` +
+
+ +
+ `) + + cy.get('option').should('not.be.visible').then(($el) => { + reasonIs($el, 'This element `` is not visible because its content is being clipped by one of its parent elements, which has a CSS property of overflow: `hidden`, `clip`, `scroll` or `auto`') + }) + + cy.get('select').should('not.be.visible').then(($el) => { + reasonIs($el, 'This element ` + + + + + + `) + + cy.get('option').should('be.visible') + cy.get('optgroup').should('be.visible') + cy.get('select').should('be.visible') + }) + + it('is visible when x direction is clip but element is visible in y direction', () => { + cy.$$('body').empty() + + add(` +
+
+
+ + +
+
+ `) + + cy.get('label').should('be.visible') + }) + + it('is hidden when x direction is hidden and y direction is coerced by browser to auto', () => { + cy.$$('body').empty() + + add(` +
+
+
+ + +
+
+ `) + + cy.get('label').should('not.be.visible') + }) + + it('is hidden when x direction is auto and y direction is coerced by browser to auto', () => { + cy.$$('body').empty() + + add(` +
+
+
+ + +
+
+ `) + + cy.get('label').should('not.be.visible') + }) + + it('is hidden when y direction is hidden and x direction is set to clip but coerced by browser to hidden', () => { + cy.$$('body').empty() + + add(` +
+
+
+ + +
+
+ `) + + cy.get('label').should('not.be.visible') + }) + + it('is hidden when y direction is auto and x direction is set to clip but coerced by browser to hidden', () => { + cy.$$('body').empty() + + add(` +
+
+
+ + +
+
+ `) + + cy.get('label').should('not.be.visible') + }) + + it('is visible when x direction is clip and y direction is visible', () => { + cy.$$('body').empty() + + add(` +
+
+
+ + +
+
+ `) + + cy.get('label').should('be.visible') + }) + + it('is hidden when y direction is overriden by setting overflow to clip', () => { + cy.$$('body').empty() + + add(` +
+
+
+ + +
+
+ `) + + cy.get('label').should('not.be.visible') + }) }) describe('css clip-path', () => { diff --git a/packages/driver/src/dom/elements/find.ts b/packages/driver/src/dom/elements/find.ts index caceed6d7295..e9366f3794b0 100644 --- a/packages/driver/src/dom/elements/find.ts +++ b/packages/driver/src/dom/elements/find.ts @@ -61,7 +61,7 @@ export const findParent = (el, condition) => { return collectParent(el) } -export const getFirstParentWithTagName = ($el, tagName) => { +export const getFirstParentWithTagName = ($el: JQuery, tagName: string) => { if (isUndefinedOrHTMLBodyDoc($el) || !tagName) { return null } diff --git a/packages/driver/src/dom/visibility.ts b/packages/driver/src/dom/visibility.ts index a2f2246d3f40..abbf84762596 100644 --- a/packages/driver/src/dom/visibility.ts +++ b/packages/driver/src/dom/visibility.ts @@ -5,7 +5,7 @@ import $elements from './elements' import $coordinates from './coordinates' import * as $transform from './transform' -const { isElement, isSelect, isBody, isHTML, isOption, isOptgroup, getParent, getFirstParentWithTagName, isAncestor, isChild, getAllParents, isDescendent, isUndefinedOrHTMLBodyDoc, elOrAncestorIsFixedOrSticky, isDetached, isFocusable, stringify: stringifyElement } = $elements +const { isElement, isBody, isHTML, isOption, isOptgroup, getParent, getFirstParentWithTagName, isAncestor, isChild, getAllParents, isDescendent, isUndefinedOrHTMLBodyDoc, elOrAncestorIsFixedOrSticky, isDetached, isFocusable, stringify: stringifyElement } = $elements const fixedOrAbsoluteRe = /(fixed|absolute)/ @@ -36,6 +36,18 @@ const ensureEl = (el, methodName) => { } } +const getFirstSelectParentFromEl = ($el: JQuery) => { + const $select = getFirstParentWithTagName($el, 'select') + + // check $select.length here first + // they may have not put the option into a select el + if ($select?.length) { + return $select + } + + return null +} + const isStrictlyHidden = (el: HTMLElement, methodName = 'isStrictlyHidden()', options = { checkOpacity: true }, recurse?) => { ensureEl(el, methodName) const $el = $jquery.wrap(el) @@ -53,13 +65,9 @@ const isStrictlyHidden = (el: HTMLElement, methodName = 'isStrictlyHidden()', op } // if its parent select is visible, then it's not hidden - const $select = getFirstParentWithTagName($el, 'select') + const $select = getFirstSelectParentFromEl($el) - // check $select.length here first - // they may have not put the option into a select el, - // in which case it will fall through to regular visibility logic - if ($select && $select.length) { - // if the select is hidden, the options in it are hidden too + if ($select) { return recurse ? recurse($select[0], methodName, options) : isStrictlyHidden($select[0], methodName, options) } } @@ -102,6 +110,15 @@ const isHiddenByAncestors = (el, methodName = 'isHiddenByAncestors()', options = ensureEl(el, methodName) const $el = $jquery.wrap(el) + // an option is considered hidden by ancestors if its parent select is hidden + if (isOption(el) || isOptgroup(el)) { + const $select = getFirstSelectParentFromEl($el) + + if ($select) { + return isHiddenByAncestors($select[0], methodName, options) + } + } + // we do some calculations taking into account the parents // to see if its hidden by a parent if (elIsHiddenByAncestors($el, options.checkOpacity)) { @@ -232,11 +249,6 @@ const canClipContent = function ($el: JQuery, $ancestor: JQuery, $ancestor: JQuery, $ancestor const elProps = $el.get(0).getBoundingClientRect() + // only check if the target el is out of bounds if the overflow is clippable in that direction + const checkXOverflow = OVERFLOW_PROPS.includes($ancestor.css('overflow-x')) + const checkYOverflow = OVERFLOW_PROPS.includes($ancestor.css('overflow-y')) + // target el is out of bounds if ( // target el is to the right of the ancestor's visible area - (elProps.left >= (ancestorProps.width + ancestorProps.left)) || + (checkXOverflow && (elProps.left >= (ancestorProps.width + ancestorProps.left))) || // target el is to the left of the ancestor's visible area - ((elProps.left + elProps.width) <= ancestorProps.left) || + (checkXOverflow && ((elProps.left + elProps.width) <= ancestorProps.left)) || // target el is under the ancestor's visible area - (elProps.top >= (ancestorProps.height + ancestorProps.top)) || + (checkYOverflow && (elProps.top >= (ancestorProps.height + ancestorProps.top))) || // target el is above the ancestor's visible area - ((elProps.top + elProps.height) <= ancestorProps.top) + (checkYOverflow && ((elProps.top + elProps.height) <= ancestorProps.top)) ) { return true } @@ -506,6 +526,13 @@ export const getReasonIsHidden = function ($el, options = { checkOpacity: true } let height = elClientHeight($el) let $parent let parentNode + let $select + + // if the element is an option or optgroup then we need to get the + // select so it can be used when determining the hidden reason + if (isOption($el[0]) || isOptgroup($el[0])) { + $select = getFirstSelectParentFromEl($el) + } // returns the reason in human terms why an element is considered not visible if (elHasDisplayNone($el)) { @@ -558,7 +585,7 @@ export const getReasonIsHidden = function ($el, options = { checkOpacity: true } return `This element \`${node}\` is not visible because it is hidden by transform.` } - if (elHasNoClientWidthOrHeight($el)) { + if (elHasNoClientWidthOrHeight($select || $el)) { return `This element \`${node}\` is not visible because it has an effective width and height of: \`${width} x ${height}\` pixels.` }