import { describe, expect, it } from "vitest"; import { FactIndex, makeContractTokenFact, makeStyleCssVarsMustBeDefinedFact, makeStyleDeclarationFact, } from "../index.js"; import type { Fact } from "../index.js"; import { ruleTokensCssVarsMustBeDefined } from "./tokens-css-vars-must-be-defined.js"; /** * ACCEPTANCE §4 (rule level) + §11 (regression lock). * * The off-contract-token rule (FUI2015) is INERT unless the contract declares * the `style.cssVars.mustBeDefined` policy fact — the cloud appends it only when * `tokenSourceFiles` is non-empty, so the `fragments` preset alone never * activates it. When active, a `var(--x)` drifts iff `--x` shares the authored * vocabulary's prefix family but is not in the vocabulary; genuinely-foreign * custom properties are ignored (no false-positive flood — the original revert). */ const POLICY = makeStyleCssVarsMustBeDefinedFact({ severity: "warn" }); function vocabulary(): Fact[] { return [ makeContractTokenFact({ name: "--fui-color-accent" }), makeContractTokenFact({ name: "--fui-space-2" }), ]; } function decl(selector: string, value: string): Fact { const file = "src/Card.module.scss"; return makeStyleDeclarationFact({ file, selector, declarationPath: selector, property: "color", value, location: { file, line: 1, column: 0 }, }); } function index(facts: Fact[]): FactIndex { const ix = new FactIndex(); ix.addMany(facts); return ix; } describe("ruleTokensCssVarsMustBeDefined", () => { it("flags an off-contract token that shares the contract prefix family (§4)", () => { const ix = index([POLICY, ...vocabulary(), decl(".b", "var(--fui-color-brand)")]); const findings = ruleTokensCssVarsMustBeDefined(ix); expect(findings).toHaveLength(1); // `warn` (governance severity) normalizes to `moderate` at the finding level. expect(findings[0].severity).toBe("moderate"); expect(findings[0].attributes).toMatchObject({ tokenName: "--fui-color-brand" }); }); it("does NOT flag an in-contract token reference (§4)", () => { const ix = index([ POLICY, ...vocabulary(), decl(".c", "var(--fui-color-accent, $fui-color-accent)"), ]); expect(ruleTokensCssVarsMustBeDefined(ix)).toHaveLength(0); }); it("does NOT flag a genuinely-foreign var that shares no contract prefix (§4)", () => { // `--brand-blue` / `--swiper-*` are outside the `--fui-` family — ignored, // exactly the false positives that got the original brief reverted. const ix = index([ POLICY, ...vocabulary(), decl(".x", "var(--brand-blue)"), decl(".y", "var(--swiper-theme-color)"), ]); expect(ruleTokensCssVarsMustBeDefined(ix)).toHaveLength(0); }); it("flags a locally-defined parallel custom property even when it has no shared prefix", () => { const file = "src/landing.scss"; const ix = index([ POLICY, ...vocabulary(), makeStyleDeclarationFact({ file, selector: ":root", declarationPath: "0", property: "--lp-accent", value: "#39594d", location: { file, line: 1, column: 0 }, }), makeStyleDeclarationFact({ file, selector: ".hero", declarationPath: "1", property: "color", value: "var(--lp-accent)", location: { file, line: 4, column: 2 }, }), decl(".swiper", "var(--swiper-theme-color)"), ]); const findings = ruleTokensCssVarsMustBeDefined(ix); expect(findings).toHaveLength(1); expect(findings[0].attributes).toMatchObject({ tokenName: "--lp-accent" }); }); it("is INERT without the activation policy fact, even with off-contract vars (§11)", () => { // No `style.cssVars.mustBeDefined` fact → the `fragments` preset alone never // fires the rule. This is the regression lock at the rule boundary. const ix = index([...vocabulary(), decl(".b", "var(--fui-color-brand)")]); expect(ruleTokensCssVarsMustBeDefined(ix)).toHaveLength(0); }); it("is INERT when the contract vocabulary is empty (no token definitions)", () => { const ix = index([POLICY, decl(".b", "var(--fui-color-brand)")]); expect(ruleTokensCssVarsMustBeDefined(ix)).toHaveLength(0); }); // #33 — CSS function names are case-insensitive; VAR()/Var() must be detected. it("detects an off-contract token through case-insensitive VAR()/Var() (#33)", () => { const ix = index([ POLICY, ...vocabulary(), decl(".b", "VAR(--fui-color-brand)"), decl(".c", "Var(--fui-color-brand)"), ]); const findings = ruleTokensCssVarsMustBeDefined(ix); expect(findings).toHaveLength(2); expect(findings[0].attributes).toMatchObject({ tokenName: "--fui-color-brand" }); }); // #17 — only the PRIMARY var() position is judged; a nested fallback var() that // is off-contract must NOT be flagged. it("ignores an off-contract var in nested FALLBACK position (#17)", () => { const ix = index([ POLICY, ...vocabulary(), decl(".c", "var(--fui-color-accent, var(--fui-color-brand))"), ]); expect(ruleTokensCssVarsMustBeDefined(ix)).toHaveLength(0); }); it("flags an off-contract PRIMARY even when its fallback is in-contract (#17)", () => { const ix = index([ POLICY, ...vocabulary(), decl(".d", "var(--fui-color-brand, var(--fui-color-accent))"), ]); const findings = ruleTokensCssVarsMustBeDefined(ix); expect(findings).toHaveLength(1); expect(findings[0].attributes).toMatchObject({ tokenName: "--fui-color-brand" }); }); // #11 — one off-contract var referenced twice in ONE declaration is one fact. it("dedupes the same off-contract var WITHIN one declaration (#11)", () => { const ix = index([ POLICY, ...vocabulary(), decl(".b", "0 0 0 1px var(--fui-color-brand), inset 0 0 0 1px var(--fui-color-brand)"), ]); expect(ruleTokensCssVarsMustBeDefined(ix)).toHaveLength(1); }); it("does NOT dedupe the same off-contract var across SEPARATE declarations (#11)", () => { const ix = index([ POLICY, ...vocabulary(), decl(".b", "var(--fui-color-brand)"), decl(".c", "var(--fui-color-brand)"), ]); expect(ruleTokensCssVarsMustBeDefined(ix)).toHaveLength(2); }); // #22/#25/#31/#36 — a FLAT vocabulary (single-segment names, no prefix family) // must still enforce drift; multi-segment foreign vars stay ignored. it("enforces drift for a FLAT (single-segment) vocabulary (#22/#25/#31/#36)", () => { const flat = (): Fact[] => [ makeContractTokenFact({ name: "--accent" }), makeContractTokenFact({ name: "--bg" }), ]; expect( ruleTokensCssVarsMustBeDefined(index([POLICY, ...flat(), decl(".b", "var(--brand)")])) ).toHaveLength(1); // Multi-segment foreign var stays ignored even in flat mode. expect( ruleTokensCssVarsMustBeDefined( index([POLICY, ...flat(), decl(".x", "var(--swiper-theme-color)")]) ) ).toHaveLength(0); // In-contract flat ref is fine. expect( ruleTokensCssVarsMustBeDefined(index([POLICY, ...flat(), decl(".y", "var(--accent)")])) ).toHaveLength(0); }); // #18 — a MIXED vocabulary (namespaced + flat) enforces BOTH namespaces // symmetrically, order-independently; multi-segment foreign stays ignored. it("enforces BOTH halves of a MIXED namespaced+flat vocabulary (#18)", () => { const mixed = (): Fact[] => [ makeContractTokenFact({ name: "--fui-color-accent" }), makeContractTokenFact({ name: "--accent" }), ]; const findings = ruleTokensCssVarsMustBeDefined( index([ POLICY, ...mixed(), decl(".a", "var(--fui-color-brand)"), // namespaced drift decl(".b", "var(--bg)"), // flat drift decl(".c", "var(--swiper-theme-color)"), // foreign multi-segment, ignored ]) ); expect(findings).toHaveLength(2); // Determinism: reversed token add-order yields identical findings count. const reversed = ruleTokensCssVarsMustBeDefined( index([ POLICY, makeContractTokenFact({ name: "--accent" }), makeContractTokenFact({ name: "--fui-color-accent" }), decl(".a", "var(--fui-color-brand)"), decl(".b", "var(--bg)"), decl(".c", "var(--swiper-theme-color)"), ]) ); expect(reversed).toHaveLength(2); }); });