Summary
getComputedStyle() reads the element's inline style attribute and nothing else. Stylesheet rules are never consulted, so any style set through a class, id or type selector is invisible to it. document.styleSheets is a stub that returns [], so there is also no way for a caller to reach the rules and resolve them by hand.
The practical effect is that a class-based style or visibility assertion silently returns the wrong answer instead of failing. That makes it a poor failure mode: the test passes while checking nothing.
Reproduction
import { Window } from 'very-happy-dom'
const window = new Window()
const document = window.document
document.head.innerHTML = '<style>.hidden { display: none } .brand { color: rgb(255, 0, 0) }</style>'
document.body.innerHTML = '<div class="hidden">a</div><div class="brand">b</div>'
const hidden = document.querySelector('.hidden')
const brand = document.querySelector('.brand')
console.log(window.getComputedStyle(hidden).display) // "block" — browsers: "none"
console.log(window.getComputedStyle(brand).color) // "rgb(0, 0, 0)" — browsers: "rgb(255, 0, 0)"
console.log(document.styleSheets.length) // 0 — browsers: 1Confirmed against main at 99cf5b2 with Bun 1.4.3.
Inline styles do work, which is what masks this — <div style="display:none"> computes display: none correctly.
Why this matters more than it looks
Every project in this org styles with utility classes (crosswind), so essentially no real component has its styles inline. In that setting:
getComputedStyle(el).displaycannot detect a hidden element.- Any helper built on it inherits the bug.
isVisible()-style checks report visible forclass="hidden". - Assertions of the form "this is hidden after the toggle" pass before the feature is written and keep passing after it breaks.
This is currently the blocker for documenting very-happy-dom as the component-testing path: the DOM layer is solid, but style and visibility assertions cannot be trusted.
Current implementation
VirtualDocument.getComputedStyle (src/nodes/VirtualDocument.ts:811) returns a Proxy whose get does:
const value = self.style.getPropertyValue(kebab)
if (value) return value
return computedDefaults(kebab, self.tagName)self.style is the inline declaration. There is no rule matching anywhere in the path, and computedDefaults is a per-tag/per-property default table.
VirtualDocument.styleSheets (src/nodes/VirtualDocument.ts:216) is:
get styleSheets(): CSSStyleSheet[] {
return []
}Note that the pieces to fix this already exist and are unused by this path:
CSSStyleSheet.replaceSync()(src/css/CSSOM.ts:320) parses CSS text intoCSSStyleRules withselectorTextand a populated.style.document.adoptedStyleSheetsis implemented and holds realCSSStyleSheets.- The selector engine already matches arbitrary selectors against elements, via
element.matches().
So this is wiring plus a cascade, not new parsing or matching.
Expected behavior
getComputedStyle should resolve a value in cascade order:
!importantdeclarations from stylesheets- the inline
styleattribute - normal stylesheet declarations, by descending specificity, then by document order for ties
- the existing per-tag defaults
and document.styleSheets should expose one CSSStyleSheet per <style> element in the document, in document order.
document.head.innerHTML = '<style>.hidden { display: none }</style>'
document.body.innerHTML = '<div class="hidden">a</div>'
getComputedStyle(document.querySelector('.hidden')).display // "none"Specificity should follow the usual (id, class/attribute/pseudo-class, type) ordering, so #x { color: blue } beats .x { color: red } regardless of source order, and a later rule beats an earlier one of equal specificity.
Worth deciding explicitly
- Inheritance. Browsers inherit
color,font-*,visibility,line-heightand others from the parent. This issue is about rules being applied at all; inherited properties could reasonably be a follow-up, but until thengetComputedStyle(child).colorwill not see a colour set on the parent. - At-rules.
replaceSyncdoes not descend into@media/@supportsblocks, so their inner rules are not matchable. They should be skipped rather than misapplied — a rule whose selector begins with@must never match an element. <link rel="stylesheet">. External stylesheets are not fetched, so they stay out of scope; only<style>elements andadoptedStyleSheetscan contribute.