Skip to content

fix(cli): stop isTransparentColor misreading opaque zero-blue colors as transparent - #3902

Open
miga-heygen wants to merge 1 commit into
mainfrom
fix/istransparentcolor-zero-blue-opaque
Open

fix(cli): stop isTransparentColor misreading opaque zero-blue colors as transparent#3902
miga-heygen wants to merge 1 commit into
mainfrom
fix/istransparentcolor-zero-blue-opaque

Conversation

@miga-heygen

Copy link
Copy Markdown
Contributor

Problem

isTransparentColor(color) in layout-audit.browser.js (the injected in-page audit script backing hyperframes 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-value rgb(r, g, b) color whose blue channel is 0 — 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-value rgba(..., 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_occluded false negative).
  • connectorAnchorRects/connectorEndpointCandidates — a node box in one of those colors silently dropped out of the anchorable-element list for connector_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 (feeding isConstraintCandidate → overflow-boundary detection), rather than calling isTransparentColor.

Fix

Delegate to the file's existing, already-correct colorAlpha(color), which parses the real alpha channel by argument position (handles both legacy rgba(r,g,b,a) and the modern rgb(r g b / a) syntax) rather than by string shape:

function isTransparentColor(color) {
  return !color || color === "transparent" || colorAlpha(color) === 0;
}

hasPaint now delegates to the corrected isTransparentColor instead of carrying its own duplicate broken check.

Tests

Added a parameterized test in layout-audit.browser.test.ts asserting text_occluded fires 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 dropped text_occluded to false while the near-black control stayed true; restored the fix and reran — all four colors correctly reported text_occluded: true. Repeated the same red→green verification for the hasPaint/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 with Error: 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 --check on both touched files — clean
  • bunx tsc --noEmit -p packages/cli — clean
  • Real-Chromium red→green verification described above (both the occlusion path and the hasPaint path)

Known, out-of-scope limitation

colorAlpha's regex only matches rgb()/rgba() — it doesn't recognize hsl()/hsla() syntax, so a genuinely-transparent hsla(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 other colorAlpha consumer in this file, not something this diff introduces — and getComputedStyle() in a real browser always normalizes colors to rgb()/rgba(), never returns hsl(), so this path is unreachable in practice. Left as-is rather than expanding colorAlpha's regex, which is out of this fix's scope.

…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 somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. colorAlpha parses alpha by argument position, so rgb(255, 0, 0)alphaFromParts([...], 3) with length 3 <= index 31 (opaque), while rgba(r,g,b,0) / modern rgb(r g b / 0)0. isTransparentColor is now correct where the old ", 0)" suffix mis-classified every opaque zero-blue color as transparent.
  • All consumers behave. Traced every isTransparentColor caller 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-match rgb(0,0,0) compare in the engine and a test-util literal. So isTransparentColor + hasPaint were the complete set of this bug — collapsing hasPaint onto 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 (auditOcclusionScenetext_occluded, path at 731). The hasPaintisConstraintCandidate → 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 is rgb/rgba-only, but getComputedStyle() normalizes to rgb()/rgba(), so the path is unreachable in the injected audit script, and the blind spot is pre-existing across every colorAlpha consumer.

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.

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.

2 participants