diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 7f049a8dc3f..08002327800 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -3193,6 +3193,11 @@ declare namespace Cypress { * @default false */ experimentalMemoryManagement: boolean + /** + * Enables an alternative, performance-optimized visibility algorithm. + * @default false + */ + experimentalFastVisibility: boolean /** * Allows for just-in-time compiling of a component test, which will only compile assets related to the component. * This results in a smaller bundle under test, reducing resource constraints on a given machine. This option is recommended @@ -3284,6 +3289,7 @@ declare namespace Cypress { * @default false */ experimentalPromptCommand?: boolean + } /** @@ -3365,14 +3371,14 @@ declare namespace Cypress { } interface SuiteConfigOverrides extends Partial< - Pick + Pick >, Partial> { browser?: IsBrowserMatcher | IsBrowserMatcher[] keystrokeDelay?: number } interface TestConfigOverrides extends Partial< - Pick + Pick >, Partial> { browser?: IsBrowserMatcher | IsBrowserMatcher[] keystrokeDelay?: number diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index 44521ba45ee..7b7fd62660c 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -253,6 +253,12 @@ const driverConfigOptions: Array = [ validation: validate.isBoolean, isExperimental: true, requireRestartOnChange: 'server', + }, { + name: 'experimentalFastVisibility', + defaultValue: false, + validation: validate.isBoolean, + isExperimental: true, + overrideLevel: 'any', }, { name: 'fileServerFolder', defaultValue: '', diff --git a/packages/driver/cypress/e2e/dom/visibility.cy.ts b/packages/driver/cypress/e2e/dom/visibility.cy.ts index 43c7e31bbbf..aed605f77f1 100644 --- a/packages/driver/cypress/e2e/dom/visibility.cy.ts +++ b/packages/driver/cypress/e2e/dom/visibility.cy.ts @@ -1,117 +1,196 @@ // @ts-ignore const { $, dom } = Cypress -describe('src/cypress/dom/visibility', () => { - const add = (el) => { - return $(el).appendTo(cy.$$('body')) +/** + * Most of the tests in here are declarative. Due to the range of visibility scenarios, test case + * understandability is paramount. These test cases are defined declaratively *in the fixture html + * file, and are iterated upon with the helper functions in the `visibility scenarios` describe block. + * + * CSS for test cases are inlined into the DOM purposefully, to ensure easy reference and debugging. + * + * Test case elements that are expected to be visible typically have a `lightgreen` background color, + * while test case elements that are expected to be hidden typically have a `lightcoral` background color. + * This helps to quickly identify if a given element's assertions are in-line with browser behavior. + * + * A test case where the visibility behavior is expected to be identical between the legacy and fast + * algorithms: + * + *
+ *
+ * + * A test case where the visibility behavior is expected to be different between the legacy and fast + * algorithms (note the css of the example does not accurately reflect this visibility behavior): + * + *
+ *
+ */ + +describe('src/cypress/dom/visibility', { + slowTestThreshold: 500, +}, () => { + function assertVisibilityForEl (el: HTMLElement) { + // once experimentalFastVisibility is added, switch based on the config value + // and use `cy-fast-expect` instead of `cy-legacy-expect` when it is enabled. + const expected = el.getAttribute('cy-expect') ?? (Cypress.config('experimentalFastVisibility') ? + el.getAttribute('cy-fast-expect') : + el.getAttribute('cy-legacy-expect')) + + if (!expected) { + throw new Error(`Expected attribute 'cy-expect' or 'cy-legacy-expect' not found on test case_ element ${el.outerHTML}`) + } + + expect( + $(el).is(`:${expected}`), + `${el.getAttribute('cy-label') ?? el.textContent ?? 'empty text content'} should be ${expected}`, + ).to.be.true + + expect(el).to.be[expected] + + cy.wrap(el).should(`be.${expected}`) + + const opposite = expected === 'hidden' ? 'visible' : 'hidden' + + expect( + $(el).is(`:${opposite}`), + `${el.getAttribute('cy-label') ?? el.textContent ?? 'empty text content'} should not be ${opposite}`, + ).to.be.false + + expect(el).to.not.be[opposite] + + cy.wrap(el).should(`not.be.${opposite}`) } - const reasonIs = ($el: JQuery, str: string) => { - expect(dom.getReasonIsHidden($el)).to.eq(str) + function prepareFixtureSection (section: string) { + cy.get('.test-section').each((el) => el.removeClass('active')) + cy.get(`[cy-section="${section}"]`).then((el) => el.addClass('active')) + cy.get(`[cy-section="${section}"]`).scrollIntoView() } - beforeEach(() => { - cy.visit('/fixtures/generic.html') - }) + function assertVisibilityForSections (sections: string[]) { + for (const section of sections) { + it(`detects visibility for ${section} test cases`, () => { + prepareFixtureSection(section) + cy.get(`[cy-section="${section}"] .testCase`).then((els) => { + els.get().forEach(assertVisibilityForEl) + }) + }) + } + } + const modes = ['fast', 'legacy'] - context('isHidden', () => { - it('exposes isHidden', () => { - expect(dom.isHidden).to.be.a('function') - }) + for (const mode of modes) { + describe(`${mode}`, { + experimentalFastVisibility: mode === 'fast', + }, () => { + beforeEach(() => { + cy.visit('/fixtures/generic.html') + }) - it('throws when not passed a DOM element', () => { - const fn = () => { - dom.isHidden(null!) - } + context('isHidden', () => { + it('exposes isHidden', () => { + expect(dom.isHidden).to.be.a('function') + }) - expect(fn).to.throw('`Cypress.dom.isHidden()` failed because it requires a DOM element. The subject received was: `null`') - }) - }) + it('throws when not passed a DOM element', () => { + const fn = () => { + dom.isHidden(null!) + } - context('isVisible', () => { - it('exposes isVisible', () => { - expect(dom.isVisible).to.be.a('function') - }) + expect(fn).to.throw('`Cypress.dom.isHidden()` failed because it requires a DOM element. The subject received was: `null`') + }) + }) - it('throws when not passed a DOM element', () => { - const fn = () => { - // @ts-ignore - dom.isVisible('form') - } + context('isVisible', () => { + it('exposes isVisible', () => { + expect(dom.isVisible).to.be.a('function') + }) - expect(fn).to.throw('`Cypress.dom.isVisible()` failed because it requires a DOM element. The subject received was: `form`') - }) - }) + it('throws when not passed a DOM element', () => { + const fn = () => { + // @ts-ignore + dom.isVisible('form') + } - context('#isScrollable', () => { - beforeEach(function () { - this.add = (el) => { - return $(el).appendTo(cy.$$('body')) - } - }) + expect(fn).to.throw('`Cypress.dom.isVisible()` failed because it requires a DOM element. The subject received was: `form`') + }) + }) - it('returns true if window and body > window height', function () { - this.add('
') - const win = cy.state('window') + context('#isScrollable', () => { + beforeEach(function () { + this.add = (el) => { + return $(el).appendTo(cy.$$('body')) + } + }) - const fn = () => { - return dom.isScrollable(win) - } + it('returns true if window and body > window height', function () { + this.add('
') + const win = cy.state('window') - expect(fn()).to.be.true - }) + const fn = () => { + return dom.isScrollable(win) + } - it('returns false if window and body < window height', () => { - cy.$$('body').html('
foo
') + expect(fn()).to.be.true + }) - const win = cy.state('window') + it('returns false if window and body < window height', () => { + cy.$$('body').html('
foo
') - const fn = () => { - return dom.isScrollable(win) - } + const win = cy.state('window') - expect(fn()).to.be.false - }) + const fn = () => { + return dom.isScrollable(win) + } - it('returns true if document element and body > window height', function () { - this.add('
') - const documentElement = Cypress.dom.wrap(cy.state('document').documentElement) + expect(fn()).to.be.false + }) - const fn = () => { - return dom.isScrollable(documentElement) - } + it('returns true if document element and body > window height', function () { + this.add('
') + const documentElement = Cypress.dom.wrap(cy.state('document').documentElement) - expect(fn()).to.be.true - }) + const fn = () => { + return dom.isScrollable(documentElement) + } - it('returns false if document element and body < window height', () => { - cy.$$('body').html('
foo
') + expect(fn()).to.be.true + }) - const documentElement = Cypress.dom.wrap(cy.state('document').documentElement) + it('returns false if document element and body < window height', () => { + cy.$$('body').html('
foo
') - const fn = () => { - return dom.isScrollable(documentElement) - } + const documentElement = Cypress.dom.wrap(cy.state('document').documentElement) - expect(fn()).to.be.false - }) + const fn = () => { + return dom.isScrollable(documentElement) + } - it('returns false el is not scrollable', function () { - const noScroll = this.add(`\ + expect(fn()).to.be.false + }) + + it('returns false el is not scrollable', function () { + const noScroll = this.add(`\
No Scroll
\ `) - const fn = () => { - return dom.isScrollable(noScroll) - } + const fn = () => { + return dom.isScrollable(noScroll) + } - expect(fn()).to.be.false - }) + expect(fn()).to.be.false + }) - it('returns false el has no overflow', function () { - const noOverflow = this.add(`\ + it('returns false el has no overflow', function () { + const noOverflow = this.add(`\
No Overflow Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Etiam porta sem malesuada magna mollis euismod. @@ -119,1387 +198,342 @@ describe('src/cypress/dom/visibility', () => {
\ `) - const fn = () => { - return dom.isScrollable(noOverflow) - } + const fn = () => { + return dom.isScrollable(noOverflow) + } - expect(fn()).to.be.false - }) + expect(fn()).to.be.false + }) - it('returns true when vertically scrollable', function () { - const vertScrollable = this.add(`\ + it('returns true when vertically scrollable', function () { + const vertScrollable = this.add(`\
Vertical Scroll
\ `) - const fn = () => { - return dom.isScrollable(vertScrollable) - } + const fn = () => { + return dom.isScrollable(vertScrollable) + } - expect(fn()).to.be.true - }) + expect(fn()).to.be.true + }) - it('returns true when horizontal scrollable', function () { - const horizScrollable = this.add(`\ + it('returns true when horizontal scrollable', function () { + const horizScrollable = this.add(`\
Horizontal Scroll
\ `) - const fn = () => { - return dom.isScrollable(horizScrollable) - } + const fn = () => { + return dom.isScrollable(horizScrollable) + } - expect(fn()).to.be.true - }) + expect(fn()).to.be.true + }) - it('returns true when overflow scroll forced and content larger', function () { - const forcedScroll = this.add(`\ + it('returns true when overflow scroll forced and content larger', function () { + const forcedScroll = this.add(`\
Forced Scroll
\ `) - const fn = () => { - return dom.isScrollable(forcedScroll) - } - - expect(fn()).to.be.true - }) - }) - - context('hidden/visible overrides', () => { - beforeEach(function () { - // ensure all tests run against a scrollable window - const scrollThisIntoView = add('
Should be in view
') - - this.$visHidden = add('
    ') - this.$parentVisHidden = add('') - this.$displayNone = add('') - this.$inputHidden = add('') - this.$divNoWidth = add('
    ') - this.$divNoHeight = add('
    ') - this.$divDetached = $('
    foo
    ') - this.$divVisible = add(`
    visible
    `) - - this.$optionInSelect = add(`\ -\ -`) - - this.$optgroupInSelect = add(`\ -\ -`) - - this.$optionInHiddenSelect = add(`\ -\ -`) - - this.$optionOutsideSelect = add(`\ -
    - -
    -
    - -
    \ -`) - - this.$optionHiddenInSelect = add(`\ -\ -`) - - this.$btnOpacityZero = add(`\ -\ -`) - - this.$btnOpacityHalf = add(`\ -\ -`) - - this.$parentOpacityZero = add(`\ -
    - -
    \ -`) - - this.$tableVisCollapse = add(`\ - - - - - - - - - - - -
    NarutoSasukeSakura
    KaguyaMadaraOrochimaro
    \ -`) - - this.$parentNoWidth = add(`\ -
    - parent width: 0 -
    `) - - this.$parentNoHeight = add(`\ -
    - parent height: 0 -
    `) - - this.$parentNoWidthHeightOverflowAuto = add(`\ -
    - parent no size, overflow: auto -
    `) - - this.$parentWithWidthHeightNoOverflow = add(`\ -
    - parent with size, overflow: hidden -
    `) - - this.$ancestorWithWidthHeightNoOverflow = add(`\ -
    -
    parent with size, overflow: hidden
    -
    `) - - this.$ancestorNoWidth = add(`\ -
    -
    - ancestor width: 0 -
    -
    `) - - this.$ancestorNoHeight = add(`\ -
    -
    - ancestor height: 0 -
    -
    `) - - this.$childPosAbs = add(`\ -
    -
    - position: absolute -
    -
    `) - - this.$childPosFixed = add(`\ -
    - -
    `) - - this.$childPointerEventsNone = add(`\ -
    - child pointer-events: none -
    \ -`) - - this.$descendentPosAbs = add(`\ -
    -
    - no width, descendant position: absolute -
    -
    `) - - this.$descendentPosFixed = add(`\ -
    -
    - -
    -
    `) - - this.$descendantInPosFixed = add(`\ -
    -
    - underneath -
    on top of the other
    -
    -
    \ -`) - - this.$coveredUpPosFixed = add(`\ -
    -
    underneath
    -
    on top
    -
    \ -`) - - this.$offScreenPosFixed = add(`\ -
    off screen
    \ -`) - - this.$parentPosAbs = add(`\ -
    -
    - parent position: absolute -
    -
    `) - - this.$parentDisplayNone = add(`\ -\ -`) - - this.$parentPointerEventsNone = add(`\ -
    - parent pointer-events: none -
    \ -`) - - this.$parentPointerEventsNoneCovered = add(`\ -
    - parent pointer-events: none -
    -covering the element with pointer-events: none\ -`) - - this.$parentDisplayInlineChildDisplayBlock = add(`\ -
    - - - -
    \ -`) - - this.$elOutOfParentBoundsToLeft = add(`\ -
    - position: absolute, out of bounds left -
    \ -`) - - this.$elOutOfParentBoundsToRight = add(`\ -
    - position: absolute, out of bounds right -
    \ -`) - - this.$elOutOfParentBoundsAbove = add(`\ -
    - position: absolute, out of bounds above -
    \ -`) - - this.$elOutOfParentBoundsBelow = add(`\ -
    - position: absolute, out of bounds below -
    \ -`) - - this.$elOutOfParentWithOverflowYHiddenBounds = add(`\ -
    - position: absolute, out of bounds below -
    \ -`) - - this.$elOutOfParentWithOverflowXHiddenBounds = add(`\ -
    - position: absolute, out of bounds below -
    \ -`) - - this.$elOutOfParentWithFlexAndOverflowHiddenBounds = add(`\ -
    -
    red
    -
    green
    -
    blue
    -
    \ -`) - - this.$elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent = add(`\ -
    -
    - Hello -
    \ -`) - - this.$elOutOfAncestorOverflowAutoBounds = add(`\ -
    -
    - out of bounds, parent wide, ancestor overflow: auto -
    -
    \ -`) - - this.$elInPosAbsParentsBounds = add(`\ -
    -
    -
    - in bounds, parent position: absolute -
    -
    -
    \ -`) - - this.$elOutOfPosAbsParentsBounds = add(`\ -
    -
    -
    - out of bounds, parent position: absolute -
    -
    -
    \ -`) - - this.$elInParentBounds = add(`\ -
    - in bounds, position: absolute -
    \ -`) - - this.$elOutOfScrollingParentBounds = add(`\ -
    -
    -
    - out of scrolling bounds, position: absolute -
    -
    -
    \ -`) - - this.$elIsOutOfBoundsOfAncestorsOverflowButWithinRelativeAncestor = add(`\ -
    -
    -
    - in bounds of ancestor, position: absolute, parent overflow: hidden -
    -
    -
    \ -`) - - this.$elIsRelativeAndOutOfBoundsOfAncestorOverflow = add(`\ -
    -
    - out of bounds, position: relative -
    -
    \ -`) - - this.$elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow = add(`\ -
    -
    - out of bounds but visible, position: relative -
    -
    \ -`) - - this.$parentOutOfBoundsButElInBounds = add(`\ -
    -
    -
    - in bounds of ancestor, parent out of bounds -
    -
    -
    \ -`) - - this.$parentWithClipPathAbsolutePositionElOutsideClipPath = add(`\ -
    - clip-path -
    \ -`) - - this.$parentWithClipPathAbsolutePositionElInsideClipPath = add(`\ -
    - clip-path -
    \ -`) - - this.$parentWithTransformScaleElOutsideScale = add(`\ -
    - TRANSFORMERS -
    \ -`) - - this.$parentWithTransformScaleElInsideScale = add(`\ -
    - TRANSFORMERS -
    \ -`) - - this.$ancestorTransformMakesElOutOfBoundsOfAncestor = add(`\ -
    -
    -
    - out of ancestor's bounds due to ancestor translate -
    -
    -
    \ -`) - - this.$ancestorTransformMakesElInBoundsOfAncestor = add(`\ -
    -
    -
    - out of ancestor's bounds due to ancestor translate -
    -
    - in ancestor's bounds due to ancestor translate -
    -
    -
    \ -`) - - // scroll the 2nd element into view so that - // there is always a scrollTop so we ensure - // its factored in (window vs viewport) calculations - scrollThisIntoView.get(1).scrollIntoView() - }) - - describe('html or body', () => { - it('is visible if html', () => { - expect(cy.$$('html').is(':hidden')).to.be.false - expect(cy.$$('html').is(':visible')).to.be.true - - expect(cy.$$('html')).not.to.be.hidden - expect(cy.$$('html')).to.be.visible - - cy.wrap(cy.$$('html')).should('not.be.hidden') - cy.wrap(cy.$$('html')).should('be.visible') - }) - - it('is visible if body', () => { - expect(cy.$$('body').is(':hidden')).to.be.false - expect(cy.$$('body').is(':visible')).to.be.true - - expect(cy.$$('body')).not.to.be.hidden - expect(cy.$$('body')).to.be.visible - - cy.wrap(cy.$$('body')).should('not.be.hidden') - cy.wrap(cy.$$('body')).should('be.visible') - }) - - it('is visible if display none on body or html', () => { - cy.$$('html').css('display', 'none') - cy.$$('body').css('display', 'none') - - expect(cy.$$('html')).not.to.be.hidden - expect(cy.$$('html')).to.be.visible - - expect(cy.$$('body')).not.to.be.hidden - expect(cy.$$('body')).to.be.visible - }) - }) - - describe('css visibility', () => { - it('is hidden if .css(visibility) is hidden', function () { - expect(this.$visHidden.is(':hidden')).to.be.true - expect(this.$visHidden.is(':visible')).to.be.false - - expect(this.$visHidden).to.be.hidden - expect(this.$visHidden).to.not.be.visible - - cy.wrap(this.$visHidden).should('be.hidden') - cy.wrap(this.$visHidden).should('not.be.visible') - }) - - it('is hidden if parents have .css(visibility) hidden', function () { - expect(this.$parentVisHidden.find('button').is(':hidden')).to.be.true - expect(this.$parentVisHidden.find('button').is(':visible')).to.be.false - - expect(this.$parentVisHidden.find('button')).to.be.hidden - expect(this.$parentVisHidden.find('button')).to.not.be.visible - - cy.wrap(this.$parentVisHidden).find('button').should('be.hidden') - cy.wrap(this.$parentVisHidden).find('button').should('not.be.visible') - }) - - it('is hidden if visibility collapse', function () { - expect(this.$tableVisCollapse.find('td.collapse')).to.be.hidden - expect(this.$tableVisCollapse.find('td.collapse')).to.not.be.visible - - expect(this.$tableVisCollapse.find('tr.collapse')).to.be.hidden - expect(this.$tableVisCollapse.find('tr.collapse')).to.not.be.visible - - expect(this.$tableVisCollapse.find('tr.collapse td')).to.be.hidden - expect(this.$tableVisCollapse.find('tr.collapse td')).to.not.be.visible - }) - - it('is hidden if parent has visibility collapse', function () { - expect(this.$tableVisCollapse.find('tr.collapse td')).to.be.hidden - expect(this.$tableVisCollapse.find('tr.collapse td')).to.not.be.visible - - expect(this.$tableVisCollapse.find('#collapse-span')).to.be.hidden - expect(this.$tableVisCollapse.find('#collapse-span')).to.not.be.visible - }) - - it('is hidden if input type hidden', function () { - expect(this.$inputHidden.is(':hidden')).to.be.true - expect(this.$inputHidden.is(':visible')).to.be.false - - expect(this.$inputHidden).to.be.hidden - expect(this.$inputHidden).to.not.be.visible - - cy.wrap(this.$inputHidden).should('be.hidden') - cy.wrap(this.$inputHidden).should('not.be.visible') - }) - }) - - describe('option and optgroup', () => { - it('is visible if option in visible select', function () { - expect(this.$optionInSelect.find('option').is(':hidden')).to.be.false - expect(this.$optionInSelect.find('option').is(':visible')).to.be.true - - expect(this.$optionInSelect.find('option')).not.to.be.hidden - expect(this.$optionInSelect.find('option')).to.be.visible - - cy.wrap(this.$optionInSelect.find('option')).should('not.be.hidden') - cy.wrap(this.$optionInSelect.find('option')).should('be.visible') - }) - - it('is visible if optgroup in visible select', function () { - expect(this.$optgroupInSelect.find('optgroup').is(':hidden')).to.be.false - expect(this.$optgroupInSelect.find('optgroup').is(':visible')).to.be.true - - expect(this.$optgroupInSelect.find('optgroup')).not.to.be.hidden - expect(this.$optgroupInSelect.find('optgroup')).to.be.visible - - cy.wrap(this.$optgroupInSelect.find('optgroup')).should('not.be.hidden') - cy.wrap(this.$optgroupInSelect.find('optgroup')).should('be.visible') - }) - - it('is hidden if option in hidden select', function () { - expect(this.$optionInHiddenSelect.find('option').is(':hidden')).to.be.true - expect(this.$optionInHiddenSelect.find('option').is(':visible')).to.be.false - - expect(this.$optionInHiddenSelect.find('option')).to.be.hidden - expect(this.$optionInHiddenSelect.find('option')).not.to.be.visible - - cy.wrap(this.$optionInHiddenSelect.find('option')).should('be.hidden') - cy.wrap(this.$optionInHiddenSelect.find('option')).should('not.be.visible') - }) - - it('is hidden if option is display none', function () { - expect(this.$optionHiddenInSelect.find('#hidden-opt').is(':hidden')).to.be.true - expect(this.$optionHiddenInSelect.find('#hidden-opt').is(':visible')).to.be.false - - expect(this.$optionHiddenInSelect.find('#hidden-opt')).to.be.hidden - expect(this.$optionHiddenInSelect.find('#hidden-opt')).not.to.be.visible - - cy.wrap(this.$optionHiddenInSelect.find('#hidden-opt')).should('be.hidden') - cy.wrap(this.$optionHiddenInSelect.find('#hidden-opt')).should('not.be.visible') - }) - - it('follows regular visibility logic if option outside of select', { browser: '!webkit' }, function () { - expect(this.$optionOutsideSelect.find('#option-hidden').is(':hidden')).to.be.true - expect(this.$optionOutsideSelect.find('#option-hidden')).to.be.hidden - cy.wrap(this.$optionOutsideSelect.find('#option-hidden')).should('be.hidden') - - expect(this.$optionOutsideSelect.find('#option-visible').is(':visible')).to.be.true - expect(this.$optionOutsideSelect.find('#option-visible')).to.be.visible - - cy.wrap(this.$optionOutsideSelect.find('#option-visible')).should('be.visible') - }) - }) - - describe('css opacity', () => { - it('is hidden if opacity is 0', function () { - expect(this.$btnOpacityZero.is(':hidden')).to.be.true - expect(this.$btnOpacityZero.is(':visible')).to.be.false - - expect(this.$btnOpacityZero).to.be.hidden - expect(this.$btnOpacityZero).not.to.be.visible - - cy.wrap(this.$btnOpacityZero).should('be.hidden') - cy.wrap(this.$btnOpacityZero).should('not.be.visible') - }) - - it('is hidden if parent has `opacity: 0`', function () { - expect(this.$parentOpacityZero.find('button').is(':hidden')).to.be.true - expect(this.$parentOpacityZero.find('button').is(':visible')).to.be.false - - expect(this.$parentOpacityZero.find('button')).to.be.hidden - expect(this.$parentOpacityZero.find('button')).not.to.be.visible - - cy.wrap(this.$parentOpacityZero.find('button')).should('be.hidden') - cy.wrap(this.$parentOpacityZero.find('button')).should('not.be.visible') - }) - - it('is visible if opacity is greater than 0 but less than 1', function () { - expect(this.$btnOpacityHalf.is(':visible')).to.be.true - expect(this.$btnOpacityHalf.is(':hidden')).to.be.false - - expect(this.$btnOpacityHalf).to.be.visible - expect(this.$btnOpacityHalf).not.to.be.hidden - - cy.wrap(this.$btnOpacityHalf).should('be.visible') - cy.wrap(this.$btnOpacityHalf).should('not.be.hidden') - }) - }) - - describe('width and height', () => { - it('is visible when el.textContent, even if offsetWidth is 0', function () { - this.$divNoWidthTextContent = add('
    width: 0
    ') - - expect(this.$divNoWidthTextContent.is(':hidden')).to.be.false - expect(this.$divNoWidthTextContent.is(':visible')).to.be.true - - expect(this.$divNoWidthTextContent).to.not.be.hidden - expect(this.$divNoWidthTextContent).to.be.visible - - cy.wrap(this.$divNoWidthTextContent).should('be.not.hidden') - cy.wrap(this.$divNoWidthTextContent).should('be.visible') - }) - - it('is visible when el.textContent, even if offsetHeight is 0', function () { - this.$divNoHeightTextContent = add('
    height: 0
    ') - - expect(this.$divNoHeightTextContent.is(':hidden')).to.be.false - expect(this.$divNoHeightTextContent.is(':visible')).to.be.true - - expect(this.$divNoHeightTextContent).to.not.be.hidden - expect(this.$divNoHeightTextContent).to.be.visible - - cy.wrap(this.$divNoHeightTextContent).should('be.not.hidden') - cy.wrap(this.$divNoHeightTextContent).should('be.visible') - }) - - it('is hidden when when el.textContent contains only whitespace and offsetWidth is 0', function () { - this.$divNoHeightBlankTextContent = add('
    \n \t
    ') - - expect(this.$divNoHeightBlankTextContent.is(':hidden')).to.be.true - expect(this.$divNoHeightBlankTextContent.is(':visible')).to.be.false - - expect(this.$divNoHeightBlankTextContent).to.be.hidden - expect(this.$divNoHeightBlankTextContent).to.not.be.visible - - cy.wrap(this.$divNoHeightBlankTextContent).should('be.hidden') - cy.wrap(this.$divNoHeightBlankTextContent).should('not.be.visible') - }) + const fn = () => { + return dom.isScrollable(forcedScroll) + } - it('is hidden when no el.textContent with offsetHeight is 0', function () { - expect(this.$divNoHeight.is(':hidden')).to.be.true - expect(this.$divNoHeight.is(':visible')).to.be.false - - expect(this.$divNoHeight).to.be.hidden - expect(this.$divNoHeight).to.not.be.visible - - cy.wrap(this.$divNoHeight).should('be.hidden') - cy.wrap(this.$divNoHeight).should('not.be.visible') - }) - - it('is hidden when no el.textContent with offsetWidth is 0', function () { - expect(this.$divNoWidth.is(':hidden')).to.be.true - expect(this.$divNoWidth.is(':visible')).to.be.false - - expect(this.$divNoWidth).to.be.hidden - expect(this.$divNoWidth).to.not.be.visible - - cy.wrap(this.$divNoWidth).should('be.hidden') - cy.wrap(this.$divNoWidth).should('not.be.visible') - }) - - 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 - }) - - it('is hidden if parent has overflow: hidden and no height', function () { - expect(this.$parentNoHeight.find('span')).to.be.hidden - expect(this.$parentNoHeight.find('span')).to.not.be.visible - }) - - it('is hidden if ancestor has overflow:hidden and no width', function () { - expect(this.$ancestorNoWidth.find('span')).to.be.hidden - expect(this.$ancestorNoWidth.find('span')).to.not.be.visible - }) - - it('is hidden if ancestor has overflow:hidden and no height', function () { - expect(this.$ancestorNoHeight.find('span')).to.be.hidden - expect(this.$ancestorNoHeight.find('span')).to.not.be.visible - }) - - it('is visible when parent has positive dimensions even with overflow hidden', function () { - expect(this.$parentWithWidthHeightNoOverflow.find('span')).to.be.visible - expect(this.$parentWithWidthHeightNoOverflow.find('span')).to.not.be.hidden - }) - - it('is visible when ancestor has positive dimensions even with overflow hidden', function () { - expect(this.$ancestorWithWidthHeightNoOverflow.find('span')).to.be.visible - expect(this.$ancestorWithWidthHeightNoOverflow.find('span')).to.not.be.hidden - }) - }) - - describe('css position', () => { - it('is visible if child has position: absolute', function () { - expect(this.$childPosAbs.find('span')).to.be.visible - expect(this.$childPosAbs.find('span')).not.be.hidden - }) - - it('is visible if child has position: fixed', function () { - expect(this.$childPosFixed.find('button')).to.be.visible - expect(this.$childPosFixed.find('button')).not.to.be.hidden - }) - - it('is visible if descendent from parent has position: fixed', function () { - expect(this.$descendentPosFixed.find('button')).to.be.visible - expect(this.$descendentPosFixed.find('button')).not.to.be.hidden - }) - - it('is visible if has position: fixed and descendent is found', function () { - expect(this.$descendantInPosFixed.find('#descendantInPosFixed')).to.be.visible - expect(this.$descendantInPosFixed.find('#descendantInPosFixed')).not.to.be.hidden - }) - - it('is hidden if position: fixed and covered up', function () { - expect(this.$coveredUpPosFixed.find('#coveredUpPosFixed')).to.be.hidden - expect(this.$coveredUpPosFixed.find('#coveredUpPosFixed')).not.to.be.visible - }) - - it('is hidden if position: fixed and off screen', function () { - expect(this.$offScreenPosFixed).to.be.hidden - expect(this.$offScreenPosFixed).not.to.be.visible - }) - - it('is visible if descendent from parent has position: absolute', function () { - expect(this.$descendentPosAbs.find('span')).to.be.visible - expect(this.$descendentPosAbs.find('span')).to.not.be.hidden - }) - - it('is hidden if only the parent has position absolute', function () { - expect(this.$parentPosAbs.find('span')).to.be.hidden - expect(this.$parentPosAbs.find('span')).to.not.be.visible - }) - - it('is visible if position: fixed and parent has pointer-events: none', function () { - expect(this.$parentPointerEventsNone.find('span')).to.be.visible - expect(this.$parentPointerEventsNone.find('span')).to.not.be.hidden - }) - - it('is not visible if covered when position: fixed and parent has pointer-events: none', function () { - expect(this.$parentPointerEventsNoneCovered.find('span')).to.be.hidden - expect(this.$parentPointerEventsNoneCovered.find('span')).to.not.be.visible - }) - - it('is visible if pointer-events: none and parent has position: fixed', function () { - expect(this.$childPointerEventsNone.find('span')).to.be.visible - expect(this.$childPointerEventsNone.find('span')).to.not.be.hidden - }) - - it('is visible when position: sticky', () => { - cy.visit('fixtures/sticky.html') - cy.get('#button').should('be.visible') - }) - }) - - describe('css display', function () { - // https://github.com/cypress-io/cypress/issues/6183 - it('parent is visible if display inline and child has display block', function () { - expect(this.$parentDisplayInlineChildDisplayBlock.find('span')).to.be.visible - expect(this.$parentDisplayInlineChildDisplayBlock.find('span')).to.not.be.hidden - }) - }) - - describe('css overflow', () => { - it('is hidden when parent overflow auto and no width/height', function () { - expect(this.$parentNoWidthHeightOverflowAuto.find('span')).to.not.be.visible - expect(this.$parentNoWidthHeightOverflowAuto.find('span')).to.be.hidden - }) - - it('is hidden when parent overflow hidden and out of bounds to left', function () { - expect(this.$elOutOfParentBoundsToLeft.find('span')).to.be.hidden - }) - - it('is hidden when parent overflow hidden and out of bounds to right', function () { - expect(this.$elOutOfParentBoundsToRight.find('span')).to.be.hidden - }) - - it('is hidden when parent overflow hidden and out of bounds above', function () { - expect(this.$elOutOfParentBoundsAbove.find('span#elOutOfParentBoundsAbove')).to.be.hidden - }) - - it('is hidden when parent overflow hidden and out of bounds below', function () { - expect(this.$elOutOfParentBoundsBelow.find('span')).to.be.hidden - }) - - it('is hidden when parent overflow-y hidden and out of bounds', function () { - expect(this.$elOutOfParentWithOverflowYHiddenBounds.find('span')).to.be.hidden - }) - - it('is hidden when parent overflow-x hidden and out of bounds', function () { - expect(this.$elOutOfParentWithOverflowXHiddenBounds.find('span')).to.be.hidden - }) - - it('is visible when parent overflow hidden but el in a closer parent with position absolute', function () { - expect(this.$elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent.find('span')).to.be.visible - }) - - it('is hidden when parent flex and overflow hidden and el out of bounds', function () { - expect(this.$elOutOfParentWithFlexAndOverflowHiddenBounds.find('#red')).to.be.visible - expect(this.$elOutOfParentWithFlexAndOverflowHiddenBounds.find('#green')).to.be.visible - expect(this.$elOutOfParentWithFlexAndOverflowHiddenBounds.find('#blue')).to.be.hidden - }) - - it('is hidden when parent is wide and ancestor is overflow auto', function () { - expect(this.$elOutOfAncestorOverflowAutoBounds.find('span')).to.be.hidden - }) - - it('is hidden when parent overflow scroll and out of bounds', function () { - expect(this.$elOutOfScrollingParentBounds.find('span')).to.be.hidden - }) - - it('is hidden when parent absolutely positioned and overflow hidden and out of bounds', function () { - expect(this.$elOutOfPosAbsParentsBounds.find('span')).to.be.hidden - }) - - it('is visible when parent absolutely positioned and overflow hidden and not out of bounds', function () { - expect(this.$elInPosAbsParentsBounds.find('span')).to.be.visible - }) - - it('is visible when parent overflow hidden and not out of bounds', function () { - expect(this.$elInParentBounds.find('span')).to.be.visible - }) - - it('is hidden when parent overflow clip and height is 0', function () { - cy.$$('body').empty() - - const el = add('
    I am not visible
    ') - - expect(el.find('#hidden')).to.be.hidden - reasonIs(el.find('#hidden'), '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`') - }) - - it('is visible when parent overflow clip and height is non-0', function () { - cy.$$('body').empty() - - const el = add('
    I am visible
    ') - - expect(el.find('#visible')).to.be.visible - }) - - it('is visible when ancestor is overflow hidden but more distant ancestor is the offset parent', function () { - expect(this.$elIsOutOfBoundsOfAncestorsOverflowButWithinRelativeAncestor.find('span')).to.be.visible - }) - - it('is hidden when relatively positioned outside ancestor with overflow hidden', function () { - expect(this.$elIsRelativeAndOutOfBoundsOfAncestorOverflow.find('span')).to.be.hidden - }) - - it('is visible when el is relatively positioned outside ancestor that does not hide overflow', function () { - expect(this.$elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow.find('span')).to.be.visible - }) - - 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 () { - 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 () { - cy.$$('body').empty() - - const el = add(` -
    -
    -
    -
    -

    Example

    -
    - -
    -
    -
    -
    -
    - `) - - 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(` -
    -
    -
    - - -
    -
    -
    - `) - - 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`') + expect(fn()).to.be.true }) - - 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', () => { - // TODO: handle clip path 'hidden' equivalents - it.skip('is hidden when outside of parents clip-path', function () { - expect(this.$parentWithClipPathAbsolutePositionElOutsideClipPath.find('span')).to.be.hidden - }) - - it('is visible when inside of parents clip-path', function () { - expect(this.$parentWithClipPathAbsolutePositionElInsideClipPath.find('span')).to.be.visible - }) - }) - - describe('css transform', () => { - describe('element visibility by css transform', () => { - it('is visible when an element is translated a bit', () => { - const el = add(`
    Translated
    `) - - expect(el).to.be.visible - }) - - it('is visible when an element is only skewed', () => { - const el = add(`
    Skewed
    `) - - expect(el).to.be.visible - }) - - it('is visible when an element is only rotated', () => { - const el = add(`
    Rotated
    `) - - expect(el).to.be.visible - }) - - it('is visible when an element is scaled by non-zero', () => { - const el = add(`
    Scaled
    `) - - expect(el).to.be.visible - }) - - it('is visible when an element is transformed in multiple ways but not scaled to zero', () => { - const el = add(`
    Multiple transform
    `) - - expect(el).to.be.visible - }) + describe('visibility scenarios', () => { + describe('html and body overrides', () => { + beforeEach(() => { + cy.visit('/fixtures/empty.html') + }) - it('is visible when an element is rotateZ(90deg)', () => { - const el = add(`
    rotateZ(90deg)
    `) + describe('html is display none', () => { + beforeEach(() => { + cy.get('html').then(($el) => { + $el.css('display', 'none') + }) + }) - expect(el).to.be.visible - }) + it('is always visible', () => { + expect(cy.$$('html').is(':hidden')).to.be.false + expect(cy.$$('html').is(':visible')).to.be.true - // https://github.com/cypress-io/cypress/issues/6745 - it('is visible even if there is a dangling element in the tree', () => { - cy.visit('/fixtures/dangling-element.html') - cy.get('.hello') - }) + expect(cy.$$('html')).not.to.be.hidden + expect(cy.$$('html')).to.be.visible - it('is hidden when an element is scaled to X axis in 0', () => { - const el = add(`
    ScaleX(0)
    `) + cy.wrap(cy.$$('html')).should('not.be.hidden') + cy.wrap(cy.$$('html')).should('be.visible') + }) + }) - expect(el).to.be.hidden - }) + describe('body', () => { + beforeEach(() => { + cy.get('body').then(($el) => { + $el.css('display', 'none') + }) + }) - it('is hidden when an element is scaled to Y axis in 0', () => { - const el = add(`
    ScaleY(0)
    `) + it('is always visible', () => { + expect(cy.$$('body').is(':hidden')).to.be.false + expect(cy.$$('body').is(':visible')).to.be.true - expect(el).to.be.hidden + expect(cy.$$('body')).not.to.be.hidden + expect(cy.$$('body')).to.be.visible + }) + }) }) - it('is hidden when an element is scaled to Z axis in 0', () => { - const el = add(`
    ScaleZ(0)
    `) + describe('basic CSS properties', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/basic-css-properties.html') + }) - expect(el).to.be.hidden + assertVisibilityForSections([ + 'visibility-property', + 'display-property', + 'opacity-property', + 'table-elements', + 'box-interactions', + 'style-filters', + 'contain-property', + 'pointer-events-none', + ]) }) - it('is hidden when an element is transformed in multiple ways but scaled to 0 in one axis', () => { - const el = add(`
    Multiple 2
    `) + describe('form elements', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/form-elements.html') + }) - expect(el).to.be.hidden + assertVisibilityForSections([ + 'select-and-option-elements', + 'optgroup-elements', + 'options-outside-select', + 'hidden-options-within-visible-select', + 'input-elements', + ]) }) - it('is hidden when an element is rotateX(90deg)', () => { - const el = add(`
    rotateX(90deg)
    `) + describe('overflow', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/overflow.html') + }) - expect(el).to.be.hidden + assertVisibilityForSections([ + 'zero-dimensions-with-overflow-hidden', + 'text-content-with-zero-dimensions', + 'positive-dimensions-with-overflow-hidden', + 'overflow-auto-with-zero-dimensions', + 'mixed-dimension-scenarios', + 'overflow-hidden', + 'overflow-y-hidden', + 'overflow-x-hidden', + 'overflow-auto-scenarios', + 'overflow-scroll-scenarios', + 'overflow-relative-positioning', + 'overflow-flex-container', + 'overflow-complex-scenarios', + 'clip-scenarios', + 'viewport-scenarios', + ]) }) - it('is hidden when an element is rotateY(90deg)', () => { - const el = add(`
    rotateY(90deg)
    `) + describe('positioning', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/positioning.html') + }) - expect(el).to.be.hidden + assertVisibilityForSections([ + 'position-fixed-element-covered-by-another', + 'static-ancestor-fixed-descendant', + 'static-parent-fixed-child', + 'positioning-with-zero-dimensions', + 'fixed-positioning-with-zero-dimensions', + 'position-absolute-scenarios', + 'position-sticky-scenarios', + 'positioning-cousin-coverage', + 'z-index-coverage', + ]) }) - it('is hidden when an element is rotateX(90deg) rotateY(90deg)', () => { - const el = add(`
    rotateX(90deg)
    `) + describe('transforms', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/transforms.html') + }) - expect(el).to.be.hidden + assertVisibilityForSections([ + 'scaling', + 'translation', + 'rotation', + 'skew', + 'matrix', + 'perspective', + 'multiple', + 'multiple-3d', + 'backface-visibility', + ]) }) + }) - it('is hidden when an element is transformed in multiple ways but rotated to 90 deg in X or Y axis', () => { - const el = add(`
    rotateX(90deg)
    `) - - expect(el).to.be.hidden - - const el2 = add(`
    rotateX(90deg)
    `) - - expect(el2).to.be.hidden - - const el3 = add(`
    rotateX(90deg)
    `) + context('#getReasonIsHidden', () => { + const reasonIs = ($el: JQuery, str: string) => { + expect(dom.getReasonIsHidden($el)).to.eq(str) + } - expect(el3).to.be.hidden - }) - }) + describe('basic css / box model', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/basic-css-properties.html') + }) - describe('when height/width is set', () => { - it('is visible when transform is not 0, but height is 0', () => { - const el = add('
    Text
    ') + it('has `display: none`', function () { + prepareFixtureSection('display-property') + cy.get('[cy-section="display-property"] .testCase[cy-expect="hidden"]:first').then(($el) => { + reasonIs($el, 'This element `` is not visible because it has CSS property: `display: none`') + }) + }) - expect(el).to.be.visible - }) + it('has a parent with `display: none`', function () { + prepareFixtureSection('display-property') + cy.get('[cy-section="display-property"] .testCase[cy-expect="hidden"] span').then(($el) => { + reasonIs($el, 'This element `` is not visible because its parent `` has CSS property: `display: none`') + }) + }) - it('is visible when transform is not 0, but width is 0', () => { - const el = add('

    Text

    ') + it('has `visibility: hidden`', function () { + prepareFixtureSection('visibility-property') + cy.get('[cy-section="visibility-property"] .testCase[cy-expect="hidden"]').then((el) => { + reasonIs($(el).first(), 'This element `` is not visible because it has CSS property: `visibility: hidden`') + }) + }) - expect(el).to.be.visible - }) + it('has parent with `visibility: hidden`', function () { + prepareFixtureSection('visibility-property') + cy.get('[cy-section="visibility-property"] .testCase[cy-expect="hidden"] button').then((el) => { + reasonIs($(el).first(), 'This element `` is not visible because its parent `` has CSS property: `visibility: hidden`') + }) + }) - it('is visible when parent transform is not 0, but height is 0', () => { - const el = add('

    Text

    ') + it('has `visibility: collapse`', function () { + prepareFixtureSection('table-elements') + cy.get('[cy-section="table-elements"] td[style*="visibility: collapse"]').then(($el) => { + reasonIs($el, 'This element `` is not visible because it has CSS property: `visibility: collapse`') + }) + }) - expect(el.find('#tr-p-0')).to.be.visible - }) + it('has parent with `visibility: collapse`', function () { + prepareFixtureSection('table-elements') + cy.get('[cy-section="table-elements"] tr[style*="visibility: collapse"] td').then(($el) => { + reasonIs($el, 'This element `` is not visible because its parent `` has CSS property: `visibility: collapse`') + }) + }) - it('is visible when parent transform is not 0, but width is 0', () => { - const el = add('

    Test

    ') + it('has `opacity: 0`', function () { + prepareFixtureSection('opacity-property') + cy.get('[cy-section="opacity-property"] .testCase[cy-expect="hidden"]').then(($el) => { + reasonIs($($el).first(), 'This element `` is not visible because it has CSS property: `opacity: 0`') + }) + }) - expect(el.find('#tr-p-1')).to.be.visible + it('has parent with `opacity: 0`', function () { + prepareFixtureSection('opacity-property') + cy.get('[cy-section="opacity-property"] .testCase[cy-expect="hidden"] button').then(($el) => { + reasonIs($el, 'This element `` is not visible because its parent `` has CSS property: `opacity: 0`') + }) + }) }) - it('is invisible when parent transform is 0, but height is not 0', () => { - const el = add('

    Test

    ') + it('is detached from the DOM', function () { + const divDetached = $('
    Detached
    ') - expect(el.find('#tr-p-2')).to.be.hidden + reasonIs(divDetached, 'This element `
    ` is not visible because it is detached from the DOM') }) - describe('invisible when overflow: hidden', () => { - it('height: 0 + overflow', () => { - const el = add('

    Test

    ') - - expect(el.find('#h0th')).to.be.hidden + describe('overflow-related', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/overflow.html') }) - it('height: 0 + overflow-x', () => { - const el = add('

    Test

    ') - - expect(el.find('#h0th')).to.be.hidden + it('has effective zero width', function () { + prepareFixtureSection('zero-dimensions-with-overflow-hidden') + cy.get('[cy-section="zero-dimensions-with-overflow-hidden"] .testCase[cy-label="Zero width ancestor, parent, self"]').then(($el) => { + reasonIs($el, 'This element `` is not visible because it has an effective width and height of: `0 x 100` pixels.') + }) }) - it('height: 0 + overflow-y', () => { - const el = add('

    Test

    ') - - expect(el.find('#h0th')).to.be.hidden + it('has effective zero height', function () { + prepareFixtureSection('zero-dimensions-with-overflow-hidden') + cy.get('[cy-section="zero-dimensions-with-overflow-hidden"] .testCase[cy-label="Zero height ancestor, parent, self"]').then(($el) => { + reasonIs($el, 'This element `` is not visible because it has an effective width and height of: `100 x 0` pixels.') + }) }) - it('width: 0 + overflow', () => { - const el = add('

    Test

    ') - - expect(el.find('#h0th')).to.be.hidden + it('has a parent with an effective zero width and overflow: hidden', function () { + prepareFixtureSection('zero-dimensions-with-overflow-hidden') + cy.get('[cy-section="zero-dimensions-with-overflow-hidden"] .testCase[cy-label="Zero height ancestor, parent, self"] span').then(($el) => { + reasonIs($el, 'This element `` is not visible because its parent `` has CSS property: `overflow: hidden` and an effective width and height of: `100 x 0` pixels.') + }) }) - it('width: 0 + overflow-x', () => { - const el = add('

    Test

    ') - - expect(el.find('#h0th')).to.be.hidden + it('element sits outside boundaries of parent with overflow clipping', function () { + prepareFixtureSection('overflow-hidden') + cy.get('[cy-section="overflow-hidden"] .testCase[cy-label="Element out of bounds right"]').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`') + }) }) + }) - it('width: 0 + overflow-y', () => { - const el = add('

    Test

    ') - - expect(el.find('#h0th')).to.be.hidden + it('is hidden because it is backface', function () { + cy.visit('/fixtures/visibility/transforms.html') + prepareFixtureSection('backface-visibility') + cy.get('[cy-section="backface-visibility"] .testCase[cy-label="RotateX 180deg with hidden backface"]').then(($el) => { + reasonIs($el, 'This element `` is not visible because it is rotated and its backface is hidden.') }) }) - }) - - it('is hidden when outside parents transform scale', function () { - expect(this.$parentWithTransformScaleElOutsideScale.find('span')).to.be.hidden - }) - - it('is visible when inside of parents transform scale', function () { - expect(this.$parentWithTransformScaleElInsideScale.find('span')).to.be.visible - }) - - it('is hidden when out of ancestor\'s bounds due to ancestor\'s transform', function () { - expect(this.$ancestorTransformMakesElOutOfBoundsOfAncestor.find('span')).to.be.hidden - }) - - it('is visible when in ancestor\'s bounds due to ancestor\'s transform', function () { - expect(this.$ancestorTransformMakesElInBoundsOfAncestor.find('#inbounds')).to.be.visible - }) - }) - - describe('#getReasonIsHidden', () => { - it('has `display: none`', function () { - reasonIs(this.$displayNone, 'This element ` +
    + `) - it('needs scroll', function () { - const el = cy.$$('body').append(` -
    -
    Big Element
    - -
    - `) + reasonIs(el.find('#needsScroll'), `This element \`\` is not visible because its ancestor has \`position: fixed\` CSS property and it is overflowed by other elements. How about scrolling to the element with \`cy.scrollIntoView()\`?`) + }) - reasonIs(el.find('#needsScroll'), `This element \`\` is not visible because its ancestor has \`position: fixed\` CSS property and it is overflowed by other elements. How about scrolling to the element with \`cy.scrollIntoView()\`?`) - }) + it('cannot determine why element is not visible', function () { + // this element is actually visible + // but used here as an example that does not match any of the above + const visible = cy.$$('
    Visible
    ') - it('cannot determine why element is not visible', function () { - // this element is actually visible - // but used here as an example that does not match any of the above - reasonIs(this.$divVisible, 'This element `
    ` is not visible.') + cy.$$('body').append(visible) + reasonIs(visible, 'This element `
    ` is not visible.') + }) }) }) - }) + } }) 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 974a909d57d..623b3a42853 100644 --- a/packages/driver/cypress/e2e/dom/visibility_shadow_dom.cy.ts +++ b/packages/driver/cypress/e2e/dom/visibility_shadow_dom.cy.ts @@ -5,62 +5,69 @@ const { $ } = Cypress describe('src/cypress/dom/visibility - shadow dom', () => { let add: (el: string, shadowEl: string, rootIdentifier: string) => JQuery - beforeEach(() => { - cy.visit('/fixtures/empty.html').then((win) => { - win.customElements.define('shadow-root', class extends win.HTMLElement { - constructor () { - super() - - this.attachShadow({ mode: 'open' }) - this.style.display = 'block' - } + // #TODO: support shadow dom in fast visibility algorithm + const modes = ['legacy'] + + for (const mode of modes) { + describe(`${mode}`, { + experimentalFastVisibility: mode === 'fast', + }, () => { + beforeEach(() => { + cy.visit('/fixtures/empty.html').then((win) => { + win.customElements.define('shadow-root', class extends win.HTMLElement { + constructor () { + super() + + this.attachShadow({ mode: 'open' }) + this.style.display = 'block' + } + }) + + add = (el, shadowEl, rootIdentifier) => { + const $el = $(el).appendTo(cy.$$('body')) + + $(shadowEl).appendTo(cy.$$(rootIdentifier)[0].shadowRoot!) + + return $el + } + + // ensure all tests run against a scrollable window + const scrollThisIntoView = $(`
    Should be in view
    `).appendTo(cy.$$('body')) + + // scroll the 2nd element into view so that + // there is always a scrollTop so we ensure + // its factored in (window vs viewport) calculations + scrollThisIntoView.get(1).scrollIntoView() + }) }) - add = (el, shadowEl, rootIdentifier) => { - const $el = $(el).appendTo(cy.$$('body')) - - $(shadowEl).appendTo(cy.$$(rootIdentifier)[0].shadowRoot!) - - return $el - } - - // ensure all tests run against a scrollable window - const scrollThisIntoView = $(`
    Should be in view
    `).appendTo(cy.$$('body')) - - // scroll the 2nd element into view so that - // there is always a scrollTop so we ensure - // its factored in (window vs viewport) calculations - scrollThisIntoView.get(1).scrollIntoView() - }) - }) - - describe('css visibility', () => { - it('is hidden if parent is shadow root and has .css(visibility) hidden', () => { - const $shadowRootVisHidden = add( + describe('css visibility', () => { + it('is hidden if parent is shadow root and has .css(visibility) hidden', () => { + const $shadowRootVisHidden = add( ``, ``, '#shadow-root-vis-hidden', - ) + ) - cy.wrap($shadowRootVisHidden).find('button', { includeShadowDom: true }).should('be.hidden') - cy.wrap($shadowRootVisHidden).find('button', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($shadowRootVisHidden).find('button', { includeShadowDom: true }).should('be.hidden') + cy.wrap($shadowRootVisHidden).find('button', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden if parent outside of shadow dom has .css(visibility) hidden', () => { - const $outsideParentVisHidden = add( + it('is hidden if parent outside of shadow dom has .css(visibility) hidden', () => { + const $outsideParentVisHidden = add( `
    `, ``, '#outside-parent-vis-hidden', - ) + ) - cy.wrap($outsideParentVisHidden).find('button', { includeShadowDom: true }).should('be.hidden') - cy.wrap($outsideParentVisHidden).find('button', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($outsideParentVisHidden).find('button', { includeShadowDom: true }).should('be.hidden') + cy.wrap($outsideParentVisHidden).find('button', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden if parent outside of shadow dom has visibility collapse', () => { - const $outsideParentVisCollapse = add( + it('is hidden if parent outside of shadow dom has visibility collapse', () => { + const $outsideParentVisCollapse = add( ` @@ -70,29 +77,29 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    Naruto
    `, `Sasuke`, '#outside-parent-vis-collapse', - ) + ) - cy.wrap($outsideParentVisCollapse).find('#collapse-span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($outsideParentVisCollapse).find('#collapse-span', { includeShadowDom: true }).should('not.be.visible') - }) - }) + cy.wrap($outsideParentVisCollapse).find('#collapse-span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($outsideParentVisCollapse).find('#collapse-span', { includeShadowDom: true }).should('not.be.visible') + }) + }) - describe('width and height', () => { - it('is hidden if parent is shadow root and has overflow: hidden and no width', () => { - const $shadowRootNoWidth = add( + describe('width and height', () => { + it('is hidden if parent is shadow root and has overflow: hidden and no width', () => { + const $shadowRootNoWidth = add( ``, `
    parent width: 0
    `, '#shadow-root-no-width', - ) + ) - cy.wrap($shadowRootNoWidth).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($shadowRootNoWidth).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($shadowRootNoWidth).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($shadowRootNoWidth).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden if parent outside of shadow dom has overflow: hidden and no width', () => { - const $outsideParentNoWidth = add( + it('is hidden if parent outside of shadow dom has overflow: hidden and no width', () => { + const $outsideParentNoWidth = add( `
    `, @@ -100,27 +107,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => { parent width: 0
    `, '#outside-parent-no-width', - ) + ) - cy.wrap($outsideParentNoWidth).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($outsideParentNoWidth).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($outsideParentNoWidth).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($outsideParentNoWidth).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden if parent is shadow root and has overflow: hidden and no height', () => { - const $shadowRootNoHeight = add( + it('is hidden if parent is shadow root and has overflow: hidden and no height', () => { + const $shadowRootNoHeight = add( ``, `
    parent height: 0
    `, '#shadow-root-no-height', - ) + ) - cy.wrap($shadowRootNoHeight).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($shadowRootNoHeight).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($shadowRootNoHeight).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($shadowRootNoHeight).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden if parent outside of shadow dom has overflow: hidden and no height', () => { - const $outsideParentNoHeight = add( + it('is hidden if parent outside of shadow dom has overflow: hidden and no height', () => { + const $outsideParentNoHeight = add( `
    `, @@ -128,16 +135,16 @@ describe('src/cypress/dom/visibility - shadow dom', () => { parent height: 0
    `, '#outside-parent-no-height', - ) + ) - cy.wrap($outsideParentNoHeight).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($outsideParentNoHeight).find('span', { includeShadowDom: true }).should('not.be.visible') - }) - }) + cy.wrap($outsideParentNoHeight).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($outsideParentNoHeight).find('span', { includeShadowDom: true }).should('not.be.visible') + }) + }) - describe('css position', () => { - it('is visible if child has position: absolute', () => { - const $childPosAbs = add( + describe('css position', () => { + it('is visible if child has position: absolute', () => { + const $childPosAbs = add( `
    `, @@ -145,14 +152,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => { position: absolute
    `, '#child-pos-absolute', - ) + ) - cy.wrap($childPosAbs).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($childPosAbs).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($childPosAbs).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($childPosAbs).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible if child has position: fixed', () => { - const $childPosFixed = add( + it('is visible if child has position: fixed', () => { + const $childPosFixed = add( `
    `, @@ -160,14 +167,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, '#child-pos-fixed', - ) + ) - cy.wrap($childPosFixed).find('button', { includeShadowDom: true }).should('be.visible') - cy.wrap($childPosFixed).find('button', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($childPosFixed).find('button', { includeShadowDom: true }).should('be.visible') + cy.wrap($childPosFixed).find('button', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible if descendent from parent has position: absolute and descendent is outside shadow dom', () => { - const $descendentPosAbsOutside = add( + it('is visible if descendent from parent has position: absolute and descendent is outside shadow dom', () => { + const $descendentPosAbsOutside = add( `
    @@ -175,14 +182,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `no width, descendant position: absolute`, '#descendent-pos-abs-outside', - ) + ) - cy.wrap($descendentPosAbsOutside).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($descendentPosAbsOutside).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($descendentPosAbsOutside).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($descendentPosAbsOutside).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible if descendent from parent has position: absolute and descendent is inside shadow dom', () => { - const $descendentPosAbsInside = add( + it('is visible if descendent from parent has position: absolute and descendent is inside shadow dom', () => { + const $descendentPosAbsInside = add( `
    `, @@ -190,14 +197,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => { no width, descendant position: absolute
    `, '#descendent-pos-abs-inside', - ) + ) - cy.wrap($descendentPosAbsInside).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($descendentPosAbsInside).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($descendentPosAbsInside).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($descendentPosAbsInside).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible if descendent from parent has position: fixed and descendent is outside shadow dom', () => { - const $descendentPosFixedOutside = add( + it('is visible if descendent from parent has position: fixed and descendent is outside shadow dom', () => { + const $descendentPosFixedOutside = add( `
    @@ -205,14 +212,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, ``, '#descendent-pos-fixed-outside', - ) + ) - cy.wrap($descendentPosFixedOutside).find('button', { includeShadowDom: true }).should('be.visible') - cy.wrap($descendentPosFixedOutside).find('button', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($descendentPosFixedOutside).find('button', { includeShadowDom: true }).should('be.visible') + cy.wrap($descendentPosFixedOutside).find('button', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible if descendent from parent has position: fixed and descendent is inside shadow dom', () => { - const $descendentPosFixedInside = add( + it('is visible if descendent from parent has position: fixed and descendent is inside shadow dom', () => { + const $descendentPosFixedInside = add( `
    `, @@ -220,162 +227,162 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, '#descendent-pos-fixed-inside', - ) + ) - cy.wrap($descendentPosFixedInside).find('button', { includeShadowDom: true }).should('be.visible') - cy.wrap($descendentPosFixedInside).find('button', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($descendentPosFixedInside).find('button', { includeShadowDom: true }).should('be.visible') + cy.wrap($descendentPosFixedInside).find('button', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is hidden if position: fixed and covered by element outside of shadow dom', () => { - const $coveredUpByOutsidePosFixed = add( + it('is hidden if position: fixed and covered by element outside of shadow dom', () => { + const $coveredUpByOutsidePosFixed = add( `
    on top
    `, `
    underneath
    `, '#covered-up-by-outside-pos-fixed', - ) + ) - cy.wrap($coveredUpByOutsidePosFixed).find('#inside-underneath', { includeShadowDom: true }).should('be.hidden') - cy.wrap($coveredUpByOutsidePosFixed).find('#inside-underneath', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($coveredUpByOutsidePosFixed).find('#inside-underneath', { includeShadowDom: true }).should('be.hidden') + cy.wrap($coveredUpByOutsidePosFixed).find('#inside-underneath', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden if outside of shadow dom with position: fixed and covered by element inside of shadow dom', () => { - const $coveredUpByShadowPosFixed = add( + it('is hidden if outside of shadow dom with position: fixed and covered by element inside of shadow dom', () => { + const $coveredUpByShadowPosFixed = add( `
    underneath
    `, `
    on top
    `, '#covered-up-by-shadow-pos-fixed', - ) + ) - cy.wrap($coveredUpByShadowPosFixed).find('#outside-underneath', { includeShadowDom: true }).should('be.hidden') - cy.wrap($coveredUpByShadowPosFixed).find('#outside-underneath', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($coveredUpByShadowPosFixed).find('#outside-underneath', { includeShadowDom: true }).should('be.hidden') + cy.wrap($coveredUpByShadowPosFixed).find('#outside-underneath', { includeShadowDom: true }).should('not.be.visible') + }) - it('is visible if position: fixed and parent outside shadow dom has pointer-events: none', () => { - const $parentPointerEventsNone = add( + it('is visible if position: fixed and parent outside shadow dom has pointer-events: none', () => { + const $parentPointerEventsNone = add( `
    `, `parent pointer-events: none`, '#parent-pointer-events-none', - ) + ) - cy.wrap($parentPointerEventsNone).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($parentPointerEventsNone).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($parentPointerEventsNone).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($parentPointerEventsNone).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is hidden if covered when position: fixed and parent outside shadow dom has pointer-events: none', () => { - const $parentPointerEventsNoneCovered = add( + it('is hidden if covered when position: fixed and parent outside shadow dom has pointer-events: none', () => { + const $parentPointerEventsNoneCovered = add( `
    covering the element with pointer-events: none`, `parent pointer-events: none`, '#parent-pointer-events-none-covered', - ) + ) - cy.wrap($parentPointerEventsNoneCovered).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($parentPointerEventsNoneCovered).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($parentPointerEventsNoneCovered).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($parentPointerEventsNoneCovered).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is visible if pointer-events: none and parent outside shadow dom has position: fixed', () => { - const $childPointerEventsNone = add( + it('is visible if pointer-events: none and parent outside shadow dom has position: fixed', () => { + const $childPointerEventsNone = add( `
    `, `child pointer-events: none`, '#child-pointer-events-none-covered', - ) + ) - cy.wrap($childPointerEventsNone).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($childPointerEventsNone).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) - }) + cy.wrap($childPointerEventsNone).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($childPointerEventsNone).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) + }) - describe('css overflow', () => { - it('is hidden when parent outside of shadow dom overflow hidden and out of bounds to left', () => { - const $elOutOfParentBoundsToLeft = add( + describe('css overflow', () => { + it('is hidden when parent outside of shadow dom overflow hidden and out of bounds to left', () => { + const $elOutOfParentBoundsToLeft = add( `
    `, `position: absolute, out of bounds left`, '#el-out-of-parent-bounds-to-left', - ) + ) - cy.wrap($elOutOfParentBoundsToLeft).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfParentBoundsToLeft).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfParentBoundsToLeft).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfParentBoundsToLeft).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent outside of shadow dom overflow hidden and out of bounds to right', () => { - const $elOutOfParentBoundsToRight = add( + it('is hidden when parent outside of shadow dom overflow hidden and out of bounds to right', () => { + const $elOutOfParentBoundsToRight = add( `
    `, `position: absolute, out of bounds right`, '#el-out-of-parent-bounds-to-right', - ) + ) - cy.wrap($elOutOfParentBoundsToRight).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfParentBoundsToRight).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfParentBoundsToRight).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfParentBoundsToRight).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent outside of shadow dom overflow hidden and out of bounds above', () => { - const $elOutOfParentBoundsAbove = add( + it('is hidden when parent outside of shadow dom overflow hidden and out of bounds above', () => { + const $elOutOfParentBoundsAbove = add( `
    `, `position: absolute, out of bounds above`, '#el-out-of-parent-bounds-above', - ) + ) - cy.wrap($elOutOfParentBoundsAbove).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfParentBoundsAbove).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfParentBoundsAbove).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfParentBoundsAbove).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent outside of shadow dom overflow hidden and out of bounds below', () => { - const $elOutOfParentBoundsBelow = add( + it('is hidden when parent outside of shadow dom overflow hidden and out of bounds below', () => { + const $elOutOfParentBoundsBelow = add( `
    `, `position: absolute, out of bounds below`, '#el-out-of-parent-bounds-below', - ) + ) - cy.wrap($elOutOfParentBoundsBelow).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfParentBoundsBelow).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfParentBoundsBelow).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfParentBoundsBelow).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent outside of shadow dom overflow hidden-y and out of bounds', () => { - const $elOutOfParentWithOverflowYHiddenBounds = add( + it('is hidden when parent outside of shadow dom overflow hidden-y and out of bounds', () => { + const $elOutOfParentWithOverflowYHiddenBounds = add( `
    `, `position: absolute, out of bounds below`, '#el-out-of-parent-with-overflow-y-hidden-bounds', - ) + ) - cy.wrap($elOutOfParentWithOverflowYHiddenBounds).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfParentWithOverflowYHiddenBounds).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfParentWithOverflowYHiddenBounds).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfParentWithOverflowYHiddenBounds).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent outside of shadow dom overflow hidden-x and out of bounds', () => { - const $elOutOfParentWithOverflowXHiddenBounds = add( + it('is hidden when parent outside of shadow dom overflow hidden-x and out of bounds', () => { + const $elOutOfParentWithOverflowXHiddenBounds = add( `
    `, `position: absolute, out of bounds below`, '#el-out-of-parent-with-overflow-x-hidden-bounds', - ) + ) - cy.wrap($elOutOfParentWithOverflowXHiddenBounds).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfParentWithOverflowXHiddenBounds).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfParentWithOverflowXHiddenBounds).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfParentWithOverflowXHiddenBounds).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is visible when parent overflow hidden but el in a closer parent outside of shadow dom with position absolute', () => { - const $elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent = add( + it('is visible when parent overflow hidden but el in a closer parent outside of shadow dom with position absolute', () => { + const $elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent = add( `
    @@ -383,14 +390,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `Hello`, '#el-out-of-parent-with-overflow-hidden-bounds-but-closer-position-absolute-parent', - ) + ) - cy.wrap($elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is hidden when parent is wide and ancestor outside shadow dom is overflow auto', () => { - const $elOutOfAncestorOverflowAutoBoundsOutside = add( + it('is hidden when parent is wide and ancestor outside shadow dom is overflow auto', () => { + const $elOutOfAncestorOverflowAutoBoundsOutside = add( `
    @@ -398,14 +405,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `out of bounds, parent wide, ancestor overflow: auto`, '#el-out-of-ancestor-overflow-auto-bounds-outside', - ) + ) - cy.wrap($elOutOfAncestorOverflowAutoBoundsOutside).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfAncestorOverflowAutoBoundsOutside).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfAncestorOverflowAutoBoundsOutside).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfAncestorOverflowAutoBoundsOutside).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent is wide and ancestor inside shadow dom is overflow auto', () => { - const $elOutOfAncestorOverflowAutoBoundsInside = add( + it('is hidden when parent is wide and ancestor inside shadow dom is overflow auto', () => { + const $elOutOfAncestorOverflowAutoBoundsInside = add( `
    `, @@ -413,27 +420,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => { out of bounds, parent wide, ancestor overflow: auto
    `, '#el-out-of-ancestor-overflow-auto-bounds-inside', - ) + ) - cy.wrap($elOutOfAncestorOverflowAutoBoundsInside).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfAncestorOverflowAutoBoundsInside).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfAncestorOverflowAutoBoundsInside).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfAncestorOverflowAutoBoundsInside).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent outside of shadow dom has overflow scroll and out of bounds', () => { - const $elOutOfScrollingParentBounds = add( + it('is hidden when parent outside of shadow dom has overflow scroll and out of bounds', () => { + const $elOutOfScrollingParentBounds = add( `
    `, `out of scrolling bounds, position: absolute`, '#el-out-of-scrolling-parent-bounds', - ) + ) - cy.wrap($elOutOfScrollingParentBounds).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfScrollingParentBounds).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfScrollingParentBounds).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfScrollingParentBounds).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when parent absolutely positioned and overflow hidden and out of bounds', () => { - const $elOutOfPosAbsParentBounds = add( + it('is hidden when parent absolutely positioned and overflow hidden and out of bounds', () => { + const $elOutOfPosAbsParentBounds = add( `
    @@ -443,14 +450,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `out of bounds, position: absolute`, '#el-out-of-pos-abs-parent-bounds', - ) + ) - cy.wrap($elOutOfPosAbsParentBounds).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elOutOfPosAbsParentBounds).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elOutOfPosAbsParentBounds).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elOutOfPosAbsParentBounds).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is visible when parent absolutely positioned and overflow hidden and not out of bounds', () => { - const $elInPosAbsParentsBounds = add( + it('is visible when parent absolutely positioned and overflow hidden and not out of bounds', () => { + const $elInPosAbsParentsBounds = add( `
    @@ -458,27 +465,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `in bounds, parent position: absolute`, '#el-in-pos-abs-parent-bounds', - ) + ) - cy.wrap($elInPosAbsParentsBounds).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($elInPosAbsParentsBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($elInPosAbsParentsBounds).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($elInPosAbsParentsBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible when parent overflow hidden and not out of bounds', () => { - const $elInParentBounds = add( + it('is visible when parent overflow hidden and not out of bounds', () => { + const $elInParentBounds = add( `
    `, `in bounds, position: absolute`, '#el-in-parent-bounds', - ) + ) - cy.wrap($elInParentBounds).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($elInParentBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($elInParentBounds).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($elInParentBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible when ancestor outside shadow dom is overflow hidden but more distant ancestor is the offset parent', () => { - const $elIsOutOfBoundsOfOutsideAncestorsOverflowButWithinRelativeAncestor = add( + it('is visible when ancestor outside shadow dom is overflow hidden but more distant ancestor is the offset parent', () => { + const $elIsOutOfBoundsOfOutsideAncestorsOverflowButWithinRelativeAncestor = add( `
    @@ -488,14 +495,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `in bounds of ancestor, position: absolute, parent overflow: hidden`, '#el-is-out-of-bounds-of-outside-ancestors-overflow-but-within-relative-ancestor', - ) + ) - cy.wrap($elIsOutOfBoundsOfOutsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($elIsOutOfBoundsOfOutsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($elIsOutOfBoundsOfOutsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($elIsOutOfBoundsOfOutsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible when ancestor inside shadow dom is overflow hidden but more distant ancestor is the offset parent', () => { - const $elIsOutOfBoundsOfInsideAncestorsOverflowButWithinRelativeAncestor = add( + it('is visible when ancestor inside shadow dom is overflow hidden but more distant ancestor is the offset parent', () => { + const $elIsOutOfBoundsOfInsideAncestorsOverflowButWithinRelativeAncestor = add( `
    `, @@ -505,14 +512,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, '#el-is-out-of-bounds-of-inside-ancestors-overflow-but-within-relative-ancestor', - ) + ) - cy.wrap($elIsOutOfBoundsOfInsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($elIsOutOfBoundsOfInsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($elIsOutOfBoundsOfInsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($elIsOutOfBoundsOfInsideAncestorsOverflowButWithinRelativeAncestor).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is hidden when relatively positioned outside of ancestor outside shadow dom with overflow hidden', () => { - const $elIsRelativeAndOutOfBoundsOfAncestorOverflow = add( + it('is hidden when relatively positioned outside of ancestor outside shadow dom with overflow hidden', () => { + const $elIsRelativeAndOutOfBoundsOfAncestorOverflow = add( `
    @@ -520,27 +527,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `out of bounds, position: relative`, '#el-is-relative-and-out-of-bounds-of-ancestor-overflow', - ) + ) - cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorOverflow).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorOverflow).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorOverflow).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorOverflow).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is visible when relatively positioned outside of ancestor outside shadow dom that does not hide overflow', () => { - const $elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow = add( + it('is visible when relatively positioned outside of ancestor outside shadow dom that does not hide overflow', () => { + const $elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow = add( `
    `, `out of bounds but visible, position: relative`, '#el-is-relative-and-out-of-bounds-of-ancestor-but-ancestor-shows-overflow', - ) + ) - cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible when parent inside shadow dom is relatively positioned out of bounds but el is relatively positioned back in bounds', () => { - const $insideParentOutOfBoundsButElInBounds = add( + it('is visible when parent inside shadow dom is relatively positioned out of bounds but el is relatively positioned back in bounds', () => { + const $insideParentOutOfBoundsButElInBounds = add( `
    @@ -550,14 +557,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => { in bounds of ancestor, parent out of bounds
    `, '#inside-parent-out-of-bounds-but-el-in-bounds', - ) + ) - cy.wrap($insideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($insideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($insideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($insideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is visible when parent outside shadow dom is relatively positioned out of bounds but el is relatively positioned back in bounds', () => { - const $outsideParentOutOfBoundsButElInBounds = add( + it('is visible when parent outside shadow dom is relatively positioned out of bounds but el is relatively positioned back in bounds', () => { + const $outsideParentOutOfBoundsButElInBounds = add( `
    @@ -567,14 +574,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, `in bounds of ancestor, parent out of bounds`, '#outside-parent-out-of-bounds-but-el-in-bounds', - ) + ) - cy.wrap($outsideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($outsideParentOutOfBoundsButElInBounds).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + 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( + it('is visible when element is statically positioned and parent element is absolutely positioned and ancestor has overflow hidden', function () { + const el = add( `
    @@ -584,14 +591,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, '#shadow', - ) + ) - cy.wrap(el).find('#visible-button', { includeShadowDom: true }).should('be.visible') - cy.wrap(el).find('#visible-button', { includeShadowDom: true }).should('not.be.hidden') - }) + 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( + it('is visible when element is relatively positioned and parent element is absolutely positioned and ancestor has overflow auto', 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') - }) - }) + 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', () => { - it('is hidden when outside parent outside of shadow dom transform scale', () => { - const $parentWithTransformScaleElOutsideScale = add( + describe('css transform', () => { + it('is hidden when outside parent outside of shadow dom transform scale', () => { + const $parentWithTransformScaleElOutsideScale = add( `
    `, `TRANSFORMERS`, '#parent-with-transform-scale-el-outside-scale', - ) + ) - cy.wrap($parentWithTransformScaleElOutsideScale).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($parentWithTransformScaleElOutsideScale).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($parentWithTransformScaleElOutsideScale).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($parentWithTransformScaleElOutsideScale).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is visible when inside parent outside of shadow dom transform scale', () => { - const $parentWithTransformScaleElInsideScale = add( + it('is visible when inside parent outside of shadow dom transform scale', () => { + const $parentWithTransformScaleElInsideScale = add( `
    `, `TRANSFORMERS`, '#parent-with-transform-scale-el-inside-scale', - ) + ) - cy.wrap($parentWithTransformScaleElInsideScale).find('span', { includeShadowDom: true }).should('be.visible') - cy.wrap($parentWithTransformScaleElInsideScale).find('span', { includeShadowDom: true }).should('not.be.hidden') - }) + cy.wrap($parentWithTransformScaleElInsideScale).find('span', { includeShadowDom: true }).should('be.visible') + cy.wrap($parentWithTransformScaleElInsideScale).find('span', { includeShadowDom: true }).should('not.be.hidden') + }) - it('is hidden when out of ancestor bounds due to ancestor within shadow dom transform', () => { - const $ancestorInsideTransformMakesElOutOfBoundsOfAncestor = add( + it('is hidden when out of ancestor bounds due to ancestor within shadow dom transform', () => { + const $ancestorInsideTransformMakesElOutOfBoundsOfAncestor = add( `
    `, @@ -654,14 +661,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
    `, '#ancestor-inside-transform-makes-el-out-of-bounds-of-ancestor', - ) + ) - cy.wrap($ancestorInsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($ancestorInsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('not.be.visible') - }) + cy.wrap($ancestorInsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($ancestorInsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('not.be.visible') + }) - it('is hidden when out of ancestor bounds due to ancestor outside shadow dom transform', () => { - const $ancestorOutsideTransformMakesElOutOfBoundsOfAncestor = add( + it('is hidden when out of ancestor bounds due to ancestor outside shadow dom transform', () => { + const $ancestorOutsideTransformMakesElOutOfBoundsOfAncestor = add( `
    @@ -671,10 +678,12 @@ describe('src/cypress/dom/visibility - shadow dom', () => { out of ancestor's bounds due to ancestor translate
    `, '#ancestor-outside-transform-makes-el-out-of-bounds-of-ancestor', - ) + ) - cy.wrap($ancestorOutsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('be.hidden') - cy.wrap($ancestorOutsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('not.be.visible') + cy.wrap($ancestorOutsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('be.hidden') + cy.wrap($ancestorOutsideTransformMakesElOutOfBoundsOfAncestor).find('span', { includeShadowDom: true }).should('not.be.visible') + }) + }) }) - }) + } }) diff --git a/packages/driver/cypress/e2e/e2e/visibility.cy.js b/packages/driver/cypress/e2e/e2e/visibility.cy.js index a6475866afe..5ad46b764b2 100644 --- a/packages/driver/cypress/e2e/e2e/visibility.cy.js +++ b/packages/driver/cypress/e2e/e2e/visibility.cy.js @@ -1,58 +1,66 @@ describe('visibility', () => { - // https://github.com/cypress-io/cypress/issues/631 - describe('with overflow and transform - slider', () => { - beforeEach(() => { - cy.visit('/fixtures/issue-631.html') - - // first slide is visible by default, nothing wrong here - cy.get('[name="test1"]').should('be.visible') - cy.get('[name="test2"]').should('not.be.visible') - cy.get('[name="test3"]').should('not.be.visible') - }) + const modes = ['fast', 'legacy'] - it('second slide', () => { - // ask for the second slide to become visible - cy.get('#button-2').click() + for (const mode of modes) { + describe(`${mode}`, { + experimentalFastVisibility: mode === 'fast', + }, () => { + // https://github.com/cypress-io/cypress/issues/631 + describe('with overflow and transform - slider', () => { + beforeEach(() => { + cy.visit('/fixtures/issue-631.html') - cy.get('[name="test1"]').should('not.be.visible') - cy.get('[name="test2"]').should('be.visible') - cy.get('[name="test3"]').should('not.be.visible') - }) + // first slide is visible by default, nothing wrong here + cy.get('[name="test1"]').should('be.visible') + cy.get('[name="test2"]').should('not.be.visible') + cy.get('[name="test3"]').should('not.be.visible') + }) - it('third slide', () => { - // ask for the second slide to become visible - cy.get('#button-3').click() + it('second slide', () => { + // ask for the second slide to become visible + cy.get('#button-2').click() - cy.get('[name="test1"]').should('not.be.visible') - cy.get('[name="test2"]').should('not.be.visible') - cy.get('[name="test3"]').should('be.visible') - }) - }) + cy.get('[name="test1"]').should('not.be.visible') + cy.get('[name="test2"]').should('be.visible') + cy.get('[name="test3"]').should('not.be.visible') + }) - describe('with shadow dom', () => { - // https://github.com/cypress-io/cypress/issues/7794 - it('fixed position ancestor does not hang when checking visibility', () => { - cy.visit('/fixtures/issue-7794.html') - cy.get('.container-2').should('be.visible') - }) + it('third slide', () => { + // ask for the second slide to become visible + cy.get('#button-3').click() - // TODO: move with tests added in this PR when it merges: https://github.com/cypress-io/cypress/pull/8166 - it('non-visible ancestor causes element to not be visible', () => { - cy.visit('/fixtures/shadow-dom.html') - cy - .get('#shadow-element-10') - .find('.shadow-div', { includeShadowDom: true }) - .should('not.be.visible') - }) - }) - - describe('css opacity', () => { - it('correctly detects visibility when opacity changes', () => { - cy.visit('/fixtures/opacity.html') - cy.get('#opacity') - .should('be.visible') - .click() - .should('not.be.visible') + cy.get('[name="test1"]').should('not.be.visible') + cy.get('[name="test2"]').should('not.be.visible') + cy.get('[name="test3"]').should('be.visible') + }) + }) + + describe('with shadow dom', () => { + // https://github.com/cypress-io/cypress/issues/7794 + it('fixed position ancestor does not hang when checking visibility', () => { + cy.visit('/fixtures/issue-7794.html') + cy.get('.container-2').should('be.visible') + }) + + // TODO: move with tests added in this PR when it merges: https://github.com/cypress-io/cypress/pull/8166 + it('non-visible ancestor causes element to not be visible', () => { + cy.visit('/fixtures/shadow-dom.html') + cy + .get('#shadow-element-10') + .find('.shadow-div', { includeShadowDom: true }) + .should('not.be.visible') + }) + }) + + describe('css opacity', () => { + it('correctly detects visibility when opacity changes', () => { + cy.visit('/fixtures/opacity.html') + cy.get('#opacity') + .should('be.visible') + .click() + .should('not.be.visible') + }) + }) }) - }) + } }) diff --git a/packages/driver/cypress/e2e/memory/virtual-scroll-stress.cy.js b/packages/driver/cypress/e2e/memory/virtual-scroll-stress.cy.js new file mode 100644 index 00000000000..086390eea28 --- /dev/null +++ b/packages/driver/cypress/e2e/memory/virtual-scroll-stress.cy.js @@ -0,0 +1,395 @@ +/** + * Virtual Scrolling Browser Crash Tests + * + * This test suite is designed to reproduce browser crashes when scrolling + * with virtual scroll libraries, as reported by users. + * + * Test scenarios include: + * - Basic virtual list scrolling + * - Dynamic height virtual lists + * - Multiple virtual lists simultaneously + * - Extreme stress tests with large datasets + * - Rapid scrolling patterns that may cause crashes + */ + +describe('Virtual Scrolling Stress Tests', { + experimentalFastVisibility: true, // if this is set to false, this spec will crash the browser + numTestsKeptInMemory: 1, +}, () => { + beforeEach(() => { + cy.visit('/fixtures/virtual-scroll-stress-test.html') + + cy.get('.header').should('be.visible') + + // Clear any existing data + cy.window().then((win) => { + if (win.clearBasicList) win.clearBasicList() + + if (win.clearDynamicList) win.clearDynamicList() + + if (win.clearMultipleLists) win.clearMultipleLists() + + if (win.clearExtremeList) win.clearExtremeList() + }) + }) + + describe('Basic Virtual List Tests', () => { + it('should load basic virtual list without crashing', () => { + cy.get('button').contains('Load Basic List').click() + + cy.get('#basicList .item', { log: false }) + .should('have.length.greaterThan', 0, { log: false }) + }) + + it('should handle normal scrolling without crashing', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Perform normal scrolling + cy.get('#basicList').scrollTo(0, 1000) + cy.get('#basicList').scrollTo(0, 2000) + cy.get('#basicList').scrollTo(0, 5000) + }) + + it('should handle rapid scrolling without crashing', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Perform rapid scrolling + cy.get('#basicList').scrollTo(0, 100) + cy.get('#basicList').scrollTo(0, 300) + cy.get('#basicList').scrollTo(0, 600) + cy.get('#basicList').scrollTo(0, 1000) + cy.get('#basicList').scrollTo(0, 1500) + cy.get('#basicList').scrollTo(0, 2000) + }) + + it('should handle stress scrolling without crashing', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Trigger stress scrolling + cy.get('button').contains('Stress Scroll').click() + + // Wait for stress scrolling to complete + cy.wait(2000) + + // Verify the page is still responsive + cy.get('#basicScrolls').should('be.visible') + cy.get('#basicItems').should('contain', '10000') + }) + + it('should handle scrolling to bottom and back to top', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Scroll to bottom + cy.get('#basicList').scrollTo('bottom') + cy.wait(500) + + // Scroll back to top + cy.get('#basicList').scrollTo('top') + cy.wait(500) + + // Verify page is still responsive + cy.get('#basicScrolls').should('be.visible') + }) + }) + + describe('Dynamic Height Virtual List Tests', () => { + it('should load dynamic height list without crashing', () => { + // Load the dynamic list + cy.get('button').contains('Load Dynamic List').click() + + // Wait for items to load + cy.get('#dynamicItems').should('contain', '5000') + cy.get('.loading').should('not.exist') + + // Verify the virtual list is rendered + cy.get('#dynamicList').should('be.visible') + cy.get('#dynamicList .item').should('have.length.greaterThan', 0) + }) + + it('should handle scrolling with dynamic heights without crashing', () => { + // Load the dynamic list + cy.get('button').contains('Load Dynamic List').click() + cy.get('#dynamicItems').should('contain', '5000') + + // Perform scrolling with dynamic heights + cy.get('#dynamicList').scrollTo(0, 500) + cy.get('#dynamicList').scrollTo(0, 1000) + cy.get('#dynamicList').scrollTo(0, 2000) + }) + + it('should handle rapid scrolling with dynamic heights', () => { + // Load the dynamic list + cy.get('button').contains('Load Dynamic List').click() + cy.get('#dynamicItems').should('contain', '5000') + + // Trigger rapid scrolling + cy.get('button').contains('Rapid Scroll').click() + + // Wait for rapid scrolling to complete + cy.wait(1500) + + // Verify the page is still responsive + cy.get('#dynamicScrolls').should('be.visible') + cy.get('#dynamicItems').should('contain', '5000') + }) + }) + + describe('Multiple Virtual Lists Tests', () => { + it('should load multiple lists without crashing', () => { + // Load multiple lists + cy.get('button').contains('Load Multiple Lists').click() + + // Wait for items to load + cy.get('#multipleItems').should('contain', '6000') + cy.get('.loading').should('not.exist') + + // Verify both lists are rendered + cy.get('#multipleListA').should('be.visible') + cy.get('#multipleListB').should('be.visible') + cy.get('#multipleListA .item').should('have.length.greaterThan', 0) + cy.get('#multipleListB .item').should('have.length.greaterThan', 0) + }) + + it('should handle simultaneous scrolling of multiple lists', () => { + // Load multiple lists + cy.get('button').contains('Load Multiple Lists').click() + cy.get('#multipleItems').should('contain', '6000') + + // Scroll both lists simultaneously + cy.get('#multipleListA').scrollTo(0, 1000) + cy.get('#multipleListB').scrollTo(0, 1500) + }) + + it('should handle stress scrolling multiple lists', () => { + // Load multiple lists + cy.get('button').contains('Load Multiple Lists').click() + cy.get('#multipleItems').should('contain', '6000') + + // Trigger stress scrolling on multiple lists + cy.get('button').contains('Stress Test All').click() + + // Wait for stress scrolling to complete + cy.wait(3000) + + // Verify the page is still responsive + cy.get('#multipleScrolls').should('be.visible') + cy.get('#multipleItems').should('contain', '6000') + }) + + it('should handle alternating scroll between lists', () => { + // Load multiple lists + cy.get('button').contains('Load Multiple Lists').click() + cy.get('#multipleItems').should('contain', '6000') + + // Alternate scrolling between lists + cy.get('#multipleListA').scrollTo(0, 500) + cy.get('#multipleListB').scrollTo(0, 500) + cy.get('#multipleListA').scrollTo(0, 1000) + cy.get('#multipleListB').scrollTo(0, 1000) + cy.get('#multipleListA').scrollTo(0, 1500) + cy.get('#multipleListB').scrollTo(0, 1500) + }) + }) + + describe('Extreme Stress Tests', () => { + it('should load extreme list without crashing', () => { + // Load the extreme list + cy.get('button').contains('Load Extreme List').click() + + // Wait for items to load (this may take longer) + cy.get('#extremeItems', { timeout: 10000 }).should('contain', '50000') + cy.get('.loading').should('not.exist') + + // Verify the virtual list is rendered + cy.get('#extremeList').should('be.visible') + cy.get('#extremeList .item').should('have.length.greaterThan', 0) + }) + + it('should handle extreme scrolling without crashing', () => { + // Load the extreme list + cy.get('button').contains('Load Extreme List').click() + cy.get('#extremeItems', { timeout: 10000 }).should('contain', '50000') + + // Perform extreme scrolling + cy.get('#extremeList').scrollTo(0, 5000) + cy.get('#extremeList').scrollTo(0, 10000) + cy.get('#extremeList').scrollTo(0, 20000) + }) + + it('should handle extreme stress scrolling test', () => { + // Load the extreme list + cy.get('button').contains('Load Extreme List').click() + cy.get('#extremeItems', { timeout: 10000 }).should('contain', '50000') + + // Trigger extreme scroll test + cy.get('button').contains('Extreme Scroll Test').click() + + // Wait for extreme scrolling to complete + cy.wait(5000) + + // Verify the page is still responsive + cy.get('#extremeScrolls').should('be.visible') + cy.get('#extremeItems').should('contain', '50000') + }) + + it('should handle rapid scrolling with heavy operations', () => { + // Load the extreme list + cy.get('button').contains('Load Extreme List').click() + cy.get('#extremeItems', { timeout: 10000 }).should('contain', '50000') + + // Click heavy operation buttons while scrolling + cy.get('#extremeList').scrollTo(0, 1000) + cy.get('#extremeList .item-btn').first().click() + cy.get('#extremeList').scrollTo(0, 2000) + cy.get('#extremeList .item-btn').eq(1).click() + cy.get('#extremeList').scrollTo(0, 3000) + cy.get('#extremeList .item-btn').eq(2).click() + }) + }) + + describe('Memory and Performance Tests', () => { + it('should handle memory pressure from multiple large lists', () => { + // Load all lists simultaneously + cy.get('button').contains('Load Basic List').click() + cy.get('button').contains('Load Dynamic List').click() + cy.get('button').contains('Load Multiple Lists').click() + cy.get('button').contains('Load Extreme List').click() + + // Wait for all lists to load + cy.get('#basicItems').should('contain', '10000') + cy.get('#dynamicItems').should('contain', '5000') + cy.get('#multipleItems').should('contain', '6000') + cy.get('#extremeItems', { timeout: 15000 }).should('contain', '50000') + + // Perform scrolling on all lists + cy.get('#basicList').scrollTo(0, 1000) + cy.get('#dynamicList').scrollTo(0, 1000) + cy.get('#multipleListA').scrollTo(0, 1000) + cy.get('#multipleListB').scrollTo(0, 1000) + cy.get('#extremeList').scrollTo(0, 1000) + }) + + it('should handle rapid scroll direction changes', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Rapid scroll direction changes + cy.get('#basicList').scrollTo(0, 1000) + cy.get('#basicList').scrollTo(0, 500) + cy.get('#basicList').scrollTo(0, 1500) + cy.get('#basicList').scrollTo(0, 800) + cy.get('#basicList').scrollTo(0, 2000) + cy.get('#basicList').scrollTo(0, 1200) + }) + + it('should handle scroll with rapid item interactions', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Scroll and interact with items rapidly + cy.get('#basicList').scrollTo(0, 1000) + cy.get('#basicList .item-btn').first().click() + cy.get('#basicList').scrollTo(0, 2000) + cy.get('#basicList .item-btn').eq(1).click() + cy.get('#basicList').scrollTo(0, 3000) + cy.get('#basicList .item-btn').eq(2).click() + }) + }) + + describe('Browser Crash Detection Tests', () => { + it('should detect if browser becomes unresponsive', () => { + // Load the extreme list + cy.get('button').contains('Load Extreme List').click() + cy.get('#extremeItems', { timeout: 10000 }).should('contain', '50000') + + // Perform operations that might cause crashes + cy.get('button').contains('Extreme Scroll Test').click() + + // Wait and check if page is still responsive + cy.wait(2000) + + // Try to interact with the page + cy.get('#extremeScrolls').should('be.visible') + cy.get('button').contains('Clear List').should('be.visible') + + // Try to scroll the main page + cy.scrollTo(0, 500) + cy.scrollTo(0, 0) + }) + + it('should detect memory leaks during extended scrolling', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Perform extended scrolling + for (let i = 0; i < 10; i++) { + cy.get('#basicList').scrollTo(0, i * 1000) + cy.wait(100) + } + + // Verify the page is still responsive + cy.get('#basicScrolls').should('be.visible') + cy.get('#basicItems').should('contain', '10000') + }) + + it('should handle scroll with rapid list clearing and reloading', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Scroll a bit + cy.get('#basicList').scrollTo(0, 1000) + + // Clear and reload multiple times + cy.get('button').contains('Clear List').click() + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + cy.get('button').contains('Clear List').click() + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Verify the page is still responsive + cy.get('#basicScrolls').should('be.visible') + }) + }) + + describe('Cross-Browser Compatibility Tests', () => { + it('should work with different scroll behaviors', () => { + // Load the basic list + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + + // Test different scroll methods + cy.get('#basicList').scrollTo(0, 1000) + cy.get('#basicList').scrollTo('bottom') + cy.get('#basicList').scrollTo('top') + cy.get('#basicList').scrollTo(0, 5000) + }) + + it('should handle scroll with different viewport sizes', () => { + // Test with different viewport sizes + cy.viewport(1920, 1080) + cy.get('button').contains('Load Basic List').click() + cy.get('#basicItems').should('contain', '10000') + cy.get('#basicList').scrollTo(0, 1000) + + cy.viewport(1366, 768) + cy.get('#basicList').scrollTo(0, 2000) + + cy.viewport(1024, 768) + cy.get('#basicList').scrollTo(0, 3000) + }) + }) +}) diff --git a/packages/driver/cypress/fixtures/virtual-scroll-stress-test.html b/packages/driver/cypress/fixtures/virtual-scroll-stress-test.html new file mode 100644 index 00000000000..17e6c4a66e1 --- /dev/null +++ b/packages/driver/cypress/fixtures/virtual-scroll-stress-test.html @@ -0,0 +1,655 @@ + + + + + + Test Website - Browser Crash Reproduction + + + +
    + Scroll: 0px +
    + +
    +
    +

    Browser Crash Test

    +

    This website is designed to reproduce browser crashes when scrolling with virtual scrolling libraries

    +
    + ⚠️ Warning: This test may cause browser crashes or performance issues. Use with caution! +
    +
    + Fallback Mode: Using native scrolling simulation. + This still reproduces the same crash scenarios with high-performance scrolling. +
    +
    + + +
    +

    Test 1: Basic Virtual List (10,000 items)

    +
    +
    + + + +
    +
    +
    +
    0
    +
    Items Rendered
    +
    +
    +
    0
    +
    Scroll Events
    +
    +
    +
    +
    +
    + + +
    +

    Test 2: Dynamic Height Virtual List (5,000 items)

    +
    +
    + + + +
    +
    +
    +
    0
    +
    Items Rendered
    +
    +
    +
    0
    +
    Scroll Events
    +
    +
    +
    +
    +
    + + +
    +

    Test 3: Multiple Virtual Lists (Crash Test)

    +
    +
    + + + +
    +
    +
    +
    0
    +
    Total Items
    +
    +
    +
    0
    +
    Total Scrolls
    +
    +
    +
    +
    +

    List A (3,000 items)

    +
    +
    +
    +

    List B (3,000 items)

    +
    +
    +
    +
    +
    + + +
    +

    Test 4: Extreme Stress Test (50,000 items)

    +
    +
    + + + +
    +
    + ⚠️ This test uses 50,000 items and aggressive scrolling. It may crash your browser! +
    +
    +
    +
    0
    +
    Items Rendered
    +
    +
    +
    0
    +
    Scroll Events
    +
    +
    +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/packages/driver/cypress/fixtures/visibility/basic-css-properties.html b/packages/driver/cypress/fixtures/visibility/basic-css-properties.html new file mode 100644 index 00000000000..2d8c7d80299 --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/basic-css-properties.html @@ -0,0 +1,92 @@ + + + + Basic CSS Properties Visibility Tests + + + + +

    Basic CSS Properties Visibility Tests

    + +
    +

    Visibility Property

    + +
    Visible by visibility
    + +
    + +
    +

    Display Property

    + +
    Visible by display: block
    + +
    + +
    +

    Opacity Property

    +
    Hidden by opacity: 0
    +
    Semi-transparent (opacity: 0.5)
    +
    Fully opaque (opacity: 1)
    +
    + +
    +
    + +
    +

    Style Filters

    + +
    Hidden by style filter: opacity(0)
    +
    + +
    +

    Table Elements

    + + + + + + + + + + +
    Normal CellNormal CellCell by visibility: collapse
    Cell hidden by row's visibility: collapse
    +
    +
    +
    + + + +
    +
    + +
    +

    Pointer Events: None

    +
    Element with pointer-events: none
    +
    + +
    +

    CSS Contain Property

    + + +
    + contain: layout +
    + + +
    + contain: paint +
    + + +
    + contain: strict +
    +
    + + + diff --git a/packages/driver/cypress/fixtures/visibility/empty.html b/packages/driver/cypress/fixtures/visibility/empty.html new file mode 100644 index 00000000000..f799f0cb387 --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/empty.html @@ -0,0 +1,4 @@ + + +
    + \ No newline at end of file diff --git a/packages/driver/cypress/fixtures/visibility/form-elements.html b/packages/driver/cypress/fixtures/visibility/form-elements.html new file mode 100644 index 00000000000..42928c9f2b4 --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/form-elements.html @@ -0,0 +1,65 @@ + + + + Form Elements Visibility Tests + + + + +

    Form Elements Visibility Tests

    + +
    +

    Select and Option Elements

    + + + +
    + +
    +

    Optgroup Elements

    + + + +
    + +
    +

    Options Outside Select

    + +
    + +
    +
    + +
    +

    Hidden Options Within Visible Select

    + +
    + +
    +

    Input Elements

    + + + +
    + + diff --git a/packages/driver/cypress/fixtures/visibility/overflow.html b/packages/driver/cypress/fixtures/visibility/overflow.html new file mode 100644 index 00000000000..100e127ecd4 --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/overflow.html @@ -0,0 +1,289 @@ + + + + Overflow Visibility Tests + + + + +

    Overflow Visibility Tests

    + +
    +

    Zero Dimensions with Overflow Hidden

    + + +
    +
    + Zero width ancestor, parent, self +
    +
    + + +
    +
    + Zero height ancestor, parent, self +
    +
    + + +
    +
    + Zero width ancestor, positive parent +
    +
    + + +
    +
    + Zero height ancestor, positive parent +
    +
    +
    + +
    +

    Text Content with Zero Dimensions

    + + +
    + lorem ipsum dolor sit amet +
    + + +
    + lorem ipsum dolor sit amet +
    + + +
    + +
    + + +
    +
    + ancestor width: 0 +
    +
    + + +
    +
    + ancestor height: 0 +
    +
    +
    + +
    +

    Positive Dimensions with Overflow Hidden

    + + +
    +
    + ancestor with size, parent with size & overflow: hidden +
    +
    + + +
    +
    + positive ancestor, zero parent +
    +
    +
    + +
    +

    Overflow Auto with Zero Dimensions

    + + +
    + parent no size, overflow: auto +
    +
    + +
    +

    Mixed Dimension Scenarios

    + + +
    Zero width
    +
    Has text content
    +
    + + +
    +
    Has text content
    +
    + + +
    +
    +
    +
    +
    +

    Overflow Hidden scenarios

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Overflow Y Hidden scenarios

    +
    +
    +
    +
    +
    +
    +
    +

    Overflow X Hidden scenarios

    +
    +
    +
    +
    +
    +
    + +
    +

    Overflow Auto Scenarios

    + + +
    +
    + out of bounds, parent wide, ancestor overflow: auto +
    +
    + + +
    + parent no size, overflow: auto +
    +
    + +
    +

    Overflow Scroll Scenarios

    + + +
    +
    +
    + out of scrolling bounds, position: absolute +
    +
    +
    +
    + +
    +

    Overflow with Relative Positioning

    + + +
    +
    + out of bounds, position: relative +
    +
    + + +
    +
    + out of bounds but visible, position: relative +
    +
    +
    + +
    +

    Overflow with Flex Container

    + + +
    +
    red
    +
    green
    +
    blue
    +
    +
    + +
    +

    Complex Overflow Scenarios

    + + +
    +
    +
    +
    +

    Example

    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    + +
    +

    Clip-Path Scenarios (note: legacy mode does not support clip-path)

    + + +
    + clip-path child of polygon that clips everything +
    + + +
    + clip-path +
    + + +
    + clipped content +
    + + +
    + visible content +
    + + +
    + elliptical content +
    + + +
    + path content +
    + + +
    clipped by clip: rect(0,0,0,0)
    + + +
    masked by CSS mask
    +
    + +
    +

    Viewport Scenarios

    +
    Element outside viewport
    +
    Position fixed element outside of viewport
    + +
    + + diff --git a/packages/driver/cypress/fixtures/visibility/positioning.html b/packages/driver/cypress/fixtures/visibility/positioning.html new file mode 100644 index 00000000000..03187ae62ed --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/positioning.html @@ -0,0 +1,135 @@ + + + + Positioning Visibility Tests + + + + +

    Positioning Visibility Tests

    +
    +

    Positioning with Zero Dimensions

    +
    + Child with absolute positioning +
    +
    + +
    +

    Fixed Positioning with Zero Dimensions

    + + + + +
    +
    + Child with position: fixed +
    +
    +
    + +
    +

    Static ancestor with fixed positioned descendant

    + +
    +
    + +
    +
    +
    + +
    + + +
    underneath
    +
    on top
    +
    + +
    +

    Static parent with a fixed child

    + + +
    + +
    +
    +
    + +
    off screen
    + + +
    + child pointer-events: none +
    +
    +
    +

    Parent with pointer-events: none

    + +
    + parent pointer-events: none +
    + + +
    + parent pointer-events: none +
    +
    covering the element with pointer-events: none
    + +
    +
    +

    Position Absolute Scenarios

    + + +
    +
    + position: absolute +
    +
    + + +
    +
    + no width, descendant position: absolute +
    +
    + + +
    +
    + parent position: absolute +
    +
    +
    + +
    +

    Position Sticky Scenarios

    + + +
    + +
    +
    + +
    +

    Positioning Cousin Coverage

    + + +
    +
    + Target element +
    +
    Covering cousin
    +
    +
    + +
    +

    Z-Index Coverage

    + + +
    +
    Lower z-index element
    +
    Higher z-index element
    +
    +
    + + + diff --git a/packages/driver/cypress/fixtures/visibility/style.css b/packages/driver/cypress/fixtures/visibility/style.css new file mode 100644 index 00000000000..e95a616f443 --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/style.css @@ -0,0 +1,21 @@ +body { margin: 20px; font-family: Arial, sans-serif; } +.test-section { display:none; margin: 20px 0; padding: 10px; border: 1px solid #ccc; } +.test-section h3 { margin-top: 0; } +.test-section.active { display: block; } +table { border-collapse: collapse; margin: 10px 0; } +td, th { border: 1px solid #ccc; padding: 5px; } +nav { + display: block; +} +nav li { + display: inline-block; + list-style: none; +} + +[cy-expect="hidden"], [cy-legacy-expect="hidden"], [cy-fast-expect="hidden"] { + background-color: lightcoral; +} + +[cy-expect="visible"], [cy-legacy-expect="visible"], [cy-fast-expect="visible"] { + background-color: lightgreen; +} diff --git a/packages/driver/cypress/fixtures/visibility/table-visibility.html b/packages/driver/cypress/fixtures/visibility/table-visibility.html new file mode 100644 index 00000000000..0d533e0995c --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/table-visibility.html @@ -0,0 +1,77 @@ + + + + Table Visibility Tests + + + + +

    Table Visibility Tests

    + +
    +

    Visibility Collapse on Table Cells

    + + + + + + +
    Normal CellCollapsed CellAnother Normal Cell
    +
    + +
    +

    Visibility Collapse on Table Rows

    + + + + + + + + + + + + + +
    Normal RowNormal Row
    Hidden Row Cell 1Hidden Row Cell 2
    Another Normal RowAnother Normal Row
    +
    + +
    +

    Mixed Visibility States

    + + + + + + + + + + + + + + + + +
    NormalCollapsedNormal
    Hidden RowHidden Row CellHidden Row
    NormalNormalNormal
    +
    + +
    +

    Normal Table (All Visible)

    + + + + + + + + + + + +
    Normal Cell 1Normal Cell 2Normal Cell 3
    Normal Row 2Normal Row 2Normal Row 2
    +
    + + diff --git a/packages/driver/cypress/fixtures/visibility/transforms.html b/packages/driver/cypress/fixtures/visibility/transforms.html new file mode 100644 index 00000000000..9f2a0998740 --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/transforms.html @@ -0,0 +1,146 @@ + + + + Transform Visibility Tests + + + + +

    CSS Transform Scenarios

    + +
    +

    Scaling

    +
    + Scaling(0,0) +
    +
    + Scaling(1,1) +
    +
    Scaled
    + + +
    ScaleX(0)
    +
    ScaleY(0)
    +
    ScaleZ(0)
    + + +
    Multiple transforms with scale(0,0)
    +
    +
    +

    Translation

    +
    + Translation(-10px) +
    +
    + Translation(10px) +
    +
    Translated in 2 dimensions
    +
    Translated in 3rd dimension
    +
    Translated in 3 dimensions
    +
    Translated outside viewport
    +
    +
    +

    Rotation

    +
    Rotated on X axis
    +
    Rotated on Y axis
    +
    Rotated on Z axis
    +
    Rotated on 3 axes
    + + +
    RotateX(90deg)
    +
    RotateY(90deg)
    +
    RotateZ(90deg)
    + + +
    RotateX(90deg) rotateY(90deg)
    + + +
    Multiple with rotateX(90deg)
    +
    Multiple with rotateY(90deg)
    +
    Multiple with rotateX(90deg) rotateY(90deg)
    +
    +
    +

    Skew

    +
    Skewed on X axis
    +
    Skewed on Y axis
    +
    Skewed on 2 axes
    +
    +
    +

    Matrix

    +
    Matrix(1, 0, 0, 1, 0, 0)
    +
    Matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
    +
    +
    +

    Perspective

    +
    Perspective(100px)
    +
    Perspective(100px) translateZ(10px)
    +
    Perspective(100px) rotateX(45deg)
    +
    Perspective(100px) rotateY(45deg)
    +
    Perspective(100px) rotateZ(45deg)
    +
    +
    +

    Multiple

    +
    Multiple transforms
    +
    +
    +

    Multiple 3D

    +
    Multiple 3D transforms
    +
    + +
    +

    Transform Edge Cases

    + + +
    Translated with text
    +
    Skewed with text
    +
    Rotated with text
    + + +
    Text content
    +
    Text content
    + + +
    +

    Test

    +
    + + +
    +

    Test

    +
    +
    +

    Test

    +
    +
    +

    Test

    +
    +
    +

    Test

    +
    +
    +

    Test

    +
    +
    +

    Test

    +
    +
    + +
    +

    Backface Visibility

    +
    Backface hidden on X axis
    +
    Backface hidden on Y axis
    +
    Backface hidden on Z axis
    +
    Backface hidden on 3 axes
    +
    Backface visible
    +
    +
    Backface hidden on Y axis
    +
    +
    +
    RotateY(45deg) with visible backface and preserve-3d
    +
    Backface hidden on Y axis
    + +
    Backface visible on Y axis
    +
    +
    + + \ No newline at end of file diff --git a/packages/driver/cypress/fixtures/visibility/ux.js b/packages/driver/cypress/fixtures/visibility/ux.js new file mode 100644 index 00000000000..9810270f0e7 --- /dev/null +++ b/packages/driver/cypress/fixtures/visibility/ux.js @@ -0,0 +1,39 @@ +document.addEventListener('DOMContentLoaded', () => { + const sections = document.querySelectorAll('.test-section').values().map((el) => { + return { + label: el.querySelector('h3')?.textContent ?? el.getAttribute('cy-section'), + section: el.getAttribute('cy-section'), + } + }) + const nav = document.createElement('nav') + + const list = document.createElement('ul') + + nav.appendChild(list) + sections.forEach(({ label, section }) => { + const li = document.createElement('li') + const a = document.createElement('a') + + a.href = `#${section}` + a.textContent = label + li.appendChild(a) + list.appendChild(li) + }) + + document.body.appendChild(nav) +}) + +document.addEventListener('click', (ev) => { + // eslint-disable-next-line no-console + console.log('click', ev.target) + const [, section] = ev.target?.href?.split('#') ?? [] + + if (!section) return + + document.querySelectorAll('.test-section.active').forEach((el) => { + el.classList.remove('active') + }) + + document.querySelector(`[cy-section="${section}"]`)?.classList.add('active') + document.querySelector(`[cy-section="${section}"]`)?.scrollIntoView() +}) diff --git a/packages/driver/src/dom/jquery.ts b/packages/driver/src/dom/jquery.ts index e39b8366725..186375864db 100644 --- a/packages/driver/src/dom/jquery.ts +++ b/packages/driver/src/dom/jquery.ts @@ -2,7 +2,7 @@ import $ from 'jquery' import _ from 'lodash' // wrap the object in jquery -const wrap = (obj) => { +export const wrap = (obj: T): JQuery => { return $(obj) } @@ -12,7 +12,7 @@ const query = (selector, context) => { } // pull out the raw elements if this is wrapped -const unwrap = function (obj) { +export const unwrap = function (obj) { if (isJquery(obj)) { // return an array of elements return obj.toArray() @@ -21,7 +21,7 @@ const unwrap = function (obj) { return obj } -const isJquery = (obj) => { +export const isJquery = (obj: any): obj is JQuery => { let hasJqueryProperty = false try { diff --git a/packages/driver/src/dom/visibility.ts b/packages/driver/src/dom/visibility.ts index abbf8476259..55d92358be8 100644 --- a/packages/driver/src/dom/visibility.ts +++ b/packages/driver/src/dom/visibility.ts @@ -4,25 +4,31 @@ import $document from './document' import $elements from './elements' import $coordinates from './coordinates' import * as $transform from './transform' - const { isElement, isBody, isHTML, isOption, isOptgroup, getParent, getFirstParentWithTagName, isAncestor, isChild, getAllParents, isDescendent, isUndefinedOrHTMLBodyDoc, elOrAncestorIsFixedOrSticky, isDetached, isFocusable, stringify: stringifyElement } = $elements - +import { fastIsHidden } from './visibility/fastIsHidden' const fixedOrAbsoluteRe = /(fixed|absolute)/ const OVERFLOW_PROPS = ['hidden', 'clip', 'scroll', 'auto'] +const { wrap } = $jquery + const isVisible = (el) => { return !isHidden(el, 'isVisible()') } -const { wrap } = $jquery - // TODO: we should prob update dom // to be passed in $utils as a dependency // because of circular references // the ignoreOpacity option exists for checking actionability // as elements with `opacity: 0` are hidden yet actionable + const isHidden = (el, methodName = 'isHidden()', options = { checkOpacity: true }) => { + if (Cypress.config('experimentalFastVisibility')) { + ensureEl(el, methodName) + + return fastIsHidden(el, options) + } + if (isStrictlyHidden(el, methodName, options, isHidden)) { return true } diff --git a/packages/driver/src/dom/visibility/MIGRATION_GUIDE.md b/packages/driver/src/dom/visibility/MIGRATION_GUIDE.md new file mode 100644 index 00000000000..93013e9829f --- /dev/null +++ b/packages/driver/src/dom/visibility/MIGRATION_GUIDE.md @@ -0,0 +1,330 @@ +# Fast Visibility Algorithm Migration Guide + +_(note: this file or content similar to it will be added to cy docs, when language / details are finalized)_ + +## Overview + +The fast visibility algorithm is designed to resolve severe performance issues with DOM visibility detection while maintaining compatibility with existing test code. This guide helps you understand the differences between legacy and fast algorithms and provides solutions for any compatibility issues that arise. + +## Why Migrate? + +### Performance Benefits +- **Significantly faster** visibility calculations for complex DOM structures +- **Reduced CPU usage** during test execution +- **Better scalability** for applications with many DOM elements +- **Improved test reliability** with more accurate geometric visibility detection + +### When to Enable Fast Visibility +Enable fast visibility if you experience: +- Slow test execution with complex DOM structures +- High CPU usage during visibility checks +- Timeouts or flaky tests related to element visibility +- Performance degradation with many DOM elements + +### When NOT to Enable Fast Visibility +**Do NOT enable fast visibility if:** +- Your tests rely heavily on Shadow DOM elements +- You have comprehensive Shadow DOM test coverage +- Your application uses Shadow DOM extensively +- You rely extensively on asserting the visibility of elements that are outside the browser's viewport +- You rely on asserting the visibility state of elements that have `pointer-events:none` + +**Current Limitations**: +- The fast visibility algorithm does not yet fully support Shadow DOM elements. Tests that interact with Shadow DOM elements may fail or behave incorrectly. +- The fast visibility algorithm considers any element that is outside of the browser's viewport as hidden. While this is an incompatibility with the legacy visibility approach, it is aligned with the visibility behavior of elements that are scrolled out of view within a scrollable container. + +## Algorithm Differences + +While comprehensive, this list may not be complete. Additional discrepancies may be found and added to our test cases as they become known. + +| Test Section and Fixture | Test Case Label | "Legacy" Considers Visible? | "Fast" Considers Visible? | Correct Behavior | Notes | +|---------|----------------|---------------------------|------------------------|------------------|-------| +| **[transforms](../../../cypress/fixtures/visibility/transforms.html)** | Perspective with rotateY | ✅ Yes | ❌ No | ✅ Yes | Certain transforms can cause elements to not be considered visible to the fast visibility algorithm if they transform an element in such a way that it is not present at the points that are sampled | +| **[positioning-with-zero-dimensions](../../../cypress/fixtures/visibility/positioning.html)** | Zero dimensions parent with absolute positioned child | ✅ Yes | ❌ No | ❌ No | Element that has `width: 0; height: 0` and an absolutely positioned child | +| **[fixed-positioning-with-zero-dimensions](../../../cypress/fixtures/visibility/positioning.html)** | Zero dimensions ancestor with fixed positioned child | ✅ Yes | ❌ No | ❌ No | Element that has `width: 0; height: 0; position: relative` and a fixed positioned grand-child. | +| **[fixed-positioning-with-zero-dimensions](../../../cypress/fixtures/visibility/positioning.html)** | Parent under zero dimensions ancestor | ✅ Yes | ❌ No | ❌ No | Statically positioned element that is a child of an element with zero dimension, and whose only child is position:fixed | +| **[position-absolute-scenarios](../../../cypress/fixtures/visibility/positioning.html)** | Normal parent with absolute positioned child | ❌ No | ✅ Yes | ❌ No | Element that is hidden by its parents overflow, but has an absolutely positioned child element. | +| **[position-absolute-scenarios](../../../cypress/fixtures/visibility/positioning.html)** | Parent container for absolute child | ❌ No | ✅ Yes | ❌ No | Container element for the absolute positioned child | +| **[position-absolute-scenarios](../../../cypress/fixtures/visibility/positioning.html)** | Normal ancestor with absolute positioned descendant | ❌ No | ✅ Yes | ✅ Yes | Ancestor has `width: 0; height: 100px; overflow: hidden` with absolute positioned descendant | +| **[positioning](../../../cypress/fixtures/visibility/positioning.html)** | Covered by an absolutely positioned cousin | ✅ Yes | ❌ No | ❌ No | Element covered by a sibling with `position: absolute` and higher z-index | +| **[overflow-auto-with-zero-dimensions](../../../cypress/fixtures/visibility/overflow.html)** | Zero dimensions with overflow auto | ✅ Yes | ❌ No | ❌ No | Element with `width: 0; height: 0px; overflow: auto`, but no absolutely positioned children | +| **[overflow-scroll-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Parent with clip-path polygon that clips everything | ✅ Yes | ❌ No | ❌ No | `clip-path: polygon(0 0, 0 0, 0 0, 0 0)` | +| **[overflow-scroll-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element outside clip-path polygon | ✅ Yes | ❌ No | ❌ No | Child element of polygon clip-path parent | +| **[overflow-scroll-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element outside clip-path inset | ✅ Yes | ❌ No | ❌ No | Child element of `clip-path: inset(25% 25% 25% 25%)` | +| **[viewport-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Absolutely positioned element outside of the viewport | ✅ Yes | ❌ No | ❌ No | Elements that are outside of the viewport must be scrolled to before the fast algorithm will consider them visible. This is aligned with scroll-container visibility. | +| **[z-index-coverage](../../../cypress/fixtures/visibility/positioning.html)** | Covered by higher z-index element | ✅ Yes | ❌ No | ❌ No | Element covered by another element with higher z-index | +| **[clip-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Element clipped by CSS clip property | ✅ Yes | ❌ No | ❌ No | Element with `clip: rect(0, 0, 0, 0)` or similar clipping | +| **[transform](../../../cypress/fixtures/visibility/transforms.html)** | Element transformed outside viewport | ✅ Yes | ❌ No | ❌ No | Element with `transform: translateX(-9999px)` or similar | +| **[contain](../../../cypress/fixtures/visibility/basic-css-properties.html)** | Element with CSS contain:paint property | ✅ Yes | ❌ No | ❌ No | Element positioned outside of a parent that has the `contain: paint` property | +| **[backdrop-filter](../../../cypress/fixtures/visibility/basic-css-properties.html)** | Element covered by an element with with backdrop-filter opacity(0) | ✅ Yes | ❌ No | ❌ No| | +| **[pointer-events-none](../../../cypress/fixtures/visibility/basic-css-properties.html)** | Element with pointer-events: none | ✅ Yes | ❌ No | ❌ No | Element has dimensions and is visible to the user, but cannot receive pointer events. | + +## Migration Steps + +### Step 1: Enable Fast Visibility +```javascript +// cypress.config.js +module.exports = { + e2e: { + experimentalFastVisibility: true + } +} +``` + +**Note**: Ensure your application under test does not extensively use custom Shadow DOM elements, as the fast visibility algorithm does not yet support Shadow DOM. + +### Step 2: Run Your Tests +Run your existing test suite to identify any failures: + +```bash +npm run test +``` + +### Step 3: Analyze Failures +Look for tests that fail with visibility-related assertions. Common patterns: + +```javascript +// These assertions may behave differently +.should('be.visible') +.should('not.be.hidden') +.is(':visible') +.is(':hidden') +``` + +### Step 4: Disable Fast Visibility for Failing Specs +For specs that fail due to Shadow DOM or other incompatibilities, disable fast visibility: + +```javascript +// In failing spec files +describe('My Test Suite', { experimentalFastVisibility: false }, () => { + it('should work with legacy visibility', () => { + // Your test here + }) +}) +``` + +This allows you to gradually migrate specs while keeping failing ones working. + +### Step 5: Fix Tests with Visibility-Related Failures +For specs that fail due to visibility algorithm differences, update the test expectations to match the correct behavior (see solutions below). + +## Common Compatibility Issues and Solutions + +### Issue 1: Elements Previously Considered Visible Are Now Hidden + +**Problem**: Test expects element to be visible, but fast algorithm correctly identifies it as hidden. + +**Solution**: Update your test expectations to match the correct behavior: + +```javascript +// Before (incorrect expectation) +cy.get('.rotated-element').should('be.visible') + +// After (correct expectation) +cy.get('.rotated-element').should('be.hidden') +``` + +**Note**: If the element should be visible, fix the CSS in your application code, not in the test. Tests should verify the actual behavior of your application. + +### Issue 2: Elements Outside Viewport + +**Problem**: Elements positioned outside the viewport are now correctly identified as hidden. + +**Solution**: Scroll the element into view before testing: + +```javascript +// Before +cy.get('.off-screen-element').should('be.visible') + +// After +cy.get('.off-screen-element').scrollIntoView().should('be.visible') +``` + +### Issue 3: Covered Elements + +**Problem**: Elements covered by other elements are now correctly identified as hidden. + +**Solution**: Test the covering element instead, or test the user interaction that reveals the covered element: + +```javascript +// Before +cy.get('.covered-element').should('be.visible') + +// After - test the covering element +cy.get('.covering-element').should('be.visible') + +// Or test the user action that reveals the element +cy.get('.toggle-button').click() +cy.get('.covered-element').should('be.visible') +``` + +**Note**: Don't modify the DOM structure in tests. Test the actual user interactions that reveal hidden elements. + +### Issue 4: Zero-Dimension Containers + +**Problem**: Containers with zero dimensions are now correctly identified as hidden, but child elements may still be visible. + +**Solution**: Test the child element instead of the container: + +```javascript +// Before - testing the container +cy.get('.zero-dimension-container').should('be.visible') + +// After - test the child element that should be visible +cy.get('.zero-dimension-container .child-element').should('be.visible') + +// Or test the user action that gives the container dimensions +cy.get('.expand-button').click() +cy.get('.zero-dimension-container').should('be.visible') +``` + +**Note**: If the container should have dimensions, fix this in your application code. If testing child elements, assert on the child elements directly. + +### Issue 5: Clipped Elements + +**Problem**: Elements clipped by CSS are now correctly identified as hidden. + +**Solution**: Update your test expectations or test the user interaction that reveals the element: + +```javascript +// Before +cy.get('.clipped-element').should('be.visible') + +// After - test that the element is hidden (correct behavior) +cy.get('.clipped-element').should('be.hidden') + +// Or test the user action that reveals the element +cy.get('.show-content-button').click() +cy.get('.clipped-element').should('be.visible') + +// Or test the container that controls the clipping +cy.get('.clipping-container').should('be.visible') +``` + +**Note**: If elements should be visible, fix the clipping in your application code. Tests should verify the actual user experience. + +### Issue 6: Pointer Events + +**Problem**: Elements with `pointer-events: none` or that have parents with `pointer-events:none` may be detected as hidden when they are visible. + +**Solution**: Do not assert visibility on elements with `pointer-events:none`, as they cannot be interacted with. + +### Issue 7: Shadow DOM incompatibilities + +**Problem:**: Elements inside shadow DOMs may not be detected properly as visible or hidden. + +**Solution:**: Test shadow dom components in isolation with component testing, and only test if the public interface of the shadow dom component is visible. You wouldn't assert on the visibility of the browser's default video play controls by querying its shadow dom: you would assert on the properties of the video element itself. + +## Rollback Plan + +If you encounter issues that can't be easily resolved: + +### Temporary Rollback +```javascript +// cypress.config.js +module.exports = { + e2e: { + experimentalFastVisibility: false // Disable fast visibility + } +} +``` + +### Gradual Migration +Enable fast visibility for specific test suites: + +```javascript +// Enable only for performance-critical tests +describe('Performance Tests', { experimentalFastVisibility: true }, () => { + it('should handle complex DOM efficiently', () => { + // Your performance tests here + }) +}) +``` + +## Best Practices + +### 1. Never Modify the Application Under Test (AUT) +**❌ Bad Practice**: Modifying CSS or DOM structure in tests +```javascript +// DON'T DO THIS - Modifying the AUT +cy.get('.element').invoke('css', 'display', 'block') +cy.get('.element').invoke('remove') +cy.get('.element').invoke('css', 'transform', 'none') +``` + +**✅ Good Practice**: Test the actual application behavior +```javascript +// DO THIS - Test real user interactions +cy.get('.toggle-button').click() +cy.get('.element').should('be.visible') +``` + +### 2. Test Element Functionality, Not Just Visibility +```javascript +// Good: Test if element is interactive +cy.get('.button').should('be.enabled').click() + +// Avoid: Testing visibility alone +cy.get('.button').should('be.visible') +``` + +### 3. Use Semantic Selectors +```javascript +// Good: Use semantic selectors +cy.get('[data-testid="submit-button"]').should('be.visible') + +// Avoid: Relying on CSS classes that might change +cy.get('.btn-primary').should('be.visible') +``` + +### 4. Test User Interactions +```javascript +// Good: Test user interactions +cy.get('.modal').should('be.visible') +cy.get('.modal .close-button').click() +cy.get('.modal').should('not.exist') + +// Avoid: Testing CSS properties directly +cy.get('.modal').should('have.css', 'display', 'block') +``` + +## Troubleshooting + +### Common Error Messages + +**"Element is not visible"** +- Check if element is covered by another element +- Verify element is not positioned outside viewport +- Ensure element has proper dimensions + +**"Element should be hidden but is visible"** +- Check for CSS transforms that might hide the element +- Verify element is not clipped by CSS +- Ensure element is not covered by other elements + +**Shadow DOM Related Errors** +- If you see errors with Shadow DOM elements, disable fast visibility +- Shadow DOM support is not yet available in the fast algorithm +- Use legacy algorithm for Shadow DOM testing until support is added + +### Debug Visibility Issues +```javascript +// Debug element visibility +cy.get('.element').then(($el) => { + console.log('Element dimensions:', $el[0].getBoundingClientRect()) + console.log('Element styles:', $el[0].computedStyleMap()) + console.log('Element visibility:', Cypress.dom.isVisible($el[0])) +}) +``` + +## Final Words + +The fast visibility algorithm is an experimental feature that provides significant performance improvements for applications with complex DOM structures. While we try to align compatibility with the legacy algorithm, we err on the side of accuracy: the fast algorithm provides more geometrically correct visibility detection. + +**Important Notes:** +- This is an **experimental feature** - if it proves beneficial, we may invest time in supporting Shadow DOM +- **Shadow DOM support is not yet available** - disable fast visibility for specs that rely heavily on Shadow DOM +- **Some compatibility differences exist** - when tests fail, the fast algorithm is likely correct and tests should be updated +- **Performance benefits are significant** - especially for applications with many DOM elements or complex layouts + +By following this migration guide, you can resolve compatibility issues and benefit from faster, more accurate visibility detection while understanding the current limitations. diff --git a/packages/driver/src/dom/visibility/fastIsHidden.ts b/packages/driver/src/dom/visibility/fastIsHidden.ts new file mode 100644 index 00000000000..19bf988b677 --- /dev/null +++ b/packages/driver/src/dom/visibility/fastIsHidden.ts @@ -0,0 +1,46 @@ +import $elements from '../elements' +import { unwrap, isJquery } from '../jquery' +const { isOption, isOptgroup, isBody, isHTML } = $elements +import { VisibilityTests, VisibilityResultKind } from './visibility-tests/VisibilityTest' + +interface VisibilityOptions { + checkOpacity: boolean +} + +export function fastIsHidden (subject: JQuery | HTMLElement, options: VisibilityOptions = { checkOpacity: true }): boolean { + if (isBody(subject) || isHTML(subject)) { + return false + } + + if (isJquery(subject)) { + const subjects = unwrap(subject) as HTMLElement | HTMLElement[] + + if (Array.isArray(subjects)) { + return subjects.some((subject: HTMLElement) => fastIsHidden(subject, options)) + } + + return fastIsHidden(subjects, options) + } + + if (isOption(subject) || isOptgroup(subject)) { + if (subject.hasAttribute('style') && subject.style.display === 'none') { + return true + } + + const select = subject.closest('select') + + if (select) { + return fastIsHidden(select, options) + } + } + + for (const test of VisibilityTests) { + const result = test(subject, options) + + if (result) { + return result.kind === VisibilityResultKind.HIDDEN + } + } + + return false +} diff --git a/packages/driver/src/dom/visibility/visibility-tests/VisibilityTest.ts b/packages/driver/src/dom/visibility/visibility-tests/VisibilityTest.ts new file mode 100644 index 00000000000..3c12c097643 --- /dev/null +++ b/packages/driver/src/dom/visibility/visibility-tests/VisibilityTest.ts @@ -0,0 +1,25 @@ +import { dimensions } from './dimensions' +import { domVisibility } from './domVisibility' +import { pointSampling } from './pointSampling' + +export const VisibilityResultKind = Object.freeze({ + VISIBLE: 'visible', + HIDDEN: 'hidden', +}) + +type VisibilityTestResultKind = typeof VisibilityResultKind[keyof typeof VisibilityResultKind] + +export interface VisibilityTestResult { + kind: VisibilityTestResultKind + reason: string +} + +export interface VisibilityTest { + (el: HTMLElement): VisibilityTestResult | undefined +} + +export const VisibilityTests: VisibilityTest[] = [ + dimensions, + domVisibility, + pointSampling, +] diff --git a/packages/driver/src/dom/visibility/visibility-tests/dimensions.ts b/packages/driver/src/dom/visibility/visibility-tests/dimensions.ts new file mode 100644 index 00000000000..b32f74e14d3 --- /dev/null +++ b/packages/driver/src/dom/visibility/visibility-tests/dimensions.ts @@ -0,0 +1,8 @@ +import { getBoundingClientRect } from './getBoundingClientRect' +import { VisibilityResultKind, VisibilityTestResult } from './VisibilityTest' + +export function dimensions (subject: HTMLElement, options: VisibilityOptions): VisibilityTestResult | undefined { + const boundingRect = getBoundingClientRect(subject) + + return boundingRect.width === 0 || boundingRect.height === 0 ? VisibilityResultKind.HIDDEN : undefined +} diff --git a/packages/driver/src/dom/visibility/visibility-tests/domVisibility.ts b/packages/driver/src/dom/visibility/visibility-tests/domVisibility.ts new file mode 100644 index 00000000000..00b54b2db36 --- /dev/null +++ b/packages/driver/src/dom/visibility/visibility-tests/domVisibility.ts @@ -0,0 +1,9 @@ +import { VisibilityResultKind } from './VisibilityTest' + +export function domVisibility (subject: HTMLElement, options: VisibilityOptions): VisibilityTestResult | undefined { + return subject.checkVisibility({ + contentVisibilityAuto: true, + opacityProperty: options.checkOpacity, + visibilityProperty: true, + }) ? VisibilityResultKind.VISIBLE : undefined +} diff --git a/packages/driver/src/dom/visibility/visibility-tests/getBoundingClientRect.ts b/packages/driver/src/dom/visibility/visibility-tests/getBoundingClientRect.ts new file mode 100644 index 00000000000..cdb9a622c0b --- /dev/null +++ b/packages/driver/src/dom/visibility/visibility-tests/getBoundingClientRect.ts @@ -0,0 +1,5 @@ +import { memoize } from './memoize' + +export const getBoundingClientRect = memoize( + (el: HTMLElement) => el.getBoundingClientRect(), +) diff --git a/packages/driver/src/dom/visibility/visibility-tests/memoize.ts b/packages/driver/src/dom/visibility/visibility-tests/memoize.ts new file mode 100644 index 00000000000..0f4e4ced626 --- /dev/null +++ b/packages/driver/src/dom/visibility/visibility-tests/memoize.ts @@ -0,0 +1,44 @@ +interface MemoizeOptions< + IndexFn extends (...args: any[]) => any, + Key = IndexFn extends (...args: any[]) => infer I ? I : never +> { + ttl?: number + key?: (...args: Parameters) => Key +} + +export function memoize any> ( + fn: T, + options?: MemoizeOptions): T { + const ttl = options?.ttl ?? 100 + const keyFn = options?.key ?? ((args: Parameters) => args[0]) + + const cache = new Map, { result: ReturnType, timestamp: number }>() + + let sweepTimeout: NodeJS.Timeout | undefined + + const sweep = () => { + sweepTimeout = undefined + cache.forEach((value, key) => { + if (value.timestamp < Date.now() - ttl) { + cache.delete(key) + } + }) + } + + return (...args: any[]) => { + sweepTimeout = sweepTimeout || setTimeout(sweep, ttl) + + const key = keyFn(args) + const cached = cache.get(key) + + if (cached && cached.timestamp > Date.now() - ttl) { + return cached.result + } + + const result = fn(...args) + + cache.set(key, { result, timestamp: Date.now() }) + + return result + } +} diff --git a/packages/driver/src/dom/visibility/visibility-tests/pointSampling.ts b/packages/driver/src/dom/visibility/visibility-tests/pointSampling.ts new file mode 100644 index 00000000000..867e6917155 --- /dev/null +++ b/packages/driver/src/dom/visibility/visibility-tests/pointSampling.ts @@ -0,0 +1,67 @@ +import { getBoundingClientRect } from './getBoundingClientRect' +import { memoize } from './memoize' +import { VisibilityResultKind } from './VisibilityTest' + +const MAX_DEPTH = 2 +const MIN_SIZE = 1 + +const SUBDIVIDE_PATTERN = [ + [0, 0], + [1, 0], + [0, 1], + [1, 1], +] as const + +const SAMPLE_POINTS = [ + ...SUBDIVIDE_PATTERN, + [0.5, 0.5], +] as const + +export function pointSampling (subject: HTMLElement, options: VisibilityOptions): VisibilityTestResult | undefined { + const boundingRect = getBoundingClientRect(subject) + + return sampleDomRectPoints(subject, boundingRect) ? VisibilityResultKind.VISIBLE : undefined +} + +function sampleDomRectPoints (el: HTMLElement, rect: DOMRect, currentDepth: number = 0): boolean { + if (currentDepth >= MAX_DEPTH) { + return false + } + + const { x, y, width, height } = rect + + const samples = SAMPLE_POINTS.map(([pX, pY]) => [x + pX * width, y + pY * height]) + + if (samples.some(([x, y]) => visibleAtPoint(el, x, y))) { + return true + } + + const subRects = subDivideRect(rect) + + debug('subRects', subRects) + + return subRects.some((subRect: DOMRect) => { + return visibleToUser(el, subRect, maxDepth, currentDepth + 1) + }) +} + +function subDivideRect ({ x, y, width, height }: DOMRect): DOMRect[] { + return SUBDIVIDE_PATTERN.map(([dX, dY]) => { + const validDim = width / 2 > MIN_SIZE && height / 2 > MIN_SIZE + + return validDim ? DOMRect.fromRect({ + x: x + dX * (width / 2), + y: y + dY * (height / 2), + width: width / 2, + height: height / 2, + }) : undefined + }).filter(Boolean) +} + +const visibleAtPoint = memoize(function (el: HTMLElement, x: number, y: number): boolean { + const elAtPoint = el.ownerDocument.elementFromPoint(x, y) + + debug('visibleAtPoint', el, elAtPoint) + + return Boolean(elAtPoint) && (elAtPoint === el || el.contains(elAtPoint)) +}) diff --git a/packages/driver/src/dom/visibility/visibleToUser.ts b/packages/driver/src/dom/visibility/visibleToUser.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/driver/test/unit/dom/visibility/memoize.spec.ts b/packages/driver/test/unit/dom/visibility/memoize.spec.ts new file mode 100644 index 00000000000..e6237dc24c7 --- /dev/null +++ b/packages/driver/test/unit/dom/visibility/memoize.spec.ts @@ -0,0 +1,64 @@ +import { memoize } from '@packages/driver/src/dom/visibility/memoize' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +describe('memoize', () => { + let fn: vi.Mock + + beforeEach(() => { + vi.useFakeTimers() + fn = vi.fn().mockReturnValue('output') + }) + + afterEach(() => { + vi.clearAllTimers() + vi.useRealTimers() + vi.restoreAllMocks() + }) + + it('should memoize the result of the function', () => { + const memoizedFn = memoize(fn, { ttl: 1000 }) + const result = memoizedFn('input') + + expect(fn).toHaveBeenCalledWith('input') + expect(result).toBe('output') + }) + + it('should return the cached result if the function is called with the same arguments', () => { + const memoizedFn = memoize(fn, { ttl: 1000 }) + const result1 = memoizedFn(1) + const result2 = memoizedFn(1) + + expect(fn).toHaveBeenCalledWith(1) + expect(fn).toHaveBeenCalledOnce() + expect(result1).toBe('output') + expect(result2).toBe('output') + }) + + it('should sweep the cache after the ttl', () => { + const memoizedFn = memoize(fn, { ttl: 1000 }) + const result1 = memoizedFn('input') + + expect(fn).toHaveBeenCalledWith('input') + expect(fn).toHaveBeenCalledTimes(1) + expect(result1).toBe('output') + + vi.advanceTimersByTime(1000) + + const result3 = memoizedFn('input') + + expect(fn).toHaveBeenCalledWith('input') + expect(fn).toHaveBeenCalledTimes(2) + expect(result3).toBe('output') + }) + + it('supports complex arguments with a custom key function', () => { + const memoizedFn = memoize(fn, { ttl: 1000, key: (args) => JSON.stringify(args) }) + const result1 = memoizedFn({ a: 'input-1', b: 'input-2' }) + const result2 = memoizedFn({ a: 'input-1', b: 'input-2' }) + + expect(fn).toHaveBeenCalledWith({ a: 'input-1', b: 'input-2' }) + expect(fn).toHaveBeenCalledTimes(1) + expect(result1).toBe('output') + expect(result2).toBe('output') + }) +})