Skip to content

--computed: diff links' and controls' :hover and :focus-visible styles - #95

Merged
tmashara merged 2 commits into
mainfrom
claude/issue-51-computed-states
Oct 29, 2026
Merged

tmashara merged 2 commits into
mainfrom
claude/issue-51-computed-states

Conversation

@tmashara

@tmashara tmashara commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

What / why

--computed only read the resting state, so a dead-CSS removal that dropped a hover colour or a focus ring passed silently (#51). Now, after the resting walk, links and controls (a[href], button, input, select, textarea, summary, [tabindex]) are read again in :hover and :focus-visible, subtrees included, and diffed like any other key:

Real output: the visual test's link (a:hover{color:red} a:focus-visible{outline:2px solid blue} on the left only) at /, and 812 links at /docs, --width 1280 --computed:

computed: 4 findings; dom identical on 2 of 2 route(s)
  body > nav > a[href="#x"]:focus-visible outline rgb(0, 0, 255) solid 2px→rgb(0, 95, 204) auto 1px  /@1280
  body > nav > a[href="#x"]:hover color rgb(255, 0, 0)→rgb(0, 0, 238)  /@1280
  body > nav > a[href="#x"]:hover border-top-color rgb(255, 0, 0)→rgb(0, 0, 238)  /@1280
  body > nav > a[href="#x"]:hover > span.icon border-top-color rgb(255, 0, 0)→rgb(0, 0, 238)  /@1280
  states: only the first 500 links and controls forced on /docs@1280 (+312)

(border-top-color follows currentColor, as it does for resting colour changes.)

The snapshot also reads outline and text-decoration-line (the usual focus ring and hover underline). These live in a new SNAP_PROPS; pixel mode's PROPS is untouched, so ElementDiff.changed and accepted fingerprints don't move.

How states are forced

  • One CDP session per side (snapshotStates in locate.ts). The resting walk keeps the elements on a page global and returns their keys in order; Runtime.getProperties + DOM.requestNode turn them into nodeIds with no DOM mutation (works for --selector :scope roots too).
  • Per state, CSS.forcePseudoState on all of them at once (["hover"], then ["focus","focus-visible"]; a new force replaces the last), then the in-page walk visits only those elements under key:hover / key:focus-visible, reusing visit (children …:hover > span, …:hover::before).
  • The CDP session opens before the walk keeps the global, and finally deletes the global and detaches, which clears every forced state, before it resolves; the page is resting for whatever runs next. Runs after the screenshots and locate, so pixels never see a forced state.
  • A state change matching the same element's resting change is dropped (collapseInherited), so a resting change isn't repeated per state. State keys don't count towards dom or unmatched.

What forcing all at once misses: rules relating two forced elements (ul:has(a:hover) a:not(:hover) dims siblings only when one is hovered), :hover on ancestors (forcing never sets it, so li:hover > ul menus aren't seen), :active, :focus-within, script hover handlers. Per-element forcing would catch the first but costs a style recalc each (~10 s at 2000 links). It never invents a finding.

Caps

  • First 500 per page per side, document order; the rest are counted (computed.unchecked, max of the two sides) and named on a states: line.
  • Elements with more than 50 descendants are skipped (a <main tabindex=-1> would re-walk the page per state).
  • The states share the existing 15 s walk budget and 50k element limit; failure modes are unchanged.

Cost (M-series mac, one side; both sides run in parallel)

links resting walk with states
50 26 ms 55 ms
500 146 ms 406 ms
2000 (500 forced) 582 ms 828 ms

How tested

  • tests/visual/computed.test.ts: hover colour one side drops → only a:hover color (+ currentColor border); a dropped focus ring → a:focus-visible outline; a:hover .icon → keyed a:hover > span.icon; identical states → nothing, and a resting text-decoration change isn't repeated per state; 501 links → unchecked 1, global gone and a resting re-read equals the one before.
  • tests/unit/computed.test.ts: a one-sided state key isn't markup (dom, unmatched), the resting dedupe, the states: line.
  • The new visual tests bite on behaviour, not just at import: with CSS.forcePseudoState stubbed out, or the state passes skipped, the hover, focus, child and cap tests fail; without the detach or the global's delete the cap test fails; without the resting dedupe the dedupe test fails. Full unit (354) and visual (385) tiers pass after rebasing on --aria (Add --aria: diff ARIA snapshots per route #93), including accepted.test.ts and the pixel-mode pins, unchanged. typecheck, lint, format:check clean.
  • Docs: docs/output.md § Computed styles, README and --help flag rows, skill reference/SKILL wording on unshot states.

Fixes #51

🤖 Generated with Claude Code

tmashara and others added 2 commits October 29, 2026 20:52
After the resting walk, the first 500 links and controls per page are forced into
:hover, then :focus+:focus-visible, over CDP (all at once), and their subtrees are
read again under `key:hover` / `key:focus-visible`. A state only reports what
differs from resting. The snapshot also reads outline and text-decoration-line
(SNAP_PROPS); pixel mode's PROPS, and so accepted fingerprints, are unchanged.

Fixes #51

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…orcing in the cap test

The global was set before the session existed, so a failed newCDPSession left it on the page.
The cap test now checks the forced :hover value, so it fails when forcing doesn't happen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@tmashara
tmashara force-pushed the claude/issue-51-computed-states branch from a2f7f4d to 9e3e338 Compare October 29, 2026 19:55
@tmashara
tmashara merged commit f3fca69 into main Oct 29, 2026
3 checks passed
@tmashara
tmashara deleted the claude/issue-51-computed-states branch October 29, 2026 19:59
@tmashara tmashara mentioned this pull request Oct 30, 2026
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.

--computed: snapshot :hover and :focus states

1 participant