import { describe, expect, it } from "vitest"; import { compileGlobalGovernanceFacts, FactIndex, makeTailwindClassFact, makeTailwindTokenResolvedFact, makeUsageNodeFact, ruleTailwindUnknownClass, runRules, } from "../index.js"; const FILE = "src/app.tsx"; const LOC = { file: FILE, line: 1, column: 7 }; function indexWithStrictMode(enabled = true): FactIndex { const ix = new FactIndex(); ix.addMany( compileGlobalGovernanceFacts({ rules: enabled ? { "tailwind/unknown-class": true } : {}, }) ); return ix; } function addUnknownResolvedClass(ix: FactIndex): void { const node = makeUsageNodeFact({ file: FILE, nodePath: "0", element: "div", location: LOC, }); ix.add(node); ix.add( makeTailwindClassFact({ file: FILE, nodeId: node.id, raw: "bg-totally-made-up", originPath: "attribute#0/class#0", prefix: null, modifiers: [], important: false, negative: false, utility: "bg", value: { kind: "token", token: "totally-made-up" }, location: LOC, }) ); ix.add( makeTailwindTokenResolvedFact({ utility: "bg", token: "totally-made-up", resolved: { kind: "unknown", source: "unknown" }, }) ); } describe("tailwind/unknown-class", () => { it("flags unresolved tokens when strict mode is enabled — ACCEPTANCE §5.31", () => { const ix = indexWithStrictMode(true); addUnknownResolvedClass(ix); const findings = ruleTailwindUnknownClass(ix); expect(findings).toHaveLength(1); expect(findings[0]).toMatchObject({ ruleId: "tailwind/unknown-class", severity: "minor", message: "`bg-totally-made-up` did not resolve to any known palette token.", attributes: { utility: "bg", token: "totally-made-up", reason: "resolution-miss", }, }); }); it("is silent by default — ACCEPTANCE §5.31", () => { const ix = indexWithStrictMode(false); addUnknownResolvedClass(ix); expect(ruleTailwindUnknownClass(ix)).toHaveLength(0); }); it("flags parser failures separately", () => { const ix = indexWithStrictMode(true); const node = makeUsageNodeFact({ file: FILE, nodePath: "0", element: "div", location: LOC, }); ix.add(node); ix.add( makeTailwindClassFact({ file: FILE, nodeId: node.id, raw: "totally-made-up", originPath: "attribute#0/class#0", prefix: null, modifiers: [], important: false, negative: false, utility: "unknown", value: { kind: "none" }, location: LOC, }) ); expect(ruleTailwindUnknownClass(ix)[0]?.attributes).toMatchObject({ reason: "parse-failure", }); }); it("does not flag box-shadow size utilities as unresolved colors", () => { const ix = indexWithStrictMode(true); const node = makeUsageNodeFact({ file: FILE, nodePath: "0", element: "div", location: LOC, }); ix.add(node); // `shadow` is in TAILWIND_COLOR_UTILITIES, but `shadow-lg` is a box-shadow // size, not a color — it must not be reported as a resolution miss. ix.add( makeTailwindClassFact({ file: FILE, nodeId: node.id, raw: "shadow-lg", originPath: "attribute#0/class#0", prefix: null, modifiers: [], important: false, negative: false, utility: "shadow", value: { kind: "token", token: "lg" }, location: LOC, }) ); ix.add( makeTailwindTokenResolvedFact({ utility: "shadow", token: "lg", resolved: { kind: "unknown", source: "unknown" }, }) ); expect(ruleTailwindUnknownClass(ix)).toHaveLength(0); }); it("is registered in runRules but stays policy-gated", () => { const ix = indexWithStrictMode(true); addUnknownResolvedClass(ix); expect(runRules(ix).map((f) => f.ruleId)).toContain("tailwind/unknown-class"); }); });