perf: experimental "fast mode" for visibility checks (#32801)

* migrate visibility tests to static fixtures

* clean up the html a little bit, remove ref to experimentalFastVisibility

* Delete packages/driver/cypress/e2e/dom/visibility-migration.md

* Update visibility.ts

* fix assertion

* fix incomplete

* stress test for visibility

* splits up visibility into multiple files for easier grokking

* middle of refactoring to un-hide the recursion

* notes to help with visibility efficiencyn

* revert splitting - these fns are too interconnected to split without resolving circular dependencies

* impl new experimental visibility algo

* new experimental useFastVisibility config option

* check four corners of rect instead of just center

* num-tests-in-memory=0

* rm ref virtuoso

* rename file to be in line with its export

* clean up jquery unwrapping

* some more cleanup

* Delete visibility-algorithm.dot

* Delete visibility-algorithm.svg

* rm unused export

* Update fastIsHidden.ts

* edit

* enable fast visibility mode in visibility related e2e tests

* enable clip-path test for fast visibility

* memoize visibleAtPoint

* fix config option typedef

* fix select option/optgroup delegation

* add note re: shadow dom

* add migration guide, additional edge cases, behavior table

* add note that the virtual scroll stress test causes browser to crash when experimentalFastVisibility is not enabled

* fix types

* add best practices note re: shadow dom

* better performance tests

* extract assert/assertDom to separate file

* fix merge

* update tests

* update algo diff

* correct entries

* revert jquery typechanges

* more ts fix

* various fix

* note firefox inconsistency vs chromium with 0-dim els

* add cfg key to system test snapshot

* ts

* more ff discrepancy

* quick fix memo

* fix regression with chained assertions

* fix jqueried ref, fix obj key in memoization

* manual chck for primitives instead of checking for "object" typeof

* changelog

* add i18n copy for experimentalFastVisibility, documented as a step to take in cfg/options.ts

* code review

* wsp cleanup

* debug lib
This commit is contained in:
Cacie Prins
2025-12-16 11:09:16 -05:00
committed by GitHub
parent 62ca034206
commit 62d189b7a3
24 changed files with 1374 additions and 684 deletions

View File

@@ -3,6 +3,10 @@
_Released 12/16/2025 (PENDING)_
**Performance:**
- Introduced a new `experimentalFastVisibility` experiment. Enabling this experiment changes how Cypress performs visibility checks and assertions. Read more about [experimental fast visibility](https://on.cypress.io/experiments/#experimental-fast-visibility). Addresses [#33044](https://github.com/cypress-io/cypress/issues/33044). Addressed in [#32801](https://github.com/cypress-io/cypress/pull/32801).
**Features:**
- `Angular` version 21 is now supported within component testing. Addressed in [#33004](https://github.com/cypress-io/cypress/pull/33004).

View File

@@ -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
@@ -3365,14 +3370,14 @@ declare namespace Cypress {
}
interface SuiteConfigOverrides extends Partial<
Pick<ConfigOptions, 'animationDistanceThreshold' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>
Pick<ConfigOptions, 'animationDistanceThreshold' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'experimentalFastVisibility' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>
>, Partial<Pick<ResolvedConfigOptions, 'baseUrl' | 'testIsolation'>> {
browser?: IsBrowserMatcher | IsBrowserMatcher[]
keystrokeDelay?: number
}
interface TestConfigOverrides extends Partial<
Pick<ConfigOptions, 'animationDistanceThreshold' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>
Pick<ConfigOptions, 'animationDistanceThreshold' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'experimentalFastVisibility' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>
>, Partial<Pick<ResolvedConfigOptions, 'baseUrl'>> {
browser?: IsBrowserMatcher | IsBrowserMatcher[]
keystrokeDelay?: number

View File

@@ -6,6 +6,17 @@ import type { TestingType } from '@packages/types'
import * as validate from './validation'
/**
* When adding a new config option, ensure the following steps are taken:
* - Add the option to the appropriate array
* - Add the type definition to `/cli/types/cypress.d.ts`
* - Ensure the `results_spec` system test passes with updated snapshots
* - Ensure the config package tests pass with updated snapshots
*
* Additionally, for experiments:
* - add name and description copy to `/packages/frontend-shared/src/locales/en-US.json`
*/
const BREAKING_OPTION_ERROR_KEY: Readonly<AllCypressErrorNames[]> = [
'CONFIG_FILE_INVALID_ROOT_CONFIG',
'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E',
@@ -253,6 +264,12 @@ const driverConfigOptions: Array<DriverConfigOption> = [
validation: validate.isBoolean,
isExperimental: true,
requireRestartOnChange: 'server',
}, {
name: 'experimentalFastVisibility',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
overrideLevel: 'any',
}, {
name: 'fileServerFolder',
defaultValue: '',

View File

@@ -38,6 +38,7 @@ exports[`config/src/index > .getDefaultValues > returns list of public config ke
"excludeSpecPattern": "*.hot-update.js",
"execTimeout": 60000,
"experimentalCspAllowList": false,
"experimentalFastVisibility": false,
"experimentalInteractiveRunEvents": false,
"experimentalMemoryManagement": false,
"experimentalModifyObstructiveThirdPartyCode": false,
@@ -133,6 +134,7 @@ exports[`config/src/index > .getDefaultValues > returns list of public config ke
"excludeSpecPattern": "*.hot-update.js",
"execTimeout": 60000,
"experimentalCspAllowList": false,
"experimentalFastVisibility": false,
"experimentalInteractiveRunEvents": false,
"experimentalMemoryManagement": false,
"experimentalModifyObstructiveThirdPartyCode": false,
@@ -226,6 +228,7 @@ exports[`config/src/index > .getPublicConfigKeys > returns list of public config
"experimentalSourceRewriting",
"experimentalSingleTabRunMode",
"experimentalWebKitSupport",
"experimentalFastVisibility",
"fileServerFolder",
"fixturesFolder",
"excludeSpecPattern",

View File

@@ -1188,6 +1188,7 @@ describe('config/src/project/utils', () => {
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalCspAllowList: { value: false, from: 'default' },
experimentalFastVisibility: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },
experimentalOriginDependencies: { value: false, from: 'default' },
@@ -1304,6 +1305,7 @@ describe('config/src/project/utils', () => {
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalCspAllowList: { value: false, from: 'default' },
experimentalFastVisibility: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },
experimentalOriginDependencies: { value: false, from: 'default' },

View File

@@ -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}`)
@@ -40,8 +41,12 @@ describe('src/cypress/dom/visibility', {
cy.get(`[cy-section="${section}"]`).scrollIntoView()
}
function assertVisibilityForSections (sections: string[]) {
function assertVisibilityForSections (sections: (string | undefined)[]) {
for (const section of sections) {
if (!section) {
continue
}
it(`detects visibility for ${section} test cases`, () => {
prepareFixtureSection(section)
cy.get(`[cy-section="${section}"] .testCase`).then((els) => {
@@ -51,108 +56,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('<div style="height: 1000px; width: 10px;" />')
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('<div style="height: 1000px; width: 10px;" />')
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('<div>foo</div>')
expect(fn()).to.be.true
})
const win = cy.state('window')
it('returns false if window and body < window height', () => {
cy.$$('body').html('<div>foo</div>')
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('<div style="height: 1000px; width: 10px;" />')
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('<div style="height: 1000px; width: 10px;" />')
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('<div>foo</div>')
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('<div>foo</div>')
const fn = () => {
return dom.isScrollable(documentElement)
}
const documentElement = Cypress.dom.wrap(cy.state('document').documentElement)
expect(fn()).to.be.false
})
const fn = () => {
return dom.isScrollable(documentElement)
}
it('returns false el is not scrollable', function () {
const noScroll = this.add(`\
expect(fn()).to.be.false
})
it('returns false el is not scrollable', function () {
const noScroll = this.add(`\
<div style="height: 100px; overflow: auto;">
<div>No Scroll</div>
</div>\
`)
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(`\
<div style="height: 100px; width: 100px; border: 1px solid green;">
<div style="height: 150px;">
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 +171,203 @@ describe('src/cypress/dom/visibility', {
</div>\
`)
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(`\
<div style="height: 100px; width: 100px; overflow: auto;">
<div style="height: 150px;">Vertical Scroll</div>
</div>\
`)
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(`\
<div style="height: 100px; width: 100px; overflow: auto; ">
<div style="height: 150px;">Horizontal Scroll</div>
</div>\
`)
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(`\
<div style="height: 100px; width: 100px; overflow: scroll; border: 1px solid yellow;">
<div style="height: 300px; width: 300px;">Forced Scroll</div>
</div>\
`)
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')
})
cy.get('body').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
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
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
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
expect(cy.$$('body')).not.to.be.hidden
expect(cy.$$('body')).to.be.visible
cy.wrap(cy.$$('body')).should('not.be.hidden')
cy.wrap(cy.$$('body')).should('be.visible')
})
})
})
it('is always visible', () => {
expect(cy.$$('html').is(':hidden')).to.be.false
expect(cy.$$('html').is(':visible')).to.be.true
describe('basic CSS properties', () => {
beforeEach(() => {
cy.visit('/fixtures/visibility/basic-css-properties.html')
})
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
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')
assertVisibilityForSections([
'visibility-property',
'display-property',
'opacity-property',
'table-elements',
'box-interactions',
])
})
})
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('form elements', () => {
beforeEach(() => {
cy.visit('/fixtures/visibility/form-elements.html')
})
expect(cy.$$('html')).not.to.be.hidden
expect(cy.$$('html')).to.be.visible
assertVisibilityForSections([
'select-and-option-elements',
'optgroup-elements',
'options-outside-select',
'hidden-options-within-visible-select',
'input-elements',
])
})
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
describe('overflow', () => {
beforeEach(() => {
cy.visit('/fixtures/visibility/overflow.html')
})
expect(cy.$$('body')).not.to.be.hidden
expect(cy.$$('body')).to.be.visible
assertVisibilityForSections([
'zero-dimensions-with-overflow-hidden',
// TODO: Firefox has slightly different behavior than chromium - address with test harness changes in https://github.com/cypress-io/cypress/issues/33127
Cypress.browser.name !== 'firefox' || mode === 'legacy' ? 'text-content-with-zero-dimensions' : undefined,
'positive-dimensions-with-overflow-hidden',
'overflow-auto-with-zero-dimensions',
Cypress.browser.name !== 'firefox' || mode === 'legacy' ? 'mixed-dimension-scenarios' : undefined,
'overflow-hidden',
'overflow-y-hidden',
'overflow-x-hidden',
'overflow-auto-scenarios',
'overflow-scroll-scenarios',
'overflow-relative-positioning',
'overflow-flex-container',
'overflow-complex-scenarios',
Cypress.browser.name !== 'firefox' || mode === 'legacy' ? 'clip-path-scenarios' : undefined,
])
})
cy.wrap(cy.$$('body')).should('not.be.hidden')
cy.wrap(cy.$$('body')).should('be.visible')
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',
])
})
describe('transforms', () => {
beforeEach(() => {
cy.visit('/fixtures/visibility/transforms.html')
})
assertVisibilityForSections([
'scaling',
Cypress.browser.name !== 'firefox' || mode === 'legacy' ? 'translation' : undefined,
'rotation',
'skew',
'matrix',
'perspective',
'multiple',
'multiple-3d',
'backface-visibility',
])
})
})
})
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',
])
})
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',
])
})
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',
])
})
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',
])
})
describe('transforms', () => {
beforeEach(() => {
cy.visit('/fixtures/visibility/transforms.html')
})
assertVisibilityForSections([
'scaling',
'translation',
'rotation',
'skew',
'matrix',
'perspective',
'multiple',
'multiple-3d',
'backface-visibility',
])
})
})
}
context('#getReasonIsHidden', () => {
const reasonIs = ($el: JQuery, str: string) => {

View File

@@ -5,62 +5,69 @@ const { $ } = Cypress
describe('src/cypress/dom/visibility - shadow dom', () => {
let add: (el: string, shadowEl: string, rootIdentifier: string) => JQuery<HTMLElement>
beforeEach(() => {
cy.visit('/fixtures/empty.html').then((win) => {
win.customElements.define('shadow-root', class extends win.HTMLElement {
constructor () {
super()
// #TODO: support shadow dom in fast visibility algorithm: https://github.com/cypress-io/cypress/issues/33046
const modes = ['legacy']
this.attachShadow({ mode: 'open' })
this.style.display = 'block'
}
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 = $(`<div style='height: 1000px; width: 10px;'></div><div>Should be in view</div>`).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 = $(`<div style='height: 1000px; width: 10px;'></div><div>Should be in view</div>`).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 id="shadow-root-vis-hidden" style="visibility: hidden;"></shadow-root>`,
`<button>parent visibility: hidden</button>`,
'#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(
`<div style="visibility: hidden;">
<shadow-root id="outside-parent-vis-hidden"></shadow-root>
</div>`,
`<button>parent visibility: hidden</button>`,
'#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(
`<table>
<tr>
<td>Naruto</td>
@@ -70,29 +77,29 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</table>`,
`<span id='collapse-span'>Sasuke</span>`,
'#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(
`<shadow-root id="shadow-root-no-width" style='width: 0; height: 100px; overflow: hidden;'></shadow-root>`,
`<div style='height: 500px; width: 500px;'>
<span>parent width: 0</span>
</div>`,
'#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(
`<div style='width: 0; height: 100px; overflow: hidden;'>
<shadow-root id="outside-parent-no-width"></shadow-root>
</div>`,
@@ -100,27 +107,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<span>parent width: 0</span>
</div>`,
'#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(
`<shadow-root id="shadow-root-no-height" style='width: 100px; height: 0; overflow: hidden;'></shadow-root>`,
`<div style='height: 500px; width: 500px;'>
<span>parent height: 0</span>
</div>`,
'#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(
`<div style='width: 100px; height: 0; overflow: hidden;'>
<shadow-root id="outside-parent-no-height"></shadow-root>
</div>`,
@@ -128,16 +135,16 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<span>parent height: 0</span>
</div>`,
'#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(
`<div style='width: 0; height: 100px; overflow: hidden;'>
<shadow-root id="child-pos-absolute"></shadow-root>
</div>`,
@@ -145,14 +152,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<span style="position: absolute;">position: absolute</span>
</div>`,
'#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(
`<div style='width: 0; height: 100px; overflow: hidden;'>
<shadow-root id="child-pos-fixed"></shadow-root>
</div>`,
@@ -160,14 +167,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<button style="position: fixed; top: 0;">position: fixed</button>
</div>`,
'#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(
`<div style='width: 0; height: 100px; overflow: hidden;'>
<div style='height: 500px; width: 500px; position: absolute;'>
<shadow-root id="descendent-pos-abs-outside"></shadow-root>
@@ -175,14 +182,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span>no width, descendant position: absolute</span>`,
'#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(
`<div style='width: 0; height: 100px; overflow: hidden;'>
<shadow-root id="descendent-pos-abs-inside"></shadow-root>
</div>`,
@@ -190,14 +197,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<span>no width, descendant position: absolute</span>
</div>`,
'#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(
`<div style='width: 0; height: 100px; overflow: hidden;'>
<div style='height: 500px; width: 500px; position: fixed; top: 0; right: 0;'>
<shadow-root id="descendent-pos-fixed-outside"></shadow-root>
@@ -205,14 +212,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<button>no width, descendant position: fixed</button>`,
'#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(
`<div style='width: 0; height: 100px; overflow: hidden;'>
<shadow-root id="descendent-pos-fixed-inside"></shadow-root>
</div>`,
@@ -220,162 +227,162 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<button>no width, descendant position: fixed</button>
</div>`,
'#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(
`<div>
<shadow-root id="covered-up-by-outside-pos-fixed"></shadow-root>
<div style="position: fixed; bottom: 0; left: 0">on top</div>
</div>`,
`<div id="inside-underneath" style="position: fixed; bottom: 0; left: 0">underneath</div>`,
'#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(
`<div>
<div id="outside-underneath" style="position: fixed; bottom: 0; left: 0">underneath</div>
<shadow-root id="covered-up-by-shadow-pos-fixed"></shadow-root>
</div>`,
`<div style="position: fixed; bottom: 0; left: 0">on top</div>`,
'#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(
`<div style="pointer-events: none;">
<shadow-root id="parent-pointer-events-none"></shadow-root>
</div>`,
`<span style="position: fixed; top: 20px;">parent pointer-events: none</span>`,
'#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(
`<div style="pointer-events: none;">
<shadow-root id="parent-pointer-events-none-covered"></shadow-root>
</div>
<span style="position: fixed; top: 40px; background: red;">covering the element with pointer-events: none</span>`,
`<span style="position: fixed; top: 40px;">parent pointer-events: none</span>`,
'#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(
`<div style="position: fixed; top: 60px;">
<shadow-root id="child-pointer-events-none-covered"></shadow-root>
</div>`,
`<span style="pointer-events: none;">child pointer-events: none</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
<shadow-root id="el-out-of-parent-bounds-to-left"></shadow-root>
</div>`,
`<span style='position: absolute; width: 100px; height: 100px; left: -100px; top: 0;'>position: absolute, out of bounds left</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
<shadow-root id="el-out-of-parent-bounds-to-right"></shadow-root>
</div>`,
`<span style='position: absolute; width: 100px; height: 100px; right: -100px; top: 0;'>position: absolute, out of bounds right</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
<shadow-root id="el-out-of-parent-bounds-above"></shadow-root>
</div>`,
`<span style='position: absolute; width: 100px; height: 100px; top: -100px; left: 0;'>position: absolute, out of bounds above</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
<shadow-root id="el-out-of-parent-bounds-below"></shadow-root>
</div>`,
`<span style='position: absolute; width: 100px; height: 100px; bottom: -100px; left: 0;'>position: absolute, out of bounds below</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow-y: hidden; position: relative;'>
<shadow-root id="el-out-of-parent-with-overflow-y-hidden-bounds"></shadow-root>
</div>`,
`<span style='position: absolute; top: 200px; left: 0;'>position: absolute, out of bounds below</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow-x: hidden; position: relative;'>
<shadow-root id="el-out-of-parent-with-overflow-x-hidden-bounds"></shadow-root>
</div>`,
`<span style='position: absolute; top: 0; left: 200px;'>position: absolute, out of bounds below</span>`,
'#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(
`<div style="border: 1px solid red; width: 200px; height: 200px; overflow: hidden;">
<div style="position: absolute; left: 300px; border: 1px solid blue; width: 200px; height: 200px;">
<shadow-root id="el-out-of-parent-with-overflow-hidden-bounds-but-closer-position-absolute-parent"></shadow-root>
@@ -383,14 +390,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span style="border: 1px solid green;">Hello</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: auto;'>
<div style='width: 1000px; position: relative;'>
<shadow-root id="el-out-of-ancestor-overflow-auto-bounds-outside"></shadow-root>
@@ -398,14 +405,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span style='position: absolute; left: 300px; top: 0px;'>out of bounds, parent wide, ancestor overflow: auto</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: auto;'>
<shadow-root id="el-out-of-ancestor-overflow-auto-bounds-inside"></shadow-root>
</div>`,
@@ -413,27 +420,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<span style='position: absolute; left: 300px; top: 0px;'>out of bounds, parent wide, ancestor overflow: auto</span>
</div>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: scroll; position: relative; top: 700px; left: 700px;'>
<shadow-root id="el-out-of-scrolling-parent-bounds"></shadow-root>
</div>`,
`<span style='position: absolute; left: 300px; top: 0;'>out of scrolling bounds, position: absolute</span>`,
'#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(
`<div id="ancestor-el" style='width: 100px; height: 100px; overflow: hidden; position: relative; top: 700px; left: 700px;'>
<div>
<div id="parent-el" style='position: absolute;'>
@@ -443,14 +450,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span id="el-under-test" style='position: absolute; left: -350px; top: 0;'>out of bounds, position: absolute</span>`,
'#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(
`<div style='width: 200px; height: 200px; overflow: hidden; position: relative;'>
<div style='position: absolute;'>
<shadow-root id="el-in-pos-abs-parent-bounds"></shadow-root>
@@ -458,27 +465,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span style='position: absolute; left: 50px; top: 50px;'>in bounds, parent position: absolute</span>`,
'#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(
`<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
<shadow-root id="el-in-parent-bounds"></shadow-root>
</div>`,
`<span style='position: absolute; left: 0; top: 0;'>in bounds, position: absolute</span>`,
'#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(
`<div style='padding: 100px; position: relative;'>
<div style='overflow: hidden;'>
<div>
@@ -488,14 +495,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span style='position: absolute; left: 0; top: 0;'>in bounds of ancestor, position: absolute, parent overflow: hidden</span>`,
'#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(
`<div style='padding: 100px; position: relative;'>
<shadow-root id="el-is-out-of-bounds-of-inside-ancestors-overflow-but-within-relative-ancestor"></shadow-root>
</div>`,
@@ -505,14 +512,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>
</div>`,
'#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(
`<div style='overflow: hidden;'>
<div>
<shadow-root id="el-is-relative-and-out-of-bounds-of-ancestor-overflow"></shadow-root>
@@ -520,27 +527,27 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span style='position: relative; left: 0; top: -200px;'>out of bounds, position: relative</span>`,
'#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(
`<div>
<shadow-root id="el-is-relative-and-out-of-bounds-of-ancestor-but-ancestor-shows-overflow"></shadow-root>
</div>`,
`<span style='position: relative; left: 0; top: -200px;'>out of bounds but visible, position: relative</span>`,
'#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(
`<div style='position: relative; padding: 20px;'>
<div style='overflow: hidden;'>
<shadow-root id="inside-parent-out-of-bounds-but-el-in-bounds"></shadow-root>
@@ -550,14 +557,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<span style='position: relative; left: 0; top: 100px;'>in bounds of ancestor, parent out of bounds</span>
</div>`,
'#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(
`<div style='position: relative; padding: 20px;'>
<div style='overflow: hidden;'>
<div style='position: relative; left: 0; top: -100px;'>
@@ -567,14 +574,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>`,
`<span style='position: relative; left: 0; top: 100px;'>in bounds of ancestor, parent out of bounds</span>`,
'#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(
`<div id="breaking-container" style="overflow: hidden">
<div>
<shadow-root id="shadow"></shadow-root>
@@ -584,14 +591,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<button id="visible-button">Try me</button>
</div>`,
'#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(
`<div style="height: 200px; position: relative; display: flex">
<div style="border: 5px solid red">
<div
@@ -609,42 +616,42 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<button id="visible-button">Try me</button>
</div>`,
'#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(
`<div style="transform: scale(0,0)">
<shadow-root id="parent-with-transform-scale-el-outside-scale"></shadow-root>
</div>`,
`<span>TRANSFORMERS</span>`,
'#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(
`<div style="transform: scale(1,1)">
<shadow-root id="parent-with-transform-scale-el-inside-scale"></shadow-root>
</div>`,
`<span>TRANSFORMERS</span>`,
'#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(
`<div style='margin-left: 100px; overflow: hidden; width: 100px;'>
<shadow-root id="ancestor-inside-transform-makes-el-out-of-bounds-of-ancestor"></shadow-root>
</div>`,
@@ -654,14 +661,14 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
</div>
</div>`,
'#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(
`<div style='margin-left: 100px; overflow: hidden; width: 100px;'>
<div style='transform: translateX(-100px); width: 200px;'>
<shadow-root id="ancestor-outside-transform-makes-el-out-of-bounds-of-ancestor"></shadow-root>
@@ -671,10 +678,12 @@ describe('src/cypress/dom/visibility - shadow dom', () => {
<span>out of ancestor's bounds due to ancestor translate</span>
</div>`,
'#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')
})
})
})
})
}
})

View File

@@ -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')
const modes = ['fast', 'legacy']
// 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')
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')
// 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('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('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()
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')
})
})
})
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('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()
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')
})
})
}
})

View File

@@ -36,12 +36,12 @@
</div>
</div>
<div class="test-section" cy-section="input-elements">
<h3>Input Elements</h3>
<input class="testCase" cy-expect="hidden" cy-label="Hidden input" type="hidden" value="hidden input">
<input class="testCase" cy-expect="visible" cy-label="Visible input" type="text" value="visible input">
<div class="test-section" cy-section="style-filters">
<h3>Style Filters</h3>
<!-- while opacity(0) renders the element as fully transparent, it is still considered visible: it has a box model, and it is clickable -->
<div class="testCase" cy-expect="visible" cy-label="Hidden by style filter: opacity(0)" style="filter: opacity(0);">Hidden by style filter: opacity(0)</div>
</div>
<div class="test-section" cy-section="table-elements">
<h3>Table Elements</h3>
<table>
@@ -63,5 +63,30 @@
</span>
</div>
</div>
<div class="test-section" cy-section="pointer-events-none">
<h3>Pointer Events: None</h3>
<div class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" cy-label="Element with pointer-events: none" style="pointer-events: none;">Element with pointer-events: none</div>
</div>
<div class="test-section" cy-section="contain-property">
<h3>CSS Contain Property</h3>
<!-- Element with contain: layout -->
<div class="testCase" cy-expect="visible" cy-label="Element with contain: layout" style="width: 100px; height: 100px; background: lightcoral; contain: layout;">
<span class="testCase" cy-expect="visible" cy-label="Child of contain: layout element">contain: layout</span>
</div>
<!-- Element with contain: paint -->
<div class="testCase" cy-expect="visible" cy-label="Element with contain: paint" style="width: 100px; height: 100px; background: lightcoral; contain: paint;">
<span class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" cy-label="Child positioned out of bounds of its `contain: paint` parent" style="position:relative; top:500px">contain: paint</span>
</div>
<!-- Element with contain: strict -->
<div class="testCase" cy-expect="visible" cy-label="Element with contain: strict" style="width: 100px; height: 100px; background: lightcoral; contain: strict;">
<span class="testCase" cy-expect="visible" cy-label="Child of contain: strict element">contain: strict</span>
</div>
</div>
</body>
</html>

View File

@@ -11,8 +11,8 @@
<div class="test-section" cy-section="select-and-option-elements" cy-has-conflicts="true">
<h3>Select and Option Elements</h3>
<select class="testCase" cy-expect="visible" id="visible-select">
<option class="testCase" cy-fast-expect="hidden" cy-legacy-expect="visible" id="visible-option" cy-label="first option">Visible Option</option>
<option class="testCase" cy-fast-expect="hidden" cy-legacy-expect="visible" id="visible-option-2">Another Visible Option</option>
<option class="testCase" cy-expect="visible" id="visible-option" cy-label="first option">Visible Option</option>
<option class="testCase" cy-expect="visible" id="visible-option-2">Another Visible Option</option>
</select>
<select class="testCase" cy-expect="hidden" id="hidden-select" style="display: none;">
@@ -23,9 +23,9 @@
<div class="test-section" cy-section="optgroup-elements" cy-has-conflicts="true">
<h3>Optgroup Elements</h3>
<select class="testCase" cy-expect="visible" id="select-with-optgroup" cy-label="select with optgroup">
<optgroup class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" id="visible-optgroup" cy-label="Visible opt Group">
<option class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden">Option 1</option>
<option class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden">Option 2</option>
<optgroup class="testCase" cy-expect="visible" id="visible-optgroup" cy-label="Visible opt Group">
<option class="testCase" cy-expect="visible">Option 1</option>
<option class="testCase" cy-expect="visible">Option 2</option>
</optgroup>
</select>
@@ -50,7 +50,7 @@
<div class="test-section" cy-section="hidden-options-within-visible-select" cy-has-conflicts="true">
<h3>Hidden Options Within Visible Select</h3>
<select class="testCase" cy-expect="visible" id="select-with-hidden-option">
<option class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" id="visible-option-in-select">Visible Option</option>
<option class="testCase" cy-expect="visible" id="visible-option-in-select">Visible Option</option>
<option class="testCase" cy-expect="hidden" id="hidden-option-in-select" style="display: none;">Hidden Option</option>
</select>
</div>

View File

@@ -95,7 +95,7 @@
<h3>Overflow Auto with Zero Dimensions</h3>
<!-- Zero dimensions with overflow auto -->
<div class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" cy-label="Zero dimensions with overflow auto" style="width: 0; height: 0px; overflow: auto; background: lightgreen;">
<div class="testCase" cy-fast-expect="hidden" cy-legacy-expect="visible" cy-label="Zero dimensions with overflow auto" style="width: 0; height: 0px; overflow: auto; background: lightgreen;">
<span class="testCase" cy-expect="hidden" cy-label="Self under zero dimensions with overflow auto">parent no size, overflow: auto</span>
</div>
</div>
@@ -105,15 +105,14 @@
<!-- Zero width, positive height -->
<div class="testCase" cy-expect="visible" cy-label="Zero width with text content" style="width: 0; height: 100px; background: lightgreen;">Zero width</div>
<div class="testCase" cy-expect="visible" cy-label="Zero width with text content (blue)" style="width: 0; height: 100px; background: lightgreen;">Has text content</div>
<div class="testCase" cy-expect="hidden" cy-label="Zero width with whitespace only" style="width: 0; height: 50px; background: lightcoral;"> </div>
<!-- Positive width, zero height -->
<div class="testCase" cy-expect="hidden" cy-label="Zero height without text content" style="width: 100px; height: 0; background: lightcoral;"></div>
<div class="testCase" cy-expect="visible" cy-label="Zero height with text content" style="width: 100px; height: 0; background: lightgreen;">Has text content</div>
<div class="testCase" cy-expect="hidden" cy-label="Zero height with whitespace only" style="width: 50px; height: 0; background: lightcoral;"> </div>
<!-- Zero dimensions -->
<div style="width: 10; height: 10; background: lightyellow">
<div class="testCase" cy-expect="hidden" cy-label="Zero dimensions without content" style="width:0; height:0"></div>
</div>
@@ -267,6 +266,23 @@
<div class="testCase" cy-expect="visible" cy-label="Parent with clip-path path" style="position: relative; clip-path: path('M 0 0 L 100 0 L 100 100 L 0 100 Z');">
<span class="testCase" cy-expect="visible" cy-label="Element inside clip-path path" style="background: lightgreen;">path content</span>
</div>
<!-- CSS clip property -->
<div class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" cy-label="Element clipped by CSS clip property" style="position: absolute; clip: rect(0, 0, 0, 0); background: lightcoral; width: 100px; height: 100px;">clipped by clip: rect(0,0,0,0)</div>
<!-- CSS mask property note: this yields an element that is not rendered as visible, but has a box model that is visible and is clickable -->
<div class="testCase" cy-expect="visible" cy-label="Element masked by CSS mask" style="width: 100px; height: 100px; background: lightcoral; mask: linear-gradient(black, black); mask-size: 0 0;">masked by CSS mask</div>
</div>
<div cy-section="viewport-scenarios">
<h3>Viewport Scenarios</h3>
<div class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" cy-label="Position absolute element outside viewport" style="position: absolute; top: -100px; left: -100px; width: 100px; height: 100px; background: lightcoral;">Element outside viewport</div>
<div class="testCase" cy-expect="hidden" cy-label="Position fixed element outside of viewport" style="position: fixed; top: -100px; left: -100px; width: 100px; height: 100px; background: lightcoral;">Position fixed element outside of viewport</div>
<iframe style="display:none;">
<body>
<div class="testCase" cy-expect="hidden" cy-label="Element inside hidden iframe" style="width: 100px; height: 100px; background: lightcoral;">Element inside hidden iframe</div>
</body>
</iframe>
</div>
</body>
</html>

View File

@@ -109,5 +109,27 @@
</div>
</div>
<div class="test-section" cy-section="positioning-cousin-coverage" >
<h3>Positioning Cousin Coverage</h3>
<!-- Element covered by an absolutely positioned cousin -->
<div style="position: relative;">
<div style="width: 200px; height: 100px; background: lightblue;">
<span class="testCase" cy-fast-expect="hidden" cy-legacy-expect="visible" cy-label="Target element to be covered" style="display: block; width: 100px; height: 50px; background: lightgreen;">Target element</span>
</div>
<div class="testCase" cy-expect="visible" cy-label="Absolutely positioned cousin covering target" style="position: absolute; top: 0px; left: 0px; width: 100px; height: 50px; background: lightcoral;">Covering cousin</div>
</div>
</div>
<div class="test-section" cy-section="z-index-coverage">
<h3>Z-Index Coverage</h3>
<!-- Element covered by higher z-index element -->
<div style="position: relative; width: 300px; height: 150px; background: lightgray;">
<div class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" cy-label="Element covered by higher z-index" style="position: absolute; top: 20px; left: 20px; width: 100px; height: 100px; background: lightgreen; z-index: 1;">Lower z-index element</div>
<div class="testCase" cy-expect="visible" cy-label="Higher z-index covering element" style="position: absolute; top: 20px; left: 20px; width: 100px; height: 100px; background: lightcoral; z-index: 2;">Higher z-index element</div>
</div>
</div>
</body>
</html>

View File

@@ -37,6 +37,7 @@
<div class="testCase" cy-expect="visible" cy-label="Translate 2D" style="transform: translate(10px, 15px)">Translated in 2 dimensions</div>
<div class="testCase" cy-expect="visible" cy-label="Translate Z" style="transform: translateZ(10px)">Translated in 3rd dimension</div>
<div class="testCase" cy-expect="visible" cy-label="Translate 3D" style="transform: translate3d(10px, 15px, 20px)">Translated in 3 dimensions</div>
<div class="testCase" cy-legacy-expect="visible" cy-fast-expect="hidden" cy-label="Translate outside viewport" style="transform: translateY(-9999px)">Translated outside viewport</div>
</div>
<div class="test-section" cy-section="rotation">
<h3>Rotation</h3>

View File

@@ -329,13 +329,16 @@ chai.use((chai, u) => {
length = $utils.normalizeNumber(length)
// filter out anything not currently in our document
if ($dom.isDetached(obj)) {
obj = (this._obj = obj.filter((index, el) => {
// Cast to JQuery since we've already checked isJquery || isElement above
const $obj = (obj as JQuery<any>)
if ($dom.isDetached($obj)) {
obj = (this._obj = $obj.filter((index, el) => {
return $dom.isAttached(el)
}))
}
const node = obj && obj.length ? $dom.stringify(obj, 'short') : obj.selector
const node = $obj.length ? $dom.stringify($obj, 'short') : ($obj as any).selector
// if our length assertion fails we need to check to
// ensure that the length argument is a finite number
@@ -399,16 +402,18 @@ chai.use((chai, u) => {
}
} else {
let isAttached
// Cast to JQuery since we've already checked isJquery || isElement above
const $obj = (obj as JQuery<any>)
if (!obj.length) {
if (!$obj.length) {
this._obj = null
}
const node = obj && obj.length ? $dom.stringify(obj, 'short') : obj.selector
const node = $obj.length ? $dom.stringify($obj, 'short') : ($obj as any).selector
try {
return this.assert(
(isAttached = $dom.isAttached(obj)),
(isAttached = $dom.isAttached($obj)),
'expected \#{act} to exist in the DOM',
'expected \#{act} not to exist in the DOM',
node,

View File

@@ -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(!!utils.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'))
}
}

View File

@@ -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,

View File

@@ -2,7 +2,7 @@ import $ from 'jquery'
import _ from 'lodash'
// wrap the object in jquery
const wrap = (obj) => {
export function wrap (obj) {
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<any> => {
let hasJqueryProperty = false
try {

View File

@@ -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
}

View File

@@ -0,0 +1,322 @@
# Fast Visibility Algorithm Migration Guide
## Overview
This is an in-depth record of the differences between the experimental "fast" visibility algorithm, compared with the legacy algorithm.
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
Even if you are not regularly asserting on the visibility of elements, Cypress inherently uses the active visibility algorithm to determine _actionability_, as well. That means every time you `cy.click()` on an element, Cypress runs this visibility algorithm under the hood.
- **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 = {
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
npx run cypress # or your specific cypress command
```
### 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, visually inspect the application under test at the failing assertion. If you can both see and click on the element in the browser, this algorithm should consider it visible.
## 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 a Shadow DOM may not be detected properly as visible or hidden.
**Solution:**: Disable the `experimentalFastVisibility` setting for Shadow DOM tests.
## Rollback Plan
If you encounter issues that can't be easily resolved:
### Temporary Rollback
```javascript
// cypress.config.js
module.exports = {
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
**If the element should be visible, but Cypress determines that it is hidden:**
- Verify that the element is actually visible, and within the browser viewport. If you have to scroll to view the element, Cypress will not consider it visible.
- Verify that the element has proper dimensions. If either its height or width are zero, re-assess if this is the best element to be interacting with.
- Verify that the element does not have `pointer-events:none`
- In some extreme CSS `transform` scenarios, the element can be so distorted that Cypress fails to sample a visible point. If you hit this edge case, re-assess the usefulness of the assertion and/or interaction.
**Shadow DOM Related Errors**
Shadow DOM support is not yet available in the fast algorithm. Disable fast visibility for tests that rely on Shadow DOM support.
### 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.

View File

@@ -0,0 +1,126 @@
import $elements from '../elements'
import { memoize } from './memoize'
import { unwrap, wrap, isJquery } from '../jquery'
import Debug from 'debug'
const debug = Debug('cypress:driver:dom:visibility:fastIsHidden')
const { isOption, isOptgroup, isBody, isHTML } = $elements
const getBoundingClientRect = memoize((el: HTMLElement) => el.getBoundingClientRect())
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))
})
export function fastIsHidden (subject: JQuery<HTMLElement> | 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)
}
}
// contentVisibilityAuto is a valid browser API but not yet in TypeScript's CheckVisibilityOptions
if (!subject.checkVisibility({
contentVisibilityAuto: true,
opacityProperty: options.checkOpacity,
visibilityProperty: true,
} as CheckVisibilityOptions)) {
return true
}
const boundingRect = getBoundingClientRect(subject)
if (visibleToUser(subject, boundingRect)) {
debug('visibleToUser', subject, boundingRect)
return false
}
return true
}
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)
})
}
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)
}

View File

@@ -0,0 +1,42 @@
export function memoize<T> (fn: (...args: any[]) => T): (...args: any[]) => T {
const objIds = new WeakMap<any, number>()
let nextObjId = 0
const cache = new WeakMap<any, { result: T, timestamp: number }>()
function composeKey (...args: any[]): { key } {
return {
key: args.map((arg) => {
if (typeof arg === 'object' && arg !== null) {
return `${typeof arg}:${arg}`
}
if (['number', 'string', 'boolean'].includes(typeof arg)) {
return `${typeof arg}:${String(arg)}`
}
if (!objIds.has(arg)) {
objIds.set(arg, nextObjId)
nextObjId++
}
return `obj:${objIds.get(arg)}`
}).join('|'),
}
}
return (...args: any[]) => {
const key = composeKey(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
}
}

View File

@@ -219,7 +219,9 @@ export const preprocessLogLikeForSerialization = (props, attemptToSerializeFunct
const serializableArray: any[] = []
// in the case we are dealing with a jQuery array, preprocess to a native array to nuke any prevObject(s) or unserializable values
props.each((key) => serializableArray.push(preprocessLogLikeForSerialization(props[key], attemptToSerializeFunctions)))
props.each((key) => {
serializableArray.push(preprocessLogLikeForSerialization(props[key], attemptToSerializeFunctions))
})
return serializableArray
}

View File

@@ -584,6 +584,10 @@
"name": "CSP Allow List",
"description": "Enables Cypress to selectively permit Content-Security-Policy and Content-Security-Policy-Report-Only header directives, including those that might otherwise block Cypress from running."
},
"experimentalFastVisibility": {
"name": "Fast visibility",
"description": "Enables a new [visibility algorithm](https://on.cypress.io/experiments/#experimental-fast-visibility) that is faster and more accurate, but may break some tests that rely on specialized semantics of the legacy visibility algorithm."
},
"experimentalInteractiveRunEvents": {
"name": "Interactive run events",
"description": "Allows listening to the [`before:run`](https://on.cypress.io/before-run), [`after:run`](https://on.cypress.io/after-run), [`before:spec`](https://on.cypress.io/before-spec), and [`after:spec`](https://on.cypress.io/after-spec) events in plugins during interactive mode."

View File

@@ -32,6 +32,7 @@ exports['module api and after:run results'] = `
"experimentalSourceRewriting": false,
"experimentalSingleTabRunMode": false,
"experimentalWebKitSupport": false,
"experimentalFastVisibility": false,
"fileServerFolder": "/path/to/fileServerFolder",
"fixturesFolder": "/path/to/fixturesFolder",
"excludeSpecPattern": "*.hot-update.js",