fix(cli): stop isTransparentColor misreading opaque zero-blue colors as transparent - #3902
fix(cli): stop isTransparentColor misreading opaque zero-blue colors as transparent#3902miga-heygen wants to merge 1 commit into
Conversation
…as transparent
isTransparentColor detected transparency via a string-suffix check
(color.endsWith(", 0)")), which also matches any fully-opaque 3-value
rgb(r, g, b) color whose blue channel is 0 — pure red, pure green, pure
yellow, and any other zero-blue color, not just genuinely transparent
rgba(..., 0) colors. This silently broke occlusion detection and
connector-anchor detection for elements painted in those colors.
Delegate to the file's existing colorAlpha, which already parses the real
alpha channel by argument position rather than by string shape. A second,
independent copy of the same buggy suffix check existed in hasPaint (feeding
overflow-boundary detection) — replaced with a delegation to the
now-corrected isTransparentColor instead of a second inline reimplementation.
Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com>
somanshreddy
left a comment
There was a problem hiding this comment.
Independent review at 2f19f3eb (manual pass — Codex spend-capped this session, so this is direct source verification, not a codex run). Verdict: correct and well-scoped, no blockers. One non-blocking test-debt note.
Verified at source
- The fix is right.
colorAlphaparses alpha by argument position, sorgb(255, 0, 0)→alphaFromParts([...], 3)withlength 3 <= index 3→1(opaque), whilergba(r,g,b,0)/ modernrgb(r g b / 0)→0.isTransparentColoris now correct where the old", 0)"suffix mis-classified every opaque zero-blue color as transparent. - All consumers behave. Traced every
isTransparentColorcaller at head —hasPaint(327), occluder-alpha compositing (731–733),isPaintedPanel(1176), plus the definition (592). The flip changes exactly two input classes and nothing else: (a) opaque zero-blue → now correctly painted/occluding [the intended fix]; (b) genuine transparents ("",transparent,rgba(…,0),rgb(… / 0)) still read transparent — no regression. - Completeness — the second instance is the last one. Grepped the repo at head for the string-suffix pattern; the only other
", 0)"hits are an unrelated exact-matchrgb(0,0,0)compare in the engine and a test-util literal. SoisTransparentColor+hasPaintwere the complete set of this bug — collapsinghasPaintonto the shared function also removes the duplication class, not just the second copy.
Non-blocking: the hasPaint/overflow path is unpinned
The new parameterized test pins only the occlusion consumer (auditOcclusionScene → text_occluded, path at 731). The hasPaint → isConstraintCandidate → overflow-boundary path (the "second independent instance") has no committed test with a zero-blue opaque backgroundColor — the constraint tests (#bubble at 405, etc.) set no such background. Reverting the line-327 delegation back to an inline endsWith(", 0)") — the exact duplication that created this bug in the first place — leaves the suite green. A constraint/overflow case with a zero-blue painted box (expecting text_box_overflow to survive) would pin it and foreclose the re-inlining.
I confirmed the committed occlusion test is meaningful by hand: pre-fix, rgb(0, 255, 0) matches ", 0)" → treated transparent → occluder alpha 0 → text_occluded false → the test fails on revert. (I could not execute the suite — happy-dom collection fails with No such built-in module: node: on an unmodified checkout too, a pre-existing sandbox limit — so this is a static trace of the assertion, resting on your real-Chromium revert run for the runtime confirmation.)
Minor / praise
- The fix is broader than the description claims — it also closes modern
rgb(r g b / 0)transparency, which the old suffix check silently missed (it only ended in"/ 0)", not", 0)"). Worth a line so a later reader knows that's intended, not incidental. - The
hsla(…, 0)out-of-scope note is a fair call:colorAlpha's regex isrgb/rgba-only, butgetComputedStyle()normalizes torgb()/rgba(), so the path is unreachable in the injected audit script, and the blind spot is pre-existing across everycolorAlphaconsumer.
Not approving here — posting as a comment (I hold review, not stamp authority on this channel trigger). Clean on my pass; the one note is test debt, your call whether to close it in this PR or follow-up.
Problem
isTransparentColor(color)inlayout-audit.browser.js(the injected in-page audit script backinghyperframes check's layout gate) detected "is this color transparent" with a string-suffix check:color.endsWith(", 0)"). That suffix also matches any fully-opaque 3-valuergb(r, g, b)color whose blue channel is0— pure red (rgb(255,0,0)), pure green (rgb(0,255,0)), pure yellow (rgb(255,255,0)), and any other zero-blue color — not just genuinely transparent 4-valuergba(..., 0)colors.This silently broke two things for elements in one of those colors:
isOpaqueOccluder/hasOpaqueBackground— a solid pure-green/red/yellow card sitting on top of text was never flagged as occluding it (text_occludedfalse negative).connectorAnchorRects/connectorEndpointCandidates— a node box in one of those colors silently dropped out of the anchorable-element list forconnector_detached/connector_orphan.A second, independent instance of the identical bug was found while reading the source:
hasPaint(style)re-implemented its own inline copy of the same buggy suffix check (feedingisConstraintCandidate→ overflow-boundary detection), rather than callingisTransparentColor.Fix
Delegate to the file's existing, already-correct
colorAlpha(color), which parses the real alpha channel by argument position (handles both legacyrgba(r,g,b,a)and the modernrgb(r g b / a)syntax) rather than by string shape:hasPaintnow delegates to the correctedisTransparentColorinstead of carrying its own duplicate broken check.Tests
Added a parameterized test in
layout-audit.browser.test.tsassertingtext_occludedfires for a pure green/red/yellow opaque occluder (previously silently exempted).Verified against a real revert: reverted just the source file (kept the test), reran a throwaway real-Chromium harness driving the actual script via
page.evaluate— the pure red/green/yellow cases droppedtext_occludedtofalsewhile the near-black control stayedtrue; restored the fix and reran — all four colors correctly reportedtext_occluded: true. Repeated the same red→green verification for thehasPaint/overflow-boundary path (a small card with a zero-blue background and text wider than the card): pre-fix, the overflow finding silently disappeared for the green card while a non-zero-blue control still reported it; post-fix, both report it.The test suite itself (
layout-audit.browser.test.ts,@vitest-environment happy-dom) cannot be executed in this sandbox — collection fails withError: No such built-in module: node:, a pre-existing environment limitation confirmed identical on an unmodified checkout before making any change, unrelated to this diff.Verification
bunx oxlint/bunx oxfmt --checkon both touched files — cleanbunx tsc --noEmit -p packages/cli— cleanhasPaintpath)Known, out-of-scope limitation
colorAlpha's regex only matchesrgb()/rgba()— it doesn't recognizehsl()/hsla()syntax, so a genuinely-transparenthsla(h, s%, l%, 0)string would now read as opaque (the old suffix check happened to catch this by coincidence, since its literal tail also ends in", 0)"). This is a pre-existing blind spot already shared by every othercolorAlphaconsumer in this file, not something this diff introduces — andgetComputedStyle()in a real browser always normalizes colors torgb()/rgba(), never returnshsl(), so this path is unreachable in practice. Left as-is rather than expandingcolorAlpha's regex, which is out of this fix's scope.