Skip to content

fix(driver): make fast visibility shadow-DOM aware#33737

Open
mschile wants to merge 6 commits intomschile/pensive-rubin-df92b9from
mschile/pensive-black-034995
Open

fix(driver): make fast visibility shadow-DOM aware#33737
mschile wants to merge 6 commits intomschile/pensive-rubin-df92b9from
mschile/pensive-black-034995

Conversation

@mschile
Copy link
Copy Markdown
Contributor

@mschile mschile commented May 4, 2026

Additional details

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 (host's box doesn't include the sample coord) or compared against a host they don't light-tree-contain. The whole visibility_shadow_dom.cy.ts suite was gated to legacy mode only as a result.

This PR pierces nested shadow roots from the document-level hit using the existing getShadowElementFromPoint() helper, and replaces contains() with a shadow-aware ancestor walk via findParent() (which crosses shadow boundaries via getRootNode().host). Both helpers are pre-existing — no new utility code.

The shadow-DOM visibility suite is now enabled under both modes. 8 tests where the fast multi-sample and legacy center-point algorithms inherently diverge for shadow subjects are scoped-skipped under fast via a tiny itSkipFast helper, 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:

  • Cover detection where the cover is narrower than the underneath element (2 tests). Legacy samples only the center → cover hits → reports hidden; fast samples the corners → uncovered corners are visible → reports visible. Same divergence exists in non-shadow, where the equivalent fixtures are sized so the cover fully encloses the underneath.
  • pointer-events: none across shadow boundaries (2 tests). Non-shadow analogues pass under fast, but the inheritance through the host changes what elementFromPoint returns inside the shadow tree.
  • Shadow content escaping its host's bounding box via position: absolute / relative (4 tests). document.elementFromPoint doesn't return the host at the shadow content's rendered coordinates because the host's flow box doesn't include them; falling back to shadowRoot.elementFromPoint was tried but does not resolve these cases either.

This PR is stacked on top of #33736 because it builds on that branch's elementFromPoint related changes.

Steps to test

  1. yarn workspace @packages/driver cypress:run -- --spec cypress/e2e/dom/visibility_shadow_dom.cy.ts — should report 78 passing, 8 pending, 0 failing.
  2. 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).

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 — elementFromPoint pierces shadow roots, and ancestor checks cross host boundaries.

PR Tasks


Note

Medium Risk
Changes core fastIsHidden visibility logic (hit-testing and scrolling/clipping) and could alter visibility outcomes in edge cases, especially with nested shadow roots and overflow scrolling. Test updates mitigate regressions but several shadow-specific scenarios are now explicitly skipped under fast mode.

Overview
Makes the fast visibility algorithm shadow-DOM aware by piercing nested shadow roots during hit-testing (getShadowElementFromPoint) and replacing contains() with a shadow-boundary-crossing ancestor walk (findParent).

Adjusts fast mode’s scrolling/clipping behavior by replacing the prior “any clipping ancestor” check with an isClippedByAncestor test that (a) walks across shadow boundaries, (b) treats auto/scroll overflow as clipping, and (c) verifies the element is actually out-of-bounds of the ancestor before blocking scrollIntoView.

Enables the shadow-DOM visibility E2E suite to run in both fast and legacy modes, adding a itSkipFast helper to mark a small set of known-divergent cases as pending under fast while keeping legacy coverage intact.

Reviewed by Cursor Bugbot for commit f5a46c4. Bugbot is set up for automated code reviews on this repo. Configure here.

mschile added 2 commits May 4, 2026 16:42
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
Copy link
Copy Markdown

cypress Bot commented May 4, 2026

cypress    Run #70431

Run Properties:  status check passed Passed #70431  •  git commit f5a46c4214: fix(driver): exempt the document root from the clipping-ancestor check
Project cypress
Branch Review mschile/pensive-black-034995
Run status status check passed Passed #70431
Run duration 18m 30s
Commit git commit f5a46c4214: fix(driver): exempt the document root from the clipping-ancestor check
Committer Matthew Schile
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 9
Tests that did not run due to a developer annotating a test with .skip  Pending 1112
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 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.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8a3dea9. Configure here.

Comment thread packages/driver/src/dom/visibility/fastIsHidden.ts
mschile added 3 commits May 4, 2026 18:28
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant