diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js index e5cab77a1..4f0ae4cb7 100644 --- a/tests/detect-antipatterns.test.js +++ b/tests/detect-antipatterns.test.js @@ -675,6 +675,18 @@ describe('detectText — overused fonts', () => { }); describe('detectHtml — overused fonts system stack', () => { + test('Inter before a system stack still flags overused-font', async () => { + const page = `
world
`; + await withStaticFixture({ 'index.html': page }, async ({ file }) => { + const f = await detectHtml(file); + expect(f.some(r => r.antipattern === 'overused-font' && /inter/i.test(r.snippet))).toBe(true); + }); + }); + test('checkPageTypography regex path skips Roboto in system stack', () => { const html = `