fix(driver): make fast visibility shadow-DOM aware#33737
Conversation
Resolves #33046 The fast visibility algorithm used document.elementFromPoint() and Element.contains() in visibleAtPoint(), neither of which crosses shadow boundaries — so subjects inside a shadow root were either missed at sample points or compared against a host they don't light-tree-contain. Pierce nested shadow roots from the document-level hit using the existing getShadowElementFromPoint() helper, and replace contains() with a shadow-aware ancestor walk via findParent(), which crosses shadow boundaries via getRootNode().host. Enables the shadow-DOM visibility suite under fast mode (previously gated to legacy only). 8 tests where the fast multi-sample and legacy center-point algorithms inherently diverge for shadow subjects (cover detection with covers narrower than the underneath, pointer-events: none across host boundaries, complex out-of-bounds overflow) are scoped-skipped under fast via an itSkipFast helper, with a comment pointing back to the issue for fixture-level follow-up.
`hasClippingAncestor` was walking via `parentElement`, which returns null at shadow root boundaries — so a shadow descendant's `overflow: hidden` ancestor in the host's light tree was invisible to the check. The clipping-ancestor guard then incorrectly returned false, and `scrollIntoView` ran on a subject that should have been left clipped, exposing content the test author intentionally hid. Switch to `getParentNode` (already used elsewhere in the driver for shadow-aware traversal) so the parent walk crosses shadow boundaries via `getRootNode().host`. Fixes the three driver-integration regressions surfaced by enabling the shadow-DOM visibility suite under fast mode: - is hidden when parent outside of shadow dom overflow hidden and out of bounds below - is hidden when parent outside of shadow dom overflow hidden-y and out of bounds - is hidden when parent outside of shadow dom has overflow scroll and out of bounds
cypress
|
||||||||||||||||||||||||||||
| Project |
cypress
|
| Branch Review |
mschile/pensive-black-034995
|
| Run status |
|
| Run duration | 19m 13s |
| Commit |
|
| Committer | Matthew Schile |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
8
|
|
|
1184
|
|
|
0
|
|
|
24926
|
| View all changes introduced in this branch ↗︎ | |
Warning
No Report: Something went wrong and we could not generate a report for the Application Quality products.
…t visibility The clipping-ancestor guard in `fastIsHidden` only skipped programmatic scroll-into-view for ancestors with `overflow: hidden` or `clip`, treating `scroll` and `auto` as scrollable enough to expose otherwise out-of-bounds content. Per Cypress visibility semantics — "is the user seeing this right now?" — content that is clipped because the user has not scrolled the container should report hidden, the same way it does for `overflow: hidden`. Treating `scroll` and `auto` the same way fixes the Firefox shadow-DOM regression (the existing "is hidden when parent outside of shadow dom has overflow scroll and out of bounds" test passes in Chrome only by chance, because Chrome's `scrollIntoView` on an absolutely-positioned descendant of an overflow:scroll container behaves differently than Firefox's). Pull the four overflow values into a single set so the relationship is explicit.
The previous clipping-ancestor guard skipped programmatic scrolling whenever any ancestor on the off-screen axis had clipping `overflow`. That was too coarse: a subject merely below the fold of an `overflow: hidden` (or `clip`/`scroll`/`auto`) container is not intentionally clipped — it is just outside the viewport — and `scrollIntoView` brings it into view without exposing anything the author wanted to hide. Conversely, a subject positioned outside the ancestor's box (e.g. `position: absolute; bottom: -100px`) is what the guard meant to protect. Replace `hasClippingAncestor` with `isClippedByAncestor`: it only reports clipping when the subject's rect actually falls outside the ancestor's rect on the same axis the subject is off-screen. Treats `scroll`/`auto` the same as `hidden`/`clip` because the user has not scrolled the container, so out-of-bounds content is hidden right now — this addresses the Cursor Bugbot concern by virtue of the in-bounds check, not by excluding scrollable values. Concrete effects: - Unblocks three previously-skipped shadow DOM overflow tests where the subject is in-bounds of an overflow ancestor but the ancestor is below the fold. - Keeps the Firefox CI fix for `overflow: scroll` out-of-bounds shadow subjects (they remain reported hidden). - No change in behavior for non-shadow visibility cases that already passed: the guard still only matters when the subject is outside the viewport, and only fires when the subject is also outside the clipping ancestor.
…ed under fast The earlier comment said fixtures were "authored to legacy semantics," which is true but doesn't pinpoint *why* fast cannot match. Replace with the actual mechanism for each remaining skip: the cover-detection mismatch (multi-sample corners vs center-only), `pointer-events: none` (browsers skip such elements in `elementFromPoint`, so no sample ever lands on the subject), and a clipping ancestor between the subject and its containing block (legacy's `canClipContent` ignores it via `offsetParent` rules). The corresponding non-shadow scenarios either avoid these patterns or aren't exercised by `visibility.cy.ts` — documented so the next person to look at the skips understands what work the fix would actually require.
The previous commit treated `overflow: scroll`/`auto` ancestors as clipping when the subject was outside their box. That broke an existing fast-mode test: setting `overflow-x: hidden` on body causes computed `overflow-y` to become `auto` (per CSS spec), which then fired the clipping check on the off-screen axis and prevented the scroll-into-view that elements below the fold need. `<body>` and `<html>` are the page's scroll container, not real clipping containers — programmatic scroll there is exactly what the user would do to bring an element below the fold into view, and it does not surface anything the test author intentionally hid. Skip them while walking clipping ancestors so the orthogonal-axis case works again. Local non-shadow visibility now reports 136 passing / 0 failing.
Adds packages/driver/cypress/e2e/dom/_TEMP_fast_vis_shadow_playground.cy.ts so the fix in #33046 / this PR can be poked at in `cypress open`. The spec is a focused subset of visibility_shadow_dom.cy.ts (5 scenarios that the fix unblocks under fast, plus the 4 still-skipped divergences) so each example is visible in isolation in the runner. The "still skipped" describe uses a local `itDivergent = mode === 'fast' ? it.skip : it` helper so CI stays green; flip it to `it` locally to watch fast turn red and inspect the AUT. DELETE before merging — the `_TEMP_` prefix is a flag.
| // - clipping ancestor between the subject and its containing block (legacy's | ||
| // `canClipContent` uses `offsetParent` rules to ignore such ancestors) | ||
| // Tracked as follow-up to https://github.com/cypress-io/cypress/issues/33046. | ||
| const itSkipFast = mode === 'fast' ? it.skip : it |
There was a problem hiding this comment.
PR claims 8 skipped tests but only 5 exist
Medium Severity
The PR description states "8 tests … are scoped-skipped under fast" and expects "78 passing, 8 pending," but only 5 itSkipFast calls exist in the file (lines 248, 262, 276, 303, 612). With 43 tests × 2 modes − 5 skips, the actual result is 81 passing / 5 pending. The "shadow content escaping its host's bounding box" bucket claims 4 tests but only 1 (line 612) uses itSkipFast. If the other 3 tests in that bucket genuinely fail under fast mode, CI will break; if they pass, the PR description is misleading.
Reviewed by Cursor Bugbot for commit 5bcc769. Configure here.
Cursor Bugbot flagged that the temporary playground at
`packages/driver/cypress/e2e/dom/_TEMP_fast_vis_shadow_playground.cy.ts`
gets picked up by glob-based CI and adds noise to the run. Wrap the
top-level describe with `Cypress.env('CI') ? describe.skip : describe`
so it only executes locally in `cypress open`. The authoritative
shadow-DOM coverage is still in `visibility_shadow_dom.cy.ts`.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e0b1328. Configure here.
…ues only
Cursor Bugbot pointed out that the previous "skip body/html entirely
from the clipping check" rule was too broad — it correctly handled the
common `body { overflow-x: hidden }` pattern (where CSS converts
`overflow-y` to computed `auto`) but also let an explicit
`body { overflow: hidden }` slip through, even though that does
intentionally clip its content.
Split the clipping-overflow set in two:
- `CLIPPING_OVERFLOW` (`hidden` / `clip` / `scroll` / `auto`) — used for
ordinary ancestors. The user has not scrolled the container, so any
out-of-bounds content on the off-screen axis is hidden right now.
- `DOC_ROOT_CLIPPING_OVERFLOW` (`hidden` / `clip` only) — used when the
ancestor is `<body>` or `<html>`. The page's scroll container should
not have `auto`/`scroll` (often the CSS-spec auto-conversion) treated
as clipping; programmatic vertical scroll has to remain available for
the orthogonal-axis case. Explicit `hidden`/`clip` on the document
root, however, really is a clip and is still detected.
Local: shadow 81 passing / 5 pending, non-shadow 136 passing / 0 failing.
…to mschile/pensive-black-034995 # Conflicts: # packages/driver/src/dom/visibility/fastIsHidden.ts


