ReviewOS

stacks/very-happy-dom

@media rules never apply, so the cascade and matchMedia disagree, and there is no emulateMedia()

#1611
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Problem

Two halves of media-query support disagree, and neither can be re-pointed at runtime.

matchMedia answers prefers-color-scheme (Window.ts:818), but the cascade refuses to look inside an @media block:

// src/css/cascade.ts:102
function isUnmatchable(selectorText: string): boolean {
  // `replaceSync` does not descend into at-rule blocks, so a `@media` rule
  // arrives with its selector intact ...
  return selectorText.length === 0 || selectorText.startsWith('@')
}

So:

const w = new Window()
w.document.head.innerHTML = `<style>
  .b { color: red }
  @media (prefers-color-scheme: dark) { .b { color: blue } }
</style>`
w.document.body.innerHTML = '<div class="b"></div>'

w.matchMedia('(prefers-color-scheme: dark)').matches  // false
w.getComputedStyle(el).getPropertyValue('color')      // 'red'

matches is false there because prefersColorScheme defaults to 'light' — and the only place it can be set is the Browser constructor (Browser.ts:32). BrowserContext._emulation() carries permissions, geolocation and offline, and not this, so there is no page.emulateMedia({ colorScheme: 'dark' }) and no way to reach the other branch of a dark-mode stylesheet at all.

typeof page.emulateMedia   // 'undefined'

Why it matters

This is the shape of #1600 again: two APIs that should be answering the same question about the same element, giving different answers. Someone confirms matchMedia('(prefers-color-scheme: dark)') reports true, reasonably concludes dark mode is active, and writes a colour assertion that compares the light value to the light value.

The @media half fails quietly in one direction only — rules inside the block are dropped, so the page always renders as if every media query were false. A responsive stylesheet whose mobile rules live in @media (max-width: 640px) is untestable: setViewport({ width: 375 }) changes what matchMedia says and nothing about what getComputedStyle returns.

Width and height queries already evaluate against the viewport in matchMedia, so the information the cascade needs is present and unused.

Proposed scope

Two independent pieces; the first is worth doing even alone.

Apply @media rules in the cascade. When a rule's selector starts with @media, evaluate the condition through the same code matchMedia uses and, if it holds, descend into the block and treat its rules as ordinary ones at their own specificity. That means collectCascade needs to walk CSSMediaRule.cssRules — CSSMediaRule exists (CSSOM.ts:180) with mediaText and conditionText. Non-@media at-rules must keep being skipped; the current blanket startsWith('@') is right for @font-face and wrong for @media, so the fix is to narrow it rather than remove it.

The media-condition evaluation should be extracted out of Window.matchMedia and shared. Two parsers that disagree about (min-width: 640px) would be worse than one that is only used in one place.

Add page.emulateMedia({ media, colorScheme, reducedMotion, forcedColors }). Route it through BrowserContext._emulation() and _applyEmulation(), which already exist for geolocation and offline, so a live page picks the change up. media: 'print' then also becomes meaningful once @media print blocks are read.

Acceptance

  • The reproduction above reports blue after await page.emulateMedia({ colorScheme: 'dark' }), and red before.
  • A @media (max-width: 640px) rule applies after setViewport({ width: 375 }) and not at width: 1280, in getComputedStyle and in getBoundingClientRect alike — the latter because #1600 routed box metrics through the same resolution.
  • matchMedia and the cascade agree for the same query, asserted together, so they cannot drift apart again.
  • @font-face and other at-rules are still skipped, asserted, so narrowing the guard does not let a non-@media block through as declarations.

Found while assessing whether wildloop's Playwright specs could port here.

Sign in to comment on this issue.