import { describe, expect, it } from "vitest"; import { compileGlobalGovernanceFacts, FactIndex, g, makeTailwindClassFact, makeUsageNodeFact, ruleTailwindArbitrarySpacing, runRules, } from "../index.js"; import type { TailwindValue } from "../index.js"; const FILE = "components/product/gallery.tsx"; function indexWithSpacingPolicy(values = [0, 4, 8, 16, 32]): FactIndex { const ix = new FactIndex(); ix.addMany( compileGlobalGovernanceFacts({ scales: { space: g.scale.px(values), }, styles: [ g.styles.rawSpacing().mustMatchScale("space", { appliesTo: ["margin", "padding", "gap"], severity: "warn", }), ], }), ); return ix; } function addClass(ix: FactIndex, input: { raw: string; utility: string; value: TailwindValue; negative?: boolean; }) { const node = makeUsageNodeFact({ file: FILE, nodePath: "0:0", element: "button", location: { file: FILE, line: 12, column: 6 }, }); ix.add(node); ix.add( makeTailwindClassFact({ file: FILE, nodeId: node.id, raw: input.raw, originPath: `attribute#0/class#${input.raw}`, prefix: null, modifiers: [], important: false, negative: input.negative ?? false, utility: input.utility, value: input.value, location: { file: FILE, line: 12, column: 30 }, }), ); } describe("tailwind/arbitrary-spacing", () => { it("flags arbitrary percent spacing — ACCEPTANCE §3.18", () => { const ix = indexWithSpacingPolicy(); addClass(ix, { raw: "m-[15%]", utility: "m", value: { kind: "arbitrary", raw: "15%" }, }); const findings = ruleTailwindArbitrarySpacing(ix); expect(findings).toHaveLength(1); expect(findings[0]).toMatchObject({ ruleId: "tailwind/arbitrary-spacing", severity: "moderate", message: "`m-[15%]` is an arbitrary spacing value not on the project's spacing scale.", attributes: { rawValue: "15%", utility: "m", property: "margin", scale: "space", source: "tailwind", }, }); expect(findings[0].fix).toMatchObject({ kind: "replaceClassToken", from: "m-[15%]", to: null, deterministic: false, }); }); it("normalizes rem values before checking a px scale — ACCEPTANCE §3.21", () => { const ix = indexWithSpacingPolicy([0, 4, 8, 16, 32]); addClass(ix, { raw: "p-[1rem]", utility: "p", value: { kind: "arbitrary", raw: "1rem" }, }); addClass(ix, { raw: "p-[1.5rem]", utility: "p", value: { kind: "arbitrary", raw: "1.5rem" }, }); const findings = ruleTailwindArbitrarySpacing(ix); expect(findings).toHaveLength(1); expect(findings[0].attributes).toMatchObject({ rawClass: "p-[1.5rem]", rawValue: "1.5rem", normalizedValue: 24, assumedRootFontSizePx: 16, }); expect(findings[0].fix).toMatchObject({ kind: "replaceClassToken", from: "p-[1.5rem]", to: null, deterministic: false, }); }); it("does not flag positional arbitrary offsets — ACCEPTANCE §3.22", () => { const ix = indexWithSpacingPolicy(); addClass(ix, { raw: "top-[-1px]", utility: "top", value: { kind: "arbitrary", raw: "-1px" }, }); expect(ruleTailwindArbitrarySpacing(ix)).toHaveLength(0); }); it("allows negative values whose magnitude is on the scale", () => { const ix = indexWithSpacingPolicy(); addClass(ix, { raw: "-mx-[4px]", utility: "mx", negative: true, value: { kind: "arbitrary", raw: "4px" }, }); expect(ruleTailwindArbitrarySpacing(ix)).toHaveLength(0); }); it("is registered in runRules", () => { const ix = indexWithSpacingPolicy(); addClass(ix, { raw: "gap-[13px]", utility: "gap", value: { kind: "arbitrary", raw: "13px" }, }); expect(runRules(ix).map((f) => f.ruleId)).toContain("tailwind/arbitrary-spacing"); }); });