diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index def6278cb..216ec462a 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -2976,7 +2976,12 @@ function resolveBackgroundInfo(el, win, customPropMap) { // verbatim, and without this substitution the layer would read as // unparseable and force a needless abstention. if ((!bg || bg.a < 0.1) && /^currentcolor$/i.test(String(style.backgroundColor || '').trim())) { - bg = parseRgb(style.color) || parseAnyColor(style.color); + // The static cascade resolves var() text tokens before checks run, so + // style.color is normally already an rgb string here; parseColorResolved + // is defense in depth for any future caller that passes a live + // customPropMap (it matches the text-color path in checkElementColors + // and reduces to parseAnyColor when the map is null or absent). + bg = parseRgb(style.color) || parseColorResolved(style.color, customPropMap); } if (bg && bg.a > 0.1) { @@ -3805,12 +3810,14 @@ function checkElementGlowDOM(el) { // Use parent's background — glow radiates outward, so the surrounding context matters // If resolveBackground returns null (gradient), try to infer from the gradient colors const parentBgInfo = resolveBackgroundInfo(el.parentElement || el); - // Unknown surface (an unreadable layer on the way up): abstain. The - // gradient hunt below would walk PAST that layer and score the glow - // against a background the visitor never sees. - if (parentBgInfo.unresolved) return []; + // Unknown surface (an unreadable layer on the way up): skip only the + // gradient hunt below, which would walk PAST that layer and score the + // glow against a background the visitor never sees. checkGlow still runs + // with a null surface: the zero-offset chromatic halo tell holds on ANY + // background, and the static loop already passes the unresolved walk's + // null color straight through (detect-html.mjs uses resolveBackground). let parentBg = parentBgInfo.color; - if (!parentBg) { + if (!parentBg && !parentBgInfo.unresolved) { // Gradient background — sample its colors to determine if it's dark let cur = el.parentElement; while (cur && cur.nodeType === 1) { diff --git a/cli/engine/rules/checks.mjs b/cli/engine/rules/checks.mjs index f19e11b8a..907790e90 100644 --- a/cli/engine/rules/checks.mjs +++ b/cli/engine/rules/checks.mjs @@ -1742,7 +1742,12 @@ function resolveBackgroundInfo(el, win, customPropMap) { // verbatim, and without this substitution the layer would read as // unparseable and force a needless abstention. if ((!bg || bg.a < 0.1) && /^currentcolor$/i.test(String(style.backgroundColor || '').trim())) { - bg = parseRgb(style.color) || parseAnyColor(style.color); + // The static cascade resolves var() text tokens before checks run, so + // style.color is normally already an rgb string here; parseColorResolved + // is defense in depth for any future caller that passes a live + // customPropMap (it matches the text-color path in checkElementColors + // and reduces to parseAnyColor when the map is null or absent). + bg = parseRgb(style.color) || parseColorResolved(style.color, customPropMap); } if (bg && bg.a > 0.1) { @@ -2571,12 +2576,14 @@ function checkElementGlowDOM(el) { // Use parent's background — glow radiates outward, so the surrounding context matters // If resolveBackground returns null (gradient), try to infer from the gradient colors const parentBgInfo = resolveBackgroundInfo(el.parentElement || el); - // Unknown surface (an unreadable layer on the way up): abstain. The - // gradient hunt below would walk PAST that layer and score the glow - // against a background the visitor never sees. - if (parentBgInfo.unresolved) return []; + // Unknown surface (an unreadable layer on the way up): skip only the + // gradient hunt below, which would walk PAST that layer and score the + // glow against a background the visitor never sees. checkGlow still runs + // with a null surface: the zero-offset chromatic halo tell holds on ANY + // background, and the static loop already passes the unresolved walk's + // null color straight through (detect-html.mjs uses resolveBackground). let parentBg = parentBgInfo.color; - if (!parentBg) { + if (!parentBg && !parentBgInfo.unresolved) { // Gradient background — sample its colors to determine if it's dark let cur = el.parentElement; while (cur && cur.nodeType === 1) { diff --git a/tests/detect-antipatterns-browser.test.mjs b/tests/detect-antipatterns-browser.test.mjs index dd815cf02..05d39f5de 100644 --- a/tests/detect-antipatterns-browser.test.mjs +++ b/tests/detect-antipatterns-browser.test.mjs @@ -112,6 +112,23 @@ describe('detectUrl — browser-only fixtures', () => { } }); + it('dark-glow: unreadable url() surface keeps zero-offset halos, abstains on offset chromatic shadows', async () => { + // Mirrors the static assertions in detect-antipatterns-fixtures.test.mjs. + // The browser adapter (checkElementGlowDOM) once returned [] for the + // whole element when the parent surface was unresolved, which also + // dropped zero-offset chromatic halos that need no background at all. + const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/glow.html`, { visualContrast: false }); + const glow = f.filter(r => r.antipattern === 'dark-glow'); + assert.ok( + glow.some(g => /Zero-offset box-shadow glow \(#d946ef\)/i.test(g.snippet || '')), + 'expected zero-offset halo finding under unreadable image surface', + ); + assert.equal( + glow.filter(g => /#10b981/i.test(g.snippet || '')).length, 0, + 'offset chromatic shadow on unknown surface must not be scored', + ); + }); + it('shadowed form.id: a