Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/driver/cypress/e2e/dom/visibility.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ describe('src/cypress/dom/visibility', {
'overflow-flex-container',
'overflow-complex-scenarios',
Cypress.browser.name !== 'firefox' || mode === 'legacy' ? 'clip-path-scenarios' : undefined,
'scrollable-viewport-scenarios',
])
})

Expand Down Expand Up @@ -366,6 +367,53 @@ describe('src/cypress/dom/visibility', {
])
})
})

it('scrolls off-screen elements into view before checking visibility', () => {
cy.visit('/fixtures/visibility/overflow.html')
cy.window().then((win) => {
const doc = win.document
const el = doc.createElement('div')

el.textContent = 'off-screen element'
el.style.cssText = 'position: absolute; top: 5000px; width: 100px; height: 100px; background: green;'
doc.body.appendChild(el)

const rect = el.getBoundingClientRect()

expect(rect.top, `element top (${rect.top}) should be greater than viewport height (${win.innerHeight})`).to.be.greaterThan(win.innerHeight)
expect(Cypress.config('experimentalFastVisibility'), `experimentalFastVisibility should be ${mode === 'fast'}`).to.eq(mode === 'fast')

if (mode === 'fast') {
expect(dom.isVisible(el), 'off-screen element should be visible in fast mode (scrolled into view)').to.be.true
} else {
expect(dom.isVisible(el), 'off-screen element should be visible in legacy mode').to.be.true
}
})
})

it('does not scroll when scrollBehavior is false', {
scrollBehavior: false,
}, () => {
cy.visit('/fixtures/visibility/overflow.html')
cy.window().then((win) => {
const doc = win.document
const el = doc.createElement('div')

el.textContent = 'off-screen element'
el.style.cssText = 'position: absolute; top: 5000px; width: 100px; height: 100px; background: green;'
doc.body.appendChild(el)

const rect = el.getBoundingClientRect()

expect(rect.top, `element top (${rect.top}) should be greater than viewport height (${win.innerHeight})`).to.be.greaterThan(win.innerHeight)

if (mode === 'fast') {
expect(dom.isVisible(el), 'off-screen element should be hidden in fast mode when scrollBehavior is false').to.be.false
} else {
expect(dom.isVisible(el), 'off-screen element should be visible in legacy mode').to.be.true
}
})
})
})
}

Expand Down
7 changes: 7 additions & 0 deletions packages/driver/cypress/fixtures/visibility/overflow.html
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ <h3>Clip-Path Scenarios (note: legacy mode does not support clip-path)</h3>
<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 class="test-section" cy-section="scrollable-viewport-scenarios" style="position: relative; min-height: 5000px;">
<h3>Scrollable Viewport Scenarios</h3>
<div class="testCase" cy-expect="visible" cy-label="Element below the fold" style="position: absolute; top: 3000px; width: 100px; height: 100px; background: lightgreen;">
Below the fold element
</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>
Expand Down
12 changes: 6 additions & 6 deletions packages/driver/src/dom/visibility/MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ Enable fast visibility if you experience:
- 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

Expand All @@ -54,7 +52,7 @@ While comprehensive, this list may not be complete. Additional discrepancies may
| **[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. |
| **[viewport-scenarios](../../../cypress/fixtures/visibility/overflow.html)** | Absolutely positioned element outside of the viewport | ✅ Yes | ❌ No | ❌ No | Elements positioned outside the scrollable document bounds (e.g. negative absolute positioning) cannot be scrolled into view and remain hidden. Elements that are merely below the fold are automatically scrolled into view per the `scrollBehavior` config before visibility is checked. |
| **[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 |
Expand Down Expand Up @@ -129,9 +127,11 @@ cy.get('.rotated-element').should('be.hidden')

### Issue 2: Elements Outside Viewport

**Problem**: Elements positioned outside the viewport are now correctly identified as hidden.
**Problem**: Elements positioned outside the scrollable document bounds (e.g. `position: absolute; top: -100px`) are identified as hidden because they cannot be scrolled into view.

**Solution**: Scroll the element into view before testing:
**Note**: Elements that are simply below the fold or off-screen but within the scrollable document are now automatically scrolled into view before visibility is checked, using the `scrollBehavior` configuration setting. This matches the behavior of action commands like `cy.click()`. If `scrollBehavior` is set to `false`, off-screen elements will still be considered hidden.

**Solution** (for elements outside scrollable bounds):

```javascript
// Before
Expand Down Expand Up @@ -290,7 +290,7 @@ 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 is actually visible. Elements below the fold are automatically scrolled into view before checking visibility (per the `scrollBehavior` config). Elements outside the scrollable document bounds (e.g. negative absolute positioning) cannot be scrolled into view and will be considered hidden.
- 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.
Expand Down
33 changes: 32 additions & 1 deletion packages/driver/src/dom/visibility/fastIsHidden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ const debug = Debug('cypress:driver:dom:visibility:fastIsHidden')

const { isOption, isOptgroup, isBody, isHTML } = $elements

const scrollBehaviorBlockMap: Record<string, ScrollLogicalPosition> = {
top: 'start',
bottom: 'end',
center: 'center',
nearest: 'nearest',
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

const getBoundingClientRect = memoize((el: HTMLElement) => el.getBoundingClientRect())

const visibleAtPoint = memoize(function (el: HTMLElement, x: number, y: number): boolean {
Expand Down Expand Up @@ -55,7 +62,18 @@ export function fastIsHidden (subject: JQuery<HTMLElement> | HTMLElement, option
return true
}

const boundingRect = getBoundingClientRect(subject)
let boundingRect = getBoundingClientRect(subject)

if (isOutsideViewport(subject, boundingRect)) {
const scrollBehavior = Cypress.config('scrollBehavior')

if (scrollBehavior !== false) {
const block = scrollBehaviorBlockMap[scrollBehavior as string] || 'start'

subject.scrollIntoView({ block, behavior: 'instant' as ScrollBehavior })
boundingRect = subject.getBoundingClientRect()
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

if (visibleToUser(subject, boundingRect)) {
debug('visibleToUser', subject, boundingRect)
Expand Down Expand Up @@ -96,6 +114,19 @@ function visibleToUser (el: HTMLElement, rect: DOMRect, maxDepth: number = 2, cu
})
}

function isOutsideViewport (el: HTMLElement, rect: DOMRect): boolean {
const win = el.ownerDocument.defaultView

if (!win) return false

return (
rect.bottom <= 0 ||
rect.right <= 0 ||
rect.top >= win.innerHeight ||
rect.left >= win.innerWidth
)
}

function subDivideRect ({ x, y, width, height }: DOMRect): DOMRect[] {
return [
DOMRect.fromRect({
Expand Down
Loading