test: run DOM suites against headless Chromium - #256
Merged
Conversation
Tasty compiles to CSS, and only a real CSS engine can tell you whether that CSS is valid. jsdom and happy-dom reject @container, @starting-style, @Property, @function, and CSS nesting outright, which had quietly hollowed out the suites that depended on them: 53 of the 54 snapshots in advanced-states.test.tsx were empty strings asserting nothing, six tests were empty `it()` bodies, and two were `it.skip`. Split Vitest into two projects — `node` for pure logic (parser, style handlers, pipeline, SSR strings, Babel extractor) and `browser` for anything touching `document` or asserting on CSS the engine parsed. jsdom and happy-dom are dropped entirely. Total runtime went 11.3s to 4.6s, mostly because jsdom's 54s of environment setup is gone. Three places would otherwise have lost coverage in the move: - Two @Property tests relied on happy-dom rejecting the rule natively, which Chromium accepts. They now stub insertRule explicitly, so the branch stays covered and cannot silently lapse if happy-dom adds support. - useKeyframes only asserted rule content in text mode; Chromium round-trips CSSKeyframesRule.cssText, so it asserts in both. - The @function client CSSOM path had no coverage at all. Also fixes a bug the new environment surfaced: gridColumns/gridRows only expanded real numbers, so `gridColumns: { '': '3' }` emitted `grid-template-columns: 3` — invalid CSS browsers drop silently. The cause was createStyle skipping converters for string values. Changing that gate meant gridTemplate had to fall back per segment, or `auto / 1fr` would have collapsed to `/`. Carries a changeset; snapshots for it live in the same file as the migration, hence one commit. New src/applied-styles.test.tsx closes the largest remaining gap: nothing verified the generated CSS actually applies. It asserts on getComputedStyle — tokens resolving through @Property initial values, specificity doubling beating a later rule, state maps switching values, container and media queries matching, shadow DOM isolation. The four weak `not.toBe(...)` assertions in value-mods.test.tsx became positive ones. CI installs Chromium via Playwright, cached on the lockfile hash. `pnpm bench` now targets the node project only — running both concurrently let the browser compete for CPU and halved the pipeline numbers the README quotes, which are re-measured here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
📦 Snapshot releasePublished |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a headless Chromium test environment so Tasty's CSS is verified against a real CSS engine, and fixes one bug that environment immediately surfaced.
Why
Tasty compiles to CSS, and only a CSS engine can tell you whether that CSS is valid. jsdom and happy-dom reject
@container,@starting-style,@property,@function, and CSS nesting outright — which had quietly hollowed out the suites that depended on them:advanced-states.test.tsxwere"". jsdom rejected every@media/@parent/@rootrule atinsertRule, so the snapshots asserted nothing at all.it()bodies and two wereit.skip, all annotated "jsdom limitation" —@containerand@starting-stylehad no coverage whatsoever.@functionatinsertRule", "happy-dom'sCSSKeyframesRule.cssTextomits the steps".What changed
Two Vitest projects (
vitest.config.ts):nodebrowserdocument, rendering React, or asserting on parsed CSSEnvironment is decided by an explicit
BROWSER_TESTSlist, so the@vitest-environmentpragmas are gone.jsdomandhappy-domare removed as dependencies. A DOM test left in thenodeproject fails loudly withdocument is not defined, so misplacement is cheap.Runtime went 11.3s → 4.6s, mostly because jsdom's 54s of environment setup is gone.
Coverage that would otherwise have been lost in the move. Three suites passed for the wrong reason once Chromium replaced happy-dom, and were fixed rather than left green:
@propertytests relied on happy-dom rejecting the rule, which Chromium accepts — they'd have passed vacuously. They now stubinsertRuleviasimulateNoAtPropertySupport(), which also can't silently lapse the day happy-dom adds support.useKeyframesonly asserted rule content in text mode. Chromium round-tripsCSSKeyframesRule.cssText, so it now asserts in both (the helper needed brace-matching — Chromium serializes keyframes multi-line).@functionclient CSSOM path had no coverage at all; added.New
src/applied-styles.test.tsxcloses the largest remaining gap: nothing verified the generated CSS applies. Every suite asserted on CSS text, and the only fourgetComputedStyleuses were weak negatives (not.toBe('red')) that pass even when no CSS applies. 18 tests now assert on computed values — tokens resolving through@propertyinitial values, specificity doubling beating a later equally-specific rule, state maps switching on mod toggle, container and media queries actually matching, shadow DOM isolation, CSSOM vs text injection. Each has a contrasting counterpart, so none can pass vacuously. The four weak assertions invalue-mods.test.tsxbecame positive ones.Bug fix included
gridColumns/gridRowsonly expanded real numbers, sogridColumns: { '': '3', '@media(w <= 600px)': '1' }emittedgrid-template-columns: 3— invalid CSS browsers drop silently. Every value in a state map is a string, so responsive grids were the common way to hit it.The root cause was in
createStyle, which skipped converters entirely for string values. Changing that gate meantgridTemplatealso had to fall back per segment, orauto / 1frwould have collapsed to/. Verified the edges against the engine rather than by assumption:CSS.supports('grid-template-columns', ...)confirms3is invalid but0is valid (a zero-length track), so digit strings ≥ 1 expand as counts and'0'stays a length. Negative counts used to throw insideString.repeat.Covered by
src/styles/grid.test.tsand carries apatchchangeset.Benchmarks
Adding the browser project silently broke
pnpm bench— it ran both projects concurrently, and the browser competing for CPU roughly halved the Node numbers (renderStylesread 33k concurrent vs 60k isolated). Split intopnpm bench(Node, the documented numbers) andpnpm bench:browser.Re-measured on the same M1 Max / Node 22 the README claims, two isolated runs agreeing within ~10%.
parseStylematched; the pipeline figures were optimistic:renderStyles5 flat props (cold)renderStylesstate map (cold)renderStyles(cached)parseStateKeysimple (cold)parseStateKeycomplex (cold)Also corrected "cache multipliers are 30x–100x" — the real range is 10x–300x.
Reviewer notes
isDevEnv()can't work in a browser. There's noprocessglobal, soNODE_ENVis unreachable and onlylocalStorage.TASTY_DEBUGflips it — thevi.stubEnv('NODE_ENV', …)tests were only ever exercising the Node path. They now useenableDevWarnings()(src/test/dev-env.ts). Whether shipped code should be able to warn in browsers is a separate call.pnpm test:setuponce to download Chromium. Documented inCONTRIBUTING.md; CI installs and caches it on the lockfile hash.size-limitformainis at 56.72 kB against a 57 kB limit. Thin headroom.Verification
pnpm build,lint,format:check,typecheck,knip,test,sizeall pass locally. 68 files / 1974 tests green; browser suite stable across repeated runs.🤖 Generated with Claude Code