Additional details
The fast visibility algorithm used
document.elementFromPoint()andElement.contains()invisibleAtPoint(), neither of which crosses shadow boundaries — so subjects inside a shadow root were either missed at sample points (host's box doesn't include the sample coord) or compared against a host they don't light-tree-contain. The wholevisibility_shadow_dom.cy.tssuite was gated tolegacymode only as a result.This PR pierces nested shadow roots from the document-level hit using the existing
getShadowElementFromPoint()helper, and replacescontains()with a shadow-aware ancestor walk viafindParent()(which crosses shadow boundaries viagetRootNode().host). Both helpers are pre-existing — no new utility code. It also makes the rubin-branch's clipping-ancestor scroll guard shadow-aware (walks viagetParentNode), refines it to only fire when the subject is actually out-of-bounds of the ancestor, treatsoverflow: scroll/autothe same ashidden/clip, and exempts<body>/<html>(the page's scroll container) from the check.The shadow-DOM visibility suite is now enabled under both modes. 5 tests where the fast multi-sample and legacy center-point algorithms inherently diverge for shadow subjects are scoped-skipped under fast via a tiny
itSkipFasthelper, with a comment pointing back to the issue for fixture-level follow-up. The skip is fast-only — legacy still runs all 43.The skipped scenarios fall into three buckets, each requiring fixture work rather than further algorithm changes:
pointer-events: noneon the subject or an ancestor (2 tests). Browsers skip such elements inelementFromPoint, so fast can never find the subject at any sample point. The non-shadowpointer-eventsfixtures aren't actually exercised byvisibility.cy.ts, so the same gap exists there but isn't surfaced.canClipContentusesoffsetParentrules to ignore such ancestors; fast does not.This PR is stacked on top of #33736 because it builds on that branch's
elementFromPointrelated changes.Steps to test
yarn workspace @packages/driver cypress:run -- --spec cypress/e2e/dom/visibility_shadow_dom.cy.ts— should report 81 passing, 5 pending, 0 failing.yarn workspace @packages/driver cypress:run -- --spec cypress/e2e/dom/visibility.cy.ts— non-shadow visibility behavior must remain unchanged from fix(driver): scroll off-screen elements into view in fast visibility algorithm #33736's baseline (no new failures attributable to this PR).yarn workspace @packages/driver cypress:openand pickdom/_TEMP_fast_vis_shadow_playground.cy.tsto poke at each scenario in isolation. The playground isdescribe.skip-ed under CI to avoid spec-list noise — it only runs interactively.How has the user experience changed?
Before: with
experimentalFastVisibility: true, visibility assertions on Shadow DOM subjects could give different results from non-shadow subjects (the documented "Current Limitations" in the migration guide). Test suites covering shadow components against fast visibility were effectively unsupported.After: shadow DOM subjects use the same algorithmic path as non-shadow subjects under fast visibility —
elementFromPointpierces shadow roots, and ancestor checks cross host boundaries.PR Tasks
cypress-documentation? #6435type definitions?Note
Medium Risk
Changes core visibility detection in the driver (hit-testing, ancestor walking, and scroll/clipping heuristics), which can affect many assertions across browsers despite being well-covered by e2e tests.
Overview
Fast visibility now pierces Shadow DOM when hit-testing and determining containment:
visibleAtPointusesgetShadowElementFromPoint()and replacesel.contains()with a shadow-aware ancestor walk viafindParent().The scroll/clipping guard was refined to detect actual out-of-bounds clipping (not merely below-the-fold), treats
overflow: scroll/autoas clipping, exempts document root overflow auto/scroll, and walks ancestors across shadow boundaries.Shadow DOM visibility e2e coverage is enabled for both
fastandlegacy, with a small set of known fast/legacy divergences skipped under fast; an interactive, CI-skipped playground spec was added for manual exploration.Reviewed by Cursor Bugbot for commit 4373146. Bugbot is set up for automated code reviews on this repo. Configure here.