diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 6ea19b1cead..290c8f809ea 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 1d91f16da6c..765ffe0f1bd 100644 --- a/packages/driver/cypress/e2e/dom/visibility.cy.ts +++ b/packages/driver/cypress/e2e/dom/visibility.cy.ts @@ -7,7 +7,8 @@ describe('src/cypress/dom/visibility', { 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') ?? el.getAttribute('cy-legacy-expect') + const breakingChangeExpectedProp = Cypress.config('experimentalFastVisibility') ? 'cy-fast-expect' : 'cy-legacy-expect' + const expected = el.getAttribute('cy-expect') ?? el.getAttribute(breakingChangeExpectedProp) if (!expected) { throw new Error(`Expected attribute 'cy-expect' or 'cy-legacy-expect' not found on test case_ element ${el.outerHTML}`) @@ -51,108 +52,114 @@ describe('src/cypress/dom/visibility', { } } - beforeEach(() => { - cy.visit('/fixtures/generic.html') - }) + 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) + } + + expect(fn()).to.be.false + }) - it('returns false el is not scrollable', function () { - const noScroll = this.add(`\ + 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. @@ -160,201 +167,205 @@ 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) - } + const fn = () => { + return dom.isScrollable(forcedScroll) + } - expect(fn()).to.be.true - }) - }) - - describe('visibility scenarios', () => { - describe('html and body overrides', () => { - beforeEach(() => { - cy.visit('/fixtures/empty.html') + expect(fn()).to.be.true + }) }) - describe('when display none', () => { - beforeEach(() => { - cy.get('html').then(($el) => { - $el.css('display', 'none') + describe('visibility scenarios', () => { + describe('html and body overrides', () => { + beforeEach(() => { + cy.visit('/fixtures/empty.html') }) - cy.get('body').then(($el) => { - $el.css('display', 'none') - }) - }) + describe('when display none', () => { + beforeEach(() => { + cy.get('html').then(($el) => { + $el.css('display', 'none') + }) - it('is always visible', () => { - expect(cy.$$('html').is(':hidden')).to.be.false - expect(cy.$$('html').is(':visible')).to.be.true + cy.get('body').then(($el) => { + $el.css('display', 'none') + }) + }) - expect(cy.$$('html')).not.to.be.hidden - expect(cy.$$('html')).to.be.visible + it('is always visible', () => { + expect(cy.$$('html').is(':hidden')).to.be.false + expect(cy.$$('html').is(':visible')).to.be.true - cy.wrap(cy.$$('html')).should('not.be.hidden') - cy.wrap(cy.$$('html')).should('be.visible') - expect(cy.$$('body').is(':hidden')).to.be.false - expect(cy.$$('body').is(':visible')).to.be.true + 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 + cy.wrap(cy.$$('html')).should('not.be.hidden') + cy.wrap(cy.$$('html')).should('be.visible') + expect(cy.$$('body').is(':hidden')).to.be.false + expect(cy.$$('body').is(':visible')).to.be.true - cy.wrap(cy.$$('body')).should('not.be.hidden') - cy.wrap(cy.$$('body')).should('be.visible') - }) - }) + 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') + }) + }) - describe('when not display none', () => { - it('is visible', () => { - expect(cy.$$('html').is(':hidden')).to.be.false - expect(cy.$$('html').is(':visible')).to.be.true + describe('when not display none', () => { + it('is visible', () => { + 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 + 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') - expect(cy.$$('body').is(':hidden')).to.be.false - expect(cy.$$('body').is(':visible')).to.be.true + cy.wrap(cy.$$('html')).should('not.be.hidden') + cy.wrap(cy.$$('html')).should('be.visible') + 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 + 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') + cy.wrap(cy.$$('body')).should('not.be.hidden') + cy.wrap(cy.$$('body')).should('be.visible') + }) + }) }) - }) - }) - describe('basic CSS properties', () => { - beforeEach(() => { - cy.visit('/fixtures/visibility/basic-css-properties.html') - }) + describe('basic CSS properties', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/basic-css-properties.html') + }) - assertVisibilityForSections([ - 'visibility-property', - 'display-property', - 'opacity-property', - 'input-elements', - 'table-elements', - 'box-interactions', - ]) - }) + assertVisibilityForSections([ + 'visibility-property', + 'display-property', + 'opacity-property', + 'table-elements', + 'box-interactions', + ]) + }) - describe('form elements', () => { - beforeEach(() => { - cy.visit('/fixtures/visibility/form-elements.html') - }) + describe('form elements', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/form-elements.html') + }) - assertVisibilityForSections([ - 'select-and-option-elements', - 'optgroup-elements', - 'options-outside-select', - 'hidden-options-within-visible-select', - 'input-elements', - ]) - }) + assertVisibilityForSections([ + 'select-and-option-elements', + 'optgroup-elements', + 'options-outside-select', + 'hidden-options-within-visible-select', + 'input-elements', + ]) + }) - describe('overflow', () => { - beforeEach(() => { - cy.visit('/fixtures/visibility/overflow.html') - }) + describe('overflow', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/overflow.html') + }) - 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-path-scenarios', - ]) - }) + 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-path-scenarios', + ]) + }) - describe('positioning', () => { - beforeEach(() => { - cy.visit('/fixtures/visibility/positioning.html') - }) + describe('positioning', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/positioning.html') + }) - 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', - ]) - }) + 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', + ]) + }) - describe('transforms', () => { - beforeEach(() => { - cy.visit('/fixtures/visibility/transforms.html') - }) + describe('transforms', () => { + beforeEach(() => { + cy.visit('/fixtures/visibility/transforms.html') + }) - assertVisibilityForSections([ - 'scaling', - 'translation', - 'rotation', - 'skew', - 'matrix', - 'perspective', - 'multiple', - 'multiple-3d', - 'backface-visibility', - ]) + assertVisibilityForSections([ + 'scaling', + 'translation', + 'rotation', + 'skew', + 'matrix', + 'perspective', + 'multiple', + 'multiple-3d', + 'backface-visibility', + ]) + }) + }) }) - }) + } context('#getReasonIsHidden', () => { const reasonIs = ($el: JQuery, str: string) => { 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..8057c3e83dc --- /dev/null +++ b/packages/driver/cypress/e2e/memory/virtual-scroll-stress.cy.js @@ -0,0 +1,462 @@ +/** + * 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 + */ + +for (const experimentalFastVisibility of [true, false]) { + describe(`Virtual Scrolling Stress Tests, experimentalFastVisibility: ${experimentalFastVisibility}`, { + experimentalFastVisibility, // if this is set to false, this spec will crash the browser + }, () => { + const counts = [ + 100, + 1000, + 3992, + ...( + experimentalFastVisibility ? [ + 10000, + 100000, + ] : [] + ), + ] + + for (const count of counts) { + describe(`basic list (${count} count)`, () => { + beforeEach(() => { + cy.log(`loading basic list with ${count}items`) + cy.visit('/fixtures/virtual-scroll-stress-test.html') + cy.get('input[data-cy="item-count"]').clear().type(count) + cy.get('input[data-cy="list-id"]').clear().type('basic-list') + cy.get('select[data-cy="list-type"]').select('basic') + cy.get('button[data-cy="add-list"]').click() + }) + + it(`should load the virtual list (${count} count) without crashing`, () => { + cy.get('#basic-list .item') + .should('have.length.greaterThan', 0, { log: false }) + }) + + it('handles normal scrolling without crashing', () => { + cy.get('#basic-list').scrollTo(0, 1000) + cy.get('#basic-list').scrollTo(0, 2000) + cy.get('#basic-list').scrollTo(0, 5000) + }) + + it('handles rapid scrolling without crashing', () => { + cy.get('#basic-list').scrollTo(0, 100) + cy.get('#basic-list').scrollTo(0, 300) + cy.get('#basic-list').scrollTo(0, 600) + cy.get('#basic-list').scrollTo(0, 1000) + cy.get('#basic-list').scrollTo(0, 1500) + cy.get('#basic-list').scrollTo(0, 2000) + }) + + it('handles stress scrolling without crashing', () => { + cy.get('button[data-cy="stress-scroll"]').click() + cy.wait(2000) + cy.get('#basic-list').scrollTo(0, 10000) + }) + }) + + /* + describe('dynamic list', () => { + beforeEach(() => { + cy.get('input[data-cy="item-count"]').clear().type(i) + cy.get('input[data-cy="list-id"]').clear().type('dynamic-list') + cy.get('select[data-cy="list-type"]').select('dynamic') + cy.get('button[data-cy="add-list"]').click() + }) + }) + + describe('extreme list', () => { + beforeEach(() => { + cy.get('input[data-cy="item-count"]').clear().type(i) + cy.get('input[data-cy="list-id"]').clear().type('extreme-list') + cy.get('select[data-cy="list-type"]').select('extreme') + cy.get('button[data-cy="add-list"]').click() + }) + }) + + describe('multiple lists', () => { + beforeEach(() => { + cy.get('input[data-cy="item-count"]').clear().type(count) + cy.get('input[data-cy="list-id"]').clear().type('multiple-list') + cy.get('select[data-cy="list-type"]').select('multiple') + cy.get('button[data-cy="add-list"]').click() + }) + }) + */ + } + + // NOTE: we don't even need the more complex tests, the basic tests are enough to cause the crash + describe.skip('old tests', () => { + 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..629fbdedb6d --- /dev/null +++ b/packages/driver/cypress/fixtures/virtual-scroll-stress-test.html @@ -0,0 +1,518 @@ + + + + + + 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 Native Scrolling

+
+
+ + + + + + + +
+
+
+
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 index 726195b1993..2d8c7d80299 100644 --- a/packages/driver/cypress/fixtures/visibility/basic-css-properties.html +++ b/packages/driver/cypress/fixtures/visibility/basic-css-properties.html @@ -36,12 +36,12 @@

Opacity Property

-
-

Input Elements

- - +
+

Style Filters

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

Table Elements

@@ -63,5 +63,30 @@

Table Elements

+ +
+

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/form-elements.html b/packages/driver/cypress/fixtures/visibility/form-elements.html index bac1d20d04f..3e4aa95c603 100644 --- a/packages/driver/cypress/fixtures/visibility/form-elements.html +++ b/packages/driver/cypress/fixtures/visibility/form-elements.html @@ -11,8 +11,8 @@

Form Elements Visibility Tests

Select and Option Elements

- - - + + + @@ -50,7 +50,7 @@

Options Outside Select

Hidden Options Within Visible Select

diff --git a/packages/driver/cypress/fixtures/visibility/overflow.html b/packages/driver/cypress/fixtures/visibility/overflow.html index 589691779dd..430032f2848 100644 --- a/packages/driver/cypress/fixtures/visibility/overflow.html +++ b/packages/driver/cypress/fixtures/visibility/overflow.html @@ -95,7 +95,7 @@

Positive Dimensions with Overflow Hidden

Overflow Auto with Zero Dimensions

-
+
parent no size, overflow: auto
@@ -267,6 +267,23 @@

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

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 index 0dbb50eacf9..03187ae62ed 100644 --- a/packages/driver/cypress/fixtures/visibility/positioning.html +++ b/packages/driver/cypress/fixtures/visibility/positioning.html @@ -109,5 +109,27 @@

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/transforms.html b/packages/driver/cypress/fixtures/visibility/transforms.html index 210195cbd92..9f2a0998740 100644 --- a/packages/driver/cypress/fixtures/visibility/transforms.html +++ b/packages/driver/cypress/fixtures/visibility/transforms.html @@ -37,6 +37,7 @@

Translation

Translated in 2 dimensions
Translated in 3rd dimension
Translated in 3 dimensions
+
Translated outside viewport

Rotation

diff --git a/packages/driver/src/cypress/assertions/assert.ts b/packages/driver/src/cypress/assertions/assert.ts new file mode 100644 index 00000000000..0f21eecd15b --- /dev/null +++ b/packages/driver/src/cypress/assertions/assert.ts @@ -0,0 +1,85 @@ +import $dom from '../../dom' +import $ from 'jquery' + +export const selectors = { + visible: 'visible', + hidden: 'hidden', + selected: 'selected', + checked: 'checked', + enabled: 'enabled', + disabled: 'disabled', + focus: 'focused', + focused: 'focused', +} as const + +export const accessors = { + attr: 'attribute', + css: 'CSS property', + prop: 'property', +} as const + +export type Accessors = keyof typeof accessors + +export type Selectors = keyof typeof selectors + +export type Methods = Accessors | Selectors | 'data' | 'class' | 'empty' | 'id' | 'html' | 'text' | 'value' | 'descendants' | 'match' + +export type PartialAssertionArgs = [Chai.Message, Chai.Message, any?, any?, boolean?] + +export interface Callbacks { + onInvalid: (method, obj) => void + onError: (err, method, obj, negated) => void +} + +// reset the obj under test +// to be re-wrapped in our own +// jquery, so we can control +// the methods on it +export const wrap = (ctx) => $(ctx._obj) + +export function assertDom (ctx: Chai.AssertionStatic, utils: Chai.ChaiUtils, callbacks: Callbacks, method: Methods, ...args: PartialAssertionArgs) { + if (!$dom.isDom(ctx._obj) && !$dom.isJquery(ctx._obj)) { + try { + // always fail the assertion + // if we aren't a DOM like object + // depends on the "negate" flag + const negate = utils.flag(ctx, 'negate') + + return ctx.assert(!!negate, ...args) + } catch (err) { + return callbacks.onInvalid(method, ctx._obj) + } + } +} + +export function assert (ctx: Chai.AssertionStatic, utils: Chai.ChaiUtils, callbacks: Callbacks, method: Methods, ...[bool, ...args]: Chai.AssertionArgs) { + assertDom(ctx, utils, callbacks, method, ...args) + + try { + // reset obj to wrapped + const orig = ctx._obj + const selector = ctx._obj.selector + + ctx._obj = wrap(ctx) + + if (ctx._obj.length === 0) { + // From jQuery 3.x .selector API is deprecated. (https://api.jquery.com/selector/) + // Because of that, wrap() above removes selector property. + // That's why we're caching the value of selector above and using it here. + ctx._obj = selector ?? 'subject' + // if no element found, fail the existence check + // depends on the negate flag + ctx.assert(!!chai.util.flag(ctx, 'negate'), ...args) + } + + // apply the assertion + const ret = ctx.assert(bool, ...args) + + ctx._obj = orig + + return ret + } catch (err) { + // send it up with the obj and whether it was negated + return callbacks.onError(err, method, ctx._obj, utils.flag(ctx, 'negate')) + } +} diff --git a/packages/driver/src/cypress/chai_jquery.ts b/packages/driver/src/cypress/chai_jquery.ts index a1e4897221d..91ac66e31bf 100644 --- a/packages/driver/src/cypress/chai_jquery.ts +++ b/packages/driver/src/cypress/chai_jquery.ts @@ -1,34 +1,8 @@ import _ from 'lodash' -import $ from 'jquery' import $dom from '../dom' import $elements from '../dom/elements' - -type Accessors = keyof typeof accessors -type Selectors = keyof typeof selectors -type Methods = Accessors | Selectors | 'data' | 'class' | 'empty' | 'id' | 'html' | 'text' | 'value' | 'descendants' | 'match' - -const selectors = { - visible: 'visible', - hidden: 'hidden', - selected: 'selected', - checked: 'checked', - enabled: 'enabled', - disabled: 'disabled', - focus: 'focused', - focused: 'focused', -} as const - -const accessors = { - attr: 'attribute', - css: 'CSS property', - prop: 'property', -} as const - -// reset the obj under test -// to be re-wrapped in our own -// jquery, so we can control -// the methods on it -const wrap = (ctx) => $(ctx._obj) +import type { Methods, PartialAssertionArgs } from './assertions/assert' +import { assert, assertDom, accessors, selectors, wrap } from './assertions/assert' const maybeCastNumberToString = (num: number | string) => { // if this is a finite number (no Infinity or NaN) @@ -44,55 +18,20 @@ interface Callbacks { export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, callbacks: Callbacks) => { const { inspect, flag } = chaiUtils - const assertDom = (ctx, method: Methods, ...args) => { - if (!$dom.isDom(ctx._obj) && !$dom.isJquery(ctx._obj)) { - try { - // always fail the assertion - // if we aren't a DOM like object - // depends on the "negate" flag - return ctx.assert(!!ctx.__flags.negate, ...args) - } catch (err) { - return callbacks.onInvalid(method, ctx._obj) - } - } - } - - const assert = (ctx, method: Methods, bool: boolean, ...args) => { - assertDom(ctx, method, ...args) - - try { - // reset obj to wrapped - const orig = ctx._obj - const selector = ctx._obj.selector - - ctx._obj = wrap(ctx) - - if (ctx._obj.length === 0) { - // From jQuery 3.x .selector API is deprecated. (https://api.jquery.com/selector/) - // Because of that, wrap() above removes selector property. - // That's why we're caching the value of selector above and using it here. - ctx._obj = selector ?? 'subject' - // if no element found, fail the existence check - // depends on the negate flag - ctx.assert(!!ctx.__flags.negate, ...args) - } - - // apply the assertion - const ret = ctx.assert(bool, ...args) - - ctx._obj = orig - - return ret - } catch (err) { - // send it up with the obj and whether it was negated - return callbacks.onError(err, method, ctx._obj, flag(ctx, 'negate')) - } - } - - const assertPartial = (ctx, method: Methods, actual, expected, message: string, notMessage: string, ...args) => { - if (ctx.__flags.contains || ctx.__flags.includes) { + const assertPartial = ( + ctx: Chai.AssertionStatic, + chaiUtils: Chai.ChaiUtils, + callbacks: Callbacks, + method: Methods, + actual: any, + expected: any, + ...[message, notMessage, ...args]: PartialAssertionArgs + ) => { + if (chaiUtils.flag(ctx, 'contains') || chaiUtils.flag(ctx, 'includes')) { return assert( ctx, + chaiUtils, + callbacks, method, _.includes(actual, expected), `expected #{this} to contain ${message}`, @@ -103,6 +42,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca return assert( ctx, + chaiUtils, + callbacks, method, actual === expected, `expected #{this} to have ${message}`, @@ -112,7 +53,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca } chai.Assertion.addMethod('data', function (...args) { - assertDom(this, 'data') + // @ts-expect-error - Custom assertions expect messages for failures + assertDom(this, chaiUtils, callbacks, 'data') let a = new chai.Assertion(wrap(this).data()) @@ -127,6 +69,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca chai.Assertion.addMethod('class', function (className: string) { return assert( this, + chaiUtils, + callbacks, 'class', wrap(this).hasClass(className), 'expected #{this} to have class #{exp}', @@ -140,6 +84,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca return assert( this, + chaiUtils, + callbacks, 'id', wrap(this).prop('id') === id, 'expected #{this} to have id #{exp}', @@ -151,6 +97,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca chai.Assertion.addMethod('html', function (html: string) { assertDom( this, + chaiUtils, + callbacks, 'html', 'expected #{this} to have HTML #{exp}', 'expected #{this} not to have HTML #{exp}', @@ -161,6 +109,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca return assertPartial( this, + chaiUtils, + callbacks, 'html', actual, html, @@ -176,6 +126,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca assertDom( this, + chaiUtils, + callbacks, 'text', 'expected #{this} to have text #{exp}', 'expected #{this} not to have text #{exp}', @@ -186,6 +138,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca return assertPartial( this, + chaiUtils, + callbacks, 'text', actual, text, @@ -207,6 +161,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca assertDom( this, + chaiUtils, + callbacks, 'value', 'expected #{this} to have value #{exp}', 'expected #{this} not to have value #{exp}', @@ -217,6 +173,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca return assertPartial( this, + chaiUtils, + callbacks, 'value', actual, value, @@ -230,6 +188,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca chai.Assertion.addMethod('descendants', function (selector: string) { return assert( this, + chaiUtils, + callbacks, 'descendants', wrap(this).has(selector).length > 0, 'expected #{this} to have descendants #{exp}', @@ -243,6 +203,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca if ($dom.isDom(this._obj)) { return assert( this, + chaiUtils, + callbacks, 'empty', wrap(this).is(':empty'), 'expected #{this} to be #{exp}', @@ -262,6 +224,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca if ($dom.isDom(this._obj)) { return assert( this, + chaiUtils, + callbacks, 'match', wrap(this).is(selector), 'expected #{this} to match #{exp}', @@ -280,6 +244,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca return chai.Assertion.addProperty(sel, function () { return assert( this, + chaiUtils, + callbacks, sel, wrap(this).is(`:${sel}`), 'expected #{this} to be #{exp}', @@ -295,6 +261,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca return chai.Assertion.addMethod(acc, function (name, val) { assertDom( this, + chaiUtils, + callbacks, acc, `expected #{this} to have ${description} #{exp}`, `expected #{this} not to have ${description} #{exp}`, @@ -307,6 +275,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca if (arguments.length === 1) { assert( this, + chaiUtils, + callbacks, acc, actual !== undefined, `expected #{this} to have ${description} #{exp}`, @@ -342,6 +312,8 @@ export const $chaiJquery = (chai: Chai.ChaiStatic, chaiUtils: Chai.ChaiUtils, ca assert( this, + chaiUtils, + callbacks, acc, (actual != null) && (actual === val), message, 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..22cb9d15b7c --- /dev/null +++ b/packages/driver/src/dom/visibility/fastIsHidden.ts @@ -0,0 +1,150 @@ +import $elements from '../elements' + +import { unwrap, wrap, isJquery } from '../jquery' +const { isOption, isOptgroup, isBody, isHTML } = $elements + +const DEBUG = false + +function debug (...args: any[]) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('DEBUG:', ...args) + } +} + +function memoize (fn: (...args: any[]) => T): (...args: any[]) => T { + const cache = new Map() + + return (...args: any[]) => { + const key = args + + const cached = cache.get(key) + + if (cached && cached.timestamp > Date.now() - 100) { + return cached.result + } + + const result = fn(...args) + + cache.set(key, { result, timestamp: Date.now() }) + + return result + } +} + +const getBoundingClientRect = memoize((el: HTMLElement) => el.getBoundingClientRect()) + +function subDivideRect ({ x, y, width, height }: DOMRect): DOMRect[] { + return [ + DOMRect.fromRect({ + x, + y, + width: width / 2, + height: height / 2, + }), + DOMRect.fromRect({ + x: x + width / 2, + y, + width: width / 2, + height: height / 2, + }), + DOMRect.fromRect({ + x, + y: y + height / 2, + width: width / 2, + height: height / 2, + }), + DOMRect.fromRect({ + x: x + width / 2, + y: y + height / 2, + width: width / 2, + height: height / 2, + }), + ].filter((rect: DOMRect) => rect.width > 1 && rect.height > 1) +} + +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)) +}) + +function visibleToUser (el: HTMLElement, rect: DOMRect, maxDepth: number = 2, currentDepth: number = 0): boolean { + if (currentDepth >= maxDepth) { + return false + } + + const { x, y, width, height } = rect + + const samples = [ + [x, y], + [x + width, y], + [x, y + height], + [x + width, y + height], + [x + width / 2, y + height / 2], + ] + + if (samples.some(([x, y]) => visibleAtPoint(el, x, y))) { + debug('some samples are visible') + + return true + } + + const subRects = subDivideRect(rect) + + debug('subRects', subRects) + + return subRects.some((subRect: DOMRect) => { + return visibleToUser(el, subRect, maxDepth, currentDepth + 1) + }) +} + +export function fastIsHidden (subject: JQuery | HTMLElement, options: { checkOpacity: boolean } = { checkOpacity: true }): boolean { + debug('fastIsHidden', subject) + + 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(wrap(select), options) + } + } + + if (!subject.checkVisibility({ + contentVisibilityAuto: true, + opacityProperty: options.checkOpacity, + visibilityProperty: true, + })) { + return true + } + + const boundingRect = getBoundingClientRect(subject) + + if (visibleToUser(subject, boundingRect)) { + debug('visibleToUser', subject, boundingRect) + + return false + } + + return true +}