diff --git a/packages/lint/src/rules/fonts.test.ts b/packages/lint/src/rules/fonts.test.ts index 6afa663bbe2..fdbabb76696 100644 --- a/packages/lint/src/rules/fonts.test.ts +++ b/packages/lint/src/rules/fonts.test.ts @@ -14,6 +14,33 @@ async function findAliasFindings(html: string) { describe("font rules", () => { describe("system_font_will_alias", () => { + it("reports an aliased family reached through a referenced custom property", async () => { + const findings = await findAliasFindings(`
`); + expect(findings).toHaveLength(1); + expect(findings[0]?.message).toContain("'verdana' → Inter"); + }); + + it.each([ + ':root { --font-family: "Verdana"; }', + 'p::before { content: "font-family: Verdana;"; }', + ])("ignores aliased names outside font declarations: %s", async (css) => { + const findings = await findAliasFindings(`
`); + expect(findings).toHaveLength(0); + }); + + it("does not treat quoted @font-face text as an alias declaration", async () => { + const findings = await findAliasFindings(`
`); + expect(findings).toHaveLength(1); + expect(findings[0]!.message).toContain("'verdana' → Inter"); + }); + it("flags SF Mono as aliased to JetBrains Mono", async () => { const html = `
@@ -99,6 +126,99 @@ describe("font rules", () => { }); describe("font_family_without_font_face", () => { + it.each([ + ':root { --font-family: "Brand Heading"; } h1 { font-family: var(--font-family); }', + ':root { --family: "Brand Heading"; --heading: var(--family); } h1 { font-family: var(--heading); }', + ':root { --heading: "Brand Heading"; } h1 { font-family: serif, var(--heading), sans-serif; }', + ':root { --heading: "Brand Heading", sans-serif; } h1 { font-family: var(--heading); }', + ':root { --Heading: "Brand Heading"; --heading: Inter; } h1 { font-family: var(--Heading); }', + ':root { --heading: "Brand Heading"; } h1 { font-family: var(--heading, "Unused Fallback"); }', + ])("reports the missing family in a referenced custom property: %s", async (css) => { + const findings = await findByCode( + `
`, + "font_family_without_font_face", + ); + expect(findings).toHaveLength(1); + expect(findings[0]?.message).toContain("brand heading"); + expect(findings[0]?.message).not.toContain("unused fallback"); + }); + + it("resolves referenced font tokens across style blocks and locates their use", async () => { + const findings = await findByCode( + `
+
`, + "font_family_without_font_face", + ); + expect(findings).toHaveLength(1); + expect(findings[0]).toMatchObject({ line: 3, column: 6 }); + }); + + it.each([ + '@font-face { font-family: "Brand Heading"; src: local("Arial"); } :root { --heading: "Brand Heading"; } h1 { font-family: var(--heading); }', + ":root { --heading: var(--other); --other: var(--heading); } h1 { font-family: var(--heading); }", + ':root { --heading: "Brand Heading"; } h1 { font-family: var(--unresolved); }', + ':root { --heading: "Brand Heading"; } h1 { font-family: "var(--heading)"; }', + ])("keeps declared or unresolved token references quiet: %s", async (css) => { + const findings = await findByCode( + `
`, + "font_family_without_font_face", + ); + expect(findings).toHaveLength(0); + }); + + it.each([ + ':root { --font-family: "Brand Heading"; }', + ':root { --brand-font-family: "Brand Heading"; }', + 'p::before { content: "font-family: Brand Heading;"; }', + ])("ignores family names outside font declarations: %s", async (css) => { + const findings = await findByCode( + `
`, + "font_family_without_font_face", + ); + expect(findings).toHaveLength(0); + }); + + it.each([ + 'p::before { content: "@font-face { font-family: Brand Heading; }"; }', + '@font-face { --font-family: "Brand Heading"; src: local("Arial"); }', + ])("requires a real font-face descriptor: %s", async (css) => { + const findings = await findByCode( + `
`, + "font_family_without_font_face", + ); + expect(findings).toHaveLength(1); + expect(findings[0]!.message).toContain("brand heading"); + }); + + it("reports only the real missing family and locates its declaration", async () => { + const findings = await findByCode( + `
`, + "font_family_without_font_face", + ); + expect(findings).toHaveLength(1); + expect(findings[0]).toMatchObject({ line: 4, column: 5 }); + expect(findings[0]!.message).toContain("real brand"); + expect(findings[0]!.message).not.toContain("unused brand"); + expect(findings[0]!.message).not.toContain("another brand"); + }); + + it("still finds a real font face inside a conditional rule", async () => { + const findings = await findByCode( + `
`, + "font_family_without_font_face", + ); + expect(findings).toHaveLength(0); + }); + it("flags font-family used without @font-face", async () => { const html = `
diff --git a/packages/lint/src/rules/fonts.ts b/packages/lint/src/rules/fonts.ts index cb9bb7df59a..f8fdd74d786 100644 --- a/packages/lint/src/rules/fonts.ts +++ b/packages/lint/src/rules/fonts.ts @@ -30,40 +30,36 @@ const GENERIC_FAMILIES = new Set([ "revert", ]); -// A CSS comment can contain a `}` (e.g. `@font-face { /* 400 } regular */ -// font-family: 'X'; ... }`), which truncates the naive `@font-face\s*\{[^}]*\}` -// block match at the comment's brace — so the rule never sees the real -// `font-family` and reports a false-positive font_family_without_font_face. -// Large/"framework" stylesheets hit this far more often than minimal ones, -// which is why a simple