fix(web): visibility reads the box, the style, and closed details, SVG included

Merged
OK
1mo ago
+238-24

Repro: toBeHidden() failed on four elements Playwright's own toBeHidden calls hidden, and isVisible() said true for each.

<svg style="visibility:hidden">...</svg>
<div style="width:0;height:0;overflow:hidden"><button>Clipped</button></div>
<details><summary>More</summary><button>Inside closed</button></details>
<div style="display:contents"></div>
  • The reader's hidden state counted client rects. An SVG skipped computed style altogether, a zero-size box still had one rect, closed <details> content still laid out, and display: contents returned visible before looking at anything under it.
  • isHidden now reads what Playwright's isElementVisible reads: computed style for any Element (SVG included), visibility other than visible or display: none is hidden, content of a closed details outside its summary is hidden, a box is visible only with width and height both above zero, and a display: contents element is visible only when some child element or text node is. packages/web/src/in-page/read-semantics.ts
  • styleOf reads any element now, so the name computation's separate nameStyleOf is gone.
  • The same state feeds toBeVisible, the visible query filter's final check, and which nodes the agent's screen lists. The isVisibleEmptyBox floor (MIN_BOX_SIDE) is untouched.
  • The tree walk prunes on its own test, hidesSubtree (aria-hidden, display: none, visibility, closed details). A box with no size does not clip fixed, absolute, or overflowing descendants, so the walk goes on through it and only leaves that element unlisted. The testbed's /canvas page is the case: every control is position: fixed, <html> lays out to zero height, and pruning there read the page as one node. The document root is listed whatever its own box.
  • Three role-mapping fixtures were empty zero-height blocks that only the rect-count quirk kept in the tree; they get a 1px box since those tests are about roles.
flowchart TD
  A[aria-hidden=true] -->|yes| H[hidden]
  A -->|no| C{display: contents?}
  C -->|yes| K{some child element or text visible?}
  K -->|yes| V[visible]
  K -->|no| H
  C -->|no| S{display none or visibility not visible?}
  S -->|yes| H
  S -->|no| D{inside closed details, outside its summary?}
  D -->|yes| H
  D -->|no| B{width > 0 and height > 0?}
  B -->|yes| V
  B -->|no| H

packages/web/tests/integration/visibility.test.ts checks each case and its control against Playwright's isHidden() on the same element; the runner-level kitchen sink in packages/e2e/tests/integration/web-platform.test.ts asserts toBeHidden/toBeVisible/isVisible() through the CLI path. The aria-hidden ancestor case is #494 and stays as is.


Devin Review